diff --git a/.github/actions/setup-macos-codesigning/action.yaml b/.github/actions/setup-macos-codesigning/action.yaml index 56cdba48..5f24114b 100644 --- a/.github/actions/setup-macos-codesigning/action.yaml +++ b/.github/actions/setup-macos-codesigning/action.yaml @@ -75,7 +75,7 @@ runs: print -n "${MACOS_SIGNING_CERT}" | base64 --decode --output="${certificate_path}" - : "${MACOS_KEYCHAIN_PASSWORD:="$(print ${RANDOM} | sha1sum | head -c 32)"}" + : "${MACOS_KEYCHAIN_PASSWORD:="$(print ${RANDOM} | shasum | head -c 32)"}" print '::group::Keychain setup' security create-keychain -p "${MACOS_KEYCHAIN_PASSWORD}" ${keychain_path} diff --git a/.github/scripts/.build.zsh b/.github/scripts/.build.zsh index 4d2f08a0..708fdcf2 100755 --- a/.github/scripts/.build.zsh +++ b/.github/scripts/.build.zsh @@ -242,6 +242,7 @@ ${_usage_host:-}" -G "${generator}" -DQT_VERSION=${QT_VERSION:-6} -DCMAKE_BUILD_TYPE=${config} + -DCMAKE_INSTALL_PREFIX=/usr ) local cmake_version diff --git a/.github/workflows/build-project.yaml b/.github/workflows/build-project.yaml index 556ec4f8..8f29b9a5 100644 --- a/.github/workflows/build-project.yaml +++ b/.github/workflows/build-project.yaml @@ -3,7 +3,7 @@ on: workflow_call: jobs: check-event: - name: Check GitHub Event Data 📡 + name: Check GitHub Event Data 🔎 runs-on: ubuntu-22.04 defaults: run: @@ -29,7 +29,7 @@ jobs: case "${GITHUB_EVENT_NAME}" in pull_request) config_data=('codesign:false' 'notarize:false' 'package:false' 'config:RelWithDebInfo') - if gh pr view --json labels \ + if gh pr view ${{ github.event.number }} --json labels \ | jq -e -r '.labels[] | select(.name == "Seeking Testers")' > /dev/null; then config_data[0]='codesign:true' config_data[2]='package:true' @@ -77,9 +77,9 @@ jobs: if (( ${+RUNNER_DEBUG} )) setopt XTRACE print '::group::Clean Homebrew Environment' - typeset -a to_remove=() + local -a to_remove=() - if (( #to_remove > 0 )) brew uninstall --ignore-dependencies ${to_remove} + if (( #to_remove )) brew uninstall --ignore-dependencies ${to_remove} print '::endgroup::' local product_name @@ -100,7 +100,7 @@ jobs: - name: Set Up Codesigning 🔑 uses: ./.github/actions/setup-macos-codesigning - if: ${{ fromJSON(needs.check-event.outputs.codesign) }} + if: fromJSON(needs.check-event.outputs.codesign) id: codesign with: codesignIdentity: ${{ secrets.MACOS_SIGNING_APPLICATION_IDENTITY }} diff --git a/CI/libndi-create-deb.sh b/CI/libndi-create-deb.sh index db0ad283..6ae6592d 100755 --- a/CI/libndi-create-deb.sh +++ b/CI/libndi-create-deb.sh @@ -1,4 +1,7 @@ #!/bin/bash + +# This script is called by CI/libndi-package.sh + set -ex SDK_ROOT="/tmp/ndisdk" diff --git a/CI/libndi-create-dev-deb.sh b/CI/libndi-create-dev-deb.sh index d57d9b90..8da50209 100755 --- a/CI/libndi-create-dev-deb.sh +++ b/CI/libndi-create-dev-deb.sh @@ -1,4 +1,7 @@ #!/bin/bash + +# This script is called by CI/libndi-package.sh + set -ex SDK_ROOT="/tmp/ndisdk" diff --git a/CI/libndi-get.sh b/CI/libndi-get.sh index 2070bca5..317c184b 100755 --- a/CI/libndi-get.sh +++ b/CI/libndi-get.sh @@ -1,16 +1,14 @@ #!/bin/bash - set -e LIBNDI_INSTALLER_NAME="Install_NDI_SDK_v5_Linux" LIBNDI_INSTALLER="$LIBNDI_INSTALLER_NAME.tar.gz" -LIBNDI_INSTALLER_SHA256="00d0bedc2c72736d82883fc0fd6bc1a544e7958c7e46db79f326633d44e15153" #sudo apt-get install curl pushd /tmp + curl -L -o $LIBNDI_INSTALLER https://downloads.ndi.tv/SDK/NDI_SDK_Linux/$LIBNDI_INSTALLER -f --retry 5 -echo "$LIBNDI_INSTALLER_SHA256 $LIBNDI_INSTALLER" | sha256sum -c tar -xf $LIBNDI_INSTALLER yes | PAGER="cat" sh $LIBNDI_INSTALLER_NAME.sh @@ -18,8 +16,10 @@ rm -rf /tmp/ndisdk mv "/tmp/NDI SDK for Linux" /tmp/ndisdk ls /tmp/ndisdk -# NOTE: This does an actual local install... -#sudo cp -P ndisdk/lib/x86_64-linux-gnu/* /usr/local/lib/ -#sudo ldconfig +if [ $1 == "install" ]; then + # NOTE: This should do an actual local install... + sudo cp -P /tmp/ndisdk/lib/x86_64-linux-gnu/* /usr/local/lib/ + sudo ldconfig +fi popd diff --git a/CI/libndi-package.sh b/CI/libndi-package.sh index 3ace482e..d6fb3a47 100755 --- a/CI/libndi-package.sh +++ b/CI/libndi-package.sh @@ -1,8 +1,12 @@ #!/bin/bash +# +# This script calls libndi-get.sh and then libndi-create-deb.sh and libndi-create-dev-deb.sh +# + set -e -LIBNDI_VERSION="5.5.3" +LIBNDI_VERSION="5.6.0" SCRIPT_DIR=$(dirname "$0") diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a7b15ee..46d87a6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,9 +4,6 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCO project(${_name} VERSION ${_version}) -option(ENABLE_FRONTEND_API "Use obs-frontend-api for UI functionality" ON) -option(ENABLE_QT "Use Qt functionality" ON) - include(compilerconfig) include(defaults) include(helpers) @@ -34,9 +31,13 @@ if(ENABLE_QT) AUTORCC ON) endif() -# Add your custom source files here - header files are optional and only required for visibility +# cmake-format: off +# Add your custom source files here Header files are optional and only required for visibility. # e.g. in Xcode or Visual Studio -target_sources(${CMAKE_PROJECT_NAME} PRIVATE src/plugin-main.cpp +# cmake-format: on +target_sources( + ${CMAKE_PROJECT_NAME} + PRIVATE src/plugin-main.cpp src/plugin-main.h src/obs-ndi-source.cpp src/obs-ndi-output.cpp diff --git a/README.md b/README.md index 9d476d8c..e68c65eb 100644 --- a/README.md +++ b/README.md @@ -16,52 +16,76 @@ Network A/V in OBS Studio with NewTek's NDI technology. ## Requirements * OBS >=28 -* NDI 5 Runtime (optionally installed via NDI Tools) - -# Install -1. Download and install the Linux, MacOS, or Windows version at [Releases](https://github.com/obs-ndi/obs-ndi/releases). - * Linux: - 1. Download [obs-ndi-4.11.1-linux-x86_64.deb](https://github.com/obs-ndi/obs-ndi/releases/download/4.11.1/obs-ndi-4.11.1-linux-x86_64.deb) - 2. `sudo dpkg -i obs-ndi-4.11.1-linux-x86_64.deb` - * MacOS: - 1. Download [obs-ndi-4.11.1-macos-universal.pkg](https://github.com/obs-ndi/obs-ndi/releases/download/4.11.1/obs-ndi-4.11.1-macos-universal.pkg) - 2. Run `obs-ndi-4.11.1-macos-universal.pkg` - * Windows: - 1. Download [obs-ndi-4.11.1-windows-x64-Installer.exe](https://github.com/obs-ndi/obs-ndi/releases/download/4.11.1/obs-ndi-4.11.1-windows-x64-Installer.exe) - 2. Run `obs-ndi-4.11.1-windows-x64-Installer.exe` -2. Download and install the NDI 5 runtime from: - * Linux: - ``` - #!/bin/bash - set -e - LIBNDI_INSTALLER_NAME="Install_NDI_SDK_v5_Linux" - LIBNDI_INSTALLER="$LIBNDI_INSTALLER_NAME.tar.gz" - LIBNDI_INSTALLER_SHA256="00d0bedc2c72736d82883fc0fd6bc1a544e7958c7e46db79f326633d44e15153" - pushd /tmp - sudo apt-get install curl - curl -L -o $LIBNDI_INSTALLER https://downloads.ndi.tv/SDK/NDI_SDK_Linux/$LIBNDI_INSTALLER -f --retry 5 - echo "$LIBNDI_INSTALLER_SHA256 $LIBNDI_INSTALLER" | sha256sum -c - tar -xf $LIBNDI_INSTALLER - yes | PAGER="cat" sh $LIBNDI_INSTALLER_NAME.sh - rm -rf ndisdk - mv "NDI SDK for Linux" ndisdk - sudo cp -P ndisdk/lib/x86_64-linux-gnu/* /usr/local/lib/ - sudo ldconfig - echo libndi installed to /usr/local/lib/ - ls -la /usr/local/lib/libndi* - rm -rf ndisdk - popd - ``` +* NDI 5 Runtime + We are not allowed to directly distribute the NDI runtime here, but you can get it from either + [the links below](#Download_&_Install_The_NDI_5_Runtime), or installing + [NDI Tools](https://ndi.video/tools/), or the [NDI SDK](https://ndi.video/download-ndi-sdk/). + +# Install OBS-NDI +Download and install the Linux, MacOS, or Windows version at [Releases](https://github.com/obs-ndi/obs-ndi/releases). + +* Linux + 1. Download [obs-ndi-4.12.0-linux-x86_64.deb](https://github.com/obs-ndi/obs-ndi/releases/download/4.12.0/obs-ndi-4.12.0-linux-x86_64.deb) + 2. `sudo dpkg -i obs-ndi-4.12.0-linux-x86_64.deb` + 3. If this does not work then try: + ``` + $ sudo ln -s /usr/local/lib/obs-plugins/obs-ndi.so /usr/lib/x86_64-linux-gnu/obs-plugins/obs-ndi.so + $ sudo ln -s /usr/local/share/obs/obs-plugins/obs-ndi/ /usr/share/obs/obs-plugins/obs-ndi + ``` +* MacOS: + 1. Download [obs-ndi-4.12.0-macos-universal.pkg](https://github.com/obs-ndi/obs-ndi/releases/download/4.12.0/obs-ndi-4.12.0-macos-universal.pkg) + 2. Run `obs-ndi-4.12.0-macos-universal.pkg` + If MacOS complains about the file, either: + 1. Allow it in `System Settings`->`Privacy & Security` + -or- + 2. Run + ``` + % sudo xattr -r -d com.apple.quarantine obs-ndi-4.12.0-macos-universal.pkg + ``` +* Windows: + 1. Download [obs-ndi-4.12.0-windows-x64-Installer.exe](https://github.com/obs-ndi/obs-ndi/releases/download/4.12.0/obs-ndi-4.12.0-windows-x64-Installer.exe) + 2. Run `obs-ndi-4.12.0-windows-x64-Installer.exe` + +# Download & Install The NDI 5 Runtime +* Linux - there is no redist, so you have to do something like the following (also at [./CI/libndi-get.sh](./CI/libndi-get.sh)) +``` +#!/bin/bash + +set -e + +LIBNDI_INSTALLER_NAME="Install_NDI_SDK_v5_Linux" +LIBNDI_INSTALLER="$LIBNDI_INSTALLER_NAME.tar.gz" +LIBNDI_INSTALLER_SHA256="7e5c54693d6aee6b6f1d6d49f48d4effd7281abd216d9ff601be2d55af12f7f5" + +#sudo apt-get install curl + +pushd /tmp +curl -L -o $LIBNDI_INSTALLER https://downloads.ndi.tv/SDK/NDI_SDK_Linux/$LIBNDI_INSTALLER -f --retry 5 +echo "$LIBNDI_INSTALLER_SHA256 $LIBNDI_INSTALLER" | sha256sum -c +tar -xf $LIBNDI_INSTALLER +yes | PAGER="cat" sh $LIBNDI_INSTALLER_NAME.sh + +rm -rf /tmp/ndisdk +mv "/tmp/NDI SDK for Linux" /tmp/ndisdk +ls /tmp/ndisdk + +if [ $1 == "install" ]; then + # NOTE: This does an actual local install... + sudo cp -P /tmp/ndisdk/lib/x86_64-linux-gnu/* /usr/local/lib/ + sudo ldconfig +fi + +popd +``` +* MacOS: http://ndi.link/NDIRedistV5Apple +* Windows: http://ndi.link/NDIRedistV5 - * MacOS: http://ndi.link/NDIRedistV5Apple - * Windows: http://ndi.link/NDIRedistV5 ## Uninstall - Reference: https://obsproject.com/kb/plugins-guide#install-or-remove-plugins ### Linux @@ -74,42 +98,54 @@ Reference: https://obsproject.com/kb/plugins-guide#install-or-remove-plugins ``` ### MacOS - 1. Open Finder 2. Show hidden files with `Command-Shift-.` -3. Delete `~/Library/Application Support/obs-studio/plugins/obs-ndi.plugin` -4. Optionally delete NDI Tools/Runtime: +3. Delete `~/Library/Application Support/obs-studio/obs-plugins/obs-ndi.plugin` +4. Delete `~/Library/Application Support/obs-studio/data/obs-plugins/obs-ndi/` +5. Optionally delete NDI Tools/Runtime: 1. Finder->Applications: Delete all `NDI *` applications 2. Delete `/Library/Application Support/NewTek/NDI` 2. Delete `/usr/local/lib/libndi.*` ### Windows - 1. Add/Remove Programs 2. Delete `%ProgramFiles%\obs-studio\obs-plugins\64-bit\obs-ndi.*` -3. Optionally delete NDI Tools/Runtime: +3. Delete `%ProgramFiles%\obs-studio\data\obs-plugins\obs-ndi\` +4. Optionally delete NDI Tools/Runtime: 1. Add/Remove Programs 2. Delete `%ProgramFiles%\NDI\NDI 5 Runtime` 3. Delete `%ProgramFiles%\NDI\NDI 5 Tools` # Development -## Compiling +## Building ### Windows -In PowerShell v5+ terminal: +In PowerShell Core 7+ terminal: ``` git clone https://github.com/obs-ndi/obs-ndi.git cd obs-ndi -.github/scripts/Build-Windows.ps1 -... -tbd... ``` +First build: +``` +.github/scripts/Build-Windows.ps1 && .github/scripts/Package-Windows.ps1 -BuildInstaller +``` +Subsequent builds: +``` +.github/scripts/Build-Windows.ps1 -SkipDeps && .github/scripts/Package-Windows.ps1 -BuildInstaller +``` +See `Help .github/scripts/Build-Windows.ps1` for more details. If you get `SecurityError/PSSecurityException/UnauthorizedAccess` error then: ``` -PS ...\obs-ndi\obs-ndi> Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser +Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser +``` + + ### Linux NOTE: Only Debian and Ubuntu are officially supported @@ -118,7 +154,9 @@ In terminal: ``` git clone https://github.com/obs-ndi/obs-ndi.git cd obs-ndi -.github/scripts/build-linux.sh +.github/scripts/build-linux +... +.github/scripts/package-linux ... sudo cp -r release/obs-plugins/64bit/* /usr/local/lib/x86_64-linux-gnu/obs-plugins/ ... @@ -126,46 +164,45 @@ sudo cp -r release/data/obs-plugins/* /usr/local/share/obs/obs-plugins/ ... sudo ldconfig ``` +Subsequent builds can be sped up by using `build-linux --skip-deps`. +See `build-linux --help` for more details. ### OS X In terminal: ``` git clone https://github.com/obs-ndi/obs-ndi.git cd obs-ndi -.github/scripts/build-macos.zsh +.github/scripts/build-macos +... +.github/scripts/package-macos ... cp -r release/obs-ndi.plugin ~/Library/Application\ Support/obs-studio/plugins/ ... ``` -Subsequent builds can be sped up by using `build-macos.zsh --skip-deps`. -[For some reason `--skip-all` doesn't work.] -See `build-macos.zsh --help` for more details. +Subsequent builds can be sped up by using `build-macos --skip-deps`. +See `build-macos --help` for more details. ## Formatting -From a bash shell (confirmed also works on WSL): -``` -.github/scripts/check-format.sh 1 -``` -NOTE: `obs-ndi` is based on [`obsplugin-template`](https://github.com/obsproject/obs-plugintemplate) that [requires `clang-format-13`](https://github.com/obsproject/obs-plugintemplate/blob/525650f97209450cf2dcc06ff28ad941cc1bbd7b/.github/scripts/check-format.sh#L29-L42): -``` -if type clang-format-13 2> /dev/null ; then - CLANG_FORMAT=clang-format-13 -elif type clang-format 2> /dev/null ; then - # Clang format found, but need to check version - CLANG_FORMAT=clang-format - V=$(clang-format --version) - if [[ $V != *"version 13.0"* ]]; then - echo "clang-format is not 13.0 (returned ${V})" - exit 1 - fi -else - echo "No appropriate clang-format found (expected clang-format-13.0.0, or clang-format)" - exit 1 -fi +Requires [obsproject/tools/]clang-format@13, cmakelang, and zsh installed. + +From a Linux or MacOS terminal: ``` -[MacOS brew only has formulaes for clang-format 15, 11, or 8.](https://formulae.brew.sh/formula/clang-format) -If you want to check-format on MacOS then you will need to: -1. brew install clang-format -2. edit `.github/scripts/check-format.sh` and change all `13` to `15` -3. !!DO NOT COMMIT THESE CHANGES!! +./build-aux/run-clang-format +./build-aux/run-cmake-format +``` + + diff --git a/buildspec.json b/buildspec.json index 6ed1b62a..ebf52337 100644 --- a/buildspec.json +++ b/buildspec.json @@ -22,24 +22,24 @@ } }, "prebuilt": { - "version": "2023-10-26", + "version": "2023-04-12", "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", "label": "Pre-Built obs-deps", "hashes": { - "macos": "eebdcc77829206eb0fee46975f69cedcea8a67386d97f73ba277da4925ca0893", - "windows-x64": "58b3164d94fa69054a7cb0d54fef93d93c0a6304721dce4e6de3eaf3403877a6" + "macos": "9535c6e1ad96f7d49960251e85a245774088d48da1d602bb82f734b10219125a", + "windows-x64": "c13a14a1acc4224b21304d97b63da4121de1ed6981297e50496fbc474abc0503" } }, "qt6": { - "version": "2023-10-26", + "version": "2023-04-12", "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", "label": "Pre-Built Qt6", "hashes": { - "macos": "f0e842934a87cec3cd02630e393ed78768a7f9b205c5a59e40a28db8d4659a01", - "windows-x64": "6aad335e2d4632392895e46a8c31d73f6219ec0b71bec2c23ce42ac473d10a3e" + "macos": "eb7614544ab4f3d2c6052c797635602280ca5b028a6b987523d8484222ce45d1", + "windows-x64": "4d39364b8a8dee5aa24fcebd8440d5c22bb4551c6b440ffeacce7d61f2ed1add" }, "debugSymbols": { - "windows-x64": "f9adeb05441f21afb0cbcf33f273a7cd2a588ff8216a11e17865e2a5dc9801af" + "windows-x64": "f34ee5067be19ed370268b15c53684b7b8aaa867dc800b68931df905d679e31f" } } }, diff --git a/cmake/common/buildspec_common.cmake b/cmake/common/buildspec_common.cmake index 49e4efd3..0a4d2388 100644 --- a/cmake/common/buildspec_common.cmake +++ b/cmake/common/buildspec_common.cmake @@ -74,6 +74,7 @@ function(_setup_obs_studio) endif() message(STATUS "Patch libobs - begin") + # cmake-format: off # Idea shared by RoyShilkrot on OBS Discord #plugins-and-tools: # https://github.com/occ-ai/obs-polyglot/blob/a86b5778cd200b6aeb4ef65a49e621905b18e823/cmake/common/buildspec_common.cmake#L77-L82 # To create/update libobs.patch: @@ -82,11 +83,12 @@ function(_setup_obs_studio) # 3. Edit libobs/CMakeLists.txt as desired # 4. git diff libobs/CMakeLists.txt > libobs.patch # 5. Copy libobs.patch to obs-ndi root, verify build success, commit+push + # cmake-format: on execute_process( - COMMAND patch --forward "libobs/CMakeLists.txt" "${CMAKE_CURRENT_SOURCE_DIR}/libobs.patch" #| grep "hunks failed" && [ $? -eq 0 ] && false + COMMAND patch --forward "libobs/CMakeLists.txt" "${CMAKE_CURRENT_SOURCE_DIR}/libobs.patch" OUTPUT_VARIABLE _patch_output WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}") - if (_patch_output MATCHES ".*hunks failed.*") + if(_patch_output MATCHES ".*hunks failed.*") message(FATAL_ERROR "Patch libobs failed") endif() message(STATUS "Patch libobs - end") diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index cad5ce9f..e433c275 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -2,7 +2,8 @@ NDIPlugin.Default="Default" NDIPlugin.NDISourceName="NDI™ Source" NDIPlugin.SourceProps.SourceName="Source name" NDIPlugin.SourceProps.Bandwidth="Bandwidth" -NDIPlugin.SourceProps.Sync="Sync" +NDIPlugin.SourceProps.Sync="Audio/Video Sync" +NDIPlugin.NDIFrameSync="Framesync (experimental)" NDIPlugin.SourceProps.HWAccel="Allow hardware acceleration" NDIPlugin.SourceProps.AlphaBlendingFix="Fix alpha blending (adds a filter to this source)" NDIPlugin.SourceProps.ColorRange="YUV Range" @@ -11,7 +12,8 @@ NDIPlugin.SourceProps.ColorRange.Full="Full" NDIPlugin.SourceProps.ColorSpace="YUV Color Space" NDIPlugin.SourceProps.Latency="Latency Mode" NDIPlugin.SourceProps.Latency.Normal="Normal (safe)" -NDIPlugin.SourceProps.Latency.Low="Low (experimental)" +NDIPlugin.SourceProps.Latency.Low="Low" +NDIPlugin.SourceProps.Latency.Lowest="Lowest (unbuffered)" NDIPlugin.SourceProps.Audio="Enable audio" NDIPlugin.SourceProps.PTZ="Pan Tilt Zoom" NDIPlugin.SourceProps.Pan="Pan" diff --git a/data/locale/es-ES.ini b/data/locale/es-ES.ini index ae0f2006..38569351 100644 --- a/data/locale/es-ES.ini +++ b/data/locale/es-ES.ini @@ -11,7 +11,8 @@ NDIPlugin.SourceProps.ColorRange.Full="Completo" NDIPlugin.SourceProps.ColorSpace="Espacio de color YUV" NDIPlugin.SourceProps.Latency="Modo de latencia" NDIPlugin.SourceProps.Latency.Normal="Normal (segura)" -NDIPlugin.SourceProps.Latency.Low="Baja (experimental)" +NDIPlugin.SourceProps.Latency.Low="Baja" +NDIPlugin.SourceProps.Latency.Lowest="Mínima (sin búfer)" NDIPlugin.BWMode.Highest="La más alta" NDIPlugin.BWMode.Lowest="La más baja" NDIPlugin.BWMode.AudioOnly="Solo Audio" diff --git a/data/locale/fr-FR.ini b/data/locale/fr-FR.ini index a023ca9c..1cdf90cd 100644 --- a/data/locale/fr-FR.ini +++ b/data/locale/fr-FR.ini @@ -4,22 +4,23 @@ NDIPlugin.SourceProps.SourceName="Source" NDIPlugin.SourceProps.Bandwidth="Bandwidth" NDIPlugin.SourceProps.Sync="Synchronisation" NDIPlugin.SourceProps.HWAccel="Autoriser l'accélération matérielle" -NDIPlugin.SourceProps.AlphaBlendingFix="Correction du compositing alpha (ajoute un filtre à la source)" +NDIPlugin.SourceProps.AlphaBlendingFix="Correction de simulation de transparence (ajoute un filtre à la source)" NDIPlugin.SourceProps.ColorRange="Gamme de couleurs YUV" NDIPlugin.SourceProps.ColorRange.Partial="Partielle" NDIPlugin.SourceProps.ColorRange.Full="Complète" -NDIPlugin.SourceProps.ColorSpace="Espace de couleurs YUV" +NDIPlugin.SourceProps.ColorSpace="Espace colorimétrique YUV" NDIPlugin.SourceProps.Latency="Latence" NDIPlugin.SourceProps.Latency.Normal="Normale (sûr)" -NDIPlugin.SourceProps.Latency.Low="Réduite (expérimental)" +NDIPlugin.SourceProps.Latency.Low="Réduite" +NDIPlugin.SourceProps.Latency.Lowest="Minimum (non-tamponné)" NDIPlugin.BWMode.Highest="Haut débit" NDIPlugin.BWMode.Lowest="Bas débit" NDIPlugin.BWMode.AudioOnly="Audio seulement" NDIPlugin.SyncMode.NDITimestamp="Réseau" -NDIPlugin.SyncMode.NDISourceTimecode="Timing de la source" +NDIPlugin.SyncMode.NDISourceTimecode="Code temporel de la source" NDIPlugin.OutputName="Sortie NDI™" NDIPlugin.OutputProps.NDIName="Nom de la sortie" -NDIPlugin.FilterProps.NDIName="Nom NDI" +NDIPlugin.FilterProps.NDIName="Nom NDI™" NDIPlugin.FilterProps.NDIName.Default="Sortie NDI dédiée" NDIPlugin.FilterProps.ApplySettings="Appliquer les changements" NDIPlugin.Menu.OutputSettings="NDI™ - Sortie principale" @@ -30,8 +31,8 @@ NDIPlugin.OutputSettings.Main.Name="Nom NDI™" NDIPlugin.OutputSettings.Preview.Name="Nom NDI™" NDIPlugin.FilterName="Sortie NDI™ dédiée" NDIPlugin.AudioFilterName="Sortie NDI™ dédiée (Audio seul)" -NDIPlugin.PremultipliedAlphaFilterName="obs-ndi - Correction du compositing alpha" -NDIPlugin.LibError.Title="Runtime NDI™ introuvable" -NDIPlugin.LibError.Message.Win="La bibliothèque NDI™ est introuvable sur votre système.
Téléchargez l'installateur du runtime ici : http://new.tk/NDIRedistV5" -NDIPlugin.LibError.Message.macOS="Le bibliothèque NDI™ est introuvable sur votre système.
Téléchargez l'installateur du runtime ici : http://new.tk/NDIRedistV5Apple" -NDIPlugin.LibError.Message.Linux="La bibliothèque NDI™ est introuvable. Installez libndi sur votre système.
Vous pouvez en trouvez une copie sur la page GitHub d'obs-ndi." +NDIPlugin.PremultipliedAlphaFilterName="obs-ndi - Correction de simulation de transparence" +NDIPlugin.LibError.Title="Bibliothèque NDI™ introuvable" +NDIPlugin.LibError.Message.Win="La bibliothèque NDI™ est introuvable sur votre système.
Téléchargez l'installateur ici : http://new.tk/NDIRedistV5" +NDIPlugin.LibError.Message.macOS="Le bibliothèque NDI™ est introuvable sur votre système.
Téléchargez l'installateur ici : http://new.tk/NDIRedistV5Apple" +NDIPlugin.LibError.Message.Linux="La bibliothèque NDI™ est introuvable. Installez libndi sur votre système.
Une copie est disponible sur la page GitHub d'obs-ndi : Github OBS-NDI." diff --git a/data/locale/ro-RO.ini b/data/locale/ro-RO.ini new file mode 100644 index 00000000..a2e94333 --- /dev/null +++ b/data/locale/ro-RO.ini @@ -0,0 +1,42 @@ +NDIPlugin.Default="Implicit" +NDIPlugin.NDISourceName="Sursă NDI™" +NDIPlugin.SourceProps.SourceName="Numele sursei" +NDIPlugin.SourceProps.Bandwidth="Lățimea de bandă" +NDIPlugin.SourceProps.Sync="Sincronizare" +NDIPlugin.SourceProps.HWAccel="Permite accelerarea hardware" +NDIPlugin.SourceProps.AlphaBlendingFix="Corectează amestecul alfa (adaugă un filtru la această sursă)" +NDIPlugin.SourceProps.ColorRange="Gama YUV" +NDIPlugin.SourceProps.ColorRange.Partial="Parțială" +NDIPlugin.SourceProps.ColorRange.Full="Completă" +NDIPlugin.SourceProps.ColorSpace="Spațiu de culoare YUV" +NDIPlugin.SourceProps.Latency="Mod de latență" +NDIPlugin.SourceProps.Latency.Normal="Normal (sigur)" +NDIPlugin.SourceProps.Latency.Low="Scăzut (experimental)" +NDIPlugin.SourceProps.Audio="Activează audio" +NDIPlugin.BWMode.Highest="Cea mai înaltă" +NDIPlugin.BWMode.Lowest="Cea mai scăzută" +NDIPlugin.BWMode.AudioOnly="Numai audio" +NDIPlugin.SyncMode.NDITimestamp="Rețea" +NDIPlugin.SyncMode.NDISourceTimecode="Timing-ul sursei" +NDIPlugin.OutputName="Ieșire NDI™" +NDIPlugin.OutputProps.NDIName="Nume ieșire" +NDIPlugin.FilterProps.NDIName="Nume NDI" +NDIPlugin.FilterProps.NDIName.Default="Ieșire NDI dedicată" +NDIPlugin.FilterProps.ApplySettings="Apică modificările" +NDIPlugin.Menu.OutputSettings="Setări ieșire NDI™" +NDIPlugin.OutputSettings.DialogTitle="Setări pentru ieșirea NDI™" +NDIPlugin.OutputSettings.GroupBox.Main="Ieșirea de program" +NDIPlugin.OutputSettings.GroupBox.Preview="Ieșirea de previzualizare" +NDIPlugin.OutputSettings.GroupBox.Tally="Semnalizare" +NDIPlugin.OutputSettings.GroupBox.Tally.Enable="Activează" +NDIPlugin.OutputSettings.GroupBox.Tally.Program="Program" +NDIPlugin.OutputSettings.GroupBox.Tally.Preview="Previzualizare" +NDIPlugin.OutputSettings.Main.Name="Numele ieșirii de program" +NDIPlugin.OutputSettings.Preview.Name="Numele ieșirii de previzualizare" +NDIPlugin.FilterName="Ieșire NDI™ dedicată" +NDIPlugin.AudioFilterName="Ieșire NDI™ dedicată (numai audio)" +NDIPlugin.PremultipliedAlphaFilterName="obs-ndi - Corectare amestec alfa" +NDIPlugin.LibError.Title="NDI™ Runtime nu a fost găsit" +NDIPlugin.LibError.Message.Win="NDI™ Runtime nu a fost găsit.
Descărcați programul de instalare aici: http://new.tk/NDIRedistV5" +NDIPlugin.LibError.Message.macOS="NDI™ Runtime nu a fost găsit.
Descărcați programul de instalare aici: http://new.tk/NDIRedistV5Apple" +NDIPlugin.LibError.Message.Linux="NDI™ Runtime nu a fost găsit. Instalați libndi pe sistemul dumneavoastră.
Puteți obține o copie libndi pentru Linux pe pagina obs-ndi de la GitHub." \ No newline at end of file diff --git a/data/locale/uk-UA.ini b/data/locale/uk-UA.ini index dd5374ce..1940116a 100644 --- a/data/locale/uk-UA.ini +++ b/data/locale/uk-UA.ini @@ -11,7 +11,8 @@ NDIPlugin.SourceProps.ColorRange.Full="Повний" NDIPlugin.SourceProps.ColorSpace="Колірний простір YUV" NDIPlugin.SourceProps.Latency="Режим затримки" NDIPlugin.SourceProps.Latency.Normal="Нормальний (безпечний)" -NDIPlugin.SourceProps.Latency.Low="Низький (експериментальний)" +NDIPlugin.SourceProps.Latency.Low="Низький" +NDIPlugin.SourceProps.Latency.Lowest="Мінімум (небуферизований)" NDIPlugin.BWMode.Highest="Найвищий" NDIPlugin.BWMode.Lowest="Найнижчий" NDIPlugin.BWMode.AudioOnly="Тільки аудіо" diff --git a/src/forms/output-settings.cpp b/src/forms/output-settings.cpp index 6a6df8d0..8f5d1f84 100644 --- a/src/forms/output-settings.cpp +++ b/src/forms/output-settings.cpp @@ -68,10 +68,8 @@ void OutputSettings::onFormAccepted() } } -void OutputSettings::showEvent(QShowEvent *event) +void OutputSettings::showEvent(QShowEvent *) { - UNUSED_PARAMETER(event); - Config *conf = Config::Current(); ui->mainOutputGroupBox->setChecked(conf->OutputEnabled); diff --git a/src/main-output.cpp b/src/main-output.cpp index 825be4e7..cb425172 100644 --- a/src/main-output.cpp +++ b/src/main-output.cpp @@ -32,7 +32,7 @@ void main_output_start(const char *output_name) return; blog(LOG_INFO, - "main_output_start: starting NDI main output with name '%s'", + "[obs-ndi] main_output_start: starting NDI main output with name '%s'", output_name); obs_data_t *settings = obs_data_create(); @@ -46,10 +46,13 @@ void main_output_start(const char *output_name) return; } - obs_output_start(main_out); + auto result = obs_output_start(main_out); + blog(LOG_INFO, + "[obs-ndi] main_output_start: obs_output_start result=%d", result); + main_output_running = true; - blog(LOG_INFO, "main_output_start: started NDI main output"); + blog(LOG_INFO, "[obs-ndi] main_output_start: started NDI main output"); } void main_output_stop() @@ -57,7 +60,7 @@ void main_output_stop() if (!main_output_running) return; - blog(LOG_INFO, "main_output_stop: stopping NDI main output"); + blog(LOG_INFO, "[obs-ndi] main_output_stop: stopping NDI main output"); obs_output_stop(main_out); obs_output_release(main_out); @@ -65,9 +68,8 @@ void main_output_stop() main_output_running = false; - blog(LOG_INFO, "main_output_stop: stopped NDI main output"); + blog(LOG_INFO, "[obs-ndi] main_output_stop: stopped NDI main output"); } - bool main_output_is_running() { return main_output_running; diff --git a/src/obs-ndi-filter.cpp b/src/obs-ndi-filter.cpp index 421d5919..5b372734 100644 --- a/src/obs-ndi-filter.cpp +++ b/src/obs-ndi-filter.cpp @@ -33,13 +33,16 @@ with this program. If not, see #define TEXFORMAT GS_BGRA #define FLT_PROP_NAME "ndi_filter_ndiname" -struct ndi_filter { +typedef struct { obs_source_t *context; + NDIlib_send_instance_t ndi_sender; + pthread_mutex_t ndi_sender_video_mutex; pthread_mutex_t ndi_sender_audio_mutex; - struct obs_video_info ovi; - struct obs_audio_info oai; + + obs_video_info ovi; + obs_audio_info oai; uint32_t known_width; uint32_t known_height; @@ -52,27 +55,24 @@ struct ndi_filter { video_t *video_output; bool is_audioonly; - os_performance_token_t *perf_token; -}; + uint8_t *audio_conv_buffer; + size_t audio_conv_buffer_size; +} ndi_filter_t; -const char *ndi_filter_getname(void *data) +const char *ndi_filter_getname(void *) { - UNUSED_PARAMETER(data); return obs_module_text("NDIPlugin.FilterName"); } -const char *ndi_audiofilter_getname(void *data) +const char *ndi_audiofilter_getname(void *) { - UNUSED_PARAMETER(data); return obs_module_text("NDIPlugin.AudioFilterName"); } void ndi_filter_update(void *data, obs_data_t *settings); -obs_properties_t *ndi_filter_getproperties(void *data) +obs_properties_t *ndi_filter_getproperties(void *) { - UNUSED_PARAMETER(data); - obs_properties_t *props = obs_properties_create(); obs_properties_set_flags(props, OBS_PROPERTIES_DEFER_UPDATE); @@ -84,35 +84,25 @@ obs_properties_t *ndi_filter_getproperties(void *data) obs_properties_add_button( props, "ndi_apply", obs_module_text("NDIPlugin.FilterProps.ApplySettings"), - [](obs_properties_t *pps, obs_property_t *prop, - void *private_data) { - UNUSED_PARAMETER(pps); - UNUSED_PARAMETER(prop); - struct ndi_filter *s = - (struct ndi_filter *)private_data; - obs_data_t *settings = - obs_source_get_settings(s->context); + [](obs_properties_t *, obs_property_t *, void *private_data) { + auto s = (ndi_filter_t *)private_data; + auto settings = obs_source_get_settings(s->context); ndi_filter_update(s, settings); obs_data_release(settings); return true; }); - obs_properties_add_button(props, "ndi_website", "NDI.NewTek.com", - [](obs_properties_t *pps, - obs_property_t *prop, void *private_data) { - UNUSED_PARAMETER(pps); - UNUSED_PARAMETER(prop); - UNUSED_PARAMETER(private_data); + obs_properties_add_button( + props, "ndi_website", "NDI.NewTek.com", + [](obs_properties_t *, obs_property_t *, void *) { #if defined(_WIN32) - ShellExecute(NULL, L"open", - L"http://ndi.newtek.com", - NULL, NULL, - SW_SHOWNORMAL); + ShellExecute(NULL, L"open", L"http://ndi.newtek.com", + NULL, NULL, SW_SHOWNORMAL); #elif defined(__linux__) || defined(__APPLE__) - (void)!system("open http://ndi.newtek.com"); + (void)!system("open http://ndi.newtek.com"); #endif - return true; - }); + return true; + }); return props; } @@ -126,36 +116,33 @@ void ndi_filter_getdefaults(obs_data_t *defaults) void ndi_filter_raw_video(void *data, video_data *frame) { - auto s = (struct ndi_filter *)data; + auto f = (ndi_filter_t *)data; if (!frame || !frame->data[0]) return; NDIlib_video_frame_v2_t video_frame = {0}; - video_frame.xres = s->known_width; - video_frame.yres = s->known_height; + video_frame.xres = f->known_width; + video_frame.yres = f->known_height; video_frame.FourCC = NDIlib_FourCC_type_BGRA; - video_frame.frame_rate_N = s->ovi.fps_num; - video_frame.frame_rate_D = s->ovi.fps_den; + video_frame.frame_rate_N = f->ovi.fps_num; + video_frame.frame_rate_D = f->ovi.fps_den; video_frame.picture_aspect_ratio = 0; // square pixels video_frame.frame_format_type = NDIlib_frame_format_type_progressive; video_frame.timecode = (frame->timestamp / 100); video_frame.p_data = frame->data[0]; video_frame.line_stride_in_bytes = frame->linesize[0]; - pthread_mutex_lock(&s->ndi_sender_video_mutex); - ndiLib->send_send_video_v2(s->ndi_sender, &video_frame); - pthread_mutex_unlock(&s->ndi_sender_video_mutex); + pthread_mutex_lock(&f->ndi_sender_video_mutex); + ndiLib->send_send_video_v2(f->ndi_sender, &video_frame); + pthread_mutex_unlock(&f->ndi_sender_video_mutex); } -void ndi_filter_offscreen_render(void *data, uint32_t cx, uint32_t cy) +void ndi_filter_offscreen_render(void *data, uint32_t, uint32_t) { - UNUSED_PARAMETER(cx); - UNUSED_PARAMETER(cy); + auto f = (ndi_filter_t *)data; - auto s = (struct ndi_filter *)data; - - obs_source_t *target = obs_filter_get_parent(s->context); + obs_source_t *target = obs_filter_get_parent(f->context); if (!target) { return; } @@ -163,10 +150,10 @@ void ndi_filter_offscreen_render(void *data, uint32_t cx, uint32_t cy) uint32_t width = obs_source_get_base_width(target); uint32_t height = obs_source_get_base_height(target); - gs_texrender_reset(s->texrender); + gs_texrender_reset(f->texrender); - if (gs_texrender_begin(s->texrender, width, height)) { - struct vec4 background; + if (gs_texrender_begin(f->texrender, width, height)) { + vec4 background; vec4_zero(&background); gs_clear(GS_CLEAR_COLOR, &background, 0.0f, 0); @@ -179,67 +166,66 @@ void ndi_filter_offscreen_render(void *data, uint32_t cx, uint32_t cy) obs_source_video_render(target); gs_blend_state_pop(); - gs_texrender_end(s->texrender); + gs_texrender_end(f->texrender); - if (s->known_width != width || s->known_height != height) { + if (f->known_width != width || f->known_height != height) { - gs_stagesurface_destroy(s->stagesurface); - s->stagesurface = gs_stagesurface_create(width, height, + gs_stagesurface_destroy(f->stagesurface); + f->stagesurface = gs_stagesurface_create(width, height, TEXFORMAT); video_output_info vi = {0}; vi.format = VIDEO_FORMAT_BGRA; vi.width = width; vi.height = height; - vi.fps_den = s->ovi.fps_den; - vi.fps_num = s->ovi.fps_num; + vi.fps_den = f->ovi.fps_den; + vi.fps_num = f->ovi.fps_num; vi.cache_size = 16; vi.colorspace = VIDEO_CS_DEFAULT; vi.range = VIDEO_RANGE_DEFAULT; - vi.name = obs_source_get_name(s->context); + vi.name = obs_source_get_name(f->context); - video_output_close(s->video_output); - video_output_open(&s->video_output, &vi); - video_output_connect(s->video_output, nullptr, - ndi_filter_raw_video, s); + video_output_close(f->video_output); + video_output_open(&f->video_output, &vi); + video_output_connect(f->video_output, nullptr, + ndi_filter_raw_video, f); - s->known_width = width; - s->known_height = height; + f->known_width = width; + f->known_height = height; } - struct video_frame output_frame; - if (video_output_lock_frame(s->video_output, &output_frame, 1, + video_frame output_frame; + if (video_output_lock_frame(f->video_output, &output_frame, 1, os_gettime_ns())) { - if (s->video_data) { - gs_stagesurface_unmap(s->stagesurface); - s->video_data = nullptr; + if (f->video_data) { + gs_stagesurface_unmap(f->stagesurface); + f->video_data = nullptr; } gs_stage_texture( - s->stagesurface, - gs_texrender_get_texture(s->texrender)); - gs_stagesurface_map(s->stagesurface, &s->video_data, - &s->video_linesize); + f->stagesurface, + gs_texrender_get_texture(f->texrender)); + gs_stagesurface_map(f->stagesurface, &f->video_data, + &f->video_linesize); uint32_t linesize = output_frame.linesize[0]; - for (uint32_t i = 0; i < s->known_height; ++i) { + for (uint32_t i = 0; i < f->known_height; ++i) { uint32_t dst_offset = linesize * i; - uint32_t src_offset = s->video_linesize * i; + uint32_t src_offset = f->video_linesize * i; memcpy(output_frame.data[0] + dst_offset, - s->video_data + src_offset, linesize); + f->video_data + src_offset, linesize); } - video_output_unlock_frame(s->video_output); + video_output_unlock_frame(f->video_output); } } } void ndi_filter_update(void *data, obs_data_t *settings) { - UNUSED_PARAMETER(settings); - auto s = (struct ndi_filter *)data; + auto f = (ndi_filter_t *)data; - obs_remove_main_render_callback(ndi_filter_offscreen_render, s); + obs_remove_main_render_callback(ndi_filter_offscreen_render, f); NDIlib_send_create_t send_desc; send_desc.p_ndi_name = obs_data_get_string(settings, FLT_PROP_NAME); @@ -247,144 +233,176 @@ void ndi_filter_update(void *data, obs_data_t *settings) send_desc.clock_video = false; send_desc.clock_audio = false; - pthread_mutex_lock(&s->ndi_sender_video_mutex); - pthread_mutex_lock(&s->ndi_sender_audio_mutex); - - ndiLib->send_destroy(s->ndi_sender); - s->ndi_sender = ndiLib->send_create(&send_desc); - - pthread_mutex_unlock(&s->ndi_sender_audio_mutex); - pthread_mutex_unlock(&s->ndi_sender_video_mutex); - - if (!s->is_audioonly) { - obs_add_main_render_callback(ndi_filter_offscreen_render, s); + if (!f->is_audioonly) { + pthread_mutex_lock(&f->ndi_sender_video_mutex); + } + pthread_mutex_lock(&f->ndi_sender_audio_mutex); + ndiLib->send_destroy(f->ndi_sender); + f->ndi_sender = ndiLib->send_create(&send_desc); + pthread_mutex_unlock(&f->ndi_sender_audio_mutex); + if (!f->is_audioonly) { + pthread_mutex_unlock(&f->ndi_sender_video_mutex); + obs_add_main_render_callback(ndi_filter_offscreen_render, f); } } void *ndi_filter_create(obs_data_t *settings, obs_source_t *source) { - auto s = (struct ndi_filter *)bzalloc(sizeof(struct ndi_filter)); - s->is_audioonly = false; - s->context = source; - s->texrender = gs_texrender_create(TEXFORMAT, GS_ZS_NONE); - s->video_data = nullptr; - s->perf_token = os_request_high_performance("NDI Filter"); - pthread_mutex_init(&s->ndi_sender_video_mutex, NULL); - pthread_mutex_init(&s->ndi_sender_audio_mutex, NULL); - - obs_get_video_info(&s->ovi); - obs_get_audio_info(&s->oai); - - ndi_filter_update(s, settings); - return s; + auto name = obs_data_get_string(settings, FLT_PROP_NAME); + blog(LOG_INFO, "[obs-ndi] +ndi_filter_create(name=\"%s\")", name); + + auto f = (ndi_filter_t *)bzalloc(sizeof(ndi_filter_t)); + f->context = source; + f->texrender = gs_texrender_create(TEXFORMAT, GS_ZS_NONE); + pthread_mutex_init(&f->ndi_sender_video_mutex, NULL); + pthread_mutex_init(&f->ndi_sender_audio_mutex, NULL); + obs_get_video_info(&f->ovi); + obs_get_audio_info(&f->oai); + + ndi_filter_update(f, settings); + + blog(LOG_INFO, "[obs-ndi] -ndi_filter_create(...)"); + + return f; } void *ndi_filter_create_audioonly(obs_data_t *settings, obs_source_t *source) { - auto s = (struct ndi_filter *)bzalloc(sizeof(struct ndi_filter)); - s->is_audioonly = true; - s->context = source; - s->perf_token = os_request_high_performance("NDI Filter (Audio Only)"); - pthread_mutex_init(&s->ndi_sender_audio_mutex, NULL); - pthread_mutex_init(&s->ndi_sender_video_mutex, NULL); + auto name = obs_data_get_string(settings, FLT_PROP_NAME); + blog(LOG_INFO, "[obs-ndi] +ndi_filter_create_audioonly(name=\"%s\")", + name); + + auto f = (ndi_filter_t *)bzalloc(sizeof(ndi_filter_t)); + f->is_audioonly = true; + f->context = source; + pthread_mutex_init(&f->ndi_sender_audio_mutex, NULL); + obs_get_audio_info(&f->oai); + + ndi_filter_update(f, settings); - obs_get_audio_info(&s->oai); + blog(LOG_INFO, "[obs-ndi] -ndi_filter_create_audioonly(...)"); - ndi_filter_update(s, settings); - return s; + return f; } void ndi_filter_destroy(void *data) { - auto s = (struct ndi_filter *)data; + blog(LOG_INFO, "[obs-ndi] +ndi_filter_destroy(...)"); - obs_remove_main_render_callback(ndi_filter_offscreen_render, s); - video_output_close(s->video_output); + auto f = (ndi_filter_t *)data; - pthread_mutex_lock(&s->ndi_sender_video_mutex); - pthread_mutex_lock(&s->ndi_sender_audio_mutex); + obs_remove_main_render_callback(ndi_filter_offscreen_render, f); + video_output_close(f->video_output); - ndiLib->send_destroy(s->ndi_sender); + pthread_mutex_lock(&f->ndi_sender_video_mutex); + pthread_mutex_lock(&f->ndi_sender_audio_mutex); + ndiLib->send_destroy(f->ndi_sender); + pthread_mutex_unlock(&f->ndi_sender_audio_mutex); + pthread_mutex_unlock(&f->ndi_sender_video_mutex); - pthread_mutex_unlock(&s->ndi_sender_audio_mutex); - pthread_mutex_unlock(&s->ndi_sender_video_mutex); + gs_stagesurface_unmap(f->stagesurface); + gs_stagesurface_destroy(f->stagesurface); + gs_texrender_destroy(f->texrender); - gs_stagesurface_unmap(s->stagesurface); - gs_stagesurface_destroy(s->stagesurface); - gs_texrender_destroy(s->texrender); - - if (s->perf_token) { - os_end_high_performance(s->perf_token); + if (f->audio_conv_buffer) { + blog(LOG_INFO, + "[obs-ndi] ndi_filter_destroy: freeing %zu bytes", + f->audio_conv_buffer_size); + bfree(f->audio_conv_buffer); + f->audio_conv_buffer = nullptr; } - bfree(s); + bfree(f); + + blog(LOG_INFO, "[obs-ndi] -ndi_filter_destroy(...)"); } void ndi_filter_destroy_audioonly(void *data) { - auto s = (struct ndi_filter *)data; + blog(LOG_INFO, "[obs-ndi] +ndi_filter_destroy_audioonly(...)"); + + auto f = (ndi_filter_t *)data; - pthread_mutex_lock(&s->ndi_sender_audio_mutex); - ndiLib->send_destroy(s->ndi_sender); - pthread_mutex_unlock(&s->ndi_sender_audio_mutex); + pthread_mutex_lock(&f->ndi_sender_audio_mutex); + ndiLib->send_destroy(f->ndi_sender); + pthread_mutex_unlock(&f->ndi_sender_audio_mutex); - if (s->perf_token) { - os_end_high_performance(s->perf_token); + if (f->audio_conv_buffer) { + bfree(f->audio_conv_buffer); + f->audio_conv_buffer = nullptr; } - bfree(s); + bfree(f); + + blog(LOG_INFO, "[obs-ndi] -ndi_filter_destroy_audioonly(...)"); } -void ndi_filter_tick(void *data, float seconds) +void ndi_filter_tick(void *data, float) { - UNUSED_PARAMETER(seconds); - auto s = (struct ndi_filter *)data; - obs_get_video_info(&s->ovi); + auto f = (ndi_filter_t *)data; + obs_get_video_info(&f->ovi); } -void ndi_filter_videorender(void *data, gs_effect_t *effect) +void ndi_filter_videorender(void *data, gs_effect_t *) { - UNUSED_PARAMETER(effect); - auto s = (struct ndi_filter *)data; - obs_source_skip_video_filter(s->context); + auto f = (ndi_filter_t *)data; + obs_source_skip_video_filter(f->context); } -struct obs_audio_data *ndi_filter_asyncaudio(void *data, - struct obs_audio_data *audio_data) +obs_audio_data *ndi_filter_asyncaudio(void *data, obs_audio_data *audio_data) { - auto s = (struct ndi_filter *)data; + // NOTE: The logic in this function should be similar to + // obs-ndi-output::ndi_output_raw_audio + auto f = (ndi_filter_t *)data; - obs_get_audio_info(&s->oai); + obs_get_audio_info(&f->oai); NDIlib_audio_frame_v2_t audio_frame = {0}; - audio_frame.sample_rate = s->oai.samples_per_sec; - audio_frame.no_channels = s->oai.speakers; - audio_frame.timecode = (int64_t)(audio_data->timestamp / 100); + audio_frame.sample_rate = f->oai.samples_per_sec; + audio_frame.no_channels = f->oai.speakers; + audio_frame.timecode = audio_data->timestamp / 100; audio_frame.no_samples = audio_data->frames; audio_frame.channel_stride_in_bytes = audio_frame.no_samples * 4; - size_t data_size = + const size_t data_size = audio_frame.no_channels * audio_frame.channel_stride_in_bytes; - uint8_t *ndi_data = (uint8_t *)bmalloc(data_size); + + if (data_size > f->audio_conv_buffer_size) { + blog(LOG_INFO, + "[obs-ndi] ndi_filter_asyncaudio: growing audio_conv_buffer from %zu to %zu bytes", + f->audio_conv_buffer_size, data_size); + if (f->audio_conv_buffer) { + blog(LOG_INFO, + "[obs-ndi] ndi_filter_asyncaudio: freeing %zu bytes", + f->audio_conv_buffer_size); + bfree(f->audio_conv_buffer); + } + blog(LOG_INFO, + "[obs-ndi] ndi_filter_asyncaudio: allocating %zu bytes", + data_size); + f->audio_conv_buffer = (uint8_t *)bmalloc(data_size); + f->audio_conv_buffer_size = data_size; + } for (int i = 0; i < audio_frame.no_channels; ++i) { - memcpy(&ndi_data[i * audio_frame.channel_stride_in_bytes], + memcpy(f->audio_conv_buffer + + (i * audio_frame.channel_stride_in_bytes), audio_data->data[i], audio_frame.channel_stride_in_bytes); } - audio_frame.p_data = (float *)ndi_data; - pthread_mutex_lock(&s->ndi_sender_audio_mutex); - ndiLib->send_send_audio_v2(s->ndi_sender, &audio_frame); - pthread_mutex_unlock(&s->ndi_sender_audio_mutex); + audio_frame.p_data = (float *)f->audio_conv_buffer; + + pthread_mutex_lock(&f->ndi_sender_audio_mutex); + ndiLib->send_send_audio_v2(f->ndi_sender, &audio_frame); + pthread_mutex_unlock(&f->ndi_sender_audio_mutex); - bfree(ndi_data); return audio_data; } -struct obs_source_info create_ndi_filter_info() +obs_source_info create_ndi_filter_info() { - struct obs_source_info ndi_filter_info = {}; + obs_source_info ndi_filter_info = {}; ndi_filter_info.id = "ndi_filter"; ndi_filter_info.type = OBS_SOURCE_TYPE_FILTER; ndi_filter_info.output_flags = OBS_SOURCE_VIDEO; @@ -406,9 +424,9 @@ struct obs_source_info create_ndi_filter_info() return ndi_filter_info; } -struct obs_source_info create_ndi_audiofilter_info() +obs_source_info create_ndi_audiofilter_info() { - struct obs_source_info ndi_filter_info = {}; + obs_source_info ndi_filter_info = {}; ndi_filter_info.id = "ndi_audiofilter"; ndi_filter_info.type = OBS_SOURCE_TYPE_FILTER; ndi_filter_info.output_flags = OBS_SOURCE_AUDIO; @@ -418,8 +436,8 @@ struct obs_source_info create_ndi_audiofilter_info() ndi_filter_info.get_defaults = ndi_filter_getdefaults; ndi_filter_info.create = ndi_filter_create_audioonly; - ndi_filter_info.destroy = ndi_filter_destroy_audioonly; ndi_filter_info.update = ndi_filter_update; + ndi_filter_info.destroy = ndi_filter_destroy_audioonly; ndi_filter_info.filter_audio = ndi_filter_asyncaudio; diff --git a/src/obs-ndi-output.cpp b/src/obs-ndi-output.cpp index fff45f23..89254da3 100644 --- a/src/obs-ndi-output.cpp +++ b/src/obs-ndi-output.cpp @@ -61,13 +61,14 @@ static void convert_i444_to_uyvy(uint8_t *input[], uint32_t in_linesize[], } } -struct ndi_output { +typedef struct { obs_output_t *output; const char *ndi_name; bool uses_video; bool uses_audio; bool started; + NDIlib_send_instance_t ndi_sender; uint32_t frame_width; @@ -84,20 +85,15 @@ struct ndi_output { uint8_t *audio_conv_buffer; size_t audio_conv_buffer_size; +} ndi_output_t; - os_performance_token_t *perf_token; -}; - -const char *ndi_output_getname(void *data) +const char *ndi_output_getname(void *) { - UNUSED_PARAMETER(data); return obs_module_text("NDIPlugin.OutputName"); } -obs_properties_t *ndi_output_getproperties(void *data) +obs_properties_t *ndi_output_getproperties(void *) { - UNUSED_PARAMETER(data); - obs_properties_t *props = obs_properties_create(); obs_properties_set_flags(props, OBS_PROPERTIES_DEFER_UPDATE); @@ -117,14 +113,26 @@ void ndi_output_getdefaults(obs_data_t *settings) obs_data_set_default_bool(settings, "uses_audio", true); } -bool ndi_output_start(void *data) -{ - blog(LOG_INFO, "+ndi_output_start(...)"); +void ndi_output_update(void *data, obs_data_t *settings); - auto o = (struct ndi_output *)data; +void *ndi_output_create(obs_data_t *settings, obs_output_t *output) +{ + auto name = obs_data_get_string(settings, "ndi_name"); + blog(LOG_INFO, "[obs-ndi] +ndi_output_create('%s'...)", name); + auto o = (ndi_output_t *)bzalloc(sizeof(ndi_output_t)); + o->output = output; + ndi_output_update(o, settings); + blog(LOG_INFO, "[obs-ndi] -ndi_output_create('%s'...)", name); + return o; +} +bool ndi_output_start(void *data) +{ + auto o = (ndi_output_t *)data; + blog(LOG_INFO, "[obs-ndi] +ndi_output_start('%s'...)", o->ndi_name); if (o->started) { - blog(LOG_INFO, "-ndi_output_start()"); + blog(LOG_INFO, "[obs-ndi] -ndi_output_start('%s'...)", + o->ndi_name); return false; } @@ -133,9 +141,10 @@ bool ndi_output_start(void *data) audio_t *audio = obs_output_audio(o->output); if (!video && !audio) { - blog(LOG_ERROR, "'%s': no video and audio available", + blog(LOG_ERROR, "[obs-ndi] '%s': no video and audio available", + o->ndi_name); + blog(LOG_INFO, "[obs-ndi] -ndi_output_start('%s'...)", o->ndi_name); - blog(LOG_INFO, "-ndi_output_start()"); return false; } @@ -175,9 +184,11 @@ bool ndi_output_start(void *data) break; default: - blog(LOG_WARNING, "unsupported pixel format %d", + blog(LOG_WARNING, + "[obs-ndi] warning: unsupported pixel format %d", format); - blog(LOG_INFO, "-ndi_output_start()"); + blog(LOG_INFO, "[obs-ndi] -ndi_output_start('%s'...)", + o->ndi_name); return false; } @@ -201,37 +212,41 @@ bool ndi_output_start(void *data) o->ndi_sender = ndiLib->send_create(&send_desc); if (o->ndi_sender) { - if (o->perf_token) { - os_end_high_performance(o->perf_token); - } - o->perf_token = os_request_high_performance("NDI Output"); - o->started = obs_output_begin_data_capture(o->output, flags); if (o->started) { - blog(LOG_INFO, "'%s': ndi output started", o->ndi_name); + blog(LOG_INFO, "[obs-ndi] '%s': ndi output started", + o->ndi_name); } else { - blog(LOG_ERROR, "'%s': data capture start failed", + blog(LOG_ERROR, + "[obs-ndi] '%s': data capture start failed", o->ndi_name); } } else { - blog(LOG_ERROR, "'%s': ndi sender init failed", o->ndi_name); + blog(LOG_ERROR, "[obs-ndi] '%s': ndi sender init failed", + o->ndi_name); } - blog(LOG_INFO, "-ndi_output_start()"); + blog(LOG_INFO, "[obs-ndi] -ndi_output_start('%s'...)", o->ndi_name); return o->started; } -void ndi_output_stop(void *data, uint64_t ts) +void ndi_output_update(void *data, obs_data_t *settings) { - blog(LOG_INFO, "+ndi_output_stop(...)"); - - UNUSED_PARAMETER(ts); - - auto o = (struct ndi_output *)data; + auto o = (ndi_output_t *)data; + o->ndi_name = obs_data_get_string(settings, "ndi_name"); + blog(LOG_INFO, "[obs-ndi] ndi_output_update('%s'...)", o->ndi_name); + o->uses_video = obs_data_get_bool(settings, "uses_video"); + o->uses_audio = obs_data_get_bool(settings, "uses_audio"); +} +void ndi_output_stop(void *data, uint64_t) +{ + auto o = (ndi_output_t *)data; + blog(LOG_INFO, "[obs-ndi] +ndi_output_stop('%s'...)", o->ndi_name); if (!o->started) { - blog(LOG_INFO, "-ndi_output_stop(...)"); + blog(LOG_INFO, "[obs-ndi] -ndi_output_stop('%s'...)", + o->ndi_name); return; } @@ -239,15 +254,12 @@ void ndi_output_stop(void *data, uint64_t ts) obs_output_end_data_capture(o->output); - if (o->perf_token) { - os_end_high_performance(o->perf_token); - o->perf_token = nullptr; - } - if (o->ndi_sender) { - blog(LOG_INFO, "+ndiLib->send_destroy(o->ndi_sender)"); + blog(LOG_INFO, + "[obs-ndi] +ndiLib->send_destroy(o->ndi_sender)"); ndiLib->send_destroy(o->ndi_sender); - blog(LOG_INFO, "-ndiLib->send_destroy(o->ndi_sender)"); + blog(LOG_INFO, + "[obs-ndi] -ndiLib->send_destroy(o->ndi_sender)"); o->ndi_sender = nullptr; } @@ -264,45 +276,27 @@ void ndi_output_stop(void *data, uint64_t ts) o->audio_channels = 0; o->audio_samplerate = 0; - blog(LOG_INFO, "-ndi_output_stop(...)"); -} - -void ndi_output_update(void *data, obs_data_t *settings) -{ - auto o = (struct ndi_output *)data; - o->ndi_name = obs_data_get_string(settings, "ndi_name"); - o->uses_video = obs_data_get_bool(settings, "uses_video"); - o->uses_audio = obs_data_get_bool(settings, "uses_audio"); -} - -void *ndi_output_create(obs_data_t *settings, obs_output_t *output) -{ - auto o = (struct ndi_output *)bzalloc(sizeof(struct ndi_output)); - o->output = output; - o->started = false; - o->audio_conv_buffer = nullptr; - o->audio_conv_buffer_size = 0; - o->perf_token = NULL; - ndi_output_update(o, settings); - return o; + blog(LOG_INFO, "[obs-ndi] -ndi_output_stop('%s'...)", o->ndi_name); } void ndi_output_destroy(void *data) { - blog(LOG_INFO, "+ndi_output_destroy(...)"); - auto o = (struct ndi_output *)data; + auto o = (ndi_output_t *)data; + blog(LOG_INFO, "[obs-ndi] +ndi_output_destroy('%s'...)", o->ndi_name); if (o->audio_conv_buffer) { + blog(LOG_INFO, + "[obs-ndi] ndi_output_destroy: freeing %zu bytes", + o->audio_conv_buffer_size); bfree(o->audio_conv_buffer); o->audio_conv_buffer = nullptr; } bfree(o); - blog(LOG_INFO, "-ndi_output_destroy(...)"); + blog(LOG_INFO, "[obs-ndi] -ndi_output_destroy('%s'...)", o->ndi_name); } -void ndi_output_rawvideo(void *data, struct video_data *frame) +void ndi_output_rawvideo(void *data, video_data *frame) { - auto o = (struct ndi_output *)data; - + auto o = (ndi_output_t *)data; if (!o->started || !o->frame_width || !o->frame_height) return; @@ -313,10 +307,10 @@ void ndi_output_rawvideo(void *data, struct video_data *frame) video_frame.xres = width; video_frame.yres = height; video_frame.frame_rate_N = (int)(o->video_framerate * 100); - video_frame.frame_rate_D = - 100; // TODO fixme: broken on fractional framerates + // TODO fixme: broken on fractional framerates + video_frame.frame_rate_D = 100; video_frame.frame_format_type = NDIlib_frame_format_type_progressive; - video_frame.timecode = (int64_t)(frame->timestamp / 100); + video_frame.timecode = frame->timestamp / 100; video_frame.FourCC = o->frame_fourcc; if (video_frame.FourCC == NDIlib_FourCC_type_UYVY) { @@ -332,57 +326,71 @@ void ndi_output_rawvideo(void *data, struct video_data *frame) ndiLib->send_send_video_async_v2(o->ndi_sender, &video_frame); } -void ndi_output_rawaudio(void *data, struct audio_data *frame) +void ndi_output_rawaudio(void *data, audio_data *frame) { - auto o = (struct ndi_output *)data; - + // NOTE: The logic in this function should be similar to + // obs-ndi-filter::ndi_filter_asyncaudio + auto o = (ndi_output_t *)data; if (!o->started || !o->audio_samplerate || !o->audio_channels) return; NDIlib_audio_frame_v3_t audio_frame = {0}; audio_frame.sample_rate = o->audio_samplerate; audio_frame.no_channels = (int)o->audio_channels; + audio_frame.timecode = NDIlib_send_timecode_synthesize; audio_frame.no_samples = frame->frames; audio_frame.channel_stride_in_bytes = frame->frames * 4; audio_frame.FourCC = NDIlib_FourCC_audio_type_FLTP; - const size_t data_size = (size_t)audio_frame.no_channels * - (size_t)audio_frame.channel_stride_in_bytes; + const size_t data_size = + audio_frame.no_channels * audio_frame.channel_stride_in_bytes; + if (data_size > o->audio_conv_buffer_size) { + blog(LOG_INFO, + "[obs-ndi] ndi_output_rawaudio: growing audio_conv_buffer from %zu to %zu bytes", + o->audio_conv_buffer_size, data_size); if (o->audio_conv_buffer) { + blog(LOG_INFO, + "[obs-ndi] ndi_output_rawaudio: freeing %zu bytes", + o->audio_conv_buffer_size); bfree(o->audio_conv_buffer); } + blog(LOG_INFO, + "[obs-ndi] ndi_output_rawaudio: allocating %zu bytes", + data_size); o->audio_conv_buffer = (uint8_t *)bmalloc(data_size); o->audio_conv_buffer_size = data_size; } for (int i = 0; i < audio_frame.no_channels; ++i) { memcpy(o->audio_conv_buffer + - ((size_t)i * - (size_t)audio_frame.channel_stride_in_bytes), + (i * audio_frame.channel_stride_in_bytes), frame->data[i], audio_frame.channel_stride_in_bytes); } audio_frame.p_data = o->audio_conv_buffer; - audio_frame.timecode = NDIlib_send_timecode_synthesize; ndiLib->send_send_audio_v3(o->ndi_sender, &audio_frame); } -struct obs_output_info create_ndi_output_info() +obs_output_info create_ndi_output_info() { - struct obs_output_info ndi_output_info = {}; + obs_output_info ndi_output_info = {}; ndi_output_info.id = "ndi_output"; ndi_output_info.flags = OBS_OUTPUT_AV; + ndi_output_info.get_name = ndi_output_getname; ndi_output_info.get_properties = ndi_output_getproperties; ndi_output_info.get_defaults = ndi_output_getdefaults; + ndi_output_info.create = ndi_output_create; - ndi_output_info.destroy = ndi_output_destroy; - ndi_output_info.update = ndi_output_update; ndi_output_info.start = ndi_output_start; + ndi_output_info.update = ndi_output_update; ndi_output_info.stop = ndi_output_stop; + ndi_output_info.destroy = ndi_output_destroy; + ndi_output_info.raw_video = ndi_output_rawvideo; ndi_output_info.raw_audio = ndi_output_rawaudio; + return ndi_output_info; } diff --git a/src/obs-ndi-source.cpp b/src/obs-ndi-source.cpp index 4280a3df..b7e7bd72 100644 --- a/src/obs-ndi-source.cpp +++ b/src/obs-ndi-source.cpp @@ -26,14 +26,16 @@ with this program. If not, see #include #include #include +#include #include "plugin-main.h" #include "Config.h" #define PROP_SOURCE "ndi_source_name" #define PROP_BANDWIDTH "ndi_bw_mode" -#define PROP_HW_ACCEL "ndi_recv_hw_accel" #define PROP_SYNC "ndi_sync" +#define PROP_FRAMESYNC "ndi_framesync" +#define PROP_HW_ACCEL "ndi_recv_hw_accel" #define PROP_FIX_ALPHA "ndi_fix_alpha_blending" #define PROP_YUV_RANGE "yuv_range" #define PROP_YUV_COLORSPACE "yuv_colorspace" @@ -44,6 +46,7 @@ with this program. If not, see #define PROP_TILT "ndi_tilt" #define PROP_ZOOM "ndi_zoom" +#define PROP_BW_UNDEFINED -1 #define PROP_BW_HIGHEST 0 #define PROP_BW_LOWEST 1 #define PROP_BW_AUDIO_ONLY 2 @@ -59,39 +62,54 @@ with this program. If not, see #define PROP_YUV_SPACE_BT601 1 #define PROP_YUV_SPACE_BT709 2 +#define PROP_LATENCY_UNDEFINED -1 #define PROP_LATENCY_NORMAL 0 #define PROP_LATENCY_LOW 1 +#define PROP_LATENCY_LOWEST 2 extern NDIlib_find_instance_t ndi_finder; -struct ndi_source { - obs_source_t *source; - NDIlib_recv_instance_t ndi_receiver; +typedef struct { + bool enabled; + float pan; + float tilt; + float zoom; +} ptz_t; + +typedef struct { + QByteArray ndi_receiver_name; + QByteArray ndi_source_name; + int bandwidth; int sync_mode; + bool framesync_enabled; + bool hw_accel_enabled; video_range_type yuv_range; video_colorspace yuv_colorspace; - pthread_t av_thread; - bool running; - NDIlib_tally_t tally; - bool alpha_filter_enabled; + int latency; bool audio_enabled; - os_performance_token_t *perf_token; - char *ndi_name; - int bandwidth; - bool hwAccelEnabled; -}; + ptz_t ptz; + NDIlib_tally_t tally; +} ndi_source_config_t; + +typedef struct { + obs_source_t *obs_source; + ndi_source_config_t config; + + bool running; + pthread_t av_thread; +} ndi_source_t; static obs_source_t *find_filter_by_id(obs_source_t *context, const char *id) { if (!context) return nullptr; - struct search_context { + typedef struct { const char *query; obs_source_t *result; - }; + } search_context_t; - struct search_context filter_search = {}; + search_context_t filter_search = {}; filter_search.query = id; filter_search.result = nullptr; @@ -102,8 +120,8 @@ static obs_source_t *find_filter_by_id(obs_source_t *context, const char *id) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wshadow" #endif - struct search_context *filter_search = - static_cast(param); + search_context_t *filter_search = + static_cast(param); const char *id = obs_source_get_id(filter); #if defined(__linux__) #pragma GCC diagnostic pop @@ -174,27 +192,22 @@ static obs_source_frame *blank_video_frame() return frame; } -const char *ndi_source_getname(void *data) +const char *ndi_source_getname(void *) { - UNUSED_PARAMETER(data); return obs_module_text("NDIPlugin.NDISourceName"); } -obs_properties_t *ndi_source_getproperties(void *data) +obs_properties_t *ndi_source_getproperties(void *) { - UNUSED_PARAMETER(data); - obs_properties_t *props = obs_properties_create(); obs_property_t *source_list = obs_properties_add_list( props, PROP_SOURCE, obs_module_text("NDIPlugin.SourceProps.SourceName"), OBS_COMBO_TYPE_EDITABLE, OBS_COMBO_FORMAT_STRING); - uint32_t nbSources = 0; const NDIlib_source_t *sources = ndiLib->find_get_current_sources(ndi_finder, &nbSources); - for (uint32_t i = 0; i < nbSources; ++i) { obs_property_list_add_string(source_list, sources[i].p_ndi_name, sources[i].p_ndi_name); @@ -204,7 +217,6 @@ obs_properties_t *ndi_source_getproperties(void *data) props, PROP_BANDWIDTH, obs_module_text("NDIPlugin.SourceProps.Bandwidth"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_list_add_int(bw_modes, obs_module_text("NDIPlugin.BWMode.Highest"), PROP_BW_HIGHEST); @@ -214,7 +226,6 @@ obs_properties_t *ndi_source_getproperties(void *data) obs_property_list_add_int(bw_modes, obs_module_text("NDIPlugin.BWMode.AudioOnly"), PROP_BW_AUDIO_ONLY); - #if defined(__linux__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wshadow" @@ -243,7 +254,6 @@ obs_properties_t *ndi_source_getproperties(void *data) obs_property_t *sync_modes = obs_properties_add_list( props, PROP_SYNC, obs_module_text("NDIPlugin.SourceProps.Sync"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_list_add_int( sync_modes, obs_module_text("NDIPlugin.SyncMode.NDITimestamp"), PROP_SYNC_NDI_TIMESTAMP); @@ -252,6 +262,9 @@ obs_properties_t *ndi_source_getproperties(void *data) obs_module_text("NDIPlugin.SyncMode.NDISourceTimecode"), PROP_SYNC_NDI_SOURCE_TIMECODE); + obs_properties_add_bool(props, PROP_FRAMESYNC, + obs_module_text("NDIPlugin.NDIFrameSync")); + obs_properties_add_bool( props, PROP_HW_ACCEL, obs_module_text("NDIPlugin.SourceProps.HWAccel")); @@ -264,7 +277,6 @@ obs_properties_t *ndi_source_getproperties(void *data) props, PROP_YUV_RANGE, obs_module_text("NDIPlugin.SourceProps.ColorRange"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_list_add_int( yuv_ranges, obs_module_text("NDIPlugin.SourceProps.ColorRange.Partial"), @@ -278,7 +290,6 @@ obs_properties_t *ndi_source_getproperties(void *data) props, PROP_YUV_COLORSPACE, obs_module_text("NDIPlugin.SourceProps.ColorSpace"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_list_add_int(yuv_spaces, "BT.709", PROP_YUV_SPACE_BT709); obs_property_list_add_int(yuv_spaces, "BT.601", PROP_YUV_SPACE_BT601); @@ -286,7 +297,6 @@ obs_properties_t *ndi_source_getproperties(void *data) props, PROP_LATENCY, obs_module_text("NDIPlugin.SourceProps.Latency"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - obs_property_list_add_int( latency_modes, obs_module_text("NDIPlugin.SourceProps.Latency.Normal"), @@ -295,42 +305,40 @@ obs_properties_t *ndi_source_getproperties(void *data) latency_modes, obs_module_text("NDIPlugin.SourceProps.Latency.Low"), PROP_LATENCY_LOW); + obs_property_list_add_int( + latency_modes, + obs_module_text("NDIPlugin.SourceProps.Latency.Lowest"), + PROP_LATENCY_LOWEST); obs_properties_add_bool(props, PROP_AUDIO, obs_module_text("NDIPlugin.SourceProps.Audio")); - obs_properties_t *group = obs_properties_create(); - + obs_properties_t *group_ptz = obs_properties_create(); obs_properties_add_float_slider( - group, PROP_PAN, obs_module_text("NDIPlugin.SourceProps.Pan"), - -1.0, 1.0, 0.001); + group_ptz, PROP_PAN, + obs_module_text("NDIPlugin.SourceProps.Pan"), -1.0, 1.0, 0.001); obs_properties_add_float_slider( - group, PROP_TILT, obs_module_text("NDIPlugin.SourceProps.Tilt"), - -1.0, 1.0, 0.001); + group_ptz, PROP_TILT, + obs_module_text("NDIPlugin.SourceProps.Tilt"), -1.0, 1.0, + 0.001); obs_properties_add_float_slider( - group, PROP_ZOOM, obs_module_text("NDIPlugin.SourceProps.Zoom"), - 0.0, 1.0, 0.001); - + group_ptz, PROP_ZOOM, + obs_module_text("NDIPlugin.SourceProps.Zoom"), 0.0, 1.0, 0.001); obs_properties_add_group(props, PROP_PTZ, obs_module_text("NDIPlugin.SourceProps.PTZ"), - OBS_GROUP_CHECKABLE, group); - - obs_properties_add_button(props, "ndi_website", "NDI.NewTek.com", - [](obs_properties_t *pps, - obs_property_t *prop, void *private_data) { - UNUSED_PARAMETER(pps); - UNUSED_PARAMETER(prop); - UNUSED_PARAMETER(private_data); + OBS_GROUP_CHECKABLE, group_ptz); + + obs_properties_add_button( + props, "ndi_website", "NDI.NewTek.com", + [](obs_properties_t *, obs_property_t *, void *) { #if defined(_WIN32) - ShellExecute(NULL, L"open", - L"http://ndi.newtek.com", - NULL, NULL, - SW_SHOWNORMAL); + ShellExecute(NULL, L"open", L"http://ndi.newtek.com", + NULL, NULL, SW_SHOWNORMAL); #elif defined(__linux__) || defined(__APPLE__) - (void)!system("open http://ndi.newtek.com"); + (void)!system("open http://ndi.newtek.com"); #endif - return true; - }); + return true; + }); return props; } @@ -348,374 +356,712 @@ void ndi_source_getdefaults(obs_data_t *settings) obs_data_set_default_bool(settings, PROP_AUDIO, true); } -void ndi_source_ptz(struct ndi_source *s, obs_data_t *settings) -{ - if (!obs_data_get_bool(settings, PROP_PTZ)) - return; - float pan = (float)obs_data_get_double(settings, PROP_PAN); - float tilt = (float)obs_data_get_double(settings, PROP_TILT); - ndiLib->recv_ptz_pan_tilt(s->ndi_receiver, pan, tilt); - float zoom = (float)obs_data_get_double(settings, PROP_ZOOM); - ndiLib->recv_ptz_zoom(s->ndi_receiver, zoom); -} +void ndi_source_thread_process_audio2(ndi_source_config_t *config, + NDIlib_audio_frame_v2_t *ndi_audio_frame2, + obs_source_t *obs_source, + obs_source_audio *obs_audio_frame); -void *ndi_source_poll_audio_video(void *data) -{ - auto s = (struct ndi_source *)data; +void ndi_source_thread_process_audio3(ndi_source_config_t *config, + NDIlib_audio_frame_v3_t *ndi_audio_frame3, + obs_source_t *obs_source, + obs_source_audio *obs_audio_frame); - blog(LOG_INFO, "A/V thread for '%s' started", - obs_source_get_name(s->source)); +void ndi_source_thread_process_video2(ndi_source_config_t *config, + NDIlib_video_frame_v2_t *ndi_video_frame2, + obs_source *obs_source, + obs_source_frame *obs_video_frame); + +void *ndi_source_thread(void *data) +{ + auto s = (ndi_source_t *)data; + auto obs_source = s->obs_source; + QByteArray obs_source_ndi_receiver_name_utf8 = + QString(obs_source_get_name(obs_source)).toUtf8(); + const char *obs_source_ndi_receiver_name = + obs_source_ndi_receiver_name_utf8.constData(); + blog(LOG_INFO, "[obs-ndi] +ndi_source_thread('%s'...)", + obs_source_ndi_receiver_name); + + ndi_source_config_t config_most_recent = {}; + ndi_source_config_t config_last_used = {}; + config_last_used.bandwidth = PROP_BW_UNDEFINED; + config_last_used.latency = PROP_LATENCY_UNDEFINED; - NDIlib_audio_frame_v3_t audio_frame; obs_source_audio obs_audio_frame = {}; - NDIlib_video_frame_v2_t video_frame; obs_source_frame obs_video_frame = {}; - bool connected = false; - if (s->perf_token) { - os_end_high_performance(s->perf_token); - } - s->perf_token = os_request_high_performance("NDI Receiver Thread"); + NDIlib_recv_create_v3_t recv_desc; + recv_desc.allow_video_fields = true; + + NDIlib_recv_instance_t ndi_receiver = nullptr; + NDIlib_video_frame_v2_t video_frame2; + NDIlib_metadata_frame_t metadata_frame; + NDIlib_framesync_instance_t ndi_frame_sync = nullptr; + NDIlib_audio_frame_v2_t audio_frame2; + int64_t timestamp_audio = 0; + int64_t timestamp_video = 0; + + NDIlib_audio_frame_v3_t audio_frame3; NDIlib_frame_type_e frame_received = NDIlib_frame_type_none; + + NDIlib_recv_create_v3_t *reset_recv_desc = &recv_desc; + while (s->running) { - if (ndiLib->recv_get_no_connections(s->ndi_receiver) == 0) { - if (connected) - connected = false; - std::this_thread::sleep_for( - std::chrono::milliseconds(100)); - continue; + // + // Main NDI receiver loop + // + + // semi-atomic not *TOO* heavy snapshot + config_most_recent = s->config; + + if (config_most_recent.ndi_receiver_name != + config_last_used.ndi_receiver_name) { + config_last_used.ndi_receiver_name = + config_most_recent.ndi_receiver_name; + + reset_recv_desc = &recv_desc; + obs_source_ndi_receiver_name_utf8 = + config_most_recent.ndi_receiver_name; + obs_source_ndi_receiver_name = + obs_source_ndi_receiver_name_utf8.constData(); + recv_desc.p_ndi_recv_name = + obs_source_ndi_receiver_name; + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' ndi_receiver_name changed; Setting recv_desc.p_ndi_recv_name='%s'", + obs_source_ndi_receiver_name, + recv_desc.p_ndi_recv_name); } - if (!connected) { - connected = true; - obs_data_t *settings = - obs_source_get_settings(s->source); - ndi_source_ptz(s, settings); - obs_data_release(settings); + if (config_most_recent.ndi_source_name != + config_last_used.ndi_source_name) { + config_last_used.ndi_source_name = + config_most_recent.ndi_source_name; + + reset_recv_desc = &recv_desc; + recv_desc.source_to_connect_to.p_ndi_name = + config_most_recent.ndi_source_name; + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' ndi_source_name changed; Setting recv_desc.source_to_connect_to.p_ndi_name='%s'", + obs_source_ndi_receiver_name, + recv_desc.source_to_connect_to.p_ndi_name); } + if (config_most_recent.bandwidth != + config_last_used.bandwidth) { + config_last_used.bandwidth = + config_most_recent.bandwidth; + + reset_recv_desc = &recv_desc; + switch (config_most_recent.bandwidth) { + case PROP_BW_HIGHEST: + default: + recv_desc.bandwidth = + NDIlib_recv_bandwidth_highest; + break; + case PROP_BW_LOWEST: + recv_desc.bandwidth = + NDIlib_recv_bandwidth_lowest; + break; + case PROP_BW_AUDIO_ONLY: + recv_desc.bandwidth = + NDIlib_recv_bandwidth_audio_only; + obs_source_output_video(obs_source, + blank_video_frame()); + break; + } + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' bandwidth changed; Setting recv_desc.bandwidth='%d'", + obs_source_ndi_receiver_name, recv_desc.bandwidth); + } + if (config_most_recent.latency != config_last_used.latency) { + config_last_used.latency = config_most_recent.latency; + + reset_recv_desc = &recv_desc; + if (config_most_recent.latency == PROP_LATENCY_NORMAL) + recv_desc.color_format = + NDIlib_recv_color_format_UYVY_BGRA; + else + recv_desc.color_format = + NDIlib_recv_color_format_fastest; + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' latency changed; Setting recv_desc.color_format='%d'", + obs_source_ndi_receiver_name, + recv_desc.color_format); + } + if (config_most_recent.framesync_enabled != + config_last_used.framesync_enabled) { + config_last_used.framesync_enabled = + config_most_recent.framesync_enabled; + + reset_recv_desc = &recv_desc; + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' framesync changed to %s", + obs_source_ndi_receiver_name, + config_most_recent.framesync_enabled ? "enabled" + : "disabled"); + } + if (reset_recv_desc) { + reset_recv_desc = nullptr; - frame_received = - ndiLib->recv_capture_v3(s->ndi_receiver, &video_frame, - &audio_frame, nullptr, 100); - - if (frame_received == NDIlib_frame_type_audio) { - if (s->audio_enabled) { - const int channelCount = - audio_frame.no_channels > 8 - ? 8 - : audio_frame.no_channels; - - obs_audio_frame.speakers = - channel_count_to_layout(channelCount); - - switch (s->sync_mode) { - case PROP_SYNC_NDI_TIMESTAMP: - obs_audio_frame.timestamp = - (uint64_t)(audio_frame.timestamp * - 100); - break; + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' Resetting NDI receiver...", + obs_source_ndi_receiver_name); - case PROP_SYNC_NDI_SOURCE_TIMECODE: - obs_audio_frame.timestamp = - (uint64_t)(audio_frame.timecode * - 100); + if (ndi_frame_sync) { + ndiLib->framesync_destroy(ndi_frame_sync); + ndi_frame_sync = nullptr; + } + + if (ndi_receiver) { +#if 1 + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' ndiLib->recv_destroy(ndi_receiver)", + obs_source_ndi_receiver_name); +#endif + ndiLib->recv_destroy(ndi_receiver); + ndi_receiver = nullptr; + } +#if 1 + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' +ndi_receiver = ndiLib->recv_create_v3(&recv_desc)", + obs_source_ndi_receiver_name); +#endif + ndi_receiver = ndiLib->recv_create_v3(&recv_desc); +#if 1 + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' -ndi_receiver = ndiLib->recv_create_v3(&recv_desc)", + obs_source_ndi_receiver_name); +#endif + if (!ndi_receiver) { + blog(LOG_ERROR, + "[obs-ndi] ndi_source_thread: '%s' Cannot create ndi_receiver for NDI source '%s'", + obs_source_ndi_receiver_name, + recv_desc.source_to_connect_to.p_ndi_name); + break; + } + + if (config_most_recent.framesync_enabled) { + timestamp_audio = 0; + timestamp_video = 0; + +#if 1 + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' +ndi_frame_sync = ndiLib->framesync_create(ndi_receiver)", + obs_source_ndi_receiver_name); +#endif + ndi_frame_sync = + ndiLib->framesync_create(ndi_receiver); +#if 1 + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' -ndi_frame_sync = ndiLib->framesync_create(ndi_receiver); ndi_frame_sync=%p", + obs_source_ndi_receiver_name, + ndi_frame_sync); +#endif + if (!ndi_frame_sync) { + blog(LOG_ERROR, + "[obs-ndi] ndi_source_thread: '%s' Cannot create ndi_frame_sync for NDI source '%s'", + obs_source_ndi_receiver_name, + recv_desc.source_to_connect_to + .p_ndi_name); break; } + } + } + + if (ndiLib->recv_get_no_connections(ndi_receiver) == 0) { +#if 0 + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' No connection", + obs_source_ndi_receiver_name); +#endif + std::this_thread::sleep_for( + std::chrono::milliseconds(100)); + continue; + } - obs_audio_frame.samples_per_sec = - audio_frame.sample_rate; - obs_audio_frame.format = - AUDIO_FORMAT_FLOAT_PLANAR; - obs_audio_frame.frames = audio_frame.no_samples; + if (config_most_recent.hw_accel_enabled != + config_last_used.hw_accel_enabled) { + config_last_used.hw_accel_enabled = + config_most_recent.hw_accel_enabled; - for (int i = 0; i < channelCount; ++i) { - obs_audio_frame.data[i] = - (uint8_t *)audio_frame.p_data + - i * audio_frame.channel_stride_in_bytes; + NDIlib_metadata_frame_t hwAccelMetadata; + hwAccelMetadata.p_data = + config_most_recent.hw_accel_enabled + ? (char *)"" + : (char *)""; + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' hw_accel_enabled changed; Sending NDI metadata '%s'", + obs_source_ndi_receiver_name, + hwAccelMetadata.p_data); + ndiLib->recv_send_metadata(ndi_receiver, + &hwAccelMetadata); + } + + if (config_most_recent.ptz.enabled) { + const static float tollerance = 0.001f; + if (fabs(config_most_recent.ptz.pan - + config_last_used.ptz.pan) > tollerance || + fabs(config_most_recent.ptz.tilt - + config_last_used.ptz.tilt) > tollerance || + fabs(config_most_recent.ptz.zoom - + config_last_used.ptz.zoom) > tollerance) { + if (ndiLib->recv_ptz_is_supported( + ndi_receiver)) { + config_last_used.ptz = + config_most_recent.ptz; + + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' ptz changed; Sending PTZ pan=%f, tilt=%f, zoom=%f", + obs_source_ndi_receiver_name, + config_most_recent.ptz.pan, + config_most_recent.ptz.tilt, + config_most_recent.ptz.zoom); + ndiLib->recv_ptz_pan_tilt( + ndi_receiver, + config_most_recent.ptz.pan, + config_most_recent.ptz.tilt); + ndiLib->recv_ptz_zoom( + ndi_receiver, + config_most_recent.ptz.zoom); } + } + } + + if (config_most_recent.tally.on_preview != + config_last_used.tally.on_preview || + config_most_recent.tally.on_program != + config_last_used.tally.on_program) { + config_last_used.tally = config_most_recent.tally; + + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread: '%s' tally changed; Sending tally on_preview=%d, on_program=%d", + obs_source_ndi_receiver_name, + config_most_recent.tally.on_preview, + config_most_recent.tally.on_program); + ndiLib->recv_set_tally(ndi_receiver, + &config_most_recent.tally); + } + + if (ndi_frame_sync) { + // + // AUDIO + // + audio_frame2 = {}; + ndiLib->framesync_capture_audio( + ndi_frame_sync, &audio_frame2, + 0, // "Your desired sample rate. 0 for “use source”." + 0, // "Your desired channel count. 0 for “use source”." + 1024); + if (audio_frame2.p_data && + (audio_frame2.timestamp > timestamp_audio)) { + //blog(LOG_INFO, "a");//udio_frame"; + timestamp_audio = audio_frame2.timestamp; + ndi_source_thread_process_audio2( + &config_most_recent, &audio_frame2, + obs_source, &obs_audio_frame); + } + ndiLib->framesync_free_audio(ndi_frame_sync, + &audio_frame2); + + // + // VIDEO + // + video_frame2 = {}; + ndiLib->framesync_capture_video( + ndi_frame_sync, &video_frame2, + NDIlib_frame_format_type_progressive); + if (video_frame2.p_data && + (video_frame2.timestamp > timestamp_video)) { + //blog(LOG_INFO, "v");//ideo_frame"; + timestamp_video = video_frame2.timestamp; + ndi_source_thread_process_video2( + &config_most_recent, &video_frame2, + obs_source, &obs_video_frame); + } + ndiLib->framesync_free_video(ndi_frame_sync, + &video_frame2); - obs_source_output_audio(s->source, - &obs_audio_frame); + // TODO: More accurate sleep that subtracts the duration of this loop iteration? + std::this_thread::sleep_for( + std::chrono::milliseconds(5)); + } else { + frame_received = ndiLib->recv_capture_v3(ndi_receiver, + &video_frame2, + &audio_frame3, + nullptr, 100); + + if (frame_received == NDIlib_frame_type_audio) { + ndi_source_thread_process_audio3( + &config_most_recent, &audio_frame3, + obs_source, &obs_audio_frame); + ndiLib->recv_free_audio_v3(ndi_receiver, + &audio_frame3); + continue; + } + + if (frame_received == NDIlib_frame_type_video) { + ndi_source_thread_process_video2( + &config_most_recent, &video_frame2, + obs_source, &obs_video_frame); + ndiLib->recv_free_video_v2(ndi_receiver, + &video_frame2); + continue; } - ndiLib->recv_free_audio_v3(s->ndi_receiver, - &audio_frame); } + } - if (frame_received == NDIlib_frame_type_video) { - switch (video_frame.FourCC) { - case NDIlib_FourCC_type_BGRA: - obs_video_frame.format = VIDEO_FORMAT_BGRA; - break; + if (ndi_frame_sync) { + ndiLib->framesync_destroy(ndi_frame_sync); + ndi_frame_sync = nullptr; + } - case NDIlib_FourCC_type_BGRX: - obs_video_frame.format = VIDEO_FORMAT_BGRX; - break; + if (ndi_receiver) { + ndiLib->recv_destroy(ndi_receiver); + ndi_receiver = nullptr; + } - case NDIlib_FourCC_type_RGBA: - case NDIlib_FourCC_type_RGBX: - obs_video_frame.format = VIDEO_FORMAT_RGBA; - break; + blog(LOG_INFO, "[obs-ndi] -ndi_source_thread('%s'...)", + obs_source_ndi_receiver_name); - case NDIlib_FourCC_type_UYVY: - case NDIlib_FourCC_type_UYVA: - obs_video_frame.format = VIDEO_FORMAT_UYVY; - break; + return nullptr; +} - case NDIlib_FourCC_type_I420: - obs_video_frame.format = VIDEO_FORMAT_I420; - break; +void ndi_source_thread_process_audio2(ndi_source_config_t *config, + NDIlib_audio_frame_v2_t *ndi_audio_frame2, + obs_source_t *obs_source, + obs_source_audio *obs_audio_frame) +{ + if (!config->audio_enabled) { + return; + } - case NDIlib_FourCC_type_NV12: - obs_video_frame.format = VIDEO_FORMAT_NV12; - break; + const int channelCount = ndi_audio_frame2->no_channels > 8 + ? 8 + : ndi_audio_frame2->no_channels; - default: - blog(LOG_INFO, - "warning: unsupported video pixel format: %d", - video_frame.FourCC); - break; - } + obs_audio_frame->speakers = channel_count_to_layout(channelCount); - switch (s->sync_mode) { - case PROP_SYNC_NDI_TIMESTAMP: - obs_video_frame.timestamp = - (uint64_t)(video_frame.timestamp * 100); - break; + switch (config->sync_mode) { + case PROP_SYNC_NDI_TIMESTAMP: + obs_audio_frame->timestamp = + (uint64_t)(ndi_audio_frame2->timestamp * 100); + break; - case PROP_SYNC_NDI_SOURCE_TIMECODE: - obs_video_frame.timestamp = - (uint64_t)(video_frame.timecode * 100); - break; - } + case PROP_SYNC_NDI_SOURCE_TIMECODE: + obs_audio_frame->timestamp = + (uint64_t)(ndi_audio_frame2->timecode * 100); + break; + } - obs_video_frame.width = video_frame.xres; - obs_video_frame.height = video_frame.yres; - obs_video_frame.linesize[0] = - video_frame.line_stride_in_bytes; - obs_video_frame.data[0] = video_frame.p_data; - - video_format_get_parameters( - s->yuv_colorspace, s->yuv_range, - obs_video_frame.color_matrix, - obs_video_frame.color_range_min, - obs_video_frame.color_range_max); - - obs_source_output_video(s->source, &obs_video_frame); - ndiLib->recv_free_video_v2(s->ndi_receiver, - &video_frame); - } + obs_audio_frame->samples_per_sec = ndi_audio_frame2->sample_rate; + obs_audio_frame->format = AUDIO_FORMAT_FLOAT_PLANAR; + obs_audio_frame->frames = ndi_audio_frame2->no_samples; + for (int i = 0; i < channelCount; ++i) { + obs_audio_frame->data[i] = + (uint8_t *)ndi_audio_frame2->p_data + + (i * ndi_audio_frame2->channel_stride_in_bytes); } - os_end_high_performance(s->perf_token); - s->perf_token = NULL; + obs_source_output_audio(obs_source, obs_audio_frame); +} - blog(LOG_INFO, "audio thread for '%s' completed", - obs_source_get_name(s->source)); - return nullptr; +void ndi_source_thread_process_audio3(ndi_source_config_t *config, + NDIlib_audio_frame_v3_t *ndi_audio_frame3, + obs_source_t *obs_source, + obs_source_audio *obs_audio_frame) +{ + if (!config->audio_enabled) { + return; + } + + const int channelCount = ndi_audio_frame3->no_channels > 8 + ? 8 + : ndi_audio_frame3->no_channels; + + obs_audio_frame->speakers = channel_count_to_layout(channelCount); + + switch (config->sync_mode) { + case PROP_SYNC_NDI_TIMESTAMP: + obs_audio_frame->timestamp = + (uint64_t)(ndi_audio_frame3->timestamp * 100); + break; + + case PROP_SYNC_NDI_SOURCE_TIMECODE: + obs_audio_frame->timestamp = + (uint64_t)(ndi_audio_frame3->timecode * 100); + break; + } + + obs_audio_frame->samples_per_sec = ndi_audio_frame3->sample_rate; + obs_audio_frame->format = AUDIO_FORMAT_FLOAT_PLANAR; + obs_audio_frame->frames = ndi_audio_frame3->no_samples; + for (int i = 0; i < channelCount; ++i) { + obs_audio_frame->data[i] = + (uint8_t *)ndi_audio_frame3->p_data + + (i * ndi_audio_frame3->channel_stride_in_bytes); + } + + obs_source_output_audio(obs_source, obs_audio_frame); } -void ndi_source_update(void *data, obs_data_t *settings) +void ndi_source_thread_process_video2(ndi_source_config_t *config, + NDIlib_video_frame_v2_t *ndi_video_frame, + obs_source *obs_source, + obs_source_frame *obs_video_frame) { - auto s = (struct ndi_source *)data; - - const char *ndi_name = obs_data_get_string(settings, PROP_SOURCE); - long long bandwidth = obs_data_get_int(settings, PROP_BANDWIDTH); - bool hwAccelEnabled = obs_data_get_bool(settings, PROP_HW_ACCEL); - if (!s->ndi_name || strcmp(ndi_name, s->ndi_name) != 0 || - s->bandwidth != bandwidth) { - s->bandwidth = bandwidth; - bfree(s->ndi_name); - s->ndi_name = bstrdup(ndi_name); - if (s->running) { - s->running = false; - pthread_join(s->av_thread, NULL); - } - s->running = false; - ndiLib->recv_destroy(s->ndi_receiver); - - NDIlib_recv_create_v3_t recv_desc; - recv_desc.source_to_connect_to.p_ndi_name = ndi_name; - recv_desc.allow_video_fields = true; - recv_desc.color_format = NDIlib_recv_color_format_UYVY_BGRA; - - switch (obs_data_get_int(settings, PROP_BANDWIDTH)) { - case PROP_BW_HIGHEST: - default: - recv_desc.bandwidth = NDIlib_recv_bandwidth_highest; - break; - case PROP_BW_LOWEST: - recv_desc.bandwidth = NDIlib_recv_bandwidth_lowest; - break; - case PROP_BW_AUDIO_ONLY: - recv_desc.bandwidth = NDIlib_recv_bandwidth_audio_only; - obs_source_output_video(s->source, blank_video_frame()); - break; - } + switch (ndi_video_frame->FourCC) { + case NDIlib_FourCC_type_BGRA: + obs_video_frame->format = VIDEO_FORMAT_BGRA; + break; - s->ndi_receiver = ndiLib->recv_create_v3(&recv_desc); - if (s->ndi_receiver) { - if (hwAccelEnabled) { - NDIlib_metadata_frame_t hwAccelMetadata; - hwAccelMetadata.p_data = - (char *)""; - ndiLib->recv_send_metadata(s->ndi_receiver, - &hwAccelMetadata); - } - s->hwAccelEnabled = hwAccelEnabled; + case NDIlib_FourCC_type_BGRX: + obs_video_frame->format = VIDEO_FORMAT_BGRX; + break; - s->running = true; - pthread_create(&s->av_thread, nullptr, - ndi_source_poll_audio_video, data); + case NDIlib_FourCC_type_RGBA: + case NDIlib_FourCC_type_RGBX: + obs_video_frame->format = VIDEO_FORMAT_RGBA; + break; - blog(LOG_INFO, "started A/V threads for source '%s'", - recv_desc.source_to_connect_to.p_ndi_name); + case NDIlib_FourCC_type_UYVY: + case NDIlib_FourCC_type_UYVA: + obs_video_frame->format = VIDEO_FORMAT_UYVY; + break; - // Update tally status - Config *conf = Config::Current(); - s->tally.on_preview = conf->TallyPreviewEnabled && - obs_source_showing(s->source); - s->tally.on_program = conf->TallyProgramEnabled && - obs_source_active(s->source); - ndiLib->recv_set_tally(s->ndi_receiver, &s->tally); - } else { - blog(LOG_ERROR, - "can't create a receiver for NDI source '%s'", - ndi_name); - } + case NDIlib_FourCC_type_I420: + obs_video_frame->format = VIDEO_FORMAT_I420; + break; + + case NDIlib_FourCC_type_NV12: + obs_video_frame->format = VIDEO_FORMAT_NV12; + break; + + default: + blog(LOG_INFO, + "[obs-ndi] warning: unsupported video pixel format: %d", + ndi_video_frame->FourCC); + break; } - if (s->ndi_receiver) { - if (s->hwAccelEnabled != hwAccelEnabled) { - NDIlib_metadata_frame_t hwAccelMetadata; - hwAccelMetadata.p_data = - hwAccelEnabled - ? (char *)"" - : (char *)""; - ndiLib->recv_send_metadata(s->ndi_receiver, - &hwAccelMetadata); - s->hwAccelEnabled = hwAccelEnabled; - } - ndi_source_ptz(s, settings); + switch (config->sync_mode) { + case PROP_SYNC_NDI_TIMESTAMP: + obs_video_frame->timestamp = + (uint64_t)(ndi_video_frame->timestamp * 100); + break; + + case PROP_SYNC_NDI_SOURCE_TIMECODE: + obs_video_frame->timestamp = + (uint64_t)(ndi_video_frame->timecode * 100); + break; } - s->alpha_filter_enabled = obs_data_get_bool(settings, PROP_FIX_ALPHA); - // Don't persist this value in settings - obs_data_set_bool(settings, PROP_FIX_ALPHA, false); + obs_video_frame->width = ndi_video_frame->xres; + obs_video_frame->height = ndi_video_frame->yres; + obs_video_frame->linesize[0] = ndi_video_frame->line_stride_in_bytes; + obs_video_frame->data[0] = ndi_video_frame->p_data; - if (s->alpha_filter_enabled) { - obs_source_t *existing_filter = - find_filter_by_id(s->source, OBS_NDI_ALPHA_FILTER_ID); + video_format_get_parameters(config->yuv_colorspace, config->yuv_range, + obs_video_frame->color_matrix, + obs_video_frame->color_range_min, + obs_video_frame->color_range_max); + + obs_source_output_video(obs_source, obs_video_frame); +} + +void ndi_source_thread_start(ndi_source_t *s) +{ + s->running = true; + pthread_create(&s->av_thread, nullptr, ndi_source_thread, s); + blog(LOG_INFO, + "[obs-ndi] ndi_source_thread_start: '%s' Started A/V ndi_source_thread for NDI source '%s'", + s->config.ndi_receiver_name.constData(), + s->config.ndi_source_name.constData()); +} + +void ndi_source_thread_stop(ndi_source_t *s) +{ + if (s->running) { + s->running = false; + pthread_join(s->av_thread, NULL); + } +} + +void ndi_source_update(void *data, obs_data_t *settings) +{ + auto s = (ndi_source_t *)data; + auto obs_source = s->obs_source; + auto name = obs_source_get_name(obs_source); + blog(LOG_INFO, "[obs-ndi] +ndi_source_update('%s'...)", name); + ndi_source_config_t config = s->config; + + config.ndi_source_name = obs_data_get_string(settings, PROP_SOURCE); + config.bandwidth = (int)obs_data_get_int(settings, PROP_BANDWIDTH); + + config.sync_mode = (int)obs_data_get_int(settings, PROP_SYNC); + // if sync mode is set to the unsupported "Internal" mode, set it + // to "Source Timing" mode and apply that change to the settings data + if (config.sync_mode == PROP_SYNC_INTERNAL) { + config.sync_mode = PROP_SYNC_NDI_SOURCE_TIMECODE; + obs_data_set_int(settings, PROP_SYNC, + PROP_SYNC_NDI_SOURCE_TIMECODE); + } + + config.framesync_enabled = obs_data_get_bool(settings, PROP_FRAMESYNC); + + config.hw_accel_enabled = obs_data_get_bool(settings, PROP_HW_ACCEL); + + bool alpha_filter_enabled = obs_data_get_bool(settings, PROP_FIX_ALPHA); + // Prevent duplicate filters by not persisting this value in settings + obs_data_set_bool(settings, PROP_FIX_ALPHA, false); + if (alpha_filter_enabled) { + obs_source_t *existing_filter = + find_filter_by_id(obs_source, OBS_NDI_ALPHA_FILTER_ID); if (!existing_filter) { obs_source_t *new_filter = obs_source_create( OBS_NDI_ALPHA_FILTER_ID, obs_module_text( "NDIPlugin.PremultipliedAlphaFilterName"), nullptr, nullptr); - obs_source_filter_add(s->source, new_filter); + obs_source_filter_add(obs_source, new_filter); obs_source_release(new_filter); } } - s->sync_mode = (int)obs_data_get_int(settings, PROP_SYNC); - // if sync mode is set to the unsupported "Internal" mode, set it - // to "Source Timing" mode and apply that change to the settings data - if (s->sync_mode == PROP_SYNC_INTERNAL) { - s->sync_mode = PROP_SYNC_NDI_SOURCE_TIMECODE; - obs_data_set_int(settings, PROP_SYNC, - PROP_SYNC_NDI_SOURCE_TIMECODE); - } - - s->yuv_range = prop_to_range_type( + config.yuv_range = prop_to_range_type( (int)obs_data_get_int(settings, PROP_YUV_RANGE)); - s->yuv_colorspace = prop_to_colorspace( + config.yuv_colorspace = prop_to_colorspace( (int)obs_data_get_int(settings, PROP_YUV_COLORSPACE)); - const bool is_unbuffered = - (obs_data_get_int(settings, PROP_LATENCY) == PROP_LATENCY_LOW); - obs_source_set_async_unbuffered(s->source, is_unbuffered); + config.latency = (int)obs_data_get_int(settings, PROP_LATENCY); + // Disable OBS buffering only for "Lowest" latency mode + const bool is_unbuffered = (config.latency == PROP_LATENCY_LOWEST); + obs_source_set_async_unbuffered(obs_source, is_unbuffered); + + config.audio_enabled = obs_data_get_bool(settings, PROP_AUDIO); + obs_source_set_audio_active(obs_source, config.audio_enabled); + + bool ptz_enabled = obs_data_get_bool(settings, PROP_PTZ); + float pan = (float)obs_data_get_double(settings, PROP_PAN); + float tilt = (float)obs_data_get_double(settings, PROP_TILT); + float zoom = (float)obs_data_get_double(settings, PROP_ZOOM); + config.ptz = {ptz_enabled, pan, tilt, zoom}; + + // Update tally status + Config *conf = Config::Current(); + config.tally.on_preview = conf->TallyPreviewEnabled && + obs_source_showing(obs_source); + config.tally.on_program = conf->TallyProgramEnabled && + obs_source_active(obs_source); + + s->config = config; + + if (!config.ndi_source_name.isEmpty()) { + if (!s->running) { + ndi_source_thread_start(s); + } + } else { + ndi_source_thread_stop(s); + } - s->audio_enabled = obs_data_get_bool(settings, PROP_AUDIO); - obs_source_set_audio_active(s->source, s->audio_enabled); + blog(LOG_INFO, "[obs-ndi] -ndi_source_update('%s'...)", name); } void ndi_source_shown(void *data) { - auto s = (struct ndi_source *)data; - - if (s->ndi_receiver) { - s->tally.on_preview = (Config::Current())->TallyPreviewEnabled; - ndiLib->recv_set_tally(s->ndi_receiver, &s->tally); - } + auto s = (ndi_source_t *)data; + auto name = obs_source_get_name(s->obs_source); + blog(LOG_INFO, "[obs-ndi] ndi_source_shown('%s'...)", name); + s->config.tally.on_preview = (Config::Current())->TallyPreviewEnabled; } void ndi_source_hidden(void *data) { - auto s = (struct ndi_source *)data; - - if (s->ndi_receiver) { - s->tally.on_preview = false; - ndiLib->recv_set_tally(s->ndi_receiver, &s->tally); - } + auto s = (ndi_source_t *)data; + auto name = obs_source_get_name(s->obs_source); + blog(LOG_INFO, "[obs-ndi] ndi_source_hidden('%s'...)", name); + s->config.tally.on_preview = false; } void ndi_source_activated(void *data) { - auto s = (struct ndi_source *)data; - - if (s->ndi_receiver) { - s->tally.on_program = (Config::Current())->TallyProgramEnabled; - ndiLib->recv_set_tally(s->ndi_receiver, &s->tally); - } + auto s = (ndi_source_t *)data; + auto name = obs_source_get_name(s->obs_source); + blog(LOG_INFO, "[obs-ndi] ndi_source_activated('%s'...)", name); + s->config.tally.on_program = (Config::Current())->TallyProgramEnabled; } void ndi_source_deactivated(void *data) { - auto s = (struct ndi_source *)data; + auto s = (ndi_source_t *)data; + auto name = obs_source_get_name(s->obs_source); + blog(LOG_INFO, "[obs-ndi] ndi_source_deactivated('%s'...)", name); + s->config.tally.on_program = false; +} - if (s->ndi_receiver) { - s->tally.on_program = false; - ndiLib->recv_set_tally(s->ndi_receiver, &s->tally); - } +void ndi_source_renamed(void *data, calldata_t *) +{ + auto s = (ndi_source_t *)data; + auto name = obs_source_get_name(s->obs_source); + blog(LOG_INFO, "[obs-ndi] ndi_source_renamed: name='%s'", name); + s->config.ndi_receiver_name = + QString("OBS-NDI '%1'").arg(name).toUtf8(); } -void *ndi_source_create(obs_data_t *settings, obs_source_t *source) +void *ndi_source_create(obs_data_t *settings, obs_source_t *obs_source) { - auto s = (struct ndi_source *)bzalloc(sizeof(struct ndi_source)); - s->source = source; - s->running = false; - s->perf_token = NULL; + auto name = obs_source_get_name(obs_source); + blog(LOG_INFO, "[obs-ndi] +ndi_source_create('%s'...)", name); + + auto s = (ndi_source_t *)bzalloc(sizeof(ndi_source_t)); + s->obs_source = obs_source; + s->config.ndi_receiver_name = + QString("OBS-NDI '%1'").arg(name).toUtf8(); + + auto sh = obs_source_get_signal_handler(s->obs_source); + signal_handler_connect(sh, "rename", ndi_source_renamed, s); + ndi_source_update(s, settings); + + blog(LOG_INFO, "[obs-ndi] -ndi_source_create('%s'...)", name); + return s; } void ndi_source_destroy(void *data) { - auto s = (struct ndi_source *)data; - s->running = false; - pthread_join(s->av_thread, NULL); - ndiLib->recv_destroy(s->ndi_receiver); - bfree(s->ndi_name); + auto s = (ndi_source_t *)data; + auto name = obs_source_get_name(s->obs_source); + blog(LOG_INFO, "[obs-ndi] +ndi_source_destroy('%s'...)", name); + + signal_handler_disconnect(obs_source_get_signal_handler(s->obs_source), + "rename", ndi_source_renamed, s); + + ndi_source_thread_stop(s); + bfree(s); + + blog(LOG_INFO, "[obs-ndi] -ndi_source_destroy('%s'...)", name); } -struct obs_source_info create_ndi_source_info() +obs_source_info create_ndi_source_info() { - struct obs_source_info ndi_source_info = {}; + obs_source_info ndi_source_info = {}; ndi_source_info.id = "ndi_source"; ndi_source_info.type = OBS_SOURCE_TYPE_INPUT; ndi_source_info.output_flags = OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE; + ndi_source_info.get_name = ndi_source_getname; ndi_source_info.get_properties = ndi_source_getproperties; ndi_source_info.get_defaults = ndi_source_getdefaults; - ndi_source_info.update = ndi_source_update; + + ndi_source_info.create = ndi_source_create; + ndi_source_info.activate = ndi_source_activated; ndi_source_info.show = ndi_source_shown; + ndi_source_info.update = ndi_source_update; ndi_source_info.hide = ndi_source_hidden; - ndi_source_info.activate = ndi_source_activated; ndi_source_info.deactivate = ndi_source_deactivated; - ndi_source_info.create = ndi_source_create; ndi_source_info.destroy = ndi_source_destroy; return ndi_source_info; diff --git a/src/plugin-main.cpp b/src/plugin-main.cpp index 18e179ed..38c308e8 100644 --- a/src/plugin-main.cpp +++ b/src/plugin-main.cpp @@ -45,6 +45,16 @@ with this program. If not, see OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") +const char *obs_module_name() +{ + return "obs-ndi"; +} + +const char *obs_module_description() +{ + return "NDI input/output integration for OBS Studio"; +} + const NDIlib_v4 *ndiLib = nullptr; extern struct obs_source_info create_ndi_source_info(); @@ -73,16 +83,19 @@ OutputSettings *output_settings = nullptr; bool obs_module_load(void) { - obs_log(LOG_INFO, "obs_module_load: hello ! (version %s)", - PLUGIN_VERSION); + blog(LOG_INFO, + "[obs-ndi] obs_module_load: you can haz obs-ndi (Version %s)", + PLUGIN_VERSION); + blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), + QT_VERSION_STR); QMainWindow *main_window = (QMainWindow *)obs_frontend_get_main_window(); ndiLib = load_ndilib(); if (!ndiLib) { - obs_log(LOG_ERROR, - "obs_module_load: load_ndilib() failed; Module won't load."); + blog(LOG_ERROR, + "[obs-ndi] obs_module_load: load_ndilib() failed; Module won't load."); const char *msg_string_name = ""; #ifdef _MSC_VER @@ -107,14 +120,14 @@ bool obs_module_load(void) } if (!ndiLib->initialize()) { - obs_log(LOG_ERROR, - "obs_module_load: ndiLib->initialize() failed; CPU unsupported by NDI library. Module won't load."); + blog(LOG_ERROR, + "[obs-ndi] obs_module_load: ndiLib->initialize() failed; CPU unsupported by NDI library. Module won't load."); return false; } - obs_log(LOG_INFO, - "obs_module_load: NDI library initialized successfully (%s)", - ndiLib->version()); + blog(LOG_INFO, + "[obs-ndi] obs_module_load: NDI library initialized successfully ('%s')", + ndiLib->version()); NDIlib_find_create_t find_desc = {0}; find_desc.show_local_sources = true; @@ -194,9 +207,14 @@ bool obs_module_load(void) return true; } -void obs_module_unload() +void obs_module_post_load(void) { - obs_log(LOG_INFO, "+obs_module_unload()"); + blog(LOG_INFO, "[obs-ndi] obs_module_post_load: ..."); +} + +void obs_module_unload(void) +{ + blog(LOG_INFO, "[obs-ndi] +obs_module_unload()"); if (ndiLib) { if (ndi_finder) { @@ -211,19 +229,9 @@ void obs_module_unload() delete loaded_lib; } - obs_log(LOG_INFO, "obs_module_unload: goodbye !"); - - obs_log(LOG_INFO, "-obs_module_unload()"); -} + blog(LOG_INFO, "[obs-ndi] obs_module_unload: goodbye !"); -const char *obs_module_name() -{ - return "obs-ndi"; -} - -const char *obs_module_description() -{ - return "NDI input/output integration for OBS Studio"; + blog(LOG_INFO, "[obs-ndi] -obs_module_unload()"); } const NDIlib_v4 *load_ndilib() @@ -240,28 +248,28 @@ const NDIlib_v4 *load_ndilib() for (QString location : locations) { path = QDir::cleanPath( QDir(location).absoluteFilePath(NDILIB_LIBRARY_NAME)); - obs_log(LOG_INFO, "load_ndilib: Trying '%s'", - path.toUtf8().constData()); + blog(LOG_INFO, "[obs-ndi] load_ndilib: Trying '%s'", + path.toUtf8().constData()); QFileInfo libPath(path); if (libPath.exists() && libPath.isFile()) { path = libPath.absoluteFilePath(); - obs_log(LOG_INFO, - "load_ndilib: Found NDI library at '%s'", - path.toUtf8().constData()); + blog(LOG_INFO, + "[obs-ndi] load_ndilib: Found NDI library at '%s'", + path.toUtf8().constData()); loaded_lib = new QLibrary(path, nullptr); if (loaded_lib->load()) { - obs_log(LOG_INFO, - "load_ndilib: NDI runtime loaded successfully"); + blog(LOG_INFO, + "[obs-ndi] load_ndilib: NDI runtime loaded successfully"); NDIlib_v5_load_ lib_load = (NDIlib_v5_load_)loaded_lib->resolve( "NDIlib_v5_load"); if (lib_load != nullptr) { - obs_log(LOG_INFO, - "load_ndilib: NDIlib_v5_load found"); + blog(LOG_INFO, + "[obs-ndi] load_ndilib: NDIlib_v5_load found"); return lib_load(); } else { - obs_log(LOG_ERROR, - "load_ndilib: ERROR: NDIlib_v5_load not found in loaded library"); + blog(LOG_ERROR, + "[obs-ndi] load_ndilib: ERROR: NDIlib_v5_load not found in loaded library"); } } else { delete loaded_lib; @@ -269,6 +277,7 @@ const NDIlib_v4 *load_ndilib() } } } - obs_log(LOG_ERROR, "load_ndilib: ERROR: Can't find the NDI library"); + blog(LOG_ERROR, + "[obs-ndi] load_ndilib: ERROR: Can't find the NDI library"); return nullptr; } diff --git a/src/premultiplied-alpha-filter.cpp b/src/premultiplied-alpha-filter.cpp index b536666a..4d6804a6 100644 --- a/src/premultiplied-alpha-filter.cpp +++ b/src/premultiplied-alpha-filter.cpp @@ -25,28 +25,21 @@ struct alpha_filter { gs_effect_t *effect; }; -const char *alpha_filter_getname(void *data) +const char *alpha_filter_getname(void *) { - UNUSED_PARAMETER(data); return obs_module_text("NDIPlugin.PremultipliedAlphaFilterName"); } -obs_properties_t *alpha_filter_getproperties(void *data) +obs_properties_t *alpha_filter_getproperties(void *) { - UNUSED_PARAMETER(data); obs_properties_t *props = obs_properties_create(); return props; } -void alpha_filter_update(void *data, obs_data_t *settings) -{ - UNUSED_PARAMETER(data); - UNUSED_PARAMETER(settings); -} +void alpha_filter_update(void *, obs_data_t *) {} -void *alpha_filter_create(obs_data_t *settings, obs_source_t *source) +void *alpha_filter_create(obs_data_t *, obs_source_t *source) { - UNUSED_PARAMETER(settings); struct alpha_filter *s = (struct alpha_filter *)bzalloc(sizeof(struct alpha_filter)); s->context = source; @@ -60,9 +53,8 @@ void alpha_filter_destroy(void *data) bfree(s); } -void alpha_filter_videorender(void *data, gs_effect_t *effect) +void alpha_filter_videorender(void *data, gs_effect_t *) { - UNUSED_PARAMETER(effect); struct alpha_filter *s = (struct alpha_filter *)data; if (!obs_source_process_filter_begin(s->context, GS_RGBA, diff --git a/src/preview-output.cpp b/src/preview-output.cpp index 5850cf3e..1c6fe35f 100644 --- a/src/preview-output.cpp +++ b/src/preview-output.cpp @@ -50,6 +50,8 @@ void preview_output_init(const char *default_name) if (context.output) return; + blog(LOG_INFO, "[obs-ndi] preview_output_init('%s')", default_name); + obs_data_t *output_settings = obs_data_create(); obs_data_set_string(output_settings, "ndi_name", default_name); obs_data_set_bool(output_settings, "uses_audio", false); @@ -64,7 +66,7 @@ void preview_output_start(const char *output_name) return; blog(LOG_INFO, - "preview_output_start: starting NDI preview output with name '%s'", + "[obs-ndi] preview_output_start: starting NDI preview output with name '%s'", output_name); obs_get_video_info(&context.ovi); @@ -82,38 +84,30 @@ void preview_output_start(const char *output_name) const audio_output_info *mainAOI = audio_output_get_info(obs_get_audio()); - video_output_info vi = {0}; - vi.name = output_name; - vi.format = VIDEO_FORMAT_BGRA; - vi.width = width; - vi.height = height; - vi.fps_den = context.ovi.fps_den; - vi.fps_num = context.ovi.fps_num; - vi.cache_size = 16; - vi.colorspace = mainVOI->colorspace; - vi.range = mainVOI->range; - - video_output_open(&context.video_queue, &vi); - - audio_output_info ai = {0}; - ai.name = output_name; - ai.format = mainAOI->format; - ai.samples_per_sec = mainAOI->samples_per_sec; - ai.speakers = mainAOI->speakers; - ai.input_callback = [](void *param, uint64_t start_ts, uint64_t end_ts, - uint64_t *new_ts, uint32_t active_mixers, - struct audio_output_data *mixes) { - UNUSED_PARAMETER(param); - UNUSED_PARAMETER(start_ts); - UNUSED_PARAMETER(end_ts); - UNUSED_PARAMETER(new_ts); - UNUSED_PARAMETER(active_mixers); - UNUSED_PARAMETER(mixes); - return false; - }; - ai.input_param = nullptr; - - audio_output_open(&context.dummy_audio_queue, &ai); + video_output_info voi = {0}; + voi.name = output_name; + voi.format = VIDEO_FORMAT_BGRA; + voi.width = width; + voi.height = height; + voi.fps_den = context.ovi.fps_den; + voi.fps_num = context.ovi.fps_num; + voi.cache_size = 16; + voi.colorspace = mainVOI->colorspace; + voi.range = mainVOI->range; + + video_output_open(&context.video_queue, &voi); + + audio_output_info aoi = {0}; + aoi.name = output_name; + aoi.format = mainAOI->format; + aoi.samples_per_sec = mainAOI->samples_per_sec; + aoi.speakers = mainAOI->speakers; + aoi.input_callback = [](void *, uint64_t, uint64_t, uint64_t *, + uint32_t, + struct audio_output_data *) { return false; }; + aoi.input_param = nullptr; + + audio_output_open(&context.dummy_audio_queue, &aoi); obs_frontend_add_event_callback(on_preview_scene_changed, &context); if (obs_frontend_preview_program_mode_active()) { @@ -135,7 +129,8 @@ void preview_output_start(const char *output_name) obs_output_start(context.output); context.enabled = true; - blog(LOG_INFO, "preview_output_start: started NDI preview output"); + blog(LOG_INFO, + "[obs-ndi] preview_output_start: started NDI preview output"); } void preview_output_stop() @@ -143,7 +138,8 @@ void preview_output_stop() if (!context.enabled) return; - blog(LOG_INFO, "preview_output_stop: stopping NDI preview output"); + blog(LOG_INFO, + "[obs-ndi] preview_output_stop: stopping NDI preview output"); obs_output_stop(context.output); @@ -163,11 +159,14 @@ void preview_output_stop() context.enabled = false; - blog(LOG_INFO, "preview_output_stop: stopped NDI preview output"); + blog(LOG_INFO, + "[obs-ndi] preview_output_stop: stopped NDI preview output"); } void preview_output_deinit() { + blog(LOG_INFO, "[obs-ndi] preview_output_deinit()"); + obs_output_release(context.output); context.output = nullptr; @@ -181,6 +180,7 @@ bool preview_output_is_enabled() void on_preview_scene_changed(enum obs_frontend_event event, void *param) { + blog(LOG_INFO, "[obs-ndi] on_preview_scene_changed(%d)", event); auto ctx = (struct preview_output *)param; switch (event) { case OBS_FRONTEND_EVENT_STUDIO_MODE_ENABLED: @@ -203,13 +203,9 @@ void on_preview_scene_changed(enum obs_frontend_event event, void *param) } } -void render_preview_source(void *param, uint32_t cx, uint32_t cy) +void render_preview_source(void *param, uint32_t, uint32_t) { - UNUSED_PARAMETER(cx); - UNUSED_PARAMETER(cy); - auto ctx = (struct preview_output *)param; - if (!ctx->current_source) return;