diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini
index 768edafb4..17dc8372f 100644
--- a/UI/data/locale/en-US.ini
+++ b/UI/data/locale/en-US.ini
@@ -778,6 +778,7 @@ Basic.Settings.General.Theme="Theme"
Basic.Settings.General.Language="Language"
Basic.Settings.General.EnableAutoUpdates="Automatically check for updates on startup"
Basic.Settings.General.OpenStatsOnStartup="Open stats dialog on startup"
+Basic.Settings.General.HideOBSWindowsFromCapture="Hide OBS windows from display capture"
Basic.Settings.General.WarnBeforeStartingStream="Show confirmation dialog when starting streams"
Basic.Settings.General.WarnBeforeStoppingStream="Show confirmation dialog when stopping streams"
Basic.Settings.General.WarnBeforeStoppingRecord="Show confirmation dialog when stopping recording"
diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui
index fee695752..e180119ae 100644
--- a/UI/forms/OBSBasicSettings.ui
+++ b/UI/forms/OBSBasicSettings.ui
@@ -245,6 +245,13 @@
+ -
+
+
+ Basic.Settings.General.HideOBSWindowsFromCapture
+
+
+
diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp
index 87f458b84..48f6b41ff 100644
--- a/UI/obs-app.cpp
+++ b/UI/obs-app.cpp
@@ -1571,6 +1571,44 @@ bool OBSApp::TranslateString(const char *lookupVal, const char **out) const
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 *context, const char *sourceText,
const char *disambiguation, int n) const
{
diff --git a/UI/obs-app.hpp b/UI/obs-app.hpp
index a534f6d25..1d76fe38b 100644
--- a/UI/obs-app.hpp
+++ b/UI/obs-app.hpp
@@ -105,6 +105,8 @@ private:
void AddExtraThemeColor(QPalette &pal, int group, const char *name,
uint32_t color);
+ bool notify(QObject *receiver, QEvent *e) override;
+
public:
OBSApp(int &argc, char **argv, profiler_name_store_t *store);
~OBSApp();
diff --git a/UI/platform-osx.mm b/UI/platform-osx.mm
index 35891b1b9..2c9e37ada 100644
--- a/UI/platform-osx.mm
+++ b/UI/platform-osx.mm
@@ -196,6 +196,12 @@ void SetAlwaysOnTop(QWidget *window, bool enable)
window->show();
}
+bool SetDisplayAffinitySupported(void)
+{
+ // Not implemented yet
+ return false;
+}
+
typedef void (*set_int_t)(int);
void EnableOSXVSync(bool enable)
diff --git a/UI/platform-windows.cpp b/UI/platform-windows.cpp
index 9e757f73f..c0cad3577 100644
--- a/UI/platform-windows.cpp
+++ b/UI/platform-windows.cpp
@@ -162,6 +162,20 @@ uint32_t GetWindowsVersion()
return ver;
}
+uint32_t GetWindowsBuild()
+{
+ static uint32_t build = 0;
+
+ if (build == 0) {
+ struct win_version_info ver_info;
+
+ get_win_ver(&ver_info);
+ build = ver_info.build;
+ }
+
+ return build;
+}
+
void SetAeroEnabled(bool enable)
{
static HRESULT(WINAPI * func)(UINT) = nullptr;
@@ -229,6 +243,27 @@ void SetWin32DropStyle(QWidget *window)
SetWindowLongPtr(hwnd, GWL_EXSTYLE, ex_style);
}
+bool SetDisplayAffinitySupported(void)
+{
+ static bool checked = false;
+ static bool supported;
+
+ /* this has to be version gated as setting WDA_EXCLUDEFROMCAPTURE on
+ older Windows builds behaves like WDA_MONITOR (black box) */
+
+ if (!checked) {
+ if (GetWindowsVersion() > 0x0A00 ||
+ GetWindowsVersion() == 0x0A00 && GetWindowsBuild() > 19041)
+ supported = true;
+ else
+ supported = false;
+
+ checked = true;
+ }
+
+ return supported;
+}
+
bool DisableAudioDucking(bool disable)
{
ComPtr devEmum;
diff --git a/UI/platform-x11.cpp b/UI/platform-x11.cpp
index 60035210a..536b1ea86 100644
--- a/UI/platform-x11.cpp
+++ b/UI/platform-x11.cpp
@@ -251,3 +251,9 @@ void SetAlwaysOnTop(QWidget *window, bool enable)
window->setWindowFlags(flags);
window->show();
}
+
+bool SetDisplayAffinitySupported(void)
+{
+ // Not implemented yet
+ return false;
+}
diff --git a/UI/platform.hpp b/UI/platform.hpp
index 7bac1e1f7..30ef393d8 100644
--- a/UI/platform.hpp
+++ b/UI/platform.hpp
@@ -37,8 +37,11 @@ std::vector GetPreferredLocales();
bool IsAlwaysOnTop(QWidget *window);
void SetAlwaysOnTop(QWidget *window, bool enable);
+bool SetDisplayAffinitySupported(void);
+
#ifdef _WIN32
uint32_t GetWindowsVersion();
+uint32_t GetWindowsBuild();
void SetAeroEnabled(bool enable);
void SetProcessPriority(const char *priority);
void SetWin32DropStyle(QWidget *window);
diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp
index 53310ae65..7670af985 100644
--- a/UI/window-basic-main.cpp
+++ b/UI/window-basic-main.cpp
@@ -9947,3 +9947,29 @@ void OBSBasic::UpdatePreviewSafeAreas()
drawSafeAreas = config_get_bool(App()->GlobalConfig(), "BasicWindow",
"ShowSafeAreas");
}
+
+void OBSBasic::SetDisplayAffinity(QWindow *window)
+{
+ if (!SetDisplayAffinitySupported())
+ return;
+
+ bool hideFromCapture = config_get_bool(App()->GlobalConfig(),
+ "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();
+
+ if (hideFromCapture)
+ SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE);
+ else
+ SetWindowDisplayAffinity(hwnd, WDA_NONE);
+#else
+// TODO: Implement for other platforms if possible. Don't forget to
+// implement SetDisplayAffinitySupported too!
+#endif
+}
diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp
index d68070041..565314bcb 100644
--- a/UI/window-basic-main.hpp
+++ b/UI/window-basic-main.hpp
@@ -943,6 +943,8 @@ public:
void UpdateEditMenu();
+ void SetDisplayAffinity(QWindow *window);
+
protected:
virtual void closeEvent(QCloseEvent *event) override;
virtual void changeEvent(QEvent *event) override;
diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp
index b5d4be7ee..10b278c33 100644
--- a/UI/window-basic-settings.cpp
+++ b/UI/window-basic-settings.cpp
@@ -383,6 +383,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
HookWidget(ui->theme, 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);
@@ -589,6 +590,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
#ifdef _WIN32
uint32_t winVer = GetWindowsVersion();
if (winVer > 0 && winVer < 0x602) {
+ // Older than Windows 8
toggleAero = new QCheckBox(
QTStr("Basic.Settings.Video.DisableAero"), this);
QFormLayout *videoLayout = reinterpret_cast(
@@ -600,6 +602,11 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
&OBSBasicSettings::ToggleDisableAero);
}
+ if (!SetDisplayAffinitySupported()) {
+ delete ui->hideOBSFromCapture;
+ ui->hideOBSFromCapture = nullptr;
+ }
+
#define PROCESS_PRIORITY(val) \
{ \
"Basic.Settings.Advanced.General.ProcessPriority."##val, val \
@@ -627,6 +634,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
delete ui->processPriority;
delete ui->enableNewSocketLoop;
delete ui->enableLowLatencyMode;
+ delete ui->hideOBSFromCapture;
#ifdef __linux__
delete ui->browserHWAccel;
delete ui->sourcesGroup;
@@ -642,6 +650,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
ui->processPriority = nullptr;
ui->enableNewSocketLoop = nullptr;
ui->enableLowLatencyMode = nullptr;
+ ui->hideOBSFromCapture = nullptr;
#ifdef __linux__
ui->browserHWAccel = nullptr;
ui->sourcesGroup = nullptr;
@@ -1226,6 +1235,15 @@ void OBSBasicSettings::LoadGeneralSettings()
"OpenStatsOnStartup");
ui->openStatsOnStartup->setChecked(openStatsOnStartup);
+#if defined(_WIN32)
+ if (ui->hideOBSFromCapture) {
+ bool hideWindowFromCapture =
+ config_get_bool(GetGlobalConfig(), "BasicWindow",
+ "HideOBSWindowsFromCapture");
+ ui->hideOBSFromCapture->setChecked(hideWindowFromCapture);
+ }
+#endif
+
bool recordWhenStreaming = config_get_bool(
GetGlobalConfig(), "BasicWindow", "RecordWhenStreaming");
ui->recordWhenStreaming->setChecked(recordWhenStreaming);
@@ -2974,6 +2992,20 @@ void OBSBasicSettings::SaveGeneralSettings()
config_set_bool(GetGlobalConfig(), "General",
"EnableAutoUpdates",
ui->enableAutoUpdates->isChecked());
+#endif
+#ifdef _WIN32
+ if (WidgetChanged(ui->hideOBSFromCapture)) {
+ bool hide_window = ui->hideOBSFromCapture->isChecked();
+ config_set_bool(GetGlobalConfig(), "BasicWindow",
+ "HideOBSWindowsFromCapture", hide_window);
+
+ QWindowList windows = QGuiApplication::allWindows();
+ for (auto window : windows) {
+ if (window->isVisible()) {
+ main->SetDisplayAffinity(window);
+ }
+ }
+ }
#endif
if (WidgetChanged(ui->openStatsOnStartup))
config_set_bool(main->Config(), "General", "OpenStatsOnStartup",
diff --git a/UI/window-projector.cpp b/UI/window-projector.cpp
index 7eb04e011..f831a3b50 100644
--- a/UI/window-projector.cpp
+++ b/UI/window-projector.cpp
@@ -30,6 +30,10 @@ OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor,
if (isAlwaysOnTop)
setWindowFlags(Qt::WindowStaysOnTopHint);
+ // Mark the window as a projector so SetDisplayAffinity
+ // can skip it
+ windowHandle()->setProperty("isOBSProjectorWindow", true);
+
#if defined(__linux__) || defined(__FreeBSD__) || defined(__DragonFly__)
// Prevents resizing of projector windows
setAttribute(Qt::WA_PaintOnScreen, false);