Add Stream Deck condition type

The Stream Deck plugin to enable communication with the Advanced Scene
Switcher can be found here:
https://github.com/WarmUpTill/advanced-scene-switcher-streamdeck-plugin
This commit is contained in:
WarmUpTill 2024-09-04 19:25:43 +02:00 committed by WarmUpTill
parent b60235d964
commit faa65facfe
5 changed files with 548 additions and 0 deletions

View File

@ -745,6 +745,15 @@ AdvSceneSwitcher.condition.usb.vendorName="Vendor Name:"
AdvSceneSwitcher.condition.usb.productName="Product Name:"
AdvSceneSwitcher.condition.usb.serialNumber="Serial Number:"
AdvSceneSwitcher.condition.noDevicesFoundWarning="No USB devices detected!\nThe plugin might not have the required permissions to check for USB devices."
AdvSceneSwitcher.condition.streamDeck="Stream Deck"
AdvSceneSwitcher.condition.streamDeck.checkKeyState="Check key state"
AdvSceneSwitcher.condition.streamDeck.keyState.pressed="Pressed"
AdvSceneSwitcher.condition.streamDeck.keyState.released="Released"
AdvSceneSwitcher.condition.streamDeck.checkKeyPosition="Check key position"
AdvSceneSwitcher.condition.streamDeck.checkData="Check data field"
AdvSceneSwitcher.condition.streamDeck.startListen="Start listening"
AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
AdvSceneSwitcher.condition.streamDeck.pluginDownload="<html><head/><body><p>The Stream Deck plugin can be found <a href=\"https://github.com/WarmUpTill/advanced-scene-switcher-streamdeck-plugin/releases\"><span style=\" text-decoration: underline; color:#268bd2;\">here on GitHub</span></a>.</p></body></html>"
# Macro Actions
AdvSceneSwitcher.action.unknown="Unknown action"
@ -1881,6 +1890,13 @@ AdvSceneSwitcher.tempVar.media.vlc.disc_number="Disc number (VLC)"
AdvSceneSwitcher.tempVar.media.vlc.disc_total="Disc total (VLC)"
AdvSceneSwitcher.tempVar.media.vlc.metadata.description="This property is exposed by the VLC Source type.\nNot all of these might be applicable to every file.\nIn case the metadata is not available an empty string will be returned."
AdvSceneSwitcher.tempVar.streamDeck.keyPressed="Key pressed"
AdvSceneSwitcher.tempVar.streamDeck.keyPressed.description="Will return \"0\" when the key is pressed, and \"1\" otherwise."
AdvSceneSwitcher.tempVar.streamDeck.row="Key row"
AdvSceneSwitcher.tempVar.streamDeck.column="Key column"
AdvSceneSwitcher.tempVar.streamDeck.data="Data"
AdvSceneSwitcher.tempVar.streamDeck.data.description="The value of the \"Data\" field configured in the action on the Stream Deck side."
AdvSceneSwitcher.selectScene="--select scene--"
AdvSceneSwitcher.selectPreviousScene="Previous Scene"
AdvSceneSwitcher.selectCurrentScene="Current Scene"

View File

@ -30,6 +30,7 @@ install_advss_plugin_dependency(...)
add_plugin(midi)
add_plugin(openvr)
add_plugin(stream-deck)
add_plugin(twitch)
add_plugin(usb)
add_plugin(video)

View File

