Compare commits

...

13 Commits

Author SHA1 Message Date
WarmUpTill
e1020a1909 Fix poad load steps being executed too frequently
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2025-10-30 20:48:33 +01:00
WarmUpTill
8b0bd4193b Fix temp var save / loading not working 2025-10-30 20:48:33 +01:00
WarmUpTill
d55bb6bc86 Fall back to obs_frontend_get_current_scene()
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
If there wasn't any scene change yet GetCurrentScene() would always
return nullptr and break various scene checks.
For example, this could happen when startup up a fresh OBS install for
the first time.
2025-10-29 12:26:44 +01:00
WarmUpTill
0583331bfd Fix scene selection not working without secondary canvases 2025-10-29 12:26:44 +01:00
WarmUpTill
6932de866d Refactor Twitch event server migration and reconnect handling
Some checks failed
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
Check locale / ubuntu64 (push) Has been cancelled
This should avoid any events being lost due to server migration.
2025-10-28 19:20:52 +01:00
WarmUpTill
00db0cf7c4 CI: Refactor OpenSSL handling on MacOS to support MQTT SSL 2025-10-28 19:20:52 +01:00
WarmUpTill
e9baf27ca2 Add helper to find recent versions of OpenSSL on Windows 2025-10-28 19:20:52 +01:00
WarmUpTill
b1a5db0c9c Fix crash when switching macros after deleting one containing temp refs 2025-10-28 19:20:52 +01:00
WarmUpTill
8f3b868fd9 Limit projector action to main canvas and improve layout 2025-10-28 19:20:52 +01:00
WarmUpTill
b3bf89840b Add GetPath() 2025-10-28 19:20:52 +01:00
WarmUpTill
84f7d0d214 Add SSL support to MQTT connections
Also fixes crash on startup if SSL was used while there was no support
for encrypted connections yet
2025-10-28 19:20:52 +01:00
WarmUpTill
e1164c4fa3 Refactor help icon usage 2025-10-28 19:20:52 +01:00
WarmUpTill
4534b23bad Enable Windows and MacOS build with OBS versions older than 31.1.1 2025-10-28 19:20:52 +01:00
37 changed files with 651 additions and 217 deletions

View File

@@ -22,7 +22,7 @@ runs:
- name: Setup cmake
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.28.x'
cmake-version: '3.x.x'
- name: Restore cached dependencies
id: restore-cache

View File

@@ -394,8 +394,11 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
popd
pushd ${advss_dep_path}
log_info "Prepare openssl ..."
rm -rf ${advss_dep_path}/openssl ${advss_dep_path}/openssl_build
mkdir ${advss_dep_path}/openssl_build
pushd ${advss_dep_path}/openssl_build
rm -rf openssl
git clone https://github.com/openssl/openssl.git --branch openssl-3.1.2 --depth 1
mv openssl openssl_x86
@@ -403,25 +406,27 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
log_info "Building openssl x86 ..."
export MACOSX_DEPLOYMENT_TARGET=10.9
cd openssl_x86
./Configure darwin64-x86_64-cc shared
make
pushd openssl_x86
./Configure darwin64-x86_64-cc no-shared no-module no-zlib --prefix=${advss_dep_path}
make -j$(nproc)
popd
log_info "Building openssl arm ..."
export MACOSX_DEPLOYMENT_TARGET=10.15
cd ../openssl_arm
./Configure enable-rc5 zlib darwin64-arm64-cc no-asm
make
pushd openssl_arm
./Configure enable-rc5 darwin64-arm64-cc no-shared no-module no-asm no-zlib --prefix=${advss_dep_path}
make -j$(nproc)
log_info "Install openssl ..."
make install
popd
log_info "Combine arm and x86 openssl binaries ..."
cd ..
mkdir openssl-combined
lipo -create openssl_x86/libcrypto.a openssl_arm/libcrypto.a -output openssl-combined/libcrypto.a
lipo -create openssl_x86/libssl.a openssl_arm/libssl.a -output openssl-combined/libssl.a
lipo -create openssl_x86/libcrypto.a openssl_arm/libcrypto.a -output ${advss_dep_path}/lib/libcrypto.a
lipo -create openssl_x86/libssl.a openssl_arm/libssl.a -output ${advss_dep_path}/lib/libssl.a
log_info "Clean up openssl dir ..."
mv openssl_x86 openssl
rm -rf openssl_arm
rm -rf openssl_x86 openssl_arm
popd
pushd ${project_root}/deps/libusb
@@ -439,14 +444,16 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
mkdir ${project_root}/deps/libusb/out_x86
./autogen.sh
./configure --host=x86_64-apple-darwin --prefix=${advss_dep_path}
make && make install
make -j$(nproc)
make install
log_info "Configure libusb arm ..."
make clean
rm -r ${project_root}/deps/libusb/out_x86
mkdir ${project_root}/deps/libusb/out_x86
./configure --host=aarch64-apple-darwin --prefix=${project_root}/deps/libusb/out_x86
make && make install
make -j$(nproc)
make install
log_info "Building libusb arm ..."
make clean
@@ -459,7 +466,8 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
export MACOSX_DEPLOYMENT_TARGET=10.15
mkdir ${project_root}/deps/libusb/out_arm
./configure --host=aarch64-apple-darwin --prefix=${project_root}/deps/libusb/out_arm
make && make install
make -j$(nproc)
make install
log_info "Combine arm and x86 libusb binaries ..."
lipo -create ${project_root}/deps/libusb/out_x86/lib/libusb-1.0.0.dylib \
@@ -491,8 +499,8 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
-DPAHO_BUILD_SHARED=OFF
-DPAHO_BUILD_STATIC=ON
-DPAHO_WITH_MQTT_C=ON
-DOPENSSL_ROOT_DIR="${advss_dep_path}"
-DPAHO_WITH_SSL=OFF # TODO: figure out linking issues with openssl
-DPAHO_WITH_SSL=ON
-DOPENSSL_USE_STATIC_LIBS=ON
)
pushd ${mqtt_dir}

View File

@@ -253,12 +253,8 @@ ${_usage_host:-}"
macos-*)
if (( ${+CI} )) typeset -gx NSUnbufferedIO=YES
local openssl_lib_dir="${advss_deps_path}/openssl-combined/"
local openssl_include_dir="${advss_deps_path}/openssl/include"
cmake_args+=(
-DOPENSSL_INCLUDE_DIR="${openssl_include_dir}"
-DOPENSSL_LIBRARIES="${openssl_lib_dir}/libcrypto.a;${openssl_lib_dir}/libssl.a"
-DCMAKE_PREFIX_PATH="${advss_deps_path}"
--preset ${_preset}
)

View File

