Merge branch '4.x-current' into bugfix/cmake-frontend-lib

This commit is contained in:
Stéphane Lepin 2020-03-28 19:17:19 +01:00
commit 824cfd4871
52 changed files with 4240 additions and 1535 deletions

5
.gitignore vendored
View File

@ -4,6 +4,7 @@
/build32/
/build64/
/release/
/package/
/installer/Output/
.vscode
.idea
.vscode

View File

@ -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

View File

@ -1,8 +1,6 @@
#!/bin/sh
set -ex
cd /root/obs-websocket
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make -j4

View File

@ -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\obs-studio-last-tag-built.txt
if exist "%OBSPath%\obs-studio-last-tag-built.txt" (
set /p OBSLastTagBuilt=<"%OBSPath%\obs-studio-last-tag-built.txt"
) else (
set OBSLastTagBuilt=0
)
REM If obs-studio directory exists, run git pull and get the latest tag number.
if exist C:\projects\obs-studio\ (
if exist %OBSPath% (
echo obs-studio directory exists
echo Updating tag info
cd C:\projects\obs-studio\
git describe --tags --abbrev=0 > C:\projects\latest-obs-studio-tag-pre-pull.txt
set /p OBSLatestTagPrePull=<C:\projects\latest-obs-studio-tag-pre-pull.txt
cd /D %OBSPath%
git describe --tags --abbrev=0 --exclude="*-rc*" > "%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-post-pull.txt
set /p OBSLatestTag=<C:\projects\latest-obs-studio-tag-post-pull.txt
echo %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=<C:\projects\obs-studio-latest-tag.txt
git clone https://github.com/obsproject/obs-studio %OBSPath%
cd /D %OBSPath%\
git describe --tags --abbrev=0 --exclude="*-rc*" > "%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=<C:\projects\obs-studio-last-tag-built.txt
REM echo Building obs-studio %OBSLatestTag% 32-bit ^(Build Config: %build_config%^)...
REM call msbuild /m /p:Configuration=%build_config% %OBSPath%\build32\obs-studio.sln
REM echo Building obs-studio %OBSLatestTag% 64-bit ^(Build Config: %build_config%^)...
REM call msbuild /m /p:Configuration=%build_config% %OBSPath%\build64\obs-studio.sln
cd ..
git describe --tags --abbrev=0 > "%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"

6
CI/download-obs-deps.cmd Normal file
View File

@ -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."
)

View File

@ -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

View File

@ -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

View File

@ -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

8
CI/install-qt-win.cmd Normal file
View File

@ -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%

View File

@ -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

View File

@ -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

23
CI/package-ubuntu.sh Executable file
View File

@ -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/*

12
CI/package-windows.cmd Normal file
View File

@ -0,0 +1,12 @@
mkdir package
cd package
git rev-parse --short HEAD > package-version.txt
set /p PackageVersion=<package-version.txt
del package-version.txt
REM Package ZIP archive
7z a "obs-websocket-%PackageVersion%-Windows.zip" "..\release\*"
REM Build installer
iscc ..\installer\installer.iss /O. /F"obs-websocket-%PackageVersion%-Windows"

View File

@ -1,24 +0,0 @@
#!/bin/sh
set -e
cd /root/obs-websocket
export GIT_HASH=$(git rev-parse --short HEAD)
export PKG_VERSION="1-$GIT_HASH-$TRAVIS_BRANCH-git"
if [ -n "${TRAVIS_TAG}" ]; then
export PKG_VERSION="$TRAVIS_TAG"
fi
cd /root/obs-websocket/build
PAGER=cat checkinstall -y --type=debian --fstrans=no --nodoc \
--backup=no --deldoc=yes --install=no \
--pkgname=obs-websocket --pkgversion="$PKG_VERSION" \
--pkglicense="GPLv2.0" --maintainer="contact@slepin.fr" \
--pkggroup="video" \
--pkgsource="https://github.com/Palakis/obs-websocket" \
--pakdir="/package"
chmod ao+r /package/*

7
CI/prepare-windows.cmd Normal file
View File

@ -0,0 +1,7 @@
mkdir build32
mkdir build64
cd build32
cmake -G "Visual Studio 16 2019" -A Win32 -DCMAKE_SYSTEM_VERSION=10.0 -DQTDIR="%QTDIR32%" -DLibObs_DIR="%OBSPath%\build32\libobs" -DLIBOBS_INCLUDE_DIR="%OBSPath%\libobs" -DLIBOBS_LIB="%OBSPath%\build32\libobs\%build_config%\obs.lib" -DOBS_FRONTEND_LIB="%OBSPath%\build32\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib" ..
cd ..\build64
cmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_SYSTEM_VERSION=10.0 -DQTDIR="%QTDIR64%" -DLibObs_DIR="%OBSPath%\build64\libobs" -DLIBOBS_INCLUDE_DIR="%OBSPath%\libobs" -DLIBOBS_LIB="%OBSPath%\build64\libobs\%build_config%\obs.lib" -DOBS_FRONTEND_LIB="%OBSPath%\build64\UI\obs-frontend-api\%build_config%\obs-frontend-api.lib" ..

View File

@ -8,23 +8,15 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (WIN32 OR APPLE)
include(external/FindLibObs.cmake)
endif()
add_definitions(-DASIO_STANDALONE)
if (UNIX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
endif()
if (WIN32 OR APPLE)
include(external/FindLibObs.cmake)
endif()
find_package(LibObs REQUIRED)
find_package(Qt5Core REQUIRED)
find_package(Qt5Widgets REQUIRED)
find_package(Qt5 REQUIRED COMPONENTS Core Widgets)
set(obs-websocket_SOURCES
src/obs-websocket.cpp
@ -42,9 +34,14 @@ set(obs-websocket_SOURCES
src/WSRequestHandler_Streaming.cpp
src/WSRequestHandler_StudioMode.cpp
src/WSRequestHandler_Transitions.cpp
src/WSRequestHandler_Outputs.cpp
src/WSEvents.cpp
src/Config.cpp
src/Utils.cpp
src/rpc/RpcRequest.cpp
src/rpc/RpcResponse.cpp
src/rpc/RpcEvent.cpp
src/protocol/OBSRemoteProtocol.cpp
src/forms/settings-dialog.cpp)
set(obs-websocket_HEADERS
@ -55,6 +52,10 @@ set(obs-websocket_HEADERS
src/WSEvents.h
src/Config.h
src/Utils.h
src/rpc/RpcRequest.h
src/rpc/RpcResponse.h
src/rpc/RpcEvent.h
src/protocol/OBSRemoteProtocol.h
src/forms/settings-dialog.h)
# --- Platform-independent build settings ---
@ -83,6 +84,10 @@ if(WIN32)
message(FATAL_ERROR "Could not find OBS Frontend API's library !")
endif()
if(MSVC)
add_compile_options("/MP")
endif()
add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
@ -119,6 +124,16 @@ if(WIN32)
"$<TARGET_FILE:obs-websocket>"
"${RELEASE_DIR}/obs-plugins/${ARCH_NAME}")
# In Release mode, copy Qt image format plugins
COMMAND if $<CONFIG:Release>==1 (
"${CMAKE_COMMAND}" -E copy
"${QTDIR}/plugins/imageformats/qjpeg.dll"
"${RELEASE_DIR}/bin/${ARCH_NAME}/imageformats/qjpeg.dll")
COMMAND if $<CONFIG:RelWithDebInfo>==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 $<CONFIG:RelWithDebInfo>==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")

View File

@ -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:

View File

@ -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\

View File

@ -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'

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<!-- This file was generated based on handlebars templates. Do not edit directly! -->
# 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&lt;Output&gt;_ | 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&lt;SceneItemTransform&gt; (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&lt;Object&gt;_ | 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`. |
---

View File

@ -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.

View File

@ -16,12 +16,15 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <inttypes.h>
#include <QtWidgets/QMainWindow>
#include <QtCore/QDir>
#include <QtCore/QUrl>
#include <obs-frontend-api.h>
#include <obs.hpp>
#include <util/platform.h>
#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<current_search*>(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<QSpinBox*>("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<QPushButton*>("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<QLayout*>("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<QPushButton*>(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<struct enum_params*>(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);
}

View File

@ -31,55 +31,57 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <obs-module.h>
#include <util/config-file.h>
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);
};

View File

@ -17,45 +17,21 @@
* with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <inttypes.h>
#include <util/platform.h>
#include <media-io/video-io.h>
#include <QtWidgets/QPushButton>
#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<WSEvents*>(param);
OBSSource transition = calldata_get_pointer<obs_source_t>(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<WSEvents*>(param);
OBSSource transition = calldata_get_pointer<obs_source_t>(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<WSEvents*>(param);
OBSSource transition = calldata_get_pointer<obs_source_t>(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<obs_source_t>(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<WSEvents*>(param);
OBSSource source = calldata_get_pointer<obs_source_t>(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<WSEvents*>(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());

View File

@ -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);

View File

@ -17,6 +17,8 @@
* with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <functional>
#include <obs-data.h>
#include "Config.h"
@ -24,204 +26,149 @@
#include "WSRequestHandler.h"
QHash<QString, HandlerResponse(*)(WSRequestHandler*)> 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<QString, RpcMethodHandler> 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<QString> WSRequestHandler::authNotRequired {
const QSet<QString> 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);
}

View File

@ -19,145 +19,147 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#pragma once
#include <QtCore/QString>
#include <QtCore/QHash>
#include <QtCore/QSet>
#include <QtCore/QVariantHash>
#include <QtCore/QString>
#include <QtCore/QSharedPointer>
#include <obs.hpp>
#include <obs-frontend-api.h>
#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<QString, RpcMethodHandler> messageMap;
static const QSet<QString> 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<QString, HandlerResponse(*)(WSRequestHandler*)> messageMap;
static QSet<QString> 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&);
};

View File

@ -1,10 +1,13 @@
#include "WSRequestHandler.h"
#include <QtCore/QByteArray>
#include <QtGui/QImageWriter>
#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<QString> names = req->messageMap.keys();
names.sort(Qt::CaseInsensitive);
QList<QString> names = messageMap.keys();
QList<QByteArray> 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 <enable> parameter missing");
RpcResponse WSRequestHandler::SetHeartbeat(const RpcRequest& request) {
if (!request.hasField("enable")) {
return request.failed("Heartbeat <enable> 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("<filename-formatting> parameter missing");
RpcResponse WSRequestHandler::SetFilenameFormatting(const RpcRequest& request) {
if (!request.hasField("filename-formatting")) {
return request.failed("<filename-formatting> 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();
}

View File

@ -0,0 +1,184 @@
#include <functional>
#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<RpcResponse (obs_output_t*)> 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<Output>} `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<obs_data_array_t*>(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();
});
}

View File

@ -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);
}

View File

@ -1,6 +1,17 @@
#include "WSRequestHandler.h"
#include <functional>
#include <util/platform.h>
#include "Utils.h"
#include "WSRequestHandler.h"
RpcResponse ifCanPause(const RpcRequest& request, std::function<RpcResponse()> 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);
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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<SceneItemTransform> (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<SceneItemTransform> (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<DuplicateSceneItemData*>(_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);
}

View File

@ -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<obs_sceneitem_t*> 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<struct reorder_context*>(param);
OBSSceneItemAutoRelease sceneItem = Utils::GetSceneItemFromItem(scene, item);
obs_sceneitem_release(sceneItem); // ref dec
QVector<struct obs_sceneitem_order_info> 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();
}

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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();
}

View File

@ -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);
}

View File

@ -31,6 +31,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#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());
}
});
}

View File

@ -30,11 +30,8 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <websocketpp/server.hpp>
#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;
}

View File

@ -18,6 +18,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <obs-module.h>
#include <obs-frontend-api.h>
#include <obs-data.h>
#include <QtCore/QTimer>
#include <QtWidgets/QAction>
@ -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!");

View File

@ -38,6 +38,11 @@ using OBSDataArrayAutoRelease =
using OBSOutputAutoRelease =
OBSRef<obs_output_t*, ___output_dummy_addref, obs_output_release>;
void ___data_item_dummy_addref(obs_data_item_t*);
void ___data_item_release(obs_data_item_t*);
using OBSDataItemAutoRelease =
OBSRef<obs_data_item_t*, ___data_item_dummy_addref, ___data_item_release>;
class Config;
typedef std::shared_ptr<Config> 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__)

View File

@ -0,0 +1,117 @@
/*
obs-websocket
Copyright (C) 2016-2019 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include <inttypes.h>
#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);
}

View File

@ -0,0 +1,38 @@
/*
obs-websocket
Copyright (C) 2016-2019 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <string>
#include <obs-data.h>
#include <QtCore/QString>
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);
};

35
src/rpc/RpcEvent.cpp Normal file
View File

@ -0,0 +1,35 @@
/*
obs-websocket
Copyright (C) 2016-2020 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "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);
}
}

60
src/rpc/RpcEvent.h Normal file
View File

@ -0,0 +1,60 @@
/*
obs-websocket
Copyright (C) 2016-2020 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <obs-data.h>
#include <QtCore/QString>
#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;
};

104
src/rpc/RpcRequest.cpp Normal file
View File

@ -0,0 +1,104 @@
/*
obs-websocket
Copyright (C) 2016-2019 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "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);
}

65
src/rpc/RpcRequest.h Normal file
View File

@ -0,0 +1,65 @@
/*
obs-websocket
Copyright (C) 2016-2019 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <obs-data.h>
#include <QtCore/QString>
#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;
};

48
src/rpc/RpcResponse.cpp Normal file
View File

@ -0,0 +1,48 @@
/*
obs-websocket
Copyright (C) 2016-2019 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#include "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;
}

70
src/rpc/RpcResponse.h Normal file
View File

@ -0,0 +1,70 @@
/*
obs-websocket
Copyright (C) 2016-2019 Stéphane Lepin <stephane.lepin@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
#include <obs-data.h>
#include <QtCore/QString>
#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;
};