diff --git a/.gitignore b/.gitignore index 599feb88..b7e57102 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /build32/ /build64/ /release/ +/package/ /installer/Output/ - -.vscode \ No newline at end of file +.idea +.vscode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8e712fbb..00000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -language: cpp - -env: - global: - # AWS key ID - - secure: pAiNUGVbjP12BfnWPk0FFTkbnk4Tocvv88XiT3rzRqkQaD7/iyEogLBfHM4nOEgFiIMHbC41aE83w5JgRNPwn6mTgoQBOglzqq1tGuXfqPyV2VStk8beji1evubGoVjjPaoPTFyIdQc5GGxdHyogI/ed9Hb3ccyykYvjyolj9XoCiW42QHx60AHGwl+So+dEa8xydj9SLRPlZ/AitmI/cPVN3YotA7s37BLFiab54enxk7T4rwpR1nU0HVfoCpn5F4wZYxRq+LlSVFzC8vVE9cpDSLS5kjrZIZaT18tYG1/untCj+wqMIZbghaJXLtPSRW2YPHcJTz8q1YSXnJ19+0uiAIMAqaVv0kD5BAM97byYDBW+b9H6SYFkb/Pw/qcK9amMzMBjDPFpYFkl9Q2kzhsNs3HsZf/flSZjtrkQJiP3SOi/KvKzVK9X4Wym6hYZWHgmMTTYFrvr6BYnf2GkpfKNjm1d2kc0NNrq4d5H4NOEQB8MP+QH+o+BPeM6d9dthrUc1Pw+BXzOAr85CN4qtpPGoAl/Dbfgd6eu/88E2LpUufW2VFAOPWjykSOqzSN3orh7AaWuE34VFEnQ+2y3uIE8AKoyXzJv6zYkyNnNewKZeGe2kKYNwLn5UxQA9JEj7a+tvVevk4xBSkkjFAvjSG2z8/F1FXNbEfoLX1Hz/bU= - # AWS key secret - - secure: bGwljoP3E1OVBXLXox0O6p8kwQXLcNQ8YDKVa4H8u9Y+Ic7uqE4iV3rYS3ynNWSBMVRWY3ZbyClnhrCNwRhBAlcd8qWSJdpjVzs6HdQyzhuKa1P3V4FJPb7upGP/5R/DECGwex8Mun9dmXpYDak75LxfKIJUidPis5VDCYqul7k/xVVCou6Ctjpj7vQhWXDj2G/py+mdB8DERhymnQCtyK1Ziu8c4QlFKByZmnD72GFm/h3JPI1Pq1V2mz3x6x6GaYjb9Rdbd0UNwqjGQX4q2M/c3GEJa6B2JBCoTncawNZBNnPUF9qtv+zh0TNaNHMRWX13AJ/qYB+nVDub0C9b/6Mc48mt0Tv4ze15MproVrylZdV6qHYEG8yGPBqpTVbRP6gv6Y2TXIHWoTzqA+F/Gv2IDChyHXsld/MQQS2MSo5iaYktIrZKtX8Z0qAmTzPwIVBromaSI3vrE7UH0fRSQ6fAM8+Tn+MRthOBdqu23kS1dnG+X2CPbUhBfsJp0OSwVQD5jQtA51/sREVeGFiJvzQIkvwQDjb5MYilsRnwmoBXemkLmqaviXVY4rz1o5AIvz2pgZS2YggK1xHZCuI5tSjcNEkb77VwZTfsqrdDo9EJh6VgfdnGlHQhR2/A5hUJ4ANpJ/LgZlgfVp71Xg2GWQW6M4Znc5uj6A6xLBkO6FA= - -cache: - directories: - - node_modules - -matrix: - include: - - os: linux - env: _generate_docs - script: "./CI/generate-docs.sh" - - - os: linux - env: _linux_build - dist: trusty - sudo: required - services: - - docker - before_install: - - docker run -d --name xenial -v $(dirname $(pwd)):/root -v /home/travis/package:/package - -e TRAVIS_BRANCH="$TRAVIS_BRANCH" -e TRAVIS_TAG="$TRAVIS_TAG" -w /root nimmis/ubuntu:16.04 - - docker exec -it xenial /root/obs-websocket/CI/install-dependencies-xenial.sh - script: - - docker exec -it xenial /root/obs-websocket/CI/build-xenial.sh - after_success: - - docker exec -it xenial /root/obs-websocket/CI/package-xenial.sh - -deploy: -- provider: s3 - region: eu-central-1 - bucket: obs-websocket-linux-builds - access_key_id: "$AWS_ID" - secret_access_key: "$AWS_SECRET" - local_dir: /home/travis/package - skip_cleanup: true - acl: public_read - on: - repo: Palakis/obs-websocket - condition: - - "$TRAVIS_OS_NAME = linux" - - "-d /home/travis/package" - all_branches: true diff --git a/CI/build-xenial.sh b/CI/build-ubuntu.sh similarity index 78% rename from CI/build-xenial.sh rename to CI/build-ubuntu.sh index cc7e53ef..b19158ae 100755 --- a/CI/build-xenial.sh +++ b/CI/build-ubuntu.sh @@ -1,8 +1,6 @@ #!/bin/sh set -ex -cd /root/obs-websocket - mkdir build && cd build cmake -DCMAKE_INSTALL_PREFIX=/usr .. make -j4 diff --git a/CI/install-build-obs.cmd b/CI/checkout-cmake-obs-windows.cmd similarity index 51% rename from CI/install-build-obs.cmd rename to CI/checkout-cmake-obs-windows.cmd index 61e46503..0fc6c12c 100644 --- a/CI/install-build-obs.cmd +++ b/CI/checkout-cmake-obs-windows.cmd @@ -18,25 +18,25 @@ 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\latest-obs-studio-tag-pre-pull.txt - set /p OBSLatestTagPrePull= "%OBSPath%\latest-obs-studio-tag-pre-pull.txt" + set /p OBSLatestTagPrePull=<"%OBSPath%\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.txt + git describe --tags --abbrev=0 --exclude="*-rc*" > "%OBSPath%\latest-obs-studio-tag-post-pull.txt" + set /p OBSLatestTagPostPull=<"%OBSPath%\latest-obs-studio-tag-post-pull.txt" + set /p OBSLatestTag=<"%OBSPath%\latest-obs-studio-tag-post-pull.txt" + echo %OBSLatestTagPostPull%> "%OBSPath%\latest-obs-studio-tag.txt" ) REM Check the obs-studio tags for mismatches. @@ -58,22 +58,22 @@ if not %OBSLatestTagPostPull%==%OBSLastTagBuilt% ( 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 ( +if not exist %OBSPath% ( echo obs-studio directory does not exist - git clone https://github.com/obsproject/obs-studio - cd C:\projects\obs-studio\ - git describe --tags --abbrev=0 > C:\projects\obs-studio-latest-tag.txt - set /p OBSLatestTag= "%OBSPath%\obs-studio-latest-tag.txt" + set /p OBSLatestTag=<"%OBSPath%\obs-studio-latest-tag.txt" set BuildOBS=true ) REM If the needed obs-studio libs for this build_config do not exist, REM set the build flag. -if not exist C:\projects\obs-studio\build32\libobs\%build_config%\obs.lib ( +if not exist %OBSPath%\build32\libobs\%build_config%\obs.lib ( echo obs-studio\build32\libobs\%build_config%\obs.lib does not exist set BuildOBS=true ) -if not exist C:\projects\obs-studio\build32\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib ( +if not exist %OBSPath%\build32\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib ( echo obs-studio\build32\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib does not exist set BuildOBS=true ) @@ -95,35 +95,43 @@ echo: REM If the build flag is set, build obs-studio. if defined BuildOBS ( echo Building obs-studio... + cd /D %OBSPath% 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 + + echo Removing previous build dirs... + if exist build32 rmdir /s /q "%OBSPath%\build32" + if exist build64 rmdir /s /q "%OBSPath%\build64" + + echo Making new build dirs... mkdir build32 mkdir build64 - echo Running cmake for obs-studio %OBSLatestTag% 32-bit... - cd ./build32 - cmake -G "Visual Studio 14 2015" -DBUILD_CAPTIONS=true -DDISABLE_PLUGINS=true -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true .. + + echo Running cmake for obs-studio %OBSLatestTag% 32-bit... + cd build32 + cmake -G "Visual Studio 16 2019" -A Win32 -DCMAKE_SYSTEM_VERSION=10.0 -DQTDIR="%QTDIR32%" -DDepsPath="%DepsPath32%" -DBUILD_CAPTIONS=true -DDISABLE_PLUGINS=true -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" -DBUILD_CAPTIONS=true -DDISABLE_PLUGINS=true -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true .. + + echo Running cmake for obs-studio %OBSLatestTag% 64-bit... + cd ..\build64 + cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_SYSTEM_VERSION=10.0 -DQTDIR="%QTDIR64%" -DDepsPath="%DepsPath64%" -DBUILD_CAPTIONS=true -DDISABLE_PLUGINS=true -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= "%OBSPath%\obs-studio-last-tag-built.txt" + set /p OBSLastTagBuilt=<"%OBSPath%\obs-studio-last-tag-built.txt" ) else ( echo Last OBS tag built is: %OBSLastTagBuilt% echo No need to rebuild OBS. ) + +dir "%OBSPath%\libobs" diff --git a/CI/download-obs-deps.cmd b/CI/download-obs-deps.cmd new file mode 100644 index 00000000..ff4ffd57 --- /dev/null +++ b/CI/download-obs-deps.cmd @@ -0,0 +1,6 @@ +if not exist %DepsBasePath% ( + curl -o %DepsBasePath%.zip -kLO https://obsproject.com/downloads/dependencies2017.zip -f --retry 5 -C - + 7z x %DepsBasePath%.zip -o%DepsBasePath% +) else ( + echo "OBS dependencies are already there. Download skipped." +) diff --git a/CI/generate-docs.sh b/CI/generate-docs.sh index 5e7d6d01..bb1d3dfd 100755 --- a/CI/generate-docs.sh +++ b/CI/generate-docs.sh @@ -4,6 +4,9 @@ echo "-- Generating documentation." echo "-- Node version: $(node -v)" echo "-- NPM version: $(npm -v)" +git fetch origin +git checkout ${CHECKOUT_REF/refs\/heads\//} + cd docs npm install npm run build @@ -15,19 +18,14 @@ if git diff --quiet; then exit 0 fi -if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_BRANCH" != "4.x-current" ]; then - echo "-- Skipping documentation deployment because this is either a pull request or a non-master branch." - exit 0 -fi - REMOTE_URL="$(git config remote.origin.url)" TARGET_REPO=${REMOTE_URL/https:\/\/github.com\//github.com/} GITHUB_REPO=https://${GH_TOKEN:-git}@${TARGET_REPO} -git config user.name "Travis CI" +git config user.name "Azure CI" git config user.email "$COMMIT_AUTHOR_EMAIL" git add ./generated git pull -git commit -m "docs(travis): Update protocol.md - $(git rev-parse --short HEAD) [skip ci]" -git push -q $GITHUB_REPO HEAD:$TRAVIS_BRANCH +git commit -m "docs(ci): Update protocol.md - $(git rev-parse --short HEAD) [skip ci]" +git push -q $GITHUB_REPO diff --git a/CI/install-dependencies-ubuntu.sh b/CI/install-dependencies-ubuntu.sh new file mode 100755 index 00000000..d0e16f53 --- /dev/null +++ b/CI/install-dependencies-ubuntu.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -ex + +sudo add-apt-repository -y ppa:obsproject/obs-studio +sudo apt-get -qq update + +sudo apt-get install -y \ + libc-dev-bin \ + libc6-dev git \ + build-essential \ + checkinstall \ + cmake \ + obs-studio \ + qtbase5-dev + +# Dirty hack +sudo wget -O /usr/include/obs/obs-frontend-api.h https://raw.githubusercontent.com/obsproject/obs-studio/25.0.0/UI/obs-frontend-api/obs-frontend-api.h + +sudo ldconfig diff --git a/CI/install-dependencies-xenial.sh b/CI/install-dependencies-xenial.sh deleted file mode 100755 index bd9dd03d..00000000 --- a/CI/install-dependencies-xenial.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -set -ex - -add-apt-repository -y ppa:obsproject/obs-studio -apt-get -qq update - -apt-get install -y \ - libc-dev-bin \ - libc6-dev git \ - build-essential \ - checkinstall \ - cmake \ - obs-studio \ - qtbase5-dev - -# Dirty hack -wget -O /usr/include/obs/obs-frontend-api.h https://raw.githubusercontent.com/obsproject/obs-studio/master/UI/obs-frontend-api/obs-frontend-api.h - -ldconfig diff --git a/CI/install-qt-win.cmd b/CI/install-qt-win.cmd new file mode 100644 index 00000000..e0537fe8 --- /dev/null +++ b/CI/install-qt-win.cmd @@ -0,0 +1,8 @@ +if not exist %QtBaseDir% ( + curl -kLO https://cdn-fastly.obsproject.com/downloads/Qt_5.10.1.7z -f --retry 5 -z Qt_5.10.1.7z + 7z x Qt_5.10.1.7z -o%QtBaseDir% +) else ( + echo "Qt is already installed. Download skipped." +) + +dir %QtBaseDir% diff --git a/CI/install-setup-qt.cmd b/CI/install-setup-qt.cmd deleted file mode 100644 index e7dc3784..00000000 --- a/CI/install-setup-qt.cmd +++ /dev/null @@ -1,6 +0,0 @@ -@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 diff --git a/CI/package-macos.sh b/CI/package-macos.sh index 5411ad2d..5210ba5b 100755 --- a/CI/package-macos.sh +++ b/CI/package-macos.sh @@ -19,10 +19,12 @@ export VERSION="$GIT_HASH-$GIT_BRANCH_OR_TAG" export LATEST_VERSION="$GIT_BRANCH_OR_TAG" export FILENAME="obs-websocket-$VERSION.pkg" -export LATEST_FILENAME="obs-websocket-latest-$LATEST_VERSION.pkg" echo "[obs-websocket] Modifying obs-websocket.so" install_name_tool \ + -add_rpath @executable_path/../Frameworks/QtWidgets.framework/Versions/5/ \ + -add_rpath @executable_path/../Frameworks/QtGui.framework/Versions/5/ \ + -add_rpath @executable_path/../Frameworks/QtCore.framework/Versions/5/ \ -change /usr/local/opt/qt/lib/QtWidgets.framework/Versions/5/QtWidgets @rpath/QtWidgets \ -change /usr/local/opt/qt/lib/QtGui.framework/Versions/5/QtGui @rpath/QtGui \ -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore @rpath/QtCore \ @@ -37,4 +39,3 @@ packagesbuild ./CI/macos/obs-websocket.pkgproj echo "[obs-websocket] Renaming obs-websocket.pkg to $FILENAME" mv ./release/obs-websocket.pkg ./release/$FILENAME -cp ./release/$FILENAME ./release/$LATEST_FILENAME diff --git a/CI/package-ubuntu.sh b/CI/package-ubuntu.sh new file mode 100755 index 00000000..367e002c --- /dev/null +++ b/CI/package-ubuntu.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +export GIT_HASH=$(git rev-parse --short HEAD) +export PKG_VERSION="1-$GIT_HASH-$BRANCH_SHORT_NAME-git" + +if [[ "$BRANCH_FULL_NAME" =~ "^refs/tags/" ]]; then + export PKG_VERSION="$BRANCH_SHORT_NAME" +fi + +cd ./build + +PAGER="cat" sudo checkinstall -y --type=debian --fstrans=no --nodoc \ + --backup=no --deldoc=yes --install=no \ + --pkgname=obs-websocket --pkgversion="$PKG_VERSION" \ + --pkglicense="GPLv2.0" --maintainer="stephane.lepin@gmail.com" \ + --pkggroup="video" \ + --pkgsource="https://github.com/Palakis/obs-websocket" \ + --requires="obs-studio,libqt5core5a,libqt5widgets5,qt5-image-formats-plugins" \ + --pakdir="../package" + +sudo chmod ao+r ../package/* diff --git a/CI/package-windows.cmd b/CI/package-windows.cmd new file mode 100644 index 00000000..fe752995 --- /dev/null +++ b/CI/package-windows.cmd @@ -0,0 +1,12 @@ +mkdir package +cd package + +git rev-parse --short HEAD > package-version.txt +set /p PackageVersion=" "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") + # In Release mode, copy Qt image format plugins + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy + "${QTDIR}/plugins/imageformats/qjpeg.dll" + "${RELEASE_DIR}/bin/${ARCH_NAME}/imageformats/qjpeg.dll") + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy + "${QTDIR}/plugins/imageformats/qjpeg.dll" + "${RELEASE_DIR}/bin/${ARCH_NAME}/imageformats/qjpeg.dll") + # If config is RelWithDebInfo, package release files COMMAND if $==1 ( "${CMAKE_COMMAND}" -E make_directory @@ -173,8 +188,8 @@ if(UNIX AND NOT APPLE) install(TARGETS obs-websocket LIBRARY DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/obs-plugins") # Dirty fix for Ubuntu - install(TARGETS obs-websocket - LIBRARY DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/${UNAME_MACHINE}-linux-gnu/obs-plugins") + install(TARGETS obs-websocket + LIBRARY DESTINATION "${CMAKE_INSTALL_PREFIX}/lib/${UNAME_MACHINE}-linux-gnu/obs-plugins") install(FILES ${locale_files} DESTINATION "${CMAKE_INSTALL_PREFIX}/share/obs/obs-plugins/obs-websocket/locale") diff --git a/README.md b/README.md index 1db8d261..83862294 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,24 @@ obs-websocket ============== -Remote control of OBS Studio made easy. + +WebSockets API for OBS Studio. Follow the main author on Twitter for news & updates : [@LePalakis](https://twitter.com/LePalakis) -[![Build Status - Windows](https://ci.appveyor.com/api/projects/status/github/Palakis/obs-websocket)](https://ci.appveyor.com/project/Palakis/obs-websocket/history) [![Build Status - Linux & OS X](https://travis-ci.org/Palakis/obs-websocket.svg?branch=master)](https://travis-ci.org/Palakis/obs-websocket) +[![Build Status - Windows](https://ci.appveyor.com/api/projects/status/github/Palakis/obs-websocket)](https://ci.appveyor.com/project/Palakis/obs-websocket/history) [![Build Status - Linux](https://travis-ci.org/Palakis/obs-websocket.svg?branch=master)](https://travis-ci.org/Palakis/obs-websocket) ## Downloads -Binaries for Windows and Linux are available in the [Releases](https://github.com/Palakis/obs-websocket/releases) section. + +Binaries for Windows, MacOS, and Linux are available in the [Releases](https://github.com/Palakis/obs-websocket/releases) section. ## Using obs-websocket + A web client and frontend made by [t2t2](https://github.com/t2t2/obs-tablet-remote) (compatible with tablets and other touch interfaces) is available here : http://t2t2.github.io/obs-tablet-remote/ It is **highly recommended** to protect obs-websocket with a password against unauthorized control. To do this, open the "Websocket server settings" dialog under OBS' "Tools" menu. In the settings dialogs, you can enable or disable authentication and set a password for it. ### Possible use cases + - Remote control OBS from a phone or tablet on the same local network - Change your stream overlay/graphics based on the current scene (like the AGDQ overlay does) - Automate scene switching with a third-party program (e.g. : auto-pilot, foot pedal, ...) @@ -30,6 +34,7 @@ Here's a list of available language APIs for obs-websocket : - Python 2 and 3: [obs-websocket-py](https://github.com/Elektordi/obs-websocket-py) by Guillaume Genty a.k.a Elektordi - Python 3.5+ with asyncio: [obs-ws-rc](https://github.com/KirillMysnik/obs-ws-rc) by Kirill Mysnik - Java 8+: [obs-websocket-java](https://github.com/Twasi/websocket-obs-java) by TwasiNET +- Golang: [go-obs-websocket](https://github.com/christopher-dG/go-obs-websocket) by Chris de Graaf I'd like to know what you're building with or for obs-websocket. If you do something in this fashion, feel free to drop me an email at `stephane /dot/ lepin /at/ gmail /dot/ com` ! @@ -60,7 +65,7 @@ If your Pull Request is not ready to merge yet, tag it with the `work in progres Source code is indented with tabs, with spaces allowed for alignment. Regarding protocol changes: new and updated request types / events must always come with accompanying documentation comments (see existing protocol elements for examples). -These are using to automatically generate the [protocol specification](docs/generated/protocol.md). +These are required to automatically generate the [protocol specification document](docs/generated/protocol.md). Among other recommendations: favor return-early code and avoid wrapping huge portions of code in conditionals. As an example, this: diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index c12a2501..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,40 +0,0 @@ -environment: - CURL_VERSION: 7.39.0 - -install: - - git submodule update --init --recursive - - cd C:\projects\ - - if not exist dependencies2015.zip curl -kLO https://obsproject.com/downloads/dependencies2015.zip -f --retry 5 -C - - - 7z x dependencies2015.zip -odependencies2015 - - set DepsPath32=%CD%\dependencies2015\win32 - - set DepsPath64=%CD%\dependencies2015\win64 - - call C:\projects\obs-websocket\CI\install-setup-qt.cmd - - set build_config=RelWithDebInfo - - call C:\projects\obs-websocket\CI\install-build-obs.cmd - - cd C:\projects\obs-websocket\ - - mkdir build32 - - mkdir build64 - - cd ./build32 - - 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 - - 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: - - 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\build64\obs-websocket.sln /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" - -before_deploy: - - 7z a "C:\projects\obs-websocket\build.zip" C:\projects\obs-websocket\release\* - - set PATH=%PATH%;"C:\\Program Files (x86)\\Inno Setup 5" - - iscc "C:\projects\obs-websocket\installer\installer.iss" - -deploy_script: - - ps: Push-AppveyorArtifact "C:\projects\obs-websocket\build.zip" -FileName "obs-websocket-$(git log --pretty=format:'%h' -n 1)-Windows.zip" - - ps: Push-AppveyorArtifact "C:\projects\obs-websocket\installer\Output\obs-websocket-Windows-Installer.exe" -FileName "obs-websocket-$(git log --pretty=format:'%h' -n 1)-Windows-Installer.exe" - -test: off - -cache: - - C:\projects\dependencies2015.zip - - C:\projects\obs-studio-last-tag-built.txt - - C:\projects\obs-studio\ diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ec85baf6..11c09166 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,23 +1,158 @@ -pool: - vmImage: 'macOS-10.13' +jobs: +- job: 'GenerateDocs' + condition: | + or( + eq(variables['Build.SourceBranch'], 'refs/heads/4.x-current'), + eq(variables['Build.SourceBranch'], 'refs/heads/master') + ) + pool: + vmImage: 'ubuntu-18.04' + steps: + - checkout: self + submodules: false -steps: -- checkout: self - submodules: true + - script: ./CI/generate-docs.sh + displayName: 'Generate docs' + env: + CHECKOUT_REF: $(Build.SourceBranch) + GH_TOKEN: $(GithubToken) -- script: ./CI/install-dependencies-macos.sh - displayName: 'Install Dependencies' +- job: 'Build_Windows' + pool: + vmImage: 'windows-2019' + variables: + build_config: RelWithDebInfo + DepsBasePath: 'D:\obsdependencies' + DepsPath32: '$(DepsBasePath)\win32' + DepsPath64: '$(DepsBasePath)\win64' + QtBaseDir: 'D:\QtDep' + QTDIR32: '$(QtBaseDir)\5.10.1\msvc2017' + QTDIR64: '$(QtBaseDir)\5.10.1\msvc2017_64' + OBSPath: 'D:\obs-studio' + steps: + - checkout: self + submodules: true -- script: ./CI/install-build-obs-macos.sh - displayName: 'Build OBS' + - script: ./CI/install-qt-win.cmd + displayName: 'Install Qt' + env: + QtBaseDir: $(QtBaseDir) -- script: ./CI/build-macos.sh - displayName: 'Build obs-websocket' + - task: Cache@2 + displayName: Restore cached OBS Studio dependencies + inputs: + key: 'obsdeps | "$(Agent.OS)"' + restoreKeys: | + obsdeps | "$(Agent.OS)" + path: $(DepsBasePath) -- script: ./CI/package-macos.sh - displayName: 'Package' + - script: ./CI/download-obs-deps.cmd + displayName: 'Download OBS Studio dependencies' -- task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: './release' - artifactName: 'build' + - task: Cache@2 + displayName: Restore cached OBS Studio builds + inputs: + key: 'obs | "$(Agent.OS)"' + restoreKeys: | + obs | "$(Agent.OS)" + path: $(OBSPath) + + - script: ./CI/checkout-cmake-obs-windows.cmd + displayName: 'Checkout & CMake OBS Studio' + env: + build_config: $(build_config) + DepsPath32: $(DepsPath32) + DepsPath64: $(DepsPath64) + QTDIR32: $(QTDIR32) + QTDIR64: $(QTDIR64) + OBSPath: $(OBSPath) + + - task: MSBuild@1 + displayName: 'Build OBS Studio 32-bit' + inputs: + msbuildArguments: '/m /p:Configuration=$(build_config)' + solution: '$(OBSPath)\build32\obs-studio.sln' + + - task: MSBuild@1 + displayName: 'Build OBS Studio 64-bit' + inputs: + msbuildArguments: '/m /p:Configuration=$(build_config)' + solution: '$(OBSPath)\build64\obs-studio.sln' + + - script: ./CI/prepare-windows.cmd + displayName: 'CMake obs-websocket' + env: + build_config: $(build_config) + QTDIR32: $(QTDIR32) + QTDIR64: $(QTDIR64) + OBSPath: $(OBSPath) + + - task: MSBuild@1 + displayName: 'Build obs-websocket 32-bit' + inputs: + msbuildArguments: '/m /p:Configuration=$(build_config)' + solution: '.\build32\obs-websocket.sln' + + - task: MSBuild@1 + displayName: 'Build obs-websocket 64-bit' + inputs: + msbuildArguments: '/m /p:Configuration=$(build_config)' + solution: '.\build64\obs-websocket.sln' + + - script: ./CI/package-windows.cmd + displayName: 'Package obs-websocket' + + - task: PublishBuildArtifacts@1 + displayName: 'Upload package artifacts' + inputs: + pathtoPublish: './package' + artifactName: 'windows_build' + +- job: 'Build_Linux' + pool: + vmImage: 'ubuntu-18.04' + variables: + BUILD_REASON: $(Build.Reason) + BRANCH_SHORT_NAME: $(Build.SourceBranchName) + BRANCH_FULL_NAME: $(Build.SourceBranch) + steps: + - checkout: self + submodules: true + + - script: ./CI/install-dependencies-ubuntu.sh + displayName: 'Install dependencies' + + - script: ./CI/build-ubuntu.sh + displayName: 'Build obs-websocket' + + - script: ./CI/package-ubuntu.sh + displayName: 'Package obs-websocket' + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: './package' + artifactName: 'deb_build' + +- job: 'Build_macOS' + pool: + vmImage: 'macos-10.14' + steps: + - checkout: self + submodules: true + + - script: ./CI/install-dependencies-macos.sh + displayName: 'Install dependencies' + + - script: ./CI/install-build-obs-macos.sh + displayName: 'Build OBS' + + - script: ./CI/build-macos.sh + displayName: 'Build obs-websocket' + + - script: ./CI/package-macos.sh + displayName: 'Package obs-websocket' + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: './release' + artifactName: 'macos_build' diff --git a/docs/generated/comments.json b/docs/generated/comments.json index 3f4d5d28..2c54dc9a 100644 --- a/docs/generated/comments.json +++ b/docs/generated/comments.json @@ -6,9 +6,11 @@ "property": [ "{Number} `cy`", "{Number} `cx`", + "{Number} `alignment` The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis.", "{String} `name` The name of this Scene Item.", "{int} `id` Scene item ID", "{Boolean} `render` Whether or not this Scene Item is set to \"visible\".", + "{Boolean} `muted` Whether or not this Scene Item is muted.", "{Boolean} `locked` Whether or not this Scene Item is locked and can't be moved around", "{Number} `source_cx`", "{Number} `source_cy`", @@ -30,6 +32,11 @@ "name": "cx", "description": "" }, + { + "type": "Number", + "name": "alignment", + "description": "The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis." + }, { "type": "String", "name": "name", @@ -45,6 +52,11 @@ "name": "render", "description": "Whether or not this Scene Item is set to \"visible\"." }, + { + "type": "Boolean", + "name": "muted", + "description": "Whether or not this Scene Item is muted." + }, { "type": "Boolean", "name": "locked", @@ -333,6 +345,135 @@ }, "examples": [] }, + { + "subheads": [], + "typedef": "{Object} `Output`", + "property": [ + "{String} `name` Output name", + "{String} `type` Output type/kind", + "{int} `width` Video output width", + "{int} `height` Video output height", + "{Object} `flags` Output flags", + "{int} `flags.rawValue` Raw flags value", + "{boolean} `flags.audio` Output uses audio", + "{boolean} `flags.video` Output uses video", + "{boolean} `flags.encoded` Output is encoded", + "{boolean} `flags.multiTrack` Output uses several audio tracks", + "{boolean} `flags.service` Output uses a service", + "{Object} `settings` Output name", + "{boolean} `active` Output status (active or not)", + "{boolean} `reconnecting` Output reconnection status (reconnecting or not)", + "{double} `congestion` Output congestion", + "{int} `totalFrames` Number of frames sent", + "{int} `droppedFrames` Number of frames dropped", + "{int} `totalBytes` Total bytes sent" + ], + "properties": [ + { + "type": "String", + "name": "name", + "description": "Output name" + }, + { + "type": "String", + "name": "type", + "description": "Output type/kind" + }, + { + "type": "int", + "name": "width", + "description": "Video output width" + }, + { + "type": "int", + "name": "height", + "description": "Video output height" + }, + { + "type": "Object", + "name": "flags", + "description": "Output flags" + }, + { + "type": "int", + "name": "flags.rawValue", + "description": "Raw flags value" + }, + { + "type": "boolean", + "name": "flags.audio", + "description": "Output uses audio" + }, + { + "type": "boolean", + "name": "flags.video", + "description": "Output uses video" + }, + { + "type": "boolean", + "name": "flags.encoded", + "description": "Output is encoded" + }, + { + "type": "boolean", + "name": "flags.multiTrack", + "description": "Output uses several audio tracks" + }, + { + "type": "boolean", + "name": "flags.service", + "description": "Output uses a service" + }, + { + "type": "Object", + "name": "settings", + "description": "Output name" + }, + { + "type": "boolean", + "name": "active", + "description": "Output status (active or not)" + }, + { + "type": "boolean", + "name": "reconnecting", + "description": "Output reconnection status (reconnecting or not)" + }, + { + "type": "double", + "name": "congestion", + "description": "Output congestion" + }, + { + "type": "int", + "name": "totalFrames", + "description": "Number of frames sent" + }, + { + "type": "int", + "name": "droppedFrames", + "description": "Number of frames dropped" + }, + { + "type": "int", + "name": "totalBytes", + "description": "Total bytes sent" + } + ], + "typedefs": [ + { + "type": "Object", + "name": "Output", + "description": "" + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + }, { "subheads": [], "typedef": "{Object} `Scene`", @@ -639,7 +780,8 @@ "description": "A transition (other than \"cut\") has begun.", "return": [ "{String} `name` Transition name.", - "{int} `duration` Transition duration (in milliseconds).", + "{String} `type` Transition type.", + "{int} `duration` Transition duration (in milliseconds). Will be -1 for any transition with a fixed duration, such as a Stinger, due to limitations of the OBS API.", "{String} `from-scene` Source scene of the transition", "{String} `to-scene` Destination scene of the transition" ], @@ -653,10 +795,15 @@ "name": "name", "description": "Transition name." }, + { + "type": "String", + "name": "type", + "description": "Transition type." + }, { "type": "int", "name": "duration", - "description": "Transition duration (in milliseconds)." + "description": "Transition duration (in milliseconds). Will be -1 for any transition with a fixed duration, such as a Stinger, due to limitations of the OBS API." }, { "type": "String", @@ -694,6 +841,134 @@ "lead": "", "type": "class", "examples": [] + }, + { + "subheads": [], + "description": "A transition (other than \"cut\") has ended.\nPlease note that the `from-scene` field is not available in TransitionEnd.", + "return": [ + "{String} `name` Transition name.", + "{String} `type` Transition type.", + "{int} `duration` Transition duration (in milliseconds).", + "{String} `to-scene` Destination scene of the transition" + ], + "api": "events", + "name": "TransitionEnd", + "category": "transitions", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Transition name." + }, + { + "type": "String", + "name": "type", + "description": "Transition type." + }, + { + "type": "int", + "name": "duration", + "description": "Transition duration (in milliseconds)." + }, + { + "type": "String", + "name": "to-scene", + "description": "Destination scene of the transition" + } + ], + "names": [ + { + "name": "", + "description": "TransitionEnd" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionEnd" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A stinger transition has finished playing its video.", + "return": [ + "{String} `name` Transition name.", + "{String} `type` Transition type.", + "{int} `duration` Transition duration (in milliseconds).", + "{String} `from-scene` Source scene of the transition", + "{String} `to-scene` Destination scene of the transition" + ], + "api": "events", + "name": "TransitionVideoEnd", + "category": "transitions", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Transition name." + }, + { + "type": "String", + "name": "type", + "description": "Transition type." + }, + { + "type": "int", + "name": "duration", + "description": "Transition duration (in milliseconds)." + }, + { + "type": "String", + "name": "from-scene", + "description": "Source scene of the transition" + }, + { + "type": "String", + "name": "to-scene", + "description": "Destination scene of the transition" + } + ], + "names": [ + { + "name": "", + "description": "TransitionVideoEnd" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionVideoEnd" + }, + "lead": "", + "type": "class", + "examples": [] } ], "profiles": [ @@ -1197,6 +1472,72 @@ "lead": "", "type": "class", "examples": [] + }, + { + "subheads": [], + "description": "Current recording paused", + "api": "events", + "name": "RecordingPaused", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "RecordingPaused" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "RecordingPaused" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Current recording resumed", + "api": "events", + "name": "RecordingResumed", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "RecordingResumed" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "RecordingResumed" + }, + "lead": "", + "type": "class", + "examples": [] } ], "replay buffer": [ @@ -1470,6 +1811,55 @@ "lead": "", "type": "class", "examples": [] + }, + { + "subheads": [], + "description": "A custom broadcast message was received", + "return": [ + "{String} `realm` Identifier provided by the sender", + "{Object} `data` User-defined data" + ], + "api": "events", + "name": "BroadcastCustomMessage", + "category": "general", + "since": "4.7.0", + "returns": [ + { + "type": "String", + "name": "realm", + "description": "Identifier provided by the sender" + }, + { + "type": "Object", + "name": "data", + "description": "User-defined data" + } + ], + "names": [ + { + "name": "", + "description": "BroadcastCustomMessage" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "BroadcastCustomMessage" + }, + "lead": "", + "type": "class", + "examples": [] } ], "sources": [ @@ -1968,6 +2358,61 @@ "type": "class", "examples": [] }, + { + "subheads": [], + "description": "The visibility/enabled state of a filter changed", + "return": [ + "{String} `sourceName` Source name", + "{String} `filterName` Filter name", + "{Boolean} `filterEnabled` New filter state" + ], + "api": "events", + "name": "SourceFilterVisibilityChanged", + "category": "sources", + "since": "4.7.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Filter name" + }, + { + "type": "Boolean", + "name": "filterEnabled", + "description": "New filter state" + } + ], + "names": [ + { + "name": "", + "description": "SourceFilterVisibilityChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "SourceFilterVisibilityChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, { "subheads": [], "description": "Filters in a source have been reordered.", @@ -2261,6 +2706,67 @@ "type": "class", "examples": [] }, + { + "subheads": [], + "description": "An item's locked status has been toggled.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item in the scene.", + "{int} `item-id` Scene item ID", + "{boolean} `item-locked` New locked state of the item." + ], + "api": "events", + "name": "SceneItemLockChanged", + "category": "sources", + "since": "unreleased", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item in the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Scene item ID" + }, + { + "type": "boolean", + "name": "item-locked", + "description": "New locked state of the item." + } + ], + "names": [ + { + "name": "", + "description": "SceneItemLockChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "unreleased" + } + ], + "heading": { + "level": 2, + "text": "SceneItemLockChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, { "subheads": [], "description": "An item's transform has been changed.", @@ -2535,7 +3041,8 @@ "{double} `version` OBSRemote compatible API version. Fixed to 1.1 for retrocompatibility.", "{String} `obs-websocket-version` obs-websocket plugin version.", "{String} `obs-studio-version` OBS Studio program version.", - "{String} `available-requests` List of available request types, formatted as a comma-separated list string (e.g. : \"Method1,Method2,Method3\")." + "{String} `available-requests` List of available request types, formatted as a comma-separated list string (e.g. : \"Method1,Method2,Method3\").", + "{String} `supported-image-export-formats` List of supported formats for features that use image export (like the TakeSourceScreenshot request type) formatted as a comma-separated list string" ], "api": "requests", "name": "GetVersion", @@ -2561,6 +3068,11 @@ "type": "String", "name": "available-requests", "description": "List of available request types, formatted as a comma-separated list string (e.g. : \"Method1,Method2,Method3\")." + }, + { + "type": "String", + "name": "supported-image-export-formats", + "description": "List of supported formats for features that use image export (like the TakeSourceScreenshot request type) formatted as a comma-separated list string" } ], "names": [ @@ -2849,6 +3361,55 @@ "type": "class", "examples": [] }, + { + "subheads": [], + "description": "Broadcast custom message to all connected WebSocket clients", + "param": [ + "{String} `realm` Identifier to be choosen by the client", + "{Object} `data` User-defined data" + ], + "api": "requests", + "name": "BroadcastCustomMessage", + "category": "general", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "realm", + "description": "Identifier to be choosen by the client" + }, + { + "type": "Object", + "name": "data", + "description": "User-defined data" + } + ], + "names": [ + { + "name": "", + "description": "BroadcastCustomMessage" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "BroadcastCustomMessage" + }, + "lead": "", + "type": "class", + "examples": [] + }, { "subheads": [], "description": "Get basic OBS video information", @@ -2939,6 +3500,249 @@ "lead": "", "type": "class", "examples": [] + }, + { + "subheads": [], + "description": "Open a projector window or create a projector on a monitor. Requires OBS v24.0.4 or newer.", + "param": [ + "{String (Optional)} `type` Type of projector: Preview (default), Source, Scene, StudioProgram, or Multiview (case insensitive).", + "{int (Optional)} `monitor` Monitor to open the projector on. If -1 or omitted, opens a window.", + "{String (Optional)} `geometry` Size and position of the projector window (only if monitor is -1). Encoded in Base64 using Qt's geometry encoding (https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to OBS's saved projectors.", + "{String (Optional)} `name` Name of the source or scene to be displayed (ignored for other projector types)." + ], + "api": "requests", + "name": "OpenProjector", + "category": "general", + "since": "unreleased", + "params": [ + { + "type": "String (Optional)", + "name": "type", + "description": "Type of projector: Preview (default), Source, Scene, StudioProgram, or Multiview (case insensitive)." + }, + { + "type": "int (Optional)", + "name": "monitor", + "description": "Monitor to open the projector on. If -1 or omitted, opens a window." + }, + { + "type": "String (Optional)", + "name": "geometry", + "description": "Size and position of the projector window (only if monitor is -1). Encoded in Base64 using Qt's geometry encoding (https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to OBS's saved projectors." + }, + { + "type": "String (Optional)", + "name": "name", + "description": "Name of the source or scene to be displayed (ignored for other projector types)." + } + ], + "names": [ + { + "name": "", + "description": "OpenProjector" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "unreleased" + } + ], + "heading": { + "level": 2, + "text": "OpenProjector" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "outputs": [ + { + "subheads": [], + "description": "List existing outputs", + "return": "{Array} `outputs` Outputs list", + "api": "requests", + "name": "ListOutputs", + "category": "outputs", + "since": "4.7.0", + "returns": [ + { + "type": "Array", + "name": "outputs", + "description": "Outputs list" + } + ], + "names": [ + { + "name": "", + "description": "ListOutputs" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "ListOutputs" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get information about a single output", + "param": "{String} `outputName` Output name", + "return": "{Output} `outputInfo` Output info", + "api": "requests", + "name": "GetOutputInfo", + "category": "outputs", + "since": "4.7.0", + "returns": [ + { + "type": "Output", + "name": "outputInfo", + "description": "Output info" + } + ], + "params": [ + { + "type": "String", + "name": "outputName", + "description": "Output name" + } + ], + "names": [ + { + "name": "", + "description": "GetOutputInfo" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "GetOutputInfo" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Start an output", + "param": "{String} `outputName` Output name", + "api": "requests", + "name": "StartOutput", + "category": "outputs", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "outputName", + "description": "Output name" + } + ], + "names": [ + { + "name": "", + "description": "StartOutput" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "StartOutput" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Stop an output", + "param": [ + "{String} `outputName` Output name", + "{boolean (optional)} `force` Force stop (default: false)" + ], + "api": "requests", + "name": "StopOutput", + "category": "outputs", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "outputName", + "description": "Output name" + }, + { + "type": "boolean (optional)", + "name": "force", + "description": "Force stop (default: false)" + } + ], + "names": [ + { + "name": "", + "description": "StopOutput" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "StopOutput" + }, + "lead": "", + "type": "class", + "examples": [] } ], "profiles": [ @@ -3166,6 +3970,72 @@ "type": "class", "examples": [] }, + { + "subheads": [], + "description": "Pause the current recording.\nReturns an error if recording is not active or already paused.", + "api": "requests", + "name": "PauseRecording", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "PauseRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "PauseRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Resume/unpause the current recording (if paused).\nReturns an error if recording is not active or not paused.", + "api": "requests", + "name": "ResumeRecording", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "ResumeRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "ResumeRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, { "subheads": [], "description": "\n\nPlease note: if `SetRecordingFolder` is called while a recording is\nin progress, the change won't be applied immediately and will be\neffective on the next recording.", @@ -3529,6 +4399,7 @@ "{int} `crop.bottom` The number of pixels cropped off the bottom of the source before scaling.", "{int} `crop.left` The number of pixels cropped off the left of the source before scaling.", "{bool} `visible` If the source is visible.", + "{bool} `muted` If the source is muted.", "{bool} `locked` If the source's transform is locked.", "{String} `bounds.type` Type of bounding box. Can be \"OBS_BOUNDS_STRETCH\", \"OBS_BOUNDS_SCALE_INNER\", \"OBS_BOUNDS_SCALE_OUTER\", \"OBS_BOUNDS_SCALE_TO_WIDTH\", \"OBS_BOUNDS_SCALE_TO_HEIGHT\", \"OBS_BOUNDS_MAX_ONLY\" or \"OBS_BOUNDS_NONE\".", "{int} `bounds.alignment` Alignment of the bounding box.", @@ -3537,9 +4408,8 @@ "{int} `sourceWidth` Base width (without scaling) of the source", "{int} `sourceHeight` Base source (without scaling) of the source", "{double} `width` Scene item width (base source width multiplied by the horizontal scaling factor)", - "{double} `height` Scene item height (base source height multiplied by the vertical scaling factor)" - ], - "property": [ + "{double} `height` Scene item height (base source height multiplied by the vertical scaling factor)", + "{int} `alignment` The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis.", "{String (optional)} `parentGroupName` Name of the item's parent (if this item belongs to a group)", "{Array (optional)} `groupChildren` List of children (if this item is a group)" ], @@ -3608,6 +4478,11 @@ "name": "visible", "description": "If the source is visible." }, + { + "type": "bool", + "name": "muted", + "description": "If the source is muted." + }, { "type": "bool", "name": "locked", @@ -3652,6 +4527,21 @@ "type": "double", "name": "height", "description": "Scene item height (base source height multiplied by the vertical scaling factor)" + }, + { + "type": "int", + "name": "alignment", + "description": "The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis." + }, + { + "type": "String (optional)", + "name": "parentGroupName", + "description": "Name of the item's parent (if this item belongs to a group)" + }, + { + "type": "Array (optional)", + "name": "groupChildren", + "description": "List of children (if this item is a group)" } ], "params": [ @@ -3666,18 +4556,6 @@ "description": "The name of the source." } ], - "properties": [ - { - "type": "String (optional)", - "name": "parentGroupName", - "description": "Name of the item's parent (if this item belongs to a group)" - }, - { - "type": "Array (optional)", - "name": "groupChildren", - "description": "List of children (if this item is a group)" - } - ], "names": [ { "name": "", @@ -6198,6 +7076,7 @@ "param": "{String} `sourceName` Source name", "return": [ "{Array} `filters` List of filters for the specified source", + "{Boolean} `filters.*.enabled` Filter status (enabled or not)", "{String} `filters.*.type` Filter type", "{String} `filters.*.name` Filter name", "{Object} `filters.*.settings` Filter settings" @@ -6212,6 +7091,11 @@ "name": "filters", "description": "List of filters for the specified source" }, + { + "type": "Boolean", + "name": "filters.*.enabled", + "description": "Filter status (enabled or not)" + }, { "type": "String", "name": "filters.*.type", @@ -6261,6 +7145,83 @@ "type": "class", "examples": [] }, + { + "subheads": [], + "description": "List filters applied to a source", + "param": [ + "{String} `sourceName` Source name", + "{String} `filterName` Source filter name" + ], + "return": [ + "{Boolean} `enabled` Filter status (enabled or not)", + "{String} `type` Filter type", + "{String} `name` Filter name", + "{Object} `settings` Filter settings" + ], + "api": "requests", + "name": "GetSourceFilterInfo", + "category": "sources", + "since": "4.7.0", + "returns": [ + { + "type": "Boolean", + "name": "enabled", + "description": "Filter status (enabled or not)" + }, + { + "type": "String", + "name": "type", + "description": "Filter type" + }, + { + "type": "String", + "name": "name", + "description": "Filter name" + }, + { + "type": "Object", + "name": "settings", + "description": "Filter settings" + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Source filter name" + } + ], + "names": [ + { + "name": "", + "description": "GetSourceFilterInfo" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourceFilterInfo" + }, + "lead": "", + "type": "class", + "examples": [] + }, { "subheads": [], "description": "Add a new filter to a source. Available source types along with their settings properties are available from `GetSourceTypesList`.", @@ -6538,9 +7499,64 @@ }, { "subheads": [], - "description": "\n\nAt least `embedPictureFormat` or `saveToFilePath` must be specified.\n\nClients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is\npreserved if only one of these two parameters is specified.", + "description": "Change the visibility/enabled state of a filter", "param": [ "{String} `sourceName` Source name", + "{String} `filterName` Source filter name", + "{Boolean} `filterEnabled` New filter state" + ], + "api": "requests", + "name": "SetSourceFilterVisibility", + "category": "sources", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Source filter name" + }, + { + "type": "Boolean", + "name": "filterEnabled", + "description": "New filter state" + } + ], + "names": [ + { + "name": "", + "description": "SetSourceFilterVisibility" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "SetSourceFilterVisibility" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nAt least `embedPictureFormat` or `saveToFilePath` must be specified.\n\nClients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is\npreserved if only one of these two parameters is specified.", + "param": [ + "{String} `sourceName` Source name. Note that, since scenes are also sources, you can also provide a scene name.", "{String (optional)} `embedPictureFormat` Format of the Data URI encoded picture. Can be \"png\", \"jpg\", \"jpeg\" or \"bmp\" (or any other value supported by Qt's Image module)", "{String (optional)} `saveToFilePath` Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path.", "{int (optional)} `width` Screenshot width. Defaults to the source's base width.", @@ -6576,7 +7592,7 @@ { "type": "String", "name": "sourceName", - "description": "Source name" + "description": "Source name. Note that, since scenes are also sources, you can also provide a scene name." }, { "type": "String (optional)", @@ -6737,9 +7753,9 @@ "{Object (optional)} `stream.settings` Settings for the stream.", "{String (optional)} `stream.settings.server` The publish URL.", "{String (optional)} `stream.settings.key` The publish key of the stream.", - "{boolean (optional)} `stream.settings.use-auth` Indicates whether authentication should be used when connecting to the streaming server.", - "{String (optional)} `stream.settings.username` If authentication is enabled, the username for the streaming server. Ignored if `use-auth` is not set to `true`.", - "{String (optional)} `stream.settings.password` If authentication is enabled, the password for the streaming server. Ignored if `use-auth` is not set to `true`." + "{boolean (optional)} `stream.settings.use_auth` Indicates whether authentication should be used when connecting to the streaming server.", + "{String (optional)} `stream.settings.username` If authentication is enabled, the username for the streaming server. Ignored if `use_auth` is not set to `true`.", + "{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", @@ -6778,18 +7794,18 @@ }, { "type": "boolean (optional)", - "name": "stream.settings.use-auth", + "name": "stream.settings.use_auth", "description": "Indicates whether authentication should be used when connecting to the streaming server." }, { "type": "String (optional)", "name": "stream.settings.username", - "description": "If authentication is enabled, the username for the streaming server. Ignored if `use-auth` is not set to `true`." + "description": "If authentication is enabled, the username for the streaming server. Ignored if `use_auth` is not set to `true`." }, { "type": "String (optional)", "name": "stream.settings.password", - "description": "If authentication is enabled, the password for the streaming server. Ignored if `use-auth` is not set to `true`." + "description": "If authentication is enabled, the password for the streaming server. Ignored if `use_auth` is not set to `true`." } ], "names": [ @@ -6859,7 +7875,7 @@ "{Object} `settings` The actual settings of the stream.", "{String (optional)} `settings.server` The publish URL.", "{String (optional)} `settings.key` The publish key.", - "{boolean (optional)} `settings.use-auth` Indicates whether authentication should be used when connecting to the streaming server.", + "{boolean (optional)} `settings.use_auth` Indicates whether authentication should be used when connecting to the streaming server.", "{String (optional)} `settings.username` The username for the streaming service.", "{String (optional)} `settings.password` The password for the streaming service.", "{boolean} `save` Persist the settings to disk." @@ -6891,7 +7907,7 @@ }, { "type": "boolean (optional)", - "name": "settings.use-auth", + "name": "settings.use_auth", "description": "Indicates whether authentication should be used when connecting to the streaming server." }, { @@ -6944,9 +7960,9 @@ "{Object} `settings` Stream settings object.", "{String} `settings.server` The publish URL.", "{String} `settings.key` The publish key of the stream.", - "{boolean} `settings.use-auth` Indicates whether authentication should be used when connecting to the streaming server.", - "{String} `settings.username` The username to use when accessing the streaming server. Only present if `use-auth` is `true`.", - "{String} `settings.password` The password to use when accessing the streaming server. Only present if `use-auth` is `true`." + "{boolean} `settings.use_auth` Indicates whether authentication should be used when connecting to the streaming server.", + "{String} `settings.username` The username to use when accessing the streaming server. Only present if `use_auth` is `true`.", + "{String} `settings.password` The password to use when accessing the streaming server. Only present if `use_auth` is `true`." ], "api": "requests", "name": "GetStreamSettings", @@ -6975,18 +7991,18 @@ }, { "type": "boolean", - "name": "settings.use-auth", + "name": "settings.use_auth", "description": "Indicates whether authentication should be used when connecting to the streaming server." }, { "type": "String", "name": "settings.username", - "description": "The username to use when accessing the streaming server. Only present if `use-auth` is `true`." + "description": "The username to use when accessing the streaming server. Only present if `use_auth` is `true`." }, { "type": "String", "name": "settings.password", - "description": "The password to use when accessing the streaming server. Only present if `use-auth` is `true`." + "description": "The password to use when accessing the streaming server. Only present if `use_auth` is `true`." } ], "names": [ diff --git a/docs/generated/protocol.md b/docs/generated/protocol.md index c17d9f1c..77f780d5 100644 --- a/docs/generated/protocol.md +++ b/docs/generated/protocol.md @@ -1,6 +1,6 @@ -# obs-websocket 4.6.0 protocol reference +# obs-websocket 4.7.0 protocol reference # General Introduction Messages are exchanged between the client and the server as JSON objects. @@ -46,6 +46,7 @@ auth_response = base64_encode(auth_response_hash) * [SceneItem](#sceneitem) * [SceneItemTransform](#sceneitemtransform) * [OBSStats](#obsstats) + * [Output](#output) * [Scene](#scene) - [Events](#events) * [Scenes](#scenes) @@ -58,6 +59,8 @@ auth_response = base64_encode(auth_response_hash) + [TransitionListChanged](#transitionlistchanged) + [TransitionDurationChanged](#transitiondurationchanged) + [TransitionBegin](#transitionbegin) + + [TransitionEnd](#transitionend) + + [TransitionVideoEnd](#transitionvideoend) * [Profiles](#profiles) + [ProfileChanged](#profilechanged) + [ProfileListChanged](#profilelistchanged) @@ -72,6 +75,8 @@ auth_response = base64_encode(auth_response_hash) + [RecordingStarted](#recordingstarted) + [RecordingStopping](#recordingstopping) + [RecordingStopped](#recordingstopped) + + [RecordingPaused](#recordingpaused) + + [RecordingResumed](#recordingresumed) * [Replay Buffer](#replay-buffer) + [ReplayStarting](#replaystarting) + [ReplayStarted](#replaystarted) @@ -81,6 +86,7 @@ auth_response = base64_encode(auth_response_hash) + [Exiting](#exiting) * [General](#general) + [Heartbeat](#heartbeat) + + [BroadcastCustomMessage](#broadcastcustommessage) * [Sources](#sources) + [SourceCreated](#sourcecreated) + [SourceDestroyed](#sourcedestroyed) @@ -91,11 +97,13 @@ auth_response = base64_encode(auth_response_hash) + [SourceRenamed](#sourcerenamed) + [SourceFilterAdded](#sourcefilteradded) + [SourceFilterRemoved](#sourcefilterremoved) + + [SourceFilterVisibilityChanged](#sourcefiltervisibilitychanged) + [SourceFiltersReordered](#sourcefiltersreordered) + [SourceOrderChanged](#sourceorderchanged) + [SceneItemAdded](#sceneitemadded) + [SceneItemRemoved](#sceneitemremoved) + [SceneItemVisibilityChanged](#sceneitemvisibilitychanged) + + [SceneItemLockChanged](#sceneitemlockchanged) + [SceneItemTransformChanged](#sceneitemtransformchanged) + [SceneItemSelected](#sceneitemselected) + [SceneItemDeselected](#sceneitemdeselected) @@ -111,7 +119,14 @@ auth_response = base64_encode(auth_response_hash) + [SetFilenameFormatting](#setfilenameformatting) + [GetFilenameFormatting](#getfilenameformatting) + [GetStats](#getstats) + + [BroadcastCustomMessage](#broadcastcustommessage-1) + [GetVideoInfo](#getvideoinfo) + + [OpenProjector](#openprojector) + * [Outputs](#outputs) + + [ListOutputs](#listoutputs) + + [GetOutputInfo](#getoutputinfo) + + [StartOutput](#startoutput) + + [StopOutput](#stopoutput) * [Profiles](#profiles-1) + [SetCurrentProfile](#setcurrentprofile) + [GetCurrentProfile](#getcurrentprofile) @@ -120,6 +135,8 @@ auth_response = base64_encode(auth_response_hash) + [StartStopRecording](#startstoprecording) + [StartRecording](#startrecording) + [StopRecording](#stoprecording) + + [PauseRecording](#pauserecording) + + [ResumeRecording](#resumerecording) + [SetRecordingFolder](#setrecordingfolder) + [GetRecordingFolder](#getrecordingfolder) * [Replay Buffer](#replay-buffer-1) @@ -166,11 +183,13 @@ auth_response = base64_encode(auth_response_hash) + [SetBrowserSourceProperties](#setbrowsersourceproperties) + [GetSpecialSources](#getspecialsources) + [GetSourceFilters](#getsourcefilters) + + [GetSourceFilterInfo](#getsourcefilterinfo) + [AddFilterToSource](#addfiltertosource) + [RemoveFilterFromSource](#removefilterfromsource) + [ReorderSourceFilter](#reordersourcefilter) + [MoveSourceFilter](#movesourcefilter) + [SetSourceFilterSettings](#setsourcefiltersettings) + + [SetSourceFilterVisibility](#setsourcefiltervisibility) + [TakeSourceScreenshot](#takesourcescreenshot) * [Streaming](#streaming-1) + [GetStreamingStatus](#getstreamingstatus) @@ -207,9 +226,11 @@ These are complex types, such as `Source` and `Scene`, which are used as argumen | ---- | :---: | ------------| | `cy` | _Number_ | | | `cx` | _Number_ | | +| `alignment` | _Number_ | The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis. | | `name` | _String_ | The name of this Scene Item. | | `id` | _int_ | Scene item ID | | `render` | _Boolean_ | Whether or not this Scene Item is set to "visible". | +| `muted` | _Boolean_ | Whether or not this Scene Item is muted. | | `locked` | _Boolean_ | Whether or not this Scene Item is locked and can't be moved around | | `source_cx` | _Number_ | | | `source_cy` | _Number_ | | @@ -256,6 +277,27 @@ These are complex types, such as `Source` and `Scene`, which are used as argumen | `cpu-usage` | _double_ | Current CPU usage (percentage) | | `memory-usage` | _double_ | Current RAM usage (in megabytes) | | `free-disk-space` | _double_ | Free recording disk space (in megabytes) | +## Output +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Output name | +| `type` | _String_ | Output type/kind | +| `width` | _int_ | Video output width | +| `height` | _int_ | Video output height | +| `flags` | _Object_ | Output flags | +| `flags.rawValue` | _int_ | Raw flags value | +| `flags.audio` | _boolean_ | Output uses audio | +| `flags.video` | _boolean_ | Output uses video | +| `flags.encoded` | _boolean_ | Output is encoded | +| `flags.multiTrack` | _boolean_ | Output uses several audio tracks | +| `flags.service` | _boolean_ | Output uses a service | +| `settings` | _Object_ | Output name | +| `active` | _boolean_ | Output status (active or not) | +| `reconnecting` | _boolean_ | Output reconnection status (reconnecting or not) | +| `congestion` | _double_ | Output congestion | +| `totalFrames` | _int_ | Number of frames sent | +| `droppedFrames` | _int_ | Number of frames dropped | +| `totalBytes` | _int_ | Total bytes sent | ## Scene | Name | Type | Description | | ---- | :---: | ------------| @@ -396,6 +438,47 @@ A transition (other than "cut") has begun. | Name | Type | Description | | ---- | :---: | ------------| | `name` | _String_ | Transition name. | +| `type` | _String_ | Transition type. | +| `duration` | _int_ | Transition duration (in milliseconds). Will be -1 for any transition with a fixed duration, such as a Stinger, due to limitations of the OBS API. | +| `from-scene` | _String_ | Source scene of the transition | +| `to-scene` | _String_ | Destination scene of the transition | + + +--- + +### TransitionEnd + + +- Added in v4.8.0 + +A transition (other than "cut") has ended. +Please note that the `from-scene` field is not available in TransitionEnd. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Transition name. | +| `type` | _String_ | Transition type. | +| `duration` | _int_ | Transition duration (in milliseconds). | +| `to-scene` | _String_ | Destination scene of the transition | + + +--- + +### TransitionVideoEnd + + +- Added in v4.8.0 + +A stinger transition has finished playing its video. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Transition name. | +| `type` | _String_ | Transition type. | | `duration` | _int_ | Transition duration (in milliseconds). | | `from-scene` | _String_ | Source scene of the transition | | `to-scene` | _String_ | Destination scene of the transition | @@ -579,6 +662,32 @@ _No additional response items._ --- +### RecordingPaused + + +- Added in v4.7.0 + +Current recording paused + +**Response Items:** + +_No additional response items._ + +--- + +### RecordingResumed + + +- Added in v4.7.0 + +Current recording resumed + +**Response Items:** + +_No additional response items._ + +--- + ## Replay Buffer ### ReplayStarting @@ -675,6 +784,23 @@ Emitted every 2 seconds after enabling it by calling SetHeartbeat. | `stats` | _OBSStats_ | OBS Stats | +--- + +### BroadcastCustomMessage + + +- Added in v4.7.0 + +A custom broadcast message was received + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `realm` | _String_ | Identifier provided by the sender | +| `data` | _Object_ | User-defined data | + + --- ## Sources @@ -839,6 +965,24 @@ A filter was removed from a source. | `filterType` | _String_ | Filter type | +--- + +### SourceFilterVisibilityChanged + + +- Added in v4.7.0 + +The visibility/enabled state of a filter changed + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filterName` | _String_ | Filter name | +| `filterEnabled` | _Boolean_ | New filter state | + + --- ### SourceFiltersReordered @@ -932,6 +1076,25 @@ An item's visibility has been toggled. | `item-visible` | _boolean_ | New visibility state of the item. | +--- + +### SceneItemLockChanged + + +- Unreleased + +An item's locked status has been toggled. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | Name of the scene. | +| `item-name` | _String_ | Name of the item in the scene. | +| `item-id` | _int_ | Scene item ID | +| `item-locked` | _boolean_ | New locked state of the item. | + + --- ### SceneItemTransformChanged @@ -1061,6 +1224,7 @@ _No specified parameters._ | `obs-websocket-version` | _String_ | obs-websocket plugin version. | | `obs-studio-version` | _String_ | OBS Studio program version. | | `available-requests` | _String_ | List of available request types, formatted as a comma-separated list string (e.g. : "Method1,Method2,Method3"). | +| `supported-image-export-formats` | _String_ | List of supported formats for features that use image export (like the TakeSourceScreenshot request type) formatted as a comma-separated list string | --- @@ -1186,6 +1350,27 @@ _No specified parameters._ | `stats` | _OBSStats_ | OBS stats | +--- + +### BroadcastCustomMessage + + +- Added in v4.7.0 + +Broadcast custom message to all connected WebSocket clients + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `realm` | _String_ | Identifier to be choosen by the client | +| `data` | _Object_ | User-defined data | + + +**Response Items:** + +_No additional response items._ + --- ### GetVideoInfo @@ -1214,6 +1399,115 @@ _No specified parameters._ | `colorRange` | _String_ | Color range (full or partial) | +--- + +### OpenProjector + + +- Unreleased + +Open a projector window or create a projector on a monitor. Requires OBS v24.0.4 or newer. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `type` | _String (Optional)_ | Type of projector: Preview (default), Source, Scene, StudioProgram, or Multiview (case insensitive). | +| `monitor` | _int (Optional)_ | Monitor to open the projector on. If -1 or omitted, opens a window. | +| `geometry` | _String (Optional)_ | Size and position of the projector window (only if monitor is -1). Encoded in Base64 using Qt's geometry encoding (https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to OBS's saved projectors. | +| `name` | _String (Optional)_ | Name of the source or scene to be displayed (ignored for other projector types). | + + +**Response Items:** + +_No additional response items._ + +--- + +## Outputs + +### ListOutputs + + +- Added in v4.7.0 + +List existing outputs + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputs` | _Array<Output>_ | Outputs list | + + +--- + +### GetOutputInfo + + +- Added in v4.7.0 + +Get information about a single output + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputName` | _String_ | Output name | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputInfo` | _Output_ | Output info | + + +--- + +### StartOutput + + +- Added in v4.7.0 + +Start an output + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputName` | _String_ | Output name | + + +**Response Items:** + +_No additional response items._ + +--- + +### StopOutput + + +- Added in v4.7.0 + +Stop an output + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputName` | _String_ | Output name | +| `force` | _boolean (optional)_ | Force stop (default: false) | + + +**Response Items:** + +_No additional response items._ + --- ## Profiles @@ -1333,6 +1627,42 @@ _No additional response items._ --- +### PauseRecording + + +- Added in v4.7.0 + +Pause the current recording. +Returns an error if recording is not active or already paused. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### ResumeRecording + + +- Added in v4.7.0 + +Resume/unpause the current recording (if paused). +Returns an error if recording is not active or not paused. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + ### SetRecordingFolder @@ -1550,6 +1880,7 @@ Coordinates are relative to the item's parent (the scene or group it belongs to) | `crop.bottom` | _int_ | The number of pixels cropped off the bottom of the source before scaling. | | `crop.left` | _int_ | The number of pixels cropped off the left of the source before scaling. | | `visible` | _bool_ | If the source is visible. | +| `muted` | _bool_ | If the source is muted. | | `locked` | _bool_ | If the source's transform is locked. | | `bounds.type` | _String_ | Type of bounding box. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE". | | `bounds.alignment` | _int_ | Alignment of the bounding box. | @@ -1559,6 +1890,9 @@ Coordinates are relative to the item's parent (the scene or group it belongs to) | `sourceHeight` | _int_ | Base source (without scaling) of the source | | `width` | _double_ | Scene item width (base source width multiplied by the horizontal scaling factor) | | `height` | _double_ | Scene item height (base source height multiplied by the vertical scaling factor) | +| `alignment` | _int_ | The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis. | +| `parentGroupName` | _String (optional)_ | Name of the item's parent (if this item belongs to a group) | +| `groupChildren` | _Array<SceneItemTransform> (optional)_ | List of children (if this item is a group) | --- @@ -2400,11 +2734,39 @@ List filters applied to a source | Name | Type | Description | | ---- | :---: | ------------| | `filters` | _Array<Object>_ | List of filters for the specified source | +| `filters.*.enabled` | _Boolean_ | Filter status (enabled or not) | | `filters.*.type` | _String_ | Filter type | | `filters.*.name` | _String_ | Filter name | | `filters.*.settings` | _Object_ | Filter settings | +--- + +### GetSourceFilterInfo + + +- Added in v4.7.0 + +List filters applied to a source + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filterName` | _String_ | Source filter name | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `enabled` | _Boolean_ | Filter status (enabled or not) | +| `type` | _String_ | Filter type | +| `name` | _String_ | Filter name | +| `settings` | _Object_ | Filter settings | + + --- ### AddFilterToSource @@ -2511,6 +2873,28 @@ Update settings of a filter | `filterSettings` | _Object_ | New settings. These will be merged to the current filter settings. | +**Response Items:** + +_No additional response items._ + +--- + +### SetSourceFilterVisibility + + +- Added in v4.7.0 + +Change the visibility/enabled state of a filter + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filterName` | _String_ | Source filter name | +| `filterEnabled` | _Boolean_ | New filter state | + + **Response Items:** _No additional response items._ @@ -2533,7 +2917,7 @@ preserved if only one of these two parameters is specified. | Name | Type | Description | | ---- | :---: | ------------| -| `sourceName` | _String_ | Source name | +| `sourceName` | _String_ | Source name. Note that, since scenes are also sources, you can also provide a scene name. | | `embedPictureFormat` | _String (optional)_ | Format of the Data URI encoded picture. Can be "png", "jpg", "jpeg" or "bmp" (or any other value supported by Qt's Image module) | | `saveToFilePath` | _String (optional)_ | Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path. | | `width` | _int (optional)_ | Screenshot width. Defaults to the source's base width. | @@ -2612,9 +2996,9 @@ Will return an `error` if streaming is already active. | `stream.settings` | _Object (optional)_ | Settings for the stream. | | `stream.settings.server` | _String (optional)_ | The publish URL. | | `stream.settings.key` | _String (optional)_ | The publish key of the stream. | -| `stream.settings.use-auth` | _boolean (optional)_ | Indicates whether authentication should be used when connecting to the streaming server. | -| `stream.settings.username` | _String (optional)_ | If authentication is enabled, the username for the streaming server. Ignored if `use-auth` is not set to `true`. | -| `stream.settings.password` | _String (optional)_ | If authentication is enabled, the password for the streaming server. Ignored if `use-auth` is not set to `true`. | +| `stream.settings.use_auth` | _boolean (optional)_ | Indicates whether authentication should be used when connecting to the streaming server. | +| `stream.settings.username` | _String (optional)_ | If authentication is enabled, the username for the streaming server. Ignored if `use_auth` is not set to `true`. | +| `stream.settings.password` | _String (optional)_ | If authentication is enabled, the password for the streaming server. Ignored if `use_auth` is not set to `true`. | **Response Items:** @@ -2656,7 +3040,7 @@ Sets one or more attributes of the current streaming server settings. Any option | `settings` | _Object_ | The actual settings of the stream. | | `settings.server` | _String (optional)_ | The publish URL. | | `settings.key` | _String (optional)_ | The publish key. | -| `settings.use-auth` | _boolean (optional)_ | Indicates whether authentication should be used when connecting to the streaming server. | +| `settings.use_auth` | _boolean (optional)_ | Indicates whether authentication should be used when connecting to the streaming server. | | `settings.username` | _String (optional)_ | The username for the streaming service. | | `settings.password` | _String (optional)_ | The password for the streaming service. | | `save` | _boolean_ | Persist the settings to disk. | @@ -2687,9 +3071,9 @@ _No specified parameters._ | `settings` | _Object_ | Stream settings object. | | `settings.server` | _String_ | The publish URL. | | `settings.key` | _String_ | The publish key of the stream. | -| `settings.use-auth` | _boolean_ | Indicates whether authentication should be used when connecting to the streaming server. | -| `settings.username` | _String_ | The username to use when accessing the streaming server. Only present if `use-auth` is `true`. | -| `settings.password` | _String_ | The password to use when accessing the streaming server. Only present if `use-auth` is `true`. | +| `settings.use_auth` | _boolean_ | Indicates whether authentication should be used when connecting to the streaming server. | +| `settings.username` | _String_ | The username to use when accessing the streaming server. Only present if `use_auth` is `true`. | +| `settings.password` | _String_ | The password to use when accessing the streaming server. Only present if `use_auth` is `true`. | --- diff --git a/docs/partials/introduction.md b/docs/partials/introduction.md index a3e3bf2a..c8de4503 100644 --- a/docs/partials/introduction.md +++ b/docs/partials/introduction.md @@ -1,4 +1,4 @@ -# obs-websocket 4.6.0 protocol reference +# obs-websocket 4.7.0 protocol reference # General Introduction Messages are exchanged between the client and the server as JSON objects. diff --git a/src/Utils.cpp b/src/Utils.cpp index 9212d993..978162be 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -16,12 +16,15 @@ You should have received a copy of the GNU General Public License along with this program. If not, see */ +#include #include #include #include #include #include +#include + #include "obs-websocket.h" #include "Utils.h" @@ -101,9 +104,11 @@ obs_data_array_t* Utils::GetSceneItems(obs_source_t* source) { * @typedef {Object} `SceneItem` An OBS Scene Item. * @property {Number} `cy` * @property {Number} `cx` + * @property {Number} `alignment` The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis. * @property {String} `name` The name of this Scene Item. * @property {int} `id` Scene item ID * @property {Boolean} `render` Whether or not this Scene Item is set to "visible". + * @property {Boolean} `muted` Whether or not this Scene Item is muted. * @property {Boolean} `locked` Whether or not this Scene Item is locked and can't be moved around * @property {Number} `source_cx` * @property {Number} `source_cy` @@ -143,6 +148,8 @@ obs_data_t* Utils::GetSceneItemData(obs_sceneitem_t* item) { obs_data_set_double(data, "y", pos.y); obs_data_set_int(data, "source_cx", (int)item_width); obs_data_set_int(data, "source_cy", (int)item_height); + obs_data_set_bool(data, "muted", obs_source_muted(itemSource)); + obs_data_set_int(data, "alignment", (int)obs_sceneitem_get_alignment(item)); obs_data_set_double(data, "cx", item_width * scale.x); obs_data_set_double(data, "cy", item_height * scale.y); obs_data_set_bool(data, "render", obs_sceneitem_visible(item)); @@ -173,23 +180,39 @@ obs_data_t* Utils::GetSceneItemData(obs_sceneitem_t* item) { return data; } -obs_sceneitem_t* Utils::GetSceneItemFromItem(obs_source_t* source, obs_data_t* item) { - OBSSceneItem sceneItem; - if (obs_data_has_user_value(item, "id")) { - sceneItem = GetSceneItemFromId(source, obs_data_get_int(item, "id")); - if (obs_data_has_user_value(item, "name") && - (QString)obs_source_get_name(obs_sceneitem_get_source(sceneItem)) != - (QString)obs_data_get_string(item, "name")) { - return nullptr; - } - } - else if (obs_data_has_user_value(item, "name")) { - sceneItem = GetSceneItemFromName(source, obs_data_get_string(item, "name")); - } - return sceneItem; +obs_sceneitem_t* Utils::GetSceneItemFromItem(obs_scene_t* scene, obs_data_t* itemInfo) { + if (!scene) { + return nullptr; + } + + OBSDataItemAutoRelease idInfoItem = obs_data_item_byname(itemInfo, "id"); + int id = obs_data_item_get_int(idInfoItem); + + OBSDataItemAutoRelease nameInfoItem = obs_data_item_byname(itemInfo, "name"); + const char* name = obs_data_item_get_string(nameInfoItem); + + if (idInfoItem) { + obs_sceneitem_t* sceneItem = GetSceneItemFromId(scene, id); + obs_source_t* sceneItemSource = obs_sceneitem_get_source(sceneItem); + + QString sceneItemName = obs_source_get_name(sceneItemSource); + if (nameInfoItem && (QString(name) != sceneItemName)) { + return nullptr; + } + + return sceneItem; + } else if (nameInfoItem) { + return GetSceneItemFromName(scene, name); + } + + return nullptr; } -obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name) { +obs_sceneitem_t* Utils::GetSceneItemFromName(obs_scene_t* scene, QString name) { + if (!scene) { + return nullptr; + } + struct current_search { QString query; obs_sceneitem_t* result; @@ -199,11 +222,6 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name) current_search search; search.query = name; search.result = nullptr; - search.enumCallback = nullptr; - - OBSScene scene = obs_scene_from_source(source); - if (!scene) - return nullptr; search.enumCallback = []( obs_scene_t* scene, @@ -236,10 +254,13 @@ obs_sceneitem_t* Utils::GetSceneItemFromName(obs_source_t* source, QString name) return search.result; } -// TODO refactor this to unify it with GetSceneItemFromName -obs_sceneitem_t* Utils::GetSceneItemFromId(obs_source_t* source, size_t id) { +obs_sceneitem_t* Utils::GetSceneItemFromId(obs_scene_t* scene, int64_t id) { + if (!scene) { + return nullptr; + } + struct current_search { - size_t query; + int query; obs_sceneitem_t* result; bool (*enumCallback)(obs_scene_t*, obs_sceneitem_t*, void*); }; @@ -247,21 +268,16 @@ obs_sceneitem_t* Utils::GetSceneItemFromId(obs_source_t* source, size_t id) { current_search search; search.query = id; search.result = nullptr; - search.enumCallback = nullptr; - - OBSScene scene = obs_scene_from_source(source); - if (!scene) - return nullptr; search.enumCallback = []( - obs_scene_t* scene, - obs_sceneitem_t* currentItem, - void* param) + obs_scene_t* scene, + obs_sceneitem_t* currentItem, + void* param) { current_search* search = reinterpret_cast(param); if (obs_sceneitem_is_group(currentItem)) { - obs_sceneitem_group_enum_items(currentItem, search->enumCallback, param); + obs_sceneitem_group_enum_items(currentItem, search->enumCallback, search); if (search->result) { return false; } @@ -319,17 +335,19 @@ obs_source_t* Utils::GetTransitionFromName(QString searchName) { return foundTransition; } -obs_source_t* Utils::GetSceneFromNameOrCurrent(QString sceneName) { +obs_scene_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; + // increase the returned source's refcount + OBSSourceAutoRelease sceneSource = nullptr; - if (sceneName.isEmpty() || sceneName.isNull()) - scene = obs_frontend_get_current_scene(); - else - scene = obs_get_source_by_name(sceneName.toUtf8()); + if (sceneName.isEmpty() || sceneName.isNull()) { + sceneSource = obs_frontend_get_current_scene(); + } + else { + sceneSource = obs_get_source_by_name(sceneName.toUtf8()); + } - return scene; + return obs_scene_from_source(sceneSource); } obs_data_array_t* Utils::GetScenes() { @@ -362,18 +380,37 @@ QSpinBox* Utils::GetTransitionDurationControl() { return window->findChild("transitionDuration"); } -int Utils::GetTransitionDuration() { - QSpinBox* control = GetTransitionDurationControl(); - if (control) - return control->value(); - else +int Utils::GetTransitionDuration(obs_source_t* transition) { + if (!transition || obs_source_get_type(transition) != OBS_SOURCE_TYPE_TRANSITION) { return -1; -} + } -void Utils::SetTransitionDuration(int ms) { - QSpinBox* control = GetTransitionDurationControl(); - if (control && ms >= 0) - control->setValue(ms); + QString transitionKind = obs_source_get_id(transition); + if (transitionKind == "cut_transition") { + // If this is a Cut transition, return 0 + return 0; + } + + if (obs_transition_fixed(transition)) { + // If this transition has a fixed duration (such as a Stinger), + // we don't currently have a way of retrieving that number. + // For now, return -1 to indicate that we don't know the actual duration. + return -1; + } + + OBSSourceAutoRelease destinationScene = obs_transition_get_active_source(transition); + OBSDataAutoRelease destinationSettings = obs_source_get_private_settings(destinationScene); + + // Detect if transition is the global transition or a transition override. + // Fetching the duration is different depending on the case. + obs_data_item_t* transitionDurationItem = obs_data_item_byname(destinationSettings, "transition_duration"); + int duration = ( + transitionDurationItem + ? obs_data_item_get_int(transitionDurationItem) + : obs_frontend_get_transition_duration() + ); + + return duration; } bool Utils::SetTransitionByName(QString transitionName) { @@ -387,38 +424,35 @@ bool Utils::SetTransitionByName(QString transitionName) { } } -QPushButton* Utils::GetPreviewModeButtonControl() { - QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); - return main->findChild("modeSwitch"); -} +obs_data_t* Utils::GetTransitionData(obs_source_t* transition) { + int duration = Utils::GetTransitionDuration(transition); + if (duration < 0) { + blog(LOG_WARNING, "GetTransitionData: duration is negative !"); + } -QLayout* Utils::GetPreviewLayout() { - QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); - return main->findChild("previewLayout"); -} + OBSSourceAutoRelease sourceScene = obs_transition_get_source(transition, OBS_TRANSITION_SOURCE_A); + OBSSourceAutoRelease destinationScene = obs_transition_get_active_source(transition); -void Utils::TransitionToProgram() { - if (!obs_frontend_preview_program_mode_active()) - return; + obs_data_t* transitionData = obs_data_create(); + obs_data_set_string(transitionData, "name", obs_source_get_name(transition)); + obs_data_set_string(transitionData, "type", obs_source_get_id(transition)); + obs_data_set_int(transitionData, "duration", duration); - // WARNING : if the layout created in OBS' CreateProgramOptions() changes - // then this won't work as expected + // When a transition starts and while it is running, SOURCE_A is the source scene + // and SOURCE_B is the destination scene. + // Before the transition_end event is triggered on a transition, the destination scene + // goes into SOURCE_A and SOURCE_B becomes null. This means that, in transition_stop + // we don't know what was the source scene + // TODO fix this in libobs - QMainWindow* main = (QMainWindow*)obs_frontend_get_main_window(); + bool isTransitionEndEvent = (sourceScene == destinationScene); + if (!isTransitionEndEvent) { + obs_data_set_string(transitionData, "from-scene", obs_source_get_name(sourceScene)); + } + + obs_data_set_string(transitionData, "to-scene", obs_source_get_name(destinationScene)); - // The program options widget is the second item in the left-to-right layout - QWidget* programOptions = GetPreviewLayout()->itemAt(1)->widget(); - - // The "Transition" button lies in the mainButtonLayout - // which is the first itemin the program options' layout - QLayout* mainButtonLayout = programOptions->layout()->itemAt(1)->layout(); - QWidget* transitionBtnWidget = mainButtonLayout->itemAt(0)->widget(); - - // Try to cast that widget into a button - QPushButton* transitionBtn = qobject_cast(transitionBtnWidget); - - // Perform a click on that button - transitionBtn->click(); + return transitionData; } QString Utils::OBSVersionString() { @@ -588,7 +622,7 @@ void Utils::StartReplayBuffer() { obs_output_t* rpOutput = obs_frontend_get_replay_buffer_output(); OBSData outputHotkeys = obs_hotkeys_save_output(rpOutput); - OBSData dummyBinding = obs_data_create(); + OBSDataAutoRelease 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); @@ -744,6 +778,19 @@ obs_data_t* Utils::GetSceneItemPropertiesData(obs_sceneitem_t* sceneItem) { return data; } +obs_data_t* Utils::GetSourceFilterInfo(obs_source_t* filter, bool includeSettings) +{ + obs_data_t* data = obs_data_create(); + obs_data_set_bool(data, "enabled", obs_source_enabled(filter)); + obs_data_set_string(data, "type", obs_source_get_id(filter)); + obs_data_set_string(data, "name", obs_source_get_name(filter)); + if (includeSettings) { + OBSDataAutoRelease settings = obs_source_get_settings(filter); + obs_data_set_obj(data, "settings", settings); + } + return data; +} + obs_data_array_t* Utils::GetSourceFiltersList(obs_source_t* source, bool includeSettings) { struct enum_params { @@ -764,14 +811,36 @@ obs_data_array_t* Utils::GetSourceFiltersList(obs_source_t* source, bool include { auto enumParams = reinterpret_cast(param); - OBSDataAutoRelease filter = obs_data_create(); - obs_data_set_string(filter, "type", obs_source_get_id(child)); - obs_data_set_string(filter, "name", obs_source_get_name(child)); - if (enumParams->includeSettings) { - obs_data_set_obj(filter, "settings", obs_source_get_settings(child)); - } - obs_data_array_push_back(enumParams->filters, filter); + OBSDataAutoRelease filterData = Utils::GetSourceFilterInfo(child, enumParams->includeSettings); + obs_data_array_push_back(enumParams->filters, filterData); }, &enumParams); return enumParams.filters; } + +void getPauseRecordingFunctions(RecordingPausedFunction* recPausedFuncPtr, PauseRecordingFunction* pauseRecFuncPtr) +{ + void* frontendApi = os_dlopen("obs-frontend-api"); + + if (recPausedFuncPtr) { + *recPausedFuncPtr = (RecordingPausedFunction)os_dlsym(frontendApi, "obs_frontend_recording_paused"); + } + + if (pauseRecFuncPtr) { + *pauseRecFuncPtr = (PauseRecordingFunction)os_dlsym(frontendApi, "obs_frontend_recording_pause"); + } +} + +QString Utils::nsToTimestamp(uint64_t ns) +{ + uint64_t ms = ns / 1000000ULL; + uint64_t secs = ms / 1000ULL; + uint64_t minutes = secs / 60ULL; + + uint64_t hoursPart = minutes / 60ULL; + uint64_t minutesPart = minutes % 60ULL; + uint64_t secsPart = secs % 60ULL; + uint64_t msPart = ms % 1000ULL; + + return QString::asprintf("%02" PRIu64 ":%02" PRIu64 ":%02" PRIu64 ".%03" PRIu64, hoursPart, minutesPart, secsPart, msPart); +} diff --git a/src/Utils.h b/src/Utils.h index 65498a96..5934ad4e 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -31,55 +31,57 @@ with this program. If not, see #include #include -class Utils { - public: - static obs_data_array_t* StringListToArray(char** strings, const char* key); - static obs_data_array_t* GetSceneItems(obs_source_t* source); - static obs_data_t* GetSceneItemData(obs_sceneitem_t* item); - static obs_sceneitem_t* GetSceneItemFromName( - obs_source_t* source, QString name); - static obs_sceneitem_t* GetSceneItemFromId(obs_source_t* source, size_t id); - static obs_sceneitem_t* GetSceneItemFromItem(obs_source_t* source, obs_data_t* item); - static obs_source_t* GetTransitionFromName(QString transitionName); - static obs_source_t* GetSceneFromNameOrCurrent(QString sceneName); - static obs_data_t* GetSceneItemPropertiesData(obs_sceneitem_t* item); +typedef void(*PauseRecordingFunction)(bool); +typedef bool(*RecordingPausedFunction)(); - static obs_data_array_t* GetSourceFiltersList(obs_source_t* source, bool includeSettings); +namespace Utils { + obs_data_array_t* StringListToArray(char** strings, const char* key); + obs_data_array_t* GetSceneItems(obs_source_t* source); + obs_data_t* GetSceneItemData(obs_sceneitem_t* item); - static bool IsValidAlignment(const uint32_t alignment); + // These two functions support nested lookup into groups + obs_sceneitem_t* GetSceneItemFromName(obs_scene_t* scene, QString name); + obs_sceneitem_t* GetSceneItemFromId(obs_scene_t* scene, int64_t id); - static obs_data_array_t* GetScenes(); - static obs_data_t* GetSceneData(obs_source_t* source); + obs_sceneitem_t* GetSceneItemFromItem(obs_scene_t* scene, obs_data_t* item); + obs_scene_t* GetSceneFromNameOrCurrent(QString sceneName); + obs_data_t* GetSceneItemPropertiesData(obs_sceneitem_t* item); + + obs_data_t* GetSourceFilterInfo(obs_source_t* filter, bool includeSettings); + obs_data_array_t* GetSourceFiltersList(obs_source_t* source, bool includeSettings); + + bool IsValidAlignment(const uint32_t alignment); + + obs_data_array_t* GetScenes(); + obs_data_t* GetSceneData(obs_source_t* source); // TODO contribute a proper frontend API method for this to OBS and remove this hack - static QSpinBox* GetTransitionDurationControl(); - static int GetTransitionDuration(); - static void SetTransitionDuration(int ms); + QSpinBox* GetTransitionDurationControl(); + int GetTransitionDuration(obs_source_t* transition); + obs_source_t* GetTransitionFromName(QString transitionName); + bool SetTransitionByName(QString transitionName); + obs_data_t* GetTransitionData(obs_source_t* transition); - static bool SetTransitionByName(QString transitionName); + QString OBSVersionString(); - static QPushButton* GetPreviewModeButtonControl(); - static QLayout* GetPreviewLayout(); - - // TODO contribute a proper frontend API method for this to OBS and remove this hack - static void TransitionToProgram(); - - static QString OBSVersionString(); - - static QSystemTrayIcon* GetTrayIcon(); - static void SysTrayNotify( + QSystemTrayIcon* GetTrayIcon(); + void SysTrayNotify( QString text, QSystemTrayIcon::MessageIcon n, QString title = QString("obs-websocket")); - static const char* GetRecordingFolder(); - static bool SetRecordingFolder(const char* path); + const char* GetRecordingFolder(); + bool SetRecordingFolder(const char* path); - static QString ParseDataToQueryString(obs_data_t* data); - static obs_hotkey_t* FindHotkeyByName(QString name); - static bool ReplayBufferEnabled(); - static void StartReplayBuffer(); - static bool IsRPHotkeySet(); - static const char* GetFilenameFormatting(); - static bool SetFilenameFormatting(const char* filenameFormatting); + QString ParseDataToQueryString(obs_data_t* data); + obs_hotkey_t* FindHotkeyByName(QString name); + + bool ReplayBufferEnabled(); + void StartReplayBuffer(); + bool IsRPHotkeySet(); + + const char* GetFilenameFormatting(); + bool SetFilenameFormatting(const char* filenameFormatting); + + QString nsToTimestamp(uint64_t ns); }; diff --git a/src/WSEvents.cpp b/src/WSEvents.cpp index 5be8e9eb..6010275f 100644 --- a/src/WSEvents.cpp +++ b/src/WSEvents.cpp @@ -17,45 +17,21 @@ * with this program. If not, see */ +#include #include +#include #include -#include "Config.h" -#include "Utils.h" #include "WSEvents.h" #include "obs-websocket.h" +#include "Config.h" +#include "Utils.h" +#include "rpc/RpcEvent.h" #define STATUS_INTERVAL 2000 -bool transitionIsCut(obs_source_t* transition) { - if (!transition) - return false; - - if (obs_source_get_type(transition) == OBS_SOURCE_TYPE_TRANSITION - && QString(obs_source_get_id(transition)) == "cut_transition") { - return true; - } - return false; -} - -const char* nsToTimestamp(uint64_t ns) { - uint64_t ms = ns / (1000 * 1000); - uint64_t secs = ms / 1000; - uint64_t minutes = secs / 60; - - uint64_t hoursPart = minutes / 60; - uint64_t minutesPart = minutes % 60; - uint64_t secsPart = secs % 60; - uint64_t msPart = ms % 1000; - - char* ts = (char*)bmalloc(64); - sprintf(ts, "%02lu:%02lu:%02lu.%03lu", hoursPart, minutesPart, secsPart, msPart); - - return ts; -} - const char* sourceTypeToString(obs_source_type type) { switch (type) { case OBS_SOURCE_TYPE_INPUT: @@ -86,7 +62,8 @@ const char* calldata_get_string(const calldata_t* data, const char* name) { WSEvents::WSEvents(WSServerPtr srv) : _srv(srv), _streamStarttime(0), - _recStarttime(0), + _lastBytesSent(0), + _lastBytesSentTime(0), HeartbeatIsActive(false), pulse(false) { @@ -143,118 +120,131 @@ void WSEvents::FrontendEventHandler(enum obs_frontend_event event, void* private return; } - if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) { - owner->hookTransitionBeginEvent(); - } - else if (event == OBS_FRONTEND_EVENT_SCENE_CHANGED) { - owner->OnSceneChange(); - } - else if (event == OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED) { - owner->OnSceneListChange(); - } - else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED) { - owner->hookTransitionBeginEvent(); - owner->OnSceneCollectionChange(); - } - else if (event == OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED) { - owner->OnSceneCollectionListChange(); - } - else if (event == OBS_FRONTEND_EVENT_TRANSITION_CHANGED) { - owner->OnTransitionChange(); - } - else if (event == OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED) { - owner->hookTransitionBeginEvent(); - owner->OnTransitionListChange(); - } - else if (event == OBS_FRONTEND_EVENT_PROFILE_CHANGED) { - owner->OnProfileChange(); - } - else if (event == OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED) { - owner->OnProfileListChange(); - } - else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTING) { - owner->OnStreamStarting(); - } - else if (event == OBS_FRONTEND_EVENT_STREAMING_STARTED) { - owner->streamStatusTimer.start(STATUS_INTERVAL); - owner->StreamStatus(); + switch (event) { + case OBS_FRONTEND_EVENT_FINISHED_LOADING: + owner->hookTransitionPlaybackEvents(); + break; + + case OBS_FRONTEND_EVENT_SCENE_CHANGED: + owner->OnSceneChange(); + break; - owner->OnStreamStarted(); - } - else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPING) { - owner->streamStatusTimer.stop(); - owner->OnStreamStopping(); - } - else if (event == OBS_FRONTEND_EVENT_STREAMING_STOPPED) { - owner->OnStreamStopped(); - } - else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTING) { - owner->OnRecordingStarting(); - } - else if (event == OBS_FRONTEND_EVENT_RECORDING_STARTED) { - owner->OnRecordingStarted(); - } - else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPING) { - owner->OnRecordingStopping(); - } - else if (event == OBS_FRONTEND_EVENT_RECORDING_STOPPED) { - owner->OnRecordingStopped(); - } - else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING) { - owner->OnReplayStarting(); - } - else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED) { - owner->OnReplayStarted(); - } - else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING) { - owner->OnReplayStopping(); - } - else if (event == OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED) { - 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_PREVIEW_SCENE_CHANGED) { - owner->OnPreviewSceneChanged(); - } - else if (event == OBS_FRONTEND_EVENT_EXIT) { - owner->unhookTransitionBeginEvent(); - owner->OnExit(); + case OBS_FRONTEND_EVENT_SCENE_LIST_CHANGED: + owner->OnSceneListChange(); + break; + + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_CHANGED: + owner->hookTransitionPlaybackEvents(); + owner->OnSceneCollectionChange(); + break; + + case OBS_FRONTEND_EVENT_SCENE_COLLECTION_LIST_CHANGED: + owner->OnSceneCollectionListChange(); + break; + + case OBS_FRONTEND_EVENT_TRANSITION_CHANGED: + owner->OnTransitionChange(); + break; + + case OBS_FRONTEND_EVENT_TRANSITION_LIST_CHANGED: + owner->hookTransitionPlaybackEvents(); + owner->OnTransitionListChange(); + break; + + case OBS_FRONTEND_EVENT_PROFILE_CHANGED: + owner->OnProfileChange(); + break; + + case OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED: + owner->OnProfileListChange(); + break; + + case OBS_FRONTEND_EVENT_STREAMING_STARTING: + owner->OnStreamStarting(); + break; + + case OBS_FRONTEND_EVENT_STREAMING_STARTED: + owner->streamStatusTimer.start(STATUS_INTERVAL); + owner->StreamStatus(); + owner->OnStreamStarted(); + break; + + case OBS_FRONTEND_EVENT_STREAMING_STOPPING: + owner->streamStatusTimer.stop(); + owner->OnStreamStopping(); + break; + + case OBS_FRONTEND_EVENT_STREAMING_STOPPED: + owner->OnStreamStopped(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_STARTING: + owner->OnRecordingStarting(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_STARTED: + owner->OnRecordingStarted(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_STOPPING: + owner->OnRecordingStopping(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_STOPPED: + owner->OnRecordingStopped(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_PAUSED: + owner->OnRecordingPaused(); + break; + + case OBS_FRONTEND_EVENT_RECORDING_UNPAUSED: + owner->OnRecordingResumed(); + break; + + case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTING: + owner->OnReplayStarting(); + break; + + case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STARTED: + owner->OnReplayStarted(); + break; + + case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPING: + owner->OnReplayStopping(); + break; + + case OBS_FRONTEND_EVENT_REPLAY_BUFFER_STOPPED: + owner->OnReplayStopped(); + break; + + case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED: + owner->OnStudioModeSwitched(true); + break; + + case OBS_FRONTEND_EVENT_STUDIO_MODE_DISABLED: + owner->OnStudioModeSwitched(false); + break; + + case OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED: + owner->OnPreviewSceneChanged(); + break; + + case OBS_FRONTEND_EVENT_EXIT: + owner->unhookTransitionPlaybackEvents(); + owner->OnExit(); + break; } } void WSEvents::broadcastUpdate(const char* updateType, obs_data_t* additionalFields = nullptr) { - OBSDataAutoRelease update = obs_data_create(); - obs_data_set_string(update, "update-type", updateType); + uint64_t streamTime = getStreamingTime(); + uint64_t recordingTime = getRecordingTime(); + RpcEvent event(QString(updateType), streamTime, recordingTime, additionalFields); - const char* ts = nullptr; - if (obs_frontend_streaming_active()) { - ts = nsToTimestamp(os_gettime_ns() - _streamStarttime); - obs_data_set_string(update, "stream-timecode", ts); - bfree((void*)ts); - } - - if (obs_frontend_recording_active()) { - ts = nsToTimestamp(os_gettime_ns() - _recStarttime); - obs_data_set_string(update, "rec-timecode", ts); - bfree((void*)ts); - } - - if (additionalFields) - obs_data_apply(update, additionalFields); - - QString json = obs_data_get_json(update); - _srv->broadcast(json.toStdString()); - - if (GetConfig()->DebugEnabled) { - blog(LOG_INFO, "Update << '%s'", json.toUtf8().constData()); - } + _srv->broadcast(event); } void WSEvents::connectSourceSignals(obs_source_t* source) { @@ -285,6 +275,8 @@ void WSEvents::connectSourceSignals(obs_source_t* source) { signal_handler_connect(sh, "item_remove", OnSceneItemDelete, this); signal_handler_connect(sh, "item_visible", OnSceneItemVisibilityChanged, this); + signal_handler_connect(sh, + "item_locked", OnSceneItemLockChanged, this); signal_handler_connect(sh, "item_transform", OnSceneItemTransform, this); signal_handler_connect(sh, "item_select", OnSceneItemSelected, this); signal_handler_connect(sh, "item_deselect", OnSceneItemDeselected, this); @@ -314,14 +306,38 @@ void WSEvents::disconnectSourceSignals(obs_source_t* source) { signal_handler_disconnect(sh, "item_remove", OnSceneItemDelete, this); signal_handler_disconnect(sh, "item_visible", OnSceneItemVisibilityChanged, this); + signal_handler_disconnect(sh, + "item_locked", OnSceneItemLockChanged, this); signal_handler_disconnect(sh, "item_transform", OnSceneItemTransform, this); signal_handler_disconnect(sh, "item_select", OnSceneItemSelected, this); signal_handler_disconnect(sh, "item_deselect", OnSceneItemDeselected, this); signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this); + signal_handler_disconnect(sh, "transition_stop", OnTransitionEnd, this); + signal_handler_disconnect(sh, "transition_video_stop", OnTransitionVideoEnd, this); } -void WSEvents::hookTransitionBeginEvent() { +void WSEvents::connectFilterSignals(obs_source_t* filter) { + if (!filter) { + return; + } + + signal_handler_t* sh = obs_source_get_signal_handler(filter); + + signal_handler_connect(sh, "enable", OnSourceFilterVisibilityChanged, this); +} + +void WSEvents::disconnectFilterSignals(obs_source_t* filter) { + if (!filter) { + return; + } + + signal_handler_t* sh = obs_source_get_signal_handler(filter); + + signal_handler_disconnect(sh, "enable", OnSourceFilterVisibilityChanged, this); +} + +void WSEvents::hookTransitionPlaybackEvents() { obs_frontend_source_list transitions = {}; obs_frontend_get_transitions(&transitions); @@ -330,12 +346,16 @@ void WSEvents::hookTransitionBeginEvent() { signal_handler_t* sh = obs_source_get_signal_handler(transition); signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this); signal_handler_connect(sh, "transition_start", OnTransitionBegin, this); + signal_handler_disconnect(sh, "transition_stop", OnTransitionEnd, this); + signal_handler_connect(sh, "transition_stop", OnTransitionEnd, this); + signal_handler_disconnect(sh, "transition_video_stop", OnTransitionVideoEnd, this); + signal_handler_connect(sh, "transition_video_stop", OnTransitionVideoEnd, this); } obs_frontend_source_list_free(&transitions); } -void WSEvents::unhookTransitionBeginEvent() { +void WSEvents::unhookTransitionPlaybackEvents() { obs_frontend_source_list transitions = {}; obs_frontend_get_transitions(&transitions); @@ -343,31 +363,41 @@ void WSEvents::unhookTransitionBeginEvent() { obs_source_t* transition = transitions.sources.array[i]; signal_handler_t* sh = obs_source_get_signal_handler(transition); signal_handler_disconnect(sh, "transition_start", OnTransitionBegin, this); + signal_handler_disconnect(sh, "transition_stop", OnTransitionEnd, this); + signal_handler_disconnect(sh, "transition_video_stop", OnTransitionVideoEnd, this); } obs_frontend_source_list_free(&transitions); } -uint64_t WSEvents::GetStreamingTime() { - if (obs_frontend_streaming_active()) - return (os_gettime_ns() - _streamStarttime); - else +uint64_t getOutputRunningTime(obs_output_t* output) { + if (!output || !obs_output_active(output)) { return 0; + } + + video_t* video = obs_output_video(output); + uint64_t frameTimeNs = video_output_get_frame_time(video); + int totalFrames = obs_output_get_total_frames(output); + + return (((uint64_t)totalFrames) * frameTimeNs); } -const char* WSEvents::GetStreamingTimecode() { - return nsToTimestamp(GetStreamingTime()); +uint64_t WSEvents::getStreamingTime() { + OBSOutputAutoRelease streamingOutput = obs_frontend_get_streaming_output(); + return getOutputRunningTime(streamingOutput); } -uint64_t WSEvents::GetRecordingTime() { - if (obs_frontend_recording_active()) - return (os_gettime_ns() - _recStarttime); - else - return 0; +uint64_t WSEvents::getRecordingTime() { + OBSOutputAutoRelease recordingOutput = obs_frontend_get_recording_output(); + return getOutputRunningTime(recordingOutput); } -const char* WSEvents::GetRecordingTimecode() { - return nsToTimestamp(GetRecordingTime()); +QString WSEvents::getStreamingTimecode() { + return Utils::nsToTimestamp(getStreamingTime()); +} + +QString WSEvents::getRecordingTimecode() { + return Utils::nsToTimestamp(getRecordingTime()); } /** @@ -520,6 +550,7 @@ void WSEvents::OnStreamStarting() { void WSEvents::OnStreamStarted() { _streamStarttime = os_gettime_ns(); _lastBytesSent = 0; + broadcastUpdate("StreamStarted"); } @@ -536,7 +567,6 @@ void WSEvents::OnStreamStarted() { void WSEvents::OnStreamStopping() { OBSDataAutoRelease data = obs_data_create(); obs_data_set_bool(data, "preview-only", false); - broadcastUpdate("StreamStopping", data); } @@ -550,6 +580,7 @@ void WSEvents::OnStreamStopping() { */ void WSEvents::OnStreamStopped() { _streamStarttime = 0; + broadcastUpdate("StreamStopped"); } @@ -574,7 +605,6 @@ void WSEvents::OnRecordingStarting() { * @since 0.3 */ void WSEvents::OnRecordingStarted() { - _recStarttime = os_gettime_ns(); broadcastUpdate("RecordingStarted"); } @@ -599,10 +629,33 @@ void WSEvents::OnRecordingStopping() { * @since 0.3 */ void WSEvents::OnRecordingStopped() { - _recStarttime = 0; broadcastUpdate("RecordingStopped"); } +/** + * Current recording paused + * + * @api events + * @name RecordingPaused + * @category recording + * @since 4.7.0 + */ +void WSEvents::OnRecordingPaused() { + broadcastUpdate("RecordingPaused"); +} + +/** + * Current recording resumed + * + * @api events + * @name RecordingResumed + * @category recording + * @since 4.7.0 + */ +void WSEvents::OnRecordingResumed() { + broadcastUpdate("RecordingResumed"); +} + /** * A request to start the replay buffer has been issued. * @@ -694,6 +747,7 @@ void WSEvents::OnExit() { void WSEvents::StreamStatus() { bool streamingActive = obs_frontend_streaming_active(); bool recordingActive = obs_frontend_recording_active(); + bool recordingPaused = obs_frontend_recording_paused(); bool replayBufferActive = obs_frontend_replay_buffer_active(); OBSOutputAutoRelease streamOutput = obs_frontend_get_streaming_output(); @@ -720,9 +774,7 @@ void WSEvents::StreamStatus() { _lastBytesSent = bytesSent; _lastBytesSentTime = bytesSentTime; - uint64_t totalStreamTime = - (os_gettime_ns() - _streamStarttime) / 1000000000; - + uint64_t totalStreamTime = (getStreamingTime() / 1000000000ULL); int totalFrames = obs_output_get_total_frames(streamOutput); int droppedFrames = obs_output_get_frames_dropped(streamOutput); @@ -732,6 +784,7 @@ void WSEvents::StreamStatus() { obs_data_set_bool(data, "streaming", streamingActive); obs_data_set_bool(data, "recording", recordingActive); + obs_data_set_bool(data, "recording-paused", recordingPaused); obs_data_set_bool(data, "replay-buffer-active", replayBufferActive); obs_data_set_int(data, "bytes-per-sec", bytesPerSec); @@ -778,6 +831,7 @@ void WSEvents::Heartbeat() { bool streamingActive = obs_frontend_streaming_active(); bool recordingActive = obs_frontend_recording_active(); + bool recordingPaused = obs_frontend_recording_paused(); OBSDataAutoRelease data = obs_data_create(); OBSOutputAutoRelease recordOutput = obs_frontend_get_recording_output(); @@ -786,23 +840,24 @@ void WSEvents::Heartbeat() { pulse = !pulse; obs_data_set_bool(data, "pulse", pulse); - obs_data_set_string(data, "current-profile", obs_frontend_get_current_profile()); + char* currentProfile = obs_frontend_get_current_profile(); + obs_data_set_string(data, "current-profile", currentProfile); + bfree(currentProfile); 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-time", (getStreamingTime() / 1000000000ULL)); 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); + obs_data_set_bool(data, "recording-paused", recordingPaused); 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-time", (getRecordingTime() / 1000000000ULL)); 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)); } @@ -834,7 +889,10 @@ void WSEvents::TransitionDurationChanged(int ms) { * A transition (other than "cut") has begun. * * @return {String} `name` Transition name. - * @return {int} `duration` Transition duration (in milliseconds). + * @return {String} `type` Transition type. + * @return {int} `duration` Transition duration (in milliseconds). + * Will be -1 for any transition with a fixed duration, + * such as a Stinger, due to limitations of the OBS API. * @return {String} `from-scene` Source scene of the transition * @return {String} `to-scene` Destination scene of the transition * @@ -847,40 +905,66 @@ void WSEvents::OnTransitionBegin(void* param, calldata_t* data) { auto instance = reinterpret_cast(param); OBSSource transition = calldata_get_pointer(data, "source"); - if (!transition) return; - - // Detect if transition is the global transition or a transition override. - // Fetching the duration is different depending on the case. - OBSSourceAutoRelease sourceScene = obs_transition_get_source(transition, OBS_TRANSITION_SOURCE_A); - OBSSourceAutoRelease destinationScene = obs_transition_get_active_source(transition); - OBSDataAutoRelease destinationSettings = obs_source_get_private_settings(destinationScene); - int duration = -1; - if (obs_data_has_default_value(destinationSettings, "transition_duration") || - obs_data_has_user_value(destinationSettings, "transition_duration")) - { - duration = obs_data_get_int(destinationSettings, "transition_duration"); - } else { - duration = Utils::GetTransitionDuration(); - } - - OBSDataAutoRelease fields = obs_data_create(); - obs_data_set_string(fields, "name", obs_source_get_name(transition)); - if (duration >= 0) { - obs_data_set_int(fields, "duration", duration); - } else { - blog(LOG_WARNING, "OnTransitionBegin: duration is negative !"); - } - - if (sourceScene) { - obs_data_set_string(fields, "from-scene", obs_source_get_name(sourceScene)); - } - if (destinationScene) { - obs_data_set_string(fields, "to-scene", obs_source_get_name(destinationScene)); + if (!transition) { + return; } + OBSDataAutoRelease fields = Utils::GetTransitionData(transition); instance->broadcastUpdate("TransitionBegin", fields); } +/** +* A transition (other than "cut") has ended. +* Please note that the `from-scene` field is not available in TransitionEnd. +* +* @return {String} `name` Transition name. +* @return {String} `type` Transition type. +* @return {int} `duration` Transition duration (in milliseconds). +* @return {String} `to-scene` Destination scene of the transition +* +* @api events +* @name TransitionEnd +* @category transitions +* @since 4.8.0 +*/ +void WSEvents::OnTransitionEnd(void* param, calldata_t* data) { + auto instance = reinterpret_cast(param); + + OBSSource transition = calldata_get_pointer(data, "source"); + if (!transition) { + return; + } + + OBSDataAutoRelease fields = Utils::GetTransitionData(transition); + instance->broadcastUpdate("TransitionEnd", fields); +} + +/** +* A stinger transition has finished playing its video. +* +* @return {String} `name` Transition name. +* @return {String} `type` Transition type. +* @return {int} `duration` Transition duration (in milliseconds). +* @return {String} `from-scene` Source scene of the transition +* @return {String} `to-scene` Destination scene of the transition +* +* @api events +* @name TransitionVideoEnd +* @category transitions +* @since 4.8.0 +*/ +void WSEvents::OnTransitionVideoEnd(void* param, calldata_t* data) { + auto instance = reinterpret_cast(param); + + OBSSource transition = calldata_get_pointer(data, "source"); + if (!transition) { + return; + } + + OBSDataAutoRelease fields = Utils::GetTransitionData(transition); + instance->broadcastUpdate("TransitionVideoEnd", fields); +} + /** * A source has been created. A source can be an input, a scene or a transition. * @@ -1138,6 +1222,8 @@ void WSEvents::OnSourceFilterAdded(void* param, calldata_t* data) { if (!filter) { return; } + + self->connectFilterSignals(filter); OBSDataAutoRelease filterSettings = obs_source_get_settings(filter); @@ -1170,6 +1256,11 @@ void WSEvents::OnSourceFilterRemoved(void* param, calldata_t* data) { } obs_source_t* filter = calldata_get_pointer(data, "filter"); + if (!filter) { + return; + } + + self->disconnectFilterSignals(filter); OBSDataAutoRelease fields = obs_data_create(); obs_data_set_string(fields, "sourceName", obs_source_get_name(source)); @@ -1178,6 +1269,36 @@ void WSEvents::OnSourceFilterRemoved(void* param, calldata_t* data) { self->broadcastUpdate("SourceFilterRemoved", fields); } +/** + * The visibility/enabled state of a filter changed + * + * @return {String} `sourceName` Source name + * @return {String} `filterName` Filter name + * @return {Boolean} `filterEnabled` New filter state + * + * @api events + * @name SourceFilterVisibilityChanged + * @category sources + * @since 4.7.0 + */ +void WSEvents::OnSourceFilterVisibilityChanged(void* param, calldata_t* data) { + auto self = reinterpret_cast(param); + + OBSSource source = calldata_get_pointer(data, "source"); + if (!source) { + return; + } + + OBSSource parent = obs_filter_get_parent(source); + + OBSDataAutoRelease fields = obs_data_create(); + obs_data_set_string(fields, "sourceName", obs_source_get_name(parent)); + obs_data_set_string(fields, "filterName", obs_source_get_name(source)); + obs_data_set_bool(fields, "filterEnabled", obs_source_enabled(source)); + + self->broadcastUpdate("SourceFilterVisibilityChanged", fields); +} + /** * Filters in a source have been reordered. * @@ -1354,6 +1475,44 @@ void WSEvents::OnSceneItemVisibilityChanged(void* param, calldata_t* data) { instance->broadcastUpdate("SceneItemVisibilityChanged", fields); } +/** + * An item's locked status has been toggled. + * + * @return {String} `scene-name` Name of the scene. + * @return {String} `item-name` Name of the item in the scene. + * @return {int} `item-id` Scene item ID + * @return {boolean} `item-locked` New locked state of the item. + * + * @api events + * @name SceneItemLockChanged + * @category sources + * @since unreleased + */ +void WSEvents::OnSceneItemLockChanged(void* param, calldata_t* data) { + auto instance = reinterpret_cast(param); + + obs_scene_t* scene = nullptr; + calldata_get_ptr(data, "scene", &scene); + + obs_sceneitem_t* sceneItem = nullptr; + calldata_get_ptr(data, "item", &sceneItem); + + bool locked = false; + calldata_get_bool(data, "locked", &locked); + + const char* sceneName = + obs_source_get_name(obs_scene_get_source(scene)); + const char* sceneItemName = + obs_source_get_name(obs_sceneitem_get_source(sceneItem)); + + OBSDataAutoRelease fields = obs_data_create(); + obs_data_set_string(fields, "scene-name", sceneName); + obs_data_set_string(fields, "item-name", sceneItemName); + obs_data_set_int(fields, "item-id", obs_sceneitem_get_id(sceneItem)); + obs_data_set_bool(fields, "item-locked", locked); + instance->broadcastUpdate("SceneItemLockChanged", fields); +} + /** * An item's transform has been changed. * @@ -1505,6 +1664,25 @@ void WSEvents::OnStudioModeSwitched(bool checked) { broadcastUpdate("StudioModeSwitched", data); } +/** + * A custom broadcast message was received + * + * @return {String} `realm` Identifier provided by the sender + * @return {Object} `data` User-defined data + * + * @api events + * @name BroadcastCustomMessage + * @category general + * @since 4.7.0 + */ +void WSEvents::OnBroadcastCustomMessage(QString realm, obs_data_t* data) { + OBSDataAutoRelease broadcastData = obs_data_create(); + obs_data_set_string(broadcastData, "realm", realm.toUtf8().constData()); + obs_data_set_obj(broadcastData, "data", data); + + broadcastUpdate("BroadcastCustomMessage", broadcastData); +} + /** * @typedef {Object} `OBSStats` * @property {double} `fps` Current framerate. @@ -1530,12 +1708,12 @@ obs_data_t* WSEvents::GetStats() { double averageFrameTime = (double)obs_get_average_frame_time_ns() / 1000000.0; config_t* currentProfile = obs_frontend_get_profile_config(); - QString outputMode = config_get_string(currentProfile, "Output", "Mode"); - QString path = (outputMode == "Advanced") ? + const char* outputMode = config_get_string(currentProfile, "Output", "Mode"); + const char* path = strcmp(outputMode, "Advanced") ? config_get_string(currentProfile, "SimpleOutput", "FilePath") : config_get_string(currentProfile, "AdvOut", "RecFilePath"); - double freeDiskSpace = (double)os_get_free_disk_space(path.toUtf8()) / (1024.0 * 1024.0); + double freeDiskSpace = (double)os_get_free_disk_space(path) / (1024.0 * 1024.0); obs_data_set_double(stats, "fps", obs_get_active_fps()); obs_data_set_int(stats, "render-total-frames", obs_get_total_frames()); diff --git a/src/WSEvents.h b/src/WSEvents.h index 8a6bbec4..06ce7f2c 100644 --- a/src/WSEvents.h +++ b/src/WSEvents.h @@ -40,15 +40,22 @@ public: void connectSourceSignals(obs_source_t* source); void disconnectSourceSignals(obs_source_t* source); - void hookTransitionBeginEvent(); - void unhookTransitionBeginEvent(); + void connectFilterSignals(obs_source_t* filter); + void disconnectFilterSignals(obs_source_t* filter); - uint64_t GetStreamingTime(); - const char* GetStreamingTimecode(); - uint64_t GetRecordingTime(); - const char* GetRecordingTimecode(); + void hookTransitionPlaybackEvents(); + void unhookTransitionPlaybackEvents(); + + uint64_t getStreamingTime(); + uint64_t getRecordingTime(); + + QString getStreamingTimecode(); + QString getRecordingTimecode(); + obs_data_t* GetStats(); + void OnBroadcastCustomMessage(QString realm, obs_data_t* data); + bool HeartbeatIsActive; private slots: @@ -65,7 +72,6 @@ private: bool pulse; uint64_t _streamStarttime; - uint64_t _recStarttime; uint64_t _lastBytesSent; uint64_t _lastBytesSentTime; @@ -93,6 +99,8 @@ private: void OnRecordingStarted(); void OnRecordingStopping(); void OnRecordingStopped(); + void OnRecordingPaused(); + void OnRecordingResumed(); void OnReplayStarting(); void OnReplayStarted(); @@ -108,6 +116,8 @@ private: enum obs_frontend_event event, void* privateData); static void OnTransitionBegin(void* param, calldata_t* data); + static void OnTransitionEnd(void* param, calldata_t* data); + static void OnTransitionVideoEnd(void* param, calldata_t* data); static void OnSourceCreate(void* param, calldata_t* data); static void OnSourceDestroy(void* param, calldata_t* data); @@ -121,12 +131,14 @@ private: static void OnSourceFilterAdded(void* param, calldata_t* data); static void OnSourceFilterRemoved(void* param, calldata_t* data); + static void OnSourceFilterVisibilityChanged(void* param, calldata_t* data); static void OnSourceFilterOrderChanged(void* param, calldata_t* data); static void OnSceneReordered(void* param, calldata_t* data); static void OnSceneItemAdd(void* param, calldata_t* data); static void OnSceneItemDelete(void* param, calldata_t* data); static void OnSceneItemVisibilityChanged(void* param, calldata_t* data); + static void OnSceneItemLockChanged(void* param, calldata_t* data); static void OnSceneItemTransform(void* param, calldata_t* data); static void OnSceneItemSelected(void* param, calldata_t* data); static void OnSceneItemDeselected(void* param, calldata_t* data); diff --git a/src/WSRequestHandler.cpp b/src/WSRequestHandler.cpp index 098b84ee..f2ae6c47 100644 --- a/src/WSRequestHandler.cpp +++ b/src/WSRequestHandler.cpp @@ -17,6 +17,8 @@ * with this program. If not, see */ +#include + #include #include "Config.h" @@ -24,204 +26,149 @@ #include "WSRequestHandler.h" -QHash WSRequestHandler::messageMap { - { "GetVersion", WSRequestHandler::HandleGetVersion }, - { "GetAuthRequired", WSRequestHandler::HandleGetAuthRequired }, - { "Authenticate", WSRequestHandler::HandleAuthenticate }, +using namespace std::placeholders; - { "GetStats", WSRequestHandler::HandleGetStats }, - { "SetHeartbeat", WSRequestHandler::HandleSetHeartbeat }, - { "GetVideoInfo", WSRequestHandler::HandleGetVideoInfo }, +const QHash WSRequestHandler::messageMap { + { "GetVersion", &WSRequestHandler::GetVersion }, + { "GetAuthRequired", &WSRequestHandler::GetAuthRequired }, + { "Authenticate", &WSRequestHandler::Authenticate }, - { "SetFilenameFormatting", WSRequestHandler::HandleSetFilenameFormatting }, - { "GetFilenameFormatting", WSRequestHandler::HandleGetFilenameFormatting }, + { "GetStats", &WSRequestHandler::GetStats }, + { "SetHeartbeat", &WSRequestHandler::SetHeartbeat }, + { "GetVideoInfo", &WSRequestHandler::GetVideoInfo }, + { "OpenProjector", &WSRequestHandler::OpenProjector }, - { "SetCurrentScene", WSRequestHandler::HandleSetCurrentScene }, - { "GetCurrentScene", WSRequestHandler::HandleGetCurrentScene }, - { "GetSceneList", WSRequestHandler::HandleGetSceneList }, + { "SetFilenameFormatting", &WSRequestHandler::SetFilenameFormatting }, + { "GetFilenameFormatting", &WSRequestHandler::GetFilenameFormatting }, - { "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 }, - { "DeleteSceneItem", WSRequestHandler::HandleDeleteSceneItem }, - { "DuplicateSceneItem", WSRequestHandler::HandleDuplicateSceneItem }, - { "ReorderSceneItems", WSRequestHandler::HandleReorderSceneItems }, + { "BroadcastCustomMessage", &WSRequestHandler::BroadcastCustomMessage }, - { "GetStreamingStatus", WSRequestHandler::HandleGetStreamingStatus }, - { "StartStopStreaming", WSRequestHandler::HandleStartStopStreaming }, - { "StartStopRecording", WSRequestHandler::HandleStartStopRecording }, - { "StartStreaming", WSRequestHandler::HandleStartStreaming }, - { "StopStreaming", WSRequestHandler::HandleStopStreaming }, - { "StartRecording", WSRequestHandler::HandleStartRecording }, - { "StopRecording", WSRequestHandler::HandleStopRecording }, + { "SetCurrentScene", &WSRequestHandler::SetCurrentScene }, + { "GetCurrentScene", &WSRequestHandler::GetCurrentScene }, + { "GetSceneList", &WSRequestHandler::GetSceneList }, - { "StartStopReplayBuffer", WSRequestHandler::HandleStartStopReplayBuffer }, - { "StartReplayBuffer", WSRequestHandler::HandleStartReplayBuffer }, - { "StopReplayBuffer", WSRequestHandler::HandleStopReplayBuffer }, - { "SaveReplayBuffer", WSRequestHandler::HandleSaveReplayBuffer }, + { "SetSourceRender", &WSRequestHandler::SetSceneItemRender }, // Retrocompat + { "SetSceneItemRender", &WSRequestHandler::SetSceneItemRender }, + { "SetSceneItemPosition", &WSRequestHandler::SetSceneItemPosition }, + { "SetSceneItemTransform", &WSRequestHandler::SetSceneItemTransform }, + { "SetSceneItemCrop", &WSRequestHandler::SetSceneItemCrop }, + { "GetSceneItemProperties", &WSRequestHandler::GetSceneItemProperties }, + { "SetSceneItemProperties", &WSRequestHandler::SetSceneItemProperties }, + { "ResetSceneItem", &WSRequestHandler::ResetSceneItem }, + { "DeleteSceneItem", &WSRequestHandler::DeleteSceneItem }, + { "DuplicateSceneItem", &WSRequestHandler::DuplicateSceneItem }, + { "ReorderSceneItems", &WSRequestHandler::ReorderSceneItems }, - { "SetRecordingFolder", WSRequestHandler::HandleSetRecordingFolder }, - { "GetRecordingFolder", WSRequestHandler::HandleGetRecordingFolder }, + { "GetStreamingStatus", &WSRequestHandler::GetStreamingStatus }, + { "StartStopStreaming", &WSRequestHandler::StartStopStreaming }, + { "StartStopRecording", &WSRequestHandler::StartStopRecording }, - { "GetTransitionList", WSRequestHandler::HandleGetTransitionList }, - { "GetCurrentTransition", WSRequestHandler::HandleGetCurrentTransition }, - { "SetCurrentTransition", WSRequestHandler::HandleSetCurrentTransition }, - { "SetTransitionDuration", WSRequestHandler::HandleSetTransitionDuration }, - { "GetTransitionDuration", WSRequestHandler::HandleGetTransitionDuration }, + { "StartStreaming", &WSRequestHandler::StartStreaming }, + { "StopStreaming", &WSRequestHandler::StopStreaming }, - { "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 }, - { "TakeSourceScreenshot", WSRequestHandler::HandleTakeSourceScreenshot }, + { "StartRecording", &WSRequestHandler::StartRecording }, + { "StopRecording", &WSRequestHandler::StopRecording }, + { "PauseRecording", &WSRequestHandler::PauseRecording }, + { "ResumeRecording", &WSRequestHandler::ResumeRecording }, - { "GetSourceFilters", WSRequestHandler::HandleGetSourceFilters }, - { "AddFilterToSource", WSRequestHandler::HandleAddFilterToSource }, - { "RemoveFilterFromSource", WSRequestHandler::HandleRemoveFilterFromSource }, - { "ReorderSourceFilter", WSRequestHandler::HandleReorderSourceFilter }, - { "MoveSourceFilter", WSRequestHandler::HandleMoveSourceFilter }, - { "SetSourceFilterSettings", WSRequestHandler::HandleSetSourceFilterSettings }, + { "StartStopReplayBuffer", &WSRequestHandler::StartStopReplayBuffer }, + { "StartReplayBuffer", &WSRequestHandler::StartReplayBuffer }, + { "StopReplayBuffer", &WSRequestHandler::StopReplayBuffer }, + { "SaveReplayBuffer", &WSRequestHandler::SaveReplayBuffer }, - { "SetCurrentSceneCollection", WSRequestHandler::HandleSetCurrentSceneCollection }, - { "GetCurrentSceneCollection", WSRequestHandler::HandleGetCurrentSceneCollection }, - { "ListSceneCollections", WSRequestHandler::HandleListSceneCollections }, + { "SetRecordingFolder", &WSRequestHandler::SetRecordingFolder }, + { "GetRecordingFolder", &WSRequestHandler::GetRecordingFolder }, - { "SetCurrentProfile", WSRequestHandler::HandleSetCurrentProfile }, - { "GetCurrentProfile", WSRequestHandler::HandleGetCurrentProfile }, - { "ListProfiles", WSRequestHandler::HandleListProfiles }, + { "GetTransitionList", &WSRequestHandler::GetTransitionList }, + { "GetCurrentTransition", &WSRequestHandler::GetCurrentTransition }, + { "SetCurrentTransition", &WSRequestHandler::SetCurrentTransition }, + { "SetTransitionDuration", &WSRequestHandler::SetTransitionDuration }, + { "GetTransitionDuration", &WSRequestHandler::GetTransitionDuration }, - { "SetStreamSettings", WSRequestHandler::HandleSetStreamSettings }, - { "GetStreamSettings", WSRequestHandler::HandleGetStreamSettings }, - { "SaveStreamSettings", WSRequestHandler::HandleSaveStreamSettings }, + { "SetVolume", &WSRequestHandler::SetVolume }, + { "GetVolume", &WSRequestHandler::GetVolume }, + { "ToggleMute", &WSRequestHandler::ToggleMute }, + { "SetMute", &WSRequestHandler::SetMute }, + { "GetMute", &WSRequestHandler::GetMute }, + { "SetSyncOffset", &WSRequestHandler::SetSyncOffset }, + { "GetSyncOffset", &WSRequestHandler::GetSyncOffset }, + { "GetSpecialSources", &WSRequestHandler::GetSpecialSources }, + { "GetSourcesList", &WSRequestHandler::GetSourcesList }, + { "GetSourceTypesList", &WSRequestHandler::GetSourceTypesList }, + { "GetSourceSettings", &WSRequestHandler::GetSourceSettings }, + { "SetSourceSettings", &WSRequestHandler::SetSourceSettings }, + { "TakeSourceScreenshot", &WSRequestHandler::TakeSourceScreenshot }, + + { "GetSourceFilters", &WSRequestHandler::GetSourceFilters }, + { "GetSourceFilterInfo", &WSRequestHandler::GetSourceFilterInfo }, + { "AddFilterToSource", &WSRequestHandler::AddFilterToSource }, + { "RemoveFilterFromSource", &WSRequestHandler::RemoveFilterFromSource }, + { "ReorderSourceFilter", &WSRequestHandler::ReorderSourceFilter }, + { "MoveSourceFilter", &WSRequestHandler::MoveSourceFilter }, + { "SetSourceFilterSettings", &WSRequestHandler::SetSourceFilterSettings }, + { "SetSourceFilterVisibility", &WSRequestHandler::SetSourceFilterVisibility }, + + { "SetCurrentSceneCollection", &WSRequestHandler::SetCurrentSceneCollection }, + { "GetCurrentSceneCollection", &WSRequestHandler::GetCurrentSceneCollection }, + { "ListSceneCollections", &WSRequestHandler::ListSceneCollections }, + + { "SetCurrentProfile", &WSRequestHandler::SetCurrentProfile }, + { "GetCurrentProfile", &WSRequestHandler::GetCurrentProfile }, + { "ListProfiles", &WSRequestHandler::ListProfiles }, + + { "SetStreamSettings", &WSRequestHandler::SetStreamSettings }, + { "GetStreamSettings", &WSRequestHandler::GetStreamSettings }, + { "SaveStreamSettings", &WSRequestHandler::SaveStreamSettings }, #if BUILD_CAPTIONS - { "SendCaptions", WSRequestHandler::HandleSendCaptions }, + { "SendCaptions", &WSRequestHandler::SendCaptions }, #endif - { "GetStudioModeStatus", WSRequestHandler::HandleGetStudioModeStatus }, - { "GetPreviewScene", WSRequestHandler::HandleGetPreviewScene }, - { "SetPreviewScene", WSRequestHandler::HandleSetPreviewScene }, - { "TransitionToProgram", WSRequestHandler::HandleTransitionToProgram }, - { "EnableStudioMode", WSRequestHandler::HandleEnableStudioMode }, - { "DisableStudioMode", WSRequestHandler::HandleDisableStudioMode }, - { "ToggleStudioMode", WSRequestHandler::HandleToggleStudioMode }, + { "GetStudioModeStatus", &WSRequestHandler::GetStudioModeStatus }, + { "GetPreviewScene", &WSRequestHandler::GetPreviewScene }, + { "SetPreviewScene", &WSRequestHandler::SetPreviewScene }, + { "TransitionToProgram", &WSRequestHandler::TransitionToProgram }, + { "EnableStudioMode", &WSRequestHandler::EnableStudioMode }, + { "DisableStudioMode", &WSRequestHandler::DisableStudioMode }, + { "ToggleStudioMode", &WSRequestHandler::ToggleStudioMode }, - { "SetTextGDIPlusProperties", WSRequestHandler::HandleSetTextGDIPlusProperties }, - { "GetTextGDIPlusProperties", WSRequestHandler::HandleGetTextGDIPlusProperties }, + { "SetTextGDIPlusProperties", &WSRequestHandler::SetTextGDIPlusProperties }, + { "GetTextGDIPlusProperties", &WSRequestHandler::GetTextGDIPlusProperties }, - { "SetTextFreetype2Properties", WSRequestHandler::HandleSetTextFreetype2Properties }, - { "GetTextFreetype2Properties", WSRequestHandler::HandleGetTextFreetype2Properties }, + { "SetTextFreetype2Properties", &WSRequestHandler::SetTextFreetype2Properties }, + { "GetTextFreetype2Properties", &WSRequestHandler::GetTextFreetype2Properties }, - { "GetBrowserSourceProperties", WSRequestHandler::HandleGetBrowserSourceProperties }, - { "SetBrowserSourceProperties", WSRequestHandler::HandleSetBrowserSourceProperties } + { "GetBrowserSourceProperties", &WSRequestHandler::GetBrowserSourceProperties }, + { "SetBrowserSourceProperties", &WSRequestHandler::SetBrowserSourceProperties }, + + { "ListOutputs", &WSRequestHandler::ListOutputs }, + { "GetOutputInfo", &WSRequestHandler::GetOutputInfo }, + { "StartOutput", &WSRequestHandler::StartOutput }, + { "StopOutput", &WSRequestHandler::StopOutput } }; -QSet WSRequestHandler::authNotRequired { +const QSet WSRequestHandler::authNotRequired { "GetVersion", "GetAuthRequired", "Authenticate" }; WSRequestHandler::WSRequestHandler(ConnectionProperties& connProperties) : - _messageId(0), - _requestType(""), - data(nullptr), _connProperties(connProperties) { } -std::string WSRequestHandler::processIncomingMessage(std::string& textMessage) { - if (GetConfig()->DebugEnabled) { - blog(LOG_INFO, "Request >> '%s'", textMessage.c_str()); - } - - OBSDataAutoRelease responseData = processRequest(textMessage); - std::string response = obs_data_get_json(responseData); - - if (GetConfig()->DebugEnabled) { - blog(LOG_INFO, "Response << '%s'", response.c_str()); - } - - return response; -} - -HandlerResponse WSRequestHandler::processRequest(std::string& textMessage){ - std::string msgContainer(textMessage); - const char* msg = msgContainer.c_str(); - - data = obs_data_create_from_json(msg); - if (!data) { - blog(LOG_ERROR, "invalid JSON payload received for '%s'", msg); - return SendErrorResponse("invalid JSON payload"); - } - - if (!hasField("request-type") || !hasField("message-id")) { - return SendErrorResponse("missing request parameters"); - } - - _requestType = obs_data_get_string(data, "request-type"); - _messageId = obs_data_get_string(data, "message-id"); - +RpcResponse WSRequestHandler::processRequest(const RpcRequest& request){ if (GetConfig()->AuthRequired - && (!authNotRequired.contains(_requestType)) + && (!authNotRequired.contains(request.methodName())) && (!_connProperties.isAuthenticated())) { - return SendErrorResponse("Not Authenticated"); + return RpcResponse::fail(request, "Not Authenticated"); } - HandlerResponse (*handlerFunc)(WSRequestHandler*) = (messageMap[_requestType]); + RpcMethodHandler handlerFunc = messageMap[request.methodName()]; if (!handlerFunc) { - return SendErrorResponse("invalid request type"); + return RpcResponse::fail(request, "invalid request type"); } - return handlerFunc(this); -} - -WSRequestHandler::~WSRequestHandler() { -} - -HandlerResponse WSRequestHandler::SendOKResponse(obs_data_t* additionalFields) { - return SendResponse("ok", additionalFields); -} - -HandlerResponse WSRequestHandler::SendErrorResponse(const char* errorMessage) { - OBSDataAutoRelease fields = obs_data_create(); - obs_data_set_string(fields, "error", errorMessage); - - return SendResponse("error", fields); -} - -HandlerResponse WSRequestHandler::SendErrorResponse(obs_data_t* additionalFields) { - return SendResponse("error", additionalFields); -} - -HandlerResponse WSRequestHandler::SendResponse(const char* status, obs_data_t* fields) { - obs_data_t* response = obs_data_create(); - obs_data_set_string(response, "message-id", _messageId); - obs_data_set_string(response, "status", status); - - if (fields) { - obs_data_apply(response, fields); - } - - return response; -} - -bool WSRequestHandler::hasField(QString name) { - if (!data || name.isEmpty() || name.isNull()) - return false; - - return obs_data_has_user_value(data, name.toUtf8()); + return std::bind(handlerFunc, this, _1)(request); } diff --git a/src/WSRequestHandler.h b/src/WSRequestHandler.h index 4bae1a53..d17384ee 100644 --- a/src/WSRequestHandler.h +++ b/src/WSRequestHandler.h @@ -19,145 +19,147 @@ with this program. If not, see #pragma once +#include #include #include -#include -#include -#include #include #include #include "ConnectionProperties.h" +#include "rpc/RpcRequest.h" +#include "rpc/RpcResponse.h" + #include "obs-websocket.h" -typedef obs_data_t* HandlerResponse; - -class WSRequestHandler : public QObject { - Q_OBJECT +class WSRequestHandler; +typedef RpcResponse(WSRequestHandler::*RpcMethodHandler)(const RpcRequest&); +class WSRequestHandler { public: explicit WSRequestHandler(ConnectionProperties& connProperties); - ~WSRequestHandler(); - std::string processIncomingMessage(std::string& textMessage); - bool hasField(QString name); + RpcResponse processRequest(const RpcRequest& textMessage); private: - const char* _messageId; - const char* _requestType; ConnectionProperties& _connProperties; - OBSDataAutoRelease data; - HandlerResponse processRequest(std::string& textMessage); + static const QHash messageMap; + static const QSet authNotRequired; - HandlerResponse SendOKResponse(obs_data_t* additionalFields = nullptr); - HandlerResponse SendErrorResponse(const char* errorMessage); - HandlerResponse SendErrorResponse(obs_data_t* additionalFields = nullptr); - HandlerResponse SendResponse(const char* status, obs_data_t* additionalFields = nullptr); + RpcResponse GetVersion(const RpcRequest&); + RpcResponse GetAuthRequired(const RpcRequest&); + RpcResponse Authenticate(const RpcRequest&); - static QHash messageMap; - static QSet authNotRequired; + RpcResponse GetStats(const RpcRequest&); + RpcResponse SetHeartbeat(const RpcRequest&); + RpcResponse GetVideoInfo(const RpcRequest&); + RpcResponse OpenProjector(const RpcRequest&); - static HandlerResponse HandleGetVersion(WSRequestHandler* req); - static HandlerResponse HandleGetAuthRequired(WSRequestHandler* req); - static HandlerResponse HandleAuthenticate(WSRequestHandler* req); + RpcResponse SetFilenameFormatting(const RpcRequest&); + RpcResponse GetFilenameFormatting(const RpcRequest&); - static HandlerResponse HandleGetStats(WSRequestHandler* req); - static HandlerResponse HandleSetHeartbeat(WSRequestHandler* req); - static HandlerResponse HandleGetVideoInfo(WSRequestHandler* req); + RpcResponse BroadcastCustomMessage(const RpcRequest&); - static HandlerResponse HandleSetFilenameFormatting(WSRequestHandler* req); - static HandlerResponse HandleGetFilenameFormatting(WSRequestHandler* req); + RpcResponse SetCurrentScene(const RpcRequest&); + RpcResponse GetCurrentScene(const RpcRequest&); + RpcResponse GetSceneList(const RpcRequest&); - static HandlerResponse HandleSetCurrentScene(WSRequestHandler* req); - static HandlerResponse HandleGetCurrentScene(WSRequestHandler* req); - static HandlerResponse HandleGetSceneList(WSRequestHandler* req); + RpcResponse SetSceneItemRender(const RpcRequest&); + RpcResponse SetSceneItemPosition(const RpcRequest&); + RpcResponse SetSceneItemTransform(const RpcRequest&); + RpcResponse SetSceneItemCrop(const RpcRequest&); + RpcResponse GetSceneItemProperties(const RpcRequest&); + RpcResponse SetSceneItemProperties(const RpcRequest&); + RpcResponse ResetSceneItem(const RpcRequest&); + RpcResponse DuplicateSceneItem(const RpcRequest&); + RpcResponse DeleteSceneItem(const RpcRequest&); + RpcResponse ReorderSceneItems(const RpcRequest&); - static HandlerResponse HandleSetSceneItemRender(WSRequestHandler* req); - static HandlerResponse HandleSetSceneItemPosition(WSRequestHandler* req); - static HandlerResponse HandleSetSceneItemTransform(WSRequestHandler* req); - static HandlerResponse HandleSetSceneItemCrop(WSRequestHandler* req); - static HandlerResponse HandleGetSceneItemProperties(WSRequestHandler* req); - static HandlerResponse HandleSetSceneItemProperties(WSRequestHandler* req); - static HandlerResponse HandleResetSceneItem(WSRequestHandler* req); - static HandlerResponse HandleDuplicateSceneItem(WSRequestHandler* req); - static HandlerResponse HandleDeleteSceneItem(WSRequestHandler* req); - static HandlerResponse HandleReorderSceneItems(WSRequestHandler* req); + RpcResponse GetStreamingStatus(const RpcRequest&); + RpcResponse StartStopStreaming(const RpcRequest&); + RpcResponse StartStopRecording(const RpcRequest&); - static HandlerResponse HandleGetStreamingStatus(WSRequestHandler* req); - static HandlerResponse HandleStartStopStreaming(WSRequestHandler* req); - static HandlerResponse HandleStartStopRecording(WSRequestHandler* req); - static HandlerResponse HandleStartStreaming(WSRequestHandler* req); - static HandlerResponse HandleStopStreaming(WSRequestHandler* req); - static HandlerResponse HandleStartRecording(WSRequestHandler* req); - static HandlerResponse HandleStopRecording(WSRequestHandler* req); + RpcResponse StartStreaming(const RpcRequest&); + RpcResponse StopStreaming(const RpcRequest&); - static HandlerResponse HandleStartStopReplayBuffer(WSRequestHandler* req); - static HandlerResponse HandleStartReplayBuffer(WSRequestHandler* req); - static HandlerResponse HandleStopReplayBuffer(WSRequestHandler* req); - static HandlerResponse HandleSaveReplayBuffer(WSRequestHandler* req); + RpcResponse StartRecording(const RpcRequest&); + RpcResponse StopRecording(const RpcRequest&); + RpcResponse PauseRecording(const RpcRequest&); + RpcResponse ResumeRecording(const RpcRequest&); - static HandlerResponse HandleSetRecordingFolder(WSRequestHandler* req); - static HandlerResponse HandleGetRecordingFolder(WSRequestHandler* req); + RpcResponse StartStopReplayBuffer(const RpcRequest&); + RpcResponse StartReplayBuffer(const RpcRequest&); + RpcResponse StopReplayBuffer(const RpcRequest&); + RpcResponse SaveReplayBuffer(const RpcRequest&); - static HandlerResponse HandleGetTransitionList(WSRequestHandler* req); - static HandlerResponse HandleGetCurrentTransition(WSRequestHandler* req); - static HandlerResponse HandleSetCurrentTransition(WSRequestHandler* req); + RpcResponse SetRecordingFolder(const RpcRequest&); + RpcResponse GetRecordingFolder(const RpcRequest&); - static HandlerResponse HandleSetVolume(WSRequestHandler* req); - static HandlerResponse HandleGetVolume(WSRequestHandler* req); - static HandlerResponse HandleToggleMute(WSRequestHandler* req); - static HandlerResponse HandleSetMute(WSRequestHandler* req); - static HandlerResponse HandleGetMute(WSRequestHandler* req); - static HandlerResponse HandleSetSyncOffset(WSRequestHandler* req); - static HandlerResponse HandleGetSyncOffset(WSRequestHandler* req); - static HandlerResponse HandleGetSpecialSources(WSRequestHandler* req); - static HandlerResponse HandleGetSourcesList(WSRequestHandler* req); - static HandlerResponse HandleGetSourceTypesList(WSRequestHandler* req); - static HandlerResponse HandleGetSourceSettings(WSRequestHandler* req); - static HandlerResponse HandleSetSourceSettings(WSRequestHandler* req); - static HandlerResponse HandleTakeSourceScreenshot(WSRequestHandler* req); + RpcResponse GetTransitionList(const RpcRequest&); + RpcResponse GetCurrentTransition(const RpcRequest&); + RpcResponse SetCurrentTransition(const RpcRequest&); - static HandlerResponse HandleGetSourceFilters(WSRequestHandler* req); - static HandlerResponse HandleAddFilterToSource(WSRequestHandler* req); - static HandlerResponse HandleRemoveFilterFromSource(WSRequestHandler* req); - static HandlerResponse HandleReorderSourceFilter(WSRequestHandler* req); - static HandlerResponse HandleMoveSourceFilter(WSRequestHandler* req); - static HandlerResponse HandleSetSourceFilterSettings(WSRequestHandler* req); + RpcResponse SetVolume(const RpcRequest&); + RpcResponse GetVolume(const RpcRequest&); + RpcResponse ToggleMute(const RpcRequest&); + RpcResponse SetMute(const RpcRequest&); + RpcResponse GetMute(const RpcRequest&); + RpcResponse SetSyncOffset(const RpcRequest&); + RpcResponse GetSyncOffset(const RpcRequest&); + RpcResponse GetSpecialSources(const RpcRequest&); + RpcResponse GetSourcesList(const RpcRequest&); + RpcResponse GetSourceTypesList(const RpcRequest&); + RpcResponse GetSourceSettings(const RpcRequest&); + RpcResponse SetSourceSettings(const RpcRequest&); + RpcResponse TakeSourceScreenshot(const RpcRequest&); - static HandlerResponse HandleSetCurrentSceneCollection(WSRequestHandler* req); - static HandlerResponse HandleGetCurrentSceneCollection(WSRequestHandler* req); - static HandlerResponse HandleListSceneCollections(WSRequestHandler* req); + RpcResponse GetSourceFilters(const RpcRequest&); + RpcResponse GetSourceFilterInfo(const RpcRequest&); + RpcResponse AddFilterToSource(const RpcRequest&); + RpcResponse RemoveFilterFromSource(const RpcRequest&); + RpcResponse ReorderSourceFilter(const RpcRequest&); + RpcResponse MoveSourceFilter(const RpcRequest&); + RpcResponse SetSourceFilterSettings(const RpcRequest&); + RpcResponse SetSourceFilterVisibility(const RpcRequest&); - static HandlerResponse HandleSetCurrentProfile(WSRequestHandler* req); - static HandlerResponse HandleGetCurrentProfile(WSRequestHandler* req); - static HandlerResponse HandleListProfiles(WSRequestHandler* req); + RpcResponse SetCurrentSceneCollection(const RpcRequest&); + RpcResponse GetCurrentSceneCollection(const RpcRequest&); + RpcResponse ListSceneCollections(const RpcRequest&); - static HandlerResponse HandleSetStreamSettings(WSRequestHandler* req); - static HandlerResponse HandleGetStreamSettings(WSRequestHandler* req); - static HandlerResponse HandleSaveStreamSettings(WSRequestHandler* req); + RpcResponse SetCurrentProfile(const RpcRequest&); + RpcResponse GetCurrentProfile(const RpcRequest&); + RpcResponse ListProfiles(const RpcRequest&); + + RpcResponse SetStreamSettings(const RpcRequest&); + RpcResponse GetStreamSettings(const RpcRequest&); + RpcResponse SaveStreamSettings(const RpcRequest&); #if BUILD_CAPTIONS - static HandlerResponse HandleSendCaptions(WSRequestHandler * req); + RpcResponse SendCaptions(const RpcRequest&); #endif - static HandlerResponse HandleSetTransitionDuration(WSRequestHandler* req); - static HandlerResponse HandleGetTransitionDuration(WSRequestHandler* req); + RpcResponse SetTransitionDuration(const RpcRequest&); + RpcResponse GetTransitionDuration(const RpcRequest&); - static HandlerResponse HandleGetStudioModeStatus(WSRequestHandler* req); - static HandlerResponse HandleGetPreviewScene(WSRequestHandler* req); - static HandlerResponse HandleSetPreviewScene(WSRequestHandler* req); - static HandlerResponse HandleTransitionToProgram(WSRequestHandler* req); - static HandlerResponse HandleEnableStudioMode(WSRequestHandler* req); - static HandlerResponse HandleDisableStudioMode(WSRequestHandler* req); - static HandlerResponse HandleToggleStudioMode(WSRequestHandler* req); + RpcResponse GetStudioModeStatus(const RpcRequest&); + RpcResponse GetPreviewScene(const RpcRequest&); + RpcResponse SetPreviewScene(const RpcRequest&); + RpcResponse TransitionToProgram(const RpcRequest&); + RpcResponse EnableStudioMode(const RpcRequest&); + RpcResponse DisableStudioMode(const RpcRequest&); + RpcResponse ToggleStudioMode(const RpcRequest&); - static HandlerResponse HandleSetTextGDIPlusProperties(WSRequestHandler* req); - static HandlerResponse HandleGetTextGDIPlusProperties(WSRequestHandler* req); + RpcResponse SetTextGDIPlusProperties(const RpcRequest&); + RpcResponse GetTextGDIPlusProperties(const RpcRequest&); - static HandlerResponse HandleSetTextFreetype2Properties(WSRequestHandler* req); - static HandlerResponse HandleGetTextFreetype2Properties(WSRequestHandler* req); + RpcResponse SetTextFreetype2Properties(const RpcRequest&); + RpcResponse GetTextFreetype2Properties(const RpcRequest&); - static HandlerResponse HandleSetBrowserSourceProperties(WSRequestHandler* req); - static HandlerResponse HandleGetBrowserSourceProperties(WSRequestHandler* req); + RpcResponse SetBrowserSourceProperties(const RpcRequest&); + RpcResponse GetBrowserSourceProperties(const RpcRequest&); + + RpcResponse ListOutputs(const RpcRequest&); + RpcResponse GetOutputInfo(const RpcRequest&); + RpcResponse StartOutput(const RpcRequest&); + RpcResponse StopOutput(const RpcRequest&); }; diff --git a/src/WSRequestHandler_General.cpp b/src/WSRequestHandler_General.cpp index 0ca572af..56eeec8b 100644 --- a/src/WSRequestHandler_General.cpp +++ b/src/WSRequestHandler_General.cpp @@ -1,10 +1,13 @@ +#include "WSRequestHandler.h" + +#include +#include + #include "obs-websocket.h" #include "Config.h" #include "Utils.h" #include "WSEvents.h" -#include "WSRequestHandler.h" - #define CASE(x) case x: return #x; const char *describe_output_format(int format) { switch (format) { @@ -60,32 +63,41 @@ const char *describe_scale_type(int scale) { * @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"). + * @return {String} `supported-image-export-formats` List of supported formats for features that use image export (like the TakeSourceScreenshot request type) formatted as a comma-separated list string * * @api requests * @name GetVersion * @category general * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleGetVersion(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetVersion(const RpcRequest& request) { QString obsVersion = Utils::OBSVersionString(); - QList names = req->messageMap.keys(); - names.sort(Qt::CaseInsensitive); + QList names = messageMap.keys(); + QList imageWriterFormats = QImageWriter::supportedImageFormats(); // (Palakis) OBS' data arrays only support object arrays, so I improvised. QString requests; + names.sort(Qt::CaseInsensitive); requests += names.takeFirst(); - for (QString reqName : names) { + for (const QString& reqName : names) { requests += ("," + reqName); } + QString supportedImageExportFormats; + supportedImageExportFormats += QString::fromUtf8(imageWriterFormats.takeFirst()); + for (const QByteArray& format : imageWriterFormats) { + supportedImageExportFormats += ("," + QString::fromUtf8(format)); + } + OBSDataAutoRelease data = obs_data_create(); obs_data_set_double(data, "version", 1.1); 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()); + obs_data_set_string(data, "supported-image-export-formats", supportedImageExportFormats.toUtf8()); - return req->SendOKResponse(data); + return request.success(data); } /** @@ -101,7 +113,7 @@ HandlerResponse WSRequestHandler::HandleGetVersion(WSRequestHandler* req) { * @category general * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleGetAuthRequired(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetAuthRequired(const RpcRequest& request) { bool authRequired = GetConfig()->AuthRequired; OBSDataAutoRelease data = obs_data_create(); @@ -115,7 +127,7 @@ HandlerResponse WSRequestHandler::HandleGetAuthRequired(WSRequestHandler* req) { config->Salt.toUtf8()); } - return req->SendOKResponse(data); + return request.success(data); } /** @@ -128,26 +140,26 @@ HandlerResponse WSRequestHandler::HandleGetAuthRequired(WSRequestHandler* req) { * @category general * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleAuthenticate(WSRequestHandler* req) { - if (!req->hasField("auth")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::Authenticate(const RpcRequest& request) { + if (!request.hasField("auth")) { + return request.failed("missing request parameters"); } - if (req->_connProperties.isAuthenticated()) { - return req->SendErrorResponse("already authenticated"); + if (_connProperties.isAuthenticated()) { + return request.failed("already authenticated"); } - QString auth = obs_data_get_string(req->data, "auth"); + QString auth = obs_data_get_string(request.parameters(), "auth"); if (auth.isEmpty()) { - return req->SendErrorResponse("auth not specified!"); + return request.failed("auth not specified!"); } if (GetConfig()->CheckAuth(auth) == false) { - return req->SendErrorResponse("Authentication Failed."); + return request.failed("Authentication Failed."); } - req->_connProperties.setAuthenticated(true); - return req->SendOKResponse(); + _connProperties.setAuthenticated(true); + return request.success(); } /** @@ -160,17 +172,18 @@ HandlerResponse WSRequestHandler::HandleAuthenticate(WSRequestHandler* req) { * @category general * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleSetHeartbeat(WSRequestHandler* req) { - if (!req->hasField("enable")) { - return req->SendErrorResponse("Heartbeat parameter missing"); +RpcResponse WSRequestHandler::SetHeartbeat(const RpcRequest& request) { + if (!request.hasField("enable")) { + return request.failed("Heartbeat parameter missing"); } auto events = GetEventsSystem(); - events->HeartbeatIsActive = obs_data_get_bool(req->data, "enable"); + events->HeartbeatIsActive = obs_data_get_bool(request.parameters(), "enable"); OBSDataAutoRelease response = obs_data_create(); obs_data_set_bool(response, "enable", events->HeartbeatIsActive); - return req->SendOKResponse(response); + + return request.success(response); } /** @@ -183,18 +196,19 @@ HandlerResponse WSRequestHandler::HandleSetHeartbeat(WSRequestHandler* req) { * @category general * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleSetFilenameFormatting(WSRequestHandler* req) { - if (!req->hasField("filename-formatting")) { - return req->SendErrorResponse(" parameter missing"); +RpcResponse WSRequestHandler::SetFilenameFormatting(const RpcRequest& request) { + if (!request.hasField("filename-formatting")) { + return request.failed(" parameter missing"); } - QString filenameFormatting = obs_data_get_string(req->data, "filename-formatting"); + QString filenameFormatting = obs_data_get_string(request.parameters(), "filename-formatting"); if (filenameFormatting.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } Utils::SetFilenameFormatting(filenameFormatting.toUtf8()); - return req->SendOKResponse(); + + return request.success(); } /** @@ -207,10 +221,11 @@ HandlerResponse WSRequestHandler::HandleSetFilenameFormatting(WSRequestHandler* * @category general * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleGetFilenameFormatting(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetFilenameFormatting(const RpcRequest& request) { OBSDataAutoRelease response = obs_data_create(); obs_data_set_string(response, "filename-formatting", Utils::GetFilenameFormatting()); - return req->SendOKResponse(response); + + return request.success(response); } /** @@ -223,14 +238,49 @@ HandlerResponse WSRequestHandler::HandleGetFilenameFormatting(WSRequestHandler* * @category general * @since 4.6.0 */ -HandlerResponse WSRequestHandler::HandleGetStats(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetStats(const RpcRequest& request) { OBSDataAutoRelease stats = GetEventsSystem()->GetStats(); OBSDataAutoRelease response = obs_data_create(); obs_data_set_obj(response, "stats", stats); - return req->SendOKResponse(response); + + return request.success(response); } +/** + * Broadcast custom message to all connected WebSocket clients + * + * @param {String} `realm` Identifier to be choosen by the client + * @param {Object} `data` User-defined data + * + * @api requests + * @name BroadcastCustomMessage + * @category general + * @since 4.7.0 + */ +RpcResponse WSRequestHandler::BroadcastCustomMessage(const RpcRequest& request) { + if (!request.hasField("realm") || !request.hasField("data")) { + return request.failed("missing request parameters"); + } + + QString realm = obs_data_get_string(request.parameters(), "realm"); + OBSDataAutoRelease data = obs_data_get_obj(request.parameters(), "data"); + + if (realm.isEmpty()) { + return request.failed("realm not specified!"); + } + + if (!data) { + return request.failed("data not specified!"); + } + + auto events = GetEventsSystem(); + events->OnBroadcastCustomMessage(realm, data); + + return request.success(); +} + + /** * Get basic OBS video information * @@ -249,9 +299,10 @@ HandlerResponse WSRequestHandler::HandleGetStats(WSRequestHandler* req) { * @category general * @since 4.6.0 */ -HandlerResponse WSRequestHandler::HandleGetVideoInfo(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetVideoInfo(const RpcRequest& request) { obs_video_info ovi; obs_get_video_info(&ovi); + OBSDataAutoRelease response = obs_data_create(); obs_data_set_int(response, "baseWidth", ovi.base_width); obs_data_set_int(response, "baseHeight", ovi.base_height); @@ -262,5 +313,34 @@ HandlerResponse WSRequestHandler::HandleGetVideoInfo(WSRequestHandler* req) { obs_data_set_string(response, "colorSpace", describe_color_space(ovi.colorspace)); obs_data_set_string(response, "colorRange", describe_color_range(ovi.range)); obs_data_set_string(response, "scaleType", describe_scale_type(ovi.scale_type)); - return req->SendOKResponse(response); + + return request.success(response); +} + +/** + * Open a projector window or create a projector on a monitor. Requires OBS v24.0.4 or newer. + * + * @param {String (Optional)} `type` Type of projector: Preview (default), Source, Scene, StudioProgram, or Multiview (case insensitive). + * @param {int (Optional)} `monitor` Monitor to open the projector on. If -1 or omitted, opens a window. + * @param {String (Optional)} `geometry` Size and position of the projector window (only if monitor is -1). Encoded in Base64 using Qt's geometry encoding (https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to OBS's saved projectors. + * @param {String (Optional)} `name` Name of the source or scene to be displayed (ignored for other projector types). + * + * @api requests + * @name OpenProjector + * @category general + * @since unreleased + */ +RpcResponse WSRequestHandler::OpenProjector(const RpcRequest& request) { + const char* type = obs_data_get_string(request.parameters(), "type"); + + int monitor = -1; + if (request.hasField("monitor")) { + monitor = obs_data_get_int(request.parameters(), "monitor"); + } + + const char* geometry = obs_data_get_string(request.parameters(), "geometry"); + const char* name = obs_data_get_string(request.parameters(), "name"); + + obs_frontend_open_projector(type, monitor, geometry, name); + return request.success(); } diff --git a/src/WSRequestHandler_Outputs.cpp b/src/WSRequestHandler_Outputs.cpp new file mode 100644 index 00000000..dad0bf19 --- /dev/null +++ b/src/WSRequestHandler_Outputs.cpp @@ -0,0 +1,184 @@ +#include + +#include "WSRequestHandler.h" + +/** +* @typedef {Object} `Output` +* @property {String} `name` Output name +* @property {String} `type` Output type/kind +* @property {int} `width` Video output width +* @property {int} `height` Video output height +* @property {Object} `flags` Output flags +* @property {int} `flags.rawValue` Raw flags value +* @property {boolean} `flags.audio` Output uses audio +* @property {boolean} `flags.video` Output uses video +* @property {boolean} `flags.encoded` Output is encoded +* @property {boolean} `flags.multiTrack` Output uses several audio tracks +* @property {boolean} `flags.service` Output uses a service +* @property {Object} `settings` Output name +* @property {boolean} `active` Output status (active or not) +* @property {boolean} `reconnecting` Output reconnection status (reconnecting or not) +* @property {double} `congestion` Output congestion +* @property {int} `totalFrames` Number of frames sent +* @property {int} `droppedFrames` Number of frames dropped +* @property {int} `totalBytes` Total bytes sent +*/ +obs_data_t* getOutputInfo(obs_output_t* output) +{ + if (!output) { + return nullptr; + } + + OBSDataAutoRelease settings = obs_output_get_settings(output); + + uint32_t rawFlags = obs_output_get_flags(output); + OBSDataAutoRelease flags = obs_data_create(); + obs_data_set_int(flags, "rawValue", rawFlags); + obs_data_set_bool(flags, "audio", rawFlags & OBS_OUTPUT_AUDIO); + obs_data_set_bool(flags, "video", rawFlags & OBS_OUTPUT_VIDEO); + obs_data_set_bool(flags, "encoded", rawFlags & OBS_OUTPUT_ENCODED); + obs_data_set_bool(flags, "multiTrack", rawFlags & OBS_OUTPUT_MULTI_TRACK); + obs_data_set_bool(flags, "service", rawFlags & OBS_OUTPUT_SERVICE); + + obs_data_t* data = obs_data_create(); + + obs_data_set_string(data, "name", obs_output_get_name(output)); + obs_data_set_string(data, "type", obs_output_get_id(output)); + obs_data_set_int(data, "width", obs_output_get_width(output)); + obs_data_set_int(data, "height", obs_output_get_height(output)); + obs_data_set_obj(data, "flags", flags); + obs_data_set_obj(data, "settings", settings); + + obs_data_set_bool(data, "active", obs_output_active(output)); + obs_data_set_bool(data, "reconnecting", obs_output_reconnecting(output)); + obs_data_set_double(data, "congestion", obs_output_get_congestion(output)); + obs_data_set_int(data, "totalFrames", obs_output_get_total_frames(output)); + obs_data_set_int(data, "droppedFrames", obs_output_get_frames_dropped(output)); + obs_data_set_int(data, "totalBytes", obs_output_get_total_bytes(output)); + + return data; +} + +RpcResponse findOutputOrFail(const RpcRequest& request, std::function callback) +{ + if (!request.hasField("outputName")) { + return request.failed("missing request parameters"); + } + + const char* outputName = obs_data_get_string(request.parameters(), "outputName"); + OBSOutputAutoRelease output = obs_get_output_by_name(outputName); + if (!output) { + return request.failed("specified output doesn't exist"); + } + + return callback(output); +} + +/** +* List existing outputs +* +* @return {Array} `outputs` Outputs list +* +* @api requests +* @name ListOutputs +* @category outputs +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::ListOutputs(const RpcRequest& request) +{ + OBSDataArrayAutoRelease outputs = obs_data_array_create(); + + obs_enum_outputs([](void* param, obs_output_t* output) { + obs_data_array_t* outputs = reinterpret_cast(param); + + OBSDataAutoRelease outputInfo = getOutputInfo(output); + obs_data_array_push_back(outputs, outputInfo); + + return true; + }, outputs); + + OBSDataAutoRelease fields = obs_data_create(); + obs_data_set_array(fields, "outputs", outputs); + + return request.success(fields); +} + +/** +* Get information about a single output +* +* @param {String} `outputName` Output name +* +* @return {Output} `outputInfo` Output info +* +* @api requests +* @name GetOutputInfo +* @category outputs +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::GetOutputInfo(const RpcRequest& request) +{ + return findOutputOrFail(request, [request](obs_output_t* output) { + OBSDataAutoRelease outputInfo = getOutputInfo(output); + + OBSDataAutoRelease fields = obs_data_create(); + obs_data_set_obj(fields, "outputInfo", outputInfo); + return request.success(fields); + }); +} + +/** +* Start an output +* +* @param {String} `outputName` Output name +* +* @api requests +* @name StartOutput +* @category outputs +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::StartOutput(const RpcRequest& request) +{ + return findOutputOrFail(request, [request](obs_output_t* output) { + if (obs_output_active(output)) { + return request.failed("output already active"); + } + + bool success = obs_output_start(output); + if (!success) { + QString lastError = obs_output_get_last_error(output); + QString errorMessage = QString("output start failed: %1").arg(lastError); + return request.failed(errorMessage); + } + + return request.success(); + }); +} + +/** +* Stop an output +* +* @param {String} `outputName` Output name +* @param {boolean (optional)} `force` Force stop (default: false) +* +* @api requests +* @name StopOutput +* @category outputs +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::StopOutput(const RpcRequest& request) +{ + return findOutputOrFail(request, [request](obs_output_t* output) { + if (!obs_output_active(output)) { + return request.failed("output not active"); + } + + bool forceStop = obs_data_get_bool(request.parameters(), "force"); + if (forceStop) { + obs_output_force_stop(output); + } else { + obs_output_stop(output); + } + + return request.success(); + }); +} diff --git a/src/WSRequestHandler_Profiles.cpp b/src/WSRequestHandler_Profiles.cpp index 4bcc7d4d..78e3ce2d 100644 --- a/src/WSRequestHandler_Profiles.cpp +++ b/src/WSRequestHandler_Profiles.cpp @@ -12,19 +12,19 @@ * @category profiles * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleSetCurrentProfile(WSRequestHandler* req) { - if (!req->hasField("profile-name")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetCurrentProfile(const RpcRequest& request) { + if (!request.hasField("profile-name")) { + return request.failed("missing request parameters"); } - QString profileName = obs_data_get_string(req->data, "profile-name"); + QString profileName = obs_data_get_string(request.parameters(), "profile-name"); if (profileName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } // TODO : check if profile exists obs_frontend_set_current_profile(profileName.toUtf8()); - return req->SendOKResponse(); + return request.success(); } /** @@ -37,10 +37,12 @@ HandlerResponse WSRequestHandler::HandleSetCurrentProfile(WSRequestHandler* req) * @category profiles * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleGetCurrentProfile(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetCurrentProfile(const RpcRequest& request) { OBSDataAutoRelease response = obs_data_create(); - obs_data_set_string(response, "profile-name", obs_frontend_get_current_profile()); - return req->SendOKResponse(response); + char* currentProfile = obs_frontend_get_current_profile(); + obs_data_set_string(response, "profile-name", currentProfile); + bfree(currentProfile); + return request.success(response); } /** @@ -53,7 +55,7 @@ HandlerResponse WSRequestHandler::HandleGetCurrentProfile(WSRequestHandler* req) * @category profiles * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleListProfiles(WSRequestHandler* req) { +RpcResponse WSRequestHandler::ListProfiles(const RpcRequest& request) { char** profiles = obs_frontend_get_profiles(); OBSDataArrayAutoRelease list = Utils::StringListToArray(profiles, "profile-name"); bfree(profiles); @@ -61,5 +63,5 @@ HandlerResponse WSRequestHandler::HandleListProfiles(WSRequestHandler* req) { OBSDataAutoRelease response = obs_data_create(); obs_data_set_array(response, "profiles", list); - return req->SendOKResponse(response); + return request.success(response); } diff --git a/src/WSRequestHandler_Recording.cpp b/src/WSRequestHandler_Recording.cpp index b8600935..df2ed4a0 100644 --- a/src/WSRequestHandler_Recording.cpp +++ b/src/WSRequestHandler_Recording.cpp @@ -1,6 +1,17 @@ +#include "WSRequestHandler.h" + +#include +#include #include "Utils.h" -#include "WSRequestHandler.h" +RpcResponse ifCanPause(const RpcRequest& request, std::function callback) +{ + if (!obs_frontend_recording_active()) { + return request.failed("recording is not active"); + } + + return callback(); +} /** * Toggle recording on or off. @@ -10,13 +21,9 @@ * @category recording * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req) { - if (obs_frontend_recording_active()) - obs_frontend_recording_stop(); - else - obs_frontend_recording_start(); - - return req->SendOKResponse(); +RpcResponse WSRequestHandler::StartStopRecording(const RpcRequest& request) { + (obs_frontend_recording_active() ? obs_frontend_recording_stop() : obs_frontend_recording_start()); + return request.success(); } /** @@ -28,13 +35,13 @@ HandlerResponse WSRequestHandler::HandleStartStopRecording(WSRequestHandler* req * @category recording * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { - if (obs_frontend_recording_active() == false) { - obs_frontend_recording_start(); - return req->SendOKResponse(); - } else { - return req->SendErrorResponse("recording already active"); +RpcResponse WSRequestHandler::StartRecording(const RpcRequest& request) { + if (obs_frontend_recording_active()) { + return request.failed("recording already active"); } + + obs_frontend_recording_start(); + return request.success(); } /** @@ -46,13 +53,53 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { * @category recording * @since 4.1.0 */ - HandlerResponse WSRequestHandler::HandleStopRecording(WSRequestHandler* req) { - if (obs_frontend_recording_active() == true) { - obs_frontend_recording_stop(); - return req->SendOKResponse(); - } else { - return req->SendErrorResponse("recording not active"); + RpcResponse WSRequestHandler::StopRecording(const RpcRequest& request) { + if (!obs_frontend_recording_active()) { + return request.failed("recording not active"); } + + obs_frontend_recording_stop(); + return request.success(); +} + +/** +* Pause the current recording. +* Returns an error if recording is not active or already paused. +* +* @api requests +* @name PauseRecording +* @category recording +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::PauseRecording(const RpcRequest& request) { + return ifCanPause(request, [request]() { + if (obs_frontend_recording_paused()) { + return request.failed("recording already paused"); + } + + obs_frontend_recording_pause(true); + return request.success(); + }); +} + +/** +* Resume/unpause the current recording (if paused). +* Returns an error if recording is not active or not paused. +* +* @api requests +* @name ResumeRecording +* @category recording +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::ResumeRecording(const RpcRequest& request) { + return ifCanPause(request, [request]() { + if (!obs_frontend_recording_paused()) { + return request.failed("recording is not paused"); + } + + obs_frontend_recording_pause(false); + return request.success(); + }); } /** @@ -63,7 +110,6 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { * in progress, the change won't be applied immediately and will be * effective on the next recording. * - * * @param {String} `rec-folder` Path of the recording folder. * * @api requests @@ -71,18 +117,18 @@ HandlerResponse WSRequestHandler::HandleStartRecording(WSRequestHandler* req) { * @category recording * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleSetRecordingFolder(WSRequestHandler* req) { - if (!req->hasField("rec-folder")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetRecordingFolder(const RpcRequest& request) { + if (!request.hasField("rec-folder")) { + return request.failed("missing request parameters"); } - const char* newRecFolder = obs_data_get_string(req->data, "rec-folder"); + const char* newRecFolder = obs_data_get_string(request.parameters(), "rec-folder"); bool success = Utils::SetRecordingFolder(newRecFolder); if (!success) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - return req->SendOKResponse(); + return request.success(); } /** @@ -95,11 +141,11 @@ HandlerResponse WSRequestHandler::HandleSetRecordingFolder(WSRequestHandler* req * @category recording * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetRecordingFolder(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetRecordingFolder(const RpcRequest& request) { const char* recFolder = Utils::GetRecordingFolder(); OBSDataAutoRelease response = obs_data_create(); obs_data_set_string(response, "rec-folder", recFolder); - return req->SendOKResponse(response); + return request.success(response); } diff --git a/src/WSRequestHandler_ReplayBuffer.cpp b/src/WSRequestHandler_ReplayBuffer.cpp index 86add84a..510b8f05 100644 --- a/src/WSRequestHandler_ReplayBuffer.cpp +++ b/src/WSRequestHandler_ReplayBuffer.cpp @@ -10,13 +10,13 @@ * @category replay buffer * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleStartStopReplayBuffer(WSRequestHandler* req) { +RpcResponse WSRequestHandler::StartStopReplayBuffer(const RpcRequest& request) { if (obs_frontend_replay_buffer_active()) { obs_frontend_replay_buffer_stop(); } else { Utils::StartReplayBuffer(); } - return req->SendOKResponse(); + return request.success(); } /** @@ -31,17 +31,17 @@ HandlerResponse WSRequestHandler::HandleStartStopReplayBuffer(WSRequestHandler* * @category replay buffer * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleStartReplayBuffer(WSRequestHandler* req) { +RpcResponse WSRequestHandler::StartReplayBuffer(const RpcRequest& request) { if (!Utils::ReplayBufferEnabled()) { - return req->SendErrorResponse("replay buffer disabled in settings"); + return request.failed("replay buffer disabled in settings"); } if (obs_frontend_replay_buffer_active() == true) { - return req->SendErrorResponse("replay buffer already active"); + return request.failed("replay buffer already active"); } Utils::StartReplayBuffer(); - return req->SendOKResponse(); + return request.success(); } /** @@ -53,12 +53,12 @@ HandlerResponse WSRequestHandler::HandleStartReplayBuffer(WSRequestHandler* req) * @category replay buffer * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleStopReplayBuffer(WSRequestHandler* req) { +RpcResponse WSRequestHandler::StopReplayBuffer(const RpcRequest& request) { if (obs_frontend_replay_buffer_active() == true) { obs_frontend_replay_buffer_stop(); - return req->SendOKResponse(); + return request.success(); } else { - return req->SendErrorResponse("replay buffer not active"); + return request.failed("replay buffer not active"); } } @@ -72,9 +72,9 @@ HandlerResponse WSRequestHandler::HandleStopReplayBuffer(WSRequestHandler* req) * @category replay buffer * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleSaveReplayBuffer(WSRequestHandler* req) { +RpcResponse WSRequestHandler::SaveReplayBuffer(const RpcRequest& request) { if (!obs_frontend_replay_buffer_active()) { - return req->SendErrorResponse("replay buffer not active"); + return request.failed("replay buffer not active"); } OBSOutputAutoRelease replayOutput = obs_frontend_get_replay_buffer_output(); @@ -84,5 +84,5 @@ HandlerResponse WSRequestHandler::HandleSaveReplayBuffer(WSRequestHandler* req) proc_handler_call(ph, "save", &cd); calldata_free(&cd); - return req->SendOKResponse(); + return request.success(); } diff --git a/src/WSRequestHandler_SceneCollections.cpp b/src/WSRequestHandler_SceneCollections.cpp index d4159321..1f87996b 100644 --- a/src/WSRequestHandler_SceneCollections.cpp +++ b/src/WSRequestHandler_SceneCollections.cpp @@ -12,19 +12,19 @@ * @category scene collections * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleSetCurrentSceneCollection(WSRequestHandler* req) { - if (!req->hasField("sc-name")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetCurrentSceneCollection(const RpcRequest& request) { + if (!request.hasField("sc-name")) { + return request.failed("missing request parameters"); } - QString sceneCollection = obs_data_get_string(req->data, "sc-name"); + QString sceneCollection = obs_data_get_string(request.parameters(), "sc-name"); if (sceneCollection.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } // TODO : Check if specified profile exists and if changing is allowed obs_frontend_set_current_scene_collection(sceneCollection.toUtf8()); - return req->SendOKResponse(); + return request.success(); } /** @@ -37,12 +37,14 @@ HandlerResponse WSRequestHandler::HandleSetCurrentSceneCollection(WSRequestHandl * @category scene collections * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleGetCurrentSceneCollection(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetCurrentSceneCollection(const RpcRequest& request) { OBSDataAutoRelease response = obs_data_create(); - obs_data_set_string(response, "sc-name", - obs_frontend_get_current_scene_collection()); - return req->SendOKResponse(response); + char* sceneCollection = obs_frontend_get_current_scene_collection(); + obs_data_set_string(response, "sc-name", sceneCollection); + bfree(sceneCollection); + + return request.success(response); } /** @@ -55,7 +57,7 @@ HandlerResponse WSRequestHandler::HandleGetCurrentSceneCollection(WSRequestHandl * @category scene collections * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleListSceneCollections(WSRequestHandler* req) { +RpcResponse WSRequestHandler::ListSceneCollections(const RpcRequest& request) { char** sceneCollections = obs_frontend_get_scene_collections(); OBSDataArrayAutoRelease list = Utils::StringListToArray(sceneCollections, "sc-name"); @@ -64,5 +66,5 @@ HandlerResponse WSRequestHandler::HandleListSceneCollections(WSRequestHandler* r OBSDataAutoRelease response = obs_data_create(); obs_data_set_array(response, "scene-collections", list); - return req->SendOKResponse(response); + return request.success(response); } diff --git a/src/WSRequestHandler_SceneItems.cpp b/src/WSRequestHandler_SceneItems.cpp index fd5c138f..5e736802 100644 --- a/src/WSRequestHandler_SceneItems.cpp +++ b/src/WSRequestHandler_SceneItems.cpp @@ -21,6 +21,7 @@ * @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 {bool} `muted` If the source is muted. * @return {bool} `locked` If the source's transform is locked. * @return {String} `bounds.type` Type of bounding box. Can be "OBS_BOUNDS_STRETCH", "OBS_BOUNDS_SCALE_INNER", "OBS_BOUNDS_SCALE_OUTER", "OBS_BOUNDS_SCALE_TO_WIDTH", "OBS_BOUNDS_SCALE_TO_HEIGHT", "OBS_BOUNDS_MAX_ONLY" or "OBS_BOUNDS_NONE". * @return {int} `bounds.alignment` Alignment of the bounding box. @@ -30,40 +31,40 @@ * @return {int} `sourceHeight` Base source (without scaling) of the source * @return {double} `width` Scene item width (base source width multiplied by the horizontal scaling factor) * @return {double} `height` Scene item height (base source height multiplied by the vertical scaling factor) -* @property {String (optional)} `parentGroupName` Name of the item's parent (if this item belongs to a group) -* @property {Array (optional)} `groupChildren` List of children (if this item is a group) +* @return {int} `alignment` The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis. +* @return {String (optional)} `parentGroupName` Name of the item's parent (if this item belongs to a group) +* @return {Array (optional)} `groupChildren` List of children (if this item is a group) * * @api requests * @name GetSceneItemProperties * @category scene items * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler* req) { - if (!req->hasField("item")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::GetSceneItemProperties(const RpcRequest& request) { + if (!request.hasField("item")) { + return request.failed("missing request parameters"); } - QString itemName = obs_data_get_string(req->data, "item"); + QString itemName = obs_data_get_string(request.parameters(), "item"); if (itemName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - QString sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + QString sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } - OBSSceneItemAutoRelease sceneItem = - Utils::GetSceneItemFromName(scene, itemName); + OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } OBSDataAutoRelease data = Utils::GetSceneItemPropertiesData(sceneItem); obs_data_set_string(data, "name", itemName.toUtf8()); - return req->SendOKResponse(data); + return request.success(data); } /** @@ -94,38 +95,38 @@ HandlerResponse WSRequestHandler::HandleGetSceneItemProperties(WSRequestHandler* * @category scene items * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* req) { - if (!req->hasField("item")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetSceneItemProperties(const RpcRequest& request) { + if (!request.hasField("item")) { + return request.failed("missing request parameters"); } - QString itemName = obs_data_get_string(req->data, "item"); + QString itemName = obs_data_get_string(request.parameters(), "item"); if (itemName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - QString sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + QString sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } bool badRequest = false; - OBSDataAutoRelease errorMessage = obs_data_create(); + OBSDataAutoRelease errorData = obs_data_create(); obs_sceneitem_defer_update_begin(sceneItem); - if (req->hasField("position")) { + if (request.hasField("position")) { vec2 oldPosition; OBSDataAutoRelease positionError = obs_data_create(); obs_sceneitem_get_pos(sceneItem, &oldPosition); - OBSDataAutoRelease reqPosition = obs_data_get_obj(req->data, "position"); + OBSDataAutoRelease reqPosition = obs_data_get_obj(request.parameters(), "position"); vec2 newPosition = oldPosition; if (obs_data_has_user_value(reqPosition, "x")) { newPosition.x = obs_data_get_int(reqPosition, "x"); @@ -141,20 +142,20 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* else { badRequest = true; obs_data_set_string(positionError, "alignment", "invalid"); - obs_data_set_obj(errorMessage, "position", positionError); + obs_data_set_obj(errorData, "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 (request.hasField("rotation")) { + obs_sceneitem_set_rot(sceneItem, (float)obs_data_get_double(request.parameters(), "rotation")); } - if (req->hasField("scale")) { + if (request.hasField("scale")) { vec2 oldScale; obs_sceneitem_get_scale(sceneItem, &oldScale); - OBSDataAutoRelease reqScale = obs_data_get_obj(req->data, "scale"); + OBSDataAutoRelease reqScale = obs_data_get_obj(request.parameters(), "scale"); vec2 newScale = oldScale; if (obs_data_has_user_value(reqScale, "x")) { newScale.x = obs_data_get_double(reqScale, "x"); @@ -165,10 +166,10 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* obs_sceneitem_set_scale(sceneItem, &newScale); } - if (req->hasField("crop")) { + if (request.hasField("crop")) { obs_sceneitem_crop oldCrop; obs_sceneitem_get_crop(sceneItem, &oldCrop); - OBSDataAutoRelease reqCrop = obs_data_get_obj(req->data, "crop"); + OBSDataAutoRelease reqCrop = obs_data_get_obj(request.parameters(), "crop"); obs_sceneitem_crop newCrop = oldCrop; if (obs_data_has_user_value(reqCrop, "top")) { newCrop.top = obs_data_get_int(reqCrop, "top"); @@ -185,18 +186,18 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* obs_sceneitem_set_crop(sceneItem, &newCrop); } - if (req->hasField("visible")) { - obs_sceneitem_set_visible(sceneItem, obs_data_get_bool(req->data, "visible")); + if (request.hasField("visible")) { + obs_sceneitem_set_visible(sceneItem, obs_data_get_bool(request.parameters(), "visible")); } - if (req->hasField("locked")) { - obs_sceneitem_set_locked(sceneItem, obs_data_get_bool(req->data, "locked")); + if (request.hasField("locked")) { + obs_sceneitem_set_locked(sceneItem, obs_data_get_bool(request.parameters(), "locked")); } - if (req->hasField("bounds")) { + if (request.hasField("bounds")) { bool badBounds = false; OBSDataAutoRelease boundsError = obs_data_create(); - OBSDataAutoRelease reqBounds = obs_data_get_obj(req->data, "bounds"); + OBSDataAutoRelease reqBounds = obs_data_get_obj(request.parameters(), "bounds"); if (obs_data_has_user_value(reqBounds, "type")) { QString newBoundsType = obs_data_get_string(reqBounds, "type"); if (newBoundsType == "OBS_BOUNDS_NONE") { @@ -246,17 +247,17 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* } } if (badBounds) { - obs_data_set_obj(errorMessage, "bounds", boundsError); + obs_data_set_obj(errorData, "bounds", boundsError); } } obs_sceneitem_defer_update_end(sceneItem); if (badRequest) { - return req->SendErrorResponse(errorMessage); + return request.failed("error", errorData); } - return req->SendOKResponse(); + return request.success(); } /** @@ -270,27 +271,27 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemProperties(WSRequestHandler* * @category scene items * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleResetSceneItem(WSRequestHandler* req) { +RpcResponse WSRequestHandler::ResetSceneItem(const RpcRequest& request) { // TODO: remove this request, or refactor it to ResetSource - if (!req->hasField("item")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("item")) { + return request.failed("missing request parameters"); } - const char* itemName = obs_data_get_string(req->data, "item"); + const char* itemName = obs_data_get_string(request.parameters(), "item"); if (!itemName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - const char* sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + const char* sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } OBSSource sceneItemSource = obs_sceneitem_get_source(sceneItem); @@ -298,7 +299,7 @@ HandlerResponse WSRequestHandler::HandleResetSceneItem(WSRequestHandler* req) { OBSDataAutoRelease settings = obs_source_get_settings(sceneItemSource); obs_source_update(sceneItemSource, settings); - return req->SendOKResponse(); + return request.success(); } /** @@ -314,34 +315,34 @@ HandlerResponse WSRequestHandler::HandleResetSceneItem(WSRequestHandler* req) { * @since 0.3 * @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties. */ -HandlerResponse WSRequestHandler::HandleSetSceneItemRender(WSRequestHandler* req) { - if (!req->hasField("source") || - !req->hasField("render")) +RpcResponse WSRequestHandler::SetSceneItemRender(const RpcRequest& request) { + if (!request.hasField("source") || + !request.hasField("render")) { - return req->SendErrorResponse("missing request parameters"); + return request.failed("missing request parameters"); } - const char* itemName = obs_data_get_string(req->data, "source"); - bool isVisible = obs_data_get_bool(req->data, "render"); + const char* itemName = obs_data_get_string(request.parameters(), "source"); + bool isVisible = obs_data_get_bool(request.parameters(), "render"); if (!itemName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - const char* sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + const char* sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } obs_sceneitem_set_visible(sceneItem, isVisible); - return req->SendOKResponse(); + return request.success(); } /** @@ -359,34 +360,34 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemRender(WSRequestHandler* req * @since 4.0.0 * @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties. */ -HandlerResponse WSRequestHandler::HandleSetSceneItemPosition(WSRequestHandler* req) { - if (!req->hasField("item") || - !req->hasField("x") || !req->hasField("y")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetSceneItemPosition(const RpcRequest& request) { + if (!request.hasField("item") || + !request.hasField("x") || !request.hasField("y")) { + return request.failed("missing request parameters"); } - QString itemName = obs_data_get_string(req->data, "item"); + QString itemName = obs_data_get_string(request.parameters(), "item"); if (itemName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - QString sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + QString sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene could not be found"); + return request.failed("requested scene could not be found"); } OBSSceneItem sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } vec2 item_position = { 0 }; - item_position.x = obs_data_get_double(req->data, "x"); - item_position.y = obs_data_get_double(req->data, "y"); + item_position.x = obs_data_get_double(request.parameters(), "x"); + item_position.y = obs_data_get_double(request.parameters(), "y"); obs_sceneitem_set_pos(sceneItem, &item_position); - return req->SendOKResponse(); + return request.success(); } /** @@ -404,34 +405,34 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemPosition(WSRequestHandler* r * @since 4.0.0 * @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties. */ -HandlerResponse WSRequestHandler::HandleSetSceneItemTransform(WSRequestHandler* req) { - if (!req->hasField("item") || - !req->hasField("x-scale") || - !req->hasField("y-scale") || - !req->hasField("rotation")) +RpcResponse WSRequestHandler::SetSceneItemTransform(const RpcRequest& request) { + if (!request.hasField("item") || + !request.hasField("x-scale") || + !request.hasField("y-scale") || + !request.hasField("rotation")) { - return req->SendErrorResponse("missing request parameters"); + return request.failed("missing request parameters"); } - QString itemName = obs_data_get_string(req->data, "item"); + QString itemName = obs_data_get_string(request.parameters(), "item"); if (itemName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - QString sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + QString sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } 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"); + scale.x = obs_data_get_double(request.parameters(), "x-scale"); + scale.y = obs_data_get_double(request.parameters(), "y-scale"); + float rotation = obs_data_get_double(request.parameters(), "rotation"); OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } obs_sceneitem_defer_update_begin(sceneItem); @@ -441,7 +442,7 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemTransform(WSRequestHandler* obs_sceneitem_defer_update_end(sceneItem); - return req->SendOKResponse(); + return request.success(); } /** @@ -460,36 +461,36 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemTransform(WSRequestHandler* * @since 4.1.0 * @deprecated Since 4.3.0. Prefer the use of SetSceneItemProperties. */ -HandlerResponse WSRequestHandler::HandleSetSceneItemCrop(WSRequestHandler* req) { - if (!req->hasField("item")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetSceneItemCrop(const RpcRequest& request) { + if (!request.hasField("item")) { + return request.failed("missing request parameters"); } - QString itemName = obs_data_get_string(req->data, "item"); + QString itemName = obs_data_get_string(request.parameters(), "item"); if (itemName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } - QString sceneName = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + QString sceneName = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromName(scene, itemName); if (!sceneItem) { - return req->SendErrorResponse("specified scene item doesn't exist"); + return request.failed("specified scene item doesn't exist"); } 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"); + crop.top = obs_data_get_int(request.parameters(), "top"); + crop.bottom = obs_data_get_int(request.parameters(), "bottom"); + crop.left = obs_data_get_int(request.parameters(), "left"); + crop.right = obs_data_get_int(request.parameters(), "right"); obs_sceneitem_set_crop(sceneItem, &crop); - return req->SendOKResponse(); + return request.success(); } /** @@ -505,38 +506,26 @@ HandlerResponse WSRequestHandler::HandleSetSceneItemCrop(WSRequestHandler* req) * @category scene items * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleDeleteSceneItem(WSRequestHandler* req) { - if (!req->hasField("item")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::DeleteSceneItem(const RpcRequest& request) { + if (!request.hasField("item")) { + return request.failed("missing request parameters"); } - const char* sceneName = obs_data_get_string(req->data, "scene"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); + const char* sceneName = obs_data_get_string(request.parameters(), "scene"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } - OBSDataAutoRelease item = obs_data_get_obj(req->data, "item"); + OBSDataAutoRelease item = obs_data_get_obj(request.parameters(), "item"); OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromItem(scene, item); if (!sceneItem) { - return req->SendErrorResponse("item with id/name combination not found in specified scene"); + return request.failed("item with id/name combination not found in specified scene"); } obs_sceneitem_remove(sceneItem); - return req->SendOKResponse(); -} - -struct DuplicateSceneItemData { - obs_sceneitem_t *referenceItem; - obs_source_t *fromSource; - obs_sceneitem_t *newItem; -}; - -static void DuplicateSceneItem(void *_data, obs_scene_t *scene) { - DuplicateSceneItemData *data = (DuplicateSceneItemData *)_data; - data->newItem = obs_scene_add(scene, data->fromSource); - obs_sceneitem_set_visible(data->newItem, obs_sceneitem_visible(data->referenceItem)); + return request.success(); } /** @@ -558,27 +547,33 @@ static void DuplicateSceneItem(void *_data, obs_scene_t *scene) { * @category scene items * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleDuplicateSceneItem(WSRequestHandler* req) { - if (!req->hasField("item")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::DuplicateSceneItem(const RpcRequest& request) { + struct DuplicateSceneItemData { + obs_sceneitem_t *referenceItem; + obs_source_t *fromSource; + obs_sceneitem_t *newItem; + }; + + if (!request.hasField("item")) { + return request.failed("missing request parameters"); } - const char* fromSceneName = obs_data_get_string(req->data, "fromScene"); - OBSSourceAutoRelease fromScene = Utils::GetSceneFromNameOrCurrent(fromSceneName); + const char* fromSceneName = obs_data_get_string(request.parameters(), "fromScene"); + OBSScene fromScene = Utils::GetSceneFromNameOrCurrent(fromSceneName); if (!fromScene) { - return req->SendErrorResponse("requested fromScene doesn't exist"); + return request.failed("requested fromScene doesn't exist"); } - const char* toSceneName = obs_data_get_string(req->data, "toScene"); - OBSSourceAutoRelease toScene = Utils::GetSceneFromNameOrCurrent(toSceneName); + const char* toSceneName = obs_data_get_string(request.parameters(), "toScene"); + OBSScene toScene = Utils::GetSceneFromNameOrCurrent(toSceneName); if (!toScene) { - return req->SendErrorResponse("requested toScene doesn't exist"); + return request.failed("requested toScene doesn't exist"); } - OBSDataAutoRelease item = obs_data_get_obj(req->data, "item"); + OBSDataAutoRelease item = obs_data_get_obj(request.parameters(), "item"); OBSSceneItemAutoRelease referenceItem = Utils::GetSceneItemFromItem(fromScene, item); if (!referenceItem) { - return req->SendErrorResponse("item with id/name combination not found in specified scene"); + return request.failed("item with id/name combination not found in specified scene"); } DuplicateSceneItemData data; @@ -586,12 +581,16 @@ HandlerResponse WSRequestHandler::HandleDuplicateSceneItem(WSRequestHandler* req data.referenceItem = referenceItem; obs_enter_graphics(); - obs_scene_atomic_update(obs_scene_from_source(toScene), DuplicateSceneItem, &data); + obs_scene_atomic_update(toScene, [](void *_data, obs_scene_t *scene) { + auto data = reinterpret_cast(_data); + data->newItem = obs_scene_add(scene, data->fromSource); + obs_sceneitem_set_visible(data->newItem, obs_sceneitem_visible(data->referenceItem)); + }, &data); obs_leave_graphics(); obs_sceneitem_t *newItem = data.newItem; if (!newItem) { - return req->SendErrorResponse("Error duplicating scene item"); + return request.failed("Error duplicating scene item"); } OBSDataAutoRelease itemData = obs_data_create(); @@ -600,7 +599,7 @@ HandlerResponse WSRequestHandler::HandleDuplicateSceneItem(WSRequestHandler* req OBSDataAutoRelease responseData = obs_data_create(); obs_data_set_obj(responseData, "item", itemData); - obs_data_set_string(responseData, "scene", obs_source_get_name(toScene)); + obs_data_set_string(responseData, "scene", obs_source_get_name(obs_scene_get_source(toScene))); - return req->SendOKResponse(responseData); + return request.success(responseData); } diff --git a/src/WSRequestHandler_Scenes.cpp b/src/WSRequestHandler_Scenes.cpp index 6e8429c8..9399ac5d 100644 --- a/src/WSRequestHandler_Scenes.cpp +++ b/src/WSRequestHandler_Scenes.cpp @@ -18,19 +18,19 @@ * @category scenes * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleSetCurrentScene(WSRequestHandler* req) { - if (!req->hasField("scene-name")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetCurrentScene(const RpcRequest& request) { + if (!request.hasField("scene-name")) { + return request.failed("missing request parameters"); } - const char* sceneName = obs_data_get_string(req->data, "scene-name"); + const char* sceneName = obs_data_get_string(request.parameters(), "scene-name"); OBSSourceAutoRelease source = obs_get_source_by_name(sceneName); if (source) { obs_frontend_set_current_scene(source); - return req->SendOKResponse(); + return request.success(); } else { - return req->SendErrorResponse("requested scene does not exist"); + return request.failed("requested scene does not exist"); } } @@ -45,7 +45,7 @@ HandlerResponse WSRequestHandler::HandleSetCurrentScene(WSRequestHandler* req) { * @category scenes * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleGetCurrentScene(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetCurrentScene(const RpcRequest& request) { OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene(); OBSDataArrayAutoRelease sceneItems = Utils::GetSceneItems(currentScene); @@ -53,7 +53,7 @@ HandlerResponse WSRequestHandler::HandleGetCurrentScene(WSRequestHandler* req) { obs_data_set_string(data, "name", obs_source_get_name(currentScene)); obs_data_set_array(data, "sources", sceneItems); - return req->SendOKResponse(data); + return request.success(data); } /** @@ -67,7 +67,7 @@ HandlerResponse WSRequestHandler::HandleGetCurrentScene(WSRequestHandler* req) { * @category scenes * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleGetSceneList(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetSceneList(const RpcRequest& request) { OBSSourceAutoRelease currentScene = obs_frontend_get_current_scene(); OBSDataArrayAutoRelease scenes = Utils::GetScenes(); @@ -76,7 +76,7 @@ HandlerResponse WSRequestHandler::HandleGetSceneList(WSRequestHandler* req) { obs_source_get_name(currentScene)); obs_data_set_array(data, "scenes", scenes); - return req->SendOKResponse(data); + return request.success(data); } /** @@ -92,50 +92,59 @@ HandlerResponse WSRequestHandler::HandleGetSceneList(WSRequestHandler* req) { * @category scenes * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleReorderSceneItems(WSRequestHandler* req) { - QString sceneName = obs_data_get_string(req->data, "scene"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(sceneName); +RpcResponse WSRequestHandler::ReorderSceneItems(const RpcRequest& request) { + QString sceneName = obs_data_get_string(request.parameters(), "scene"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(sceneName); if (!scene) { - return req->SendErrorResponse("requested scene doesn't exist"); + return request.failed("requested scene doesn't exist"); } - OBSDataArrayAutoRelease items = obs_data_get_array(req->data, "items"); + OBSDataArrayAutoRelease items = obs_data_get_array(request.parameters(), "items"); if (!items) { - return req->SendErrorResponse("sceneItem order not specified"); + return request.failed("sceneItem order not specified"); } - size_t count = obs_data_array_count(items); + struct reorder_context { + obs_data_array_t* items; + bool success; + QString errorMessage; + }; - std::vector newOrder; - newOrder.reserve(count); + struct reorder_context ctx; + ctx.success = false; + ctx.items = items; - for (size_t i = 0; i < count; ++i) { - OBSDataAutoRelease item = obs_data_array_item(items, i); + obs_scene_atomic_update(scene, [](void* param, obs_scene_t* scene) { + auto ctx = reinterpret_cast(param); - OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromItem(scene, item); - obs_sceneitem_release(sceneItem); // ref dec + QVector orderList; + struct obs_sceneitem_order_info info; - if (!sceneItem) { - return req->SendErrorResponse("Invalid sceneItem id or name specified"); - } - - for (size_t j = 0; j <= i; ++j) { - if (sceneItem == newOrder[j]) { - return req->SendErrorResponse("Duplicate sceneItem in specified order"); + size_t itemCount = obs_data_array_count(ctx->items); + for (int i = 0; i < itemCount; i++) { + OBSDataAutoRelease item = obs_data_array_item(ctx->items, i); + + OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromItem(scene, item); + if (!sceneItem) { + ctx->success = false; + ctx->errorMessage = "Invalid sceneItem id or name specified"; + return; } + + info.group = nullptr; + info.item = sceneItem; + orderList.insert(0, info); } - newOrder.push_back(sceneItem); + ctx->success = obs_scene_reorder_items2(scene, orderList.data(), orderList.size()); + if (!ctx->success) { + ctx->errorMessage = "Invalid sceneItem order"; + } + }, &ctx); + + if (!ctx.success) { + return request.failed(ctx.errorMessage); } - bool success = obs_scene_reorder_items(obs_scene_from_source(scene), newOrder.data(), count); - if (!success) { - return req->SendErrorResponse("Invalid sceneItem order"); - } - - for (auto const& item: newOrder) { - obs_sceneitem_release(item); - } - - return req->SendOKResponse(); + return request.success(); } diff --git a/src/WSRequestHandler_Sources.cpp b/src/WSRequestHandler_Sources.cpp index b48bd9b1..e043ee0e 100644 --- a/src/WSRequestHandler_Sources.cpp +++ b/src/WSRequestHandler_Sources.cpp @@ -21,7 +21,7 @@ * @category sources * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleGetSourcesList(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetSourcesList(const RpcRequest& request) { OBSDataArrayAutoRelease sourcesArray = obs_data_array_create(); @@ -64,7 +64,7 @@ HandlerResponse WSRequestHandler::HandleGetSourcesList(WSRequestHandler* req) OBSDataAutoRelease response = obs_data_create(); obs_data_set_array(response, "sources", sourcesArray); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -89,7 +89,7 @@ HandlerResponse WSRequestHandler::HandleGetSourcesList(WSRequestHandler* req) * @category sources * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleGetSourceTypesList(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetSourceTypesList(const RpcRequest& request) { OBSDataArrayAutoRelease idsArray = obs_data_array_create(); @@ -142,7 +142,7 @@ HandlerResponse WSRequestHandler::HandleGetSourceTypesList(WSRequestHandler* req OBSDataAutoRelease response = obs_data_create(); obs_data_set_array(response, "types", idsArray); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -159,20 +159,20 @@ HandlerResponse WSRequestHandler::HandleGetSourceTypesList(WSRequestHandler* req * @category sources * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleGetVolume(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetVolume(const RpcRequest& request) { - if (!req->hasField("source")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); if (sourceName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSDataAutoRelease response = obs_data_create(); @@ -180,7 +180,7 @@ HandlerResponse WSRequestHandler::HandleGetVolume(WSRequestHandler* req) obs_data_set_double(response, "volume", obs_source_get_volume(source)); obs_data_set_bool(response, "muted", obs_source_muted(source)); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -194,26 +194,26 @@ HandlerResponse WSRequestHandler::HandleGetVolume(WSRequestHandler* req) * @category sources * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleSetVolume(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetVolume(const RpcRequest& request) { - if (!req->hasField("source") || !req->hasField("volume")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source") || !request.hasField("volume")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); - float sourceVolume = obs_data_get_double(req->data, "volume"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); + float sourceVolume = obs_data_get_double(request.parameters(), "volume"); if (sourceName.isEmpty() || sourceVolume < 0.0 || sourceVolume > 1.0) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } obs_source_set_volume(source, sourceVolume); - return req->SendOKResponse(); + return request.success(); } /** @@ -229,27 +229,27 @@ HandlerResponse WSRequestHandler::HandleSetVolume(WSRequestHandler* req) * @category sources * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleGetMute(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetMute(const RpcRequest& request) { - if (!req->hasField("source")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); if (sourceName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSDataAutoRelease response = obs_data_create(); obs_data_set_string(response, "name", obs_source_get_name(source)); obs_data_set_bool(response, "muted", obs_source_muted(source)); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -263,26 +263,26 @@ HandlerResponse WSRequestHandler::HandleGetMute(WSRequestHandler* req) * @category sources * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleSetMute(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetMute(const RpcRequest& request) { - if (!req->hasField("source") || !req->hasField("mute")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source") || !request.hasField("mute")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); - bool mute = obs_data_get_bool(req->data, "mute"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); + bool mute = obs_data_get_bool(request.parameters(), "mute"); if (sourceName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } obs_source_set_muted(source, mute); - return req->SendOKResponse(); + return request.success(); } /** @@ -295,24 +295,24 @@ HandlerResponse WSRequestHandler::HandleSetMute(WSRequestHandler* req) * @category sources * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleToggleMute(WSRequestHandler* req) +RpcResponse WSRequestHandler::ToggleMute(const RpcRequest& request) { - if (!req->hasField("source")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); if (sourceName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } obs_source_set_muted(source, !obs_source_muted(source)); - return req->SendOKResponse(); + return request.success(); } /** @@ -326,26 +326,26 @@ HandlerResponse WSRequestHandler::HandleToggleMute(WSRequestHandler* req) * @category sources * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleSetSyncOffset(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetSyncOffset(const RpcRequest& request) { - if (!req->hasField("source") || !req->hasField("offset")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source") || !request.hasField("offset")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); - int64_t sourceSyncOffset = (int64_t)obs_data_get_int(req->data, "offset"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); + int64_t sourceSyncOffset = (int64_t)obs_data_get_int(request.parameters(), "offset"); - if (sourceName.isEmpty() || sourceSyncOffset < 0) { - return req->SendErrorResponse("invalid request parameters"); + if (sourceName.isEmpty()) { + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } obs_source_set_sync_offset(source, sourceSyncOffset); - return req->SendOKResponse(); + return request.success(); } /** @@ -361,27 +361,27 @@ HandlerResponse WSRequestHandler::HandleSetSyncOffset(WSRequestHandler* req) * @category sources * @since 4.2.0 */ -HandlerResponse WSRequestHandler::HandleGetSyncOffset(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetSyncOffset(const RpcRequest& request) { - if (!req->hasField("source")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source")) { + return request.failed("missing request parameters"); } - QString sourceName = obs_data_get_string(req->data, "source"); + QString sourceName = obs_data_get_string(request.parameters(), "source"); if (sourceName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName.toUtf8()); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSDataAutoRelease response = obs_data_create(); obs_data_set_string(response, "name", obs_source_get_name(source)); obs_data_set_int(response, "offset", obs_source_get_sync_offset(source)); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -399,24 +399,24 @@ HandlerResponse WSRequestHandler::HandleGetSyncOffset(WSRequestHandler* req) * @category sources * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleGetSourceSettings(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetSourceSettings(const RpcRequest& request) { - if (!req->hasField("sourceName")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } - if (req->hasField("sourceType")) { + if (request.hasField("sourceType")) { QString actualSourceType = obs_source_get_id(source); - QString requestedType = obs_data_get_string(req->data, "sourceType"); + QString requestedType = obs_data_get_string(request.parameters(), "sourceType"); if (actualSourceType != requestedType) { - return req->SendErrorResponse("specified source exists but is not of expected type"); + return request.failed("specified source exists but is not of expected type"); } } @@ -427,7 +427,7 @@ HandlerResponse WSRequestHandler::HandleGetSourceSettings(WSRequestHandler* req) obs_data_set_string(response, "sourceType", obs_source_get_id(source)); obs_data_set_obj(response, "sourceSettings", sourceSettings); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -446,29 +446,29 @@ HandlerResponse WSRequestHandler::HandleGetSourceSettings(WSRequestHandler* req) * @category sources * @since 4.3.0 */ -HandlerResponse WSRequestHandler::HandleSetSourceSettings(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetSourceSettings(const RpcRequest& request) { - if (!req->hasField("sourceName") || !req->hasField("sourceSettings")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName") || !request.hasField("sourceSettings")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } - if (req->hasField("sourceType")) { + if (request.hasField("sourceType")) { QString actualSourceType = obs_source_get_id(source); - QString requestedType = obs_data_get_string(req->data, "sourceType"); + QString requestedType = obs_data_get_string(request.parameters(), "sourceType"); if (actualSourceType != requestedType) { - return req->SendErrorResponse("specified source exists but is not of expected type"); + return request.failed("specified source exists but is not of expected type"); } } OBSDataAutoRelease currentSettings = obs_source_get_settings(source); - OBSDataAutoRelease newSettings = obs_data_get_obj(req->data, "sourceSettings"); + OBSDataAutoRelease newSettings = obs_data_get_obj(request.parameters(), "sourceSettings"); OBSDataAutoRelease sourceSettings = obs_data_create(); obs_data_apply(sourceSettings, currentSettings); @@ -482,7 +482,7 @@ HandlerResponse WSRequestHandler::HandleSetSourceSettings(WSRequestHandler* req) obs_data_set_string(response, "sourceType", obs_source_get_id(source)); obs_data_set_obj(response, "sourceSettings", sourceSettings); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -524,27 +524,27 @@ HandlerResponse WSRequestHandler::HandleSetSourceSettings(WSRequestHandler* req) * @category sources * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetTextGDIPlusProperties(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetTextGDIPlusProperties(const RpcRequest& request) { - const char* sourceName = obs_data_get_string(req->data, "source"); + const char* sourceName = obs_data_get_string(request.parameters(), "source"); if (!sourceName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } QString sourceId = obs_source_get_id(source); if (sourceId != "text_gdiplus") { - return req->SendErrorResponse("not a text gdi plus source"); + return request.failed("not a text gdi plus source"); } OBSDataAutoRelease response = obs_source_get_settings(source); obs_data_set_string(response, "source", obs_source_get_name(source)); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -585,77 +585,77 @@ HandlerResponse WSRequestHandler::HandleGetTextGDIPlusProperties(WSRequestHandle * @category sources * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetTextGDIPlusProperties(const RpcRequest& request) { - if (!req->hasField("source")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "source"); + const char* sourceName = obs_data_get_string(request.parameters(), "source"); if (!sourceName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } QString sourceId = obs_source_get_id(source); if (sourceId != "text_gdiplus") { - return req->SendErrorResponse("not a text gdi plus source"); + return request.failed("not a text gdi plus source"); } OBSDataAutoRelease settings = obs_source_get_settings(source); - if (req->hasField("align")) { - obs_data_set_string(settings, "align", obs_data_get_string(req->data, "align")); + if (request.hasField("align")) { + obs_data_set_string(settings, "align", obs_data_get_string(request.parameters(), "align")); } - if (req->hasField("bk_color")) { - obs_data_set_int(settings, "bk_color", obs_data_get_int(req->data, "bk_color")); + if (request.hasField("bk_color")) { + obs_data_set_int(settings, "bk_color", obs_data_get_int(request.parameters(), "bk_color")); } - if (req->hasField("bk-opacity")) { - obs_data_set_int(settings, "bk_opacity", obs_data_get_int(req->data, "bk_opacity")); + if (request.hasField("bk-opacity")) { + obs_data_set_int(settings, "bk_opacity", obs_data_get_int(request.parameters(), "bk_opacity")); } - if (req->hasField("chatlog")) { - obs_data_set_bool(settings, "chatlog", obs_data_get_bool(req->data, "chatlog")); + if (request.hasField("chatlog")) { + obs_data_set_bool(settings, "chatlog", obs_data_get_bool(request.parameters(), "chatlog")); } - if (req->hasField("chatlog_lines")) { - obs_data_set_int(settings, "chatlog_lines", obs_data_get_int(req->data, "chatlog_lines")); + if (request.hasField("chatlog_lines")) { + obs_data_set_int(settings, "chatlog_lines", obs_data_get_int(request.parameters(), "chatlog_lines")); } - if (req->hasField("color")) { - obs_data_set_int(settings, "color", obs_data_get_int(req->data, "color")); + if (request.hasField("color")) { + obs_data_set_int(settings, "color", obs_data_get_int(request.parameters(), "color")); } - if (req->hasField("extents")) { - obs_data_set_bool(settings, "extents", obs_data_get_bool(req->data, "extents")); + if (request.hasField("extents")) { + obs_data_set_bool(settings, "extents", obs_data_get_bool(request.parameters(), "extents")); } - if (req->hasField("extents_wrap")) { - obs_data_set_bool(settings, "extents_wrap", obs_data_get_bool(req->data, "extents_wrap")); + if (request.hasField("extents_wrap")) { + obs_data_set_bool(settings, "extents_wrap", obs_data_get_bool(request.parameters(), "extents_wrap")); } - if (req->hasField("extents_cx")) { - obs_data_set_int(settings, "extents_cx", obs_data_get_int(req->data, "extents_cx")); + if (request.hasField("extents_cx")) { + obs_data_set_int(settings, "extents_cx", obs_data_get_int(request.parameters(), "extents_cx")); } - if (req->hasField("extents_cy")) { - obs_data_set_int(settings, "extents_cy", obs_data_get_int(req->data, "extents_cy")); + if (request.hasField("extents_cy")) { + obs_data_set_int(settings, "extents_cy", obs_data_get_int(request.parameters(), "extents_cy")); } - if (req->hasField("file")) { - obs_data_set_string(settings, "file", obs_data_get_string(req->data, "file")); + if (request.hasField("file")) { + obs_data_set_string(settings, "file", obs_data_get_string(request.parameters(), "file")); } - if (req->hasField("font")) { + if (request.hasField("font")) { OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); if (font_obj) { - OBSDataAutoRelease req_font_obj = obs_data_get_obj(req->data, "font"); + OBSDataAutoRelease req_font_obj = obs_data_get_obj(request.parameters(), "font"); if (obs_data_has_user_value(req_font_obj, "face")) { obs_data_set_string(font_obj, "face", obs_data_get_string(req_font_obj, "face")); @@ -675,57 +675,57 @@ HandlerResponse WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandle } } - if (req->hasField("gradient")) { - obs_data_set_bool(settings, "gradient", obs_data_get_bool(req->data, "gradient")); + if (request.hasField("gradient")) { + obs_data_set_bool(settings, "gradient", obs_data_get_bool(request.parameters(), "gradient")); } - if (req->hasField("gradient_color")) { - obs_data_set_int(settings, "gradient_color", obs_data_get_int(req->data, "gradient_color")); + if (request.hasField("gradient_color")) { + obs_data_set_int(settings, "gradient_color", obs_data_get_int(request.parameters(), "gradient_color")); } - if (req->hasField("gradient_dir")) { - obs_data_set_double(settings, "gradient_dir", obs_data_get_double(req->data, "gradient_dir")); + if (request.hasField("gradient_dir")) { + obs_data_set_double(settings, "gradient_dir", obs_data_get_double(request.parameters(), "gradient_dir")); } - if (req->hasField("gradient_opacity")) { - obs_data_set_int(settings, "gradient_opacity", obs_data_get_int(req->data, "gradient_opacity")); + if (request.hasField("gradient_opacity")) { + obs_data_set_int(settings, "gradient_opacity", obs_data_get_int(request.parameters(), "gradient_opacity")); } - if (req->hasField("outline")) { - obs_data_set_bool(settings, "outline", obs_data_get_bool(req->data, "outline")); + if (request.hasField("outline")) { + obs_data_set_bool(settings, "outline", obs_data_get_bool(request.parameters(), "outline")); } - if (req->hasField("outline_size")) { - obs_data_set_int(settings, "outline_size", obs_data_get_int(req->data, "outline_size")); + if (request.hasField("outline_size")) { + obs_data_set_int(settings, "outline_size", obs_data_get_int(request.parameters(), "outline_size")); } - if (req->hasField("outline_color")) { - obs_data_set_int(settings, "outline_color", obs_data_get_int(req->data, "outline_color")); + if (request.hasField("outline_color")) { + obs_data_set_int(settings, "outline_color", obs_data_get_int(request.parameters(), "outline_color")); } - if (req->hasField("outline_opacity")) { - obs_data_set_int(settings, "outline_opacity", obs_data_get_int(req->data, "outline_opacity")); + if (request.hasField("outline_opacity")) { + obs_data_set_int(settings, "outline_opacity", obs_data_get_int(request.parameters(), "outline_opacity")); } - if (req->hasField("read_from_file")) { - obs_data_set_bool(settings, "read_from_file", obs_data_get_bool(req->data, "read_from_file")); + if (request.hasField("read_from_file")) { + obs_data_set_bool(settings, "read_from_file", obs_data_get_bool(request.parameters(), "read_from_file")); } - if (req->hasField("text")) { - obs_data_set_string(settings, "text", obs_data_get_string(req->data, "text")); + if (request.hasField("text")) { + obs_data_set_string(settings, "text", obs_data_get_string(request.parameters(), "text")); } - if (req->hasField("valign")) { - obs_data_set_string(settings, "valign", obs_data_get_string(req->data, "valign")); + if (request.hasField("valign")) { + obs_data_set_string(settings, "valign", obs_data_get_string(request.parameters(), "valign")); } - if (req->hasField("vertical")) { - obs_data_set_bool(settings, "vertical", obs_data_get_bool(req->data, "vertical")); + if (request.hasField("vertical")) { + obs_data_set_bool(settings, "vertical", obs_data_get_bool(request.parameters(), "vertical")); } obs_source_update(source, settings); - return req->SendOKResponse(); + return request.success(); } /** @@ -755,27 +755,27 @@ HandlerResponse WSRequestHandler::HandleSetTextGDIPlusProperties(WSRequestHandle * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleGetTextFreetype2Properties(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetTextFreetype2Properties(const RpcRequest& request) { - const char* sourceName = obs_data_get_string(req->data, "source"); + const char* sourceName = obs_data_get_string(request.parameters(), "source"); if (!sourceName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } QString sourceId = obs_source_get_id(source); if (sourceId != "text_ft2_source") { - return req->SendErrorResponse("not a freetype 2 source"); + return request.failed("not a freetype 2 source"); } OBSDataAutoRelease response = obs_source_get_settings(source); obs_data_set_string(response, "source", sourceName); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -803,45 +803,45 @@ HandlerResponse WSRequestHandler::HandleGetTextFreetype2Properties(WSRequestHand * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleSetTextFreetype2Properties(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetTextFreetype2Properties(const RpcRequest& request) { - const char* sourceName = obs_data_get_string(req->data, "source"); + const char* sourceName = obs_data_get_string(request.parameters(), "source"); if (!sourceName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } QString sourceId = obs_source_get_id(source); if (sourceId != "text_ft2_source") { - return req->SendErrorResponse("not text freetype 2 source"); + return request.failed("not text freetype 2 source"); } OBSDataAutoRelease settings = obs_source_get_settings(source); - if (req->hasField("color1")) { - obs_data_set_int(settings, "color1", obs_data_get_int(req->data, "color1")); + if (request.hasField("color1")) { + obs_data_set_int(settings, "color1", obs_data_get_int(request.parameters(), "color1")); } - if (req->hasField("color2")) { - obs_data_set_int(settings, "color2", obs_data_get_int(req->data, "color2")); + if (request.hasField("color2")) { + obs_data_set_int(settings, "color2", obs_data_get_int(request.parameters(), "color2")); } - if (req->hasField("custom_width")) { - obs_data_set_int(settings, "custom_width", obs_data_get_int(req->data, "custom_width")); + if (request.hasField("custom_width")) { + obs_data_set_int(settings, "custom_width", obs_data_get_int(request.parameters(), "custom_width")); } - if (req->hasField("drop_shadow")) { - obs_data_set_bool(settings, "drop_shadow", obs_data_get_bool(req->data, "drop_shadow")); + if (request.hasField("drop_shadow")) { + obs_data_set_bool(settings, "drop_shadow", obs_data_get_bool(request.parameters(), "drop_shadow")); } - if (req->hasField("font")) { + if (request.hasField("font")) { OBSDataAutoRelease font_obj = obs_data_get_obj(settings, "font"); if (font_obj) { - OBSDataAutoRelease req_font_obj = obs_data_get_obj(req->data, "font"); + OBSDataAutoRelease req_font_obj = obs_data_get_obj(request.parameters(), "font"); if (obs_data_has_user_value(req_font_obj, "face")) { obs_data_set_string(font_obj, "face", obs_data_get_string(req_font_obj, "face")); @@ -861,33 +861,33 @@ HandlerResponse WSRequestHandler::HandleSetTextFreetype2Properties(WSRequestHand } } - if (req->hasField("from_file")) { - obs_data_set_bool(settings, "from_file", obs_data_get_bool(req->data, "from_file")); + if (request.hasField("from_file")) { + obs_data_set_bool(settings, "from_file", obs_data_get_bool(request.parameters(), "from_file")); } - if (req->hasField("log_mode")) { - obs_data_set_bool(settings, "log_mode", obs_data_get_bool(req->data, "log_mode")); + if (request.hasField("log_mode")) { + obs_data_set_bool(settings, "log_mode", obs_data_get_bool(request.parameters(), "log_mode")); } - if (req->hasField("outline")) { - obs_data_set_bool(settings, "outline", obs_data_get_bool(req->data, "outline")); + if (request.hasField("outline")) { + obs_data_set_bool(settings, "outline", obs_data_get_bool(request.parameters(), "outline")); } - if (req->hasField("text")) { - obs_data_set_string(settings, "text", obs_data_get_string(req->data, "text")); + if (request.hasField("text")) { + obs_data_set_string(settings, "text", obs_data_get_string(request.parameters(), "text")); } - if (req->hasField("text_file")) { - obs_data_set_string(settings, "text_file", obs_data_get_string(req->data, "text_file")); + if (request.hasField("text_file")) { + obs_data_set_string(settings, "text_file", obs_data_get_string(request.parameters(), "text_file")); } - if (req->hasField("word_wrap")) { - obs_data_set_bool(settings, "word_wrap", obs_data_get_bool(req->data, "word_wrap")); + if (request.hasField("word_wrap")) { + obs_data_set_bool(settings, "word_wrap", obs_data_get_bool(request.parameters(), "word_wrap")); } obs_source_update(source, settings); - return req->SendOKResponse(); + return request.success(); } /** @@ -910,27 +910,27 @@ HandlerResponse WSRequestHandler::HandleSetTextFreetype2Properties(WSRequestHand * @category sources * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetBrowserSourceProperties(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetBrowserSourceProperties(const RpcRequest& request) { - const char* sourceName = obs_data_get_string(req->data, "source"); + const char* sourceName = obs_data_get_string(request.parameters(), "source"); if (!sourceName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } QString sourceId = obs_source_get_id(source); - if (sourceId != "browser_source") { - return req->SendErrorResponse("not a browser source"); + if (sourceId != "browser_source" && sourceId != "linuxbrowser-source") { + return request.failed("not a browser source"); } OBSDataAutoRelease response = obs_source_get_settings(source); obs_data_set_string(response, "source", obs_source_get_name(source)); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -952,68 +952,68 @@ HandlerResponse WSRequestHandler::HandleGetBrowserSourceProperties(WSRequestHand * @category sources * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleSetBrowserSourceProperties(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetBrowserSourceProperties(const RpcRequest& request) { - if (!req->hasField("source")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("source")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "source"); + const char* sourceName = obs_data_get_string(request.parameters(), "source"); if (!sourceName) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } QString sourceId = obs_source_get_id(source); - if(sourceId != "browser_source") { - return req->SendErrorResponse("not a browser source"); + if(sourceId != "browser_source" && sourceId != "linuxbrowser-source") { + return request.failed("not a browser source"); } OBSDataAutoRelease settings = obs_source_get_settings(source); - if (req->hasField("restart_when_active")) { - obs_data_set_bool(settings, "restart_when_active", obs_data_get_bool(req->data, "restart_when_active")); + if (request.hasField("restart_when_active")) { + obs_data_set_bool(settings, "restart_when_active", obs_data_get_bool(request.parameters(), "restart_when_active")); } - if (req->hasField("shutdown")) { - obs_data_set_bool(settings, "shutdown", obs_data_get_bool(req->data, "shutdown")); + if (request.hasField("shutdown")) { + obs_data_set_bool(settings, "shutdown", obs_data_get_bool(request.parameters(), "shutdown")); } - if (req->hasField("is_local_file")) { - obs_data_set_bool(settings, "is_local_file", obs_data_get_bool(req->data, "is_local_file")); + if (request.hasField("is_local_file")) { + obs_data_set_bool(settings, "is_local_file", obs_data_get_bool(request.parameters(), "is_local_file")); } - if (req->hasField("local_file")) { - obs_data_set_string(settings, "local_file", obs_data_get_string(req->data, "local_file")); + if (request.hasField("local_file")) { + obs_data_set_string(settings, "local_file", obs_data_get_string(request.parameters(), "local_file")); } - if (req->hasField("url")) { - obs_data_set_string(settings, "url", obs_data_get_string(req->data, "url")); + if (request.hasField("url")) { + obs_data_set_string(settings, "url", obs_data_get_string(request.parameters(), "url")); } - if (req->hasField("css")) { - obs_data_set_string(settings, "css", obs_data_get_string(req->data, "css")); + if (request.hasField("css")) { + obs_data_set_string(settings, "css", obs_data_get_string(request.parameters(), "css")); } - if (req->hasField("width")) { - obs_data_set_int(settings, "width", obs_data_get_int(req->data, "width")); + if (request.hasField("width")) { + obs_data_set_int(settings, "width", obs_data_get_int(request.parameters(), "width")); } - if (req->hasField("height")) { - obs_data_set_int(settings, "height", obs_data_get_int(req->data, "height")); + if (request.hasField("height")) { + obs_data_set_int(settings, "height", obs_data_get_int(request.parameters(), "height")); } - if (req->hasField("fps")) { - obs_data_set_int(settings, "fps", obs_data_get_int(req->data, "fps")); + if (request.hasField("fps")) { + obs_data_set_int(settings, "fps", obs_data_get_int(request.parameters(), "fps")); } obs_source_update(source, settings); - return req->SendOKResponse(); + return request.success(); } /** @@ -1030,7 +1030,7 @@ HandlerResponse WSRequestHandler::HandleSetBrowserSourceProperties(WSRequestHand * @category sources * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetSpecialSources(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetSpecialSources(const RpcRequest& request) { OBSDataAutoRelease response = obs_data_create(); @@ -1052,7 +1052,7 @@ HandlerResponse WSRequestHandler::HandleGetSpecialSources(WSRequestHandler* req) } } - return req->SendOKResponse(response); + return request.success(response); } /** @@ -1061,6 +1061,7 @@ HandlerResponse WSRequestHandler::HandleGetSpecialSources(WSRequestHandler* req) * @param {String} `sourceName` Source name * * @return {Array} `filters` List of filters for the specified source +* @return {Boolean} `filters.*.enabled` Filter status (enabled or not) * @return {String} `filters.*.type` Filter type * @return {String} `filters.*.name` Filter name * @return {Object} `filters.*.settings` Filter settings @@ -1070,23 +1071,61 @@ HandlerResponse WSRequestHandler::HandleGetSpecialSources(WSRequestHandler* req) * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleGetSourceFilters(WSRequestHandler* req) +RpcResponse WSRequestHandler::GetSourceFilters(const RpcRequest& request) { - if (!req->hasField("sourceName")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSDataArrayAutoRelease filters = Utils::GetSourceFiltersList(source, true); OBSDataAutoRelease response = obs_data_create(); obs_data_set_array(response, "filters", filters); - return req->SendOKResponse(response); + return request.success(response); +} + +/** +* List filters applied to a source +* +* @param {String} `sourceName` Source name +* @param {String} `filterName` Source filter name +* +* @return {Boolean} `enabled` Filter status (enabled or not) +* @return {String} `type` Filter type +* @return {String} `name` Filter name +* @return {Object} `settings` Filter settings +* +* @api requests +* @name GetSourceFilterInfo +* @category sources +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::GetSourceFilterInfo(const RpcRequest& request) +{ + if (!request.hasField("sourceName") || !request.hasField("filterName")) { + return request.failed("missing request parameters"); + } + + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); + if (!source) { + return request.failed("specified source doesn't exist"); + } + + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); + OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName); + if (!filter) { + return request.failed("specified filter doesn't exist on specified source"); + } + + OBSDataAutoRelease response = Utils::GetSourceFilterInfo(filter, true); + return request.success(response); } /** @@ -1102,40 +1141,40 @@ HandlerResponse WSRequestHandler::HandleGetSourceFilters(WSRequestHandler* req) * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleAddFilterToSource(WSRequestHandler* req) +RpcResponse WSRequestHandler::AddFilterToSource(const RpcRequest& request) { - if (!req->hasField("sourceName") || !req->hasField("filterName") || - !req->hasField("filterType") || !req->hasField("filterSettings")) + if (!request.hasField("sourceName") || !request.hasField("filterName") || + !request.hasField("filterType") || !request.hasField("filterSettings")) { - return req->SendErrorResponse("missing request parameters"); + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); - const char* filterName = obs_data_get_string(req->data, "filterName"); - const char* filterType = obs_data_get_string(req->data, "filterType"); - OBSDataAutoRelease filterSettings = obs_data_get_obj(req->data, "filterSettings"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); + const char* filterType = obs_data_get_string(request.parameters(), "filterType"); + OBSDataAutoRelease filterSettings = obs_data_get_obj(request.parameters(), "filterSettings"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSSourceAutoRelease existingFilter = obs_source_get_filter_by_name(source, filterName); if (existingFilter) { - return req->SendErrorResponse("filter name already taken"); + return request.failed("filter name already taken"); } OBSSourceAutoRelease filter = obs_source_create_private(filterType, filterName, filterSettings); if (!filter) { - return req->SendErrorResponse("filter creation failed"); + return request.failed("filter creation failed"); } if (obs_source_get_type(filter) != OBS_SOURCE_TYPE_FILTER) { - return req->SendErrorResponse("invalid filter type"); + return request.failed("invalid filter type"); } obs_source_filter_add(source, filter); - return req->SendOKResponse(); + return request.success(); } /** @@ -1149,28 +1188,28 @@ HandlerResponse WSRequestHandler::HandleAddFilterToSource(WSRequestHandler* req) * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleRemoveFilterFromSource(WSRequestHandler* req) +RpcResponse WSRequestHandler::RemoveFilterFromSource(const RpcRequest& request) { - if (!req->hasField("sourceName") || !req->hasField("filterName")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName") || !request.hasField("filterName")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); - const char* filterName = obs_data_get_string(req->data, "filterName"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName); if (!filter) { - return req->SendErrorResponse("specified filter doesn't exist"); + return request.failed("specified filter doesn't exist"); } obs_source_filter_remove(source, filter); - return req->SendOKResponse(); + return request.success(); } /** @@ -1185,28 +1224,28 @@ HandlerResponse WSRequestHandler::HandleRemoveFilterFromSource(WSRequestHandler* * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleReorderSourceFilter(WSRequestHandler* req) +RpcResponse WSRequestHandler::ReorderSourceFilter(const RpcRequest& request) { - if (!req->hasField("sourceName") || !req->hasField("filterName") || !req->hasField("newIndex")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName") || !request.hasField("filterName") || !request.hasField("newIndex")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); - const char* filterName = obs_data_get_string(req->data, "filterName"); - int newIndex = obs_data_get_int(req->data, "newIndex"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); + int newIndex = obs_data_get_int(request.parameters(), "newIndex"); if (newIndex < 0) { - return req->SendErrorResponse("invalid index"); + return request.failed("invalid index"); } OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName); if (!filter) { - return req->SendErrorResponse("specified filter doesn't exist"); + return request.failed("specified filter doesn't exist"); } struct filterSearch { @@ -1226,7 +1265,7 @@ HandlerResponse WSRequestHandler::HandleReorderSourceFilter(WSRequestHandler* re int lastFilterIndex = ctx.i + 1; if (newIndex > lastFilterIndex) { - return req->SendErrorResponse("index out of bounds"); + return request.failed("index out of bounds"); } int currentIndex = ctx.filterIndex; @@ -1243,7 +1282,7 @@ HandlerResponse WSRequestHandler::HandleReorderSourceFilter(WSRequestHandler* re } } - return req->SendOKResponse(); + return request.success(); } /** @@ -1258,24 +1297,24 @@ HandlerResponse WSRequestHandler::HandleReorderSourceFilter(WSRequestHandler* re * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleMoveSourceFilter(WSRequestHandler* req) +RpcResponse WSRequestHandler::MoveSourceFilter(const RpcRequest& request) { - if (!req->hasField("sourceName") || !req->hasField("filterName") || !req->hasField("movementType")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName") || !request.hasField("filterName") || !request.hasField("movementType")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); - const char* filterName = obs_data_get_string(req->data, "filterName"); - QString movementType(obs_data_get_string(req->data, "movementType")); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); + QString movementType(obs_data_get_string(request.parameters(), "movementType")); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName); if (!filter) { - return req->SendErrorResponse("specified filter doesn't exist"); + return request.failed("specified filter doesn't exist"); } obs_order_movement movement; @@ -1292,12 +1331,12 @@ HandlerResponse WSRequestHandler::HandleMoveSourceFilter(WSRequestHandler* req) movement = OBS_ORDER_MOVE_BOTTOM; } else { - return req->SendErrorResponse("invalid value for movementType: must be either 'up', 'down', 'top' or 'bottom'."); + return request.failed("invalid value for movementType: must be either 'up', 'down', 'top' or 'bottom'."); } obs_source_filter_set_order(source, filter, movement); - return req->SendOKResponse(); + return request.success(); } /** @@ -1312,31 +1351,67 @@ HandlerResponse WSRequestHandler::HandleMoveSourceFilter(WSRequestHandler* req) * @category sources * @since 4.5.0 */ -HandlerResponse WSRequestHandler::HandleSetSourceFilterSettings(WSRequestHandler* req) +RpcResponse WSRequestHandler::SetSourceFilterSettings(const RpcRequest& request) { - if (!req->hasField("sourceName") || !req->hasField("filterName") || !req->hasField("filterSettings")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("sourceName") || !request.hasField("filterName") || !request.hasField("filterSettings")) { + return request.failed("missing request parameters"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); - const char* filterName = obs_data_get_string(req->data, "filterName"); - OBSDataAutoRelease newFilterSettings = obs_data_get_obj(req->data, "filterSettings"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); + OBSDataAutoRelease newFilterSettings = obs_data_get_obj(request.parameters(), "filterSettings"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist"); + return request.failed("specified source doesn't exist"); } OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName); if (!filter) { - return req->SendErrorResponse("specified filter doesn't exist"); + return request.failed("specified filter doesn't exist"); } OBSDataAutoRelease settings = obs_source_get_settings(filter); obs_data_apply(settings, newFilterSettings); obs_source_update(filter, settings); - return req->SendOKResponse(); + return request.success(); +} + +/** +* Change the visibility/enabled state of a filter +* +* @param {String} `sourceName` Source name +* @param {String} `filterName` Source filter name +* @param {Boolean} `filterEnabled` New filter state +* +* @api requests +* @name SetSourceFilterVisibility +* @category sources +* @since 4.7.0 +*/ +RpcResponse WSRequestHandler::SetSourceFilterVisibility(const RpcRequest& request) +{ + if (!request.hasField("sourceName") || !request.hasField("filterName") || !request.hasField("filterEnabled")) { + return request.failed("missing request parameters"); + } + + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); + OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); + if (!source) { + return request.failed("specified source doesn't exist"); + } + + const char* filterName = obs_data_get_string(request.parameters(), "filterName"); + OBSSourceAutoRelease filter = obs_source_get_filter_by_name(source, filterName); + if (!filter) { + return request.failed("specified filter doesn't exist on specified source"); + } + + bool filterEnabled = obs_data_get_bool(request.parameters(), "filterEnabled"); + obs_source_set_enabled(filter, filterEnabled); + + return request.success(); } /** @@ -1349,7 +1424,7 @@ HandlerResponse WSRequestHandler::HandleSetSourceFilterSettings(WSRequestHandler * Clients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is * preserved if only one of these two parameters is specified. * -* @param {String} `sourceName` Source name +* @param {String} `sourceName` Source name. Note that, since scenes are also sources, you can also provide a scene name. * @param {String (optional)} `embedPictureFormat` Format of the Data URI encoded picture. Can be "png", "jpg", "jpeg" or "bmp" (or any other value supported by Qt's Image module) * @param {String (optional)} `saveToFilePath` Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path. * @param {int (optional)} `width` Screenshot width. Defaults to the source's base width. @@ -1364,19 +1439,19 @@ HandlerResponse WSRequestHandler::HandleSetSourceFilterSettings(WSRequestHandler * @category sources * @since 4.6.0 */ -HandlerResponse WSRequestHandler::HandleTakeSourceScreenshot(WSRequestHandler* req) { - if (!req->hasField("sourceName")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::TakeSourceScreenshot(const RpcRequest& request) { + if (!request.hasField("sourceName")) { + return request.failed("missing request parameters"); } - if (!req->hasField("embedPictureFormat") && !req->hasField("saveToFilePath")) { - return req->SendErrorResponse("At least 'embedPictureFormat' or 'saveToFilePath' must be specified"); + if (!request.hasField("embedPictureFormat") && !request.hasField("saveToFilePath")) { + return request.failed("At least 'embedPictureFormat' or 'saveToFilePath' must be specified"); } - const char* sourceName = obs_data_get_string(req->data, "sourceName"); + const char* sourceName = obs_data_get_string(request.parameters(), "sourceName"); OBSSourceAutoRelease source = obs_get_source_by_name(sourceName); if (!source) { - return req->SendErrorResponse("specified source doesn't exist");; + return request.failed("specified source doesn't exist");; } const uint32_t sourceWidth = obs_source_get_base_width(source); @@ -1386,18 +1461,18 @@ HandlerResponse WSRequestHandler::HandleTakeSourceScreenshot(WSRequestHandler* r uint32_t imgWidth = sourceWidth; uint32_t imgHeight = sourceHeight; - if (req->hasField("width")) { - imgWidth = obs_data_get_int(req->data, "width"); + if (request.hasField("width")) { + imgWidth = obs_data_get_int(request.parameters(), "width"); - if (!req->hasField("height")) { + if (!request.hasField("height")) { imgHeight = ((double)imgWidth / sourceAspectRatio); } } - if (req->hasField("height")) { - imgHeight = obs_data_get_int(req->data, "height"); + if (request.hasField("height")) { + imgHeight = obs_data_get_int(request.parameters(), "height"); - if (!req->hasField("width")) { + if (!request.hasField("width")) { imgWidth = ((double)imgHeight * sourceAspectRatio); } } @@ -1449,25 +1524,25 @@ HandlerResponse WSRequestHandler::HandleTakeSourceScreenshot(WSRequestHandler* r obs_leave_graphics(); if (!renderSuccess) { - return req->SendErrorResponse("Source render failed"); + return request.failed("Source render failed"); } OBSDataAutoRelease response = obs_data_create(); - if (req->hasField("embedPictureFormat")) { - const char* pictureFormat = obs_data_get_string(req->data, "embedPictureFormat"); + if (request.hasField("embedPictureFormat")) { + const char* pictureFormat = obs_data_get_string(request.parameters(), "embedPictureFormat"); QByteArrayList supportedFormats = QImageWriter::supportedImageFormats(); if (!supportedFormats.contains(pictureFormat)) { QString errorMessage = QString("Unsupported picture format: %1").arg(pictureFormat); - return req->SendErrorResponse(errorMessage.toUtf8()); + return request.failed(errorMessage.toUtf8()); } QByteArray encodedImgBytes; QBuffer buffer(&encodedImgBytes); buffer.open(QBuffer::WriteOnly); if (!sourceImage.save(&buffer, pictureFormat)) { - return req->SendErrorResponse("Embed image encoding failed"); + return request.failed("Embed image encoding failed"); } buffer.close(); @@ -1479,17 +1554,17 @@ HandlerResponse WSRequestHandler::HandleTakeSourceScreenshot(WSRequestHandler* r obs_data_set_string(response, "img", imgBase64.toUtf8()); } - if (req->hasField("saveToFilePath")) { - QString filePathStr = obs_data_get_string(req->data, "saveToFilePath"); + if (request.hasField("saveToFilePath")) { + QString filePathStr = obs_data_get_string(request.parameters(), "saveToFilePath"); QFileInfo filePathInfo(filePathStr); QString absoluteFilePath = filePathInfo.absoluteFilePath(); if (!sourceImage.save(absoluteFilePath)) { - return req->SendErrorResponse("Image save failed"); + return request.failed("Image save failed"); } obs_data_set_string(response, "imageFile", absoluteFilePath.toUtf8()); } obs_data_set_string(response, "sourceName", obs_source_get_name(source)); - return req->SendOKResponse(response); + return request.success(response); } diff --git a/src/WSRequestHandler_Streaming.cpp b/src/WSRequestHandler_Streaming.cpp index bbbf4bb1..744a691f 100644 --- a/src/WSRequestHandler_Streaming.cpp +++ b/src/WSRequestHandler_Streaming.cpp @@ -20,28 +20,26 @@ * @category streaming * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleGetStreamingStatus(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetStreamingStatus(const RpcRequest& request) { auto events = GetEventsSystem(); 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, "recording-paused", obs_frontend_recording_paused()); obs_data_set_bool(data, "preview-only", false); - const char* tc = nullptr; if (obs_frontend_streaming_active()) { - tc = events->GetStreamingTimecode(); - obs_data_set_string(data, "stream-timecode", tc); - bfree((void*)tc); + QString streamingTimecode = events->getStreamingTimecode(); + obs_data_set_string(data, "stream-timecode", streamingTimecode.toUtf8().constData()); } if (obs_frontend_recording_active()) { - tc = events->GetRecordingTimecode(); - obs_data_set_string(data, "rec-timecode", tc); - bfree((void*)tc); + QString recordingTimecode = events->getRecordingTimecode(); + obs_data_set_string(data, "rec-timecode", recordingTimecode.toUtf8().constData()); } - return req->SendOKResponse(data); + return request.success(data); } /** @@ -52,11 +50,11 @@ HandlerResponse WSRequestHandler::HandleGetStreamingStatus(WSRequestHandler* req * @category streaming * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleStartStopStreaming(WSRequestHandler* req) { +RpcResponse WSRequestHandler::StartStopStreaming(const RpcRequest& request) { if (obs_frontend_streaming_active()) - return HandleStopStreaming(req); + return StopStreaming(request); else - return HandleStartStreaming(req); + return StartStreaming(request); } /** @@ -69,24 +67,24 @@ HandlerResponse WSRequestHandler::HandleStartStopStreaming(WSRequestHandler* req * @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`. + * @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 */ -HandlerResponse WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) { +RpcResponse WSRequestHandler::StartStreaming(const RpcRequest& request) { 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"); + if (request.hasField("stream")) { + OBSDataAutoRelease streamData = obs_data_get_obj(request.parameters(), "stream"); OBSDataAutoRelease newSettings = obs_data_get_obj(streamData, "settings"); OBSDataAutoRelease newMetadata = obs_data_get_obj(streamData, "metadata"); @@ -159,9 +157,9 @@ HandlerResponse WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) { obs_frontend_set_streaming_service(configuredService); } - return req->SendOKResponse(); + return request.success(); } else { - return req->SendErrorResponse("streaming already active"); + return request.failed("streaming already active"); } } @@ -174,12 +172,12 @@ HandlerResponse WSRequestHandler::HandleStartStreaming(WSRequestHandler* req) { * @category streaming * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleStopStreaming(WSRequestHandler* req) { +RpcResponse WSRequestHandler::StopStreaming(const RpcRequest& request) { if (obs_frontend_streaming_active() == true) { obs_frontend_streaming_stop(); - return req->SendOKResponse(); + return request.success(); } else { - return req->SendErrorResponse("streaming not active"); + return request.failed("streaming not active"); } } @@ -190,7 +188,7 @@ HandlerResponse WSRequestHandler::HandleStopStreaming(WSRequestHandler* req) { * @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 {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. @@ -200,21 +198,22 @@ HandlerResponse WSRequestHandler::HandleStopStreaming(WSRequestHandler* req) { * @category streaming * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) { +RpcResponse WSRequestHandler::SetStreamSettings(const RpcRequest& request) { OBSService service = obs_frontend_get_streaming_service(); - OBSDataAutoRelease requestSettings = obs_data_get_obj(req->data, "settings"); + OBSDataAutoRelease requestSettings = obs_data_get_obj(request.parameters(), "settings"); if (!requestSettings) { - return req->SendErrorResponse("'settings' are required'"); + return request.failed("'settings' are required'"); } QString serviceType = obs_service_get_type(service); - QString requestedType = obs_data_get_string(req->data, "type"); + QString requestedType = obs_data_get_string(request.parameters(), "type"); if (requestedType != nullptr && requestedType != serviceType) { OBSDataAutoRelease hotkeys = obs_hotkeys_save_service(service); service = obs_service_create( requestedType.toUtf8(), STREAM_SERVICE_ID, requestSettings, hotkeys); + obs_frontend_set_streaming_service(service); } 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 @@ -233,17 +232,19 @@ HandlerResponse WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) } //if save is specified we should immediately save the streaming service - if (obs_data_get_bool(req->data, "save")) { + if (obs_data_get_bool(request.parameters(), "save")) { obs_frontend_save_streaming_service(); } - OBSDataAutoRelease serviceSettings = obs_service_get_settings(service); + OBSService responseService = obs_frontend_get_streaming_service(); + OBSDataAutoRelease serviceSettings = obs_service_get_settings(responseService); + const char* responseType = obs_service_get_type(responseService); OBSDataAutoRelease response = obs_data_create(); - obs_data_set_string(response, "type", requestedType.toUtf8()); + obs_data_set_string(response, "type", responseType); obs_data_set_obj(response, "settings", serviceSettings); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -253,16 +254,16 @@ HandlerResponse WSRequestHandler::HandleSetStreamSettings(WSRequestHandler* req) * @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 authentication 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`. + * @return {boolean} `settings.use_auth` Indicates whether authentication 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 */ -HandlerResponse WSRequestHandler::HandleGetStreamSettings(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetStreamSettings(const RpcRequest& request) { OBSService service = obs_frontend_get_streaming_service(); const char* serviceType = obs_service_get_type(service); @@ -272,7 +273,7 @@ HandlerResponse WSRequestHandler::HandleGetStreamSettings(WSRequestHandler* req) obs_data_set_string(response, "type", serviceType); obs_data_set_obj(response, "settings", settings); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -283,9 +284,9 @@ HandlerResponse WSRequestHandler::HandleGetStreamSettings(WSRequestHandler* req) * @category streaming * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleSaveStreamSettings(WSRequestHandler* req) { +RpcResponse WSRequestHandler::SaveStreamSettings(const RpcRequest& request) { obs_frontend_save_streaming_service(); - return req->SendOKResponse(); + return request.success(); } @@ -301,18 +302,19 @@ HandlerResponse WSRequestHandler::HandleSaveStreamSettings(WSRequestHandler* req * @since 4.6.0 */ #if BUILD_CAPTIONS -HandlerResponse WSRequestHandler::HandleSendCaptions(WSRequestHandler* req) { - if (!req->hasField("text")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SendCaptions(const RpcRequest& request) { + if (!request.hasField("text")) { + return request.failed("missing request parameters"); } OBSOutputAutoRelease output = obs_frontend_get_streaming_output(); if (output) { - const char* caption = obs_data_get_string(req->data, "text"); - obs_output_output_caption_text1(output, caption); + const char* caption = obs_data_get_string(request.parameters(), "text"); + // Send caption text with immediately (0 second delay) + obs_output_output_caption_text2(output, caption, 0.0); } - return req->SendOKResponse(); + return request.success(); } #endif diff --git a/src/WSRequestHandler_StudioMode.cpp b/src/WSRequestHandler_StudioMode.cpp index de64fc97..fb958f3b 100644 --- a/src/WSRequestHandler_StudioMode.cpp +++ b/src/WSRequestHandler_StudioMode.cpp @@ -12,13 +12,13 @@ * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetStudioModeStatus(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetStudioModeStatus(const RpcRequest& request) { bool previewActive = obs_frontend_preview_program_mode_active(); OBSDataAutoRelease response = obs_data_create(); obs_data_set_bool(response, "studio-mode", previewActive); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -33,9 +33,9 @@ HandlerResponse WSRequestHandler::HandleGetStudioModeStatus(WSRequestHandler* re * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetPreviewScene(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetPreviewScene(const RpcRequest& request) { if (!obs_frontend_preview_program_mode_active()) { - return req->SendErrorResponse("studio mode not enabled"); + return request.failed("studio mode not enabled"); } OBSSourceAutoRelease scene = obs_frontend_get_current_preview_scene(); @@ -45,7 +45,7 @@ HandlerResponse WSRequestHandler::HandleGetPreviewScene(WSRequestHandler* req) { obs_data_set_string(data, "name", obs_source_get_name(scene)); obs_data_set_array(data, "sources", sceneItems); - return req->SendOKResponse(data); + return request.success(data); } /** @@ -59,23 +59,23 @@ HandlerResponse WSRequestHandler::HandleGetPreviewScene(WSRequestHandler* req) { * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleSetPreviewScene(WSRequestHandler* req) { +RpcResponse WSRequestHandler::SetPreviewScene(const RpcRequest& request) { if (!obs_frontend_preview_program_mode_active()) { - return req->SendErrorResponse("studio mode not enabled"); + return request.failed("studio mode not enabled"); } - if (!req->hasField("scene-name")) { - return req->SendErrorResponse("missing request parameters"); + if (!request.hasField("scene-name")) { + return request.failed("missing request parameters"); } - const char* scene_name = obs_data_get_string(req->data, "scene-name"); - OBSSourceAutoRelease scene = Utils::GetSceneFromNameOrCurrent(scene_name); + const char* scene_name = obs_data_get_string(request.parameters(), "scene-name"); + OBSScene scene = Utils::GetSceneFromNameOrCurrent(scene_name); if (!scene) { - return req->SendErrorResponse("specified scene doesn't exist"); + return request.failed("specified scene doesn't exist"); } - obs_frontend_set_current_preview_scene(scene); - return req->SendOKResponse(); + obs_frontend_set_current_preview_scene(obs_scene_get_source(scene)); + return request.success(); } /** @@ -91,37 +91,37 @@ HandlerResponse WSRequestHandler::HandleSetPreviewScene(WSRequestHandler* req) { * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleTransitionToProgram(WSRequestHandler* req) { +RpcResponse WSRequestHandler::TransitionToProgram(const RpcRequest& request) { if (!obs_frontend_preview_program_mode_active()) { - return req->SendErrorResponse("studio mode not enabled"); + return request.failed("studio mode not enabled"); } - if (req->hasField("with-transition")) { + if (request.hasField("with-transition")) { OBSDataAutoRelease transitionInfo = - obs_data_get_obj(req->data, "with-transition"); + obs_data_get_obj(request.parameters(), "with-transition"); if (obs_data_has_user_value(transitionInfo, "name")) { QString transitionName = obs_data_get_string(transitionInfo, "name"); if (transitionName.isEmpty()) { - return req->SendErrorResponse("invalid request parameters"); + return request.failed("invalid request parameters"); } bool success = Utils::SetTransitionByName(transitionName); if (!success) { - return req->SendErrorResponse("specified transition doesn't exist"); + return request.failed("specified transition doesn't exist"); } } if (obs_data_has_user_value(transitionInfo, "duration")) { int transitionDuration = obs_data_get_int(transitionInfo, "duration"); - Utils::SetTransitionDuration(transitionDuration); + obs_frontend_set_transition_duration(transitionDuration); } } - Utils::TransitionToProgram(); - return req->SendOKResponse(); + obs_frontend_preview_program_trigger_transition(); + return request.success(); } /** @@ -132,9 +132,13 @@ HandlerResponse WSRequestHandler::HandleTransitionToProgram(WSRequestHandler* re * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleEnableStudioMode(WSRequestHandler* req) { - obs_frontend_set_preview_program_mode(true); - return req->SendOKResponse(); +RpcResponse WSRequestHandler::EnableStudioMode(const RpcRequest& request) { + obs_queue_task(OBS_TASK_UI, [](void* param) { + obs_frontend_set_preview_program_mode(true); + + UNUSED_PARAMETER(param); + }, nullptr, true); + return request.success(); } /** @@ -145,9 +149,14 @@ HandlerResponse WSRequestHandler::HandleEnableStudioMode(WSRequestHandler* req) * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleDisableStudioMode(WSRequestHandler* req) { - obs_frontend_set_preview_program_mode(false); - return req->SendOKResponse(); +RpcResponse WSRequestHandler::DisableStudioMode(const RpcRequest& request) { + obs_queue_task(OBS_TASK_UI, [](void* param) { + obs_frontend_set_preview_program_mode(false); + + UNUSED_PARAMETER(param); + }, nullptr, true); + + return request.success(); } /** @@ -158,8 +167,13 @@ HandlerResponse WSRequestHandler::HandleDisableStudioMode(WSRequestHandler* req) * @category studio mode * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleToggleStudioMode(WSRequestHandler* req) { - bool previewProgramMode = obs_frontend_preview_program_mode_active(); - obs_frontend_set_preview_program_mode(!previewProgramMode); - return req->SendOKResponse(); +RpcResponse WSRequestHandler::ToggleStudioMode(const RpcRequest& request) { + obs_queue_task(OBS_TASK_UI, [](void* param) { + bool previewProgramMode = obs_frontend_preview_program_mode_active(); + obs_frontend_set_preview_program_mode(!previewProgramMode); + + UNUSED_PARAMETER(param); + }, nullptr, true); + + return request.success(); } diff --git a/src/WSRequestHandler_Transitions.cpp b/src/WSRequestHandler_Transitions.cpp index b3ff9ac9..7c22687c 100644 --- a/src/WSRequestHandler_Transitions.cpp +++ b/src/WSRequestHandler_Transitions.cpp @@ -14,7 +14,7 @@ * @category transitions * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetTransitionList(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetTransitionList(const RpcRequest& request) { OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition(); obs_frontend_source_list transitionList = {}; obs_frontend_get_transitions(&transitionList); @@ -34,7 +34,7 @@ HandlerResponse WSRequestHandler::HandleGetTransitionList(WSRequestHandler* req) obs_source_get_name(currentTransition)); obs_data_set_array(response, "transitions", transitions); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -48,7 +48,7 @@ HandlerResponse WSRequestHandler::HandleGetTransitionList(WSRequestHandler* req) * @category transitions * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleGetCurrentTransition(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetCurrentTransition(const RpcRequest& request) { OBSSourceAutoRelease currentTransition = obs_frontend_get_current_transition(); OBSDataAutoRelease response = obs_data_create(); @@ -56,9 +56,9 @@ HandlerResponse WSRequestHandler::HandleGetCurrentTransition(WSRequestHandler* r obs_source_get_name(currentTransition)); if (!obs_transition_fixed(currentTransition)) - obs_data_set_int(response, "duration", Utils::GetTransitionDuration()); + obs_data_set_int(response, "duration", obs_frontend_get_transition_duration()); - return req->SendOKResponse(response); + return request.success(response); } /** @@ -71,18 +71,18 @@ HandlerResponse WSRequestHandler::HandleGetCurrentTransition(WSRequestHandler* r * @category transitions * @since 0.3 */ -HandlerResponse WSRequestHandler::HandleSetCurrentTransition(WSRequestHandler* req) { - if (!req->hasField("transition-name")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetCurrentTransition(const RpcRequest& request) { + if (!request.hasField("transition-name")) { + return request.failed("missing request parameters"); } - QString name = obs_data_get_string(req->data, "transition-name"); + QString name = obs_data_get_string(request.parameters(), "transition-name"); bool success = Utils::SetTransitionByName(name); if (!success) { - return req->SendErrorResponse("requested transition does not exist"); + return request.failed("requested transition does not exist"); } - return req->SendOKResponse(); + return request.success(); } /** @@ -95,14 +95,14 @@ HandlerResponse WSRequestHandler::HandleSetCurrentTransition(WSRequestHandler* r * @category transitions * @since 4.0.0 */ -HandlerResponse WSRequestHandler::HandleSetTransitionDuration(WSRequestHandler* req) { - if (!req->hasField("duration")) { - return req->SendErrorResponse("missing request parameters"); +RpcResponse WSRequestHandler::SetTransitionDuration(const RpcRequest& request) { + if (!request.hasField("duration")) { + return request.failed("missing request parameters"); } - int ms = obs_data_get_int(req->data, "duration"); - Utils::SetTransitionDuration(ms); - return req->SendOKResponse(); + int ms = obs_data_get_int(request.parameters(), "duration"); + obs_frontend_set_transition_duration(ms); + return request.success(); } /** @@ -115,8 +115,8 @@ HandlerResponse WSRequestHandler::HandleSetTransitionDuration(WSRequestHandler* * @category transitions * @since 4.1.0 */ -HandlerResponse WSRequestHandler::HandleGetTransitionDuration(WSRequestHandler* req) { +RpcResponse WSRequestHandler::GetTransitionDuration(const RpcRequest& request) { OBSDataAutoRelease response = obs_data_create(); - obs_data_set_int(response, "transition-duration", Utils::GetTransitionDuration()); - return req->SendOKResponse(response); + obs_data_set_int(response, "transition-duration", obs_frontend_get_transition_duration()); + return request.success(response); } diff --git a/src/WSServer.cpp b/src/WSServer.cpp index 5249fc87..c9f09362 100644 --- a/src/WSServer.cpp +++ b/src/WSServer.cpp @@ -31,6 +31,7 @@ with this program. If not, see #include "obs-websocket.h" #include "Config.h" #include "Utils.h" +#include "protocol/OBSRemoteProtocol.h" QT_USE_NAMESPACE @@ -124,8 +125,15 @@ void WSServer::stop() blog(LOG_INFO, "server stopped successfully"); } -void WSServer::broadcast(std::string message) +void WSServer::broadcast(const RpcEvent& event) { + OBSRemoteProtocol protocol; + std::string message = protocol.encodeEvent(event); + + if (GetConfig()->DebugEnabled) { + blog(LOG_INFO, "Update << '%s'", message.c_str()); + } + QMutexLocker locker(&_clMutex); for (connection_hdl hdl : _connections) { if (GetConfig()->AuthRequired) { @@ -134,7 +142,15 @@ void WSServer::broadcast(std::string message) continue; } } - _server.send(hdl, message, websocketpp::frame::opcode::text); + + websocketpp::lib::error_code errorCode; + _server.send(hdl, message, websocketpp::frame::opcode::text, errorCode); + + if (errorCode) { + std::string errorCodeMessage = errorCode.message(); + blog(LOG_INFO, "server(broadcast): send failed: %s", + errorCodeMessage.c_str()); + } } } @@ -163,10 +179,26 @@ void WSServer::onMessage(connection_hdl hdl, server::message_ptr message) ConnectionProperties& connProperties = _connectionProperties[hdl]; locker.unlock(); - WSRequestHandler handler(connProperties); - std::string response = handler.processIncomingMessage(payload); + if (GetConfig()->DebugEnabled) { + blog(LOG_INFO, "Request >> '%s'", payload.c_str()); + } - _server.send(hdl, response, websocketpp::frame::opcode::text); + WSRequestHandler requestHandler(connProperties); + OBSRemoteProtocol protocol; + std::string response = protocol.processMessage(requestHandler, payload); + + if (GetConfig()->DebugEnabled) { + blog(LOG_INFO, "Response << '%s'", response.c_str()); + } + + websocketpp::lib::error_code errorCode; + _server.send(hdl, response, websocketpp::frame::opcode::text, errorCode); + + if (errorCode) { + std::string errorCodeMessage = errorCode.message(); + blog(LOG_INFO, "server(response): send failed: %s", + errorCodeMessage.c_str()); + } }); } diff --git a/src/WSServer.h b/src/WSServer.h index e72e5fb2..723f51af 100644 --- a/src/WSServer.h +++ b/src/WSServer.h @@ -30,11 +30,8 @@ with this program. If not, see #include #include "ConnectionProperties.h" - #include "WSRequestHandler.h" - -QT_FORWARD_DECLARE_CLASS(QWebSocketServer) -QT_FORWARD_DECLARE_CLASS(QWebSocket) +#include "rpc/RpcEvent.h" using websocketpp::connection_hdl; @@ -49,7 +46,7 @@ public: virtual ~WSServer(); void start(quint16 port); void stop(); - void broadcast(std::string message); + void broadcast(const RpcEvent& event); QThreadPool* threadPool() { return &_threadPool; } diff --git a/src/obs-websocket.cpp b/src/obs-websocket.cpp index b780f070..9f19ab9e 100644 --- a/src/obs-websocket.cpp +++ b/src/obs-websocket.cpp @@ -18,6 +18,7 @@ with this program. If not, see #include #include +#include #include #include @@ -35,6 +36,11 @@ void ___data_dummy_addref(obs_data_t*) {} void ___data_array_dummy_addref(obs_data_array_t*) {} void ___output_dummy_addref(obs_output_t*) {} +void ___data_item_dummy_addref(obs_data_item_t*) {} +void ___data_item_release(obs_data_item_t* dataItem) { + obs_data_item_release(&dataItem); +} + OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("obs-websocket", "en-US") @@ -55,10 +61,6 @@ bool obs_module_load(void) { _server = WSServerPtr(new WSServer()); _eventsSystem = WSEventsPtr(new WSEvents(_server)); - if (_config->ServerEnabled) { - _server->start(_config->ServerPort); - } - // UI setup obs_frontend_push_ui_translation(obs_module_get_string); QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window(); @@ -75,6 +77,17 @@ bool obs_module_load(void) { settingsDialog->ToggleShowHide(); }); + // Setup event handler to start the server once OBS is ready + auto eventCallback = [](enum obs_frontend_event event, void *param) { + if (event == OBS_FRONTEND_EVENT_FINISHED_LOADING) { + if (_config->ServerEnabled) { + _server->start(_config->ServerPort); + } + obs_frontend_remove_event_callback((obs_frontend_event_cb)param, nullptr); + } + }; + obs_frontend_add_event_callback(eventCallback, (void*)(obs_frontend_event_cb)eventCallback); + // Loading finished blog(LOG_INFO, "module loaded!"); diff --git a/src/obs-websocket.h b/src/obs-websocket.h index 011ffcee..5f118d28 100644 --- a/src/obs-websocket.h +++ b/src/obs-websocket.h @@ -38,6 +38,11 @@ using OBSDataArrayAutoRelease = using OBSOutputAutoRelease = OBSRef; +void ___data_item_dummy_addref(obs_data_item_t*); +void ___data_item_release(obs_data_item_t*); +using OBSDataItemAutoRelease = + OBSRef; + class Config; typedef std::shared_ptr ConfigPtr; @@ -51,6 +56,6 @@ ConfigPtr GetConfig(); WSServerPtr GetServer(); WSEventsPtr GetEventsSystem(); -#define OBS_WEBSOCKET_VERSION "4.7.0" +#define OBS_WEBSOCKET_VERSION "4.8.0" #define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__) diff --git a/src/protocol/OBSRemoteProtocol.cpp b/src/protocol/OBSRemoteProtocol.cpp new file mode 100644 index 00000000..e5b1da1a --- /dev/null +++ b/src/protocol/OBSRemoteProtocol.cpp @@ -0,0 +1,117 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +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 +*/ + +#include + +#include "OBSRemoteProtocol.h" +#include "../WSRequestHandler.h" +#include "../rpc/RpcEvent.h" +#include "../Utils.h" + +std::string OBSRemoteProtocol::processMessage(WSRequestHandler& requestHandler, std::string message) +{ + std::string msgContainer(message); + const char* msg = msgContainer.c_str(); + + OBSDataAutoRelease data = obs_data_create_from_json(msg); + if (!data) { + blog(LOG_ERROR, "invalid JSON payload received for '%s'", msg); + return errorResponse(QString::Null(), "invalid JSON payload"); + } + + if (!obs_data_has_user_value(data, "request-type") || !obs_data_has_user_value(data, "message-id")) { + return errorResponse(QString::Null(), "missing request parameters"); + } + + QString methodName = obs_data_get_string(data, "request-type"); + QString messageId = obs_data_get_string(data, "message-id"); + + OBSDataAutoRelease params = obs_data_create(); + obs_data_apply(params, data); + obs_data_unset_user_value(params, "request-type"); + obs_data_unset_user_value(params, "message-id"); + + RpcRequest request(messageId, methodName, params); + RpcResponse response = requestHandler.processRequest(request); + + OBSData additionalFields = response.additionalFields(); + switch (response.status()) { + case RpcResponse::Status::Ok: + return successResponse(messageId, additionalFields); + case RpcResponse::Status::Error: + return errorResponse(messageId, response.errorMessage(), additionalFields); + } + + return std::string(); +} + +std::string OBSRemoteProtocol::encodeEvent(const RpcEvent& event) +{ + OBSDataAutoRelease eventData = obs_data_create(); + + QString updateType = event.updateType(); + obs_data_set_string(eventData, "update-type", updateType.toUtf8().constData()); + + if (obs_frontend_streaming_active()) { + QString streamingTimecode = Utils::nsToTimestamp(event.streamTime()); + obs_data_set_string(eventData, "stream-timecode", streamingTimecode.toUtf8().constData()); + } + + if (obs_frontend_recording_active()) { + QString recordingTimecode = Utils::nsToTimestamp(event.recordingTime()); + obs_data_set_string(eventData, "rec-timecode", recordingTimecode.toUtf8().constData()); + } + + OBSData additionalFields = event.additionalFields(); + if (additionalFields) { + obs_data_apply(eventData, additionalFields); + } + + return std::string(obs_data_get_json(eventData)); +} + +std::string OBSRemoteProtocol::buildResponse(QString messageId, QString status, obs_data_t* fields) +{ + OBSDataAutoRelease response = obs_data_create(); + if (!messageId.isNull()) { + obs_data_set_string(response, "message-id", messageId.toUtf8().constData()); + } + obs_data_set_string(response, "status", status.toUtf8().constData()); + + if (fields) { + obs_data_apply(response, fields); + } + + std::string responseString = obs_data_get_json(response); + return responseString; +} + +std::string OBSRemoteProtocol::successResponse(QString messageId, obs_data_t* fields) +{ + return buildResponse(messageId, "ok", fields); +} + +std::string OBSRemoteProtocol::errorResponse(QString messageId, QString errorMessage, obs_data_t* additionalFields) +{ + OBSDataAutoRelease fields = obs_data_create(); + if (additionalFields) { + obs_data_apply(fields, additionalFields); + } + obs_data_set_string(fields, "error", errorMessage.toUtf8().constData()); + return buildResponse(messageId, "error", fields); +} diff --git a/src/protocol/OBSRemoteProtocol.h b/src/protocol/OBSRemoteProtocol.h new file mode 100644 index 00000000..03d8aa7d --- /dev/null +++ b/src/protocol/OBSRemoteProtocol.h @@ -0,0 +1,38 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +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 +*/ + +#pragma once + +#include +#include +#include + +class WSRequestHandler; +class RpcEvent; + +class OBSRemoteProtocol +{ +public: + std::string processMessage(WSRequestHandler& requestHandler, std::string message); + std::string encodeEvent(const RpcEvent& event); + +private: + std::string buildResponse(QString messageId, QString status, obs_data_t* fields = nullptr); + std::string successResponse(QString messageId, obs_data_t* fields = nullptr); + std::string errorResponse(QString messageId, QString errorMessage, obs_data_t* additionalFields = nullptr); +}; diff --git a/src/rpc/RpcEvent.cpp b/src/rpc/RpcEvent.cpp new file mode 100644 index 00000000..a8d3b06b --- /dev/null +++ b/src/rpc/RpcEvent.cpp @@ -0,0 +1,35 @@ +/* +obs-websocket +Copyright (C) 2016-2020 Stéphane Lepin + +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 +*/ + +#include "RpcEvent.h" + +RpcEvent::RpcEvent( + const QString& updateType, + uint64_t streamTime, uint64_t recordingTime, + obs_data_t* additionalFields +) : + _updateType(updateType), + _streamTime(streamTime), + _recordingTime(recordingTime), + _additionalFields(nullptr) +{ + if (additionalFields) { + _additionalFields = obs_data_create(); + obs_data_apply(_additionalFields, additionalFields); + } +} \ No newline at end of file diff --git a/src/rpc/RpcEvent.h b/src/rpc/RpcEvent.h new file mode 100644 index 00000000..6dd0df99 --- /dev/null +++ b/src/rpc/RpcEvent.h @@ -0,0 +1,60 @@ +/* +obs-websocket +Copyright (C) 2016-2020 Stéphane Lepin + +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 +*/ + +#pragma once + +#include +#include + +#include "../obs-websocket.h" + +class RpcEvent +{ +public: + explicit RpcEvent( + const QString& updateType, + uint64_t streamTime, uint64_t recordingTime, + obs_data_t* additionalFields = nullptr + ); + + const QString& updateType() const + { + return _updateType; + } + + const uint64_t streamTime() const + { + return _streamTime; + } + + const uint64_t recordingTime() const + { + return _recordingTime; + } + + const OBSData additionalFields() const + { + return OBSData(_additionalFields); + } + +private: + QString _updateType; + uint64_t _streamTime; + uint64_t _recordingTime; + OBSDataAutoRelease _additionalFields; +}; diff --git a/src/rpc/RpcRequest.cpp b/src/rpc/RpcRequest.cpp new file mode 100644 index 00000000..04d62cb9 --- /dev/null +++ b/src/rpc/RpcRequest.cpp @@ -0,0 +1,104 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +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 +*/ + +#include "RpcRequest.h" +#include "RpcResponse.h" + +RpcRequest::RpcRequest(const QString& messageId, const QString& methodName, obs_data_t* params) : + _messageId(messageId), + _methodName(methodName), + _parameters(nullptr) +{ + if (params) { + _parameters = obs_data_create(); + obs_data_apply(_parameters, params); + } +} + +const RpcResponse RpcRequest::success(obs_data_t* additionalFields) const +{ + return RpcResponse::ok(*this, additionalFields); +} + +const RpcResponse RpcRequest::failed(const QString& errorMessage, obs_data_t* additionalFields) const +{ + return RpcResponse::fail(*this, errorMessage, additionalFields); +} + +const bool RpcRequest::hasField(QString name, obs_data_type expectedFieldType, obs_data_number_type expectedNumberType) const +{ + if (!_parameters || name.isEmpty() || name.isNull()) { + return false; + } + + OBSDataItemAutoRelease dataItem = obs_data_item_byname(_parameters, name.toUtf8()); + if (!dataItem) { + return false; + } + + if (expectedFieldType != OBS_DATA_NULL) { + obs_data_type fieldType = obs_data_item_gettype(dataItem); + if (fieldType != expectedFieldType) { + return false; + } + + if (fieldType == OBS_DATA_NUMBER && expectedNumberType != OBS_DATA_NUM_INVALID) { + obs_data_number_type numberType = obs_data_item_numtype(dataItem); + if (numberType != expectedNumberType) { + return false; + } + } + } + + return true; +} + +const bool RpcRequest::hasBool(QString fieldName) const +{ + return this->hasField(fieldName, OBS_DATA_BOOLEAN); +} + +const bool RpcRequest::hasString(QString fieldName) const +{ + return this->hasField(fieldName, OBS_DATA_STRING); +} + +const bool RpcRequest::hasNumber(QString fieldName, obs_data_number_type expectedNumberType) const +{ + return this->hasField(fieldName, OBS_DATA_NUMBER, expectedNumberType); +} + +const bool RpcRequest::hasInteger(QString fieldName) const +{ + return this->hasNumber(fieldName, OBS_DATA_NUM_INT); +} + +const bool RpcRequest::hasDouble(QString fieldName) const +{ + return this->hasNumber(fieldName, OBS_DATA_NUM_DOUBLE); +} + +const bool RpcRequest::hasArray(QString fieldName) const +{ + return this->hasField(fieldName, OBS_DATA_ARRAY); +} + +const bool RpcRequest::hasObject(QString fieldName) const +{ + return this->hasField(fieldName, OBS_DATA_OBJECT); +} diff --git a/src/rpc/RpcRequest.h b/src/rpc/RpcRequest.h new file mode 100644 index 00000000..3e360150 --- /dev/null +++ b/src/rpc/RpcRequest.h @@ -0,0 +1,65 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +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 +*/ + +#pragma once + +#include +#include +#include "../obs-websocket.h" + +// forward declarations +class RpcResponse; + +class RpcRequest +{ +public: + explicit RpcRequest(const QString& messageId, const QString& methodName, obs_data_t* params); + + const QString& messageId() const + { + return _messageId; + } + + const QString& methodName() const + { + return _methodName; + } + + const OBSData parameters() const + { + return OBSData(_parameters); + } + + const RpcResponse success(obs_data_t* additionalFields = nullptr) const; + const RpcResponse failed(const QString& errorMessage, obs_data_t* additionalFields = nullptr) const; + + const bool hasField(QString fieldName, obs_data_type expectedFieldType = OBS_DATA_NULL, + obs_data_number_type expectedNumberType = OBS_DATA_NUM_INVALID) const; + const bool hasBool(QString fieldName) const; + const bool hasString(QString fieldName) const; + const bool hasNumber(QString fieldName, obs_data_number_type expectedNumberType = OBS_DATA_NUM_INVALID) const; + const bool hasInteger(QString fieldName) const; + const bool hasDouble(QString fieldName) const; + const bool hasArray(QString fieldName) const; + const bool hasObject(QString fieldName) const; + +private: + const QString _messageId; + const QString _methodName; + OBSDataAutoRelease _parameters; +}; diff --git a/src/rpc/RpcResponse.cpp b/src/rpc/RpcResponse.cpp new file mode 100644 index 00000000..17f9f6e9 --- /dev/null +++ b/src/rpc/RpcResponse.cpp @@ -0,0 +1,48 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +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 +*/ + +#include "RpcResponse.h" +#include "RpcRequest.h" + +RpcResponse::RpcResponse( + Status status, const QString& messageId, + const QString& methodName, obs_data_t* additionalFields +) : + _status(status), + _messageId(messageId), + _methodName(methodName), + _additionalFields(nullptr) +{ + if (additionalFields) { + _additionalFields = obs_data_create(); + obs_data_apply(_additionalFields, additionalFields); + } +} + +const RpcResponse RpcResponse::ok(const RpcRequest& request, obs_data_t* additionalFields) +{ + RpcResponse response(Status::Ok, request.messageId(), request.methodName(), additionalFields); + return response; +} + +const RpcResponse RpcResponse::fail(const RpcRequest& request, const QString& errorMessage, obs_data_t* additionalFields) +{ + RpcResponse response(Status::Error, request.messageId(), request.methodName(), additionalFields); + response._errorMessage = errorMessage; + return response; +} diff --git a/src/rpc/RpcResponse.h b/src/rpc/RpcResponse.h new file mode 100644 index 00000000..a6381bfd --- /dev/null +++ b/src/rpc/RpcResponse.h @@ -0,0 +1,70 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +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 +*/ + +#pragma once + +#include +#include +#include "../obs-websocket.h" + +class RpcRequest; + +class RpcResponse +{ +public: + enum Status { Unknown, Ok, Error }; + + static RpcResponse ofRequest(const RpcRequest& request); + static const RpcResponse ok(const RpcRequest& request, obs_data_t* additionalFields = nullptr); + static const RpcResponse fail( + const RpcRequest& request, const QString& errorMessage, + obs_data_t* additionalFields = nullptr + ); + + Status status() { + return _status; + } + + const QString& messageId() const { + return _messageId; + } + + const QString& methodName() const { + return _methodName; + } + + const QString& errorMessage() const { + return _errorMessage; + } + + const OBSData additionalFields() const { + return OBSData(_additionalFields); + } + +private: + explicit RpcResponse( + Status status, + const QString& messageId, const QString& methodName, + obs_data_t* additionalFields = nullptr + ); + const Status _status; + const QString _messageId; + const QString _methodName; + QString _errorMessage; + OBSDataAutoRelease _additionalFields; +};