@@ -234,8 +234,24 @@ function Build {
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
"-DPAHO_WITH_MQTT_C=ON"
"-DPAHO_WITH_SSL=ON"
)
# Try to find OpenSSL installed via winget
$pf64 = Join-Path $Env:ProgramFiles "OpenSSL-Win64"
$pf = Join-Path $Env:ProgramFiles "OpenSSL"
$possibleDirs = @($pf64, $pf)
$opensslDir = $possibleDirs | Where-Object { Test-Path (Join-Path $_ "include\openssl\ssl.h") } | Select-Object -First 1
if ($opensslDir) {
Write-Host "Detected OpenSSL at: $opensslDir"
$MqttCmakeArgs += "-DOPENSSL_ROOT_DIR=$opensslDir"
$MqttCmakeArgs += "-DOPENSSL_CRYPTO_LIBRARY=$opensslDir\lib\VC\x64\MD\libcrypto.lib"
$MqttCmakeArgs += "-DOPENSSL_SSL_LIBRARY=$opensslDir\lib\VC\x64\MD\libssl.lib"
} else {
Write-Warning "OpenSSL not found - maybe cmake will find it ..."
}
Log-Information "Configuring paho.mqtt.cpp..."
Invoke-External cmake -S ${MqttPath} -B ${MqttBuildPath} @MqttCmakeArgs

View File

@@ -18,5 +18,7 @@ if (( ! ${+commands[brew]} )) {
}
brew bundle --file ${SCRIPT_HOME}/.Brewfile
rehash
# Workaround to make sure locally built openssl is picked up by cmake
brew uninstall --ignore-dependencies openssl@3 || true
rehash || true
log_group

View File

@@ -6,7 +6,7 @@ on:
description: "Project name detected by parsing build spec file"
value: ${{ jobs.check-event.outputs.pluginName }}
env:
DEP_DIR: .deps/advss-build-dependencies-2
DEP_DIR: .deps/advss-build-dependencies-3
jobs:
check-event:
name: Check GitHub Event Data 🔎
@@ -213,7 +213,7 @@ jobs:
- name: Set up CMake 🏗️
uses: jwlawson/actions-setup-cmake@v1.13
with:
cmake-version: '3.24.x'
cmake-version: '3.x.x'
- name: Set up Homebrew 🍺
uses: Homebrew/actions/setup-homebrew@master

View File

@@ -34,6 +34,11 @@ include(cmake/common/get_git_revision_description.cmake)
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
git_describe(GIT_TAG)
# Helper for OpenSSL
if(OS_WINDOWS)
include(cmake/windows/wingetssl.cmake)
endif()
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
set(GIT_TAG ${PROJECT_VERSION})
endif()

View File

@@ -0,0 +1,78 @@
# ---------------------------------------------------------------------------
# Detects OpenSSL installed via winget or other common Windows locations,
# without requiring the user to manually set OPENSSL_ROOT_DIR.
# ---------------------------------------------------------------------------
if(WIN32 AND (NOT OpenSSL_FOUND))
set(_openssl_roots
"$ENV{ProgramFiles}/OpenSSL-Win64" "$ENV{ProgramFiles}/OpenSSL"
"$ENV{ProgramW6432}/OpenSSL-Win64")
set(_openssl_lib_suffixes "lib/VC/x64/MD" "lib/VC/x64/MDd" "lib/VC/x64/MT"
"lib/VC/x64/MTd" "lib")
# Determine which configuration we're building
if(CMAKE_BUILD_TYPE MATCHES "Debug")
set(_is_debug TRUE)
else()
set(_is_debug FALSE)
endif()
# Determine which runtime we use Default to /MD (shared CRT)
set(_crt_kind "MD")
if(MSVC)
if(CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "MultiThreaded")
if(CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "Debug")
set(_crt_kind "MTd")
else()
set(_crt_kind "MT")
endif()
else()
if(_is_debug)
set(_crt_kind "MDd")
else()
set(_crt_kind "MD")
endif()
endif()
endif()
message(STATUS "Looking for OpenSSL built with CRT variant: ${_crt_kind}")
# Try to find the root and corresponding lib path
foreach(_root ${_openssl_roots})
if(EXISTS "${_root}/include/openssl/ssl.h")
foreach(_suffix ${_openssl_lib_suffixes})
if(_suffix MATCHES "${_crt_kind}$"
AND EXISTS "${_root}/${_suffix}/libcrypto.lib")
set(OPENSSL_ROOT_DIR
"${_root}"
CACHE PATH "Path to OpenSSL root")
set(OPENSSL_CRYPTO_LIBRARY
"${_root}/${_suffix}/libcrypto.lib"
CACHE FILEPATH "OpenSSL crypto lib")
set(OPENSSL_SSL_LIBRARY
"${_root}/${_suffix}/libssl.lib"
CACHE FILEPATH "OpenSSL ssl lib")
set(OPENSSL_INCLUDE_DIR
"${_root}/include"
CACHE PATH "OpenSSL include dir")
set(OpenSSL_FOUND
TRUE
CACHE BOOL "Whether OpenSSL was found")
message(STATUS "Found OpenSSL at: ${_root}/${_suffix}")
return()
endif()
endforeach()
endif()
if(OpenSSL_FOUND)
break()
endif()
endforeach()
if(NOT OpenSSL_FOUND)
message(WARNING "Could not auto-detect OpenSSL under Program Files. "
"Might have to set OPENSSL_ROOT_DIR manually.")
endif()
endif()

View File

@@ -1514,6 +1514,13 @@ AdvSceneSwitcher.mqttConnection.name="Name:"
AdvSceneSwitcher.mqttConnection.address="Address:"
AdvSceneSwitcher.mqttConnection.username="Username:"
AdvSceneSwitcher.mqttConnection.password="Password:"
AdvSceneSwitcher.mqttConnection.trustStore="Trust store:"
AdvSceneSwitcher.mqttConnection.trustStore.help="The file in PEM format containing the public digital certificates trusted by the client."
AdvSceneSwitcher.mqttConnection.keyStore="Key store:"
AdvSceneSwitcher.mqttConnection.keyStore.help="The file in PEM format containing the public certificate chain of the client.\nIt may also include the client's private key."
AdvSceneSwitcher.mqttConnection.privateKey="Private key:"
AdvSceneSwitcher.mqttConnection.privateKey.help="If not included in the key store, this is the file in PEM format containing the client's private key."
AdvSceneSwitcher.mqttConnection.verifyServerCert="Verify server certificate"
AdvSceneSwitcher.mqttConnection.reconnect="Reconnect automatically:"
AdvSceneSwitcher.mqttConnection.reconnectDelay="Automatically reconnect after:"
AdvSceneSwitcher.mqttConnection.connectOnStart="Connect on startup:"

View File

@@ -459,7 +459,7 @@ void SwitcherData::LoadSettings(obs_data_t *obj)
LoadHotkeys(obj);
LoadUISettings(obj);
RunPostLoadSteps();
RunAndClearPostLoadSteps();
// Reset on startup and scene collection change
ResetLastOpenedTab();

View File

