From f3d5cfbd1819edb4bb9f711bba8ea6ecee49bca0 Mon Sep 17 00:00:00 2001 From: tt2468 Date: Mon, 8 Mar 2021 03:56:43 -0800 Subject: [PATCH] Initial commit for attempted rewrite --- .editorconfig | 10 + .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE.md | 20 + .github/images/mediaunit_logo_black.png | Bin 0 -> 55089 bytes .github/images/obsws_logo.png | Bin 0 -> 58525 bytes .../images/supportclass_logo_blacktext.png | Bin 0 -> 8035 bytes .github/pull_request_template.md | 35 + .github/workflows/pr_push.yml | 412 + .github/workflows/tag_release.yml | 484 + .gitignore | 10 + .gitmodules | 6 + BUILDING.md | 66 + CI/build-macos.sh | 28 + CI/build-ubuntu.sh | 6 + CI/download-obs-deps.cmd | 6 + CI/generate-docs.sh | 31 + CI/install-build-obs-macos.sh | 42 + CI/install-dependencies-macos.sh | 56 + CI/install-dependencies-ubuntu.sh | 19 + CI/install-qt-win.cmd | 8 + CI/macos/Brewfile | 10 + CI/macos/obs-websocket.pkgproj | 726 + CI/macos/qt.rb | 145 + CI/package-macos.sh | 90 + CI/package-ubuntu.sh | 24 + CI/package-windows.cmd | 12 + CI/prepare-obs-windows.cmd | 37 + CI/prepare-windows.cmd | 7 + CMakeLists.txt | 192 + CONTRIBUTING.md | 82 + LICENSE | 339 + README.md | 132 + SSL-TUNNELLING.md | 45 + azure-pipelines.yml | 183 + crowdin.yml | 3 + data/locale/ar-SA.ini | 1 + data/locale/de-DE.ini | 18 + data/locale/en-US.ini | 21 + data/locale/es-ES.ini | 16 + data/locale/fr-FR.ini | 16 + data/locale/hi-IN.ini | 1 + data/locale/it-IT.ini | 18 + data/locale/ja-JP.ini | 16 + data/locale/ko-KR.ini | 18 + data/locale/nl-NL.ini | 18 + data/locale/pl-PL.ini | 15 + data/locale/pt-PT.ini | 18 + data/locale/ru-RU.ini | 18 + data/locale/zh-CN.ini | 16 + data/locale/zh-TW.ini | 9 + deps/asio | 1 + deps/websocketpp | 1 + docs/.editorconfig | 11 + docs/.gitignore | 4 + docs/.npmrc | 1 + docs/README.md | 21 + docs/comments.js | 104 + docs/config.json | 5 + docs/docs.js | 37 + docs/generated/comments.json | 11161 ++++++++++++++++ docs/generated/protocol.md | 4448 ++++++ docs/package.json | 21 + docs/partials/eventsHeader.md | 11 + docs/partials/introduction.md | 38 + docs/partials/requestsHeader.md | 11 + docs/partials/typedefsHeader.md | 2 + docs/protocol.hbs | 112 + external/FindLibObs.cmake | 107 + installer/installer.iss | 69 + src/forms/settings-dialog.cpp | 39 + src/forms/settings-dialog.h | 40 + src/forms/settings-dialog.ui | 158 + src/obs-websocket.cpp | 57 + src/obs-websocket.h | 30 + src/rpc/RpcEvent.cpp | 14 + src/rpc/RpcEvent.h | 28 + src/rpc/RpcRequest.h | 123 + src/rpc/RpcRequestData.cpp | 1 + src/rpc/RpcRequestResponse.cpp | 1 + 79 files changed, 20144 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/images/mediaunit_logo_black.png create mode 100644 .github/images/obsws_logo.png create mode 100644 .github/images/supportclass_logo_blacktext.png create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/pr_push.yml create mode 100644 .github/workflows/tag_release.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 BUILDING.md create mode 100755 CI/build-macos.sh create mode 100755 CI/build-ubuntu.sh create mode 100644 CI/download-obs-deps.cmd create mode 100755 CI/generate-docs.sh create mode 100755 CI/install-build-obs-macos.sh create mode 100755 CI/install-dependencies-macos.sh create mode 100755 CI/install-dependencies-ubuntu.sh create mode 100644 CI/install-qt-win.cmd create mode 100644 CI/macos/Brewfile create mode 100644 CI/macos/obs-websocket.pkgproj create mode 100644 CI/macos/qt.rb create mode 100755 CI/package-macos.sh create mode 100755 CI/package-ubuntu.sh create mode 100644 CI/package-windows.cmd create mode 100644 CI/prepare-obs-windows.cmd create mode 100644 CI/prepare-windows.cmd create mode 100644 CMakeLists.txt create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SSL-TUNNELLING.md create mode 100644 azure-pipelines.yml create mode 100644 crowdin.yml create mode 100644 data/locale/ar-SA.ini create mode 100644 data/locale/de-DE.ini create mode 100644 data/locale/en-US.ini create mode 100644 data/locale/es-ES.ini create mode 100644 data/locale/fr-FR.ini create mode 100644 data/locale/hi-IN.ini create mode 100644 data/locale/it-IT.ini create mode 100644 data/locale/ja-JP.ini create mode 100644 data/locale/ko-KR.ini create mode 100644 data/locale/nl-NL.ini create mode 100644 data/locale/pl-PL.ini create mode 100644 data/locale/pt-PT.ini create mode 100644 data/locale/ru-RU.ini create mode 100644 data/locale/zh-CN.ini create mode 100644 data/locale/zh-TW.ini create mode 160000 deps/asio create mode 160000 deps/websocketpp create mode 100644 docs/.editorconfig create mode 100644 docs/.gitignore create mode 100644 docs/.npmrc create mode 100644 docs/README.md create mode 100644 docs/comments.js create mode 100644 docs/config.json create mode 100644 docs/docs.js create mode 100644 docs/generated/comments.json create mode 100644 docs/generated/protocol.md create mode 100644 docs/package.json create mode 100644 docs/partials/eventsHeader.md create mode 100644 docs/partials/introduction.md create mode 100644 docs/partials/requestsHeader.md create mode 100644 docs/partials/typedefsHeader.md create mode 100644 docs/protocol.hbs create mode 100644 external/FindLibObs.cmake create mode 100644 installer/installer.iss create mode 100644 src/forms/settings-dialog.cpp create mode 100644 src/forms/settings-dialog.h create mode 100644 src/forms/settings-dialog.ui create mode 100644 src/obs-websocket.cpp create mode 100644 src/obs-websocket.h create mode 100644 src/rpc/RpcEvent.cpp create mode 100644 src/rpc/RpcEvent.h create mode 100644 src/rpc/RpcRequest.h create mode 100644 src/rpc/RpcRequestData.cpp create mode 100644 src/rpc/RpcRequestResponse.cpp diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..14b0f7bd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +[*] +insert_final_newline = true + +[*.{c,cpp,h,hpp}] +indent_style = tab +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..69945f88 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +open_collective: obs-websocket +github: Palakis +custom: https://www.paypal.me/stephanelepin diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..215d0d54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,20 @@ +##### Issue type + + + + + +##### Description + + +##### Steps to reproduce and other useful info + + +##### Technical information +- **Operating System** : +- **OBS Studio version** : +- **obs-websocket version** : + +##### Development Environment + + diff --git a/.github/images/mediaunit_logo_black.png b/.github/images/mediaunit_logo_black.png new file mode 100644 index 0000000000000000000000000000000000000000..7757d38b4715c0c142f9072334f3e73eea4c2e12 GIT binary patch literal 55089 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

XLN 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�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≺jiJ0R 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 z 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 literal 0 HcmV?d00001 diff --git a/.github/images/supportclass_logo_blacktext.png b/.github/images/supportclass_logo_blacktext.png new file mode 100644 index 0000000000000000000000000000000000000000..b171e7af60e37ae53a3aa9bb77176a17c7751f2f GIT binary patch literal 8035 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 I^0txx10)uuZU6uP literal 0 HcmV?d00001 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..d6fe377d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ + + + + +### Description + + +### Motivation and Context + + + + +### How Has This Been Tested? + +Tested OS(s): + +### Types of changes + + + + + + + + + +### Checklist: + + +- [ ] I have read the [**contributing** document](https://github.com/Palakis/obs-websocket/blob/4.x-current/CONTRIBUTING.md). +- [ ] My code is not on the master branch. +- [ ] The code has been tested. +- [ ] All commit messages are properly formatted and commits squashed where appropriate. +- [ ] I have included updates to all appropriate documentation. + diff --git a/.github/workflows/pr_push.yml b/.github/workflows/pr_push.yml new file mode 100644 index 00000000..cad2023f --- /dev/null +++ b/.github/workflows/pr_push.yml @@ -0,0 +1,412 @@ +name: 'CI Multiplatform Build' + +on: + push: + paths-ignore: + - 'docs/**' + branches: + - 4.x-current + pull_request: + paths-ignore: + - '**.md' + branches: + - 4.x-current + +jobs: + windows: + name: 'Windows 32+64bit' + runs-on: [windows-latest] + if: contains(github.event.head_commit.message, '[skip ci]') != true + env: + QT_VERSION: '5.10.1' + WINDOWS_DEPS_VERSION: '2017' + CMAKE_GENERATOR: "Visual Studio 16 2019" + CMAKE_SYSTEM_VERSION: "10.0" + steps: + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.0.0 + - name: 'Checkout' + uses: actions/checkout@v2 + with: + path: ${{ github.workspace }}/obs-websocket + submodules: 'recursive' + - name: 'Checkout OBS' + uses: actions/checkout@v2 + with: + repository: obsproject/obs-studio + path: ${{ github.workspace }}/obs-studio + submodules: 'recursive' + - name: 'Get OBS-Studio git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git fetch --prune --unshallow + echo ::set-env name=OBS_GIT_BRANCH::$(git rev-parse --abbrev-ref HEAD) + echo ::set-env name=OBS_GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=OBS_GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git checkout ${{ env.OBS_GIT_TAG }} + git submodule update + - name: 'Get obs-websocket git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-websocket + run: | + git fetch --prune --unshallow + echo ::set-env name=GIT_BRANCH::${{ github.event.pull_request.head.ref }} + echo ::set-env name=GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Install prerequisite: QT' + run: | + curl -kLO https://cdn-fastly.obsproject.com/downloads/Qt_${{ env.QT_VERSION }}.7z -f --retry 5 -C - + 7z x Qt_${{ env.QT_VERSION }}.7z -o"${{ github.workspace }}\cmbuild\QT" + - name: 'Install prerequisite: Pre-built OBS dependencies' + run: | + curl -kLO https://cdn-fastly.obsproject.com/downloads/dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -f --retry 5 -C - + 7z x dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -o"${{ github.workspace }}\cmbuild\deps" + - name: 'Restore OBS 32-bit build v${{ env.OBS_GIT_TAG }} from cache' + id: build-cache-obs-32 + uses: actions/cache@v1 + env: + CACHE_NAME: 'build-cache-obs-32' + with: + path: ${{ github.workspace }}/obs-studio/build32 + key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }} + restore-keys: | + ${{ runner.os }}-${{ env.CACHE_NAME }}- + - name: 'Configure OBS 32-bit' + if: steps.build-cache-obs-32.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + mkdir .\build32 + cd .\build32 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win32" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES .. + - name: 'Build OBS-Studio 32-bit' + if: steps.build-cache-obs-32.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + msbuild /m /p:Configuration=RelWithDebInfo .\build32\libobs\libobs.vcxproj + msbuild /m /p:Configuration=RelWithDebInfo .\build32\UI\obs-frontend-api\obs-frontend-api.vcxproj + - name: 'Restore OBS 64-bit build v${{ env.OBS_GIT_TAG }} from cache' + id: build-cache-obs-64 + uses: actions/cache@v1 + env: + CACHE_NAME: 'build-cache-obs-64' + with: + path: ${{ github.workspace }}/obs-studio/build64 + key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }} + restore-keys: | + ${{ runner.os }}-${{ env.CACHE_NAME }}- + - name: 'Configure OBS 64-bit' + if: steps.build-cache-obs-64.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + mkdir .\build64 + cd .\build64 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017_64" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win64" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES .. + - name: 'Build OBS-Studio 64-bit' + if: steps.build-cache-obs-64.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + msbuild /m /p:Configuration=RelWithDebInfo .\build64\libobs\libobs.vcxproj + msbuild /m /p:Configuration=RelWithDebInfo .\build64\UI\obs-frontend-api\obs-frontend-api.vcxproj + - name: 'Configure obs-websocket 64-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + mkdir .\build64 + cd .\build64 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017_64" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build64\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build64\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build64\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" .. + - name: 'Configure obs-websocket 32-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + mkdir .\build32 + cd .\build32 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build32\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build32\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build32\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" .. + - name: 'Build obs-websocket 64-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: msbuild /m /p:Configuration=RelWithDebInfo .\build64\obs-websocket.sln + - name: 'Build obs-websocket 32-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: msbuild /m /p:Configuration=RelWithDebInfo .\build32\obs-websocket.sln + - name: 'Set PR artifact filename' + shell: bash + run: | + FILENAME="obs-websocket-${{ env.GIT_HASH }}-Windows" + echo "::set-env name=FILENAME::$FILENAME" + - name: 'Package obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + mkdir package + cd package + 7z a "${{ env.WIN_FILENAME }}.zip" "..\release\*" + iscc ..\installer\installer.iss /O. /F"${{ env.WIN_FILENAME }}-Installer" + - name: 'Publish ${{ env.WIN_FILENAME }}.zip' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_HASH }}-Windows' + path: ${{ github.workspace }}/obs-websocket/package/*.zip + - name: 'Publish ${{ env.WIN_FILENAME }}-Installer.exe' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_HASH }}-Windows-Installer' + path: ${{ github.workspace }}/obs-websocket/package/*.exe + ubuntu64: + name: "Linux/Ubuntu 64-bit" + runs-on: [ubuntu-latest] + if: contains(github.event.head_commit.message, '[skip ci]') != true + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + path: ${{ github.workspace }}/obs-websocket + submodules: 'recursive' + - name: 'Checkout OBS' + uses: actions/checkout@v2 + with: + repository: obsproject/obs-studio + path: ${{ github.workspace }}/obs-studio + submodules: 'recursive' + - name: 'Get OBS-Studio git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git fetch --prune --unshallow + echo ::set-env name=OBS_GIT_BRANCH::$(git rev-parse --abbrev-ref HEAD) + echo ::set-env name=OBS_GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=OBS_GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git checkout ${{ env.OBS_GIT_TAG }} + git submodule update + - name: 'Get obs-websocket git info' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + git fetch --prune --unshallow + echo ::set-env name=GIT_BRANCH::${{ github.event.pull_request.head.ref }} + echo ::set-env name=GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Install prerequisites (Apt)' + shell: bash + run: | + sudo dpkg --add-architecture amd64 + sudo apt-get -qq update + sudo apt-get install -y \ + build-essential \ + checkinstall \ + cmake \ + libasound2-dev \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libcurl4-openssl-dev \ + libfdk-aac-dev \ + libfontconfig-dev \ + libfreetype6-dev \ + libgl1-mesa-dev \ + libjack-jackd2-dev \ + libjansson-dev \ + libluajit-5.1-dev \ + libpulse-dev \ + libqt5x11extras5-dev \ + libspeexdsp-dev \ + libswresample-dev \ + libswscale-dev \ + libudev-dev \ + libv4l-dev \ + libva-dev \ + libvlc-dev \ + libx11-dev \ + libx264-dev \ + libxcb-randr0-dev \ + libxcb-shm0-dev \ + libxcb-xinerama0-dev \ + libxcomposite-dev \ + libxinerama-dev \ + libmbedtls-dev \ + pkg-config \ + python3-dev \ + qtbase5-dev \ + libqt5svg5-dev \ + swig + - name: 'Configure OBS-Studio' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + mkdir ./build + cd ./build + cmake -DDISABLE_PLUGINS=YES -DENABLE_SCRIPTING=NO -DUNIX_STRUCTURE=YES -DCMAKE_INSTALL_PREFIX=/usr .. + - name: 'Build OBS-Studio' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + set -e + cd ./build + make -j4 libobs obs-frontend-api + - name: 'Install OBS-Studio' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + cd ./build + sudo cp ./libobs/libobs.so /usr/lib + sudo cp ./UI/obs-frontend-api/libobs-frontend-api.so /usr/lib + sudo mkdir -p /usr/include/obs + sudo cp ../UI/obs-frontend-api/obs-frontend-api.h /usr/include/obs/obs-frontend-api.h + - name: 'Configure obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + mkdir ./build + cd ./build + cmake -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DCMAKE_INSTALL_PREFIX=/usr .. + - name: 'Build obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + set -e + cd ./build + make -j4 + - name: 'Set PR artifact filename' + shell: bash + run: | + FILENAME="obs-websocket-1-${{ env.GIT_HASH }}-1_amd64.deb" + echo "::set-env name=FILENAME::$FILENAME" + - name: 'Package ${{ env.FILENAME }}' + if: success() + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + VERSION="1-${{ env.GIT_HASH }}-git" + cd ./build + sudo checkinstall -y --type=debian --fstrans=no -nodoc \ + --backup=no --deldoc=yes --install=no --pkgname=obs-websocket --pkgversion=$VERSION \ + --pkglicense="GPLv2.0" --maintainer="${{ github.event.pusher.email }}" --pkggroup="video" \ + --pkgsource="${{ github.event.repository.html_url }}" \ + --requires="obs-studio,libqt5core5a,libqt5widgets5,qt5-image-formats-plugins" \ + --pakdir="../package" + sudo chmod ao+r ../package/* + cd - + - name: 'Publish ${{ env.FILENAME }}' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_HASH }}-linux' + path: '${{ github.workspace }}/obs-websocket/package/*.deb' + macos64: + name: "macOS 64-bit" + runs-on: [macos-latest] + if: contains(github.event.head_commit.message, '[skip ci]') != true + env: + MACOS_DEPS_VERSION: '2020-04-18' + QT_VERSION: '5.14.1' + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + path: ${{ github.workspace }}/obs-websocket + submodules: 'recursive' + - name: 'Checkout OBS' + uses: actions/checkout@v2 + with: + repository: obsproject/obs-studio + path: ${{ github.workspace }}/obs-studio + submodules: 'recursive' + - name: 'Get OBS-Studio git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git fetch --prune --unshallow + echo ::set-env name=OBS_GIT_BRANCH::$(git rev-parse --abbrev-ref HEAD) + echo ::set-env name=OBS_GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=OBS_GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git checkout ${{ env.OBS_GIT_TAG }} + git submodule update + - name: 'Get obs-websocket git info' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + git fetch --prune --unshallow + echo ::set-env name=GIT_BRANCH::${{ github.event.pull_request.head.ref }} + echo ::set-env name=GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Install prerequisites (Homebrew)' + shell: bash + run: | + brew bundle --file ${{ github.workspace }}/obs-websocket/CI/macos/Brewfile + - name: 'Install prerequisite: Pre-built OBS dependencies' + shell: bash + run: | + curl -L -O https://github.com/obsproject/obs-deps/releases/download/${{ env.MACOS_DEPS_VERSION }}/osx-deps-${{ env.MACOS_DEPS_VERSION }}.tar.gz + tar -xf ${{ github.workspace }}/osx-deps-${{ env.MACOS_DEPS_VERSION }}.tar.gz -C "/tmp" + - name: 'Configure OBS Studio' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + mkdir build + cd build + cmake -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 -DENABLE_SCRIPTING=NO -DDepsPath=/tmp/obsdeps -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/${{ env.QT_VERSION }}/lib/cmake .. + - name: 'Build OBS Studio libraries' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + set -e + cd ./build + make -j4 libobs obs-frontend-api + - name: 'Configure obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + mkdir build + cd build + cmake -DQTDIR=/usr/local/Cellar/qt/${{ env.QT_VERSION }} -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DLIBOBS_LIB=${{ github.workspace }}/obs-studio/libobs -DOBS_FRONTEND_LIB="${{ github.workspace }}/obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr .. + - name: 'Build obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + set -e + cd ./build + make -j4 + - name: 'Install prerequisite: Packages app' + if: success() + shell: bash + run: | + curl -L -O https://s3-us-west-2.amazonaws.com/obs-nightly/Packages.pkg + sudo installer -pkg ${{ github.workspace }}/Packages.pkg -target / + - name: 'Set PR artifact filename' + shell: bash + run: | + FILENAME_UNSIGNED="obs-websocket-${{ env.GIT_HASH }}-macOS-Unsigned.pkg" + echo "::set-env name=FILENAME_UNSIGNED::$FILENAME_UNSIGNED" + - name: 'Fix linked dynamic library paths' + if: success() + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + install_name_tool -change /usr/local/opt/qt/lib/QtWidgets.framework/Versions/5/QtWidgets @executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets ./build/obs-websocket.so + install_name_tool -change /usr/local/opt/qt/lib/QtGui.framework/Versions/5/QtGui @executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui ./build/obs-websocket.so + install_name_tool -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore @executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore ./build/obs-websocket.so + echo "Dependencies for obs-websocket" + otool -L ./build/obs-websocket.so + - name: 'Package ${{ env.FILENAME }}' + if: success() + working-directory: ./obs-websocket + shell: bash + run: | + packagesbuild ./CI/macos/obs-websocket.pkgproj + mv ./release/obs-websocket.pkg ./release/${{ env.FILENAME_UNSIGNED }} + - name: 'Publish ${{ env.FILENAME_UNSIGNED }} artifact' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_HASH }}-macOS' + path: ${{ github.workspace }}/obs-websocket/release/*.pkg diff --git a/.github/workflows/tag_release.yml b/.github/workflows/tag_release.yml new file mode 100644 index 00000000..f499e0bb --- /dev/null +++ b/.github/workflows/tag_release.yml @@ -0,0 +1,484 @@ +name: 'CI Multiplatform Release' + +on: + push: + paths-ignore: + - 'docs/**' + tags: + - '[45].[0-9]+.[0-9]+' + +jobs: + windows: + name: 'Windows 32+64bit' + runs-on: [windows-latest] + env: + QT_VERSION: '5.10.1' + WINDOWS_DEPS_VERSION: '2017' + CMAKE_GENERATOR: "Visual Studio 16 2019" + CMAKE_SYSTEM_VERSION: "10.0" + steps: + - name: 'Add msbuild to PATH' + uses: microsoft/setup-msbuild@v1.0.0 + - name: 'Checkout' + uses: actions/checkout@v2 + with: + path: ${{ github.workspace }}/obs-websocket + submodules: 'recursive' + - name: 'Checkout OBS' + uses: actions/checkout@v2 + with: + repository: obsproject/obs-studio + path: ${{ github.workspace }}/obs-studio + submodules: 'recursive' + - name: 'Get OBS-Studio git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git fetch --prune --unshallow + echo ::set-env name=OBS_GIT_BRANCH::$(git rev-parse --abbrev-ref HEAD) + echo ::set-env name=OBS_GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=OBS_GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git checkout ${{ env.OBS_GIT_TAG }} + git submodule update + - name: 'Get obs-websocket git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-websocket + run: | + git fetch --prune --unshallow + echo ::set-env name=GIT_BRANCH::${{ github.event.pull_request.head.ref }} + echo ::set-env name=GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Install prerequisite: QT' + run: | + curl -kLO https://cdn-fastly.obsproject.com/downloads/Qt_${{ env.QT_VERSION }}.7z -f --retry 5 -C - + 7z x Qt_${{ env.QT_VERSION }}.7z -o"${{ github.workspace }}\cmbuild\QT" + - name: 'Install prerequisite: Pre-built OBS dependencies' + run: | + curl -kLO https://cdn-fastly.obsproject.com/downloads/dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -f --retry 5 -C - + 7z x dependencies${{ env.WINDOWS_DEPS_VERSION }}.zip -o"${{ github.workspace }}\cmbuild\deps" + - name: 'Restore OBS 32-bit build v${{ env.OBS_GIT_TAG }} from cache' + id: build-cache-obs-32 + uses: actions/cache@v1 + env: + CACHE_NAME: 'build-cache-obs-32' + with: + path: ${{ github.workspace }}/obs-studio/build32 + key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }} + restore-keys: | + ${{ runner.os }}-${{ env.CACHE_NAME }}- + - name: 'Configure OBS 32-bit' + if: steps.build-cache-obs-32.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + mkdir .\build32 + cd .\build32 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win32" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES .. + - name: 'Build OBS-Studio 32-bit' + if: steps.build-cache-obs-32.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + msbuild /m /p:Configuration=RelWithDebInfo .\build32\libobs\libobs.vcxproj + msbuild /m /p:Configuration=RelWithDebInfo .\build32\UI\obs-frontend-api\obs-frontend-api.vcxproj + - name: 'Restore OBS 64-bit build v${{ env.OBS_GIT_TAG }} from cache' + id: build-cache-obs-64 + uses: actions/cache@v1 + env: + CACHE_NAME: 'build-cache-obs-64' + with: + path: ${{ github.workspace }}/obs-studio/build64 + key: ${{ runner.os }}-${{ env.CACHE_NAME }}-${{ env.OBS_GIT_TAG }} + restore-keys: | + ${{ runner.os }}-${{ env.CACHE_NAME }}- + - name: 'Configure OBS 64-bit' + if: steps.build-cache-obs-64.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + mkdir .\build64 + cd .\build64 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017_64" -DDepsPath="${{ github.workspace }}\cmbuild\deps\win64" -DCOPIED_DEPENDENCIES=NO -DCOPY_DEPENDENCIES=YES .. + - name: 'Build OBS-Studio 64-bit' + if: steps.build-cache-obs-64.outputs.cache-hit != 'true' + working-directory: ${{ github.workspace }}/obs-studio + run: | + msbuild /m /p:Configuration=RelWithDebInfo .\build64\libobs\libobs.vcxproj + msbuild /m /p:Configuration=RelWithDebInfo .\build64\UI\obs-frontend-api\obs-frontend-api.vcxproj + - name: 'Configure obs-websocket 64-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + mkdir .\build64 + cd .\build64 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A x64 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017_64" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build64\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build64\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build64\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" .. + - name: 'Configure obs-websocket 32-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + mkdir .\build32 + cd .\build32 + cmake -G "${{ env.CMAKE_GENERATOR }}" -A Win32 -DCMAKE_SYSTEM_VERSION="${{ env.CMAKE_SYSTEM_VERSION }}" -DQTDIR="${{ github.workspace }}\cmbuild\QT\${{ env.QT_VERSION }}\msvc2017" -DLibObs_DIR="${{ github.workspace }}\obs-studio\build32\libobs" -DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs" -DLIBOBS_LIB="${{ github.workspace }}\obs-studio\build32\libobs\RelWithDebInfo\obs.lib" -DOBS_FRONTEND_LIB="${{ github.workspace }}\obs-studio\build32\UI\obs-frontend-api\RelWithDebInfo\obs-frontend-api.lib" .. + - name: 'Build obs-websocket 64-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: msbuild /m /p:Configuration=RelWithDebInfo .\build64\obs-websocket.sln + - name: 'Build obs-websocket 32-bit' + working-directory: ${{ github.workspace }}/obs-websocket + run: msbuild /m /p:Configuration=RelWithDebInfo .\build32\obs-websocket.sln + - name: 'Set release filename' + shell: bash + run: | + FILENAME="obs-websocket-${{ env.GIT_TAG }}-Windows" + echo "::set-env name=WIN_FILENAME::$FILENAME" + - name: 'Package obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + mkdir package + cd package + 7z a "${{ env.WIN_FILENAME }}.zip" "..\release\*" + iscc ..\installer\installer.iss /O. /F"${{ env.WIN_FILENAME }}-Installer" + - name: 'Publish ${{ env.WIN_FILENAME }}.zip' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_TAG }}-Windows' + path: ${{ github.workspace }}/obs-websocket/package/*.zip + - name: 'Publish ${{ env.WIN_FILENAME }}-Installer.exe' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_TAG }}-Windows-Installer' + path: ${{ github.workspace }}/obs-websocket/package/*.exe + ubuntu64: + name: "Linux/Ubuntu 64-bit" + runs-on: [ubuntu-latest] + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + path: ${{ github.workspace }}/obs-websocket + submodules: 'recursive' + - name: 'Checkout OBS' + uses: actions/checkout@v2 + with: + repository: obsproject/obs-studio + path: ${{ github.workspace }}/obs-studio + submodules: 'recursive' + - name: 'Get OBS-Studio git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git fetch --prune --unshallow + echo ::set-env name=OBS_GIT_BRANCH::$(git rev-parse --abbrev-ref HEAD) + echo ::set-env name=OBS_GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=OBS_GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git checkout ${{ env.OBS_GIT_TAG }} + git submodule update + - name: 'Get obs-websocket git info' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + git fetch --prune --unshallow + echo ::set-env name=GIT_BRANCH::${{ github.event.pull_request.head.ref }} + echo ::set-env name=GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Install prerequisites (Apt)' + shell: bash + run: | + sudo dpkg --add-architecture amd64 + sudo apt-get -qq update + sudo apt-get install -y \ + build-essential \ + checkinstall \ + cmake \ + libasound2-dev \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libcurl4-openssl-dev \ + libfdk-aac-dev \ + libfontconfig-dev \ + libfreetype6-dev \ + libgl1-mesa-dev \ + libjack-jackd2-dev \ + libjansson-dev \ + libluajit-5.1-dev \ + libpulse-dev \ + libqt5x11extras5-dev \ + libspeexdsp-dev \ + libswresample-dev \ + libswscale-dev \ + libudev-dev \ + libv4l-dev \ + libva-dev \ + libvlc-dev \ + libx11-dev \ + libx264-dev \ + libxcb-randr0-dev \ + libxcb-shm0-dev \ + libxcb-xinerama0-dev \ + libxcomposite-dev \ + libxinerama-dev \ + libmbedtls-dev \ + pkg-config \ + python3-dev \ + qtbase5-dev \ + libqt5svg5-dev \ + swig + - name: 'Configure OBS-Studio' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + mkdir ./build + cd ./build + cmake -DDISABLE_PLUGINS=YES -DENABLE_SCRIPTING=NO -DUNIX_STRUCTURE=YES -DCMAKE_INSTALL_PREFIX=/usr .. + - name: 'Build OBS-Studio' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + set -e + cd ./build + make -j4 libobs obs-frontend-api + - name: 'Install OBS-Studio' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + cd ./build + sudo cp ./libobs/libobs.so /usr/lib + sudo cp ./UI/obs-frontend-api/libobs-frontend-api.so /usr/lib + sudo mkdir -p /usr/include/obs + sudo cp ../UI/obs-frontend-api/obs-frontend-api.h /usr/include/obs/obs-frontend-api.h + - name: 'Configure obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + mkdir ./build + cd ./build + cmake -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DCMAKE_INSTALL_PREFIX=/usr .. + - name: 'Build obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + set -e + cd ./build + make -j4 + - name: 'Set release filename' + shell: bash + run: | + FILENAME="obs-websocket-${{ env.GIT_TAG }}-1_amd64.deb" + echo "::set-env name=LINUX_FILENAME::$FILENAME" + - name: 'Package ${{ env.LINUX_FILENAME }}' + if: success() + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + VERSION="${{ env.GIT_TAG }}" + cd ./build + sudo checkinstall -y --type=debian --fstrans=no -nodoc \ + --backup=no --deldoc=yes --install=no --pkgname=obs-websocket --pkgversion=$VERSION \ + --pkglicense="GPLv2.0" --maintainer="${{ github.event.pusher.email }}" --pkggroup="video" \ + --pkgsource="${{ github.event.repository.html_url }}" \ + --requires="obs-studio,libqt5core5a,libqt5widgets5,qt5-image-formats-plugins" \ + --pakdir="../package" + sudo chmod ao+r ../package/* + cd - + - name: 'Publish ${{ env.LINUX_FILENAME }}' + if: success() + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_TAG }}-linux' + path: '${{ github.workspace }}/obs-websocket/package/*.deb' + macos64: + name: "macOS 64-bit" + runs-on: [macos-latest] + env: + MACOS_DEPS_VERSION: '2020-04-18' + QT_VERSION: '5.14.1' + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + path: ${{ github.workspace }}/obs-websocket + submodules: 'recursive' + - name: 'Checkout OBS' + uses: actions/checkout@v2 + with: + repository: obsproject/obs-studio + path: ${{ github.workspace }}/obs-studio + submodules: 'recursive' + - name: 'Get OBS-Studio git info' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git fetch --prune --unshallow + echo ::set-env name=OBS_GIT_BRANCH::$(git rev-parse --abbrev-ref HEAD) + echo ::set-env name=OBS_GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=OBS_GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Checkout last OBS-Studio release (${{ env.OBS_GIT_TAG }})' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + git checkout ${{ env.OBS_GIT_TAG }} + git submodule update + - name: 'Get obs-websocket git info' + working-directory: ${{ github.workspace }}/obs-websocket + run: | + git fetch --prune --unshallow + echo ::set-env name=GIT_BRANCH::${{ github.event.pull_request.head.ref }} + echo ::set-env name=GIT_HASH::$(git rev-parse --short HEAD) + echo ::set-env name=GIT_TAG::$(git describe --tags --abbrev=0) + - name: 'Install prerequisites (Homebrew)' + shell: bash + run: | + brew bundle --file ${{ github.workspace }}/obs-websocket/CI/macos/Brewfile + - name: 'Install prerequisite: Pre-built OBS dependencies' + shell: bash + run: | + curl -L -O https://github.com/obsproject/obs-deps/releases/download/${{ env.MACOS_DEPS_VERSION }}/osx-deps-${{ env.MACOS_DEPS_VERSION }}.tar.gz + tar -xf ${{ github.workspace }}/osx-deps-${{ env.MACOS_DEPS_VERSION }}.tar.gz -C "/tmp" + - name: 'Configure OBS Studio' + shell: bash + working-directory: ${{ github.workspace }}/obs-studio + run: | + mkdir build + cd build + cmake -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 -DENABLE_SCRIPTING=NO -DDepsPath=/tmp/obsdeps -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt/${{ env.QT_VERSION }}/lib/cmake .. + - name: 'Build OBS Studio libraries' + working-directory: ${{ github.workspace }}/obs-studio + shell: bash + run: | + set -e + cd ./build + make -j4 libobs obs-frontend-api + - name: 'Configure obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + mkdir build + cd build + cmake -DQTDIR=/usr/local/Cellar/qt/${{ env.QT_VERSION }} -DLIBOBS_INCLUDE_DIR=${{ github.workspace }}/obs-studio/libobs -DLIBOBS_LIB=${{ github.workspace }}/obs-studio/libobs -DOBS_FRONTEND_LIB="${{ github.workspace }}/obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX=/usr .. + - name: 'Build obs-websocket' + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + set -e + cd ./build + make -j4 + - name: 'Install prerequisite: Packages app' + if: success() + shell: bash + run: | + curl -L -O https://s3-us-west-2.amazonaws.com/obs-nightly/Packages.pkg + sudo installer -pkg ${{ github.workspace }}/Packages.pkg -target / + - name: 'Set release filename' + if: success() && startsWith(github.ref, 'refs/tags') + shell: bash + run: | + FILENAME_UNSIGNED="obs-websocket-${{ env.GIT_TAG }}-macOS-Unsigned.pkg" + FILENAME="obs-websocket-${{ env.GIT_TAG }}-macOS.pkg" + echo "::set-env name=MAC_FILENAME_UNSIGNED::$FILENAME_UNSIGNED" + echo "::set-env name=MAC_FILENAME::$FILENAME" + - name: 'Fix linked dynamic library paths' + if: success() + working-directory: ${{ github.workspace }}/obs-websocket + shell: bash + run: | + install_name_tool -change /usr/local/opt/qt/lib/QtWidgets.framework/Versions/5/QtWidgets @executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets ./build/obs-websocket.so + install_name_tool -change /usr/local/opt/qt/lib/QtGui.framework/Versions/5/QtGui @executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui ./build/obs-websocket.so + install_name_tool -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore @executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore ./build/obs-websocket.so + echo "Dependencies for obs-websocket" + otool -L ./build/obs-websocket.so + - name: 'Install Apple Developer Certificate' + if: success() + uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 + with: + p12-file-base64: ${{ secrets.MACOS_CERT_CODESIGN }} + p12-password: ${{ secrets.MACOS_CERT_PASS }} + - name: 'Code signing' + if: success() + working-directory: ./obs-websocket + shell: bash + run: | + set -e + codesign --sign "${{ secrets.MACOS_IDENT_CODESIGN }}" ./build/obs-websocket.so + packagesbuild ./CI/macos/obs-websocket.pkgproj + mv ./release/obs-websocket.pkg ./release/${{ env.MAC_FILENAME_UNSIGNED }} + productsign --sign "${{ secrets.MACOS_IDENT_INSTALLER }}" ./release/${{ env.MAC_FILENAME_UNSIGNED }} ./release/${{ env.MAC_FILENAME }} + rm ./release/${{ env.MAC_FILENAME_UNSIGNED }} + - name: 'Notarization' + if: success() + working-directory: ./obs-websocket + shell: bash + run: | + set -e + xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "${{ secrets.MACOS_IDENT_USER }}" -p "${{ secrets.MACOS_IDENT_PASS }}" + xcnotary precheck ./release/${{ env.MAC_FILENAME }} + if [ "$?" -eq 0 ]; then xcnotary notarize ./release/${{ env.MAC_FILENAME }} --developer-account "${{ secrets.MACOS_IDENT_USER }}" --developer-password-keychain-item "AC_PASSWORD" --provider "${{ secrets.MACOS_IDENT_PROVIDER }}"; fi + - name: 'Publish ${{ env.MAC_FILENAME }} artifact' + if: success() && startsWith(github.ref, 'refs/tags') + uses: actions/upload-artifact@v2-preview + with: + name: '${{ env.GIT_TAG }}-macOS' + path: ${{ github.workspace }}/obs-websocket/release/*.pkg + make-release: + name: 'Create and upload release' + runs-on: [ubuntu-latest] + needs: [windows, ubuntu64, macos64] + steps: + - name: 'Get the version' + shell: bash + id: get_version + run: | + echo ::set-env name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} + - name: 'Create Release' + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.TAG_VERSION }} + release_name: obs-websocket ${{ env.TAG_VERSION }} + draft: false + prerelease: false + - name: 'Download release artifacts' + uses: actions/download-artifact@v2-preview + - name: 'Upload Windows .zip artifact to release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ github.workspace }}/${{ env.TAG_VERSION }}-Windows/obs-websocket-${{ env.TAG_VERSION }}-Windows.zip + asset_name: obs-websocket-${{ env.TAG_VERSION }}-Windows.zip + asset_content_type: application/zip + - name: 'Upload Windows .exe artifact to release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ github.workspace }}/${{ env.TAG_VERSION }}-Windows-Installer/obs-websocket-${{ env.TAG_VERSION }}-Windows-Installer.exe + asset_name: obs-websocket-${{ env.TAG_VERSION }}-Windows-Installer.exe + asset_content_type: application/zip + - name: 'Upload Linux artifact to release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ github.workspace }}/${{ env.TAG_VERSION }}-linux/obs-websocket_${{ env.TAG_VERSION }}-1_amd64.deb + asset_name: obs-websocket-${{ env.TAG_VERSION }}-1_amd64.deb + asset_content_type: application/octet-stream + - name: 'Upload macOS artifact to release' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ github.workspace }}/${{ env.TAG_VERSION }}-macOS/obs-websocket-${{ env.TAG_VERSION }}-macOS.pkg + asset_name: obs-websocket-${{ env.TAG_VERSION }}-macOS.pkg + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b7e57102 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*~ +.DS_Store +/build/ +/build32/ +/build64/ +/release/ +/package/ +/installer/Output/ +.idea +.vscode diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..3e9749f0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "deps/websocketpp"] + path = deps/websocketpp + url = https://github.com/zaphoyd/websocketpp.git +[submodule "deps/asio"] + path = deps/asio + url = https://github.com/chriskohlhoff/asio.git diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 00000000..bec47951 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,66 @@ +# Compiling obs-websocket + +## Prerequisites + +You'll need [Qt 5.10.x](https://download.qt.io/official_releases/qt/5.10/), +[CMake](https://cmake.org/download/) and a working [OBS Studio development environment](https://obsproject.com/wiki/install-instructions) installed on your +computer. + +## Windows + +In cmake-gui, you'll have to set the following variables : + +- **QTDIR** (path) : location of the Qt environment suited for your compiler and architecture +- **LIBOBS_INCLUDE_DIR** (path) : location of the libobs subfolder in the source code of OBS Studio +- **LIBOBS_LIB** (filepath) : location of the obs.lib file +- **OBS_FRONTEND_LIB** (filepath) : location of the obs-frontend-api.lib file + +## Linux + +On Debian/Ubuntu : + +```shell +sudo apt-get install libboost-all-dev +git clone --recursive https://github.com/Palakis/obs-websocket.git +cd obs-websocket +mkdir build && cd build +cmake -DLIBOBS_INCLUDE_DIR="" -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=true .. +make -j4 +sudo make install +``` + +On other linux OS's, use this cmake command instead: + +```shell +cmake -DLIBOBS_INCLUDE_DIR="" -DCMAKE_INSTALL_PREFIX=/usr .. +``` + +## OS X + +As a prerequisite, you will need Xcode for your current OSX version, the Xcode command line tools, and [Homebrew](https://brew.sh/). +Homebrew's setup will guide you in getting your system set up, you should be good to go once Homebrew is successfully up and running. + +Use of the macOS CI scripts is recommended. Please note that these +scripts install new software and can change several settings on your system. An +existing obs-studio development environment is not required, as +`install-build-obs-macos.sh` will install it for you. If you already have a +working obs-studio development environment and have built obs-studio, you can +skip that script. + +Of course, you're encouraged to dig through the contents of these scripts to +look for issues or specificities. + +```shell +git clone --recursive https://github.com/Palakis/obs-websocket.git +cd obs-websocket +./CI/install-dependencies-macos.sh +./CI/install-build-obs-macos.sh +./CI/build-macos.sh +./CI/package-macos.sh +``` + +This will result in a ready-to-use `obs-websocket.pkg` installer in the `release` subfolder. + +## Automated Builds + +[![Build Status](https://dev.azure.com/Palakis/obs-websocket/_apis/build/status/Palakis.obs-websocket?branchName=4.x-current)](https://dev.azure.com/Palakis/obs-websocket/_build/latest?definitionId=2&branchName=4.x-current) diff --git a/CI/build-macos.sh b/CI/build-macos.sh new file mode 100755 index 00000000..f528ce2e --- /dev/null +++ b/CI/build-macos.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +OSTYPE=$(uname) + +if [ "${OSTYPE}" != "Darwin" ]; then + echo "[obs-websocket - Error] macOS build script can be run on Darwin-type OS only." + exit 1 +fi + +HAS_CMAKE=$(type cmake 2>/dev/null) + +if [ "${HAS_CMAKE}" = "" ]; then + echo "[obs-websocket - Error] CMake not installed - please run 'install-dependencies-macos.sh' first." + exit 1 +fi + +#export QT_PREFIX="$(find /usr/local/Cellar/qt5 -d 1 | tail -n 1)" + +echo "[obs-websocket] Building 'obs-websocket' for macOS." +mkdir -p build && cd build +cmake .. \ + -DQTDIR=/usr/local/opt/qt \ + -DLIBOBS_INCLUDE_DIR=../../obs-studio/libobs \ + -DLIBOBS_LIB=../../obs-studio/libobs \ + -DOBS_FRONTEND_LIB="$(pwd)/../../obs-studio/build/UI/obs-frontend-api/libobs-frontend-api.dylib" \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DCMAKE_INSTALL_PREFIX=/usr \ +&& make -j4 diff --git a/CI/build-ubuntu.sh b/CI/build-ubuntu.sh new file mode 100755 index 00000000..498840ef --- /dev/null +++ b/CI/build-ubuntu.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -ex + +mkdir build && cd build +cmake -DCMAKE_INSTALL_PREFIX=/usr -DUSE_UBUNTU_FIX=true .. +make -j4 diff --git a/CI/download-obs-deps.cmd b/CI/download-obs-deps.cmd new file mode 100644 index 00000000..ff4ffd57 --- /dev/null +++ b/CI/download-obs-deps.cmd @@ -0,0 +1,6 @@ +if not exist %DepsBasePath% ( + curl -o %DepsBasePath%.zip -kLO https://obsproject.com/downloads/dependencies2017.zip -f --retry 5 -C - + 7z x %DepsBasePath%.zip -o%DepsBasePath% +) else ( + echo "OBS dependencies are already there. Download skipped." +) diff --git a/CI/generate-docs.sh b/CI/generate-docs.sh new file mode 100755 index 00000000..bb1d3dfd --- /dev/null +++ b/CI/generate-docs.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e +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 + +echo "-- Documentation successfully generated." + +if git diff --quiet; then + echo "-- No documentation changes to commit." + 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 "Azure CI" +git config user.email "$COMMIT_AUTHOR_EMAIL" + +git add ./generated +git pull +git commit -m "docs(ci): Update protocol.md - $(git rev-parse --short HEAD) [skip ci]" +git push -q $GITHUB_REPO diff --git a/CI/install-build-obs-macos.sh b/CI/install-build-obs-macos.sh new file mode 100755 index 00000000..4af63b49 --- /dev/null +++ b/CI/install-build-obs-macos.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +OSTYPE=$(uname) + +if [ "${OSTYPE}" != "Darwin" ]; then + echo "[obs-websocket - Error] macOS obs-studio build script can be run on Darwin-type OS only." + exit 1 +fi + +HAS_CMAKE=$(type cmake 2>/dev/null) +HAS_GIT=$(type git 2>/dev/null) + +if [ "${HAS_CMAKE}" = "" ]; then + echo "[obs-websocket - Error] CMake not installed - please run 'install-dependencies-macos.sh' first." + exit 1 +fi + +if [ "${HAS_GIT}" = "" ]; then + echo "[obs-websocket - Error] Git not installed - please install Xcode developer tools or via Homebrew." + exit 1 +fi + +echo "[obs-websocket] Downloading and unpacking OBS dependencies" +wget --quiet --retry-connrefused --waitretry=1 https://obs-nightly.s3.amazonaws.com/osx-deps-2018-08-09.tar.gz +tar -xf ./osx-deps-2018-08-09.tar.gz -C /tmp + +# Build obs-studio +cd .. +echo "[obs-websocket] Cloning obs-studio from GitHub.." +git clone https://github.com/obsproject/obs-studio +cd obs-studio +OBSLatestTag=$(git describe --tags --abbrev=0) +git checkout $OBSLatestTag +mkdir build && cd build +echo "[obs-websocket] Building obs-studio.." +cmake .. \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.11 \ + -DDISABLE_PLUGINS=true \ + -DENABLE_SCRIPTING=0 \ + -DDepsPath=/tmp/obsdeps \ + -DCMAKE_PREFIX_PATH=/usr/local/opt/qt/lib/cmake \ +&& make -j4 diff --git a/CI/install-dependencies-macos.sh b/CI/install-dependencies-macos.sh new file mode 100755 index 00000000..267fa49a --- /dev/null +++ b/CI/install-dependencies-macos.sh @@ -0,0 +1,56 @@ +#!/bin/sh + + + +OSTYPE=$(uname) + +if [ "${OSTYPE}" != "Darwin" ]; then + echo "[obs-websocket - Error] macOS install dependencies script can be run on Darwin-type OS only." + exit 1 +fi + +HAS_BREW=$(type brew 2>/dev/null) + +if [ "${HAS_BREW}" = "" ]; then + echo "[obs-websocket - Error] Please install Homebrew (https://www.brew.sh/) to build obs-websocket on macOS." + exit 1 +fi + +# OBS Studio deps +echo "[obs-websocket] Updating Homebrew.." +brew update >/dev/null +echo "[obs-websocket] Checking installed Homebrew formulas.." +BREW_PACKAGES=$(brew list) +BREW_DEPENDENCIES="jack speexdsp ccache swig mbedtls" + +for DEPENDENCY in ${BREW_DEPENDENCIES}; do + if echo "${BREW_PACKAGES}" | grep -q "^${DEPENDENCY}\$"; then + echo "[obs-websocket] Upgrading OBS-Studio dependency '${DEPENDENCY}'.." + brew upgrade ${DEPENDENCY} 2>/dev/null + else + echo "[obs-websocket] Installing OBS-Studio dependency '${DEPENDENCY}'.." + brew install ${DEPENDENCY} 2>/dev/null + fi +done + +# qtwebsockets deps +echo "[obs-websocket] Installing obs-websocket dependency 'QT 5.10.1'.." + +brew install ./CI/macos/qt.rb + +# Pin this version of QT5 to avoid `brew upgrade` +# upgrading it to incompatible version +brew pin qt + +# Fetch and install Packages app +# =!= NOTICE =!= +# Installs a LaunchDaemon under /Library/LaunchDaemons/fr.whitebox.packages.build.dispatcher.plist +# =!= NOTICE =!= + +HAS_PACKAGES=$(type packagesbuild 2>/dev/null) + +if [ "${HAS_PACKAGES}" = "" ]; then + echo "[obs-websocket] Installing Packaging app (might require password due to 'sudo').." + curl -o './Packages.pkg' --retry-connrefused -s --retry-delay 1 'https://s3-us-west-2.amazonaws.com/obs-nightly/Packages.pkg' + sudo installer -pkg ./Packages.pkg -target / +fi diff --git a/CI/install-dependencies-ubuntu.sh b/CI/install-dependencies-ubuntu.sh new file mode 100755 index 00000000..4ed044da --- /dev/null +++ b/CI/install-dependencies-ubuntu.sh @@ -0,0 +1,19 @@ +#!/bin/sh +set -ex + +sudo add-apt-repository -y ppa:obsproject/obs-studio +sudo apt-get -qq update + +sudo apt-get install -y \ + libc-dev-bin \ + libc6-dev git \ + build-essential \ + checkinstall \ + cmake \ + obs-studio \ + qtbase5-dev + +# Dirty hack +sudo wget -O /usr/include/obs/obs-frontend-api.h https://raw.githubusercontent.com/obsproject/obs-studio/26.0.0/UI/obs-frontend-api/obs-frontend-api.h + +sudo ldconfig diff --git a/CI/install-qt-win.cmd b/CI/install-qt-win.cmd new file mode 100644 index 00000000..e0537fe8 --- /dev/null +++ b/CI/install-qt-win.cmd @@ -0,0 +1,8 @@ +if not exist %QtBaseDir% ( + curl -kLO https://cdn-fastly.obsproject.com/downloads/Qt_5.10.1.7z -f --retry 5 -z Qt_5.10.1.7z + 7z x Qt_5.10.1.7z -o%QtBaseDir% +) else ( + echo "Qt is already installed. Download skipped." +) + +dir %QtBaseDir% diff --git a/CI/macos/Brewfile b/CI/macos/Brewfile new file mode 100644 index 00000000..20fabb71 --- /dev/null +++ b/CI/macos/Brewfile @@ -0,0 +1,10 @@ +tap "akeru-inc/tap" +brew "jack" +brew "speexdsp" +brew "cmake" +brew "freetype" +brew "fdk-aac" +brew "https://gist.githubusercontent.com/DDRBoxman/9c7a2b08933166f4b61ed9a44b242609/raw/ef4de6c587c6bd7f50210eccd5bd51ff08e6de13/qt.rb" +brew "swig", link: false +brew "https://gist.githubusercontent.com/DDRBoxman/4cada55c51803a2f963fa40ce55c9d3e/raw/572c67e908bfbc1bcb8c476ea77ea3935133f5b5/swig.rb" +brew "akeru-inc/tap/xcnotary" \ No newline at end of file diff --git a/CI/macos/obs-websocket.pkgproj b/CI/macos/obs-websocket.pkgproj new file mode 100644 index 00000000..7c574505 --- /dev/null +++ b/CI/macos/obs-websocket.pkgproj @@ -0,0 +1,726 @@ + + + + + PROJECT + + PACKAGE_FILES + + DEFAULT_INSTALL_LOCATION + / + HIERARCHY + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + ../../build/obs-websocket.so + PATH_TYPE + 1 + PERMISSIONS + 493 + TYPE + 3 + UID + 0 + + + GID + 80 + PATH + bin + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 2 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + ../../data + PATH_TYPE + 1 + PERMISSIONS + 493 + TYPE + 3 + UID + 0 + + + GID + 80 + PATH + obs-websocket + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 2 + UID + 0 + + + GID + 80 + PATH + plugins + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 2 + UID + 0 + + + GID + 80 + PATH + obs-studio + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 2 + UID + 0 + + + GID + 80 + PATH + Application Support + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Automator + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Documentation + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Extensions + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Filesystems + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Frameworks + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Input Methods + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Internet Plug-Ins + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchAgents + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchDaemons + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PreferencePanes + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Preferences + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Printers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PrivilegedHelperTools + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickLook + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickTime + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Screen Savers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Scripts + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Services + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Widgets + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + Library + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + Shared + PATH_TYPE + 0 + PERMISSIONS + 1023 + TYPE + 1 + UID + 0 + + + GID + 80 + PATH + Users + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + / + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + PAYLOAD_TYPE + 0 + VERSION + 4 + + PACKAGE_SCRIPTS + + RESOURCES + + + PACKAGE_SETTINGS + + AUTHENTICATION + 1 + CONCLUSION_ACTION + 0 + IDENTIFIER + fr.palakis.obs-websocket + OVERWRITE_PERMISSIONS + + VERSION + 4.9.0 + + PROJECT_COMMENTS + + NOTES + + PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBIVE1M + IDQuMDEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvaHRtbDQv + c3RyaWN0LmR0ZCI+CjxodG1sPgo8aGVhZD4KPG1ldGEgaHR0cC1l + cXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7 + IGNoYXJzZXQ9VVRGLTgiPgo8bWV0YSBodHRwLWVxdWl2PSJDb250 + ZW50LVN0eWxlLVR5cGUiIGNvbnRlbnQ9InRleHQvY3NzIj4KPHRp + dGxlPjwvdGl0bGU+CjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29u + dGVudD0iQ29jb2EgSFRNTCBXcml0ZXIiPgo8bWV0YSBuYW1lPSJD + b2NvYVZlcnNpb24iIGNvbnRlbnQ9IjE0MDQuMTMiPgo8c3R5bGUg + dHlwZT0idGV4dC9jc3MiPgo8L3N0eWxlPgo8L2hlYWQ+Cjxib2R5 + Pgo8L2JvZHk+CjwvaHRtbD4K + + + PROJECT_SETTINGS + + BUILD_PATH + + PATH + ../../release + PATH_TYPE + 1 + + EXCLUDED_FILES + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + .DS_Store + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove .DS_Store files + PROXY_TOOLTIP + Remove ".DS_Store" files created by the Finder. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + .pbdevelopment + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove .pbdevelopment files + PROXY_TOOLTIP + Remove ".pbdevelopment" files created by ProjectBuilder or Xcode. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + CVS + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .cvsignore + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + .cvspass + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + .svn + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .git + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .gitignore + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove SCM metadata + PROXY_TOOLTIP + Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + classes.nib + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + designable.db + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + info.nib + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Optimize nib files + PROXY_TOOLTIP + Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + Resources Disabled + TYPE + 1 + + + PROTECTED + + PROXY_NAME + Remove Resources Disabled folders + PROXY_TOOLTIP + Remove "Resources Disabled" folders. + STATE + + + + SEPARATOR + + + + NAME + obs-websocket + + + TYPE + 1 + VERSION + 2 + + diff --git a/CI/macos/qt.rb b/CI/macos/qt.rb new file mode 100644 index 00000000..4405a240 --- /dev/null +++ b/CI/macos/qt.rb @@ -0,0 +1,145 @@ +# Patches for Qt must be at the very least submitted to Qt's Gerrit codereview +# rather than their bug-report Jira. The latter is rarely reviewed by Qt. +class Qt < Formula + desc "Cross-platform application and UI framework" + homepage "https://www.qt.io/" + url "https://download.qt.io/official_releases/qt/5.10/5.10.1/single/qt-everywhere-src-5.10.1.tar.xz" + mirror "https://www.mirrorservice.org/sites/download.qt-project.org/official_releases/qt/5.10/5.10.1/single/qt-everywhere-src-5.10.1.tar.xz" + sha256 "05ffba7b811b854ed558abf2be2ddbd3bb6ddd0b60ea4b5da75d277ac15e740a" + head "https://code.qt.io/qt/qt5.git", :branch => "5.10", :shallow => false + + bottle do + sha256 "8b4bad005596a5f8790150fe455db998ac2406f4e0f04140d6656205d844d266" => :high_sierra + sha256 "9c488554935fb573554a4e36d36d3c81e47245b7fefc4b61edef894e67ba1740" => :sierra + sha256 "c0407afba5951df6cc4c6f6c1c315972bd41c99cecb4e029919c4c15ab6f7bdc" => :el_capitan + end + + keg_only "Qt 5 has CMake issues when linked" + + option "with-docs", "Build documentation" + option "with-examples", "Build examples" + option "without-proprietary-codecs", "Don't build with proprietary codecs (e.g. mp3)" + + # OS X 10.7 Lion is still supported in Qt 5.5, but is no longer a reference + # configuration and thus untested in practice. Builds on OS X 10.7 have been + # reported to fail: . + # depends_on :macos => :mountain_lion + + depends_on "pkg-config" => :build + depends_on :xcode => :build + depends_on "mysql" => :optional + depends_on "postgresql" => :optional + + # Restore `.pc` files for framework-based build of Qt 5 on OS X. This + # partially reverts merged + # between the 5.5.1 and 5.6.0 releases. (Remove this as soon as feasible!) + # + # Core formulae known to fail without this patch (as of 2016-10-15): + # * gnuplot (with `--with-qt` option) + # * mkvtoolnix (with `--with-qt` option, silent build failure) + # * poppler (with `--with-qt` option) + patch do + url "https://raw.githubusercontent.com/Homebrew/formula-patches/e8fe6567/qt5/restore-pc-files.patch" + sha256 "48ff18be2f4050de7288bddbae7f47e949512ac4bcd126c2f504be2ac701158b" + end + + def install + args = %W[ + -verbose + -prefix #{prefix} + -release + -opensource -confirm-license + -system-zlib + -qt-libpng + -qt-libjpeg + -qt-freetype + -qt-pcre + -nomake tests + -no-rpath + -pkg-config + -dbus-runtime + ] + + args << "-nomake" << "examples" if build.without? "examples" + + if build.with? "mysql" + args << "-plugin-sql-mysql" + (buildpath/"brew_shim/mysql_config").write <<~EOS + #!/bin/sh + if [ x"$1" = x"--libs" ]; then + mysql_config --libs | sed "s/-lssl -lcrypto//" + else + exec mysql_config "$@" + fi + EOS + chmod 0755, "brew_shim/mysql_config" + args << "-mysql_config" << buildpath/"brew_shim/mysql_config" + end + + args << "-plugin-sql-psql" if build.with? "postgresql" + args << "-proprietary-codecs" if build.with? "proprietary-codecs" + + system "./configure", *args + system "make" + ENV.deparallelize + system "make", "install" + + if build.with? "docs" + system "make", "docs" + system "make", "install_docs" + end + + # Some config scripts will only find Qt in a "Frameworks" folder + frameworks.install_symlink Dir["#{lib}/*.framework"] + + # The pkg-config files installed suggest that headers can be found in the + # `include` directory. Make this so by creating symlinks from `include` to + # the Frameworks' Headers folders. + Pathname.glob("#{lib}/*.framework/Headers") do |path| + include.install_symlink path => path.parent.basename(".framework") + end + + # Move `*.app` bundles into `libexec` to expose them to `brew linkapps` and + # because we don't like having them in `bin`. + # (Note: This move breaks invocation of Assistant via the Help menu + # of both Designer and Linguist as that relies on Assistant being in `bin`.) + libexec.mkpath + Pathname.glob("#{bin}/*.app") { |app| mv app, libexec } + end + + def caveats; <<~EOS + We agreed to the Qt opensource license for you. + If this is unacceptable you should uninstall. + EOS + end + + test do + (testpath/"hello.pro").write <<~EOS + QT += core + QT -= gui + TARGET = hello + CONFIG += console + CONFIG -= app_bundle + TEMPLATE = app + SOURCES += main.cpp + EOS + + (testpath/"main.cpp").write <<~EOS + #include + #include + + int main(int argc, char *argv[]) + { + QCoreApplication a(argc, argv); + qDebug() << "Hello World!"; + return 0; + } + EOS + + system bin/"qmake", testpath/"hello.pro" + system "make" + assert_predicate testpath/"hello", :exist? + assert_predicate testpath/"main.o", :exist? + system "./hello" + end +end \ No newline at end of file diff --git a/CI/package-macos.sh b/CI/package-macos.sh new file mode 100755 index 00000000..f309cf34 --- /dev/null +++ b/CI/package-macos.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +set -e + +OSTYPE=$(uname) + +if [ "${OSTYPE}" != "Darwin" ]; then + echo "[obs-websocket - Error] macOS package script can be run on Darwin-type OS only." + exit 1 +fi + +echo "[obs-websocket] Preparing package build" +export QT_CELLAR_PREFIX="$(/usr/bin/find /usr/local/Cellar/qt -d 1 | sort -t '.' -k 1,1n -k 2,2n -k 3,3n | tail -n 1)" + +GIT_HASH=$(git rev-parse --short HEAD) +GIT_BRANCH_OR_TAG=$(git name-rev --name-only HEAD | awk -F/ '{print $NF}') + +VERSION="$GIT_HASH-$GIT_BRANCH_OR_TAG" + +FILENAME_UNSIGNED="obs-websocket-$VERSION-Unsigned.pkg" +FILENAME="obs-websocket-$VERSION.pkg" + +echo "[obs-websocket] Modifying obs-websocket.so" +install_name_tool \ + -change /usr/local/opt/qt/lib/QtWidgets.framework/Versions/5/QtWidgets \ + @executable_path/../Frameworks/QtWidgets.framework/Versions/5/QtWidgets \ + -change /usr/local/opt/qt/lib/QtGui.framework/Versions/5/QtGui \ + @executable_path/../Frameworks/QtGui.framework/Versions/5/QtGui \ + -change /usr/local/opt/qt/lib/QtCore.framework/Versions/5/QtCore \ + @executable_path/../Frameworks/QtCore.framework/Versions/5/QtCore \ + ./build/obs-websocket.so + +# Check if replacement worked +echo "[obs-websocket] Dependencies for obs-websocket" +otool -L ./build/obs-websocket.so + +if [[ "$RELEASE_MODE" == "True" ]]; then + echo "[obs-websocket] Signing plugin binary: obs-websocket.so" + codesign --sign "$CODE_SIGNING_IDENTITY" ./build/obs-websocket.so +else + echo "[obs-websocket] Skipped plugin codesigning" +fi + +echo "[obs-websocket] Actual package build" +packagesbuild ./CI/macos/obs-websocket.pkgproj + +echo "[obs-websocket] Renaming obs-websocket.pkg to $FILENAME" +mv ./release/obs-websocket.pkg ./release/$FILENAME_UNSIGNED + +if [[ "$RELEASE_MODE" == "True" ]]; then + echo "[obs-websocket] Signing installer: $FILENAME" + productsign \ + --sign "$INSTALLER_SIGNING_IDENTITY" \ + ./release/$FILENAME_UNSIGNED \ + ./release/$FILENAME + rm ./release/$FILENAME_UNSIGNED + + echo "[obs-websocket] Submitting installer $FILENAME for notarization" + zip -r ./release/$FILENAME.zip ./release/$FILENAME + UPLOAD_RESULT=$(xcrun altool \ + --notarize-app \ + --primary-bundle-id "fr.palakis.obs-websocket" \ + --username "$AC_USERNAME" \ + --password "$AC_PASSWORD" \ + --asc-provider "$AC_PROVIDER_SHORTNAME" \ + --file "./release/$FILENAME.zip") + rm ./release/$FILENAME.zip + + REQUEST_UUID=$(echo $UPLOAD_RESULT | awk -F ' = ' '/RequestUUID/ {print $2}') + echo "Request UUID: $REQUEST_UUID" + + echo "[obs-websocket] Wait for notarization result" + # Pieces of code borrowed from rednoah/notarized-app + while sleep 30 && date; do + CHECK_RESULT=$(xcrun altool \ + --notarization-info "$REQUEST_UUID" \ + --username "$AC_USERNAME" \ + --password "$AC_PASSWORD" \ + --asc-provider "$AC_PROVIDER_SHORTNAME") + echo $CHECK_RESULT + + if ! grep -q "Status: in progress" <<< "$CHECK_RESULT"; then + echo "[obs-websocket] Staple ticket to installer: $FILENAME" + xcrun stapler staple ./release/$FILENAME + break + fi + done +else + echo "[obs-websocket] Skipped installer codesigning and notarization" +fi \ No newline at end of file diff --git a/CI/package-ubuntu.sh b/CI/package-ubuntu.sh new file mode 100755 index 00000000..517e0054 --- /dev/null +++ b/CI/package-ubuntu.sh @@ -0,0 +1,24 @@ +#!/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" + echo "[obs-websocket] Branch is a tag. Setting version to $PKG_VERSION." +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 \(\>= 25.0.7\), libqt5core5a, libqt5widgets5, qt5-image-formats-plugins" \ + --pakdir="../package" + +sudo chmod ao+r ../package/* diff --git a/CI/package-windows.cmd b/CI/package-windows.cmd new file mode 100644 index 00000000..e3c3d445 --- /dev/null +++ b/CI/package-windows.cmd @@ -0,0 +1,12 @@ +mkdir package +cd package + +git rev-parse --short HEAD > package-version.txt +set /p PackageVersion= "%OBSPath%\obs-studio-latest-tag.txt" + set /p OBSLatestTag=<"%OBSPath%\obs-studio-latest-tag.txt" +) + +REM Prepare OBS Studio builds + +echo Running CMake... +cd /D %OBSPath% +echo git checkout %OBSLatestTag% +git checkout %OBSLatestTag% +echo: + +if not exist build32 mkdir build32 +if not exist build64 mkdir build64 + +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%" -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 16 2019" -A x64 -DCMAKE_SYSTEM_VERSION=10.0 -DQTDIR="%QTDIR64%" -DDepsPath="%DepsPath64%" -DDISABLE_PLUGINS=true -DCOPIED_DEPENDENCIES=false -DCOPY_DEPENDENCIES=true .. +echo: +echo: + +dir "%OBSPath%\libobs" diff --git a/CI/prepare-windows.cmd b/CI/prepare-windows.cmd new file mode 100644 index 00000000..09057149 --- /dev/null +++ b/CI/prepare-windows.cmd @@ -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" .. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..f50a6aec --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,192 @@ +cmake_minimum_required(VERSION 3.5) +project(obs-websocket VERSION 5.0.0) + +set(CMAKE_PREFIX_PATH "${QTDIR}") +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_definitions(-DASIO_STANDALONE) + +if (${CMAKE_SYSTEM_PROCESSOR} MATCHES "arm") + set(CMAKE_CXX_FLAGS "-mfpu=neon") +endif() + +if (WIN32 OR APPLE) + include(external/FindLibObs.cmake) +endif() + +find_package(LibObs REQUIRED) +find_package(Qt5 REQUIRED COMPONENTS Core Widgets) + +set(obs-websocket_SOURCES + src/obs-websocket.cpp + src/rpc/RpcRequestData.cpp + src/rpc/RpcRequestResponse.cpp + src/rpc/RpcEvent.cpp + src/forms/settings-dialog.cpp) + +set(obs-websocket_HEADERS + src/obs-websocket.h + src/rpc/RpcRequest.h + src/rpc/RpcEvent.h + src/forms/settings-dialog.h) + +# --- Platform-independent build settings --- +add_library(obs-websocket MODULE + ${obs-websocket_SOURCES} + ${obs-websocket_HEADERS}) + +include_directories( + "${LIBOBS_INCLUDE_DIR}/../UI/obs-frontend-api" + ${Qt5Core_INCLUDES} + ${Qt5Widgets_INCLUDES} + "${CMAKE_SOURCE_DIR}/deps/asio/asio/include" + "${CMAKE_SOURCE_DIR}/deps/websocketpp") + +target_link_libraries(obs-websocket + libobs + Qt5::Core + Qt5::Widgets) + +# --- End of section --- + +# --- Windows-specific build settings and tasks --- +if(WIN32) + if(NOT DEFINED OBS_FRONTEND_LIB) + set(OBS_FRONTEND_LIB "OBS_FRONTEND_LIB-NOTFOUND" CACHE FILEPATH "OBS frontend library") + message(FATAL_ERROR "Could not find OBS Frontend API's library !") + endif() + + if(MSVC) + # Enable Multicore Builds and disable FH4 (to not depend on VCRUNTIME140_1.DLL) + add_definitions(/MP /d2FH4-) + endif() + + add_definitions(-D_WEBSOCKETPP_CPP11_STL_) + + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(ARCH_NAME "64bit") + set(OBS_BUILDDIR_ARCH "build64") + else() + set(ARCH_NAME "32bit") + set(OBS_BUILDDIR_ARCH "build32") + endif() + + include_directories( + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/UI" + ) + + target_link_libraries(obs-websocket + "${OBS_FRONTEND_LIB}") + + # --- Release package helper --- + # The "release" folder has a structure similar OBS' one on Windows + set(RELEASE_DIR "${PROJECT_SOURCE_DIR}/release") + + add_custom_command(TARGET obs-websocket POST_BUILD + # If config is Release, package release files + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E make_directory + "${RELEASE_DIR}/data/obs-plugins/obs-websocket" + "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") + + COMMAND if $==1 ("${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/data" + "${RELEASE_DIR}/data/obs-plugins/obs-websocket") + + COMMAND if $==1 ("${CMAKE_COMMAND}" -E copy + "$" + "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") + + # In Release mode, copy Qt image format plugins + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy + "${QTDIR}/plugins/imageformats/qjpeg.dll" + "${RELEASE_DIR}/bin/${ARCH_NAME}/imageformats/qjpeg.dll") + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy + "${QTDIR}/plugins/imageformats/qjpeg.dll" + "${RELEASE_DIR}/bin/${ARCH_NAME}/imageformats/qjpeg.dll") + + # If config is RelWithDebInfo, package release files + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E make_directory + "${RELEASE_DIR}/data/obs-plugins/obs-websocket" + "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") + + COMMAND if $==1 ("${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/data" + "${RELEASE_DIR}/data/obs-plugins/obs-websocket") + + COMMAND if $==1 ("${CMAKE_COMMAND}" -E copy + "$" + "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") + + COMMAND if $==1 ("${CMAKE_COMMAND}" -E copy + "$" + "${RELEASE_DIR}/obs-plugins/${ARCH_NAME}") + + # Copy to obs-studio dev environment for immediate testing + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy + "$" + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/obs-plugins/${ARCH_NAME}") + + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy + "$" + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/obs-plugins/${ARCH_NAME}") + + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E make_directory + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/data/obs-plugins/obs-websocket") + + COMMAND if $==1 ( + "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/data" + "${LIBOBS_INCLUDE_DIR}/../${OBS_BUILDDIR_ARCH}/rundir/$/data/obs-plugins/obs-websocket") + ) + # --- End of sub-section --- + +endif() +# --- End of section --- + +# --- Linux-specific build settings and tasks --- +if(UNIX AND NOT APPLE) + include(GNUInstallDirs) + + set_target_properties(obs-websocket PROPERTIES PREFIX "") + target_link_libraries(obs-websocket obs-frontend-api) + + file(GLOB locale_files data/locale/*.ini) + + set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS + OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_WRITE GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) + + if(${USE_UBUNTU_FIX}) + install(TARGETS obs-websocket LIBRARY + DESTINATION "/usr/lib/obs-plugins" + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ) + endif() + install(TARGETS obs-websocket LIBRARY + DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/obs-plugins" + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ) + + install(FILES ${locale_files} + DESTINATION "${CMAKE_INSTALL_FULL_DATAROOTDIR}/obs/obs-plugins/obs-websocket/locale") +endif() +# --- End of section --- + +# -- OS X specific build settings and tasks -- +if(APPLE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++ -fvisibility=default") + + set(CMAKE_SKIP_RPATH TRUE) + set_target_properties(obs-websocket PROPERTIES PREFIX "") + target_link_libraries(obs-websocket "${OBS_FRONTEND_LIB}") +endif() +# -- End of section -- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f94782b9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,82 @@ +# Contributing to obs-websocket + +## Translating obs-websocket to your language + +Localization happens on [Crowdin](https://crowdin.com/project/obs-websocket) + +## Branches + +**Development happens on `4.x-current`** + +## Writing code for obs-websocket + +### Code Formatting Guidelines + +* Function and variable names: snake_case for C names, camelCase for C++ method names + +* Request and Event names should use MixedCaps names + +* Request and Event json properties should use camelCase. For more detailed info on property naming, see [Google's JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml) + +* Code is indented with Tabs. Assume they are 8 columns wide + +* 80 columns max code width. (Docs can be larger) + +* New and updated requests/events must always come with accompanying documentation comments (see existing protocol elements for examples). +These are required to automatically generate the [protocol specification document](docs/generated/protocol.md). + +### Code Best-Practices + +* Favor return-early code and avoid wrapping huge portions of code in conditionals. As an example, this: +```cpp +if (success) { + return req->SendOKResponse(); +} else { + return req->SendErrorResponse("something went wrong"); +} +``` +is better like this: +```cpp +if (!success) { + return req->SendErrorResponse("something went wrong"); +} +return req->SendOKResponse(); +``` + +* Some example common response/request property names are: + * `sceneName` - The name of a scene + * `sourceName` - The name of a source + * `fromScene` - From a scene - scene name + +### Commit Guidelines + +* Commits follow the 50/72 standard: + * 50 characters max for the commit title (excluding scope name) + * One empty line after the title + * Description wrapped to 72 columns max width per line. + +* Commit titles: + * Use present tense + * Prefix the title with a "scope" name + * e.g: "CI: fix wrong behaviour when packaging for OS X" + * Typical scopes: CI, General, Requests, Events, Server + +**Example commit:** + +``` +Requests: Add GetTransitionPosition + +Adds a new request called `GetTransitionPosition` which gets the current +transition's state from 0.0f to 1.0f. Works with both auto and manual +transitions. +``` + +### Pull Requests + +* Pull Requests must never be based off your fork's main branch (in this case, `4.x-current`). + * Start your work in a newly named branch based on the upstream main one (e.g.: `feature/cool-new-feature`, `bugfix/fix-palakis-mistakes`, ...) + +* Only open a pull request if you are ready to show off your work. + +* If your work is not done yet, but for any reason you need to PR it (like collecting discussions, testing with CI, getting testers), + create it as a Draft Pull Request (open the little arrow menu next to the "Create pull request" button, then select "Create draft pull request"). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1b8a5cdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..06a28135 --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# obs-websocket + +

+ +

+ +WebSockets API for OBS Studio. + +[![Build Status](https://dev.azure.com/Palakis/obs-websocket/_apis/build/status/Palakis.obs-websocket?branchName=4.x-current)](https://dev.azure.com/Palakis/obs-websocket/_build/latest?definitionId=2&branchName=4.x-current) +[![CodeFactor](https://www.codefactor.io/repository/github/palakis/obs-websocket/badge)](https://www.codefactor.io/repository/github/palakis/obs-websocket) +[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/fold_left.svg?style=social&label=Follow%20%40LePalakis)](https://twitter.com/LePalakis) +[![Discord](https://img.shields.io/discord/715691013825364120.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/WBaSQ3A) +[![Financial Contributors on Open Collective](https://opencollective.com/obs-websocket/all/badge.svg?label=financial+contributors)](https://opencollective.com/obs-websocket) + +## Downloads + +Binaries for Windows, MacOS, and Linux are available in the [Releases](https://github.com/Palakis/obs-websocket/releases) section. + +### Homebrew + +If you're using MacOS you can use Homebrew for installation as well: + +```sh +brew install obs-websocket +``` + +## Using obs-websocket + +Here is a list of available web clients: (compatible with tablets and other touch interfaces) + +- [Niek/obs-web](https://github.com/Niek/obs-web) +- [t2t2/obs-tablet-remote](https://github.com/t2t2/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 +- Automate scene switching with a third-party program (e.g. : auto-pilot, foot pedal, ...) + +### For developers + +The server is a typical Websockets server running by default on port 4444 (the port number can be changed in the Settings dialog). +The protocol understood by the server is documented in [PROTOCOL.md](docs/generated/protocol.md). + +Here's a list of available language APIs for obs-websocket : +- Javascript (browser & nodejs): [obs-websocket-js](https://github.com/haganbmj/obs-websocket-js) by Brendan Hagan +- C#/VB.NET: [obs-websocket-dotnet](https://github.com/Palakis/obs-websocket-dotnet) +- 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 +- Python 3.6+ with asyncio: [simpleobsws](https://github.com/IRLToolkit/simpleobsws) by tt2468 +- Java 8+: [obs-websocket-java](https://github.com/Twasi/websocket-obs-java) by TwasiNET +- Java 11+: [obs-java-client](https://github.com/harm27/obs-java-client) by harm27 +- Golang: [go-obs-websocket](https://github.com/christopher-dG/go-obs-websocket) by Chris de Graaf +- Rust: [obws](https://github.com/dnaka91/obws) by dnaka91 +- HTTP API: [obs-websocket-http](https://github.com/IRLToolkit/obs-websocket-http) by tt2468 +- CLI: [obs-cli](https://github.com/leafac/obs-cli) by leafac + +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 a message in `#project-showoff` in the [discord server!](https://discord.gg/WBaSQ3A) + +### Securing obs-websocket (via TLS/SSL) + +If you are intending to use obs-websocket outside of a LAN environment, it is highly recommended to secure the connection using a tunneling service. + +See the SSL [tunnelling guide](SSL-TUNNELLING.md) for easy instructions on how to encrypt your websocket connection. + +## Compiling obs-websocket + +See the [build instructions](BUILDING.md). + +## Contributing + +See [the contributing document](/CONTRIBUTING.md) + +## Translations + +**Your help is welcome on translations.** + +Please join the localization project on [Crowdin](https://crowdin.com/project/obs-websocket) + +## Special thanks + +Thank you so much to all of the contibutors [(here)](https://github.com/Palakis/obs-websocket/graphs/contributors) for your amazing help. + +And also: special thanks to supporters of the project! + +## Supporters + +These supporters have contributed financially to the project and made possible the addition of several features into obs-websocket. Many thanks to them! + +--- + +[Support Class](http://supportclass.net) designs and develops professional livestreams, with services ranging from broadcast graphics design and integration to event organization, along many other skills. + +[![Support Class](.github/images/supportclass_logo_blacktext.png)](http://supportclass.net) + +--- + +[MediaUnit](http://www.mediaunit.no) is a Norwegian media company developing products and services for the media industry, primarly focused on web and events. + +[![MediaUnit](.github/images/mediaunit_logo_black.png)](http://www.mediaunit.no/) + +## Contributors + +### Code Contributors + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + + +### Financial Contributors + +Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/obs-websocket/contribute)] + +#### Individuals + + + +#### Organizations + +Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/obs-websocket/contribute)] + + + + + + + + + + + diff --git a/SSL-TUNNELLING.md b/SSL-TUNNELLING.md new file mode 100644 index 00000000..b841e711 --- /dev/null +++ b/SSL-TUNNELLING.md @@ -0,0 +1,45 @@ +# Connecting over a TLS/secure connection (or remotely) + +If you want to expose the WebSocket server of obs-websocket over a secure TLS connection (or to connect remotely), the easiest approach is to use a localhost tunneling service like [ngrok](https://ngrok.com/) or [pagekite](https://pagekite.net/). + +**Before doing this, secure the WebSocket server first by enabling authentication with a strong password!** + +**Please bear in mind that doing this will expose your OBS instance to the open Internet and the security risks it implies. *You've been warned!*** + + +## ngrok + +[Install the ngrok CLI tool](https://ngrok.com/download) on a linux OS, then start ngrok bound to port 4444 like this: + +```bash +ngrok http 4444 +``` + +The ngrok command will output something like this: + +```text +ngrok by @inconshreveable + +Tunnel Status online +Version 2.0/2.0 +Web Interface http://127.0.0.1:4040 +Forwarding http://TUNNEL_ID.ngrok.io -> localhost:4444 +Forwarding https://TUNNEL_ID.ngrok.io -> localhost:4444 +``` + +Where `TUNNEL_ID` is, as the name implies, the unique name of your ngrok tunnel. You'll get a new one every time you start ngrok. + +Then, use `wss://TUNNEL_ID.ngrok.io` to connect to obs-websocket over TLS. + +See the [ngrok documentation](https://ngrok.com/docs) for more tunneling options and settings. + + +## PageKite + +[Install the PageKite CLI tool](http://pagekite.net/downloads), then start PageKite bound to port 4444 like this (replace NAME with one of your choosing): + +```bash +python pagekite.py 4444 NAME.pagekite.me +``` + +Then, use `wss://NAME.pagekite.me` to connect to obs-websocket over TLS. diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..a42fc561 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,183 @@ +variables: + isReleaseMode: ${{ startsWith(variables['Build.SourceBranch'], 'refs/tags/') }} + +trigger: + branches: + include: + - '4.x-current' + tags: + include: + - '*' + +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 + + - script: ./CI/generate-docs.sh + displayName: 'Generate docs' + env: + CHECKOUT_REF: $(Build.SourceBranch) + GH_TOKEN: $(GithubToken) + +- 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-qt-win.cmd + displayName: 'Install Qt' + env: + QtBaseDir: $(QtBaseDir) + + - task: Cache@2 + displayName: Restore cached OBS Studio dependencies + inputs: + key: 'obsdeps | "$(Agent.OS)"' + restoreKeys: | + obsdeps | "$(Agent.OS)" + path: $(DepsBasePath) + + - script: ./CI/download-obs-deps.cmd + displayName: 'Download OBS Studio dependencies' + + - task: Cache@2 + displayName: Restore cached OBS Studio builds + inputs: + key: 'obs | "$(Agent.OS)"' + restoreKeys: | + obs | "$(Agent.OS)" + path: $(OBSPath) + + - script: ./CI/prepare-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' + + - task: InstallAppleCertificate@2 + displayName: 'Install release signing certificates' + condition: eq(variables['isReleaseMode'], true) + inputs: + certSecureFile: 'Certificates.p12' + certPwd: $(secrets.macOS.certificatesImportPassword) + + - script: ./CI/package-macos.sh + displayName: 'Package obs-websocket' + env: + RELEASE_MODE: $(isReleaseMode) + CODE_SIGNING_IDENTITY: $(secrets.macOS.codeSigningIdentity) + INSTALLER_SIGNING_IDENTITY: $(secrets.macOS.installerSigningIdentity) + AC_USERNAME: $(secrets.macOS.notarization.username) + AC_PASSWORD: $(secrets.macOS.notarization.password) + AC_PROVIDER_SHORTNAME: $(secrets.macOS.notarization.providerShortName) + + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: './release' + artifactName: 'macos_build' diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..e09f5f04 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /data/locale/en-US.ini + translation: /data/locale/%locale%.ini diff --git a/data/locale/ar-SA.ini b/data/locale/ar-SA.ini new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/data/locale/ar-SA.ini @@ -0,0 +1 @@ + diff --git a/data/locale/de-DE.ini b/data/locale/de-DE.ini new file mode 100644 index 00000000..d2f5efc9 --- /dev/null +++ b/data/locale/de-DE.ini @@ -0,0 +1,18 @@ +OBSWebsocket.Settings.DialogTitle="WebSockets-Servereinstellungen" +OBSWebsocket.Settings.ServerEnable="WebSockets-Server aktivieren" +OBSWebsocket.Settings.ServerPort="Server-Port" +OBSWebsocket.Settings.AuthRequired="Authentifizierung aktivieren" +OBSWebsocket.Settings.Password="Passwort" +OBSWebsocket.Settings.LockToIPv4="Nur IPv4 verwenden (deaktiviert IPv6)" +OBSWebsocket.Settings.DebugEnable="Debug-Protokollierung aktivieren" +OBSWebsocket.Settings.AlertsEnable="Infobereichbenachrichtigungen aktivieren" +OBSWebsocket.NotifyConnect.Title="Neue Websocket-Verbindung" +OBSWebsocket.NotifyConnect.Message="Client %1 verbunden" +OBSWebsocket.NotifyDisconnect.Title="Websocket-Client getrennt" +OBSWebsocket.NotifyDisconnect.Message="Client %1 getrennt" +OBSWebsocket.Server.StartFailed.Title="Websocket-Serverfehler" +OBSWebsocket.Server.StartFailed.Message="Der WebSocket-Server konnte nicht gestartet werden, mögliche Gründe:\n - Der TCP-Port %1 wird möglicherweise gerade von einem anderen Programm verwendet. Versuchen Sie einen anderen Port in den Websocket-Servereinstellungen zu setzen oder alle Programme zu beenden, die den Port möglicherweise verwenden.\n - Fehler: %2" +OBSWebsocket.ProfileChanged.Started="WebSockets-Server in diesem Profil aktiviert. Server gestartet." +OBSWebsocket.ProfileChanged.Stopped="WebSockets-Server in diesem Profil deaktiviert. Server gestoppt." +OBSWebsocket.ProfileChanged.Restarted="WebSockets-Server in diesem Profil geändert. Server startet neu." + diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini new file mode 100644 index 00000000..0382082f --- /dev/null +++ b/data/locale/en-US.ini @@ -0,0 +1,21 @@ +OBSWebsocket.Settings.DialogTitle="WebSockets Server Settings" +OBSWebsocket.Settings.ServerEnable="Enable WebSockets server" +OBSWebsocket.Settings.ServerPort="Server Port" +OBSWebsocket.Settings.AuthRequired="Enable authentication" +OBSWebsocket.Settings.Password="Password" +OBSWebsocket.Settings.LockToIPv4="Lock server to only using IPv4" +OBSWebsocket.Settings.DebugEnable="Enable debug logging" +OBSWebsocket.Settings.AlertsEnable="Enable System Tray Alerts" +OBSWebsocket.Settings.AuthDisabledWarning="Running obs-websocket with authentication disabled is not recommended, as it allows attackers to easily collect sensetive data. Are you sure you want to proceed?" +OBSWebsocket.NotifyConnect.Title="New WebSocket connection" +OBSWebsocket.NotifyConnect.Message="Client %1 connected" +OBSWebsocket.NotifyDisconnect.Title="WebSocket client disconnected" +OBSWebsocket.NotifyDisconnect.Message="Client %1 disconnected" +OBSWebsocket.Server.StartFailed.Title="WebSockets Server failure" +OBSWebsocket.Server.StartFailed.Message="The WebSockets server failed to start, maybe because:\n - TCP port %1 may currently be in use elsewhere on this system, possibly by another application. Try setting a different TCP port in the WebSocket server settings, or stop any application that could be using this port.\n - Error message: %2" +OBSWebsocket.ProfileChanged.Started="WebSockets server enabled in this profile. Server started." +OBSWebsocket.ProfileChanged.Stopped="WebSockets server disabled in this profile. Server stopped." +OBSWebsocket.ProfileChanged.Restarted="WebSockets server port changed in this profile. Server restarted." +OBSWebsocket.InitialPasswordSetup.Title="obs-websocket - Server Password Configuration" +OBSWebsocket.InitialPasswordSetup.Text="It looks like you are running obs-websocket for the first time. Do you want to configure a password now for the WebSockets server? Setting a password is highly recommended." +OBSWebsocket.InitialPasswordSetup.DismissedText="You can configure a server password later in the WebSockets Server Settings. (Under the Tools menu of OBS Studio)" diff --git a/data/locale/es-ES.ini b/data/locale/es-ES.ini new file mode 100644 index 00000000..7c82ef2b --- /dev/null +++ b/data/locale/es-ES.ini @@ -0,0 +1,16 @@ +OBSWebsocket.Settings.DialogTitle="Configuración del servidor WebSocket" +OBSWebsocket.Settings.ServerEnable="Habilitar el servidor WebSockets" +OBSWebsocket.Settings.ServerPort="Puerto del Servidor" +OBSWebsocket.Settings.AuthRequired="Habilitar autenticación" +OBSWebsocket.Settings.Password="Contraseña" +OBSWebsocket.Settings.DebugEnable="Habilitar registro de depuración" +OBSWebsocket.Settings.AlertsEnable="Habilitar alertas en la bandeja de sistema" +OBSWebsocket.NotifyConnect.Title="Nueva conexión WebSocket" +OBSWebsocket.NotifyConnect.Message="Cliente %1 conectado" +OBSWebsocket.NotifyDisconnect.Title="Cliente WebSocket desconectado" +OBSWebsocket.NotifyDisconnect.Message="Cliente %1 desconectado" +OBSWebsocket.Server.StartFailed.Title="Falla en el servidor WebSockets" +OBSWebsocket.ProfileChanged.Started="El servidor WebSocket esta habilitado en este perfil. El servidor ha iniciado." +OBSWebsocket.ProfileChanged.Stopped="Servidor WebSockets deshabilitado en este perfil. Servidor detenido." +OBSWebsocket.ProfileChanged.Restarted="Puerto del servidor WebSockets cambiado en este perfil. Servidor reiniciado." + diff --git a/data/locale/fr-FR.ini b/data/locale/fr-FR.ini new file mode 100644 index 00000000..ed8fb085 --- /dev/null +++ b/data/locale/fr-FR.ini @@ -0,0 +1,16 @@ +OBSWebsocket.Settings.DialogTitle="Paramètres du serveur WebSockets" +OBSWebsocket.Settings.ServerEnable="Activer le serveur WebSocket" +OBSWebsocket.Settings.ServerPort="Port du serveur" +OBSWebsocket.Settings.AuthRequired="Activer l'authentification" +OBSWebsocket.Settings.Password="Mot de passe" +OBSWebsocket.Settings.DebugEnable="Débogage dans le fichier journal" +OBSWebsocket.Settings.AlertsEnable="Notifications de connexion/déconnexion" +OBSWebsocket.NotifyConnect.Title="Nouvelle connexion WebSocket" +OBSWebsocket.NotifyConnect.Message="Le client %1 s'est connecté" +OBSWebsocket.NotifyDisconnect.Title="Déconnexion WebSocket" +OBSWebsocket.NotifyDisconnect.Message="Le client %1 s'est déconnecté" +OBSWebsocket.Server.StartFailed.Title="Impossible de démarrer le serveur WebSockets" +OBSWebsocket.ProfileChanged.Started="Serveur WebSockets actif dans ce profil." +OBSWebsocket.ProfileChanged.Stopped="Serveur WebSockets désactivé dans ce profil." +OBSWebsocket.ProfileChanged.Restarted="Le port actuel diffère du port configuré dans ce profil. Serveur WebSockets redémarré." + diff --git a/data/locale/hi-IN.ini b/data/locale/hi-IN.ini new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/data/locale/hi-IN.ini @@ -0,0 +1 @@ + diff --git a/data/locale/it-IT.ini b/data/locale/it-IT.ini new file mode 100644 index 00000000..4142ed47 --- /dev/null +++ b/data/locale/it-IT.ini @@ -0,0 +1,18 @@ +OBSWebsocket.Settings.DialogTitle="Impostazioni del server di WebSocket" +OBSWebsocket.Settings.ServerEnable="Abilitare il server WebSockets" +OBSWebsocket.Settings.ServerPort="Porta del server" +OBSWebsocket.Settings.AuthRequired="Abilitare l'autenticazione" +OBSWebsocket.Settings.Password="Password" +OBSWebsocket.Settings.LockToIPv4="Blocca il server per usare solo IPv4" +OBSWebsocket.Settings.DebugEnable="Attivare la registrazione di debug" +OBSWebsocket.Settings.AlertsEnable="Attivare gli avvisi di vassoio di sistema" +OBSWebsocket.NotifyConnect.Title="Nuova connessione WebSocket" +OBSWebsocket.NotifyConnect.Message="%1 cliente collegato" +OBSWebsocket.NotifyDisconnect.Title="WebSocket cliente disconnesso" +OBSWebsocket.NotifyDisconnect.Message="%1 cliente disconnesso" +OBSWebsocket.Server.StartFailed.Title="Errore del WebSocket Server" +OBSWebsocket.Server.StartFailed.Message="L'avvio del server WebSockets è fallito, forse perché:\n - La porta TCP %1 può attualmente essere in uso altrove su questo sistema, forse da un'altra applicazione. Provare a impostare una porta TCP diversa nelle impostazioni del server WebSocket o interrompere qualsiasi applicazione che potrebbe utilizzare questa porta.\n - Messaggio di errore: %2" +OBSWebsocket.ProfileChanged.Started="Server WebSockets abilitato in questo profilo. Il server è avviato." +OBSWebsocket.ProfileChanged.Stopped="Server WebSocket disabilitato in questo profilo. Server interrotto." +OBSWebsocket.ProfileChanged.Restarted="La porta del server WebSocket è stata modificata in questo profilo. Il server è stato riavviato." + diff --git a/data/locale/ja-JP.ini b/data/locale/ja-JP.ini new file mode 100644 index 00000000..4218cdd7 --- /dev/null +++ b/data/locale/ja-JP.ini @@ -0,0 +1,16 @@ +OBSWebsocket.Settings.DialogTitle="Websocket サーバー設定" +OBSWebsocket.Settings.ServerEnable="WebSockets サーバーを有効にする" +OBSWebsocket.Settings.ServerPort="サーバーポート" +OBSWebsocket.Settings.AuthRequired="認証を有効にする" +OBSWebsocket.Settings.Password="パスワード" +OBSWebsocket.Settings.DebugEnable="デバッグログを有効にする" +OBSWebsocket.Settings.AlertsEnable="システムトレイ通知を有効にする" +OBSWebsocket.NotifyConnect.Title="新しい WebSocket 接続" +OBSWebsocket.NotifyConnect.Message="接続されているクライアント %1" +OBSWebsocket.NotifyDisconnect.Title="WebSocket クライアントが切断されました" +OBSWebsocket.NotifyDisconnect.Message="切断されたクライアント %1" +OBSWebsocket.Server.StartFailed.Title="WebSockets サーバー障害" +OBSWebsocket.ProfileChanged.Started="このプロファイルでWebSocketサーバが有効になりました。サーバを起動しました。" +OBSWebsocket.ProfileChanged.Stopped="このプロファイルでWebSocketサーバが無効になりました。サーバを停止しました。" +OBSWebsocket.ProfileChanged.Restarted="このプロファイルでWebSocketサーバの通信ポートが変更されました。サーバを再起動しました。" + diff --git a/data/locale/ko-KR.ini b/data/locale/ko-KR.ini new file mode 100644 index 00000000..0154da74 --- /dev/null +++ b/data/locale/ko-KR.ini @@ -0,0 +1,18 @@ +OBSWebsocket.Settings.DialogTitle="웹소켓 서버 설정" +OBSWebsocket.Settings.ServerEnable="웹소켓 서버 활성화" +OBSWebsocket.Settings.ServerPort="서버 포트" +OBSWebsocket.Settings.AuthRequired="인증 활성화" +OBSWebsocket.Settings.Password="비밀번호" +OBSWebsocket.Settings.LockToIPv4="IPv4만 이용하여 서버를 연결" +OBSWebsocket.Settings.DebugEnable="디버그 로깅 활성화" +OBSWebsocket.Settings.AlertsEnable="시스템 트레이 알림 활성화" +OBSWebsocket.NotifyConnect.Title="새로운 웹소켓 연결" +OBSWebsocket.NotifyConnect.Message="클라이언트 %1 가 연결되었습니다" +OBSWebsocket.NotifyDisconnect.Title="웹소켓 클라이언트가 연결 해제되었습니다" +OBSWebsocket.NotifyDisconnect.Message="클라이언트 %1 가 연결이 해제되었습니다" +OBSWebsocket.Server.StartFailed.Title="웹소켓 서버 시작이 실패하였습니다" +OBSWebsocket.Server.StartFailed.Message="웹소켓 서버가 시작되지 못했습니다. \n 시스템 상의 다른 프로그램이 TCP 포트 %1을 사용 중인 것으로 보입니다. 다른 TCP 포트를 사용하거나, 해당 포트를 사용 중인 프로그램을 종료하고 다시 시도해주세요. \n 에러 메시지: %2" +OBSWebsocket.ProfileChanged.Started="프로파일 설정에 따라 웹소켓 서버가 활성화되었습니다. 서버가 시작되었습니다." +OBSWebsocket.ProfileChanged.Stopped="프로파일 설정에 따라 웹소켓 서버가 비활성화되었습니다. 서버가 중지되었습니다." +OBSWebsocket.ProfileChanged.Restarted="프로파일 설정에 따라 웹소켓 포트가 변경되었습니다. 서버가 재시작되었습니다." + diff --git a/data/locale/nl-NL.ini b/data/locale/nl-NL.ini new file mode 100644 index 00000000..29e72d61 --- /dev/null +++ b/data/locale/nl-NL.ini @@ -0,0 +1,18 @@ +OBSWebsocket.Settings.DialogTitle="WebSockets Server Instellingen" +OBSWebsocket.Settings.ServerEnable="WebSockets server inschakelen" +OBSWebsocket.Settings.ServerPort="Serverpoort" +OBSWebsocket.Settings.AuthRequired="Verificatie inschakelen" +OBSWebsocket.Settings.Password="Wachtwoord" +OBSWebsocket.Settings.LockToIPv4="Server vergrendelen om alleen IPv4 te gebruiken" +OBSWebsocket.Settings.DebugEnable="Activeer debug logs" +OBSWebsocket.Settings.AlertsEnable="Systemvak waarschuwingen inschakelen" +OBSWebsocket.NotifyConnect.Title="Nieuwe WebSocket verbinding" +OBSWebsocket.NotifyConnect.Message="Client %1 verbonden" +OBSWebsocket.NotifyDisconnect.Title="WebSocket client connectie verbroken" +OBSWebsocket.NotifyDisconnect.Message="Client %1 losgekoppeld" +OBSWebsocket.Server.StartFailed.Title="Fout in WebSocket server" +OBSWebsocket.Server.StartFailed.Message="De obs-websocket server kan niet worden gestart, misschien omdat: \n - TCP-poort %1 momenteel elders wordt gebruikt op dit systeem, eventueel door een andere toepassing. Probeer een andere TCP-poort in te stellen in de WebSocket Server-instellingen of stop elke toepassing die deze poort zou kunnen gebruiken.\n Een onbekende Netwerkfout op uw systeem. Probeer het opnieuw door de instellingen te wijzigen, OBS te herstarten of uw systeem te herstarten." +OBSWebsocket.ProfileChanged.Started="WebSockets server ingeschakeld in dit profiel. Server gestart." +OBSWebsocket.ProfileChanged.Stopped="WebSockets server uitgeschakeld in dit profiel. Server is gestopt." +OBSWebsocket.ProfileChanged.Restarted="WebSockets server poort is veranderd in dit profiel. Server is herstart." + diff --git a/data/locale/pl-PL.ini b/data/locale/pl-PL.ini new file mode 100644 index 00000000..8b434cda --- /dev/null +++ b/data/locale/pl-PL.ini @@ -0,0 +1,15 @@ +OBSWebsocket.Settings.DialogTitle="Ustawienia serwera WebSockets" +OBSWebsocket.Settings.ServerEnable="Włącz serwer WebSockets" +OBSWebsocket.Settings.ServerPort="Port serwera" +OBSWebsocket.Settings.AuthRequired="Wymagaj uwierzytelniania" +OBSWebsocket.Settings.Password="Hasło" +OBSWebsocket.Settings.LockToIPv4="Zablokuj serwer tylko za pomocą IPv4" +OBSWebsocket.Settings.DebugEnable="Włącz rejestrowanie debugowania" +OBSWebsocket.Settings.AlertsEnable="Włącz powiadomienia w zasobniku systemowym" +OBSWebsocket.NotifyConnect.Title="Nowe połączenie WebSocket" +OBSWebsocket.NotifyConnect.Message="Klient %1 połączony" +OBSWebsocket.NotifyDisconnect.Title="Klient WebSocket odłączony" +OBSWebsocket.NotifyDisconnect.Message="Klient %1 rozłączony" +OBSWebsocket.Server.StartFailed.Title="Awaria serwera WebSockets" +OBSWebsocket.ProfileChanged.Started="Serwer WebSockets włączony w tym profilu. Serwer uruchomiony." + diff --git a/data/locale/pt-PT.ini b/data/locale/pt-PT.ini new file mode 100644 index 00000000..99de1931 --- /dev/null +++ b/data/locale/pt-PT.ini @@ -0,0 +1,18 @@ +OBSWebsocket.Settings.DialogTitle="Configurações do servidor de WebSockets" +OBSWebsocket.Settings.ServerEnable="Habilitar servidor de WebSockets" +OBSWebsocket.Settings.ServerPort="Porta do Servidor" +OBSWebsocket.Settings.AuthRequired="Activar autenticação" +OBSWebsocket.Settings.Password="Palavra passe" +OBSWebsocket.Settings.LockToIPv4="Forçar apenas o uso de IPv4 (desabilitar IPv6)" +OBSWebsocket.Settings.DebugEnable="Habilitar registro de debug" +OBSWebsocket.Settings.AlertsEnable="Ativar Alertas da bandeja do sistema" +OBSWebsocket.NotifyConnect.Title="Nova conexão WebSocket" +OBSWebsocket.NotifyConnect.Message="Cliente %1 conectado" +OBSWebsocket.NotifyDisconnect.Title="WebSocket cliente desconectado" +OBSWebsocket.NotifyDisconnect.Message="Cliente %1 desconectado" +OBSWebsocket.Server.StartFailed.Title="Falha do servidor de WebSocket" +OBSWebsocket.Server.StartFailed.Message="O servidor de WebSockets falhou ao iniciar, possivelmente porque:\n - A porta TCP %1 pode já estar em uso em algum outro lugar neste sistema, possivelmente por outro aplicativo. Tente definir uma porta TCP diferente nas configurações do servidor de WebSockets, ou pare qualquer aplicativo que possa estar usando essa porta.\n - Mensagem de erro: %2" +OBSWebsocket.ProfileChanged.Started="Servidor de WebSockets habilitado nesse perfil. Servidor iniciado." +OBSWebsocket.ProfileChanged.Stopped="Servidor de WebSockets desabilitado nesse perfil. Servidor parado." +OBSWebsocket.ProfileChanged.Restarted="Porta do servidor de WebSockets foi alterada neste perfil. Servidor reiniciado." + diff --git a/data/locale/ru-RU.ini b/data/locale/ru-RU.ini new file mode 100644 index 00000000..efbf1f46 --- /dev/null +++ b/data/locale/ru-RU.ini @@ -0,0 +1,18 @@ +OBSWebsocket.Settings.DialogTitle="Настройки сервера WebSockets" +OBSWebsocket.Settings.ServerEnable="Включить сервер WebSockets" +OBSWebsocket.Settings.ServerPort="Порт сервера" +OBSWebsocket.Settings.AuthRequired="Включить авторизацию" +OBSWebsocket.Settings.Password="Пароль" +OBSWebsocket.Settings.LockToIPv4="Блокировка сервера только с использованием IPv4" +OBSWebsocket.Settings.DebugEnable="Включить ведение журнала отладки" +OBSWebsocket.Settings.AlertsEnable="Включить оповещения в системном трее" +OBSWebsocket.NotifyConnect.Title="Новое соединение WebSocket" +OBSWebsocket.NotifyConnect.Message="Клиент %1 подключен" +OBSWebsocket.NotifyDisconnect.Title="Клиент WebSocket отключён" +OBSWebsocket.NotifyDisconnect.Message="Клиент %1 отключен" +OBSWebsocket.Server.StartFailed.Title="Сбой сервера WebSockets" +OBSWebsocket.Server.StartFailed.Message="Сервер WebSockets не запустился, возможно, потому, что:\n-TCP-порт %1 в настоящее время может использоваться в другом месте этой системы, возможно, другим приложением. Попробуйте установить другой TCP-порт в настройках сервера WebSocket или остановить любое приложение, которое может использовать этот порт.\n-сообщение об ошибке: %2" +OBSWebsocket.ProfileChanged.Started="Сервер WebSockets включен в этом профиле. Сервер запущен." +OBSWebsocket.ProfileChanged.Stopped="Сервер WebSockets отключен в этом профиле. Сервер остановлен." +OBSWebsocket.ProfileChanged.Restarted="Порт сервера WebSockets изменен в этом профиле. Сервер перезапущен." + diff --git a/data/locale/zh-CN.ini b/data/locale/zh-CN.ini new file mode 100644 index 00000000..cab511cc --- /dev/null +++ b/data/locale/zh-CN.ini @@ -0,0 +1,16 @@ +OBSWebsocket.Settings.DialogTitle="WebSockets 服务器设置" +OBSWebsocket.Settings.ServerEnable="启用 WebSockets 服务器" +OBSWebsocket.Settings.ServerPort="服务器端口" +OBSWebsocket.Settings.AuthRequired="启用身份验证" +OBSWebsocket.Settings.Password="密码" +OBSWebsocket.Settings.DebugEnable="启用调试日志" +OBSWebsocket.Settings.AlertsEnable="启用系统托盘通知" +OBSWebsocket.NotifyConnect.Title="新 WebSocket 连接" +OBSWebsocket.NotifyConnect.Message="客户端 %1 已连接" +OBSWebsocket.NotifyDisconnect.Title="WebSocket 客户端已断开" +OBSWebsocket.NotifyDisconnect.Message="客户端 %1 已断开连接" +OBSWebsocket.Server.StartFailed.Title="WebSockets 服务器错误" +OBSWebsocket.ProfileChanged.Started="此配置文件中启用了 WebSockets 服务器。服务器已启动。" +OBSWebsocket.ProfileChanged.Stopped="此配置文件中禁用了 WebSockets 服务器。服务器已停止。" +OBSWebsocket.ProfileChanged.Restarted="此配置文件中的 WebSockets 服务器端口已更改。服务器已重新启动。" + diff --git a/data/locale/zh-TW.ini b/data/locale/zh-TW.ini new file mode 100644 index 00000000..41cc0591 --- /dev/null +++ b/data/locale/zh-TW.ini @@ -0,0 +1,9 @@ +OBSWebsocket.Settings.ServerPort="伺服器連接埠" +OBSWebsocket.Settings.DebugEnable="啟用除錯日誌" +OBSWebsocket.Settings.AlertsEnable="啟用系統列通知" +OBSWebsocket.NotifyConnect.Title="新的 WebSocket 連線" +OBSWebsocket.NotifyConnect.Message="客戶端 %1 已連線" +OBSWebsocket.NotifyDisconnect.Title="WebSocket 客戶端已離線" +OBSWebsocket.NotifyDisconnect.Message="客戶端 %1 已離線" +OBSWebsocket.Server.StartFailed.Title="WebSocket 伺服器錯誤" + diff --git a/deps/asio b/deps/asio new file mode 160000 index 00000000..b73dc1d2 --- /dev/null +++ b/deps/asio @@ -0,0 +1 @@ +Subproject commit b73dc1d2c0ecb9452a87c26544d7f71e24342df6 diff --git a/deps/websocketpp b/deps/websocketpp new file mode 160000 index 00000000..c6d7e295 --- /dev/null +++ b/deps/websocketpp @@ -0,0 +1 @@ +Subproject commit c6d7e295bf5a0ab9b5f896720cc1a0e0fdc397a7 diff --git a/docs/.editorconfig b/docs/.editorconfig new file mode 100644 index 00000000..2e0a7808 --- /dev/null +++ b/docs/.editorconfig @@ -0,0 +1,11 @@ +[*] +end_of_line = lf +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md, *.mustache] +trim_trailing_whitespace = false +insert_final_newline = false diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..8c549731 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +node_modules +logs +*.log +npm-debug.log* diff --git a/docs/.npmrc b/docs/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/docs/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..265cbd22 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +## Installation + +Install node and update npm if necessary. + +```sh +cd obs-websocket/docs +npm install +``` + +## Build + +```sh +# Just extract the comments. +npm run comments + +# Just render the markdown. +npm run docs + +# Do both comments and markdown. +npm run build +``` diff --git a/docs/comments.js b/docs/comments.js new file mode 100644 index 00000000..c396c5da --- /dev/null +++ b/docs/comments.js @@ -0,0 +1,104 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const parseComments = require('parse-comments'); +const config = require('./config.json'); + +/** + * Read each file and call `parse-comments` on it. + * + * @param {String|Array} `files` List of file paths to read from. + * @return {Object|Array} Array of `parse-comments` objects. + */ +const parseFiles = files => { + let response = []; + files.forEach(file => { + const f = fs.readFileSync(file, 'utf8').toString(); + response = response.concat(parseComments(f)); + }); + + return response; +}; + +/** + * Filters/sorts the results from `parse-comments`. + * @param {Object|Array} `comments` Array of `parse-comments` objects. + * @return {Object} Filtered comments sorted by `@api` and `@category`. + */ +const processComments = comments => { + let sorted = {}; + let errors = []; + + comments.forEach(comment => { + if (comment.typedef) { + comment.comment = undefined; + comment.context = undefined; + sorted['typedefs'] = sorted['typedefs'] || []; + sorted['typedefs'].push(comment); + return; + } + + if (typeof comment.api === 'undefined') return; + let validationFailures = validateComment(comment); + + if (validationFailures) { + errors.push(validationFailures); + } + + // Store the object based on its api (ie. requests, events) and category (ie. general, scenes, etc). + comment.category = comment.category || 'miscellaneous'; + + // Remove some unnecessary properties to avoid result differences in travis. + comment.comment = undefined; + comment.context = undefined; + + // Create an entry in sorted for the api/category if one does not exist. + sorted[comment.api] = sorted[comment.api] || {}; + sorted[comment.api][comment.category] = sorted[comment.api][comment.category] || []; + + // Store the comment in the appropriate api/category. + sorted[comment.api][comment.category].push(comment); + }); + + if (errors.length) { + throw JSON.stringify(errors, null, 2); + } + + return sorted; +}; + +// Rudimentary validation of documentation content, returns an error object or undefined. +const validateComment = comment => { + let errors = []; + [].concat(comment.params).concat(comment.returns).filter(Boolean).forEach(param => { + if (typeof param.name !== 'string' || param.name === '') { + errors.push({ + description: `Invalid param or return value name`, + param: param + }); + } + + if (typeof param.type !== 'string' || param.type === '') { + errors.push({ + description: `Invalid param or return value type`, + param: param + }); + } + }); + + if (errors.length) { + return { + errors: errors, + fullContext: Object.assign({}, comment) + }; + } +}; + +const files = glob.sync(config.srcGlob); +const comments = processComments(parseFiles(files)); + +if (!fs.existsSync(config.outDirectory)){ + fs.mkdirSync(config.outDirectory); +} + +fs.writeFileSync(path.join(config.outDirectory, 'comments.json'), JSON.stringify(comments, null, 2)); diff --git a/docs/config.json b/docs/config.json new file mode 100644 index 00000000..ab2a2391 --- /dev/null +++ b/docs/config.json @@ -0,0 +1,5 @@ +{ + "srcGlob": "./../src/**/*.@(cpp|h)", + "srcTemplate": "./protocol.hbs", + "outDirectory": "./generated" +} diff --git a/docs/docs.js b/docs/docs.js new file mode 100644 index 00000000..b66cb0f3 --- /dev/null +++ b/docs/docs.js @@ -0,0 +1,37 @@ +const fs = require('fs'); +const path = require('path'); +const toc = require('markdown-toc'); +const handlebars = require('handlebars'); +const config = require('./config.json'); + +const helpers = require('handlebars-helpers')({ + handlebars: handlebars +}); + +// Allows pipe characters to be used within markdown tables. +handlebars.registerHelper('depipe', (text) => { + return typeof text === 'string' ? text.replace('|', '\\|') : text; +}); + +const insertHeader = (text) => { + return '\n\n' + text; +}; + +/** + * Writes `protocol.md` using `protocol.mustache`. + * + * @param {Object} `data` Data to assign to the mustache template. + */ +const generateProtocol = (templatePath, data) => { + const template = fs.readFileSync(templatePath).toString(); + const generated = handlebars.compile(template)(data); + return insertHeader(toc.insert(generated)); +}; + +if (!fs.existsSync(config.outDirectory)){ + fs.mkdirSync(config.outDirectory); +} + +const comments = fs.readFileSync(path.join(config.outDirectory, 'comments.json'), 'utf8'); +const markdown = generateProtocol(config.srcTemplate, JSON.parse(comments)); +fs.writeFileSync(path.join(config.outDirectory, 'protocol.md'), markdown); diff --git a/docs/generated/comments.json b/docs/generated/comments.json new file mode 100644 index 00000000..e3922a8e --- /dev/null +++ b/docs/generated/comments.json @@ -0,0 +1,11161 @@ +{ + "typedefs": [ + { + "subheads": [], + "typedef": "{Object} `SceneItem` An OBS Scene Item.", + "property": [ + "{Number} `cy`", + "{Number} `cx`", + "{Number} `alignment` The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis.", + "{String} `name` The name of this Scene Item.", + "{int} `id` Scene item ID", + "{Boolean} `render` Whether or not this Scene Item is set to \"visible\".", + "{Boolean} `muted` Whether or not this Scene Item is muted.", + "{Boolean} `locked` Whether or not this Scene Item is locked and can't be moved around", + "{Number} `source_cx`", + "{Number} `source_cy`", + "{String} `type` Source type. Value is one of the following: \"input\", \"filter\", \"transition\", \"scene\" or \"unknown\"", + "{Number} `volume`", + "{Number} `x`", + "{Number} `y`", + "{String (optional)} `parentGroupName` Name of the item's parent (if this item belongs to a group)", + "{Array (optional)} `groupChildren` List of children (if this item is a group)" + ], + "properties": [ + { + "type": "Number", + "name": "cy", + "description": "" + }, + { + "type": "Number", + "name": "cx", + "description": "" + }, + { + "type": "Number", + "name": "alignment", + "description": "The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis." + }, + { + "type": "String", + "name": "name", + "description": "The name of this Scene Item." + }, + { + "type": "int", + "name": "id", + "description": "Scene item ID" + }, + { + "type": "Boolean", + "name": "render", + "description": "Whether or not this Scene Item is set to \"visible\"." + }, + { + "type": "Boolean", + "name": "muted", + "description": "Whether or not this Scene Item is muted." + }, + { + "type": "Boolean", + "name": "locked", + "description": "Whether or not this Scene Item is locked and can't be moved around" + }, + { + "type": "Number", + "name": "source_cx", + "description": "" + }, + { + "type": "Number", + "name": "source_cy", + "description": "" + }, + { + "type": "String", + "name": "type", + "description": "Source type. Value is one of the following: \"input\", \"filter\", \"transition\", \"scene\" or \"unknown\"" + }, + { + "type": "Number", + "name": "volume", + "description": "" + }, + { + "type": "Number", + "name": "x", + "description": "" + }, + { + "type": "Number", + "name": "y", + "description": "" + }, + { + "type": "String (optional)", + "name": "parentGroupName", + "description": "Name of the item's parent (if this item belongs to a group)" + }, + { + "type": "Array (optional)", + "name": "groupChildren", + "description": "List of children (if this item is a group)" + } + ], + "typedefs": [ + { + "type": "Object", + "name": "SceneItem", + "description": "An OBS Scene Item." + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + }, + { + "subheads": [], + "typedef": "{Object} `SceneItemTransform`", + "property": [ + "{double} `position.x` The x position of the scene item from the left.", + "{double} `position.y` The y position of the scene item from the top.", + "{int} `position.alignment` The point on the scene item that the item is manipulated from.", + "{double} `rotation` The clockwise rotation of the scene item in degrees around the point of alignment.", + "{double} `scale.x` The x-scale factor of the scene item.", + "{double} `scale.y` The y-scale factor of the scene item.", + "{String} `scale.filter` The scale filter of the source. Can be \"OBS_SCALE_DISABLE\", \"OBS_SCALE_POINT\", \"OBS_SCALE_BICUBIC\", \"OBS_SCALE_BILINEAR\", \"OBS_SCALE_LANCZOS\" or \"OBS_SCALE_AREA\".", + "{int} `crop.top` The number of pixels cropped off the top of the scene item before scaling.", + "{int} `crop.right` The number of pixels cropped off the right of the scene item before scaling.", + "{int} `crop.bottom` The number of pixels cropped off the bottom of the scene item before scaling.", + "{int} `crop.left` The number of pixels cropped off the left of the scene item before scaling.", + "{bool} `visible` If the scene item is visible.", + "{bool} `locked` If the scene item is locked in position.", + "{String} `bounds.type` Type of bounding box. Can be \"OBS_BOUNDS_STRETCH\", \"OBS_BOUNDS_SCALE_INNER\", \"OBS_BOUNDS_SCALE_OUTER\", \"OBS_BOUNDS_SCALE_TO_WIDTH\", \"OBS_BOUNDS_SCALE_TO_HEIGHT\", \"OBS_BOUNDS_MAX_ONLY\" or \"OBS_BOUNDS_NONE\".", + "{int} `bounds.alignment` Alignment of the bounding box.", + "{double} `bounds.x` Width of the bounding box.", + "{double} `bounds.y` Height of the bounding box.", + "{int} `sourceWidth` Base width (without scaling) of the source", + "{int} `sourceHeight` Base source (without scaling) of the source", + "{double} `width` Scene item width (base source width multiplied by the horizontal scaling factor)", + "{double} `height` Scene item height (base source height multiplied by the vertical scaling factor)", + "{String (optional)} `parentGroupName` Name of the item's parent (if this item belongs to a group)", + "{Array (optional)} `groupChildren` List of children (if this item is a group)" + ], + "properties": [ + { + "type": "double", + "name": "position.x", + "description": "The x position of the scene item from the left." + }, + { + "type": "double", + "name": "position.y", + "description": "The y position of the scene item from the top." + }, + { + "type": "int", + "name": "position.alignment", + "description": "The point on the scene item that the item is manipulated from." + }, + { + "type": "double", + "name": "rotation", + "description": "The clockwise rotation of the scene item in degrees around the point of alignment." + }, + { + "type": "double", + "name": "scale.x", + "description": "The x-scale factor of the scene item." + }, + { + "type": "double", + "name": "scale.y", + "description": "The y-scale factor of the scene item." + }, + { + "type": "String", + "name": "scale.filter", + "description": "The scale filter of the source. Can be \"OBS_SCALE_DISABLE\", \"OBS_SCALE_POINT\", \"OBS_SCALE_BICUBIC\", \"OBS_SCALE_BILINEAR\", \"OBS_SCALE_LANCZOS\" or \"OBS_SCALE_AREA\"." + }, + { + "type": "int", + "name": "crop.top", + "description": "The number of pixels cropped off the top of the scene item before scaling." + }, + { + "type": "int", + "name": "crop.right", + "description": "The number of pixels cropped off the right of the scene item before scaling." + }, + { + "type": "int", + "name": "crop.bottom", + "description": "The number of pixels cropped off the bottom of the scene item before scaling." + }, + { + "type": "int", + "name": "crop.left", + "description": "The number of pixels cropped off the left of the scene item before scaling." + }, + { + "type": "bool", + "name": "visible", + "description": "If the scene item is visible." + }, + { + "type": "bool", + "name": "locked", + "description": "If the scene item is locked in position." + }, + { + "type": "String", + "name": "bounds.type", + "description": "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\"." + }, + { + "type": "int", + "name": "bounds.alignment", + "description": "Alignment of the bounding box." + }, + { + "type": "double", + "name": "bounds.x", + "description": "Width of the bounding box." + }, + { + "type": "double", + "name": "bounds.y", + "description": "Height of the bounding box." + }, + { + "type": "int", + "name": "sourceWidth", + "description": "Base width (without scaling) of the source" + }, + { + "type": "int", + "name": "sourceHeight", + "description": "Base source (without scaling) of the source" + }, + { + "type": "double", + "name": "width", + "description": "Scene item width (base source width multiplied by the horizontal scaling factor)" + }, + { + "type": "double", + "name": "height", + "description": "Scene item height (base source height multiplied by the vertical scaling factor)" + }, + { + "type": "String (optional)", + "name": "parentGroupName", + "description": "Name of the item's parent (if this item belongs to a group)" + }, + { + "type": "Array (optional)", + "name": "groupChildren", + "description": "List of children (if this item is a group)" + } + ], + "typedefs": [ + { + "type": "Object", + "name": "SceneItemTransform", + "description": "" + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + }, + { + "subheads": [], + "typedef": "{Object} `OBSStats`", + "property": [ + "{double} `fps` Current framerate.", + "{int} `render-total-frames` Number of frames rendered", + "{int} `render-missed-frames` Number of frames missed due to rendering lag", + "{int} `output-total-frames` Number of frames outputted", + "{int} `output-skipped-frames` Number of frames skipped due to encoding lag", + "{double} `average-frame-time` Average frame render time (in milliseconds)", + "{double} `cpu-usage` Current CPU usage (percentage)", + "{double} `memory-usage` Current RAM usage (in megabytes)", + "{double} `free-disk-space` Free recording disk space (in megabytes)" + ], + "properties": [ + { + "type": "double", + "name": "fps", + "description": "Current framerate." + }, + { + "type": "int", + "name": "render-total-frames", + "description": "Number of frames rendered" + }, + { + "type": "int", + "name": "render-missed-frames", + "description": "Number of frames missed due to rendering lag" + }, + { + "type": "int", + "name": "output-total-frames", + "description": "Number of frames outputted" + }, + { + "type": "int", + "name": "output-skipped-frames", + "description": "Number of frames skipped due to encoding lag" + }, + { + "type": "double", + "name": "average-frame-time", + "description": "Average frame render time (in milliseconds)" + }, + { + "type": "double", + "name": "cpu-usage", + "description": "Current CPU usage (percentage)" + }, + { + "type": "double", + "name": "memory-usage", + "description": "Current RAM usage (in megabytes)" + }, + { + "type": "double", + "name": "free-disk-space", + "description": "Free recording disk space (in megabytes)" + } + ], + "typedefs": [ + { + "type": "Object", + "name": "OBSStats", + "description": "" + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + }, + { + "subheads": [], + "typedef": "{Object} `Output`", + "property": [ + "{String} `name` Output name", + "{String} `type` Output type/kind", + "{int} `width` Video output width", + "{int} `height` Video output height", + "{Object} `flags` Output flags", + "{int} `flags.rawValue` Raw flags value", + "{boolean} `flags.audio` Output uses audio", + "{boolean} `flags.video` Output uses video", + "{boolean} `flags.encoded` Output is encoded", + "{boolean} `flags.multiTrack` Output uses several audio tracks", + "{boolean} `flags.service` Output uses a service", + "{Object} `settings` Output name", + "{boolean} `active` Output status (active or not)", + "{boolean} `reconnecting` Output reconnection status (reconnecting or not)", + "{double} `congestion` Output congestion", + "{int} `totalFrames` Number of frames sent", + "{int} `droppedFrames` Number of frames dropped", + "{int} `totalBytes` Total bytes sent" + ], + "properties": [ + { + "type": "String", + "name": "name", + "description": "Output name" + }, + { + "type": "String", + "name": "type", + "description": "Output type/kind" + }, + { + "type": "int", + "name": "width", + "description": "Video output width" + }, + { + "type": "int", + "name": "height", + "description": "Video output height" + }, + { + "type": "Object", + "name": "flags", + "description": "Output flags" + }, + { + "type": "int", + "name": "flags.rawValue", + "description": "Raw flags value" + }, + { + "type": "boolean", + "name": "flags.audio", + "description": "Output uses audio" + }, + { + "type": "boolean", + "name": "flags.video", + "description": "Output uses video" + }, + { + "type": "boolean", + "name": "flags.encoded", + "description": "Output is encoded" + }, + { + "type": "boolean", + "name": "flags.multiTrack", + "description": "Output uses several audio tracks" + }, + { + "type": "boolean", + "name": "flags.service", + "description": "Output uses a service" + }, + { + "type": "Object", + "name": "settings", + "description": "Output name" + }, + { + "type": "boolean", + "name": "active", + "description": "Output status (active or not)" + }, + { + "type": "boolean", + "name": "reconnecting", + "description": "Output reconnection status (reconnecting or not)" + }, + { + "type": "double", + "name": "congestion", + "description": "Output congestion" + }, + { + "type": "int", + "name": "totalFrames", + "description": "Number of frames sent" + }, + { + "type": "int", + "name": "droppedFrames", + "description": "Number of frames dropped" + }, + { + "type": "int", + "name": "totalBytes", + "description": "Total bytes sent" + } + ], + "typedefs": [ + { + "type": "Object", + "name": "Output", + "description": "" + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + }, + { + "subheads": [], + "typedef": "{Object} `ScenesCollection`", + "property": "{String} `sc-name` Name of the scene collection", + "properties": [ + { + "type": "String", + "name": "sc-name", + "description": "Name of the scene collection" + } + ], + "typedefs": [ + { + "type": "Object", + "name": "ScenesCollection", + "description": "" + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + }, + { + "subheads": [], + "typedef": "{Object} `Scene`", + "property": [ + "{String} `name` Name of the currently active scene.", + "{Array} `sources` Ordered list of the current scene's source items." + ], + "properties": [ + { + "type": "String", + "name": "name", + "description": "Name of the currently active scene." + }, + { + "type": "Array", + "name": "sources", + "description": "Ordered list of the current scene's source items." + } + ], + "typedefs": [ + { + "type": "Object", + "name": "Scene", + "description": "" + } + ], + "name": "", + "heading": { + "level": 2, + "text": "" + }, + "examples": [] + } + ], + "events": { + "scenes": [ + { + "subheads": [], + "description": "Indicates a scene change.", + "return": [ + "{String} `scene-name` The new scene.", + "{Array} `sources` List of scene items in the new scene. Same specification as [`GetCurrentScene`](#getcurrentscene)." + ], + "api": "events", + "name": "SwitchScenes", + "category": "scenes", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "The new scene." + }, + { + "type": "Array", + "name": "sources", + "description": "List of scene items in the new scene. Same specification as [`GetCurrentScene`](#getcurrentscene)." + } + ], + "names": [ + { + "name": "", + "description": "SwitchScenes" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "SwitchScenes" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: This event is not fired when the scenes are reordered.", + "return": "{Array} `scenes` Scenes list.", + "api": "events", + "name": "ScenesChanged", + "category": "scenes", + "since": "0.3", + "returns": [ + { + "type": "Array", + "name": "scenes", + "description": "Scenes list." + } + ], + "names": [ + { + "name": "", + "description": "ScenesChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "ScenesChanged" + }, + "lead": "The scene list has been modified. Scenes have been added, removed, or renamed.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Triggered when switching to another scene collection or when renaming the current scene collection.", + "return": "{String} `sceneCollection` Name of the new current scene collection.", + "api": "events", + "name": "SceneCollectionChanged", + "category": "scenes", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "sceneCollection", + "description": "Name of the new current scene collection." + } + ], + "names": [ + { + "name": "", + "description": "SceneCollectionChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SceneCollectionChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Triggered when a scene collection is created, added, renamed, or removed.", + "return": [ + "{Array} `sceneCollections` Scene collections list.", + "{String} `sceneCollections.*.name` Scene collection name." + ], + "api": "events", + "name": "SceneCollectionListChanged", + "category": "scenes", + "since": "4.0.0", + "returns": [ + { + "type": "Array", + "name": "sceneCollections", + "description": "Scene collections list." + }, + { + "type": "String", + "name": "sceneCollections.*.name", + "description": "Scene collection name." + } + ], + "names": [ + { + "name": "", + "description": "SceneCollectionListChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SceneCollectionListChanged" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "transitions": [ + { + "subheads": [], + "description": "The active transition has been changed.", + "return": "{String} `transition-name` The name of the new active transition.", + "api": "events", + "name": "SwitchTransition", + "category": "transitions", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "transition-name", + "description": "The name of the new active transition." + } + ], + "names": [ + { + "name": "", + "description": "SwitchTransition" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SwitchTransition" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "The list of available transitions has been modified.\nTransitions have been added, removed, or renamed.", + "return": [ + "{Array} `transitions` Transitions list.", + "{String} `transitions.*.name` Transition name." + ], + "api": "events", + "name": "TransitionListChanged", + "category": "transitions", + "since": "4.0.0", + "returns": [ + { + "type": "Array", + "name": "transitions", + "description": "Transitions list." + }, + { + "type": "String", + "name": "transitions.*.name", + "description": "Transition name." + } + ], + "names": [ + { + "name": "", + "description": "TransitionListChanged" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionListChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "The active transition duration has been changed.", + "return": "{int} `new-duration` New transition duration.", + "api": "events", + "name": "TransitionDurationChanged", + "category": "transitions", + "since": "4.0.0", + "returns": [ + { + "type": "int", + "name": "new-duration", + "description": "New transition duration." + } + ], + "names": [ + { + "name": "", + "description": "TransitionDurationChanged" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionDurationChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A transition (other than \"cut\") has begun.", + "return": [ + "{String} `name` Transition name.", + "{String} `type` Transition type.", + "{int} `duration` Transition duration (in milliseconds). Will be -1 for any transition with a fixed duration, such as a Stinger, due to limitations of the OBS API.", + "{String} `from-scene` Source scene of the transition", + "{String} `to-scene` Destination scene of the transition" + ], + "api": "events", + "name": "TransitionBegin", + "category": "transitions", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Transition name." + }, + { + "type": "String", + "name": "type", + "description": "Transition type." + }, + { + "type": "int", + "name": "duration", + "description": "Transition duration (in milliseconds). Will be -1 for any transition with a fixed duration, such as a Stinger, due to limitations of the OBS API." + }, + { + "type": "String", + "name": "from-scene", + "description": "Source scene of the transition" + }, + { + "type": "String", + "name": "to-scene", + "description": "Destination scene of the transition" + } + ], + "names": [ + { + "name": "", + "description": "TransitionBegin" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionBegin" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A transition (other than \"cut\") has ended.\nNote: The `from-scene` field is not available in TransitionEnd.", + "return": [ + "{String} `name` Transition name.", + "{String} `type` Transition type.", + "{int} `duration` Transition duration (in milliseconds).", + "{String} `to-scene` Destination scene of the transition" + ], + "api": "events", + "name": "TransitionEnd", + "category": "transitions", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Transition name." + }, + { + "type": "String", + "name": "type", + "description": "Transition type." + }, + { + "type": "int", + "name": "duration", + "description": "Transition duration (in milliseconds)." + }, + { + "type": "String", + "name": "to-scene", + "description": "Destination scene of the transition" + } + ], + "names": [ + { + "name": "", + "description": "TransitionEnd" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionEnd" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A stinger transition has finished playing its video.", + "return": [ + "{String} `name` Transition name.", + "{String} `type` Transition type.", + "{int} `duration` Transition duration (in milliseconds).", + "{String} `from-scene` Source scene of the transition", + "{String} `to-scene` Destination scene of the transition" + ], + "api": "events", + "name": "TransitionVideoEnd", + "category": "transitions", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Transition name." + }, + { + "type": "String", + "name": "type", + "description": "Transition type." + }, + { + "type": "int", + "name": "duration", + "description": "Transition duration (in milliseconds)." + }, + { + "type": "String", + "name": "from-scene", + "description": "Source scene of the transition" + }, + { + "type": "String", + "name": "to-scene", + "description": "Destination scene of the transition" + } + ], + "names": [ + { + "name": "", + "description": "TransitionVideoEnd" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionVideoEnd" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "profiles": [ + { + "subheads": [], + "description": "Triggered when switching to another profile or when renaming the current profile.", + "return": "{String} `profile` Name of the new current profile.", + "api": "events", + "name": "ProfileChanged", + "category": "profiles", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "profile", + "description": "Name of the new current profile." + } + ], + "names": [ + { + "name": "", + "description": "ProfileChanged" + } + ], + "categories": [ + { + "name": "", + "description": "profiles" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "ProfileChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Triggered when a profile is created, added, renamed, or removed.", + "return": [ + "{Array} `profiles` Profiles list.", + "{String} `profiles.*.name` Profile name." + ], + "api": "events", + "name": "ProfileListChanged", + "category": "profiles", + "since": "4.0.0", + "returns": [ + { + "type": "Array", + "name": "profiles", + "description": "Profiles list." + }, + { + "type": "String", + "name": "profiles.*.name", + "description": "Profile name." + } + ], + "names": [ + { + "name": "", + "description": "ProfileListChanged" + } + ], + "categories": [ + { + "name": "", + "description": "profiles" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "ProfileListChanged" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "streaming": [ + { + "subheads": [], + "description": "A request to start streaming has been issued.", + "return": "{boolean} `preview-only` Always false (retrocompatibility).", + "api": "events", + "name": "StreamStarting", + "category": "streaming", + "since": "0.3", + "returns": [ + { + "type": "boolean", + "name": "preview-only", + "description": "Always false (retrocompatibility)." + } + ], + "names": [ + { + "name": "", + "description": "StreamStarting" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StreamStarting" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Streaming started successfully.", + "api": "events", + "name": "StreamStarted", + "category": "streaming", + "since": "0.3", + "names": [ + { + "name": "", + "description": "StreamStarted" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StreamStarted" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A request to stop streaming has been issued.", + "return": "{boolean} `preview-only` Always false (retrocompatibility).", + "api": "events", + "name": "StreamStopping", + "category": "streaming", + "since": "0.3", + "returns": [ + { + "type": "boolean", + "name": "preview-only", + "description": "Always false (retrocompatibility)." + } + ], + "names": [ + { + "name": "", + "description": "StreamStopping" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StreamStopping" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Streaming stopped successfully.", + "api": "events", + "name": "StreamStopped", + "category": "streaming", + "since": "0.3", + "names": [ + { + "name": "", + "description": "StreamStopped" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StreamStopped" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Emitted every 2 seconds when stream is active.", + "return": [ + "{boolean} `streaming` Current streaming state.", + "{boolean} `recording` Current recording state.", + "{boolean} `replay-buffer-active` Replay Buffer status", + "{int} `bytes-per-sec` Amount of data per second (in bytes) transmitted by the stream encoder.", + "{int} `kbits-per-sec` Amount of data per second (in kilobits) transmitted by the stream encoder.", + "{double} `strain` Percentage of dropped frames.", + "{int} `total-stream-time` Total time (in seconds) since the stream started.", + "{int} `num-total-frames` Total number of frames transmitted since the stream started.", + "{int} `num-dropped-frames` Number of frames dropped by the encoder since the stream started.", + "{double} `fps` Current framerate.", + "{int} `render-total-frames` Number of frames rendered", + "{int} `render-missed-frames` Number of frames missed due to rendering lag", + "{int} `output-total-frames` Number of frames outputted", + "{int} `output-skipped-frames` Number of frames skipped due to encoding lag", + "{double} `average-frame-time` Average frame time (in milliseconds)", + "{double} `cpu-usage` Current CPU usage (percentage)", + "{double} `memory-usage` Current RAM usage (in megabytes)", + "{double} `free-disk-space` Free recording disk space (in megabytes)", + "{boolean} `preview-only` Always false (retrocompatibility)." + ], + "api": "events", + "name": "StreamStatus", + "category": "streaming", + "since": "0.3", + "returns": [ + { + "type": "boolean", + "name": "streaming", + "description": "Current streaming state." + }, + { + "type": "boolean", + "name": "recording", + "description": "Current recording state." + }, + { + "type": "boolean", + "name": "replay-buffer-active", + "description": "Replay Buffer status" + }, + { + "type": "int", + "name": "bytes-per-sec", + "description": "Amount of data per second (in bytes) transmitted by the stream encoder." + }, + { + "type": "int", + "name": "kbits-per-sec", + "description": "Amount of data per second (in kilobits) transmitted by the stream encoder." + }, + { + "type": "double", + "name": "strain", + "description": "Percentage of dropped frames." + }, + { + "type": "int", + "name": "total-stream-time", + "description": "Total time (in seconds) since the stream started." + }, + { + "type": "int", + "name": "num-total-frames", + "description": "Total number of frames transmitted since the stream started." + }, + { + "type": "int", + "name": "num-dropped-frames", + "description": "Number of frames dropped by the encoder since the stream started." + }, + { + "type": "double", + "name": "fps", + "description": "Current framerate." + }, + { + "type": "int", + "name": "render-total-frames", + "description": "Number of frames rendered" + }, + { + "type": "int", + "name": "render-missed-frames", + "description": "Number of frames missed due to rendering lag" + }, + { + "type": "int", + "name": "output-total-frames", + "description": "Number of frames outputted" + }, + { + "type": "int", + "name": "output-skipped-frames", + "description": "Number of frames skipped due to encoding lag" + }, + { + "type": "double", + "name": "average-frame-time", + "description": "Average frame time (in milliseconds)" + }, + { + "type": "double", + "name": "cpu-usage", + "description": "Current CPU usage (percentage)" + }, + { + "type": "double", + "name": "memory-usage", + "description": "Current RAM usage (in megabytes)" + }, + { + "type": "double", + "name": "free-disk-space", + "description": "Free recording disk space (in megabytes)" + }, + { + "type": "boolean", + "name": "preview-only", + "description": "Always false (retrocompatibility)." + } + ], + "names": [ + { + "name": "", + "description": "StreamStatus" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StreamStatus" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "recording": [ + { + "subheads": [], + "description": "\n\nNote: `recordingFilename` is not provided in this event because this information\nis not available at the time this event is emitted.", + "api": "events", + "name": "RecordingStarting", + "category": "recording", + "since": "0.3", + "names": [ + { + "name": "", + "description": "RecordingStarting" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "RecordingStarting" + }, + "lead": "A request to start recording has been issued.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Recording started successfully.", + "return": "{String} `recordingFilename` Absolute path to the file of the current recording.", + "api": "events", + "name": "RecordingStarted", + "category": "recording", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "recordingFilename", + "description": "Absolute path to the file of the current recording." + } + ], + "names": [ + { + "name": "", + "description": "RecordingStarted" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "RecordingStarted" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A request to stop recording has been issued.", + "return": "{String} `recordingFilename` Absolute path to the file of the current recording.", + "api": "events", + "name": "RecordingStopping", + "category": "recording", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "recordingFilename", + "description": "Absolute path to the file of the current recording." + } + ], + "names": [ + { + "name": "", + "description": "RecordingStopping" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "RecordingStopping" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Recording stopped successfully.", + "return": "{String} `recordingFilename` Absolute path to the file of the current recording.", + "api": "events", + "name": "RecordingStopped", + "category": "recording", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "recordingFilename", + "description": "Absolute path to the file of the current recording." + } + ], + "names": [ + { + "name": "", + "description": "RecordingStopped" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "RecordingStopped" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Current recording paused", + "api": "events", + "name": "RecordingPaused", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "RecordingPaused" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "RecordingPaused" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Current recording resumed", + "api": "events", + "name": "RecordingResumed", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "RecordingResumed" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "RecordingResumed" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "replay buffer": [ + { + "subheads": [], + "description": "A request to start the replay buffer has been issued.", + "api": "events", + "name": "ReplayStarting", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "ReplayStarting" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "ReplayStarting" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Replay Buffer started successfully", + "api": "events", + "name": "ReplayStarted", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "ReplayStarted" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "ReplayStarted" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A request to stop the replay buffer has been issued.", + "api": "events", + "name": "ReplayStopping", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "ReplayStopping" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "ReplayStopping" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Replay Buffer stopped successfully", + "api": "events", + "name": "ReplayStopped", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "ReplayStopped" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "ReplayStopped" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "other": [ + { + "subheads": [], + "description": "OBS is exiting.", + "api": "events", + "name": "Exiting", + "category": "other", + "since": "0.3", + "names": [ + { + "name": "", + "description": "Exiting" + } + ], + "categories": [ + { + "name": "", + "description": "other" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "Exiting" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "general": [ + { + "subheads": [], + "description": "Emitted every 2 seconds after enabling it by calling SetHeartbeat.", + "return": [ + "{boolean} `pulse` Toggles between every JSON message as an \"I am alive\" indicator.", + "{string (optional)} `current-profile` Current active profile.", + "{string (optional)} `current-scene` Current active scene.", + "{boolean (optional)} `streaming` Current streaming state.", + "{int (optional)} `total-stream-time` Total time (in seconds) since the stream started.", + "{int (optional)} `total-stream-bytes` Total bytes sent since the stream started.", + "{int (optional)} `total-stream-frames` Total frames streamed since the stream started.", + "{boolean (optional)} `recording` Current recording state.", + "{int (optional)} `total-record-time` Total time (in seconds) since recording started.", + "{int (optional)} `total-record-bytes` Total bytes recorded since the recording started.", + "{int (optional)} `total-record-frames` Total frames recorded since the recording started.", + "{OBSStats} `stats` OBS Stats" + ], + "api": "events", + "name": "Heartbeat", + "category": "general", + "since": "v0.3", + "returns": [ + { + "type": "boolean", + "name": "pulse", + "description": "Toggles between every JSON message as an \"I am alive\" indicator." + }, + { + "type": "string (optional)", + "name": "current-profile", + "description": "Current active profile." + }, + { + "type": "string (optional)", + "name": "current-scene", + "description": "Current active scene." + }, + { + "type": "boolean (optional)", + "name": "streaming", + "description": "Current streaming state." + }, + { + "type": "int (optional)", + "name": "total-stream-time", + "description": "Total time (in seconds) since the stream started." + }, + { + "type": "int (optional)", + "name": "total-stream-bytes", + "description": "Total bytes sent since the stream started." + }, + { + "type": "int (optional)", + "name": "total-stream-frames", + "description": "Total frames streamed since the stream started." + }, + { + "type": "boolean (optional)", + "name": "recording", + "description": "Current recording state." + }, + { + "type": "int (optional)", + "name": "total-record-time", + "description": "Total time (in seconds) since recording started." + }, + { + "type": "int (optional)", + "name": "total-record-bytes", + "description": "Total bytes recorded since the recording started." + }, + { + "type": "int (optional)", + "name": "total-record-frames", + "description": "Total frames recorded since the recording started." + }, + { + "type": "OBSStats", + "name": "stats", + "description": "OBS Stats" + } + ], + "names": [ + { + "name": "", + "description": "Heartbeat" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "v0.3" + } + ], + "heading": { + "level": 2, + "text": "Heartbeat" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A custom broadcast message, sent by the server, requested by one of the websocket clients.", + "return": [ + "{String} `realm` Identifier provided by the sender", + "{Object} `data` User-defined data" + ], + "api": "events", + "name": "BroadcastCustomMessage", + "category": "general", + "since": "4.7.0", + "returns": [ + { + "type": "String", + "name": "realm", + "description": "Identifier provided by the sender" + }, + { + "type": "Object", + "name": "data", + "description": "User-defined data" + } + ], + "names": [ + { + "name": "", + "description": "BroadcastCustomMessage" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "BroadcastCustomMessage" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "sources": [ + { + "subheads": [], + "description": "A source has been created. A source can be an input, a scene or a transition.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceType` Source type. Can be \"input\", \"scene\", \"transition\" or \"filter\".", + "{String} `sourceKind` Source kind.", + "{Object} `sourceSettings` Source settings" + ], + "api": "events", + "name": "SourceCreated", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceType", + "description": "Source type. Can be \"input\", \"scene\", \"transition\" or \"filter\"." + }, + { + "type": "String", + "name": "sourceKind", + "description": "Source kind." + }, + { + "type": "Object", + "name": "sourceSettings", + "description": "Source settings" + } + ], + "names": [ + { + "name": "", + "description": "SourceCreated" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceCreated" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A source has been destroyed/removed. A source can be an input, a scene or a transition.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceType` Source type. Can be \"input\", \"scene\", \"transition\" or \"filter\".", + "{String} `sourceKind` Source kind." + ], + "api": "events", + "name": "SourceDestroyed", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceType", + "description": "Source type. Can be \"input\", \"scene\", \"transition\" or \"filter\"." + }, + { + "type": "String", + "name": "sourceKind", + "description": "Source kind." + } + ], + "names": [ + { + "name": "", + "description": "SourceDestroyed" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceDestroyed" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "The volume of a source has changed.", + "return": [ + "{String} `sourceName` Source name", + "{float} `volume` Source volume" + ], + "api": "events", + "name": "SourceVolumeChanged", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "float", + "name": "volume", + "description": "Source volume" + } + ], + "names": [ + { + "name": "", + "description": "SourceVolumeChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceVolumeChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A source has been muted or unmuted.", + "return": [ + "{String} `sourceName` Source name", + "{boolean} `muted` Mute status of the source" + ], + "api": "events", + "name": "SourceMuteStateChanged", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "boolean", + "name": "muted", + "description": "Mute status of the source" + } + ], + "names": [ + { + "name": "", + "description": "SourceMuteStateChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceMuteStateChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A source has removed audio.", + "return": "{String} `sourceName` Source name", + "api": "events", + "name": "SourceAudioDeactivated", + "category": "sources", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + } + ], + "names": [ + { + "name": "", + "description": "SourceAudioDeactivated" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "SourceAudioDeactivated" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A source has added audio.", + "return": "{String} `sourceName` Source name", + "api": "events", + "name": "SourceAudioActivated", + "category": "sources", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + } + ], + "names": [ + { + "name": "", + "description": "SourceAudioActivated" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "SourceAudioActivated" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "The audio sync offset of a source has changed.", + "return": [ + "{String} `sourceName` Source name", + "{int} `syncOffset` Audio sync offset of the source (in nanoseconds)" + ], + "api": "events", + "name": "SourceAudioSyncOffsetChanged", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "int", + "name": "syncOffset", + "description": "Audio sync offset of the source (in nanoseconds)" + } + ], + "names": [ + { + "name": "", + "description": "SourceAudioSyncOffsetChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceAudioSyncOffsetChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Audio mixer routing changed on a source.", + "return": [ + "{String} `sourceName` Source name", + "{Array} `mixers` Routing status of the source for each audio mixer (array of 6 values)", + "{int} `mixers.*.id` Mixer number", + "{boolean} `mixers.*.enabled` Routing status", + "{String} `hexMixersValue` Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value" + ], + "api": "events", + "name": "SourceAudioMixersChanged", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "Array", + "name": "mixers", + "description": "Routing status of the source for each audio mixer (array of 6 values)" + }, + { + "type": "int", + "name": "mixers.*.id", + "description": "Mixer number" + }, + { + "type": "boolean", + "name": "mixers.*.enabled", + "description": "Routing status" + }, + { + "type": "String", + "name": "hexMixersValue", + "description": "Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value" + } + ], + "names": [ + { + "name": "", + "description": "SourceAudioMixersChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceAudioMixersChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A source has been renamed.", + "return": [ + "{String} `previousName` Previous source name", + "{String} `newName` New source name", + "{String} `sourceType` Type of source (input, scene, filter, transition)" + ], + "api": "events", + "name": "SourceRenamed", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "previousName", + "description": "Previous source name" + }, + { + "type": "String", + "name": "newName", + "description": "New source name" + }, + { + "type": "String", + "name": "sourceType", + "description": "Type of source (input, scene, filter, transition)" + } + ], + "names": [ + { + "name": "", + "description": "SourceRenamed" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceRenamed" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A filter was added to a source.", + "return": [ + "{String} `sourceName` Source name", + "{String} `filterName` Filter name", + "{String} `filterType` Filter type", + "{Object} `filterSettings` Filter settings" + ], + "api": "events", + "name": "SourceFilterAdded", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Filter name" + }, + { + "type": "String", + "name": "filterType", + "description": "Filter type" + }, + { + "type": "Object", + "name": "filterSettings", + "description": "Filter settings" + } + ], + "names": [ + { + "name": "", + "description": "SourceFilterAdded" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceFilterAdded" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A filter was removed from a source.", + "return": [ + "{String} `sourceName` Source name", + "{String} `filterName` Filter name", + "{String} `filterType` Filter type" + ], + "api": "events", + "name": "SourceFilterRemoved", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Filter name" + }, + { + "type": "String", + "name": "filterType", + "description": "Filter type" + } + ], + "names": [ + { + "name": "", + "description": "SourceFilterRemoved" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceFilterRemoved" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "The visibility/enabled state of a filter changed", + "return": [ + "{String} `sourceName` Source name", + "{String} `filterName` Filter name", + "{Boolean} `filterEnabled` New filter state" + ], + "api": "events", + "name": "SourceFilterVisibilityChanged", + "category": "sources", + "since": "4.7.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Filter name" + }, + { + "type": "Boolean", + "name": "filterEnabled", + "description": "New filter state" + } + ], + "names": [ + { + "name": "", + "description": "SourceFilterVisibilityChanged" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "SourceFilterVisibilityChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Filters in a source have been reordered.", + "return": [ + "{String} `sourceName` Source name", + "{Array} `filters` Ordered Filters list", + "{String} `filters.*.name` Filter name", + "{String} `filters.*.type` Filter type", + "{boolean} `filters.*.enabled` Filter visibility status" + ], + "api": "events", + "name": "SourceFiltersReordered", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "Array", + "name": "filters", + "description": "Ordered Filters list" + }, + { + "type": "String", + "name": "filters.*.name", + "description": "Filter name" + }, + { + "type": "String", + "name": "filters.*.type", + "description": "Filter type" + }, + { + "type": "boolean", + "name": "filters.*.enabled", + "description": "Filter visibility status" + } + ], + "names": [ + { + "name": "", + "description": "SourceFiltersReordered" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SourceFiltersReordered" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "media": [ + { + "subheads": [], + "description": "\n\nNote: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaPlaying", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaPlaying" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaPlaying" + }, + "lead": "A media source has started playing.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaPaused", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaPaused" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaPaused" + }, + "lead": "A media source has been paused.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaRestarted", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaRestarted" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaRestarted" + }, + "lead": "A media source has been restarted.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaStopped", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaStopped" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaStopped" + }, + "lead": "A media source has been stopped.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaNext", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaNext" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaNext" + }, + "lead": "A media source has gone to the next item in the playlist.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaPrevious", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaPrevious" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaPrevious" + }, + "lead": "A media source has gone to the previous item in the playlist.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: These events are emitted by the OBS sources themselves. For example when the media file starts playing. The behavior depends on the type of media source being used.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaStarted", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaStarted" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaStarted" + }, + "lead": "A media source has been started.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: These events are emitted by the OBS sources themselves. For example when the media file ends. The behavior depends on the type of media source being used.", + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceKind` The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + ], + "api": "events", + "name": "MediaEnded", + "category": "media", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceKind", + "description": "The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`)" + } + ], + "names": [ + { + "name": "", + "description": "MediaEnded" + } + ], + "categories": [ + { + "name": "", + "description": "media" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "MediaEnded" + }, + "lead": "A media source has ended.", + "type": "class", + "examples": [] + } + ], + "scene items": [ + { + "subheads": [], + "description": "Scene items within a scene have been reordered.", + "return": [ + "{String} `scene-name` Name of the scene where items have been reordered.", + "{Array} `scene-items` Ordered list of scene items", + "{String} `scene-items.*.source-name` Item source name", + "{int} `scene-items.*.item-id` Scene item unique ID" + ], + "api": "events", + "name": "SourceOrderChanged", + "category": "scene items", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene where items have been reordered." + }, + { + "type": "Array", + "name": "scene-items", + "description": "Ordered list of scene items" + }, + { + "type": "String", + "name": "scene-items.*.source-name", + "description": "Item source name" + }, + { + "type": "int", + "name": "scene-items.*.item-id", + "description": "Scene item unique ID" + } + ], + "names": [ + { + "name": "", + "description": "SourceOrderChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SourceOrderChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item has been added to a scene.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item added to the scene.", + "{int} `item-id` Scene item ID" + ], + "api": "events", + "name": "SceneItemAdded", + "category": "scene items", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item added to the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Scene item ID" + } + ], + "names": [ + { + "name": "", + "description": "SceneItemAdded" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemAdded" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item has been removed from a scene.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item removed from the scene.", + "{int} `item-id` Scene item ID" + ], + "api": "events", + "name": "SceneItemRemoved", + "category": "scene items", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item removed from the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Scene item ID" + } + ], + "names": [ + { + "name": "", + "description": "SceneItemRemoved" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemRemoved" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item's visibility has been toggled.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item in the scene.", + "{int} `item-id` Scene item ID", + "{boolean} `item-visible` New visibility state of the item." + ], + "api": "events", + "name": "SceneItemVisibilityChanged", + "category": "scene items", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item in the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Scene item ID" + }, + { + "type": "boolean", + "name": "item-visible", + "description": "New visibility state of the item." + } + ], + "names": [ + { + "name": "", + "description": "SceneItemVisibilityChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemVisibilityChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item's locked status has been toggled.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item in the scene.", + "{int} `item-id` Scene item ID", + "{boolean} `item-locked` New locked state of the item." + ], + "api": "events", + "name": "SceneItemLockChanged", + "category": "scene items", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item in the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Scene item ID" + }, + { + "type": "boolean", + "name": "item-locked", + "description": "New locked state of the item." + } + ], + "names": [ + { + "name": "", + "description": "SceneItemLockChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemLockChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item's transform has been changed.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item in the scene.", + "{int} `item-id` Scene item ID", + "{SceneItemTransform} `transform` Scene item transform properties" + ], + "api": "events", + "name": "SceneItemTransformChanged", + "category": "scene items", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item in the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Scene item ID" + }, + { + "type": "SceneItemTransform", + "name": "transform", + "description": "Scene item transform properties" + } + ], + "names": [ + { + "name": "", + "description": "SceneItemTransformChanged" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemTransformChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item is selected.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item in the scene.", + "{int} `item-id` Name of the item in the scene." + ], + "api": "events", + "name": "SceneItemSelected", + "category": "scene items", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item in the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Name of the item in the scene." + } + ], + "names": [ + { + "name": "", + "description": "SceneItemSelected" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemSelected" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "A scene item is deselected.", + "return": [ + "{String} `scene-name` Name of the scene.", + "{String} `item-name` Name of the item in the scene.", + "{int} `item-id` Name of the item in the scene." + ], + "api": "events", + "name": "SceneItemDeselected", + "category": "scene items", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene." + }, + { + "type": "String", + "name": "item-name", + "description": "Name of the item in the scene." + }, + { + "type": "int", + "name": "item-id", + "description": "Name of the item in the scene." + } + ], + "names": [ + { + "name": "", + "description": "SceneItemDeselected" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SceneItemDeselected" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "studio mode": [ + { + "subheads": [], + "description": "The selected preview scene has changed (only available in Studio Mode).", + "return": [ + "{String} `scene-name` Name of the scene being previewed.", + "{Array} `sources` List of sources composing the scene. Same specification as [`GetCurrentScene`](#getcurrentscene)." + ], + "api": "events", + "name": "PreviewSceneChanged", + "category": "studio mode", + "since": "4.1.0", + "returns": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene being previewed." + }, + { + "type": "Array", + "name": "sources", + "description": "List of sources composing the scene. Same specification as [`GetCurrentScene`](#getcurrentscene)." + } + ], + "names": [ + { + "name": "", + "description": "PreviewSceneChanged" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "PreviewSceneChanged" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Studio Mode has been enabled or disabled.", + "return": "{boolean} `new-state` The new enabled state of Studio Mode.", + "api": "events", + "name": "StudioModeSwitched", + "category": "studio mode", + "since": "4.1.0", + "returns": [ + { + "type": "boolean", + "name": "new-state", + "description": "The new enabled state of Studio Mode." + } + ], + "names": [ + { + "name": "", + "description": "StudioModeSwitched" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "StudioModeSwitched" + }, + "lead": "", + "type": "class", + "examples": [] + } + ] + }, + "requests": { + "general": [ + { + "subheads": [], + "description": "Returns the latest version of the plugin and the API.", + "return": [ + "{double} `version` OBSRemote compatible API version. Fixed to 1.1 for retrocompatibility.", + "{String} `obs-websocket-version` obs-websocket plugin version.", + "{String} `obs-studio-version` OBS Studio program version.", + "{String} `available-requests` List of available request types, formatted as a comma-separated list string (e.g. : \"Method1,Method2,Method3\").", + "{String} `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", + "returns": [ + { + "type": "double", + "name": "version", + "description": "OBSRemote compatible API version. Fixed to 1.1 for retrocompatibility." + }, + { + "type": "String", + "name": "obs-websocket-version", + "description": "obs-websocket plugin version." + }, + { + "type": "String", + "name": "obs-studio-version", + "description": "OBS Studio program version." + }, + { + "type": "String", + "name": "available-requests", + "description": "List of available request types, formatted as a comma-separated list string (e.g. : \"Method1,Method2,Method3\")." + }, + { + "type": "String", + "name": "supported-image-export-formats", + "description": "List of supported formats for features that use image export (like the TakeSourceScreenshot request type) formatted as a comma-separated list string" + } + ], + "names": [ + { + "name": "", + "description": "GetVersion" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "GetVersion" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Tells the client if authentication is required. If so, returns authentication parameters `challenge`\nand `salt` (see \"Authentication\" for more information).", + "return": [ + "{boolean} `authRequired` Indicates whether authentication is required.", + "{String (optional)} `challenge`", + "{String (optional)} `salt`" + ], + "api": "requests", + "name": "GetAuthRequired", + "category": "general", + "since": "0.3", + "returns": [ + { + "type": "boolean", + "name": "authRequired", + "description": "Indicates whether authentication is required." + }, + { + "type": "String (optional)", + "name": "challenge", + "description": "" + }, + { + "type": "String (optional)", + "name": "salt", + "description": "" + } + ], + "names": [ + { + "name": "", + "description": "GetAuthRequired" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "GetAuthRequired" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Attempt to authenticate the client to the server.", + "param": "{String} `auth` Response to the auth challenge (see \"Authentication\" for more information).", + "api": "requests", + "name": "Authenticate", + "category": "general", + "since": "0.3", + "params": [ + { + "type": "String", + "name": "auth", + "description": "Response to the auth challenge (see \"Authentication\" for more information)." + } + ], + "names": [ + { + "name": "", + "description": "Authenticate" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "Authenticate" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Enable/disable sending of the Heartbeat event", + "param": "{boolean} `enable` Starts/Stops emitting heartbeat messages", + "api": "requests", + "name": "SetHeartbeat", + "category": "general", + "since": "4.3.0", + "deprecated": "Since 4.9.0. Please poll the appropriate data using requests. Will be removed in v5.0.0.", + "params": [ + { + "type": "boolean", + "name": "enable", + "description": "Starts/Stops emitting heartbeat messages" + } + ], + "names": [ + { + "name": "", + "description": "SetHeartbeat" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "deprecateds": [ + { + "name": "", + "description": "Since 4.9.0. Please poll the appropriate data using requests. Will be removed in v5.0.0." + } + ], + "heading": { + "level": 2, + "text": "SetHeartbeat" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the filename formatting string", + "param": "{String} `filename-formatting` Filename formatting string to set.", + "api": "requests", + "name": "SetFilenameFormatting", + "category": "general", + "since": "4.3.0", + "params": [ + { + "type": "String", + "name": "filename-formatting", + "description": "Filename formatting string to set." + } + ], + "names": [ + { + "name": "", + "description": "SetFilenameFormatting" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "SetFilenameFormatting" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the filename formatting string", + "return": "{String} `filename-formatting` Current filename formatting string.", + "api": "requests", + "name": "GetFilenameFormatting", + "category": "general", + "since": "4.3.0", + "returns": [ + { + "type": "String", + "name": "filename-formatting", + "description": "Current filename formatting string." + } + ], + "names": [ + { + "name": "", + "description": "GetFilenameFormatting" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "GetFilenameFormatting" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get OBS stats (almost the same info as provided in OBS' stats window)", + "return": "{OBSStats} `stats` [OBS stats](#obsstats)", + "api": "requests", + "name": "GetStats", + "category": "general", + "since": "4.6.0", + "returns": [ + { + "type": "OBSStats", + "name": "stats", + "description": "[OBS stats](#obsstats)" + } + ], + "names": [ + { + "name": "", + "description": "GetStats" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "GetStats" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Broadcast custom message to all connected WebSocket clients", + "param": [ + "{String} `realm` Identifier to be choosen by the client", + "{Object} `data` User-defined data" + ], + "api": "requests", + "name": "BroadcastCustomMessage", + "category": "general", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "realm", + "description": "Identifier to be choosen by the client" + }, + { + "type": "Object", + "name": "data", + "description": "User-defined data" + } + ], + "names": [ + { + "name": "", + "description": "BroadcastCustomMessage" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "BroadcastCustomMessage" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get basic OBS video information", + "return": [ + "{int} `baseWidth` Base (canvas) width", + "{int} `baseHeight` Base (canvas) height", + "{int} `outputWidth` Output width", + "{int} `outputHeight` Output height", + "{String} `scaleType` Scaling method used if output size differs from base size", + "{double} `fps` Frames rendered per second", + "{String} `videoFormat` Video color format", + "{String} `colorSpace` Color space for YUV", + "{String} `colorRange` Color range (full or partial)" + ], + "api": "requests", + "name": "GetVideoInfo", + "category": "general", + "since": "4.6.0", + "returns": [ + { + "type": "int", + "name": "baseWidth", + "description": "Base (canvas) width" + }, + { + "type": "int", + "name": "baseHeight", + "description": "Base (canvas) height" + }, + { + "type": "int", + "name": "outputWidth", + "description": "Output width" + }, + { + "type": "int", + "name": "outputHeight", + "description": "Output height" + }, + { + "type": "String", + "name": "scaleType", + "description": "Scaling method used if output size differs from base size" + }, + { + "type": "double", + "name": "fps", + "description": "Frames rendered per second" + }, + { + "type": "String", + "name": "videoFormat", + "description": "Video color format" + }, + { + "type": "String", + "name": "colorSpace", + "description": "Color space for YUV" + }, + { + "type": "String", + "name": "colorRange", + "description": "Color range (full or partial)" + } + ], + "names": [ + { + "name": "", + "description": "GetVideoInfo" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "GetVideoInfo" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Open a projector window or create a projector on a monitor. Requires OBS v24.0.4 or newer.", + "param": [ + "{String (Optional)} `type` Type of projector: `Preview` (default), `Source`, `Scene`, `StudioProgram`, or `Multiview` (case insensitive).", + "{int (Optional)} `monitor` Monitor to open the projector on. If -1 or omitted, opens a window.", + "{String (Optional)} `geometry` Size and position of the projector window (only if monitor is -1). Encoded in Base64 using [Qt's geometry encoding](https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to OBS's saved projectors.", + "{String (Optional)} `name` Name of the source or scene to be displayed (ignored for other projector types)." + ], + "api": "requests", + "name": "OpenProjector", + "category": "general", + "since": "4.8.0", + "params": [ + { + "type": "String (Optional)", + "name": "type", + "description": "Type of projector: `Preview` (default), `Source`, `Scene`, `StudioProgram`, or `Multiview` (case insensitive)." + }, + { + "type": "int (Optional)", + "name": "monitor", + "description": "Monitor to open the projector on. If -1 or omitted, opens a window." + }, + { + "type": "String (Optional)", + "name": "geometry", + "description": "Size and position of the projector window (only if monitor is -1). Encoded in Base64 using [Qt's geometry encoding](https://doc.qt.io/qt-5/qwidget.html#saveGeometry). Corresponds to OBS's saved projectors." + }, + { + "type": "String (Optional)", + "name": "name", + "description": "Name of the source or scene to be displayed (ignored for other projector types)." + } + ], + "names": [ + { + "name": "", + "description": "OpenProjector" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "OpenProjector" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Executes hotkey routine, identified by hotkey unique name", + "param": "{String} `hotkeyName` Unique name of the hotkey, as defined when registering the hotkey (e.g. \"ReplayBuffer.Save\")", + "api": "requests", + "name": "TriggerHotkeyByName", + "category": "general", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "hotkeyName", + "description": "Unique name of the hotkey, as defined when registering the hotkey (e.g. \"ReplayBuffer.Save\")" + } + ], + "names": [ + { + "name": "", + "description": "TriggerHotkeyByName" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "TriggerHotkeyByName" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Executes hotkey routine, identified by bound combination of keys. A single key combination might trigger multiple hotkey routines depending on user settings", + "param": [ + "{String} `keyId` Main key identifier (e.g. `OBS_KEY_A` for key \"A\"). Available identifiers [here](https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h)", + "{Object (Optional)} `keyModifiers` Optional key modifiers object. False entries can be ommitted", + "{boolean} `keyModifiers.shift` Trigger Shift Key", + "{boolean} `keyModifiers.alt` Trigger Alt Key", + "{boolean} `keyModifiers.control` Trigger Control (Ctrl) Key", + "{boolean} `keyModifiers.command` Trigger Command Key (Mac)" + ], + "api": "requests", + "name": "TriggerHotkeyBySequence", + "category": "general", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "keyId", + "description": "Main key identifier (e.g. `OBS_KEY_A` for key \"A\"). Available identifiers [here](https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h)" + }, + { + "type": "Object (Optional)", + "name": "keyModifiers", + "description": "Optional key modifiers object. False entries can be ommitted" + }, + { + "type": "boolean", + "name": "keyModifiers.shift", + "description": "Trigger Shift Key" + }, + { + "type": "boolean", + "name": "keyModifiers.alt", + "description": "Trigger Alt Key" + }, + { + "type": "boolean", + "name": "keyModifiers.control", + "description": "Trigger Control (Ctrl) Key" + }, + { + "type": "boolean", + "name": "keyModifiers.command", + "description": "Trigger Command Key (Mac)" + } + ], + "names": [ + { + "name": "", + "description": "TriggerHotkeyBySequence" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "TriggerHotkeyBySequence" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Executes a list of requests sequentially (one-by-one on the same thread).", + "param": [ + "{Array} `requests` Array of requests to perform. Executed in order.", + "{String} `requests.*.request-type` Request type. Eg. `GetVersion`.", + "{String (Optional)} `requests.*.message-id` ID of the individual request. Can be any string and not required to be unique. Defaults to empty string if not specified.", + "{boolean (Optional)} `abortOnFail` Stop processing batch requests if one returns a failure." + ], + "return": [ + "{Array} `results` Batch requests results, ordered sequentially.", + "{String} `results.*.message-id` ID of the individual request which was originally provided by the client.", + "{String} `results.*.status` Status response as string. Either `ok` or `error`.", + "{String (Optional)} `results.*.error` Error message accompanying an `error` status." + ], + "api": "requests", + "name": "ExecuteBatch", + "category": "general", + "since": "4.9.0", + "returns": [ + { + "type": "Array", + "name": "results", + "description": "Batch requests results, ordered sequentially." + }, + { + "type": "String", + "name": "results.*.message-id", + "description": "ID of the individual request which was originally provided by the client." + }, + { + "type": "String", + "name": "results.*.status", + "description": "Status response as string. Either `ok` or `error`." + }, + { + "type": "String (Optional)", + "name": "results.*.error", + "description": "Error message accompanying an `error` status." + } + ], + "params": [ + { + "type": "Array", + "name": "requests", + "description": "Array of requests to perform. Executed in order." + }, + { + "type": "String", + "name": "requests.*.request-type", + "description": "Request type. Eg. `GetVersion`." + }, + { + "type": "String (Optional)", + "name": "requests.*.message-id", + "description": "ID of the individual request. Can be any string and not required to be unique. Defaults to empty string if not specified." + }, + { + "type": "boolean (Optional)", + "name": "abortOnFail", + "description": "Stop processing batch requests if one returns a failure." + } + ], + "names": [ + { + "name": "", + "description": "ExecuteBatch" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "ExecuteBatch" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Waits for the specified duration. Designed to be used in `ExecuteBatch` operations.", + "param": "{int} `sleepMillis` Delay in milliseconds to wait before continuing.", + "api": "requests", + "name": "Sleep", + "category": "general", + "since": "unreleased", + "params": [ + { + "type": "int", + "name": "sleepMillis", + "description": "Delay in milliseconds to wait before continuing." + } + ], + "names": [ + { + "name": "", + "description": "Sleep" + } + ], + "categories": [ + { + "name": "", + "description": "general" + } + ], + "sinces": [ + { + "name": "", + "description": "unreleased" + } + ], + "heading": { + "level": 2, + "text": "Sleep" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "media control": [ + { + "subheads": [], + "description": "Pause or play a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)", + "param": [ + "{String} `sourceName` Source name.", + "{boolean} `playPause` Whether to pause or play the source. `false` for play, `true` for pause." + ], + "api": "requests", + "name": "PlayPauseMedia", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "boolean", + "name": "playPause", + "description": "Whether to pause or play the source. `false` for play, `true` for pause." + } + ], + "names": [ + { + "name": "", + "description": "PlayPauseMedia" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "PlayPauseMedia" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Restart a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)", + "param": "{String} `sourceName` Source name.", + "api": "requests", + "name": "RestartMedia", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "RestartMedia" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "RestartMedia" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Stop a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)", + "param": "{String} `sourceName` Source name.", + "api": "requests", + "name": "StopMedia", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "StopMedia" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "StopMedia" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Skip to the next media item in the playlist. Supports only vlc media source (as of OBS v25.0.8)", + "param": "{String} `sourceName` Source name.", + "api": "requests", + "name": "NextMedia", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "NextMedia" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "NextMedia" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Go to the previous media item in the playlist. Supports only vlc media source (as of OBS v25.0.8)", + "param": "{String} `sourceName` Source name.", + "api": "requests", + "name": "PreviousMedia", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "PreviousMedia" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "PreviousMedia" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the length of media in milliseconds. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)\nNote: For some reason, for the first 5 or so seconds that the media is playing, the total duration can be off by upwards of 50ms.", + "param": "{String} `sourceName` Source name.", + "return": "{int} `mediaDuration` The total length of media in milliseconds..", + "api": "requests", + "name": "GetMediaDuration", + "category": "media control", + "since": "4.9.0", + "returns": [ + { + "type": "int", + "name": "mediaDuration", + "description": "The total length of media in milliseconds.." + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetMediaDuration" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetMediaDuration" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current timestamp of media in milliseconds. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)", + "param": "{String} `sourceName` Source name.", + "return": "{int} `timestamp` The time in milliseconds since the start of the media.", + "api": "requests", + "name": "GetMediaTime", + "category": "media control", + "since": "4.9.0", + "returns": [ + { + "type": "int", + "name": "timestamp", + "description": "The time in milliseconds since the start of the media." + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetMediaTime" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetMediaTime" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the timestamp of a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)", + "param": [ + "{String} `sourceName` Source name.", + "{int} `timestamp` Milliseconds to set the timestamp to." + ], + "api": "requests", + "name": "SetMediaTime", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "int", + "name": "timestamp", + "description": "Milliseconds to set the timestamp to." + } + ], + "names": [ + { + "name": "", + "description": "SetMediaTime" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "SetMediaTime" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Scrub media using a supplied offset. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)\nNote: Due to processing/network delays, this request is not perfect. The processing rate of this request has also not been tested.", + "param": [ + "{String} `sourceName` Source name.", + "{int} `timeOffset` Millisecond offset (positive or negative) to offset the current media position." + ], + "api": "requests", + "name": "ScrubMedia", + "category": "media control", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "int", + "name": "timeOffset", + "description": "Millisecond offset (positive or negative) to offset the current media position." + } + ], + "names": [ + { + "name": "", + "description": "ScrubMedia" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "ScrubMedia" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current playing state of a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8)", + "param": "{String} `sourceName` Source name.", + "return": "{String} `mediaState` The media state of the provided source. States: `none`, `playing`, `opening`, `buffering`, `paused`, `stopped`, `ended`, `error`, `unknown`", + "api": "requests", + "name": "GetMediaState", + "category": "media control", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "mediaState", + "description": "The media state of the provided source. States: `none`, `playing`, `opening`, `buffering`, `paused`, `stopped`, `ended`, `error`, `unknown`" + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetMediaState" + } + ], + "categories": [ + { + "name": "", + "description": "media control" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetMediaState" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "sources": [ + { + "subheads": [], + "description": "List the media state of all media sources (vlc and media source)", + "return": [ + "{Array} `mediaSources` Array of sources", + "{String} `mediaSources.*.sourceName` Unique source name", + "{String} `mediaSources.*.sourceKind` Unique source internal type (a.k.a `ffmpeg_source` or `vlc_source`)", + "{String} `mediaSources.*.mediaState` The current state of media for that source. States: `none`, `playing`, `opening`, `buffering`, `paused`, `stopped`, `ended`, `error`, `unknown`" + ], + "api": "requests", + "name": "GetMediaSourcesList", + "category": "sources", + "since": "4.9.0", + "returns": [ + { + "type": "Array", + "name": "mediaSources", + "description": "Array of sources" + }, + { + "type": "String", + "name": "mediaSources.*.sourceName", + "description": "Unique source name" + }, + { + "type": "String", + "name": "mediaSources.*.sourceKind", + "description": "Unique source internal type (a.k.a `ffmpeg_source` or `vlc_source`)" + }, + { + "type": "String", + "name": "mediaSources.*.mediaState", + "description": "The current state of media for that source. States: `none`, `playing`, `opening`, `buffering`, `paused`, `stopped`, `ended`, `error`, `unknown`" + } + ], + "names": [ + { + "name": "", + "description": "GetMediaSourcesList" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetMediaSourcesList" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Create a source and add it as a sceneitem to a scene.", + "param": [ + "{String} `sourceName` Source name.", + "{String} `sourceKind` Source kind, Eg. `vlc_source`.", + "{String} `sceneName` Scene to add the new source to.", + "{Object (optional)} `sourceSettings` Source settings data.", + "{boolean (optional)} `setVisible` Set the created SceneItem as visible or not. Defaults to true" + ], + "return": "{int} `itemId` ID of the SceneItem in the scene.", + "api": "requests", + "name": "CreateSource", + "category": "sources", + "since": "4.9.0", + "returns": [ + { + "type": "int", + "name": "itemId", + "description": "ID of the SceneItem in the scene." + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "String", + "name": "sourceKind", + "description": "Source kind, Eg. `vlc_source`." + }, + { + "type": "String", + "name": "sceneName", + "description": "Scene to add the new source to." + }, + { + "type": "Object (optional)", + "name": "sourceSettings", + "description": "Source settings data." + }, + { + "type": "boolean (optional)", + "name": "setVisible", + "description": "Set the created SceneItem as visible or not. Defaults to true" + } + ], + "names": [ + { + "name": "", + "description": "CreateSource" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "CreateSource" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "List all sources available in the running OBS instance", + "return": [ + "{Array} `sources` Array of sources", + "{String} `sources.*.name` Unique source name", + "{String} `sources.*.typeId` Non-unique source internal type (a.k.a kind)", + "{String} `sources.*.type` Source type. Value is one of the following: \"input\", \"filter\", \"transition\", \"scene\" or \"unknown\"" + ], + "api": "requests", + "name": "GetSourcesList", + "category": "sources", + "since": "4.3.0", + "returns": [ + { + "type": "Array", + "name": "sources", + "description": "Array of sources" + }, + { + "type": "String", + "name": "sources.*.name", + "description": "Unique source name" + }, + { + "type": "String", + "name": "sources.*.typeId", + "description": "Non-unique source internal type (a.k.a kind)" + }, + { + "type": "String", + "name": "sources.*.type", + "description": "Source type. Value is one of the following: \"input\", \"filter\", \"transition\", \"scene\" or \"unknown\"" + } + ], + "names": [ + { + "name": "", + "description": "GetSourcesList" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourcesList" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get a list of all available sources types", + "return": [ + "{Array} `types` Array of source types", + "{String} `types.*.typeId` Non-unique internal source type ID", + "{String} `types.*.displayName` Display name of the source type", + "{String} `types.*.type` Type. Value is one of the following: \"input\", \"filter\", \"transition\" or \"other\"", + "{Object} `types.*.defaultSettings` Default settings of this source type", + "{Object} `types.*.caps` Source type capabilities", + "{Boolean} `types.*.caps.isAsync` True if source of this type provide frames asynchronously", + "{Boolean} `types.*.caps.hasVideo` True if sources of this type provide video", + "{Boolean} `types.*.caps.hasAudio` True if sources of this type provide audio", + "{Boolean} `types.*.caps.canInteract` True if interaction with this sources of this type is possible", + "{Boolean} `types.*.caps.isComposite` True if sources of this type composite one or more sub-sources", + "{Boolean} `types.*.caps.doNotDuplicate` True if sources of this type should not be fully duplicated", + "{Boolean} `types.*.caps.doNotSelfMonitor` True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be" + ], + "api": "requests", + "name": "GetSourceTypesList", + "category": "sources", + "since": "4.3.0", + "returns": [ + { + "type": "Array", + "name": "types", + "description": "Array of source types" + }, + { + "type": "String", + "name": "types.*.typeId", + "description": "Non-unique internal source type ID" + }, + { + "type": "String", + "name": "types.*.displayName", + "description": "Display name of the source type" + }, + { + "type": "String", + "name": "types.*.type", + "description": "Type. Value is one of the following: \"input\", \"filter\", \"transition\" or \"other\"" + }, + { + "type": "Object", + "name": "types.*.defaultSettings", + "description": "Default settings of this source type" + }, + { + "type": "Object", + "name": "types.*.caps", + "description": "Source type capabilities" + }, + { + "type": "Boolean", + "name": "types.*.caps.isAsync", + "description": "True if source of this type provide frames asynchronously" + }, + { + "type": "Boolean", + "name": "types.*.caps.hasVideo", + "description": "True if sources of this type provide video" + }, + { + "type": "Boolean", + "name": "types.*.caps.hasAudio", + "description": "True if sources of this type provide audio" + }, + { + "type": "Boolean", + "name": "types.*.caps.canInteract", + "description": "True if interaction with this sources of this type is possible" + }, + { + "type": "Boolean", + "name": "types.*.caps.isComposite", + "description": "True if sources of this type composite one or more sub-sources" + }, + { + "type": "Boolean", + "name": "types.*.caps.doNotDuplicate", + "description": "True if sources of this type should not be fully duplicated" + }, + { + "type": "Boolean", + "name": "types.*.caps.doNotSelfMonitor", + "description": "True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be" + } + ], + "names": [ + { + "name": "", + "description": "GetSourceTypesList" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourceTypesList" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the volume of the specified source. Default response uses mul format, NOT SLIDER PERCENTAGE.", + "param": [ + "{String} `source` Source name.", + "{boolean (optional)} `useDecibel` Output volume in decibels of attenuation instead of amplitude/mul." + ], + "return": [ + "{String} `name` Source name.", + "{double} `volume` Volume of the source. Between `0.0` and `20.0` if using mul, under `26.0` if using dB.", + "{boolean} `muted` Indicates whether the source is muted." + ], + "api": "requests", + "name": "GetVolume", + "category": "sources", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Source name." + }, + { + "type": "double", + "name": "volume", + "description": "Volume of the source. Between `0.0` and `20.0` if using mul, under `26.0` if using dB." + }, + { + "type": "boolean", + "name": "muted", + "description": "Indicates whether the source is muted." + } + ], + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "boolean (optional)", + "name": "useDecibel", + "description": "Output volume in decibels of attenuation instead of amplitude/mul." + } + ], + "names": [ + { + "name": "", + "description": "GetVolume" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "GetVolume" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the volume of the specified source. Default request format uses mul, NOT SLIDER PERCENTAGE.", + "param": [ + "{String} `source` Source name.", + "{double} `volume` Desired volume. Must be between `0.0` and `20.0` for mul, and under 26.0 for dB. OBS will interpret dB values under -100.0 as Inf. Note: The OBS volume sliders only reach a maximum of 1.0mul/0.0dB, however OBS actually supports larger values.", + "{boolean (optional)} `useDecibel` Interperet `volume` data as decibels instead of amplitude/mul." + ], + "api": "requests", + "name": "SetVolume", + "category": "sources", + "since": "4.0.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "double", + "name": "volume", + "description": "Desired volume. Must be between `0.0` and `20.0` for mul, and under 26.0 for dB. OBS will interpret dB values under -100.0 as Inf. Note: The OBS volume sliders only reach a maximum of 1.0mul/0.0dB, however OBS actually supports larger values." + }, + { + "type": "boolean (optional)", + "name": "useDecibel", + "description": "Interperet `volume` data as decibels instead of amplitude/mul." + } + ], + "names": [ + { + "name": "", + "description": "SetVolume" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SetVolume" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the mute status of a specified source.", + "param": "{String} `source` Source name.", + "return": [ + "{String} `name` Source name.", + "{boolean} `muted` Mute status of the source." + ], + "api": "requests", + "name": "GetMute", + "category": "sources", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Source name." + }, + { + "type": "boolean", + "name": "muted", + "description": "Mute status of the source." + } + ], + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetMute" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "GetMute" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Sets the mute status of a specified source.", + "param": [ + "{String} `source` Source name.", + "{boolean} `mute` Desired mute status." + ], + "api": "requests", + "name": "SetMute", + "category": "sources", + "since": "4.0.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "boolean", + "name": "mute", + "description": "Desired mute status." + } + ], + "names": [ + { + "name": "", + "description": "SetMute" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SetMute" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Inverts the mute status of a specified source.", + "param": "{String} `source` Source name.", + "api": "requests", + "name": "ToggleMute", + "category": "sources", + "since": "4.0.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "ToggleMute" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "ToggleMute" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the audio's active status of a specified source.", + "param": "{String} `sourceName` Source name.", + "return": "{boolean} `audioActive` Audio active status of the source.", + "api": "requests", + "name": "GetAudioActive", + "category": "sources", + "since": "4.9.0", + "returns": [ + { + "type": "boolean", + "name": "audioActive", + "description": "Audio active status of the source." + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetAudioActive" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetAudioActive" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: If the new name already exists as a source, obs-websocket will return an error.", + "param": [ + "{String} `sourceName` Source name.", + "{String} `newName` New source name." + ], + "api": "requests", + "name": "SetSourceName", + "category": "sources", + "since": "4.8.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "String", + "name": "newName", + "description": "New source name." + } + ], + "names": [ + { + "name": "", + "description": "SetSourceName" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "SetSourceName" + }, + "lead": "Sets (aka rename) the name of a source. Also works with scenes since scenes are technically sources in OBS.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the audio sync offset of a specified source.", + "param": [ + "{String} `source` Source name.", + "{int} `offset` The desired audio sync offset (in nanoseconds)." + ], + "api": "requests", + "name": "SetSyncOffset", + "category": "sources", + "since": "4.2.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "int", + "name": "offset", + "description": "The desired audio sync offset (in nanoseconds)." + } + ], + "names": [ + { + "name": "", + "description": "SetSyncOffset" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "SetSyncOffset" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the audio sync offset of a specified source.", + "param": "{String} `source` Source name.", + "return": [ + "{String} `name` Source name.", + "{int} `offset` The audio sync offset (in nanoseconds)." + ], + "api": "requests", + "name": "GetSyncOffset", + "category": "sources", + "since": "4.2.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Source name." + }, + { + "type": "int", + "name": "offset", + "description": "The audio sync offset (in nanoseconds)." + } + ], + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetSyncOffset" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "GetSyncOffset" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get settings of the specified source", + "param": [ + "{String} `sourceName` Source name.", + "{String (optional)} `sourceType` Type of the specified source. Useful for type-checking if you expect a specific settings schema." + ], + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceType` Type of the specified source", + "{Object} `sourceSettings` Source settings (varies between source types, may require some probing around)." + ], + "api": "requests", + "name": "GetSourceSettings", + "category": "sources", + "since": "4.3.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceType", + "description": "Type of the specified source" + }, + { + "type": "Object", + "name": "sourceSettings", + "description": "Source settings (varies between source types, may require some probing around)." + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "String (optional)", + "name": "sourceType", + "description": "Type of the specified source. Useful for type-checking if you expect a specific settings schema." + } + ], + "names": [ + { + "name": "", + "description": "GetSourceSettings" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourceSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set settings of the specified source.", + "param": [ + "{String} `sourceName` Source name.", + "{String (optional)} `sourceType` Type of the specified source. Useful for type-checking to avoid settings a set of settings incompatible with the actual source's type.", + "{Object} `sourceSettings` Source settings (varies between source types, may require some probing around)." + ], + "return": [ + "{String} `sourceName` Source name", + "{String} `sourceType` Type of the specified source", + "{Object} `sourceSettings` Updated source settings" + ], + "api": "requests", + "name": "SetSourceSettings", + "category": "sources", + "since": "4.3.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "sourceType", + "description": "Type of the specified source" + }, + { + "type": "Object", + "name": "sourceSettings", + "description": "Updated source settings" + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "String (optional)", + "name": "sourceType", + "description": "Type of the specified source. Useful for type-checking to avoid settings a set of settings incompatible with the actual source's type." + }, + { + "type": "Object", + "name": "sourceSettings", + "description": "Source settings (varies between source types, may require some probing around)." + } + ], + "names": [ + { + "name": "", + "description": "SetSourceSettings" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "SetSourceSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current properties of a Text GDI Plus source.", + "param": "{String} `source` Source name.", + "return": [ + "{String} `source` Source name.", + "{String} `align` Text Alignment (\"left\", \"center\", \"right\").", + "{int} `bk_color` Background color.", + "{int} `bk_opacity` Background opacity (0-100).", + "{boolean} `chatlog` Chat log.", + "{int} `chatlog_lines` Chat log lines.", + "{int} `color` Text color.", + "{boolean} `extents` Extents wrap.", + "{int} `extents_cx` Extents cx.", + "{int} `extents_cy` Extents cy.", + "{String} `file` File path name.", + "{boolean} `read_from_file` Read text from the specified file.", + "{Object} `font` Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`", + "{String} `font.face` Font face.", + "{int} `font.flags` Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`", + "{int} `font.size` Font text size.", + "{String} `font.style` Font Style (unknown function).", + "{boolean} `gradient` Gradient enabled.", + "{int} `gradient_color` Gradient color.", + "{float} `gradient_dir` Gradient direction.", + "{int} `gradient_opacity` Gradient opacity (0-100).", + "{boolean} `outline` Outline.", + "{int} `outline_color` Outline color.", + "{int} `outline_size` Outline size.", + "{int} `outline_opacity` Outline opacity (0-100).", + "{String} `text` Text content to be displayed.", + "{String} `valign` Text vertical alignment (\"top\", \"center\", \"bottom\").", + "{boolean} `vertical` Vertical text enabled." + ], + "api": "requests", + "name": "GetTextGDIPlusProperties", + "category": "sources", + "since": "4.1.0", + "returns": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "String", + "name": "align", + "description": "Text Alignment (\"left\", \"center\", \"right\")." + }, + { + "type": "int", + "name": "bk_color", + "description": "Background color." + }, + { + "type": "int", + "name": "bk_opacity", + "description": "Background opacity (0-100)." + }, + { + "type": "boolean", + "name": "chatlog", + "description": "Chat log." + }, + { + "type": "int", + "name": "chatlog_lines", + "description": "Chat log lines." + }, + { + "type": "int", + "name": "color", + "description": "Text color." + }, + { + "type": "boolean", + "name": "extents", + "description": "Extents wrap." + }, + { + "type": "int", + "name": "extents_cx", + "description": "Extents cx." + }, + { + "type": "int", + "name": "extents_cy", + "description": "Extents cy." + }, + { + "type": "String", + "name": "file", + "description": "File path name." + }, + { + "type": "boolean", + "name": "read_from_file", + "description": "Read text from the specified file." + }, + { + "type": "Object", + "name": "font", + "description": "Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`" + }, + { + "type": "String", + "name": "font.face", + "description": "Font face." + }, + { + "type": "int", + "name": "font.flags", + "description": "Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`" + }, + { + "type": "int", + "name": "font.size", + "description": "Font text size." + }, + { + "type": "String", + "name": "font.style", + "description": "Font Style (unknown function)." + }, + { + "type": "boolean", + "name": "gradient", + "description": "Gradient enabled." + }, + { + "type": "int", + "name": "gradient_color", + "description": "Gradient color." + }, + { + "type": "float", + "name": "gradient_dir", + "description": "Gradient direction." + }, + { + "type": "int", + "name": "gradient_opacity", + "description": "Gradient opacity (0-100)." + }, + { + "type": "boolean", + "name": "outline", + "description": "Outline." + }, + { + "type": "int", + "name": "outline_color", + "description": "Outline color." + }, + { + "type": "int", + "name": "outline_size", + "description": "Outline size." + }, + { + "type": "int", + "name": "outline_opacity", + "description": "Outline opacity (0-100)." + }, + { + "type": "String", + "name": "text", + "description": "Text content to be displayed." + }, + { + "type": "String", + "name": "valign", + "description": "Text vertical alignment (\"top\", \"center\", \"bottom\")." + }, + { + "type": "boolean", + "name": "vertical", + "description": "Vertical text enabled." + } + ], + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetTextGDIPlusProperties" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetTextGDIPlusProperties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the current properties of a Text GDI Plus source.", + "param": [ + "{String} `source` Name of the source.", + "{String (optional)} `align` Text Alignment (\"left\", \"center\", \"right\").", + "{int (optional)} `bk_color` Background color.", + "{int (optional)} `bk_opacity` Background opacity (0-100).", + "{boolean (optional)} `chatlog` Chat log.", + "{int (optional)} `chatlog_lines` Chat log lines.", + "{int (optional)} `color` Text color.", + "{boolean (optional)} `extents` Extents wrap.", + "{int (optional)} `extents_cx` Extents cx.", + "{int (optional)} `extents_cy` Extents cy.", + "{String (optional)} `file` File path name.", + "{boolean (optional)} `read_from_file` Read text from the specified file.", + "{Object (optional)} `font` Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`", + "{String (optional)} `font.face` Font face.", + "{int (optional)} `font.flags` Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`", + "{int (optional)} `font.size` Font text size.", + "{String (optional)} `font.style` Font Style (unknown function).", + "{boolean (optional)} `gradient` Gradient enabled.", + "{int (optional)} `gradient_color` Gradient color.", + "{float (optional)} `gradient_dir` Gradient direction.", + "{int (optional)} `gradient_opacity` Gradient opacity (0-100).", + "{boolean (optional)} `outline` Outline.", + "{int (optional)} `outline_color` Outline color.", + "{int (optional)} `outline_size` Outline size.", + "{int (optional)} `outline_opacity` Outline opacity (0-100).", + "{String (optional)} `text` Text content to be displayed.", + "{String (optional)} `valign` Text vertical alignment (\"top\", \"center\", \"bottom\").", + "{boolean (optional)} `vertical` Vertical text enabled.", + "{boolean (optional)} `render` Visibility of the scene item." + ], + "api": "requests", + "name": "SetTextGDIPlusProperties", + "category": "sources", + "since": "4.1.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Name of the source." + }, + { + "type": "String (optional)", + "name": "align", + "description": "Text Alignment (\"left\", \"center\", \"right\")." + }, + { + "type": "int (optional)", + "name": "bk_color", + "description": "Background color." + }, + { + "type": "int (optional)", + "name": "bk_opacity", + "description": "Background opacity (0-100)." + }, + { + "type": "boolean (optional)", + "name": "chatlog", + "description": "Chat log." + }, + { + "type": "int (optional)", + "name": "chatlog_lines", + "description": "Chat log lines." + }, + { + "type": "int (optional)", + "name": "color", + "description": "Text color." + }, + { + "type": "boolean (optional)", + "name": "extents", + "description": "Extents wrap." + }, + { + "type": "int (optional)", + "name": "extents_cx", + "description": "Extents cx." + }, + { + "type": "int (optional)", + "name": "extents_cy", + "description": "Extents cy." + }, + { + "type": "String (optional)", + "name": "file", + "description": "File path name." + }, + { + "type": "boolean (optional)", + "name": "read_from_file", + "description": "Read text from the specified file." + }, + { + "type": "Object (optional)", + "name": "font", + "description": "Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`" + }, + { + "type": "String (optional)", + "name": "font.face", + "description": "Font face." + }, + { + "type": "int (optional)", + "name": "font.flags", + "description": "Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`" + }, + { + "type": "int (optional)", + "name": "font.size", + "description": "Font text size." + }, + { + "type": "String (optional)", + "name": "font.style", + "description": "Font Style (unknown function)." + }, + { + "type": "boolean (optional)", + "name": "gradient", + "description": "Gradient enabled." + }, + { + "type": "int (optional)", + "name": "gradient_color", + "description": "Gradient color." + }, + { + "type": "float (optional)", + "name": "gradient_dir", + "description": "Gradient direction." + }, + { + "type": "int (optional)", + "name": "gradient_opacity", + "description": "Gradient opacity (0-100)." + }, + { + "type": "boolean (optional)", + "name": "outline", + "description": "Outline." + }, + { + "type": "int (optional)", + "name": "outline_color", + "description": "Outline color." + }, + { + "type": "int (optional)", + "name": "outline_size", + "description": "Outline size." + }, + { + "type": "int (optional)", + "name": "outline_opacity", + "description": "Outline opacity (0-100)." + }, + { + "type": "String (optional)", + "name": "text", + "description": "Text content to be displayed." + }, + { + "type": "String (optional)", + "name": "valign", + "description": "Text vertical alignment (\"top\", \"center\", \"bottom\")." + }, + { + "type": "boolean (optional)", + "name": "vertical", + "description": "Vertical text enabled." + }, + { + "type": "boolean (optional)", + "name": "render", + "description": "Visibility of the scene item." + } + ], + "names": [ + { + "name": "", + "description": "SetTextGDIPlusProperties" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "SetTextGDIPlusProperties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current properties of a Text Freetype 2 source.", + "param": "{String} `source` Source name.", + "return": [ + "{String} `source` Source name", + "{int} `color1` Gradient top color.", + "{int} `color2` Gradient bottom color.", + "{int} `custom_width` Custom width (0 to disable).", + "{boolean} `drop_shadow` Drop shadow.", + "{Object} `font` Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`", + "{String} `font.face` Font face.", + "{int} `font.flags` Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`", + "{int} `font.size` Font text size.", + "{String} `font.style` Font Style (unknown function).", + "{boolean} `from_file` Read text from the specified file.", + "{boolean} `log_mode` Chat log.", + "{boolean} `outline` Outline.", + "{String} `text` Text content to be displayed.", + "{String} `text_file` File path.", + "{boolean} `word_wrap` Word wrap." + ], + "api": "requests", + "name": "GetTextFreetype2Properties", + "category": "sources", + "since": "4.5.0", + "returns": [ + { + "type": "String", + "name": "source", + "description": "Source name" + }, + { + "type": "int", + "name": "color1", + "description": "Gradient top color." + }, + { + "type": "int", + "name": "color2", + "description": "Gradient bottom color." + }, + { + "type": "int", + "name": "custom_width", + "description": "Custom width (0 to disable)." + }, + { + "type": "boolean", + "name": "drop_shadow", + "description": "Drop shadow." + }, + { + "type": "Object", + "name": "font", + "description": "Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`" + }, + { + "type": "String", + "name": "font.face", + "description": "Font face." + }, + { + "type": "int", + "name": "font.flags", + "description": "Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`" + }, + { + "type": "int", + "name": "font.size", + "description": "Font text size." + }, + { + "type": "String", + "name": "font.style", + "description": "Font Style (unknown function)." + }, + { + "type": "boolean", + "name": "from_file", + "description": "Read text from the specified file." + }, + { + "type": "boolean", + "name": "log_mode", + "description": "Chat log." + }, + { + "type": "boolean", + "name": "outline", + "description": "Outline." + }, + { + "type": "String", + "name": "text", + "description": "Text content to be displayed." + }, + { + "type": "String", + "name": "text_file", + "description": "File path." + }, + { + "type": "boolean", + "name": "word_wrap", + "description": "Word wrap." + } + ], + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetTextFreetype2Properties" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "GetTextFreetype2Properties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the current properties of a Text Freetype 2 source.", + "param": [ + "{String} `source` Source name.", + "{int (optional)} `color1` Gradient top color.", + "{int (optional)} `color2` Gradient bottom color.", + "{int (optional)} `custom_width` Custom width (0 to disable).", + "{boolean (optional)} `drop_shadow` Drop shadow.", + "{Object (optional)} `font` Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`", + "{String (optional)} `font.face` Font face.", + "{int (optional)} `font.flags` Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`", + "{int (optional)} `font.size` Font text size.", + "{String (optional)} `font.style` Font Style (unknown function).", + "{boolean (optional)} `from_file` Read text from the specified file.", + "{boolean (optional)} `log_mode` Chat log.", + "{boolean (optional)} `outline` Outline.", + "{String (optional)} `text` Text content to be displayed.", + "{String (optional)} `text_file` File path.", + "{boolean (optional)} `word_wrap` Word wrap." + ], + "api": "requests", + "name": "SetTextFreetype2Properties", + "category": "sources", + "since": "4.5.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "int (optional)", + "name": "color1", + "description": "Gradient top color." + }, + { + "type": "int (optional)", + "name": "color2", + "description": "Gradient bottom color." + }, + { + "type": "int (optional)", + "name": "custom_width", + "description": "Custom width (0 to disable)." + }, + { + "type": "boolean (optional)", + "name": "drop_shadow", + "description": "Drop shadow." + }, + { + "type": "Object (optional)", + "name": "font", + "description": "Holds data for the font. Ex: `\"font\": { \"face\": \"Arial\", \"flags\": 0, \"size\": 150, \"style\": \"\" }`" + }, + { + "type": "String (optional)", + "name": "font.face", + "description": "Font face." + }, + { + "type": "int (optional)", + "name": "font.flags", + "description": "Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8`" + }, + { + "type": "int (optional)", + "name": "font.size", + "description": "Font text size." + }, + { + "type": "String (optional)", + "name": "font.style", + "description": "Font Style (unknown function)." + }, + { + "type": "boolean (optional)", + "name": "from_file", + "description": "Read text from the specified file." + }, + { + "type": "boolean (optional)", + "name": "log_mode", + "description": "Chat log." + }, + { + "type": "boolean (optional)", + "name": "outline", + "description": "Outline." + }, + { + "type": "String (optional)", + "name": "text", + "description": "Text content to be displayed." + }, + { + "type": "String (optional)", + "name": "text_file", + "description": "File path." + }, + { + "type": "boolean (optional)", + "name": "word_wrap", + "description": "Word wrap." + } + ], + "names": [ + { + "name": "", + "description": "SetTextFreetype2Properties" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "SetTextFreetype2Properties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get current properties for a Browser Source.", + "param": "{String} `source` Source name.", + "return": [ + "{String} `source` Source name.", + "{boolean} `is_local_file` Indicates that a local file is in use.", + "{String} `local_file` file path.", + "{String} `url` Url.", + "{String} `css` CSS to inject.", + "{int} `width` Width.", + "{int} `height` Height.", + "{int} `fps` Framerate.", + "{boolean} `shutdown` Indicates whether the source should be shutdown when not visible." + ], + "api": "requests", + "name": "GetBrowserSourceProperties", + "category": "sources", + "since": "4.1.0", + "deprecated": "Since 4.8.0. Prefer the use of GetSourceSettings. Will be removed in v5.0.0", + "returns": [ + { + "type": "String", + "name": "source", + "description": "Source name." + }, + { + "type": "boolean", + "name": "is_local_file", + "description": "Indicates that a local file is in use." + }, + { + "type": "String", + "name": "local_file", + "description": "file path." + }, + { + "type": "String", + "name": "url", + "description": "Url." + }, + { + "type": "String", + "name": "css", + "description": "CSS to inject." + }, + { + "type": "int", + "name": "width", + "description": "Width." + }, + { + "type": "int", + "name": "height", + "description": "Height." + }, + { + "type": "int", + "name": "fps", + "description": "Framerate." + }, + { + "type": "boolean", + "name": "shutdown", + "description": "Indicates whether the source should be shutdown when not visible." + } + ], + "params": [ + { + "type": "String", + "name": "source", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetBrowserSourceProperties" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "deprecateds": [ + { + "name": "", + "description": "Since 4.8.0. Prefer the use of GetSourceSettings. Will be removed in v5.0.0" + } + ], + "heading": { + "level": 2, + "text": "GetBrowserSourceProperties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set current properties for a Browser Source.", + "param": [ + "{String} `source` Name of the source.", + "{boolean (optional)} `is_local_file` Indicates that a local file is in use.", + "{String (optional)} `local_file` file path.", + "{String (optional)} `url` Url.", + "{String (optional)} `css` CSS to inject.", + "{int (optional)} `width` Width.", + "{int (optional)} `height` Height.", + "{int (optional)} `fps` Framerate.", + "{boolean (optional)} `shutdown` Indicates whether the source should be shutdown when not visible.", + "{boolean (optional)} `render` Visibility of the scene item." + ], + "api": "requests", + "name": "SetBrowserSourceProperties", + "category": "sources", + "deprecated": "Since 4.8.0. Prefer the use of SetSourceSettings. Will be removed in v5.0.0", + "since": "4.1.0", + "params": [ + { + "type": "String", + "name": "source", + "description": "Name of the source." + }, + { + "type": "boolean (optional)", + "name": "is_local_file", + "description": "Indicates that a local file is in use." + }, + { + "type": "String (optional)", + "name": "local_file", + "description": "file path." + }, + { + "type": "String (optional)", + "name": "url", + "description": "Url." + }, + { + "type": "String (optional)", + "name": "css", + "description": "CSS to inject." + }, + { + "type": "int (optional)", + "name": "width", + "description": "Width." + }, + { + "type": "int (optional)", + "name": "height", + "description": "Height." + }, + { + "type": "int (optional)", + "name": "fps", + "description": "Framerate." + }, + { + "type": "boolean (optional)", + "name": "shutdown", + "description": "Indicates whether the source should be shutdown when not visible." + }, + { + "type": "boolean (optional)", + "name": "render", + "description": "Visibility of the scene item." + } + ], + "names": [ + { + "name": "", + "description": "SetBrowserSourceProperties" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "deprecateds": [ + { + "name": "", + "description": "Since 4.8.0. Prefer the use of SetSourceSettings. Will be removed in v5.0.0" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "SetBrowserSourceProperties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get configured special sources like Desktop Audio and Mic/Aux sources.", + "return": [ + "{String (optional)} `desktop-1` Name of the first Desktop Audio capture source.", + "{String (optional)} `desktop-2` Name of the second Desktop Audio capture source.", + "{String (optional)} `mic-1` Name of the first Mic/Aux input source.", + "{String (optional)} `mic-2` Name of the second Mic/Aux input source.", + "{String (optional)} `mic-3` NAme of the third Mic/Aux input source." + ], + "api": "requests", + "name": "GetSpecialSources", + "category": "sources", + "since": "4.1.0", + "returns": [ + { + "type": "String (optional)", + "name": "desktop-1", + "description": "Name of the first Desktop Audio capture source." + }, + { + "type": "String (optional)", + "name": "desktop-2", + "description": "Name of the second Desktop Audio capture source." + }, + { + "type": "String (optional)", + "name": "mic-1", + "description": "Name of the first Mic/Aux input source." + }, + { + "type": "String (optional)", + "name": "mic-2", + "description": "Name of the second Mic/Aux input source." + }, + { + "type": "String (optional)", + "name": "mic-3", + "description": "NAme of the third Mic/Aux input source." + } + ], + "names": [ + { + "name": "", + "description": "GetSpecialSources" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetSpecialSources" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "List filters applied to a source", + "param": "{String} `sourceName` Source name", + "return": [ + "{Array} `filters` List of filters for the specified source", + "{Boolean} `filters.*.enabled` Filter status (enabled or not)", + "{String} `filters.*.type` Filter type", + "{String} `filters.*.name` Filter name", + "{Object} `filters.*.settings` Filter settings" + ], + "api": "requests", + "name": "GetSourceFilters", + "category": "sources", + "since": "4.5.0", + "returns": [ + { + "type": "Array", + "name": "filters", + "description": "List of filters for the specified source" + }, + { + "type": "Boolean", + "name": "filters.*.enabled", + "description": "Filter status (enabled or not)" + }, + { + "type": "String", + "name": "filters.*.type", + "description": "Filter type" + }, + { + "type": "String", + "name": "filters.*.name", + "description": "Filter name" + }, + { + "type": "Object", + "name": "filters.*.settings", + "description": "Filter settings" + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + } + ], + "names": [ + { + "name": "", + "description": "GetSourceFilters" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourceFilters" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "List filters applied to a source", + "param": [ + "{String} `sourceName` Source name", + "{String} `filterName` Source filter name" + ], + "return": [ + "{Boolean} `enabled` Filter status (enabled or not)", + "{String} `type` Filter type", + "{String} `name` Filter name", + "{Object} `settings` Filter settings" + ], + "api": "requests", + "name": "GetSourceFilterInfo", + "category": "sources", + "since": "4.7.0", + "returns": [ + { + "type": "Boolean", + "name": "enabled", + "description": "Filter status (enabled or not)" + }, + { + "type": "String", + "name": "type", + "description": "Filter type" + }, + { + "type": "String", + "name": "name", + "description": "Filter name" + }, + { + "type": "Object", + "name": "settings", + "description": "Filter settings" + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Source filter name" + } + ], + "names": [ + { + "name": "", + "description": "GetSourceFilterInfo" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourceFilterInfo" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Add a new filter to a source. Available source types along with their settings properties are available from `GetSourceTypesList`.", + "param": [ + "{String} `sourceName` Name of the source on which the filter is added", + "{String} `filterName` Name of the new filter", + "{String} `filterType` Filter type", + "{Object} `filterSettings` Filter settings" + ], + "api": "requests", + "name": "AddFilterToSource", + "category": "sources", + "since": "4.5.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Name of the source on which the filter is added" + }, + { + "type": "String", + "name": "filterName", + "description": "Name of the new filter" + }, + { + "type": "String", + "name": "filterType", + "description": "Filter type" + }, + { + "type": "Object", + "name": "filterSettings", + "description": "Filter settings" + } + ], + "names": [ + { + "name": "", + "description": "AddFilterToSource" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "AddFilterToSource" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Remove a filter from a source", + "param": [ + "{String} `sourceName` Name of the source from which the specified filter is removed", + "{String} `filterName` Name of the filter to remove" + ], + "api": "requests", + "name": "RemoveFilterFromSource", + "category": "sources", + "since": "4.5.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Name of the source from which the specified filter is removed" + }, + { + "type": "String", + "name": "filterName", + "description": "Name of the filter to remove" + } + ], + "names": [ + { + "name": "", + "description": "RemoveFilterFromSource" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "RemoveFilterFromSource" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Move a filter in the chain (absolute index positioning)", + "param": [ + "{String} `sourceName` Name of the source to which the filter belongs", + "{String} `filterName` Name of the filter to reorder", + "{Integer} `newIndex` Desired position of the filter in the chain" + ], + "api": "requests", + "name": "ReorderSourceFilter", + "category": "sources", + "since": "4.5.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Name of the source to which the filter belongs" + }, + { + "type": "String", + "name": "filterName", + "description": "Name of the filter to reorder" + }, + { + "type": "Integer", + "name": "newIndex", + "description": "Desired position of the filter in the chain" + } + ], + "names": [ + { + "name": "", + "description": "ReorderSourceFilter" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "ReorderSourceFilter" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Move a filter in the chain (relative positioning)", + "param": [ + "{String} `sourceName` Name of the source to which the filter belongs", + "{String} `filterName` Name of the filter to reorder", + "{String} `movementType` How to move the filter around in the source's filter chain. Either \"up\", \"down\", \"top\" or \"bottom\"." + ], + "api": "requests", + "name": "MoveSourceFilter", + "category": "sources", + "since": "4.5.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Name of the source to which the filter belongs" + }, + { + "type": "String", + "name": "filterName", + "description": "Name of the filter to reorder" + }, + { + "type": "String", + "name": "movementType", + "description": "How to move the filter around in the source's filter chain. Either \"up\", \"down\", \"top\" or \"bottom\"." + } + ], + "names": [ + { + "name": "", + "description": "MoveSourceFilter" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "MoveSourceFilter" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Update settings of a filter", + "param": [ + "{String} `sourceName` Name of the source to which the filter belongs", + "{String} `filterName` Name of the filter to reconfigure", + "{Object} `filterSettings` New settings. These will be merged to the current filter settings." + ], + "api": "requests", + "name": "SetSourceFilterSettings", + "category": "sources", + "since": "4.5.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Name of the source to which the filter belongs" + }, + { + "type": "String", + "name": "filterName", + "description": "Name of the filter to reconfigure" + }, + { + "type": "Object", + "name": "filterSettings", + "description": "New settings. These will be merged to the current filter settings." + } + ], + "names": [ + { + "name": "", + "description": "SetSourceFilterSettings" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "SetSourceFilterSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Change the visibility/enabled state of a filter", + "param": [ + "{String} `sourceName` Source name", + "{String} `filterName` Source filter name", + "{Boolean} `filterEnabled` New filter state" + ], + "api": "requests", + "name": "SetSourceFilterVisibility", + "category": "sources", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "filterName", + "description": "Source filter name" + }, + { + "type": "Boolean", + "name": "filterEnabled", + "description": "New filter state" + } + ], + "names": [ + { + "name": "", + "description": "SetSourceFilterVisibility" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "SetSourceFilterVisibility" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the audio monitoring type of the specified source.", + "param": "{String} `sourceName` Source name.", + "return": "{String} `monitorType` The monitor type in use. Options: `none`, `monitorOnly`, `monitorAndOutput`.", + "api": "requests", + "name": "GetAudioMonitorType", + "category": "sources", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "monitorType", + "description": "The monitor type in use. Options: `none`, `monitorOnly`, `monitorAndOutput`." + } + ], + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "GetAudioMonitorType" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "GetAudioMonitorType" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the audio monitoring type of the specified source.", + "param": [ + "{String} `sourceName` Source name.", + "{String} `monitorType` The monitor type to use. Options: `none`, `monitorOnly`, `monitorAndOutput`." + ], + "api": "requests", + "name": "SetAudioMonitorType", + "category": "sources", + "since": "4.8.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + }, + { + "type": "String", + "name": "monitorType", + "description": "The monitor type to use. Options: `none`, `monitorOnly`, `monitorAndOutput`." + } + ], + "names": [ + { + "name": "", + "description": "SetAudioMonitorType" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "SetAudioMonitorType" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the default settings for a given source type.", + "param": "{String} `sourceKind` Source kind. Also called \"source id\" in libobs terminology.", + "return": [ + "{String} `sourceKind` Source kind. Same value as the `sourceKind` parameter.", + "{Object} `defaultSettings` Settings object for source." + ], + "api": "requests", + "name": "GetSourceDefaultSettings", + "category": "sources", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sourceKind", + "description": "Source kind. Same value as the `sourceKind` parameter." + }, + { + "type": "Object", + "name": "defaultSettings", + "description": "Settings object for source." + } + ], + "params": [ + { + "type": "String", + "name": "sourceKind", + "description": "Source kind. Also called \"source id\" in libobs terminology." + } + ], + "names": [ + { + "name": "", + "description": "GetSourceDefaultSettings" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetSourceDefaultSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nAt least `embedPictureFormat` or `saveToFilePath` must be specified.\n\nClients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is\npreserved if only one of these two parameters is specified.", + "param": [ + "{String (optional)} `sourceName` Source name. Note: Since scenes are also sources, you can also provide a scene name. If not provided, the currently active scene is used.", + "{String (optional)} `embedPictureFormat` Format of the Data URI encoded picture. Can be \"png\", \"jpg\", \"jpeg\" or \"bmp\" (or any other value supported by Qt's Image module)", + "{String (optional)} `saveToFilePath` Full file path (file extension included) where the captured image is to be saved. Can be in a format different from `pictureFormat`. Can be a relative path.", + "{String (optional)} `fileFormat` Format to save the image file as (one of the values provided in the `supported-image-export-formats` response field of `GetVersion`). If not specified, tries to guess based on file extension.", + "{int (optional)} `compressionQuality` Compression ratio between -1 and 100 to write the image with. -1 is automatic, 1 is smallest file/most compression, 100 is largest file/least compression. Varies with image type.", + "{int (optional)} `width` Screenshot width. Defaults to the source's base width.", + "{int (optional)} `height` Screenshot height. Defaults to the source's base height." + ], + "return": [ + "{String} `sourceName` Source name", + "{String} `img` Image Data URI (if `embedPictureFormat` was specified in the request)", + "{String} `imageFile` Absolute path to the saved image file (if `saveToFilePath` was specified in the request)" + ], + "api": "requests", + "name": "TakeSourceScreenshot", + "category": "sources", + "since": "4.6.0", + "returns": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name" + }, + { + "type": "String", + "name": "img", + "description": "Image Data URI (if `embedPictureFormat` was specified in the request)" + }, + { + "type": "String", + "name": "imageFile", + "description": "Absolute path to the saved image file (if `saveToFilePath` was specified in the request)" + } + ], + "params": [ + { + "type": "String (optional)", + "name": "sourceName", + "description": "Source name. Note: Since scenes are also sources, you can also provide a scene name. If not provided, the currently active scene is used." + }, + { + "type": "String (optional)", + "name": "embedPictureFormat", + "description": "Format of the Data URI encoded picture. Can be \"png\", \"jpg\", \"jpeg\" or \"bmp\" (or any other value supported by Qt's Image module)" + }, + { + "type": "String (optional)", + "name": "saveToFilePath", + "description": "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." + }, + { + "type": "String (optional)", + "name": "fileFormat", + "description": "Format to save the image file as (one of the values provided in the `supported-image-export-formats` response field of `GetVersion`). If not specified, tries to guess based on file extension." + }, + { + "type": "int (optional)", + "name": "compressionQuality", + "description": "Compression ratio between -1 and 100 to write the image with. -1 is automatic, 1 is smallest file/most compression, 100 is largest file/least compression. Varies with image type." + }, + { + "type": "int (optional)", + "name": "width", + "description": "Screenshot width. Defaults to the source's base width." + }, + { + "type": "int (optional)", + "name": "height", + "description": "Screenshot height. Defaults to the source's base height." + } + ], + "names": [ + { + "name": "", + "description": "TakeSourceScreenshot" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "TakeSourceScreenshot" + }, + "lead": "Takes a picture snapshot of a source and then can either or both: \t- Send it over as a Data URI (base64-encoded data) in the response (by specifying `embedPictureFormat` in the request) \t- Save it to disk (by specifying `saveToFilePath` in the request)", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Refreshes the specified browser source.", + "param": "{String} `sourceName` Source name.", + "api": "requests", + "name": "RefreshBrowserSource", + "category": "sources", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sourceName", + "description": "Source name." + } + ], + "names": [ + { + "name": "", + "description": "RefreshBrowserSource" + } + ], + "categories": [ + { + "name": "", + "description": "sources" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "RefreshBrowserSource" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "outputs": [ + { + "subheads": [], + "description": "List existing outputs", + "return": "{Array} `outputs` Outputs list", + "api": "requests", + "name": "ListOutputs", + "category": "outputs", + "since": "4.7.0", + "returns": [ + { + "type": "Array", + "name": "outputs", + "description": "Outputs list" + } + ], + "names": [ + { + "name": "", + "description": "ListOutputs" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "ListOutputs" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get information about a single output", + "param": "{String} `outputName` Output name", + "return": "{Output} `outputInfo` Output info", + "api": "requests", + "name": "GetOutputInfo", + "category": "outputs", + "since": "4.7.0", + "returns": [ + { + "type": "Output", + "name": "outputInfo", + "description": "Output info" + } + ], + "params": [ + { + "type": "String", + "name": "outputName", + "description": "Output name" + } + ], + "names": [ + { + "name": "", + "description": "GetOutputInfo" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "GetOutputInfo" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: Controlling outputs is an experimental feature of obs-websocket. Some plugins which add outputs to OBS may not function properly when they are controlled in this way.", + "param": "{String} `outputName` Output name", + "api": "requests", + "name": "StartOutput", + "category": "outputs", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "outputName", + "description": "Output name" + } + ], + "names": [ + { + "name": "", + "description": "StartOutput" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "StartOutput" + }, + "lead": "Start an output", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: Controlling outputs is an experimental feature of obs-websocket. Some plugins which add outputs to OBS may not function properly when they are controlled in this way.", + "param": [ + "{String} `outputName` Output name", + "{boolean (optional)} `force` Force stop (default: false)" + ], + "api": "requests", + "name": "StopOutput", + "category": "outputs", + "since": "4.7.0", + "params": [ + { + "type": "String", + "name": "outputName", + "description": "Output name" + }, + { + "type": "boolean (optional)", + "name": "force", + "description": "Force stop (default: false)" + } + ], + "names": [ + { + "name": "", + "description": "StopOutput" + } + ], + "categories": [ + { + "name": "", + "description": "outputs" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "StopOutput" + }, + "lead": "Stop an output", + "type": "class", + "examples": [] + } + ], + "profiles": [ + { + "subheads": [], + "description": "Set the currently active profile.", + "param": "{String} `profile-name` Name of the desired profile.", + "api": "requests", + "name": "SetCurrentProfile", + "category": "profiles", + "since": "4.0.0", + "params": [ + { + "type": "String", + "name": "profile-name", + "description": "Name of the desired profile." + } + ], + "names": [ + { + "name": "", + "description": "SetCurrentProfile" + } + ], + "categories": [ + { + "name": "", + "description": "profiles" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SetCurrentProfile" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the name of the current profile.", + "return": "{String} `profile-name` Name of the currently active profile.", + "api": "requests", + "name": "GetCurrentProfile", + "category": "profiles", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "profile-name", + "description": "Name of the currently active profile." + } + ], + "names": [ + { + "name": "", + "description": "GetCurrentProfile" + } + ], + "categories": [ + { + "name": "", + "description": "profiles" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "GetCurrentProfile" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get a list of available profiles.", + "return": [ + "{Array} `profiles` List of available profiles.", + "{String} `profiles.*.profile-name` Filter name" + ], + "api": "requests", + "name": "ListProfiles", + "category": "profiles", + "since": "4.0.0", + "returns": [ + { + "type": "Array", + "name": "profiles", + "description": "List of available profiles." + }, + { + "type": "String", + "name": "profiles.*.profile-name", + "description": "Filter name" + } + ], + "names": [ + { + "name": "", + "description": "ListProfiles" + } + ], + "categories": [ + { + "name": "", + "description": "profiles" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "ListProfiles" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "recording": [ + { + "subheads": [], + "description": "Get current recording status.", + "return": [ + "{boolean} `isRecording` Current recording status.", + "{boolean} `isRecordingPaused` Whether the recording is paused or not.", + "{String (optional)} `recordTimecode` Time elapsed since recording started (only present if currently recording).", + "{String (optional)} `recordingFilename` Absolute path to the recording file (only present if currently recording)." + ], + "api": "requests", + "name": "GetRecordingStatus", + "category": "recording", + "since": "4.9.0", + "returns": [ + { + "type": "boolean", + "name": "isRecording", + "description": "Current recording status." + }, + { + "type": "boolean", + "name": "isRecordingPaused", + "description": "Whether the recording is paused or not." + }, + { + "type": "String (optional)", + "name": "recordTimecode", + "description": "Time elapsed since recording started (only present if currently recording)." + }, + { + "type": "String (optional)", + "name": "recordingFilename", + "description": "Absolute path to the recording file (only present if currently recording)." + } + ], + "names": [ + { + "name": "", + "description": "GetRecordingStatus" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetRecordingStatus" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Toggle recording on or off (depending on the current recording state).", + "api": "requests", + "name": "StartStopRecording", + "category": "recording", + "since": "0.3", + "names": [ + { + "name": "", + "description": "StartStopRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StartStopRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Start recording.\nWill return an `error` if recording is already active.", + "api": "requests", + "name": "StartRecording", + "category": "recording", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "StartRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "StartRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Stop recording.\nWill return an `error` if recording is not active.", + "api": "requests", + "name": "StopRecording", + "category": "recording", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "StopRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "StopRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Pause the current recording.\nReturns an error if recording is not active or already paused.", + "api": "requests", + "name": "PauseRecording", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "PauseRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "PauseRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Resume/unpause the current recording (if paused).\nReturns an error if recording is not active or not paused.", + "api": "requests", + "name": "ResumeRecording", + "category": "recording", + "since": "4.7.0", + "names": [ + { + "name": "", + "description": "ResumeRecording" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.7.0" + } + ], + "heading": { + "level": 2, + "text": "ResumeRecording" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nNote: If `SetRecordingFolder` is called while a recording is\nin progress, the change won't be applied immediately and will be\neffective on the next recording.", + "param": "{String} `rec-folder` Path of the recording folder.", + "api": "requests", + "name": "SetRecordingFolder", + "category": "recording", + "since": "4.1.0", + "params": [ + { + "type": "String", + "name": "rec-folder", + "description": "Path of the recording folder." + } + ], + "names": [ + { + "name": "", + "description": "SetRecordingFolder" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "SetRecordingFolder" + }, + "lead": "In the current profile, sets the recording folder of the Simple and Advanced output modes to the specified value.", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the path of the current recording folder.", + "return": "{String} `rec-folder` Path of the recording folder.", + "api": "requests", + "name": "GetRecordingFolder", + "category": "recording", + "since": "4.1.0", + "returns": [ + { + "type": "String", + "name": "rec-folder", + "description": "Path of the recording folder." + } + ], + "names": [ + { + "name": "", + "description": "GetRecordingFolder" + } + ], + "categories": [ + { + "name": "", + "description": "recording" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetRecordingFolder" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "replay buffer": [ + { + "subheads": [], + "description": "Get the status of the OBS replay buffer.", + "return": "{boolean} `isReplayBufferActive` Current recording status.", + "api": "requests", + "name": "GetReplayBufferStatus", + "category": "replay buffer", + "since": "4.9.0", + "returns": [ + { + "type": "boolean", + "name": "isReplayBufferActive", + "description": "Current recording status." + } + ], + "names": [ + { + "name": "", + "description": "GetReplayBufferStatus" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetReplayBufferStatus" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Toggle the Replay Buffer on/off (depending on the current state of the replay buffer).", + "api": "requests", + "name": "StartStopReplayBuffer", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "StartStopReplayBuffer" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "StartStopReplayBuffer" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Start recording into the Replay Buffer.\nWill return an `error` if the Replay Buffer is already active or if the\n\"Save Replay Buffer\" hotkey is not set in OBS' settings.\nSetting this hotkey is mandatory, even when triggering saves only\nthrough obs-websocket.", + "api": "requests", + "name": "StartReplayBuffer", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "StartReplayBuffer" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "StartReplayBuffer" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Stop recording into the Replay Buffer.\nWill return an `error` if the Replay Buffer is not active.", + "api": "requests", + "name": "StopReplayBuffer", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "StopReplayBuffer" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "StopReplayBuffer" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Flush and save the contents of the Replay Buffer to disk. This is\nbasically the same as triggering the \"Save Replay Buffer\" hotkey.\nWill return an `error` if the Replay Buffer is not active.", + "api": "requests", + "name": "SaveReplayBuffer", + "category": "replay buffer", + "since": "4.2.0", + "names": [ + { + "name": "", + "description": "SaveReplayBuffer" + } + ], + "categories": [ + { + "name": "", + "description": "replay buffer" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "SaveReplayBuffer" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "scene collections": [ + { + "subheads": [], + "description": "Change the active scene collection.", + "param": "{String} `sc-name` Name of the desired scene collection.", + "api": "requests", + "name": "SetCurrentSceneCollection", + "category": "scene collections", + "since": "4.0.0", + "params": [ + { + "type": "String", + "name": "sc-name", + "description": "Name of the desired scene collection." + } + ], + "names": [ + { + "name": "", + "description": "SetCurrentSceneCollection" + } + ], + "categories": [ + { + "name": "", + "description": "scene collections" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SetCurrentSceneCollection" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the name of the current scene collection.", + "return": "{String} `sc-name` Name of the currently active scene collection.", + "api": "requests", + "name": "GetCurrentSceneCollection", + "category": "scene collections", + "since": "4.0.0", + "returns": [ + { + "type": "String", + "name": "sc-name", + "description": "Name of the currently active scene collection." + } + ], + "names": [ + { + "name": "", + "description": "GetCurrentSceneCollection" + } + ], + "categories": [ + { + "name": "", + "description": "scene collections" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "GetCurrentSceneCollection" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "List available scene collections", + "return": "{Array} `scene-collections` Scene collections list", + "api": "requests", + "name": "ListSceneCollections", + "category": "scene collections", + "since": "4.0.0", + "returns": [ + { + "type": "Array", + "name": "scene-collections", + "description": "Scene collections list" + } + ], + "names": [ + { + "name": "", + "description": "ListSceneCollections" + } + ], + "categories": [ + { + "name": "", + "description": "scene collections" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "ListSceneCollections" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "scene items": [ + { + "subheads": [], + "description": "Get a list of all scene items in a scene.", + "param": "{String (optional)} `sceneName` Name of the scene to get the list of scene items from. Defaults to the current scene if not specified.", + "return": [ + "{String} `sceneName` Name of the requested (or current) scene", + "{Array} `sceneItems` Array of scene items", + "{int} `sceneItems.*.itemId` Unique item id of the source item", + "{String} `sceneItems.*.sourceKind` ID if the scene item's source. For example `vlc_source` or `image_source`", + "{String} `sceneItems.*.sourceName` Name of the scene item's source", + "{String} `sceneItems.*.sourceType` Type of the scene item's source. Either `input`, `group`, or `scene`" + ], + "api": "requests", + "name": "GetSceneItemList", + "category": "scene items", + "since": "4.9.0", + "returns": [ + { + "type": "String", + "name": "sceneName", + "description": "Name of the requested (or current) scene" + }, + { + "type": "Array", + "name": "sceneItems", + "description": "Array of scene items" + }, + { + "type": "int", + "name": "sceneItems.*.itemId", + "description": "Unique item id of the source item" + }, + { + "type": "String", + "name": "sceneItems.*.sourceKind", + "description": "ID if the scene item's source. For example `vlc_source` or `image_source`" + }, + { + "type": "String", + "name": "sceneItems.*.sourceName", + "description": "Name of the scene item's source" + }, + { + "type": "String", + "name": "sceneItems.*.sourceType", + "description": "Type of the scene item's source. Either `input`, `group`, or `scene`" + } + ], + "params": [ + { + "type": "String (optional)", + "name": "sceneName", + "description": "Name of the scene to get the list of scene items from. Defaults to the current scene if not specified." + } + ], + "names": [ + { + "name": "", + "description": "GetSceneItemList" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetSceneItemList" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Gets the scene specific properties of the specified source item.\nCoordinates are relative to the item's parent (the scene or group it belongs to).", + "param": [ + "{String (optional)} `scene-name` Name of the scene the scene item belongs to. Defaults to the current scene.", + "{String | Object} `item` Scene Item name (if this field is a string) or specification (if it is an object).", + "{String (optional)} `item.name` Scene Item name (if the `item` field is an object)", + "{int (optional)} `item.id` Scene Item ID (if the `item` field is an object)" + ], + "return": [ + "{String} `name` Scene Item name.", + "{int} `itemId` Scene Item ID.", + "{double} `position.x` The x position of the source from the left.", + "{double} `position.y` The y position of the source from the top.", + "{int} `position.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.", + "{double} `rotation` The clockwise rotation of the item in degrees around the point of alignment.", + "{double} `scale.x` The x-scale factor of the source.", + "{double} `scale.y` The y-scale factor of the source.", + "{String} `scale.filter` The scale filter of the source. Can be \"OBS_SCALE_DISABLE\", \"OBS_SCALE_POINT\", \"OBS_SCALE_BICUBIC\", \"OBS_SCALE_BILINEAR\", \"OBS_SCALE_LANCZOS\" or \"OBS_SCALE_AREA\".", + "{int} `crop.top` The number of pixels cropped off the top of the source before scaling.", + "{int} `crop.right` The number of pixels cropped off the right of the source before scaling.", + "{int} `crop.bottom` The number of pixels cropped off the bottom of the source before scaling.", + "{int} `crop.left` The number of pixels cropped off the left of the source before scaling.", + "{bool} `visible` If the source is visible.", + "{bool} `muted` If the source is muted.", + "{bool} `locked` If the source's transform is locked.", + "{String} `bounds.type` Type of bounding box. Can be \"OBS_BOUNDS_STRETCH\", \"OBS_BOUNDS_SCALE_INNER\", \"OBS_BOUNDS_SCALE_OUTER\", \"OBS_BOUNDS_SCALE_TO_WIDTH\", \"OBS_BOUNDS_SCALE_TO_HEIGHT\", \"OBS_BOUNDS_MAX_ONLY\" or \"OBS_BOUNDS_NONE\".", + "{int} `bounds.alignment` Alignment of the bounding box.", + "{double} `bounds.x` Width of the bounding box.", + "{double} `bounds.y` Height of the bounding box.", + "{int} `sourceWidth` Base width (without scaling) of the source", + "{int} `sourceHeight` Base source (without scaling) of the source", + "{double} `width` Scene item width (base source width multiplied by the horizontal scaling factor)", + "{double} `height` Scene item height (base source height multiplied by the vertical scaling factor)", + "{String (optional)} `parentGroupName` Name of the item's parent (if this item belongs to a group)", + "{Array (optional)} `groupChildren` List of children (if this item is a group)" + ], + "api": "requests", + "name": "GetSceneItemProperties", + "category": "scene items", + "since": "4.3.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Scene Item name." + }, + { + "type": "int", + "name": "itemId", + "description": "Scene Item ID." + }, + { + "type": "double", + "name": "position.x", + "description": "The x position of the source from the left." + }, + { + "type": "double", + "name": "position.y", + "description": "The y position of the source from the top." + }, + { + "type": "int", + "name": "position.alignment", + "description": "The point on the source that the item is manipulated from. The sum of 1=Left or 2=Right, and 4=Top or 8=Bottom, or omit to center on that axis." + }, + { + "type": "double", + "name": "rotation", + "description": "The clockwise rotation of the item in degrees around the point of alignment." + }, + { + "type": "double", + "name": "scale.x", + "description": "The x-scale factor of the source." + }, + { + "type": "double", + "name": "scale.y", + "description": "The y-scale factor of the source." + }, + { + "type": "String", + "name": "scale.filter", + "description": "The scale filter of the source. Can be \"OBS_SCALE_DISABLE\", \"OBS_SCALE_POINT\", \"OBS_SCALE_BICUBIC\", \"OBS_SCALE_BILINEAR\", \"OBS_SCALE_LANCZOS\" or \"OBS_SCALE_AREA\"." + }, + { + "type": "int", + "name": "crop.top", + "description": "The number of pixels cropped off the top of the source before scaling." + }, + { + "type": "int", + "name": "crop.right", + "description": "The number of pixels cropped off the right of the source before scaling." + }, + { + "type": "int", + "name": "crop.bottom", + "description": "The number of pixels cropped off the bottom of the source before scaling." + }, + { + "type": "int", + "name": "crop.left", + "description": "The number of pixels cropped off the left of the source before scaling." + }, + { + "type": "bool", + "name": "visible", + "description": "If the source is visible." + }, + { + "type": "bool", + "name": "muted", + "description": "If the source is muted." + }, + { + "type": "bool", + "name": "locked", + "description": "If the source's transform is locked." + }, + { + "type": "String", + "name": "bounds.type", + "description": "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\"." + }, + { + "type": "int", + "name": "bounds.alignment", + "description": "Alignment of the bounding box." + }, + { + "type": "double", + "name": "bounds.x", + "description": "Width of the bounding box." + }, + { + "type": "double", + "name": "bounds.y", + "description": "Height of the bounding box." + }, + { + "type": "int", + "name": "sourceWidth", + "description": "Base width (without scaling) of the source" + }, + { + "type": "int", + "name": "sourceHeight", + "description": "Base source (without scaling) of the source" + }, + { + "type": "double", + "name": "width", + "description": "Scene item width (base source width multiplied by the horizontal scaling factor)" + }, + { + "type": "double", + "name": "height", + "description": "Scene item height (base source height multiplied by the vertical scaling factor)" + }, + { + "type": "String (optional)", + "name": "parentGroupName", + "description": "Name of the item's parent (if this item belongs to a group)" + }, + { + "type": "Array (optional)", + "name": "groupChildren", + "description": "List of children (if this item is a group)" + } + ], + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the scene item belongs to. Defaults to the current scene." + }, + { + "type": "String | Object", + "name": "item", + "description": "Scene Item name (if this field is a string) or specification (if it is an object)." + }, + { + "type": "String (optional)", + "name": "item.name", + "description": "Scene Item name (if the `item` field is an object)" + }, + { + "type": "int (optional)", + "name": "item.id", + "description": "Scene Item ID (if the `item` field is an object)" + } + ], + "names": [ + { + "name": "", + "description": "GetSceneItemProperties" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "GetSceneItemProperties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Sets the scene specific properties of a source. Unspecified properties will remain unchanged.\nCoordinates are relative to the item's parent (the scene or group it belongs to).", + "param": [ + "{String (optional)} `scene-name` Name of the scene the source item belongs to. Defaults to the current scene.", + "{String | Object} `item` Scene Item name (if this field is a string) or specification (if it is an object).", + "{String (optional)} `item.name` Scene Item name (if the `item` field is an object)", + "{int (optional)} `item.id` Scene Item ID (if the `item` field is an object)", + "{double (optional)} `position.x` The new x position of the source.", + "{double (optional)} `position.y` The new y position of the source.", + "{int (optional)} `position.alignment` The new alignment of the source.", + "{double (optional)} `rotation` The new clockwise rotation of the item in degrees.", + "{double (optional)} `scale.x` The new x scale of the item.", + "{double (optional)} `scale.y` The new y scale of the item.", + "{String (optional)} `scale.filter` The new scale filter of the source. Can be \"OBS_SCALE_DISABLE\", \"OBS_SCALE_POINT\", \"OBS_SCALE_BICUBIC\", \"OBS_SCALE_BILINEAR\", \"OBS_SCALE_LANCZOS\" or \"OBS_SCALE_AREA\".", + "{int (optional)} `crop.top` The new amount of pixels cropped off the top of the source before scaling.", + "{int (optional)} `crop.bottom` The new amount of pixels cropped off the bottom of the source before scaling.", + "{int (optional)} `crop.left` The new amount of pixels cropped off the left of the source before scaling.", + "{int (optional)} `crop.right` The new amount of pixels cropped off the right of the source before scaling.", + "{bool (optional)} `visible` The new visibility of the source. 'true' shows source, 'false' hides source.", + "{bool (optional)} `locked` The new locked status of the source. 'true' keeps it in its current position, 'false' allows movement.", + "{String (optional)} `bounds.type` The new bounds type of the source. Can be \"OBS_BOUNDS_STRETCH\", \"OBS_BOUNDS_SCALE_INNER\", \"OBS_BOUNDS_SCALE_OUTER\", \"OBS_BOUNDS_SCALE_TO_WIDTH\", \"OBS_BOUNDS_SCALE_TO_HEIGHT\", \"OBS_BOUNDS_MAX_ONLY\" or \"OBS_BOUNDS_NONE\".", + "{int (optional)} `bounds.alignment` The new alignment of the bounding box. (0-2, 4-6, 8-10)", + "{double (optional)} `bounds.x` The new width of the bounding box.", + "{double (optional)} `bounds.y` The new height of the bounding box." + ], + "api": "requests", + "name": "SetSceneItemProperties", + "category": "scene items", + "since": "4.3.0", + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the source item belongs to. Defaults to the current scene." + }, + { + "type": "String | Object", + "name": "item", + "description": "Scene Item name (if this field is a string) or specification (if it is an object)." + }, + { + "type": "String (optional)", + "name": "item.name", + "description": "Scene Item name (if the `item` field is an object)" + }, + { + "type": "int (optional)", + "name": "item.id", + "description": "Scene Item ID (if the `item` field is an object)" + }, + { + "type": "double (optional)", + "name": "position.x", + "description": "The new x position of the source." + }, + { + "type": "double (optional)", + "name": "position.y", + "description": "The new y position of the source." + }, + { + "type": "int (optional)", + "name": "position.alignment", + "description": "The new alignment of the source." + }, + { + "type": "double (optional)", + "name": "rotation", + "description": "The new clockwise rotation of the item in degrees." + }, + { + "type": "double (optional)", + "name": "scale.x", + "description": "The new x scale of the item." + }, + { + "type": "double (optional)", + "name": "scale.y", + "description": "The new y scale of the item." + }, + { + "type": "String (optional)", + "name": "scale.filter", + "description": "The new scale filter of the source. Can be \"OBS_SCALE_DISABLE\", \"OBS_SCALE_POINT\", \"OBS_SCALE_BICUBIC\", \"OBS_SCALE_BILINEAR\", \"OBS_SCALE_LANCZOS\" or \"OBS_SCALE_AREA\"." + }, + { + "type": "int (optional)", + "name": "crop.top", + "description": "The new amount of pixels cropped off the top of the source before scaling." + }, + { + "type": "int (optional)", + "name": "crop.bottom", + "description": "The new amount of pixels cropped off the bottom of the source before scaling." + }, + { + "type": "int (optional)", + "name": "crop.left", + "description": "The new amount of pixels cropped off the left of the source before scaling." + }, + { + "type": "int (optional)", + "name": "crop.right", + "description": "The new amount of pixels cropped off the right of the source before scaling." + }, + { + "type": "bool (optional)", + "name": "visible", + "description": "The new visibility of the source. 'true' shows source, 'false' hides source." + }, + { + "type": "bool (optional)", + "name": "locked", + "description": "The new locked status of the source. 'true' keeps it in its current position, 'false' allows movement." + }, + { + "type": "String (optional)", + "name": "bounds.type", + "description": "The new bounds type of the source. 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\"." + }, + { + "type": "int (optional)", + "name": "bounds.alignment", + "description": "The new alignment of the bounding box. (0-2, 4-6, 8-10)" + }, + { + "type": "double (optional)", + "name": "bounds.x", + "description": "The new width of the bounding box." + }, + { + "type": "double (optional)", + "name": "bounds.y", + "description": "The new height of the bounding box." + } + ], + "names": [ + { + "name": "", + "description": "SetSceneItemProperties" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.3.0" + } + ], + "heading": { + "level": 2, + "text": "SetSceneItemProperties" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Reset a scene item.", + "param": [ + "{String (optional)} `scene-name` Name of the scene the scene item belongs to. Defaults to the current scene.", + "{String | Object} `item` Scene Item name (if this field is a string) or specification (if it is an object).", + "{String (optional)} `item.name` Scene Item name (if the `item` field is an object)", + "{int (optional)} `item.id` Scene Item ID (if the `item` field is an object)" + ], + "api": "requests", + "name": "ResetSceneItem", + "category": "scene items", + "since": "4.2.0", + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the scene item belongs to. Defaults to the current scene." + }, + { + "type": "String | Object", + "name": "item", + "description": "Scene Item name (if this field is a string) or specification (if it is an object)." + }, + { + "type": "String (optional)", + "name": "item.name", + "description": "Scene Item name (if the `item` field is an object)" + }, + { + "type": "int (optional)", + "name": "item.id", + "description": "Scene Item ID (if the `item` field is an object)" + } + ], + "names": [ + { + "name": "", + "description": "ResetSceneItem" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.2.0" + } + ], + "heading": { + "level": 2, + "text": "ResetSceneItem" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Show or hide a specified source item in a specified scene.", + "param": [ + "{String (optional)} `scene-name` Name of the scene the scene item belongs to. Defaults to the currently active scene.", + "{String (optional)} `source` Scene Item name.", + "{int (optional)} `item` Scene Item id", + "{boolean} `render` true = shown ; false = hidden" + ], + "api": "requests", + "name": "SetSceneItemRender", + "category": "scene items", + "since": "0.3", + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the scene item belongs to. Defaults to the currently active scene." + }, + { + "type": "String (optional)", + "name": "source", + "description": "Scene Item name." + }, + { + "type": "int (optional)", + "name": "item", + "description": "Scene Item id" + }, + { + "type": "boolean", + "name": "render", + "description": "true = shown ; false = hidden" + } + ], + "names": [ + { + "name": "", + "description": "SetSceneItemRender" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "SetSceneItemRender" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Sets the coordinates of a specified source item.", + "param": [ + "{String (optional)} `scene-name` Name of the scene the scene item belongs to. Defaults to the current scene.", + "{String} `item` Scene Item name.", + "{double} `x` X coordinate.", + "{double} `y` Y coordinate." + ], + "api": "requests", + "name": "SetSceneItemPosition", + "category": "scene items", + "since": "4.0.0", + "deprecated": "Since 4.3.0. Prefer the use of SetSceneItemProperties.", + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the scene item belongs to. Defaults to the current scene." + }, + { + "type": "String", + "name": "item", + "description": "Scene Item name." + }, + { + "type": "double", + "name": "x", + "description": "X coordinate." + }, + { + "type": "double", + "name": "y", + "description": "Y coordinate." + } + ], + "names": [ + { + "name": "", + "description": "SetSceneItemPosition" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "deprecateds": [ + { + "name": "", + "description": "Since 4.3.0. Prefer the use of SetSceneItemProperties." + } + ], + "heading": { + "level": 2, + "text": "SetSceneItemPosition" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the transform of the specified source item.", + "param": [ + "{String (optional)} `scene-name` Name of the scene the scene item belongs to. Defaults to the current scene.", + "{String} `item` Scene Item name.", + "{double} `x-scale` Width scale factor.", + "{double} `y-scale` Height scale factor.", + "{double} `rotation` Source item rotation (in degrees)." + ], + "api": "requests", + "name": "SetSceneItemTransform", + "category": "scene items", + "since": "4.0.0", + "deprecated": "Since 4.3.0. Prefer the use of SetSceneItemProperties.", + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the scene item belongs to. Defaults to the current scene." + }, + { + "type": "String", + "name": "item", + "description": "Scene Item name." + }, + { + "type": "double", + "name": "x-scale", + "description": "Width scale factor." + }, + { + "type": "double", + "name": "y-scale", + "description": "Height scale factor." + }, + { + "type": "double", + "name": "rotation", + "description": "Source item rotation (in degrees)." + } + ], + "names": [ + { + "name": "", + "description": "SetSceneItemTransform" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "deprecateds": [ + { + "name": "", + "description": "Since 4.3.0. Prefer the use of SetSceneItemProperties." + } + ], + "heading": { + "level": 2, + "text": "SetSceneItemTransform" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Sets the crop coordinates of the specified source item.", + "param": [ + "{String (optional)} `scene-name` Name of the scene the scene item belongs to. Defaults to the current scene.", + "{String} `item` Scene Item name.", + "{int} `top` Pixel position of the top of the source item.", + "{int} `bottom` Pixel position of the bottom of the source item.", + "{int} `left` Pixel position of the left of the source item.", + "{int} `right` Pixel position of the right of the source item." + ], + "api": "requests", + "name": "SetSceneItemCrop", + "category": "scene items", + "since": "4.1.0", + "deprecated": "Since 4.3.0. Prefer the use of SetSceneItemProperties.", + "params": [ + { + "type": "String (optional)", + "name": "scene-name", + "description": "Name of the scene the scene item belongs to. Defaults to the current scene." + }, + { + "type": "String", + "name": "item", + "description": "Scene Item name." + }, + { + "type": "int", + "name": "top", + "description": "Pixel position of the top of the source item." + }, + { + "type": "int", + "name": "bottom", + "description": "Pixel position of the bottom of the source item." + }, + { + "type": "int", + "name": "left", + "description": "Pixel position of the left of the source item." + }, + { + "type": "int", + "name": "right", + "description": "Pixel position of the right of the source item." + } + ], + "names": [ + { + "name": "", + "description": "SetSceneItemCrop" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "deprecateds": [ + { + "name": "", + "description": "Since 4.3.0. Prefer the use of SetSceneItemProperties." + } + ], + "heading": { + "level": 2, + "text": "SetSceneItemCrop" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Deletes a scene item.", + "param": [ + "{String (optional)} `scene` Name of the scene the scene item belongs to. Defaults to the current scene.", + "{Object} `item` Scene item to delete (required)", + "{String} `item.name` Scene Item name (prefer `id`, including both is acceptable).", + "{int} `item.id` Scene Item ID." + ], + "api": "requests", + "name": "DeleteSceneItem", + "category": "scene items", + "since": "4.5.0", + "params": [ + { + "type": "String (optional)", + "name": "scene", + "description": "Name of the scene the scene item belongs to. Defaults to the current scene." + }, + { + "type": "Object", + "name": "item", + "description": "Scene item to delete (required)" + }, + { + "type": "String", + "name": "item.name", + "description": "Scene Item name (prefer `id`, including both is acceptable)." + }, + { + "type": "int", + "name": "item.id", + "description": "Scene Item ID." + } + ], + "names": [ + { + "name": "", + "description": "DeleteSceneItem" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "DeleteSceneItem" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Creates a scene item in a scene. In other words, this is how you add a source into a scene.", + "param": [ + "{String} `sceneName` Name of the scene to create the scene item in", + "{String} `sourceName` Name of the source to be added", + "{boolean} `setVisible` Whether to make the sceneitem visible on creation or not. Default `true`" + ], + "return": "{int} `itemId` Numerical ID of the created scene item", + "api": "requests", + "name": "AddSceneItem", + "category": "scene items", + "since": "4.9.0", + "returns": [ + { + "type": "int", + "name": "itemId", + "description": "Numerical ID of the created scene item" + } + ], + "params": [ + { + "type": "String", + "name": "sceneName", + "description": "Name of the scene to create the scene item in" + }, + { + "type": "String", + "name": "sourceName", + "description": "Name of the source to be added" + }, + { + "type": "boolean", + "name": "setVisible", + "description": "Whether to make the sceneitem visible on creation or not. Default `true`" + } + ], + "names": [ + { + "name": "", + "description": "AddSceneItem" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "AddSceneItem" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Duplicates a scene item.", + "param": [ + "{String (optional)} `fromScene` Name of the scene to copy the item from. Defaults to the current scene.", + "{String (optional)} `toScene` Name of the scene to create the item in. Defaults to the current scene.", + "{Object} `item` Scene Item to duplicate from the source scene (required)", + "{String} `item.name` Scene Item name (prefer `id`, including both is acceptable).", + "{int} `item.id` Scene Item ID." + ], + "return": [ + "{String} `scene` Name of the scene where the new item was created", + "{Object} `item` New item info", + "{int} `item.id` New item ID", + "{String} `item.name` New item name" + ], + "api": "requests", + "name": "DuplicateSceneItem", + "category": "scene items", + "since": "4.5.0", + "returns": [ + { + "type": "String", + "name": "scene", + "description": "Name of the scene where the new item was created" + }, + { + "type": "Object", + "name": "item", + "description": "New item info" + }, + { + "type": "int", + "name": "item.id", + "description": "New item ID" + }, + { + "type": "String", + "name": "item.name", + "description": "New item name" + } + ], + "params": [ + { + "type": "String (optional)", + "name": "fromScene", + "description": "Name of the scene to copy the item from. Defaults to the current scene." + }, + { + "type": "String (optional)", + "name": "toScene", + "description": "Name of the scene to create the item in. Defaults to the current scene." + }, + { + "type": "Object", + "name": "item", + "description": "Scene Item to duplicate from the source scene (required)" + }, + { + "type": "String", + "name": "item.name", + "description": "Scene Item name (prefer `id`, including both is acceptable)." + }, + { + "type": "int", + "name": "item.id", + "description": "Scene Item ID." + } + ], + "names": [ + { + "name": "", + "description": "DuplicateSceneItem" + } + ], + "categories": [ + { + "name": "", + "description": "scene items" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "DuplicateSceneItem" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "scenes": [ + { + "subheads": [], + "description": "Switch to the specified scene.", + "param": "{String} `scene-name` Name of the scene to switch to.", + "api": "requests", + "name": "SetCurrentScene", + "category": "scenes", + "since": "0.3", + "params": [ + { + "type": "String", + "name": "scene-name", + "description": "Name of the scene to switch to." + } + ], + "names": [ + { + "name": "", + "description": "SetCurrentScene" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "SetCurrentScene" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current scene's name and source items.", + "return": [ + "{String} `name` Name of the currently active scene.", + "{Array} `sources` Ordered list of the current scene's source items." + ], + "api": "requests", + "name": "GetCurrentScene", + "category": "scenes", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Name of the currently active scene." + }, + { + "type": "Array", + "name": "sources", + "description": "Ordered list of the current scene's source items." + } + ], + "names": [ + { + "name": "", + "description": "GetCurrentScene" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "GetCurrentScene" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get a list of scenes in the currently active profile.", + "return": [ + "{String} `current-scene` Name of the currently active scene.", + "{Array} `scenes` Ordered list of the current profile's scenes (See [GetCurrentScene](#getcurrentscene) for more information)." + ], + "api": "requests", + "name": "GetSceneList", + "category": "scenes", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "current-scene", + "description": "Name of the currently active scene." + }, + { + "type": "Array", + "name": "scenes", + "description": "Ordered list of the current profile's scenes (See [GetCurrentScene](#getcurrentscene) for more information)." + } + ], + "names": [ + { + "name": "", + "description": "GetSceneList" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "GetSceneList" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Create a new scene scene.", + "param": "{String} `sceneName` Name of the scene to create.", + "api": "requests", + "name": "CreateScene", + "category": "scenes", + "since": "4.9.0", + "params": [ + { + "type": "String", + "name": "sceneName", + "description": "Name of the scene to create." + } + ], + "names": [ + { + "name": "", + "description": "CreateScene" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "CreateScene" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Changes the order of scene items in the requested scene.", + "param": [ + "{String (optional)} `scene` Name of the scene to reorder (defaults to current).", + "{Array} `items` Ordered list of objects with name and/or id specified. Id preferred due to uniqueness per scene", + "{int (optional)} `items.*.id` Id of a specific scene item. Unique on a scene by scene basis.", + "{String (optional)} `items.*.name` Name of a scene item. Sufficiently unique if no scene items share sources within the scene." + ], + "api": "requests", + "name": "ReorderSceneItems", + "category": "scenes", + "since": "4.5.0", + "params": [ + { + "type": "String (optional)", + "name": "scene", + "description": "Name of the scene to reorder (defaults to current)." + }, + { + "type": "Array", + "name": "items", + "description": "Ordered list of objects with name and/or id specified. Id preferred due to uniqueness per scene" + }, + { + "type": "int (optional)", + "name": "items.*.id", + "description": "Id of a specific scene item. Unique on a scene by scene basis." + }, + { + "type": "String (optional)", + "name": "items.*.name", + "description": "Name of a scene item. Sufficiently unique if no scene items share sources within the scene." + } + ], + "names": [ + { + "name": "", + "description": "ReorderSceneItems" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.5.0" + } + ], + "heading": { + "level": 2, + "text": "ReorderSceneItems" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set a scene to use a specific transition override.", + "param": [ + "{String} `sceneName` Name of the scene to switch to.", + "{String} `transitionName` Name of the transition to use.", + "{int (Optional)} `transitionDuration` Duration in milliseconds of the transition if transition is not fixed. Defaults to the current duration specified in the UI if there is no current override and this value is not given." + ], + "api": "requests", + "name": "SetSceneTransitionOverride", + "category": "scenes", + "since": "4.8.0", + "params": [ + { + "type": "String", + "name": "sceneName", + "description": "Name of the scene to switch to." + }, + { + "type": "String", + "name": "transitionName", + "description": "Name of the transition to use." + }, + { + "type": "int (Optional)", + "name": "transitionDuration", + "description": "Duration in milliseconds of the transition if transition is not fixed. Defaults to the current duration specified in the UI if there is no current override and this value is not given." + } + ], + "names": [ + { + "name": "", + "description": "SetSceneTransitionOverride" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "SetSceneTransitionOverride" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Remove any transition override on a scene.", + "param": "{String} `sceneName` Name of the scene to switch to.", + "api": "requests", + "name": "RemoveSceneTransitionOverride", + "category": "scenes", + "since": "4.8.0", + "params": [ + { + "type": "String", + "name": "sceneName", + "description": "Name of the scene to switch to." + } + ], + "names": [ + { + "name": "", + "description": "RemoveSceneTransitionOverride" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "RemoveSceneTransitionOverride" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current scene transition override.", + "param": "{String} `sceneName` Name of the scene to switch to.", + "return": [ + "{String} `transitionName` Name of the current overriding transition. Empty string if no override is set.", + "{int} `transitionDuration` Transition duration. `-1` if no override is set." + ], + "api": "requests", + "name": "GetSceneTransitionOverride", + "category": "scenes", + "since": "4.8.0", + "returns": [ + { + "type": "String", + "name": "transitionName", + "description": "Name of the current overriding transition. Empty string if no override is set." + }, + { + "type": "int", + "name": "transitionDuration", + "description": "Transition duration. `-1` if no override is set." + } + ], + "params": [ + { + "type": "String", + "name": "sceneName", + "description": "Name of the scene to switch to." + } + ], + "names": [ + { + "name": "", + "description": "GetSceneTransitionOverride" + } + ], + "categories": [ + { + "name": "", + "description": "scenes" + } + ], + "sinces": [ + { + "name": "", + "description": "4.8.0" + } + ], + "heading": { + "level": 2, + "text": "GetSceneTransitionOverride" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "streaming": [ + { + "subheads": [], + "description": "Get current streaming and recording status.", + "return": [ + "{boolean} `streaming` Current streaming status.", + "{boolean} `recording` Current recording status.", + "{boolean} `recording-paused` If recording is paused.", + "{boolean} `preview-only` Always false. Retrocompatibility with OBSRemote.", + "{String (optional)} `stream-timecode` Time elapsed since streaming started (only present if currently streaming).", + "{String (optional)} `rec-timecode` Time elapsed since recording started (only present if currently recording)." + ], + "api": "requests", + "name": "GetStreamingStatus", + "category": "streaming", + "since": "0.3", + "returns": [ + { + "type": "boolean", + "name": "streaming", + "description": "Current streaming status." + }, + { + "type": "boolean", + "name": "recording", + "description": "Current recording status." + }, + { + "type": "boolean", + "name": "recording-paused", + "description": "If recording is paused." + }, + { + "type": "boolean", + "name": "preview-only", + "description": "Always false. Retrocompatibility with OBSRemote." + }, + { + "type": "String (optional)", + "name": "stream-timecode", + "description": "Time elapsed since streaming started (only present if currently streaming)." + }, + { + "type": "String (optional)", + "name": "rec-timecode", + "description": "Time elapsed since recording started (only present if currently recording)." + } + ], + "names": [ + { + "name": "", + "description": "GetStreamingStatus" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "GetStreamingStatus" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Toggle streaming on or off (depending on the current stream state).", + "api": "requests", + "name": "StartStopStreaming", + "category": "streaming", + "since": "0.3", + "names": [ + { + "name": "", + "description": "StartStopStreaming" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "StartStopStreaming" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Start streaming.\nWill return an `error` if streaming is already active.", + "param": [ + "{Object (optional)} `stream` Special stream configuration. Note: these won't be saved to OBS' configuration.", + "{String (optional)} `stream.type` If specified ensures the type of stream matches the given type (usually 'rtmp_custom' or 'rtmp_common'). If the currently configured stream type does not match the given stream type, all settings must be specified in the `settings` object or an error will occur when starting the stream.", + "{Object (optional)} `stream.metadata` Adds the given object parameters as encoded query string parameters to the 'key' of the RTMP stream. Used to pass data to the RTMP service about the streaming. May be any String, Numeric, or Boolean field.", + "{Object (optional)} `stream.settings` Settings for the stream.", + "{String (optional)} `stream.settings.server` The publish URL.", + "{String (optional)} `stream.settings.key` The publish key of the stream.", + "{boolean (optional)} `stream.settings.use_auth` Indicates whether authentication should be used when connecting to the streaming server.", + "{String (optional)} `stream.settings.username` If authentication is enabled, the username for the streaming server. Ignored if `use_auth` is not set to `true`.", + "{String (optional)} `stream.settings.password` If authentication is enabled, the password for the streaming server. Ignored if `use_auth` is not set to `true`." + ], + "api": "requests", + "name": "StartStreaming", + "category": "streaming", + "since": "4.1.0", + "params": [ + { + "type": "Object (optional)", + "name": "stream", + "description": "Special stream configuration. Note: these won't be saved to OBS' configuration." + }, + { + "type": "String (optional)", + "name": "stream.type", + "description": "If specified ensures the type of stream matches the given type (usually 'rtmp_custom' or 'rtmp_common'). If the currently configured stream type does not match the given stream type, all settings must be specified in the `settings` object or an error will occur when starting the stream." + }, + { + "type": "Object (optional)", + "name": "stream.metadata", + "description": "Adds the given object parameters as encoded query string parameters to the 'key' of the RTMP stream. Used to pass data to the RTMP service about the streaming. May be any String, Numeric, or Boolean field." + }, + { + "type": "Object (optional)", + "name": "stream.settings", + "description": "Settings for the stream." + }, + { + "type": "String (optional)", + "name": "stream.settings.server", + "description": "The publish URL." + }, + { + "type": "String (optional)", + "name": "stream.settings.key", + "description": "The publish key of the stream." + }, + { + "type": "boolean (optional)", + "name": "stream.settings.use_auth", + "description": "Indicates whether authentication should be used when connecting to the streaming server." + }, + { + "type": "String (optional)", + "name": "stream.settings.username", + "description": "If authentication is enabled, the username for the streaming server. Ignored if `use_auth` is not set to `true`." + }, + { + "type": "String (optional)", + "name": "stream.settings.password", + "description": "If authentication is enabled, the password for the streaming server. Ignored if `use_auth` is not set to `true`." + } + ], + "names": [ + { + "name": "", + "description": "StartStreaming" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "StartStreaming" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Stop streaming.\nWill return an `error` if streaming is not active.", + "api": "requests", + "name": "StopStreaming", + "category": "streaming", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "StopStreaming" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "StopStreaming" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Sets one or more attributes of the current streaming server settings. Any options not passed will remain unchanged. Returns the updated settings in response. If 'type' is different than the current streaming service type, all settings are required. Returns the full settings of the stream (the same as GetStreamSettings).", + "param": [ + "{String} `type` The type of streaming service configuration, usually `rtmp_custom` or `rtmp_common`.", + "{Object} `settings` The actual settings of the stream.", + "{String (optional)} `settings.server` The publish URL.", + "{String (optional)} `settings.key` The publish key.", + "{boolean (optional)} `settings.use_auth` Indicates whether authentication should be used when connecting to the streaming server.", + "{String (optional)} `settings.username` The username for the streaming service.", + "{String (optional)} `settings.password` The password for the streaming service.", + "{boolean} `save` Persist the settings to disk." + ], + "api": "requests", + "name": "SetStreamSettings", + "category": "streaming", + "since": "4.1.0", + "params": [ + { + "type": "String", + "name": "type", + "description": "The type of streaming service configuration, usually `rtmp_custom` or `rtmp_common`." + }, + { + "type": "Object", + "name": "settings", + "description": "The actual settings of the stream." + }, + { + "type": "String (optional)", + "name": "settings.server", + "description": "The publish URL." + }, + { + "type": "String (optional)", + "name": "settings.key", + "description": "The publish key." + }, + { + "type": "boolean (optional)", + "name": "settings.use_auth", + "description": "Indicates whether authentication should be used when connecting to the streaming server." + }, + { + "type": "String (optional)", + "name": "settings.username", + "description": "The username for the streaming service." + }, + { + "type": "String (optional)", + "name": "settings.password", + "description": "The password for the streaming service." + }, + { + "type": "boolean", + "name": "save", + "description": "Persist the settings to disk." + } + ], + "names": [ + { + "name": "", + "description": "SetStreamSettings" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "SetStreamSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current streaming server settings.", + "return": [ + "{String} `type` The type of streaming service configuration. Possible values: 'rtmp_custom' or 'rtmp_common'.", + "{Object} `settings` Stream settings object.", + "{String} `settings.server` The publish URL.", + "{String} `settings.key` The publish key of the stream.", + "{boolean} `settings.use_auth` Indicates whether authentication should be used when connecting to the streaming server.", + "{String} `settings.username` The username to use when accessing the streaming server. Only present if `use_auth` is `true`.", + "{String} `settings.password` The password to use when accessing the streaming server. Only present if `use_auth` is `true`." + ], + "api": "requests", + "name": "GetStreamSettings", + "category": "streaming", + "since": "4.1.0", + "returns": [ + { + "type": "String", + "name": "type", + "description": "The type of streaming service configuration. Possible values: 'rtmp_custom' or 'rtmp_common'." + }, + { + "type": "Object", + "name": "settings", + "description": "Stream settings object." + }, + { + "type": "String", + "name": "settings.server", + "description": "The publish URL." + }, + { + "type": "String", + "name": "settings.key", + "description": "The publish key of the stream." + }, + { + "type": "boolean", + "name": "settings.use_auth", + "description": "Indicates whether authentication should be used when connecting to the streaming server." + }, + { + "type": "String", + "name": "settings.username", + "description": "The username to use when accessing the streaming server. Only present if `use_auth` is `true`." + }, + { + "type": "String", + "name": "settings.password", + "description": "The password to use when accessing the streaming server. Only present if `use_auth` is `true`." + } + ], + "names": [ + { + "name": "", + "description": "GetStreamSettings" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetStreamSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Save the current streaming server settings to disk.", + "api": "requests", + "name": "SaveStreamSettings", + "category": "streaming", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "SaveStreamSettings" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "SaveStreamSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Send the provided text as embedded CEA-608 caption data.", + "param": "{String} `text` Captions text", + "api": "requests", + "name": "SendCaptions", + "category": "streaming", + "since": "4.6.0", + "params": [ + { + "type": "String", + "name": "text", + "description": "Captions text" + } + ], + "names": [ + { + "name": "", + "description": "SendCaptions" + } + ], + "categories": [ + { + "name": "", + "description": "streaming" + } + ], + "sinces": [ + { + "name": "", + "description": "4.6.0" + } + ], + "heading": { + "level": 2, + "text": "SendCaptions" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "studio mode": [ + { + "subheads": [], + "description": "Indicates if Studio Mode is currently enabled.", + "return": "{boolean} `studio-mode` Indicates if Studio Mode is enabled.", + "api": "requests", + "name": "GetStudioModeStatus", + "category": "studio mode", + "since": "4.1.0", + "returns": [ + { + "type": "boolean", + "name": "studio-mode", + "description": "Indicates if Studio Mode is enabled." + } + ], + "names": [ + { + "name": "", + "description": "GetStudioModeStatus" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetStudioModeStatus" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the name of the currently previewed scene and its list of sources.\nWill return an `error` if Studio Mode is not enabled.", + "return": [ + "{String} `name` The name of the active preview scene.", + "{Array} `sources`" + ], + "api": "requests", + "name": "GetPreviewScene", + "category": "studio mode", + "since": "4.1.0", + "returns": [ + { + "type": "String", + "name": "name", + "description": "The name of the active preview scene." + }, + { + "type": "Array", + "name": "sources", + "description": "" + } + ], + "names": [ + { + "name": "", + "description": "GetPreviewScene" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetPreviewScene" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the active preview scene.\nWill return an `error` if Studio Mode is not enabled.", + "param": "{String} `scene-name` The name of the scene to preview.", + "api": "requests", + "name": "SetPreviewScene", + "category": "studio mode", + "since": "4.1.0", + "params": [ + { + "type": "String", + "name": "scene-name", + "description": "The name of the scene to preview." + } + ], + "names": [ + { + "name": "", + "description": "SetPreviewScene" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "SetPreviewScene" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Transitions the currently previewed scene to the main output.\nWill return an `error` if Studio Mode is not enabled.", + "param": [ + "{Object (optional)} `with-transition` Change the active transition before switching scenes. Defaults to the active transition.", + "{String} `with-transition.name` Name of the transition.", + "{int (optional)} `with-transition.duration` Transition duration (in milliseconds)." + ], + "api": "requests", + "name": "TransitionToProgram", + "category": "studio mode", + "since": "4.1.0", + "params": [ + { + "type": "Object (optional)", + "name": "with-transition", + "description": "Change the active transition before switching scenes. Defaults to the active transition." + }, + { + "type": "String", + "name": "with-transition.name", + "description": "Name of the transition." + }, + { + "type": "int (optional)", + "name": "with-transition.duration", + "description": "Transition duration (in milliseconds)." + } + ], + "names": [ + { + "name": "", + "description": "TransitionToProgram" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "TransitionToProgram" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Enables Studio Mode.", + "api": "requests", + "name": "EnableStudioMode", + "category": "studio mode", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "EnableStudioMode" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "EnableStudioMode" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Disables Studio Mode.", + "api": "requests", + "name": "DisableStudioMode", + "category": "studio mode", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "DisableStudioMode" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "DisableStudioMode" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Toggles Studio Mode (depending on the current state of studio mode).", + "api": "requests", + "name": "ToggleStudioMode", + "category": "studio mode", + "since": "4.1.0", + "names": [ + { + "name": "", + "description": "ToggleStudioMode" + } + ], + "categories": [ + { + "name": "", + "description": "studio mode" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "ToggleStudioMode" + }, + "lead": "", + "type": "class", + "examples": [] + } + ], + "transitions": [ + { + "subheads": [], + "description": "List of all transitions available in the frontend's dropdown menu.", + "return": [ + "{String} `current-transition` Name of the currently active transition.", + "{Array} `transitions` List of transitions.", + "{String} `transitions.*.name` Name of the transition." + ], + "api": "requests", + "name": "GetTransitionList", + "category": "transitions", + "since": "4.1.0", + "returns": [ + { + "type": "String", + "name": "current-transition", + "description": "Name of the currently active transition." + }, + { + "type": "Array", + "name": "transitions", + "description": "List of transitions." + }, + { + "type": "String", + "name": "transitions.*.name", + "description": "Name of the transition." + } + ], + "names": [ + { + "name": "", + "description": "GetTransitionList" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetTransitionList" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the name of the currently selected transition in the frontend's dropdown menu.", + "return": [ + "{String} `name` Name of the selected transition.", + "{int (optional)} `duration` Transition duration (in milliseconds) if supported by the transition." + ], + "api": "requests", + "name": "GetCurrentTransition", + "category": "transitions", + "since": "0.3", + "returns": [ + { + "type": "String", + "name": "name", + "description": "Name of the selected transition." + }, + { + "type": "int (optional)", + "name": "duration", + "description": "Transition duration (in milliseconds) if supported by the transition." + } + ], + "names": [ + { + "name": "", + "description": "GetCurrentTransition" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "GetCurrentTransition" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the active transition.", + "param": "{String} `transition-name` The name of the transition.", + "api": "requests", + "name": "SetCurrentTransition", + "category": "transitions", + "since": "0.3", + "params": [ + { + "type": "String", + "name": "transition-name", + "description": "The name of the transition." + } + ], + "names": [ + { + "name": "", + "description": "SetCurrentTransition" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "0.3" + } + ], + "heading": { + "level": 2, + "text": "SetCurrentTransition" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Set the duration of the currently selected transition if supported.", + "param": "{int} `duration` Desired duration of the transition (in milliseconds).", + "api": "requests", + "name": "SetTransitionDuration", + "category": "transitions", + "since": "4.0.0", + "params": [ + { + "type": "int", + "name": "duration", + "description": "Desired duration of the transition (in milliseconds)." + } + ], + "names": [ + { + "name": "", + "description": "SetTransitionDuration" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.0.0" + } + ], + "heading": { + "level": 2, + "text": "SetTransitionDuration" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the duration of the currently selected transition if supported.", + "return": "{int} `transition-duration` Duration of the current transition (in milliseconds).", + "api": "requests", + "name": "GetTransitionDuration", + "category": "transitions", + "since": "4.1.0", + "returns": [ + { + "type": "int", + "name": "transition-duration", + "description": "Duration of the current transition (in milliseconds)." + } + ], + "names": [ + { + "name": "", + "description": "GetTransitionDuration" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.1.0" + } + ], + "heading": { + "level": 2, + "text": "GetTransitionDuration" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the position of the current transition.", + "return": "{double} `position` current transition position. This value will be between 0.0 and 1.0. Note: Transition returns 1.0 when not active.", + "api": "requests", + "name": "GetTransitionPosition", + "category": "transitions", + "since": "4.9.0", + "returns": [ + { + "type": "double", + "name": "position", + "description": "current transition position. This value will be between 0.0 and 1.0. Note: Transition returns 1.0 when not active." + } + ], + "names": [ + { + "name": "", + "description": "GetTransitionPosition" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetTransitionPosition" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Get the current settings of a transition", + "param": "{String} `transitionName` Transition name", + "return": "{Object} `transitionSettings` Current transition settings", + "api": "requests", + "name": "GetTransitionSettings", + "category": "transitions", + "since": "4.9.0", + "returns": [ + { + "type": "Object", + "name": "transitionSettings", + "description": "Current transition settings" + } + ], + "params": [ + { + "type": "String", + "name": "transitionName", + "description": "Transition name" + } + ], + "names": [ + { + "name": "", + "description": "GetTransitionSettings" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "GetTransitionSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Change the current settings of a transition", + "param": [ + "{String} `transitionName` Transition name", + "{Object} `transitionSettings` Transition settings (they can be partial)" + ], + "return": "{Object} `transitionSettings` Updated transition settings", + "api": "requests", + "name": "SetTransitionSettings", + "category": "transitions", + "since": "4.9.0", + "returns": [ + { + "type": "Object", + "name": "transitionSettings", + "description": "Updated transition settings" + } + ], + "params": [ + { + "type": "String", + "name": "transitionName", + "description": "Transition name" + }, + { + "type": "Object", + "name": "transitionSettings", + "description": "Transition settings (they can be partial)" + } + ], + "names": [ + { + "name": "", + "description": "SetTransitionSettings" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "SetTransitionSettings" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "Release the T-Bar (like a user releasing their mouse button after moving it).\n*YOU MUST CALL THIS if you called `SetTBarPosition` with the `release` parameter set to `false`.*", + "api": "requests", + "name": "ReleaseTBar", + "category": "transitions", + "since": "4.9.0", + "names": [ + { + "name": "", + "description": "ReleaseTBar" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "ReleaseTBar" + }, + "lead": "", + "type": "class", + "examples": [] + }, + { + "subheads": [], + "description": "\n\nIf your code needs to perform multiple successive T-Bar moves (e.g. : in an animation, or in response to a user moving a T-Bar control in your User Interface), set `release` to false and call `ReleaseTBar` later once the animation/interaction is over.", + "param": [ + "{double} `position` T-Bar position. This value must be between 0.0 and 1.0.", + "{boolean (optional)} `release` Whether or not the T-Bar gets released automatically after setting its new position (like a user releasing their mouse button after moving the T-Bar). Call `ReleaseTBar` manually if you set `release` to false. Defaults to true." + ], + "api": "requests", + "name": "SetTBarPosition", + "category": "transitions", + "since": "4.9.0", + "params": [ + { + "type": "double", + "name": "position", + "description": "T-Bar position. This value must be between 0.0 and 1.0." + }, + { + "type": "boolean (optional)", + "name": "release", + "description": "Whether or not the T-Bar gets released automatically after setting its new position (like a user releasing their mouse button after moving the T-Bar). Call `ReleaseTBar` manually if you set `release` to false. Defaults to true." + } + ], + "names": [ + { + "name": "", + "description": "SetTBarPosition" + } + ], + "categories": [ + { + "name": "", + "description": "transitions" + } + ], + "sinces": [ + { + "name": "", + "description": "4.9.0" + } + ], + "heading": { + "level": 2, + "text": "SetTBarPosition" + }, + "lead": "Set the manual position of the T-Bar (in Studio Mode) to the specified value. Will return an error if OBS is not in studio mode or if the current transition doesn't support T-Bar control.", + "type": "class", + "examples": [] + } + ] + } +} \ No newline at end of file diff --git a/docs/generated/protocol.md b/docs/generated/protocol.md new file mode 100644 index 00000000..4ea8b89e --- /dev/null +++ b/docs/generated/protocol.md @@ -0,0 +1,4448 @@ + + +# obs-websocket 4.9.0 protocol reference + +# General Introduction +Messages are exchanged between the client and the server as JSON objects. +This protocol is based on the original OBS Remote protocol created by Bill Hamilton, with new commands specific to OBS Studio. As of v5.0.0, backwards compatability with the protocol will not be kept. + +# Authentication +**Starting with obs-websocket 4.9, authentication is enabled by default and users are encouraged to configure a password on first run.** + +`obs-websocket` uses SHA256 to transmit credentials. + +A request for [`GetAuthRequired`](#getauthrequired) returns two elements: +- A `challenge`: a random string that will be used to generate the auth response. +- A `salt`: applied to the password when generating the auth response. + +To generate the answer to the auth challenge, follow this procedure: +- Concatenate the user declared password with the `salt` sent by the server (in this order: `password + server salt`). +- Generate a binary SHA256 hash of the result and encode the resulting SHA256 binary hash to base64, known as a `base64 secret`. +- Concatenate the base64 secret with the `challenge` sent by the server (in this order: `base64 secret + server challenge`). +- Generate a binary SHA256 hash of the result and encode it to base64. +- Voilà, this last base64 string is the `auth response`. You may now use it to authenticate to the server with the [`Authenticate`](#authenticate) request. + +Pseudo Code Example: +``` +password = "supersecretpassword" +challenge = "ztTBnnuqrqaKDzRM3xcVdbYm" +salt = "PZVbYpvAnZut2SS6JNJytDm9" + +secret_string = password + salt +secret_hash = binary_sha256(secret_string) +secret = base64_encode(secret_hash) + +auth_response_string = secret + challenge +auth_response_hash = binary_sha256(auth_response_string) +auth_response = base64_encode(auth_response_hash) +``` + +You can also refer to any of the client libraries listed on the [README](README.md) for examples of how to authenticate. + + + + +# Table of Contents + + + +- [Typedefs](#typedefs) + * [SceneItem](#sceneitem) + * [SceneItemTransform](#sceneitemtransform) + * [OBSStats](#obsstats) + * [Output](#output) + * [ScenesCollection](#scenescollection) + * [Scene](#scene) +- [Events](#events) + * [Scenes](#scenes) + + [SwitchScenes](#switchscenes) + + [ScenesChanged](#sceneschanged) + + [SceneCollectionChanged](#scenecollectionchanged) + + [SceneCollectionListChanged](#scenecollectionlistchanged) + * [Transitions](#transitions) + + [SwitchTransition](#switchtransition) + + [TransitionListChanged](#transitionlistchanged) + + [TransitionDurationChanged](#transitiondurationchanged) + + [TransitionBegin](#transitionbegin) + + [TransitionEnd](#transitionend) + + [TransitionVideoEnd](#transitionvideoend) + * [Profiles](#profiles) + + [ProfileChanged](#profilechanged) + + [ProfileListChanged](#profilelistchanged) + * [Streaming](#streaming) + + [StreamStarting](#streamstarting) + + [StreamStarted](#streamstarted) + + [StreamStopping](#streamstopping) + + [StreamStopped](#streamstopped) + + [StreamStatus](#streamstatus) + * [Recording](#recording) + + [RecordingStarting](#recordingstarting) + + [RecordingStarted](#recordingstarted) + + [RecordingStopping](#recordingstopping) + + [RecordingStopped](#recordingstopped) + + [RecordingPaused](#recordingpaused) + + [RecordingResumed](#recordingresumed) + * [Replay Buffer](#replay-buffer) + + [ReplayStarting](#replaystarting) + + [ReplayStarted](#replaystarted) + + [ReplayStopping](#replaystopping) + + [ReplayStopped](#replaystopped) + * [Other](#other) + + [Exiting](#exiting) + * [General](#general) + + [Heartbeat](#heartbeat) + + [BroadcastCustomMessage](#broadcastcustommessage) + * [Sources](#sources) + + [SourceCreated](#sourcecreated) + + [SourceDestroyed](#sourcedestroyed) + + [SourceVolumeChanged](#sourcevolumechanged) + + [SourceMuteStateChanged](#sourcemutestatechanged) + + [SourceAudioDeactivated](#sourceaudiodeactivated) + + [SourceAudioActivated](#sourceaudioactivated) + + [SourceAudioSyncOffsetChanged](#sourceaudiosyncoffsetchanged) + + [SourceAudioMixersChanged](#sourceaudiomixerschanged) + + [SourceRenamed](#sourcerenamed) + + [SourceFilterAdded](#sourcefilteradded) + + [SourceFilterRemoved](#sourcefilterremoved) + + [SourceFilterVisibilityChanged](#sourcefiltervisibilitychanged) + + [SourceFiltersReordered](#sourcefiltersreordered) + * [Media](#media) + + [MediaPlaying](#mediaplaying) + + [MediaPaused](#mediapaused) + + [MediaRestarted](#mediarestarted) + + [MediaStopped](#mediastopped) + + [MediaNext](#medianext) + + [MediaPrevious](#mediaprevious) + + [MediaStarted](#mediastarted) + + [MediaEnded](#mediaended) + * [Scene Items](#scene-items) + + [SourceOrderChanged](#sourceorderchanged) + + [SceneItemAdded](#sceneitemadded) + + [SceneItemRemoved](#sceneitemremoved) + + [SceneItemVisibilityChanged](#sceneitemvisibilitychanged) + + [SceneItemLockChanged](#sceneitemlockchanged) + + [SceneItemTransformChanged](#sceneitemtransformchanged) + + [SceneItemSelected](#sceneitemselected) + + [SceneItemDeselected](#sceneitemdeselected) + * [Studio Mode](#studio-mode) + + [PreviewSceneChanged](#previewscenechanged) + + [StudioModeSwitched](#studiomodeswitched) +- [Requests](#requests) + * [General](#general-1) + + [GetVersion](#getversion) + + [GetAuthRequired](#getauthrequired) + + [Authenticate](#authenticate) + + [SetHeartbeat](#setheartbeat) + + [SetFilenameFormatting](#setfilenameformatting) + + [GetFilenameFormatting](#getfilenameformatting) + + [GetStats](#getstats) + + [BroadcastCustomMessage](#broadcastcustommessage-1) + + [GetVideoInfo](#getvideoinfo) + + [OpenProjector](#openprojector) + + [TriggerHotkeyByName](#triggerhotkeybyname) + + [TriggerHotkeyBySequence](#triggerhotkeybysequence) + + [ExecuteBatch](#executebatch) + + [Sleep](#sleep) + * [Media Control](#media-control) + + [PlayPauseMedia](#playpausemedia) + + [RestartMedia](#restartmedia) + + [StopMedia](#stopmedia) + + [NextMedia](#nextmedia) + + [PreviousMedia](#previousmedia) + + [GetMediaDuration](#getmediaduration) + + [GetMediaTime](#getmediatime) + + [SetMediaTime](#setmediatime) + + [ScrubMedia](#scrubmedia) + + [GetMediaState](#getmediastate) + * [Sources](#sources-1) + + [GetMediaSourcesList](#getmediasourceslist) + + [CreateSource](#createsource) + + [GetSourcesList](#getsourceslist) + + [GetSourceTypesList](#getsourcetypeslist) + + [GetVolume](#getvolume) + + [SetVolume](#setvolume) + + [GetMute](#getmute) + + [SetMute](#setmute) + + [ToggleMute](#togglemute) + + [GetAudioActive](#getaudioactive) + + [SetSourceName](#setsourcename) + + [SetSyncOffset](#setsyncoffset) + + [GetSyncOffset](#getsyncoffset) + + [GetSourceSettings](#getsourcesettings) + + [SetSourceSettings](#setsourcesettings) + + [GetTextGDIPlusProperties](#gettextgdiplusproperties) + + [SetTextGDIPlusProperties](#settextgdiplusproperties) + + [GetTextFreetype2Properties](#gettextfreetype2properties) + + [SetTextFreetype2Properties](#settextfreetype2properties) + + [GetBrowserSourceProperties](#getbrowsersourceproperties) + + [SetBrowserSourceProperties](#setbrowsersourceproperties) + + [GetSpecialSources](#getspecialsources) + + [GetSourceFilters](#getsourcefilters) + + [GetSourceFilterInfo](#getsourcefilterinfo) + + [AddFilterToSource](#addfiltertosource) + + [RemoveFilterFromSource](#removefilterfromsource) + + [ReorderSourceFilter](#reordersourcefilter) + + [MoveSourceFilter](#movesourcefilter) + + [SetSourceFilterSettings](#setsourcefiltersettings) + + [SetSourceFilterVisibility](#setsourcefiltervisibility) + + [GetAudioMonitorType](#getaudiomonitortype) + + [SetAudioMonitorType](#setaudiomonitortype) + + [GetSourceDefaultSettings](#getsourcedefaultsettings) + + [TakeSourceScreenshot](#takesourcescreenshot) + + [RefreshBrowserSource](#refreshbrowsersource) + * [Outputs](#outputs) + + [ListOutputs](#listoutputs) + + [GetOutputInfo](#getoutputinfo) + + [StartOutput](#startoutput) + + [StopOutput](#stopoutput) + * [Profiles](#profiles-1) + + [SetCurrentProfile](#setcurrentprofile) + + [GetCurrentProfile](#getcurrentprofile) + + [ListProfiles](#listprofiles) + * [Recording](#recording-1) + + [GetRecordingStatus](#getrecordingstatus) + + [StartStopRecording](#startstoprecording) + + [StartRecording](#startrecording) + + [StopRecording](#stoprecording) + + [PauseRecording](#pauserecording) + + [ResumeRecording](#resumerecording) + + [SetRecordingFolder](#setrecordingfolder) + + [GetRecordingFolder](#getrecordingfolder) + * [Replay Buffer](#replay-buffer-1) + + [GetReplayBufferStatus](#getreplaybufferstatus) + + [StartStopReplayBuffer](#startstopreplaybuffer) + + [StartReplayBuffer](#startreplaybuffer) + + [StopReplayBuffer](#stopreplaybuffer) + + [SaveReplayBuffer](#savereplaybuffer) + * [Scene Collections](#scene-collections) + + [SetCurrentSceneCollection](#setcurrentscenecollection) + + [GetCurrentSceneCollection](#getcurrentscenecollection) + + [ListSceneCollections](#listscenecollections) + * [Scene Items](#scene-items-1) + + [GetSceneItemList](#getsceneitemlist) + + [GetSceneItemProperties](#getsceneitemproperties) + + [SetSceneItemProperties](#setsceneitemproperties) + + [ResetSceneItem](#resetsceneitem) + + [SetSceneItemRender](#setsceneitemrender) + + [SetSceneItemPosition](#setsceneitemposition) + + [SetSceneItemTransform](#setsceneitemtransform) + + [SetSceneItemCrop](#setsceneitemcrop) + + [DeleteSceneItem](#deletesceneitem) + + [AddSceneItem](#addsceneitem) + + [DuplicateSceneItem](#duplicatesceneitem) + * [Scenes](#scenes-1) + + [SetCurrentScene](#setcurrentscene) + + [GetCurrentScene](#getcurrentscene) + + [GetSceneList](#getscenelist) + + [CreateScene](#createscene) + + [ReorderSceneItems](#reordersceneitems) + + [SetSceneTransitionOverride](#setscenetransitionoverride) + + [RemoveSceneTransitionOverride](#removescenetransitionoverride) + + [GetSceneTransitionOverride](#getscenetransitionoverride) + * [Streaming](#streaming-1) + + [GetStreamingStatus](#getstreamingstatus) + + [StartStopStreaming](#startstopstreaming) + + [StartStreaming](#startstreaming) + + [StopStreaming](#stopstreaming) + + [SetStreamSettings](#setstreamsettings) + + [GetStreamSettings](#getstreamsettings) + + [SaveStreamSettings](#savestreamsettings) + + [SendCaptions](#sendcaptions) + * [Studio Mode](#studio-mode-1) + + [GetStudioModeStatus](#getstudiomodestatus) + + [GetPreviewScene](#getpreviewscene) + + [SetPreviewScene](#setpreviewscene) + + [TransitionToProgram](#transitiontoprogram) + + [EnableStudioMode](#enablestudiomode) + + [DisableStudioMode](#disablestudiomode) + + [ToggleStudioMode](#togglestudiomode) + * [Transitions](#transitions-1) + + [GetTransitionList](#gettransitionlist) + + [GetCurrentTransition](#getcurrenttransition) + + [SetCurrentTransition](#setcurrenttransition) + + [SetTransitionDuration](#settransitionduration) + + [GetTransitionDuration](#gettransitionduration) + + [GetTransitionPosition](#gettransitionposition) + + [GetTransitionSettings](#gettransitionsettings) + + [SetTransitionSettings](#settransitionsettings) + + [ReleaseTBar](#releasetbar) + + [SetTBarPosition](#settbarposition) + + + +# Typedefs +These are complex types, such as `Source` and `Scene`, which are used as arguments or return values in multiple requests and/or events. + + +## SceneItem +| Name | Type | Description | +| ---- | :---: | ------------| +| `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_ | | +| `type` | _String_ | Source type. Value is one of the following: "input", "filter", "transition", "scene" or "unknown" | +| `volume` | _Number_ | | +| `x` | _Number_ | | +| `y` | _Number_ | | +| `parentGroupName` | _String (optional)_ | Name of the item's parent (if this item belongs to a group) | +| `groupChildren` | _Array<SceneItem> (optional)_ | List of children (if this item is a group) | +## SceneItemTransform +| Name | Type | Description | +| ---- | :---: | ------------| +| `position.x` | _double_ | The x position of the scene item from the left. | +| `position.y` | _double_ | The y position of the scene item from the top. | +| `position.alignment` | _int_ | The point on the scene item that the item is manipulated from. | +| `rotation` | _double_ | The clockwise rotation of the scene item in degrees around the point of alignment. | +| `scale.x` | _double_ | The x-scale factor of the scene item. | +| `scale.y` | _double_ | The y-scale factor of the scene item. | +| `scale.filter` | _String_ | The scale filter of the source. Can be "OBS_SCALE_DISABLE", "OBS_SCALE_POINT", "OBS_SCALE_BICUBIC", "OBS_SCALE_BILINEAR", "OBS_SCALE_LANCZOS" or "OBS_SCALE_AREA". | +| `crop.top` | _int_ | The number of pixels cropped off the top of the scene item before scaling. | +| `crop.right` | _int_ | The number of pixels cropped off the right of the scene item before scaling. | +| `crop.bottom` | _int_ | The number of pixels cropped off the bottom of the scene item before scaling. | +| `crop.left` | _int_ | The number of pixels cropped off the left of the scene item before scaling. | +| `visible` | _bool_ | If the scene item is visible. | +| `locked` | _bool_ | If the scene item is locked in position. | +| `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. | +| `bounds.x` | _double_ | Width of the bounding box. | +| `bounds.y` | _double_ | Height of the bounding box. | +| `sourceWidth` | _int_ | Base width (without scaling) of the source | +| `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) | +| `parentGroupName` | _String (optional)_ | Name of the item's parent (if this item belongs to a group) | +| `groupChildren` | _Array<SceneItemTransform> (optional)_ | List of children (if this item is a group) | +## OBSStats +| Name | Type | Description | +| ---- | :---: | ------------| +| `fps` | _double_ | Current framerate. | +| `render-total-frames` | _int_ | Number of frames rendered | +| `render-missed-frames` | _int_ | Number of frames missed due to rendering lag | +| `output-total-frames` | _int_ | Number of frames outputted | +| `output-skipped-frames` | _int_ | Number of frames skipped due to encoding lag | +| `average-frame-time` | _double_ | Average frame render time (in milliseconds) | +| `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 | +## ScenesCollection +| Name | Type | Description | +| ---- | :---: | ------------| +| `sc-name` | _String_ | Name of the scene collection | +## Scene +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Name of the currently active scene. | +| `sources` | _Array<SceneItem>_ | Ordered list of the current scene's source items. | + + + +# Events +Events are broadcast by the server to each connected client when a recognized action occurs within OBS. + +An event message will contain at least the following base fields: +- `update-type` _String_: the type of event. +- `stream-timecode` _String (optional)_: time elapsed between now and stream start (only present if OBS Studio is streaming). +- `rec-timecode` _String (optional)_: time elapsed between now and recording start (only present if OBS Studio is recording). + +Timecodes are sent using the format: `HH:MM:SS.mmm` + +Additional fields may be present in the event message depending on the event type. + + +## Scenes + +### SwitchScenes + + +- Added in v0.3 + +Indicates a scene change. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | The new scene. | +| `sources` | _Array<SceneItem>_ | List of scene items in the new scene. Same specification as [`GetCurrentScene`](#getcurrentscene). | + + +--- + +### ScenesChanged + + +- Added in v0.3 + + + +Note: This event is not fired when the scenes are reordered. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scenes` | _Array<Scene>_ | Scenes list. | + + +--- + +### SceneCollectionChanged + + +- Added in v4.0.0 + +Triggered when switching to another scene collection or when renaming the current scene collection. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneCollection` | _String_ | Name of the new current scene collection. | + + +--- + +### SceneCollectionListChanged + + +- Added in v4.0.0 + +Triggered when a scene collection is created, added, renamed, or removed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneCollections` | _Array<Object>_ | Scene collections list. | +| `sceneCollections.*.name` | _String_ | Scene collection name. | + + +--- + +## Transitions + +### SwitchTransition + + +- Added in v4.0.0 + +The active transition has been changed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transition-name` | _String_ | The name of the new active transition. | + + +--- + +### TransitionListChanged + + +- Added in v4.0.0 + +The list of available transitions has been modified. +Transitions have been added, removed, or renamed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transitions` | _Array<Object>_ | Transitions list. | +| `transitions.*.name` | _String_ | Transition name. | + + +--- + +### TransitionDurationChanged + + +- Added in v4.0.0 + +The active transition duration has been changed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `new-duration` | _int_ | New transition duration. | + + +--- + +### TransitionBegin + + +- Added in v4.0.0 + +A transition (other than "cut") has begun. + +**Response Items:** + +| 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. +Note: 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 | + + +--- + +## Profiles + +### ProfileChanged + + +- Added in v4.0.0 + +Triggered when switching to another profile or when renaming the current profile. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `profile` | _String_ | Name of the new current profile. | + + +--- + +### ProfileListChanged + + +- Added in v4.0.0 + +Triggered when a profile is created, added, renamed, or removed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `profiles` | _Array<Object>_ | Profiles list. | +| `profiles.*.name` | _String_ | Profile name. | + + +--- + +## Streaming + +### StreamStarting + + +- Added in v0.3 + +A request to start streaming has been issued. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `preview-only` | _boolean_ | Always false (retrocompatibility). | + + +--- + +### StreamStarted + + +- Added in v0.3 + +Streaming started successfully. + +**Response Items:** + +_No additional response items._ + +--- + +### StreamStopping + + +- Added in v0.3 + +A request to stop streaming has been issued. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `preview-only` | _boolean_ | Always false (retrocompatibility). | + + +--- + +### StreamStopped + + +- Added in v0.3 + +Streaming stopped successfully. + +**Response Items:** + +_No additional response items._ + +--- + +### StreamStatus + + +- Added in v0.3 + +Emitted every 2 seconds when stream is active. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `streaming` | _boolean_ | Current streaming state. | +| `recording` | _boolean_ | Current recording state. | +| `replay-buffer-active` | _boolean_ | Replay Buffer status | +| `bytes-per-sec` | _int_ | Amount of data per second (in bytes) transmitted by the stream encoder. | +| `kbits-per-sec` | _int_ | Amount of data per second (in kilobits) transmitted by the stream encoder. | +| `strain` | _double_ | Percentage of dropped frames. | +| `total-stream-time` | _int_ | Total time (in seconds) since the stream started. | +| `num-total-frames` | _int_ | Total number of frames transmitted since the stream started. | +| `num-dropped-frames` | _int_ | Number of frames dropped by the encoder since the stream started. | +| `fps` | _double_ | Current framerate. | +| `render-total-frames` | _int_ | Number of frames rendered | +| `render-missed-frames` | _int_ | Number of frames missed due to rendering lag | +| `output-total-frames` | _int_ | Number of frames outputted | +| `output-skipped-frames` | _int_ | Number of frames skipped due to encoding lag | +| `average-frame-time` | _double_ | Average frame time (in milliseconds) | +| `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) | +| `preview-only` | _boolean_ | Always false (retrocompatibility). | + + +--- + +## Recording + +### RecordingStarting + + +- Added in v0.3 + + + +Note: `recordingFilename` is not provided in this event because this information +is not available at the time this event is emitted. + +**Response Items:** + +_No additional response items._ + +--- + +### RecordingStarted + + +- Added in v0.3 + +Recording started successfully. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `recordingFilename` | _String_ | Absolute path to the file of the current recording. | + + +--- + +### RecordingStopping + + +- Added in v0.3 + +A request to stop recording has been issued. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `recordingFilename` | _String_ | Absolute path to the file of the current recording. | + + +--- + +### RecordingStopped + + +- Added in v0.3 + +Recording stopped successfully. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `recordingFilename` | _String_ | Absolute path to the file of the current recording. | + + +--- + +### 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 + + +- Added in v4.2.0 + +A request to start the replay buffer has been issued. + +**Response Items:** + +_No additional response items._ + +--- + +### ReplayStarted + + +- Added in v4.2.0 + +Replay Buffer started successfully + +**Response Items:** + +_No additional response items._ + +--- + +### ReplayStopping + + +- Added in v4.2.0 + +A request to stop the replay buffer has been issued. + +**Response Items:** + +_No additional response items._ + +--- + +### ReplayStopped + + +- Added in v4.2.0 + +Replay Buffer stopped successfully + +**Response Items:** + +_No additional response items._ + +--- + +## Other + +### Exiting + + +- Added in v0.3 + +OBS is exiting. + +**Response Items:** + +_No additional response items._ + +--- + +## General + +### Heartbeat + + +- Added in vv0.3 + +Emitted every 2 seconds after enabling it by calling SetHeartbeat. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `pulse` | _boolean_ | Toggles between every JSON message as an "I am alive" indicator. | +| `current-profile` | _string (optional)_ | Current active profile. | +| `current-scene` | _string (optional)_ | Current active scene. | +| `streaming` | _boolean (optional)_ | Current streaming state. | +| `total-stream-time` | _int (optional)_ | Total time (in seconds) since the stream started. | +| `total-stream-bytes` | _int (optional)_ | Total bytes sent since the stream started. | +| `total-stream-frames` | _int (optional)_ | Total frames streamed since the stream started. | +| `recording` | _boolean (optional)_ | Current recording state. | +| `total-record-time` | _int (optional)_ | Total time (in seconds) since recording started. | +| `total-record-bytes` | _int (optional)_ | Total bytes recorded since the recording started. | +| `total-record-frames` | _int (optional)_ | Total frames recorded since the recording started. | +| `stats` | _OBSStats_ | OBS Stats | + + +--- + +### BroadcastCustomMessage + + +- Added in v4.7.0 + +A custom broadcast message, sent by the server, requested by one of the websocket clients. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `realm` | _String_ | Identifier provided by the sender | +| `data` | _Object_ | User-defined data | + + +--- + +## Sources + +### SourceCreated + + +- Added in v4.6.0 + +A source has been created. A source can be an input, a scene or a transition. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceType` | _String_ | Source type. Can be "input", "scene", "transition" or "filter". | +| `sourceKind` | _String_ | Source kind. | +| `sourceSettings` | _Object_ | Source settings | + + +--- + +### SourceDestroyed + + +- Added in v4.6.0 + +A source has been destroyed/removed. A source can be an input, a scene or a transition. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceType` | _String_ | Source type. Can be "input", "scene", "transition" or "filter". | +| `sourceKind` | _String_ | Source kind. | + + +--- + +### SourceVolumeChanged + + +- Added in v4.6.0 + +The volume of a source has changed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `volume` | _float_ | Source volume | + + +--- + +### SourceMuteStateChanged + + +- Added in v4.6.0 + +A source has been muted or unmuted. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `muted` | _boolean_ | Mute status of the source | + + +--- + +### SourceAudioDeactivated + + +- Added in v4.9.0 + +A source has removed audio. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | + + +--- + +### SourceAudioActivated + + +- Added in v4.9.0 + +A source has added audio. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | + + +--- + +### SourceAudioSyncOffsetChanged + + +- Added in v4.6.0 + +The audio sync offset of a source has changed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `syncOffset` | _int_ | Audio sync offset of the source (in nanoseconds) | + + +--- + +### SourceAudioMixersChanged + + +- Added in v4.6.0 + +Audio mixer routing changed on a source. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `mixers` | _Array<Object>_ | Routing status of the source for each audio mixer (array of 6 values) | +| `mixers.*.id` | _int_ | Mixer number | +| `mixers.*.enabled` | _boolean_ | Routing status | +| `hexMixersValue` | _String_ | Raw mixer flags (little-endian, one bit per mixer) as an hexadecimal value | + + +--- + +### SourceRenamed + + +- Added in v4.6.0 + +A source has been renamed. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `previousName` | _String_ | Previous source name | +| `newName` | _String_ | New source name | +| `sourceType` | _String_ | Type of source (input, scene, filter, transition) | + + +--- + +### SourceFilterAdded + + +- Added in v4.6.0 + +A filter was added to a source. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filterName` | _String_ | Filter name | +| `filterType` | _String_ | Filter type | +| `filterSettings` | _Object_ | Filter settings | + + +--- + +### SourceFilterRemoved + + +- Added in v4.6.0 + +A filter was removed from a source. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filterName` | _String_ | Filter name | +| `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 + + +- Added in v4.6.0 + +Filters in a source have been reordered. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filters` | _Array<Object>_ | Ordered Filters list | +| `filters.*.name` | _String_ | Filter name | +| `filters.*.type` | _String_ | Filter type | +| `filters.*.enabled` | _boolean_ | Filter visibility status | + + +--- + +## Media + +### MediaPlaying + + +- Added in v4.9.0 + + + +Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaPaused + + +- Added in v4.9.0 + + + +Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaRestarted + + +- Added in v4.9.0 + + + +Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaStopped + + +- Added in v4.9.0 + + + +Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaNext + + +- Added in v4.9.0 + + + +Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaPrevious + + +- Added in v4.9.0 + + + +Note: This event is only emitted when something actively controls the media/VLC source. In other words, the source will never emit this on its own naturally. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaStarted + + +- Added in v4.9.0 + + + +Note: These events are emitted by the OBS sources themselves. For example when the media file starts playing. The behavior depends on the type of media source being used. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +### MediaEnded + + +- Added in v4.9.0 + + + +Note: These events are emitted by the OBS sources themselves. For example when the media file ends. The behavior depends on the type of media source being used. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceKind` | _String_ | The ID type of the source (Eg. `vlc_source` or `ffmpeg_source`) | + + +--- + +## Scene Items + +### SourceOrderChanged + + +- Added in v4.0.0 + +Scene items within a scene have been reordered. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | Name of the scene where items have been reordered. | +| `scene-items` | _Array<Object>_ | Ordered list of scene items | +| `scene-items.*.source-name` | _String_ | Item source name | +| `scene-items.*.item-id` | _int_ | Scene item unique ID | + + +--- + +### SceneItemAdded + + +- Added in v4.0.0 + +A scene item has been added to a scene. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | Name of the scene. | +| `item-name` | _String_ | Name of the item added to the scene. | +| `item-id` | _int_ | Scene item ID | + + +--- + +### SceneItemRemoved + + +- Added in v4.0.0 + +A scene item has been removed from a scene. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | Name of the scene. | +| `item-name` | _String_ | Name of the item removed from the scene. | +| `item-id` | _int_ | Scene item ID | + + +--- + +### SceneItemVisibilityChanged + + +- Added in v4.0.0 + +A scene item's visibility 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-visible` | _boolean_ | New visibility state of the item. | + + +--- + +### SceneItemLockChanged + + +- Added in v4.8.0 + +A scene 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 + + +- Added in v4.6.0 + +A scene item's transform has been changed. + +**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 | +| `transform` | _SceneItemTransform_ | Scene item transform properties | + + +--- + +### SceneItemSelected + + +- Added in v4.6.0 + +A scene item is selected. + +**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_ | Name of the item in the scene. | + + +--- + +### SceneItemDeselected + + +- Added in v4.6.0 + +A scene item is deselected. + +**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_ | Name of the item in the scene. | + + +--- + +## Studio Mode + +### PreviewSceneChanged + + +- Added in v4.1.0 + +The selected preview scene has changed (only available in Studio Mode). + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | Name of the scene being previewed. | +| `sources` | _Array<SceneItem>_ | List of sources composing the scene. Same specification as [`GetCurrentScene`](#getcurrentscene). | + + +--- + +### StudioModeSwitched + + +- Added in v4.1.0 + +Studio Mode has been enabled or disabled. + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `new-state` | _boolean_ | The new enabled state of Studio Mode. | + + +--- + + + + +# Requests +Requests are sent by the client and require at least the following two fields: +- `request-type` _String_: String name of the request type. +- `message-id` _String_: Client defined identifier for the message, will be echoed in the response. + +Once a request is sent, the server will return a JSON response with at least the following fields: +- `message-id` _String_: The client defined identifier specified in the request. +- `status` _String_: Response status, will be one of the following: `ok`, `error` +- `error` _String (Optional)_: An error message accompanying an `error` status. + +Additional information may be required/returned depending on the request type. See below for more information. + + +## General + +### GetVersion + + +- Added in v0.3 + +Returns the latest version of the plugin and the API. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `version` | _double_ | OBSRemote compatible API version. Fixed to 1.1 for retrocompatibility. | +| `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 | + + +--- + +### GetAuthRequired + + +- Added in v0.3 + +Tells the client if authentication is required. If so, returns authentication parameters `challenge` +and `salt` (see "Authentication" for more information). + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `authRequired` | _boolean_ | Indicates whether authentication is required. | +| `challenge` | _String (optional)_ | | +| `salt` | _String (optional)_ | | + + +--- + +### Authenticate + + +- Added in v0.3 + +Attempt to authenticate the client to the server. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `auth` | _String_ | Response to the auth challenge (see "Authentication" for more information). | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetHeartbeat + +- **⚠️ Deprecated. Since 4.9.0. Please poll the appropriate data using requests. Will be removed in v5.0.0. ⚠️** + +- Added in v4.3.0 + +Enable/disable sending of the Heartbeat event + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `enable` | _boolean_ | Starts/Stops emitting heartbeat messages | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetFilenameFormatting + + +- Added in v4.3.0 + +Set the filename formatting string + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `filename-formatting` | _String_ | Filename formatting string to set. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetFilenameFormatting + + +- Added in v4.3.0 + +Get the filename formatting string + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `filename-formatting` | _String_ | Current filename formatting string. | + + +--- + +### GetStats + + +- Added in v4.6.0 + +Get OBS stats (almost the same info as provided in OBS' stats window) + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `stats` | _OBSStats_ | [OBS stats](#obsstats) | + + +--- + +### 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 + + +- Added in v4.6.0 + +Get basic OBS video information + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `baseWidth` | _int_ | Base (canvas) width | +| `baseHeight` | _int_ | Base (canvas) height | +| `outputWidth` | _int_ | Output width | +| `outputHeight` | _int_ | Output height | +| `scaleType` | _String_ | Scaling method used if output size differs from base size | +| `fps` | _double_ | Frames rendered per second | +| `videoFormat` | _String_ | Video color format | +| `colorSpace` | _String_ | Color space for YUV | +| `colorRange` | _String_ | Color range (full or partial) | + + +--- + +### OpenProjector + + +- Added in v4.8.0 + +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._ + +--- + +### TriggerHotkeyByName + + +- Added in v4.9.0 + +Executes hotkey routine, identified by hotkey unique name + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `hotkeyName` | _String_ | Unique name of the hotkey, as defined when registering the hotkey (e.g. "ReplayBuffer.Save") | + + +**Response Items:** + +_No additional response items._ + +--- + +### TriggerHotkeyBySequence + + +- Added in v4.9.0 + +Executes hotkey routine, identified by bound combination of keys. A single key combination might trigger multiple hotkey routines depending on user settings + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `keyId` | _String_ | Main key identifier (e.g. `OBS_KEY_A` for key "A"). Available identifiers [here](https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h) | +| `keyModifiers` | _Object (Optional)_ | Optional key modifiers object. False entries can be ommitted | +| `keyModifiers.shift` | _boolean_ | Trigger Shift Key | +| `keyModifiers.alt` | _boolean_ | Trigger Alt Key | +| `keyModifiers.control` | _boolean_ | Trigger Control (Ctrl) Key | +| `keyModifiers.command` | _boolean_ | Trigger Command Key (Mac) | + + +**Response Items:** + +_No additional response items._ + +--- + +### ExecuteBatch + + +- Added in v4.9.0 + +Executes a list of requests sequentially (one-by-one on the same thread). + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `requests` | _Array<Object>_ | Array of requests to perform. Executed in order. | +| `requests.*.request-type` | _String_ | Request type. Eg. `GetVersion`. | +| `requests.*.message-id` | _String (Optional)_ | ID of the individual request. Can be any string and not required to be unique. Defaults to empty string if not specified. | +| `abortOnFail` | _boolean (Optional)_ | Stop processing batch requests if one returns a failure. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `results` | _Array<Object>_ | Batch requests results, ordered sequentially. | +| `results.*.message-id` | _String_ | ID of the individual request which was originally provided by the client. | +| `results.*.status` | _String_ | Status response as string. Either `ok` or `error`. | +| `results.*.error` | _String (Optional)_ | Error message accompanying an `error` status. | + + +--- + +### Sleep + + +- Unreleased + +Waits for the specified duration. Designed to be used in `ExecuteBatch` operations. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sleepMillis` | _int_ | Delay in milliseconds to wait before continuing. | + + +**Response Items:** + +_No additional response items._ + +--- + +## Media Control + +### PlayPauseMedia + + +- Added in v4.9.0 + +Pause or play a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `playPause` | _boolean_ | Whether to pause or play the source. `false` for play, `true` for pause. | + + +**Response Items:** + +_No additional response items._ + +--- + +### RestartMedia + + +- Added in v4.9.0 + +Restart a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +### StopMedia + + +- Added in v4.9.0 + +Stop a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +### NextMedia + + +- Added in v4.9.0 + +Skip to the next media item in the playlist. Supports only vlc media source (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +### PreviousMedia + + +- Added in v4.9.0 + +Go to the previous media item in the playlist. Supports only vlc media source (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetMediaDuration + + +- Added in v4.9.0 + +Get the length of media in milliseconds. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) +Note: For some reason, for the first 5 or so seconds that the media is playing, the total duration can be off by upwards of 50ms. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `mediaDuration` | _int_ | The total length of media in milliseconds.. | + + +--- + +### GetMediaTime + + +- Added in v4.9.0 + +Get the current timestamp of media in milliseconds. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `timestamp` | _int_ | The time in milliseconds since the start of the media. | + + +--- + +### SetMediaTime + + +- Added in v4.9.0 + +Set the timestamp of a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `timestamp` | _int_ | Milliseconds to set the timestamp to. | + + +**Response Items:** + +_No additional response items._ + +--- + +### ScrubMedia + + +- Added in v4.9.0 + +Scrub media using a supplied offset. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) +Note: Due to processing/network delays, this request is not perfect. The processing rate of this request has also not been tested. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `timeOffset` | _int_ | Millisecond offset (positive or negative) to offset the current media position. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetMediaState + + +- Added in v4.9.0 + +Get the current playing state of a media source. Supports ffmpeg and vlc media sources (as of OBS v25.0.8) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `mediaState` | _String_ | The media state of the provided source. States: `none`, `playing`, `opening`, `buffering`, `paused`, `stopped`, `ended`, `error`, `unknown` | + + +--- + +## Sources + +### GetMediaSourcesList + + +- Added in v4.9.0 + +List the media state of all media sources (vlc and media source) + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `mediaSources` | _Array<Object>_ | Array of sources | +| `mediaSources.*.sourceName` | _String_ | Unique source name | +| `mediaSources.*.sourceKind` | _String_ | Unique source internal type (a.k.a `ffmpeg_source` or `vlc_source`) | +| `mediaSources.*.mediaState` | _String_ | The current state of media for that source. States: `none`, `playing`, `opening`, `buffering`, `paused`, `stopped`, `ended`, `error`, `unknown` | + + +--- + +### CreateSource + + +- Added in v4.9.0 + +Create a source and add it as a sceneitem to a scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `sourceKind` | _String_ | Source kind, Eg. `vlc_source`. | +| `sceneName` | _String_ | Scene to add the new source to. | +| `sourceSettings` | _Object (optional)_ | Source settings data. | +| `setVisible` | _boolean (optional)_ | Set the created SceneItem as visible or not. Defaults to true | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `itemId` | _int_ | ID of the SceneItem in the scene. | + + +--- + +### GetSourcesList + + +- Added in v4.3.0 + +List all sources available in the running OBS instance + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sources` | _Array<Object>_ | Array of sources | +| `sources.*.name` | _String_ | Unique source name | +| `sources.*.typeId` | _String_ | Non-unique source internal type (a.k.a kind) | +| `sources.*.type` | _String_ | Source type. Value is one of the following: "input", "filter", "transition", "scene" or "unknown" | + + +--- + +### GetSourceTypesList + + +- Added in v4.3.0 + +Get a list of all available sources types + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `types` | _Array<Object>_ | Array of source types | +| `types.*.typeId` | _String_ | Non-unique internal source type ID | +| `types.*.displayName` | _String_ | Display name of the source type | +| `types.*.type` | _String_ | Type. Value is one of the following: "input", "filter", "transition" or "other" | +| `types.*.defaultSettings` | _Object_ | Default settings of this source type | +| `types.*.caps` | _Object_ | Source type capabilities | +| `types.*.caps.isAsync` | _Boolean_ | True if source of this type provide frames asynchronously | +| `types.*.caps.hasVideo` | _Boolean_ | True if sources of this type provide video | +| `types.*.caps.hasAudio` | _Boolean_ | True if sources of this type provide audio | +| `types.*.caps.canInteract` | _Boolean_ | True if interaction with this sources of this type is possible | +| `types.*.caps.isComposite` | _Boolean_ | True if sources of this type composite one or more sub-sources | +| `types.*.caps.doNotDuplicate` | _Boolean_ | True if sources of this type should not be fully duplicated | +| `types.*.caps.doNotSelfMonitor` | _Boolean_ | True if sources of this type may cause a feedback loop if it's audio is monitored and shouldn't be | + + +--- + +### GetVolume + + +- Added in v4.0.0 + +Get the volume of the specified source. Default response uses mul format, NOT SLIDER PERCENTAGE. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `useDecibel` | _boolean (optional)_ | Output volume in decibels of attenuation instead of amplitude/mul. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Source name. | +| `volume` | _double_ | Volume of the source. Between `0.0` and `20.0` if using mul, under `26.0` if using dB. | +| `muted` | _boolean_ | Indicates whether the source is muted. | + + +--- + +### SetVolume + + +- Added in v4.0.0 + +Set the volume of the specified source. Default request format uses mul, NOT SLIDER PERCENTAGE. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `volume` | _double_ | Desired volume. Must be between `0.0` and `20.0` for mul, and under 26.0 for dB. OBS will interpret dB values under -100.0 as Inf. Note: The OBS volume sliders only reach a maximum of 1.0mul/0.0dB, however OBS actually supports larger values. | +| `useDecibel` | _boolean (optional)_ | Interperet `volume` data as decibels instead of amplitude/mul. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetMute + + +- Added in v4.0.0 + +Get the mute status of a specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Source name. | +| `muted` | _boolean_ | Mute status of the source. | + + +--- + +### SetMute + + +- Added in v4.0.0 + +Sets the mute status of a specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `mute` | _boolean_ | Desired mute status. | + + +**Response Items:** + +_No additional response items._ + +--- + +### ToggleMute + + +- Added in v4.0.0 + +Inverts the mute status of a specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetAudioActive + + +- Added in v4.9.0 + +Get the audio's active status of a specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `audioActive` | _boolean_ | Audio active status of the source. | + + +--- + +### SetSourceName + + +- Added in v4.8.0 + + + +Note: If the new name already exists as a source, obs-websocket will return an error. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `newName` | _String_ | New source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSyncOffset + + +- Added in v4.2.0 + +Set the audio sync offset of a specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `offset` | _int_ | The desired audio sync offset (in nanoseconds). | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetSyncOffset + + +- Added in v4.2.0 + +Get the audio sync offset of a specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Source name. | +| `offset` | _int_ | The audio sync offset (in nanoseconds). | + + +--- + +### GetSourceSettings + + +- Added in v4.3.0 + +Get settings of the specified source + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `sourceType` | _String (optional)_ | Type of the specified source. Useful for type-checking if you expect a specific settings schema. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceType` | _String_ | Type of the specified source | +| `sourceSettings` | _Object_ | Source settings (varies between source types, may require some probing around). | + + +--- + +### SetSourceSettings + + +- Added in v4.3.0 + +Set settings of the specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `sourceType` | _String (optional)_ | Type of the specified source. Useful for type-checking to avoid settings a set of settings incompatible with the actual source's type. | +| `sourceSettings` | _Object_ | Source settings (varies between source types, may require some probing around). | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `sourceType` | _String_ | Type of the specified source | +| `sourceSettings` | _Object_ | Updated source settings | + + +--- + +### GetTextGDIPlusProperties + + +- Added in v4.1.0 + +Get the current properties of a Text GDI Plus source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `align` | _String_ | Text Alignment ("left", "center", "right"). | +| `bk_color` | _int_ | Background color. | +| `bk_opacity` | _int_ | Background opacity (0-100). | +| `chatlog` | _boolean_ | Chat log. | +| `chatlog_lines` | _int_ | Chat log lines. | +| `color` | _int_ | Text color. | +| `extents` | _boolean_ | Extents wrap. | +| `extents_cx` | _int_ | Extents cx. | +| `extents_cy` | _int_ | Extents cy. | +| `file` | _String_ | File path name. | +| `read_from_file` | _boolean_ | Read text from the specified file. | +| `font` | _Object_ | Holds data for the font. Ex: `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }` | +| `font.face` | _String_ | Font face. | +| `font.flags` | _int_ | Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8` | +| `font.size` | _int_ | Font text size. | +| `font.style` | _String_ | Font Style (unknown function). | +| `gradient` | _boolean_ | Gradient enabled. | +| `gradient_color` | _int_ | Gradient color. | +| `gradient_dir` | _float_ | Gradient direction. | +| `gradient_opacity` | _int_ | Gradient opacity (0-100). | +| `outline` | _boolean_ | Outline. | +| `outline_color` | _int_ | Outline color. | +| `outline_size` | _int_ | Outline size. | +| `outline_opacity` | _int_ | Outline opacity (0-100). | +| `text` | _String_ | Text content to be displayed. | +| `valign` | _String_ | Text vertical alignment ("top", "center", "bottom"). | +| `vertical` | _boolean_ | Vertical text enabled. | + + +--- + +### SetTextGDIPlusProperties + + +- Added in v4.1.0 + +Set the current properties of a Text GDI Plus source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Name of the source. | +| `align` | _String (optional)_ | Text Alignment ("left", "center", "right"). | +| `bk_color` | _int (optional)_ | Background color. | +| `bk_opacity` | _int (optional)_ | Background opacity (0-100). | +| `chatlog` | _boolean (optional)_ | Chat log. | +| `chatlog_lines` | _int (optional)_ | Chat log lines. | +| `color` | _int (optional)_ | Text color. | +| `extents` | _boolean (optional)_ | Extents wrap. | +| `extents_cx` | _int (optional)_ | Extents cx. | +| `extents_cy` | _int (optional)_ | Extents cy. | +| `file` | _String (optional)_ | File path name. | +| `read_from_file` | _boolean (optional)_ | Read text from the specified file. | +| `font` | _Object (optional)_ | Holds data for the font. Ex: `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }` | +| `font.face` | _String (optional)_ | Font face. | +| `font.flags` | _int (optional)_ | Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8` | +| `font.size` | _int (optional)_ | Font text size. | +| `font.style` | _String (optional)_ | Font Style (unknown function). | +| `gradient` | _boolean (optional)_ | Gradient enabled. | +| `gradient_color` | _int (optional)_ | Gradient color. | +| `gradient_dir` | _float (optional)_ | Gradient direction. | +| `gradient_opacity` | _int (optional)_ | Gradient opacity (0-100). | +| `outline` | _boolean (optional)_ | Outline. | +| `outline_color` | _int (optional)_ | Outline color. | +| `outline_size` | _int (optional)_ | Outline size. | +| `outline_opacity` | _int (optional)_ | Outline opacity (0-100). | +| `text` | _String (optional)_ | Text content to be displayed. | +| `valign` | _String (optional)_ | Text vertical alignment ("top", "center", "bottom"). | +| `vertical` | _boolean (optional)_ | Vertical text enabled. | +| `render` | _boolean (optional)_ | Visibility of the scene item. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetTextFreetype2Properties + + +- Added in v4.5.0 + +Get the current properties of a Text Freetype 2 source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name | +| `color1` | _int_ | Gradient top color. | +| `color2` | _int_ | Gradient bottom color. | +| `custom_width` | _int_ | Custom width (0 to disable). | +| `drop_shadow` | _boolean_ | Drop shadow. | +| `font` | _Object_ | Holds data for the font. Ex: `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }` | +| `font.face` | _String_ | Font face. | +| `font.flags` | _int_ | Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8` | +| `font.size` | _int_ | Font text size. | +| `font.style` | _String_ | Font Style (unknown function). | +| `from_file` | _boolean_ | Read text from the specified file. | +| `log_mode` | _boolean_ | Chat log. | +| `outline` | _boolean_ | Outline. | +| `text` | _String_ | Text content to be displayed. | +| `text_file` | _String_ | File path. | +| `word_wrap` | _boolean_ | Word wrap. | + + +--- + +### SetTextFreetype2Properties + + +- Added in v4.5.0 + +Set the current properties of a Text Freetype 2 source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `color1` | _int (optional)_ | Gradient top color. | +| `color2` | _int (optional)_ | Gradient bottom color. | +| `custom_width` | _int (optional)_ | Custom width (0 to disable). | +| `drop_shadow` | _boolean (optional)_ | Drop shadow. | +| `font` | _Object (optional)_ | Holds data for the font. Ex: `"font": { "face": "Arial", "flags": 0, "size": 150, "style": "" }` | +| `font.face` | _String (optional)_ | Font face. | +| `font.flags` | _int (optional)_ | Font text styling flag. `Bold=1, Italic=2, Bold Italic=3, Underline=5, Strikeout=8` | +| `font.size` | _int (optional)_ | Font text size. | +| `font.style` | _String (optional)_ | Font Style (unknown function). | +| `from_file` | _boolean (optional)_ | Read text from the specified file. | +| `log_mode` | _boolean (optional)_ | Chat log. | +| `outline` | _boolean (optional)_ | Outline. | +| `text` | _String (optional)_ | Text content to be displayed. | +| `text_file` | _String (optional)_ | File path. | +| `word_wrap` | _boolean (optional)_ | Word wrap. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetBrowserSourceProperties + +- **⚠️ Deprecated. Since 4.8.0. Prefer the use of GetSourceSettings. Will be removed in v5.0.0 ⚠️** + +- Added in v4.1.0 + +Get current properties for a Browser Source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Source name. | +| `is_local_file` | _boolean_ | Indicates that a local file is in use. | +| `local_file` | _String_ | file path. | +| `url` | _String_ | Url. | +| `css` | _String_ | CSS to inject. | +| `width` | _int_ | Width. | +| `height` | _int_ | Height. | +| `fps` | _int_ | Framerate. | +| `shutdown` | _boolean_ | Indicates whether the source should be shutdown when not visible. | + + +--- + +### SetBrowserSourceProperties + +- **⚠️ Deprecated. Since 4.8.0. Prefer the use of SetSourceSettings. Will be removed in v5.0.0 ⚠️** + +- Added in v4.1.0 + +Set current properties for a Browser Source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `source` | _String_ | Name of the source. | +| `is_local_file` | _boolean (optional)_ | Indicates that a local file is in use. | +| `local_file` | _String (optional)_ | file path. | +| `url` | _String (optional)_ | Url. | +| `css` | _String (optional)_ | CSS to inject. | +| `width` | _int (optional)_ | Width. | +| `height` | _int (optional)_ | Height. | +| `fps` | _int (optional)_ | Framerate. | +| `shutdown` | _boolean (optional)_ | Indicates whether the source should be shutdown when not visible. | +| `render` | _boolean (optional)_ | Visibility of the scene item. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetSpecialSources + + +- Added in v4.1.0 + +Get configured special sources like Desktop Audio and Mic/Aux sources. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `desktop-1` | _String (optional)_ | Name of the first Desktop Audio capture source. | +| `desktop-2` | _String (optional)_ | Name of the second Desktop Audio capture source. | +| `mic-1` | _String (optional)_ | Name of the first Mic/Aux input source. | +| `mic-2` | _String (optional)_ | Name of the second Mic/Aux input source. | +| `mic-3` | _String (optional)_ | NAme of the third Mic/Aux input source. | + + +--- + +### GetSourceFilters + + +- Added in v4.5.0 + +List filters applied to a source + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `filters` | _Array<Object>_ | List of filters for the specified source | +| `filters.*.enabled` | _Boolean_ | Filter status (enabled or not) | +| `filters.*.type` | _String_ | Filter type | +| `filters.*.name` | _String_ | Filter name | +| `filters.*.settings` | _Object_ | Filter settings | + + +--- + +### GetSourceFilterInfo + + +- Added in v4.7.0 + +List filters applied to a source + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `filterName` | _String_ | Source filter name | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `enabled` | _Boolean_ | Filter status (enabled or not) | +| `type` | _String_ | Filter type | +| `name` | _String_ | Filter name | +| `settings` | _Object_ | Filter settings | + + +--- + +### AddFilterToSource + + +- Added in v4.5.0 + +Add a new filter to a source. Available source types along with their settings properties are available from `GetSourceTypesList`. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Name of the source on which the filter is added | +| `filterName` | _String_ | Name of the new filter | +| `filterType` | _String_ | Filter type | +| `filterSettings` | _Object_ | Filter settings | + + +**Response Items:** + +_No additional response items._ + +--- + +### RemoveFilterFromSource + + +- Added in v4.5.0 + +Remove a filter from a source + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Name of the source from which the specified filter is removed | +| `filterName` | _String_ | Name of the filter to remove | + + +**Response Items:** + +_No additional response items._ + +--- + +### ReorderSourceFilter + + +- Added in v4.5.0 + +Move a filter in the chain (absolute index positioning) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Name of the source to which the filter belongs | +| `filterName` | _String_ | Name of the filter to reorder | +| `newIndex` | _Integer_ | Desired position of the filter in the chain | + + +**Response Items:** + +_No additional response items._ + +--- + +### MoveSourceFilter + + +- Added in v4.5.0 + +Move a filter in the chain (relative positioning) + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Name of the source to which the filter belongs | +| `filterName` | _String_ | Name of the filter to reorder | +| `movementType` | _String_ | How to move the filter around in the source's filter chain. Either "up", "down", "top" or "bottom". | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSourceFilterSettings + + +- Added in v4.5.0 + +Update settings of a filter + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Name of the source to which the filter belongs | +| `filterName` | _String_ | Name of the filter to reconfigure | +| `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._ + +--- + +### GetAudioMonitorType + + +- Added in v4.8.0 + +Get the audio monitoring type of the specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `monitorType` | _String_ | The monitor type in use. Options: `none`, `monitorOnly`, `monitorAndOutput`. | + + +--- + +### SetAudioMonitorType + + +- Added in v4.8.0 + +Set the audio monitoring type of the specified source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | +| `monitorType` | _String_ | The monitor type to use. Options: `none`, `monitorOnly`, `monitorAndOutput`. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetSourceDefaultSettings + + +- Added in v4.9.0 + +Get the default settings for a given source type. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceKind` | _String_ | Source kind. Also called "source id" in libobs terminology. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceKind` | _String_ | Source kind. Same value as the `sourceKind` parameter. | +| `defaultSettings` | _Object_ | Settings object for source. | + + +--- + +### TakeSourceScreenshot + + +- Added in v4.6.0 + + + +At least `embedPictureFormat` or `saveToFilePath` must be specified. + +Clients can specify `width` and `height` parameters to receive scaled pictures. Aspect ratio is +preserved if only one of these two parameters is specified. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String (optional)_ | Source name. Note: Since scenes are also sources, you can also provide a scene name. If not provided, the currently active scene is used. | +| `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. | +| `fileFormat` | _String (optional)_ | Format to save the image file as (one of the values provided in the `supported-image-export-formats` response field of `GetVersion`). If not specified, tries to guess based on file extension. | +| `compressionQuality` | _int (optional)_ | Compression ratio between -1 and 100 to write the image with. -1 is automatic, 1 is smallest file/most compression, 100 is largest file/least compression. Varies with image type. | +| `width` | _int (optional)_ | Screenshot width. Defaults to the source's base width. | +| `height` | _int (optional)_ | Screenshot height. Defaults to the source's base height. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name | +| `img` | _String_ | Image Data URI (if `embedPictureFormat` was specified in the request) | +| `imageFile` | _String_ | Absolute path to the saved image file (if `saveToFilePath` was specified in the request) | + + +--- + +### RefreshBrowserSource + + +- Added in v4.9.0 + +Refreshes the specified browser source. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sourceName` | _String_ | Source name. | + + +**Response Items:** + +_No additional response items._ + +--- + +## Outputs + +### ListOutputs + + +- Added in v4.7.0 + +List existing outputs + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputs` | _Array<Output>_ | Outputs list | + + +--- + +### GetOutputInfo + + +- Added in v4.7.0 + +Get information about a single output + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputName` | _String_ | Output name | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputInfo` | _Output_ | Output info | + + +--- + +### StartOutput + + +- Added in v4.7.0 + + + +Note: Controlling outputs is an experimental feature of obs-websocket. Some plugins which add outputs to OBS may not function properly when they are controlled in this way. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputName` | _String_ | Output name | + + +**Response Items:** + +_No additional response items._ + +--- + +### StopOutput + + +- Added in v4.7.0 + + + +Note: Controlling outputs is an experimental feature of obs-websocket. Some plugins which add outputs to OBS may not function properly when they are controlled in this way. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `outputName` | _String_ | Output name | +| `force` | _boolean (optional)_ | Force stop (default: false) | + + +**Response Items:** + +_No additional response items._ + +--- + +## Profiles + +### SetCurrentProfile + + +- Added in v4.0.0 + +Set the currently active profile. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `profile-name` | _String_ | Name of the desired profile. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetCurrentProfile + + +- Added in v4.0.0 + +Get the name of the current profile. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `profile-name` | _String_ | Name of the currently active profile. | + + +--- + +### ListProfiles + + +- Added in v4.0.0 + +Get a list of available profiles. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `profiles` | _Array<Object>_ | List of available profiles. | +| `profiles.*.profile-name` | _String_ | Filter name | + + +--- + +## Recording + +### GetRecordingStatus + + +- Added in v4.9.0 + +Get current recording status. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `isRecording` | _boolean_ | Current recording status. | +| `isRecordingPaused` | _boolean_ | Whether the recording is paused or not. | +| `recordTimecode` | _String (optional)_ | Time elapsed since recording started (only present if currently recording). | +| `recordingFilename` | _String (optional)_ | Absolute path to the recording file (only present if currently recording). | + + +--- + +### StartStopRecording + + +- Added in v0.3 + +Toggle recording on or off (depending on the current recording state). + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### StartRecording + + +- Added in v4.1.0 + +Start recording. +Will return an `error` if recording is already active. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### StopRecording + + +- Added in v4.1.0 + +Stop recording. +Will return an `error` if recording is not active. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_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 + + +- Added in v4.1.0 + + + +Note: If `SetRecordingFolder` is called while a recording is +in progress, the change won't be applied immediately and will be +effective on the next recording. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `rec-folder` | _String_ | Path of the recording folder. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetRecordingFolder + + +- Added in v4.1.0 + +Get the path of the current recording folder. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `rec-folder` | _String_ | Path of the recording folder. | + + +--- + +## Replay Buffer + +### GetReplayBufferStatus + + +- Added in v4.9.0 + +Get the status of the OBS replay buffer. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `isReplayBufferActive` | _boolean_ | Current recording status. | + + +--- + +### StartStopReplayBuffer + + +- Added in v4.2.0 + +Toggle the Replay Buffer on/off (depending on the current state of the replay buffer). + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### StartReplayBuffer + + +- Added in v4.2.0 + +Start recording into the Replay Buffer. +Will return an `error` if the Replay Buffer is already active or if the +"Save Replay Buffer" hotkey is not set in OBS' settings. +Setting this hotkey is mandatory, even when triggering saves only +through obs-websocket. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### StopReplayBuffer + + +- Added in v4.2.0 + +Stop recording into the Replay Buffer. +Will return an `error` if the Replay Buffer is not active. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### SaveReplayBuffer + + +- Added in v4.2.0 + +Flush and save the contents of the Replay Buffer to disk. This is +basically the same as triggering the "Save Replay Buffer" hotkey. +Will return an `error` if the Replay Buffer is not active. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +## Scene Collections + +### SetCurrentSceneCollection + + +- Added in v4.0.0 + +Change the active scene collection. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sc-name` | _String_ | Name of the desired scene collection. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetCurrentSceneCollection + + +- Added in v4.0.0 + +Get the name of the current scene collection. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sc-name` | _String_ | Name of the currently active scene collection. | + + +--- + +### ListSceneCollections + + +- Added in v4.0.0 + +List available scene collections + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-collections` | _Array<ScenesCollection>_ | Scene collections list | + + +--- + +## Scene Items + +### GetSceneItemList + + +- Added in v4.9.0 + +Get a list of all scene items in a scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String (optional)_ | Name of the scene to get the list of scene items from. Defaults to the current scene if not specified. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String_ | Name of the requested (or current) scene | +| `sceneItems` | _Array<Object>_ | Array of scene items | +| `sceneItems.*.itemId` | _int_ | Unique item id of the source item | +| `sceneItems.*.sourceKind` | _String_ | ID if the scene item's source. For example `vlc_source` or `image_source` | +| `sceneItems.*.sourceName` | _String_ | Name of the scene item's source | +| `sceneItems.*.sourceType` | _String_ | Type of the scene item's source. Either `input`, `group`, or `scene` | + + +--- + +### GetSceneItemProperties + + +- Added in v4.3.0 + +Gets the scene specific properties of the specified source item. +Coordinates are relative to the item's parent (the scene or group it belongs to). + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the current scene. | +| `item` | _String \| Object_ | Scene Item name (if this field is a string) or specification (if it is an object). | +| `item.name` | _String (optional)_ | Scene Item name (if the `item` field is an object) | +| `item.id` | _int (optional)_ | Scene Item ID (if the `item` field is an object) | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Scene Item name. | +| `itemId` | _int_ | Scene Item ID. | +| `position.x` | _double_ | The x position of the source from the left. | +| `position.y` | _double_ | The y position of the source from the top. | +| `position.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. | +| `rotation` | _double_ | The clockwise rotation of the item in degrees around the point of alignment. | +| `scale.x` | _double_ | The x-scale factor of the source. | +| `scale.y` | _double_ | The y-scale factor of the source. | +| `scale.filter` | _String_ | The scale filter of the source. Can be "OBS_SCALE_DISABLE", "OBS_SCALE_POINT", "OBS_SCALE_BICUBIC", "OBS_SCALE_BILINEAR", "OBS_SCALE_LANCZOS" or "OBS_SCALE_AREA". | +| `crop.top` | _int_ | The number of pixels cropped off the top of the source before scaling. | +| `crop.right` | _int_ | The number of pixels cropped off the right of the source before scaling. | +| `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. | +| `bounds.x` | _double_ | Width of the bounding box. | +| `bounds.y` | _double_ | Height of the bounding box. | +| `sourceWidth` | _int_ | Base width (without scaling) of the source | +| `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) | +| `parentGroupName` | _String (optional)_ | Name of the item's parent (if this item belongs to a group) | +| `groupChildren` | _Array<SceneItemTransform> (optional)_ | List of children (if this item is a group) | + + +--- + +### SetSceneItemProperties + + +- Added in v4.3.0 + +Sets the scene specific properties of a source. Unspecified properties will remain unchanged. +Coordinates are relative to the item's parent (the scene or group it belongs to). + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the source item belongs to. Defaults to the current scene. | +| `item` | _String \| Object_ | Scene Item name (if this field is a string) or specification (if it is an object). | +| `item.name` | _String (optional)_ | Scene Item name (if the `item` field is an object) | +| `item.id` | _int (optional)_ | Scene Item ID (if the `item` field is an object) | +| `position.x` | _double (optional)_ | The new x position of the source. | +| `position.y` | _double (optional)_ | The new y position of the source. | +| `position.alignment` | _int (optional)_ | The new alignment of the source. | +| `rotation` | _double (optional)_ | The new clockwise rotation of the item in degrees. | +| `scale.x` | _double (optional)_ | The new x scale of the item. | +| `scale.y` | _double (optional)_ | The new y scale of the item. | +| `scale.filter` | _String (optional)_ | The new scale filter of the source. Can be "OBS_SCALE_DISABLE", "OBS_SCALE_POINT", "OBS_SCALE_BICUBIC", "OBS_SCALE_BILINEAR", "OBS_SCALE_LANCZOS" or "OBS_SCALE_AREA". | +| `crop.top` | _int (optional)_ | The new amount of pixels cropped off the top of the source before scaling. | +| `crop.bottom` | _int (optional)_ | The new amount of pixels cropped off the bottom of the source before scaling. | +| `crop.left` | _int (optional)_ | The new amount of pixels cropped off the left of the source before scaling. | +| `crop.right` | _int (optional)_ | The new amount of pixels cropped off the right of the source before scaling. | +| `visible` | _bool (optional)_ | The new visibility of the source. 'true' shows source, 'false' hides source. | +| `locked` | _bool (optional)_ | The new locked status of the source. 'true' keeps it in its current position, 'false' allows movement. | +| `bounds.type` | _String (optional)_ | The new bounds type of the source. 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 (optional)_ | The new alignment of the bounding box. (0-2, 4-6, 8-10) | +| `bounds.x` | _double (optional)_ | The new width of the bounding box. | +| `bounds.y` | _double (optional)_ | The new height of the bounding box. | + + +**Response Items:** + +_No additional response items._ + +--- + +### ResetSceneItem + + +- Added in v4.2.0 + +Reset a scene item. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the current scene. | +| `item` | _String \| Object_ | Scene Item name (if this field is a string) or specification (if it is an object). | +| `item.name` | _String (optional)_ | Scene Item name (if the `item` field is an object) | +| `item.id` | _int (optional)_ | Scene Item ID (if the `item` field is an object) | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSceneItemRender + + +- Added in v0.3 + +Show or hide a specified source item in a specified scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the currently active scene. | +| `source` | _String (optional)_ | Scene Item name. | +| `item` | _int (optional)_ | Scene Item id | +| `render` | _boolean_ | true = shown ; false = hidden | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSceneItemPosition + +- **⚠️ Deprecated. Since 4.3.0. Prefer the use of SetSceneItemProperties. ⚠️** + +- Added in v4.0.0 + +Sets the coordinates of a specified source item. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the current scene. | +| `item` | _String_ | Scene Item name. | +| `x` | _double_ | X coordinate. | +| `y` | _double_ | Y coordinate. | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSceneItemTransform + +- **⚠️ Deprecated. Since 4.3.0. Prefer the use of SetSceneItemProperties. ⚠️** + +- Added in v4.0.0 + +Set the transform of the specified source item. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the current scene. | +| `item` | _String_ | Scene Item name. | +| `x-scale` | _double_ | Width scale factor. | +| `y-scale` | _double_ | Height scale factor. | +| `rotation` | _double_ | Source item rotation (in degrees). | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSceneItemCrop + +- **⚠️ Deprecated. Since 4.3.0. Prefer the use of SetSceneItemProperties. ⚠️** + +- Added in v4.1.0 + +Sets the crop coordinates of the specified source item. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the current scene. | +| `item` | _String_ | Scene Item name. | +| `top` | _int_ | Pixel position of the top of the source item. | +| `bottom` | _int_ | Pixel position of the bottom of the source item. | +| `left` | _int_ | Pixel position of the left of the source item. | +| `right` | _int_ | Pixel position of the right of the source item. | + + +**Response Items:** + +_No additional response items._ + +--- + +### DeleteSceneItem + + +- Added in v4.5.0 + +Deletes a scene item. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene` | _String (optional)_ | Name of the scene the scene item belongs to. Defaults to the current scene. | +| `item` | _Object_ | Scene item to delete (required) | +| `item.name` | _String_ | Scene Item name (prefer `id`, including both is acceptable). | +| `item.id` | _int_ | Scene Item ID. | + + +**Response Items:** + +_No additional response items._ + +--- + +### AddSceneItem + + +- Added in v4.9.0 + +Creates a scene item in a scene. In other words, this is how you add a source into a scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String_ | Name of the scene to create the scene item in | +| `sourceName` | _String_ | Name of the source to be added | +| `setVisible` | _boolean_ | Whether to make the sceneitem visible on creation or not. Default `true` | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `itemId` | _int_ | Numerical ID of the created scene item | + + +--- + +### DuplicateSceneItem + + +- Added in v4.5.0 + +Duplicates a scene item. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `fromScene` | _String (optional)_ | Name of the scene to copy the item from. Defaults to the current scene. | +| `toScene` | _String (optional)_ | Name of the scene to create the item in. Defaults to the current scene. | +| `item` | _Object_ | Scene Item to duplicate from the source scene (required) | +| `item.name` | _String_ | Scene Item name (prefer `id`, including both is acceptable). | +| `item.id` | _int_ | Scene Item ID. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene` | _String_ | Name of the scene where the new item was created | +| `item` | _Object_ | New item info | +| `item.id` | _int_ | New item ID | +| `item.name` | _String_ | New item name | + + +--- + +## Scenes + +### SetCurrentScene + + +- Added in v0.3 + +Switch to the specified scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | Name of the scene to switch to. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetCurrentScene + + +- Added in v0.3 + +Get the current scene's name and source items. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Name of the currently active scene. | +| `sources` | _Array<SceneItem>_ | Ordered list of the current scene's source items. | + + +--- + +### GetSceneList + + +- Added in v0.3 + +Get a list of scenes in the currently active profile. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `current-scene` | _String_ | Name of the currently active scene. | +| `scenes` | _Array<Scene>_ | Ordered list of the current profile's scenes (See [GetCurrentScene](#getcurrentscene) for more information). | + + +--- + +### CreateScene + + +- Added in v4.9.0 + +Create a new scene scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String_ | Name of the scene to create. | + + +**Response Items:** + +_No additional response items._ + +--- + +### ReorderSceneItems + + +- Added in v4.5.0 + +Changes the order of scene items in the requested scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene` | _String (optional)_ | Name of the scene to reorder (defaults to current). | +| `items` | _Array<Scene>_ | Ordered list of objects with name and/or id specified. Id preferred due to uniqueness per scene | +| `items.*.id` | _int (optional)_ | Id of a specific scene item. Unique on a scene by scene basis. | +| `items.*.name` | _String (optional)_ | Name of a scene item. Sufficiently unique if no scene items share sources within the scene. | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetSceneTransitionOverride + + +- Added in v4.8.0 + +Set a scene to use a specific transition override. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String_ | Name of the scene to switch to. | +| `transitionName` | _String_ | Name of the transition to use. | +| `transitionDuration` | _int (Optional)_ | Duration in milliseconds of the transition if transition is not fixed. Defaults to the current duration specified in the UI if there is no current override and this value is not given. | + + +**Response Items:** + +_No additional response items._ + +--- + +### RemoveSceneTransitionOverride + + +- Added in v4.8.0 + +Remove any transition override on a scene. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String_ | Name of the scene to switch to. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetSceneTransitionOverride + + +- Added in v4.8.0 + +Get the current scene transition override. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `sceneName` | _String_ | Name of the scene to switch to. | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transitionName` | _String_ | Name of the current overriding transition. Empty string if no override is set. | +| `transitionDuration` | _int_ | Transition duration. `-1` if no override is set. | + + +--- + +## Streaming + +### GetStreamingStatus + + +- Added in v0.3 + +Get current streaming and recording status. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `streaming` | _boolean_ | Current streaming status. | +| `recording` | _boolean_ | Current recording status. | +| `recording-paused` | _boolean_ | If recording is paused. | +| `preview-only` | _boolean_ | Always false. Retrocompatibility with OBSRemote. | +| `stream-timecode` | _String (optional)_ | Time elapsed since streaming started (only present if currently streaming). | +| `rec-timecode` | _String (optional)_ | Time elapsed since recording started (only present if currently recording). | + + +--- + +### StartStopStreaming + + +- Added in v0.3 + +Toggle streaming on or off (depending on the current stream state). + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### StartStreaming + + +- Added in v4.1.0 + +Start streaming. +Will return an `error` if streaming is already active. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `stream` | _Object (optional)_ | Special stream configuration. Note: these won't be saved to OBS' configuration. | +| `stream.type` | _String (optional)_ | If specified ensures the type of stream matches the given type (usually 'rtmp_custom' or 'rtmp_common'). If the currently configured stream type does not match the given stream type, all settings must be specified in the `settings` object or an error will occur when starting the stream. | +| `stream.metadata` | _Object (optional)_ | Adds the given object parameters as encoded query string parameters to the 'key' of the RTMP stream. Used to pass data to the RTMP service about the streaming. May be any String, Numeric, or Boolean field. | +| `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`. | + + +**Response Items:** + +_No additional response items._ + +--- + +### StopStreaming + + +- Added in v4.1.0 + +Stop streaming. +Will return an `error` if streaming is not active. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### SetStreamSettings + + +- Added in v4.1.0 + +Sets one or more attributes of the current streaming server settings. Any options not passed will remain unchanged. Returns the updated settings in response. If 'type' is different than the current streaming service type, all settings are required. Returns the full settings of the stream (the same as GetStreamSettings). + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `type` | _String_ | The type of streaming service configuration, usually `rtmp_custom` or `rtmp_common`. | +| `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.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. | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetStreamSettings + + +- Added in v4.1.0 + +Get the current streaming server settings. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `type` | _String_ | The type of streaming service configuration. Possible values: 'rtmp_custom' or 'rtmp_common'. | +| `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`. | + + +--- + +### SaveStreamSettings + + +- Added in v4.1.0 + +Save the current streaming server settings to disk. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### SendCaptions + + +- Added in v4.6.0 + +Send the provided text as embedded CEA-608 caption data. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `text` | _String_ | Captions text | + + +**Response Items:** + +_No additional response items._ + +--- + +## Studio Mode + +### GetStudioModeStatus + + +- Added in v4.1.0 + +Indicates if Studio Mode is currently enabled. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `studio-mode` | _boolean_ | Indicates if Studio Mode is enabled. | + + +--- + +### GetPreviewScene + + +- Added in v4.1.0 + +Get the name of the currently previewed scene and its list of sources. +Will return an `error` if Studio Mode is not enabled. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | The name of the active preview scene. | +| `sources` | _Array<SceneItem>_ | | + + +--- + +### SetPreviewScene + + +- Added in v4.1.0 + +Set the active preview scene. +Will return an `error` if Studio Mode is not enabled. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `scene-name` | _String_ | The name of the scene to preview. | + + +**Response Items:** + +_No additional response items._ + +--- + +### TransitionToProgram + + +- Added in v4.1.0 + +Transitions the currently previewed scene to the main output. +Will return an `error` if Studio Mode is not enabled. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `with-transition` | _Object (optional)_ | Change the active transition before switching scenes. Defaults to the active transition. | +| `with-transition.name` | _String_ | Name of the transition. | +| `with-transition.duration` | _int (optional)_ | Transition duration (in milliseconds). | + + +**Response Items:** + +_No additional response items._ + +--- + +### EnableStudioMode + + +- Added in v4.1.0 + +Enables Studio Mode. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### DisableStudioMode + + +- Added in v4.1.0 + +Disables Studio Mode. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### ToggleStudioMode + + +- Added in v4.1.0 + +Toggles Studio Mode (depending on the current state of studio mode). + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +## Transitions + +### GetTransitionList + + +- Added in v4.1.0 + +List of all transitions available in the frontend's dropdown menu. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `current-transition` | _String_ | Name of the currently active transition. | +| `transitions` | _Array<Object>_ | List of transitions. | +| `transitions.*.name` | _String_ | Name of the transition. | + + +--- + +### GetCurrentTransition + + +- Added in v0.3 + +Get the name of the currently selected transition in the frontend's dropdown menu. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `name` | _String_ | Name of the selected transition. | +| `duration` | _int (optional)_ | Transition duration (in milliseconds) if supported by the transition. | + + +--- + +### SetCurrentTransition + + +- Added in v0.3 + +Set the active transition. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transition-name` | _String_ | The name of the transition. | + + +**Response Items:** + +_No additional response items._ + +--- + +### SetTransitionDuration + + +- Added in v4.0.0 + +Set the duration of the currently selected transition if supported. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `duration` | _int_ | Desired duration of the transition (in milliseconds). | + + +**Response Items:** + +_No additional response items._ + +--- + +### GetTransitionDuration + + +- Added in v4.1.0 + +Get the duration of the currently selected transition if supported. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transition-duration` | _int_ | Duration of the current transition (in milliseconds). | + + +--- + +### GetTransitionPosition + + +- Added in v4.9.0 + +Get the position of the current transition. + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `position` | _double_ | current transition position. This value will be between 0.0 and 1.0. Note: Transition returns 1.0 when not active. | + + +--- + +### GetTransitionSettings + + +- Added in v4.9.0 + +Get the current settings of a transition + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transitionName` | _String_ | Transition name | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transitionSettings` | _Object_ | Current transition settings | + + +--- + +### SetTransitionSettings + + +- Added in v4.9.0 + +Change the current settings of a transition + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transitionName` | _String_ | Transition name | +| `transitionSettings` | _Object_ | Transition settings (they can be partial) | + + +**Response Items:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `transitionSettings` | _Object_ | Updated transition settings | + + +--- + +### ReleaseTBar + + +- Added in v4.9.0 + +Release the T-Bar (like a user releasing their mouse button after moving it). +*YOU MUST CALL THIS if you called `SetTBarPosition` with the `release` parameter set to `false`.* + +**Request Fields:** + +_No specified parameters._ + +**Response Items:** + +_No additional response items._ + +--- + +### SetTBarPosition + + +- Added in v4.9.0 + + + +If your code needs to perform multiple successive T-Bar moves (e.g. : in an animation, or in response to a user moving a T-Bar control in your User Interface), set `release` to false and call `ReleaseTBar` later once the animation/interaction is over. + +**Request Fields:** + +| Name | Type | Description | +| ---- | :---: | ------------| +| `position` | _double_ | T-Bar position. This value must be between 0.0 and 1.0. | +| `release` | _boolean (optional)_ | Whether or not the T-Bar gets released automatically after setting its new position (like a user releasing their mouse button after moving the T-Bar). Call `ReleaseTBar` manually if you set `release` to false. Defaults to true. | + + +**Response Items:** + +_No additional response items._ + +--- + diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..487be3db --- /dev/null +++ b/docs/package.json @@ -0,0 +1,21 @@ +{ + "name": "obs-websocket-docs", + "version": "1.0.0", + "description": "", + "main": "docs.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "docs": "node ./docs.js", + "comments": "node ./comments.js", + "build": "npm run comments && npm run docs" + }, + "author": "", + "license": "ISC", + "dependencies": { + "glob": "^7.1.2", + "handlebars": "^4.0.10", + "handlebars-helpers": "^0.9.6", + "markdown-toc": "^1.1.0", + "parse-comments": "^0.4.3" + } +} diff --git a/docs/partials/eventsHeader.md b/docs/partials/eventsHeader.md new file mode 100644 index 00000000..e8ccff21 --- /dev/null +++ b/docs/partials/eventsHeader.md @@ -0,0 +1,11 @@ +# Events +Events are broadcast by the server to each connected client when a recognized action occurs within OBS. + +An event message will contain at least the following base fields: +- `update-type` _String_: the type of event. +- `stream-timecode` _String (optional)_: time elapsed between now and stream start (only present if OBS Studio is streaming). +- `rec-timecode` _String (optional)_: time elapsed between now and recording start (only present if OBS Studio is recording). + +Timecodes are sent using the format: `HH:MM:SS.mmm` + +Additional fields may be present in the event message depending on the event type. diff --git a/docs/partials/introduction.md b/docs/partials/introduction.md new file mode 100644 index 00000000..9fbd30d4 --- /dev/null +++ b/docs/partials/introduction.md @@ -0,0 +1,38 @@ +# obs-websocket 4.9.0 protocol reference + +# General Introduction +Messages are exchanged between the client and the server as JSON objects. +This protocol is based on the original OBS Remote protocol created by Bill Hamilton, with new commands specific to OBS Studio. As of v5.0.0, backwards compatability with the protocol will not be kept. + +# Authentication +**Starting with obs-websocket 4.9, authentication is enabled by default and users are encouraged to configure a password on first run.** + +`obs-websocket` uses SHA256 to transmit credentials. + +A request for [`GetAuthRequired`](#getauthrequired) returns two elements: +- A `challenge`: a random string that will be used to generate the auth response. +- A `salt`: applied to the password when generating the auth response. + +To generate the answer to the auth challenge, follow this procedure: +- Concatenate the user declared password with the `salt` sent by the server (in this order: `password + server salt`). +- Generate a binary SHA256 hash of the result and encode the resulting SHA256 binary hash to base64, known as a `base64 secret`. +- Concatenate the base64 secret with the `challenge` sent by the server (in this order: `base64 secret + server challenge`). +- Generate a binary SHA256 hash of the result and encode it to base64. +- Voilà, this last base64 string is the `auth response`. You may now use it to authenticate to the server with the [`Authenticate`](#authenticate) request. + +Pseudo Code Example: +``` +password = "supersecretpassword" +challenge = "ztTBnnuqrqaKDzRM3xcVdbYm" +salt = "PZVbYpvAnZut2SS6JNJytDm9" + +secret_string = password + salt +secret_hash = binary_sha256(secret_string) +secret = base64_encode(secret_hash) + +auth_response_string = secret + challenge +auth_response_hash = binary_sha256(auth_response_string) +auth_response = base64_encode(auth_response_hash) +``` + +You can also refer to any of the client libraries listed on the [README](README.md) for examples of how to authenticate. diff --git a/docs/partials/requestsHeader.md b/docs/partials/requestsHeader.md new file mode 100644 index 00000000..b73efc0c --- /dev/null +++ b/docs/partials/requestsHeader.md @@ -0,0 +1,11 @@ +# Requests +Requests are sent by the client and require at least the following two fields: +- `request-type` _String_: String name of the request type. +- `message-id` _String_: Client defined identifier for the message, will be echoed in the response. + +Once a request is sent, the server will return a JSON response with at least the following fields: +- `message-id` _String_: The client defined identifier specified in the request. +- `status` _String_: Response status, will be one of the following: `ok`, `error` +- `error` _String (Optional)_: An error message accompanying an `error` status. + +Additional information may be required/returned depending on the request type. See below for more information. diff --git a/docs/partials/typedefsHeader.md b/docs/partials/typedefsHeader.md new file mode 100644 index 00000000..2e1a057d --- /dev/null +++ b/docs/partials/typedefsHeader.md @@ -0,0 +1,2 @@ +# Typedefs +These are complex types, such as `Source` and `Scene`, which are used as arguments or return values in multiple requests and/or events. diff --git a/docs/protocol.hbs b/docs/protocol.hbs new file mode 100644 index 00000000..154491a1 --- /dev/null +++ b/docs/protocol.hbs @@ -0,0 +1,112 @@ +{{#read "partials/introduction.md"}}{{/read}} + + + +# Table of Contents + + + + +{{#read "partials/typedefsHeader.md"}}{{/read}} + +{{#each typedefs}} +## {{typedefs.0.name}} +| Name | Type | Description | +| ---- | :---: | ------------| +{{#each properties}} +| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} | +{{/each}} +{{/each}} + + + +{{#read "partials/eventsHeader.md"}}{{/read}} + +{{#each events}} +## {{capitalizeAll @key}} + +{{#each this}} +### {{name}} + +{{#if deprecated}} +- **⚠️ Deprecated. {{deprecated}} ⚠️** +{{/if}} + +{{#eq since "unreleased"}} +- Unreleased +{{else}} +- Added in v{{since}} +{{/eq}} + +{{{description}}} + +**Response Items:** + +{{#if returns.length}} +| Name | Type | Description | +| ---- | :---: | ------------| +{{#each returns}} +| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} | +{{/each}} + +{{else}} +_No additional response items._ +{{/if}} + +--- + +{{/each}} +{{/each}} + + + +{{#read "partials/requestsHeader.md"}}{{/read}} + +{{#each requests}} +## {{capitalizeAll @key}} + +{{#each this}} +### {{name}} + +{{#if deprecated}} +- **⚠️ Deprecated. {{deprecated}} ⚠️** +{{/if}} + +{{#eq since "unreleased"}} +- Unreleased +{{else}} +- Added in v{{since}} +{{/eq}} + +{{{description}}} + +**Request Fields:** + +{{#if params.length}} +| Name | Type | Description | +| ---- | :---: | ------------| +{{#each params}} +| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} | +{{/each}} + +{{else}} +_No specified parameters._ +{{/if}} + +**Response Items:** + +{{#if returns.length}} +| Name | Type | Description | +| ---- | :---: | ------------| +{{#each returns}} +| `{{name}}` | _{{depipe type}}_ | {{{depipe description}}} | +{{/each}} + +{{else}} +_No additional response items._ +{{/if}} + +--- + +{{/each}} +{{/each}} diff --git a/external/FindLibObs.cmake b/external/FindLibObs.cmake new file mode 100644 index 00000000..ab0a3dea --- /dev/null +++ b/external/FindLibObs.cmake @@ -0,0 +1,107 @@ +# This module can be copied and used by external plugins for OBS +# +# Once done these will be defined: +# +# LIBOBS_FOUND +# LIBOBS_INCLUDE_DIRS +# LIBOBS_LIBRARIES + +find_package(PkgConfig QUIET) +if (PKG_CONFIG_FOUND) + pkg_check_modules(_OBS QUIET obs libobs) +endif() + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) +else() + set(_lib_suffix 32) +endif() + +if(DEFINED CMAKE_BUILD_TYPE) + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + set(_build_type_base "debug") + else() + set(_build_type_base "release") + endif() +endif() + +find_path(LIBOBS_INCLUDE_DIR + NAMES obs.h + HINTS + ENV obsPath${_lib_suffix} + ENV obsPath + ${obsPath} + PATHS + /usr/include /usr/local/include /opt/local/include /sw/include + PATH_SUFFIXES + libobs + ) + +function(find_obs_lib base_name repo_build_path lib_name) + string(TOUPPER "${base_name}" base_name_u) + + if(DEFINED _build_type_base) + set(_build_type_${repo_build_path} "${_build_type_base}/${repo_build_path}") + set(_build_type_${repo_build_path}${_lib_suffix} "${_build_type_base}${_lib_suffix}/${repo_build_path}") + endif() + + find_library(${base_name_u}_LIB + NAMES ${_${base_name_u}_LIBRARIES} ${lib_name} lib${lib_name} + HINTS + ENV obsPath${_lib_suffix} + ENV obsPath + ${obsPath} + ${_${base_name_u}_LIBRARY_DIRS} + PATHS + /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} lib + libs${_lib_suffix} libs + bin${_lib_suffix} bin + ../lib${_lib_suffix} ../lib + ../libs${_lib_suffix} ../libs + ../bin${_lib_suffix} ../bin + # base repo non-msvc-specific search paths + ${_build_type_${repo_build_path}} + ${_build_type_${repo_build_path}${_lib_suffix}} + build/${repo_build_path} + build${_lib_suffix}/${repo_build_path} + # base repo msvc-specific search paths on windows + build${_lib_suffix}/${repo_build_path}/Debug + build${_lib_suffix}/${repo_build_path}/RelWithDebInfo + build/${repo_build_path}/Debug + build/${repo_build_path}/RelWithDebInfo + ) +endfunction() + +find_obs_lib(LIBOBS libobs obs) + +if(MSVC) + find_obs_lib(W32_PTHREADS deps/w32-pthreads w32-pthreads) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libobs DEFAULT_MSG LIBOBS_LIB LIBOBS_INCLUDE_DIR) +mark_as_advanced(LIBOBS_INCLUDE_DIR LIBOBS_LIB) + +if(LIBOBS_FOUND) + if(MSVC) + if (NOT DEFINED W32_PTHREADS_LIB) + message(FATAL_ERROR "Could not find the w32-pthreads library" ) + endif() + + set(W32_PTHREADS_INCLUDE_DIR ${LIBOBS_INCLUDE_DIR}/../deps/w32-pthreads) + endif() + + set(LIBOBS_INCLUDE_DIRS ${LIBOBS_INCLUDE_DIR} ${W32_PTHREADS_INCLUDE_DIR}) + set(LIBOBS_LIBRARIES ${LIBOBS_LIB} ${W32_PTHREADS_LIB}) + include(${LIBOBS_INCLUDE_DIR}/../cmake/external/ObsPluginHelpers.cmake) + + # allows external plugins to easily use/share common dependencies that are often included with libobs (such as FFmpeg) + if(NOT DEFINED INCLUDED_LIBOBS_CMAKE_MODULES) + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${LIBOBS_INCLUDE_DIR}/../cmake/Modules/") + set(INCLUDED_LIBOBS_CMAKE_MODULES true) + endif() +else() + message(FATAL_ERROR "Could not find the libobs library" ) +endif() diff --git a/installer/installer.iss b/installer/installer.iss new file mode 100644 index 00000000..0109c12d --- /dev/null +++ b/installer/installer.iss @@ -0,0 +1,69 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "obs-websocket" +#define MyAppVersion "4.9.0" +#define MyAppPublisher "Stephane Lepin" +#define MyAppURL "http://github.com/Palakis/obs-websocket" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{117EE44F-48E1-49E5-A381-CC8D9195CF35} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={code:GetDirName} +DefaultGroupName={#MyAppName} +OutputBaseFilename=obs-websocket-Windows-Installer +Compression=lzma +SolidCompression=yes +DirExistsWarning=no + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Files] +Source: "..\release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "..\LICENSE"; Flags: dontcopy +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" + +[Code] +procedure InitializeWizard(); +var + GPLText: AnsiString; + Page: TOutputMsgMemoWizardPage; +begin + ExtractTemporaryFile('LICENSE'); + LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText); + + Page := CreateOutputMsgMemoPage(wpWelcome, + 'License Information', 'Please review the license terms before installing obs-websocket', + 'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.', + String(GPLText) + ); +end; + +// credit where it's due : +// following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 +function GetDirName(Value: string): string; +var + InstallPath: string; +begin + // initialize default path, which will be returned when the following registry + // key queries fail due to missing keys or for some different reason + Result := '{pf}\obs-studio'; + // query the first registry value; if this succeeds, return the obtained value + if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then + Result := InstallPath +end; + diff --git a/src/forms/settings-dialog.cpp b/src/forms/settings-dialog.cpp new file mode 100644 index 00000000..c90c00d0 --- /dev/null +++ b/src/forms/settings-dialog.cpp @@ -0,0 +1,39 @@ +#include "settings-dialog.h" + +#include +#include +#include + +#include "../obs-websocket.h" + + +#define CHANGE_ME "changeme" + +SettingsDialog::SettingsDialog(QWidget* parent) : + QDialog(parent, Qt::Dialog), + ui(new Ui::SettingsDialog) +{ + ui->setupUi(this); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, + this, &SettingsDialog::FormAccepted); +} + +void SettingsDialog::showEvent(QShowEvent* event) { + ; +} + +void SettingsDialog::ToggleShowHide() { + if (!isVisible()) + setVisible(true); + else + setVisible(false); +} + +void SettingsDialog::FormAccepted() { + ; +} + +SettingsDialog::~SettingsDialog() { + delete ui; +} diff --git a/src/forms/settings-dialog.h b/src/forms/settings-dialog.h new file mode 100644 index 00000000..37e78fdb --- /dev/null +++ b/src/forms/settings-dialog.h @@ -0,0 +1,40 @@ +/* +obs-websocket +Copyright (C) 2016-2019 Stéphane Lepin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program. If not, see +*/ + +#pragma once + +#include + +#include "ui_settings-dialog.h" + +class SettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SettingsDialog(QWidget* parent = 0); + ~SettingsDialog(); + void showEvent(QShowEvent* event); + void ToggleShowHide(); + +private Q_SLOTS: + void FormAccepted(); + +private: + Ui::SettingsDialog* ui; +}; diff --git a/src/forms/settings-dialog.ui b/src/forms/settings-dialog.ui new file mode 100644 index 00000000..237fb932 --- /dev/null +++ b/src/forms/settings-dialog.ui @@ -0,0 +1,158 @@ + + + SettingsDialog + + + + 0 + 0 + 407 + 216 + + + + + 0 + 0 + + + + OBSWebsocket.Settings.DialogTitle + + + false + + + + QLayout::SetDefaultConstraint + + + + + + + OBSWebsocket.Settings.AuthRequired + + + + + + + OBSWebsocket.Settings.Password + + + + + + + QLineEdit::Password + + + + + + + OBSWebsocket.Settings.ServerEnable + + + true + + + + + + + OBSWebsocket.Settings.ServerPort + + + + + + + 1024 + + + 65535 + + + 4444 + + + + + + + OBSWebsocket.Settings.AlertsEnable + + + true + + + + + + + OBSWebsocket.Settings.DebugEnable + + + false + + + + + + + OBSWebsocket.Settings.LockToIPv4 + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SettingsDialog + accept() + + + 248 + 294 + + + 157 + 314 + + + + + buttonBox + rejected() + SettingsDialog + reject() + + + 316 + 300 + + + 286 + 314 + + + + + diff --git a/src/obs-websocket.cpp b/src/obs-websocket.cpp new file mode 100644 index 00000000..3404bebd --- /dev/null +++ b/src/obs-websocket.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +#include +#include +#include + +#include "obs-websocket.h" +#include "forms/settings-dialog.h" + +void ___source_dummy_addref(obs_source_t*) {} +void ___sceneitem_dummy_addref(obs_sceneitem_t*) {} +void ___data_dummy_addref(obs_data_t*) {} +void ___data_array_dummy_addref(obs_data_array_t*) {} +void ___output_dummy_addref(obs_output_t*) {} + +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") + +SettingsDialog* settingsDialog = nullptr; + +bool obs_module_load(void) { + blog(LOG_INFO, "you can haz websockets (version %s)", OBS_WEBSOCKET_VERSION); + blog(LOG_INFO, "qt version (compile-time): %s ; qt version (run-time): %s", + QT_VERSION_STR, qVersion()); + + // UI setup + obs_frontend_push_ui_translation(obs_module_get_string); + QMainWindow* mainWindow = (QMainWindow*)obs_frontend_get_main_window(); + settingsDialog = new SettingsDialog(mainWindow); + obs_frontend_pop_ui_translation(); + + const char* menuActionText = + obs_module_text("OBSWebsocket.Settings.DialogTitle"); + QAction* menuAction = + (QAction*)obs_frontend_add_tools_menu_qaction(menuActionText); + QObject::connect(menuAction, &QAction::triggered, [] { + // The settings dialog belongs to the main window. Should be ok + // to pass the pointer to this QAction belonging to the main window + settingsDialog->ToggleShowHide(); + }); + + // Loading finished + blog(LOG_INFO, "module loaded!"); + + return true; +} + +void obs_module_unload() { + blog(LOG_INFO, "goodbye!"); +} \ No newline at end of file diff --git a/src/obs-websocket.h b/src/obs-websocket.h new file mode 100644 index 00000000..1cd6e7fc --- /dev/null +++ b/src/obs-websocket.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +void ___source_dummy_addref(obs_source_t*); +void ___sceneitem_dummy_addref(obs_sceneitem_t*); +void ___data_dummy_addref(obs_data_t*); +void ___data_array_dummy_addref(obs_data_array_t*); +void ___output_dummy_addref(obs_output_t*); + +using OBSSourceAutoRelease = + OBSRef; +using OBSSceneItemAutoRelease = + OBSRef; +using OBSDataAutoRelease = + OBSRef; +using OBSDataArrayAutoRelease = + OBSRef; +using OBSOutputAutoRelease = + OBSRef; + +void ___data_item_dummy_addref(obs_data_item_t*); +void ___data_item_release(obs_data_item_t*); +using OBSDataItemAutoRelease = + OBSRef; + +#define OBS_WEBSOCKET_VERSION "5.0.0" + +#define blog(level, msg, ...) blog(level, "[obs-websocket] " msg, ##__VA_ARGS__) diff --git a/src/rpc/RpcEvent.cpp b/src/rpc/RpcEvent.cpp new file mode 100644 index 00000000..fee5e6c7 --- /dev/null +++ b/src/rpc/RpcEvent.cpp @@ -0,0 +1,14 @@ +#include "RpcEvent.h" + +RpcEvent::RpcEvent ( + const QString& updateType, + obs_data_t* additionalFields +) : + _updateType(updateType), + _additionalFields(nullptr) +{ + if (additionalFields) { + _additionalFields = obs_data_create(); + obs_data_apply(_additionalFields, additionalFields); + } +} \ No newline at end of file diff --git a/src/rpc/RpcEvent.h b/src/rpc/RpcEvent.h new file mode 100644 index 00000000..b0cbde60 --- /dev/null +++ b/src/rpc/RpcEvent.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "../obs-websocket.h" + +class RpcEvent { + public: + explicit RpcEvent ( + const QString& updateType, + obs_data_t* additionalFields = nullptr + ); + + const QString& updateType() const + { + return _updateType; + } + + const OBSData additionalFields() const + { + return OBSData(_additionalFields); + } + + private: + QString _updateType; + OBSDataAutoRelease _additionalFields; +}; \ No newline at end of file diff --git a/src/rpc/RpcRequest.h b/src/rpc/RpcRequest.h new file mode 100644 index 00000000..c23b18ab --- /dev/null +++ b/src/rpc/RpcRequest.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include +#include "../obs-websocket.h" + +namespace RequestStatus { + typedef uint16_t ResponseCode; + + static ResponseCode const Unknown = 0; + + static ResponseCode const Success = 100; + + // The request is denied because the client is not authenticated + static ResponseCode const AuthenticationMissing = 200; + // Connection has already been authenticated (for modules utilizing a request to provide authentication) + static ResponseCode const AlreadyAuthenticated = 201; + // Authentication request was denied (for modules utilizing a request to provide authentication) + static ResponseCode const AuthenticationDenied = 202; + // The request type is invalid (does not exist) + static ResponseCode const InvalidRequestType = 203; + // Generic error code (comment is expected to be provided) + static ResponseCode const GenericError = 204; + + // A required request parameter is missing + static ResponseCode const MissingRequestParameter = 300; + + // Generic invalid request parameter message + static ResponseCode const InvalidRequestParameter = 400; + // A request parameter has the wrong data type + static ResponseCode const InvalidRequestParameterDataType = 401; + // A request parameter (float or int) is out of valid range + static ResponseCode const RequestParameterOutOfRange = 402; + // A request parameter (string or array) is empty and cannot be + static ResponseCode const RequestParameterEmpty = 403; + + // An output is running and cannot be in order to perform the request (generic) + static ResponseCode const OutputRunning = 500; + // An output is not running and should be + static ResponseCode const OutputNotRunning = 501; + // Stream is running and cannot be + static ResponseCode const StreamRunning = 502; + // Stream is not running and should be + static ResponseCode const StreamNotRunning = 503; + // Record is running and cannot be + static ResponseCode const RecordRunning = 504; + // Record is not running and should be + static ResponseCode const RecordNotRunning = 505; + // Record is paused and cannot be + static ResponseCode const RecordPaused = 506; + // Replay buffer is running and cannot be + static ResponseCode const ReplayBufferRunning = 507; + // Replay buffer is not running and should be + static ResponseCode const ReplayBufferNotRunning = 508; + // Replay buffer is disabled and cannot be + static ResponseCode const ReplayBufferDisabled = 509; + // Studio mode is active and cannot be + static ResponseCode const StudioModeActive = 510; + // Studio mode is not active and should be + static ResponseCode const StudioModeNotActive = 511; + + // The specified source was of the invalid type (Eg. input instead of scene) + static ResponseCode const InvalidSourceType = 600; + // The specified source was not found (generic for input, filter, transition, scene) + static ResponseCode const SourceNotFound = 601; + // The specified source already exists. Applicable to inputs, filters, transitions, scenes + static ResponseCode const SourceAlreadyExists = 602; + // The specified input was not found + static ResponseCode const InputNotFound = 603; + // The specified input had the wrong kind + static ResponseCode const InvalidInputKind = 604; + // The specified filter was not found + static ResponseCode const FilterNotFound = 605; + // The specified transition was not found + static ResponseCode const TransitionNotFound = 606; + // The specified transition does not support setting its position (transition is of fixed type) + static ResponseCode const TransitionDurationFixed = 607; + // The specified scene was not found + static ResponseCode const SceneNotFound = 608; + // The specified scene item was not found + static ResponseCode const SceneItemNotFound = 609; + // The specified scene collection was not found + static ResponseCode const SceneCollectionNotFound = 610; + // The specified profile was not found + static ResponseCode const ProfileNotFound = 611; + // The specified output was not found + static ResponseCode const OutputNotFound = 612; + // The specified encoder was not found + static ResponseCode const EncoderNotFound = 613; + // The specified service was not found + static ResponseCode const ServiceNotFound = 614; + + // Processing the request failed unexpectedly + static ResponseCode const RequestProcessingFailed = 700; + // Starting the Output failed + static ResponseCode const OutputStartFailed = 701; + // Duplicating the scene item failed + static ResponseCode const SceneItemDuplicationFailed = 702; + // Rendering the screenshot failed + static ResponseCode const ScreenshotRenderFailed = 703; + // Encoding the screenshot failed + static ResponseCode const ScreenshotEncodeFailed = 704; + // Saving the screenshot failed + static ResponseCode const ScreenshotSaveFailed = 705; +}; + +class RpcRequest; + +class RpcResponse { + public: + ; + + private: + ; +}; + +class RpcRequest { + public: + ; + + private: + ; +}; \ No newline at end of file diff --git a/src/rpc/RpcRequestData.cpp b/src/rpc/RpcRequestData.cpp new file mode 100644 index 00000000..8cc5a4d4 --- /dev/null +++ b/src/rpc/RpcRequestData.cpp @@ -0,0 +1 @@ +#include "RpcRequest.h" \ No newline at end of file diff --git a/src/rpc/RpcRequestResponse.cpp b/src/rpc/RpcRequestResponse.cpp new file mode 100644 index 00000000..8cc5a4d4 --- /dev/null +++ b/src/rpc/RpcRequestResponse.cpp @@ -0,0 +1 @@ +#include "RpcRequest.h" \ No newline at end of file