Compare commits

...

127 Commits
4.2.0 ... 4.3.3

Author SHA1 Message Date
6eb49930bf general: version bump to 4.3.3 2018-03-19 01:11:35 +01:00
3a0d5fb190 CI: let's try with VS2015 instead 2018-03-18 23:25:38 +01:00
8c2eee2e25 CI: update Windows builds to VS2017 and Qt 5.10.1 2018-03-18 23:25:31 +01:00
2e19c5f08a docs: update version 2018-03-14 23:03:14 +01:00
548b53437f general: version bump to 4.3.2 2018-03-14 22:58:53 +01:00
e1ca9a8029 notifications: check if system tray is available before notifying 2018-03-14 22:26:08 +01:00
389ef2aea9 Config: remove flawed qstring_data_copy and add toUtf8() macro 2018-03-14 22:26:00 +01:00
357691bad5 CI: bump to latest OBS version 2018-01-22 23:24:09 +01:00
beadb56b05 general: update version to 4.3.1 2018-01-22 23:13:27 +01:00
745fb5ea29 Merge branch 'scenecol-crash-fix' 2018-01-22 23:11:24 +01:00
58434cac3b events: fix crash on exit 2018-01-22 23:10:33 +01:00
c34dff17ff events: fix crash when switching between scene collections 2018-01-22 23:03:20 +01:00
73f00ca195 CI: update Packages version 2018-01-12 11:13:16 +01:00
40503b4212 docs(travis): Update protocol.md - 4ada388 [skip ci] 2018-01-12 08:28:38 +00:00
4ada388828 general: version bump 2018-01-12 09:25:41 +01:00
e2d261259d docs(travis): Update protocol.md - 677e393 [skip ci] 2018-01-12 08:20:52 +00:00
677e393f47 Add support for filename formatting get/set (#162) 2018-01-12 09:18:58 +01:00
e5dae8f5a7 UI: update translations from Crowdin 2018-01-12 08:54:12 +01:00
650de119a4 general: New Crowdin translations (#171) 2018-01-04 08:31:45 +01:00
6d7975afeb requests: Create folder for 'SetRecordingFolder' if it does not exist (#163) 2017-12-16 16:49:38 +01:00
811e3f8cc8 Update Crowdin configuration file 2017-11-14 10:09:14 +01:00
4f3be34db1 ui: update translations from Crowdin 2017-11-14 10:01:04 +01:00
b160fd2320 docs: update README and BUILDING documents 2017-11-14 09:44:24 +01:00
002bf08b97 docs(travis): Update protocol.md - a6bab96 [skip ci] 2017-11-14 08:24:23 +00:00
a6bab968f5 requests: Get/Set local file path in BrowserSource settings 2017-11-14 09:21:58 +01:00
408b336057 docs(travis): Update protocol.md - 7e57161 [skip ci] 2017-11-14 08:15:34 +00:00
7e5716185e code: reorder and refactor WSRequestHandlers 2017-11-14 09:14:33 +01:00
6d47bd6477 Merge pull request #159 from haganbmj/docs-safety
docs: fail CI build if documentation is invalid
2017-11-14 07:17:44 +01:00
989c8b1857 docs: fail CI build if documentation is invalid 2017-11-13 23:25:54 -05:00
06e9e0afab docs(travis): Update protocol.md - d75523c [skip ci] 2017-11-13 19:37:48 +00:00
d75523c111 Merge pull request #157 from haganbmj/patch-2
docs: update glob to account for relocation to src
2017-11-13 20:36:47 +01:00
dacec803c6 docs: Fix documentation failure 2017-11-13 13:57:42 -05:00
0ae4416242 docs: mustache helper safety check 2017-11-13 13:50:56 -05:00
079d7eb6ca docs(travis): Update protocol.md - b764b4d [skip ci] 2017-11-13 16:58:21 +00:00
b764b4d0e5 code: split WSRequestHandler into several code/compile units 2017-11-13 17:56:58 +01:00
2c2d61d908 docs(travis): Update protocol.md - 6a323b9 [skip ci] 2017-11-13 16:18:44 +00:00
6a323b9371 code: move code units to src/ 2017-11-13 17:17:34 +01:00
d76ff16162 ui: show a warning message when server can't bind to configured port 2017-11-13 16:39:32 +01:00
952f3a586a server: refactor connect/disconnect popup messages localization 2017-11-13 15:56:46 +01:00
3ecb9702ef docs(travis): Update protocol.md - 563936e [skip ci] 2017-11-13 14:03:18 +00:00
563936ea08 requests: add GetSourceTypesList 2017-11-13 15:01:41 +01:00
13cd2704ae Revert "docs: bump to 4.2.1 release"
This reverts commit ae83d9dca1.
2017-11-13 08:59:54 +01:00
ae83d9dca1 docs: bump to 4.2.1 release 2017-11-13 08:58:00 +01:00
66a059ecdf Automatic function-scope OBS _release() + refactor to camelCase (#149)
- Refactor all internal variables to camelCase
- Use obs pointers with *AutoRelease classes for automatic refcounting
    - Important: some pointers require prior investigation to determine if refcounting is needed, or which 
      between OBSRef and *AutoRelease should be used.
2017-11-13 08:44:26 +01:00
53936a4f76 request handler: use static request QHash
Inspired by @yinzara's fork
2017-11-06 13:03:05 +01:00
488a095fdf ci(macos): fix broken Packages installation 2017-11-06 12:34:47 +01:00
1878de9f6b docs(travis): Update protocol.md - 2a2f344 [skip ci] 2017-11-06 09:55:25 +00:00
2a2f3441ef requests: don't persist stream settings in StartStreaming 2017-11-06 10:53:05 +01:00
53c939a97f Merge pull request #146 from RytoEX/fix-ci
Fix CI builds on Windows and Linux
2017-10-28 18:13:20 +02:00
8d7ed32fc2 Windows CI: Build with Qt 5.7.1 if Qt 5.7.0 is unavailable 2017-10-28 00:29:48 -04:00
a1fa5dc3cb Windows CI: Only build obs-studio if not cached
Run a series of checks to determine if CI needs to build obs-studio
before trying to build obs-websocket.
2017-10-28 00:28:47 -04:00
4b9a84ccee docs(travis): Update protocol.md - 650957b [skip ci] 2017-10-27 21:10:39 +00:00
650957b6d1 Merge pull request #151 from haganbmj/patch-1
docs: correct GetSceneItemProperties name
2017-10-27 23:09:32 +02:00
6e21d041fe docs: correct GetSceneItemProperties name
Fixes #150
2017-10-27 12:45:29 -04:00
e2b70fd795 docs: realign comments 2017-10-27 15:51:23 +02:00
2d49bcc437 docs(travis): Update protocol.md - 8b3dce3 [skip ci] 2017-10-27 12:32:25 +00:00
8b3dce3256 requests: add GetSourcesList 2017-10-27 14:29:58 +02:00
8cf6a1e72c SetSourceSettings: fix oops 2017-10-27 14:29:41 +02:00
a3ce7197ac docs(travis): Update protocol.md - 5da1e55 [skip ci] 2017-10-27 12:27:53 +00:00
5da1e55f8e docs: Fix new requests names 2017-10-27 14:27:00 +02:00
661fd4efa8 docs(travis): Update protocol.md - 047e6e1 [skip ci] 2017-10-27 12:21:14 +00:00
047e6e11bf Merge remote-tracking branch 'origin/master' 2017-10-27 14:19:33 +02:00
db8bc1af2d requests: add GetSourceSettings and SetSourceSettings 2017-10-27 14:18:45 +02:00
1386e4f91c docs(travis): Update protocol.md - 11e2717 [skip ci] 2017-10-27 08:39:02 +00:00
11e2717809 docs(travis): Update protocol.md - 57bc0a2 2017-10-27 10:37:30 +02:00
57bc0a2b95 Merge pull request #136 from TStod/tstod/SceneItemGetter
[Safe For Merge] Unified Setter and Getter for scene specific source properties
2017-10-27 10:31:50 +02:00
8efb30c4ee Merge pull request #148 from haganbmj/docs-deprecated
docs: Add deprecated attribute
2017-10-27 10:31:16 +02:00
969feefcc5 Merge remote-tracking branch 'origin/master' 2017-10-27 10:25:07 +02:00
db1527ab9b StartStreaming: add missing comment 2017-10-27 10:24:58 +02:00
e88a60fa50 docs(travis): Update protocol.md - 7ab3e38 [skip ci] 2017-10-27 08:21:09 +00:00
7ab3e38da7 StartStreaming: fix request docs + code clean 2017-10-27 10:19:27 +02:00
7b0b836809 requests: continued refactor of Get/Set/Save/Start stream settings 2017-10-27 10:03:22 +02:00
c8e7cfcd7b requests: Refactor Get/SetStreamSettings code 2017-10-27 09:10:04 +02:00
c516c89c97 docs: Add deprecated attribute 2017-10-25 11:59:14 -05:00
c2937d7857 replay buffer: start without "Save" hotkey set 2017-10-25 18:29:42 +02:00
fe644cfa82 Merge pull request #142 from RytoEX/save-replay-buffer
Fix saving replay buffer when no hotkey is set
2017-10-25 09:46:28 +02:00
e84e5388a5 fixing scale data get in setter 2017-10-24 11:54:21 -04:00
5e11d0ea13 general: style fixes + change request handler lookup 2017-10-24 17:16:06 +02:00
a3ecb6e0e9 adding missing scale setter in setter 2017-10-24 02:16:39 -04:00
1d30f13fd8 adding error response object method 2017-10-23 12:04:43 -04:00
b9ae28483c adding error messages 2017-10-23 11:34:29 -04:00
6a6d316e09 ci(linux + win): bump to latest OBS version 2017-10-22 00:01:44 +02:00
22d2ee6bbd Merge pull request #141 from PicoCentauri/master
macOS CI: Updated dependencies and namespaces
2017-10-21 23:57:41 +02:00
68e55613fc macOS CI: Updated dependencies and namespaces
Following the discussion of #105 the Qt version in the dependency
installation is now hardcoded to version 5.9.2, which is the same as the
main obs version for 20.1.0. Furthermore the names are changed from
osx to MacOS.
2017-10-19 17:32:58 +02:00
1d44d3a109 Fix saving replay buffer when no hotkey is set
This commit makes saving the replay buffer not rely on whether or not a
hotkey is set in OBS.
2017-10-19 05:34:39 -04:00
11a641cc0d replay buffer: update for Advanced output mode 2017-10-19 10:09:00 +02:00
7570fbf7a4 strings to ENUM strings 2017-10-17 19:07:40 -04:00
67f7e28867 fixing bounds type setter 2017-10-16 12:29:49 -04:00
9b2d30b4d5 switch string to ifelseif 2017-10-16 12:17:29 -04:00
399815525f wrapping up setter and getter 2017-10-16 12:01:41 -04:00
191cfa08d5 server: Refactor mutex lock handling 2017-10-16 15:29:23 +02:00
fcf1fa8aff studio mode: Remove parts of Qt UI hacks 2017-10-16 15:28:54 +02:00
73302cb060 docs(travis): Update protocol.md - 70a8533 [skip ci] 2017-10-16 11:13:41 +00:00
70a8533f5e docs(requests): fix SetStreamSettings 2017-10-16 13:12:49 +02:00
fe724db12d removing copy paste error 2017-10-15 17:28:07 -04:00
096a8ec6ba wip on setter and getter 2017-10-15 14:21:45 -04:00
cc5f9c9aa7 refining bounds and adding scale to setter 2017-10-15 11:57:21 -04:00
3e14b41600 ands to ors 2017-10-14 16:44:02 -04:00
ccc2bd8667 trying to fix duplicate case statment error 2017-10-14 16:35:36 -04:00
45f86b17f1 more WIP on setter 2017-10-14 15:18:06 -04:00
90aebecc5b small fixes 2017-10-14 14:36:23 -04:00
b0170ef671 more WIP on setter 2017-10-14 13:10:40 -04:00
d418b4e624 fixing methods for getting inner object keys 2017-10-11 12:07:33 -04:00
47505547af fixing some types 2017-10-11 11:51:05 -04:00
de2e73c9d4 WIP setter and fixing small typo 2017-10-10 21:14:27 -04:00
ab1a43163b early return on bad param 2017-10-04 13:26:15 -04:00
2556dd320f fixing bounds alignemnt getter 2017-10-04 13:05:01 -04:00
2120381c0e correnting method name 2017-10-04 12:48:40 -04:00
dc06900f7f another typo 2017-10-04 12:44:07 -04:00
7571dd5042 fixing typo 2017-10-04 12:13:52 -04:00
d6caa872b8 WIP some small fixes 2017-10-04 02:06:38 -04:00
2e9829ddd1 WIP questions to be answered 2017-10-04 00:51:16 -04:00
9621ea90f7 more WIP 2017-10-03 20:30:51 -04:00
f58da1254b wip 2017-10-03 20:23:55 -04:00
a6ba7f8feb docs(travis): Update protocol.md - 9c89f12 [skip ci] 2017-10-02 07:08:16 +00:00
9c89f12275 docs: fix spec version links 2017-10-02 09:07:17 +02:00
41181f7260 docs(travis): Update protocol.md - 7251862 [skip ci] 2017-09-26 09:21:30 +00:00
7251862ecf Merge pull request #124 from haganbmj/patch-2
docs: HandleSetHeartbeat -> SetHeartbeat
2017-09-26 11:19:15 +02:00
c0512d5b5f docs: HandleSetHeartbeat -> SetHeartbeat 2017-09-25 11:32:34 -05:00
b9731dff21 docs(travis): Update protocol.md - 3981abc [skip ci] 2017-09-25 14:50:02 +00:00
3981abc5ca General: code and docs cleanup 2017-09-25 16:48:46 +02:00
6aef437f58 UI: update French localization 2017-09-25 16:29:50 +02:00
9b7752896a Adding Heartbeat to obs-websocket. (#112)
* Added a Heartbeat in WSEvents (.cpp and .h updated)
Lines where the coded is added are marked with "//mod-0x =============" (x denotes a number).

* Updated the Heartbeat:
- Default it is off, and via JSON request can be switched on.
- The Heartbeat has configurable elements. Each individual element can be requested via a JSON request to be added to the Heartbeat.
- Maximum elements are in this example JSON update:
{
    "current-profile": "#2-PGH",
    "current-scene": "#2-HG",
    "pulse": true,
    "recording": false,
    "streaming": false,
    "total-record-time": 0,
    "total-stream-time": 0,
    "update-type": "Heartbeat"
}

* Cleaning

* Process all Palakis his comment/advice, and added 3 key/value pairs.

* Processed cleaning up comments as proposed by Stephane.

* Still found tabs in the comment header, and replaced by spaces.

* FUNCTIONAL MODIFICATION:
- Removed selective content of Heartbeat, and show all key/value parameters.
- Only request left is "on" or "off" selection of heartbeat.
FIX:
- Show streaming/recording related data only when streaming/recording is active, due to OBS showing wrong values when not active.

* Changed request type to start the Heartbeat to a boolean parameter.

* Delete ZERO_CHECK.log

* Delete obs-websocket_autogen.log

* Delete obs-websocket.log
2017-09-24 22:54:29 +02:00
d074027610 Merge pull request #121 from haganbmj/toggle-notifications
general(notifications): Toggle tray notifications/alerts (Closes #120)
2017-09-24 22:53:41 +02:00
b137148186 general(notifications): Toggle tray notifications/alerts
Closes #120
2017-09-12 10:41:35 -05:00
e1f6260034 docs(travis): Update protocol.md - b24cbaa [skip ci] 2017-09-08 20:14:45 +00:00
b24cbaa904 General: version bump to 4.2.1 2017-09-08 22:13:58 +02:00
63 changed files with 6969 additions and 4604 deletions

View File

@ -35,10 +35,10 @@ matrix:
- os: osx - os: osx
env: _macos_build env: _macos_build
osx_image: xcode8.3 osx_image: xcode8.3
before_install: "./CI/install-dependencies-osx.sh" before_install: "./CI/install-dependencies-macos.sh"
script: "./CI/build-osx.sh" script: "./CI/build-macos.sh"
after_success: after_success:
- ./CI/package-osx.sh - ./CI/package-macos.sh
deploy: deploy:
- provider: s3 - provider: s3

View File

@ -1,6 +1,6 @@
# Compiling obs-websocket # Compiling obs-websocket
## Prerequisites ## Prerequisites
You'll need [QT 5.7.0](https://download.qt.io/official_releases/qt/5.7/5.7.0/), CMake, and a working development environment for OBS Studio installed on your computer. You'll need [QT 5.9.0](https://download.qt.io/official_releases/qt/5.7/5.7.0/), CMake, and a working development environment for OBS Studio installed on your computer.
## Windows ## Windows
In cmake-gui, you'll have to set the following variables : In cmake-gui, you'll have to set the following variables :
@ -22,7 +22,16 @@ sudo make install
``` ```
## OS X ## OS X
*To do* Use of the Travis 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-dependencies-macos.sh` will install it for you.
Of course, you're encouraged to dig through the contents of these scripts to look for issues or specificities.
```
git clone --recursive https://github.com/Palakis/obs-websocket.git
cd obs-websocket
./CI/install-dependencies-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 ## Automated Builds
- Windows : [![Automated Build status for Windows](https://ci.appveyor.com/api/projects/status/github/Palakis/obs-websocket)](https://ci.appveyor.com/project/Palakis/obs-websocket/history) - Windows : [![Automated Build status for Windows](https://ci.appveyor.com/api/projects/status/github/Palakis/obs-websocket)](https://ci.appveyor.com/project/Palakis/obs-websocket/history)

View File

@ -7,6 +7,7 @@ mkdir build && cd build
cmake .. \ cmake .. \
-DQTDIR=/usr/local/opt/qt \ -DQTDIR=/usr/local/opt/qt \
-DLIBOBS_INCLUDE_DIR=../../obs-studio/libobs \ -DLIBOBS_INCLUDE_DIR=../../obs-studio/libobs \
-DLIBOBS_LIB=../../obs-studio/libobs \
-DOBS_FRONTEND_LIB="$(pwd)/../../obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" \ -DOBS_FRONTEND_LIB="$(pwd)/../../obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" \
-DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_INSTALL_PREFIX=/usr \
&& make -j4 && make -j4

View File

@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
set -e
echo "-- Generating documentation." echo "-- Generating documentation."
echo "-- Node version: $(node -v)" echo "-- Node version: $(node -v)"
echo "-- NPM version: $(npm -v)" echo "-- NPM version: $(npm -v)"

118
CI/install-build-obs.cmd Normal file
View File

@ -0,0 +1,118 @@
@echo off
SETLOCAL EnableDelayedExpansion
REM Check if obs-studio build exists.
REM If the obs-studio directory does exist, check if the last OBS tag built
REM matches the latest OBS tag.
REM If the tags match, do not build obs-studio.
REM If the tags do not match, build obs-studio.
REM If the obs-studio directory doesn't exist, build obs-studio.
echo Checking for obs-studio build...
set OBSLatestTagPrePull=0
set OBSLatestTagPostPull=0
echo Latest tag pre-pull: %OBSLatestTagPrePull%
echo Latest tag post-pull: %OBSLatestTagPostPull%
REM Set up the build flag as undefined.
set "BuildOBS="
REM Check the last tag successfully built by CI.
if exist C:\projects\obs-studio-last-tag-built.txt (
set /p OBSLastTagBuilt=<C:\projects\obs-studio-last-tag-built.txt
) else (
set OBSLastTagBuilt=0
)
REM If obs-studio directory exists, run git pull and get the latest tag number.
if exist C:\projects\obs-studio\ (
echo obs-studio directory exists
echo Updating tag info
cd C:\projects\obs-studio\
git describe --tags --abbrev=0 > C:\projects\latest-obs-studio-tag-pre-pull.txt
set /p OBSLatestTagPrePull=<C:\projects\latest-obs-studio-tag-pre-pull.txt
git checkout master
git pull
git describe --tags --abbrev=0 > C:\projects\latest-obs-studio-tag-post-pull.txt
set /p OBSLatestTagPostPull=<C:\projects\latest-obs-studio-tag-post-pull.txt
set /p OBSLatestTag=<C:\projects\latest-obs-studio-tag-post-pull.txt
echo %OBSLatestTagPostPull%> C:\projects\latest-obs-studio-tag.txt
)
REM Check the obs-studio tags for mismatches.
REM If a new tag was pulled, set the build flag.
if not %OBSLatestTagPrePull%==%OBSLatestTagPostPull% (
echo Latest tag pre-pull: %OBSLatestTagPrePull%
echo Latest tag post-pull: %OBSLatestTagPostPull%
echo Tags do not match. Need to rebuild OBS.
set BuildOBS=true
)
REM If the latest git tag doesn't match the last built tag, set the build flag.
if not %OBSLatestTagPostPull%==%OBSLastTagBuilt% (
echo Last built OBS tag: %OBSLastTagBuilt%
echo Latest tag post-pull: %OBSLatestTagPostPull%
echo Tags do not match. Need to rebuild OBS.
set BuildOBS=true
)
REM If obs-studio directory does not exist, clone the git repo, get the latest
REM tag number, and set the build flag.
if not exist C:\projects\obs-studio (
echo obs-studio directory does not exist
git clone --recursive https://github.com/jp9000/obs-studio
cd C:\projects\obs-studio\
git describe --tags --abbrev=0 > C:\projects\obs-studio-latest-tag.txt
set /p OBSLatestTag=<C:\projects\obs-studio-latest-tag.txt
set BuildOBS=true
)
REM Some debug info
echo:
echo Latest tag pre-pull: %OBSLatestTagPrePull%
echo Latest tag post-pull: %OBSLatestTagPostPull%
echo Latest tag: %OBSLatestTag%
echo Last built OBS tag: %OBSLastTagBuilt%
if defined BuildOBS (
echo BuildOBS: true
) else (
echo BuildOBS: false
)
echo:
REM If the build flag is set, build obs-studio.
if defined BuildOBS (
echo Building obs-studio...
echo git checkout %OBSLatestTag%
git checkout %OBSLatestTag%
echo:
echo Removing previous build dirs...
if exist build rmdir /s /q C:\projects\obs-studio\build
if exist build32 rmdir /s /q C:\projects\obs-studio\build32
if exist build64 rmdir /s /q C:\projects\obs-studio\build64
echo Making new build dirs...
mkdir build
mkdir build32
mkdir build64
echo Running cmake for obs-studio %OBSLatestTag% 32-bit...
cd ./build32
cmake -G "Visual Studio 14 2015" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true ..
echo:
echo:
echo Running cmake for obs-studio %OBSLatestTag% 64-bit...
cd ../build64
cmake -G "Visual Studio 14 2015 Win64" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true ..
echo:
echo:
echo Building obs-studio %OBSLatestTag% 32-bit ^(Build Config: %build_config%^)...
call msbuild /m /p:Configuration=%build_config% C:\projects\obs-studio\build32\obs-studio.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
echo Building obs-studio %OBSLatestTag% 64-bit ^(Build Config: %build_config%^)...
call msbuild /m /p:Configuration=%build_config% C:\projects\obs-studio\build64\obs-studio.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
cd ..
git describe --tags --abbrev=0 > C:\projects\obs-studio-last-tag-built.txt
set /p OBSLastTagBuilt=<C:\projects\obs-studio-last-tag-built.txt
) else (
echo Last OBS tag built is: %OBSLastTagBuilt%
echo No need to rebuild OBS.
)

View File

@ -7,22 +7,26 @@ brew install ffmpeg
brew install libav brew install libav
# qtwebsockets deps # qtwebsockets deps
brew install qt5 # qt latest
#brew install qt5
# qt 5.9.2
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/2b121c9a96e58a5da14228630cb71d5bead7137e/Formula/qt.rb
#echo "Qt path: $(find /usr/local/Cellar/qt5 -d 1 | tail -n 1)" #echo "Qt path: $(find /usr/local/Cellar/qt5 -d 1 | tail -n 1)"
# Build obs-studio # Build obs-studio
cd .. cd ..
git clone --recursive https://github.com/jp9000/obs-studio git clone --recursive https://github.com/jp9000/obs-studio
cd obs-studio cd obs-studio
git checkout 19.0.3 git checkout 21.0.0
mkdir build && cd build mkdir build && cd build
cmake .. \ cmake .. \
-DCMAKE_PREFIX_PATH=/usr/local/opt/qt/lib/cmake \ -DCMAKE_PREFIX_PATH=/usr/local/opt/qt/lib/cmake \
&& make -j4 && make -j4
sudo make install
# Packages app # Packages app
cd .. cd ..
curl -L -O https://www.slepin.fr/obs-websocket/ci/Packages.pkg -f --retry 5 -C - curl -L -O http://s.sudre.free.fr/Software/files/Packages.dmg -f --retry 5 -C -
sudo installer -pkg ./Packages.pkg -target / hdiutil attach ./Packages.dmg
sudo installer -pkg /Volumes/Packages\ 1.2.2/packages/Packages.pkg -target /

View File

@ -48,7 +48,7 @@ apt-get install -y libqt5websockets5-dev
cd /root cd /root
git clone https://github.com/jp9000/obs-studio ./obs-studio git clone https://github.com/jp9000/obs-studio ./obs-studio
cd obs-studio cd obs-studio
git checkout 19.0.3 git checkout 21.0.0
mkdir build && cd build mkdir build && cd build
cmake -DUNIX_STRUCTURE=1 -DCMAKE_INSTALL_PREFIX=/usr .. cmake -DUNIX_STRUCTURE=1 -DCMAKE_INSTALL_PREFIX=/usr ..
make -j4 make -j4

6
CI/install-setup-qt.cmd Normal file
View File

@ -0,0 +1,6 @@
@echo off
REM Set default values to use AppVeyor's built-in Qt.
set QTDIR32=C:\Qt\5.10.1\msvc2015
set QTDIR64=C:\Qt\5.10.1\msvc2015_64
set QTCompileVersion=5.10.1

View File

@ -635,7 +635,7 @@
<key>OVERWRITE_PERMISSIONS</key> <key>OVERWRITE_PERMISSIONS</key>
<false/> <false/>
<key>VERSION</key> <key>VERSION</key>
<string>4.0.0</string> <string>4.3.3</string>
</dict> </dict>
<key>PROJECT_COMMENTS</key> <key>PROJECT_COMMENTS</key>
<dict> <dict>

View File

@ -59,7 +59,7 @@ otool -L ./build/obs-websocket.so
chmod -w ./build/QtWebSockets ./build/QtNetwork chmod -w ./build/QtWebSockets ./build/QtNetwork
echo "-- Actual package build" echo "-- Actual package build"
packagesbuild ./CI/osx/obs-websocket.pkgproj packagesbuild ./CI/macos/obs-websocket.pkgproj
echo "-- Renaming obs-websocket.pkg to $FILENAME" echo "-- Renaming obs-websocket.pkg to $FILENAME"
mv ./release/obs-websocket.pkg ./release/$FILENAME mv ./release/obs-websocket.pkg ./release/$FILENAME

View File

@ -17,22 +17,33 @@ add_subdirectory(deps/mbedtls EXCLUDE_FROM_ALL)
set(ENABLE_PROGRAMS false) set(ENABLE_PROGRAMS false)
set(obs-websocket_SOURCES set(obs-websocket_SOURCES
obs-websocket.cpp src/obs-websocket.cpp
WSServer.cpp src/WSServer.cpp
WSRequestHandler.cpp src/WSRequestHandler.cpp
WSEvents.cpp src/WSRequestHandler_General.cpp
Config.cpp src/WSRequestHandler_Profiles.cpp
Utils.cpp src/WSRequestHandler_Recording.cpp
forms/settings-dialog.cpp) src/WSRequestHandler_ReplayBuffer.cpp
src/WSRequestHandler_SceneCollections.cpp
src/WSRequestHandler_Scenes.cpp
src/WSRequestHandler_SceneItems.cpp
src/WSRequestHandler_Sources.cpp
src/WSRequestHandler_Streaming.cpp
src/WSRequestHandler_StudioMode.cpp
src/WSRequestHandler_Transitions.cpp
src/WSEvents.cpp
src/Config.cpp
src/Utils.cpp
src/forms/settings-dialog.cpp)
set(obs-websocket_HEADERS set(obs-websocket_HEADERS
obs-websocket.h src/obs-websocket.h
WSServer.h src/WSServer.h
WSRequestHandler.h src/WSRequestHandler.h
WSEvents.h src/WSEvents.h
Config.h src/Config.h
Utils.h src/Utils.h
forms/settings-dialog.h) src/forms/settings-dialog.h)
# --- Platform-independent build settings --- # --- Platform-independent build settings ---
add_library(obs-websocket MODULE add_library(obs-websocket MODULE

View File

@ -41,16 +41,21 @@ See the [build instructions](BUILDING.md).
In order of appearance: In order of appearance:
- [Brendan H.](https://github.com/haganbmj) : Code contributions and gooder English in the Protocol specification - [Brendan H.](https://github.com/haganbmj) : Code contributions and gooder English in the Protocol specification
- [Mikhail Swift](https://github.com/mikhailswift) : Code contributions - [Mikhail Swift](https://github.com/mikhailswift) : Code contributions
- [Tobias Frahmer](https://github.com/Frahmer) : German translation - [Tobias Frahmer](https://github.com/Frahmer) : German localization
- [Genture](https://github.com/Genteure) : Simplified Chinese and Traditional Chinese translations - [Genture](https://github.com/Genteure) : Simplified Chinese and Traditional Chinese localizations
- [Larissa Gabilan](https://github.com/laris151) : Portuguese translation - [Larissa Gabilan](https://github.com/laris151) : Portuguese localization
- [Andy Asquelt](https://github.com/asquelt) : Polish translation - [Andy Asquelt](https://github.com/asquelt) : Polish localization
- [Marcel Haazen](https://github.com/inpothet) : Dutch translation - [Marcel Haazen](https://github.com/inpothet) : Dutch localization
- [Peter Antonvich](https://github.com/pantonvich) : Code contributions - [Peter Antonvich](https://github.com/pantonvich) : Code contributions
- [yinzara](https://github.com/yinzara) : Code contributions - [yinzara](https://github.com/yinzara) : Code contributions
- [Chris Angelico](https://github.com/Rosuav) : Code contributions - [Chris Angelico](https://github.com/Rosuav) : Code contributions
- [Guillaume "Elektordi" Genty](https://github.com/Elektordi) : Code contributions - [Guillaume "Elektordi" Genty](https://github.com/Elektordi) : Code contributions
- [Marwin M](https://github.com/dragonbane0) : Code contributions - [Marwin M](https://github.com/dragonbane0) : Code contributions
- [Logan S.](https://github.com/lsdaniel) : Code contributions
- [RainbowEK](https://github.com/RainbowEK) : Code contributions
- [RytoEX](https://github.com/RytoEX) : CI script and code contributions
- [Theodore Stoddard](https://github.com/TStod) : Code contributions
- [Philip Loche](https://github.com/PicoCentauri) : Code contributions
And also: special thanks to supporters of the project! And also: special thanks to supporters of the project!

File diff suppressed because it is too large Load Diff

View File

@ -4,34 +4,20 @@ environment:
install: install:
- git submodule update --init --recursive - git submodule update --init --recursive
- cd C:\projects\ - cd C:\projects\
- if not exist dependencies2013.zip curl -kLO https://obsproject.com/downloads/dependencies2013.zip -f --retry 5 -C - - if not exist dependencies2015.zip curl -kLO https://obsproject.com/downloads/dependencies2015.zip -f --retry 5 -C -
- 7z x dependencies2013.zip -odependencies2013 - 7z x dependencies2015.zip -odependencies2015
- if not exist qt570.zip curl -kLO https://www.slepin.fr/obs-websocket/ci/qt570.zip -f --retry 5 -C - - set DepsPath32=%CD%\dependencies2015\win32
- 7z x qt570.zip -o"Qt5.7.0" - set DepsPath64=%CD%\dependencies2015\win64
- set DepsPath32=%CD%\dependencies2013\win32 - call C:\projects\obs-websocket\CI\install-setup-qt.cmd
- set DepsPath64=%CD%\dependencies2013\win64
- set QTDIR32=%CD%\Qt5.7.0\msvc2013
- set QTDIR64=%CD%\Qt5.7.0\msvc2013_64
- set build_config=Release - set build_config=Release
- git clone --recursive https://github.com/jp9000/obs-studio - call C:\projects\obs-websocket\CI\install-build-obs.cmd
- cd C:\projects\obs-studio\
- git checkout 19.0.3
- mkdir build
- mkdir build32
- mkdir build64
- cd ./build32
- cmake -G "Visual Studio 12 2013" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true ..
- cd ../build64
- cmake -G "Visual Studio 12 2013 Win64" -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true ..
- call msbuild /m /p:Configuration=%build_config% C:\projects\obs-studio\build32\obs-studio.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
- call msbuild /m /p:Configuration=%build_config% C:\projects\obs-studio\build64\obs-studio.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
- cd C:\projects\obs-websocket\ - cd C:\projects\obs-websocket\
- mkdir build32 - mkdir build32
- mkdir build64 - mkdir build64
- cd ./build32 - cd ./build32
- cmake -G "Visual Studio 12 2013" -DQTDIR="%QTDIR32%" -DLibObs_DIR="C:\projects\obs-studio\build32\libobs" -DLIBOBS_INCLUDE_DIR="C:\projects\obs-studio\libobs" -DLIBOBS_LIB="C:\projects\obs-studio\build32\libobs\%build_config%\obs.lib" -DOBS_FRONTEND_LIB="C:\projects\obs-studio\build32\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib" .. - cmake -G "Visual Studio 14 2015" -DQTDIR="%QTDIR32%" -DLibObs_DIR="C:\projects\obs-studio\build32\libobs" -DLIBOBS_INCLUDE_DIR="C:\projects\obs-studio\libobs" -DLIBOBS_LIB="C:\projects\obs-studio\build32\libobs\%build_config%\obs.lib" -DOBS_FRONTEND_LIB="C:\projects\obs-studio\build32\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib" ..
- cd ../build64 - cd ../build64
- cmake -G "Visual Studio 12 2013 Win64" -DQTDIR="%QTDIR64%" -DLibObs_DIR="C:\projects\obs-studio\build64\libobs" -DLIBOBS_INCLUDE_DIR="C:\projects\obs-studio\libobs" -DLIBOBS_LIB="C:\projects\obs-studio\build64\libobs\%build_config%\obs.lib" -DOBS_FRONTEND_LIB="C:\projects\obs-studio\build64\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib" .. - cmake -G "Visual Studio 14 2015 Win64" -DQTDIR="%QTDIR64%" -DLibObs_DIR="C:\projects\obs-studio\build64\libobs" -DLIBOBS_INCLUDE_DIR="C:\projects\obs-studio\libobs" -DLIBOBS_LIB="C:\projects\obs-studio\build64\libobs\%build_config%\obs.lib" -DOBS_FRONTEND_LIB="C:\projects\obs-studio\build64\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib" ..
build_script: build_script:
- call msbuild /m /p:Configuration=%build_config% C:\projects\obs-websocket\build32\obs-websocket.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - call msbuild /m /p:Configuration=%build_config% C:\projects\obs-websocket\build32\obs-websocket.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
@ -46,5 +32,6 @@ deploy_script:
test: off test: off
cache: cache:
- C:\projects\dependencies2013.zip - C:\projects\dependencies2015.zip
- C:\projects\qt570.zip - C:\projects\obs-studio-last-tag-built.txt
- C:\projects\obs-studio\

3
crowdin.yml Normal file
View File

@ -0,0 +1,3 @@
files:
- source: /data/locale/en-US.ini
translation: /data/locale/%locale%.ini

View File

@ -4,3 +4,11 @@ OBSWebsocket.Settings.ServerEnable="Websocket-Server aktivieren"
OBSWebsocket.Settings.ServerPort="Server Port" OBSWebsocket.Settings.ServerPort="Server Port"
OBSWebsocket.Settings.AuthRequired="Authentifizierung erforderlich" OBSWebsocket.Settings.AuthRequired="Authentifizierung erforderlich"
OBSWebsocket.Settings.Password="Passwort" OBSWebsocket.Settings.Password="Passwort"
OBSWebsocket.Settings.DebugEnable="Debug-Protokollierung aktivieren"
OBSWebsocket.Settings.AlertsEnable="Infobereich-Benachrichtigungen aktivieren"
OBSWebsocket.NotifyConnect.Title="Neue WebSocket Verbindung"
OBSWebsocket.NotifyConnect.Message="Client %1 verbunden"
OBSWebsocket.NotifyDisconnect.Title="WebSocket-Client getrennt"
OBSWebsocket.NotifyDisconnect.Message="Client %1 getrennt"
OBSWebsocket.Server.StartFailed.Title="WebSocket-Server Fehler"
OBSWebsocket.Server.StartFailed.Message="Der WebSocket-Server konnte nicht gestartet werden, mögliche Gründe:\n - TCP Port %1 wird möglicherweise gerade von einem anderen Programm verwendet. Versuchen Sie einen anderen Port in den WebSocket-Server Einstellungen zu setzten oder alle Programme zu beenden, die den Port möglicherweise verwenden.\n - Ein unbekannter Netzwerkfehler ist aufgetreten. Versuchen Sie es erneut mit anderen Einstellungen, einem OBS neustart oder einem System neustart."

View File

@ -5,6 +5,10 @@ OBSWebsocket.Settings.ServerPort="Server Port"
OBSWebsocket.Settings.AuthRequired="Enable authentication" OBSWebsocket.Settings.AuthRequired="Enable authentication"
OBSWebsocket.Settings.Password="Password" OBSWebsocket.Settings.Password="Password"
OBSWebsocket.Settings.DebugEnable="Enable debug logging" OBSWebsocket.Settings.DebugEnable="Enable debug logging"
OBSWebsocket.ConnectNotify.Connected="New WebSocket connection" OBSWebsocket.Settings.AlertsEnable="Enable System Tray Alerts"
OBSWebsocket.ConnectNotify.Disconnected="WebSocket client disconnected" OBSWebsocket.NotifyConnect.Title="New WebSocket connection"
OBSWebsocket.ConnectNotify.ClientIP="Client Address:" OBSWebsocket.NotifyConnect.Message="Client %1 connected"
OBSWebsocket.NotifyDisconnect.Title="WebSocket client disconnected"
OBSWebsocket.NotifyDisconnect.Message="Client %1 disconnected"
OBSWebsocket.Server.StartFailed.Title="WebSocket Server failure"
OBSWebsocket.Server.StartFailed.Message="The obs-websocket server failed to start, maybe because:\n - TCP port %1 may currently be in use elsewhere on this system, possibly by another application. Try setting a different TCP port in the WebSocket server settings, or stop any application that could be using this port.\n - An unknown network error happened on your system. Try again by changing settings, restarting OBS or restarting your system."

14
data/locale/es-ES.ini Normal file
View File

@ -0,0 +1,14 @@
OBSWebsocket.Menu.SettingsItem="Configuración del servidor obs-websocket"
OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Habilitar el servidor Websocket"
OBSWebsocket.Settings.ServerPort="Puerto del Servidor"
OBSWebsocket.Settings.AuthRequired="Habilitar autenticación"
OBSWebsocket.Settings.Password="Contraseña"
OBSWebsocket.Settings.DebugEnable="Habilitar registro de depuración"
OBSWebsocket.Settings.AlertsEnable="Habilitar alertas de la bandeja de sistema"
OBSWebsocket.NotifyConnect.Title="Nueva conexión WebSocket"
OBSWebsocket.NotifyConnect.Message="Cliente %1 conectado"
OBSWebsocket.NotifyDisconnect.Title="Cliente WebSocket desconectado"
OBSWebsocket.NotifyDisconnect.Message="Cliente %1 desconectado"
OBSWebsocket.Server.StartFailed.Title="Fallo del servidor WebSocket"
OBSWebsocket.Server.StartFailed.Message="El servidor obs-websocket no se pudo iniciar, tal vez porque: \n - el puerto TCP %1 podría estar actualmente en uso en este sistema, posiblemente por otra aplicación. Intente configurar un puerto TCP diferente en la configuración del servidor WebSocket, o detenga cualquier aplicación que pudiese estar utilizando este puerto \n - Un error de red desconocido ha ocurrido en su sistema. Inténtalo de nuevo cambiando la configuración, reiniciando OBS o reiniciando su sistema."

View File

@ -1,9 +1,13 @@
OBSWebsocket.Menu.SettingsItem="Paramètres du serveur Websocket" OBSWebsocket.Menu.SettingsItem="Paramètres du serveur Websocket"
OBSWebsocket.Settings.DialogTitle="obs-websocket" OBSWebsocket.Settings.ServerEnable="Activer le serveur WebSocket"
OBSWebsocket.Settings.ServerEnable="Activer le serveur Websockets"
OBSWebsocket.Settings.ServerPort="Port du serveur" OBSWebsocket.Settings.ServerPort="Port du serveur"
OBSWebsocket.Settings.AuthRequired="Activer l'authentification" OBSWebsocket.Settings.AuthRequired="Activer l'authentification"
OBSWebsocket.Settings.Password="Mot de passe" OBSWebsocket.Settings.Password="Mot de passe"
OBSWebsocket.ConnectNotify.Connected="Nouvelle connexion WebSocket" OBSWebsocket.Settings.DebugEnable="Débogage dans le fichier journal"
OBSWebsocket.ConnectNotify.Disconnected="Déconnexion WebSocket" OBSWebsocket.Settings.AlertsEnable="Notifications de connexion/déconnexion"
OBSWebsocket.ConnectNotify.ClientIP="Adresse du client :" OBSWebsocket.NotifyConnect.Title="Nouvelle connexion WebSocket"
OBSWebsocket.NotifyConnect.Message="Le client %1 s'est connecté"
OBSWebsocket.NotifyDisconnect.Title="Déconnexion WebSocket"
OBSWebsocket.NotifyDisconnect.Message="Le client %1 s'est déconnecté"
OBSWebsocket.Server.StartFailed.Title="Impossible de démarrer le serveur WebSocket"
OBSWebsocket.Server.StartFailed.Message="Le serveur WebSocket n'a pas pu démarrer, peut-être parce que :\n - Le port TCP %1 est en cours d'utilisation sur ce système, certainement par un autre programme. Essayez un port différent dans les réglages du serveur WebSocket, ou arrêtez tout programme susceptible d'utiliser ce port.\n - Une erreur réseau inconnue est survenue. Essayez à nouveau en modifiant vos réglages, en redémarrant OBS ou en redémarrant votre ordinateur."

14
data/locale/it-IT.ini Normal file
View File

@ -0,0 +1,14 @@
OBSWebsocket.Menu.SettingsItem="Impostazioni del server di WebSocket"
OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Abilitare il server Websocket"
OBSWebsocket.Settings.ServerPort="Porta del server"
OBSWebsocket.Settings.AuthRequired="Abilitare l'autenticazione"
OBSWebsocket.Settings.Password="Password"
OBSWebsocket.Settings.DebugEnable="Attivare la registrazione di debug"
OBSWebsocket.Settings.AlertsEnable="Attivare gli avvisi di vassoio di sistema"
OBSWebsocket.NotifyConnect.Title="Nuova connessione WebSocket"
OBSWebsocket.NotifyConnect.Message="%1 cliente collegato"
OBSWebsocket.NotifyDisconnect.Title="WebSocket cliente disconnesso"
OBSWebsocket.NotifyDisconnect.Message="%1 cliente disconnesso"
OBSWebsocket.Server.StartFailed.Title="Errore del WebSocket Server"
OBSWebsocket.Server.StartFailed.Message="Impossibile avviare, forse perché il server di obs-websocket: \n - %1 porta TCP potrebbe essere attualmente in uso altrove su questo sistema, possibilmente da un'altra applicazione. Provare a impostare una porta TCP diversa nelle impostazioni del server di WebSocket, o arrestare tutte le applicazioni che potrebbero utilizzare questa porta. \n - è verificato un errore di rete sconosciuto sul sistema. Riprova modificando le impostazioni, riavviare OBS o riavvio del sistema."

8
data/locale/ja-JP.ini Normal file
View File

@ -0,0 +1,8 @@
OBSWebsocket.Menu.SettingsItem="Websocket サーバー設定"
OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Websocket サーバーを有効にする"
OBSWebsocket.Settings.ServerPort="サーバーポート"
OBSWebsocket.Settings.AuthRequired="認証を有効にする"
OBSWebsocket.Settings.Password="パスワード"
OBSWebsocket.Settings.DebugEnable="デバッグログを有効にする"
OBSWebsocket.NotifyConnect.Title="新しいWebSocket接続"

0
data/locale/ko-KR.ini Normal file
View File

View File

@ -1,6 +1,14 @@
OBSWebsocket.Menu.SettingsItem="Websocket server instellingen" OBSWebsocket.Menu.SettingsItem="Websocket server instellingen"
OBSWebsocket.Settings.DialogTitle="obs-websocket" OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Activeer Websocket server" OBSWebsocket.Settings.ServerEnable="Activeer Websocket server"
OBSWebsocket.Settings.ServerPort="Server Poort" OBSWebsocket.Settings.ServerPort="Serverpoort"
OBSWebsocket.Settings.AuthRequired="Activeer authenticatie" OBSWebsocket.Settings.AuthRequired="Activeer authenticatie"
OBSWebsocket.Settings.Password="Wachtwoord" OBSWebsocket.Settings.Password="Wachtwoord"
OBSWebsocket.Settings.DebugEnable="Activeer debug logs"
OBSWebsocket.Settings.AlertsEnable="Systemvak waarschuwingen inschakelen"
OBSWebsocket.NotifyConnect.Title="Nieuwe WebSocket verbinding"
OBSWebsocket.NotifyConnect.Message="Client %1 verbonden"
OBSWebsocket.NotifyDisconnect.Title="WebSocket client connectie verbroken"
OBSWebsocket.NotifyDisconnect.Message="Client %1 losgekoppeld"
OBSWebsocket.Server.StartFailed.Title="WebSocket Server mislukt"
OBSWebsocket.Server.StartFailed.Message="De obs-websocket server kan niet worden gestart, misschien omdat: \n - TCP-poort %1 momenteel wordt gebruikt elders op dit systeem, eventueel door een andere toepassing. Probeer een andere TCP-poort instellen in de WebSocket Server-instellingen of stoppen van elke toepassing die deze poort zouden kunnen gebruiken.\n Een onbekende netwerkfout gebeurde op uw systeem. Probeer het opnieuw door de instellingen wijzigen, OBS herstarten of opnieuw opstarten van uw systeem."

View File

@ -4,3 +4,11 @@ OBSWebsocket.Settings.ServerEnable="Włącz serwer zdalnego sterowania (Websocke
OBSWebsocket.Settings.ServerPort="Port serwera" OBSWebsocket.Settings.ServerPort="Port serwera"
OBSWebsocket.Settings.AuthRequired="Wymagaj hasła" OBSWebsocket.Settings.AuthRequired="Wymagaj hasła"
OBSWebsocket.Settings.Password="Hasło" OBSWebsocket.Settings.Password="Hasło"
OBSWebsocket.Settings.DebugEnable="Włącz rejestrowanie debugowania"
OBSWebsocket.Settings.AlertsEnable="Włącz powiadomienia o zasobniku systemowym"
OBSWebsocket.NotifyConnect.Title="Nowe połączenie WebSocket"
OBSWebsocket.NotifyConnect.Message="Klient %1 połączony"
OBSWebsocket.NotifyDisconnect.Title="Klient WebSocket odłączony"
OBSWebsocket.NotifyDisconnect.Message="Klient %1 połączony"
OBSWebsocket.Server.StartFailed.Title="Awaria serwera WebSocket"
OBSWebsocket.Server.StartFailed.Message="Nie udało się uruchomić serwera obs-websocket, może a powodu: \n - TCP port %1 może być obecnie używany gdzie indziej w tym systemie, możliwie przez inną aplikację. Spróbuj ustawić inny port TCP w ustawieniach serwera WebSocket, lub zatrzymać dowolną aplikację, która może używać tego portu \n -nieznany błąd sieci wydarzył się w systemie. Spróbuj ponownie, zmieniając ustawienia, ponownie uruchamiając OBS lub ponownie uruchamiając system."

View File

@ -1,5 +1,4 @@
OBSWebsocket.Menu.SettingsItem="Configuraçes do Servidor Websocket" OBSWebsocket.Menu.SettingsItem="Configuraçes do Servidor Websocket"
OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Habilitar o Servidor Websocket" OBSWebsocket.Settings.ServerEnable="Habilitar o Servidor Websocket"
OBSWebsocket.Settings.ServerPort="Porta do Servidor" OBSWebsocket.Settings.ServerPort="Porta do Servidor"
OBSWebsocket.Settings.AuthRequired="Autenticação Requerida" OBSWebsocket.Settings.AuthRequired="Autenticação Requerida"

14
data/locale/pt-PT.ini Normal file
View File

@ -0,0 +1,14 @@
OBSWebsocket.Menu.SettingsItem="Configurações do servidor Websocket"
OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Habilitar servidor de WebSockets"
OBSWebsocket.Settings.ServerPort="Porta do Servidor"
OBSWebsocket.Settings.AuthRequired="Activar autenticação"
OBSWebsocket.Settings.Password="Palavra passe"
OBSWebsocket.Settings.DebugEnable="Habilitar registro de debug"
OBSWebsocket.Settings.AlertsEnable="Ativar Alertas da bandeja do sistema"
OBSWebsocket.NotifyConnect.Title="Nova conexão WebSocket"
OBSWebsocket.NotifyConnect.Message="Cliente %1 conectado"
OBSWebsocket.NotifyDisconnect.Title="WebSocket cliente desconectado"
OBSWebsocket.NotifyDisconnect.Message="Cliente %1 desconectado"
OBSWebsocket.Server.StartFailed.Title="Falha do servidor de WebSocket"
OBSWebsocket.Server.StartFailed.Message="O servidor obs-websocket falhou ao iniciar, talvez porque: \n - TCP port %1 pode estar atualmente em uso em outro lugar sobre este sistema, possivelmente por outro aplicativo. Tente definir uma porta TCP diferente nas configurações do servidor de WebSocket, ou parar qualquer aplicativo que poderia estar usando este porto \n - um erro de rede desconhecido aconteceu no seu sistema. Tente novamente alterar configurações, reiniciando OBS ou reiniciando o sistema."

14
data/locale/ru-RU.ini Normal file
View File

@ -0,0 +1,14 @@
OBSWebsocket.Menu.SettingsItem="Параметры сервера Websocket"
OBSWebsocket.Settings.DialogTitle="obs-websocket"
OBSWebsocket.Settings.ServerEnable="Включить сервер Websocket"
OBSWebsocket.Settings.ServerPort="Порт сервера"
OBSWebsocket.Settings.AuthRequired="Включить аутентификацию"
OBSWebsocket.Settings.Password="Пароль"
OBSWebsocket.Settings.DebugEnable="Включить ведение журнала отладки"
OBSWebsocket.Settings.AlertsEnable="Включить оповещения в системном трее"
OBSWebsocket.NotifyConnect.Title="Новое соединение WebSocket"
OBSWebsocket.NotifyConnect.Message="Клиент %1 подключен"
OBSWebsocket.NotifyDisconnect.Title="Клиент WebSocket отключён"
OBSWebsocket.NotifyDisconnect.Message="Клиент %1 отключен"
OBSWebsocket.Server.StartFailed.Title="Сбой сервера WebSocket"
OBSWebsocket.Server.StartFailed.Message="Сбой запуска сервера obs-websocket. Вероятные причины:\n - Возможно, TCP-порт %1 занят другим приложением в системе. Попробуйте задать другой TCP-порт в настройках сервера WebSocket или закройте приложение, которое может использовать данный порт.\n - Произошла неизвестная сетевая ошибка в системе. Попробуйте снова после изменения настроек, перезапуска OBS или перезапуска системы."

View File

@ -1,6 +1,8 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const glob = require('glob'); const glob = require('glob');
const parseComments = require('parse-comments'); const parseComments = require('parse-comments');
const config = require('./config.json');
/** /**
* Read each file and call `parse-comments` on it. * Read each file and call `parse-comments` on it.
@ -25,9 +27,15 @@ const parseFiles = files => {
*/ */
const processComments = comments => { const processComments = comments => {
let sorted = {}; let sorted = {};
let errors = [];
comments.forEach(comment => { comments.forEach(comment => {
if (typeof comment.api === 'undefined') 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). // Store the object based on its api (ie. requests, events) and category (ie. general, scenes, etc).
comment.category = comment.category || 'miscellaneous'; comment.category = comment.category || 'miscellaneous';
@ -44,9 +52,47 @@ const processComments = comments => {
sorted[comment.api][comment.category].push(comment); sorted[comment.api][comment.category].push(comment);
}); });
if (errors.length) {
throw JSON.stringify(errors, null, 2);
}
return sorted; return sorted;
}; };
const files = glob.sync("./../*.@(cpp|h)"); // 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)
};
}
return;
}
const files = glob.sync(config.srcGlob);
const comments = processComments(parseFiles(files)); const comments = processComments(parseFiles(files));
fs.writeFileSync('./generated/comments.json', JSON.stringify(comments, null, 2));
if (!fs.existsSync(config.outDirectory)){
fs.mkdirSync(config.outDirectory);
}
fs.writeFileSync(path.join(config.outDirectory, 'comments.json'), JSON.stringify(comments, null, 2));

5
docs/config.json Normal file
View File

@ -0,0 +1,5 @@
{
"srcGlob": "./../src/**/*.@(cpp|h)",
"srcTemplate": "./protocol.hbs",
"outDirectory": "./generated"
}

View File

@ -1,6 +1,8 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path');
const toc = require('markdown-toc'); const toc = require('markdown-toc');
const handlebars = require('handlebars'); const handlebars = require('handlebars');
const config = require('./config.json');
const helpers = require('handlebars-helpers')({ const helpers = require('handlebars-helpers')({
handlebars: handlebars handlebars: handlebars
@ -8,7 +10,7 @@ const helpers = require('handlebars-helpers')({
// Allows pipe characters to be used within markdown tables. // Allows pipe characters to be used within markdown tables.
handlebars.registerHelper('depipe', (text) => { handlebars.registerHelper('depipe', (text) => {
return text.replace('|', `\\|`); return typeof text === 'string' ? text.replace('|', '\\|') : text;
}); });
const insertHeader = (text) => { const insertHeader = (text) => {
@ -26,6 +28,10 @@ const generateProtocol = (templatePath, data) => {
return insertHeader(toc.insert(generated)); return insertHeader(toc.insert(generated));
}; };
const comments = fs.readFileSync('./generated/comments.json', 'utf8'); if (!fs.existsSync(config.outDirectory)){
const markdown = generateProtocol('./protocol.hbs', JSON.parse(comments)); fs.mkdirSync(config.outDirectory);
fs.writeFileSync('./generated/protocol.md', markdown); }
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);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,4 @@
# obs-websocket 4.2.0 protocol reference # obs-websocket 4.3.2 protocol reference
**This is the reference for obs-websocket 4.2.0. See the list below for older versions.**
- [4.1.0 protocol reference](https://github.com/Palakis/obs-websocket/blob/4.1.0/PROTOCOL.md)
- [4.0.0 protocol reference](https://github.com/Palakis/obs-websocket/blob/4.0.0/PROTOCOL.md)
# General Introduction # General Introduction
Messages are exchanged between the client and the server as JSON objects. Messages are exchanged between the client and the server as JSON objects.

View File

@ -15,6 +15,10 @@
{{#each this}} {{#each this}}
### {{name}} ### {{name}}
{{#if deprecated}}
- **⚠️ Deprecated. {{deprecated}} ⚠️**
{{/if}}
{{#eq since "unreleased"}} {{#eq since "unreleased"}}
- Unreleased - Unreleased
{{else}} {{else}}
@ -29,7 +33,7 @@
| Name | Type | Description | | Name | Type | Description |
| ---- | :---: | ------------| | ---- | :---: | ------------|
{{#each returns}} {{#each returns}}
| `{{name}}` | _{{depipe type}}_ | {{{description}}} | | `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
{{/each}} {{/each}}
{{else}} {{else}}
@ -51,6 +55,10 @@ _No additional response items._
{{#each this}} {{#each this}}
### {{name}} ### {{name}}
{{#if deprecated}}
- **⚠️ Deprecated. {{deprecated}} ⚠️**
{{/if}}
{{#eq since "unreleased"}} {{#eq since "unreleased"}}
- Unreleased - Unreleased
{{else}} {{else}}
@ -65,7 +73,7 @@ _No additional response items._
| Name | Type | Description | | Name | Type | Description |
| ---- | :---: | ------------| | ---- | :---: | ------------|
{{#each params}} {{#each params}}
| `{{name}}` | _{{depipe type}}_ | {{{description}}} | | `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
{{/each}} {{/each}}
{{else}} {{else}}
@ -78,7 +86,7 @@ _No specified parameters._
| Name | Type | Description | | Name | Type | Description |
| ---- | :---: | ------------| | ---- | :---: | ------------|
{{#each returns}} {{#each returns}}
| `{{name}}` | _{{depipe type}}_ | {{{description}}} | | `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} |
{{/each}} {{/each}}
{{else}} {{else}}
@ -89,4 +97,3 @@ _No additional response items._
{{/each}} {{/each}}
{{/each}} {{/each}}

View File

@ -2,8 +2,8 @@
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "obs-websocket" #define MyAppName "obs-websocket"
#define MyAppVersion "4.2.0" #define MyAppVersion "4.3.3"
#define MyAppPublisher "St<EFBFBD>phane Lepin" #define MyAppPublisher "Stephane Lepin"
#define MyAppURL "http://github.com/Palakis/obs-websocket" #define MyAppURL "http://github.com/Palakis/obs-websocket"
[Setup] [Setup]

View File

@ -26,11 +26,15 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#define PARAM_ENABLE "ServerEnabled" #define PARAM_ENABLE "ServerEnabled"
#define PARAM_PORT "ServerPort" #define PARAM_PORT "ServerPort"
#define PARAM_DEBUG "DebugEnabled" #define PARAM_DEBUG "DebugEnabled"
#define PARAM_ALERT "AlertsEnabled"
#define PARAM_AUTHREQUIRED "AuthRequired" #define PARAM_AUTHREQUIRED "AuthRequired"
#define PARAM_SECRET "AuthSecret" #define PARAM_SECRET "AuthSecret"
#define PARAM_SALT "AuthSalt" #define PARAM_SALT "AuthSalt"
#include "Config.h" #include "Config.h"
#include "Utils.h"
#define QT_TO_UTF8(str) str.toUtf8().constData()
Config* Config::_instance = new Config(); Config* Config::_instance = new Config();
@ -38,27 +42,31 @@ Config::Config() :
ServerEnabled(true), ServerEnabled(true),
ServerPort(4444), ServerPort(4444),
DebugEnabled(false), DebugEnabled(false),
AlertsEnabled(true),
AuthRequired(false), AuthRequired(false),
Secret(""), Secret(""),
Salt(""), Salt(""),
SettingsLoaded(false) { SettingsLoaded(false)
{
// OBS Config defaults // OBS Config defaults
config_t* obs_config = obs_frontend_get_global_config(); config_t* obsConfig = obs_frontend_get_global_config();
if (obs_config) { if (obsConfig) {
config_set_default_bool(obs_config, config_set_default_bool(obsConfig,
SECTION_NAME, PARAM_ENABLE, ServerEnabled); SECTION_NAME, PARAM_ENABLE, ServerEnabled);
config_set_default_uint(obs_config, config_set_default_uint(obsConfig,
SECTION_NAME, PARAM_PORT, ServerPort); SECTION_NAME, PARAM_PORT, ServerPort);
config_set_default_bool(obs_config, config_set_default_bool(obsConfig,
SECTION_NAME, PARAM_DEBUG, DebugEnabled); SECTION_NAME, PARAM_DEBUG, DebugEnabled);
config_set_default_bool(obsConfig,
SECTION_NAME, PARAM_ALERT, AlertsEnabled);
config_set_default_bool(obs_config, config_set_default_bool(obsConfig,
SECTION_NAME, PARAM_AUTHREQUIRED, AuthRequired); SECTION_NAME, PARAM_AUTHREQUIRED, AuthRequired);
config_set_default_string(obs_config, config_set_default_string(obsConfig,
SECTION_NAME, PARAM_SECRET, Secret); SECTION_NAME, PARAM_SECRET, QT_TO_UTF8(Secret));
config_set_default_string(obs_config, config_set_default_string(obsConfig,
SECTION_NAME, PARAM_SALT, Salt); SECTION_NAME, PARAM_SALT, QT_TO_UTF8(Salt));
} }
mbedtls_entropy_init(&entropy); mbedtls_entropy_init(&entropy);
@ -74,108 +82,112 @@ Config::~Config() {
} }
void Config::Load() { void Config::Load() {
config_t* obs_config = obs_frontend_get_global_config(); config_t* obsConfig = obs_frontend_get_global_config();
ServerEnabled = config_get_bool(obs_config, SECTION_NAME, PARAM_ENABLE); ServerEnabled = config_get_bool(obsConfig, SECTION_NAME, PARAM_ENABLE);
ServerPort = config_get_uint(obs_config, SECTION_NAME, PARAM_PORT); ServerPort = config_get_uint(obsConfig, SECTION_NAME, PARAM_PORT);
DebugEnabled = config_get_bool(obs_config, SECTION_NAME, PARAM_DEBUG); DebugEnabled = config_get_bool(obsConfig, SECTION_NAME, PARAM_DEBUG);
AlertsEnabled = config_get_bool(obsConfig, SECTION_NAME, PARAM_ALERT);
AuthRequired = config_get_bool(obs_config, SECTION_NAME, PARAM_AUTHREQUIRED); AuthRequired = config_get_bool(obsConfig, SECTION_NAME, PARAM_AUTHREQUIRED);
Secret = config_get_string(obs_config, SECTION_NAME, PARAM_SECRET); Secret = config_get_string(obsConfig, SECTION_NAME, PARAM_SECRET);
Salt = config_get_string(obs_config, SECTION_NAME, PARAM_SALT); Salt = config_get_string(obsConfig, SECTION_NAME, PARAM_SALT);
} }
void Config::Save() { void Config::Save() {
config_t* obs_config = obs_frontend_get_global_config(); config_t* obsConfig = obs_frontend_get_global_config();
config_set_bool(obs_config, SECTION_NAME, PARAM_ENABLE, ServerEnabled); config_set_bool(obsConfig, SECTION_NAME, PARAM_ENABLE, ServerEnabled);
config_set_uint(obs_config, SECTION_NAME, PARAM_PORT, ServerPort); config_set_uint(obsConfig, SECTION_NAME, PARAM_PORT, ServerPort);
config_set_bool(obs_config, SECTION_NAME, PARAM_DEBUG, DebugEnabled); config_set_bool(obsConfig, SECTION_NAME, PARAM_DEBUG, DebugEnabled);
config_set_bool(obsConfig, SECTION_NAME, PARAM_ALERT, AlertsEnabled);
config_set_bool(obs_config, SECTION_NAME, PARAM_AUTHREQUIRED, AuthRequired); config_set_bool(obsConfig, SECTION_NAME, PARAM_AUTHREQUIRED, AuthRequired);
config_set_string(obs_config, SECTION_NAME, PARAM_SECRET, Secret); config_set_string(obsConfig, SECTION_NAME, PARAM_SECRET,
config_set_string(obs_config, SECTION_NAME, PARAM_SALT, Salt); QT_TO_UTF8(Secret));
config_set_string(obsConfig, SECTION_NAME, PARAM_SALT,
QT_TO_UTF8(Salt));
config_save(obs_config); config_save(obsConfig);
} }
const char* Config::GenerateSalt() { QString Config::GenerateSalt() {
// Generate 32 random chars // Generate 32 random chars
unsigned char* random_chars = (unsigned char*)bzalloc(32); unsigned char* randomChars = (unsigned char*)bzalloc(32);
mbedtls_ctr_drbg_random(&rng, random_chars, 32); mbedtls_ctr_drbg_random(&rng, randomChars, 32);
// Convert the 32 random chars to a base64 string // Convert the 32 random chars to a base64 string
char* salt = (char*)bzalloc(64); char* salt = (char*)bzalloc(64);
size_t salt_bytes; size_t saltBytes;
mbedtls_base64_encode( mbedtls_base64_encode(
(unsigned char*)salt, 64, &salt_bytes, (unsigned char*)salt, 64, &saltBytes,
random_chars, 32); randomChars, 32);
bfree(random_chars); bfree(randomChars);
return salt; return salt;
} }
const char* Config::GenerateSecret(const char* password, const char* salt) { QString Config::GenerateSecret(QString password, QString salt) {
// Concatenate the password and the salt // Concatenate the password and the salt
std::string passAndSalt = ""; QString passAndSalt = "";
passAndSalt += password; passAndSalt += password;
passAndSalt += salt; passAndSalt += salt;
// Generate a SHA256 hash of the password // Generate a SHA256 hash of the password
unsigned char* challengeHash = (unsigned char*)bzalloc(32); unsigned char* challengeHash = (unsigned char*)bzalloc(32);
mbedtls_sha256( mbedtls_sha256(
(unsigned char*)passAndSalt.c_str(), passAndSalt.length(), (unsigned char*)passAndSalt.toUtf8().constData(), passAndSalt.length(),
challengeHash, 0); challengeHash, 0);
// Encode SHA256 hash to Base64 // Encode SHA256 hash to Base64
char* challenge = (char*)bzalloc(64); char* challenge = (char*)bzalloc(64);
size_t challenge_bytes = 0; size_t challengeBytes = 0;
mbedtls_base64_encode( mbedtls_base64_encode(
(unsigned char*)challenge, 64, &challenge_bytes, (unsigned char*)challenge, 64, &challengeBytes,
challengeHash, 32); challengeHash, 32);
bfree(challengeHash); bfree(challengeHash);
return challenge; return challenge;
} }
void Config::SetPassword(const char* password) { void Config::SetPassword(QString password) {
const char* new_salt = GenerateSalt(); QString newSalt = GenerateSalt();
const char* new_challenge = GenerateSecret(password, new_salt); QString newChallenge = GenerateSecret(password, newSalt);
this->Salt = new_salt; this->Salt = newSalt;
this->Secret = new_challenge; this->Secret = newChallenge;
} }
bool Config::CheckAuth(const char* response) { bool Config::CheckAuth(QString response) {
// Concatenate auth secret with the challenge sent to the user // Concatenate auth secret with the challenge sent to the user
std::string challengeAndResponse = ""; QString challengeAndResponse = "";
challengeAndResponse += this->Secret; challengeAndResponse += Secret;
challengeAndResponse += this->SessionChallenge; challengeAndResponse += SessionChallenge;
// Generate a SHA256 hash of challengeAndResponse // Generate a SHA256 hash of challengeAndResponse
unsigned char* hash = (unsigned char*)bzalloc(32); unsigned char* hash = (unsigned char*)bzalloc(32);
mbedtls_sha256( mbedtls_sha256(
(unsigned char*)challengeAndResponse.c_str(), (unsigned char*)challengeAndResponse.toUtf8().constData(),
challengeAndResponse.length(), challengeAndResponse.length(),
hash, 0); hash, 0);
// Encode the SHA256 hash to Base64 // Encode the SHA256 hash to Base64
char* expected_response = (char*)bzalloc(64); char* expectedResponse = (char*)bzalloc(64);
size_t base64_size = 0; size_t base64_size = 0;
mbedtls_base64_encode( mbedtls_base64_encode(
(unsigned char*)expected_response, 64, &base64_size, (unsigned char*)expectedResponse, 64, &base64_size,
hash, 32); hash, 32);
bool authSuccess = false; bool authSuccess = false;
if (strcmp(expected_response, response) == 0) { if (response == QString(expectedResponse)) {
SessionChallenge = GenerateSalt(); SessionChallenge = GenerateSalt();
authSuccess = true; authSuccess = true;
} }
bfree(hash); bfree(hash);
bfree(expected_response); bfree(expectedResponse);
return authSuccess; return authSuccess;
} }

View File

@ -19,6 +19,8 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#ifndef CONFIG_H #ifndef CONFIG_H
#define CONFIG_H #define CONFIG_H
#include <QString>
#include <mbedtls/entropy.h> #include <mbedtls/entropy.h>
#include <mbedtls/ctr_drbg.h> #include <mbedtls/ctr_drbg.h>
@ -29,21 +31,22 @@ class Config {
void Load(); void Load();
void Save(); void Save();
void SetPassword(const char* password); void SetPassword(QString password);
bool CheckAuth(const char* userChallenge); bool CheckAuth(QString userChallenge);
const char* GenerateSalt(); QString GenerateSalt();
static const char* GenerateSecret( static QString GenerateSecret(
const char* password, const char* salt); QString password, QString salt);
bool ServerEnabled; bool ServerEnabled;
uint64_t ServerPort; uint64_t ServerPort;
bool DebugEnabled; bool DebugEnabled;
bool AlertsEnabled;
bool AuthRequired; bool AuthRequired;
const char* Secret; QString Secret;
const char* Salt; QString Salt;
const char* SessionChallenge; QString SessionChallenge;
bool SettingsLoaded; bool SettingsLoaded;
static Config* Current(); static Config* Current();

View File

@ -24,10 +24,11 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "obs-websocket.h" #include "obs-websocket.h"
#include "Utils.h" #include "Utils.h"
#include "Config.h"
Q_DECLARE_METATYPE(OBSScene); Q_DECLARE_METATYPE(OBSScene);
obs_data_array_t* string_list_to_array(char** strings, char* key) { obs_data_array_t* Utils::StringListToArray(char** strings, char* key) {
if (!strings) if (!strings)
return obs_data_array_create(); return obs_data_array_create();
@ -37,13 +38,11 @@ obs_data_array_t* string_list_to_array(char** strings, char* key) {
for (int i = 0; value != nullptr; i++) { for (int i = 0; value != nullptr; i++) {
value = strings[i]; value = strings[i];
obs_data_t* item = obs_data_create(); OBSDataAutoRelease item = obs_data_create();
obs_data_set_string(item, key, value); obs_data_set_string(item, key, value);
if (value) if (value)
obs_data_array_push_back(list, item); obs_data_array_push_back(list, item);
obs_data_release(item);
} }
return list; return list;
@ -51,7 +50,7 @@ obs_data_array_t* string_list_to_array(char** strings, char* key) {
obs_data_array_t* Utils::GetSceneItems(obs_source_t* source) { obs_data_array_t* Utils::GetSceneItems(obs_source_t* source) {
obs_data_array_t* items = obs_data_array_create(); obs_data_array_t* items = obs_data_array_create();
obs_scene_t* scene = obs_scene_from_source(source); OBSScene scene = obs_scene_from_source(source);
if (!scene) if (!scene)
return nullptr; return nullptr;
@ -63,10 +62,8 @@ obs_data_array_t* Utils::GetSceneItems(obs_source_t* source) {
{ {
obs_data_array_t* data = static_cast<obs_data_array_t*>(param); obs_data_array_t* data = static_cast<obs_data_array_t*>(param);
obs_data_t* item_data = GetSceneItemData(currentItem); OBSDataAutoRelease itemData = GetSceneItemData(currentItem);
obs_data_array_insert(data, 0, item_data); obs_data_array_insert(data, 0, itemData);
obs_data_release(item_data);
return true; return true;
}, items); }, items);
@ -83,9 +80,10 @@ obs_data_t* Utils::GetSceneItemData(obs_sceneitem_t* item) {
vec2 scale; vec2 scale;
obs_sceneitem_get_scale(item, &scale); obs_sceneitem_get_scale(item, &scale);
obs_source_t* item_source = obs_sceneitem_get_source(item); // obs_sceneitem_get_source doesn't increase the refcount
float item_width = float(obs_source_get_width(item_source)); OBSSource itemSource = obs_sceneitem_get_source(item);
float item_height = float(obs_source_get_height(item_source)); float item_width = float(obs_source_get_width(itemSource));
float item_height = float(obs_source_get_height(itemSource));
obs_data_t* data = obs_data_create(); obs_data_t* data = obs_data_create();
obs_data_set_string(data, "name", obs_data_set_string(data, "name",
@ -105,9 +103,9 @@ obs_data_t* Utils::GetSceneItemData(obs_sceneitem_t* item) {
return data; return data;
} }
obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, const char* name) { obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name) {
struct current_search { struct current_search {
const char* query; QString query;
obs_sceneitem_t* result; obs_sceneitem_t* result;
}; };
@ -115,8 +113,8 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, const char* n
search.query = name; search.query = name;
search.result = nullptr; search.result = nullptr;
obs_scene_t* scene = obs_scene_from_source(source); OBSScene scene = obs_scene_from_source(source);
if (scene == nullptr) if (!scene)
return nullptr; return nullptr;
obs_scene_enum_items(scene, []( obs_scene_enum_items(scene, [](
@ -126,10 +124,10 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, const char* n
{ {
current_search* search = static_cast<current_search*>(param); current_search* search = static_cast<current_search*>(param);
const char* currentItemName = QString currentItemName =
obs_source_get_name(obs_sceneitem_get_source(currentItem)); obs_source_get_name(obs_sceneitem_get_source(currentItem));
if (strcmp(currentItemName, search->query) == 0) { if (currentItemName == search->query) {
search->result = currentItem; search->result = currentItem;
obs_sceneitem_addref(search->result); obs_sceneitem_addref(search->result);
return false; return false;
@ -141,36 +139,53 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, const char* n
return search.result; return search.result;
} }
obs_source_t* Utils::GetTransitionFromName(const char* search_name) { bool Utils::IsValidAlignment(const uint32_t alignment) {
obs_source_t* found_transition = NULL; switch (alignment) {
case OBS_ALIGN_CENTER:
case OBS_ALIGN_LEFT:
case OBS_ALIGN_RIGHT:
case OBS_ALIGN_TOP:
case OBS_ALIGN_BOTTOM:
case OBS_ALIGN_TOP | OBS_ALIGN_LEFT:
case OBS_ALIGN_TOP | OBS_ALIGN_RIGHT:
case OBS_ALIGN_BOTTOM | OBS_ALIGN_LEFT:
case OBS_ALIGN_BOTTOM | OBS_ALIGN_RIGHT: {
return true;
}
}
return false;
}
obs_source_t* Utils::GetTransitionFromName(QString searchName) {
obs_source_t* foundTransition = nullptr;
obs_frontend_source_list transition_list = {}; obs_frontend_source_list transition_list = {};
obs_frontend_get_transitions(&transition_list); obs_frontend_get_transitions(&transition_list);
for (size_t i = 0; i < transition_list.sources.num; i++) { for (size_t i = 0; i < transition_list.sources.num; i++) {
obs_source_t* transition = transition_list.sources.array[i]; obs_source_t* transition = transition_list.sources.array[i];
QString transitionName = obs_source_get_name(transition);
const char* transition_name = obs_source_get_name(transition); if (transitionName == searchName) {
if (strcmp(transition_name, search_name) == 0) foundTransition = transition;
{ obs_source_addref(foundTransition);
found_transition = transition;
obs_source_addref(found_transition);
break; break;
} }
} }
obs_frontend_source_list_free(&transition_list); obs_frontend_source_list_free(&transition_list);
return foundTransition;
return found_transition;
} }
obs_source_t* Utils::GetSceneFromNameOrCurrent(const char* scene_name) { obs_source_t* Utils::GetSceneFromNameOrCurrent(QString sceneName) {
// Both obs_frontend_get_current_scene() and obs_get_source_by_name()
// do addref on the return source, so no need to use an OBSSource helper
obs_source_t* scene = nullptr; obs_source_t* scene = nullptr;
if (!scene_name || !strlen(scene_name)) if (sceneName.isEmpty() || sceneName.isNull())
scene = obs_frontend_get_current_scene(); scene = obs_frontend_get_current_scene();
else else
scene = obs_get_source_by_name(scene_name); scene = obs_get_source_by_name(sceneName.toUtf8());
return scene; return scene;
} }
@ -182,45 +197,24 @@ obs_data_array_t* Utils::GetScenes() {
obs_data_array_t* scenes = obs_data_array_create(); obs_data_array_t* scenes = obs_data_array_create();
for (size_t i = 0; i < sceneList.sources.num; i++) { for (size_t i = 0; i < sceneList.sources.num; i++) {
obs_source_t* scene = sceneList.sources.array[i]; obs_source_t* scene = sceneList.sources.array[i];
OBSDataAutoRelease sceneData = GetSceneData(scene);
obs_data_t* scene_data = GetSceneData(scene); obs_data_array_push_back(scenes, sceneData);
obs_data_array_push_back(scenes, scene_data);
obs_data_release(scene_data);
} }
obs_frontend_source_list_free(&sceneList); obs_frontend_source_list_free(&sceneList);
return scenes; return scenes;
} }
obs_data_t* Utils::GetSceneData(obs_source* source) { obs_data_t* Utils::GetSceneData(obs_source_t* source) {
obs_data_array_t* scene_items = GetSceneItems(source); OBSDataArrayAutoRelease sceneItems = GetSceneItems(source);
obs_data_t* sceneData = obs_data_create(); obs_data_t* sceneData = obs_data_create();
obs_data_set_string(sceneData, "name", obs_source_get_name(source)); obs_data_set_string(sceneData, "name", obs_source_get_name(source));
obs_data_set_array(sceneData, "sources", scene_items); obs_data_set_array(sceneData, "sources", sceneItems);
obs_data_array_release(scene_items);
return sceneData; return sceneData;
} }
obs_data_array_t* Utils::GetSceneCollections() {
char** scene_collections = obs_frontend_get_scene_collections();
obs_data_array_t* list = string_list_to_array(scene_collections, "sc-name");
bfree(scene_collections);
return list;
}
obs_data_array_t* Utils::GetProfiles() {
char** profiles = obs_frontend_get_profiles();
obs_data_array_t* list = string_list_to_array(profiles, "profile-name");
bfree(profiles);
return list;
}
QSpinBox* Utils::GetTransitionDurationControl() { QSpinBox* Utils::GetTransitionDurationControl() {
QMainWindow* window = (QMainWindow*)obs_frontend_get_main_window(); QMainWindow* window = (QMainWindow*)obs_frontend_get_main_window();
return window->findChild<QSpinBox*>("transitionDuration"); return window->findChild<QSpinBox*>("transitionDuration");
@ -240,12 +234,11 @@ void Utils::SetTransitionDuration(int ms) {
control->setValue(ms); control->setValue(ms);
} }
bool Utils::SetTransitionByName(const char* transition_name) { bool Utils::SetTransitionByName(QString transitionName) {
obs_source_t* transition = GetTransitionFromName(transition_name); OBSSourceAutoRelease transition = GetTransitionFromName(transitionName);
if (transition) { if (transition) {
obs_frontend_set_current_transition(transition); obs_frontend_set_current_transition(transition);
obs_source_release(transition);
return true; return true;
} else { } else {
return false; return false;
@ -266,8 +259,8 @@ obs_scene_t* Utils::SceneListItemToScene(QListWidgetItem* item) {
if (!item) if (!item)
return nullptr; return nullptr;
QVariant item_data = item->data(static_cast<int>(Qt::UserRole)); QVariant itemData = item->data(static_cast<int>(Qt::UserRole));
return item_data.value<OBSScene>(); return itemData.value<OBSScene>();
} }
QLayout* Utils::GetPreviewLayout() { QLayout* Utils::GetPreviewLayout() {
@ -275,67 +268,8 @@ QLayout* Utils::GetPreviewLayout() {
return main->findChild<QLayout*>("previewLayout"); return main->findChild<QLayout*>("previewLayout");
} }
bool Utils::IsPreviewModeActive() {
QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window();
// Clue 1 : "Studio Mode" button is toggled on
bool buttonToggledOn = GetPreviewModeButtonControl()->isChecked();
// Clue 2 : Preview layout has more than one item
int previewChildCount = GetPreviewLayout()->count();
blog(LOG_INFO, "preview layout children count : %d", previewChildCount);
return buttonToggledOn || (previewChildCount >= 2);
}
void Utils::EnablePreviewMode() {
if (!IsPreviewModeActive())
GetPreviewModeButtonControl()->click();
}
void Utils::DisablePreviewMode() {
if (IsPreviewModeActive())
GetPreviewModeButtonControl()->click();
}
void Utils::TogglePreviewMode() {
GetPreviewModeButtonControl()->click();
}
obs_scene_t* Utils::GetPreviewScene() {
if (IsPreviewModeActive()) {
QListWidget* sceneList = GetSceneListControl();
QList<QListWidgetItem*> selected = sceneList->selectedItems();
// Qt::UserRole == QtUserRole::OBSRef
obs_scene_t* scene = Utils::SceneListItemToScene(selected.first());
obs_scene_addref(scene);
return scene;
}
return nullptr;
}
bool Utils::SetPreviewScene(const char* name) {
if (IsPreviewModeActive()) {
QListWidget* sceneList = GetSceneListControl();
QList<QListWidgetItem*> matchingItems =
sceneList->findItems(name, Qt::MatchExactly);
if (matchingItems.count() > 0) {
sceneList->setCurrentItem(matchingItems.first());
return true;
} else {
return false;
}
}
return false;
}
void Utils::TransitionToProgram() { void Utils::TransitionToProgram() {
if (!IsPreviewModeActive()) if (!obs_frontend_preview_program_mode_active())
return; return;
// WARNING : if the layout created in OBS' CreateProgramOptions() changes // WARNING : if the layout created in OBS' CreateProgramOptions() changes
@ -358,7 +292,7 @@ void Utils::TransitionToProgram() {
transitionBtn->click(); transitionBtn->click();
} }
const char* Utils::OBSVersionString() { QString Utils::OBSVersionString() {
uint32_t version = obs_get_version(); uint32_t version = obs_get_version();
uint8_t major, minor, patch; uint8_t major, minor, patch;
@ -366,21 +300,27 @@ const char* Utils::OBSVersionString() {
minor = (version >> 16) & 0xFF; minor = (version >> 16) & 0xFF;
patch = version & 0xFF; patch = version & 0xFF;
char* result = (char*)bmalloc(sizeof(char) * 12); QString result = QString("%1.%2.%3")
sprintf(result, "%d.%d.%d", major, minor, patch); .arg(major).arg(minor).arg(patch);
return result; return result;
} }
QSystemTrayIcon* Utils::GetTrayIcon() { QSystemTrayIcon* Utils::GetTrayIcon() {
QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window();
if (!main) return nullptr;
return main->findChildren<QSystemTrayIcon*>().first(); return main->findChildren<QSystemTrayIcon*>().first();
} }
void Utils::SysTrayNotify(QString &text, void Utils::SysTrayNotify(QString &text,
QSystemTrayIcon::MessageIcon icon, QString title) { QSystemTrayIcon::MessageIcon icon, QString title) {
if (!QSystemTrayIcon::supportsMessages()) if (!Config::Current()->AlertsEnabled ||
!QSystemTrayIcon::isSystemTrayAvailable() ||
!QSystemTrayIcon::supportsMessages())
{
return; return;
}
QSystemTrayIcon* trayIcon = GetTrayIcon(); QSystemTrayIcon* trayIcon = GetTrayIcon();
if (trayIcon) if (trayIcon)
@ -398,9 +338,9 @@ QString Utils::FormatIPAddress(QHostAddress &addr) {
const char* Utils::GetRecordingFolder() { const char* Utils::GetRecordingFolder() {
config_t* profile = obs_frontend_get_profile_config(); config_t* profile = obs_frontend_get_profile_config();
const char* outputMode = config_get_string(profile, "Output", "Mode"); QString outputMode = config_get_string(profile, "Output", "Mode");
if (strcmp(outputMode, "Advanced") == 0) { if (outputMode == "Advanced") {
// Advanced mode // Advanced mode
return config_get_string(profile, "AdvOut", "RecFilePath"); return config_get_string(profile, "AdvOut", "RecFilePath");
} else { } else {
@ -410,13 +350,14 @@ const char* Utils::GetRecordingFolder() {
} }
bool Utils::SetRecordingFolder(const char* path) { bool Utils::SetRecordingFolder(const char* path) {
if (!QDir(path).exists()) QDir dir(path);
return false; if (!dir.exists())
dir.mkpath(".");
config_t* profile = obs_frontend_get_profile_config(); config_t* profile = obs_frontend_get_profile_config();
const char* outputMode = config_get_string(profile, "Output", "Mode"); QString outputMode = config_get_string(profile, "Output", "Mode");
if (strcmp(outputMode, "Advanced") == 0) { if (outputMode == "Advanced") {
config_set_string(profile, "AdvOut", "RecFilePath", path); config_set_string(profile, "AdvOut", "RecFilePath", path);
} else { } else {
config_set_string(profile, "SimpleOutput", "FilePath", path); config_set_string(profile, "SimpleOutput", "FilePath", path);
@ -426,61 +367,66 @@ bool Utils::SetRecordingFolder(const char* path) {
return true; return true;
} }
QString* Utils::ParseDataToQueryString(obs_data_t* data) { QString Utils::ParseDataToQueryString(obs_data_t* data) {
QString* query = nullptr; if (!data)
if (data) { return QString();
QString query;
obs_data_item_t* item = obs_data_first(data); obs_data_item_t* item = obs_data_first(data);
if (item) { if (item) {
query = new QString();
bool isFirst = true; bool isFirst = true;
do { do {
if (!obs_data_item_has_user_value(item)) if (!obs_data_item_has_user_value(item))
continue; continue;
if (!isFirst) if (!isFirst)
query->append('&'); query += "&";
else else
isFirst = false; isFirst = false;
const char* attrName = obs_data_item_get_name(item); QString attrName = obs_data_item_get_name(item);
query->append(attrName).append("="); query += (attrName + "=");
switch (obs_data_item_gettype(item)) { switch (obs_data_item_gettype(item)) {
case OBS_DATA_BOOLEAN: case OBS_DATA_BOOLEAN:
query->append(obs_data_item_get_bool(item)?"true":"false"); query += (obs_data_item_get_bool(item) ? "true" : "false");
break; break;
case OBS_DATA_NUMBER: case OBS_DATA_NUMBER:
switch (obs_data_item_numtype(item)) switch (obs_data_item_numtype(item)) {
{
case OBS_DATA_NUM_DOUBLE: case OBS_DATA_NUM_DOUBLE:
query->append( query +=
QString::number(obs_data_item_get_double(item))); QString::number(obs_data_item_get_double(item));
break; break;
case OBS_DATA_NUM_INT: case OBS_DATA_NUM_INT:
query->append( query +=
QString::number(obs_data_item_get_int(item))); QString::number(obs_data_item_get_int(item));
break; break;
case OBS_DATA_NUM_INVALID: case OBS_DATA_NUM_INVALID:
break; break;
} }
break; break;
case OBS_DATA_STRING: case OBS_DATA_STRING:
query->append(QUrl::toPercentEncoding( query +=
QString(obs_data_item_get_string(item)))); QUrl::toPercentEncoding(
QString(obs_data_item_get_string(item)));
break; break;
default: default:
//other types are not supported //other types are not supported
break; break;
} }
} while (obs_data_item_next(&item)); } while (obs_data_item_next(&item));
} }
}
return query; return query;
} }
obs_hotkey_t* Utils::FindHotkeyByName(const char* name) { obs_hotkey_t* Utils::FindHotkeyByName(QString name) {
struct current_search { struct current_search {
const char* query; QString query;
obs_hotkey_t* result; obs_hotkey_t* result;
}; };
@ -492,11 +438,11 @@ obs_hotkey_t* Utils::FindHotkeyByName(const char* name) {
current_search* search = static_cast<current_search*>(data); current_search* search = static_cast<current_search*>(data);
const char* hk_name = obs_hotkey_get_name(hotkey); const char* hk_name = obs_hotkey_get_name(hotkey);
if (strcmp(hk_name, search->query) == 0) { if (hk_name == search->query) {
search->result = hotkey; search->result = hotkey;
blog(LOG_INFO, "Utils::FindHotkeyByName: found %s", hk_name);
return false; return false;
} }
return true; return true;
}, &search); }, &search);
@ -505,27 +451,65 @@ obs_hotkey_t* Utils::FindHotkeyByName(const char* name) {
bool Utils::ReplayBufferEnabled() { bool Utils::ReplayBufferEnabled() {
config_t* profile = obs_frontend_get_profile_config(); config_t* profile = obs_frontend_get_profile_config();
const char* outputMode = config_get_string(profile, "Output", "Mode"); QString outputMode = config_get_string(profile, "Output", "Mode");
if (strcmp(outputMode, "Simple") == 0) { if (outputMode == "Simple") {
return config_get_bool(profile, "SimpleOutput", "RecRB"); return config_get_bool(profile, "SimpleOutput", "RecRB");
} }
else if (outputMode == "Advanced") {
return config_get_bool(profile, "AdvOut", "RecRB");
}
return false; return false;
} }
bool Utils::RPHotkeySet() { void Utils::StartReplayBuffer() {
obs_output_t* rp_output = obs_frontend_get_replay_buffer_output(); if (obs_frontend_replay_buffer_active())
return;
obs_data_t *hotkeys = obs_hotkeys_save_output(rp_output); if (!IsRPHotkeySet()) {
obs_data_array_t *bindings = obs_data_get_array(hotkeys, obs_output_t* rpOutput = obs_frontend_get_replay_buffer_output();
OBSData outputHotkeys = obs_hotkeys_save_output(rpOutput);
OBSData dummyBinding = obs_data_create();
obs_data_set_bool(dummyBinding, "control", true);
obs_data_set_bool(dummyBinding, "alt", true);
obs_data_set_bool(dummyBinding, "shift", true);
obs_data_set_bool(dummyBinding, "command", true);
obs_data_set_string(dummyBinding, "key", "OBS_KEY_0");
OBSDataArray rpSaveHotkey = obs_data_get_array(
outputHotkeys, "ReplayBuffer.Save");
obs_data_array_push_back(rpSaveHotkey, dummyBinding);
obs_hotkeys_load_output(rpOutput, outputHotkeys);
obs_frontend_replay_buffer_start();
obs_output_release(rpOutput);
}
else {
obs_frontend_replay_buffer_start();
}
}
bool Utils::IsRPHotkeySet() {
OBSOutputAutoRelease rpOutput = obs_frontend_get_replay_buffer_output();
OBSDataAutoRelease hotkeys = obs_hotkeys_save_output(rpOutput);
OBSDataArrayAutoRelease bindings = obs_data_get_array(hotkeys,
"ReplayBuffer.Save"); "ReplayBuffer.Save");
size_t count = obs_data_array_count(bindings); size_t count = obs_data_array_count(bindings);
obs_data_array_release(bindings);
obs_data_release(hotkeys);
obs_output_release(rp_output);
return (count > 0); return (count > 0);
} }
const char* Utils::GetFilenameFormatting() {
config_t* profile = obs_frontend_get_profile_config();
return config_get_string(profile, "Output", "FilenameFormatting");
}
bool Utils::SetFilenameFormatting(const char* filenameFormatting) {
config_t* profile = obs_frontend_get_profile_config();
config_set_string(profile, "Output", "FilenameFormatting", filenameFormatting);
config_save(profile);
return true;
}

View File

@ -19,6 +19,8 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#ifndef UTILS_H #ifndef UTILS_H
#define UTILS_H #define UTILS_H
#include <stdio.h>
#include <QSpinBox> #include <QSpinBox>
#include <QPushButton> #include <QPushButton>
#include <QLayout> #include <QLayout>
@ -26,46 +28,39 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include <QHostAddress> #include <QHostAddress>
#include <stdio.h> #include <obs.hpp>
#include <obs-module.h> #include <obs-module.h>
#include <util/config-file.h> #include <util/config-file.h>
class Utils { class Utils {
public: public:
static obs_data_array_t* StringListToArray(char** strings, char* key);
static obs_data_array_t* GetSceneItems(obs_source_t* source); static obs_data_array_t* GetSceneItems(obs_source_t* source);
static obs_data_t* GetSceneItemData(obs_scene_item* item); static obs_data_t* GetSceneItemData(obs_sceneitem_t* item);
static obs_sceneitem_t* GetSceneItemFromName( static obs_sceneitem_t* GetSceneItemFromName(
obs_source_t* source, const char* name); obs_source_t* source, QString name);
static obs_source_t* GetTransitionFromName(const char* search_name); static obs_source_t* GetTransitionFromName(QString transitionName);
static obs_source_t* GetSceneFromNameOrCurrent(const char* scene_name); static obs_source_t* GetSceneFromNameOrCurrent(QString sceneName);
static bool IsValidAlignment(const uint32_t alignment);
static obs_data_array_t* GetScenes(); static obs_data_array_t* GetScenes();
static obs_data_t* GetSceneData(obs_source* source); static obs_data_t* GetSceneData(obs_source_t* source);
static obs_data_array_t* GetSceneCollections();
static obs_data_array_t* GetProfiles();
static QSpinBox* GetTransitionDurationControl(); static QSpinBox* GetTransitionDurationControl();
static int GetTransitionDuration(); static int GetTransitionDuration();
static void SetTransitionDuration(int ms); static void SetTransitionDuration(int ms);
static bool SetTransitionByName(const char* transition_name); static bool SetTransitionByName(QString transitionName);
static QPushButton* GetPreviewModeButtonControl(); static QPushButton* GetPreviewModeButtonControl();
static QLayout* GetPreviewLayout(); static QLayout* GetPreviewLayout();
static QListWidget* GetSceneListControl(); static QListWidget* GetSceneListControl();
static obs_scene_t* SceneListItemToScene(QListWidgetItem* item); static obs_scene_t* SceneListItemToScene(QListWidgetItem* item);
static bool IsPreviewModeActive();
static void EnablePreviewMode();
static void DisablePreviewMode();
static void TogglePreviewMode();
static obs_scene_t* GetPreviewScene();
static bool SetPreviewScene(const char* name);
static void TransitionToProgram(); static void TransitionToProgram();
static const char* OBSVersionString(); static QString OBSVersionString();
static QSystemTrayIcon* GetTrayIcon(); static QSystemTrayIcon* GetTrayIcon();
static void SysTrayNotify( static void SysTrayNotify(
@ -74,13 +69,17 @@ class Utils {
QString title = QString("obs-websocket")); QString title = QString("obs-websocket"));
static QString FormatIPAddress(QHostAddress &addr); static QString FormatIPAddress(QHostAddress &addr);
static const char* GetRecordingFolder(); static const char* GetRecordingFolder();
static bool SetRecordingFolder(const char* path); static bool SetRecordingFolder(const char* path);
static QString* ParseDataToQueryString(obs_data_t * data); static QString ParseDataToQueryString(obs_data_t* data);
static obs_hotkey_t* FindHotkeyByName(const char* name); static obs_hotkey_t* FindHotkeyByName(QString name);
static bool ReplayBufferEnabled(); static bool ReplayBufferEnabled();
static bool RPHotkeySet(); static void StartReplayBuffer();
static bool IsRPHotkeySet();
static const char* GetFilenameFormatting();
static bool SetFilenameFormatting(const char* filenameFormatting);
}; };
#endif // UTILS_H #endif // UTILS_H

View File

@ -28,66 +28,74 @@
#include "obs-websocket.h" #include "obs-websocket.h"
bool transition_is_cut(obs_source_t* transition) { bool transitionIsCut(obs_source_t* transition) {
if (!transition) if (!transition)
return false; return false;
if (obs_source_get_type(transition) == OBS_SOURCE_TYPE_TRANSITION if (obs_source_get_type(transition) == OBS_SOURCE_TYPE_TRANSITION
&& strcmp(obs_source_get_id(transition), "cut_transition") == 0) { && QString(obs_source_get_id(transition)) == "cut_transition") {
return true; return true;
} }
return false; return false;
} }
const char* ns_to_timestamp(uint64_t ns) { const char* nsToTimestamp(uint64_t ns) {
uint64_t ms = ns / (1000 * 1000); uint64_t ms = ns / (1000 * 1000);
uint64_t secs = ms / 1000; uint64_t secs = ms / 1000;
uint64_t minutes = secs / 60; uint64_t minutes = secs / 60;
uint64_t hours_part = minutes / 60; uint64_t hoursPart = minutes / 60;
uint64_t minutes_part = minutes % 60; uint64_t minutesPart = minutes % 60;
uint64_t secs_part = secs % 60; uint64_t secsPart = secs % 60;
uint64_t ms_part = ms % 1000; uint64_t msPart = ms % 1000;
char* ts = (char*)bmalloc(64); char* ts = (char*)bmalloc(64);
sprintf(ts, "%02d:%02d:%02d.%03d", sprintf(ts, "%02d:%02d:%02d.%03d",
hours_part, minutes_part, secs_part, ms_part); hoursPart, minutesPart, secsPart, msPart);
return ts; return ts;
} }
void* calldata_get_ptr(const calldata_t* data, const char* name) {
void* ptr = nullptr;
calldata_get_ptr(data, name, &ptr);
return ptr;
}
WSEvents* WSEvents::Instance = nullptr; WSEvents* WSEvents::Instance = nullptr;
WSEvents::WSEvents(WSServer* srv) { WSEvents::WSEvents(WSServer* srv) {
_srv = srv; _srv = srv;
obs_frontend_add_event_callback(WSEvents::FrontendEventHandler, this); obs_frontend_add_event_callback(WSEvents::FrontendEventHandler, this);
QSpinBox* duration_control = Utils::GetTransitionDurationControl(); QSpinBox* durationControl = Utils::GetTransitionDurationControl();
connect(duration_control, SIGNAL(valueChanged(int)), connect(durationControl, SIGNAL(valueChanged(int)),
this, SLOT(TransitionDurationChanged(int))); this, SLOT(TransitionDurationChanged(int)));
QTimer* statusTimer = new QTimer(); QTimer* statusTimer = new QTimer();
connect(statusTimer, SIGNAL(timeout()), connect(statusTimer, SIGNAL(timeout()),
this, SLOT(StreamStatus())); this, SLOT(StreamStatus()));
pulse = false;
connect(statusTimer, SIGNAL(timeout()),
this, SLOT(Heartbeat()));
statusTimer->start(2000); // equal to frontend's constant BITRATE_UPDATE_SECONDS statusTimer->start(2000); // equal to frontend's constant BITRATE_UPDATE_SECONDS
QListWidget* sceneList = Utils::GetSceneListControl(); QListWidget* sceneList = Utils::GetSceneListControl();
connect(sceneList, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)), connect(sceneList, SIGNAL(currentItemChanged(QListWidgetItem*, QListWidgetItem*)),
this, SLOT(SelectedSceneChanged(QListWidgetItem*, QListWidgetItem*))); this, SLOT(SelectedSceneChanged(QListWidgetItem*, QListWidgetItem*)));
QPushButton* modeSwitch = Utils::GetPreviewModeButtonControl(); currentScene = nullptr;
connect(modeSwitch, SIGNAL(clicked(bool)), this, SLOT(ModeSwitchClicked(bool))); currentTransition = nullptr;
transition_handler = nullptr;
scene_handler = nullptr;
QTimer::singleShot(1000, this, SLOT(deferredInitOperations())); QTimer::singleShot(1000, this, SLOT(deferredInitOperations()));
_streaming_active = false; HeartbeatIsActive = false;
_recording_active = false;
_stream_starttime = 0; _streamingActive = false;
_rec_starttime = 0; _recordingActive = false;
_streamStarttime = 0;
_recStarttime = 0;
} }
WSEvents::~WSEvents() { WSEvents::~WSEvents() {
@ -95,13 +103,11 @@ WSEvents::~WSEvents() {
} }
void WSEvents::deferredInitOperations() { void WSEvents::deferredInitOperations() {
obs_source_t* transition = obs_frontend_get_current_transition(); OBSSourceAutoRelease transition = obs_frontend_get_current_transition();
connectTransitionSignals(transition); connectTransitionSignals(transition);
obs_source_release(transition);
obs_source_t* scene = obs_frontend_get_current_scene(); OBSSourceAutoRelease scene = obs_frontend_get_current_scene();
connectSceneSignals(scene); connectSceneSignals(scene);
obs_source_release(scene);
} }
void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private_data) { void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private_data) {
@ -110,8 +116,6 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private
if (!owner->_srv) if (!owner->_srv)
return; return;
// TODO : implement SourceOrderChanged and RepopulateSources
if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) { if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) {
owner->OnSceneChange(); owner->OnSceneChange();
} }
@ -140,28 +144,28 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private
owner->OnStreamStarting(); owner->OnStreamStarting();
} }
else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTED) { else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTED) {
owner->_streaming_active = true; owner->_streamingActive = true;
owner->OnStreamStarted(); owner->OnStreamStarted();
} }
else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING) { else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING) {
owner->OnStreamStopping(); owner->OnStreamStopping();
} }
else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) { else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) {
owner->_streaming_active = false; owner->_streamingActive = false;
owner->OnStreamStopped(); owner->OnStreamStopped();
} }
else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTING) { else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTING) {
owner->OnRecordingStarting(); owner->OnRecordingStarting();
} }
else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTED) { else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTED) {
owner->_recording_active = true; owner->_recordingActive = true;
owner->OnRecordingStarted(); owner->OnRecordingStarted();
} }
else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPING) { else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPING) {
owner->OnRecordingStopping(); owner->OnRecordingStopping();
} }
else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPED) { else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPED) {
owner->_recording_active = false; owner->_recordingActive = false;
owner->OnRecordingStopped(); owner->OnRecordingStopped();
} }
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING) { else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING) {
@ -176,99 +180,125 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private
else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) { else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) {
owner->OnReplayStopped(); owner->OnReplayStopped();
} }
else if (event == OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED) {
owner->OnStudioModeSwitched(true);
}
else if (event == OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED) {
owner->OnStudioModeSwitched(false);
}
else if (event == OBS_FRONTEND_EVENT_EXIT) { else if (event == OBS_FRONTEND_EVENT_EXIT) {
owner->connectSceneSignals(nullptr);
owner->connectTransitionSignals(nullptr);
owner->OnExit(); owner->OnExit();
} }
} }
void WSEvents::broadcastUpdate(const char* updateType, void WSEvents::broadcastUpdate(const char* updateType,
obs_data_t* additionalFields = NULL) { obs_data_t* additionalFields = nullptr)
obs_data_t* update = obs_data_create(); {
OBSDataAutoRelease update = obs_data_create();
obs_data_set_string(update, "update-type", updateType); obs_data_set_string(update, "update-type", updateType);
const char* ts = nullptr; const char* ts = nullptr;
if (_streaming_active) { if (_streamingActive) {
ts = ns_to_timestamp(os_gettime_ns() - _stream_starttime); ts = nsToTimestamp(os_gettime_ns() - _streamStarttime);
obs_data_set_string(update, "stream-timecode", ts); obs_data_set_string(update, "stream-timecode", ts);
bfree((void*)ts); bfree((void*)ts);
} }
if (_recording_active) { if (_recordingActive) {
ts = ns_to_timestamp(os_gettime_ns() - _rec_starttime); ts = nsToTimestamp(os_gettime_ns() - _recStarttime);
obs_data_set_string(update, "rec-timecode", ts); obs_data_set_string(update, "rec-timecode", ts);
bfree((void*)ts); bfree((void*)ts);
} }
if (additionalFields != NULL) if (additionalFields)
obs_data_apply(update, additionalFields); obs_data_apply(update, additionalFields);
const char *json = obs_data_get_json(update); QString json = obs_data_get_json(update);
_srv->broadcast(json); _srv->broadcast(json);
if (Config::Current()->DebugEnabled)
blog(LOG_DEBUG, "Update << '%s'", json);
obs_data_release(update); if (Config::Current()->DebugEnabled)
blog(LOG_DEBUG, "Update << '%s'", json.toUtf8().constData());
} }
void WSEvents::connectTransitionSignals(obs_source_t* transition) { void WSEvents::connectTransitionSignals(obs_source_t* transition) {
if (transition_handler) { signal_handler_t* sh = nullptr;
signal_handler_disconnect(transition_handler,
if (currentTransition) {
sh = obs_source_get_signal_handler(currentTransition);
signal_handler_disconnect(sh,
"transition_start", OnTransitionBegin, this); "transition_start", OnTransitionBegin, this);
} }
if (!transition_is_cut(transition)) { currentTransition = transition;
transition_handler = obs_source_get_signal_handler(transition);
signal_handler_connect(transition_handler, if (currentTransition) {
if (!transitionIsCut(transition)) {
currentTransition = transition;
sh = obs_source_get_signal_handler(currentTransition);
signal_handler_connect(sh,
"transition_start", OnTransitionBegin, this); "transition_start", OnTransitionBegin, this);
} else { }
transition_handler = nullptr; else {
currentTransition = nullptr;
}
} }
} }
void WSEvents::connectSceneSignals(obs_source_t* scene) { void WSEvents::connectSceneSignals(obs_source_t* scene) {
if (scene_handler) { signal_handler_t* sh = nullptr;
signal_handler_disconnect(scene_handler,
if (currentScene) {
sh = obs_source_get_signal_handler(currentScene);
signal_handler_disconnect(sh,
"reorder", OnSceneReordered, this); "reorder", OnSceneReordered, this);
signal_handler_disconnect(scene_handler, signal_handler_disconnect(sh,
"item_add", OnSceneItemAdd, this); "item_add", OnSceneItemAdd, this);
signal_handler_disconnect(scene_handler, signal_handler_disconnect(sh,
"item_remove", OnSceneItemDelete, this); "item_remove", OnSceneItemDelete, this);
signal_handler_disconnect(scene_handler, signal_handler_disconnect(sh,
"item_visible", OnSceneItemVisibilityChanged, this); "item_visible", OnSceneItemVisibilityChanged, this);
} }
currentScene = scene;
if (currentScene) {
// TODO : connect to all scenes, not just the current one. // TODO : connect to all scenes, not just the current one.
scene_handler = obs_source_get_signal_handler(scene); sh = obs_source_get_signal_handler(currentScene);
signal_handler_connect(scene_handler, signal_handler_connect(sh,
"reorder", OnSceneReordered, this); "reorder", OnSceneReordered, this);
signal_handler_connect(scene_handler, signal_handler_connect(sh,
"item_add", OnSceneItemAdd, this); "item_add", OnSceneItemAdd, this);
signal_handler_connect(scene_handler, signal_handler_connect(sh,
"item_remove", OnSceneItemDelete, this); "item_remove", OnSceneItemDelete, this);
signal_handler_connect(scene_handler, signal_handler_connect(sh,
"item_visible", OnSceneItemVisibilityChanged, this); "item_visible", OnSceneItemVisibilityChanged, this);
}
} }
uint64_t WSEvents::GetStreamingTime() { uint64_t WSEvents::GetStreamingTime() {
if (_streaming_active) if (_streamingActive)
return (os_gettime_ns() - _stream_starttime); return (os_gettime_ns() - _streamStarttime);
else else
return 0; return 0;
} }
const char* WSEvents::GetStreamingTimecode() { const char* WSEvents::GetStreamingTimecode() {
return ns_to_timestamp(GetStreamingTime()); return nsToTimestamp(GetStreamingTime());
} }
uint64_t WSEvents::GetRecordingTime() { uint64_t WSEvents::GetRecordingTime() {
if (_recording_active) if (_recordingActive)
return (os_gettime_ns() - _rec_starttime); return (os_gettime_ns() - _recStarttime);
else else
return 0; return 0;
} }
const char* WSEvents::GetRecordingTimecode() { const char* WSEvents::GetRecordingTimecode() {
return ns_to_timestamp(GetRecordingTime()); return nsToTimestamp(GetRecordingTime());
} }
/** /**
@ -283,24 +313,19 @@ const char* WSEvents::GetRecordingTimecode() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnSceneChange() { void WSEvents::OnSceneChange() {
obs_data_t* data = obs_data_create(); OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene();
OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(currentScene);
connectSceneSignals(currentScene);
obs_source_t* current_scene = obs_frontend_get_current_scene(); OBSDataAutoRelease data = obs_data_create();
obs_data_array_t* scene_items = Utils::GetSceneItems(current_scene); obs_data_set_string(data, "scene-name", obs_source_get_name(currentScene));
connectSceneSignals(current_scene); obs_data_set_array(data, "sources", sceneItems);
obs_data_set_string(data, "scene-name", obs_source_get_name(current_scene));
obs_data_set_array(data, "sources", scene_items);
broadcastUpdate("SwitchScenes", data); broadcastUpdate("SwitchScenes", data);
obs_data_array_release(scene_items);
obs_source_release(current_scene);
obs_data_release(data);
// Dirty fix : OBS blocks signals when swapping scenes in Studio Mode // Dirty fix : OBS blocks signals when swapping scenes in Studio Mode
// after transition end, so SelectedSceneChanged is never called... // after transition end, so SelectedSceneChanged is never called...
if (Utils::IsPreviewModeActive()) { if (obs_frontend_preview_program_mode_active()) {
QListWidget* list = Utils::GetSceneListControl(); QListWidget* list = Utils::GetSceneListControl();
SelectedSceneChanged(list->currentItem(), nullptr); SelectedSceneChanged(list->currentItem(), nullptr);
} }
@ -330,8 +355,8 @@ void WSEvents::OnSceneListChange() {
void WSEvents::OnSceneCollectionChange() { void WSEvents::OnSceneCollectionChange() {
broadcastUpdate("SceneCollectionChanged"); broadcastUpdate("SceneCollectionChanged");
scene_handler = nullptr; currentScene = nullptr;
transition_handler = nullptr; currentTransition = nullptr;
OnTransitionListChange(); OnTransitionListChange();
OnTransitionChange(); OnTransitionChange();
@ -363,17 +388,14 @@ void WSEvents::OnSceneCollectionListChange() {
* @since 4.0.0 * @since 4.0.0
*/ */
void WSEvents::OnTransitionChange() { void WSEvents::OnTransitionChange() {
obs_source_t* current_transition = obs_frontend_get_current_transition(); OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition();
connectTransitionSignals(current_transition); connectTransitionSignals(currentTransition);
obs_data_t* data = obs_data_create(); OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "transition-name", obs_data_set_string(data, "transition-name",
obs_source_get_name(current_transition)); obs_source_get_name(currentTransition));
broadcastUpdate("SwitchTransition", data); broadcastUpdate("SwitchTransition", data);
obs_data_release(data);
obs_source_release(current_transition);
} }
/** /**
@ -424,12 +446,10 @@ void WSEvents::OnProfileListChange() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnStreamStarting() { void WSEvents::OnStreamStarting() {
obs_data_t* data = obs_data_create(); OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "preview-only", false); obs_data_set_bool(data, "preview-only", false);
broadcastUpdate("StreamStarting", data); broadcastUpdate("StreamStarting", data);
obs_data_release(data);
} }
/** /**
@ -441,7 +461,7 @@ void WSEvents::OnStreamStarting() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnStreamStarted() { void WSEvents::OnStreamStarted() {
_stream_starttime = os_gettime_ns(); _streamStarttime = os_gettime_ns();
_lastBytesSent = 0; _lastBytesSent = 0;
broadcastUpdate("StreamStarted"); broadcastUpdate("StreamStarted");
} }
@ -457,12 +477,10 @@ void WSEvents::OnStreamStarted() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnStreamStopping() { void WSEvents::OnStreamStopping() {
obs_data_t* data = obs_data_create(); OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "preview-only", false); obs_data_set_bool(data, "preview-only", false);
broadcastUpdate("StreamStopping", data); broadcastUpdate("StreamStopping", data);
obs_data_release(data);
} }
/** /**
@ -474,7 +492,7 @@ void WSEvents::OnStreamStopping() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnStreamStopped() { void WSEvents::OnStreamStopped() {
_stream_starttime = 0; _streamStarttime = 0;
broadcastUpdate("StreamStopped"); broadcastUpdate("StreamStopped");
} }
@ -499,7 +517,7 @@ void WSEvents::OnRecordingStarting() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnRecordingStarted() { void WSEvents::OnRecordingStarted() {
_rec_starttime = os_gettime_ns(); _recStarttime = os_gettime_ns();
broadcastUpdate("RecordingStarted"); broadcastUpdate("RecordingStarted");
} }
@ -524,7 +542,7 @@ void WSEvents::OnRecordingStopping() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::OnRecordingStopped() { void WSEvents::OnRecordingStopped() {
_rec_starttime = 0; _recStarttime = 0;
broadcastUpdate("RecordingStopped"); broadcastUpdate("RecordingStopped");
} }
@ -608,60 +626,111 @@ void WSEvents::OnExit() {
* @since 0.3 * @since 0.3
*/ */
void WSEvents::StreamStatus() { void WSEvents::StreamStatus() {
bool streaming_active = obs_frontend_streaming_active(); bool streamingActive = obs_frontend_streaming_active();
bool recording_active = obs_frontend_recording_active(); bool recordingActive = obs_frontend_recording_active();
obs_output_t* stream_output = obs_frontend_get_streaming_output(); OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output();
if (!stream_output || !streaming_active) { if (!streamOutput || !streamingActive) {
if (stream_output) {
obs_output_release(stream_output);
}
return; return;
} }
uint64_t bytes_sent = obs_output_get_total_bytes(stream_output); uint64_t bytesSent = obs_output_get_total_bytes(streamOutput);
uint64_t bytes_sent_time = os_gettime_ns(); uint64_t bytesSentTime = os_gettime_ns();
if (bytes_sent < _lastBytesSent) if (bytesSent < _lastBytesSent)
bytes_sent = 0; bytesSent = 0;
if (bytes_sent == 0) if (bytesSent == 0)
_lastBytesSent = 0; _lastBytesSent = 0;
uint64_t bytes_between = bytes_sent - _lastBytesSent; uint64_t bytesBetween = bytesSent - _lastBytesSent;
double time_passed = double timePassed =
double(bytes_sent_time - _lastBytesSentTime) / 1000000000.0; double(bytesSentTime - _lastBytesSentTime) / 1000000000.0;
uint64_t bytes_per_sec = bytes_between / time_passed; uint64_t bytesPerSec = bytesBetween / timePassed;
_lastBytesSent = bytes_sent; _lastBytesSent = bytesSent;
_lastBytesSentTime = bytes_sent_time; _lastBytesSentTime = bytesSentTime;
uint64_t totalStreamTime = uint64_t totalStreamTime =
(os_gettime_ns() - _stream_starttime) / 1000000000; (os_gettime_ns() - _streamStarttime) / 1000000000;
int total_frames = obs_output_get_total_frames(stream_output); int totalFrames = obs_output_get_total_frames(streamOutput);
int dropped_frames = obs_output_get_frames_dropped(stream_output); int droppedFrames = obs_output_get_frames_dropped(streamOutput);
float strain = obs_output_get_congestion(stream_output); float strain = obs_output_get_congestion(streamOutput);
obs_data_t* data = obs_data_create(); OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "streaming", streaming_active); obs_data_set_bool(data, "streaming", streamingActive);
obs_data_set_bool(data, "recording", recording_active); obs_data_set_bool(data, "recording", recordingActive);
obs_data_set_int(data, "bytes-per-sec", bytes_per_sec); obs_data_set_int(data, "bytes-per-sec", bytesPerSec);
obs_data_set_int(data, "kbits-per-sec", (bytes_per_sec * 8) / 1024); obs_data_set_int(data, "kbits-per-sec", (bytesPerSec * 8) / 1024);
obs_data_set_int(data, "total-stream-time", totalStreamTime); obs_data_set_int(data, "total-stream-time", totalStreamTime);
obs_data_set_int(data, "num-total-frames", total_frames); obs_data_set_int(data, "num-total-frames", totalFrames);
obs_data_set_int(data, "num-dropped-frames", dropped_frames); obs_data_set_int(data, "num-dropped-frames", droppedFrames);
obs_data_set_double(data, "fps", obs_get_active_fps()); obs_data_set_double(data, "fps", obs_get_active_fps());
obs_data_set_double(data, "strain", strain); obs_data_set_double(data, "strain", strain);
obs_data_set_bool(data, "preview-only", false); // Retrocompat with OBSRemote obs_data_set_bool(data, "preview-only", false); // Retrocompat with OBSRemote
broadcastUpdate("StreamStatus", data); broadcastUpdate("StreamStatus", data);
}
obs_data_release(data); /**
obs_output_release(stream_output); * Emitted every 2 seconds after enabling it by calling SetHeartbeat.
*
* @return {boolean} `pulse` Toggles between every JSON meassage as an "I am alive" indicator.
* @return {string (optional)} `current-profile` Current active profile.
* @return {string (optional)} `current-scene` Current active scene.
* @return {boolean (optional)} `streaming` Current streaming state.
* @return {int (optional)} `total-stream-time` Total time (in seconds) since the stream started.
* @return {int (optional)} `total-stream-bytes` Total bytes sent since the stream started.
* @return {int (optional)} `total-stream-frames` Total frames streamed since the stream started.
* @return {boolean (optional)} `recording` Current recording state.
* @return {int (optional)} `total-record-time` Total time (in seconds) since recording started.
* @return {int (optional)} `total-record-bytes` Total bytes recorded since the recording started.
* @return {int (optional)} `total-record-frames` Total frames recorded since the recording started.
*
* @api events
* @name Heartbeat
* @category general
*/
void WSEvents::Heartbeat() {
if (!HeartbeatIsActive) return;
bool streamingActive = obs_frontend_streaming_active();
bool recordingActive = obs_frontend_recording_active();
OBSDataAutoRelease data = obs_data_create();
OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output();
OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output();
pulse = !pulse;
obs_data_set_bool(data, "pulse", pulse);
obs_data_set_string(data, "current-profile", obs_frontend_get_current_profile());
OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene();
obs_data_set_string(data, "current-scene", obs_source_get_name(currentScene));
obs_data_set_bool(data, "streaming", streamingActive);
if (streamingActive) {
uint64_t totalStreamTime = (os_gettime_ns() - _streamStarttime) / 1000000000;
obs_data_set_int(data, "total-stream-time", totalStreamTime);
obs_data_set_int(data, "total-stream-bytes", (uint64_t)obs_output_get_total_bytes(streamOutput));
obs_data_set_int(data, "total-stream-frames", obs_output_get_total_frames(streamOutput));
}
obs_data_set_bool(data, "recording", recordingActive);
if (recordingActive) {
uint64_t totalRecordTime = (os_gettime_ns() - _recStarttime) / 1000000000;
obs_data_set_int(data, "total-record-time", totalRecordTime);
obs_data_set_int(data, "total-record-bytes", (uint64_t)obs_output_get_total_bytes(recordOutput));
obs_data_set_int(data, "total-record-frames", obs_output_get_total_frames(recordOutput));
}
broadcastUpdate("Heartbeat", data);
} }
/** /**
@ -675,11 +744,10 @@ void WSEvents::StreamStatus() {
* @since 4.0.0 * @since 4.0.0
*/ */
void WSEvents::TransitionDurationChanged(int ms) { void WSEvents::TransitionDurationChanged(int ms) {
obs_data_t* fields = obs_data_create(); OBSDataAutoRelease fields = obs_data_create();
obs_data_set_int(fields, "new-duration", ms); obs_data_set_int(fields, "new-duration", ms);
broadcastUpdate("TransitionDurationChanged", fields); broadcastUpdate("TransitionDurationChanged", fields);
obs_data_release(fields);
} }
/** /**
@ -697,17 +765,13 @@ void WSEvents::OnTransitionBegin(void* param, calldata_t* data) {
UNUSED_PARAMETER(data); UNUSED_PARAMETER(data);
WSEvents* instance = static_cast<WSEvents*>(param); WSEvents* instance = static_cast<WSEvents*>(param);
obs_source_t* current_transition = obs_frontend_get_current_transition(); OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition();
const char* name = obs_source_get_name(current_transition);
int duration = Utils::GetTransitionDuration();
obs_data_t* fields = obs_data_create(); OBSDataAutoRelease fields = obs_data_create();
obs_data_set_string(fields, "name", name); obs_data_set_string(fields, "name", obs_source_get_name(currentTransition));
obs_data_set_int(fields, "duration", duration); obs_data_set_int(fields, "duration", Utils::GetTransitionDuration());
instance->broadcastUpdate("TransitionBegin", fields); instance->broadcastUpdate("TransitionBegin", fields);
obs_data_release(fields);
obs_source_release(current_transition);
} }
/** /**
@ -726,12 +790,11 @@ void WSEvents::OnSceneReordered(void* param, calldata_t* data) {
obs_scene_t* scene = nullptr; obs_scene_t* scene = nullptr;
calldata_get_ptr(data, "scene", &scene); calldata_get_ptr(data, "scene", &scene);
obs_data_t* fields = obs_data_create(); OBSDataAutoRelease fields = obs_data_create();
obs_data_set_string(fields, "scene-name", obs_data_set_string(fields, "scene-name",
obs_source_get_name(obs_scene_get_source(scene))); obs_source_get_name(obs_scene_get_source(scene)));
instance->broadcastUpdate("SourceOrderChanged", fields); instance->broadcastUpdate("SourceOrderChanged", fields);
obs_data_release(fields);
} }
/** /**
@ -751,20 +814,19 @@ void WSEvents::OnSceneItemAdd(void* param, calldata_t* data) {
obs_scene_t* scene = nullptr; obs_scene_t* scene = nullptr;
calldata_get_ptr(data, "scene", &scene); calldata_get_ptr(data, "scene", &scene);
obs_sceneitem_t* scene_item = nullptr; obs_sceneitem_t* sceneItem = nullptr;
calldata_get_ptr(data, "item", &scene_item); calldata_get_ptr(data, "item", &sceneItem);
const char* scene_name = const char* sceneName =
obs_source_get_name(obs_scene_get_source(scene)); obs_source_get_name(obs_scene_get_source(scene));
const char* sceneitem_name = const char* sceneItemName =
obs_source_get_name(obs_sceneitem_get_source(scene_item)); obs_source_get_name(obs_sceneitem_get_source(sceneItem));
obs_data_t* fields = obs_data_create(); OBSDataAutoRelease fields = obs_data_create();
obs_data_set_string(fields, "scene-name", scene_name); obs_data_set_string(fields, "scene-name", sceneName);
obs_data_set_string(fields, "item-name", sceneitem_name); obs_data_set_string(fields, "item-name", sceneItemName);
instance->broadcastUpdate("SceneItemAdded", fields); instance->broadcastUpdate("SceneItemAdded", fields);
obs_data_release(fields);
} }
/** /**
@ -784,20 +846,19 @@ void WSEvents::OnSceneItemDelete(void* param, calldata_t* data) {
obs_scene_t* scene = nullptr; obs_scene_t* scene = nullptr;
calldata_get_ptr(data, "scene", &scene); calldata_get_ptr(data, "scene", &scene);
obs_sceneitem_t* scene_item = nullptr; obs_sceneitem_t* sceneItem = nullptr;
calldata_get_ptr(data, "item", &scene_item); calldata_get_ptr(data, "item", &sceneItem);
const char* scene_name = const char* sceneName =
obs_source_get_name(obs_scene_get_source(scene)); obs_source_get_name(obs_scene_get_source(scene));
const char* sceneitem_name = const char* sceneItemName =
obs_source_get_name(obs_sceneitem_get_source(scene_item)); obs_source_get_name(obs_sceneitem_get_source(sceneItem));
obs_data_t* fields = obs_data_create(); OBSDataAutoRelease fields = obs_data_create();
obs_data_set_string(fields, "scene-name", scene_name); obs_data_set_string(fields, "scene-name", sceneName);
obs_data_set_string(fields, "item-name", sceneitem_name); obs_data_set_string(fields, "item-name", sceneItemName);
instance->broadcastUpdate("SceneItemRemoved", fields); instance->broadcastUpdate("SceneItemRemoved", fields);
obs_data_release(fields);
} }
/** /**
@ -818,24 +879,23 @@ void WSEvents::OnSceneItemVisibilityChanged(void* param, calldata_t* data) {
obs_scene_t* scene = nullptr; obs_scene_t* scene = nullptr;
calldata_get_ptr(data, "scene", &scene); calldata_get_ptr(data, "scene", &scene);
obs_sceneitem_t* scene_item = nullptr; obs_sceneitem_t* sceneItem = nullptr;
calldata_get_ptr(data, "item", &scene_item); calldata_get_ptr(data, "item", &sceneItem);
bool visible = false; bool visible = false;
calldata_get_bool(data, "visible", &visible); calldata_get_bool(data, "visible", &visible);
const char* scene_name = const char* sceneName =
obs_source_get_name(obs_scene_get_source(scene)); obs_source_get_name(obs_scene_get_source(scene));
const char* sceneitem_name = const char* sceneItemName =
obs_source_get_name(obs_sceneitem_get_source(scene_item)); obs_source_get_name(obs_sceneitem_get_source(sceneItem));
obs_data_t* fields = obs_data_create(); OBSDataAutoRelease fields = obs_data_create();
obs_data_set_string(fields, "scene-name", scene_name); obs_data_set_string(fields, "scene-name", sceneName);
obs_data_set_string(fields, "item-name", sceneitem_name); obs_data_set_string(fields, "item-name", sceneItemName);
obs_data_set_bool(fields, "item-visible", visible); obs_data_set_bool(fields, "item-visible", visible);
instance->broadcastUpdate("SceneItemVisibilityChanged", fields); instance->broadcastUpdate("SceneItemVisibilityChanged", fields);
obs_data_release(fields);
} }
/** /**
@ -850,21 +910,19 @@ void WSEvents::OnSceneItemVisibilityChanged(void* param, calldata_t* data) {
* @since 4.1.0 * @since 4.1.0
*/ */
void WSEvents::SelectedSceneChanged(QListWidgetItem* current, QListWidgetItem* prev) { void WSEvents::SelectedSceneChanged(QListWidgetItem* current, QListWidgetItem* prev) {
if (Utils::IsPreviewModeActive()) { if (obs_frontend_preview_program_mode_active()) {
obs_scene_t* scene = Utils::SceneListItemToScene(current); OBSScene scene = Utils::SceneListItemToScene(current);
if (!scene) return; if (!scene)
return;
obs_source_t* scene_source = obs_scene_get_source(scene); OBSSource sceneSource = obs_scene_get_source(scene);
obs_data_array_t* scene_items = Utils::GetSceneItems(scene_source); OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(sceneSource);
obs_data_t* data = obs_data_create(); OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "scene-name", obs_source_get_name(scene_source)); obs_data_set_string(data, "scene-name", obs_source_get_name(sceneSource));
obs_data_set_array(data, "sources", scene_items); obs_data_set_array(data, "sources", sceneItems);
broadcastUpdate("PreviewSceneChanged", data); broadcastUpdate("PreviewSceneChanged", data);
obs_data_array_release(scene_items);
obs_data_release(data);
} }
} }
@ -878,10 +936,9 @@ void WSEvents::SelectedSceneChanged(QListWidgetItem* current, QListWidgetItem* p
* @category studio mode * @category studio mode
* @since 4.1.0 * @since 4.1.0
*/ */
void WSEvents::ModeSwitchClicked(bool checked) { void WSEvents::OnStudioModeSwitched(bool checked) {
obs_data_t* data = obs_data_create(); OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "new-state", checked); obs_data_set_bool(data, "new-state", checked);
broadcastUpdate("StudioModeSwitched", data); broadcastUpdate("StudioModeSwitched", data);
obs_data_release(data);
} }

View File

@ -20,9 +20,9 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#ifndef WSEVENTS_H #ifndef WSEVENTS_H
#define WSEVENTS_H #define WSEVENTS_H
#include <obs.hpp>
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
#include <QListWidgetItem> #include <QListWidgetItem>
#include "WSServer.h" #include "WSServer.h"
class WSEvents : public QObject { class WSEvents : public QObject {
@ -31,34 +31,38 @@ class WSEvents : public QObject {
explicit WSEvents(WSServer* srv); explicit WSEvents(WSServer* srv);
~WSEvents(); ~WSEvents();
static void FrontendEventHandler( static void FrontendEventHandler(
enum obs_frontend_event event, void* private_data); enum obs_frontend_event event, void* privateData);
static WSEvents* Instance;
void connectTransitionSignals(obs_source_t* transition); void connectTransitionSignals(obs_source_t* transition);
void connectSceneSignals(obs_source_t* scene); void connectSceneSignals(obs_source_t* scene);
static WSEvents* Instance;
uint64_t GetStreamingTime(); uint64_t GetStreamingTime();
const char* GetStreamingTimecode(); const char* GetStreamingTimecode();
uint64_t GetRecordingTime(); uint64_t GetRecordingTime();
const char* GetRecordingTimecode(); const char* GetRecordingTimecode();
bool HeartbeatIsActive;
private slots: private slots:
void deferredInitOperations(); void deferredInitOperations();
void StreamStatus(); void StreamStatus();
void Heartbeat();
void TransitionDurationChanged(int ms); void TransitionDurationChanged(int ms);
void SelectedSceneChanged( void SelectedSceneChanged(
QListWidgetItem* current, QListWidgetItem* prev); QListWidgetItem* current, QListWidgetItem* prev);
void ModeSwitchClicked(bool checked);
private: private:
WSServer* _srv; WSServer* _srv;
signal_handler_t* transition_handler; OBSSource currentScene;
signal_handler_t* scene_handler; OBSSource currentTransition;
bool _streaming_active; bool pulse;
bool _recording_active;
uint64_t _stream_starttime; bool _streamingActive;
uint64_t _rec_starttime; bool _recordingActive;
uint64_t _streamStarttime;
uint64_t _recStarttime;
uint64_t _lastBytesSent; uint64_t _lastBytesSent;
uint64_t _lastBytesSentTime; uint64_t _lastBytesSentTime;
@ -92,6 +96,8 @@ class WSEvents : public QObject {
void OnReplayStopping(); void OnReplayStopping();
void OnReplayStopped(); void OnReplayStopped();
void OnStudioModeSwitched(bool enabled);
void OnExit(); void OnExit();
static void OnTransitionBegin(void* param, calldata_t* data); static void OnTransitionBegin(void* param, calldata_t* data);

217
src/WSRequestHandler.cpp Normal file
View File

@ -0,0 +1,217 @@
/**
* obs-websocket
* Copyright (C) 2016-2017 Stéphane Lepin <stephane.lepin@gmail.com>
* Copyright (C) 2017 Mikhail Swift <https://github.com/mikhailswift>
*
* 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-data.h>
#include "Config.h"
#include "Utils.h"
#include "WSRequestHandler.h"
QHash<QString, void(*)(WSRequestHandler*)> WSRequestHandler::messageMap {
{ "GetVersion", WSRequestHandler::HandleGetVersion },
{ "GetAuthRequired", WSRequestHandler::HandleGetAuthRequired },
{ "Authenticate", WSRequestHandler::HandleAuthenticate },
{ "SetHeartbeat", WSRequestHandler::HandleSetHeartbeat },
{ "SetFilenameFormatting", WSRequestHandler::HandleSetFilenameFormatting },
{ "GetFilenameFormatting", WSRequestHandler::HandleGetFilenameFormatting },
{ "SetCurrentScene", WSRequestHandler::HandleSetCurrentScene },
{ "GetCurrentScene", WSRequestHandler::HandleGetCurrentScene },
{ "GetSceneList", WSRequestHandler::HandleGetSceneList },
{ "SetSourceRender", WSRequestHandler::HandleSetSceneItemRender }, // Retrocompat
{ "SetSceneItemRender", WSRequestHandler::HandleSetSceneItemRender },
{ "SetSceneItemPosition", WSRequestHandler::HandleSetSceneItemPosition },
{ "SetSceneItemTransform", WSRequestHandler::HandleSetSceneItemTransform },
{ "SetSceneItemCrop", WSRequestHandler::HandleSetSceneItemCrop },
{ "GetSceneItemProperties", WSRequestHandler::HandleGetSceneItemProperties },
{ "SetSceneItemProperties", WSRequestHandler::HandleSetSceneItemProperties },
{ "ResetSceneItem", WSRequestHandler::HandleResetSceneItem },
{ "GetStreamingStatus", WSRequestHandler::HandleGetStreamingStatus },
{ "StartStopStreaming", WSRequestHandler::HandleStartStopStreaming },
{ "StartStopRecording", WSRequestHandler::HandleStartStopRecording },
{ "StartStreaming", WSRequestHandler::HandleStartStreaming },
{ "StopStreaming", WSRequestHandler::HandleStopStreaming },
{ "StartRecording", WSRequestHandler::HandleStartRecording },
{ "StopRecording", WSRequestHandler::HandleStopRecording },
{ "StartStopReplayBuffer", WSRequestHandler::HandleStartStopReplayBuffer },
{ "StartReplayBuffer", WSRequestHandler::HandleStartReplayBuffer },
{ "StopReplayBuffer", WSRequestHandler::HandleStopReplayBuffer },
{ "SaveReplayBuffer", WSRequestHandler::HandleSaveReplayBuffer },
{ "SetRecordingFolder", WSRequestHandler::HandleSetRecordingFolder },
{ "GetRecordingFolder", WSRequestHandler::HandleGetRecordingFolder },
{ "GetTransitionList", WSRequestHandler::HandleGetTransitionList },
{ "GetCurrentTransition", WSRequestHandler::HandleGetCurrentTransition },
{ "SetCurrentTransition", WSRequestHandler::HandleSetCurrentTransition },
{ "SetTransitionDuration", WSRequestHandler::HandleSetTransitionDuration },
{ "GetTransitionDuration", WSRequestHandler::HandleGetTransitionDuration },
{ "SetVolume", WSRequestHandler::HandleSetVolume },
{ "GetVolume", WSRequestHandler::HandleGetVolume },
{ "ToggleMute", WSRequestHandler::HandleToggleMute },
{ "SetMute", WSRequestHandler::HandleSetMute },
{ "GetMute", WSRequestHandler::HandleGetMute },
{ "SetSyncOffset", WSRequestHandler::HandleSetSyncOffset },
{ "GetSyncOffset", WSRequestHandler::HandleGetSyncOffset },
{ "GetSpecialSources", WSRequestHandler::HandleGetSpecialSources },
{ "GetSourcesList", WSRequestHandler::HandleGetSourcesList },
{ "GetSourceTypesList", WSRequestHandler::HandleGetSourceTypesList },
{ "GetSourceSettings", WSRequestHandler::HandleGetSourceSettings },
{ "SetSourceSettings", WSRequestHandler::HandleSetSourceSettings },
{ "SetCurrentSceneCollection", WSRequestHandler::HandleSetCurrentSceneCollection },
{ "GetCurrentSceneCollection", WSRequestHandler::HandleGetCurrentSceneCollection },
{ "ListSceneCollections", WSRequestHandler::HandleListSceneCollections },
{ "SetCurrentProfile", WSRequestHandler::HandleSetCurrentProfile },
{ "GetCurrentProfile", WSRequestHandler::HandleGetCurrentProfile },
{ "ListProfiles", WSRequestHandler::HandleListProfiles },
{ "SetStreamSettings", WSRequestHandler::HandleSetStreamSettings },
{ "GetStreamSettings", WSRequestHandler::HandleGetStreamSettings },
{ "SaveStreamSettings", WSRequestHandler::HandleSaveStreamSettings },
{ "GetStudioModeStatus", WSRequestHandler::HandleGetStudioModeStatus },
{ "GetPreviewScene", WSRequestHandler::HandleGetPreviewScene },
{ "SetPreviewScene", WSRequestHandler::HandleSetPreviewScene },
{ "TransitionToProgram", WSRequestHandler::HandleTransitionToProgram },
{ "EnableStudioMode", WSRequestHandler::HandleEnableStudioMode },
{ "DisableStudioMode", WSRequestHandler::HandleDisableStudioMode },
{ "ToggleStudioMode", WSRequestHandler::HandleToggleStudioMode },
{ "SetTextGDIPlusProperties", WSRequestHandler::HandleSetTextGDIPlusProperties },
{ "GetTextGDIPlusProperties", WSRequestHandler::HandleGetTextGDIPlusProperties },
{ "GetBrowserSourceProperties", WSRequestHandler::HandleGetBrowserSourceProperties },
{ "SetBrowserSourceProperties", WSRequestHandler::HandleSetBrowserSourceProperties }
};
QSet<QString> WSRequestHandler::authNotRequired {
"GetVersion",
"GetAuthRequired",
"Authenticate"
};
WSRequestHandler::WSRequestHandler(QWebSocket* client) :
_messageId(0),
_requestType(""),
data(nullptr),
_client(client)
{
}
void WSRequestHandler::processIncomingMessage(QString textMessage) {
QByteArray msgData = textMessage.toUtf8();
const char* msg = msgData.constData();
data = obs_data_create_from_json(msg);
if (!data) {
if (!msg)
msg = "<null pointer>";
blog(LOG_ERROR, "invalid JSON payload received for '%s'", msg);
SendErrorResponse("invalid JSON payload");
return;
}
if (Config::Current()->DebugEnabled) {
blog(LOG_DEBUG, "Request >> '%s'", msg);
}
if (!hasField("request-type")
|| !hasField("message-id"))
{
SendErrorResponse("missing request parameters");
return;
}
_requestType = obs_data_get_string(data, "request-type");
_messageId = obs_data_get_string(data, "message-id");
if (Config::Current()->AuthRequired
&& (_client->property(PROP_AUTHENTICATED).toBool() == false)
&& (authNotRequired.find(_requestType) == authNotRequired.end()))
{
SendErrorResponse("Not Authenticated");
return;
}
void (*handlerFunc)(WSRequestHandler*) = (messageMap[_requestType]);
if (handlerFunc != nullptr)
handlerFunc(this);
else
SendErrorResponse("invalid request type");
}
WSRequestHandler::~WSRequestHandler() {
}
void WSRequestHandler::SendOKResponse(obs_data_t* additionalFields) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "status", "ok");
obs_data_set_string(response, "message-id", _messageId);
if (additionalFields)
obs_data_apply(response, additionalFields);
SendResponse(response);
}
void WSRequestHandler::SendErrorResponse(const char* errorMessage) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "status", "error");
obs_data_set_string(response, "error", errorMessage);
obs_data_set_string(response, "message-id", _messageId);
SendResponse(response);
}
void WSRequestHandler::SendErrorResponse(obs_data_t* additionalFields) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "status", "error");
obs_data_set_string(response, "message-id", _messageId);
if (additionalFields)
obs_data_set_obj(response, "error", additionalFields);
SendResponse(response);
}
void WSRequestHandler::SendResponse(obs_data_t* response) {
QString json = obs_data_get_json(response);
_client->sendTextMessage(json);
if (Config::Current()->DebugEnabled)
blog(LOG_DEBUG, "Response << '%s'", json.toUtf8().constData());
}
bool WSRequestHandler::hasField(QString name) {
if (!data || name.isEmpty() || name.isNull())
return false;
return obs_data_has_user_value(data, name.toUtf8());
}

View File

@ -20,12 +20,16 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#ifndef WSREQUESTHANDLER_H #ifndef WSREQUESTHANDLER_H
#define WSREQUESTHANDLER_H #define WSREQUESTHANDLER_H
#include <QMap> #include <QHash>
#include <QSet>
#include <QWebSocket> #include <QWebSocket>
#include <QWebSocketServer> #include <QWebSocketServer>
#include <obs.hpp>
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
#include "obs-websocket.h"
class WSRequestHandler : public QObject { class WSRequestHandler : public QObject {
Q_OBJECT Q_OBJECT
@ -33,26 +37,31 @@ class WSRequestHandler : public QObject {
explicit WSRequestHandler(QWebSocket* client); explicit WSRequestHandler(QWebSocket* client);
~WSRequestHandler(); ~WSRequestHandler();
void processIncomingMessage(QString textMessage); void processIncomingMessage(QString textMessage);
bool hasField(const char* name); bool hasField(QString name);
private: private:
static obs_service_t* _service;
QWebSocket* _client; QWebSocket* _client;
const char* _messageId; const char* _messageId;
const char* _requestType; const char* _requestType;
obs_data_t* data; OBSDataAutoRelease data;
QMap<QString, void(*)(WSRequestHandler*)> messageMap;
QSet<QString> authNotRequired;
void SendOKResponse(obs_data_t* additionalFields = NULL); void SendOKResponse(obs_data_t* additionalFields = NULL);
void SendErrorResponse(const char* errorMessage); void SendErrorResponse(const char* errorMessage);
void SendErrorResponse(obs_data_t* additionalFields = NULL);
void SendResponse(obs_data_t* response); void SendResponse(obs_data_t* response);
static QHash<QString, void(*)(WSRequestHandler*)> messageMap;
static QSet<QString> authNotRequired;
static void HandleGetVersion(WSRequestHandler* req); static void HandleGetVersion(WSRequestHandler* req);
static void HandleGetAuthRequired(WSRequestHandler* req); static void HandleGetAuthRequired(WSRequestHandler* req);
static void HandleAuthenticate(WSRequestHandler* req); static void HandleAuthenticate(WSRequestHandler* req);
static void HandleSetHeartbeat(WSRequestHandler* req);
static void HandleSetFilenameFormatting(WSRequestHandler* req);
static void HandleGetFilenameFormatting(WSRequestHandler* req);
static void HandleSetCurrentScene(WSRequestHandler* req); static void HandleSetCurrentScene(WSRequestHandler* req);
static void HandleGetCurrentScene(WSRequestHandler* req); static void HandleGetCurrentScene(WSRequestHandler* req);
static void HandleGetSceneList(WSRequestHandler* req); static void HandleGetSceneList(WSRequestHandler* req);
@ -61,6 +70,8 @@ class WSRequestHandler : public QObject {
static void HandleSetSceneItemPosition(WSRequestHandler* req); static void HandleSetSceneItemPosition(WSRequestHandler* req);
static void HandleSetSceneItemTransform(WSRequestHandler* req); static void HandleSetSceneItemTransform(WSRequestHandler* req);
static void HandleSetSceneItemCrop(WSRequestHandler* req); static void HandleSetSceneItemCrop(WSRequestHandler* req);
static void HandleGetSceneItemProperties(WSRequestHandler* req);
static void HandleSetSceneItemProperties(WSRequestHandler* req);
static void HandleResetSceneItem(WSRequestHandler* req); static void HandleResetSceneItem(WSRequestHandler* req);
static void HandleGetStreamingStatus(WSRequestHandler* req); static void HandleGetStreamingStatus(WSRequestHandler* req);
@ -91,6 +102,10 @@ class WSRequestHandler : public QObject {
static void HandleSetSyncOffset(WSRequestHandler* req); static void HandleSetSyncOffset(WSRequestHandler* req);
static void HandleGetSyncOffset(WSRequestHandler* req); static void HandleGetSyncOffset(WSRequestHandler* req);
static void HandleGetSpecialSources(WSRequestHandler* req); static void HandleGetSpecialSources(WSRequestHandler* req);
static void HandleGetSourcesList(WSRequestHandler* req);
static void HandleGetSourceTypesList(WSRequestHandler* req);
static void HandleGetSourceSettings(WSRequestHandler* req);
static void HandleSetSourceSettings(WSRequestHandler* req);
static void HandleSetCurrentSceneCollection(WSRequestHandler* req); static void HandleSetCurrentSceneCollection(WSRequestHandler* req);
static void HandleGetCurrentSceneCollection(WSRequestHandler* req); static void HandleGetCurrentSceneCollection(WSRequestHandler* req);

View File

@ -0,0 +1,168 @@
#include <QString>
#include "Config.h"
#include "Utils.h"
#include "WSEvents.h"
#include "WSRequestHandler.h"
/**
* Returns the latest version of the plugin and the API.
*
* @return {double} `version` OBSRemote compatible API version. Fixed to 1.1 for retrocompatibility.
* @return {String} `obs-websocket-version` obs-websocket plugin version.
* @return {String} `obs-studio-version` OBS Studio program version.
* @return {String} `available-requests` List of available request types, formatted as a comma-separated list string (e.g. : "Method1,Method2,Method3").
*
* @api requests
* @name GetVersion
* @category general
* @since 0.3
*/
void WSRequestHandler::HandleGetVersion(WSRequestHandler* req) {
QString obsVersion = Utils::OBSVersionString();
QList<QString> names = req->messageMap.keys();
names.sort(Qt::CaseInsensitive);
// (Palakis) OBS' data arrays only support object arrays, so I improvised.
QString requests;
requests += names.takeFirst();
for (QString reqName : names) {
requests += ("," + reqName);
}
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "obs-websocket-version", OBS_WEBSOCKET_VERSION);
obs_data_set_string(data, "obs-studio-version", obsVersion.toUtf8());
obs_data_set_string(data, "available-requests", requests.toUtf8());
req->SendOKResponse(data);
}
/**
* Tells the client if authentication is required. If so, returns authentication parameters `challenge`
* and `salt` (see "Authentication" for more information).
*
* @return {boolean} `authRequired` Indicates whether authentication is required.
* @return {String (optional)} `challenge`
* @return {String (optional)} `salt`
*
* @api requests
* @name GetAuthRequired
* @category general
* @since 0.3
*/
void WSRequestHandler::HandleGetAuthRequired(WSRequestHandler* req) {
bool authRequired = Config::Current()->AuthRequired;
OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "authRequired", authRequired);
if (authRequired) {
obs_data_set_string(data, "challenge",
Config::Current()->SessionChallenge.toUtf8());
obs_data_set_string(data, "salt",
Config::Current()->Salt.toUtf8());
}
req->SendOKResponse(data);
}
/**
* Attempt to authenticate the client to the server.
*
* @param {String} `auth` Response to the auth challenge (see "Authentication" for more information).
*
* @api requests
* @name Authenticate
* @category general
* @since 0.3
*/
void WSRequestHandler::HandleAuthenticate(WSRequestHandler* req) {
if (!req->hasField("auth")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString auth = obs_data_get_string(req->data, "auth");
if (auth.isEmpty()) {
req->SendErrorResponse("auth not specified!");
return;
}
if ((req->_client->property(PROP_AUTHENTICATED).toBool() == false)
&& Config::Current()->CheckAuth(auth))
{
req->_client->setProperty(PROP_AUTHENTICATED, true);
req->SendOKResponse();
} else {
req->SendErrorResponse("Authentication Failed.");
}
}
/**
* Enable/disable sending of the Heartbeat event
*
* @param {boolean} `enable` Starts/Stops emitting heartbeat messages
*
* @api requests
* @name SetHeartbeat
* @category general
* @since 4.3.0
*/
void WSRequestHandler::HandleSetHeartbeat(WSRequestHandler* req) {
if (!req->hasField("enable")) {
req->SendErrorResponse("Heartbeat <enable> parameter missing");
return;
}
WSEvents::Instance->HeartbeatIsActive =
obs_data_get_bool(req->data, "enable");
OBSDataAutoRelease response = obs_data_create();
obs_data_set_bool(response, "enable",
WSEvents::Instance->HeartbeatIsActive);
req->SendOKResponse(response);
}
/**
* Set the filename formatting string
*
* @param {String} `filename-formatting` Filename formatting string to set.
*
* @api requests
* @name SetFilenameFormatting
* @category general
* @since 4.3.0
*/
void WSRequestHandler::HandleSetFilenameFormatting(WSRequestHandler* req) {
if (!req->hasField("filename-formatting")) {
req->SendErrorResponse("<filename-formatting> parameter missing");
return;
}
QString filenameFormatting = obs_data_get_string(req->data, "filename-formatting");
if (!filenameFormatting.isEmpty()) {
Utils::SetFilenameFormatting(filenameFormatting.toUtf8());
req->SendOKResponse();
} else {
req->SendErrorResponse("invalid request parameters");
}
}
/**
* Get the filename formatting string
*
* @return {String} `filename-formatting` Current filename formatting string.
*
* @api requests
* @name GetFilenameFormatting
* @category general
* @since 4.3.0
*/
void WSRequestHandler::HandleGetFilenameFormatting(WSRequestHandler* req) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "filename-formatting", Utils::GetFilenameFormatting());
req->SendOKResponse(response);
}

View File

@ -0,0 +1,70 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Set the currently active profile.
*
* @param {String} `profile-name` Name of the desired profile.
*
* @api requests
* @name SetCurrentProfile
* @category profiles
* @since 4.0.0
*/
void WSRequestHandler::HandleSetCurrentProfile(WSRequestHandler* req) {
if (!req->hasField("profile-name")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString profileName = obs_data_get_string(req->data, "profile-name");
if (!profileName.isEmpty()) {
// TODO : check if profile exists
obs_frontend_set_current_profile(profileName.toUtf8());
req->SendOKResponse();
} else {
req->SendErrorResponse("invalid request parameters");
}
}
/**
* Get the name of the current profile.
*
* @return {String} `profile-name` Name of the currently active profile.
*
* @api requests
* @name GetCurrentProfile
* @category profiles
* @since 4.0.0
*/
void WSRequestHandler::HandleGetCurrentProfile(WSRequestHandler* req) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "profile-name",
obs_frontend_get_current_profile());
req->SendOKResponse(response);
}
/**
* Get a list of available profiles.
*
* @return {Object|Array} `profiles` List of available profiles.
*
* @api requests
* @name ListProfiles
* @category profiles
* @since 4.0.0
*/
void WSRequestHandler::HandleListProfiles(WSRequestHandler* req) {
char** profiles = obs_frontend_get_profiles();
OBSDataArrayAutoRelease list =
Utils::StringListToArray(profiles, "profile-name");
bfree(profiles);
OBSDataAutoRelease response = obs_data_create();
obs_data_set_array(response, "profiles", list);
req->SendOKResponse(response);
}

View File

@ -0,0 +1,100 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Toggle recording on or off.
*
* @api requests
* @name StartStopRecording
* @category recording
* @since 0.3
*/
void WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req) {
if (obs_frontend_recording_active())
obs_frontend_recording_stop();
else
obs_frontend_recording_start();
req->SendOKResponse();
}
/**
* Start recording.
* Will return an `error` if recording is already active.
*
* @api requests
* @name StartRecording
* @category recording
* @since 4.1.0
*/
void WSRequestHandler::HandleStartRecording(WSRequestHandler* req) {
if (obs_frontend_recording_active() == false) {
obs_frontend_recording_start();
req->SendOKResponse();
} else {
req->SendErrorResponse("recording already active");
}
}
/**
* Stop recording.
* Will return an `error` if recording is not active.
*
* @api requests
* @name StopRecording
* @category recording
* @since 4.1.0
*/
void WSRequestHandler::HandleStopRecording(WSRequestHandler* req) {
if (obs_frontend_recording_active() == true) {
obs_frontend_recording_stop();
req->SendOKResponse();
} else {
req->SendErrorResponse("recording not active");
}
}
/**
* Change the current recording folder.
*
* @param {String} `rec-folder` Path of the recording folder.
*
* @api requests
* @name SetRecordingFolder
* @category recording
* @since 4.1.0
*/
void WSRequestHandler::HandleSetRecordingFolder(WSRequestHandler* req) {
if (!req->hasField("rec-folder")) {
req->SendErrorResponse("missing request parameters");
return;
}
const char* newRecFolder = obs_data_get_string(req->data, "rec-folder");
bool success = Utils::SetRecordingFolder(newRecFolder);
if (success)
req->SendOKResponse();
else
req->SendErrorResponse("invalid request parameters");
}
/**
* Get the path of the current recording folder.
*
* @return {String} `rec-folder` Path of the recording folder.
*
* @api requests
* @name GetRecordingFolder
* @category recording
* @since 4.1.0
*/
void WSRequestHandler::HandleGetRecordingFolder(WSRequestHandler* req) {
const char* recFolder = Utils::GetRecordingFolder();
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "rec-folder", recFolder);
req->SendOKResponse(response);
}

View File

@ -0,0 +1,92 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Toggle the Replay Buffer on/off.
*
* @api requests
* @name StartStopReplayBuffer
* @category replay buffer
* @since 4.2.0
*/
void WSRequestHandler::HandleStartStopReplayBuffer(WSRequestHandler* req) {
if (obs_frontend_replay_buffer_active()) {
obs_frontend_replay_buffer_stop();
} else {
Utils::StartReplayBuffer();
}
req->SendOKResponse();
}
/**
* Start recording into the Replay Buffer.
* Will return an `error` if the Replay Buffer is already active or if the
* "Save Replay Buffer" hotkey is not set in OBS' settings.
* Setting this hotkey is mandatory, even when triggering saves only
* through obs-websocket.
*
* @api requests
* @name StartReplayBuffer
* @category replay buffer
* @since 4.2.0
*/
void WSRequestHandler::HandleStartReplayBuffer(WSRequestHandler* req) {
if (!Utils::ReplayBufferEnabled()) {
req->SendErrorResponse("replay buffer disabled in settings");
return;
}
if (obs_frontend_replay_buffer_active() == true) {
req->SendErrorResponse("replay buffer already active");
return;
}
Utils::StartReplayBuffer();
req->SendOKResponse();
}
/**
* Stop recording into the Replay Buffer.
* Will return an `error` if the Replay Buffer is not active.
*
* @api requests
* @name StopReplayBuffer
* @category replay buffer
* @since 4.2.0
*/
void WSRequestHandler::HandleStopReplayBuffer(WSRequestHandler* req) {
if (obs_frontend_replay_buffer_active() == true) {
obs_frontend_replay_buffer_stop();
req->SendOKResponse();
} else {
req->SendErrorResponse("replay buffer not active");
}
}
/**
* Flush and save the contents of the Replay Buffer to disk. This is
* basically the same as triggering the "Save Replay Buffer" hotkey.
* Will return an `error` if the Replay Buffer is not active.
*
* @api requests
* @name SaveReplayBuffer
* @category replay buffer
* @since 4.2.0
*/
void WSRequestHandler::HandleSaveReplayBuffer(WSRequestHandler* req) {
if (!obs_frontend_replay_buffer_active()) {
req->SendErrorResponse("replay buffer not active");
return;
}
OBSOutputAutoRelease replayOutput = obs_frontend_get_replay_buffer_output();
calldata_t cd = { 0 };
proc_handler_t* ph = obs_output_get_proc_handler(replayOutput);
proc_handler_call(ph, "save", &cd);
calldata_free(&cd);
req->SendOKResponse();
}

View File

@ -0,0 +1,71 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Change the active scene collection.
*
* @param {String} `sc-name` Name of the desired scene collection.
*
* @api requests
* @name SetCurrentSceneCollection
* @category scene collections
* @since 4.0.0
*/
void WSRequestHandler::HandleSetCurrentSceneCollection(WSRequestHandler* req) {
if (!req->hasField("sc-name")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString sceneCollection = obs_data_get_string(req->data, "sc-name");
if (!sceneCollection.isEmpty()) {
// TODO : Check if specified profile exists and if changing is allowed
obs_frontend_set_current_scene_collection(sceneCollection.toUtf8());
req->SendOKResponse();
} else {
req->SendErrorResponse("invalid request parameters");
}
}
/**
* Get the name of the current scene collection.
*
* @return {String} `sc-name` Name of the currently active scene collection.
*
* @api requests
* @name GetCurrentSceneCollection
* @category scene collections
* @since 4.0.0
*/
void WSRequestHandler::HandleGetCurrentSceneCollection(WSRequestHandler* req) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "sc-name",
obs_frontend_get_current_scene_collection());
req->SendOKResponse(response);
}
/**
* List available scene collections
*
* @return {Object|Array} `scene-collections` Scene collections list
* @return {String} `scene-collections.*.`
*
* @api requests
* @name ListSceneCollections
* @category scene collections
* @since 4.0.0
*/
void WSRequestHandler::HandleListSceneCollections(WSRequestHandler* req) {
char** sceneCollections = obs_frontend_get_scene_collections();
OBSDataArrayAutoRelease list =
Utils::StringListToArray(sceneCollections, "sc-name");
bfree(sceneCollections);
OBSDataAutoRelease response = obs_data_create();
obs_data_set_array(response, "scene-collections", list);
req->SendOKResponse(response);
}

View File

@ -0,0 +1,568 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Gets the scene specific properties of the specified source item.
*
* @param {String (optional)} `scene-name` the name of the scene that the source item belongs to. Defaults to the current scene.
* @param {String} `item` The name of the source.
*
* @return {String} `name` The name of the source.
* @return {int} `position.x` The x position of the source from the left.
* @return {int} `position.y` The y position of the source from the top.
* @return {int} `position.alignment` The point on the source that the item is manipulated from.
* @return {double} `rotation` The clockwise rotation of the item in degrees around the point of alignment.
* @return {double} `scale.x` The x-scale factor of the source.
* @return {double} `scale.y` The y-scale factor of the source.
* @return {int} `crop.top` The number of pixels cropped off the top of the source before scaling.
* @return {int} `crop.right` The number of pixels cropped off the right of the source before scaling.
* @return {int} `crop.bottom` The number of pixels cropped off the bottom of the source before scaling.
* @return {int} `crop.left` The number of pixels cropped off the left of the source before scaling.
* @return {bool} `visible` If the source is visible.
* @return {String} `bounds.type` Type of bounding box.
* @return {int} `bounds.alignment` Alignment of the bounding box.
* @return {double} `bounds.x` Width of the bounding box.
* @return {double} `bounds.y` Height of the bounding box.
*
* @api requests
* @name GetSceneItemProperties
* @category scene items
* @since 4.3.0
*/
void WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler* req) {
if (!req->hasField("item")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString itemName = obs_data_get_string(req->data, "item");
if (itemName.isEmpty()) {
req->SendErrorResponse("invalid request parameters");
return;
}
QString sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene doesn't exist");
return;
}
OBSSceneItemAutoRelease sceneItem =
Utils::GetSceneItemFromName(scene, itemName);
if (!sceneItem) {
req->SendErrorResponse("specified scene item doesn't exist");
return;
}
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "name", itemName.toUtf8());
OBSDataAutoRelease posData = obs_data_create();
vec2 pos;
obs_sceneitem_get_pos(sceneItem, &pos);
obs_data_set_double(posData, "x", pos.x);
obs_data_set_double(posData, "y", pos.y);
obs_data_set_int(posData, "alignment", obs_sceneitem_get_alignment(sceneItem));
obs_data_set_obj(data, "position", posData);
obs_data_set_double(data, "rotation", obs_sceneitem_get_rot(sceneItem));
OBSDataAutoRelease scaleData = obs_data_create();
vec2 scale;
obs_sceneitem_get_scale(sceneItem, &scale);
obs_data_set_double(scaleData, "x", scale.x);
obs_data_set_double(scaleData, "y", scale.y);
obs_data_set_obj(data, "scale", scaleData);
OBSDataAutoRelease cropData = obs_data_create();
obs_sceneitem_crop crop;
obs_sceneitem_get_crop(sceneItem, &crop);
obs_data_set_int(cropData, "left", crop.left);
obs_data_set_int(cropData, "top", crop.top);
obs_data_set_int(cropData, "right", crop.right);
obs_data_set_int(cropData, "bottom", crop.bottom);
obs_data_set_obj(data, "crop", cropData);
obs_data_set_bool(data, "visible", obs_sceneitem_visible(sceneItem));
OBSDataAutoRelease boundsData = obs_data_create();
obs_bounds_type boundsType = obs_sceneitem_get_bounds_type(sceneItem);
if (boundsType == OBS_BOUNDS_NONE) {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_NONE");
}
else {
switch (boundsType) {
case OBS_BOUNDS_STRETCH: {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_STRETCH");
break;
}
case OBS_BOUNDS_SCALE_INNER: {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_INNER");
break;
}
case OBS_BOUNDS_SCALE_OUTER: {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_OUTER");
break;
}
case OBS_BOUNDS_SCALE_TO_WIDTH: {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_TO_WIDTH");
break;
}
case OBS_BOUNDS_SCALE_TO_HEIGHT: {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_SCALE_TO_HEIGHT");
break;
}
case OBS_BOUNDS_MAX_ONLY: {
obs_data_set_string(boundsData, "type", "OBS_BOUNDS_MAX_ONLY");
break;
}
}
obs_data_set_int(boundsData, "alignment", obs_sceneitem_get_bounds_alignment(sceneItem));
vec2 bounds;
obs_sceneitem_get_bounds(sceneItem, &bounds);
obs_data_set_double(boundsData, "x", bounds.x);
obs_data_set_double(boundsData, "y", bounds.y);
}
obs_data_set_obj(data, "bounds", boundsData);
req->SendOKResponse(data);
}
/**
* Sets the scene specific properties of a source. Unspecified properties will remain unchanged.
*
* @param {String (optional)} `scene-name` the name of the scene that the source item belongs to. Defaults to the current scene.
* @param {String} `item` The name of the source.
* @param {int} `position.x` The new x position of the source.
* @param {int} `position.y` The new y position of the source.
* @param {int} `position.alignment` The new alignment of the source.
* @param {double} `rotation` The new clockwise rotation of the item in degrees.
* @param {double} `scale.x` The new x scale of the item.
* @param {double} `scale.y` The new y scale of the item.
* @param {int} `crop.top` The new amount of pixels cropped off the top of the source before scaling.
* @param {int} `crop.bottom` The new amount of pixels cropped off the bottom of the source before scaling.
* @param {int} `crop.left` The new amount of pixels cropped off the left of the source before scaling.
* @param {int} `crop.right` The new amount of pixels cropped off the right of the source before scaling.
* @param {bool} `visible` The new visibility of the source. 'true' shows source, 'false' hides source.
* @param {String} `bounds.type` The new bounds type of the source.
* @param {int} `bounds.alignment` The new alignment of the bounding box. (0-2, 4-6, 8-10)
* @param {double} `bounds.x` The new width of the bounding box.
* @param {double} `bounds.y` The new height of the bounding box.
*
* @api requests
* @name SetSceneItemProperties
* @category scene items
* @since 4.3.0
*/
void WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* req) {
if (!req->hasField("item")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString itemName = obs_data_get_string(req->data, "item");
if (itemName.isEmpty()) {
req->SendErrorResponse("invalid request parameters");
return;
}
QString sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene doesn't exist");
return;
}
OBSSceneItemAutoRelease sceneItem =
Utils::GetSceneItemFromName(scene, itemName);
if (!sceneItem) {
req->SendErrorResponse("specified scene item doesn't exist");
return;
}
bool badRequest = false;
OBSDataAutoRelease errorMessage = obs_data_create();
if (req->hasField("position")) {
vec2 oldPosition;
OBSDataAutoRelease positionError = obs_data_create();
obs_sceneitem_get_pos(sceneItem, &oldPosition);
OBSDataAutoRelease reqPosition = obs_data_get_obj(req->data, "position");
vec2 newPosition = oldPosition;
if (obs_data_has_user_value(reqPosition, "x")) {
newPosition.x = obs_data_get_int(reqPosition, "x");
}
if (obs_data_has_user_value(reqPosition, "y")) {
newPosition.y = obs_data_get_int(reqPosition, "y");
}
if (obs_data_has_user_value(reqPosition, "alignment")) {
const uint32_t alignment = obs_data_get_int(reqPosition, "alignment");
if (Utils::IsValidAlignment(alignment)) {
obs_sceneitem_set_alignment(sceneItem, alignment);
}
else {
badRequest = true;
obs_data_set_string(positionError, "alignment", "invalid");
obs_data_set_obj(errorMessage, "position", positionError);
}
}
obs_sceneitem_set_pos(sceneItem, &newPosition);
}
if (req->hasField("rotation")) {
obs_sceneitem_set_rot(sceneItem, (float)obs_data_get_double(req->data, "rotation"));
}
if (req->hasField("scale")) {
vec2 oldScale;
obs_sceneitem_get_scale(sceneItem, &oldScale);
OBSDataAutoRelease reqScale = obs_data_get_obj(req->data, "scale");
vec2 newScale = oldScale;
if (obs_data_has_user_value(reqScale, "x")) {
newScale.x = obs_data_get_double(reqScale, "x");
}
if (obs_data_has_user_value(reqScale, "y")) {
newScale.y = obs_data_get_double(reqScale, "y");
}
obs_sceneitem_set_scale(sceneItem, &newScale);
}
if (req->hasField("crop")) {
obs_sceneitem_crop oldCrop;
obs_sceneitem_get_crop(sceneItem, &oldCrop);
OBSDataAutoRelease reqCrop = obs_data_get_obj(req->data, "crop");
obs_sceneitem_crop newCrop = oldCrop;
if (obs_data_has_user_value(reqCrop, "top")) {
newCrop.top = obs_data_get_int(reqCrop, "top");
}
if (obs_data_has_user_value(reqCrop, "right")) {
newCrop.right = obs_data_get_int(reqCrop, "right");
}
if (obs_data_has_user_value(reqCrop, "bottom")) {
newCrop.bottom = obs_data_get_int(reqCrop, "bottom");
}
if (obs_data_has_user_value(reqCrop, "left")) {
newCrop.left = obs_data_get_int(reqCrop, "left");
}
obs_sceneitem_set_crop(sceneItem, &newCrop);
}
if (req->hasField("visible")) {
obs_sceneitem_set_visible(sceneItem, obs_data_get_bool(req->data, "visible"));
}
if (req->hasField("bounds")) {
bool badBounds = false;
OBSDataAutoRelease boundsError = obs_data_create();
OBSDataAutoRelease reqBounds = obs_data_get_obj(req->data, "bounds");
if (obs_data_has_user_value(reqBounds, "type")) {
const char* newBoundsType = obs_data_get_string(reqBounds, "type");
if (newBoundsType == "OBS_BOUNDS_NONE") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_NONE);
}
else if (newBoundsType == "OBS_BOUNDS_STRETCH") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_STRETCH);
}
else if (newBoundsType == "OBS_BOUNDS_SCALE_INNER") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_SCALE_INNER);
}
else if (newBoundsType == "OBS_BOUNDS_SCALE_OUTER") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_SCALE_OUTER);
}
else if (newBoundsType == "OBS_BOUNDS_SCALE_TO_WIDTH") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_SCALE_TO_WIDTH);
}
else if (newBoundsType == "OBS_BOUNDS_SCALE_TO_HEIGHT") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_SCALE_TO_HEIGHT);
}
else if (newBoundsType == "OBS_BOUNDS_MAX_ONLY") {
obs_sceneitem_set_bounds_type(sceneItem, OBS_BOUNDS_MAX_ONLY);
}
else {
badRequest = badBounds = true;
obs_data_set_string(boundsError, "type", "invalid");
}
}
vec2 oldBounds;
obs_sceneitem_get_bounds(sceneItem, &oldBounds);
vec2 newBounds = oldBounds;
if (obs_data_has_user_value(reqBounds, "x")) {
newBounds.x = obs_data_get_double(reqBounds, "x");
}
if (obs_data_has_user_value(reqBounds, "y")) {
newBounds.y = obs_data_get_double(reqBounds, "y");
}
obs_sceneitem_set_bounds(sceneItem, &newBounds);
if (obs_data_has_user_value(reqBounds, "alignment")) {
const uint32_t bounds_alignment = obs_data_get_int(reqBounds, "alignment");
if (Utils::IsValidAlignment(bounds_alignment)) {
obs_sceneitem_set_bounds_alignment(sceneItem, bounds_alignment);
}
else {
badRequest = badBounds = true;
obs_data_set_string(boundsError, "alignment", "invalid");
}
}
if (badBounds) {
obs_data_set_obj(errorMessage, "bounds", boundsError);
}
}
if (badRequest) {
req->SendErrorResponse(errorMessage);
}
else {
req->SendOKResponse();
}
}
/**
* Reset a scene item.
*
* @param {String (optional)} `scene-name` Name of the scene the source belogns to. Defaults to the current scene.
* @param {String} `item` Name of the source item.
*
* @api requests
* @name ResetSceneItem
* @category scene items
* @since 4.2.0
*/
void WSRequestHandler::HandleResetSceneItem(WSRequestHandler* req) {
// TODO: remove this request, or refactor it to ResetSource
if (!req->hasField("item")) {
req->SendErrorResponse("missing request parameters");
return;
}
const char* itemName = obs_data_get_string(req->data, "item");
if (!itemName) {
req->SendErrorResponse("invalid request parameters");
return;
}
const char* sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene doesn't exist");
return;
}
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName);
if (sceneItem) {
OBSSource sceneItemSource = obs_sceneitem_get_source(sceneItem);
OBSDataAutoRelease settings = obs_source_get_settings(sceneItemSource);
obs_source_update(sceneItemSource, settings);
req->SendOKResponse();
}
else {
req->SendErrorResponse("specified scene item doesn't exist");
}
}
/**
* Show or hide a specified source item in a specified scene.
*
* @param {String} `source` Scene item name in the specified scene.
* @param {boolean} `render` true = shown ; false = hidden
* @param {String (optional)} `scene-name` Name of the scene where the source resides. Defaults to the currently active scene.
*
* @api requests
* @name SetSceneItemRender
* @category scene items
* @since 0.3
* @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties.
*/
void WSRequestHandler::HandleSetSceneItemRender(WSRequestHandler* req) {
if (!req->hasField("source") ||
!req->hasField("render"))
{
req->SendErrorResponse("missing request parameters");
return;
}
const char* itemName = obs_data_get_string(req->data, "source");
bool isVisible = obs_data_get_bool(req->data, "render");
if (!itemName) {
req->SendErrorResponse("invalid request parameters");
return;
}
const char* sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene doesn't exist");
return;
}
OBSSceneItemAutoRelease sceneItem =
Utils::GetSceneItemFromName(scene, itemName);
if (sceneItem) {
obs_sceneitem_set_visible(sceneItem, isVisible);
req->SendOKResponse();
}
else {
req->SendErrorResponse("specified scene item doesn't exist");
}
}
/**
* Sets the coordinates of a specified source item.
*
* @param {String (optional)} `scene-name` The name of the scene that the source item belongs to. Defaults to the current scene.
* @param {String} `item` The name of the source item.
* @param {double} `x` X coordinate.
* @param {double} `y` Y coordinate.
*
* @api requests
* @name SetSceneItemPosition
* @category scene items
* @since 4.0.0
* @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties.
*/
void WSRequestHandler::HandleSetSceneItemPosition(WSRequestHandler* req) {
if (!req->hasField("item") ||
!req->hasField("x") || !req->hasField("y")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString itemName = obs_data_get_string(req->data, "item");
if (itemName.isEmpty()) {
req->SendErrorResponse("invalid request parameters");
return;
}
QString sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene could not be found");
return;
}
OBSSceneItem sceneItem = Utils::GetSceneItemFromName(scene, itemName);
if (sceneItem) {
vec2 item_position = { 0 };
item_position.x = obs_data_get_double(req->data, "x");
item_position.y = obs_data_get_double(req->data, "y");
obs_sceneitem_set_pos(sceneItem, &item_position);
req->SendOKResponse();
}
else {
req->SendErrorResponse("specified scene item doesn't exist");
}
}
/**
* Set the transform of the specified source item.
*
* @param {String (optional)} `scene-name` The name of the scene that the source item belongs to. Defaults to the current scene.
* @param {String} `item` The name of the source item.
* @param {double} `x-scale` Width scale factor.
* @param {double} `y-scale` Height scale factor.
* @param {double} `rotation` Source item rotation (in degrees).
*
* @api requests
* @name SetSceneItemTransform
* @category scene items
* @since 4.0.0
* @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties.
*/
void WSRequestHandler::HandleSetSceneItemTransform(WSRequestHandler* req) {
if (!req->hasField("item") ||
!req->hasField("x-scale") ||
!req->hasField("y-scale") ||
!req->hasField("rotation"))
{
req->SendErrorResponse("missing request parameters");
return;
}
QString itemName = obs_data_get_string(req->data, "item");
if (itemName.isEmpty()) {
req->SendErrorResponse("invalid request parameters");
return;
}
QString sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene doesn't exist");
return;
}
vec2 scale;
scale.x = obs_data_get_double(req->data, "x-scale");
scale.y = obs_data_get_double(req->data, "y-scale");
float rotation = obs_data_get_double(req->data, "rotation");
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName);
if (sceneItem) {
obs_sceneitem_set_scale(sceneItem, &scale);
obs_sceneitem_set_rot(sceneItem, rotation);
req->SendOKResponse();
}
else {
req->SendErrorResponse("specified scene item doesn't exist");
}
}
/**
* Sets the crop coordinates of the specified source item.
*
* @param {String (optional)} `scene-name` the name of the scene that the source item belongs to. Defaults to the current scene.
* @param {String} `item` The name of the source.
* @param {int} `top` Pixel position of the top of the source item.
* @param {int} `bottom` Pixel position of the bottom of the source item.
* @param {int} `left` Pixel position of the left of the source item.
* @param {int} `right` Pixel position of the right of the source item.
*
* @api requests
* @name SetSceneItemCrop
* @category scene items
* @since 4.1.0
* @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties.
*/
void WSRequestHandler::HandleSetSceneItemCrop(WSRequestHandler* req) {
if (!req->hasField("item")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString itemName = obs_data_get_string(req->data, "item");
if (itemName.isEmpty()) {
req->SendErrorResponse("invalid request parameters");
return;
}
QString sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName);
if (!scene) {
req->SendErrorResponse("requested scene doesn't exist");
return;
}
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName);
if (sceneItem) {
struct obs_sceneitem_crop crop = { 0 };
crop.top = obs_data_get_int(req->data, "top");
crop.bottom = obs_data_get_int(req->data, "bottom");
crop.left = obs_data_get_int(req->data, "left");
crop.right = obs_data_get_int(req->data, "right");
obs_sceneitem_set_crop(sceneItem, &crop);
req->SendOKResponse();
}
else {
req->SendErrorResponse("specified scene item doesn't exist");
}
}

View File

@ -0,0 +1,76 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Switch to the specified scene.
*
* @param {String} `scene-name` Name of the scene to switch to.
*
* @api requests
* @name SetCurrentScene
* @category scenes
* @since 0.3
*/
void WSRequestHandler::HandleSetCurrentScene(WSRequestHandler* req) {
if (!req->hasField("scene-name")) {
req->SendErrorResponse("missing request parameters");
return;
}
const char* sceneName = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease source = obs_get_source_by_name(sceneName);
if (source) {
obs_frontend_set_current_scene(source);
req->SendOKResponse();
} else {
req->SendErrorResponse("requested scene does not exist");
}
}
/**
* Get the current scene's name and source items.
*
* @return {String} `name` Name of the currently active scene.
* @return {Source|Array} `sources` Ordered list of the current scene's source items.
*
* @api requests
* @name GetCurrentScene
* @category scenes
* @since 0.3
*/
void WSRequestHandler::HandleGetCurrentScene(WSRequestHandler* req) {
OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene();
OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(currentScene);
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "name", obs_source_get_name(currentScene));
obs_data_set_array(data, "sources", sceneItems);
req->SendOKResponse(data);
}
/**
* Get a list of scenes in the currently active profile.
*
* @return {String} `current-scene` Name of the currently active scene.
* @return {Scene|Array} `scenes` Ordered list of the current profile's scenes (See `[GetCurrentScene](#getcurrentscene)` for more information).
*
* @api requests
* @name GetSceneList
* @category scenes
* @since 0.3
*/
void WSRequestHandler::HandleGetSceneList(WSRequestHandler* req) {
OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene();
OBSDataArrayAutoRelease scenes = Utils::GetScenes();
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "current-scene",
obs_source_get_name(currentScene));
obs_data_set_array(data, "scenes", scenes);
req->SendOKResponse(data);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,288 @@
#include <QString>
#include "Utils.h"
#include "WSEvents.h"
#include "WSRequestHandler.h"
#define STREAM_SERVICE_ID "websocket_custom_service"
/**
* Get current streaming and recording status.
*
* @return {boolean} `streaming` Current streaming status.
* @return {boolean} `recording` Current recording status.
* @return {String (optional)} `stream-timecode` Time elapsed since streaming started (only present if currently streaming).
* @return {String (optional)} `rec-timecode` Time elapsed since recording started (only present if currently recording).
* @return {boolean} `preview-only` Always false. Retrocompatibility with OBSRemote.
*
* @api requests
* @name GetStreamingStatus
* @category streaming
* @since 0.3
*/
void WSRequestHandler::HandleGetStreamingStatus(WSRequestHandler* req) {
OBSDataAutoRelease data = obs_data_create();
obs_data_set_bool(data, "streaming", obs_frontend_streaming_active());
obs_data_set_bool(data, "recording", obs_frontend_recording_active());
obs_data_set_bool(data, "preview-only", false);
const char* tc = nullptr;
if (obs_frontend_streaming_active()) {
tc = WSEvents::Instance->GetStreamingTimecode();
obs_data_set_string(data, "stream-timecode", tc);
bfree((void*)tc);
}
if (obs_frontend_recording_active()) {
tc = WSEvents::Instance->GetRecordingTimecode();
obs_data_set_string(data, "rec-timecode", tc);
bfree((void*)tc);
}
req->SendOKResponse(data);
}
/**
* Toggle streaming on or off.
*
* @api requests
* @name StartStopStreaming
* @category streaming
* @since 0.3
*/
void WSRequestHandler::HandleStartStopStreaming(WSRequestHandler* req) {
if (obs_frontend_streaming_active())
HandleStopStreaming(req);
else
HandleStartStreaming(req);
}
/**
* Start streaming.
* Will return an `error` if streaming is already active.
*
* @param {Object (optional)} `stream` Special stream configuration. Please note: these won't be saved to OBS' configuration.
* @param {String (optional)} `stream.type` If specified ensures the type of stream matches the given type (usually 'rtmp_custom' or 'rtmp_common'). If the currently configured stream type does not match the given stream type, all settings must be specified in the `settings` object or an error will occur when starting the stream.
* @param {Object (optional)} `stream.metadata` Adds the given object parameters as encoded query string parameters to the 'key' of the RTMP stream. Used to pass data to the RTMP service about the streaming. May be any String, Numeric, or Boolean field.
* @param {Object (optional)} `stream.settings` Settings for the stream.
* @param {String (optional)} `stream.settings.server` The publish URL.
* @param {String (optional)} `stream.settings.key` The publish key of the stream.
* @param {boolean (optional)} `stream.settings.use-auth` Indicates whether authentication should be used when connecting to the streaming server.
* @param {String (optional)} `stream.settings.username` If authentication is enabled, the username for the streaming server. Ignored if `use-auth` is not set to `true`.
* @param {String (optional)} `stream.settings.password` If authentication is enabled, the password for the streaming server. Ignored if `use-auth` is not set to `true`.
*
* @api requests
* @name StartStreaming
* @category streaming
* @since 4.1.0
*/
void WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) {
if (obs_frontend_streaming_active() == false) {
OBSService configuredService = obs_frontend_get_streaming_service();
OBSService newService = nullptr;
// TODO: fix service memory leak
if (req->hasField("stream")) {
OBSDataAutoRelease streamData = obs_data_get_obj(req->data, "stream");
OBSDataAutoRelease newSettings = obs_data_get_obj(streamData, "settings");
OBSDataAutoRelease newMetadata = obs_data_get_obj(streamData, "metadata");
OBSDataAutoRelease csHotkeys =
obs_hotkeys_save_service(configuredService);
QString currentType = obs_service_get_type(configuredService);
QString newType = obs_data_get_string(streamData, "type");
if (newType.isEmpty() || newType.isNull()) {
newType = currentType;
}
//Supporting adding metadata parameters to key query string
QString query = Utils::ParseDataToQueryString(newMetadata);
if (!query.isEmpty()
&& obs_data_has_user_value(newSettings, "key"))
{
const char* key = obs_data_get_string(newSettings, "key");
int keylen = strlen(key);
bool hasQuestionMark = false;
for (int i = 0; i < keylen; i++) {
if (key[i] == '?') {
hasQuestionMark = true;
break;
}
}
if (hasQuestionMark) {
query.prepend('&');
} else {
query.prepend('?');
}
query.prepend(key);
obs_data_set_string(newSettings, "key", query.toUtf8());
}
if (newType == currentType) {
// Service type doesn't change: apply settings to current service
// By doing this, you can send a request to the websocket
// that only contains settings you want to change, instead of
// having to do a get and then change them
OBSDataAutoRelease currentSettings = obs_service_get_settings(configuredService);
OBSDataAutoRelease updatedSettings = obs_data_create();
obs_data_apply(updatedSettings, currentSettings); //first apply the existing settings
obs_data_apply(updatedSettings, newSettings); //then apply the settings from the request should they exist
newService = obs_service_create(
newType.toUtf8(), STREAM_SERVICE_ID,
updatedSettings, csHotkeys);
}
else {
// Service type changed: override service settings
newService = obs_service_create(
newType.toUtf8(), STREAM_SERVICE_ID,
newSettings, csHotkeys);
}
obs_frontend_set_streaming_service(newService);
}
obs_frontend_streaming_start();
// Stream settings provided in StartStreaming are not persisted to disk
if (newService != nullptr) {
obs_frontend_set_streaming_service(configuredService);
}
req->SendOKResponse();
} else {
req->SendErrorResponse("streaming already active");
}
}
/**
* Stop streaming.
* Will return an `error` if streaming is not active.
*
* @api requests
* @name StopStreaming
* @category streaming
* @since 4.1.0
*/
void WSRequestHandler::HandleStopStreaming(WSRequestHandler* req) {
if (obs_frontend_streaming_active() == true) {
obs_frontend_streaming_stop();
req->SendOKResponse();
} else {
req->SendErrorResponse("streaming not active");
}
}
/**
* Sets one or more attributes of the current streaming server settings. Any options not passed will remain unchanged. Returns the updated settings in response. If 'type' is different than the current streaming service type, all settings are required. Returns the full settings of the stream (the same as GetStreamSettings).
*
* @param {String} `type` The type of streaming service configuration, usually `rtmp_custom` or `rtmp_common`.
* @param {Object} `settings` The actual settings of the stream.
* @param {String (optional)} `settings.server` The publish URL.
* @param {String (optional)} `settings.key` The publish key.
* @param {boolean (optional)} `settings.use-auth` Indicates whether authentication should be used when connecting to the streaming server.
* @param {String (optional)} `settings.username` The username for the streaming service.
* @param {String (optional)} `settings.password` The password for the streaming service.
* @param {boolean} `save` Persist the settings to disk.
*
* @api requests
* @name SetStreamSettings
* @category streaming
* @since 4.1.0
*/
void WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) {
OBSService service = obs_frontend_get_streaming_service();
OBSDataAutoRelease requestSettings = obs_data_get_obj(req->data, "settings");
if (!requestSettings) {
req->SendErrorResponse("'settings' are required'");
return;
}
QString serviceType = obs_service_get_type(service);
QString requestedType = obs_data_get_string(req->data, "type");
if (requestedType != nullptr && requestedType != serviceType) {
OBSDataAutoRelease hotkeys = obs_hotkeys_save_service(service);
service = obs_service_create(
requestedType.toUtf8(), STREAM_SERVICE_ID, requestSettings, hotkeys);
} else {
// If type isn't changing, we should overlay the settings we got
// to the existing settings. By doing so, you can send a request that
// only contains the settings you want to change, instead of having to
// do a get and then change them
OBSDataAutoRelease existingSettings = obs_service_get_settings(service);
OBSDataAutoRelease newSettings = obs_data_create();
// Apply existing settings
obs_data_apply(newSettings, existingSettings);
// Then apply the settings from the request
obs_data_apply(newSettings, requestSettings);
obs_service_update(service, newSettings);
}
//if save is specified we should immediately save the streaming service
if (obs_data_get_bool(req->data, "save")) {
obs_frontend_save_streaming_service();
}
OBSDataAutoRelease serviceSettings = obs_service_get_settings(service);
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "type", requestedType.toUtf8());
obs_data_set_obj(response, "settings", serviceSettings);
req->SendOKResponse(response);
}
/**
* Get the current streaming server settings.
*
* @return {String} `type` The type of streaming service configuration. Possible values: 'rtmp_custom' or 'rtmp_common'.
* @return {Object} `settings` Stream settings object.
* @return {String} `settings.server` The publish URL.
* @return {String} `settings.key` The publish key of the stream.
* @return {boolean} `settings.use-auth` Indicates whether audentication should be used when connecting to the streaming server.
* @return {String} `settings.username` The username to use when accessing the streaming server. Only present if `use-auth` is `true`.
* @return {String} `settings.password` The password to use when accessing the streaming server. Only present if `use-auth` is `true`.
*
* @api requests
* @name GetStreamSettings
* @category streaming
* @since 4.1.0
*/
void WSRequestHandler::HandleGetStreamSettings(WSRequestHandler* req) {
OBSService service = obs_frontend_get_streaming_service();
const char* serviceType = obs_service_get_type(service);
OBSDataAutoRelease settings = obs_service_get_settings(service);
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "type", serviceType);
obs_data_set_obj(response, "settings", settings);
req->SendOKResponse(response);
}
/**
* Save the current streaming server settings to disk.
*
* @api requests
* @name SaveStreamSettings
* @category streaming
* @since 4.1.0
*/
void WSRequestHandler::HandleSaveStreamSettings(WSRequestHandler* req) {
obs_frontend_save_streaming_service();
req->SendOKResponse();
}

View File

@ -0,0 +1,173 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* Indicates if Studio Mode is currently enabled.
*
* @return {boolean} `studio-mode` Indicates if Studio Mode is enabled.
*
* @api requests
* @name GetStudioModeStatus
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleGetStudioModeStatus(WSRequestHandler* req) {
bool previewActive = obs_frontend_preview_program_mode_active();
OBSDataAutoRelease response = obs_data_create();
obs_data_set_bool(response, "studio-mode", previewActive);
req->SendOKResponse(response);
}
/**
* Get the name of the currently previewed scene and its list of sources.
* Will return an `error` if Studio Mode is not enabled.
*
* @return {String} `name` The name of the active preview scene.
* @return {Source|Array} `sources`
*
* @api requests
* @name GetPreviewScene
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleGetPreviewScene(WSRequestHandler* req) {
if (!obs_frontend_preview_program_mode_active()) {
req->SendErrorResponse("studio mode not enabled");
return;
}
OBSSourceAutoRelease scene = obs_frontend_get_current_preview_scene();
OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(scene);
OBSDataAutoRelease data = obs_data_create();
obs_data_set_string(data, "name", obs_source_get_name(scene));
obs_data_set_array(data, "sources", sceneItems);
req->SendOKResponse(data);
}
/**
* Set the active preview scene.
* Will return an `error` if Studio Mode is not enabled.
*
* @param {String} `scene-name` The name of the scene to preview.
*
* @api requests
* @name SetPreviewScene
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleSetPreviewScene(WSRequestHandler* req) {
if (!obs_frontend_preview_program_mode_active()) {
req->SendErrorResponse("studio mode not enabled");
return;
}
if (!req->hasField("scene-name")) {
req->SendErrorResponse("missing request parameters");
return;
}
const char* scene_name = obs_data_get_string(req->data, "scene-name");
OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(scene_name);
if (scene) {
obs_frontend_set_current_preview_scene(scene);
req->SendOKResponse();
} else {
req->SendErrorResponse("specified scene doesn't exist");
}
}
/**
* Transitions the currently previewed scene to the main output.
* Will return an `error` if Studio Mode is not enabled.
*
* @param {Object (optional)} `with-transition` Change the active transition before switching scenes. Defaults to the active transition.
* @param {String} `with-transition.name` Name of the transition.
* @param {int (optional)} `with-transition.duration` Transition duration (in milliseconds).
*
* @api requests
* @name TransitionToProgram
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleTransitionToProgram(WSRequestHandler* req) {
if (!obs_frontend_preview_program_mode_active()) {
req->SendErrorResponse("studio mode not enabled");
return;
}
if (req->hasField("with-transition")) {
OBSDataAutoRelease transitionInfo =
obs_data_get_obj(req->data, "with-transition");
if (obs_data_has_user_value(transitionInfo, "name")) {
QString transitionName =
obs_data_get_string(transitionInfo, "name");
if (transitionName.isEmpty()) {
req->SendErrorResponse("invalid request parameters");
return;
}
bool success = Utils::SetTransitionByName(transitionName);
if (!success) {
req->SendErrorResponse("specified transition doesn't exist");
return;
}
}
if (obs_data_has_user_value(transitionInfo, "duration")) {
int transitionDuration =
obs_data_get_int(transitionInfo, "duration");
Utils::SetTransitionDuration(transitionDuration);
}
}
Utils::TransitionToProgram();
req->SendOKResponse();
}
/**
* Enables Studio Mode.
*
* @api requests
* @name EnableStudioMode
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleEnableStudioMode(WSRequestHandler* req) {
obs_frontend_set_preview_program_mode(true);
req->SendOKResponse();
}
/**
* Disables Studio Mode.
*
* @api requests
* @name DisableStudioMode
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleDisableStudioMode(WSRequestHandler* req) {
obs_frontend_set_preview_program_mode(false);
req->SendOKResponse();
}
/**
* Toggles Studio Mode.
*
* @api requests
* @name ToggleStudioMode
* @category studio mode
* @since 4.1.0
*/
void WSRequestHandler::HandleToggleStudioMode(WSRequestHandler* req) {
bool previewProgramMode = obs_frontend_preview_program_mode_active();
obs_frontend_set_preview_program_mode(!previewProgramMode);
req->SendOKResponse();
}

View File

@ -0,0 +1,126 @@
#include <QString>
#include "Utils.h"
#include "WSRequestHandler.h"
/**
* List of all transitions available in the frontend's dropdown menu.
*
* @return {String} `current-transition` Name of the currently active transition.
* @return {Object|Array} `transitions` List of transitions.
* @return {String} `transitions[].name` Name of the transition.
*
* @api requests
* @name GetTransitionList
* @category transitions
* @since 4.1.0
*/
void WSRequestHandler::HandleGetTransitionList(WSRequestHandler* req) {
OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition();
obs_frontend_source_list transitionList = {};
obs_frontend_get_transitions(&transitionList);
OBSDataArrayAutoRelease transitions = obs_data_array_create();
for (size_t i = 0; i < transitionList.sources.num; i++) {
OBSSource transition = transitionList.sources.array[i];
OBSDataAutoRelease obj = obs_data_create();
obs_data_set_string(obj, "name", obs_source_get_name(transition));
obs_data_array_push_back(transitions, obj);
}
obs_frontend_source_list_free(&transitionList);
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "current-transition",
obs_source_get_name(currentTransition));
obs_data_set_array(response, "transitions", transitions);
req->SendOKResponse(response);
}
/**
* Get the name of the currently selected transition in the frontend's dropdown menu.
*
* @return {String} `name` Name of the selected transition.
* @return {int (optional)} `duration` Transition duration (in milliseconds) if supported by the transition.
*
* @api requests
* @name GetCurrentTransition
* @category transitions
* @since 0.3
*/
void WSRequestHandler::HandleGetCurrentTransition(WSRequestHandler* req) {
OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition();
OBSDataAutoRelease response = obs_data_create();
obs_data_set_string(response, "name",
obs_source_get_name(currentTransition));
if (!obs_transition_fixed(currentTransition))
obs_data_set_int(response, "duration", Utils::GetTransitionDuration());
req->SendOKResponse(response);
}
/**
* Set the active transition.
*
* @param {String} `transition-name` The name of the transition.
*
* @api requests
* @name SetCurrentTransition
* @category transitions
* @since 0.3
*/
void WSRequestHandler::HandleSetCurrentTransition(WSRequestHandler* req) {
if (!req->hasField("transition-name")) {
req->SendErrorResponse("missing request parameters");
return;
}
QString name = obs_data_get_string(req->data, "transition-name");
bool success = Utils::SetTransitionByName(name);
if (success)
req->SendOKResponse();
else
req->SendErrorResponse("requested transition does not exist");
}
/**
* Set the duration of the currently selected transition if supported.
*
* @param {int} `duration` Desired duration of the transition (in milliseconds).
*
* @api requests
* @name SetTransitionDuration
* @category transitions
* @since 4.0.0
*/
void WSRequestHandler::HandleSetTransitionDuration(WSRequestHandler* req) {
if (!req->hasField("duration")) {
req->SendErrorResponse("missing request parameters");
return;
}
int ms = obs_data_get_int(req->data, "duration");
Utils::SetTransitionDuration(ms);
req->SendOKResponse();
}
/**
* Get the duration of the currently selected transition if supported.
*
* @return {int} `transition-duration` Duration of the current transition (in milliseconds).
*
* @api requests
* @name GetTransitionDuration
* @category transitions
* @since 4.1.0
*/
void WSRequestHandler::HandleGetTransitionDuration(WSRequestHandler* req) {
OBSDataAutoRelease response = obs_data_create();
obs_data_set_int(response, "transition-duration",
Utils::GetTransitionDuration());
req->SendOKResponse(response);
}

View File

@ -19,6 +19,8 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <QtWebSockets/QWebSocket> #include <QtWebSockets/QWebSocket>
#include <QtCore/QThread> #include <QtCore/QThread>
#include <QtCore/QByteArray> #include <QtCore/QByteArray>
#include <QMainWindow>
#include <QMessageBox>
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
#include "WSServer.h" #include "WSServer.h"
@ -34,7 +36,8 @@ WSServer::WSServer(QObject* parent)
: QObject(parent), : QObject(parent),
_wsServer(Q_NULLPTR), _wsServer(Q_NULLPTR),
_clients(), _clients(),
_clMutex(QMutex::Recursive) { _clMutex(QMutex::Recursive)
{
_wsServer = new QWebSocketServer( _wsServer = new QWebSocketServer(
QStringLiteral("obs-websocket"), QStringLiteral("obs-websocket"),
QWebSocketServer::NonSecureMode); QWebSocketServer::NonSecureMode);
@ -53,23 +56,42 @@ void WSServer::Start(quint16 port) {
bool serverStarted = _wsServer->listen(QHostAddress::Any, port); bool serverStarted = _wsServer->listen(QHostAddress::Any, port);
if (serverStarted) { if (serverStarted) {
blog(LOG_INFO, "server started successfully on TCP port %d", port);
connect(_wsServer, SIGNAL(newConnection()), connect(_wsServer, SIGNAL(newConnection()),
this, SLOT(onNewConnection())); this, SLOT(onNewConnection()));
} }
else {
QString errorString = _wsServer->errorString();
blog(LOG_ERROR,
"error: failed to start server on TCP port %d: %s",
port, errorString.toUtf8().constData());
QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window();
obs_frontend_push_ui_translation(obs_module_get_string);
QString title = tr("OBSWebsocket.Server.StartFailed.Title");
QString msg = tr("OBSWebsocket.Server.StartFailed.Message").arg(port);
obs_frontend_pop_ui_translation();
QMessageBox::warning(mainWindow, title, msg);
}
} }
void WSServer::Stop() { void WSServer::Stop() {
_clMutex.lock(); QMutexLocker locker(&_clMutex);
for(QWebSocket* pClient : _clients) { for(QWebSocket* pClient : _clients) {
pClient->close(); pClient->close();
} }
_clMutex.unlock(); locker.unlock();
_wsServer->close(); _wsServer->close();
blog(LOG_INFO, "server stopped successfully");
} }
void WSServer::broadcast(QString message) { void WSServer::broadcast(QString message) {
_clMutex.lock(); QMutexLocker locker(&_clMutex);
for(QWebSocket* pClient : _clients) { for(QWebSocket* pClient : _clients) {
if (Config::Current()->AuthRequired if (Config::Current()->AuthRequired
&& (pClient->property(PROP_AUTHENTICATED).toBool() == false)) { && (pClient->property(PROP_AUTHENTICATED).toBool() == false)) {
@ -78,7 +100,6 @@ void WSServer::broadcast(QString message) {
} }
pClient->sendTextMessage(message); pClient->sendTextMessage(message);
} }
_clMutex.unlock();
} }
void WSServer::onNewConnection() { void WSServer::onNewConnection() {
@ -91,9 +112,9 @@ void WSServer::onNewConnection() {
pSocket->setProperty(PROP_AUTHENTICATED, false); pSocket->setProperty(PROP_AUTHENTICATED, false);
_clMutex.lock(); QMutexLocker locker(&_clMutex);
_clients << pSocket; _clients << pSocket;
_clMutex.unlock(); locker.unlock();
QHostAddress clientAddr = pSocket->peerAddress(); QHostAddress clientAddr = pSocket->peerAddress();
QString clientIp = Utils::FormatIPAddress(clientAddr); QString clientIp = Utils::FormatIPAddress(clientAddr);
@ -101,13 +122,13 @@ void WSServer::onNewConnection() {
blog(LOG_INFO, "new client connection from %s:%d", blog(LOG_INFO, "new client connection from %s:%d",
clientIp.toUtf8().constData(), pSocket->peerPort()); clientIp.toUtf8().constData(), pSocket->peerPort());
QString msg = QString(obs_module_text("OBSWebsocket.ConnectNotify.ClientIP")) obs_frontend_push_ui_translation(obs_module_get_string);
+ QString(" ") QString title = tr("OBSWebsocket.NotifyConnect.Title");
+ Utils::FormatIPAddress(clientAddr); QString msg = tr("OBSWebsocket.NotifyConnect.Message")
.arg(Utils::FormatIPAddress(clientAddr));
obs_frontend_pop_ui_translation();
Utils::SysTrayNotify(msg, Utils::SysTrayNotify(msg, QSystemTrayIcon::Information, title);
QSystemTrayIcon::Information,
QString(obs_module_text("OBSWebsocket.ConnectNotify.Connected")));
} }
} }
@ -124,9 +145,9 @@ void WSServer::onSocketDisconnected() {
if (pSocket) { if (pSocket) {
pSocket->setProperty(PROP_AUTHENTICATED, false); pSocket->setProperty(PROP_AUTHENTICATED, false);
_clMutex.lock(); QMutexLocker locker(&_clMutex);
_clients.removeAll(pSocket); _clients.removeAll(pSocket);
_clMutex.unlock(); locker.unlock();
pSocket->deleteLater(); pSocket->deleteLater();
@ -136,12 +157,12 @@ void WSServer::onSocketDisconnected() {
blog(LOG_INFO, "client %s:%d disconnected", blog(LOG_INFO, "client %s:%d disconnected",
clientIp.toUtf8().constData(), pSocket->peerPort()); clientIp.toUtf8().constData(), pSocket->peerPort());
QString msg = QString(obs_module_text("OBSWebsocket.ConnectNotify.ClientIP")) obs_frontend_push_ui_translation(obs_module_get_string);
+ QString(" ") QString title = tr("OBSWebsocket.NotifyDisconnect.Title");
+ Utils::FormatIPAddress(clientAddr); QString msg = tr("OBSWebsocket.NotifyDisconnect.Message")
.arg(Utils::FormatIPAddress(clientAddr));
obs_frontend_pop_ui_translation();
Utils::SysTrayNotify(msg, Utils::SysTrayNotify(msg, QSystemTrayIcon::Information, title);
QSystemTrayIcon::Information,
QString(obs_module_text("OBSWebsocket.ConnectNotify.Disconnected")));
} }
} }

View File

@ -18,11 +18,10 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <obs-frontend-api.h> #include <obs-frontend-api.h>
#include "obs-websocket.h" #include "../obs-websocket.h"
#include "Config.h" #include "../Config.h"
#include "WSServer.h" #include "../WSServer.h"
#include "settings-dialog.h" #include "settings-dialog.h"
#include "ui_settings-dialog.h"
#define CHANGE_ME "changeme" #define CHANGE_ME "changeme"
@ -41,55 +40,48 @@ SettingsDialog::SettingsDialog(QWidget* parent) :
AuthCheckboxChanged(); AuthCheckboxChanged();
} }
void SettingsDialog::showEvent(QShowEvent* event) void SettingsDialog::showEvent(QShowEvent* event) {
{
Config* conf = Config::Current(); Config* conf = Config::Current();
ui->serverEnabled->setChecked(conf->ServerEnabled); ui->serverEnabled->setChecked(conf->ServerEnabled);
ui->serverPort->setValue(conf->ServerPort); ui->serverPort->setValue(conf->ServerPort);
ui->debugEnabled->setChecked(conf->DebugEnabled); ui->debugEnabled->setChecked(conf->DebugEnabled);
ui->alertsEnabled->setChecked(conf->AlertsEnabled);
ui->authRequired->setChecked(conf->AuthRequired); ui->authRequired->setChecked(conf->AuthRequired);
ui->password->setText(CHANGE_ME); ui->password->setText(CHANGE_ME);
} }
void SettingsDialog::ToggleShowHide() void SettingsDialog::ToggleShowHide() {
{
if (!isVisible()) if (!isVisible())
setVisible(true); setVisible(true);
else else
setVisible(false); setVisible(false);
} }
void SettingsDialog::AuthCheckboxChanged() void SettingsDialog::AuthCheckboxChanged() {
{
if (ui->authRequired->isChecked()) if (ui->authRequired->isChecked())
ui->password->setEnabled(true); ui->password->setEnabled(true);
else else
ui->password->setEnabled(false); ui->password->setEnabled(false);
} }
void SettingsDialog::FormAccepted() void SettingsDialog::FormAccepted() {
{
Config* conf = Config::Current(); Config* conf = Config::Current();
conf->ServerEnabled = ui->serverEnabled->isChecked(); conf->ServerEnabled = ui->serverEnabled->isChecked();
conf->ServerPort = ui->serverPort->value(); conf->ServerPort = ui->serverPort->value();
conf->DebugEnabled = ui->debugEnabled->isChecked(); conf->DebugEnabled = ui->debugEnabled->isChecked();
conf->AlertsEnabled = ui->alertsEnabled->isChecked();
if (ui->authRequired->isChecked()) if (ui->authRequired->isChecked()) {
{ if (ui->password->text() != CHANGE_ME) {
if (ui->password->text() != CHANGE_ME) conf->SetPassword(ui->password->text());
{
QByteArray pwd = ui->password->text().toUtf8();
const char *new_password = pwd;
conf->SetPassword(new_password);
} }
if (strcmp(Config::Current()->Secret, "") != 0) if (!Config::Current()->Secret.isEmpty())
conf->AuthRequired = true; conf->AuthRequired = true;
else else
conf->AuthRequired = false; conf->AuthRequired = false;
@ -107,7 +99,6 @@ void SettingsDialog::FormAccepted()
WSServer::Instance->Stop(); WSServer::Instance->Stop();
} }
SettingsDialog::~SettingsDialog() SettingsDialog::~SettingsDialog() {
{
delete ui; delete ui;
} }

View File

@ -21,9 +21,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <QDialog> #include <QDialog>
namespace Ui { #include "ui_settings-dialog.h"
class SettingsDialog;
}
class SettingsDialog : public QDialog class SettingsDialog : public QDialog
{ {

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>407</width> <width>407</width>
<height>175</height> <height>195</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -80,6 +80,16 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="QCheckBox" name="alertsEnabled">
<property name="text">
<string>OBSWebsocket.Settings.AlertsEnable</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="debugEnabled"> <widget class="QCheckBox" name="debugEnabled">
<property name="text"> <property name="text">
<string>OBSWebsocket.Settings.DebugEnable</string> <string>OBSWebsocket.Settings.DebugEnable</string>
@ -113,11 +123,11 @@
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">
<x>248</x> <x>248</x>
<y>274</y> <y>294</y>
</hint> </hint>
<hint type="destinationlabel"> <hint type="destinationlabel">
<x>157</x> <x>157</x>
<y>294</y> <y>314</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>
@ -129,11 +139,11 @@
<hints> <hints>
<hint type="sourcelabel"> <hint type="sourcelabel">
<x>316</x> <x>316</x>
<y>280</y> <y>300</y>
</hint> </hint>
<hint type="destinationlabel"> <hint type="destinationlabel">
<x>286</x> <x>286</x>
<y>294</y> <y>314</y>
</hint> </hint>
</hints> </hints>
</connection> </connection>

View File

@ -28,6 +28,12 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include "Config.h" #include "Config.h"
#include "forms/settings-dialog.h" #include "forms/settings-dialog.h"
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*) {}
OBS_DECLARE_MODULE() OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US") OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US")

View File

@ -19,8 +19,27 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#ifndef OBSWEBSOCKET_H #ifndef OBSWEBSOCKET_H
#define OBSWEBSOCKET_H #define OBSWEBSOCKET_H
#include <obs.hpp>
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*);
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>;
#define PROP_AUTHENTICATED "wsclient_authenticated" #define PROP_AUTHENTICATED "wsclient_authenticated"
#define OBS_WEBSOCKET_VERSION "4.2.0" #define OBS_WEBSOCKET_VERSION "4.3.3"
#define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__) #define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__)