@@ -117,7 +117,7 @@ void MacroActionEdit::ActionSelectionChanged(const QString &text)
*_entryData = MacroActionFactory::Create(id, macro);
(*_entryData)->SetIndex(idx);
(*_entryData)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
}
auto widget = MacroActionFactory::CreateWidget(id, this, *_entryData);
QWidget::connect(widget, SIGNAL(HeaderInfoChanged(const QString &)),

View File

@@ -1,4 +1,5 @@
#include "macro-action-variable.hpp"
#include "help-icon.hpp"
#include "json-helpers.hpp"
#include "layout-helpers.hpp"
#include "macro-condition-edit.hpp"
@@ -828,7 +829,10 @@ MacroActionVariableEdit::MacroActionVariableEdit(
this)),
_randomLayout(new QVBoxLayout()),
_jsonQuery(new VariableLineEdit(this)),
_jsonQueryHelp(new QLabel(this)),
_jsonQueryHelp(new HelpIcon(
obs_module_text(
"AdvSceneSwitcher.action.variable.type.queryJson.info"),
this)),
_jsonIndex(new VariableSpinBox(this)),
_entryLayout(new QHBoxLayout())
{
@@ -859,14 +863,6 @@ MacroActionVariableEdit::MacroActionVariableEdit(
_randomNumberStart->setMaximum(9999999999);
_randomNumberEnd->setMinimum(-9999999999);
_randomNumberEnd->setMaximum(9999999999);
const QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
const QIcon icon(path);
const QPixmap pixmap = icon.pixmap(QSize(16, 16));
_jsonQueryHelp->setPixmap(pixmap);
_jsonQueryHelp->setToolTip(obs_module_text(
"AdvSceneSwitcher.action.variable.type.queryJson.info"));
_jsonIndex->setMaximum(999);
QWidget::connect(_variables, SIGNAL(SelectionChanged(const QString &)),

View File

@@ -239,7 +239,7 @@ void MacroConditionEdit::ConditionSelectionChanged(const QString &text)
(*_entryData)->SetIndex(idx);
(*_entryData)->SetLogicType(logic);
(*_entryData)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
}
auto widget =
MacroConditionFactory::CreateWidget(id, this, *_entryData);

View File

@@ -1080,7 +1080,7 @@ void MacroEdit::AddMacroAction(Macro *macro, int idx, const std::string &id,
macro->Actions().at(idx)->Load(data);
}
macro->Actions().at(idx)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
macro->UpdateActionIndices();
ui->actionsList->Insert(
idx,
@@ -1383,7 +1383,7 @@ void MacroEdit::AddMacroElseAction(Macro *macro, int idx, const std::string &id,
macro->ElseActions().at(idx)->Load(data);
}
macro->ElseActions().at(idx)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
macro->UpdateElseActionIndices();
ui->elseActionsList->Insert(
idx, new MacroActionEdit(
@@ -1583,7 +1583,7 @@ void MacroEdit::AddMacroCondition(Macro *macro, int idx, const std::string &id,
macro->Conditions().at(idx)->Load(data);
}
macro->Conditions().at(idx)->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
(*cond)->SetLogicType(logic);
macro->UpdateConditionIndices();
ui->conditionsList->Insert(

View File

@@ -416,7 +416,7 @@ void AdvSceneSwitcher::ImportMacros()
OBSDataAutoRelease array_obj = obs_data_array_item(array, i);
auto macro = std::make_shared<Macro>();
macro->Load(array_obj);
RunPostLoadSteps();
RunAndClearPostLoadSteps();
if (macroNameExists(macro->Name()) &&
!ResolveMacroImportNameConflict(macro)) {
@@ -444,7 +444,7 @@ void AdvSceneSwitcher::ImportMacros()
for (const auto &macro : importedMacros) {
macro->PostLoad();
}
RunPostLoadSteps();
RunAndClearPostLoadSteps();
ui->macros->Reset(GetMacros(),
GetGlobalMacroSettings()._highlightExecuted);
@@ -752,7 +752,7 @@ void AdvSceneSwitcher::CopyMacro()
newMacro->Load(data);
newMacro->PostLoad();
newMacro->SetName(name);
RunPostLoadSteps();
RunAndClearPostLoadSteps();
Macro::PrepareMoveToGroup(macro->Parent(), newMacro);
ui->macros->Add(newMacro, macro);

View File

@@ -113,7 +113,7 @@ void ActionQueue::Add(const std::shared_ptr<MacroAction> &action)
action->Save(data);
copy->Load(data);
copy->PostLoad();
RunPostLoadSteps();
RunAndClearPostLoadSteps();
copy->ResolveVariablesToFixedValues();
_actions.emplace_back(copy);
} else {

View File

@@ -2,31 +2,31 @@
namespace advss {
AutoUpdateTooltipLabel::AutoUpdateTooltipLabel(
AutoUpdateHelpIcon::AutoUpdateHelpIcon(
QWidget *parent, const std::function<QString()> &updateTooltipCallback,
int updateIntervalMs)
: QLabel(parent),
: HelpIcon("", parent),
_callback(updateTooltipCallback),
_timer(new QTimer(this)),
_updateIntervalMs(updateIntervalMs)
{
connect(_timer, &QTimer::timeout, this,
&AutoUpdateTooltipLabel::UpdateTooltip);
&AutoUpdateHelpIcon::UpdateTooltip);
}
void AutoUpdateTooltipLabel::enterEvent(QEnterEvent *event)
void AutoUpdateHelpIcon::enterEvent(QEnterEvent *event)
{
_timer->start(_updateIntervalMs);
QLabel::enterEvent(event);
}
void AutoUpdateTooltipLabel::leaveEvent(QEvent *event)
void AutoUpdateHelpIcon::leaveEvent(QEvent *event)
{
_timer->stop();
QLabel::leaveEvent(event);
}
void AutoUpdateTooltipLabel::UpdateTooltip()
void AutoUpdateHelpIcon::UpdateTooltip()
{
setToolTip(_callback());
}

View File

@@ -1,20 +1,19 @@
#pragma once
#include "export-symbol-helper.hpp"
#include "help-icon.hpp"
#include <functional>
#include <QLabel>
#include <QTimer>
namespace advss {
class ADVSS_EXPORT AutoUpdateTooltipLabel : public QLabel {
class ADVSS_EXPORT AutoUpdateHelpIcon : public HelpIcon {
Q_OBJECT
public:
AutoUpdateTooltipLabel(
QWidget *parent,
const std::function<QString()> &updateTooltipCallback,
int updateIntervalMs = 300);
AutoUpdateHelpIcon(QWidget *parent,
const std::function<QString()> &updateTooltipCallback,
int updateIntervalMs = 300);
protected:
void enterEvent(QEnterEvent *event) override;

View File

@@ -35,6 +35,11 @@ void FileSelection::SetPath(const QString &path)
_filePath->setText(path);
}
QString FileSelection::GetPath() const
{
return _filePath->text();
}
QString FileSelection::ValidPathOrDesktop(const QString &path)
{
QFileInfo fileInfo(path);

View File

@@ -22,6 +22,7 @@ public:
QWidget *parent = 0);
EXPORT void SetPath(const StringVariable &);
EXPORT void SetPath(const QString &);
EXPORT QString GetPath() const;
EXPORT QPushButton *Button() { return _browseButton; }
EXPORT static QString ValidPathOrDesktop(const QString &path);

View File

@@ -113,12 +113,13 @@ void RunLoadSteps(obs_data_t *obj)
}
}
void RunPostLoadSteps()
void RunAndClearPostLoadSteps()
{
std::lock_guard<std::mutex> lock(postLoadMutex);
for (const auto &func : getPostLoadSteps()) {
func();
}
getPostLoadSteps().clear();
}
void ClearPostLoadSteps()

View File

@@ -14,7 +14,7 @@ EXPORT void AddPostLoadStep(std::function<void()>);
EXPORT void AddIntervalResetStep(std::function<void()>);
void RunSaveSteps(obs_data_t *);
void RunLoadSteps(obs_data_t *);
EXPORT void RunPostLoadSteps();
EXPORT void RunAndClearPostLoadSteps();
void ClearPostLoadSteps();
EXPORT void AddPluginInitStep(std::function<void()>);

View File

@@ -239,22 +239,26 @@ void SceneSelection::ResolveVariables()
_type = Type::SCENE;
}
SceneSelection SceneSelectionWidget::CurrentSelection()
static obs_weak_canvas_t *getWeakRefToMainCanvas()
{
SceneSelection s;
static auto mainCanvas = obs_get_main_canvas();
static auto mainCanvasWeak = obs_canvas_get_weak_canvas(mainCanvas);
static auto canvas = obs_get_main_canvas();
static auto weakCanvas = obs_canvas_get_weak_canvas(canvas);
[[maybe_unused]] static const bool _ = []() {
// Let's just hope we don't have to deal with selecting scenes
// when the OBS main canvas gets deleted and release the
// references here already to avoid reporting leaks on shutdown
obs_canvas_release(mainCanvas);
obs_weak_canvas_release(mainCanvasWeak);
obs_canvas_release(canvas);
obs_weak_canvas_release(weakCanvas);
return true;
}();
return weakCanvas;
}
s._canvas = _forceMainCanvas ? OBSWeakCanvas(mainCanvasWeak)
SceneSelection SceneSelectionWidget::CurrentSelection()
{
SceneSelection s;
s._canvas = _forceMainCanvas ? OBSWeakCanvas(getWeakRefToMainCanvas())
: _canvas->GetCanvas();
const int idx = _scenes->currentIndex();
@@ -342,6 +346,10 @@ void SceneSelectionWidget::Reset()
void SceneSelectionWidget::PopulateSceneSelection(obs_weak_canvas_t *canvas)
{
if (_forceMainCanvas) {
canvas = getWeakRefToMainCanvas();
}
_scenes->clear();
if ((_current || _previous)) {
const bool isMain = IsMainCanvas(canvas);
@@ -418,7 +426,7 @@ SceneSelectionWidget::SceneSelectionWidget(QWidget *parent, bool variables,
layout->setContentsMargins(0, 0, 0, 0);
if (GetCanvasCount() <= 1) {
_canvas->hide();
LockToMainCanvas();
}
Resize();

View File

@@ -46,7 +46,7 @@ public:
bool previous = false, bool current = false,
bool preview = false);
EXPORT void SetScene(const SceneSelection &);
void LockToMainCanvas();
EXPORT void LockToMainCanvas();
protected:
void showEvent(QShowEvent *event) override;

View File

@@ -199,7 +199,17 @@ std::chrono::high_resolution_clock::time_point GetLastSceneChangeTime()
OBSWeakSource GetCurrentScene()
{
return switcher->currentScene;
if (switcher->currentScene) {
return switcher->currentScene;
}
// If there wasn't any scene switch yet switcher->currentScene will be
// null and we must use obs_frontend_get_current_scene() instead
OBSSourceAutoRelease currentSceneSource =
obs_frontend_get_current_scene();
OBSWeakSourceAutoRelease currentSceneWeakSource =
obs_source_get_weak_source(currentSceneSource);
return currentSceneWeakSource.Get();
}
OBSWeakSource GetPreviousScene()

View File

@@ -202,6 +202,8 @@ static void appendNestedMacros(std::deque<std::shared_ptr<Macro>> &macros,
dynamic_cast<MacroActionMacro *>(action.get());
if (nestedMacroAction) {
macros.push_back(nestedMacroAction->_nestedMacro);
appendNestedMacros(
macros, nestedMacroAction->_nestedMacro.get());
}
}
for (const auto &action : macro->ElseActions()) {
@@ -209,6 +211,8 @@ static void appendNestedMacros(std::deque<std::shared_ptr<Macro>> &macros,
dynamic_cast<MacroActionMacro *>(action.get());
if (nestedMacroAction) {
macros.push_back(nestedMacroAction->_nestedMacro);
appendNestedMacros(
macros, nestedMacroAction->_nestedMacro.get());
}
}
}
@@ -299,9 +303,22 @@ void TempVariableRef::Save(obs_data_t *obj, Macro *macro,
obs_data_set_obj(obj, name, data);
}
void TempVariableRef::Load(obs_data_t *obj, Macro *macro, const char *name)
void TempVariableRef::Load(obs_data_t *obj, Macro *macroPtr, const char *name)
{
if (!macro) {
std::deque<std::shared_ptr<Macro>> allMacros = GetMacros();
for (const auto &topLevelMacro : GetMacros()) {
appendNestedMacros(allMacros, topLevelMacro.get());
}
std::weak_ptr<Macro> macro;
for (const auto &macroShared : allMacros) {
if (macroShared.get() == macroPtr) {
macro = macroShared;
break;
}
}
if (macro.expired()) {
_segment.reset();
return;
}
@@ -324,10 +341,17 @@ void TempVariableRef::Load(obs_data_t *obj, Macro *macro, const char *name)
});
}
void TempVariableRef::PostLoad(int idx, SegmentType type, Macro *macro)
void TempVariableRef::PostLoad(int idx, SegmentType type,
const std::weak_ptr<Macro> &weakMacro)
{
auto childMacro = weakMacro.lock();
if (!childMacro) {
return;
}
auto macro = childMacro.get();
for (int i = 0; i < _depth; i++) {
macro = getParentMacro(macro);
macro = getParentMacro(childMacro.get());
}
if (!macro) {
@@ -404,9 +428,8 @@ TempVariableSelection::TempVariableSelection(QWidget *parent)
: QWidget(parent),
_selection(new FilterComboBox(
this, obs_module_text("AdvSceneSwitcher.tempVar.select"))),
_info(new AutoUpdateTooltipLabel(this, [this]() {
return SetupInfoLabel();
}))
_info(new AutoUpdateHelpIcon(this,
[this]() { return SetupInfoLabel(); }))
{
MacroEdit *edit = findMacroEditParent(parent);
@@ -416,12 +439,6 @@ TempVariableSelection::TempVariableSelection(QWidget *parent)
_macroEdits.push_back(edit);
}
QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
QIcon icon(path);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
_info->setPixmap(pixmap);
_info->hide();
_selection->setSizeAdjustPolicy(QComboBox::AdjustToContents);

View File

@@ -72,10 +72,10 @@ private:
enum class SegmentType { NONE, CONDITION, ACTION, ELSEACTION };
SegmentType GetType() const;
int GetIdx() const;
void PostLoad(int idx, SegmentType, Macro *);
void PostLoad(int idx, SegmentType, const std::weak_ptr<Macro> &);
std::string _id = "";
std::weak_ptr<MacroSegment> _segment;
std::weak_ptr<MacroSegment> _segment = {};
int _depth = 0;
friend TempVariable;
@@ -105,7 +105,7 @@ private:
MacroSegment *GetSegment() const;
FilterComboBox *_selection;
AutoUpdateTooltipLabel *_info;
AutoUpdateHelpIcon *_info;
std::vector<MacroEdit *> _macroEdits;
};

View File

@@ -250,6 +250,10 @@ MacroActionProjectorEdit::MacroActionProjectorEdit(
_regex(new RegexConfigWidget(this)),
_layout(new QHBoxLayout(this))
{
// The obs_frontend_open_projector() function does not seem to support
// scenes of secondary canvases
_scenes->LockToMainCanvas();
populateActionSelection(_actions);
populateWindowTypes(_windowTypes);
populateSelectionTypes(_types);
@@ -396,26 +400,29 @@ void MacroActionProjectorEdit::SetWidgetVisibility()
return;
}
_projectorWindowName->setVisible(_entryData->_action ==
const auto &action = _entryData->_action;
const auto &type = _entryData->_type;
_projectorWindowName->setVisible(action ==
MacroActionProjector::Action::CLOSE);
_regex->setVisible(_entryData->_action ==
MacroActionProjector::Action::CLOSE);
_types->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN);
_windowTypes->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN);
_scenes->setVisible(
_entryData->_action == MacroActionProjector::Action::OPEN &&
_entryData->_type == MacroActionProjector::Type::SCENE);
_sources->setVisible(
_entryData->_action == MacroActionProjector::Action::OPEN &&
_entryData->_type == MacroActionProjector::Type::SOURCE);
_monitors->setVisible(_entryData->_action ==
MacroActionProjector::Action::OPEN &&
_regex->setVisible(action == MacroActionProjector::Action::CLOSE);
_types->setVisible(action == MacroActionProjector::Action::OPEN);
_windowTypes->setVisible(action == MacroActionProjector::Action::OPEN);
_scenes->setVisible(action == MacroActionProjector::Action::OPEN &&
type == MacroActionProjector::Type::SCENE);
_sources->setVisible(action == MacroActionProjector::Action::OPEN &&
type == MacroActionProjector::Type::SOURCE);
_monitors->setVisible(action == MacroActionProjector::Action::OPEN &&
_entryData->_fullscreen);
adjustSize();
updateGeometry();
if (action == MacroActionProjector::Action::CLOSE) {
RemoveStretchIfPresent(_layout);
} else {
AddStretchIfNecessary(_layout);
}
}
} // namespace advss

View File

@@ -240,7 +240,7 @@ bool MacroActionSwitchScene::Load(obs_data_t *obj)
GetWeakCanvasByName(obs_data_get_string(obj, "canvas"));
} else {
OBSCanvasAutoRelease main = obs_get_main_canvas();
_canvas = OBSGetWeakRef(main);
_canvas = obs_canvas_get_weak_canvas(main);
}
_index.Save(obj, "index");

View File

@@ -242,7 +242,7 @@ MacroConditionStreamEdit::MacroConditionStreamEdit(
_keyFrameInterval(new VariableSpinBox()),
_streamKey(new VariableLineEdit(this)),
_serviceName(new VariableLineEdit(this)),
_currentService(new AutoUpdateTooltipLabel(
_currentService(new AutoUpdateHelpIcon(
this,
[]() {
QString formatString = obs_module_text(
@@ -255,13 +255,6 @@ MacroConditionStreamEdit::MacroConditionStreamEdit(
_keyFrameInterval->setMinimum(0);
_keyFrameInterval->setMaximum(25);
QString path = GetThemeTypeName() == "Light"
? ":/res/images/help.svg"
: ":/res/images/help_light.svg";
QIcon icon(path);
QPixmap pixmap = icon.pixmap(QSize(16, 16));
_currentService->setPixmap(pixmap);
_streamKey->setEchoMode(QLineEdit::PasswordEchoOnEdit);
populateConditionSelection(_conditions);

View File

@@ -15,12 +15,15 @@ if(NOT TARGET httplib)
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
if(OS_MACOS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
endif()
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
return()
endif()
find_package(ZLIB)

View File

@@ -3,6 +3,13 @@ project(advanced-scene-switcher-mqtt)
# --- Check paho.mqtt.cpp requirements ---
if(OS_MACOS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
endif()
find_package(OpenSSL)
find_package(PahoMqttCpp)
if(NOT PahoMqttCpp_FOUND)
message(WARNING "PahoMqttCpp not found!\n"

View File

@@ -1,4 +1,5 @@
#include "mqtt-helpers.hpp"
#include "help-icon.hpp"
#include "layout-helpers.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
@@ -22,6 +23,10 @@ MqttConnection::MqttConnection(const MqttConnection &other)
_uri(other._uri),
_username(other._username),
_password(other._password),
_trustStore(other._trustStore),
_keyStore(other._keyStore),
_privateKey(other._privateKey),
_verifyServerCert(other._verifyServerCert),
_connectOnStart(other._connectOnStart),
_reconnect(other._reconnect),
_reconnectDelay(other._reconnectDelay)
@@ -30,12 +35,20 @@ MqttConnection::MqttConnection(const MqttConnection &other)
MqttConnection::MqttConnection(const std::string &name, const std::string &uri,
const std::string &username,
const std::string &password, bool connectOnStart,
const std::string &password,
const std::string &trustStore,
const std::string &keyStore,
const std::string &privateKey,
bool verifyServerCert, bool connectOnStart,
bool reconnect, int reconnectDelay)
: Item(name),
_uri(uri),
_username(username),
_password(password),
_trustStore(trustStore),
_keyStore(keyStore),
_privateKey(privateKey),
_verifyServerCert(verifyServerCert),
_connectOnStart(connectOnStart),
_reconnect(reconnect),
_reconnectDelay(reconnectDelay)
@@ -99,26 +112,41 @@ void MqttConnection::ConnectThread()
};
do {
std::unique_lock<std::mutex> clientLock(_clientMtx);
_client = std::make_shared<mqtt::async_client>(
_uri, std::string("advss_") + _name);
#ifdef ENABLE_MQTT5_SUPPORT
auto connOpts = mqtt::connect_options_builder::v5()
#else
auto connOpts = mqtt::connect_options_builder()
#endif
.clean_start(false)
.clean_session(true)
.connect_timeout(5s)
.user_name(_username)
.password(_password)
.finalize();
_client->set_connection_lost_handler(logConnectionLost);
_client->set_message_callback(dispatchMessage);
_client->start_consuming();
try {
std::unique_lock<std::mutex> clientLock(_clientMtx);
_client = std::make_shared<mqtt::async_client>(
_uri, std::string("advss_") + _name);
mqtt::ssl_options sslOptions;
if (!_trustStore.empty()) {
sslOptions.set_trust_store(_trustStore);
}
if (!_keyStore.empty()) {
sslOptions.set_key_store(_keyStore);
}
if (!_privateKey.empty()) {
sslOptions.set_private_key(_privateKey);
}
sslOptions.set_enable_server_cert_auth(
_verifyServerCert);
#ifdef ENABLE_MQTT5_SUPPORT
auto connOpts = mqtt::connect_options_builder::v5()
#else
auto connOpts = mqtt::connect_options_builder()
#endif
.clean_start(false)
.clean_session(true)
.connect_timeout(5s)
.user_name(_username)
.password(_password)
.ssl(sslOptions)
.finalize();
_client->set_connection_lost_handler(logConnectionLost);
_client->set_message_callback(dispatchMessage);
_client->start_consuming();
vblog(LOG_INFO, "connecting to MQTT server \"%s\" ...",
_name.c_str());
auto tok = _client->connect(connOpts);
@@ -220,6 +248,10 @@ void MqttConnection::Load(obs_data_t *data)
_uri = obs_data_get_string(data, "uri");
_username = obs_data_get_string(data, "username");
_password = obs_data_get_string(data, "password");
_trustStore = obs_data_get_string(data, "trustStore");
_keyStore = obs_data_get_string(data, "keyStore");
_privateKey = obs_data_get_string(data, "privateKey");
_verifyServerCert = obs_data_get_bool(data, "verifyServerCert");
_connectOnStart = obs_data_get_bool(data, "connectOnStart");
_reconnect = obs_data_get_bool(data, "reconnect");
_reconnectDelay = obs_data_get_int(data, "reconnectDelay");
@@ -251,6 +283,10 @@ void MqttConnection::Save(obs_data_t *data) const
obs_data_set_string(data, "uri", _uri.c_str());
obs_data_set_string(data, "username", _username.c_str());
obs_data_set_string(data, "password", _password.c_str());
obs_data_set_string(data, "trustStore", _trustStore.c_str());
obs_data_set_string(data, "keyStore", _keyStore.c_str());
obs_data_set_string(data, "privateKey", _privateKey.c_str());
obs_data_set_bool(data, "verifyServerCert", _verifyServerCert);
obs_data_set_bool(data, "connectOnStart", _connectOnStart);
obs_data_set_bool(data, "reconnect", _reconnect);
obs_data_set_int(data, "reconnectDelay", _reconnectDelay);
@@ -308,6 +344,10 @@ MqttConnectionSettingsDialog::MqttConnectionSettingsDialog(
_username(new QLineEdit()),
_password(new QLineEdit()),
_showPassword(new QPushButton()),
_trustStore(new FileSelection()),
_keyStore(new FileSelection()),
_privateKey(new FileSelection()),
_verifyServerCert(new QCheckBox()),
_topics(new MqttTopicListWidget(this)),
_connectOnStart(new QCheckBox()),
_reconnect(new QCheckBox()),
@@ -324,6 +364,10 @@ MqttConnectionSettingsDialog::MqttConnectionSettingsDialog(
_uri->setText(QString::fromStdString(connection._uri));
_username->setText(QString::fromStdString(connection._username));
_password->setText(QString::fromStdString(connection._password));
_trustStore->SetPath(QString::fromStdString(connection._trustStore));
_keyStore->SetPath(QString::fromStdString(connection._keyStore));
_privateKey->SetPath(QString::fromStdString(connection._privateKey));
_verifyServerCert->setChecked(connection._verifyServerCert);
_topics->SetValues(connection._topics, connection._qos);
_reconnectDelay->setMaximum(9999);
_reconnectDelay->setSuffix("s");
@@ -364,6 +408,41 @@ MqttConnectionSettingsDialog::MqttConnectionSettingsDialog(
passLayout->addWidget(_showPassword);
_layout->addLayout(passLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.trustStore")),
row, 0);
auto trustStoreLayout = new QHBoxLayout();
trustStoreLayout->addWidget(_trustStore);
trustStoreLayout->addWidget(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.mqttConnection.trustStore.help")));
_layout->addLayout(trustStoreLayout, row, 1);
++row;
_layout->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.keyStore")),
row, 0);
auto keyStoreLayout = new QHBoxLayout();
keyStoreLayout->addWidget(_keyStore);
keyStoreLayout->addWidget(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.mqttConnection.keyStore.help")));
_layout->addLayout(keyStoreLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.privateKey")),
row, 0);
auto privateKeyLayout = new QHBoxLayout();
privateKeyLayout->addWidget(_privateKey);
privateKeyLayout->addWidget(new HelpIcon(obs_module_text(
"AdvSceneSwitcher.mqttConnection.privateKey.help")));
_layout->addLayout(privateKeyLayout, row, 1);
++row;
_layout->addWidget(
new QLabel(obs_module_text(
"AdvSceneSwitcher.mqttConnection.verifyServerCert")),
row, 0);
_layout->addWidget(_verifyServerCert, row, 1);
++row;
_layout->addWidget(new QLabel(
obs_module_text("AdvSceneSwitcher.mqttConnection.topics")));
++row;
@@ -412,6 +491,10 @@ bool MqttConnectionSettingsDialog::AskForSettings(QWidget *parent,
connection._uri = dialog._uri->text().toStdString();
connection._username = dialog._username->text().toStdString();
connection._password = dialog._password->text().toStdString();
connection._trustStore = dialog._trustStore->GetPath().toStdString();
connection._keyStore = dialog._keyStore->GetPath().toStdString();
connection._privateKey = dialog._privateKey->GetPath().toStdString();
connection._verifyServerCert = dialog._verifyServerCert->isChecked();
connection._topics = dialog._topics->GetTopics();
connection._qos = dialog._topics->GetQoS();
connection._connectOnStart = dialog._connectOnStart->isChecked();
@@ -455,6 +538,10 @@ void MqttConnectionSettingsDialog::TestConnection()
connection->_uri = _uri->text().toStdString();
connection->_username = _username->text().toStdString();
connection->_password = _password->text().toStdString();
connection->_trustStore = _trustStore->GetPath().toStdString();
connection->_keyStore = _keyStore->GetPath().toStdString();
connection->_privateKey = _privateKey->GetPath().toStdString();
connection->_verifyServerCert = _verifyServerCert->isChecked();
connection->_topics = _topics->GetTopics();
connection->_qos = _topics->GetQoS();
connection->_connectOnStart = false;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "message-dispatcher.hpp"
#include "file-selection.hpp"
#include "item-selection-helpers.hpp"
#include "topic-selection.hpp"
@@ -27,6 +28,9 @@ public:
MqttConnection(const MqttConnection &other);
MqttConnection(const std::string &name, const std::string &uri,
const std::string &username, const std::string &password,
const std::string &trustStore,
const std::string &keyStore,
const std::string &privateKey, bool verifyServerCert,
bool connectOnStart, bool reconnect, int reconnectDelay);
static std::shared_ptr<Item> Create()
{
@@ -51,8 +55,12 @@ private:
std::string _uri = "mqtt://localhost:1883";
std::string _username = "user";
std::string _password = "password";
std::string _trustStore = "";
std::string _keyStore = "";
std::string _privateKey = "";
bool _verifyServerCert = false;
std::vector<std::string> _topics = {"/#"};
std::vector<std::string> _topics = {"/example"};
std::vector<int> _qos = {1};
std::thread _thread;
@@ -91,6 +99,10 @@ private:
QLineEdit *_username;
QLineEdit *_password;
QPushButton *_showPassword;
FileSelection *_trustStore;
FileSelection *_keyStore;
FileSelection *_privateKey;
QCheckBox *_verifyServerCert;
MqttTopicListWidget *_topics;
QCheckBox *_connectOnStart;
QCheckBox *_reconnect;

View File

@@ -15,13 +15,15 @@ if(NOT TARGET httplib)
EXCLUDE_FROM_ALL)
endif()
if(NOT OPENSSL_INCLUDE_DIR OR NOT OPENSSL_LIBRARIES)
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n"
"Twitch support will be disabled!\n\n")
return()
endif()
if(OS_MACOS)
set(OPENSSL_USE_STATIC_LIBS
ON
CACHE BOOL "Use static OpenSSL" FORCE)
endif()
find_package(OpenSSL)
if(NOT OPENSSL_FOUND)
message(WARNING "OpenSSL not found!\n" "Twitch support will be disabled!\n\n")
return()
endif()
find_package(ZLIB)

View File

@@ -32,28 +32,17 @@ static const int reconnectDelay = 15;
#undef DispatchMessage
EventSub::EventSub() : QObject(nullptr)
EventSub::EventSub()
: QObject(nullptr),
_client(std::make_unique<EventSubWSClient>())
{
_client.get_alog().clear_channels(
websocketpp::log::alevel::frame_header |
websocketpp::log::alevel::frame_payload |
websocketpp::log::alevel::control);
_client.init_asio();
#ifndef _WIN32
_client.set_reuse_addr(true);
#endif
SetupClient(*_client);
_client.set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client.set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client.set_close_handler(bind(&EventSub::OnClose, this, _1));
_client.set_fail_handler(bind(&EventSub::OnFail, this, _1));
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client->set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client->set_close_handler(bind(&EventSub::OnClose, this, _1));
_client->set_fail_handler(bind(&EventSub::OnFail, this, _1));
#ifndef USE_TWITCH_CLI_MOCK
_client.set_tls_init_handler([](websocketpp::connection_hdl) {
return websocketpp::lib::make_shared<asio::ssl::context>(
asio::ssl::context::sslv23_client);
});
#endif
_url = defaultURL.data();
RegisterInstance();
}
@@ -82,28 +71,34 @@ void EventSub::UnregisterInstance()
void EventSub::ConnectThread()
{
while (!_disconnect) {
std::unique_lock<std::mutex> lock(_waitMtx);
_client.reset();
_connected = true;
websocketpp::lib::error_code ec;
EventSubWSClient::connection_ptr con =
_client.get_connection(_url, ec);
if (ec) {
blog(LOG_INFO, "Twitch EventSub failed: %s",
ec.message().c_str());
} else {
_client.connect(con);
_connection = connection_hdl(con);
_client.run();
}
_client->reset();
_connected = true;
websocketpp::lib::error_code ec;
EventSubWSClient::connection_ptr con =
_client->get_connection(_url, ec);
if (ec) {
blog(LOG_INFO, "Twitch EventSub failed: %s",
ec.message().c_str());
} else {
_client->connect(con);
_connection = connection_hdl(con);
_client->run();
}
_connected = false;
}
void EventSub::WaitAndReconnect()
{
auto thread = std::thread([this]() {
std::unique_lock<std::mutex> lock(_waitMtx);
blog(LOG_INFO,
"Twitch EventSub trying to reconnect to in %d seconds.",
reconnectDelay);
_cv.wait_for(lock, std::chrono::seconds(reconnectDelay));
}
_connected = false;
Connect();
});
thread.detach();
}
void EventSub::Connect()
@@ -137,8 +132,8 @@ void EventSub::Disconnect()
std::lock_guard<std::mutex> lock(_connectMtx);
_disconnect = true;
websocketpp::lib::error_code ec;
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
_client->close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
{
std::unique_lock<std::mutex> waitLock(_waitMtx);
_cv.notify_all();
@@ -146,8 +141,8 @@ void EventSub::Disconnect()
while (_connected) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
_client.close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
_client->close(_connection, websocketpp::close::status::normal,
"Twitch EventSub stopping", ec);
}
if (_thread.joinable()) {
@@ -238,6 +233,24 @@ std::string EventSub::AddEventSubscription(std::shared_ptr<TwitchToken> token,
return subscription.id;
}
void EventSub::SetupClient(EventSubWSClient &client)
{
client.get_alog().clear_channels(
websocketpp::log::alevel::frame_header |
websocketpp::log::alevel::frame_payload |
websocketpp::log::alevel::control);
client.init_asio();
#ifndef _WIN32
client.set_reuse_addr(true);
#endif
#ifndef USE_TWITCH_CLI_MOCK
client.set_tls_init_handler([](websocketpp::connection_hdl) {
return websocketpp::lib::make_shared<asio::ssl::context>(
asio::ssl::context::sslv23_client);
});
#endif
}
void EventSub::OnOpen(connection_hdl)
{
vblog(LOG_INFO, "Twitch EventSub connection opened");
@@ -303,13 +316,14 @@ bool EventSub::IsValidID(const std::string &id)
return !_sessionID.empty() && id == _sessionID;
}
void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
std::optional<const EventSub::ParsedMessage>
EventSub::ParseWebSocketMessage(const EventSubWSClient::message_ptr &message)
{
if (!message) {
return;
return {};
}
if (message->get_opcode() != websocketpp::frame::opcode::text) {
return;
return {};
}
std::string payload = message->get_payload();
@@ -317,7 +331,7 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
if (!json) {
blog(LOG_ERROR, "invalid JSON payload received for '%s'",
payload.c_str());
return;
return {};
}
OBSDataAutoRelease metadata = obs_data_get_obj(json, "metadata");
@@ -325,31 +339,46 @@ void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
obs_data_get_string(metadata, "message_timestamp");
if (_validateTimestamps && !isValidTimestamp(timestamp)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid timestamp %s",
"discarding Twitch EventSub with invalid timestamp %s",
timestamp.c_str());
return;
return {};
}
std::string id = obs_data_get_string(metadata, "message_id");
if (!IsValidMessageID(id)) {
blog(LOG_WARNING,
"Discarding Twitch EventSub with invalid message_id");
"discarding Twitch EventSub with invalid message_id");
return {};
}
ParsedMessage parsedMessage{obs_data_get_string(metadata,
"message_type"),
obs_data_get_obj(json, "payload")};
return parsedMessage;
}
void EventSub::OnMessage(connection_hdl, EventSubWSClient::message_ptr message)
{
const auto msg = ParseWebSocketMessage(message);
if (!msg) {
return;
}
std::string messageType = obs_data_get_string(metadata, "message_type");
OBSDataAutoRelease payloadJson = obs_data_get_obj(json, "payload");
if (messageType == "session_welcome") {
HandleWelcome(payloadJson);
} else if (messageType == "session_keepalive") {
const auto &type = msg->type;
const auto &data = msg->payload;
if (type == "session_welcome") {
HandleWelcome(data);
} else if (type == "session_keepalive") {
HandleKeepAlive();
} else if (messageType == "notification") {
HandleNotification(payloadJson);
} else if (messageType == "session_reconnect") {
HandleReconnect(payloadJson);
} else if (messageType == "revocation") {
HandleRevocation(payloadJson);
} else if (type == "notification") {
HandleNotification(data);
} else if (type == "session_reconnect") {
HandleServerMigration(data);
} else if (type == "revocation") {
HandleRevocation(data);
} else {
vblog(LOG_INFO, "ignoring message of unknown type '%s'",
messageType.c_str());
vblog(LOG_INFO,
"Twitch EventSub ignoring message of unknown type '%s'",
type.c_str());
}
}
@@ -377,9 +406,96 @@ void EventSub::HandleNotification(obs_data_t *data)
_dispatcher.DispatchMessage(event);
}
void EventSub::HandleReconnect(obs_data_t *data)
void EventSub::OnServerMigrationWelcome(
connection_hdl newHdl, std::unique_ptr<EventSubWSClient> &newClient)
{
std::lock_guard<std::mutex> lock(_connectMtx);
// Disable reconnect handling for old connection which will be closed
_client->set_close_handler([](connection_hdl) {});
_client->set_fail_handler([](connection_hdl) {});
auto connection = _client->get_con_from_hdl(_connection);
connection->set_close_handler([](connection_hdl) {
vblog(LOG_INFO, "previous Twitch EventSub connection closed");
});
connection->set_fail_handler([](connection_hdl) {});
websocketpp::lib::error_code ec;
_client->close(_connection, websocketpp::close::status::normal,
"Switching to new connection", ec);
_client.swap(newClient);
_sessionID = _migrationSessionID;
_connection = newHdl;
_client->set_open_handler(bind(&EventSub::OnOpen, this, _1));
_client->set_message_handler(bind(&EventSub::OnMessage, this, _1, _2));
_client->set_close_handler(bind(&EventSub::OnClose, this, _1));
_client->set_fail_handler(bind(&EventSub::OnFail, this, _1));
auto newConnection = _client->get_con_from_hdl(_connection);
newConnection->set_open_handler(bind(&EventSub::OnOpen, this, _1));
newConnection->set_message_handler(
bind(&EventSub::OnMessage, this, _1, _2));
newConnection->set_close_handler(bind(&EventSub::OnClose, this, _1));
newConnection->set_fail_handler(bind(&EventSub::OnFail, this, _1));
_connected = true;
_migrating = false;
}
void EventSub::StartServerMigrationClient(const std::string &url)
{
auto client = std::make_unique<EventSubWSClient>();
SetupClient(*client);
client->set_open_handler([this](connection_hdl hdl) {
vblog(LOG_INFO, "Twitch EventSub migration client opened");
});
client->set_message_handler([this,
&client](connection_hdl hdl,
EventSubWSClient::message_ptr
message) {
const auto msg = ParseWebSocketMessage(message);
if (!msg) {
return;
}
const auto &type = msg->type;
const auto &data = msg->payload;
if (type == "session_welcome") {
vblog(LOG_INFO,
"Twitch EventSub migration successful - switching to new connection");
OBSDataAutoRelease session =
obs_data_get_obj(data, "session");
_migrationSessionID =
obs_data_get_string(session, "id");
OnServerMigrationWelcome(hdl, client);
} else {
OnMessage(hdl, message);
}
});
websocketpp::lib::error_code ec;
auto con = client->get_connection(url, ec);
if (ec) {
blog(LOG_ERROR,
"Twitch EventSub migration connection failed: %s",
ec.message().c_str());
_migrating = false;
return;
}
_migrationConnection = con;
client->connect(con);
client->run();
}
void EventSub::HandleServerMigration(obs_data_t *data)
{
blog(LOG_INFO, "Twitch EventSub session_reconnect received");
OBSDataAutoRelease session = obs_data_get_obj(data, "session");
auto id = obs_data_get_string(session, "id");
if (!IsValidID(id)) {
@@ -388,8 +504,25 @@ void EventSub::HandleReconnect(obs_data_t *data)
return;
}
// TODO:
// Implement proper reconnect handing to avoid dropped events
const std::string newURL =
obs_data_get_string(session, "reconnect_url");
if (newURL.empty()) {
blog(LOG_WARNING, "missing reconnect_url in session_reconnect");
return;
}
if (_migrating.exchange(true)) {
vblog(LOG_INFO,
"ignoring Twitch EventSub session_reconnect - already in progress");
return;
}
vblog(LOG_INFO, "Twitch EventSub reconnect: connecting to %s",
newURL.c_str());
std::thread([this, newURL]() {
StartServerMigrationClient(newURL);
}).detach();
}
void EventSub::HandleRevocation(obs_data_t *data)
@@ -425,23 +558,41 @@ void EventSub::HandleRevocation(obs_data_t *data)
void EventSub::OnClose(connection_hdl hdl)
{
EventSubWSClient::connection_ptr con = _client.get_con_from_hdl(hdl);
EventSubWSClient::connection_ptr con = _client->get_con_from_hdl(hdl);
const auto msg = con->get_ec().message();
const auto reason = con->get_remote_close_reason();
const auto code = con->get_remote_close_code();
blog(LOG_INFO, "Twitch EventSub connection closed: %s / %s (%d)",
msg.c_str(), reason.c_str(), code);
ClearActiveSubscriptions();
if (_migrating) {
ClearActiveSubscriptions();
}
_connected = false;
if (_disconnect) {
return;
}
WaitAndReconnect();
}
void EventSub::OnFail(connection_hdl hdl)
{
EventSubWSClient::connection_ptr con = _client.get_con_from_hdl(hdl);
EventSubWSClient::connection_ptr con = _client->get_con_from_hdl(hdl);
auto msg = con->get_ec().message();
blog(LOG_INFO, "Twitch EventSub connection failed: %s", msg.c_str());
ClearActiveSubscriptions();
if (!_migrating) {
ClearActiveSubscriptions();
}
_connected = false;
if (_disconnect) {
return;
}
WaitAndReconnect();
}
bool Subscription::operator<(const Subscription &other) const

View File

@@ -19,10 +19,10 @@
namespace advss {
#ifdef USE_TWITCH_CLI_MOCK
typedef websocketpp::client<websocketpp::config::asio_client> EventSubWSClient;
using EventSubWSClient = websocketpp::client<websocketpp::config::asio_client>;
#else
typedef websocketpp::client<websocketpp::config::asio_tls_client>
EventSubWSClient;
using EventSubWSClient =
websocketpp::client<websocketpp::config::asio_tls_client>;
#endif
struct Event;
@@ -61,12 +61,16 @@ public:
void EnableTimestampValidation(bool enable);
private:
static void SetupClient(EventSubWSClient &);
void OnOpen(connection_hdl hdl);
void OnMessage(connection_hdl hdl,
EventSubWSClient::message_ptr message);
void OnClose(connection_hdl hdl);
void OnFail(connection_hdl hdl);
void ConnectThread();
void WaitAndReconnect();
bool IsValidMessageID(const std::string &);
bool IsValidID(const std::string &);
@@ -74,20 +78,39 @@ private:
void HandleWelcome(obs_data_t *);
void HandleKeepAlive() const;
void HandleNotification(obs_data_t *);
void HandleReconnect(obs_data_t *);
void HandleServerMigration(obs_data_t *);
void HandleRevocation(obs_data_t *);
void RegisterInstance();
void UnregisterInstance();
EventSubWSClient _client;
void StartServerMigrationClient(const std::string &url);
void OnServerMigrationWelcome(connection_hdl,
std::unique_ptr<EventSubWSClient> &);
struct ParsedMessage {
std::string type;
OBSDataAutoRelease payload;
};
std::optional<const ParsedMessage>
ParseWebSocketMessage(const EventSubWSClient::message_ptr &);
std::unique_ptr<EventSubWSClient> _client;
connection_hdl _connection;
std::unique_ptr<EventSubWSClient> _migrationClient;
connection_hdl _migrationConnection;
std::atomic_bool _migrating{false};
std::string _migrationSessionID;
std::thread _thread;
std::mutex _waitMtx;
std::mutex _connectMtx;
std::condition_variable _cv;
std::atomic_bool _connected{false};
std::atomic_bool _disconnect{false};
std::string _url;
std::string _sessionID;
bool _validateTimestamps = true;