@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.14)
project(advanced-scene-switcher-stream-deck)
add_library(${PROJECT_NAME} MODULE)
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
target_sources(${PROJECT_NAME} PRIVATE macro-condition-stream-deck.cpp
macro-condition-stream-deck.hpp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
install_advss_plugin(${PROJECT_NAME})

View File

@ -0,0 +1,403 @@
#include "macro-condition-stream-deck.hpp"
#include "layout-helpers.hpp"
#include "websocket-api.hpp"
#include <obs.hpp>
namespace advss {
const std::string MacroConditionStreamdeck::id = "streamdeck";
bool MacroConditionStreamdeck::_registered = MacroConditionFactory::Register(
MacroConditionStreamdeck::id,
{MacroConditionStreamdeck::Create, MacroConditionStreamdeckEdit::Create,
"AdvSceneSwitcher.condition.streamDeck"});
constexpr char streamDeckMessage[] = "StreamDeckKeyEvent";
static StreamDeckMessageDispatcher messageDispatcher;
static bool setup();
static bool setupDone = setup();
static void receiveStreamDeckMessage(obs_data_t *request_data, obs_data_t *);
bool setup()
{
RegisterWebsocketRequest(streamDeckMessage, receiveStreamDeckMessage);
return true;
}
static void printParseError(obs_data_t *data)
{
blog(LOG_INFO, "received unexpected stream deck message: %s",
obs_data_get_json(data) ? obs_data_get_json(data) : "''");
}
void receiveStreamDeckMessage(obs_data_t *request_data, obs_data_t *)
{
StreamDeckMessage message;
OBSDataAutoRelease data = obs_data_get_obj(request_data, "data");
if (!data) {
printParseError(request_data);
return;
}
if (!obs_data_has_user_value(data, "isKeyDownEvent")) {
printParseError(request_data);
return;
}
message.keyDown = obs_data_get_bool(data, "isKeyDownEvent");
OBSDataAutoRelease coordinates = obs_data_get_obj(data, "coordinates");
if (!coordinates) {
printParseError(request_data);
return;
}
message.row = obs_data_get_int(coordinates, "row");
message.column = obs_data_get_int(coordinates, "column");
OBSDataAutoRelease settings = obs_data_get_obj(data, "settings");
if (!settings) {
printParseError(request_data);
return;
}
message.data = obs_data_get_string(settings, "data");
messageDispatcher.DispatchMessage(message);
}
StreamDeckMessageBuffer RegisterForStreamDeckMessages()
{
return messageDispatcher.RegisterClient();
}
MacroConditionStreamdeck::MacroConditionStreamdeck(Macro *m)
: MacroCondition(m, true)
{
_messageBuffer = RegisterForStreamDeckMessages();
}
bool MacroConditionStreamdeck::MessageMatches(const StreamDeckMessage &message)
{
const bool keyStateMatches = !_pattern.checkKeyState ||
((message.keyDown && _pattern.keyDown) ||
(!message.keyDown && !message.keyDown));
const bool positionMatches = !_pattern.checkPosition ||
(message.row == _pattern.row &&
message.column == _pattern.column);
const bool dataMatches =
!_pattern.checkData ||
(_pattern.regex.Enabled()
? _pattern.regex.Matches(message.data, _pattern.data)
: message.data == std::string(_pattern.data));
return keyStateMatches && positionMatches && dataMatches;
}
bool MacroConditionStreamdeck::CheckCondition()
{
while (!_messageBuffer->Empty()) {
auto message = _messageBuffer->ConsumeMessage();
if (!message) {
continue;
}
if (!message->keyDown) {
_lastMatchingKeyIsStillPressed = false;
}
if (MessageMatches(*message)) {
_lastMatchingKeyIsStillPressed = message->keyDown;
SetTempVarValues(*message);
return true;
}
}
if (_lastMatchingKeyIsStillPressed) {
return true;
}
return false;
}
bool MacroConditionStreamdeck::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
_pattern.Save(obj);
return true;
}
bool MacroConditionStreamdeck::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_pattern.Load(obj);
return true;
}
void MacroConditionStreamdeck::StreamDeckMessagePattern::Save(
obs_data_t *obj) const
{
OBSDataAutoRelease dataObj = obs_data_create();
obs_data_set_bool(dataObj, "checkKeyState", checkKeyState);
obs_data_set_bool(dataObj, "keyDown", keyDown);
obs_data_set_bool(dataObj, "checkPosition", checkPosition);
row.Save(dataObj, "row");
column.Save(dataObj, "column");
obs_data_set_bool(dataObj, "checkData", checkData);
data.Save(dataObj, "data");
regex.Save(dataObj);
obs_data_set_obj(obj, "pattern", dataObj);
}
void MacroConditionStreamdeck::StreamDeckMessagePattern::Load(obs_data_t *obj)
{
OBSDataAutoRelease dataObj = obs_data_get_obj(obj, "pattern");
checkKeyState = obs_data_get_bool(dataObj, "checkKeyState");
keyDown = obs_data_get_bool(dataObj, "keyDown");
checkPosition = obs_data_get_bool(dataObj, "checkPosition");
row.Load(dataObj, "row");
column.Load(dataObj, "column");
checkData = obs_data_get_bool(dataObj, "checkData");
data.Load(dataObj, "data");
regex.Load(dataObj);
obs_data_set_obj(obj, "pattern", dataObj);
}
void MacroConditionStreamdeck::SetTempVarValues(const StreamDeckMessage &message)
{
SetTempVarValue("keyPressed", std::to_string(message.keyDown));
SetTempVarValue("row", std::to_string(message.row));
SetTempVarValue("column", std::to_string(message.column));
SetTempVarValue("data", message.data);
}
void MacroConditionStreamdeck::SetupTempVars()
{
AddTempvar(
"keyPressed",
obs_module_text(
"AdvSceneSwitcher.tempVar.streamDeck.keyPressed"),
obs_module_text(
"AdvSceneSwitcher.tempVar.streamDeck.keyPressed.description"));
AddTempvar("row",
obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.row"));
AddTempvar(
"column",
obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.column"));
AddTempvar(
"data",
obs_module_text("AdvSceneSwitcher.tempVar.streamDeck.data"),
obs_module_text(
"AdvSceneSwitcher.tempVar.streamDeck.data.description"));
}
MacroConditionStreamdeckEdit::MacroConditionStreamdeckEdit(
QWidget *parent, std::shared_ptr<MacroConditionStreamdeck> entryData)
: QWidget(parent),
_checkKeyState(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.checkKeyState"))),
_keyState(new QComboBox(this)),
_checkPosition(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.checkKeyPosition"))),
_row(new VariableSpinBox(this)),
_column(new VariableSpinBox(this)),
_checkData(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.checkData"))),
_data(new VariableTextEdit(this)),
_regex(new RegexConfigWidget(this)),
_listen(new QPushButton(
obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.startListen"),
this))
{
_keyState->addItem(
obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.keyState.pressed"),
true);
_keyState->addItem(
obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.keyState.released"),
false);
connect(_checkKeyState, &QCheckBox::stateChanged, this,
&MacroConditionStreamdeckEdit::CheckKeyStateChanged);
connect(_keyState, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MacroConditionStreamdeckEdit::KeyStateChanged);
connect(_checkPosition, &QCheckBox::stateChanged, this,
&MacroConditionStreamdeckEdit::CheckPositionChanged);
connect(_row,
static_cast<void (VariableSpinBox::*)(const IntVariable &)>(
&VariableSpinBox::NumberVariableChanged),
this, &MacroConditionStreamdeckEdit::RowChanged);
connect(_column,
static_cast<void (VariableSpinBox::*)(const IntVariable &)>(
&VariableSpinBox::NumberVariableChanged),
this, &MacroConditionStreamdeckEdit::ColumnChanged);
connect(_checkData, &QCheckBox::stateChanged, this,
&MacroConditionStreamdeckEdit::CheckDataChanged);
connect(_regex, &RegexConfigWidget::RegexConfigChanged, this,
&MacroConditionStreamdeckEdit::RegexChanged);
connect(_data, &VariableTextEdit::textChanged, this,
&MacroConditionStreamdeckEdit::DataChanged);
connect(_listen, &QPushButton::clicked, this,
&MacroConditionStreamdeckEdit::ListenClicked);
auto keyStateLayout = new QHBoxLayout();
keyStateLayout->addWidget(_checkKeyState);
keyStateLayout->addWidget(_keyState);
keyStateLayout->addStretch();
auto positionLayout = new QHBoxLayout();
positionLayout->addWidget(_row);
positionLayout->addWidget(_column);
auto dataLayout = new QHBoxLayout();
dataLayout->addWidget(_checkData);
dataLayout->addWidget(_regex);
dataLayout->addStretch();
auto layout = new QVBoxLayout();
layout->addLayout(keyStateLayout);
layout->addWidget(_checkPosition);
layout->addLayout(positionLayout);
layout->addLayout(dataLayout);
layout->addWidget(_data);
layout->addWidget(_listen);
layout->addWidget(new QLabel(obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.pluginDownload")));
setLayout(layout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionStreamdeckEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_checkKeyState->setChecked(_entryData->_pattern.checkKeyState);
_keyState->setCurrentIndex(
_keyState->findData(_entryData->_pattern.keyDown));
_checkPosition->setChecked(_entryData->_pattern.checkPosition);
_row->SetValue(_entryData->_pattern.row);
_column->SetValue(_entryData->_pattern.column);
_checkData->setChecked(_entryData->_pattern.checkData);
_data->setPlainText(_entryData->_pattern.data);
_regex->SetRegexConfig(_entryData->_pattern.regex);
SetWidgetVisibility();
}
void MacroConditionStreamdeckEdit::CheckKeyStateChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.checkKeyState = state;
SetWidgetVisibility();
}
void MacroConditionStreamdeckEdit::KeyStateChanged(int index)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.keyDown = _keyState->itemData(index).toBool();
}
void MacroConditionStreamdeckEdit::CheckPositionChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.checkPosition = state;
SetWidgetVisibility();
}
void MacroConditionStreamdeckEdit::RowChanged(const IntVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.row = value;
}
void MacroConditionStreamdeckEdit::ColumnChanged(const IntVariable &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.column = value;
}
void MacroConditionStreamdeckEdit::CheckDataChanged(int state)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.checkData = state;
SetWidgetVisibility();
}
void MacroConditionStreamdeckEdit::RegexChanged(const RegexConfig &regex)
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.regex = regex;
}
void MacroConditionStreamdeckEdit::DataChanged()
{
GUARD_LOADING_AND_LOCK();
_entryData->_pattern.data = _data->toPlainText().toStdString();
}
void MacroConditionStreamdeckEdit::ListenClicked()
{
_isListening = !_isListening;
_keyState->setDisabled(_isListening);
_row->setDisabled(_isListening);
_column->setDisabled(_isListening);
_data->setDisabled(_isListening);
_regex->setDisabled(_isListening);
if (_isListening) {
_messageBuffer = RegisterForStreamDeckMessages();
_listen->setText(obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.stopListen"));
} else {
_updateListenSettings.stop();
_messageBuffer.reset();
_listen->setText(obs_module_text(
"AdvSceneSwitcher.condition.streamDeck.startListen"));
return;
}
connect(&_updateListenSettings, &QTimer::timeout, this, [this]() {
if (_messageBuffer->Empty()) {
return;
}
StreamDeckMessage lastMessageInBuffer;
while (!_messageBuffer->Empty()) {
auto message = _messageBuffer->ConsumeMessage();
if (!message) {
continue;
}
lastMessageInBuffer = *message;
}
_keyState->setCurrentIndex(
_keyState->findData(lastMessageInBuffer.keyDown));
_row->SetFixedValue(lastMessageInBuffer.row);
_column->SetFixedValue(lastMessageInBuffer.column);
_data->setPlainText(lastMessageInBuffer.data);
});
_updateListenSettings.start(300);
}
void MacroConditionStreamdeckEdit::SetWidgetVisibility()
{
_keyState->setVisible(_checkKeyState->isChecked());
_row->setVisible(_checkPosition->isChecked());
_column->setVisible(_checkPosition->isChecked());
_data->setVisible(_checkData->isChecked());
_regex->setVisible(_checkData->isChecked());
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@ -0,0 +1,116 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "message-buffer.hpp"
#include "message-dispatcher.hpp"
#include "regex-config.hpp"
#include "variable-spinbox.hpp"
#include "variable-text-edit.hpp"
#include <QPushButton>
namespace advss {
struct StreamDeckMessage {
bool keyDown = true;
int row = 0;
int column = 0;
std::string data = "";
};
using StreamDeckMessageBuffer =
std::shared_ptr<MessageBuffer<StreamDeckMessage>>;
using StreamDeckMessageDispatcher = MessageDispatcher<StreamDeckMessage>;
[[nodiscard]] StreamDeckMessageBuffer RegisterForStreamDeckMessages();
class MacroConditionStreamdeck : public MacroCondition {
public:
MacroConditionStreamdeck(Macro *m);
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; };
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionStreamdeck>(m);
}
struct StreamDeckMessagePattern {
void Save(obs_data_t *) const;
void Load(obs_data_t *);
bool checkKeyState = true;
bool keyDown = true;
bool checkPosition = true;
IntVariable row;
IntVariable column;
bool checkData = true;
StringVariable data;
RegexConfig regex;
};
StreamDeckMessagePattern _pattern;
private:
void SetTempVarValues(const StreamDeckMessage &);
void SetupTempVars();
bool MessageMatches(const StreamDeckMessage &);
// Keep track of the key state so that the condition can still evaluate
// to true while the key is held down.
bool _lastMatchingKeyIsStillPressed = false;
StreamDeckMessageBuffer _messageBuffer;
static bool _registered;
static const std::string id;
};
class MacroConditionStreamdeckEdit : public QWidget {
Q_OBJECT
public:
MacroConditionStreamdeckEdit(
QWidget *parent,
std::shared_ptr<MacroConditionStreamdeck> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionStreamdeckEdit(
parent,
std::dynamic_pointer_cast<MacroConditionStreamdeck>(
cond));
}
private slots:
void CheckKeyStateChanged(int);
void KeyStateChanged(int);
void CheckPositionChanged(int);
void RowChanged(const IntVariable &);
void ColumnChanged(const IntVariable &);
void CheckDataChanged(int);
void RegexChanged(const RegexConfig &);
void DataChanged();
void ListenClicked();
private:
void SetWidgetVisibility();
QCheckBox *_checkKeyState;
QComboBox *_keyState;
QCheckBox *_checkPosition;
VariableSpinBox *_row;
VariableSpinBox *_column;
QCheckBox *_checkData;
VariableTextEdit *_data;
RegexConfigWidget *_regex;
QPushButton *_listen;
bool _isListening = false;
StreamDeckMessageBuffer _messageBuffer;
QTimer _updateListenSettings;
std::shared_ptr<MacroConditionStreamdeck> _entryData;
bool _loading = true;
};
} // namespace advss