mirror of
https://github.com/Palakis/obs-websocket.git
synced 2024-08-30 18:12:16 +00:00
Compare commits
205 Commits
5.0.0-alph
...
5.0.0-alph
Author | SHA1 | Date | |
---|---|---|---|
e1dc0c441c | |||
389cbd854c | |||
707ac3f7e3 | |||
732d5af50c | |||
0939273abf | |||
f8263caa03 | |||
5c16a11ba8 | |||
af217c05f1 | |||
e640ae1218 | |||
bb2c125601 | |||
e1cb858d2d | |||
1339202c02 | |||
4835abd9c2 | |||
964e91bbd7 | |||
9385a2449e | |||
ec79124b5f | |||
4d65c2adee | |||
851a6f8c5a | |||
e2f60b002e | |||
86506778ad | |||
0992f74fad | |||
430e61bef7 | |||
da83de7503 | |||
07249da400 | |||
899888eb6c | |||
14238027cc | |||
5cbf439f55 | |||
e05be47847 | |||
2302fdd25f | |||
1c6ec1dda2 | |||
ad347c4823 | |||
714b4db840 | |||
40ff3f6960 | |||
0f303504e1 | |||
91e3f5ee18 | |||
66f416236c | |||
b331f76d40 | |||
a898bacd79 | |||
527a008002 | |||
a0ad43c7d7 | |||
f566ccd76b | |||
2e8622e8d7 | |||
4a193d44a1 | |||
d29b87ffc1 | |||
1e6a60f545 | |||
5cd1af426a | |||
f66080a031 | |||
82ad3313e8 | |||
71bf9e9021 | |||
0c7fda28a0 | |||
e2804e2d85 | |||
b37305354d | |||
6f6fbf84d1 | |||
9749502e88 | |||
c3e6bc323a | |||
873ad1b167 | |||
a40e79e987 | |||
84e649a6f7 | |||
c4ab69481b | |||
edf4e942fa | |||
b3ef9a861e | |||
54fd7af5ef | |||
2a33179588 | |||
906c6c4871 | |||
f7ab102c21 | |||
fdc8b5546f | |||
6f72da83ff | |||
0007987219 | |||
77f8c5be4d | |||
64c3b62360 | |||
c6afc8f981 | |||
2c2b584ecc | |||
edda844a34 | |||
d642654f49 | |||
69522024d2 | |||
24cd95bca7 | |||
0f6ee87f99 | |||
9c8f056d3e | |||
41a145c57c | |||
fab56d71ea | |||
b490e4409b | |||
8fbcbad9ec | |||
82d5468b73 | |||
2e7262fe11 | |||
5f261de143 | |||
539ee3f28f | |||
29a5cfe2fe | |||
612a50efbb | |||
20426924cd | |||
889062e44b | |||
eb8d69dca5 | |||
0ed3c9b367 | |||
e47de63786 | |||
84f90a9650 | |||
3ea8506619 | |||
d4353d4bf1 | |||
d7887b4c32 | |||
de1f843ce6 | |||
5ac813b897 | |||
43e2860709 | |||
c9c5da6837 | |||
b66d2284b3 | |||
fcbe11616d | |||
6cec018c8d | |||
6d684eb07f | |||
5704ea2970 | |||
d48ddef031 | |||
1ac6ac6c87 | |||
bc7b8d330a | |||
c95511eb5f | |||
2a4e86d8da | |||
4d8013b07e | |||
b1de9c8e79 | |||
8e9b3ef7bb | |||
2e079ad681 | |||
29a72f9af8 | |||
0a294a558e | |||
959347337f | |||
c8c6417d63 | |||
2005ced682 | |||
4ca259b790 | |||
c720df5938 | |||
32a9e12f62 | |||
60f12a16f3 | |||
99cbaaf34c | |||
bc1d5386a5 | |||
bd6c663775 | |||
18ed1589ae | |||
07a20b6458 | |||
6cb8eef96d | |||
5ab091a40b | |||
c60d09246c | |||
061c228ad5 | |||
4076c0baa9 | |||
01013c1b27 | |||
1dbb7a9686 | |||
061fb6f012 | |||
32e4ad74b7 | |||
0f17d3d6f8 | |||
08eb2defbc | |||
7403264d42 | |||
32be21886c | |||
d5a702b0e8 | |||
0671ded7d1 | |||
9197a48088 | |||
5faadc12de | |||
45f7661a5d | |||
aa46bb74a5 | |||
af52a26e68 | |||
69494d9c85 | |||
152faa5fe2 | |||
622e1c9aeb | |||
ee3216968d | |||
4e956d1ef4 | |||
42c78f1831 | |||
1b25e98dc2 | |||
3687086ce0 | |||
f0b207d021 | |||
16ea2c82e1 | |||
981538aa2a | |||
9f71e4af2c | |||
0fc813f48b | |||
df7af451a7 | |||
a8d27ede9e | |||
45854e2949 | |||
181003af89 | |||
2218b7956c | |||
0e8650dbd2 | |||
da4297ee88 | |||
5350c115bf | |||
d09571f0e3 | |||
d04d42240a | |||
9ca83e7570 | |||
aa0ba78e46 | |||
b91ad0f790 | |||
4e0b8c5f11 | |||
ea948766a5 | |||
c73a153c9a | |||
61973e75dc | |||
59e1083557 | |||
0c2e40263a | |||
78d02696d0 | |||
7a888c2f92 | |||
342164dfb5 | |||
a4e62acf25 | |||
d811c95e10 | |||
eebcc25115 | |||
c9fa09edc4 | |||
69ccc99921 | |||
a5a19b9952 | |||
66ff329da4 | |||
af0ac63f2c | |||
802cb38ff6 | |||
117dcb9567 | |||
d20c0d0da7 | |||
a9d86ce35c | |||
b9b8e38998 | |||
e89c0c2b05 | |||
7e1e1bc33c | |||
537595658d | |||
c43d829845 | |||
3d2fb65357 | |||
96dcc49adb | |||
25b3bd44ba | |||
bcdb8ee352 |
@ -1,10 +1,17 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{c,cpp,h,hpp}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[CMakeLists.txt]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@ -1,3 +1 @@
|
||||
open_collective: obs-websocket
|
||||
github: Palakis
|
||||
custom: https://www.paypal.me/stephanelepin
|
||||
open_collective: obs-websocket-dev
|
||||
|
20
.github/ISSUE_TEMPLATE.md
vendored
20
.github/ISSUE_TEMPLATE.md
vendored
@ -1,20 +0,0 @@
|
||||
##### Issue type
|
||||
<!--- Uncomment one of the two options below. -->
|
||||
|
||||
<!--- - Bug report -->
|
||||
<!--- - Feature request -->
|
||||
|
||||
##### Description
|
||||
<!--- Describe the bug encountered or feature requested. -->
|
||||
|
||||
##### Steps to reproduce and other useful info
|
||||
<!--- If it's a bug, please describe the steps to reproduce it and PLEASE include an OBS log file. Otherwise, remove this section. -->
|
||||
|
||||
##### Technical information
|
||||
- **Operating System:**
|
||||
- **OBS Studio version:**
|
||||
- **obs-websocket version:**
|
||||
|
||||
##### Development Environment
|
||||
<!--- If you're trying to compile obs-websocket, please describe your compiler type and version (e.g: GCC 4.7, VC2013, ...), and the CMake settings used. -->
|
||||
<!--- Remove this section if it does not apply. -->
|
129
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
129
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or crash
|
||||
title: "Bug: "
|
||||
labels: ["Issue: Bug - Unconfirmed"]
|
||||
body:
|
||||
- type: markdown
|
||||
id: md_welcome
|
||||
attributes:
|
||||
value: This form is for reporting bugs for obs-websocket!
|
||||
- type: dropdown
|
||||
id: os_info
|
||||
attributes:
|
||||
label: Operating System Info
|
||||
description: What Operating System are you running?
|
||||
options:
|
||||
- Windows 11
|
||||
- Windows 10
|
||||
- Windows 8.1
|
||||
- macOS 12.0
|
||||
- macOS 11.6
|
||||
- macOS 11.5
|
||||
- macOS 11.4
|
||||
- macOS 11.3
|
||||
- macOS 11.2
|
||||
- macOS 11.1
|
||||
- macOS 11.0
|
||||
- macOS 10.15
|
||||
- macOS 10.14
|
||||
- macOS 10.13
|
||||
- Ubuntu 21.04
|
||||
- Ubuntu 20.10
|
||||
- Ubuntu 20.04
|
||||
- Ubuntu 18.04
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os_info_other
|
||||
attributes:
|
||||
label: Other OS
|
||||
description: "If \"Other\" was selected above, what OS are you using?"
|
||||
placeholder: "e.g., Arch Linux, FreeBSD"
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: obs_version
|
||||
attributes:
|
||||
label: OBS Studio Version
|
||||
description: What version of OBS Studio are you using?
|
||||
options:
|
||||
- 27.1.3
|
||||
- 27.1.1
|
||||
- 27.1.0
|
||||
- 27.0.1
|
||||
- Git
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: obs_version_other
|
||||
attributes:
|
||||
label: OBS Studio Version (Other)
|
||||
description: "If \"Other\" was selected above, what version of OBS Studio are you using?"
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: obs_websocket_version
|
||||
attributes:
|
||||
label: obs-websocket Version
|
||||
description: What version of obs-websocket are you using?
|
||||
options:
|
||||
- 5.0.0-alpha3
|
||||
- 5.0.0-alpha2
|
||||
- 4.9.1
|
||||
- 4.9.0
|
||||
- Git
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: obs_log_url
|
||||
attributes:
|
||||
label: OBS Studio Log URL
|
||||
description: Please provide the obsproject.com URL (from Help menu > Log Files > Upload Current/Last Log File) to the OBS log file where this issue occurred.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: obs_crash_log_url
|
||||
attributes:
|
||||
label: OBS Studio Crash Log URL
|
||||
description: If this is a crash report, please provide the obsproject.com URL to the OBS crash log file where this issue occurred.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: "What did you expect to happen?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: current_behavior
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: "What actually happened?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: "How do you trigger this bug? Please walk us through it step by step."
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional_notes
|
||||
attributes:
|
||||
label: Anything else we should know?
|
||||
validations:
|
||||
required: false
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Help/Support
|
||||
url: https://discord.gg/UjfPmYdRPZ
|
||||
about: Development-related help for obs-websocket
|
42
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
name: Feature Request
|
||||
description: Request for a new feature (request/event) to be added to obs-websocket
|
||||
title: "Feature Request: "
|
||||
labels: ["Issue: Feature Request"]
|
||||
body:
|
||||
- type: markdown
|
||||
id: md_welcome
|
||||
attributes:
|
||||
value: This form is for requesting features for obs-websocket!
|
||||
- type: dropdown
|
||||
id: feature_request_type
|
||||
attributes:
|
||||
label: Feature Request Type
|
||||
description: What kind of feature would you like to see added to obs-websocket?
|
||||
options:
|
||||
- RPC Request
|
||||
- RPC Event
|
||||
- Settings Dialog
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: feature_request_type_other
|
||||
attributes:
|
||||
label: Feature Request Type (Other)
|
||||
description: "If \"Other\" was selected above, what type of feature request do you have?"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: requested_feature
|
||||
attributes:
|
||||
label: Requested Feature
|
||||
description: "What feature would you like to see added?"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: requested_feature_scenario
|
||||
attributes:
|
||||
label: Requested Feature Usage Scenario
|
||||
description: "What is a use-case where this feature would be helpful?"
|
||||
validations:
|
||||
required: true
|
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
@ -1,6 +1,6 @@
|
||||
<!--- Please fill out the following template, which will help other contributors review your Pull Request. -->
|
||||
|
||||
<!--- Make sure you’ve read the contribution guidelines here: https://github.com/Palakis/obs-websocket/blob/master/CONTRIBUTING.md -->
|
||||
<!--- Make sure you’ve read the contribution guidelines here: https://github.com/obsproject/obs-websocket/wiki/Contributing-Guidelines -->
|
||||
|
||||
### Description
|
||||
<!--- Describe your changes. -->
|
||||
@ -18,18 +18,17 @@ Tested OS(s):
|
||||
<!--- What types of changes does your PR introduce? Uncomment all that apply -->
|
||||
|
||||
<!--- - Bug fix (non-breaking change which fixes an issue) -->
|
||||
<!--- - New request/event (non-breaking) -->
|
||||
<!--- - Documentation change (a change to documentation pages) -->
|
||||
<!--- - Enhancement (modification to a current event/request which adds functionality) -->
|
||||
<!--- - Performance enhancement (non-breaking change which improves efficiency) -->
|
||||
<!--- - Code cleanup (non-breaking change which makes code smaller or more readable) -->
|
||||
<!--- - New request/event (non-breaking) -->
|
||||
<!--- - Documentation change (a change to documentation pages) -->
|
||||
<!--- - Other Enhancement (anything not applicable to what is listed) -->
|
||||
|
||||
### Checklist:
|
||||
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
||||
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] I have read the [**contributing** document](https://github.com/Palakis/obs-websocket/blob/4.x-current/CONTRIBUTING.md).
|
||||
- [ ] My code is not on the master branch.
|
||||
- [ ] The code has been tested.
|
||||
- [ ] I have read the [Contributing Guidelines](https://github.com/obsproject/obs-websocket/wiki/Contributing-Guidelines).
|
||||
- [ ] All commit messages are properly formatted and commits squashed where appropriate.
|
||||
- [ ] My code is not on the `master` branch.
|
||||
- [ ] The code has been tested.
|
||||
- [ ] I have included updates to all appropriate documentation.
|
||||
|
||||
|
66
BUILDING.md
66
BUILDING.md
@ -1,66 +0,0 @@
|
||||
# Compiling obs-websocket
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You'll need [Qt 5.15.2 on Windows/Mac](https://download.qt.io/official_releases/qt/5.10/) or Qt 5.12.8 on Linux,
|
||||
[CMake](https://cmake.org/download/) and a working [OBS Studio development environment](https://obsproject.com/wiki/install-instructions) installed on your
|
||||
computer.
|
||||
|
||||
## Windows
|
||||
|
||||
In cmake-gui, you'll have to set the following variables :
|
||||
|
||||
- **QTDIR** (path) : location of the Qt environment suited for your compiler and architecture
|
||||
- **LIBOBS_INCLUDE_DIR** (path) : location of the libobs subfolder in the source code of OBS Studio
|
||||
- **LIBOBS_LIB** (filepath) : location of the obs.lib file
|
||||
- **OBS_FRONTEND_LIB** (filepath) : location of the obs-frontend-api.lib file
|
||||
|
||||
## Linux
|
||||
|
||||
On Debian/Ubuntu :
|
||||
|
||||
```shell
|
||||
sudo apt-get install libboost-all-dev
|
||||
git clone --recursive https://github.com/Palakis/obs-websocket.git
|
||||
cd obs-websocket
|
||||
mkdir build && cd build
|
||||
cmake -DLIBOBS_INCLUDE_DIR="<path to the libobs sub-folder in obs-studio's source code>" -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=true ..
|
||||
make -j4
|
||||
sudo make install
|
||||
```
|
||||
|
||||
On other linux OS's, use this cmake command instead:
|
||||
|
||||
```shell
|
||||
cmake -DLIBOBS_INCLUDE_DIR="<path to the libobs sub-folder in obs-studio's source code>" -DCMAKE_INSTALL_PREFIX=/usr ..
|
||||
```
|
||||
|
||||
## OS X
|
||||
|
||||
As a prerequisite, you will need Xcode for your current OSX version, the Xcode command line tools, and [Homebrew](https://brew.sh/).
|
||||
Homebrew's setup will guide you in getting your system set up, you should be good to go once Homebrew is successfully up and running.
|
||||
|
||||
Use of the macOS CI scripts is recommended. Please note that these
|
||||
scripts install new software and can change several settings on your system. An
|
||||
existing obs-studio development environment is not required, as
|
||||
`install-build-obs-macos.sh` will install it for you. If you already have a
|
||||
working obs-studio development environment and have built obs-studio, you can
|
||||
skip that script.
|
||||
|
||||
Of course, you're encouraged to dig through the contents of these scripts to
|
||||
look for issues or specificities.
|
||||
|
||||
```shell
|
||||
git clone --recursive https://github.com/Palakis/obs-websocket.git
|
||||
cd obs-websocket
|
||||
./CI/install-dependencies-macos.sh
|
||||
./CI/install-build-obs-macos.sh
|
||||
./CI/build-macos.sh
|
||||
./CI/package-macos.sh
|
||||
```
|
||||
|
||||
This will result in a ready-to-use `obs-websocket.pkg` installer in the `release` subfolder.
|
||||
|
||||
## Automated Builds
|
||||
|
||||

|
@ -3,13 +3,13 @@ set -e
|
||||
echo "-- Generating documentation."
|
||||
echo "-- Node version: $(node -v)"
|
||||
echo "-- NPM version: $(npm -v)"
|
||||
echo "-- Python3 version: $(python3 -V)"
|
||||
|
||||
git fetch origin
|
||||
git checkout ${CHECKOUT_REF/refs\/heads\//}
|
||||
|
||||
cd docs
|
||||
npm install
|
||||
npm run build
|
||||
bash build_docs.sh
|
||||
|
||||
echo "-- Documentation successfully generated."
|
||||
|
||||
|
1
CI/macos/build-plugin-macos.sh
Normal file → Executable file
1
CI/macos/build-plugin-macos.sh
Normal file → Executable file
@ -17,6 +17,7 @@ fi
|
||||
echo "[obs-websocket] Building 'obs-websocket' for macOS."
|
||||
mkdir -p build && cd build
|
||||
cmake .. \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \
|
||||
-DQTDIR=/tmp/obsdeps \
|
||||
-DLIBOBS_INCLUDE_DIR=../../obs-studio/libobs \
|
||||
-DLIBOBS_LIB=../../obs-studio/libobs \
|
||||
|
0
CI/macos/install-build-obs-macos.sh
Normal file → Executable file
0
CI/macos/install-build-obs-macos.sh
Normal file → Executable file
0
CI/macos/install-dependencies-macos.sh
Normal file → Executable file
0
CI/macos/install-dependencies-macos.sh
Normal file → Executable file
6
CI/macos/package-plugin-macos.sh
Normal file → Executable file
6
CI/macos/package-plugin-macos.sh
Normal file → Executable file
@ -27,6 +27,10 @@ install_name_tool \
|
||||
@executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui \
|
||||
-change /tmp/obsdeps/lib/QtCore.framework/Versions/5/QtCore \
|
||||
@executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore \
|
||||
-change /tmp/obsdeps/lib/QtNetwork.framework/Versions/5/QtNetwork \
|
||||
@executable_path/../Frameworks/QtNetwork.framework/Versions/5/QtNetwork \
|
||||
-change /tmp/obsdeps/lib/QtSvg.framework/Versions/5/QtSvg \
|
||||
@executable_path/../Frameworks/QtSvg.framework/Versions/5/QtSvg \
|
||||
./build/obs-websocket.so
|
||||
|
||||
# Check if replacement worked
|
||||
@ -86,4 +90,4 @@ if [[ "$RELEASE_MODE" == "True" ]]; then
|
||||
done
|
||||
else
|
||||
echo "[obs-websocket] Skipped installer codesigning and notarization"
|
||||
fi
|
||||
fi
|
||||
|
@ -36,6 +36,10 @@ if(NOT CMAKE_BUILD_TYPE)
|
||||
endif()
|
||||
|
||||
|
||||
# Plugin tests flag
|
||||
option(PLUGIN_TESTS "Enable plugin runtime tests" OFF)
|
||||
|
||||
|
||||
# Qt build stuff
|
||||
set(CMAKE_PREFIX_PATH "${QTDIR}")
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
@ -62,7 +66,7 @@ find_package(LibObs REQUIRED)
|
||||
|
||||
|
||||
# Find Qt5
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Svg Concurrent Network)
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Svg Network)
|
||||
|
||||
|
||||
# Find nlohmann
|
||||
@ -85,9 +89,10 @@ configure_file(
|
||||
set(obs-websocket_SOURCES
|
||||
src/obs-websocket.cpp
|
||||
src/Config.cpp
|
||||
src/WebSocketServer.cpp
|
||||
src/WebSocketProtocol.cpp
|
||||
src/WebSocketSession.cpp
|
||||
src/WebSocketApi.cpp
|
||||
src/websocketserver/WebSocketServer.cpp
|
||||
src/websocketserver/WebSocketServer_Protocol.cpp
|
||||
src/websocketserver/rpc/WebSocketSession.cpp
|
||||
src/eventhandler/EventHandler.cpp
|
||||
src/eventhandler/EventHandler_General.cpp
|
||||
src/eventhandler/EventHandler_Config.cpp
|
||||
@ -98,15 +103,23 @@ set(obs-websocket_SOURCES
|
||||
src/eventhandler/EventHandler_Outputs.cpp
|
||||
src/eventhandler/EventHandler_SceneItems.cpp
|
||||
src/eventhandler/EventHandler_MediaInputs.cpp
|
||||
src/eventhandler/EventHandler_Ui.cpp
|
||||
src/requesthandler/RequestHandler.cpp
|
||||
src/requesthandler/RequestBatchHandler.cpp
|
||||
src/requesthandler/RequestHandler_General.cpp
|
||||
src/requesthandler/RequestHandler_Config.cpp
|
||||
src/requesthandler/RequestHandler_Sources.cpp
|
||||
src/requesthandler/RequestHandler_Scenes.cpp
|
||||
src/requesthandler/RequestHandler_Inputs.cpp
|
||||
src/requesthandler/RequestHandler_Transitions.cpp
|
||||
src/requesthandler/RequestHandler_Filters.cpp
|
||||
src/requesthandler/RequestHandler_SceneItems.cpp
|
||||
src/requesthandler/RequestHandler_Stream.cpp
|
||||
src/requesthandler/RequestHandler_Record.cpp
|
||||
src/requesthandler/RequestHandler_MediaInputs.cpp
|
||||
src/requesthandler/RequestHandler_Ui.cpp
|
||||
src/requesthandler/rpc/Request.cpp
|
||||
src/requesthandler/rpc/RequestBatchRequest.cpp
|
||||
src/requesthandler/rpc/RequestResult.cpp
|
||||
src/forms/SettingsDialog.cpp
|
||||
src/forms/ConnectInfo.cpp
|
||||
@ -114,28 +127,46 @@ set(obs-websocket_SOURCES
|
||||
src/utils/Crypto.cpp
|
||||
src/utils/Json.cpp
|
||||
src/utils/Obs.cpp
|
||||
src/utils/Obs_StringHelper.cpp
|
||||
src/utils/Obs_EnumHelper.cpp
|
||||
src/utils/Obs_NumberHelper.cpp
|
||||
src/utils/Obs_ArrayHelper.cpp
|
||||
src/utils/Obs_ObjectHelper.cpp
|
||||
src/utils/Obs_SearchHelper.cpp
|
||||
src/utils/Obs_ActionHelper.cpp
|
||||
src/utils/Obs_VolumeMeter.cpp
|
||||
src/utils/Platform.cpp
|
||||
src/utils/Compat.cpp
|
||||
deps/qr/cpp/QrCode.cpp)
|
||||
|
||||
set(obs-websocket_HEADERS
|
||||
src/obs-websocket.h
|
||||
src/Config.h
|
||||
src/WebSocketServer.h
|
||||
src/WebSocketProtocol.h
|
||||
src/WebSocketSession.h
|
||||
src/WebSocketApi.h
|
||||
src/websocketserver/WebSocketServer.h
|
||||
src/websocketserver/types/WebSocketCloseCode.h
|
||||
src/websocketserver/types/WebSocketOpCode.h
|
||||
src/websocketserver/rpc/WebSocketSession.h
|
||||
src/eventhandler/EventHandler.h
|
||||
src/eventhandler/types/EventSubscription.h
|
||||
src/requesthandler/RequestHandler.h
|
||||
src/requesthandler/RequestBatchHandler.h
|
||||
src/requesthandler/types/RequestStatus.h
|
||||
src/requesthandler/types/RequestBatchExecutionType.h
|
||||
src/requesthandler/rpc/Request.h
|
||||
src/requesthandler/rpc/RequestBatchRequest.h
|
||||
src/requesthandler/rpc/RequestResult.h
|
||||
src/requesthandler/rpc/RequestStatus.h
|
||||
src/forms/SettingsDialog.h
|
||||
src/forms/ConnectInfo.h
|
||||
src/utils/Crypto.h
|
||||
src/utils/Json.h
|
||||
src/utils/Obs.h
|
||||
src/utils/Obs_VolumeMeter.h
|
||||
src/utils/Obs_VolumeMeter_Helpers.h
|
||||
src/utils/Platform.h
|
||||
src/utils/Compat.h
|
||||
src/utils/Utils.h
|
||||
lib/obs-websocket-api.h
|
||||
deps/qr/cpp/QrCode.hpp)
|
||||
|
||||
|
||||
@ -149,7 +180,6 @@ include_directories(
|
||||
${Qt5Core_INCLUDES}
|
||||
${Qt5Widgets_INCLUDES}
|
||||
${Qt5Svg_INCLUDES}
|
||||
${Qt5Concurrent_INCLUDES}
|
||||
${Qt5Network_INCLUDES}
|
||||
"${CMAKE_SOURCE_DIR}/deps/asio/asio/include"
|
||||
"${CMAKE_SOURCE_DIR}/deps/websocketpp")
|
||||
@ -159,10 +189,13 @@ target_link_libraries(obs-websocket
|
||||
Qt5::Core
|
||||
Qt5::Widgets
|
||||
Qt5::Svg
|
||||
Qt5::Concurrent
|
||||
Qt5::Network
|
||||
nlohmann_json::nlohmann_json)
|
||||
|
||||
if(PLUGIN_TESTS)
|
||||
target_compile_definitions(obs-websocket PRIVATE PLUGIN_TESTS)
|
||||
endif()
|
||||
|
||||
|
||||
# Windows-specific build settings and tasks
|
||||
if(WIN32)
|
||||
@ -270,6 +303,8 @@ endif()
|
||||
if(UNIX AND NOT APPLE)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
target_compile_options(obs-websocket PRIVATE -Wall -Wextra -Wpedantic -Werror -Wno-missing-field-initializers)
|
||||
|
||||
set_target_properties(obs-websocket PROPERTIES PREFIX "")
|
||||
target_link_libraries(obs-websocket obs-frontend-api)
|
||||
|
||||
|
@ -1,88 +0,0 @@
|
||||
# Contributing to obs-websocket
|
||||
|
||||
## Translating obs-websocket to your language
|
||||
|
||||
Localization happens on [Crowdin](https://crowdin.com/project/obs-websocket)
|
||||
|
||||
## Branches
|
||||
|
||||
**Development happens on `master`**
|
||||
|
||||
## Writing code for obs-websocket
|
||||
|
||||
### Code Formatting Guidelines
|
||||
|
||||
* Function and variable names: camelCase for variables, MixedCaps for method names
|
||||
|
||||
* Request and Event names should use MixedCaps names. Keep naming conformity of request naming using similar terms like `Get`, `Set`, `Get[x]List`, `Start[x]`, `Toggle[x]`.
|
||||
|
||||
* Request and Event json properties/fields should use camelCase. Try to use existing property names.
|
||||
|
||||
* Code is indented with Tabs. Assume they are 4 columns wide
|
||||
|
||||
* 80 columns max code width. (Comments/docs can be larger)
|
||||
|
||||
* New and updated requests/events must always come with accompanying documentation comments (see existing protocol elements for examples).
|
||||
These are required to automatically generate the [protocol specification document](docs/generated/protocol.md).
|
||||
|
||||
### Code Best-Practices
|
||||
|
||||
* Favor return-early code and avoid wrapping huge portions of code in conditionals. As an example, this:
|
||||
```cpp
|
||||
if (success) {
|
||||
return RequestResult::Success();
|
||||
} else {
|
||||
return RequestResult::Error(RequestStatus::GenericError);
|
||||
}
|
||||
```
|
||||
is better like this:
|
||||
```cpp
|
||||
if (!success) {
|
||||
return RequestResult::Error(RequestStatus::GenericError);
|
||||
}
|
||||
return RequestResult::Success();
|
||||
```
|
||||
|
||||
* Try to use the [built-in](https://github.com/Palakis/obs-websocket/blob/master/src/requesthandler/rpc/Request.h) request checks when possible.
|
||||
* Refer to existing requests for usage examples.
|
||||
|
||||
* Some example common response/request property names are:
|
||||
* `sceneName` - The name of a scene
|
||||
* `inputName` - The name of an input
|
||||
* `sourceName` - The name of a source (only for when multiple source types apply)
|
||||
* `sceneItemEnabled` - Whether a scene item is enabled
|
||||
|
||||
* Response parameters which have no attributed data due to an invalid state should be set to `null` (versus being left out)
|
||||
* For example, when `GetSceneList` is called and OBS is not in studio mode, `currentPreviewSceneName` will be `null`
|
||||
* If a request's core response data depends on a state, an error should be thrown unless `ignoreNonFatalRequestChecks` is set. See `GetCurrentPreviewScene` as an example.
|
||||
|
||||
### Commit Guidelines
|
||||
|
||||
* Commits follow the 50/72 standard:
|
||||
* 50 characters max for the commit title (excluding scope name)
|
||||
* One empty line after the title
|
||||
* Description wrapped to 72 columns max width per line.
|
||||
|
||||
* Commit titles:
|
||||
* Use present tense
|
||||
* Prefix the title with a "scope" name
|
||||
* e.g: "CI: fix wrong behaviour when packaging for OS X"
|
||||
* Typical scopes: CI, General, Requests, Events, Server
|
||||
|
||||
**Example commit:**
|
||||
|
||||
```
|
||||
Requests: Add GetSceneList
|
||||
|
||||
Adds a new request called `GetSceneList` which returns the current
|
||||
scene, along with an array of objects, each one with a scene name
|
||||
and index.
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* Pull Requests must never be based off your fork's main branch (in this case, `master`).
|
||||
* Start your work in a newly named branch based on the upstream main one (e.g.: `feature/cool-new-feature`, `bugfix/fix-palakis-mistakes`, ...)
|
||||
|
||||
* If your work is not done yet, but for any reason you need to PR it (like collecting discussions, testing with CI, getting testers),
|
||||
create it as a Draft Pull Request (open the little arrow menu next to the "Create pull request" button, then select "Create draft pull request").
|
86
README.md
86
README.md
@ -1,39 +1,21 @@
|
||||
# obs-websocket
|
||||
|
||||
## YOU HAVE STUMBLED UPON THE DEV BRANCH FOR V5.0.0
|
||||
|
||||
- You can find the main protocol spec here: [PROTOCOL.md](docs/generated/protocol.md).
|
||||
- You can find the planned requests sheet [here](https://docs.google.com/spreadsheets/d/1LfCZrbT8e7cSaKo_TuPDd-CJiptL7RSuo8iE63vMmMs/edit?usp=sharing)
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="/.github/images/obsws_logo.png" width=150 align="center">
|
||||
</p>
|
||||
|
||||
WebSockets API for OBS Studio.
|
||||
WebSocket API for OBS Studio.
|
||||
|
||||
[](https://github.com/Palakis/obs-websocket/actions/workflows/main.yml)
|
||||
[](https://github.com/obs-websocket/obs-websocket/actions/workflows/main.yml)
|
||||
[](https://discord.gg/WBaSQ3A)
|
||||
[](https://opencollective.com/obs-websocket)
|
||||
[](https://opencollective.com/obs-websocket-dev)
|
||||
|
||||
## Downloads
|
||||
|
||||
Binaries for Windows, MacOS, and Linux are available in the [Releases](https://github.com/Palakis/obs-websocket/releases) section.
|
||||
|
||||
### Homebrew
|
||||
|
||||
If you're using MacOS you can use Homebrew for installation as well:
|
||||
|
||||
```sh
|
||||
brew install obs-websocket
|
||||
```
|
||||
Binaries for Windows, MacOS, and Linux are available in the [Releases](https://github.com/obsproject/obs-websocket/releases) section.
|
||||
|
||||
## Using obs-websocket
|
||||
|
||||
Here is a list of available web clients: (compatible with tablets and other touch interfaces)
|
||||
|
||||
- Python 3.7+ (Asyncio): [simpleobsws](https://github.com/IRLToolkit/simpleobsws/tree/master) by IRLToolkit
|
||||
|
||||
It is **highly recommended** to protect obs-websocket with a password against unauthorized control. To do this, open the "Websocket server settings" dialog under OBS' "Tools" menu. In the settings dialogs, you can enable or disable authentication and set a password for it.
|
||||
|
||||
(Psst. You can use `--websocket_port`(value), `--websocket_password`(value), and `--websocket_debug`(flag) on the command line to override the configured values.)
|
||||
@ -44,58 +26,46 @@ It is **highly recommended** to protect obs-websocket with a password against un
|
||||
- Change your stream overlay/graphics based on the current scene
|
||||
- Automate scene switching with a third-party program (e.g. : auto-pilot, foot pedal, ...)
|
||||
|
||||
### For developers
|
||||
### Client software
|
||||
- (No known clients supporting 5.0.0 at the moment. Send a message in Discord if you have one!)
|
||||
|
||||
The server is a typical Websockets server running by default on port 4444 (the port number can be changed in the Settings dialog).
|
||||
The protocol understood by the server is documented in [PROTOCOL.md](docs/generated/protocol.md).
|
||||
### Client libraries (for developers)
|
||||
|
||||
Here's a list of available language APIs for obs-websocket :
|
||||
- (No known apis supporting 5.0.0)
|
||||
Here's a list of available language APIs for obs-websocket:
|
||||
- Python 3.7+ (Asyncio): [simpleobsws](https://github.com/IRLToolkit/simpleobsws/tree/master) by IRLToolkit
|
||||
- Rust: [obws](https://github.com/dnaka91/obws/tree/v5-api) by dnaka91
|
||||
|
||||
The server is a typical Websockets server running by default on port 4444 (the port number can be changed in the Settings dialog under `Tools`).
|
||||
The protocol we use is documented in [PROTOCOL.md](docs/generated/protocol.md).
|
||||
|
||||
We'd like to know what you're building with or for obs-websocket. If you do something in this fashion, feel free to drop a message in `#project-showoff` in the [discord server!](https://discord.gg/WBaSQ3A)
|
||||
|
||||
### Securing obs-websocket (via TLS/SSL)
|
||||
|
||||
If you are intending to use obs-websocket outside of a LAN environment, it is highly recommended to secure the connection using a tunneling service.
|
||||
|
||||
See the SSL [tunnelling guide](SSL-TUNNELLING.md) for easy instructions on how to encrypt your websocket connection.
|
||||
|
||||
## Compiling obs-websocket
|
||||
|
||||
See the [build instructions](BUILDING.md).
|
||||
|
||||
## Translations
|
||||
|
||||
**Your help is welcome on translations.**
|
||||
|
||||
Please join the localization project on [Crowdin](https://crowdin.com/project/obs-websocket)
|
||||
|
||||
## Contributors
|
||||
|
||||
### Code Contributors
|
||||
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
<a href="https://github.com/Palakis/obs-websocket/graphs/contributors"><img src="https://opencollective.com/obs-websocket/contributors.svg?width=890&button=false" /></a>
|
||||
This project exists thanks to [all the people](graphs/contributors) who contribute. [Contribute](wiki/Contributing-Guidelines).
|
||||
<a href="https://github.com/obsproject/obs-websocket/graphs/contributors"><img src="https://opencollective.com/obs-websocket-dev/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [Contribute](https://opencollective.com/obs-websocket/contribute)
|
||||
Become a financial contributor and help us sustain our community. [Contribute](https://opencollective.com/obs-websocket-dev/contribute)
|
||||
|
||||
#### Individuals
|
||||
|
||||
<a href="https://opencollective.com/obs-websocket"><img src="https://opencollective.com/obs-websocket/individuals.svg?width=890"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev"><img src="https://opencollective.com/obs-websocket-dev/individuals.svg?width=890"></a>
|
||||
|
||||
#### Organizations
|
||||
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute](https://opencollective.com/obs-websocket/contribute)
|
||||
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute](https://opencollective.com/obs-websocket-dev/contribute)
|
||||
|
||||
<a href="https://opencollective.com/obs-websocket/organization/0/website"><img src="https://opencollective.com/obs-websocket/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/1/website"><img src="https://opencollective.com/obs-websocket/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/2/website"><img src="https://opencollective.com/obs-websocket/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/3/website"><img src="https://opencollective.com/obs-websocket/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/4/website"><img src="https://opencollective.com/obs-websocket/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/5/website"><img src="https://opencollective.com/obs-websocket/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/6/website"><img src="https://opencollective.com/obs-websocket/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/7/website"><img src="https://opencollective.com/obs-websocket/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/8/website"><img src="https://opencollective.com/obs-websocket/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket/organization/9/website"><img src="https://opencollective.com/obs-websocket/organization/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/0/website"><img src="https://opencollective.com/obs-websocket-dev/organization/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/1/website"><img src="https://opencollective.com/obs-websocket-dev/organization/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/2/website"><img src="https://opencollective.com/obs-websocket-dev/organization/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/3/website"><img src="https://opencollective.com/obs-websocket-dev/organization/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/4/website"><img src="https://opencollective.com/obs-websocket-dev/organization/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/5/website"><img src="https://opencollective.com/obs-websocket-dev/organization/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/6/website"><img src="https://opencollective.com/obs-websocket-dev/organization/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/7/website"><img src="https://opencollective.com/obs-websocket-dev/organization/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/8/website"><img src="https://opencollective.com/obs-websocket-dev/organization/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/obs-websocket-dev/organization/9/website"><img src="https://opencollective.com/obs-websocket-dev/organization/9/avatar.svg"></a>
|
||||
|
@ -1,45 +0,0 @@
|
||||
# Connecting over a TLS/secure connection (or remotely)
|
||||
|
||||
If you want to expose the WebSocket server of obs-websocket over a secure TLS connection (or to connect remotely), the easiest approach is to use a localhost tunneling service like [ngrok](https://ngrok.com/) or [pagekite](https://pagekite.net/).
|
||||
|
||||
**Before doing this, secure the WebSocket server first by enabling authentication with a strong password!**
|
||||
|
||||
**Please bear in mind that doing this will expose your OBS instance to the open Internet and the security risks it implies. *You've been warned!***
|
||||
|
||||
|
||||
## ngrok
|
||||
|
||||
[Install the ngrok CLI tool](https://ngrok.com/download) on a linux OS, then start ngrok bound to port 4444 like this:
|
||||
|
||||
```bash
|
||||
ngrok http 4444
|
||||
```
|
||||
|
||||
The ngrok command will output something like this:
|
||||
|
||||
```text
|
||||
ngrok by @inconshreveable
|
||||
|
||||
Tunnel Status online
|
||||
Version 2.0/2.0
|
||||
Web Interface http://127.0.0.1:4040
|
||||
Forwarding http://TUNNEL_ID.ngrok.io -> localhost:4444
|
||||
Forwarding https://TUNNEL_ID.ngrok.io -> localhost:4444
|
||||
```
|
||||
|
||||
Where `TUNNEL_ID` is, as the name implies, the unique name of your ngrok tunnel. You'll get a new one every time you start ngrok.
|
||||
|
||||
Then, use `wss://TUNNEL_ID.ngrok.io` to connect to obs-websocket over TLS.
|
||||
|
||||
See the [ngrok documentation](https://ngrok.com/docs) for more tunneling options and settings.
|
||||
|
||||
|
||||
## PageKite
|
||||
|
||||
[Install the PageKite CLI tool](http://pagekite.net/downloads), then start PageKite bound to port 4444 like this (replace NAME with one of your choosing):
|
||||
|
||||
```bash
|
||||
python pagekite.py 4444 NAME.pagekite.me
|
||||
```
|
||||
|
||||
Then, use `wss://NAME.pagekite.me` to connect to obs-websocket over TLS.
|
@ -1,3 +0,0 @@
|
||||
files:
|
||||
- source: /data/locale/en-US.ini
|
||||
translation: /data/locale/%locale%.ini
|
@ -1,3 +1,5 @@
|
||||
OBSWebSocket.Plugin.Description="Remote-control of OBS Studio through WebSocket"
|
||||
|
||||
OBSWebSocket.Settings.DialogTitle="obs-websocket Settings"
|
||||
|
||||
OBSWebSocket.Settings.PluginSettingsTitle="Plugin Settings"
|
||||
@ -18,6 +20,8 @@ OBSWebSocket.Settings.ShowConnectInfoWarningInfoText="Are you sure that you want
|
||||
OBSWebSocket.Settings.Save.UserPasswordWarningTitle="Warning: Potential Security Issue"
|
||||
OBSWebSocket.Settings.Save.UserPasswordWarningMessage="obs-websocket stores the server password as plain text. Using a password generated by obs-websocket is highly recommended."
|
||||
OBSWebSocket.Settings.Save.UserPasswordWarningInfoText="Are you sure you want to use your own password?"
|
||||
OBSWebSocket.Settings.Save.PasswordInvalidErrorTitle="Error: Invalid Configuration"
|
||||
OBSWebSocket.Settings.Save.PasswordInvalidErrorMessage="You must use a password that is 6 or more characters."
|
||||
|
||||
OBSWebSocket.SessionTable.Title="Connected WebSocket Sessions"
|
||||
OBSWebSocket.SessionTable.RemoteAddressColumnTitle="Remote Address"
|
||||
|
2
deps/asio
vendored
2
deps/asio
vendored
Submodule deps/asio updated: b84e6c16b2...08a7029cb1
@ -1,11 +0,0 @@
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md, *.mustache]
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
5
docs/.gitignore
vendored
5
docs/.gitignore
vendored
@ -1,4 +1 @@
|
||||
node_modules
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
work
|
||||
|
148
docs/README.md
148
docs/README.md
@ -1,21 +1,127 @@
|
||||
## Installation
|
||||
|
||||
Install node and update npm if necessary.
|
||||
|
||||
```sh
|
||||
cd obs-websocket/docs
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
# Just extract the comments.
|
||||
npm run comments
|
||||
|
||||
# Just render the markdown.
|
||||
npm run docs
|
||||
|
||||
# Do both comments and markdown.
|
||||
npm run build
|
||||
```
|
||||
# obs-websocket documentation
|
||||
|
||||
This is the documentation for obs-websocket. Run build_docs.sh to auto generate the latest docs from the `src` directory. There are 3 components to the docs generation:
|
||||
- `comments/comments.js`: Generates the `work/comments.json` file from the code comments in the src directory.
|
||||
- `docs/process_comments.py`: Processes `work/comments.json` to create `generated/protocol.json`, which is a machine-readable documentation format that can be used to create obs-websocket client libraries.
|
||||
- `docs/generate_md.py`: Processes `generated/protocol.json` to create `generated/protocol.md`, which is the actual human-readable documentation.
|
||||
|
||||
Some notes about documenting:
|
||||
- The `complexity` comment line is a suggestion to the user about how much knowledge about OBS's inner workings is required to safely use the associated feature. `1` for easy, `5` for megadeath-expert.
|
||||
- The `rpcVersion` comment line is used to specify the latest available version that the feature is available in. If a feature is deprecated, then the placeholder value of `-1` should be replaced with the last RPC version that the feature will be available in. Manually specifying an RPC version automatically adds the `Deprecated` line to the entry in `generated/protocol.md`.
|
||||
- The description can be multiple lines, but must be contained above the first documentation property (the lines starting with `@`).
|
||||
- Value types are in reference to JSON values. The only ones you should use are `Any`, `String`, `Boolean`, `Number`, `Array`, `Object`.
|
||||
- `Array` types follow this format: `Array<subtype>`, for example `Array<String>` to specify an array of strings.
|
||||
|
||||
Formatting notes:
|
||||
- Fields should have their columns aligned. So in a request, the columns of all `@requestField`s should be aligned.
|
||||
- We suggest looking at how other enums/events/requests have been documented before documenting one of your own, to get a general feel of how things have been formatted.
|
||||
|
||||
## Creating enum documentation
|
||||
|
||||
Enums follow this code comment format:
|
||||
```
|
||||
/**
|
||||
* [description]
|
||||
*
|
||||
* @enumIdentifier [identifier]
|
||||
* @enumValue [value]
|
||||
* @enumType [type]
|
||||
* @rpcVersion [latest available RPC version, use `-1` unless deprecated.]
|
||||
* @initialVersion [first obs-websocket version this is found in]
|
||||
* @api enums
|
||||
*/
|
||||
```
|
||||
|
||||
Example code comment:
|
||||
```
|
||||
/**
|
||||
* The initial message sent by obs-websocket to newly connected clients.
|
||||
*
|
||||
* @enumIdentifier Hello
|
||||
* @enumValue 0
|
||||
* @enumType WebSocketOpCode
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
```
|
||||
- This is the documentation for the `WebSocketOpCode::Hello` enum identifier.
|
||||
|
||||
## Creating event documentation
|
||||
|
||||
Events follow this code comment format:
|
||||
```
|
||||
/**
|
||||
* [description]
|
||||
*
|
||||
* @dataField [field name] | [value type] | [field description]
|
||||
* [... more @dataField entries ...]
|
||||
*
|
||||
* @eventType [type]
|
||||
* @eventSubscription [EventSubscription requirement]
|
||||
* @complexity [complexity rating, 1-5]
|
||||
* @rpcVersion [latest available RPC version, use `-1` unless deprecated.]
|
||||
* @initialVersion [first obs-websocket version this is found in]
|
||||
* @category [event category]
|
||||
* @api events
|
||||
*/
|
||||
```
|
||||
|
||||
Example code comment:
|
||||
```
|
||||
/**
|
||||
* Studio mode has been enabled or disabled.
|
||||
*
|
||||
* @dataField studioModeEnabled | Boolean | True == Enabled, False == Disabled
|
||||
*
|
||||
* @eventType StudioModeStateChanged
|
||||
* @eventSubscription General
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api events
|
||||
*/
|
||||
```
|
||||
|
||||
## Creating request documentation
|
||||
|
||||
Requests follow this code comment format:
|
||||
```
|
||||
/**
|
||||
* [description]
|
||||
*
|
||||
* @requestField [optional flag][field name] | [value type] | [field description] | [value restrictions (only include if the value type is `Number`)] | [default behavior (only include if optional flag is set)]
|
||||
* [... more @requestField entries ...]
|
||||
*
|
||||
* @responseField [field name] | [value type] | [field description]
|
||||
* [... more @responseField entries ...]
|
||||
*
|
||||
* @requestType [type]
|
||||
* @complexity [complexity rating, 1-5]
|
||||
* @rpcVersion [latest available RPC version, use `-1` unless deprecated.]
|
||||
* @initialVersion [first obs-websocket version this is found in]
|
||||
* @category [request category]
|
||||
* @api requests
|
||||
*/
|
||||
```
|
||||
- The optional flag is a `?` that prefixes the field name, telling the docs processor that the field is optionally specified.
|
||||
|
||||
Example code comment:
|
||||
```
|
||||
/**
|
||||
* Gets the value of a "slot" from the selected persistent data realm.
|
||||
*
|
||||
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
|
||||
* @requestField slotName | String | The name of the slot to retrieve data from
|
||||
*
|
||||
* @responseField slotValue | String | Value associated with the slot. `null` if not set
|
||||
*
|
||||
* @requestType GetPersistentData
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
```
|
||||
|
9
docs/build_docs.sh
Executable file
9
docs/build_docs.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd comments
|
||||
npm install
|
||||
npm run comments
|
||||
|
||||
cd ../docs
|
||||
python3 process_comments.py
|
||||
python3 generate_md.py
|
104
docs/comments.js
104
docs/comments.js
@ -1,104 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const parseComments = require('parse-comments');
|
||||
const config = require('./config.json');
|
||||
|
||||
/**
|
||||
* Read each file and call `parse-comments` on it.
|
||||
*
|
||||
* @param {String|Array} `files` List of file paths to read from.
|
||||
* @return {Object|Array} Array of `parse-comments` objects.
|
||||
*/
|
||||
const parseFiles = files => {
|
||||
let response = [];
|
||||
files.forEach(file => {
|
||||
const f = fs.readFileSync(file, 'utf8').toString();
|
||||
response = response.concat(parseComments(f));
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters/sorts the results from `parse-comments`.
|
||||
* @param {Object|Array} `comments` Array of `parse-comments` objects.
|
||||
* @return {Object} Filtered comments sorted by `@api` and `@category`.
|
||||
*/
|
||||
const processComments = comments => {
|
||||
let sorted = {};
|
||||
let errors = [];
|
||||
|
||||
comments.forEach(comment => {
|
||||
if (comment.typedef) {
|
||||
comment.comment = undefined;
|
||||
comment.context = undefined;
|
||||
sorted['typedefs'] = sorted['typedefs'] || [];
|
||||
sorted['typedefs'].push(comment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof comment.api === 'undefined') return;
|
||||
let validationFailures = validateComment(comment);
|
||||
|
||||
if (validationFailures) {
|
||||
errors.push(validationFailures);
|
||||
}
|
||||
|
||||
// Store the object based on its api (ie. requests, events) and category (ie. general, scenes, etc).
|
||||
comment.category = comment.category || 'miscellaneous';
|
||||
|
||||
// Remove some unnecessary properties to avoid result differences in travis.
|
||||
comment.comment = undefined;
|
||||
comment.context = undefined;
|
||||
|
||||
// Create an entry in sorted for the api/category if one does not exist.
|
||||
sorted[comment.api] = sorted[comment.api] || {};
|
||||
sorted[comment.api][comment.category] = sorted[comment.api][comment.category] || [];
|
||||
|
||||
// Store the comment in the appropriate api/category.
|
||||
sorted[comment.api][comment.category].push(comment);
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
throw JSON.stringify(errors, null, 2);
|
||||
}
|
||||
|
||||
return sorted;
|
||||
};
|
||||
|
||||
// Rudimentary validation of documentation content, returns an error object or undefined.
|
||||
const validateComment = comment => {
|
||||
let errors = [];
|
||||
[].concat(comment.params).concat(comment.returns).filter(Boolean).forEach(param => {
|
||||
if (typeof param.name !== 'string' || param.name === '') {
|
||||
errors.push({
|
||||
description: `Invalid param or return value name`,
|
||||
param: param
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof param.type !== 'string' || param.type === '') {
|
||||
errors.push({
|
||||
description: `Invalid param or return value type`,
|
||||
param: param
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
return {
|
||||
errors: errors,
|
||||
fullContext: Object.assign({}, comment)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const files = glob.sync(config.srcGlob);
|
||||
const comments = processComments(parseFiles(files));
|
||||
|
||||
if (!fs.existsSync(config.outDirectory)){
|
||||
fs.mkdirSync(config.outDirectory);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(config.outDirectory, 'comments.json'), JSON.stringify(comments, null, 2));
|
2
docs/comments/.gitignore
vendored
Normal file
2
docs/comments/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
npm-debug.log*
|
24
docs/comments/comments.js
Normal file
24
docs/comments/comments.js
Normal file
@ -0,0 +1,24 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const parseComments = require('parse-comments');
|
||||
const config = require('./config.json');
|
||||
|
||||
const parseFiles = files => {
|
||||
let response = [];
|
||||
files.forEach(file => {
|
||||
const f = fs.readFileSync(file, 'utf8').toString();
|
||||
response = response.concat(parseComments(f));
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const files = glob.sync(config.srcGlob);
|
||||
const comments = parseFiles(files);
|
||||
|
||||
if (!fs.existsSync(config.outDirectory)){
|
||||
fs.mkdirSync(config.outDirectory);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(config.outDirectory, 'comments.json'), JSON.stringify(comments, null, 2));
|
4
docs/comments/config.json
Normal file
4
docs/comments/config.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"srcGlob": "./../../src/**/*.@(cpp|h)",
|
||||
"outDirectory": "./../work"
|
||||
}
|
15
docs/comments/package.json
Normal file
15
docs/comments/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "obs-websocket-comments",
|
||||
"version": "1.2.0",
|
||||
"description": "",
|
||||
"main": "comments.js",
|
||||
"scripts": {
|
||||
"comments": "node ./comments.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.2",
|
||||
"parse-comments": "^0.4.3"
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"srcGlob": "./../src/**/*.@(cpp|h)",
|
||||
"srcTemplate": "./protocol.hbs",
|
||||
"outDirectory": "./generated"
|
||||
}
|
37
docs/docs.js
37
docs/docs.js
@ -1,37 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const toc = require('markdown-toc');
|
||||
const handlebars = require('handlebars');
|
||||
const config = require('./config.json');
|
||||
|
||||
const helpers = require('handlebars-helpers')({
|
||||
handlebars: handlebars
|
||||
});
|
||||
|
||||
// Allows pipe characters to be used within markdown tables.
|
||||
handlebars.registerHelper('depipe', (text) => {
|
||||
return typeof text === 'string' ? text.replace('|', '\\|') : text;
|
||||
});
|
||||
|
||||
const insertHeader = (text) => {
|
||||
return '<!-- This file was generated based on handlebars templates. Do not edit directly! -->\n\n' + text;
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes `protocol.md` using `protocol.mustache`.
|
||||
*
|
||||
* @param {Object} `data` Data to assign to the mustache template.
|
||||
*/
|
||||
const generateProtocol = (templatePath, data) => {
|
||||
const template = fs.readFileSync(templatePath).toString();
|
||||
const generated = handlebars.compile(template)(data);
|
||||
return insertHeader(toc.insert(generated));
|
||||
};
|
||||
|
||||
if (!fs.existsSync(config.outDirectory)){
|
||||
fs.mkdirSync(config.outDirectory);
|
||||
}
|
||||
|
||||
const comments = fs.readFileSync(path.join(config.outDirectory, 'comments.json'), 'utf8');
|
||||
const markdown = generateProtocol(config.srcTemplate, JSON.parse(comments));
|
||||
fs.writeFileSync(path.join(config.outDirectory, 'protocol.md'), markdown);
|
306
docs/docs/generate_md.py
Normal file
306
docs/docs/generate_md.py
Normal file
@ -0,0 +1,306 @@
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [generate_md.py] [%(levelname)s] %(message)s")
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
enumTypeOrder = [
|
||||
'WebSocketOpCode',
|
||||
'WebSocketCloseCode',
|
||||
'RequestBatchExecutionType',
|
||||
'RequestStatus',
|
||||
'EventSubscription'
|
||||
]
|
||||
|
||||
categoryOrder = [
|
||||
'General',
|
||||
'Config',
|
||||
'Sources',
|
||||
'Scenes',
|
||||
'Inputs',
|
||||
'Transitions',
|
||||
'Filters',
|
||||
'Scene Items',
|
||||
'Outputs',
|
||||
'Stream',
|
||||
'Record',
|
||||
'Media Inputs',
|
||||
'Ui',
|
||||
'High-Volume'
|
||||
]
|
||||
|
||||
requestFieldHeader = """
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
| Name | Type | Description | Value Restrictions | ?Default Behavior |
|
||||
| ---- | :---: | ----------- | :----------------: | ----------------- |
|
||||
"""
|
||||
|
||||
responseFieldHeader = """
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ----------- |
|
||||
"""
|
||||
|
||||
dataFieldHeader = """
|
||||
|
||||
**Data Fields:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ----------- |
|
||||
"""
|
||||
|
||||
fragments = []
|
||||
|
||||
# Utils
|
||||
#######################################################################################################################
|
||||
|
||||
def read_file(fileName):
|
||||
with open(fileName, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
def get_fragment(name, register = True):
|
||||
global fragments
|
||||
testFragmentName = name.replace(' ', '-').replace(':', '').lower()
|
||||
if testFragmentName in fragments:
|
||||
testFragmentName += '-1'
|
||||
increment = 1
|
||||
while testFragmentName in fragments:
|
||||
increment += 1
|
||||
testFragmentName[:-1] = str(increment)
|
||||
if register:
|
||||
fragments.append(testFragmentName)
|
||||
return testFragmentName
|
||||
|
||||
def get_category_items(items, category):
|
||||
ret = []
|
||||
for item in requests:
|
||||
if item['category'] != category:
|
||||
continue
|
||||
ret.append(item)
|
||||
return ret
|
||||
|
||||
def get_enums_toc(enums):
|
||||
ret = ''
|
||||
for enumType in enumTypeOrder:
|
||||
enum = None
|
||||
for enumIt in enums:
|
||||
if enumIt['enumType'] == enumType:
|
||||
enum = enumIt
|
||||
break
|
||||
if not enum:
|
||||
continue
|
||||
typeFragment = get_fragment(enumType, False)
|
||||
ret += '- [{}](#{})\n'.format(enumType, typeFragment)
|
||||
for enumIdentifier in enum['enumIdentifiers']:
|
||||
enumIdentifier = enumIdentifier['enumIdentifier']
|
||||
enumIdentifierHeader = '{}::{}'.format(enumType, enumIdentifier)
|
||||
enumIdentifierFragment = get_fragment(enumIdentifierHeader, False)
|
||||
ret += ' - [{}](#{})\n'.format(enumIdentifierHeader, enumIdentifierFragment)
|
||||
return ret
|
||||
|
||||
def get_enums(enums):
|
||||
ret = ''
|
||||
for enumType in enumTypeOrder:
|
||||
enum = None
|
||||
for enumIt in enums:
|
||||
if enumIt['enumType'] == enumType:
|
||||
enum = enumIt
|
||||
break
|
||||
if not enum:
|
||||
continue
|
||||
typeFragment = get_fragment(enumType)
|
||||
ret += '## {}\n\n'.format(enumType)
|
||||
for enumIdentifier in enum['enumIdentifiers']:
|
||||
enumIdentifierString = enumIdentifier['enumIdentifier']
|
||||
enumIdentifierHeader = '{}::{}'.format(enumType, enumIdentifierString)
|
||||
enumIdentifierFragment = get_fragment(enumIdentifierHeader, False)
|
||||
ret += '### {}\n\n'.format(enumIdentifierHeader)
|
||||
ret += '{}\n\n'.format(enumIdentifier['description'])
|
||||
ret += '- Identifier Value: `{}`\n'.format(enumIdentifier['enumValue'])
|
||||
ret += '- Latest Supported RPC Version: `{}`\n'.format(enumIdentifier['rpcVersion'])
|
||||
if enumIdentifier['deprecated']:
|
||||
ret += '- **⚠️ Deprecated. ⚠️**\n'
|
||||
if enumIdentifier['initialVersion'].lower() == 'unreleased':
|
||||
ret += '- Unreleased\n'
|
||||
else:
|
||||
ret += '- Added in v{}\n'.format(enumIdentifier['initialVersion'])
|
||||
if enumIdentifier != enum['enumIdentifiers'][-1]:
|
||||
ret += '\n---\n\n'
|
||||
return ret
|
||||
|
||||
def get_requests_toc(requests):
|
||||
ret = ''
|
||||
for category in categoryOrder:
|
||||
requestsOut = []
|
||||
for request in requests:
|
||||
if request['category'] != category.lower():
|
||||
continue
|
||||
requestsOut.append(request)
|
||||
if not len(requestsOut):
|
||||
continue
|
||||
categoryFragment = get_fragment(category, False)
|
||||
ret += '- [{}](#{})\n'.format(category, categoryFragment)
|
||||
for request in requestsOut:
|
||||
requestType = request['requestType']
|
||||
requestTypeFragment = get_fragment(requestType, False)
|
||||
ret += ' - [{}](#{})\n'.format(requestType, requestTypeFragment)
|
||||
return ret
|
||||
|
||||
def get_requests(requests):
|
||||
ret = ''
|
||||
for category in categoryOrder:
|
||||
requestsOut = []
|
||||
for request in requests:
|
||||
if request['category'] != category.lower():
|
||||
continue
|
||||
requestsOut.append(request)
|
||||
if not len(requestsOut):
|
||||
continue
|
||||
categoryFragment = get_fragment(category)
|
||||
ret += '\n\n## {}\n\n'.format(category)
|
||||
for request in requestsOut:
|
||||
requestType = request['requestType']
|
||||
requestTypeFragment = get_fragment(requestType)
|
||||
ret += '### {}\n\n'.format(requestType)
|
||||
ret += '{}\n\n'.format(request['description'])
|
||||
ret += '- Complexity Rating: `{}/5`\n'.format(request['complexity'])
|
||||
ret += '- Latest Supported RPC Version: `{}`\n'.format(request['rpcVersion'])
|
||||
if request['deprecated']:
|
||||
ret += '- **⚠️ Deprecated. ⚠️**\n'
|
||||
if request['initialVersion'].lower() == 'unreleased':
|
||||
ret += '- Unreleased\n'
|
||||
else:
|
||||
ret += '- Added in v{}\n'.format(request['initialVersion'])
|
||||
|
||||
if request['requestFields']:
|
||||
ret += requestFieldHeader
|
||||
for requestField in request['requestFields']:
|
||||
valueType = requestField['valueType'].replace('<', "<").replace('>', ">")
|
||||
valueRestrictions = requestField['valueRestrictions'] if requestField['valueRestrictions'] else 'None'
|
||||
valueOptional = '?' if requestField['valueOptional'] else ''
|
||||
valueOptionalBehavior = requestField['valueOptionalBehavior'] if requestField['valueOptional'] and requestField['valueOptionalBehavior'] else 'N/A'
|
||||
ret += '| {}{} | {} | {} | {} | {} |\n'.format(valueOptional, requestField['valueName'], valueType, requestField['valueDescription'], valueRestrictions, valueOptionalBehavior)
|
||||
|
||||
if request['responseFields']:
|
||||
ret += responseFieldHeader
|
||||
for responseField in request['responseFields']:
|
||||
valueType = responseField['valueType'].replace('<', "<").replace('>', ">")
|
||||
ret += '| {} | {} | {} |\n'.format(responseField['valueName'], valueType, responseField['valueDescription'])
|
||||
|
||||
if request != requestsOut[-1]:
|
||||
ret += '\n---\n\n'
|
||||
return ret
|
||||
|
||||
def get_events_toc(events):
|
||||
ret = ''
|
||||
for category in categoryOrder:
|
||||
eventsOut = []
|
||||
for event in events:
|
||||
if event['category'] != category.lower():
|
||||
continue
|
||||
eventsOut.append(event)
|
||||
if not len(eventsOut):
|
||||
continue
|
||||
categoryFragment = get_fragment(category, False)
|
||||
ret += '- [{}](#{})\n'.format(category, categoryFragment)
|
||||
for event in eventsOut:
|
||||
eventType = event['eventType']
|
||||
eventTypeFragment = get_fragment(eventType, False)
|
||||
ret += ' - [{}](#{})\n'.format(eventType, eventTypeFragment)
|
||||
return ret
|
||||
|
||||
def get_events(events):
|
||||
ret = ''
|
||||
for category in categoryOrder:
|
||||
eventsOut = []
|
||||
for event in events:
|
||||
if event['category'] != category.lower():
|
||||
continue
|
||||
eventsOut.append(event)
|
||||
if not len(eventsOut):
|
||||
continue
|
||||
categoryFragment = get_fragment(category)
|
||||
ret += '## {}\n\n'.format(category)
|
||||
for event in eventsOut:
|
||||
eventType = event['eventType']
|
||||
eventTypeFragment = get_fragment(eventType)
|
||||
ret += '### {}\n\n'.format(eventType)
|
||||
ret += '{}\n\n'.format(event['description'])
|
||||
ret += '- Complexity Rating: `{}/5`\n'.format(event['complexity'])
|
||||
ret += '- Latest Supported RPC Version: `{}`\n'.format(event['rpcVersion'])
|
||||
if event['deprecated']:
|
||||
ret += '- **⚠️ Deprecated. ⚠️**\n'
|
||||
if event['initialVersion'].lower() == 'unreleased':
|
||||
ret += '- Unreleased\n'
|
||||
else:
|
||||
ret += '- Added in v{}\n'.format(event['initialVersion'])
|
||||
|
||||
if event['dataFields']:
|
||||
ret += dataFieldHeader
|
||||
for dataField in event['dataFields']:
|
||||
valueType = dataField['valueType'].replace('<', "<").replace('>', ">")
|
||||
ret += '| {} | {} | {} |\n'.format(dataField['valueName'], valueType, dataField['valueDescription'])
|
||||
|
||||
if event != eventsOut[-1]:
|
||||
ret += '\n---\n\n'
|
||||
return ret
|
||||
|
||||
# Actual code
|
||||
#######################################################################################################################
|
||||
|
||||
# Read versions json
|
||||
try:
|
||||
with open('../versions.json', 'r') as f:
|
||||
versions = json.load(f)
|
||||
except IOError:
|
||||
logging.error('Failed to get global versions. Versions file not configured?')
|
||||
os.exit(1)
|
||||
|
||||
# Read protocol json
|
||||
with open('../generated/protocol.json', 'r') as f:
|
||||
protocol = json.load(f)
|
||||
|
||||
output = "<!-- This file was automatically generated. Do not edit directly! -->\n\n"
|
||||
|
||||
# Insert introduction partial
|
||||
output += read_file('partials/introduction.md')
|
||||
logging.info('Inserted introduction section.')
|
||||
|
||||
output += '\n\n'
|
||||
|
||||
# Generate enums MD
|
||||
output += read_file('partials/enumsHeader.md')
|
||||
output += get_enums_toc(protocol['enums'])
|
||||
output += '\n\n'
|
||||
output += get_enums(protocol['enums'])
|
||||
logging.info('Inserted enums section.')
|
||||
|
||||
output += '\n\n'
|
||||
|
||||
# Generate events MD
|
||||
output += read_file('partials/eventsHeader.md')
|
||||
output += get_events_toc(protocol['events'])
|
||||
output += '\n\n'
|
||||
output += get_events(protocol['events'])
|
||||
logging.info('Inserted events section.')
|
||||
|
||||
output += '\n\n'
|
||||
|
||||
# Generate requests MD
|
||||
output += read_file('partials/requestsHeader.md')
|
||||
output += get_requests_toc(protocol['requests'])
|
||||
output += '\n\n'
|
||||
output += get_requests(protocol['requests'])
|
||||
logging.info('Inserted requests section.')
|
||||
|
||||
output += '\n\n'
|
||||
|
||||
# Write new protocol MD
|
||||
with open('../generated/protocol.md', 'w') as f:
|
||||
f.write(output)
|
||||
|
||||
logging.info('Finished generating protocol.md.')
|
@ -1,2 +1,4 @@
|
||||
# Enums
|
||||
These are enumeration declarations, which are referenced throughout obs-websocket's protocol.
|
||||
|
||||
### Enumerations Table of Contents
|
3
docs/docs/partials/eventsHeader.md
Normal file
3
docs/docs/partials/eventsHeader.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Events
|
||||
|
||||
### Events Table of Contents
|
@ -1,38 +1,37 @@
|
||||
# obs-websocket 5.0.0 protocol reference
|
||||
# Main Table of Contents
|
||||
- [obs-websocket 5.0.0 Protocol](#obs-websocket-500-protocol)
|
||||
- [Connecting to obs-websocket](#connecting-to-obs-websocket)
|
||||
- [Connection steps](#connection-steps)
|
||||
- [Creating an authentication string](#creating-an-authentication-string)
|
||||
- [Base message types](#message-types)
|
||||
- [OpCode 0 Hello](#hello-opcode-0)
|
||||
- [OpCode 1 Identify](#identify-opcode-1)
|
||||
- [OpCode 2 Identified](#identified-opcode-2)
|
||||
- [OpCode 3 Reidentify](#reidentify-opcode-3)
|
||||
- [OpCode 5 Event](#event-opcode-5)
|
||||
- [OpCode 6 Request](#request-opcode-6)
|
||||
- [OpCode 7 RequestResponse](#requestresponse-opcode-7)
|
||||
- [OpCode 8 RequestBatch](#requestbatch-opcode-8)
|
||||
- [OpCode 9 RequestBatchResponse](#requestbatchresponse-opcode-9)
|
||||
- [Enums](#enums)
|
||||
- [Events](#events)
|
||||
- [Requests](#requests)
|
||||
|
||||
# obs-websocket 5.0.0 Protocol
|
||||
|
||||
## General Introduction
|
||||
## General Intro
|
||||
obs-websocket provides a feature-rich RPC communication protocol, giving access to much of OBS's feature set. This document contains everything you should know in order to make a connection and use obs-websocket's functionality to the fullest.
|
||||
|
||||
### Design Goals
|
||||
- Abstraction of identification, events, requests, and batch requests into dedicated message types
|
||||
- Conformity of request naming using similar terms like `Get`, `Set`, `Get[x]List`, `Start[x]`, `Toggle[x]`
|
||||
- Conformity of OBS data key names like `sourceName`, `sourceKind`, `sourceType`, `sceneName`, `sceneItemName`
|
||||
- Conformity of OBS data field names like `sourceName`, `sourceKind`, `sourceType`, `sceneName`, `sceneItemName`
|
||||
- Error code response system - integer corrosponds to type of error, with optional comment
|
||||
- Possible support for multiple message encoding options: JSON and MessagePack
|
||||
- PubSub system - Allow clients to specify which events they do or don't want to receive from OBS
|
||||
- RPC versioning - Client and server negotiate the latest version of the obs-websocket protocol to communicate with.
|
||||
|
||||
|
||||
## Table of Contents
|
||||
- [Connecting to obs-websocket](#connecting-to-obs-websocket)
|
||||
- [Connection steps](#connection-steps)
|
||||
- [Creating an authentication string](#creating-an-authentication-string)
|
||||
- [Enumerations](#enumerations)
|
||||
- [Base message types](#message-types)
|
||||
- [OpCode 0 Hello](#hello-opcode-0)
|
||||
- [OpCode 1 Identify](#identify-opcode-1)
|
||||
- [OpCode 2 Identified](#identified-opcode-2)
|
||||
- [OpCode 3 Reidentify](#reidentify-opcode-3)
|
||||
- [OpCode 5 Event](#event-opcode-5)
|
||||
- [OpCode 6 Request](#request-opcode-6)
|
||||
- [OpCode 7 RequestResponse](#requestresponse-opcode-7)
|
||||
- [OpCode 8 RequestBatch](#requestbatch-opcode-8)
|
||||
- [OpCode 9 RequestBatchResponse](#requestbatchresponse-opcode-9)
|
||||
- [Events](#events)
|
||||
- [Requests](#requests)
|
||||
|
||||
|
||||
## Connecting to obs-websocket
|
||||
Here's info on how to connect to obs-websocket
|
||||
|
||||
@ -49,13 +48,13 @@ These steps should be followed precisely. Failure to connect to the server as in
|
||||
- Once the connection is upgraded, the websocket server will immediately send an [OpCode 0 `Hello`](#hello-opcode-0) message to the client.
|
||||
|
||||
- The client listens for the `Hello` and responds with an [OpCode 1 `Identify`](#identify-opcode-1) containing all appropriate session parameters.
|
||||
- If there is an `authentication` key in the `messageData` object, the server requires authentication, and the steps in [Creating an authentication string](#creating-an-authentication-string) should be followed.
|
||||
- If there is no `authentication` key, the resulting `Identify` object sent to the server does not require an `authentication` string.
|
||||
- If there is an `authentication` field in the `messageData` object, the server requires authentication, and the steps in [Creating an authentication string](#creating-an-authentication-string) should be followed.
|
||||
- If there is no `authentication` field, the resulting `Identify` object sent to the server does not require an `authentication` string.
|
||||
- The client determines if the server's `rpcVersion` is supported, and if not it provides its closest supported version in `Identify`.
|
||||
|
||||
- The server receives and processes the `Identify` sent by the client.
|
||||
- If authentication is required and the `Identify` message data does not contain an `authentication` string, or the string is not correct, the connection is closed with [`WebSocketCloseCode::AuthenticationFailed`](#websocketclosecode-enum)
|
||||
- If the client has requested an `rpcVersion` which the server cannot use, the connection is closed with [`WebSocketCloseCode::UnsupportedRpcVersion`](#websocketclosecode-enum). This system allows both the server and client to have seamless backwards compatability.
|
||||
- If authentication is required and the `Identify` message data does not contain an `authentication` string, or the string is not correct, the connection is closed with `WebSocketCloseCode::AuthenticationFailed`
|
||||
- If the client has requested an `rpcVersion` which the server cannot use, the connection is closed with `WebSocketCloseCode::UnsupportedRpcVersion`. This system allows both the server and client to have seamless backwards compatability.
|
||||
- If any other parameters are malformed (invalid type, etc), the connection is closed with an appropriate close code.
|
||||
|
||||
- Once identification is processed on the server, the server responds to the client with an [OpCode 2 `Identified`](#identified-opcode-2).
|
||||
@ -65,15 +64,15 @@ These steps should be followed precisely. Failure to connect to the server as in
|
||||
- At any time after a client has been identified, it may send an [OpCode 3 `Reidentify`](#reidentify-opcode-3) message to update certain allowed session parameters. The server will respond in the same way it does during initial identification.
|
||||
|
||||
#### Connection Notes
|
||||
- If a binary frame is received when using the `obswebsocket.json` (default) subprotocol, or a text frame is received while using the `obswebsocket.msgpack` subprotocol, the connection is closed with [`WebSocketCloseCode::MessageDecodeError`](#websocketclosecode-enum).
|
||||
- The obs-websocket server listens for any messages containing a `request-type` key in the first level JSON from unidentified clients. If a message matches, the connection is closed with [`WebSocketCloseCode::UnsupportedRpcVersion`](#websocketclosecode-enum) and a warning is logged.
|
||||
- If a message with a `messageType` is not recognized to the obs-websocket server, the connection is closed with [`WebSocketCloseCode::UnknownOpCode`](#websocketclosecode-enum).
|
||||
- At no point may the client send any message other than a single `Identify` before it has received an `Identified`. Doing so will result in the connection being closed with [`WebSocketCloseCode::NotIdentified`](#websocketclosecode-enum).
|
||||
- If a binary frame is received when using the `obswebsocket.json` (default) subprotocol, or a text frame is received while using the `obswebsocket.msgpack` subprotocol, the connection is closed with `WebSocketCloseCode::MessageDecodeError`.
|
||||
- The obs-websocket server listens for any messages containing a `request-type` field in the first level JSON from unidentified clients. If a message matches, the connection is closed with `WebSocketCloseCode::UnsupportedRpcVersion` and a warning is logged.
|
||||
- If a message with a `messageType` is not recognized to the obs-websocket server, the connection is closed with `WebSocketCloseCode::UnknownOpCode`.
|
||||
- At no point may the client send any message other than a single `Identify` before it has received an `Identified`. Doing so will result in the connection being closed with `WebSocketCloseCode::NotIdentified`.
|
||||
|
||||
---
|
||||
|
||||
### Creating an authentication string
|
||||
obs-websocket uses SHA256 to transmit authentication credentials. The server starts by sending an object in the `authentication` key of its `Hello` message data. The client processes the authentication challenge and responds via the `authentication` string in the `Identify` message data.
|
||||
obs-websocket uses SHA256 to transmit authentication credentials. The server starts by sending an object in the `authentication` field of its `Hello` message data. The client processes the authentication challenge and responds via the `authentication` string in the `Identify` message data.
|
||||
|
||||
For this guide, we'll be using `supersecretpassword` as the password.
|
||||
|
||||
@ -93,172 +92,19 @@ To generate the authentication string, follow these steps:
|
||||
|
||||
For real-world examples of the `authentication` string creation, refer to the obs-websocket client libraries listed on the [README](README.md).
|
||||
|
||||
---
|
||||
|
||||
### Enumerations
|
||||
These are the enumeration definitions for various codes used by obs-websocket.
|
||||
|
||||
#### WebSocketOpCode Enum
|
||||
```cpp
|
||||
enum WebSocketOpCode {
|
||||
Hello = 0,
|
||||
Identify = 1,
|
||||
Identified = 2,
|
||||
Reidentify = 3,
|
||||
Event = 5,
|
||||
Request = 6,
|
||||
RequestResponse = 7,
|
||||
RequestBatch = 8,
|
||||
RequestBatchResponse = 9,
|
||||
};
|
||||
```
|
||||
|
||||
#### WebSocketCloseCode Enum
|
||||
```cpp
|
||||
enum WebSocketCloseCode {
|
||||
// Internal only
|
||||
DontClose = 0,
|
||||
// Reserved
|
||||
UnknownReason = 4000,
|
||||
// The server was unable to decode the incoming websocket message
|
||||
MessageDecodeError = 4002,
|
||||
// A data key is missing but required
|
||||
MissingDataKey = 4003,
|
||||
// A data key has an invalid type
|
||||
InvalidDataKeyType = 4004,
|
||||
// The specified `op` was invalid or missing
|
||||
UnknownOpCode = 4005,
|
||||
// The client sent a websocket message without first sending `Identify` message
|
||||
NotIdentified = 4006,
|
||||
// The client sent an `Identify` message while already identified
|
||||
AlreadyIdentified = 4007,
|
||||
// The authentication attempt (via `Identify`) failed
|
||||
AuthenticationFailed = 4008,
|
||||
// The server detected the usage of an old version of the obs-websocket protocol.
|
||||
UnsupportedRpcVersion = 4009,
|
||||
// The websocket session has been invalidated by the obs-websocket server.
|
||||
SessionInvalidated = 4010,
|
||||
};
|
||||
```
|
||||
|
||||
#### EventSubscriptions Enum
|
||||
```cpp
|
||||
enum EventSubscription {
|
||||
// Set subscriptions to 0 to disable all events
|
||||
None = 0,
|
||||
// Receive events in the `General` category
|
||||
General = (1 << 0),
|
||||
// Receive events in the `Config` category
|
||||
Config = (1 << 1),
|
||||
// Receive events in the `Scenes` category
|
||||
Scenes = (1 << 2),
|
||||
// Receive events in the `Inputs` category
|
||||
Inputs = (1 << 3),
|
||||
// Receive events in the `Transitions` category
|
||||
Transitions = (1 << 4),
|
||||
// Receive events in the `Filters` category
|
||||
Filters = (1 << 5),
|
||||
// Receive events in the `Outputs` category
|
||||
Outputs = (1 << 6),
|
||||
// Receive events in the `Scene Items` category
|
||||
SceneItems = (1 << 7),
|
||||
// Receive events in the `MediaInputs` category
|
||||
MediaInputs = (1 << 8),
|
||||
// Receive all event categories
|
||||
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs),
|
||||
// InputVolumeMeters event (high-volume)
|
||||
InputVolumeMeters = (1 << 9),
|
||||
// InputActiveStateChanged event (high-volume)
|
||||
InputActiveStateChanged = (1 << 10),
|
||||
// InputShowStateChanged event (high-volume)
|
||||
InputShowStateChanged = (1 << 11),
|
||||
};
|
||||
```
|
||||
Subscriptions are a bitmask system. In many languages, to generate a bitmask that subscribes to `General` and `Scenes`, you would do: `subscriptions = ((1 << 0) | (1 << 2))`
|
||||
|
||||
#### RequestStatus Enum
|
||||
```cpp
|
||||
enum RequestStatus {
|
||||
Unknown = 0,
|
||||
|
||||
// For internal use to signify a successful parameter check
|
||||
NoError = 10,
|
||||
|
||||
Success = 100,
|
||||
|
||||
// The `requestType` field is missing from the request data
|
||||
MissingRequestType = 203,
|
||||
// The request type is invalid or does not exist
|
||||
UnknownRequestType = 204,
|
||||
// Generic error code (comment required)
|
||||
GenericError = 205,
|
||||
|
||||
// A required request parameter is missing
|
||||
MissingRequestParameter = 300,
|
||||
// The request does not have a valid requestData object.
|
||||
MissingRequestData = 301,
|
||||
|
||||
// Generic invalid request parameter message (comment required)
|
||||
InvalidRequestParameter = 400,
|
||||
// A request parameter has the wrong data type
|
||||
InvalidRequestParameterType = 401,
|
||||
// A request parameter (float or int) is out of valid range
|
||||
RequestParameterOutOfRange = 402,
|
||||
// A request parameter (string or array) is empty and cannot be
|
||||
RequestParameterEmpty = 403,
|
||||
// There are too many request parameters (eg. a request takes two optionals, where only one is allowed at a time)
|
||||
TooManyRequestParameters = 404,
|
||||
|
||||
// An output is running and cannot be in order to perform the request (generic)
|
||||
OutputRunning = 500,
|
||||
// An output is not running and should be
|
||||
OutputNotRunning = 501,
|
||||
// An output is paused and should not be
|
||||
OutputPaused = 502,
|
||||
// An output is disabled and should not be
|
||||
OutputDisabled = 503,
|
||||
// Studio mode is active and cannot be
|
||||
StudioModeActive = 504,
|
||||
// Studio mode is not active and should be
|
||||
StudioModeNotActive = 505,
|
||||
|
||||
// The resource was not found
|
||||
ResourceNotFound = 600,
|
||||
// The resource already exists
|
||||
ResourceAlreadyExists = 601,
|
||||
// The type of resource found is invalid
|
||||
InvalidResourceType = 602,
|
||||
// There are not enough instances of the resource in order to perform the request
|
||||
NotEnoughResources = 603,
|
||||
// The state of the resource is invalid. For example, if the resource is blocked from being accessed
|
||||
InvalidResourceState = 604,
|
||||
// The specified input (obs_source_t-OBS_SOURCE_TYPE_INPUT) had the wrong kind
|
||||
InvalidInputKind = 605,
|
||||
|
||||
// Creating the resource failed
|
||||
ResourceCreationFailed = 700,
|
||||
// Performing an action on the resource failed
|
||||
ResourceActionFailed = 701,
|
||||
// Processing the request failed unexpectedly (comment required)
|
||||
RequestProcessingFailed = 702,
|
||||
// The combination of request parameters cannot be used to perform an action
|
||||
CannotAct = 703,
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
## Message Types (OpCodes)
|
||||
The following message types are the low-level message types which may be sent to and from obs-websocket.
|
||||
|
||||
Messages sent from the obs-websocket server or client may contain these first-level keys, known as the base object:
|
||||
Messages sent from the obs-websocket server or client may contain these first-level fields, known as the base object:
|
||||
```
|
||||
{
|
||||
"op": number,
|
||||
"d": object
|
||||
}
|
||||
```
|
||||
- `op` is a [`WebSocketOpCode` OpCode.](#websocketopcode-enum)
|
||||
- `d` is an object of the data keys associated with the operation.
|
||||
- `op` is a `WebSocketOpCode` OpCode.
|
||||
- `d` is an object of the data fields associated with the operation.
|
||||
|
||||
---
|
||||
|
||||
@ -316,15 +162,11 @@ Authentication is not required
|
||||
{
|
||||
"rpcVersion": number,
|
||||
"authentication": string(optional),
|
||||
"ignoreInvalidMessages": bool(optional) = false,
|
||||
"ignoreNonFatalRequestChecks": bool(optional) = false,
|
||||
"eventSubscriptions": number(optional) = (EventSubscription::All)
|
||||
}
|
||||
```
|
||||
- `rpcVersion` is the version number that the client would like the obs-websocket server to use.
|
||||
- When `ignoreInvalidMessages` is true, the socket will not be closed for [`WebSocketCloseCode`](#websocketclosecode-enum): `MessageDecodeError`, `UnknownOpCode`, or `MissingDataKey`. Instead, the message will be logged and ignored.
|
||||
- When `ignoreNonFatalRequestChecks` is true, requests will ignore checks which are not critical to the function of the request. Eg calling `DeleteScene` when the target scene does not exist would still return [`RequestStatus::Success`](#requeststatus-enum) if this flag is enabled.
|
||||
- `eventSubscriptions` is a bitmask of [`EventSubscriptions`](#eventsubscriptions-enum) items to subscribe to events and event categories at will. By default, all event categories are subscribed, except for events marked as high volume. High volume events must be explicitly subscribed to.
|
||||
- `eventSubscriptions` is a bitmask of `EventSubscriptions` items to subscribe to events and event categories at will. By default, all event categories are subscribed, except for events marked as high volume. High volume events must be explicitly subscribed to.
|
||||
|
||||
**Example Message:**
|
||||
```json
|
||||
@ -373,8 +215,6 @@ Authentication is not required
|
||||
**Data Keys:**
|
||||
```
|
||||
{
|
||||
"ignoreInvalidMessages": bool(optional) = false,
|
||||
"ignoreNonFatalRequestChecks": bool(optional) = false,
|
||||
"eventSubscriptions": number(optional) = (EventSubscription::All)
|
||||
}
|
||||
```
|
||||
@ -400,7 +240,7 @@ Authentication is not required
|
||||
**Example Message:**
|
||||
```json
|
||||
{
|
||||
"op": 2,
|
||||
"op": 5,
|
||||
"d": {
|
||||
"eventType": "StudioModeStateChanged",
|
||||
"eventIntent": 1,
|
||||
@ -468,8 +308,8 @@ Authentication is not required
|
||||
"comment": string(optional)
|
||||
}
|
||||
```
|
||||
- `result` is `true` if the request resulted in [`RequestStatus::Success`](#requeststatus-enum). False if otherwise.
|
||||
- `code` is a [`RequestStatus`](#requeststatus-enum) code.
|
||||
- `result` is `true` if the request resulted in `RequestStatus::Success`. False if otherwise.
|
||||
- `code` is a `RequestStatus` code.
|
||||
- `comment` may be provided by the server on errors to offer further details on why a request failed.
|
||||
|
||||
**Example Messages:**
|
||||
@ -516,11 +356,12 @@ Failure Response
|
||||
{
|
||||
"requestId": string,
|
||||
"haltOnFailure": bool(optional) = false,
|
||||
"executionType": number(optional) = RequestBatchExecutionType::SerialRealtime
|
||||
"requests": array<object>
|
||||
}
|
||||
```
|
||||
- When `haltOnFailure` is `true`, the processing of requests will be halted on first failure. Returns only the processed requests in [`RequestBatchResponse`](#requestbatchresponse-opcode-9).
|
||||
- Requests in the `requests` array follow the same structure as the `Request` payload data format, however `requestId` is an optional key.
|
||||
- Requests in the `requests` array follow the same structure as the `Request` payload data format, however `requestId` is an optional field.
|
||||
|
||||
---
|
||||
|
3
docs/docs/partials/requestsHeader.md
Normal file
3
docs/docs/partials/requestsHeader.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Requests
|
||||
|
||||
### Requests Table of Contents
|
208
docs/docs/process_comments.py
Normal file
208
docs/docs/process_comments.py
Normal file
@ -0,0 +1,208 @@
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [process_comments.py] [%(levelname)s] %(message)s")
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# The comments parser will return a string type instead of an array if there is only one field
|
||||
def field_to_array(field):
|
||||
if type(field) == str:
|
||||
return [field]
|
||||
return field
|
||||
|
||||
# This raw JSON can be really damn unpredictable. Let's handle that
|
||||
def field_to_string(field):
|
||||
if type(field) == list:
|
||||
return field_to_string(field[0])
|
||||
elif type(field) == dict:
|
||||
return field_to_string(field['description'])
|
||||
return str(field)
|
||||
|
||||
# Make sure that everything we expect is there
|
||||
def validate_fields(data, fields):
|
||||
for field in fields:
|
||||
if field not in data:
|
||||
logging.warning('Missing required item: {}'.format(field))
|
||||
return False
|
||||
return True
|
||||
|
||||
# Get the individual components of a `requestField` or `responseField` or `dataField` entry
|
||||
def get_components(data):
|
||||
ret = []
|
||||
components_raw = data.split('|')
|
||||
for component in components_raw:
|
||||
ret.append(component.strip())
|
||||
return ret
|
||||
|
||||
# Convert all request fields from raw to final
|
||||
def get_request_fields(fields):
|
||||
fields = field_to_array(fields)
|
||||
ret = []
|
||||
for field in fields:
|
||||
components = get_components(field)
|
||||
field_out = {}
|
||||
field_out['valueName'] = components[0].replace('?', '')
|
||||
field_out['valueType'] = components[1]
|
||||
field_out['valueDescription'] = components[2]
|
||||
|
||||
valueOptionalOffset = 3
|
||||
# If value type is a number, restrictions are required. Else, should not be added.
|
||||
if field_out['valueType'].lower() == 'number':
|
||||
# In the case of a number, the optional component gets pushed back.
|
||||
valueOptionalOffset += 1
|
||||
field_out['valueRestrictions'] = components[3] if components[3].lower() != 'none' else None
|
||||
else:
|
||||
field_out['valueRestrictions'] = None
|
||||
|
||||
field_out['valueOptional'] = components[0].startswith('?')
|
||||
if field_out['valueOptional']:
|
||||
field_out['valueOptionalBehavior'] = components[valueOptionalOffset] if len(components) > valueOptionalOffset else 'Unknown'
|
||||
else:
|
||||
field_out['valueOptionalBehavior'] = None
|
||||
ret.append(field_out)
|
||||
return ret
|
||||
|
||||
# Convert all response (or event data) fields from raw to final
|
||||
def get_response_fields(fields):
|
||||
fields = field_to_array(fields)
|
||||
ret = []
|
||||
for field in fields:
|
||||
components = get_components(field)
|
||||
field_out = {}
|
||||
field_out['valueName'] = components[0]
|
||||
field_out['valueType'] = components[1]
|
||||
field_out['valueDescription'] = components[2]
|
||||
ret.append(field_out)
|
||||
return ret
|
||||
|
||||
#######################################################################################################################
|
||||
|
||||
# Read versions json
|
||||
try:
|
||||
with open('../versions.json', 'r') as f:
|
||||
versions = json.load(f)
|
||||
except IOError:
|
||||
logging.error('Failed to get global versions. Versions file not configured?')
|
||||
os.exit(1)
|
||||
|
||||
# Read the raw comments output file
|
||||
with open('../work/comments.json', 'r') as f:
|
||||
comments_raw = json.load(f)
|
||||
|
||||
# Prepare output variables
|
||||
enums = []
|
||||
requests = []
|
||||
events = []
|
||||
|
||||
enums_raw = {}
|
||||
# Process the raw comments
|
||||
for comment in comments_raw:
|
||||
# Skip unrelated comments like #include
|
||||
if 'api' not in comment:
|
||||
continue
|
||||
|
||||
api = comment['api']
|
||||
if api == 'enums':
|
||||
if not validate_fields(comment, ['description', 'enumIdentifier', 'enumType', 'rpcVersion', 'initialVersion']):
|
||||
logging.warning('Failed to process enum id comment due to missing field(s):\n{}'.format(comment))
|
||||
continue
|
||||
|
||||
enumType = field_to_string(comment['enumType'])
|
||||
|
||||
enum = {}
|
||||
# Recombines the header back into one string, allowing multi-line descriptions.
|
||||
enum['description'] = field_to_string(comment.get('lead', '')) + field_to_string(comment['description'])
|
||||
enum['enumIdentifier'] = field_to_string(comment['enumIdentifier'])
|
||||
rpcVersionRaw = field_to_string(comment['rpcVersion'])
|
||||
enum['rpcVersion'] = versions['rpcVersion'] if rpcVersionRaw == '-1' else int(rpcVersionRaw)
|
||||
enum['deprecated'] = False if rpcVersionRaw == '-1' else True
|
||||
enum['initialVersion'] = field_to_string(comment['initialVersion'])
|
||||
|
||||
if 'enumValue' in comment:
|
||||
enumValue = field_to_string(comment['enumValue'])
|
||||
enum['enumValue'] = int(enumValue) if enumValue.isdigit() else enumValue
|
||||
else:
|
||||
enum['enumValue'] = enum['enumIdentifier']
|
||||
|
||||
if enumType not in enums_raw:
|
||||
enums_raw[enumType] = {'enumIdentifiers': [enum]}
|
||||
else:
|
||||
enums_raw[enumType]['enumIdentifiers'].append(enum)
|
||||
|
||||
logging.info('Processed enum: {}::{}'.format(enumType, enum['enumIdentifier']))
|
||||
elif api == 'requests':
|
||||
if not validate_fields(comment, ['description', 'requestType', 'complexity', 'rpcVersion', 'initialVersion', 'category']):
|
||||
logging.warning('Failed to process request comment due to missing field(s):\n{}'.format(comment))
|
||||
continue
|
||||
|
||||
req = {}
|
||||
req['description'] = field_to_string(comment.get('lead', '')) + field_to_string(comment['description'])
|
||||
req['requestType'] = field_to_string(comment['requestType'])
|
||||
req['complexity'] = int(field_to_string(comment['complexity']))
|
||||
rpcVersionRaw = field_to_string(comment['rpcVersion'])
|
||||
req['rpcVersion'] = versions['rpcVersion'] if rpcVersionRaw == '-1' else int(rpcVersionRaw)
|
||||
req['deprecated'] = False if rpcVersionRaw == '-1' else True
|
||||
req['initialVersion'] = field_to_string(comment['initialVersion'])
|
||||
req['category'] = field_to_string(comment['category'])
|
||||
|
||||
try:
|
||||
if 'requestField' in comment:
|
||||
req['requestFields'] = get_request_fields(comment['requestField'])
|
||||
else:
|
||||
req['requestFields'] = []
|
||||
except:
|
||||
logging.exception('Failed to process request `{}` request fields due to error:\n'.format(req['requestType']))
|
||||
continue
|
||||
|
||||
try:
|
||||
if 'responseField' in comment:
|
||||
req['responseFields'] = get_response_fields(comment['responseField'])
|
||||
else:
|
||||
req['responseFields'] = []
|
||||
except:
|
||||
logging.exception('Failed to process request `{}` request fields due to error:\n'.format(req['requestType']))
|
||||
continue
|
||||
|
||||
logging.info('Processed request: {}'.format(req['requestType']))
|
||||
|
||||
requests.append(req)
|
||||
elif api == 'events':
|
||||
if not validate_fields(comment, ['description', 'eventType', 'eventSubscription', 'complexity', 'rpcVersion', 'initialVersion', 'category']):
|
||||
logging.warning('Failed to process event comment due to missing field(s):\n{}'.format(comment))
|
||||
continue
|
||||
|
||||
eve = {}
|
||||
eve['description'] = field_to_string(comment.get('lead', '')) + field_to_string(comment['description'])
|
||||
eve['eventType'] = field_to_string(comment['eventType'])
|
||||
eve['eventSubscription'] = field_to_string(comment['eventSubscription'])
|
||||
eve['complexity'] = int(field_to_string(comment['complexity']))
|
||||
rpcVersionRaw = field_to_string(comment['rpcVersion'])
|
||||
eve['rpcVersion'] = versions['rpcVersion'] if rpcVersionRaw == '-1' else int(rpcVersionRaw)
|
||||
eve['deprecated'] = False if rpcVersionRaw == '-1' else True
|
||||
eve['initialVersion'] = field_to_string(comment['initialVersion'])
|
||||
eve['category'] = field_to_string(comment['category'])
|
||||
|
||||
try:
|
||||
if 'dataField' in comment:
|
||||
eve['dataFields'] = get_response_fields(comment['dataField'])
|
||||
else:
|
||||
eve['dataFields'] = []
|
||||
except:
|
||||
logging.exception('Failed to process event `{}` data fields due to error:\n'.format(req['eventType']))
|
||||
continue
|
||||
|
||||
logging.info('Processed event: {}'.format(eve['eventType']))
|
||||
|
||||
events.append(eve)
|
||||
else:
|
||||
logging.warning('Comment with unknown api: {}'.format(api))
|
||||
|
||||
# Reconfigure enums to match the correct structure
|
||||
for enumType in enums_raw.keys():
|
||||
enum = enums_raw[enumType]
|
||||
enums.append({'enumType': enumType, 'enumIdentifiers': enum['enumIdentifiers']})
|
||||
|
||||
finalObject = {'enums': enums, 'requests': requests, 'events': events}
|
||||
|
||||
with open('../generated/protocol.json', 'w') as f:
|
||||
json.dump(finalObject, f, indent=2)
|
@ -1 +0,0 @@
|
||||
{}
|
4173
docs/generated/protocol.json
Normal file
4173
docs/generated/protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "obs-websocket-docs",
|
||||
"version": "1.1.0",
|
||||
"description": "",
|
||||
"main": "docs.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"docs": "node ./docs.js",
|
||||
"comments": "node ./comments.js",
|
||||
"build": "npm run comments && npm run docs"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.2",
|
||||
"handlebars": "^4.0.10",
|
||||
"handlebars-helpers": "^0.9.6",
|
||||
"markdown-toc": "^1.1.0",
|
||||
"parse-comments": "^0.4.3"
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
## Events
|
@ -1 +0,0 @@
|
||||
## Requests
|
@ -1,98 +0,0 @@
|
||||
{{#read "partials/introduction.md"}}{{/read}}
|
||||
|
||||
|
||||
|
||||
## Requests/Events Table of Contents
|
||||
<!-- toc -->
|
||||
|
||||
|
||||
{{#read "partials/eventsHeader.md"}}{{/read}}
|
||||
|
||||
{{#each events}}
|
||||
## {{capitalizeAll @key}}
|
||||
|
||||
{{#each this}}
|
||||
### {{name}}
|
||||
|
||||
{{#if deprecated}}
|
||||
- **⚠️ Deprecated. Last seen in RPC v{{deprecated}} ⚠️**
|
||||
{{/if}}
|
||||
|
||||
{{#eq since "unreleased"}}
|
||||
- Unreleased
|
||||
{{else}}
|
||||
- Added in v{{since}}
|
||||
{{/eq}}
|
||||
|
||||
{{{description}}}
|
||||
|
||||
**Response Items:**
|
||||
|
||||
{{#if returns.length}}
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
{{#each returns}}
|
||||
| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
|
||||
{{/each}}
|
||||
|
||||
{{else}}
|
||||
_No additional response items._
|
||||
{{/if}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
|
||||
|
||||
{{#read "partials/requestsHeader.md"}}{{/read}}
|
||||
|
||||
{{#each requests}}
|
||||
## {{capitalizeAll @key}}
|
||||
|
||||
{{#each this}}
|
||||
### {{name}}
|
||||
|
||||
{{#if deprecated}}
|
||||
- **⚠️ Deprecated. Last seen in RPC v{{deprecated}} ⚠️**
|
||||
{{/if}}
|
||||
|
||||
{{#eq since "unreleased"}}
|
||||
- Unreleased
|
||||
{{else}}
|
||||
- Added in v{{since}}
|
||||
{{/eq}}
|
||||
|
||||
{{{description}}}
|
||||
|
||||
**Request Fields:**
|
||||
|
||||
{{#if params.length}}
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
{{#each params}}
|
||||
| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
|
||||
{{/each}}
|
||||
|
||||
{{else}}
|
||||
_No specified parameters._
|
||||
{{/if}}
|
||||
|
||||
**Response Items:**
|
||||
|
||||
{{#if returns.length}}
|
||||
| Name | Type | Description |
|
||||
| ---- | :---: | ------------|
|
||||
{{#each returns}}
|
||||
| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
|
||||
{{/each}}
|
||||
|
||||
{{else}}
|
||||
_No additional response items._
|
||||
{{/if}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
{{/each}}
|
5
docs/versions.json
Normal file
5
docs/versions.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"obsWebSocketProjectVersion": "5.0.0",
|
||||
"obsWebSocketVersion": "5.0.0-alpha2",
|
||||
"rpcVersion": "1"
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
#define MyAppName "obs-websocket"
|
||||
#define MyAppVersion "@OBS_WEBSOCKET_VERSION@"
|
||||
#define MyAppPublisher "Stephane Lepin"
|
||||
#define MyAppURL "http://github.com/Palakis/obs-websocket"
|
||||
#define MyAppPublisher "obs-websocket"
|
||||
#define MyAppURL "http://github.com/obsproject/obs-websocket"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application.
|
||||
@ -48,6 +48,17 @@ begin
|
||||
);
|
||||
end;
|
||||
|
||||
// Validate that obs-studio is installed before installing the plugin
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
begin
|
||||
Result := '';
|
||||
|
||||
if not DirExists(ExpandConstant('{app}\obs-plugins')) then
|
||||
begin
|
||||
Result := 'The selected install directory does not appear to be valid. Please install OBS Studio before installing {#MyAppName} or correct your install path.';
|
||||
end;
|
||||
end;
|
||||
|
||||
// credit where it's due :
|
||||
// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45
|
||||
function GetDirName(Value: string): string;
|
||||
@ -56,7 +67,7 @@ var
|
||||
begin
|
||||
// initialize default path, which will be returned when the following registry
|
||||
// key queries fail due to missing keys or for some different reason
|
||||
Result := '{pf}\obs-studio';
|
||||
Result := '{commonpf}\obs-studio';
|
||||
// query the first registry value; if this succeeds, return the obtained value
|
||||
if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then
|
||||
Result := InstallPath
|
||||
|
40
lib/example/simplest-plugin.c
Normal file
40
lib/example/simplest-plugin.c
Normal file
@ -0,0 +1,40 @@
|
||||
#include <obs-module.h>
|
||||
|
||||
#include "../obs-websocket-api.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US")
|
||||
|
||||
obs_websocket_vendor vendor;
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
blog(LOG_INFO, "Example obs-websocket-api plugin loaded!");
|
||||
return true;
|
||||
}
|
||||
|
||||
void example_request_cb(obs_data_t *request_data, obs_data_t *response_data, void *priv_data);
|
||||
void obs_module_post_load(void)
|
||||
{
|
||||
vendor = obs_websocket_register_vendor("api_example_plugin");
|
||||
if (!vendor) {
|
||||
blog(LOG_ERROR, "Vendor registration failed! (obs-websocket should have logged something if installed properly.)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!obs_websocket_vendor_register_request(vendor, "example_request", example_request_cb, NULL))
|
||||
blog(LOG_ERROR, "Failed to register `example_request` request with obs-websocket.");
|
||||
}
|
||||
|
||||
void obs_module_unload(void)
|
||||
{
|
||||
blog(LOG_INFO, "Example obs-websocket-api plugin unloaded!");
|
||||
}
|
||||
|
||||
void example_request_cb(obs_data_t *request_data, obs_data_t *response_data, void *priv_data)
|
||||
{
|
||||
if (obs_data_has_user_value(request_data, "ping"))
|
||||
obs_data_set_bool(response_data, "pong", true);
|
||||
|
||||
UNUSED_PARAMETER(priv_data);
|
||||
}
|
133
lib/obs-websocket-api.h
Normal file
133
lib/obs-websocket-api.h
Normal file
@ -0,0 +1,133 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#ifndef _OBS_WEBSOCKET_API_H
|
||||
#define _OBS_WEBSOCKET_API_H
|
||||
|
||||
#include <obs.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void* obs_websocket_vendor;
|
||||
typedef void (*obs_websocket_request_callback_function)(obs_data_t*, obs_data_t*, void*);
|
||||
|
||||
struct obs_websocket_request_callback {
|
||||
obs_websocket_request_callback_function callback;
|
||||
void *priv_data;
|
||||
};
|
||||
|
||||
inline proc_handler_t *ph;
|
||||
|
||||
static inline proc_handler_t *obs_websocket_get_ph(void)
|
||||
{
|
||||
proc_handler_t *global_ph = obs_get_proc_handler();
|
||||
assert(global_ph != NULL);
|
||||
|
||||
calldata_t cd = {0};
|
||||
if (!proc_handler_call(global_ph, "obs_websocket_api_get_ph", &cd))
|
||||
blog(LOG_DEBUG, "Unable to fetch obs-websocket proc handler object. obs-websocket not installed?");
|
||||
proc_handler_t *ret = (proc_handler_t*)calldata_ptr(&cd, "ph");
|
||||
calldata_free(&cd);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline bool obs_websocket_run_simple_proc(obs_websocket_vendor vendor, const char *proc_name, calldata_t *cd)
|
||||
{
|
||||
if (!ph || !vendor || !proc_name || !strlen(proc_name) || !cd)
|
||||
return false;
|
||||
|
||||
calldata_set_ptr(cd, "vendor", vendor);
|
||||
|
||||
proc_handler_call(ph, proc_name, cd);
|
||||
return calldata_bool(cd, "success");
|
||||
}
|
||||
|
||||
// ALWAYS CALL VIA `obs_module_post_load()` CALLBACK!
|
||||
// Registers a new "vendor" (Example: obs-ndi)
|
||||
static inline obs_websocket_vendor obs_websocket_register_vendor(const char *vendor_name)
|
||||
{
|
||||
ph = obs_websocket_get_ph();
|
||||
if (!ph)
|
||||
return NULL;
|
||||
|
||||
calldata_t cd = {0};
|
||||
|
||||
calldata_set_string(&cd, "name", vendor_name);
|
||||
|
||||
proc_handler_call(ph, "vendor_register", &cd);
|
||||
obs_websocket_vendor ret = calldata_ptr(&cd, "vendor");
|
||||
calldata_free(&cd);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Registers a new request for a vendor
|
||||
static inline bool obs_websocket_vendor_register_request(obs_websocket_vendor vendor, const char *request_type, obs_websocket_request_callback_function request_callback, void* priv_data)
|
||||
{
|
||||
calldata_t cd = {0};
|
||||
|
||||
struct obs_websocket_request_callback cb = {};
|
||||
cb.callback = request_callback;
|
||||
cb.priv_data = priv_data;
|
||||
|
||||
calldata_set_string(&cd, "type", request_type);
|
||||
calldata_set_ptr(&cd, "callback", &cb);
|
||||
|
||||
bool success = obs_websocket_run_simple_proc(vendor, "vendor_request_register", &cd);
|
||||
calldata_free(&cd);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Unregisters an existing vendor request
|
||||
static inline bool obs_websocket_vendor_unregister_request(obs_websocket_vendor vendor, const char *request_type)
|
||||
{
|
||||
calldata_t cd = {0};
|
||||
|
||||
calldata_set_string(&cd, "type", request_type);
|
||||
|
||||
bool success = obs_websocket_run_simple_proc(vendor, "vendor_request_unregister", &cd);
|
||||
calldata_free(&cd);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Does not affect event_data refcount.
|
||||
// Emits an event under the vendor's name
|
||||
static inline bool obs_websocket_vendor_emit_event(obs_websocket_vendor vendor, const char *event_name, obs_data_t *event_data)
|
||||
{
|
||||
calldata_t cd = {0};
|
||||
|
||||
calldata_set_string(&cd, "type", event_name);
|
||||
calldata_set_ptr(&cd, "data", (void*)event_data);
|
||||
|
||||
bool success = obs_websocket_run_simple_proc(vendor, "vendor_event_emit", &cd);
|
||||
calldata_free(&cd);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
@ -1,7 +1,25 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include "Config.h"
|
||||
#include "plugin-macros.generated.h"
|
||||
#include "utils/Crypto.h"
|
||||
#include "utils/Platform.h"
|
||||
|
||||
@ -11,7 +29,7 @@
|
||||
#define PARAM_ENABLED "ServerEnabled"
|
||||
#define PARAM_PORT "ServerPort"
|
||||
#define PARAM_ALERTS "AlertsEnabled"
|
||||
#define PARAM_AUTHREQUIRED "AuthRequred"
|
||||
#define PARAM_AUTHREQUIRED "AuthRequired"
|
||||
#define PARAM_PASSWORD "ServerPassword"
|
||||
|
||||
#define CMDLINE_WEBSOCKET_PORT "websocket_port"
|
||||
@ -19,9 +37,9 @@
|
||||
#define CMDLINE_WEBSOCKET_DEBUG "websocket_debug"
|
||||
|
||||
Config::Config() :
|
||||
FirstLoad(true),
|
||||
PortOverridden(false),
|
||||
PasswordOverridden(false),
|
||||
FirstLoad(true),
|
||||
ServerEnabled(true),
|
||||
ServerPort(4444),
|
||||
DebugEnabled(false),
|
||||
@ -43,39 +61,53 @@ void Config::Load()
|
||||
FirstLoad = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_FIRSTLOAD);
|
||||
ServerEnabled = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_ENABLED);
|
||||
AlertsEnabled = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_ALERTS);
|
||||
ServerPort = config_get_uint(obsConfig, CONFIG_SECTION_NAME, PARAM_PORT);
|
||||
AuthRequired = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_AUTHREQUIRED);
|
||||
ServerPassword = config_get_string(obsConfig, CONFIG_SECTION_NAME, PARAM_PASSWORD);
|
||||
|
||||
// Set server password and save it to the config before processing overrides,
|
||||
// so that there is always a true configured password regardless of if
|
||||
// future loads use the override flag.
|
||||
if (FirstLoad) {
|
||||
FirstLoad = false;
|
||||
if (ServerPassword.isEmpty()) {
|
||||
blog(LOG_INFO, "[Config::Load] (FirstLoad) Generating new server password.");
|
||||
ServerPassword = QString::fromStdString(Utils::Crypto::GeneratePassword());
|
||||
} else {
|
||||
blog(LOG_INFO, "[Config::Load] (FirstLoad) Not generating new password since one is already configured.");
|
||||
}
|
||||
Save();
|
||||
}
|
||||
|
||||
// Process `--websocket_port` override
|
||||
QString portArgument = Utils::Platform::GetCommandLineArgument(CMDLINE_WEBSOCKET_PORT);
|
||||
if (portArgument != "") {
|
||||
bool ok;
|
||||
uint16_t serverPort = portArgument.toUShort(&ok);
|
||||
if (ok) {
|
||||
blog(LOG_INFO, "[Config::Load] Overriding websocket port with: %d", serverPort);
|
||||
blog(LOG_INFO, "[Config::Load] --websocket_port passed. Overriding WebSocket port with: %d", serverPort);
|
||||
PortOverridden = true;
|
||||
ServerPort = serverPort;
|
||||
} else {
|
||||
ServerPort = config_get_uint(obsConfig, CONFIG_SECTION_NAME, PARAM_PORT);
|
||||
blog(LOG_WARNING, "[Config::Load] Not overriding WebSocket port since integer conversion failed.");
|
||||
}
|
||||
} else {
|
||||
ServerPort = config_get_uint(obsConfig, CONFIG_SECTION_NAME, PARAM_PORT);
|
||||
}
|
||||
|
||||
// Process `--websocket_password` override
|
||||
QString passwordArgument = Utils::Platform::GetCommandLineArgument(CMDLINE_WEBSOCKET_PASSWORD);
|
||||
if (passwordArgument != "") {
|
||||
blog(LOG_INFO, "[Config::Load] Overriding websocket password");
|
||||
blog(LOG_INFO, "[Config::Load] --websocket_password passed. Overriding WebSocket password.");
|
||||
PasswordOverridden = true;
|
||||
AuthRequired = true;
|
||||
ServerPassword = passwordArgument;
|
||||
} else {
|
||||
AuthRequired = config_get_bool(obsConfig, CONFIG_SECTION_NAME, PARAM_AUTHREQUIRED);
|
||||
if (FirstLoad) {
|
||||
ServerPassword = Utils::Crypto::GeneratePassword();
|
||||
} else {
|
||||
ServerPassword = config_get_string(obsConfig, CONFIG_SECTION_NAME, PARAM_PASSWORD);
|
||||
}
|
||||
}
|
||||
|
||||
if (Utils::Platform::GetCommandLineFlagSet(CMDLINE_WEBSOCKET_DEBUG)) // Debug does not persist on reload, so we let people override it with a flag.
|
||||
// Process `--websocket_debug` override
|
||||
if (Utils::Platform::GetCommandLineFlagSet(CMDLINE_WEBSOCKET_DEBUG)) {
|
||||
// Debug does not persist on reload, so we let people override it with a flag.
|
||||
blog(LOG_INFO, "[Config::Load] --websocket_debug passed. Enabling debug logging.");
|
||||
DebugEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Config::Save()
|
||||
|
54
src/Config.h
54
src/Config.h
@ -1,27 +1,45 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <QString>
|
||||
#include <util/config-file.h>
|
||||
|
||||
class Config {
|
||||
public:
|
||||
Config();
|
||||
void Load();
|
||||
void Save();
|
||||
void SetDefaultsToGlobalStore();
|
||||
config_t* GetConfigStore();
|
||||
#include "plugin-macros.generated.h"
|
||||
|
||||
bool PortOverridden;
|
||||
bool PasswordOverridden;
|
||||
struct Config {
|
||||
Config();
|
||||
void Load();
|
||||
void Save();
|
||||
void SetDefaultsToGlobalStore();
|
||||
config_t* GetConfigStore();
|
||||
|
||||
bool FirstLoad;
|
||||
bool ServerEnabled;
|
||||
uint16_t ServerPort;
|
||||
bool DebugEnabled;
|
||||
bool AlertsEnabled;
|
||||
bool AuthRequired;
|
||||
QString ServerPassword;
|
||||
|
||||
private:
|
||||
std::atomic<bool> PortOverridden;
|
||||
std::atomic<bool> PasswordOverridden;
|
||||
|
||||
std::atomic<bool> FirstLoad;
|
||||
std::atomic<bool> ServerEnabled;
|
||||
std::atomic<uint16_t> ServerPort;
|
||||
std::atomic<bool> DebugEnabled;
|
||||
std::atomic<bool> AlertsEnabled;
|
||||
std::atomic<bool> AuthRequired;
|
||||
QString ServerPassword;
|
||||
};
|
||||
|
209
src/WebSocketApi.cpp
Normal file
209
src/WebSocketApi.cpp
Normal file
@ -0,0 +1,209 @@
|
||||
#include "WebSocketApi.h"
|
||||
#include "obs-websocket.h"
|
||||
|
||||
#define RETURN_STATUS(status) { calldata_set_bool(cd, "success", status); return; }
|
||||
#define RETURN_SUCCESS() RETURN_STATUS(true);
|
||||
#define RETURN_FAILURE() RETURN_STATUS(false);
|
||||
|
||||
WebSocketApi::Vendor *get_vendor(calldata_t *cd)
|
||||
{
|
||||
void *voidVendor;
|
||||
if (!calldata_get_ptr(cd, "vendor", &voidVendor)) {
|
||||
blog(LOG_WARNING, "[WebSocketApi: get_vendor] Failed due to missing `vendor` pointer.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return static_cast<WebSocketApi::Vendor*>(voidVendor);
|
||||
}
|
||||
|
||||
WebSocketApi::WebSocketApi()
|
||||
{
|
||||
blog_debug("[WebSocketApi::WebSocketApi] Setting up...");
|
||||
|
||||
_procHandler = proc_handler_create();
|
||||
|
||||
proc_handler_add(_procHandler, "bool vendor_register(in string name, out ptr vendor)", &vendor_register_cb, this);
|
||||
proc_handler_add(_procHandler, "bool vendor_request_register(in ptr vendor, in string type, in ptr callback)", &vendor_request_register_cb, this);
|
||||
proc_handler_add(_procHandler, "bool vendor_request_unregister(in ptr vendor, in string type)", &vendor_request_unregister_cb, this);
|
||||
proc_handler_add(_procHandler, "bool vendor_event_emit(in ptr vendor, in string type, in ptr data)", &vendor_event_emit_cb, this);
|
||||
|
||||
proc_handler_t *ph = obs_get_proc_handler();
|
||||
assert(ph != NULL);
|
||||
|
||||
proc_handler_add(ph, "bool obs_websocket_api_get_ph(out ptr ph)", &get_ph_cb, this);
|
||||
|
||||
blog_debug("[WebSocketApi::WebSocketApi] Finished.");
|
||||
}
|
||||
|
||||
WebSocketApi::~WebSocketApi()
|
||||
{
|
||||
blog_debug("[WebSocketApi::~WebSocketApi] Shutting down...");
|
||||
|
||||
proc_handler_destroy(_procHandler);
|
||||
|
||||
for (auto vendor : _vendors) {
|
||||
blog_debug("[WebSocketApi::~WebSocketApi] Deleting vendor: %s", vendor.first.c_str());
|
||||
delete vendor.second;
|
||||
}
|
||||
|
||||
blog_debug("[WebSocketApi::~WebSocketApi] Finished.");
|
||||
}
|
||||
|
||||
void WebSocketApi::SetEventCallback(EventCallback cb)
|
||||
{
|
||||
_eventCallback = cb;
|
||||
}
|
||||
|
||||
enum WebSocketApi::RequestReturnCode WebSocketApi::PerformVendorRequest(std::string vendorName, std::string requestType, obs_data_t *requestData, obs_data_t *responseData)
|
||||
{
|
||||
std::shared_lock l(_mutex);
|
||||
|
||||
if (_vendors.count(vendorName) == 0)
|
||||
return RequestReturnCode::NoVendor;
|
||||
|
||||
auto v = _vendors[vendorName];
|
||||
|
||||
l.unlock();
|
||||
|
||||
std::shared_lock v_l(v->_mutex);
|
||||
|
||||
if (v->_requests.count(requestType) == 0)
|
||||
return RequestReturnCode::NoVendorRequest;
|
||||
|
||||
auto cb = v->_requests[requestType];
|
||||
|
||||
v_l.unlock();
|
||||
|
||||
cb.callback(requestData, responseData, cb.priv_data);
|
||||
|
||||
return RequestReturnCode::Normal;
|
||||
}
|
||||
|
||||
void WebSocketApi::get_ph_cb(void *priv_data, calldata_t *cd)
|
||||
{
|
||||
auto c = static_cast<WebSocketApi*>(priv_data);
|
||||
|
||||
calldata_set_ptr(cd, "ph", (void*)c->_procHandler);
|
||||
|
||||
RETURN_SUCCESS();
|
||||
}
|
||||
|
||||
void WebSocketApi::vendor_register_cb(void *priv_data, calldata_t *cd)
|
||||
{
|
||||
auto c = static_cast<WebSocketApi*>(priv_data);
|
||||
|
||||
const char *vendorName;
|
||||
if (!calldata_get_string(cd, "name", &vendorName) || strlen(vendorName) == 0) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_register_cb] Failed due to missing `name` string.");
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
// Theoretically doesn't need a mutex, but it's good to be safe.
|
||||
std::unique_lock l(c->_mutex);
|
||||
|
||||
if (c->_vendors.count(vendorName)) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_register_cb] Failed because `%s` is already a registered vendor.", vendorName);
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
Vendor* v = new Vendor();
|
||||
v->_name = vendorName;
|
||||
|
||||
c->_vendors[vendorName] = v;
|
||||
|
||||
blog_debug("[WebSocketApi::vendor_register_cb] [vendorName: %s] Registered new vendor.", v->_name.c_str());
|
||||
|
||||
calldata_set_ptr(cd, "vendor", static_cast<void*>(v));
|
||||
|
||||
RETURN_SUCCESS();
|
||||
}
|
||||
|
||||
void WebSocketApi::vendor_request_register_cb(void *, calldata_t *cd)
|
||||
{
|
||||
Vendor *v = get_vendor(cd);
|
||||
if (!v)
|
||||
RETURN_FAILURE();
|
||||
|
||||
const char *requestType;
|
||||
if (!calldata_get_string(cd, "type", &requestType) || strlen(requestType) == 0) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed due to missing or empty `type` string.", v->_name.c_str());
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
void *voidCallback;
|
||||
if (!calldata_get_ptr(cd, "callback", &voidCallback) || !voidCallback) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed due to missing `callback` pointer.", v->_name.c_str());
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
auto cb = static_cast<obs_websocket_request_callback*>(voidCallback);
|
||||
|
||||
std::unique_lock l(v->_mutex);
|
||||
|
||||
if (v->_requests.count(requestType)) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed because `%s` is already a registered request.", v->_name.c_str(), requestType);
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
v->_requests[requestType] = *cb;
|
||||
|
||||
blog_debug("[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Registered new vendor request: %s", v->_name.c_str(), requestType);
|
||||
|
||||
RETURN_SUCCESS();
|
||||
}
|
||||
|
||||
void WebSocketApi::vendor_request_unregister_cb(void *, calldata_t *cd)
|
||||
{
|
||||
Vendor *v = get_vendor(cd);
|
||||
if (!v)
|
||||
RETURN_FAILURE();
|
||||
|
||||
const char *requestType;
|
||||
if (!calldata_get_string(cd, "type", &requestType) || strlen(requestType) == 0) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_request_unregister_cb] [vendorName: %s] Failed due to missing `type` string.", v->_name.c_str());
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
std::unique_lock l(v->_mutex);
|
||||
|
||||
if (!v->_requests.count(requestType)) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_request_register_cb] [vendorName: %s] Failed because `%s` is not a registered request.", v->_name.c_str(), requestType);
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
v->_requests.erase(requestType);
|
||||
|
||||
blog_debug("[WebSocketApi::vendor_request_unregister_cb] [vendorName: %s] Unregistered vendor request: %s", v->_name.c_str(), requestType);
|
||||
|
||||
RETURN_SUCCESS();
|
||||
}
|
||||
|
||||
void WebSocketApi::vendor_event_emit_cb(void *priv_data, calldata_t *cd)
|
||||
{
|
||||
auto c = static_cast<WebSocketApi*>(priv_data);
|
||||
|
||||
Vendor *v = get_vendor(cd);
|
||||
if (!v)
|
||||
RETURN_FAILURE();
|
||||
|
||||
const char *eventType;
|
||||
if (!calldata_get_string(cd, "type", &eventType) || strlen(eventType) == 0) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_event_emit_cb] [vendorName: %s] Failed due to missing `type` string.", v->_name.c_str());
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
void *voidEventData;
|
||||
if (!calldata_get_ptr(cd, "data", &voidEventData)) {
|
||||
blog(LOG_WARNING, "[WebSocketApi::vendor_event_emit_cb] [vendorName: %s] Failed due to missing `data` pointer.", v->_name.c_str());
|
||||
RETURN_FAILURE();
|
||||
}
|
||||
|
||||
auto eventData = static_cast<obs_data_t*>(voidEventData);
|
||||
|
||||
if (!c->_eventCallback)
|
||||
RETURN_FAILURE();
|
||||
|
||||
c->_eventCallback(v->_name, eventType, eventData);
|
||||
|
||||
RETURN_SUCCESS();
|
||||
}
|
46
src/WebSocketApi.h
Normal file
46
src/WebSocketApi.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <obs.h>
|
||||
|
||||
#include "../lib/obs-websocket-api.h"
|
||||
|
||||
class WebSocketApi {
|
||||
public:
|
||||
enum RequestReturnCode {
|
||||
Normal,
|
||||
NoVendor,
|
||||
NoVendorRequest,
|
||||
};
|
||||
|
||||
typedef std::function<void(std::string, std::string, obs_data_t*)> EventCallback;
|
||||
|
||||
struct Vendor {
|
||||
std::shared_mutex _mutex;
|
||||
std::string _name;
|
||||
std::map<std::string, obs_websocket_request_callback> _requests;
|
||||
};
|
||||
|
||||
WebSocketApi();
|
||||
~WebSocketApi();
|
||||
|
||||
void SetEventCallback(EventCallback cb);
|
||||
|
||||
enum RequestReturnCode PerformVendorRequest(std::string vendorName, std::string requestName, obs_data_t *requestData, obs_data_t *responseData);
|
||||
|
||||
static void get_ph_cb(void *priv_data, calldata_t *cd);
|
||||
static void vendor_register_cb(void *priv_data, calldata_t *cd);
|
||||
static void vendor_request_register_cb(void *priv_data, calldata_t *cd);
|
||||
static void vendor_request_unregister_cb(void *priv_data, calldata_t *cd);
|
||||
static void vendor_event_emit_cb(void *priv_data, calldata_t *cd);
|
||||
|
||||
private:
|
||||
std::shared_mutex _mutex;
|
||||
EventCallback _eventCallback;
|
||||
proc_handler_t *_procHandler;
|
||||
std::map<std::string, Vendor*> _vendors;
|
||||
};
|
@ -1,245 +0,0 @@
|
||||
#include <obs-module.h>
|
||||
|
||||
#include "WebSocketProtocol.h"
|
||||
#include "WebSocketSession.h"
|
||||
#include "requesthandler/RequestHandler.h"
|
||||
#include "requesthandler/rpc/RequestStatus.h"
|
||||
#include "obs-websocket.h"
|
||||
#include "Config.h"
|
||||
#include "plugin-macros.generated.h"
|
||||
#include "utils/Crypto.h"
|
||||
#include "utils/Json.h"
|
||||
#include "utils/Platform.h"
|
||||
|
||||
bool IsSupportedRpcVersion(uint8_t requestedVersion)
|
||||
{
|
||||
for (auto version : WebSocketProtocol::SupportedRpcVersions) {
|
||||
if (requestedVersion == version)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SetSessionParameters(SessionPtr session, WebSocketProtocol::ProcessResult &ret, json payloadData)
|
||||
{
|
||||
if (payloadData.contains("ignoreInvalidMessages")) {
|
||||
if (!payloadData["ignoreInvalidMessages"].is_boolean()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
|
||||
ret.closeReason = "Your `ignoreInvalidMessages` is not a boolean.";
|
||||
return;
|
||||
}
|
||||
session->SetIgnoreInvalidMessages(payloadData["ignoreInvalidMessages"]);
|
||||
}
|
||||
|
||||
if (payloadData.contains("ignoreNonFatalRequestChecks")) {
|
||||
if (!payloadData["ignoreNonFatalRequestChecks"].is_boolean()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
|
||||
ret.closeReason = "Your `ignoreNonFatalRequestChecks` is not a boolean.";
|
||||
return;
|
||||
}
|
||||
session->SetIgnoreNonFatalRequestChecks(payloadData["ignoreNonFatalRequestChecks"]);
|
||||
}
|
||||
|
||||
if (payloadData.contains("eventSubscriptions")) {
|
||||
if (!payloadData["eventSubscriptions"].is_number_unsigned()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
|
||||
ret.closeReason = "Your `eventSubscriptions` is not an unsigned number.";
|
||||
return;
|
||||
}
|
||||
session->SetEventSubscriptions(payloadData["eventSubscriptions"]);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketProtocol::ProcessMessage(SessionPtr session, WebSocketProtocol::ProcessResult &ret, uint8_t opCode, json payloadData)
|
||||
{
|
||||
if (!payloadData.is_object()) {
|
||||
if (payloadData.is_null()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
|
||||
ret.closeReason = "Your payload is missing data (`d`).";
|
||||
} else {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
|
||||
ret.closeReason = "Your payload's data (`d`) is not an object.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only `Identify` is allowed when not identified
|
||||
if (!session->IsIdentified() && opCode != 1) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::NotIdentified;
|
||||
ret.closeReason = "You attempted to send a non-Identify message while not identified.";
|
||||
return;
|
||||
}
|
||||
|
||||
switch (opCode) {
|
||||
case 1: { // Identify
|
||||
std::unique_lock<std::mutex> sessionLock(session->OperationMutex);
|
||||
if (session->IsIdentified()) {
|
||||
if (!session->IgnoreInvalidMessages()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::AlreadyIdentified;
|
||||
ret.closeReason = "You are already Identified with the obs-websocket server.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (session->AuthenticationRequired()) {
|
||||
if (!payloadData.contains("authentication")) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::AuthenticationFailed;
|
||||
ret.closeReason = "Your payload's data is missing an `authentication` string, however authentication is required.";
|
||||
return;
|
||||
}
|
||||
if (!Utils::Crypto::CheckAuthenticationString(session->Secret(), session->Challenge(), payloadData["authentication"])) {
|
||||
auto conf = GetConfig();
|
||||
if (conf && conf->AlertsEnabled) {
|
||||
QString title = obs_module_text("OBSWebSocket.TrayNotification.AuthenticationFailed.Title");
|
||||
QString body = QString(obs_module_text("OBSWebSocket.TrayNotification.AuthenticationFailed.Body")).arg(QString::fromStdString(session->RemoteAddress()));
|
||||
Utils::Platform::SendTrayNotification(QSystemTrayIcon::Warning, title, body);
|
||||
}
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::AuthenticationFailed;
|
||||
ret.closeReason = "Authentication failed.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!payloadData.contains("rpcVersion")) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
|
||||
ret.closeReason = "Your payload's data is missing an `rpcVersion`.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payloadData["rpcVersion"].is_number_unsigned()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
|
||||
ret.closeReason = "Your `rpcVersion` is not an unsigned number.";
|
||||
}
|
||||
|
||||
uint8_t requestedRpcVersion = payloadData["rpcVersion"];
|
||||
if (!IsSupportedRpcVersion(requestedRpcVersion)) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::UnsupportedRpcVersion;
|
||||
ret.closeReason = "Your requested RPC version is not supported by this server.";
|
||||
return;
|
||||
}
|
||||
session->SetRpcVersion(requestedRpcVersion);
|
||||
|
||||
SetSessionParameters(session, ret, payloadData);
|
||||
if (ret.closeCode != WebSocketServer::WebSocketCloseCode::DontClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
session->SetIsIdentified(true);
|
||||
|
||||
auto conf = GetConfig();
|
||||
if (conf && conf->AlertsEnabled) {
|
||||
QString title = obs_module_text("OBSWebSocket.TrayNotification.Identified.Title");
|
||||
QString body = QString(obs_module_text("OBSWebSocket.TrayNotification.Identified.Body")).arg(QString::fromStdString(session->RemoteAddress()));
|
||||
Utils::Platform::SendTrayNotification(QSystemTrayIcon::Information, title, body);
|
||||
}
|
||||
|
||||
ret.result["op"] = 2;
|
||||
ret.result["d"]["negotiatedRpcVersion"] = session->RpcVersion();
|
||||
} return;
|
||||
case 3: { // Reidentify
|
||||
std::unique_lock<std::mutex> sessionLock(session->OperationMutex);
|
||||
|
||||
SetSessionParameters(session, ret, payloadData);
|
||||
if (ret.closeCode != WebSocketServer::WebSocketCloseCode::DontClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
ret.result["op"] = 2;
|
||||
ret.result["d"]["negotiatedRpcVersion"] = session->RpcVersion();
|
||||
} return;
|
||||
case 6: { // Request
|
||||
// RequestID checking has to be done here where we are able to close the connection.
|
||||
if (!payloadData.contains("requestId")) {
|
||||
if (!session->IgnoreInvalidMessages()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
|
||||
ret.closeReason = "Your payload data is missing a `requestId`.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RequestHandler requestHandler;
|
||||
Request request(session, payloadData["requestType"], payloadData["requestData"]);
|
||||
|
||||
RequestResult requestResult = requestHandler.ProcessRequest(request);
|
||||
|
||||
json resultPayloadData;
|
||||
resultPayloadData["requestType"] = payloadData["requestType"];
|
||||
resultPayloadData["requestId"] = payloadData["requestId"];
|
||||
resultPayloadData["requestStatus"] = {
|
||||
{"result", requestResult.StatusCode == RequestStatus::Success},
|
||||
{"code", requestResult.StatusCode}
|
||||
};
|
||||
if (!requestResult.Comment.empty())
|
||||
resultPayloadData["requestStatus"]["comment"] = requestResult.Comment;
|
||||
if (requestResult.ResponseData.is_object())
|
||||
resultPayloadData["responseData"] = requestResult.ResponseData;
|
||||
ret.result["op"] = 7;
|
||||
ret.result["d"] = resultPayloadData;
|
||||
} return;
|
||||
case 8: { // RequestBatch
|
||||
// RequestID checking has to be done here where we are able to close the connection.
|
||||
if (!payloadData.contains("requestId")) {
|
||||
if (!session->IgnoreInvalidMessages()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
|
||||
ret.closeReason = "Your payload data is missing a `requestId`.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payloadData.contains("requests")) {
|
||||
if (!session->IgnoreInvalidMessages()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::MissingDataKey;
|
||||
ret.closeReason = "Your payload data is missing a `requests`.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payloadData["requests"].is_array()) {
|
||||
if (!session->IgnoreInvalidMessages()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::InvalidDataKeyType;
|
||||
ret.closeReason = "Your `requests` is not an array.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<json> requests = payloadData["requests"];
|
||||
json results = json::array();
|
||||
|
||||
RequestHandler requestHandler;
|
||||
for (auto requestJson : requests) {
|
||||
Request request(session, requestJson["requestType"], requestJson["requestData"]);
|
||||
|
||||
RequestResult requestResult = requestHandler.ProcessRequest(request);
|
||||
|
||||
json result;
|
||||
result["requestType"] = requestJson["requestType"];
|
||||
|
||||
if (requestJson.contains("requestId"))
|
||||
result["requestId"] = requestJson["requestId"];
|
||||
|
||||
result["requestStatus"] = {
|
||||
{"result", requestResult.StatusCode == RequestStatus::Success},
|
||||
{"code", requestResult.StatusCode}
|
||||
};
|
||||
|
||||
if (!requestResult.Comment.empty())
|
||||
result["requestStatus"]["comment"] = requestResult.Comment;
|
||||
|
||||
if (requestResult.ResponseData.is_object())
|
||||
result["responseData"] = requestResult.ResponseData;
|
||||
|
||||
results.push_back(result);
|
||||
}
|
||||
|
||||
ret.result["op"] = 9;
|
||||
ret.result["d"]["requestId"] = payloadData["requestId"];
|
||||
ret.result["d"]["results"] = results;
|
||||
} return;
|
||||
default:
|
||||
if (!session->IgnoreInvalidMessages()) {
|
||||
ret.closeCode = WebSocketServer::WebSocketCloseCode::UnknownOpCode;
|
||||
ret.closeReason = std::string("Unknown OpCode: %s") + std::to_string(opCode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "WebSocketServer.h"
|
||||
|
||||
class WebSocketSession;
|
||||
typedef std::shared_ptr<WebSocketSession> SessionPtr;
|
||||
|
||||
namespace WebSocketProtocol {
|
||||
const std::vector<uint8_t> SupportedRpcVersions{
|
||||
1
|
||||
};
|
||||
|
||||
struct ProcessResult {
|
||||
WebSocketServer::WebSocketCloseCode closeCode = WebSocketServer::WebSocketCloseCode::DontClose;
|
||||
std::string closeReason;
|
||||
json result;
|
||||
};
|
||||
|
||||
void ProcessMessage(SessionPtr session, ProcessResult &ret, uint8_t opCode, json incomingMessage);
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <QObject>
|
||||
#include <QThreadPool>
|
||||
#include <QString>
|
||||
#include <websocketpp/config/asio_no_tls.hpp>
|
||||
#include <websocketpp/server.hpp>
|
||||
|
||||
#include "utils/Json.h"
|
||||
#include "WebSocketSession.h"
|
||||
|
||||
class WebSocketServer : QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum WebSocketEncoding {
|
||||
Json,
|
||||
MsgPack
|
||||
};
|
||||
|
||||
struct WebSocketSessionState {
|
||||
websocketpp::connection_hdl hdl;
|
||||
std::string remoteAddress;
|
||||
uint64_t connectedAt;
|
||||
uint64_t incomingMessages;
|
||||
uint64_t outgoingMessages;
|
||||
bool isIdentified;
|
||||
};
|
||||
|
||||
enum WebSocketOpCode {
|
||||
Hello = 0,
|
||||
Identify = 1,
|
||||
Identified = 2,
|
||||
Reidentify = 3,
|
||||
Event = 5,
|
||||
Request = 6,
|
||||
RequestResponse = 7,
|
||||
RequestBatch = 8,
|
||||
RequestBatchResponse = 9,
|
||||
};
|
||||
|
||||
enum WebSocketCloseCode {
|
||||
// Internal only
|
||||
DontClose = 0,
|
||||
// Reserved
|
||||
UnknownReason = 4000,
|
||||
// The server was unable to decode the incoming websocket message
|
||||
MessageDecodeError = 4002,
|
||||
// A data key is missing but required
|
||||
MissingDataKey = 4003,
|
||||
// A data key has an invalid type
|
||||
InvalidDataKeyType = 4004,
|
||||
// The specified `op` was invalid or missing
|
||||
UnknownOpCode = 4005,
|
||||
// The client sent a websocket message without first sending `Identify` message
|
||||
NotIdentified = 4006,
|
||||
// The client sent an `Identify` message while already identified
|
||||
AlreadyIdentified = 4007,
|
||||
// The authentication attempt (via `Identify`) failed
|
||||
AuthenticationFailed = 4008,
|
||||
// The server detected the usage of an old version of the obs-websocket RPC protocol.
|
||||
UnsupportedRpcVersion = 4009,
|
||||
// The websocket session has been invalidated by the obs-websocket server.
|
||||
SessionInvalidated = 4010,
|
||||
};
|
||||
|
||||
WebSocketServer();
|
||||
~WebSocketServer();
|
||||
|
||||
void Start();
|
||||
void Stop();
|
||||
void InvalidateSession(websocketpp::connection_hdl hdl);
|
||||
|
||||
bool IsListening() {
|
||||
return _server.is_listening();
|
||||
}
|
||||
|
||||
std::vector<WebSocketSessionState> GetWebSocketSessions();
|
||||
|
||||
QThreadPool *GetThreadPool() {
|
||||
return &_threadPool;
|
||||
}
|
||||
|
||||
bool AuthenticationRequired;
|
||||
std::string AuthenticationSecret;
|
||||
std::string AuthenticationSalt;
|
||||
|
||||
public Q_SLOTS:
|
||||
void BroadcastEvent(uint64_t requiredIntent, std::string eventType, json eventData = nullptr, uint8_t rpcVersion = 0);
|
||||
|
||||
signals:
|
||||
void ClientConnected(const WebSocketSessionState state);
|
||||
void ClientDisconnected(const WebSocketSessionState state, const uint16_t closeCode);
|
||||
|
||||
private:
|
||||
void ServerRunner();
|
||||
|
||||
bool onValidate(websocketpp::connection_hdl hdl);
|
||||
void onOpen(websocketpp::connection_hdl hdl);
|
||||
void onClose(websocketpp::connection_hdl hdl);
|
||||
void onMessage(websocketpp::connection_hdl hdl, websocketpp::server<websocketpp::config::asio>::message_ptr message);
|
||||
|
||||
std::thread _serverThread;
|
||||
websocketpp::server<websocketpp::config::asio> _server;
|
||||
QThreadPool _threadPool;
|
||||
std::mutex _sessionMutex;
|
||||
std::map<websocketpp::connection_hdl, SessionPtr, std::owner_less<websocketpp::connection_hdl>> _sessions;
|
||||
uint16_t _serverPort;
|
||||
QString _serverPassword;
|
||||
bool _debugEnabled;
|
||||
};
|
@ -1,11 +1,32 @@
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
EventHandler::EventHandler(WebSocketServerPtr webSocketServer) :
|
||||
_webSocketServer(webSocketServer),
|
||||
_obsLoaded(false)
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
EventHandler::EventHandler() :
|
||||
_obsLoaded(false),
|
||||
_inputVolumeMetersRef(0),
|
||||
_inputActiveStateChangedRef(0),
|
||||
_inputShowStateChangedRef(0),
|
||||
_sceneItemTransformChangedRef(0)
|
||||
{
|
||||
blog(LOG_INFO, "[EventHandler::EventHandler] Setting up event handlers...");
|
||||
blog_debug("[EventHandler::EventHandler] Setting up...");
|
||||
|
||||
obs_frontend_add_event_callback(OnFrontendEvent, this);
|
||||
|
||||
@ -19,12 +40,12 @@ EventHandler::EventHandler(WebSocketServerPtr webSocketServer) :
|
||||
blog(LOG_ERROR, "[EventHandler::EventHandler] Unable to get libobs signal handler!");
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "[EventHandler::EventHandler] Finished.");
|
||||
blog_debug("[EventHandler::EventHandler] Finished.");
|
||||
}
|
||||
|
||||
EventHandler::~EventHandler()
|
||||
{
|
||||
blog(LOG_INFO, "[EventHandler::~EventHandler] Removing event handlers...");
|
||||
blog_debug("[EventHandler::~EventHandler] Shutting down...");
|
||||
|
||||
obs_frontend_remove_event_callback(OnFrontendEvent, this);
|
||||
|
||||
@ -38,7 +59,60 @@ EventHandler::~EventHandler()
|
||||
blog(LOG_ERROR, "[EventHandler::~EventHandler] Unable to get libobs signal handler!");
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "[EventHandler::~EventHandler] Finished.");
|
||||
blog_debug("[EventHandler::~EventHandler] Finished.");
|
||||
}
|
||||
|
||||
void EventHandler::SetBroadcastCallback(EventHandler::BroadcastCallback cb)
|
||||
{
|
||||
_broadcastCallback = cb;
|
||||
}
|
||||
|
||||
void EventHandler::SetObsLoadedCallback(EventHandler::ObsLoadedCallback cb)
|
||||
{
|
||||
_obsLoadedCallback = cb;
|
||||
}
|
||||
|
||||
// Function to increment refcounts for high volume event subscriptions
|
||||
void EventHandler::ProcessSubscription(uint64_t eventSubscriptions)
|
||||
{
|
||||
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0) {
|
||||
if (_inputVolumeMetersRef.fetch_add(1) == 0) {
|
||||
if (_inputVolumeMetersHandler)
|
||||
blog(LOG_WARNING, "[EventHandler::ProcessSubscription] Input volume meter handler already exists!");
|
||||
else
|
||||
_inputVolumeMetersHandler = std::make_unique<Utils::Obs::VolumeMeter::Handler>(std::bind(&EventHandler::HandleInputVolumeMeters, this, std::placeholders::_1));
|
||||
}
|
||||
}
|
||||
if ((eventSubscriptions & EventSubscription::InputActiveStateChanged) != 0)
|
||||
_inputActiveStateChangedRef++;
|
||||
if ((eventSubscriptions & EventSubscription::InputShowStateChanged) != 0)
|
||||
_inputShowStateChangedRef++;
|
||||
if ((eventSubscriptions & EventSubscription::SceneItemTransformChanged) != 0)
|
||||
_sceneItemTransformChangedRef++;
|
||||
}
|
||||
|
||||
// Function to decrement refcounts for high volume event subscriptions
|
||||
void EventHandler::ProcessUnsubscription(uint64_t eventSubscriptions)
|
||||
{
|
||||
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0) {
|
||||
if (_inputVolumeMetersRef.fetch_sub(1) == 1)
|
||||
_inputVolumeMetersHandler.reset();
|
||||
}
|
||||
if ((eventSubscriptions & EventSubscription::InputActiveStateChanged) != 0)
|
||||
_inputActiveStateChangedRef--;
|
||||
if ((eventSubscriptions & EventSubscription::InputShowStateChanged) != 0)
|
||||
_inputShowStateChangedRef--;
|
||||
if ((eventSubscriptions & EventSubscription::SceneItemTransformChanged) != 0)
|
||||
_sceneItemTransformChangedRef--;
|
||||
}
|
||||
|
||||
// Function required in order to use default arguments
|
||||
void EventHandler::BroadcastEvent(uint64_t requiredIntent, std::string eventType, json eventData, uint8_t rpcVersion)
|
||||
{
|
||||
if (!_broadcastCallback)
|
||||
return;
|
||||
|
||||
_broadcastCallback(requiredIntent, eventType, eventData, rpcVersion);
|
||||
}
|
||||
|
||||
void EventHandler::ConnectSourceSignals(obs_source_t *source) // Applies to inputs and scenes
|
||||
@ -85,7 +159,7 @@ void EventHandler::ConnectSourceSignals(obs_source_t *source) // Applies to inpu
|
||||
signal_handler_connect(sh, "item_transform", HandleSceneItemTransformChanged, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EventHandler::DisconnectSourceSignals(obs_source_t *source)
|
||||
{
|
||||
if (!source)
|
||||
@ -127,7 +201,7 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
|
||||
if (!eventHandler->_obsLoaded.load()) {
|
||||
if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) {
|
||||
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] OBS has finished loading. Connecting final handlers and enabling events...");
|
||||
blog_debug("[EventHandler::OnFrontendEvent] OBS has finished loading. Connecting final handlers and enabling events...");
|
||||
// Connect source signals and enable events only after OBS has fully loaded (to reduce extra logging).
|
||||
eventHandler->_obsLoaded.store(true);
|
||||
|
||||
@ -146,7 +220,10 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
return true;
|
||||
}, private_data);
|
||||
|
||||
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] Finished.");
|
||||
blog_debug("[EventHandler::OnFrontendEvent] Finished.");
|
||||
|
||||
if (eventHandler->_obsLoadedCallback)
|
||||
eventHandler->_obsLoadedCallback();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@ -157,7 +234,7 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
case OBS_FRONTEND_EVENT_EXIT:
|
||||
eventHandler->HandleExitStarted();
|
||||
|
||||
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] OBS is unloading. Disabling events...");
|
||||
blog_debug("[EventHandler::OnFrontendEvent] OBS is unloading. Disabling events...");
|
||||
// Disconnect source signals and disable events when OBS starts unloading (to reduce extra logging).
|
||||
eventHandler->_obsLoaded.store(false);
|
||||
|
||||
@ -176,7 +253,7 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
return true;
|
||||
}, private_data);
|
||||
|
||||
blog(LOG_INFO, "[EventHandler::OnFrontendEvent] Finished.");
|
||||
blog_debug("[EventHandler::OnFrontendEvent] Finished.");
|
||||
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED:
|
||||
@ -187,12 +264,18 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
break;
|
||||
|
||||
// Config
|
||||
//case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGING:
|
||||
// eventHandler->HandleCurrentSceneCollectionChanging();
|
||||
// break;
|
||||
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED:
|
||||
eventHandler->HandleCurrentSceneCollectionChanged();
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED:
|
||||
eventHandler->HandleSceneCollectionListChanged();
|
||||
break;
|
||||
//case OBS_FRONTEND_EVENT_PROFILE_CHANGING:
|
||||
// eventHandler->HandleCurrentProfileChanging();
|
||||
// break;
|
||||
case OBS_FRONTEND_EVENT_PROFILE_CHANGED:
|
||||
eventHandler->HandleCurrentProfileChanged();
|
||||
break;
|
||||
@ -202,7 +285,7 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
|
||||
// Scenes
|
||||
case OBS_FRONTEND_EVENT_SCENE_CHANGED:
|
||||
eventHandler->HandleCurrentSceneChanged();
|
||||
eventHandler->HandleCurrentProgramSceneChanged();
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED:
|
||||
eventHandler->HandleCurrentPreviewSceneChanged();
|
||||
@ -244,6 +327,12 @@ void EventHandler::OnFrontendEvent(enum obs_frontend_event event, void *private_
|
||||
case OBS_FRONTEND_EVENT_RECORDING_STOPPED:
|
||||
eventHandler->HandleRecordStateChanged(OBS_WEBSOCKET_OUTPUT_STOPPED);
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_RECORDING_PAUSED:
|
||||
eventHandler->HandleRecordStateChanged(OBS_WEBSOCKET_OUTPUT_PAUSED);
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED:
|
||||
eventHandler->HandleRecordStateChanged(OBS_WEBSOCKET_OUTPUT_RESUMED);
|
||||
break;
|
||||
case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING:
|
||||
eventHandler->HandleReplayBufferStateChanged(OBS_WEBSOCKET_OUTPUT_STARTING);
|
||||
break;
|
||||
@ -290,10 +379,6 @@ void EventHandler::SourceCreatedMultiHandler(void *param, calldata_t *data)
|
||||
case OBS_SOURCE_TYPE_INPUT:
|
||||
eventHandler->HandleInputCreated(source);
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_FILTER:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_TRANSITION:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_SCENE:
|
||||
eventHandler->HandleSceneCreated(source);
|
||||
break;
|
||||
@ -324,10 +409,6 @@ void EventHandler::SourceDestroyedMultiHandler(void *param, calldata_t *data)
|
||||
// We have to call `InputRemoved` with source_destroy because source_removed is not called when an input's last scene item is removed
|
||||
eventHandler->HandleInputRemoved(source);
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_FILTER:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_TRANSITION:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_SCENE:
|
||||
break;
|
||||
default:
|
||||
@ -349,10 +430,6 @@ void EventHandler::SourceRemovedMultiHandler(void *param, calldata_t *data)
|
||||
switch (obs_source_get_type(source)) {
|
||||
case OBS_SOURCE_TYPE_INPUT:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_FILTER:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_TRANSITION:
|
||||
break;
|
||||
case OBS_SOURCE_TYPE_SCENE:
|
||||
// Scenes emit the `removed` signal when they are removed from OBS, as expected
|
||||
eventHandler->HandleSceneRemoved(source);
|
||||
|
@ -1,34 +1,65 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <obs.hpp>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <util/platform.h>
|
||||
|
||||
#include "types/EventSubscription.h"
|
||||
#include "../obs-websocket.h"
|
||||
#include "../WebSocketServer.h"
|
||||
#include "../utils/Obs.h"
|
||||
|
||||
template <typename T> T* GetCalldataPointer(const calldata_t *data, const char* name) {
|
||||
void *ptr = nullptr;
|
||||
calldata_get_ptr(data, name, &ptr);
|
||||
return reinterpret_cast<T*>(ptr);
|
||||
}
|
||||
#include "../utils/Obs_VolumeMeter.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
class EventHandler
|
||||
{
|
||||
public:
|
||||
EventHandler(WebSocketServerPtr webSocketServer);
|
||||
EventHandler();
|
||||
~EventHandler();
|
||||
|
||||
typedef std::function<void(uint64_t, std::string, json, uint8_t)> BroadcastCallback;
|
||||
void SetBroadcastCallback(BroadcastCallback cb);
|
||||
typedef std::function<void()> ObsLoadedCallback;
|
||||
void SetObsLoadedCallback(ObsLoadedCallback cb);
|
||||
|
||||
void ProcessSubscription(uint64_t eventSubscriptions);
|
||||
void ProcessUnsubscription(uint64_t eventSubscriptions);
|
||||
|
||||
private:
|
||||
WebSocketServerPtr _webSocketServer;
|
||||
BroadcastCallback _broadcastCallback;
|
||||
ObsLoadedCallback _obsLoadedCallback;
|
||||
|
||||
std::atomic<bool> _obsLoaded;
|
||||
|
||||
std::unique_ptr<Utils::Obs::VolumeMeter::Handler> _inputVolumeMetersHandler;
|
||||
std::atomic<uint64_t> _inputVolumeMetersRef;
|
||||
std::atomic<uint64_t> _inputActiveStateChangedRef;
|
||||
std::atomic<uint64_t> _inputShowStateChangedRef;
|
||||
std::atomic<uint64_t> _sceneItemTransformChangedRef;
|
||||
|
||||
void ConnectSourceSignals(obs_source_t *source);
|
||||
void DisconnectSourceSignals(obs_source_t *source);
|
||||
|
||||
void BroadcastEvent(uint64_t requiredIntent, std::string eventType, json eventData = nullptr, uint8_t rpcVersion = 0);
|
||||
|
||||
// Signal handler: frontend
|
||||
static void OnFrontendEvent(enum obs_frontend_event event, void *private_data);
|
||||
|
||||
@ -52,8 +83,10 @@ class EventHandler
|
||||
void HandleStudioModeStateChanged(bool enabled);
|
||||
|
||||
// Config
|
||||
void HandleCurrentSceneCollectionChanging();
|
||||
void HandleCurrentSceneCollectionChanged();
|
||||
void HandleSceneCollectionListChanged();
|
||||
void HandleCurrentProfileChanging();
|
||||
void HandleCurrentProfileChanged();
|
||||
void HandleProfileListChanged();
|
||||
|
||||
@ -61,7 +94,7 @@ class EventHandler
|
||||
void HandleSceneCreated(obs_source_t *source);
|
||||
void HandleSceneRemoved(obs_source_t *source);
|
||||
void HandleSceneNameChanged(obs_source_t *source, std::string oldSceneName, std::string sceneName);
|
||||
void HandleCurrentSceneChanged();
|
||||
void HandleCurrentProgramSceneChanged();
|
||||
void HandleCurrentPreviewSceneChanged();
|
||||
void HandleSceneListChanged();
|
||||
|
||||
@ -69,6 +102,7 @@ class EventHandler
|
||||
void HandleInputCreated(obs_source_t *source);
|
||||
void HandleInputRemoved(obs_source_t *source);
|
||||
void HandleInputNameChanged(obs_source_t *source, std::string oldInputName, std::string inputName);
|
||||
void HandleInputVolumeMeters(std::vector<json> inputs); // AudioMeter::Handler callback
|
||||
static void HandleInputActiveStateChanged(void *param, calldata_t *data); // Direct callback
|
||||
static void HandleInputShowStateChanged(void *param, calldata_t *data); // Direct callback
|
||||
static void HandleInputMuteStateChanged(void *param, calldata_t *data); // Direct callback
|
||||
@ -77,11 +111,6 @@ class EventHandler
|
||||
static void HandleInputAudioTracksChanged(void *param, calldata_t *data); // Direct callback
|
||||
static void HandleInputAudioMonitorTypeChanged(void *param, calldata_t *data); // Direct callback
|
||||
|
||||
// Transitions
|
||||
void HandleTransitionCreated(obs_source_t *source);
|
||||
void HandleTransitionRemoved(obs_source_t *source);
|
||||
void HandleTransitionNameChanged(obs_source_t *source, std::string oldTransitionName, std::string transitionName);
|
||||
|
||||
// Outputs
|
||||
void HandleStreamStateChanged(ObsOutputState state);
|
||||
void HandleRecordStateChanged(ObsOutputState state);
|
||||
|
@ -1,30 +1,145 @@
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
/**
|
||||
* The current scene collection has begun changing.
|
||||
*
|
||||
* Note: We recommend using this event to trigger a pause of all polling requests, as performing any requests during a
|
||||
* scene collection change is considered undefined behavior and can cause crashes!
|
||||
*
|
||||
* @dataField sceneCollectionName | String | Name of the current scene collection
|
||||
*
|
||||
* @eventType CurrentSceneCollectionChanging
|
||||
* @eventSubscription Config
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleCurrentSceneCollectionChanging()
|
||||
{
|
||||
json eventData;
|
||||
eventData["sceneCollectionName"] = Utils::Obs::StringHelper::GetCurrentSceneCollection();
|
||||
BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanging", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current scene collection has changed.
|
||||
*
|
||||
* Note: If polling has been paused during `CurrentSceneCollectionChanging`, this is the que to restart polling.
|
||||
*
|
||||
* @dataField sceneCollectionName | String | Name of the new scene collection
|
||||
*
|
||||
* @eventType CurrentSceneCollectionChanged
|
||||
* @eventSubscription Config
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleCurrentSceneCollectionChanged()
|
||||
{
|
||||
json eventData;
|
||||
eventData["sceneCollectionName"] = Utils::Obs::StringHelper::GetCurrentSceneCollection();
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::Config, "CurrentSceneCollectionChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The scene collection list has changed.
|
||||
*
|
||||
* @dataField sceneCollections | Array<String> | Updated list of scene collections
|
||||
*
|
||||
* @eventType SceneCollectionListChanged
|
||||
* @eventSubscription Config
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleSceneCollectionListChanged()
|
||||
{
|
||||
json eventData;
|
||||
eventData["sceneCollections"] = Utils::Obs::ListHelper::GetSceneCollectionList();
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Config, "SceneCollectionListChanged", eventData);
|
||||
eventData["sceneCollections"] = Utils::Obs::ArrayHelper::GetSceneCollectionList();
|
||||
BroadcastEvent(EventSubscription::Config, "SceneCollectionListChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current profile has begun changing.
|
||||
*
|
||||
* @dataField profileName | String | Name of the current profile
|
||||
*
|
||||
* @eventType CurrentProfileChanging
|
||||
* @eventSubscription Config
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleCurrentProfileChanging()
|
||||
{
|
||||
json eventData;
|
||||
eventData["profileName"] = Utils::Obs::StringHelper::GetCurrentProfile();
|
||||
BroadcastEvent(EventSubscription::Config, "CurrentProfileChanging", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current profile has changed.
|
||||
*
|
||||
* @dataField profileName | String | Name of the new profile
|
||||
*
|
||||
* @eventType CurrentProfileChanged
|
||||
* @eventSubscription Config
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleCurrentProfileChanged()
|
||||
{
|
||||
json eventData;
|
||||
eventData["profileName"] = Utils::Obs::StringHelper::GetCurrentProfile();
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Config, "CurrentProfileChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::Config, "CurrentProfileChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile list has changed.
|
||||
*
|
||||
* @dataField profiles | Array<String> | Updated list of profiles
|
||||
*
|
||||
* @eventType ProfileListChanged
|
||||
* @eventSubscription Config
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleProfileListChanged()
|
||||
{
|
||||
json eventData;
|
||||
eventData["profiles"] = Utils::Obs::ListHelper::GetProfileList();
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Config, "ProfileListChanged", eventData);
|
||||
eventData["profiles"] = Utils::Obs::ArrayHelper::GetProfileList();
|
||||
BroadcastEvent(EventSubscription::Config, "ProfileListChanged", eventData);
|
||||
}
|
||||
|
@ -1,2 +1,20 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
@ -1,14 +1,36 @@
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
/**
|
||||
* OBS has begun the shutdown process.
|
||||
*
|
||||
* @eventType ExitStarted
|
||||
* @eventSubscription General
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleExitStarted()
|
||||
{
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::General, "ExitStarted");
|
||||
}
|
||||
|
||||
void EventHandler::HandleStudioModeStateChanged(bool enabled)
|
||||
{
|
||||
json eventData;
|
||||
eventData["studioModeEnabled"] = enabled;
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::General, "StudioModeStateChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::General, "ExitStarted");
|
||||
}
|
||||
|
@ -1,6 +1,41 @@
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
/**
|
||||
* An input has been created.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField inputKind | String | The kind of the input
|
||||
* @dataField unversionedInputKind | String | The unversioned kind of input (aka no `_v2` stuff)
|
||||
* @dataField inputSettings | Object | The settings configured to the input when it was created
|
||||
* @dataField defaultInputSettings | Object | The default settings for the input
|
||||
*
|
||||
* @eventType InputCreated
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputCreated(obs_source_t *source)
|
||||
{
|
||||
std::string inputKind = obs_source_get_id(source);
|
||||
@ -13,28 +48,74 @@ void EventHandler::HandleInputCreated(obs_source_t *source)
|
||||
eventData["unversionedInputKind"] = obs_source_get_unversioned_id(source);
|
||||
eventData["inputSettings"] = Utils::Json::ObsDataToJson(inputSettings);
|
||||
eventData["defaultInputSettings"] = Utils::Json::ObsDataToJson(defaultInputSettings, true);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputCreated", eventData);
|
||||
BroadcastEvent(EventSubscription::Inputs, "InputCreated", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input has been removed.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
*
|
||||
* @eventType InputRemoved
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputRemoved(obs_source_t *source)
|
||||
{
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputRemoved", eventData);
|
||||
BroadcastEvent(EventSubscription::Inputs, "InputRemoved", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleInputNameChanged(obs_source_t *source, std::string oldInputName, std::string inputName)
|
||||
/**
|
||||
* The name of an input has changed.
|
||||
*
|
||||
* @dataField oldInputName | String | Old name of the input
|
||||
* @dataField inputName | String | New name of the input
|
||||
*
|
||||
* @eventType InputNameChanged
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputNameChanged(obs_source_t *, std::string oldInputName, std::string inputName)
|
||||
{
|
||||
json eventData;
|
||||
eventData["oldInputName"] = oldInputName;
|
||||
eventData["inputName"] = inputName;
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputNameChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::Inputs, "InputNameChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input's active state has changed.
|
||||
*
|
||||
* When an input is active, it means it's being shown by the program feed.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField videoActive | Boolean | Whether the input is active
|
||||
*
|
||||
* @eventType InputActiveStateChanged
|
||||
* @eventSubscription InputActiveStateChanged
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputActiveStateChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
|
||||
if (!eventHandler->_inputActiveStateChangedRef.load())
|
||||
return;
|
||||
|
||||
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
|
||||
if (!source)
|
||||
return;
|
||||
@ -45,13 +126,32 @@ void EventHandler::HandleInputActiveStateChanged(void *param, calldata_t *data)
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["videoActive"] = obs_source_active(source);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::InputActiveStateChanged, "InputActiveStateChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::InputActiveStateChanged, "InputActiveStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input's show state has changed.
|
||||
*
|
||||
* When an input is showing, it means it's being shown by the preview or a dialog.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField videoShowing | Boolean | Whether the input is showing
|
||||
*
|
||||
* @eventType InputShowStateChanged
|
||||
* @eventSubscription InputShowStateChanged
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputShowStateChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
|
||||
if (!eventHandler->_inputShowStateChangedRef.load())
|
||||
return;
|
||||
|
||||
obs_source_t *source = GetCalldataPointer<obs_source_t>(data, "source");
|
||||
if (!source)
|
||||
return;
|
||||
@ -62,9 +162,23 @@ void EventHandler::HandleInputShowStateChanged(void *param, calldata_t *data)
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["videoShowing"] = obs_source_showing(source);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::InputShowStateChanged, "InputShowStateChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::InputShowStateChanged, "InputShowStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input's mute state has changed.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField inputMuted | Boolean | Whether the input is muted
|
||||
*
|
||||
* @eventType InputMuteStateChanged
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputMuteStateChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -79,9 +193,24 @@ void EventHandler::HandleInputMuteStateChanged(void *param, calldata_t *data)
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["inputMuted"] = obs_source_muted(source);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputMuteStateChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputMuteStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* An input's volume level has changed.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField inputVolumeMul | Number | New volume level in multimap
|
||||
* @dataField inputVolumeDb | Number | New volume level in dB
|
||||
*
|
||||
* @eventType InputVolumeChanged
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputVolumeChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -104,9 +233,23 @@ void EventHandler::HandleInputVolumeChanged(void *param, calldata_t *data)
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["inputVolumeMul"] = inputVolumeMul;
|
||||
eventData["inputVolumeDb"] = inputVolumeDb;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputVolumeChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputVolumeChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The sync offset of an input has changed.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField inputAudioSyncOffset | Number | New sync offset in milliseconds
|
||||
*
|
||||
* @eventType InputAudioSyncOffsetChanged
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputAudioSyncOffsetChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -123,9 +266,23 @@ void EventHandler::HandleInputAudioSyncOffsetChanged(void *param, calldata_t *da
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["inputAudioSyncOffset"] = inputAudioSyncOffset / 1000000;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputAudioSyncOffsetChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioSyncOffsetChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio tracks of an input have changed.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField inputAudioTracks | Array<Boolean> | Array of audio tracks along with their associated enable states
|
||||
*
|
||||
* @eventType InputAudioTracksChanged
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputAudioTracksChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -140,16 +297,35 @@ void EventHandler::HandleInputAudioTracksChanged(void *param, calldata_t *data)
|
||||
long long tracks = calldata_int(data, "mixers");
|
||||
|
||||
json inputAudioTracks;
|
||||
for (size_t i = 0; i < MAX_AUDIO_MIXES; i++) {
|
||||
inputAudioTracks[std::to_string(i + 1)] = (bool)((1 << i) & tracks);
|
||||
for (long long i = 0; i < MAX_AUDIO_MIXES; i++) {
|
||||
inputAudioTracks[std::to_string(i + 1)] = (bool)((tracks >> i) & 1);
|
||||
}
|
||||
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["inputAudioTracks"] = inputAudioTracks;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputAudioTracksChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioTracksChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The monitor type of an input has changed.
|
||||
*
|
||||
* Available types are:
|
||||
* - `OBS_MONITORING_TYPE_NONE`
|
||||
* - `OBS_MONITORING_TYPE_MONITOR_ONLY`
|
||||
* - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT`
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField monitorType | String | New monitor type of the input
|
||||
*
|
||||
* @eventType InputAudioMonitorTypeChanged
|
||||
* @eventSubscription Inputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputAudioMonitorTypeChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -163,22 +339,30 @@ void EventHandler::HandleInputAudioMonitorTypeChanged(void *param, calldata_t *d
|
||||
|
||||
enum obs_monitoring_type monitorType = (obs_monitoring_type)calldata_int(data, "type");
|
||||
|
||||
std::string monitorTypeString;
|
||||
switch (monitorType) {
|
||||
default:
|
||||
case OBS_MONITORING_TYPE_NONE:
|
||||
monitorTypeString = "OBS_WEBSOCKET_MONITOR_TYPE_NONE";
|
||||
break;
|
||||
case OBS_MONITORING_TYPE_MONITOR_ONLY:
|
||||
monitorTypeString = "OBS_WEBSOCKET_MONITOR_TYPE_MONITOR_ONLY";
|
||||
break;
|
||||
case OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT:
|
||||
monitorTypeString = "OBS_WEBSOCKET_MONITOR_TYPE_MONITOR_AND_OUTPUT";
|
||||
break;
|
||||
}
|
||||
std::string monitorTypeString = Utils::Obs::StringHelper::GetInputMonitorType(monitorType);
|
||||
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["monitorType"] = monitorTypeString;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::Inputs, "InputAudioMonitorTypeChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::Inputs, "InputAudioMonitorTypeChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* A high-volume event providing volume levels of all active inputs every 50 milliseconds.
|
||||
*
|
||||
* @dataField inputs | Array<Object> | Array of active inputs with their associated volume levels
|
||||
*
|
||||
* @eventType InputVolumeMeters
|
||||
* @eventSubscription InputVolumeMeters
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category inputs
|
||||
*/
|
||||
void EventHandler::HandleInputVolumeMeters(std::vector<json> inputs)
|
||||
{
|
||||
json eventData;
|
||||
eventData["inputs"] = inputs;
|
||||
BroadcastEvent(EventSubscription::InputVolumeMeters, "InputVolumeMeters", eventData);
|
||||
}
|
||||
|
@ -1,5 +1,23 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
#define CASE(x) case x: return #x;
|
||||
|
||||
@ -99,6 +117,19 @@ void EventHandler::SourceMediaPreviousMultiHandler(void *param, calldata_t *data
|
||||
eventHandler->HandleMediaInputActionTriggered(source, OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* A media input has started playing.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
*
|
||||
* @eventType MediaInputPlaybackStarted
|
||||
* @eventSubscription MediaInputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category media inputs
|
||||
*/
|
||||
void EventHandler::HandleMediaInputPlaybackStarted(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -112,9 +143,22 @@ void EventHandler::HandleMediaInputPlaybackStarted(void *param, calldata_t *data
|
||||
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackStarted", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackStarted", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* A media input has finished playing.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
*
|
||||
* @eventType MediaInputPlaybackEnded
|
||||
* @eventSubscription MediaInputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category media inputs
|
||||
*/
|
||||
void EventHandler::HandleMediaInputPlaybackEnded(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -128,13 +172,27 @@ void EventHandler::HandleMediaInputPlaybackEnded(void *param, calldata_t *data)
|
||||
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackEnded", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputPlaybackEnded", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* An action has been performed on an input.
|
||||
*
|
||||
* @dataField inputName | String | Name of the input
|
||||
* @dataField mediaAction | String | Action performed on the input. See `ObsMediaInputAction` enum
|
||||
*
|
||||
* @eventType MediaInputActionTriggered
|
||||
* @eventSubscription MediaInputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category media inputs
|
||||
*/
|
||||
void EventHandler::HandleMediaInputActionTriggered(obs_source_t *source, ObsMediaInputAction action)
|
||||
{
|
||||
json eventData;
|
||||
eventData["inputName"] = obs_source_get_name(source);
|
||||
eventData["mediaAction"] = GetMediaInputActionString(action);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::MediaInputs, "MediaInputActionTriggered", eventData);
|
||||
BroadcastEvent(EventSubscription::MediaInputs, "MediaInputActionTriggered", eventData);
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
#define CASE(x) case x: return #x;
|
||||
|
||||
std::string GetOutputStateString(ObsOutputState state) {
|
||||
switch (state) {
|
||||
default:
|
||||
CASE(OBS_WEBSOCKET_OUTPUT_STARTING)
|
||||
CASE(OBS_WEBSOCKET_OUTPUT_STARTED)
|
||||
CASE(OBS_WEBSOCKET_OUTPUT_STOPPING)
|
||||
CASE(OBS_WEBSOCKET_OUTPUT_STOPPED)
|
||||
CASE(OBS_WEBSOCKET_OUTPUT_PAUSED)
|
||||
CASE(OBS_WEBSOCKET_OUTPUT_RESUMED)
|
||||
}
|
||||
}
|
||||
|
||||
bool GetOutputStateActive(ObsOutputState state) {
|
||||
static bool GetOutputStateActive(ObsOutputState state) {
|
||||
switch(state) {
|
||||
case OBS_WEBSOCKET_OUTPUT_STARTED:
|
||||
case OBS_WEBSOCKET_OUTPUT_RESUMED:
|
||||
@ -30,41 +34,110 @@ bool GetOutputStateActive(ObsOutputState state) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the stream output has changed.
|
||||
*
|
||||
* @dataField outputActive | Boolean | Whether the output is active
|
||||
* @dataField outputState | String | The specific state of the output
|
||||
*
|
||||
* @eventType StreamStateChanged
|
||||
* @eventSubscription Outputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category outputs
|
||||
*/
|
||||
void EventHandler::HandleStreamStateChanged(ObsOutputState state)
|
||||
{
|
||||
json eventData;
|
||||
eventData["outputActive"] = GetOutputStateActive(state);
|
||||
eventData["outputState"] = GetOutputStateString(state);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "StreamStateChanged", eventData);
|
||||
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
|
||||
BroadcastEvent(EventSubscription::Outputs, "StreamStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the record output has changed.
|
||||
*
|
||||
* @dataField outputActive | Boolean | Whether the output is active
|
||||
* @dataField outputState | String | The specific state of the output
|
||||
*
|
||||
* @eventType RecordStateChanged
|
||||
* @eventSubscription Outputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category outputs
|
||||
*/
|
||||
void EventHandler::HandleRecordStateChanged(ObsOutputState state)
|
||||
{
|
||||
json eventData;
|
||||
eventData["outputActive"] = GetOutputStateActive(state);
|
||||
eventData["outputState"] = GetOutputStateString(state);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "RecordStateChanged", eventData);
|
||||
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
|
||||
BroadcastEvent(EventSubscription::Outputs, "RecordStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the replay buffer output has changed.
|
||||
*
|
||||
* @dataField outputActive | Boolean | Whether the output is active
|
||||
* @dataField outputState | String | The specific state of the output
|
||||
*
|
||||
* @eventType ReplayBufferStateChanged
|
||||
* @eventSubscription Outputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category outputs
|
||||
*/
|
||||
void EventHandler::HandleReplayBufferStateChanged(ObsOutputState state)
|
||||
{
|
||||
json eventData;
|
||||
eventData["outputActive"] = GetOutputStateActive(state);
|
||||
eventData["outputState"] = GetOutputStateString(state);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "ReplayBufferStateChanged", eventData);
|
||||
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
|
||||
BroadcastEvent(EventSubscription::Outputs, "ReplayBufferStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the virtualcam output has changed.
|
||||
*
|
||||
* @dataField outputActive | Boolean | Whether the output is active
|
||||
* @dataField outputState | String | The specific state of the output
|
||||
*
|
||||
* @eventType VirtualcamStateChanged
|
||||
* @eventSubscription Outputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category outputs
|
||||
*/
|
||||
void EventHandler::HandleVirtualcamStateChanged(ObsOutputState state)
|
||||
{
|
||||
json eventData;
|
||||
eventData["outputActive"] = GetOutputStateActive(state);
|
||||
eventData["outputState"] = GetOutputStateString(state);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "VirtualcamStateChanged", eventData);
|
||||
eventData["outputState"] = Utils::Obs::StringHelper::GetOutputState(state);
|
||||
BroadcastEvent(EventSubscription::Outputs, "VirtualcamStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The replay buffer has been saved.
|
||||
*
|
||||
* @dataField savedReplayPath | String | Path of the saved replay file
|
||||
*
|
||||
* @eventType ReplayBufferSaved
|
||||
* @eventSubscription Outputs
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category outputs
|
||||
*/
|
||||
void EventHandler::HandleReplayBufferSaved()
|
||||
{
|
||||
json eventData;
|
||||
eventData["savedReplayPath"] = Utils::Obs::StringHelper::GetLastReplayBufferFilePath();
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Outputs, "ReplayBufferSaved", eventData);
|
||||
BroadcastEvent(EventSubscription::Outputs, "ReplayBufferSaved", eventData);
|
||||
}
|
||||
|
@ -1,6 +1,40 @@
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
/**
|
||||
* A scene item has been created.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene the item was added to
|
||||
* @dataField sourceName | String | Name of the underlying source (input/scene)
|
||||
* @dataField sceneItemId | Number | Numeric ID of the scene item
|
||||
* @dataField sceneItemIndex | Number | Index position of the item
|
||||
*
|
||||
* @eventType SceneItemCreated
|
||||
* @eventSubscription SceneItems
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scene items
|
||||
*/
|
||||
void EventHandler::HandleSceneItemCreated(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -15,13 +49,29 @@ void EventHandler::HandleSceneItemCreated(void *param, calldata_t *data)
|
||||
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
|
||||
eventData["inputName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
|
||||
eventData["sourceName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
|
||||
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
eventData["sceneItemIndex"] = obs_sceneitem_get_order_position(sceneItem);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemCreated", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemCreated", eventData);
|
||||
}
|
||||
|
||||
// Will not be emitted if an item is removed due to the parent scene being removed.
|
||||
/**
|
||||
* A scene item has been removed.
|
||||
*
|
||||
* This event is not emitted when the scene the item is in is removed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene the item was removed from
|
||||
* @dataField sourceName | String | Name of the underlying source (input/scene)
|
||||
* @dataField sceneItemId | Number | Numeric ID of the scene item
|
||||
*
|
||||
* @eventType SceneItemRemoved
|
||||
* @eventSubscription SceneItems
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scene items
|
||||
*/
|
||||
void EventHandler::HandleSceneItemRemoved(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -36,12 +86,25 @@ void EventHandler::HandleSceneItemRemoved(void *param, calldata_t *data)
|
||||
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
|
||||
eventData["inputName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
|
||||
eventData["sourceName"] = obs_source_get_name(obs_sceneitem_get_source(sceneItem));
|
||||
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
eventData["sceneItemIndex"] = obs_sceneitem_get_order_position(sceneItem);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemRemoved", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemRemoved", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* A scene's item list has been reindexed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene
|
||||
* @dataField sceneItems | Array<Object> | Array of scene item objects
|
||||
*
|
||||
* @eventType SceneItemListReindexed
|
||||
* @eventSubscription SceneItems
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scene items
|
||||
*/
|
||||
void EventHandler::HandleSceneItemListReindexed(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -52,10 +115,25 @@ void EventHandler::HandleSceneItemListReindexed(void *param, calldata_t *data)
|
||||
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
|
||||
eventData["sceneItems"] = Utils::Obs::ListHelper::GetSceneItemList(scene, true);
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemReindexed", eventData);
|
||||
eventData["sceneItems"] = Utils::Obs::ArrayHelper::GetSceneItemList(scene, true);
|
||||
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemListReindexed", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* A scene item's enable state has changed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene the item is in
|
||||
* @dataField sceneItemId | Number | Numeric ID of the scene item
|
||||
* @dataField sceneItemEnabled | Boolean | Whether the scene item is enabled (visible)
|
||||
*
|
||||
* @eventType SceneItemEnableStateChanged
|
||||
* @eventSubscription SceneItems
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scene items
|
||||
*/
|
||||
void EventHandler::HandleSceneItemEnableStateChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -74,9 +152,24 @@ void EventHandler::HandleSceneItemEnableStateChanged(void *param, calldata_t *da
|
||||
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
|
||||
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
eventData["sceneItemEnabled"] = sceneItemEnabled;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemEnableStateChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemEnableStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* A scene item's lock state has changed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene the item is in
|
||||
* @dataField sceneItemId | Number | Numeric ID of the scene item
|
||||
* @dataField sceneItemEnabled | Boolean | Whether the scene item is locked
|
||||
*
|
||||
* @eventType SceneItemLockStateChanged
|
||||
* @eventSubscription SceneItems
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scene items
|
||||
*/
|
||||
void EventHandler::HandleSceneItemLockStateChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
@ -95,13 +188,42 @@ void EventHandler::HandleSceneItemLockStateChanged(void *param, calldata_t *data
|
||||
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
|
||||
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
eventData["sceneItemLocked"] = sceneItemLocked;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemLockStateChanged", eventData);
|
||||
eventHandler->BroadcastEvent(EventSubscription::SceneItems, "SceneItemLockStateChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The transform/crop of a scene item has changed.
|
||||
*
|
||||
* @dataField sceneName | String | The name of the scene the item is in
|
||||
* @dataField sceneItemId | Number | Numeric ID of the scene item
|
||||
* @dataField sceneItemTransform | Object | New transform/crop info of the scene item
|
||||
*
|
||||
* @eventType SceneItemTransformChanged
|
||||
* @eventSubscription SceneItemTransformChanged
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scene items
|
||||
*/
|
||||
void EventHandler::HandleSceneItemTransformChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
|
||||
if (!eventHandler->_sceneItemTransformChangedRef.load())
|
||||
return;
|
||||
|
||||
obs_scene_t *scene = GetCalldataPointer<obs_scene_t>(data, "scene");
|
||||
if (!scene)
|
||||
return;
|
||||
|
||||
obs_sceneitem_t *sceneItem = GetCalldataPointer<obs_sceneitem_t>(data, "item");
|
||||
if (!sceneItem)
|
||||
return;
|
||||
|
||||
json eventData;
|
||||
eventHandler->_webSocketServer->BroadcastEvent(EventSubscription::SceneItems, "SceneItemTransformChanged", eventData);
|
||||
eventData["sceneName"] = obs_source_get_name(obs_scene_get_source(scene));
|
||||
eventData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
eventData["sceneItemTransform"] = Utils::Obs::ObjectHelper::GetSceneItemTransform(sceneItem);
|
||||
eventHandler->BroadcastEvent(EventSubscription::SceneItemTransformChanged, "SceneItemTransformChanged", eventData);
|
||||
}
|
||||
|
@ -1,39 +1,125 @@
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
/**
|
||||
* A new scene has been created.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the new scene
|
||||
* @dataField isGroup | Boolean | Whether the new scene is a group
|
||||
*
|
||||
* @eventType SceneCreated
|
||||
* @eventSubscription Scenes
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scenes
|
||||
*/
|
||||
void EventHandler::HandleSceneCreated(obs_source_t *source)
|
||||
{
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(source);
|
||||
eventData["isGroup"] = obs_source_is_group(source);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneCreated", eventData);
|
||||
BroadcastEvent(EventSubscription::Scenes, "SceneCreated", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* A scene has been removed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the removed scene
|
||||
* @dataField isGroup | Boolean | Whether the scene was a group
|
||||
*
|
||||
* @eventType SceneRemoved
|
||||
* @eventSubscription Scenes
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scenes
|
||||
*/
|
||||
void EventHandler::HandleSceneRemoved(obs_source_t *source)
|
||||
{
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(source);
|
||||
eventData["isGroup"] = obs_source_is_group(source);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneRemoved", eventData);
|
||||
BroadcastEvent(EventSubscription::Scenes, "SceneRemoved", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleSceneNameChanged(obs_source_t *source, std::string oldSceneName, std::string sceneName)
|
||||
/**
|
||||
* The name of a scene has changed.
|
||||
*
|
||||
* @dataField oldSceneName | String | Old name of the scene
|
||||
* @dataField sceneName | String | New name of the scene
|
||||
*
|
||||
* @eventType SceneNameChanged
|
||||
* @eventSubscription Scenes
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scenes
|
||||
*/
|
||||
void EventHandler::HandleSceneNameChanged(obs_source_t *, std::string oldSceneName, std::string sceneName)
|
||||
{
|
||||
json eventData;
|
||||
eventData["oldSceneName"] = oldSceneName;
|
||||
eventData["sceneName"] = sceneName;
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneNameChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::Scenes, "SceneNameChanged", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleCurrentSceneChanged()
|
||||
/**
|
||||
* The current program scene has changed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene that was switched to
|
||||
*
|
||||
* @eventType CurrentProgramSceneChanged
|
||||
* @eventSubscription Scenes
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scenes
|
||||
*/
|
||||
void EventHandler::HandleCurrentProgramSceneChanged()
|
||||
{
|
||||
OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene();
|
||||
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(currentScene);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "CurrentSceneChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::Scenes, "CurrentProgramSceneChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The current preview scene has changed.
|
||||
*
|
||||
* @dataField sceneName | String | Name of the scene that was switched to
|
||||
*
|
||||
* @eventType CurrentPreviewSceneChanged
|
||||
* @eventSubscription Scenes
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scenes
|
||||
*/
|
||||
void EventHandler::HandleCurrentPreviewSceneChanged()
|
||||
{
|
||||
OBSSourceAutoRelease currentPreviewScene = obs_frontend_get_current_preview_scene();
|
||||
@ -44,12 +130,27 @@ void EventHandler::HandleCurrentPreviewSceneChanged()
|
||||
|
||||
json eventData;
|
||||
eventData["sceneName"] = obs_source_get_name(currentPreviewScene);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "CurrentPreviewSceneChanged", eventData);
|
||||
BroadcastEvent(EventSubscription::Scenes, "CurrentPreviewSceneChanged", eventData);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of scenes has changed.
|
||||
*
|
||||
* TODO: Make OBS fire this event when scenes are reordered.
|
||||
*
|
||||
* @dataField scenes | Array<Object> | Updated array of scenes
|
||||
*
|
||||
* @eventType SceneListChanged
|
||||
* @eventSubscription Scenes
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category scenes
|
||||
*/
|
||||
void EventHandler::HandleSceneListChanged()
|
||||
{
|
||||
json eventData;
|
||||
eventData["scenes"] = Utils::Obs::ListHelper::GetSceneList();
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Scenes, "SceneListChanged", eventData);
|
||||
eventData["scenes"] = Utils::Obs::ArrayHelper::GetSceneList();
|
||||
BroadcastEvent(EventSubscription::Scenes, "SceneListChanged", eventData);
|
||||
}
|
||||
|
@ -1,26 +1,20 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
void EventHandler::HandleTransitionCreated(obs_source_t *source)
|
||||
{
|
||||
json eventData;
|
||||
eventData["transitionName"] = obs_source_get_name(source);
|
||||
eventData["transitionKind"] = obs_source_get_id(source);
|
||||
eventData["transitionFixed"] = obs_transition_fixed(source);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Transitions, "TransitionCreated", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleTransitionRemoved(obs_source_t *source)
|
||||
{
|
||||
json eventData;
|
||||
eventData["transitionName"] = obs_source_get_name(source);
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Transitions, "TransitionRemoved", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleTransitionNameChanged(obs_source_t *source, std::string oldTransitionName, std::string transitionName)
|
||||
{
|
||||
json eventData;
|
||||
eventData["oldTransitionName"] = oldTransitionName;
|
||||
eventData["transitionName"] = transitionName;
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Transitions, "TransitionNameChanged", eventData);
|
||||
}
|
||||
|
40
src/eventhandler/EventHandler_Ui.cpp
Normal file
40
src/eventhandler/EventHandler_Ui.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
||||
/**
|
||||
* Studio mode has been enabled or disabled.
|
||||
*
|
||||
* @dataField studioModeEnabled | Boolean | True == Enabled, False == Disabled
|
||||
*
|
||||
* @eventType StudioModeStateChanged
|
||||
* @eventSubscription Ui
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category ui
|
||||
* @api events
|
||||
*/
|
||||
void EventHandler::HandleStudioModeStateChanged(bool enabled)
|
||||
{
|
||||
json eventData;
|
||||
eventData["studioModeEnabled"] = enabled;
|
||||
BroadcastEvent(EventSubscription::Ui, "StudioModeStateChanged", eventData);
|
||||
}
|
@ -1,34 +1,212 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace EventSubscription {
|
||||
enum EventSubscription {
|
||||
// Set subscriptions to 0 to disable all events
|
||||
/**
|
||||
* Subcription value used to disable all events.
|
||||
*
|
||||
* @enumIdentifier None
|
||||
* @enumValue 0
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
None = 0,
|
||||
// Receive events in the `General` category
|
||||
/**
|
||||
* Subscription value to receive events in the `General` category.
|
||||
*
|
||||
* @enumIdentifier General
|
||||
* @enumValue (1 << 0)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
General = (1 << 0),
|
||||
// Receive events in the `Config` category
|
||||
/**
|
||||
* Subscription value to receive events in the `Config` category.
|
||||
*
|
||||
* @enumIdentifier Config
|
||||
* @enumValue (1 << 1)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Config = (1 << 1),
|
||||
// Receive events in the `Scenes` category
|
||||
/**
|
||||
* Subscription value to receive events in the `Scenes` category.
|
||||
*
|
||||
* @enumIdentifier Scenes
|
||||
* @enumValue (1 << 2)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Scenes = (1 << 2),
|
||||
// Receive events in the `Inputs` category
|
||||
/**
|
||||
* Subscription value to receive events in the `Inputs` category.
|
||||
*
|
||||
* @enumIdentifier Inputs
|
||||
* @enumValue (1 << 3)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Inputs = (1 << 3),
|
||||
// Receive events in the `Transitions` category
|
||||
/**
|
||||
* Subscription value to receive events in the `Transitions` category.
|
||||
*
|
||||
* @enumIdentifier Transitions
|
||||
* @enumValue (1 << 4)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Transitions = (1 << 4),
|
||||
// Receive events in the `Filters` category
|
||||
/**
|
||||
* Subscription value to receive events in the `Filters` category.
|
||||
*
|
||||
* @enumIdentifier Filters
|
||||
* @enumValue (1 << 5)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Filters = (1 << 5),
|
||||
// Receive events in the `Outputs` category
|
||||
/**
|
||||
* Subscription value to receive events in the `Outputs` category.
|
||||
*
|
||||
* @enumIdentifier Outputs
|
||||
* @enumValue (1 << 6)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Outputs = (1 << 6),
|
||||
// Receive events in the `Scene Items` category
|
||||
/**
|
||||
* Subscription value to receive events in the `SceneItems` category.
|
||||
*
|
||||
* @enumIdentifier SceneItems
|
||||
* @enumValue (1 << 7)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
SceneItems = (1 << 7),
|
||||
// Receive events in the `MediaInputs` category
|
||||
/**
|
||||
* Subscription value to receive events in the `MediaInputs` category.
|
||||
*
|
||||
* @enumIdentifier MediaInputs
|
||||
* @enumValue (1 << 8)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
MediaInputs = (1 << 8),
|
||||
// Receive all event categories
|
||||
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs),
|
||||
// InputVolumeMeters event (high-volume)
|
||||
InputVolumeMeters = (1 << 9),
|
||||
// InputActiveStateChanged event (high-volume)
|
||||
InputActiveStateChanged = (1 << 10),
|
||||
// InputShowStateChanged event (high-volume)
|
||||
InputShowStateChanged = (1 << 11),
|
||||
/**
|
||||
* Subscription value to receive the `VendorEvent` event.
|
||||
*
|
||||
* @enumIdentifier Vendors
|
||||
* @enumValue (1 << 9)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Vendors = (1 << 9),
|
||||
/**
|
||||
* Subscription value to receive events in the `Ui` category.
|
||||
*
|
||||
* @enumIdentifier Ui
|
||||
* @enumValue (1 << 10)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Ui = (1 << 10),
|
||||
/**
|
||||
* Helper to receive all non-high-volume events.
|
||||
*
|
||||
* @enumIdentifier All
|
||||
* @enumValue (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | Vendors)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
All = (General | Config | Scenes | Inputs | Transitions | Filters | Outputs | SceneItems | MediaInputs | Ui | Vendors),
|
||||
/**
|
||||
* Subscription value to receive the `InputVolumeMeters` high-volume event.
|
||||
*
|
||||
* @enumIdentifier InputVolumeMeters
|
||||
* @enumValue (1 << 16)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
InputVolumeMeters = (1 << 16),
|
||||
/**
|
||||
* Subscription value to receive the `InputActiveStateChanged` high-volume event.
|
||||
*
|
||||
* @enumIdentifier InputActiveStateChanged
|
||||
* @enumValue (1 << 17)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
InputActiveStateChanged = (1 << 17),
|
||||
/**
|
||||
* Subscription value to receive the `InputShowStateChanged` high-volume event.
|
||||
*
|
||||
* @enumIdentifier InputShowStateChanged
|
||||
* @enumValue (1 << 18)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
InputShowStateChanged = (1 << 18),
|
||||
/**
|
||||
* Subscription value to receive the `SceneItemTransformChanged` high-volume event.
|
||||
*
|
||||
* @enumIdentifier SceneItemTransformChanged
|
||||
* @enumValue (1 << 19)
|
||||
* @enumType EventSubscription
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
SceneItemTransformChanged = (1 << 19),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
@ -1,3 +1,22 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QPainter>
|
||||
#include <QUrl>
|
||||
@ -8,7 +27,6 @@
|
||||
#include "../obs-websocket.h"
|
||||
#include "../Config.h"
|
||||
#include "../utils/Platform.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
ConnectInfo::ConnectInfo(QWidget* parent) :
|
||||
QDialog(parent, Qt::Dialog),
|
||||
@ -29,7 +47,12 @@ ConnectInfo::~ConnectInfo()
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void ConnectInfo::showEvent(QShowEvent *event)
|
||||
void ConnectInfo::showEvent(QShowEvent *)
|
||||
{
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
void ConnectInfo::RefreshData()
|
||||
{
|
||||
auto conf = GetConfig();
|
||||
if (!conf) {
|
||||
|
@ -1,7 +1,28 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
#include "ui_ConnectInfo.h"
|
||||
|
||||
class ConnectInfo : public QDialog
|
||||
@ -12,6 +33,7 @@ public:
|
||||
explicit ConnectInfo(QWidget* parent = 0);
|
||||
~ConnectInfo();
|
||||
void showEvent(QShowEvent *event);
|
||||
void RefreshData();
|
||||
|
||||
private Q_SLOTS:
|
||||
void CopyServerIpButtonClicked();
|
||||
|
@ -1,3 +1,22 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QDateTime>
|
||||
#include <QTime>
|
||||
@ -7,9 +26,8 @@
|
||||
#include "SettingsDialog.h"
|
||||
#include "../obs-websocket.h"
|
||||
#include "../Config.h"
|
||||
#include "../WebSocketServer.h"
|
||||
#include "../websocketserver/WebSocketServer.h"
|
||||
#include "../utils/Crypto.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
QString GetToolTipIconHtml()
|
||||
{
|
||||
@ -57,7 +75,7 @@ SettingsDialog::~SettingsDialog()
|
||||
delete sessionTableTimer;
|
||||
}
|
||||
|
||||
void SettingsDialog::showEvent(QShowEvent *event)
|
||||
void SettingsDialog::showEvent(QShowEvent *)
|
||||
{
|
||||
auto conf = GetConfig();
|
||||
if (!conf) {
|
||||
@ -91,7 +109,7 @@ void SettingsDialog::showEvent(QShowEvent *event)
|
||||
sessionTableTimer->start(1000);
|
||||
}
|
||||
|
||||
void SettingsDialog::closeEvent(QCloseEvent *event)
|
||||
void SettingsDialog::hideEvent(QHideEvent *)
|
||||
{
|
||||
if (sessionTableTimer->isActive())
|
||||
sessionTableTimer->stop();
|
||||
@ -118,14 +136,21 @@ void SettingsDialog::DialogButtonClicked(QAbstractButton *button)
|
||||
|
||||
void SettingsDialog::SaveFormData()
|
||||
{
|
||||
connectInfo->hide();
|
||||
|
||||
auto conf = GetConfig();
|
||||
if (!conf) {
|
||||
blog(LOG_ERROR, "[SettingsDialog::SaveFormData] Unable to retreive config!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ui->serverPasswordLineEdit->text().length() < 6) {
|
||||
QMessageBox msgBox;
|
||||
msgBox.setWindowTitle(obs_module_text("OBSWebSocket.Settings.Save.PasswordInvalidErrorTitle"));
|
||||
msgBox.setText(obs_module_text("OBSWebSocket.Settings.Save.PasswordInvalidErrorMessage"));
|
||||
msgBox.setStandardButtons(QMessageBox::Ok);
|
||||
msgBox.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show a confirmation box to the user if they attempt to provide their own password
|
||||
if (passwordManuallyEdited && (conf->ServerPassword != ui->serverPasswordLineEdit->text())) {
|
||||
QMessageBox msgBox;
|
||||
@ -146,14 +171,9 @@ void SettingsDialog::SaveFormData()
|
||||
}
|
||||
}
|
||||
|
||||
bool needsRestart = false;
|
||||
if ((conf->ServerEnabled != ui->enableWebSocketServerCheckBox->isChecked()) ||
|
||||
(conf->DebugEnabled != ui->enableDebugLoggingCheckBox->isChecked()) ||
|
||||
(conf->AuthRequired != ui->enableAuthenticationCheckBox->isChecked()) ||
|
||||
(conf->ServerPassword != ui->serverPasswordLineEdit->text()) ||
|
||||
(conf->ServerPort != ui->serverPortSpinBox->value())) {
|
||||
needsRestart = true;
|
||||
}
|
||||
bool needsRestart = (conf->ServerEnabled != ui->enableWebSocketServerCheckBox->isChecked()) ||
|
||||
(ui->enableAuthenticationCheckBox->isChecked() && conf->ServerPassword != ui->serverPasswordLineEdit->text()) ||
|
||||
(conf->ServerPort != ui->serverPortSpinBox->value());
|
||||
|
||||
conf->ServerEnabled = ui->enableWebSocketServerCheckBox->isChecked();
|
||||
conf->AlertsEnabled = ui->enableSystemTrayAlertsCheckBox->isChecked();
|
||||
@ -164,6 +184,8 @@ void SettingsDialog::SaveFormData()
|
||||
|
||||
conf->Save();
|
||||
|
||||
connectInfo->RefreshData();
|
||||
|
||||
if (needsRestart) {
|
||||
blog(LOG_INFO, "[SettingsDialog::SaveFormData] A setting was changed which requires a server restart.");
|
||||
auto server = GetWebSocketServer();
|
||||
@ -245,7 +267,7 @@ void SettingsDialog::EnableAuthenticationCheckBoxChanged()
|
||||
|
||||
void SettingsDialog::GeneratePasswordButtonClicked()
|
||||
{
|
||||
QString newPassword = Utils::Crypto::GeneratePassword();
|
||||
QString newPassword = QString::fromStdString(Utils::Crypto::GeneratePassword());
|
||||
ui->serverPasswordLineEdit->setText(newPassword);
|
||||
ui->serverPasswordLineEdit->selectAll();
|
||||
passwordManuallyEdited = false;
|
||||
|
@ -1,10 +1,31 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTimer>
|
||||
|
||||
#include "ui_SettingsDialog.h"
|
||||
#include "ConnectInfo.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
#include "ui_SettingsDialog.h"
|
||||
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
@ -14,7 +35,7 @@ public:
|
||||
explicit SettingsDialog(QWidget* parent = 0);
|
||||
~SettingsDialog();
|
||||
void showEvent(QShowEvent *event);
|
||||
void closeEvent(QCloseEvent *event);
|
||||
void hideEvent(QHideEvent *event);
|
||||
void ToggleShowHide();
|
||||
|
||||
private Q_SLOTS:
|
||||
|
@ -1,70 +1,83 @@
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtWidgets/QAction>
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QTime>
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <QAction>
|
||||
#include <QMainWindow>
|
||||
#include <obs-module.h>
|
||||
#include <obs-data.h>
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include "plugin-macros.generated.h"
|
||||
#include "obs-websocket.h"
|
||||
#include "Config.h"
|
||||
#include "WebSocketServer.h"
|
||||
#include "WebSocketApi.h"
|
||||
#include "websocketserver/WebSocketServer.h"
|
||||
#include "eventhandler/EventHandler.h"
|
||||
#include "forms/SettingsDialog.h"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US")
|
||||
OBS_MODULE_AUTHOR("OBSProject")
|
||||
const char *obs_module_name(void) { return "obs-websocket"; }
|
||||
const char *obs_module_description(void) { return obs_module_text("OBSWebSocket.Plugin.Description"); }
|
||||
|
||||
ConfigPtr _config;
|
||||
WebSocketServerPtr _webSocketServer;
|
||||
EventHandlerPtr _eventHandler;
|
||||
SettingsDialog *_settingsDialog = nullptr;
|
||||
os_cpu_usage_info_t* _cpuUsageInfo;
|
||||
ConfigPtr _config;
|
||||
EventHandlerPtr _eventHandler;
|
||||
WebSocketApiPtr _webSocketApi;
|
||||
WebSocketServerPtr _webSocketServer;
|
||||
SettingsDialog *_settingsDialog = nullptr;
|
||||
|
||||
void ___source_dummy_addref(obs_source_t*) {}
|
||||
void ___sceneitem_dummy_addref(obs_sceneitem_t*) {};
|
||||
void ___data_dummy_addref(obs_data_t*) {};
|
||||
void ___data_array_dummy_addref(obs_data_array_t*) {};
|
||||
void ___output_dummy_addref(obs_output_t*) {};
|
||||
void ___data_item_dummy_addref(obs_data_item_t*) {};
|
||||
void ___data_item_release(obs_data_item_t* dataItem){ obs_data_item_release(&dataItem); };
|
||||
void ___properties_dummy_addref(obs_properties_t*) {};
|
||||
void WebSocketApiEventCallback(std::string vendorName, std::string eventType, obs_data_t *obsEventData);
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
blog(LOG_INFO, "[obs_module_load] you can haz websockets (Version: %s | RPC Version: %d)", OBS_WEBSOCKET_VERSION, OBS_WEBSOCKET_RPC_VERSION);
|
||||
blog(LOG_INFO, "[obs_module_load] Qt version (compile-time): %s | Qt version (run-time): %s",
|
||||
QT_VERSION_STR, qVersion());
|
||||
blog(LOG_INFO, "[obs_module_load] Qt version (compile-time): %s | Qt version (run-time): %s", QT_VERSION_STR, qVersion());
|
||||
|
||||
// Randomize the random number generator
|
||||
qsrand(QTime::currentTime().msec());
|
||||
// Initialize the cpu stats
|
||||
_cpuUsageInfo = os_cpu_usage_info_start();
|
||||
|
||||
// Create the config object then load the parameters from storage
|
||||
// Create the config manager then load the parameters from storage
|
||||
_config = ConfigPtr(new Config());
|
||||
_config->Load();
|
||||
|
||||
// Initialize the event handler
|
||||
_eventHandler = EventHandlerPtr(new EventHandler());
|
||||
|
||||
// Initialize the plugin/script API
|
||||
_webSocketApi = WebSocketApiPtr(new WebSocketApi());
|
||||
_webSocketApi->SetEventCallback(WebSocketApiEventCallback);
|
||||
|
||||
// Initialize the WebSocket server
|
||||
_webSocketServer = WebSocketServerPtr(new WebSocketServer());
|
||||
|
||||
_eventHandler = EventHandlerPtr(new EventHandler(_webSocketServer));
|
||||
|
||||
// Initialize the settings dialog
|
||||
obs_frontend_push_ui_translation(obs_module_get_string);
|
||||
QMainWindow* mainWindow = reinterpret_cast<QMainWindow*>(obs_frontend_get_main_window());
|
||||
_settingsDialog = new SettingsDialog(mainWindow);
|
||||
obs_frontend_pop_ui_translation();
|
||||
|
||||
// Add the settings dialog to the tools menu
|
||||
const char* menuActionText = obs_module_text("OBSWebSocket.Settings.DialogTitle");
|
||||
QAction* menuAction = (QAction*)obs_frontend_add_tools_menu_qaction(menuActionText);
|
||||
QObject::connect(menuAction, &QAction::triggered, [] { _settingsDialog->ToggleShowHide(); });
|
||||
|
||||
_cpuUsageInfo = os_cpu_usage_info_start();
|
||||
|
||||
if (_config->ServerEnabled)
|
||||
_webSocketServer->Start();
|
||||
|
||||
// Loading finished
|
||||
blog(LOG_INFO, "[obs_module_load] Module loaded.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -72,39 +85,122 @@ void obs_module_unload()
|
||||
{
|
||||
blog(LOG_INFO, "[obs_module_unload] Shutting down...");
|
||||
|
||||
// Shutdown the WebSocket server if it is running
|
||||
if (_webSocketServer->IsListening()) {
|
||||
blog(LOG_INFO, "[obs_module_unload] WebSocket server is running. Stopping...");
|
||||
blog_debug("[obs_module_unload] WebSocket server is running. Stopping...");
|
||||
_webSocketServer->Stop();
|
||||
}
|
||||
|
||||
// Destroy the WebSocket server
|
||||
_webSocketServer.reset();
|
||||
|
||||
// Destroy the plugin/script api
|
||||
_webSocketApi.reset();
|
||||
|
||||
// Destroy the event handler
|
||||
_eventHandler.reset();
|
||||
|
||||
_config->FirstLoad = false;
|
||||
// Save and destroy the config manager
|
||||
_config->Save();
|
||||
_config.reset();
|
||||
|
||||
// Destroy the cpu stats
|
||||
os_cpu_usage_info_destroy(_cpuUsageInfo);
|
||||
|
||||
blog(LOG_INFO, "[obs_module_unload] Finished shutting down.");
|
||||
}
|
||||
|
||||
os_cpu_usage_info_t* GetCpuUsageInfo()
|
||||
{
|
||||
return _cpuUsageInfo;
|
||||
}
|
||||
|
||||
ConfigPtr GetConfig()
|
||||
{
|
||||
return _config;
|
||||
}
|
||||
|
||||
WebSocketServerPtr GetWebSocketServer()
|
||||
{
|
||||
return _webSocketServer;
|
||||
}
|
||||
|
||||
EventHandlerPtr GetEventHandler()
|
||||
{
|
||||
return _eventHandler;
|
||||
}
|
||||
|
||||
os_cpu_usage_info_t* GetCpuUsageInfo()
|
||||
WebSocketApiPtr GetWebSocketApi()
|
||||
{
|
||||
return _cpuUsageInfo;
|
||||
return _webSocketApi;
|
||||
}
|
||||
|
||||
WebSocketServerPtr GetWebSocketServer()
|
||||
{
|
||||
return _webSocketServer;
|
||||
}
|
||||
|
||||
bool IsDebugEnabled()
|
||||
{
|
||||
return !_config || _config->DebugEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* An event has been emitted from a vendor.
|
||||
*
|
||||
* A vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket.
|
||||
* If a plugin or script implements vendor requests or events, documentation is expected to be provided with them.
|
||||
*
|
||||
* @dataField vendorName | String | Name of the vendor emitting the event
|
||||
* @dataField eventType | String | Vendor-provided event typedef
|
||||
* @dataField eventData | Object | Vendor-provided event data. {} if event does not provide any data
|
||||
*
|
||||
* @eventSubscription Vendors
|
||||
* @eventType VendorEvent
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api events
|
||||
* @category general
|
||||
*/
|
||||
void WebSocketApiEventCallback(std::string vendorName, std::string eventType, obs_data_t *obsEventData)
|
||||
{
|
||||
json eventData = Utils::Json::ObsDataToJson(obsEventData);
|
||||
|
||||
json broadcastEventData;
|
||||
broadcastEventData["vendorName"] = vendorName;
|
||||
broadcastEventData["eventType"] = eventType;
|
||||
broadcastEventData["eventData"] = eventData;
|
||||
|
||||
_webSocketServer->BroadcastEvent(EventSubscription::Vendors, "VendorEvent", broadcastEventData);
|
||||
}
|
||||
|
||||
#ifdef PLUGIN_TESTS
|
||||
|
||||
static void test_vendor_request_cb(obs_data_t *requestData, obs_data_t *responseData, void *priv_data)
|
||||
{
|
||||
blog(LOG_INFO, "[test_vendor_request_cb] Request called!");
|
||||
|
||||
blog(LOG_INFO, "[test_vendor_request_cb] Request data: %s", obs_data_get_json(requestData));
|
||||
|
||||
// Set an item to the response data
|
||||
obs_data_set_string(responseData, "test", "pp");
|
||||
|
||||
// Emit an event with the request data as the event data
|
||||
obs_websocket_vendor_emit_event(priv_data, "TestEvent", requestData);
|
||||
}
|
||||
|
||||
void obs_module_post_load()
|
||||
{
|
||||
blog(LOG_INFO, "[obs_module_post_load] Post load started.");
|
||||
|
||||
auto vendor = obs_websocket_register_vendor("obs-websocket-test");
|
||||
if (!vendor) {
|
||||
blog(LOG_WARNING, "[obs_module_post_load] Failed to create vendor!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!obs_websocket_vendor_register_request(vendor, "TestRequest", test_vendor_request_cb, vendor)) {
|
||||
blog(LOG_WARNING, "[obs_module_post_load] Failed to register vendor request!");
|
||||
return;
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "[obs_module_post_load] Post load completed.");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -1,3 +1,22 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
@ -10,44 +29,29 @@
|
||||
#pragma pop_macro("strtoll")
|
||||
#endif
|
||||
|
||||
// Autorelease object definitions
|
||||
void ___source_dummy_addref(obs_source_t*);
|
||||
void ___sceneitem_dummy_addref(obs_sceneitem_t*);
|
||||
void ___data_dummy_addref(obs_data_t*);
|
||||
void ___data_array_dummy_addref(obs_data_array_t*);
|
||||
void ___output_dummy_addref(obs_output_t*);
|
||||
void ___data_item_dummy_addref(obs_data_item_t*);
|
||||
void ___data_item_release(obs_data_item_t*);
|
||||
void ___properties_dummy_addref(obs_properties_t*);
|
||||
|
||||
using OBSSourceAutoRelease =
|
||||
OBSRef<obs_source_t*, ___source_dummy_addref, obs_source_release>;
|
||||
using OBSSceneItemAutoRelease =
|
||||
OBSRef<obs_sceneitem_t*, ___sceneitem_dummy_addref, obs_sceneitem_release>;
|
||||
using OBSDataAutoRelease =
|
||||
OBSRef<obs_data_t*, ___data_dummy_addref, obs_data_release>;
|
||||
using OBSDataArrayAutoRelease =
|
||||
OBSRef<obs_data_array_t*, ___data_array_dummy_addref, obs_data_array_release>;
|
||||
using OBSOutputAutoRelease =
|
||||
OBSRef<obs_output_t*, ___output_dummy_addref, obs_output_release>;
|
||||
using OBSDataItemAutoRelease =
|
||||
OBSRef<obs_data_item_t*, ___data_item_dummy_addref, ___data_item_release>;
|
||||
using OBSPropertiesAutoDestroy =
|
||||
OBSRef<obs_properties_t*, ___properties_dummy_addref, obs_properties_destroy>;
|
||||
#include "utils/Obs.h"
|
||||
#include "plugin-macros.generated.h"
|
||||
|
||||
class Config;
|
||||
typedef std::shared_ptr<Config> ConfigPtr;
|
||||
|
||||
class WebSocketServer;
|
||||
typedef std::shared_ptr<WebSocketServer> WebSocketServerPtr;
|
||||
|
||||
class EventHandler;
|
||||
typedef std::shared_ptr<EventHandler> EventHandlerPtr;
|
||||
|
||||
ConfigPtr GetConfig();
|
||||
class WebSocketApi;
|
||||
typedef std::shared_ptr<WebSocketApi> WebSocketApiPtr;
|
||||
|
||||
WebSocketServerPtr GetWebSocketServer();
|
||||
class WebSocketServer;
|
||||
typedef std::shared_ptr<WebSocketServer> WebSocketServerPtr;
|
||||
|
||||
os_cpu_usage_info_t* GetCpuUsageInfo();
|
||||
|
||||
ConfigPtr GetConfig();
|
||||
|
||||
EventHandlerPtr GetEventHandler();
|
||||
|
||||
os_cpu_usage_info_t* GetCpuUsageInfo();
|
||||
WebSocketApiPtr GetWebSocketApi();
|
||||
|
||||
WebSocketServerPtr GetWebSocketServer();
|
||||
|
||||
bool IsDebugEnabled();
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2021 Kyle Manning <tt2468@irltoolkit.com>
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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
|
||||
@ -21,6 +22,8 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
#include <util/base.h>
|
||||
#define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__)
|
||||
|
||||
#define blog_debug(msg, ...) if (IsDebugEnabled()) blog(LOG_INFO, "[debug] " msg, ##__VA_ARGS__)
|
||||
|
||||
#define OBS_WEBSOCKET_VERSION "@OBS_WEBSOCKET_VERSION@"
|
||||
|
||||
#define OBS_WEBSOCKET_RPC_VERSION @OBS_WEBSOCKET_RPC_VERSION@
|
||||
|
225
src/requesthandler/RequestBatchHandler.cpp
Normal file
225
src/requesthandler/RequestBatchHandler.cpp
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <queue>
|
||||
#include <condition_variable>
|
||||
#include <util/profiler.hpp>
|
||||
|
||||
#include "RequestBatchHandler.h"
|
||||
#include "../utils/Compat.h"
|
||||
#include "../obs-websocket.h"
|
||||
|
||||
struct SerialFrameBatch
|
||||
{
|
||||
RequestHandler &requestHandler;
|
||||
std::queue<RequestBatchRequest> requests;
|
||||
std::vector<RequestResult> results;
|
||||
json &variables;
|
||||
bool haltOnFailure;
|
||||
|
||||
size_t frameCount;
|
||||
size_t sleepUntilFrame;
|
||||
std::mutex conditionMutex;
|
||||
std::condition_variable condition;
|
||||
|
||||
SerialFrameBatch(RequestHandler &requestHandler, json &variables, bool haltOnFailure) :
|
||||
requestHandler(requestHandler),
|
||||
variables(variables),
|
||||
haltOnFailure(haltOnFailure),
|
||||
frameCount(0),
|
||||
sleepUntilFrame(0)
|
||||
{}
|
||||
};
|
||||
|
||||
struct ParallelBatchResults
|
||||
{
|
||||
RequestHandler &requestHandler;
|
||||
std::vector<RequestResult> results;
|
||||
|
||||
std::mutex conditionMutex;
|
||||
std::condition_variable condition;
|
||||
|
||||
ParallelBatchResults(RequestHandler &requestHandler) :
|
||||
requestHandler(requestHandler)
|
||||
{}
|
||||
};
|
||||
|
||||
// `{"inputName": "inputNameVariable"}` is essentially `inputName = inputNameVariable`
|
||||
static void PreProcessVariables(const json &variables, RequestBatchRequest &request)
|
||||
{
|
||||
if (variables.empty() || !request.InputVariables.is_object() || request.InputVariables.empty() || !request.RequestData.is_object())
|
||||
return;
|
||||
|
||||
for (auto& [key, value] : request.InputVariables.items()) {
|
||||
if (!value.is_string()) {
|
||||
blog_debug("[WebSocketServer::ProcessRequestBatch] Value of field `%s` in `inputVariables `is not a string. Skipping!", key.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string valueString = value;
|
||||
if (!variables.contains(valueString)) {
|
||||
blog_debug("[WebSocketServer::ProcessRequestBatch] `inputVariables` requested variable `%s`, but it does not exist. Skipping!", valueString.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
request.RequestData[key] = variables[valueString];
|
||||
}
|
||||
|
||||
request.HasRequestData = !request.RequestData.empty();
|
||||
}
|
||||
|
||||
// `{"sceneItemIdVariable": "sceneItemId"}` is essentially `sceneItemIdVariable = sceneItemId`
|
||||
static void PostProcessVariables(json &variables, const RequestBatchRequest &request, const RequestResult &requestResult)
|
||||
{
|
||||
if (!request.OutputVariables.is_object() || request.OutputVariables.empty() || requestResult.ResponseData.empty())
|
||||
return;
|
||||
|
||||
for (auto& [key, value] : request.OutputVariables.items()) {
|
||||
if (!value.is_string()) {
|
||||
blog_debug("[WebSocketServer::ProcessRequestBatch] Value of field `%s` in `outputVariables` is not a string. Skipping!", key.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string valueString = value;
|
||||
if (!requestResult.ResponseData.contains(valueString)) {
|
||||
blog_debug("[WebSocketServer::ProcessRequestBatch] `outputVariables` requested responseData field `%s`, but it does not exist. Skipping!", valueString.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
variables[key] = requestResult.ResponseData[valueString];
|
||||
}
|
||||
}
|
||||
|
||||
static void ObsTickCallback(void *param, float)
|
||||
{
|
||||
ScopeProfiler prof{"obs_websocket_request_batch_frame_tick"};
|
||||
|
||||
auto serialFrameBatch = reinterpret_cast<SerialFrameBatch*>(param);
|
||||
|
||||
// Increment frame count
|
||||
serialFrameBatch->frameCount++;
|
||||
|
||||
if (serialFrameBatch->sleepUntilFrame) {
|
||||
if (serialFrameBatch->frameCount < serialFrameBatch->sleepUntilFrame)
|
||||
// Do not process any requests if in "sleep mode"
|
||||
return;
|
||||
else
|
||||
// Reset frame sleep until counter if not being used
|
||||
serialFrameBatch->sleepUntilFrame = 0;
|
||||
}
|
||||
|
||||
// Begin recursing any unprocessed requests
|
||||
while (!serialFrameBatch->requests.empty()) {
|
||||
// Fetch first in queue
|
||||
RequestBatchRequest request = serialFrameBatch->requests.front();
|
||||
// Pre-process batch variables
|
||||
PreProcessVariables(serialFrameBatch->variables, request);
|
||||
// Process request and get result
|
||||
RequestResult requestResult = serialFrameBatch->requestHandler.ProcessRequest(request);
|
||||
// Post-process batch variables
|
||||
PostProcessVariables(serialFrameBatch->variables, request, requestResult);
|
||||
// Add to results vector
|
||||
serialFrameBatch->results.push_back(requestResult);
|
||||
// Remove from front of queue
|
||||
serialFrameBatch->requests.pop();
|
||||
|
||||
// If haltOnFailure and the request failed, clear the queue to make the batch return early.
|
||||
if (serialFrameBatch->haltOnFailure && requestResult.StatusCode != RequestStatus::Success) {
|
||||
serialFrameBatch->requests = std::queue<RequestBatchRequest>();
|
||||
break;
|
||||
}
|
||||
|
||||
// If the processed request tells us to sleep, do so accordingly
|
||||
if (requestResult.SleepFrames) {
|
||||
serialFrameBatch->sleepUntilFrame = serialFrameBatch->frameCount + requestResult.SleepFrames;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If request queue is empty, we can notify the paused worker thread
|
||||
if (serialFrameBatch->requests.empty())
|
||||
serialFrameBatch->condition.notify_one();
|
||||
}
|
||||
|
||||
std::vector<RequestResult> RequestBatchHandler::ProcessRequestBatch(QThreadPool &threadPool, SessionPtr session, RequestBatchExecutionType::RequestBatchExecutionType executionType, std::vector<RequestBatchRequest> &requests, json &variables, bool haltOnFailure)
|
||||
{
|
||||
RequestHandler requestHandler(session);
|
||||
if (executionType == RequestBatchExecutionType::SerialRealtime) {
|
||||
std::vector<RequestResult> ret;
|
||||
|
||||
// Recurse all requests in batch serially, processing the request then moving to the next one
|
||||
for (auto &request : requests) {
|
||||
PreProcessVariables(variables, request);
|
||||
|
||||
RequestResult requestResult = requestHandler.ProcessRequest(request);
|
||||
|
||||
PostProcessVariables(variables, request, requestResult);
|
||||
|
||||
ret.push_back(requestResult);
|
||||
|
||||
if (haltOnFailure && requestResult.StatusCode != RequestStatus::Success)
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} else if (executionType == RequestBatchExecutionType::SerialFrame) {
|
||||
SerialFrameBatch serialFrameBatch(requestHandler, variables, haltOnFailure);
|
||||
|
||||
// Create Request objects in the worker thread (avoid unnecessary processing in graphics thread)
|
||||
for (auto &request : requests)
|
||||
serialFrameBatch.requests.push(request);
|
||||
|
||||
// Create a callback entry for the graphics thread to execute on each video frame
|
||||
obs_add_tick_callback(ObsTickCallback, &serialFrameBatch);
|
||||
|
||||
// Wait until the graphics thread processes the last request in the queue
|
||||
std::unique_lock<std::mutex> lock(serialFrameBatch.conditionMutex);
|
||||
serialFrameBatch.condition.wait(lock, [&serialFrameBatch]{return serialFrameBatch.requests.empty();});
|
||||
|
||||
// Remove the created callback entry since we don't need it anymore
|
||||
obs_remove_tick_callback(ObsTickCallback, &serialFrameBatch);
|
||||
|
||||
return serialFrameBatch.results;
|
||||
} else if (executionType == RequestBatchExecutionType::Parallel) {
|
||||
ParallelBatchResults parallelResults(requestHandler);
|
||||
|
||||
// Acquire the lock early to prevent the batch from finishing before we're ready
|
||||
std::unique_lock<std::mutex> lock(parallelResults.conditionMutex);
|
||||
|
||||
// Submit each request as a task to the thread pool to be processed ASAP
|
||||
for (auto &request : requests) {
|
||||
threadPool.start(Utils::Compat::CreateFunctionRunnable([¶llelResults, &request]() {
|
||||
RequestResult requestResult = parallelResults.requestHandler.ProcessRequest(request);
|
||||
|
||||
std::unique_lock<std::mutex> lock(parallelResults.conditionMutex);
|
||||
parallelResults.results.push_back(requestResult);
|
||||
lock.unlock();
|
||||
parallelResults.condition.notify_one();
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for the last request to finish processing
|
||||
size_t requestCount = requests.size();
|
||||
parallelResults.condition.wait(lock, [¶llelResults, requestCount]{return parallelResults.results.size() == requestCount;});
|
||||
|
||||
return parallelResults.results;
|
||||
}
|
||||
|
||||
// Return empty vector if not a batch somehow
|
||||
return std::vector<RequestResult>();
|
||||
}
|
28
src/requesthandler/RequestBatchHandler.h
Normal file
28
src/requesthandler/RequestBatchHandler.h
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QThreadPool>
|
||||
|
||||
#include "RequestHandler.h"
|
||||
#include "rpc/RequestBatchRequest.h"
|
||||
|
||||
namespace RequestBatchHandler {
|
||||
std::vector<RequestResult> ProcessRequestBatch(QThreadPool &threadPool, SessionPtr session, RequestBatchExecutionType::RequestBatchExecutionType executionType, std::vector<RequestBatchRequest> &requests, json &variables, bool haltOnFailure);
|
||||
}
|
@ -1,17 +1,34 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
|
||||
{
|
||||
// General
|
||||
{"GetVersion", &RequestHandler::GetVersion},
|
||||
{"BroadcastCustomEvent", &RequestHandler::BroadcastCustomEvent},
|
||||
{"GetStats", &RequestHandler::GetStats},
|
||||
{"BroadcastCustomEvent", &RequestHandler::BroadcastCustomEvent},
|
||||
{"CallVendorRequest", &RequestHandler::CallVendorRequest},
|
||||
{"GetHotkeyList", &RequestHandler::GetHotkeyList},
|
||||
{"TriggerHotkeyByName", &RequestHandler::TriggerHotkeyByName},
|
||||
{"TriggerHotkeyByKeySequence", &RequestHandler::TriggerHotkeyByKeySequence},
|
||||
{"GetStudioModeEnabled", &RequestHandler::GetStudioModeEnabled},
|
||||
{"SetStudioModeEnabled", &RequestHandler::SetStudioModeEnabled},
|
||||
{"Sleep", &RequestHandler::Sleep},
|
||||
|
||||
// Config
|
||||
@ -50,6 +67,7 @@ const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
|
||||
{"GetInputList", &RequestHandler::GetInputList},
|
||||
{"GetInputKindList", &RequestHandler::GetInputKindList},
|
||||
{"CreateInput", &RequestHandler::CreateInput},
|
||||
//{"RemoveInput", &RequestHandler::RemoveInput}, // Disabled for now. Pending obs-studio#5276
|
||||
{"SetInputName", &RequestHandler::SetInputName},
|
||||
{"GetInputDefaultSettings", &RequestHandler::GetInputDefaultSettings},
|
||||
{"GetInputSettings", &RequestHandler::GetInputSettings},
|
||||
@ -66,23 +84,67 @@ const std::map<std::string, RequestMethodHandler> RequestHandler::_handlerMap
|
||||
{"GetInputPropertiesListPropertyItems", &RequestHandler::GetInputPropertiesListPropertyItems},
|
||||
{"PressInputPropertiesButton", &RequestHandler::PressInputPropertiesButton},
|
||||
|
||||
// Transitions
|
||||
{"GetTransitionKindList", &RequestHandler::GetTransitionKindList},
|
||||
{"GetSceneTransitionList", &RequestHandler::GetSceneTransitionList},
|
||||
{"GetCurrentSceneTransition", &RequestHandler::GetCurrentSceneTransition},
|
||||
{"SetCurrentSceneTransition", &RequestHandler::SetCurrentSceneTransition},
|
||||
{"SetCurrentSceneTransitionDuration", &RequestHandler::SetCurrentSceneTransitionDuration},
|
||||
{"SetCurrentSceneTransitionSettings", &RequestHandler::SetCurrentSceneTransitionSettings},
|
||||
{"TriggerStudioModeTransition", &RequestHandler::TriggerStudioModeTransition},
|
||||
|
||||
// Scene Items
|
||||
{"GetSceneItemList", &RequestHandler::GetSceneItemList},
|
||||
{"GetGroupSceneItemList", &RequestHandler::GetGroupSceneItemList},
|
||||
{"GetSceneItemId", &RequestHandler::GetSceneItemId},
|
||||
{"CreateSceneItem", &RequestHandler::CreateSceneItem},
|
||||
{"RemoveSceneItem", &RequestHandler::RemoveSceneItem},
|
||||
{"DuplicateSceneItem", &RequestHandler::DuplicateSceneItem},
|
||||
{"GetSceneItemTransform", &RequestHandler::GetSceneItemTransform},
|
||||
{"SetSceneItemTransform", &RequestHandler::SetSceneItemTransform},
|
||||
{"GetSceneItemEnabled", &RequestHandler::GetSceneItemEnabled},
|
||||
{"SetSceneItemEnabled", &RequestHandler::SetSceneItemEnabled},
|
||||
{"GetSceneItemLocked", &RequestHandler::GetSceneItemLocked},
|
||||
{"SetSceneItemLocked", &RequestHandler::SetSceneItemLocked},
|
||||
{"GetSceneItemIndex", &RequestHandler::GetSceneItemIndex},
|
||||
{"SetSceneItemIndex", &RequestHandler::SetSceneItemIndex},
|
||||
|
||||
// Stream
|
||||
{"GetStreamStatus", &RequestHandler::GetStreamStatus},
|
||||
{"ToggleStream", &RequestHandler::ToggleStream},
|
||||
{"StartStream", &RequestHandler::StartStream},
|
||||
{"StopStream", &RequestHandler::StopStream},
|
||||
|
||||
// Record
|
||||
{"GetRecordStatus", &RequestHandler::GetRecordStatus},
|
||||
{"ToggleRecord", &RequestHandler::ToggleRecord},
|
||||
{"StartRecord", &RequestHandler::StartRecord},
|
||||
{"StopRecord", &RequestHandler::StopRecord},
|
||||
{"ToggleRecordPause", &RequestHandler::ToggleRecordPause},
|
||||
{"PauseRecord", &RequestHandler::PauseRecord},
|
||||
{"ResumeRecord", &RequestHandler::ResumeRecord},
|
||||
//{"GetRecordDirectory", &RequestHandler::GetRecordDirectory},
|
||||
|
||||
// Media Inputs
|
||||
{"GetMediaInputStatus", &RequestHandler::GetMediaInputStatus},
|
||||
{"SetMediaInputCursor", &RequestHandler::SetMediaInputCursor},
|
||||
{"OffsetMediaInputCursor", &RequestHandler::OffsetMediaInputCursor},
|
||||
{"TriggerMediaInputAction", &RequestHandler::TriggerMediaInputAction},
|
||||
|
||||
// Ui
|
||||
{"GetStudioModeEnabled", &RequestHandler::GetStudioModeEnabled},
|
||||
{"SetStudioModeEnabled", &RequestHandler::SetStudioModeEnabled},
|
||||
};
|
||||
|
||||
RequestHandler::RequestHandler(SessionPtr session) :
|
||||
_session(session)
|
||||
{
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::ProcessRequest(const Request& request)
|
||||
{
|
||||
if (!request.RequestData.is_null() && !request.RequestData.is_object())
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestParameterType, "Your request data is not an object.");
|
||||
if (!request.RequestData.is_object() && !request.RequestData.is_null())
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "Your request data is not an object.");
|
||||
|
||||
if (request.RequestType.empty())
|
||||
return RequestResult::Error(RequestStatus::MissingRequestType, "Your request is missing a `requestType`");
|
||||
|
@ -1,3 +1,22 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
@ -6,6 +25,9 @@
|
||||
|
||||
#include "rpc/Request.h"
|
||||
#include "rpc/RequestResult.h"
|
||||
#include "types/RequestStatus.h"
|
||||
#include "types/RequestBatchExecutionType.h"
|
||||
#include "../websocketserver/rpc/WebSocketSession.h"
|
||||
#include "../obs-websocket.h"
|
||||
#include "../utils/Obs.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
@ -15,19 +37,20 @@ typedef RequestResult(RequestHandler::*RequestMethodHandler)(const Request&);
|
||||
|
||||
class RequestHandler {
|
||||
public:
|
||||
RequestHandler(SessionPtr session);
|
||||
|
||||
RequestResult ProcessRequest(const Request& request);
|
||||
std::vector<std::string> GetRequestList();
|
||||
|
||||
private:
|
||||
// General
|
||||
RequestResult GetVersion(const Request&);
|
||||
RequestResult BroadcastCustomEvent(const Request&);
|
||||
RequestResult GetStats(const Request&);
|
||||
RequestResult BroadcastCustomEvent(const Request&);
|
||||
RequestResult CallVendorRequest(const Request&);
|
||||
RequestResult GetHotkeyList(const Request&);
|
||||
RequestResult TriggerHotkeyByName(const Request&);
|
||||
RequestResult TriggerHotkeyByKeySequence(const Request&);
|
||||
RequestResult GetStudioModeEnabled(const Request&);
|
||||
RequestResult SetStudioModeEnabled(const Request&);
|
||||
RequestResult Sleep(const Request&);
|
||||
|
||||
// Config
|
||||
@ -66,6 +89,7 @@ class RequestHandler {
|
||||
RequestResult GetInputList(const Request&);
|
||||
RequestResult GetInputKindList(const Request&);
|
||||
RequestResult CreateInput(const Request&);
|
||||
RequestResult RemoveInput(const Request&);
|
||||
RequestResult SetInputName(const Request&);
|
||||
RequestResult GetInputDefaultSettings(const Request&);
|
||||
RequestResult GetInputSettings(const Request&);
|
||||
@ -82,11 +106,30 @@ class RequestHandler {
|
||||
RequestResult GetInputPropertiesListPropertyItems(const Request&);
|
||||
RequestResult PressInputPropertiesButton(const Request&);
|
||||
|
||||
// Transitions
|
||||
RequestResult GetTransitionKindList(const Request&);
|
||||
RequestResult GetSceneTransitionList(const Request&);
|
||||
RequestResult GetCurrentSceneTransition(const Request&);
|
||||
RequestResult SetCurrentSceneTransition(const Request&);
|
||||
RequestResult SetCurrentSceneTransitionDuration(const Request&);
|
||||
RequestResult SetCurrentSceneTransitionSettings(const Request&);
|
||||
RequestResult TriggerStudioModeTransition(const Request&);
|
||||
|
||||
// Scene Items
|
||||
RequestResult GetSceneItemList(const Request&);
|
||||
RequestResult GetGroupSceneItemList(const Request&);
|
||||
RequestResult GetSceneItemId(const Request&);
|
||||
RequestResult CreateSceneItem(const Request&);
|
||||
RequestResult RemoveSceneItem(const Request&);
|
||||
RequestResult DuplicateSceneItem(const Request&);
|
||||
RequestResult GetSceneItemTransform(const Request&);
|
||||
RequestResult SetSceneItemTransform(const Request&);
|
||||
RequestResult GetSceneItemEnabled(const Request&);
|
||||
RequestResult SetSceneItemEnabled(const Request&);
|
||||
RequestResult GetSceneItemLocked(const Request&);
|
||||
RequestResult SetSceneItemLocked(const Request&);
|
||||
RequestResult GetSceneItemIndex(const Request&);
|
||||
RequestResult SetSceneItemIndex(const Request&);
|
||||
|
||||
// Stream
|
||||
RequestResult GetStreamStatus(const Request&);
|
||||
@ -94,5 +137,26 @@ class RequestHandler {
|
||||
RequestResult StartStream(const Request&);
|
||||
RequestResult StopStream(const Request&);
|
||||
|
||||
// Record
|
||||
RequestResult GetRecordStatus(const Request&);
|
||||
RequestResult ToggleRecord(const Request&);
|
||||
RequestResult StartRecord(const Request&);
|
||||
RequestResult StopRecord(const Request&);
|
||||
RequestResult ToggleRecordPause(const Request&);
|
||||
RequestResult PauseRecord(const Request&);
|
||||
RequestResult ResumeRecord(const Request&);
|
||||
RequestResult GetRecordDirectory(const Request&);
|
||||
|
||||
// Media Inputs
|
||||
RequestResult GetMediaInputStatus(const Request&);
|
||||
RequestResult SetMediaInputCursor(const Request&);
|
||||
RequestResult OffsetMediaInputCursor(const Request&);
|
||||
RequestResult TriggerMediaInputAction(const Request&);
|
||||
|
||||
// Ui
|
||||
RequestResult GetStudioModeEnabled(const Request&);
|
||||
RequestResult SetStudioModeEnabled(const Request&);
|
||||
|
||||
SessionPtr _session;
|
||||
static const std::map<std::string, RequestMethodHandler> _handlerMap;
|
||||
};
|
||||
|
@ -1,8 +1,42 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <util/config-file.h>
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
/**
|
||||
* Gets the value of a "slot" from the selected persistent data realm.
|
||||
*
|
||||
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
|
||||
* @requestField slotName | String | The name of the slot to retrieve data from
|
||||
*
|
||||
* @responseField slotValue | Any | Value associated with the slot. `null` if not set
|
||||
*
|
||||
* @requestType GetPersistentData
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetPersistentData(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -23,19 +57,33 @@ RequestResult RequestHandler::GetPersistentData(const Request& request)
|
||||
|
||||
json responseData;
|
||||
json persistentData;
|
||||
if (!(Utils::Json::GetJsonFileContent(persistentDataPath, persistentData) && persistentData.contains(slotName)))
|
||||
responseData["slotValue"] = nullptr;
|
||||
else
|
||||
if (Utils::Json::GetJsonFileContent(persistentDataPath, persistentData) && persistentData.contains(slotName))
|
||||
responseData["slotValue"] = persistentData[slotName];
|
||||
else
|
||||
responseData["slotValue"] = nullptr;
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a "slot" from the selected persistent data realm.
|
||||
*
|
||||
* @requestField realm | String | The data realm to select. `OBS_WEBSOCKET_DATA_REALM_GLOBAL` or `OBS_WEBSOCKET_DATA_REALM_PROFILE`
|
||||
* @requestField slotName | String | The name of the slot to retrieve data from
|
||||
* @requestField slotValue | Any | The value to apply to the slot
|
||||
*
|
||||
* @requestType SetPersistentData
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetPersistentData(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!(request.ValidateString("realm", statusCode, comment) && request.ValidateString("slotName", statusCode, comment) && request.ValidateBasic("slotName", statusCode, comment)))
|
||||
if (!(request.ValidateString("realm", statusCode, comment) && request.ValidateString("slotName", statusCode, comment) && request.ValidateBasic("slotValue", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string realm = request.RequestData["realm"];
|
||||
@ -59,14 +107,41 @@ RequestResult RequestHandler::SetPersistentData(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetSceneCollectionList(const Request& request)
|
||||
/**
|
||||
* Gets an array of all scene collections
|
||||
*
|
||||
* @responseField currentSceneCollectionName | String | The name of the current scene collection
|
||||
* @responseField sceneCollections | Array<String> | Array of all available scene collections
|
||||
*
|
||||
* @requestType GetSceneCollectionList
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneCollectionList(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["currentSceneCollectionName"] = Utils::Obs::StringHelper::GetCurrentSceneCollection();
|
||||
responseData["sceneCollections"] = Utils::Obs::ListHelper::GetSceneCollectionList();
|
||||
responseData["sceneCollections"] = Utils::Obs::ArrayHelper::GetSceneCollectionList();
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a scene collection.
|
||||
*
|
||||
* Note: This will block until the collection has finished changing.
|
||||
*
|
||||
* @requestField sceneCollectionName | String | Name of the scene collection to switch to
|
||||
*
|
||||
* @requestType SetCurrentSceneCollection
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -76,7 +151,7 @@ RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
|
||||
|
||||
std::string sceneCollectionName = request.RequestData["sceneCollectionName"];
|
||||
|
||||
auto sceneCollections = Utils::Obs::ListHelper::GetSceneCollectionList();
|
||||
auto sceneCollections = Utils::Obs::ArrayHelper::GetSceneCollectionList();
|
||||
if (std::find(sceneCollections.begin(), sceneCollections.end(), sceneCollectionName) == sceneCollections.end())
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound);
|
||||
|
||||
@ -91,6 +166,20 @@ RequestResult RequestHandler::SetCurrentSceneCollection(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new scene collection, switching to it in the process.
|
||||
*
|
||||
* Note: This will block until the collection has finished changing.
|
||||
*
|
||||
* @requestField sceneCollectionName | String | Name for the new scene collection
|
||||
*
|
||||
* @requestType CreateSceneCollection
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::CreateSceneCollection(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -100,7 +189,7 @@ RequestResult RequestHandler::CreateSceneCollection(const Request& request)
|
||||
|
||||
std::string sceneCollectionName = request.RequestData["sceneCollectionName"];
|
||||
|
||||
auto sceneCollections = Utils::Obs::ListHelper::GetSceneCollectionList();
|
||||
auto sceneCollections = Utils::Obs::ArrayHelper::GetSceneCollectionList();
|
||||
if (std::find(sceneCollections.begin(), sceneCollections.end(), sceneCollectionName) != sceneCollections.end())
|
||||
return RequestResult::Error(RequestStatus::ResourceAlreadyExists);
|
||||
|
||||
@ -108,19 +197,44 @@ RequestResult RequestHandler::CreateSceneCollection(const Request& request)
|
||||
bool success = false;
|
||||
QMetaObject::invokeMethod(mainWindow, "AddSceneCollection", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, success), Q_ARG(bool, true), Q_ARG(QString, QString::fromStdString(sceneCollectionName)));
|
||||
if (!success)
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to create the scene collection for an unknown reason");
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene collection.");
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetProfileList(const Request& request)
|
||||
/**
|
||||
* Gets an array of all profiles
|
||||
*
|
||||
* @responseField currentProfileName | String | The name of the current profile
|
||||
* @responseField profiles | Array<String> | Array of all available profiles
|
||||
*
|
||||
* @requestType GetProfileList
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetProfileList(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["currentProfileName"] = Utils::Obs::StringHelper::GetCurrentProfile();
|
||||
responseData["profiles"] = Utils::Obs::ListHelper::GetProfileList();
|
||||
responseData["profiles"] = Utils::Obs::ArrayHelper::GetProfileList();
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a profile.
|
||||
*
|
||||
* @requestField profileName | String | Name of the profile to switch to
|
||||
*
|
||||
* @requestType SetCurrentProfile
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentProfile(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -130,7 +244,7 @@ RequestResult RequestHandler::SetCurrentProfile(const Request& request)
|
||||
|
||||
std::string profileName = request.RequestData["profileName"];
|
||||
|
||||
auto profiles = Utils::Obs::ListHelper::GetProfileList();
|
||||
auto profiles = Utils::Obs::ArrayHelper::GetProfileList();
|
||||
if (std::find(profiles.begin(), profiles.end(), profileName) == profiles.end())
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound);
|
||||
|
||||
@ -145,6 +259,18 @@ RequestResult RequestHandler::SetCurrentProfile(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new profile, switching to it in the process
|
||||
*
|
||||
* @requestField profileName | String | Name for the new profile
|
||||
*
|
||||
* @requestType CreateProfile
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::CreateProfile(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -154,7 +280,7 @@ RequestResult RequestHandler::CreateProfile(const Request& request)
|
||||
|
||||
std::string profileName = request.RequestData["profileName"];
|
||||
|
||||
auto profiles = Utils::Obs::ListHelper::GetProfileList();
|
||||
auto profiles = Utils::Obs::ArrayHelper::GetProfileList();
|
||||
if (std::find(profiles.begin(), profiles.end(), profileName) != profiles.end())
|
||||
return RequestResult::Error(RequestStatus::ResourceAlreadyExists);
|
||||
|
||||
@ -164,6 +290,18 @@ RequestResult RequestHandler::CreateProfile(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a profile. If the current profile is chosen, it will change to a different profile first.
|
||||
*
|
||||
* @requestField profileName | String | Name of the profile to remove
|
||||
*
|
||||
* @requestType RemoveProfile
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::RemoveProfile(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -173,7 +311,7 @@ RequestResult RequestHandler::RemoveProfile(const Request& request)
|
||||
|
||||
std::string profileName = request.RequestData["profileName"];
|
||||
|
||||
auto profiles = Utils::Obs::ListHelper::GetProfileList();
|
||||
auto profiles = Utils::Obs::ArrayHelper::GetProfileList();
|
||||
if (std::find(profiles.begin(), profiles.end(), profileName) == profiles.end())
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound);
|
||||
|
||||
@ -186,6 +324,22 @@ RequestResult RequestHandler::RemoveProfile(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a parameter from the current profile's configuration.
|
||||
*
|
||||
* @requestField parameterCategory | String | Category of the parameter to get
|
||||
* @requestField parameterName | String | Name of the parameter to get
|
||||
*
|
||||
* @responseField parameterValue | String | Value associated with the parameter. `null` if not set and no default
|
||||
* @responseField defaultParameterValue | String | Default value associated with the parameter. `null` if no default
|
||||
*
|
||||
* @requestType GetProfileParameter
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetProfileParameter(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -216,6 +370,20 @@ RequestResult RequestHandler::GetProfileParameter(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a parameter in the current profile's configuration.
|
||||
*
|
||||
* @requestField parameterCategory | String | Category of the parameter to set
|
||||
* @requestField parameterName | String | Name of the parameter to set
|
||||
* @requestField parameterValue | String | Value of the parameter to set. Use `null` to delete
|
||||
*
|
||||
* @requestType SetProfileParameter
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetProfileParameter(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -237,13 +405,32 @@ RequestResult RequestHandler::SetProfileParameter(const Request& request)
|
||||
std::string parameterValue = request.RequestData["parameterValue"];
|
||||
config_set_string(profile, parameterCategory.c_str(), parameterName.c_str(), parameterValue.c_str());
|
||||
} else {
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestParameterType, "The parameter `parameterValue` must be a string.");
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestFieldType, "The field `parameterValue` must be a string.");
|
||||
}
|
||||
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetVideoSettings(const Request& request)
|
||||
/**
|
||||
* Gets the current video settings.
|
||||
*
|
||||
* Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. Example: `60000/1001`
|
||||
*
|
||||
* @responseField fpsNumerator | Number | Numerator of the fractional FPS value
|
||||
* @responseField fpsDenominator | Number | Denominator of the fractional FPS value
|
||||
* @responseField baseWidth | Number | Width of the base (canvas) resolution in pixels
|
||||
* @responseField baseHeight | Number | Height of the base (canvas) resolution in pixels
|
||||
* @responseField outputWidth | Number | Width of the output resolution in pixels
|
||||
* @responseField outputHeight | Number | Height of the output resolution in pixels
|
||||
*
|
||||
* @requestType GetVideoSettings
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetVideoSettings(const Request&)
|
||||
{
|
||||
struct obs_video_info ovi;
|
||||
if (!obs_get_video_info(&ovi))
|
||||
@ -260,6 +447,25 @@ RequestResult RequestHandler::GetVideoSettings(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current video settings.
|
||||
*
|
||||
* Note: Fields must be specified in pairs. For example, you cannot set only `baseWidth` without needing to specify `baseHeight`.
|
||||
*
|
||||
* @requestField ?fpsNumerator | Number | Numerator of the fractional FPS value | >= 1 | Not changed
|
||||
* @requestField ?fpsDenominator | Number | Denominator of the fractional FPS value | >= 1 | Not changed
|
||||
* @requestField ?baseWidth | Number | Width of the base (canvas) resolution in pixels | >= 1, <= 4096 | Not changed
|
||||
* @requestField ?baseHeight | Number | Height of the base (canvas) resolution in pixels | >= 1, <= 4096 | Not changed
|
||||
* @requestField ?outputWidth | Number | Width of the output resolution in pixels | >= 1, <= 4096 | Not changed
|
||||
* @requestField ?outputHeight | Number | Height of the output resolution in pixels | >= 1, <= 4096 | Not changed
|
||||
*
|
||||
* @requestType SetVideoSettings
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetVideoSettings(const Request& request)
|
||||
{
|
||||
if (obs_video_active())
|
||||
@ -267,16 +473,16 @@ RequestResult RequestHandler::SetVideoSettings(const Request& request)
|
||||
|
||||
RequestStatus::RequestStatus statusCode = RequestStatus::NoError;
|
||||
std::string comment;
|
||||
bool changeFps = (request.ValidateNumber("fpsNumerator", statusCode, comment, 1) && request.ValidateNumber("fpsDenominator", statusCode, comment, 1));
|
||||
if (!changeFps && statusCode != RequestStatus::MissingRequestParameter)
|
||||
bool changeFps = (request.Contains("fpsNumerator") && request.Contains("fpsDenominator"));
|
||||
if (changeFps && !(request.ValidateOptionalNumber("fpsNumerator", statusCode, comment, 1) && request.ValidateOptionalNumber("fpsDenominator", statusCode, comment, 1)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool changeBaseRes = (request.ValidateNumber("baseWidth", statusCode, comment, 8, 4096) && request.ValidateNumber("baseHeight", statusCode, comment, 8, 4096));
|
||||
if (!changeBaseRes && statusCode != RequestStatus::MissingRequestParameter)
|
||||
bool changeBaseRes = (request.Contains("baseWidth") && request.Contains("baseHeight"));
|
||||
if (changeBaseRes && !(request.ValidateOptionalNumber("baseWidth", statusCode, comment, 8, 4096) && request.ValidateOptionalNumber("baseHeight", statusCode, comment, 8, 4096)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool changeOutputRes = (request.ValidateNumber("outputWidth", statusCode, comment, 8, 4096) && request.ValidateNumber("outputHeight", statusCode, comment, 8, 4096));
|
||||
if (!changeOutputRes && statusCode != RequestStatus::MissingRequestParameter)
|
||||
bool changeOutputRes = (request.Contains("outputWidth") && request.Contains("outputHeight"));
|
||||
if (changeOutputRes && !(request.ValidateOptionalNumber("outputWidth", statusCode, comment, 8, 4096) && request.ValidateOptionalNumber("outputHeight", statusCode, comment, 8, 4096)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
config_t *config = obs_frontend_get_profile_config();
|
||||
@ -303,10 +509,23 @@ RequestResult RequestHandler::SetVideoSettings(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify at least one video-changing pair.");
|
||||
return RequestResult::Error(RequestStatus::MissingRequestField, "You must specify at least one video-changing pair.");
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetStreamServiceSettings(const Request& request)
|
||||
/**
|
||||
* Gets the current stream service settings (stream destination).
|
||||
*
|
||||
* @responseField streamServiceType | String | Stream service type, like `rtmp_custom` or `rtmp_common`
|
||||
* @responseField streamServiceSettings | Object | Stream service settings
|
||||
*
|
||||
* @requestType GetStreamServiceSettings
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetStreamServiceSettings(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
|
||||
@ -318,6 +537,21 @@ RequestResult RequestHandler::GetStreamServiceSettings(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current stream service settings (stream destination).
|
||||
*
|
||||
* Note: Simple RTMP settings can be set with type `rtmp_custom` and the settings fields `server` and `key`.
|
||||
*
|
||||
* @requestField streamServiceType | String | Type of stream service to apply. Example: `rtmp_common` or `rtmp_custom`
|
||||
* @requestField streamServiceSettings | Object | Settings to apply to the service
|
||||
*
|
||||
* @requestType SetStreamServiceSettings
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category config
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetStreamServiceSettings(const Request& request)
|
||||
{
|
||||
if (obs_frontend_streaming_active())
|
||||
@ -346,10 +580,10 @@ RequestResult RequestHandler::SetStreamServiceSettings(const Request& request)
|
||||
obs_service_update(currentStreamService, newStreamServiceSettings);
|
||||
} else {
|
||||
// TODO: This leaks memory. I have no idea why.
|
||||
OBSService newStreamService = obs_service_create(requestedStreamServiceType.c_str(), "obs_websocket_custom_service", requestedStreamServiceSettings, NULL);
|
||||
OBSService newStreamService = obs_service_create(requestedStreamServiceType.c_str(), "obs_websocket_custom_service", requestedStreamServiceSettings, nullptr);
|
||||
// TODO: Check service type here, instead of relying on service creation to fail.
|
||||
if (!newStreamService)
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Creating the stream service with the requested streamServiceType failed. It may be an invalid type.");
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the stream service with the requested streamServiceType. It may be an invalid type.");
|
||||
|
||||
obs_frontend_set_streaming_service(newStreamService);
|
||||
}
|
||||
|
20
src/requesthandler/RequestHandler_Filters.cpp
Normal file
20
src/requesthandler/RequestHandler_Filters.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
@ -1,14 +1,51 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <QImageWriter>
|
||||
|
||||
#include "RequestHandler.h"
|
||||
#include "../websocketserver/WebSocketServer.h"
|
||||
#include "../eventhandler/types/EventSubscription.h"
|
||||
#include "../WebSocketApi.h"
|
||||
#include "../obs-websocket.h"
|
||||
#include "../WebSocketServer.h"
|
||||
|
||||
RequestResult RequestHandler::GetVersion(const Request& request)
|
||||
|
||||
/**
|
||||
* Gets data about the current plugin and RPC version.
|
||||
*
|
||||
* @responseField obsVersion | String | Current OBS Studio version
|
||||
* @responseField obsWebSocketVersion | String | Current obs-websocket version
|
||||
* @responseField rpcVersion | Number | Current latest obs-websocket RPC version
|
||||
* @responseField availableRequests | Array<String> | Array of available RPC requests for the currently negotiated RPC version
|
||||
* @responseField supportedImageFormats | Array<String> | Image formats available in `GetSourceScreenshot` and `SaveSourceScreenshot` requests.
|
||||
*
|
||||
* @requestType GetVersion
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetVersion(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["obsVersion"] = Utils::Obs::StringHelper::GetObsVersionString();
|
||||
responseData["obsVersion"] = Utils::Obs::StringHelper::GetObsVersion();
|
||||
responseData["obsWebSocketVersion"] = OBS_WEBSOCKET_VERSION;
|
||||
responseData["rpcVersion"] = OBS_WEBSOCKET_RPC_VERSION;
|
||||
responseData["availableRequests"] = GetRequestList();
|
||||
@ -23,6 +60,50 @@ RequestResult RequestHandler::GetVersion(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets statistics about OBS, obs-websocket, and the current session.
|
||||
*
|
||||
* @responseField cpuUsage | Number | Current CPU usage in percent
|
||||
* @responseField memoryUsage | Number | Amount of memory in MB currently being used by OBS
|
||||
* @responseField availableDiskSpace | Number | Available disk space on the device being used for recording storage
|
||||
* @responseField activeFps | Number | Current FPS being rendered
|
||||
* @responseField averageFrameRenderTime | Number | Average time in milliseconds that OBS is taking to render a frame
|
||||
* @responseField renderSkippedFrames | Number | Number of frames skipped by OBS in the render thread
|
||||
* @responseField renderTotalFrames | Number | Total number of frames outputted by the render thread
|
||||
* @responseField outputSkippedFrames | Number | Number of frames skipped by OBS in the output thread
|
||||
* @responseField outputTotalFrames | Number | Total number of frames outputted by the output thread
|
||||
* @responseField webSocketSessionIncomingMessages | Number | Total number of messages received by obs-websocket from the client
|
||||
* @responseField webSocketSessionOutgoingMessages | Number | Total number of messages sent by obs-websocket to the client
|
||||
*
|
||||
* @requestType GetStats
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetStats(const Request&)
|
||||
{
|
||||
json responseData = Utils::Obs::ObjectHelper::GetStats();
|
||||
|
||||
responseData["webSocketSessionIncomingMessages"] = _session->IncomingMessages();
|
||||
responseData["webSocketSessionOutgoingMessages"] = _session->OutgoingMessages();
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a `CustomEvent` to all WebSocket clients. Receivers are clients which are identified and subscribed.
|
||||
*
|
||||
* @requestField eventData | Object | Data payload to emit to all receivers
|
||||
*
|
||||
* @requestType BroadcastCustomEvent
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::BroadcastCustomEvent(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -32,30 +113,104 @@ RequestResult RequestHandler::BroadcastCustomEvent(const Request& request)
|
||||
|
||||
auto webSocketServer = GetWebSocketServer();
|
||||
if (!webSocketServer)
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Unable to send event.");
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Unable to send event due to internal error.");
|
||||
|
||||
webSocketServer->BroadcastEvent(EventSubscription::General, "CustomEvent", request.RequestData["eventData"]);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetStats(const Request& request)
|
||||
/**
|
||||
* Call a request registered to a vendor.
|
||||
*
|
||||
* A vendor is a unique name registered by a third-party plugin or script, which allows for custom requests and events to be added to obs-websocket.
|
||||
* If a plugin or script implements vendor requests or events, documentation is expected to be provided with them.
|
||||
*
|
||||
* @requestField vendorName | String | Name of the vendor to use
|
||||
* @requestField requestType | String | The request type to call
|
||||
* @requestField ?requestData | Object | Object containing appropriate request data | {}
|
||||
*
|
||||
* @responseField responseData | Object | Object containing appropriate response data. {} if request does not provide any response data
|
||||
*
|
||||
* @requestType CallVendorRequest
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::CallVendorRequest(const Request& request)
|
||||
{
|
||||
json responseData = Utils::Obs::DataHelper::GetStats();
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateString("vendorName", statusCode, comment) || !request.ValidateString("requestType", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
responseData["webSocketSessionIncomingMessages"] = request.Session->IncomingMessages();
|
||||
responseData["webSocketSessionOutgoingMessages"] = request.Session->OutgoingMessages();
|
||||
std::string vendorName = request.RequestData["vendorName"];
|
||||
std::string requestType = request.RequestData["requestType"];
|
||||
|
||||
OBSDataAutoRelease requestData = obs_data_create();
|
||||
if (request.Contains("requestData")) {
|
||||
if (!request.ValidateOptionalObject("requestData", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
requestData = Utils::Json::JsonToObsData(request.RequestData["requestData"]);
|
||||
}
|
||||
|
||||
OBSDataAutoRelease obsResponseData = obs_data_create();
|
||||
|
||||
auto webSocketApi = GetWebSocketApi();
|
||||
if (!webSocketApi)
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Unable to call request due to internal error.");
|
||||
|
||||
auto ret = webSocketApi->PerformVendorRequest(vendorName, requestType, requestData, obsResponseData);
|
||||
switch (ret) {
|
||||
default:
|
||||
case WebSocketApi::RequestReturnCode::Normal:
|
||||
break;
|
||||
case WebSocketApi::RequestReturnCode::NoVendor:
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "No vendor was found by that name.");
|
||||
case WebSocketApi::RequestReturnCode::NoVendorRequest:
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "No request was found by that name.");
|
||||
}
|
||||
|
||||
json responseData;
|
||||
responseData["responseData"] = Utils::Json::ObsDataToJson(obsResponseData);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetHotkeyList(const Request& request)
|
||||
/**
|
||||
* Gets an array of all hotkey names in OBS
|
||||
*
|
||||
* @responseField hotkeys | Array<String> | Array of hotkey names
|
||||
*
|
||||
* @requestType GetHotkeyList
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetHotkeyList(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["hotkeys"] = Utils::Obs::ListHelper::GetHotkeyNameList();
|
||||
responseData["hotkeys"] = Utils::Obs::ArrayHelper::GetHotkeyNameList();
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a hotkey using its name. See `GetHotkeyList`
|
||||
*
|
||||
* @requestField hotkeyName | String | Name of the hotkey to trigger
|
||||
*
|
||||
* @requestType TriggerHotkeyByName
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::TriggerHotkeyByName(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -72,6 +227,23 @@ RequestResult RequestHandler::TriggerHotkeyByName(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a hotkey using a sequence of keys.
|
||||
*
|
||||
* @requestField ?keyId | String | The OBS key ID to use. See https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h | Not pressed
|
||||
* @requestField ?keyModifiers | Object | Object containing key modifiers to apply | Ignored
|
||||
* @requestField ?keyModifiers.shift | Boolean | Press Shift | Not pressed
|
||||
* @requestField ?keyModifiers.control | Boolean | Press CTRL | Not pressed
|
||||
* @requestField ?keyModifiers.alt | Boolean | Press ALT | Not pressed
|
||||
* @requestField ?keyModifiers.command | Boolean | Press CMD (Mac) | Not pressed
|
||||
*
|
||||
* @requestType TriggerHotkeyByKeySequence
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
|
||||
{
|
||||
obs_key_combination_t combo = {0};
|
||||
@ -79,8 +251,8 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
|
||||
RequestStatus::RequestStatus statusCode = RequestStatus::NoError;
|
||||
std::string comment;
|
||||
|
||||
if (request.RequestData.contains("keyId") && !request.RequestData["keyId"].is_null()) {
|
||||
if (!request.ValidateString("keyId", statusCode, comment))
|
||||
if (request.Contains("keyId")) {
|
||||
if (!request.ValidateOptionalString("keyId", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string keyId = request.RequestData["keyId"];
|
||||
@ -88,8 +260,8 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
|
||||
}
|
||||
|
||||
statusCode = RequestStatus::NoError;
|
||||
if (request.RequestData.contains("keyModifiers") && !request.RequestData["keyModifiers"].is_null()) {
|
||||
if (!request.ValidateObject("keyModifiers", statusCode, comment, true))
|
||||
if (request.Contains("keyModifiers")) {
|
||||
if (!request.ValidateOptionalObject("keyModifiers", statusCode, comment, true))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
const json keyModifiersJson = request.RequestData["keyModifiers"];
|
||||
@ -106,7 +278,7 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
|
||||
}
|
||||
|
||||
if (!combo.modifiers && (combo.key == OBS_KEY_NONE || combo.key >= OBS_KEY_LAST_VALUE))
|
||||
return RequestResult::Error(RequestStatus::CannotAct, "Your provided request parameters cannot be used to trigger a hotkey.");
|
||||
return RequestResult::Error(RequestStatus::CannotAct, "Your provided request fields cannot be used to trigger a hotkey.");
|
||||
|
||||
// Apparently things break when you don't start by setting the combo to false
|
||||
obs_hotkey_inject_event(combo, false);
|
||||
@ -116,43 +288,37 @@ RequestResult RequestHandler::TriggerHotkeyByKeySequence(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetStudioModeEnabled(const Request& request)
|
||||
{
|
||||
json responseData;
|
||||
responseData["studioModeEnabled"] = obs_frontend_preview_program_mode_active();
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::SetStudioModeEnabled(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateBoolean("studioModeEnabled", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
// Avoid queueing tasks if nothing will change
|
||||
if (obs_frontend_preview_program_mode_active() != request.RequestData["studioModeEnabled"]) {
|
||||
// (Bad) Create a boolean then pass it as a reference to the task. Requires `wait` in obs_queue_task() to be true, else undefined behavior
|
||||
bool studioModeEnabled = request.RequestData["studioModeEnabled"];
|
||||
// Queue the task inside of the UI thread to prevent race conditions
|
||||
obs_queue_task(OBS_TASK_UI, [](void* param) {
|
||||
auto studioModeEnabled = (bool*)param;
|
||||
obs_frontend_set_preview_program_mode(*studioModeEnabled);
|
||||
}, &studioModeEnabled, true);
|
||||
}
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleeps for a time duration or number of frames. Only available in request batches with types `SERIAL_REALTIME` or `SERIAL_FRAME`.
|
||||
*
|
||||
* @requestField sleepMillis | Number | Number of milliseconds to sleep for (if `SERIAL_REALTIME` mode) | >= 0, <= 50000
|
||||
* @requestField sleepFrames | Number | Number of frames to sleep for (if `SERIAL_FRAME` mode) | >= 0, <= 10000
|
||||
*
|
||||
* @requestType Sleep
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category general
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::Sleep(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateNumber("sleepMillis", statusCode, comment, 0, 50000))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
int64_t sleepMillis = request.RequestData["sleepMillis"];
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis));
|
||||
|
||||
return RequestResult::Success();
|
||||
if (request.ExecutionType == RequestBatchExecutionType::SerialRealtime) {
|
||||
if (!request.ValidateNumber("sleepMillis", statusCode, comment, 0, 50000))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
int64_t sleepMillis = request.RequestData["sleepMillis"];
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMillis));
|
||||
return RequestResult::Success();
|
||||
} else if (request.ExecutionType == RequestBatchExecutionType::SerialFrame) {
|
||||
if (!request.ValidateNumber("sleepFrames", statusCode, comment, 0, 10000))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
RequestResult ret = RequestResult::Success();
|
||||
ret.SleepFrames = request.RequestData["sleepFrames"];
|
||||
return ret;
|
||||
} else {
|
||||
return RequestResult::Error(RequestStatus::UnsupportedRequestBatchExecutionType);
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +1,112 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
/**
|
||||
* Gets an array of all inputs in OBS.
|
||||
*
|
||||
* @requestField ?inputKind | String | Restrict the array to only inputs of the specified kind | All kinds included
|
||||
*
|
||||
* @responseField inputs | Array<Object> | Array of inputs
|
||||
*
|
||||
* @requestType GetInputList
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputList(const Request& request)
|
||||
{
|
||||
std::string inputKind;
|
||||
|
||||
if (request.RequestData.contains("inputKind") && !request.RequestData["inputKind"].is_null()) {
|
||||
if (request.Contains("inputKind")) {
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateString("inputKind", statusCode, comment))
|
||||
if (!request.ValidateOptionalString("inputKind", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
inputKind = request.RequestData["inputKind"];
|
||||
}
|
||||
|
||||
json responseData;
|
||||
responseData["inputs"] = Utils::Obs::ListHelper::GetInputList(inputKind);
|
||||
responseData["inputs"] = Utils::Obs::ArrayHelper::GetInputList(inputKind);
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of all available input kinds in OBS.
|
||||
*
|
||||
* @requestField ?unversioned | Boolean | True == Return all kinds as unversioned, False == Return with version suffixes (if available) | false
|
||||
*
|
||||
* @responseField inputKinds | Array<String> | Array of input kinds
|
||||
*
|
||||
* @requestType GetInputKindList
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputKindList(const Request& request)
|
||||
{
|
||||
bool unversioned = false;
|
||||
|
||||
if (request.RequestData.contains("unversioned") && !request.RequestData["unversioned"].is_null()) {
|
||||
if (request.Contains("unversioned")) {
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateBoolean("unversioned", statusCode, comment))
|
||||
if (!request.ValidateOptionalBoolean("unversioned", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
unversioned = request.RequestData["unversioned"];
|
||||
}
|
||||
|
||||
json responseData;
|
||||
responseData["inputKinds"] = Utils::Obs::ListHelper::GetInputKindList(unversioned);
|
||||
responseData["inputKinds"] = Utils::Obs::ArrayHelper::GetInputKindList(unversioned);
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new input, adding it as a scene item to the specified scene.
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene to add the input to as a scene item
|
||||
* @requestField inputName | String | Name of the new input to created
|
||||
* @requestField inputKind | String | The kind of input to be created
|
||||
* @requestField ?inputSettings | Object | Settings object to initialize the input with | Default settings used
|
||||
* @requestField ?sceneItemEnabled | Boolean | Whether to set the created scene item to enabled or disabled | True
|
||||
*
|
||||
* @responseField sceneItemId | Number | ID of the newly created scene item
|
||||
*
|
||||
* @requestType CreateInput
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::CreateInput(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSourceAutoRelease sceneSource = request.ValidateScene("sceneName", statusCode, comment);
|
||||
if (!(request.ValidateString("inputName", statusCode, comment) &&
|
||||
request.ValidateString("inputKind", statusCode, comment) &&
|
||||
sceneSource))
|
||||
if (!(sceneSource && request.ValidateString("inputName", statusCode, comment) && request.ValidateString("inputKind", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string inputName = request.RequestData["inputName"];
|
||||
@ -52,14 +115,13 @@ RequestResult RequestHandler::CreateInput(const Request& request)
|
||||
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that input name.");
|
||||
|
||||
std::string inputKind = request.RequestData["inputKind"];
|
||||
|
||||
auto kinds = Utils::Obs::ListHelper::GetInputKindList();
|
||||
auto kinds = Utils::Obs::ArrayHelper::GetInputKindList();
|
||||
if (std::find(kinds.begin(), kinds.end(), inputKind) == kinds.end())
|
||||
return RequestResult::Error(RequestStatus::InvalidInputKind, "Your specified input kind is not supported by OBS. Check that your specified kind is properly versioned and that any necessary plugins are loaded.");
|
||||
|
||||
OBSDataAutoRelease inputSettings = nullptr;
|
||||
if (request.RequestData.contains("inputSettings") && !request.RequestData["inputSettings"].is_null()) {
|
||||
if (!request.ValidateObject("inputSettings", statusCode, comment, true))
|
||||
if (request.Contains("inputSettings")) {
|
||||
if (!request.ValidateOptionalObject("inputSettings", statusCode, comment, true))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
inputSettings = Utils::Json::JsonToObsData(request.RequestData["inputSettings"]);
|
||||
@ -68,24 +130,67 @@ RequestResult RequestHandler::CreateInput(const Request& request)
|
||||
OBSScene scene = obs_scene_from_source(sceneSource);
|
||||
|
||||
bool sceneItemEnabled = true;
|
||||
if (request.RequestData.contains("sceneItemEnabled") && !request.RequestData["sceneItemEnabled"].is_null()) {
|
||||
if (!request.ValidateBoolean("sceneItemEnabled", statusCode, comment))
|
||||
if (request.Contains("sceneItemEnabled")) {
|
||||
if (!request.ValidateOptionalBoolean("sceneItemEnabled", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
sceneItemEnabled = request.RequestData["sceneItemEnabled"];
|
||||
}
|
||||
|
||||
// Create the input and add it as a scene item to the destination scene
|
||||
obs_sceneitem_t *sceneItem = Utils::Obs::ActionHelper::CreateInput(inputName, inputKind, inputSettings, scene, sceneItemEnabled);
|
||||
OBSSceneItemAutoRelease sceneItem = Utils::Obs::ActionHelper::CreateInput(inputName, inputKind, inputSettings, scene, sceneItemEnabled);
|
||||
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Creation of the input or scene item failed.");
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Creation of the input or scene item failed.");
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an existing input.
|
||||
*
|
||||
* Note: Will immediately remove all associated scene items.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to remove
|
||||
*
|
||||
* @requestType RemoveInput
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::RemoveInput(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
|
||||
if (!input)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
// Some implementations of removing sources release before remove, and some release after.
|
||||
// Releasing afterwards guarantees that we don't accidentally destroy the source before
|
||||
// remove if we happen to hold the last ref (very, very rare)
|
||||
obs_source_remove(input);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of an input (rename).
|
||||
*
|
||||
* @requestField inputName | String | Current input name
|
||||
* @requestField newInputName | String | New name for the input
|
||||
*
|
||||
* @requestType SetInputName
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetInputName(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -105,6 +210,20 @@ RequestResult RequestHandler::SetInputName(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default settings for an input kind.
|
||||
*
|
||||
* @requestField inputKind | String | Input kind to get the default settings for
|
||||
*
|
||||
* @responseField defaultInputSettings | Object | Object of default settings for the input kind
|
||||
*
|
||||
* @requestType GetInputDefaultSettings
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -123,6 +242,23 @@ RequestResult RequestHandler::GetInputDefaultSettings(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the settings of an input.
|
||||
*
|
||||
* Note: Does not include defaults. To create the entire settings object, overlay `inputSettings` over the `defaultInputSettings` provided by `GetInputDefaultSettings`.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to get the settings of
|
||||
*
|
||||
* @responseField inputSettings | Object | Object of settings for the input
|
||||
* @responseField inputKind | String | The kind of the input
|
||||
*
|
||||
* @requestType GetInputSettings
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputSettings(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -139,6 +275,20 @@ RequestResult RequestHandler::GetInputSettings(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the settings of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to set the settings of
|
||||
* @requestField inputSettings | Object | Object of settings to apply
|
||||
* @requestField ?overlay | Boolean | True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. | true
|
||||
*
|
||||
* @requestType SetInputSettings
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetInputSettings(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -148,8 +298,8 @@ RequestResult RequestHandler::SetInputSettings(const Request& request)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool overlay = true;
|
||||
if (request.RequestData.contains("overlay") && !request.RequestData["overlay"].is_null()) {
|
||||
if (!request.ValidateBoolean("overlay", statusCode, comment))
|
||||
if (request.Contains("overlay")) {
|
||||
if (!request.ValidateOptionalBoolean("overlay", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
overlay = request.RequestData["overlay"];
|
||||
@ -174,6 +324,20 @@ RequestResult RequestHandler::SetInputSettings(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio mute state of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of input to get the mute state of
|
||||
*
|
||||
* @responseField inputMuted | Boolean | Whether the input is muted
|
||||
*
|
||||
* @requestType GetInputMute
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputMute(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -187,6 +351,19 @@ RequestResult RequestHandler::GetInputMute(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio mute state of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to set the mute state of
|
||||
* @requestField inputMuted | Boolean | Whether to mute the input or not
|
||||
*
|
||||
* @requestType SetInputMute
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetInputMute(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -200,6 +377,20 @@ RequestResult RequestHandler::SetInputMute(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the audio mute state of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to toggle the mute state of
|
||||
*
|
||||
* @responseField inputMuted | Boolean | Whether the input has been muted or unmuted
|
||||
*
|
||||
* @requestType ToggleInputMute
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::ToggleInputMute(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -216,6 +407,21 @@ RequestResult RequestHandler::ToggleInputMute(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current volume setting of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to get the volume of
|
||||
*
|
||||
* @responseField inputVolumeMul | Number | Volume setting in mul
|
||||
* @responseField inputVolumeDb | Number | Volume setting in dB
|
||||
*
|
||||
* @requestType GetInputVolume
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputVolume(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -235,6 +441,20 @@ RequestResult RequestHandler::GetInputVolume(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume setting of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to set the volume of
|
||||
* @requestField ?inputVolumeMul | Number | Volume setting in mul | >= 0, <= 20 | `inputVolumeDb` should be specified
|
||||
* @requestField ?inputVolumeDb | Number | Volume setting in dB | >= -100, <= -26 | `inputVolumeMul` should be specified
|
||||
*
|
||||
* @requestType SetInputVolume
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetInputVolume(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -243,21 +463,21 @@ RequestResult RequestHandler::SetInputVolume(const Request& request)
|
||||
if (!input)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool hasMul = request.ValidateNumber("inputVolumeMul", statusCode, comment, 0, 20);
|
||||
if (!hasMul && statusCode != RequestStatus::MissingRequestParameter)
|
||||
bool hasMul = request.Contains("inputVolumeMul");
|
||||
if (hasMul && !request.ValidateOptionalNumber("inputVolumeMul", statusCode, comment, 0, 20))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool hasDb = request.ValidateNumber("inputVolumeDb", statusCode, comment, -100, 26);
|
||||
if (!hasDb && statusCode != RequestStatus::MissingRequestParameter)
|
||||
bool hasDb = request.Contains("inputVolumeDb");
|
||||
if (hasDb && !request.ValidateOptionalNumber("inputVolumeDb", statusCode, comment, -100, 26))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
if (hasMul && hasDb)
|
||||
return RequestResult::Error(RequestStatus::TooManyRequestParameters, "You may only specify one volume parameter.");
|
||||
return RequestResult::Error(RequestStatus::TooManyRequestFields, "You may only specify one volume field.");
|
||||
|
||||
if (!hasMul && !hasDb)
|
||||
return RequestResult::Error(RequestStatus::MissingRequestParameter, "You must specify one volume parameter.");
|
||||
return RequestResult::Error(RequestStatus::MissingRequestField, "You must specify one volume field.");
|
||||
|
||||
float inputVolumeMul = 0.0;
|
||||
float inputVolumeMul;
|
||||
if (hasMul)
|
||||
inputVolumeMul = request.RequestData["inputVolumeMul"];
|
||||
else
|
||||
@ -268,6 +488,22 @@ RequestResult RequestHandler::SetInputVolume(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio sync offset of an input.
|
||||
*
|
||||
* Note: The audio sync offset can be negative too!
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to get the audio sync offset of
|
||||
*
|
||||
* @responseField inputAudioSyncOffset | Number | Audio sync offset in milliseconds
|
||||
*
|
||||
* @requestType GetInputAudioSyncOffset
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputAudioSyncOffset(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -283,6 +519,19 @@ RequestResult RequestHandler::GetInputAudioSyncOffset(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio sync offset of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to set the audio sync offset of
|
||||
* @requestField inputAudioSyncOffset | Number | New audio sync offset in milliseconds | >= -950, <= 20000
|
||||
*
|
||||
* @requestType SetInputAudioSyncOffset
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetInputAudioSyncOffset(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -297,6 +546,25 @@ RequestResult RequestHandler::SetInputAudioSyncOffset(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio monitor type of an input.
|
||||
*
|
||||
* The available audio monitor types are:
|
||||
* - `OBS_MONITORING_TYPE_NONE`
|
||||
* - `OBS_MONITORING_TYPE_MONITOR_ONLY`
|
||||
* - `OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT`
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to get the audio monitor type of
|
||||
*
|
||||
* @responseField monitorType | String | Audio monitor type
|
||||
*
|
||||
* @requestType GetInputAudioMonitorType
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputAudioMonitorType(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -306,11 +574,24 @@ RequestResult RequestHandler::GetInputAudioMonitorType(const Request& request)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["monitorType"] = Utils::Obs::StringHelper::GetInputMonitorTypeString(input);
|
||||
responseData["monitorType"] = Utils::Obs::StringHelper::GetInputMonitorType(input);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio monitor type of an input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input to set the audio monitor type of
|
||||
* @requestField monitorType | String | Audio monitor type
|
||||
*
|
||||
* @requestType SetInputAudioMonitorType
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetInputAudioMonitorType(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -328,37 +609,30 @@ RequestResult RequestHandler::SetInputAudioMonitorType(const Request& request)
|
||||
else if (monitorTypeString == "OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT")
|
||||
monitorType = OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT;
|
||||
else
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestParameter, std::string("Unknown monitor type: ") + monitorTypeString);
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestField, std::string("Unknown monitor type: ") + monitorTypeString);
|
||||
|
||||
obs_source_set_monitoring_type(input, monitorType);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
std::vector<json> GetListPropertyItems(obs_property_t *property)
|
||||
{
|
||||
std::vector<json> ret;
|
||||
|
||||
enum obs_combo_format itemFormat = obs_property_list_format(property);
|
||||
size_t itemCount = obs_property_list_item_count(property);
|
||||
|
||||
for (size_t i = 0; i < itemCount; i++) {
|
||||
json itemData;
|
||||
itemData["itemName"] = obs_property_list_item_name(property, i);
|
||||
itemData["itemEnabled"] = !obs_property_list_item_disabled(property, i);
|
||||
if (itemFormat == OBS_COMBO_FORMAT_INT) {
|
||||
itemData["itemValue"] = obs_property_list_item_int(property, i);
|
||||
} else if (itemFormat == OBS_COMBO_FORMAT_FLOAT) {
|
||||
itemData["itemValue"] = obs_property_list_item_float(property, i);
|
||||
} else if (itemFormat == OBS_COMBO_FORMAT_STRING) {
|
||||
itemData["itemValue"] = obs_property_list_item_string(property, i);
|
||||
}
|
||||
ret.push_back(itemData);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the items of a list property from an input's properties.
|
||||
*
|
||||
* Note: Use this in cases where an input provides a dynamic, selectable list of items. For example, display capture, where it provides a list of available displays.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input
|
||||
* @requestField propertyName | String | Name of the list property to get the items of
|
||||
*
|
||||
* @responseField propertyItems | Array<Object> | Array of items in the list property
|
||||
*
|
||||
* @requestType GetInputPropertiesListPropertyItems
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetInputPropertiesListPropertyItems(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -377,11 +651,26 @@ RequestResult RequestHandler::GetInputPropertiesListPropertyItems(const Request&
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceType, "The property found is not a list.");
|
||||
|
||||
json responseData;
|
||||
responseData["propertyItems"] = GetListPropertyItems(property);
|
||||
responseData["propertyItems"] = Utils::Obs::ArrayHelper::GetListPropertyItems(property);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Presses a button in the properties of an input.
|
||||
*
|
||||
* Note: Use this in cases where there is a button in the properties of an input that cannot be accessed in any other way. For example, browser sources, where there is a refresh button.
|
||||
*
|
||||
* @requestField inputName | String | Name of the input
|
||||
* @requestField propertyName | String | Name of the button property to press
|
||||
*
|
||||
* @requestType PressInputPropertiesButton
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category inputs
|
||||
*/
|
||||
RequestResult RequestHandler::PressInputPropertiesButton(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
|
198
src/requesthandler/RequestHandler_MediaInputs.cpp
Normal file
198
src/requesthandler/RequestHandler_MediaInputs.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
bool IsMediaTimeValid(obs_source_t *input)
|
||||
{
|
||||
auto mediaState = obs_source_media_get_state(input);
|
||||
return mediaState == OBS_MEDIA_STATE_PLAYING || mediaState == OBS_MEDIA_STATE_PAUSED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of a media input.
|
||||
*
|
||||
* Media States:
|
||||
* - `OBS_MEDIA_STATE_NONE`
|
||||
* - `OBS_MEDIA_STATE_PLAYING`
|
||||
* - `OBS_MEDIA_STATE_OPENING`
|
||||
* - `OBS_MEDIA_STATE_BUFFERING`
|
||||
* - `OBS_MEDIA_STATE_PAUSED`
|
||||
* - `OBS_MEDIA_STATE_STOPPED`
|
||||
* - `OBS_MEDIA_STATE_ENDED`
|
||||
* - `OBS_MEDIA_STATE_ERROR`
|
||||
*
|
||||
* @requestField inputName | String | Name of the media input
|
||||
*
|
||||
* @responseField mediaState | String | State of the media input
|
||||
* @responseField mediaDuration | Number | Total duration of the playing media in milliseconds. `null` if not playing
|
||||
* @responseField mediaCursor | Number | Position of the cursor in milliseconds. `null` if not playing
|
||||
*
|
||||
* @requestType GetMediaInputStatus
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category media inputs
|
||||
*/
|
||||
RequestResult RequestHandler::GetMediaInputStatus(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
|
||||
if (!input)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["mediaState"] = Utils::Obs::StringHelper::GetMediaInputState(input);
|
||||
|
||||
if (IsMediaTimeValid(input)) {
|
||||
responseData["mediaDuration"] = obs_source_media_get_duration(input);
|
||||
responseData["mediaCursor"] = obs_source_media_get_time(input);
|
||||
} else {
|
||||
responseData["mediaDuration"] = nullptr;
|
||||
responseData["mediaCursor"] = nullptr;
|
||||
}
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cursor position of a media input.
|
||||
*
|
||||
* This request does not perform bounds checking of the cursor position.
|
||||
*
|
||||
* @requestField inputName | String | Name of the media input
|
||||
* @requestField mediaCursor | Number | New cursor position to set | >= 0
|
||||
*
|
||||
* @requestType SetMediaInputCursor
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category media inputs
|
||||
*/
|
||||
RequestResult RequestHandler::SetMediaInputCursor(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
|
||||
if (!(input && request.ValidateNumber("mediaCursor", statusCode, comment, 0)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
if (!IsMediaTimeValid(input))
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceState, "The media input must be playing or paused in order to set the cursor position.");
|
||||
|
||||
int64_t mediaCursor = request.RequestData["mediaCursor"];
|
||||
|
||||
// Yes, we're setting the time without checking if it's valid. Can't baby everything.
|
||||
obs_source_media_set_time(input, mediaCursor);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Offsets the current cursor position of a media input by the specified value.
|
||||
*
|
||||
* This request does not perform bounds checking of the cursor position.
|
||||
*
|
||||
* @requestField inputName | String | Name of the media input
|
||||
* @requestField mediaCursorOffset | Number | Value to offset the current cursor position by | None
|
||||
*
|
||||
* @requestType OffsetMediaInputCursor
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category media inputs
|
||||
*/
|
||||
RequestResult RequestHandler::OffsetMediaInputCursor(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
|
||||
if (!(input && request.ValidateNumber("mediaCursorOffset", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
if (!IsMediaTimeValid(input))
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceState, "The media input must be playing or paused in order to set the cursor position.");
|
||||
|
||||
int64_t mediaCursorOffset = request.RequestData["mediaCursorOffset"];
|
||||
int64_t mediaCursor = obs_source_media_get_time(input) + mediaCursorOffset;
|
||||
|
||||
if (mediaCursor < 0)
|
||||
mediaCursor = 0;
|
||||
|
||||
obs_source_media_set_time(input, mediaCursor);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers an action on a media input.
|
||||
*
|
||||
* @requestField inputName | String | Name of the media input
|
||||
* @requestField mediaAction | String | Identifier of the `ObsMediaInputAction` enum
|
||||
*
|
||||
* @requestType TriggerMediaInputAction
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category media inputs
|
||||
*/
|
||||
RequestResult RequestHandler::TriggerMediaInputAction(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSourceAutoRelease input = request.ValidateInput("inputName", statusCode, comment);
|
||||
if (!(input && request.ValidateString("mediaAction", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string mediaActionString = request.RequestData["mediaAction"];
|
||||
auto mediaAction = Utils::Obs::EnumHelper::GetMediaInputAction(mediaActionString);
|
||||
|
||||
switch (mediaAction) {
|
||||
default:
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NONE:
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestField, "You have specified an invalid media input action.");
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY:
|
||||
// Shoutout to whoever implemented this API call like this
|
||||
obs_source_media_play_pause(input, false);
|
||||
break;
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE:
|
||||
obs_source_media_play_pause(input, true);
|
||||
break;
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP:
|
||||
obs_source_media_stop(input);
|
||||
break;
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART:
|
||||
// I'm only implementing this because I'm nice. I think its a really dumb action.
|
||||
obs_source_media_restart(input);
|
||||
break;
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT:
|
||||
obs_source_media_next(input);
|
||||
break;
|
||||
case OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS:
|
||||
obs_source_media_previous(input);
|
||||
break;
|
||||
}
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
204
src/requesthandler/RequestHandler_Record.cpp
Normal file
204
src/requesthandler/RequestHandler_Record.cpp
Normal file
@ -0,0 +1,204 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
/**
|
||||
* Gets the status of the record output.
|
||||
*
|
||||
* @responseField outputActive | Boolean | Whether the output is active
|
||||
* @responseField ouputPaused | Boolean | Whether the output is paused
|
||||
* @responseField outputTimecode | String | Current formatted timecode string for the output
|
||||
* @responseField outputDuration | Number | Current duration in milliseconds for the output
|
||||
* @responseField outputBytes | Number | Number of bytes sent by the output
|
||||
*
|
||||
* @requestType GetRecordStatus
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::GetRecordStatus(const Request&)
|
||||
{
|
||||
OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output();
|
||||
|
||||
uint64_t outputDuration = Utils::Obs::NumberHelper::GetOutputDuration(recordOutput);
|
||||
|
||||
json responseData;
|
||||
responseData["outputActive"] = obs_output_active(recordOutput);
|
||||
responseData["outputPaused"] = obs_output_paused(recordOutput);
|
||||
responseData["outputTimecode"] = Utils::Obs::StringHelper::DurationToTimecode(outputDuration);
|
||||
responseData["outputDuration"] = outputDuration;
|
||||
responseData["outputBytes"] = (uint64_t)obs_output_get_total_bytes(recordOutput);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the status of the record output.
|
||||
*
|
||||
* @requestType ToggleRecord
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::ToggleRecord(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
if (obs_frontend_recording_active()) {
|
||||
obs_frontend_recording_stop();
|
||||
responseData["outputActive"] = false;
|
||||
} else {
|
||||
obs_frontend_recording_start();
|
||||
responseData["outputActive"] = true;
|
||||
}
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the record output.
|
||||
*
|
||||
* @requestType StartRecord
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::StartRecord(const Request&)
|
||||
{
|
||||
if (obs_frontend_recording_active())
|
||||
return RequestResult::Error(RequestStatus::OutputRunning);
|
||||
|
||||
// TODO: Call signal directly to perform blocking wait
|
||||
obs_frontend_recording_start();
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the record output.
|
||||
*
|
||||
* @requestType StopRecord
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::StopRecord(const Request&)
|
||||
{
|
||||
if (!obs_frontend_recording_active())
|
||||
return RequestResult::Error(RequestStatus::OutputNotRunning);
|
||||
|
||||
// TODO: Call signal directly to perform blocking wait
|
||||
obs_frontend_recording_stop();
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles pause on the record output.
|
||||
*
|
||||
* @requestType ToggleRecordPause
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::ToggleRecordPause(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
if (obs_frontend_recording_paused()) {
|
||||
obs_frontend_recording_pause(false);
|
||||
responseData["outputPaused"] = false;
|
||||
} else {
|
||||
obs_frontend_recording_pause(true);
|
||||
responseData["outputPaused"] = true;
|
||||
}
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the record output.
|
||||
*
|
||||
* @requestType PauseRecord
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::PauseRecord(const Request&)
|
||||
{
|
||||
if (obs_frontend_recording_paused())
|
||||
return RequestResult::Error(RequestStatus::OutputPaused);
|
||||
|
||||
// TODO: Call signal directly to perform blocking wait
|
||||
obs_frontend_recording_pause(true);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the record output.
|
||||
*
|
||||
* @requestType ResumeRecord
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::ResumeRecord(const Request&)
|
||||
{
|
||||
if (!obs_frontend_recording_paused())
|
||||
return RequestResult::Error(RequestStatus::OutputNotPaused);
|
||||
|
||||
// TODO: Call signal directly to perform blocking wait
|
||||
obs_frontend_recording_pause(false);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current directory that the record output is set to.
|
||||
*
|
||||
* @responseField recordDirectory | String | Output directory
|
||||
*
|
||||
* @requestType GetRecordDirectory
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category record
|
||||
*/
|
||||
RequestResult RequestHandler::GetRecordDirectory(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["recordDirectory"] = Utils::Obs::StringHelper::GetCurrentRecordOutputPath();
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
@ -1,5 +1,40 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
/**
|
||||
* Gets a list of all scene items in a scene.
|
||||
*
|
||||
* Scenes only
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene to get the items of
|
||||
*
|
||||
* @responseField sceneItems | Array<Object> | Array of scene items in the scene
|
||||
*
|
||||
* @requestType GetSceneItemList
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneItemList(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -9,11 +44,29 @@ RequestResult RequestHandler::GetSceneItemList(const Request& request)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItems"] = Utils::Obs::ListHelper::GetSceneItemList(obs_scene_from_source(scene));
|
||||
responseData["sceneItems"] = Utils::Obs::ArrayHelper::GetSceneItemList(obs_scene_from_source(scene));
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basically GetSceneItemList, but for groups.
|
||||
*
|
||||
* Using groups at all in OBS is discouraged, as they are very broken under the hood.
|
||||
*
|
||||
* Groups only
|
||||
*
|
||||
* @requestField sceneName | String | Name of the group to get the items of
|
||||
*
|
||||
* @responseField sceneItems | Array<Object> | Array of scene items in the group
|
||||
*
|
||||
* @requestType GetGroupItemList
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetGroupSceneItemList(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -23,11 +76,66 @@ RequestResult RequestHandler::GetGroupSceneItemList(const Request& request)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItems"] = Utils::Obs::ListHelper::GetSceneItemList(obs_group_from_source(scene));
|
||||
responseData["sceneItems"] = Utils::Obs::ArrayHelper::GetSceneItemList(obs_group_from_source(scene));
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a scene for a source, and returns its id.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene or group to search in
|
||||
* @requestField sourceName | String | Name of the source to find
|
||||
*
|
||||
* @responseField sceneItemId | Number | Numeric ID of the scene item
|
||||
*
|
||||
* @requestType GetSceneItemId
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneItemId(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneAutoRelease scene = request.ValidateScene2("sceneName", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!(scene && request.ValidateString("sourceName", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string sourceName = request.RequestData["sourceName"];
|
||||
|
||||
OBSSceneItemAutoRelease item = Utils::Obs::SearchHelper::GetSceneItemByName(scene, sourceName);
|
||||
if (!item)
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "No scene items were found in the specified scene by that name.");
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemId"] = obs_sceneitem_get_id(item);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new scene item using a source.
|
||||
*
|
||||
* Scenes only
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene to create the new item in
|
||||
* @requestField sourceName | String | Name of the source to add to the scene
|
||||
* @requestField ?sceneItemEnabled | Boolean | Enable state to apply to the scene item on creation | True
|
||||
*
|
||||
* @responseField sceneItemId | Number | Numeric ID of the scene item
|
||||
*
|
||||
* @requestType CreateSceneItem
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::CreateSceneItem(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -46,10 +154,15 @@ RequestResult RequestHandler::CreateSceneItem(const Request& request)
|
||||
return RequestResult::Error(RequestStatus::CannotAct, "You cannot create scene item of a scene within itself.");
|
||||
|
||||
bool sceneItemEnabled = true;
|
||||
if (request.RequestData.contains("sceneItemEnabled") && request.RequestData["sceneItemEnabled"].is_boolean())
|
||||
if (request.Contains("sceneItemEnabled")) {
|
||||
if (!request.ValidateOptionalBoolean("sceneItemEnabled", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemEnabled = request.RequestData["sceneItemEnabled"];
|
||||
}
|
||||
|
||||
obs_sceneitem_t *sceneItem = Utils::Obs::ActionHelper::CreateSceneItem(source, scene, sceneItemEnabled);
|
||||
OBSSceneItemAutoRelease sceneItem = Utils::Obs::ActionHelper::CreateSceneItem(source, scene, sceneItemEnabled);
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene item.");
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemId"] = obs_sceneitem_get_id(sceneItem);
|
||||
@ -57,6 +170,21 @@ RequestResult RequestHandler::CreateSceneItem(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a scene item from a scene.
|
||||
*
|
||||
* Scenes only
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
*
|
||||
* @requestType RemoveSceneItem
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::RemoveSceneItem(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -65,7 +193,441 @@ RequestResult RequestHandler::RemoveSceneItem(const Request& request)
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
// Makes the UI log `User Removed source '[source]' from scene '(null)'`. This is not a problem, just a side effect.
|
||||
obs_sceneitem_remove(sceneItem);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates a scene item, copying all transform and crop info.
|
||||
*
|
||||
* Scenes only
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
* @requestField ?destinationSceneName | String | Name of the scene to create the duplicated item in | `sceneName` is assumed
|
||||
*
|
||||
* @responseField sceneItemId | Number | Numeric ID of the duplicated scene item
|
||||
*
|
||||
* @requestType DuplicateSceneItem
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::DuplicateSceneItem(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment);
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
// Get destination scene
|
||||
obs_scene_t *destinationScene;
|
||||
if (request.Contains("destinationSceneName")) {
|
||||
destinationScene = request.ValidateScene2("destinationSceneName", statusCode, comment);
|
||||
if (!destinationScene)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
} else {
|
||||
destinationScene = obs_sceneitem_get_scene(sceneItem);
|
||||
obs_scene_addref(destinationScene);
|
||||
}
|
||||
|
||||
if (obs_sceneitem_is_group(sceneItem) && obs_sceneitem_get_scene(sceneItem) == destinationScene) {
|
||||
obs_scene_release(destinationScene);
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Scenes may only have one instance of a group.");
|
||||
}
|
||||
|
||||
// Get scene item details
|
||||
OBSSource sceneItemSource = obs_sceneitem_get_source(sceneItem);
|
||||
bool sceneItemEnabled = obs_sceneitem_visible(sceneItem);
|
||||
obs_transform_info sceneItemTransform;
|
||||
obs_sceneitem_crop sceneItemCrop;
|
||||
obs_sceneitem_get_info(sceneItem, &sceneItemTransform);
|
||||
obs_sceneitem_get_crop(sceneItem, &sceneItemCrop);
|
||||
|
||||
// Create the new item
|
||||
OBSSceneItemAutoRelease newSceneItem = Utils::Obs::ActionHelper::CreateSceneItem(sceneItemSource, destinationScene, sceneItemEnabled, &sceneItemTransform, &sceneItemCrop);
|
||||
obs_scene_release(destinationScene);
|
||||
if (!newSceneItem)
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene item.");
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemId"] = obs_sceneitem_get_id(newSceneItem);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transform and crop info of a scene item.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
*
|
||||
* @responseField sceneItemTransform | Object | Object containing scene item transform info
|
||||
*
|
||||
* @requestType GetSceneItemTransform
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneItemTransform(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemTransform"] = Utils::Obs::ObjectHelper::GetSceneItemTransform(sceneItem);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transform and crop info of a scene item.
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
* @requestField sceneItemTransform | Object | Object containing scene item transform info to update
|
||||
*
|
||||
* @requestType SetSceneItemTransform
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::SetSceneItemTransform(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!(sceneItem && request.ValidateObject("sceneItemTransform", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
// Create a fake request to use checks on the sub object
|
||||
Request r("", request.RequestData["sceneItemTransform"]);
|
||||
|
||||
bool transformChanged = false;
|
||||
bool cropChanged = false;
|
||||
obs_transform_info sceneItemTransform;
|
||||
obs_sceneitem_crop sceneItemCrop;
|
||||
obs_sceneitem_get_info(sceneItem, &sceneItemTransform);
|
||||
obs_sceneitem_get_crop(sceneItem, &sceneItemCrop);
|
||||
|
||||
OBSSource source = obs_sceneitem_get_source(sceneItem);
|
||||
float sourceWidth = float(obs_source_get_width(source));
|
||||
float sourceHeight = float(obs_source_get_height(source));
|
||||
|
||||
if (r.Contains("positionX")) {
|
||||
if (!r.ValidateOptionalNumber("positionX", statusCode, comment, -90001.0, 90001.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.pos.x = r.RequestData["positionX"];
|
||||
transformChanged = true;
|
||||
}
|
||||
if (r.Contains("positionY")) {
|
||||
if (!r.ValidateOptionalNumber("positionY", statusCode, comment, -90001.0, 90001.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.pos.y = r.RequestData["positionY"];
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("rotation")) {
|
||||
if (!r.ValidateOptionalNumber("rotation", statusCode, comment, -360.0, 360.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.rot = r.RequestData["rotation"];
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("scaleX")) {
|
||||
if (!r.ValidateOptionalNumber("scaleX", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
float scaleX = r.RequestData["scaleX"];
|
||||
float finalWidth = scaleX * sourceWidth;
|
||||
if (!(finalWidth > -90001.0 && finalWidth < 90001.0))
|
||||
return RequestResult::Error(RequestStatus::RequestFieldOutOfRange, "The field scaleX is too small or large for the current source resolution.");
|
||||
sceneItemTransform.scale.x = scaleX;
|
||||
transformChanged = true;
|
||||
}
|
||||
if (r.Contains("scaleY")) {
|
||||
if (!r.ValidateOptionalNumber("scaleY", statusCode, comment, -90001.0, 90001.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
float scaleY = r.RequestData["scaleY"];
|
||||
float finalHeight = scaleY * sourceHeight;
|
||||
if (!(finalHeight > -90001.0 && finalHeight < 90001.0))
|
||||
return RequestResult::Error(RequestStatus::RequestFieldOutOfRange, "The field scaleY is too small or large for the current source resolution.");
|
||||
sceneItemTransform.scale.y = scaleY;
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("alignment")) {
|
||||
if (!r.ValidateOptionalNumber("alignment", statusCode, comment, 0, std::numeric_limits<uint32_t>::max()))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.alignment = r.RequestData["alignment"];
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("boundsType")) {
|
||||
if (!r.ValidateOptionalString("boundsType", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
std::string boundsTypeString = r.RequestData["boundsType"];
|
||||
enum obs_bounds_type boundsType = Utils::Obs::EnumHelper::GetSceneItemBoundsType(boundsTypeString);
|
||||
if (boundsType == OBS_BOUNDS_NONE && boundsTypeString != "OBS_BOUNDS_NONE")
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestField, "The field boundsType has an invalid value.");
|
||||
sceneItemTransform.bounds_type = boundsType;
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("boundsAlignment")) {
|
||||
if (!r.ValidateOptionalNumber("boundsAlignment", statusCode, comment, 0, std::numeric_limits<uint32_t>::max()))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.bounds_alignment = r.RequestData["boundsAlignment"];
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("boundsWidth")) {
|
||||
if (!r.ValidateOptionalNumber("boundsWidth", statusCode, comment, 1.0, 90001.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.bounds.x = r.RequestData["boundsWidth"];
|
||||
transformChanged = true;
|
||||
}
|
||||
if (r.Contains("boundsHeight")) {
|
||||
if (!r.ValidateOptionalNumber("boundsHeight", statusCode, comment, 1.0, 90001.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemTransform.bounds.y = r.RequestData["boundsHeight"];
|
||||
transformChanged = true;
|
||||
}
|
||||
|
||||
if (r.Contains("cropLeft")) {
|
||||
if (!r.ValidateOptionalNumber("cropLeft", statusCode, comment, 0.0, 100000.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemCrop.left = r.RequestData["cropLeft"];
|
||||
cropChanged = true;
|
||||
}
|
||||
if (r.Contains("cropRight")) {
|
||||
if (!r.ValidateOptionalNumber("cropRight", statusCode, comment, 0.0, 100000.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemCrop.right = r.RequestData["cropRight"];
|
||||
cropChanged = true;
|
||||
}
|
||||
if (r.Contains("cropTop")) {
|
||||
if (!r.ValidateOptionalNumber("cropTop", statusCode, comment, 0.0, 100000.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemCrop.top = r.RequestData["cropTop"];
|
||||
cropChanged = true;
|
||||
}
|
||||
if (r.Contains("cropBottom")) {
|
||||
if (!r.ValidateOptionalNumber("cropBottom", statusCode, comment, 0.0, 100000.0))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
sceneItemCrop.bottom = r.RequestData["cropBottom"];
|
||||
cropChanged = true;
|
||||
}
|
||||
|
||||
if (!transformChanged && !cropChanged)
|
||||
return RequestResult::Error(RequestStatus::CannotAct, "You have not provided any valid transform changes.");
|
||||
|
||||
if (transformChanged)
|
||||
obs_sceneitem_set_info(sceneItem, &sceneItemTransform);
|
||||
|
||||
if (cropChanged)
|
||||
obs_sceneitem_set_crop(sceneItem, &sceneItemCrop);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the enable state of a scene item.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
*
|
||||
* @responseField sceneItemEnabled | Boolean | Whether the scene item is enabled. `true` for enabled, `false` for disabled
|
||||
*
|
||||
* @requestType GetSceneItemEnabled
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneItemEnabled(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemEnabled"] = obs_sceneitem_visible(sceneItem);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the enable state of a scene item.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
* @requestField sceneItemEnabled | Boolean | New enable state of the scene item
|
||||
*
|
||||
* @requestType SetSceneItemEnabled
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::SetSceneItemEnabled(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!(sceneItem && request.ValidateBoolean("sceneItemEnabled", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool sceneItemEnabled = request.RequestData["sceneItemEnabled"];
|
||||
|
||||
obs_sceneitem_set_visible(sceneItem, sceneItemEnabled);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lock state of a scene item.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
*
|
||||
* @responseField sceneItemLocked | Boolean | Whether the scene item is locked. `true` for locked, `false` for unlocked
|
||||
*
|
||||
* @requestType GetSceneItemLocked
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneItemLocked(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemLocked"] = obs_sceneitem_locked(sceneItem);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lock state of a scene item.
|
||||
*
|
||||
* Scenes and Group
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
* @requestField sceneItemLocked | Boolean | New lock state of the scene item
|
||||
*
|
||||
* @requestType SetSceneItemLocked
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::SetSceneItemLocked(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!(sceneItem && request.ValidateBoolean("sceneItemLocked", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
bool sceneItemLocked = request.RequestData["sceneItemLocked"];
|
||||
|
||||
obs_sceneitem_set_locked(sceneItem, sceneItemLocked);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index position of a scene item in a scene.
|
||||
*
|
||||
* An index of 0 is at the bottom of the source list in the UI.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
*
|
||||
* @responseField sceneItemIndex | Number | Index position of the scene item
|
||||
*
|
||||
* @requestType GetSceneItemIndex
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneItemIndex(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!sceneItem)
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
json responseData;
|
||||
responseData["sceneItemIndex"] = obs_sceneitem_get_order_position(sceneItem);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the index position of a scene item in a scene.
|
||||
*
|
||||
* Scenes and Groups
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene the item is in
|
||||
* @requestField sceneItemId | Number | Numeric ID of the scene item | >= 0
|
||||
* @requestField sceneItemIndex | Number | New index position of the scene item | >= 0
|
||||
*
|
||||
* @requestType SetSceneItemIndex
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scene items
|
||||
*/
|
||||
RequestResult RequestHandler::SetSceneItemIndex(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
OBSSceneItemAutoRelease sceneItem = request.ValidateSceneItem("sceneName", "sceneItemId", statusCode, comment, OBS_WEBSOCKET_SCENE_FILTER_SCENE_OR_GROUP);
|
||||
if (!(sceneItem && request.ValidateNumber("sceneItemIndex", statusCode, comment, 0, 8192)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
int sceneItemIndex = request.RequestData["sceneItemIndex"];
|
||||
|
||||
obs_sceneitem_set_order_position(sceneItem, sceneItemIndex);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
@ -1,11 +1,47 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
RequestResult RequestHandler::GetSceneList(const Request& request)
|
||||
/**
|
||||
* Gets an array of all scenes in OBS.
|
||||
*
|
||||
* @responseField currentProgramSceneName | String | Current program scene
|
||||
* @responseField currentPreviewSceneName | String | Current preview scene. `null` if not in studio mode
|
||||
* @responseField scenes | Array<Object> | Array of scenes in OBS
|
||||
*
|
||||
* @requestType GetSceneList
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneList(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
|
||||
OBSSourceAutoRelease currentProgramScene = obs_frontend_get_current_scene();
|
||||
responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
|
||||
if (currentProgramScene)
|
||||
responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
|
||||
else
|
||||
responseData["currentProgramSceneName"] = nullptr;
|
||||
|
||||
OBSSourceAutoRelease currentPreviewScene = obs_frontend_get_current_preview_scene();
|
||||
if (currentPreviewScene)
|
||||
@ -13,19 +49,44 @@ RequestResult RequestHandler::GetSceneList(const Request& request)
|
||||
else
|
||||
responseData["currentPreviewSceneName"] = nullptr;
|
||||
|
||||
responseData["scenes"] = Utils::Obs::ListHelper::GetSceneList();
|
||||
responseData["scenes"] = Utils::Obs::ArrayHelper::GetSceneList();
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetCurrentProgramScene(const Request& request)
|
||||
/**
|
||||
* Gets the current program scene.
|
||||
*
|
||||
* @responseField currentProgramSceneName | String | Current program scene
|
||||
*
|
||||
* @requestType GetCurrentProgramScene
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::GetCurrentProgramScene(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
OBSSourceAutoRelease currentProgramScene = obs_frontend_get_current_scene();
|
||||
responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current program scene.
|
||||
*
|
||||
* @requestField sceneName | String | Scene to set as the current program scene
|
||||
*
|
||||
* @requestType SetCurrentProgramScene
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentProgramScene(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -39,7 +100,21 @@ RequestResult RequestHandler::SetCurrentProgramScene(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::GetCurrentPreviewScene(const Request& request)
|
||||
/**
|
||||
* Gets the current preview scene.
|
||||
*
|
||||
* Only available when studio mode is enabled.
|
||||
*
|
||||
* @responseField currentPreviewSceneName | String | Current preview scene
|
||||
*
|
||||
* @requestType GetCurrentPreviewScene
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::GetCurrentPreviewScene(const Request&)
|
||||
{
|
||||
if (!obs_frontend_preview_program_mode_active())
|
||||
return RequestResult::Error(RequestStatus::StudioModeNotActive);
|
||||
@ -48,9 +123,24 @@ RequestResult RequestHandler::GetCurrentPreviewScene(const Request& request)
|
||||
|
||||
json responseData;
|
||||
responseData["currentPreviewSceneName"] = obs_source_get_name(currentPreviewScene);
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current preview scene.
|
||||
*
|
||||
* Only available when studio mode is enabled.
|
||||
*
|
||||
* @requestField sceneName | String | Scene to set as the current preview scene
|
||||
*
|
||||
* @requestType SetCurrentPreviewScene
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentPreviewScene(const Request& request)
|
||||
{
|
||||
if (!obs_frontend_preview_program_mode_active())
|
||||
@ -67,6 +157,18 @@ RequestResult RequestHandler::SetCurrentPreviewScene(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new scene in OBS.
|
||||
*
|
||||
* @requestField sceneName | String | Name for the new scene
|
||||
*
|
||||
* @requestType CreateScene
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::CreateScene(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -81,11 +183,26 @@ RequestResult RequestHandler::CreateScene(const Request& request)
|
||||
return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that scene name.");
|
||||
|
||||
obs_scene_t *createdScene = obs_scene_create(sceneName.c_str());
|
||||
if (!createdScene)
|
||||
return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene.");
|
||||
|
||||
obs_scene_release(createdScene);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a scene from OBS.
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene to remove
|
||||
*
|
||||
* @requestType RemoveScene
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::RemoveScene(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
@ -102,6 +219,19 @@ RequestResult RequestHandler::RemoveScene(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name of a scene (rename).
|
||||
*
|
||||
* @requestField sceneName | String | Name of the scene to be renamed
|
||||
* @requestField newSceneName | String | New name for the scene
|
||||
*
|
||||
* @requestType SetSceneName
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category scenes
|
||||
*/
|
||||
RequestResult RequestHandler::SetSceneName(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
|
@ -1,3 +1,22 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QImageWriter>
|
||||
#include <QFileInfo>
|
||||
@ -90,18 +109,30 @@ bool IsImageFormatValid(std::string format)
|
||||
return supportedFormats.contains(format.c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active and show state of a source.
|
||||
*
|
||||
* **Compatible with inputs and scenes.**
|
||||
*
|
||||
* @requestField sourceName | String | Name of the source to get the active state of
|
||||
*
|
||||
* @responseField videoActive | Boolean | Whether the source is showing in Program
|
||||
* @responseField videoShowing | Boolean | Whether the source is showing in the UI (Preview, Projector, Properties)
|
||||
*
|
||||
* @requestType GetSourceActive
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category sources
|
||||
*/
|
||||
RequestResult RequestHandler::GetSourceActive(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateString("sourceName", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string sourceName = request.RequestData["sourceName"];
|
||||
|
||||
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.c_str());
|
||||
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
|
||||
if (!source)
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound);
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT && obs_source_get_type(source) != OBS_SOURCE_TYPE_SCENE)
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceType, "The specified source is not an input or a scene.");
|
||||
@ -112,47 +143,65 @@ RequestResult RequestHandler::GetSourceActive(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Base64-encoded screenshot of a source.
|
||||
*
|
||||
* The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
|
||||
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
|
||||
*
|
||||
* **Compatible with inputs and scenes.**
|
||||
*
|
||||
* @requestField sourceName | String | Name of the source to take a screenshot of
|
||||
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
|
||||
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Source value is used
|
||||
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Source value is used
|
||||
* @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1
|
||||
*
|
||||
* @responseField imageData | String | Base64-encoded screenshot
|
||||
*
|
||||
* @requestType GetSourceScreenshot
|
||||
* @complexity 4
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category sources
|
||||
*/
|
||||
RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!(request.ValidateString("sourceName", statusCode, comment) && request.ValidateString("imageFormat", statusCode, comment)))
|
||||
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
|
||||
if (!(source && request.ValidateString("imageFormat", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string sourceName = request.RequestData["sourceName"];
|
||||
|
||||
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.c_str());
|
||||
if (!source)
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound);
|
||||
|
||||
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT && obs_source_get_type(source) != OBS_SOURCE_TYPE_SCENE)
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceType, "The specified source is not an input or a scene.");
|
||||
|
||||
std::string imageFormat = request.RequestData["imageFormat"];
|
||||
|
||||
if (!IsImageFormatValid(imageFormat))
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "Your specified image format is invalid or not supported by this system.");
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestField, "Your specified image format is invalid or not supported by this system.");
|
||||
|
||||
uint32_t requestedWidth{0};
|
||||
uint32_t requestedHeight{0};
|
||||
int compressionQuality{-1};
|
||||
|
||||
if (request.RequestData.contains("imageWidth") && !request.RequestData["imageWidth"].is_null()) {
|
||||
if (!request.ValidateNumber("imageWidth", statusCode, comment, 8, 4096))
|
||||
if (request.Contains("imageWidth")) {
|
||||
if (!request.ValidateOptionalNumber("imageWidth", statusCode, comment, 8, 4096))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
requestedWidth = request.RequestData["imageWidth"];
|
||||
}
|
||||
|
||||
if (request.RequestData.contains("imageHeight") && !request.RequestData["imageHeight"].is_null()) {
|
||||
if (!request.ValidateNumber("imageHeight", statusCode, comment, 8, 4096))
|
||||
if (request.Contains("imageHeight")) {
|
||||
if (!request.ValidateOptionalNumber("imageHeight", statusCode, comment, 8, 4096))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
requestedHeight = request.RequestData["imageHeight"];
|
||||
}
|
||||
|
||||
if (request.RequestData.contains("imageCompressionQuality") && !request.RequestData["imageCompressionQuality"].is_null()) {
|
||||
if (!request.ValidateNumber("imageCompressionQuality", statusCode, comment, -1, 100))
|
||||
if (request.Contains("imageCompressionQuality")) {
|
||||
if (!request.ValidateOptionalNumber("imageCompressionQuality", statusCode, comment, -1, 100))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
compressionQuality = request.RequestData["imageCompressionQuality"];
|
||||
@ -180,47 +229,71 @@ RequestResult RequestHandler::GetSourceScreenshot(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a screenshot of a source to the filesystem.
|
||||
*
|
||||
* The `imageWidth` and `imageHeight` parameters are treated as "scale to inner", meaning the smallest ratio will be used and the aspect ratio of the original resolution is kept.
|
||||
* If `imageWidth` and `imageHeight` are not specified, the compressed image will use the full resolution of the source.
|
||||
*
|
||||
* **Compatible with inputs and scenes.**
|
||||
*
|
||||
* @requestField sourceName | String | Name of the source to take a screenshot of
|
||||
* @requestField imageFormat | String | Image compression format to use. Use `GetVersion` to get compatible image formats
|
||||
* @requestField imageFilePath | String | Path to save the screenshot file to. Eg. `C:\Users\user\Desktop\screenshot.png`
|
||||
* @requestField ?imageWidth | Number | Width to scale the screenshot to | >= 8, <= 4096 | Source value is used
|
||||
* @requestField ?imageHeight | Number | Height to scale the screenshot to | >= 8, <= 4096 | Source value is used
|
||||
* @requestField ?imageCompressionQuality | Number | Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" (whatever that means, idk) | >= -1, <= 100 | -1
|
||||
*
|
||||
* @responseField imageData | String | Base64-encoded screenshot
|
||||
*
|
||||
* @requestType SaveSourceScreenshot
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category sources
|
||||
*/
|
||||
RequestResult RequestHandler::SaveSourceScreenshot(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!(request.ValidateString("sourceName", statusCode, comment) && request.ValidateString("imageFilePath", statusCode, comment) && request.ValidateString("imageFormat", statusCode, comment)))
|
||||
OBSSourceAutoRelease source = request.ValidateSource("sourceName", statusCode, comment);
|
||||
if (!(source && request.ValidateString("imageFormat", statusCode, comment) && request.ValidateString("imageFilePath", statusCode, comment)))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string sourceName = request.RequestData["sourceName"];
|
||||
|
||||
OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.c_str());
|
||||
if (!source)
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "No source was found by that name.");
|
||||
|
||||
if (obs_source_get_type(source) != OBS_SOURCE_TYPE_INPUT && obs_source_get_type(source) != OBS_SOURCE_TYPE_SCENE)
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceType, "The specified source is not an input or a scene.");
|
||||
|
||||
std::string imageFormat = request.RequestData["imageFormat"];
|
||||
std::string imageFilePath = request.RequestData["imageFilePath"];
|
||||
|
||||
if (!IsImageFormatValid(imageFormat))
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestParameter, "Your specified image format is invalid or not supported by this system.");
|
||||
return RequestResult::Error(RequestStatus::InvalidRequestField, "Your specified image format is invalid or not supported by this system.");
|
||||
|
||||
QFileInfo filePathInfo(QString::fromStdString(imageFilePath));
|
||||
if (!filePathInfo.absoluteDir().exists())
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "The directory for your file path does not exist.");
|
||||
|
||||
uint32_t requestedWidth{0};
|
||||
uint32_t requestedHeight{0};
|
||||
int compressionQuality{-1};
|
||||
|
||||
if (request.RequestData.contains("imageWidth") && !request.RequestData["imageWidth"].is_null()) {
|
||||
if (!request.ValidateNumber("imageWidth", statusCode, comment, 8, 4096))
|
||||
if (request.Contains("imageWidth")) {
|
||||
if (!request.ValidateOptionalNumber("imageWidth", statusCode, comment, 8, 4096))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
requestedWidth = request.RequestData["imageWidth"];
|
||||
}
|
||||
|
||||
if (request.RequestData.contains("imageHeight") && !request.RequestData["imageHeight"].is_null()) {
|
||||
if (!request.ValidateNumber("imageHeight", statusCode, comment, 8, 4096))
|
||||
if (request.Contains("imageHeight")) {
|
||||
if (!request.ValidateOptionalNumber("imageHeight", statusCode, comment, 8, 4096))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
requestedHeight = request.RequestData["imageHeight"];
|
||||
}
|
||||
|
||||
if (request.RequestData.contains("imageCompressionQuality") && !request.RequestData["imageCompressionQuality"].is_null()) {
|
||||
if (!request.ValidateNumber("imageCompressionQuality", statusCode, comment, -1, 100))
|
||||
if (request.Contains("imageCompressionQuality")) {
|
||||
if (!request.ValidateOptionalNumber("imageCompressionQuality", statusCode, comment, -1, 100))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
compressionQuality = request.RequestData["imageCompressionQuality"];
|
||||
@ -232,12 +305,6 @@ RequestResult RequestHandler::SaveSourceScreenshot(const Request& request)
|
||||
if (!success)
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "Failed to render screenshot.");
|
||||
|
||||
std::string imageFilePath = request.RequestData["imageFilePath"];
|
||||
|
||||
QFileInfo filePathInfo(QString::fromStdString(imageFilePath));
|
||||
if (!filePathInfo.absoluteDir().exists())
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "The directory for your file path does not exist.");
|
||||
|
||||
QString absoluteFilePath = filePathInfo.absoluteFilePath();
|
||||
|
||||
if (!renderedImage.save(absoluteFilePath, imageFormat.c_str(), compressionQuality))
|
||||
|
@ -1,14 +1,53 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
RequestResult RequestHandler::GetStreamStatus(const Request& request)
|
||||
/**
|
||||
* Gets the status of the stream output.
|
||||
*
|
||||
* @responseField outputActive | Boolean | Whether the output is active
|
||||
* @responseField outputReconnecting | Boolean | Whether the output is currently reconnecting
|
||||
* @responseField outputTimecode | String | Current formatted timecode string for the output
|
||||
* @responseField outputDuration | Number | Current duration in milliseconds for the output
|
||||
* @responseField outputBytes | Number | Number of bytes sent by the output
|
||||
* @responseField outputSkippedFrames | Number | Number of frames skipped by the output's process
|
||||
* @responseField outputTotalFrames | Number | Total number of frames delivered by the output's process
|
||||
*
|
||||
* @requestType GetStreamStatus
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category stream
|
||||
*/
|
||||
RequestResult RequestHandler::GetStreamStatus(const Request&)
|
||||
{
|
||||
OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output();
|
||||
|
||||
uint64_t outputDuration = Utils::Obs::NumberHelper::GetOutputDuration(streamOutput);
|
||||
|
||||
json responseData;
|
||||
responseData["outputActive"] = obs_output_active(streamOutput);
|
||||
responseData["outputReconnecting"] = obs_output_reconnecting(streamOutput);
|
||||
responseData["outputTimecode"] = Utils::Obs::StringHelper::GetOutputTimecodeString(streamOutput);
|
||||
responseData["outputDuration"] = Utils::Obs::NumberHelper::GetOutputDuration(streamOutput);
|
||||
responseData["outputTimecode"] = Utils::Obs::StringHelper::DurationToTimecode(outputDuration);
|
||||
responseData["outputDuration"] = outputDuration;
|
||||
responseData["outputBytes"] = (uint64_t)obs_output_get_total_bytes(streamOutput);
|
||||
responseData["outputSkippedFrames"] = obs_output_get_frames_dropped(streamOutput);
|
||||
responseData["outputTotalFrames"] = obs_output_get_total_frames(streamOutput);
|
||||
@ -16,7 +55,19 @@ RequestResult RequestHandler::GetStreamStatus(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::ToggleStream(const Request& request)
|
||||
/**
|
||||
* Toggles the status of the stream output.
|
||||
*
|
||||
* @responseField outputActive | Boolean | New state of the stream output
|
||||
*
|
||||
* @requestType ToggleStream
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category stream
|
||||
*/
|
||||
RequestResult RequestHandler::ToggleStream(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
if (obs_frontend_streaming_active()) {
|
||||
@ -30,7 +81,17 @@ RequestResult RequestHandler::ToggleStream(const Request& request)
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::StartStream(const Request& request)
|
||||
/**
|
||||
* Starts the stream output.
|
||||
*
|
||||
* @requestType StartStream
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category stream
|
||||
*/
|
||||
RequestResult RequestHandler::StartStream(const Request&)
|
||||
{
|
||||
if (obs_frontend_streaming_active())
|
||||
return RequestResult::Error(RequestStatus::OutputRunning);
|
||||
@ -41,7 +102,17 @@ RequestResult RequestHandler::StartStream(const Request& request)
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
RequestResult RequestHandler::StopStream(const Request& request)
|
||||
/**
|
||||
* Stops the stream output.
|
||||
*
|
||||
* @requestType StopStream
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category stream
|
||||
*/
|
||||
RequestResult RequestHandler::StopStream(const Request&)
|
||||
{
|
||||
if (!obs_frontend_streaming_active())
|
||||
return RequestResult::Error(RequestStatus::OutputNotRunning);
|
||||
|
249
src/requesthandler/RequestHandler_Transitions.cpp
Normal file
249
src/requesthandler/RequestHandler_Transitions.cpp
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
/**
|
||||
* Gets an array of all available transition kinds.
|
||||
*
|
||||
* Similar to `GetInputKindList`
|
||||
*
|
||||
* @responseField transitionKinds | Array<String> | Array of transition kinds
|
||||
*
|
||||
* @requestType GetTransitionKindList
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::GetTransitionKindList(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["transitionKinds"] = Utils::Obs::ArrayHelper::GetTransitionKindList();
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of all scene transitions in OBS.
|
||||
*
|
||||
* @responseField currentSceneTransitionName | String | Name of the current scene transition. Can be null
|
||||
* @responseField currentSceneTransitionKind | String | Kind of the current scene transition. Can be null
|
||||
* @responseField transitions | Array<Object> | Array of transitions
|
||||
*
|
||||
* @requestType GetSceneTransitionList
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::GetSceneTransitionList(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
|
||||
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
|
||||
if (transition) {
|
||||
responseData["currentSceneTransitionName"] = obs_source_get_name(transition);
|
||||
responseData["currentSceneTransitionKind"] = obs_source_get_id(transition);
|
||||
} else {
|
||||
responseData["currentSceneTransitionName"] = nullptr;
|
||||
responseData["currentSceneTransitionKind"] = nullptr;
|
||||
}
|
||||
|
||||
responseData["transitions"] = Utils::Obs::ArrayHelper::GetSceneTransitionList();
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the current scene transition.
|
||||
*
|
||||
* @responseField transitionName | String | Name of the transition
|
||||
* @responseField transitionKind | String | Kind of the transition
|
||||
* @responseField transitionFixed | Boolean | Whether the transition uses a fixed (unconfigurable) duration
|
||||
* @responseField transitionDuration | Number | Configured transition duration in milliseconds. `null` if transition is fixed
|
||||
* @responseField transitionConfigurable | Boolean | Whether the transition supports being configured
|
||||
* @responseField transitionSettings | Object | Object of settings for the transition. `null` if transition is not configurable
|
||||
*
|
||||
* @requestType GetCurrentSceneTransition
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::GetCurrentSceneTransition(const Request&)
|
||||
{
|
||||
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
|
||||
if (!transition)
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceState, "OBS does not currently have a scene transition set."); // This should not happen!
|
||||
|
||||
json responseData;
|
||||
responseData["transitionName"] = obs_source_get_name(transition);
|
||||
responseData["transitionKind"] = obs_source_get_id(transition);
|
||||
|
||||
if (obs_transition_fixed(transition)) {
|
||||
responseData["transitionFixed"] = true;
|
||||
responseData["transitionDuration"] = nullptr;
|
||||
} else {
|
||||
responseData["transitionFixed"] = false;
|
||||
responseData["transitionDuration"] = obs_frontend_get_transition_duration();
|
||||
}
|
||||
|
||||
if (obs_source_configurable(transition)) {
|
||||
responseData["transitionConfigurable"] = true;
|
||||
OBSDataAutoRelease transitionSettings = obs_source_get_settings(transition);
|
||||
responseData["transitionSettings"] = Utils::Json::ObsDataToJson(transitionSettings);
|
||||
} else {
|
||||
responseData["transitionConfigurable"] = false;
|
||||
responseData["transitionSettings"] = nullptr;
|
||||
}
|
||||
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current scene transition.
|
||||
*
|
||||
* Small note: While the namespace of scene transitions is generally unique, that uniqueness is not a guarantee as it is with other resources like inputs.
|
||||
*
|
||||
* @requestField transitionName | String | Name of the transition to make active
|
||||
*
|
||||
* @requestType SetCurrentSceneTransition
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentSceneTransition(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateString("transitionName", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
std::string transitionName = request.RequestData["transitionName"];
|
||||
|
||||
OBSSourceAutoRelease transition = Utils::Obs::SearchHelper::GetSceneTransitionByName(transitionName);
|
||||
if (!transition)
|
||||
return RequestResult::Error(RequestStatus::ResourceNotFound, "No scene transition was found by that name.");
|
||||
|
||||
obs_frontend_set_current_transition(transition);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration of the current scene transition, if it is not fixed.
|
||||
*
|
||||
* @requestField transitionDuration | Number | Duration in milliseconds | >= 50, <= 20000
|
||||
*
|
||||
* @requestType SetCurrentSceneTransitionDuration
|
||||
* @complexity 2
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentSceneTransitionDuration(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateNumber("transitionDuration", statusCode, comment, 50, 20000))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
int transitionDuration = request.RequestData["transitionDuration"];
|
||||
|
||||
obs_frontend_set_transition_duration(transitionDuration);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the settings of the current scene transition.
|
||||
*
|
||||
* @requestField transitionSettings | Object | Settings object to apply to the transition. Can be `{}`
|
||||
* @requestField ?overlay | Boolean | Whether to overlay over the current settings or replace them | true
|
||||
*
|
||||
* @requestType SetCurrentSceneTransitionSettings
|
||||
* @complexity 3
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::SetCurrentSceneTransitionSettings(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateObject("transitionSettings", statusCode, comment, true))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
|
||||
if (!transition)
|
||||
return RequestResult::Error(RequestStatus::InvalidResourceState, "OBS does not currently have a scene transition set."); // This should not happen!
|
||||
|
||||
if (!obs_source_configurable(transition))
|
||||
return RequestResult::Error(RequestStatus::ResourceNotConfigurable, "The current transition does not support custom settings.");
|
||||
|
||||
bool overlay = true;
|
||||
if (request.Contains("overlay")) {
|
||||
if (!request.ValidateOptionalBoolean("overlay", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
overlay = request.RequestData["overlay"];
|
||||
}
|
||||
|
||||
OBSDataAutoRelease newSettings = Utils::Json::JsonToObsData(request.RequestData["transitionSettings"]);
|
||||
if (!newSettings)
|
||||
return RequestResult::Error(RequestStatus::RequestProcessingFailed, "An internal data conversion operation failed. Please report this!");
|
||||
|
||||
if (overlay)
|
||||
obs_source_update(transition, newSettings);
|
||||
else
|
||||
obs_source_reset_settings(transition, newSettings);
|
||||
|
||||
obs_source_update_properties(transition);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the current scene transition. Same functionality as the `Transition` button in studio mode.
|
||||
*
|
||||
* @requestType TriggerStudioModeTransition
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api requests
|
||||
* @category transitions
|
||||
*/
|
||||
RequestResult RequestHandler::TriggerStudioModeTransition(const Request&)
|
||||
{
|
||||
if (!obs_frontend_preview_program_mode_active())
|
||||
return RequestResult::Error(RequestStatus::StudioModeNotActive);
|
||||
|
||||
OBSSourceAutoRelease previewScene = obs_frontend_get_current_preview_scene();
|
||||
|
||||
obs_frontend_set_current_scene(previewScene);
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
72
src/requesthandler/RequestHandler_Ui.cpp
Normal file
72
src/requesthandler/RequestHandler_Ui.cpp
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestHandler.h"
|
||||
|
||||
/**
|
||||
* Gets whether studio is enabled.
|
||||
*
|
||||
* @responseField studioModeEnabled | Boolean | Whether studio mode is enabled
|
||||
*
|
||||
* @requestType GetStudioModeEnabled
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category ui
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::GetStudioModeEnabled(const Request&)
|
||||
{
|
||||
json responseData;
|
||||
responseData["studioModeEnabled"] = obs_frontend_preview_program_mode_active();
|
||||
return RequestResult::Success(responseData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables studio mode
|
||||
*
|
||||
* @requestField studioModeEnabled | Boolean | True == Enabled, False == Disabled
|
||||
*
|
||||
* @requestType SetStudioModeEnabled
|
||||
* @complexity 1
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @category ui
|
||||
* @api requests
|
||||
*/
|
||||
RequestResult RequestHandler::SetStudioModeEnabled(const Request& request)
|
||||
{
|
||||
RequestStatus::RequestStatus statusCode;
|
||||
std::string comment;
|
||||
if (!request.ValidateBoolean("studioModeEnabled", statusCode, comment))
|
||||
return RequestResult::Error(statusCode, comment);
|
||||
|
||||
// Avoid queueing tasks if nothing will change
|
||||
if (obs_frontend_preview_program_mode_active() != request.RequestData["studioModeEnabled"]) {
|
||||
// (Bad) Create a boolean then pass it as a reference to the task. Requires `wait` in obs_queue_task() to be true, else undefined behavior
|
||||
bool studioModeEnabled = request.RequestData["studioModeEnabled"];
|
||||
// Queue the task inside of the UI thread to prevent race conditions
|
||||
obs_queue_task(OBS_TASK_UI, [](void* param) {
|
||||
auto studioModeEnabled = (bool*)param;
|
||||
obs_frontend_set_preview_program_mode(*studioModeEnabled);
|
||||
}, &studioModeEnabled, true);
|
||||
}
|
||||
|
||||
return RequestResult::Success();
|
||||
}
|
@ -1,7 +1,26 @@
|
||||
#include "Request.h"
|
||||
#include "../../plugin-macros.generated.h"
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
json GetDefaultJsonObject(json requestData)
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "Request.h"
|
||||
#include "../../obs-websocket.h"
|
||||
|
||||
json GetDefaultJsonObject(const json &requestData)
|
||||
{
|
||||
// Always provide an object to prevent exceptions while running checks in requests
|
||||
if (!requestData.is_object())
|
||||
@ -10,133 +29,177 @@ json GetDefaultJsonObject(json requestData)
|
||||
return requestData;
|
||||
}
|
||||
|
||||
Request::Request(SessionPtr session, std::string requestType, json requestData) :
|
||||
Session(session),
|
||||
RpcVersion(session->RpcVersion()),
|
||||
IgnoreNonFatalRequestChecks(session->IgnoreNonFatalRequestChecks()),
|
||||
Request::Request(const std::string &requestType, const json &requestData, const RequestBatchExecutionType::RequestBatchExecutionType executionType) :
|
||||
RequestType(requestType),
|
||||
RequestData(GetDefaultJsonObject(requestData))
|
||||
HasRequestData(requestData.is_object()),
|
||||
RequestData(GetDefaultJsonObject(requestData)),
|
||||
ExecutionType(executionType)
|
||||
{
|
||||
}
|
||||
|
||||
const bool Request::ValidateBasic(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
bool Request::Contains(const std::string &keyName) const
|
||||
{
|
||||
if (!HasRequestData()) {
|
||||
return (RequestData.contains(keyName) && !RequestData[keyName].is_null());
|
||||
}
|
||||
|
||||
bool Request::ValidateBasic(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
{
|
||||
if (!HasRequestData) {
|
||||
statusCode = RequestStatus::MissingRequestData;
|
||||
comment = "Your request data is missing or invalid (non-object)";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!RequestData.contains(keyName) || RequestData[keyName].is_null()) {
|
||||
statusCode = RequestStatus::MissingRequestParameter;
|
||||
comment = std::string("Your request is missing the `") + keyName + "` parameter.";
|
||||
statusCode = RequestStatus::MissingRequestField;
|
||||
comment = std::string("Your request is missing the `") + keyName + "` field.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool Request::ValidateNumber(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue, const double maxValue) const
|
||||
bool Request::ValidateOptionalNumber(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue, const double maxValue) const
|
||||
{
|
||||
if (!ValidateBasic(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
if (!RequestData[keyName].is_number()) {
|
||||
statusCode = RequestStatus::InvalidRequestParameterType;
|
||||
comment = std::string("The parameter `") + keyName + "` must be a number.";
|
||||
statusCode = RequestStatus::InvalidRequestFieldType;
|
||||
comment = std::string("The field value of `") + keyName + "` must be a number.";
|
||||
return false;
|
||||
}
|
||||
|
||||
double value = RequestData[keyName];
|
||||
if (value < minValue) {
|
||||
statusCode = RequestStatus::RequestParameterOutOfRange;
|
||||
comment = std::string("The parameter `") + keyName + "` is below the minimum of `" + std::to_string(minValue) + "`";
|
||||
statusCode = RequestStatus::RequestFieldOutOfRange;
|
||||
comment = std::string("The field value of `") + keyName + "` is below the minimum of `" + std::to_string(minValue) + "`";
|
||||
return false;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
statusCode = RequestStatus::RequestParameterOutOfRange;
|
||||
comment = std::string("The parameter `") + keyName + "` is above the maximum of `" + std::to_string(maxValue) + "`";
|
||||
statusCode = RequestStatus::RequestFieldOutOfRange;
|
||||
comment = std::string("The field value of `") + keyName + "` is above the maximum of `" + std::to_string(maxValue) + "`";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool Request::ValidateString(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
bool Request::ValidateNumber(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue, const double maxValue) const
|
||||
{
|
||||
if (!ValidateBasic(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
if (!ValidateOptionalNumber(keyName, statusCode, comment, minValue, maxValue))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Request::ValidateOptionalString(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
{
|
||||
if (!RequestData[keyName].is_string()) {
|
||||
statusCode = RequestStatus::InvalidRequestParameterType;
|
||||
comment = std::string("The parameter `") + keyName + "` must be a string.";
|
||||
statusCode = RequestStatus::InvalidRequestFieldType;
|
||||
comment = std::string("The field value of `") + keyName + "` must be a string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RequestData[keyName].get<std::string>().empty() && !allowEmpty) {
|
||||
statusCode = RequestStatus::RequestParameterEmpty;
|
||||
comment = std::string("The parameter `") + keyName + "` must not be empty.";
|
||||
statusCode = RequestStatus::RequestFieldEmpty;
|
||||
comment = std::string("The field value of `") + keyName + "` must not be empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool Request::ValidateBoolean(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
bool Request::ValidateString(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
{
|
||||
if (!ValidateBasic(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
if (!ValidateOptionalString(keyName, statusCode, comment, allowEmpty))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Request::ValidateOptionalBoolean(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
{
|
||||
if (!RequestData[keyName].is_boolean()) {
|
||||
statusCode = RequestStatus::InvalidRequestParameterType;
|
||||
comment = std::string("The parameter `") + keyName + "` must be boolean.";
|
||||
statusCode = RequestStatus::InvalidRequestFieldType;
|
||||
comment = std::string("The field value of `") + keyName + "` must be boolean.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool Request::ValidateObject(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
bool Request::ValidateBoolean(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
{
|
||||
if (!ValidateBasic(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
if (!ValidateOptionalBoolean(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Request::ValidateOptionalObject(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
{
|
||||
if (!RequestData[keyName].is_object()) {
|
||||
statusCode = RequestStatus::InvalidRequestParameterType;
|
||||
comment = std::string("The parameter `") + keyName + "` must be an object.";
|
||||
statusCode = RequestStatus::InvalidRequestFieldType;
|
||||
comment = std::string("The field value of `") + keyName + "` must be an object.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RequestData[keyName].empty() && !allowEmpty) {
|
||||
statusCode = RequestStatus::RequestParameterEmpty;
|
||||
comment = std::string("The parameter `") + keyName + "` must not be empty.";
|
||||
statusCode = RequestStatus::RequestFieldEmpty;
|
||||
comment = std::string("The field value of `") + keyName + "` must not be empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool Request::ValidateArray(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
bool Request::ValidateObject(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
{
|
||||
if (!ValidateBasic(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
if (!ValidateOptionalObject(keyName, statusCode, comment, allowEmpty))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Request::ValidateOptionalArray(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
{
|
||||
if (!RequestData[keyName].is_array()) {
|
||||
statusCode = RequestStatus::InvalidRequestParameterType;
|
||||
comment = std::string("The parameter `") + keyName + "` must be an array.";
|
||||
statusCode = RequestStatus::InvalidRequestFieldType;
|
||||
comment = std::string("The field value of `") + keyName + "` must be an array.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RequestData[keyName].empty() && !allowEmpty) {
|
||||
statusCode = RequestStatus::RequestParameterEmpty;
|
||||
comment = std::string("The parameter `") + keyName + "` must not be empty.";
|
||||
statusCode = RequestStatus::RequestFieldEmpty;
|
||||
comment = std::string("The field value of `") + keyName + "` must not be empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
obs_source_t *Request::ValidateSource(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
bool Request::ValidateArray(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty) const
|
||||
{
|
||||
if (!ValidateBasic(keyName, statusCode, comment))
|
||||
return false;
|
||||
|
||||
if (!ValidateOptionalArray(keyName, statusCode, comment, allowEmpty))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
obs_source_t *Request::ValidateSource(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
{
|
||||
if (!ValidateString(keyName, statusCode, comment))
|
||||
return nullptr;
|
||||
@ -153,7 +216,7 @@ obs_source_t *Request::ValidateSource(const std::string keyName, RequestStatus::
|
||||
return ret;
|
||||
}
|
||||
|
||||
obs_source_t *Request::ValidateScene(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter) const
|
||||
obs_source_t *Request::ValidateScene(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter) const
|
||||
{
|
||||
obs_source_t *ret = ValidateSource(keyName, statusCode, comment);
|
||||
if (!ret)
|
||||
@ -170,19 +233,53 @@ obs_source_t *Request::ValidateScene(const std::string keyName, RequestStatus::R
|
||||
if (filter == OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY && isGroup) {
|
||||
obs_source_release(ret);
|
||||
statusCode = RequestStatus::InvalidResourceType;
|
||||
comment = "The specified source is not a scene.";
|
||||
comment = "The specified source is not a scene. (Is group)";
|
||||
return nullptr;
|
||||
} else if (filter == OBS_WEBSOCKET_SCENE_FILTER_GROUP_ONLY && !isGroup) {
|
||||
obs_source_release(ret);
|
||||
statusCode = RequestStatus::InvalidResourceType;
|
||||
comment = "The specified source is not a group.";
|
||||
comment = "The specified source is not a group. (Is scene)";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
obs_source_t *Request::ValidateInput(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
obs_scene_t *Request::ValidateScene2(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter) const
|
||||
{
|
||||
OBSSourceAutoRelease sceneSource = ValidateSource(keyName, statusCode, comment);
|
||||
if (!sceneSource)
|
||||
return nullptr;
|
||||
|
||||
if (obs_source_get_type(sceneSource) != OBS_SOURCE_TYPE_SCENE) {
|
||||
statusCode = RequestStatus::InvalidResourceType;
|
||||
comment = "The specified source is not a scene.";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool isGroup = obs_source_is_group(sceneSource);
|
||||
if (isGroup) {
|
||||
if (filter == OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY) {
|
||||
statusCode = RequestStatus::InvalidResourceType;
|
||||
comment = "The specified source is not a scene. (Is group)";
|
||||
return nullptr;
|
||||
}
|
||||
OBSScene ret = obs_group_from_source(sceneSource);
|
||||
obs_scene_addref(ret);
|
||||
return ret;
|
||||
} else {
|
||||
if (filter == OBS_WEBSOCKET_SCENE_FILTER_GROUP_ONLY) {
|
||||
statusCode = RequestStatus::InvalidResourceType;
|
||||
comment = "The specified source is not a group. (Is scene)";
|
||||
return nullptr;
|
||||
}
|
||||
OBSScene ret = obs_scene_from_source(sceneSource);
|
||||
obs_scene_addref(ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
obs_source_t *Request::ValidateInput(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const
|
||||
{
|
||||
obs_source_t *ret = ValidateSource(keyName, statusCode, comment);
|
||||
if (!ret)
|
||||
@ -198,18 +295,15 @@ obs_source_t *Request::ValidateInput(const std::string keyName, RequestStatus::R
|
||||
return ret;
|
||||
}
|
||||
|
||||
obs_sceneitem_t *Request::ValidateSceneItem(const std::string sceneKeyName, const std::string sceneItemIdKeyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter) const
|
||||
obs_sceneitem_t *Request::ValidateSceneItem(const std::string &sceneKeyName, const std::string &sceneItemIdKeyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter) const
|
||||
{
|
||||
OBSSource sceneSource = ValidateScene(sceneKeyName, statusCode, comment, filter);
|
||||
obs_source_release(sceneSource);
|
||||
if (!sceneSource)
|
||||
OBSSceneAutoRelease scene = ValidateScene2(sceneKeyName, statusCode, comment, filter);
|
||||
if (!scene)
|
||||
return nullptr;
|
||||
|
||||
if (!ValidateNumber(sceneItemIdKeyName, statusCode, comment, 0))
|
||||
return nullptr;
|
||||
|
||||
OBSScene scene = obs_scene_from_source(sceneSource);
|
||||
|
||||
int64_t sceneItemId = RequestData[sceneItemIdKeyName];
|
||||
|
||||
OBSSceneItem sceneItem = obs_scene_find_sceneitem_by_id(scene, sceneItemId);
|
||||
|
@ -1,7 +1,26 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestStatus.h"
|
||||
#include "../../WebSocketSession.h"
|
||||
#include "../types/RequestStatus.h"
|
||||
#include "../types/RequestBatchExecutionType.h"
|
||||
#include "../../utils/Json.h"
|
||||
|
||||
enum ObsWebSocketSceneFilter {
|
||||
@ -12,29 +31,32 @@ enum ObsWebSocketSceneFilter {
|
||||
|
||||
struct Request
|
||||
{
|
||||
Request(SessionPtr session, const std::string requestType, const json requestData = nullptr);
|
||||
Request(const std::string &requestType, const json &requestData = nullptr, const RequestBatchExecutionType::RequestBatchExecutionType executionType = RequestBatchExecutionType::None);
|
||||
|
||||
const bool HasRequestData() const
|
||||
{
|
||||
return RequestData.is_object() && !RequestData.empty();
|
||||
}
|
||||
// Contains the key and is not null
|
||||
bool Contains(const std::string &keyName) const;
|
||||
|
||||
const bool ValidateBasic(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
const bool ValidateNumber(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue = -INFINITY, const double maxValue = INFINITY) const;
|
||||
const bool ValidateString(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
const bool ValidateBoolean(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
const bool ValidateObject(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
const bool ValidateArray(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
bool ValidateBasic(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
bool ValidateOptionalNumber(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue = -INFINITY, const double maxValue = INFINITY) const;
|
||||
bool ValidateNumber(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const double minValue = -INFINITY, const double maxValue = INFINITY) const;
|
||||
bool ValidateOptionalString(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
bool ValidateString(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
bool ValidateOptionalBoolean(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
bool ValidateBoolean(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
bool ValidateOptionalObject(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
bool ValidateObject(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
bool ValidateOptionalArray(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
bool ValidateArray(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const bool allowEmpty = false) const;
|
||||
|
||||
// All return values have incremented refcounts
|
||||
obs_source_t *ValidateSource(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
obs_source_t *ValidateScene(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter = OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY) const;
|
||||
obs_source_t *ValidateInput(const std::string keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
obs_sceneitem_t *ValidateSceneItem(const std::string sceneKeyName, const std::string sceneItemIdKeyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter = OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY) const;
|
||||
obs_source_t *ValidateSource(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
obs_source_t *ValidateScene(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter = OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY) const;
|
||||
obs_scene_t *ValidateScene2(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter = OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY) const;
|
||||
obs_source_t *ValidateInput(const std::string &keyName, RequestStatus::RequestStatus &statusCode, std::string &comment) const;
|
||||
obs_sceneitem_t *ValidateSceneItem(const std::string &sceneKeyName, const std::string &sceneItemIdKeyName, RequestStatus::RequestStatus &statusCode, std::string &comment, const ObsWebSocketSceneFilter filter = OBS_WEBSOCKET_SCENE_FILTER_SCENE_ONLY) const;
|
||||
|
||||
SessionPtr Session;
|
||||
const uint8_t RpcVersion;
|
||||
const bool IgnoreNonFatalRequestChecks;
|
||||
const std::string RequestType;
|
||||
const json RequestData;
|
||||
};
|
||||
std::string RequestType;
|
||||
bool HasRequestData;
|
||||
json RequestData;
|
||||
RequestBatchExecutionType::RequestBatchExecutionType ExecutionType;
|
||||
};
|
||||
|
26
src/requesthandler/rpc/RequestBatchRequest.cpp
Normal file
26
src/requesthandler/rpc/RequestBatchRequest.cpp
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestBatchRequest.h"
|
||||
|
||||
RequestBatchRequest::RequestBatchRequest(const std::string &requestType, const json &requestData, RequestBatchExecutionType::RequestBatchExecutionType executionType, const json &inputVariables, const json &outputVariables) :
|
||||
Request(requestType, requestData, executionType),
|
||||
InputVariables(inputVariables),
|
||||
OutputVariables(outputVariables)
|
||||
{
|
||||
}
|
28
src/requesthandler/rpc/RequestBatchRequest.h
Normal file
28
src/requesthandler/rpc/RequestBatchRequest.h
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Request.h"
|
||||
|
||||
struct RequestBatchRequest : Request {
|
||||
RequestBatchRequest(const std::string &requestType, const json &requestData, RequestBatchExecutionType::RequestBatchExecutionType executionType, const json &inputVariables = nullptr, const json &outputVariables = nullptr);
|
||||
|
||||
json InputVariables;
|
||||
json OutputVariables;
|
||||
};
|
@ -1,9 +1,29 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "RequestResult.h"
|
||||
|
||||
RequestResult::RequestResult(RequestStatus::RequestStatus statusCode, json responseData, std::string comment) :
|
||||
StatusCode(statusCode),
|
||||
ResponseData(responseData),
|
||||
Comment(comment)
|
||||
Comment(comment),
|
||||
SleepFrames(0)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,25 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestStatus.h"
|
||||
#include "../types/RequestStatus.h"
|
||||
#include "../../utils/Json.h"
|
||||
|
||||
struct RequestResult
|
||||
@ -11,4 +30,5 @@ struct RequestResult
|
||||
RequestStatus::RequestStatus StatusCode;
|
||||
json ResponseData;
|
||||
std::string Comment;
|
||||
size_t SleepFrames;
|
||||
};
|
||||
|
@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace RequestStatus {
|
||||
enum RequestStatus {
|
||||
Unknown = 0,
|
||||
|
||||
// For internal use to signify a successful parameter check
|
||||
NoError = 10,
|
||||
|
||||
Success = 100,
|
||||
|
||||
// The `requestType` field is missing from the request data
|
||||
MissingRequestType = 203,
|
||||
// The request type is invalid or does not exist
|
||||
UnknownRequestType = 204,
|
||||
// Generic error code (comment required)
|
||||
GenericError = 205,
|
||||
|
||||
// A required request parameter is missing
|
||||
MissingRequestParameter = 300,
|
||||
// The request does not have a valid requestData object.
|
||||
MissingRequestData = 301,
|
||||
|
||||
// Generic invalid request parameter message (comment required)
|
||||
InvalidRequestParameter = 400,
|
||||
// A request parameter has the wrong data type
|
||||
InvalidRequestParameterType = 401,
|
||||
// A request parameter (float or int) is out of valid range
|
||||
RequestParameterOutOfRange = 402,
|
||||
// A request parameter (string or array) is empty and cannot be
|
||||
RequestParameterEmpty = 403,
|
||||
// There are too many request parameters (eg. a request takes two optionals, where only one is allowed at a time)
|
||||
TooManyRequestParameters = 404,
|
||||
|
||||
// An output is running and cannot be in order to perform the request (generic)
|
||||
OutputRunning = 500,
|
||||
// An output is not running and should be
|
||||
OutputNotRunning = 501,
|
||||
// An output is paused and should not be
|
||||
OutputPaused = 502,
|
||||
// An output is disabled and should not be
|
||||
OutputDisabled = 503,
|
||||
// Studio mode is active and cannot be
|
||||
StudioModeActive = 504,
|
||||
// Studio mode is not active and should be
|
||||
StudioModeNotActive = 505,
|
||||
|
||||
// The resource was not found
|
||||
ResourceNotFound = 600,
|
||||
// The resource already exists
|
||||
ResourceAlreadyExists = 601,
|
||||
// The type of resource found is invalid
|
||||
InvalidResourceType = 602,
|
||||
// There are not enough instances of the resource in order to perform the request
|
||||
NotEnoughResources = 603,
|
||||
// The state of the resource is invalid. For example, if the resource is blocked from being accessed
|
||||
InvalidResourceState = 604,
|
||||
// The specified input (obs_source_t-OBS_SOURCE_TYPE_INPUT) had the wrong kind
|
||||
InvalidInputKind = 605,
|
||||
|
||||
// Creating the resource failed
|
||||
ResourceCreationFailed = 700,
|
||||
// Performing an action on the resource failed
|
||||
ResourceActionFailed = 701,
|
||||
// Processing the request failed unexpectedly (comment required)
|
||||
RequestProcessingFailed = 702,
|
||||
// The combination of request parameters cannot be used to perform an action
|
||||
CannotAct = 703,
|
||||
};
|
||||
};
|
84
src/requesthandler/types/RequestBatchExecutionType.h
Normal file
84
src/requesthandler/types/RequestBatchExecutionType.h
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace RequestBatchExecutionType {
|
||||
enum RequestBatchExecutionType {
|
||||
/**
|
||||
* Not a request batch.
|
||||
*
|
||||
* @enumIdentifier None
|
||||
* @enumValue -1
|
||||
* @enumType RequestBatchExecutionType
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
None = -1,
|
||||
/**
|
||||
* A request batch which processes all requests serially, as fast as possible.
|
||||
*
|
||||
* Note: To introduce artificial delay, use the `Sleep` request and the `sleepMillis` request field.
|
||||
*
|
||||
* @enumIdentifier SerialRealtime
|
||||
* @enumValue 0
|
||||
* @enumType RequestBatchExecutionType
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
SerialRealtime = 0,
|
||||
/**
|
||||
* A request batch type which processes all requests serially, in sync with the graphics thread. Designed to
|
||||
* provide high accuracy for animations.
|
||||
*
|
||||
* Note: To introduce artificial delay, use the `Sleep` request and the `sleepFrames` request field.
|
||||
*
|
||||
* @enumIdentifier SerialFrame
|
||||
* @enumValue 1
|
||||
* @enumType RequestBatchExecutionType
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
SerialFrame = 1,
|
||||
/**
|
||||
* A request batch type which processes all requests using all available threads in the thread pool.
|
||||
*
|
||||
* Note: This is mainly experimental, and only really shows its colors during requests which require lots of
|
||||
* active processing, like `GetSourceScreenshot`.
|
||||
*
|
||||
* @enumIdentifier Parallel
|
||||
* @enumValue 2
|
||||
* @enumType RequestBatchExecutionType
|
||||
* @rpcVersion -1
|
||||
* @initialVersion 5.0.0
|
||||
* @api enums
|
||||
*/
|
||||
Parallel = 2,
|
||||
};
|
||||
|
||||
inline bool IsValid(int executionType)
|
||||
{
|
||||
return executionType >= None && executionType <= Parallel;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user