diff --git a/.github/workflows/auto-build.yml b/.github/workflows/auto-build.yml index 56359568c..422735473 100644 --- a/.github/workflows/auto-build.yml +++ b/.github/workflows/auto-build.yml @@ -38,18 +38,24 @@ jobs: { label: "windows-x64", os: "blacksmith-2vcpu-windows-2025", + native_target: "x86_64-pc-windows-msvc", + native_platform_key: "win32-x64", builder_args: "--win nsis portable --x64", artifact_paths: "opennow-stable/dist-release/*-x64.exe\n", }, { label: "windows-arm64", os: "blacksmith-2vcpu-windows-2025", + native_target: "aarch64-pc-windows-msvc", + native_platform_key: "win32-arm64", builder_args: "--win nsis portable --arm64", artifact_paths: "opennow-stable/dist-release/*-arm64.exe\n", }, { label: "macos-x64", os: "blacksmith-6vcpu-macos-15", + native_target: "x86_64-apple-darwin", + native_platform_key: "darwin-x64", builder_args: "--mac dmg zip --x64", artifact_paths: "opennow-stable/dist-release/*.dmg\n" + @@ -58,6 +64,8 @@ jobs: { label: "macos-arm64", os: "blacksmith-6vcpu-macos-15", + native_target: "aarch64-apple-darwin", + native_platform_key: "darwin-arm64", builder_args: "--mac dmg zip --arm64", artifact_paths: "opennow-stable/dist-release/*.dmg\n" + @@ -66,6 +74,8 @@ jobs: { label: "linux-x64", os: "blacksmith-2vcpu-ubuntu-2404", + native_target: "x86_64-unknown-linux-gnu", + native_platform_key: "linux-x64", builder_args: "--linux AppImage deb --x64", artifact_paths: "opennow-stable/dist-release/*-x86_64.AppImage\n" + @@ -74,6 +84,8 @@ jobs: { label: "linux-arm64", os: "blacksmith-2vcpu-ubuntu-2404-arm", + native_target: "aarch64-unknown-linux-gnu", + native_platform_key: "linux-arm64", builder_args: "--linux AppImage deb --arm64", artifact_paths: "opennow-stable/dist-release/*-arm64.AppImage\n" + @@ -87,7 +99,11 @@ jobs: const isDevValidationPullRequest = isPullRequest && baseRef === "dev"; const include = isDevValidationPullRequest ? builds.filter(({ label }) => - label === "windows-x64" || label === "linux-x64" || label === "linux-arm64") + label === "windows-x64" + || label === "macos-x64" + || label === "macos-arm64" + || label === "linux-x64" + || label === "linux-arm64") : builds; fs.appendFileSync(process.env.GITHUB_OUTPUT, `matrix=${JSON.stringify({ include })}\n`); @@ -121,30 +137,63 @@ jobs: cache-dependency-path: opennow-stable/package-lock.json - name: Setup Rust - if: runner.os == 'Linux' uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.native_target }} - - name: Install Linux native streamer dependencies + - name: Install Linux native streamer build dependencies if: runner.os == 'Linux' - env: - DEBIAN_FRONTEND: noninteractive run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - build-essential \ - pkg-config \ + sudo apt-get install -y \ + libasound2-dev \ + libpulse-dev \ libx11-dev \ - libglib2.0-dev \ - libgstreamer1.0-dev \ - libgstreamer-plugins-base1.0-dev \ - libgstreamer-plugins-bad1.0-dev \ - gstreamer1.0-libav \ - gstreamer1.0-plugins-base \ - gstreamer1.0-plugins-good \ - gstreamer1.0-plugins-bad \ - gstreamer1.0-nice \ - gstreamer1.0-gl \ - gstreamer1.0-x + libxcursor-dev \ + libxext-dev \ + libxfixes-dev \ + libxi-dev \ + libxrandr-dev \ + libxrender-dev \ + libxss-dev + + - name: Install Windows ARM64 compiler + if: matrix.native_target == 'aarch64-pc-windows-msvc' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $installerRoot = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer" + $installPath = & "$installerRoot\vswhere.exe" -latest -products * -property installationPath + if (-not $installPath) { + throw "Visual Studio Build Tools installation was not found." + } + $compiler = Get-ChildItem "$installPath\VC\Tools\MSVC\*\bin\Hostx64\arm64\cl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $compiler) { + $arguments = "modify --installPath `"$installPath`" --quiet --norestart --add Microsoft.VisualStudio.Component.VC.Tools.ARM64" + $install = Start-Process "$installerRoot\vs_installer.exe" -ArgumentList $arguments -Wait -PassThru + if ($install.ExitCode -notin 0, 3010) { + throw "Visual Studio ARM64 compiler installation failed with exit code $($install.ExitCode)." + } + $compiler = Get-ChildItem "$installPath\VC\Tools\MSVC\*\bin\Hostx64\arm64\cl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + } + if (-not $compiler) { + throw "Visual Studio ARM64 compiler is unavailable after installation." + } + Write-Host "Using Windows ARM64 compiler: $($compiler.FullName)" + + - name: Setup Windows ARM64 build tools + if: matrix.native_target == 'aarch64-pc-windows-msvc' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64_arm64 + + - name: Pin CMake for bundled SDL2 + if: runner.os == 'macOS' + shell: bash + run: | + python3 -m venv "$RUNNER_TEMP/cmake-venv" + "$RUNNER_TEMP/cmake-venv/bin/pip" install --disable-pip-version-check cmake==3.31.6 + echo "$RUNNER_TEMP/cmake-venv/bin" >> "$GITHUB_PATH" - name: Install dependencies run: npm ci @@ -165,10 +214,14 @@ jobs: id: build run: npm run build - - name: Test and build Linux native streamer - if: runner.os == 'Linux' + - name: Test and build native streamer v2 + env: + OPENNOW_NATIVE_STREAMER_TARGET: ${{ matrix.native_target }} + OPENNOW_NATIVE_STREAMER_PLATFORM_KEY: ${{ matrix.native_platform_key }} run: | - cargo test --manifest-path ../native/opennow-streamer/Cargo.toml --features gstreamer + cargo fmt --manifest-path ../native/opennow-streamer/Cargo.toml --all -- --check + cargo clippy --manifest-path ../native/opennow-streamer/Cargo.toml --workspace --all-targets -- -D warnings + cargo test --manifest-path ../native/opennow-streamer/Cargo.toml --workspace npm run native:build - name: CI summary diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 430de07c5..a1feb2f74 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,14 +36,27 @@ jobs: prerelease: ${{ steps.release.outputs.prerelease }} make_latest: ${{ steps.release.outputs.make_latest }} release_tag: ${{ steps.release.outputs.release_tag }} - source_ref: ${{ steps.release.outputs.source_ref }} release_type: ${{ steps.release.outputs.release_type }} release_name: ${{ steps.release.outputs.release_name }} previous_tag: ${{ steps.release.outputs.previous_tag }} steps: + - name: Validate manual release source + if: github.event_name == 'workflow_dispatch' + shell: bash + env: + REF_NAME: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} + run: | + if [[ "$REF_TYPE" != "branch" || "$REF_NAME" != "dev" ]]; then + echo "Manual releases must be dispatched from the dev branch." >&2 + exit 1 + fi + - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/dev' || github.sha }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -127,10 +140,8 @@ jobs: if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then release_tag="v${version}" - source_ref="$REF_NAME" else release_tag="$raw" - source_ref="$REF_NAME" fi release_pages="$(gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/releases?per_page=100")" @@ -167,7 +178,6 @@ jobs: echo "prerelease=$prerelease" echo "make_latest=$make_latest" echo "release_tag=$release_tag" - echo "source_ref=$source_ref" echo "release_type=$release_type" echo "release_name=$release_name" echo "previous_tag=$previous_tag" @@ -186,7 +196,7 @@ jobs: - name: Checkout source uses: actions/checkout@v4 with: - ref: ${{ needs.preflight.outputs.source_ref }} + ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/dev' || github.sha }} fetch-depth: 0 token: ${{ secrets.RELEASE_PUSH_TOKEN || secrets.GITHUB_TOKEN }} @@ -197,7 +207,6 @@ jobs: EVENT_NAME: ${{ github.event_name }} REF_TYPE: ${{ github.ref_type }} RELEASE_VERSION: ${{ needs.preflight.outputs.version }} - SOURCE_REF: ${{ needs.preflight.outputs.source_ref }} RELEASE_TYPE: ${{ needs.preflight.outputs.release_type }} run: | if [[ "$RELEASE_TYPE" == "nightly" ]]; then @@ -225,7 +234,7 @@ jobs: echo "No version bump changes to commit." else git commit -m "chore(release): prepare v${RELEASE_VERSION}" - git push origin HEAD:"$SOURCE_REF" + git push origin HEAD:dev fi echo "release_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" @@ -243,8 +252,7 @@ jobs: os: blacksmith-4vcpu-windows-2025 builder_args: "--win nsis portable --x64" native_build: "true" - bundle_gstreamer: "1" - native_target: "" + native_target: "x86_64-pc-windows-msvc" native_platform_key: "win32-x64" artifact_paths: | opennow-stable/dist-release/*-x64.exe @@ -253,9 +261,8 @@ jobs: - label: windows-arm64 os: blacksmith-4vcpu-windows-2025 builder_args: "--win nsis portable --arm64" - native_build: "false" - bundle_gstreamer: "0" - native_target: "" + native_build: "true" + native_target: "aarch64-pc-windows-msvc" native_platform_key: "win32-arm64" artifact_paths: | opennow-stable/dist-release/*-arm64.exe @@ -263,7 +270,6 @@ jobs: os: blacksmith-6vcpu-macos-15 builder_args: "--mac dmg zip --x64" native_build: "true" - bundle_gstreamer: "1" native_target: "x86_64-apple-darwin" native_platform_key: "darwin-x64" arch: x64 @@ -275,7 +281,6 @@ jobs: os: blacksmith-6vcpu-macos-15 builder_args: "--mac dmg zip --arm64" native_build: "true" - bundle_gstreamer: "1" native_target: "aarch64-apple-darwin" native_platform_key: "darwin-arm64" arch: arm64 @@ -287,8 +292,7 @@ jobs: os: blacksmith-4vcpu-ubuntu-2404 builder_args: "--linux AppImage deb --x64" native_build: "true" - bundle_gstreamer: "0" - native_target: "" + native_target: "x86_64-unknown-linux-gnu" native_platform_key: "linux-x64" artifact_paths: | opennow-stable/dist-release/*-x86_64.AppImage @@ -298,8 +302,7 @@ jobs: os: blacksmith-4vcpu-ubuntu-2404-arm builder_args: "--linux AppImage deb --arm64" native_build: "true" - bundle_gstreamer: "0" - native_target: "" + native_target: "aarch64-unknown-linux-gnu" native_platform_key: "linux-arm64" artifact_paths: | opennow-stable/dist-release/*-arm64.AppImage @@ -316,8 +319,6 @@ jobs: ELECTRON_BUILDER_CACHE: ${{ github.workspace }}/.cache/electron-builder npm_config_audit: "false" npm_config_fund: "false" - GSTREAMER_VERSION: "1.28.2" - GSTREAMER_WINDOWS_VERSION: "1.28.3" CARGO_NET_RETRY: "10" CARGO_HTTP_MULTIPLEXING: "false" CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse @@ -329,7 +330,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ needs.prepare-source.outputs.release_sha }} + ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/dev' || github.sha }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -342,6 +343,60 @@ jobs: if: matrix.native_build == 'true' uses: dtolnay/rust-toolchain@stable + - name: Install Linux native streamer build dependencies + if: matrix.native_build == 'true' && runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libasound2-dev \ + libpulse-dev \ + libx11-dev \ + libxcursor-dev \ + libxext-dev \ + libxfixes-dev \ + libxi-dev \ + libxrandr-dev \ + libxrender-dev \ + libxss-dev + + - name: Install Windows ARM64 compiler + if: matrix.native_target == 'aarch64-pc-windows-msvc' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $installerRoot = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer" + $installPath = & "$installerRoot\vswhere.exe" -latest -products * -property installationPath + if (-not $installPath) { + throw "Visual Studio Build Tools installation was not found." + } + $compiler = Get-ChildItem "$installPath\VC\Tools\MSVC\*\bin\Hostx64\arm64\cl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $compiler) { + $arguments = "modify --installPath `"$installPath`" --quiet --norestart --add Microsoft.VisualStudio.Component.VC.Tools.ARM64" + $install = Start-Process "$installerRoot\vs_installer.exe" -ArgumentList $arguments -Wait -PassThru + if ($install.ExitCode -notin 0, 3010) { + throw "Visual Studio ARM64 compiler installation failed with exit code $($install.ExitCode)." + } + $compiler = Get-ChildItem "$installPath\VC\Tools\MSVC\*\bin\Hostx64\arm64\cl.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + } + if (-not $compiler) { + throw "Visual Studio ARM64 compiler is unavailable after installation." + } + Write-Host "Using Windows ARM64 compiler: $($compiler.FullName)" + + - name: Setup Windows ARM64 build tools + if: matrix.native_target == 'aarch64-pc-windows-msvc' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64_arm64 + + - name: Pin CMake for bundled SDL2 + if: matrix.native_build == 'true' && runner.os == 'macOS' + shell: bash + run: | + python3 -m venv "$RUNNER_TEMP/cmake-venv" + "$RUNNER_TEMP/cmake-venv/bin/pip" install --disable-pip-version-check cmake==3.31.6 + echo "$RUNNER_TEMP/cmake-venv/bin" >> "$GITHUB_PATH" + - name: Install Rust target if: matrix.native_build == 'true' && matrix.native_target != '' run: rustup target add ${{ matrix.native_target }} @@ -368,225 +423,6 @@ jobs: restore-keys: | ${{ runner.os }}-electron- - - name: Install Linux build/runtime packages - if: runner.os == 'Linux' - env: - DEBIAN_FRONTEND: noninteractive - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - fakeroot \ - rpm \ - build-essential \ - pkg-config \ - libx11-dev \ - libglib2.0-dev \ - libgstreamer1.0-dev \ - libgstreamer-plugins-base1.0-dev \ - libgstreamer-plugins-bad1.0-dev \ - gstreamer1.0-alsa \ - gstreamer1.0-libav \ - gstreamer1.0-plugins-base \ - gstreamer1.0-plugins-good \ - gstreamer1.0-plugins-bad \ - gstreamer1.0-plugins-ugly \ - gstreamer1.0-nice \ - gstreamer1.0-vaapi \ - gstreamer1.0-gl \ - gstreamer1.0-x \ - libva2 \ - libva-drm2 \ - libvulkan1 \ - mesa-vulkan-drivers - - - name: Cache macOS GStreamer packages - if: matrix.native_build == 'true' && runner.os == 'macOS' - uses: actions/cache@v4 - with: - path: ${{ github.workspace }}/.cache/gstreamer-macos - key: gstreamer-macos-${{ env.GSTREAMER_VERSION }} - - - name: Install macOS GStreamer build/runtime packages - if: matrix.native_build == 'true' && runner.os == 'macOS' - run: | - primary_base="https://gstreamer.freedesktop.org/data/pkg/macos/${GSTREAMER_VERSION}" - mirror_base="https://gstreamer.freedesktop.org/pkg/macos/${GSTREAMER_VERSION}" - cache_dir="${GITHUB_WORKSPACE}/.cache/gstreamer-macos/${GSTREAMER_VERSION}" - mkdir -p "$cache_dir" - runtime="$cache_dir/gstreamer-1.0-${GSTREAMER_VERSION}-universal.pkg" - devel="$cache_dir/gstreamer-1.0-devel-${GSTREAMER_VERSION}-universal.pkg" - download_pkg() { - local name="$1" - local destination="$2" - local partial="${destination}.part" - if [ -s "$destination" ]; then - return 0 - fi - rm -f "$partial" - if ! curl --fail --location --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 30 --continue-at - --output "$partial" "$primary_base/$name"; then - curl --fail --location --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 30 --continue-at - --output "$partial" "$mirror_base/$name" - fi - test -s "$partial" - mv "$partial" "$destination" - } - download_pkg "gstreamer-1.0-${GSTREAMER_VERSION}-universal.pkg" "$runtime" - download_pkg "gstreamer-1.0-devel-${GSTREAMER_VERSION}-universal.pkg" "$devel" - test -s "$runtime" - test -s "$devel" - sudo installer -pkg "$runtime" -target / - sudo installer -pkg "$devel" -target / - root="/Library/Frameworks/GStreamer.framework/Versions/1.0" - echo "GSTREAMER_1_0_ROOT_MACOS=$root" >> "$GITHUB_ENV" - echo "$root/bin" >> "$GITHUB_PATH" - echo "PKG_CONFIG_PATH=$root/lib/pkgconfig:$PKG_CONFIG_PATH" >> "$GITHUB_ENV" - - - name: Cache Windows GStreamer SDK - if: matrix.native_build == 'true' && runner.os == 'Windows' - uses: actions/cache@v4 - with: - path: | - C:\gstreamer - ${{ github.workspace }}\.cache\gstreamer-windows - key: gstreamer-windows-${{ env.GSTREAMER_WINDOWS_VERSION }} - - - name: Install Windows GStreamer SDK - if: matrix.native_build == 'true' && runner.os == 'Windows' - shell: pwsh - run: | - $installRoot = "C:\gstreamer\1.0\msvc_x86_64" - $roots = @( - $installRoot, - "C:\Program Files\gstreamer\1.0\msvc_x86_64" - ) - $searchBases = @("C:\gstreamer", "C:\Program Files\gstreamer") - $script:gstreamerPcPaths = @() - $script:lastInstallerExitCode = $null - function Test-GStreamerRoot([string] $candidate) { - return [bool]($candidate -and - (Test-Path (Join-Path $candidate "lib\pkgconfig\gstreamer-1.0.pc")) -and - ((Test-Path (Join-Path $candidate "bin\pkg-config.exe")) -or (Test-Path (Join-Path $candidate "bin\pkgconf.exe")))) - } - function Find-GStreamerRoot { - $script:gstreamerPcPaths = @() - foreach ($candidate in $roots) { - if (Test-GStreamerRoot $candidate) { return $candidate } - } - foreach ($base in $searchBases) { - if (-not (Test-Path $base)) { continue } - $pcFiles = @(Get-ChildItem -Path $base -Filter "gstreamer-1.0.pc" -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like "*\lib\pkgconfig\gstreamer-1.0.pc" }) - foreach ($pc in $pcFiles) { - $script:gstreamerPcPaths += $pc.FullName - $root = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $pc.FullName)) - if (Test-GStreamerRoot $root) { return $root } - } - } - return $null - } - function Get-GStreamerInstaller([string] $fileName, [string] $downloadDir, [string[]] $urlBases) { - New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null - $output = Join-Path $downloadDir $fileName - $partial = "$output.part" - if ((Test-Path $output) -and ((Get-Item $output).Length -gt 0)) { - Write-Host "Using cached GStreamer MSI: $output" - return $output - } - foreach ($base in $urlBases) { - $url = "$base/$fileName" - Write-Host "Downloading GStreamer MSI from $url" - if ((Test-Path $partial) -and ((Get-Item $partial).Length -eq 0)) { Remove-Item -Force $partial } - $process = Start-Process -FilePath "curl.exe" -ArgumentList @( - "--fail", - "--location", - "--retry", "8", - "--retry-all-errors", - "--retry-delay", "5", - "--connect-timeout", "30", - "--continue-at", "-", - "--output", $partial, - $url - ) -Wait -PassThru -NoNewWindow - if ($process.ExitCode -eq 0 -and (Test-Path $partial) -and ((Get-Item $partial).Length -gt 0)) { - Move-Item -Force $partial $output - return $output - } - Write-Host "GStreamer MSI download failed from $url with curl exit code $($process.ExitCode)." - if ((Test-Path $partial) -and ((Get-Item $partial).Length -eq 0)) { Remove-Item -Force $partial } - } - if (Test-Path $partial) { - Write-Host "Removing incomplete GStreamer MSI download: $partial" - Remove-Item -Force $partial - } - return $null - } - function Install-GStreamerSdk([string] $version) { - $urlBases = @( - "https://gstreamer.freedesktop.org/data/pkg/windows/$version/msvc", - "https://gstreamer.freedesktop.org/pkg/windows/$version/msvc" - ) - $downloadDir = Join-Path $env:GITHUB_WORKSPACE ".cache\gstreamer-windows\$version" - $installer = Get-GStreamerInstaller "gstreamer-1.0-msvc-x86_64-$version.exe" $downloadDir $urlBases - if (-not $installer) { return $false } - $install = Start-Process -FilePath $installer -ArgumentList @( - "/VERYSILENT", - "/SUPPRESSMSGBOXES", - "/NORESTART", - "/TYPE=devel", - "/DIR=$installRoot" - ) -Wait -PassThru -NoNewWindow - $script:lastInstallerExitCode = $install.ExitCode - if ($install.ExitCode -ne 0) { return $false } - return [bool](Find-GStreamerRoot) - } - $root = Find-GStreamerRoot - if ($root) { - Write-Host "Using cached GStreamer Windows SDK: $root" - } elseif (-not (Install-GStreamerSdk $env:GSTREAMER_WINDOWS_VERSION)) { - [void](Find-GStreamerRoot) - Write-Host "GStreamer explicit roots searched: $($roots -join ', ')" - Write-Host "GStreamer recursive search bases: $($searchBases -join ', ')" - Write-Host "Discovered gstreamer-1.0.pc paths: $(if ($script:gstreamerPcPaths.Count) { $script:gstreamerPcPaths -join ', ' } else { 'none' })" - Write-Error "Failed to install/discover GStreamer Windows SDK version $env:GSTREAMER_WINDOWS_VERSION. Installer exit code: $script:lastInstallerExitCode." - exit 1 - } - $root = Find-GStreamerRoot - Write-Host "GStreamer explicit roots searched: $($roots -join ', ')" - Write-Host "GStreamer recursive search bases: $($searchBases -join ', ')" - Write-Host "Discovered gstreamer-1.0.pc paths: $(if ($script:gstreamerPcPaths.Count) { $script:gstreamerPcPaths -join ', ' } else { 'none' })" - if (-not $root) { - Write-Error "GStreamer SDK root not found. Checked: $($roots -join ', '). Expected lib\pkgconfig\gstreamer-1.0.pc and bin\pkg-config.exe or bin\pkgconf.exe." - exit 1 - } - $gstInspect = Join-Path $root "bin\gst-inspect-1.0.exe" - if (-not (Test-Path $gstInspect)) { - Write-Error "gst-inspect-1.0.exe not found in $root\bin after GStreamer install." - exit 1 - } - $h264Decoders = @("avdec_h264", "d3d11h264dec", "d3d12h264dec") - $availableH264Decoders = @() - foreach ($decoder in $h264Decoders) { - $inspect = Start-Process -FilePath $gstInspect -ArgumentList @($decoder) -Wait -PassThru -NoNewWindow - if ($inspect.ExitCode -eq 0) { - $availableH264Decoders += $decoder - Write-Host "GStreamer decoder available: $decoder" - } else { - Write-Host "GStreamer decoder unavailable: $decoder" - } - } - if ($availableH264Decoders.Count -eq 0) { - Write-Error "GStreamer Windows SDK installed, but no H.264 decoder plugins were found among: $($h264Decoders -join ', ')" - exit 1 - } - # Official Cerbero Windows packages disable gstvulkan. OpenNOW injects a - # vendored Vulkan plugin into the private runtime after bundling. - Write-Host "Skipping base-SDK Vulkan element checks; Vulkan is injected into the private OpenNOW runtime bundle." - "GSTREAMER_1_0_ROOT_MSVC_X86_64=$root" | Out-File -FilePath $env:GITHUB_ENV -Append - "$root\bin" | Out-File -FilePath $env:GITHUB_PATH -Append - - - name: Build patched Windows GStreamer D3D11 plugin - if: matrix.native_build == 'true' && runner.os == 'Windows' - shell: pwsh - run: ./scripts/build-patched-gstreamer-d3d11.ps1 -GStreamerRoot "$env:GSTREAMER_1_0_ROOT_MSVC_X86_64" -Version "$env:GSTREAMER_WINDOWS_VERSION" - - name: Install FPM for arm64 deb builds if: matrix.label == 'linux-arm64' run: sudo apt-get install -y ruby ruby-dev build-essential && sudo gem install fpm @@ -621,8 +457,6 @@ jobs: - name: Build native streamer if: matrix.native_build == 'true' env: - OPENNOW_NATIVE_STREAMER_FEATURES: gstreamer - OPENNOW_BUNDLE_GSTREAMER_RUNTIME: ${{ matrix.bundle_gstreamer }} OPENNOW_NATIVE_STREAMER_TARGET: ${{ matrix.native_target }} OPENNOW_NATIVE_STREAMER_PLATFORM_KEY: ${{ matrix.native_platform_key }} run: npm run native:build @@ -777,24 +611,10 @@ jobs: contents: write steps: - - name: Resolve release branch - id: branch - shell: bash - env: - REF_TYPE: ${{ github.ref_type }} - REF_NAME: ${{ github.ref_name }} - run: | - if [[ "$REF_TYPE" == "branch" ]]; then - branch="$REF_NAME" - else - branch="main" - fi - echo "branch=$branch" >> "$GITHUB_OUTPUT" - - name: Checkout release branch uses: actions/checkout@v4 with: - ref: ${{ steps.branch.outputs.branch }} + ref: ${{ github.event_name == 'workflow_dispatch' && 'refs/heads/dev' || 'refs/heads/main' }} fetch-depth: 0 token: ${{ secrets.RELEASE_PUSH_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 4c8d3cf47..c8578af58 100644 --- a/.gitignore +++ b/.gitignore @@ -62,5 +62,3 @@ result .deriveddata-device/ .opencode vendor/ -!native/opennow-streamer/vendor/ -!native/opennow-streamer/vendor/** diff --git a/locales/en.json b/locales/en.json index 8208f4318..90064318d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -541,8 +541,8 @@ "advancedTiming": "Decode {{decode}}ms · Render {{render}}ms · JitterBuf {{jitterBuffer}}ms · Jitter {{jitter}}ms", "advancedInputQueue": "Input queue {{buffered}}KB · peak {{peak}}KB · drops {{drops}} · sched {{scheduling}}ms · residual {{residual}}px", "advancedMouseFlush": "Mouse flush {{interval}}ms · {{rate}}/s · PR {{partiallyReliable}}", - "advancedGstreamerEnabled": "GStreamer enabled · {{state}}", - "advancedGstreamerDisabled": "GStreamer disabled · Chromium WebRTC", + "advancedNativeEnabled": "Native streamer enabled · {{state}}", + "advancedNativeDisabled": "Native streamer disabled · Chromium WebRTC", "advancedIceCandidate": "ICE {{transport}} candidate", "advancedShaderActive": "Shader FX active (WebGL post-processing)", "advancedDecoderRecovery": "Decoder recovery {{state}} · attempts {{attempts}} · action {{action}}", @@ -1032,11 +1032,11 @@ "nativeStreamer": { "title": "Native Streamer", "runtime": "Runtime", - "runtimeDescription": "Enable the experimental streamer and inspect its local GStreamer readiness.", + "runtimeDescription": "Enable the experimental streamer and inspect its local native runtime readiness.", "videoOutput": "Video Output", "videoOutputDescription": "Hardware capability, rendering backend, frame pacing, and window mode.", "nativeStreaming": "Native Streaming", - "nativeStreamingHint": "Native streaming is experimental and may have platform-specific bugs, glitches, or fallbacks to Chromium/WebRTC. Enable it only if you are comfortable testing the GStreamer-based desktop streamer for new sessions.", + "nativeStreamingHint": "Native streaming is experimental and may have platform-specific bugs, glitches, or fallbacks to Chromium/WebRTC. Enable it only if you are comfortable testing the self-contained desktop streamer for new sessions.", "enablePromptKicker": "Experimental", "enablePromptTitle": "Enable Native Streamer?", "enablePromptBody": "The native streamer is still experimental. Internal mode keeps video inside the OpenNOW window; external floating mode is available as a fallback in settings.", @@ -1052,13 +1052,12 @@ "showNativeStreamerStatsHint": "Display the native streamer's own diagnostics overlay during native sessions.", "streamerStatus": "Streamer Status", "checkNativeStreamer": "Check native streamer", - "gstreamerReady": "GStreamer Ready", + "nativeRuntimeReady": "Native Runtime Ready", "notReady": "Not Ready", - "statusDefaultHint": "OpenNOW will check the bundled GStreamer streamer when this tab opens.", - "gstreamerRuntime": "GStreamer Runtime", - "bundledPathDetected": "Bundled path detected", + "statusDefaultHint": "OpenNOW will check the bundled native streamer when this tab opens.", + "nativeRuntime": "Native Runtime", + "nativeExecutablePath": "Native executable path", "runtimeDefaultHint": "Packaged Windows/macOS builds auto-detect a bundled runtime next to the native streamer. Linux uses distro packages.", - "linuxRuntimeHint": "Linux AppImage/private GStreamer bundling is intentionally not used by default because VAAPI/V4L2/Vulkan plugins must match the host distro and GPU driver stack. .deb packages declare Debian/Ubuntu dependencies automatically.", "videoPath": "Video Path", "thisPc": "This PC", "supportedVideoBackends": "Backend Compatibility", @@ -1072,13 +1071,13 @@ "noRenderer": "No renderer", "systemMemory": "System memory", "noCodecsAvailable": "No compatible codecs were detected for this backend.", - "probingBackends": "Probing the installed GPU drivers and GStreamer plugins…", + "probingBackends": "Probing installed native video backends…", "activePath": "Active path", "capabilityProbeHint": "Refresh the streamer status to probe the video backends supported by this PC.", - "backendUnavailableOnPc": "This backend is not supported by the installed GPU drivers and GStreamer plugins.", + "backendUnavailableOnPc": "This backend is not supported by the installed native runtime and GPU drivers.", "codecSupportUnknown": "Codec support unknown", "memoryPathUnknown": "Memory path unknown", - "videoPathDefaultHint": "OpenNOW will show the active hardware decode path after GStreamer is detected.", + "videoPathDefaultHint": "OpenNOW will show the active hardware decode path after the native runtime is detected.", "directxBackend": "Video Backend", "directxBackendHint": "Applies to the next native streamer process. Unsupported choices are disabled from the live capability probe. Auto selects the best compatible path.", "framePacing": "Frame Pacing", @@ -1088,12 +1087,12 @@ "renderMode": "Render Mode", "renderModeInternal": "Internal (One Window)", "renderModeExternal": "External (Floating)", - "renderModeHint": "Internal (default) embeds native video in the OpenNOW window. External keeps the floating GStreamer window for testing/fallback. Change applies to the next native streamer process.", + "renderModeHint": "Internal (default) embeds native video in the OpenNOW window. External uses the native presenter's own window for testing. The change applies to the next native streamer process.", "renderModeInternalOnlyHint": "This platform uses Internal (one window) only. Keyboard, mouse, and controller input stay in Electron and are forwarded over IPC. External floating mode is Windows-only.", "transportMode": "Transport Mode", "transportModeWebrtc": "WebRTC (Default)", "transportModeNvst": "NVST (Experimental)", - "transportModeHint": "WebRTC is the supported path. NVST (experimental) uses classic RTSPS + UDP video (Moonlight-hypothesis scaffold) while keyboard/mouse/controller stay on WebRTC SCTP datachannels. First live sessions may need SRTP/pcap validation.", + "transportModeHint": "WebRTC uses Chromium for media. NVST sends H.264 video through the native UDP/SRTP decoder while audio and input continue over WebRTC. Select NVST to test native hardware video decoding.", "transportModeWindowsOnlyHint": "NVST transport is Windows-only for now. This platform always uses WebRTC." }, "thanks": { diff --git a/native/gstreamer-patches/0001-d3d11-enable-tearing-vrr.patch b/native/gstreamer-patches/0001-d3d11-enable-tearing-vrr.patch deleted file mode 100644 index 1bfce4f65..000000000 --- a/native/gstreamer-patches/0001-d3d11-enable-tearing-vrr.patch +++ /dev/null @@ -1,55 +0,0 @@ ---- a/subprojects/gst-plugins-bad/sys/d3d11/gstd3d11window_win32.cpp -+++ b/subprojects/gst-plugins-bad/sys/d3d11/gstd3d11window_win32.cpp -@@ -1155,9 +1155,36 @@ - DXGI_SWAP_CHAIN_DESC desc = { 0, }; - IDXGISwapChain *new_swapchain = NULL; - GstD3D11Device *device = window->device; -+ HRESULT hr; - - self->have_swapchain1 = FALSE; - -+ const gchar *allow_tearing_env = g_getenv ("OPENNOW_NATIVE_D3D_ALLOW_TEARING"); -+ if (allow_tearing_env && -+ (!g_ascii_strcasecmp (allow_tearing_env, "1") || -+ !g_ascii_strcasecmp (allow_tearing_env, "true") || -+ !g_ascii_strcasecmp (allow_tearing_env, "on") || -+ !g_ascii_strcasecmp (allow_tearing_env, "yes"))) { -+ IDXGIFactory1 *factory = -+ gst_d3d11_device_get_dxgi_factory_handle (device); -+ ComPtr < IDXGIFactory5 > factory5; -+ BOOL allow_tearing = FALSE; -+ -+ hr = factory->QueryInterface (IID_PPV_ARGS (&factory5)); -+ if (SUCCEEDED (hr)) { -+ hr = factory5->CheckFeatureSupport (DXGI_FEATURE_PRESENT_ALLOW_TEARING, -+ &allow_tearing, sizeof (allow_tearing)); -+ } -+ -+ if (SUCCEEDED (hr) && allow_tearing) { -+ swapchain_flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; -+ GST_INFO_OBJECT (self, "DXGI tearing/VRR presentation enabled"); -+ } else { -+ GST_WARNING_OBJECT (self, -+ "DXGI tearing/VRR presentation requested but unsupported"); -+ } -+ } -+ - { - DXGI_SWAP_CHAIN_DESC1 desc1 = { 0, }; - desc1.Width = 0; -@@ -1309,6 +1336,15 @@ - return GST_D3D11_WINDOW_FLOW_CLOSED; - } - -+ DXGI_SWAP_CHAIN_DESC swapchain_desc = { 0, }; -+ BOOL exclusive_fullscreen = FALSE; -+ if (SUCCEEDED (window->swap_chain->GetDesc (&swapchain_desc)) && -+ (swapchain_desc.Flags & DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING) && -+ (FAILED (window->swap_chain->GetFullscreenState ( -+ &exclusive_fullscreen, nullptr)) || !exclusive_fullscreen)) { -+ present_flags |= DXGI_PRESENT_ALLOW_TEARING; -+ } -+ - if (self->have_swapchain1) { - IDXGISwapChain1 *swap_chain1 = (IDXGISwapChain1 *) window->swap_chain; - DXGI_PRESENT_PARAMETERS present_params = { 0, }; diff --git a/native/opennow-streamer/Cargo.lock b/native/opennow-streamer/Cargo.lock index 83236c1bd..56da13163 100644 --- a/native/opennow-streamer/Cargo.lock +++ b/native/opennow-streamer/Cargo.lock @@ -2,41 +2,151 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] -name = "android_system_properties" -version = "0.1.5" +name = "apple-cryptokit-rs" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "508eed99695e0ddc94adb8dda623477e76bb8b5dcc9f59a01b43ce646f715343" dependencies = [ - "libc", + "serde", + "serde_json", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror", + "time", ] [[package]] -name = "anyhow" -version = "1.0.104" +name = "asn1-rs-derive" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "atomic_refcell" -version = "0.1.14" +name = "audiopus_sys" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" +checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651" +dependencies = [ + "cmake", + "log", + "pkg-config", +] [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "untrusted", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" @@ -44,32 +154,56 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" -version = "2.11.1" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] -name = "bitstream-io" -version = "4.10.0" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "no_std_io2", + "generic-array", ] [[package]] -name = "bumpalo" -version = "3.20.3" +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] [[package]] -name = "byte-slice-cast" -version = "1.2.3" +name = "bytemuck" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "bytes" @@ -79,22 +213,26 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.4.0" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] [[package]] -name = "cfg-expr" -version = "0.20.7" +name = "ccm" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" dependencies = [ - "smallvec", - "target-lexicon", + "aead", + "cipher", + "ctr", + "subtle", ] [[package]] @@ -105,575 +243,476 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", + "cipher", "cpufeatures", - "rand_core", ] [[package]] -name = "chrono" -version = "0.4.45" +name = "chacha20poly1305" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "iana-time-zone", - "num-traits", - "windows-link", + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] [[package]] -name = "cpufeatures" -version = "0.3.0" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "libc", + "cc", ] [[package]] -name = "deranged" -version = "0.5.8" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] [[package]] -name = "either" -version = "1.15.0" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "equivalent" -version = "1.0.2" +name = "core-foundation" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "futures" -version = "0.3.32" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "libc", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "crc" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ - "futures-core", - "futures-sink", + "crc-catalog", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "crc-catalog" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] -name = "futures-executor" -version = "0.3.32" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "cfg-if", ] [[package]] -name = "futures-io" -version = "0.3.33" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "proc-macro2", - "quote", - "syn", + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", ] [[package]] -name = "futures-sink" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" - -[[package]] -name = "futures-task" -version = "0.3.32" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] [[package]] -name = "futures-util" -version = "0.3.32" +name = "ctr" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", + "cipher", ] [[package]] -name = "getrandom" -version = "0.4.3" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "libc", - "r-efi", - "rand_core", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "gio" -version = "0.22.8" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "pin-project-lite", - "smallvec", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "gio-sys" -version = "0.22.0" +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys", + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "glib" -version = "0.22.5" +name = "der-parser" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b7df55594e0e787d1560e23f7e12d7360d0b22e7b7c228ec2488b9e59b1b6b" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", ] [[package]] -name = "glib-macros" -version = "0.22.2" +name = "der_derive" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda575994e3689b1bc12f89c3df621ead46ff292623b76b4710a3a5b79be54bb" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ - "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "glib-sys" -version = "0.22.3" +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eb23a616a3dbc7fc15bbd26f58756ff0b04c8a894df3f0680cd21011db6a642" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "libc", - "system-deps", + "block-buffer", + "const-oid", + "crypto-common", + "subtle", ] [[package]] -name = "gobject-sys" -version = "0.22.0" +name = "dimpl" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18eda93f09d3778f38255b231b17ef67195013a592c91624a4daf8bead875565" +checksum = "ba6aa42b0c64c3e5311a2afad224b32db1ee129d21c63daaaf8ea747b846cdbc" dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gst-plugin-rtp" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a8636a5d8ab4d590e66c4cd103beac097d4c1f053f5723e47f141f5af67d0e" -dependencies = [ - "anyhow", - "atomic_refcell", - "bitstream-io", - "byte-slice-cast", - "chrono", - "futures", - "gio", - "glib", - "gst-plugin-version-helper", - "gstreamer", - "gstreamer-audio", - "gstreamer-base", - "gstreamer-net", - "gstreamer-rtp", - "gstreamer-video", - "hex", + "aes", + "aes-gcm", + "arrayvec", + "aws-lc-rs", + "ccm", + "chacha20", + "chacha20poly1305", + "der", + "ecdsa", + "generic-array", + "hkdf", + "hmac", "log", + "nom 8.0.0", + "once_cell", + "p256", + "p384", + "pkcs8", "rand", - "rtcp-types", - "rtp-types", - "slab", - "smallvec", - "thiserror 2.0.18", + "rand_core 0.6.4", + "rcgen", + "sec1", + "sha2", + "signature", + "spki", + "subtle", "time", - "tokio", - "tokio-util", + "x25519-dalek", + "x509-cert", ] [[package]] -name = "gst-plugin-version-helper" -version = "0.8.4" +name = "dispatch2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94668bc2592732b8c2b653668ae41211d45988fb61264888b9c2d545d4bd826d" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "chrono", - "toml_edit", + "bitflags 2.13.1", + "objc2", ] [[package]] -name = "gstreamer" -version = "0.25.3" +name = "displaydoc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4527e1b9bae8d29ce137bde5b8eec8ae8f78f13ad00fc6e70cbe227d6ad027" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ - "cfg-if", - "futures-channel", - "futures-core", - "futures-util", - "glib", - "gstreamer-sys", - "itertools", - "kstring", - "libc", - "muldiv", - "num-integer", - "num-rational", - "option-operations", - "pastey", - "pin-project-lite", - "smallvec", - "thiserror 2.0.18", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] -name = "gstreamer-audio" -version = "0.25.3" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f97532c3e21287a6e94563a328cce89367324acf8a4489291b40ede2e39163a0" -dependencies = [ - "cfg-if", - "glib", - "gstreamer", - "gstreamer-audio-sys", - "gstreamer-base", - "libc", - "smallvec", -] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "gstreamer-audio-sys" -version = "0.25.3" +name = "ecdsa" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2abfb81a073c5c8fb115a3639f47b6430f669ee9fa29123bb0116c9ef59d7d16" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "glib-sys", - "gobject-sys", - "gstreamer-base-sys", - "gstreamer-sys", - "libc", - "system-deps", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] -name = "gstreamer-base" -version = "0.25.0" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08353a8a382be9a49b15fb9c46b3abd6f8a6e6439e1eaedc87d08f1abdcfad1" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "atomic_refcell", - "cfg-if", - "glib", - "gstreamer", - "gstreamer-base-sys", - "libc", + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] -name = "gstreamer-base-sys" -version = "0.25.0" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6569606feeb89cfcf95a6476a64a0f0aec83fadcef0e91c24e576f7851ceac3a" -dependencies = [ - "glib-sys", - "gobject-sys", - "gstreamer-sys", - "libc", - "system-deps", -] +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "gstreamer-net" -version = "0.25.0" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abcad04d471a4f2c859ef1287f22581b07064e357cc89dfa415ffc99b2a3193d" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "gio", - "glib", - "gstreamer", - "gstreamer-net-sys", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "gstreamer-net-sys" -version = "0.25.0" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcefb342a98ffca0b106bc9fea13d5df15879e1613b4dc652a6ec1960c32584c" -dependencies = [ - "gio-sys", - "glib-sys", - "gstreamer-sys", - "libc", - "system-deps", -] +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "gstreamer-rtp" -version = "0.25.0" +name = "find-msvc-tools" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea01e76ad8fd2e688a4f8a166060c9f9ba13dd6355ad7e22dbb793325d14411" -dependencies = [ - "glib", - "gstreamer", - "gstreamer-rtp-sys", - "libc", -] +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] -name = "gstreamer-rtp-sys" -version = "0.25.0" +name = "flagset" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf8df32469d863ce4375a978233a40efec206c5256bf0c14ee42bb745d8a793" -dependencies = [ - "glib-sys", - "gstreamer-base-sys", - "gstreamer-sys", - "libc", - "system-deps", -] +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] -name = "gstreamer-sdp" -version = "0.25.2" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63c5dba39f65d0ed2cadcaa277fc905c30c9c7db37e024ad3bed7b0492042ad" -dependencies = [ - "glib", - "gstreamer", - "gstreamer-sdp-sys", -] +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "gstreamer-sdp-sys" -version = "0.25.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20f0eb41ecfbacbf6a29d9457e6de0f59e2638f47fbdb6a6c1bcfb720c2b9ee" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "glib-sys", - "gstreamer-sys", - "libc", - "system-deps", + "typenum", + "version_check", + "zeroize", ] [[package]] -name = "gstreamer-sys" -version = "0.25.0" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85d09343b4c23d64b3ef35f1f644598860cc9a4617e7ccded141de97cd528608" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "glib-sys", - "gobject-sys", "libc", - "system-deps", + "wasi", ] [[package]] -name = "gstreamer-video" -version = "0.25.2" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf728cb21499561ea0d6ce584e550bf2b98b279cc7741067ee42f41f98b486e" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "futures-channel", - "glib", - "gstreamer", - "gstreamer-base", - "gstreamer-video-sys", "libc", - "thiserror 2.0.18", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "gstreamer-video-sys" -version = "0.25.0" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f82631a5063057c10583a57ba0fbce689e67122cfdb5eddbeaa43eb812e75" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "glib-sys", - "gobject-sys", - "gstreamer-base-sys", - "gstreamer-sys", + "cfg-if", "libc", - "system-deps", + "r-efi 6.0.0", ] [[package]] -name = "gstreamer-webrtc" -version = "0.25.2" +name = "ghash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb47b71d463547b50ec8d0c69e175cb3f5cfc28f55c4cfd6735c9d368202a74" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ - "glib", - "gstreamer", - "gstreamer-sdp", - "gstreamer-webrtc-sys", - "libc", + "opaque-debug", + "polyval", ] [[package]] -name = "gstreamer-webrtc-sys" -version = "0.25.0" +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "405721d1f15bbda47a46f9645077859215a947cdd6af6c7f18e4f4d0cbe63bd7" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "glib-sys", - "gstreamer-sdp-sys", - "gstreamer-sys", - "libc", - "system-deps", + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "hkdf" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", + "hmac", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "cc", + "digest", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "inout" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "equivalent", - "hashbrown", + "generic-array", ] [[package]] -name = "itertools" -version = "0.15.0" +name = "is" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +checksum = "d08f9118d003d441f79c1070e84d0a2a89f35721ca8ae0cd5e1f9026dc4a2517" dependencies = [ - "either", + "crc", + "serde", + "str0m-proto", + "subtle", + "tracing", ] [[package]] @@ -683,24 +722,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "js-sys" -version = "0.3.103" +name = "jobserver" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", + "getrandom 0.4.3", + "libc", ] [[package]] -name = "kstring" -version = "2.0.2" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" -dependencies = [ - "static_assertions", -] +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -714,6 +749,15 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.8.0" @@ -721,89 +765,502 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "muldiv" -version = "1.0.1" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "no_std_io2" -version = "0.9.4" +name = "nasm-rs" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149" dependencies = [ - "memchr", + "log", ] [[package]] -name = "num-conv" -version = "0.2.2" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] [[package]] -name = "num-integer" -version = "0.1.46" +name = "nom" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" dependencies = [ - "num-traits", + "memchr", ] [[package]] -name = "num-rational" -version = "0.4.2" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "num-integer", - "num-traits", + "windows-sys 0.61.2", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num-bigint" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ - "autocfg", + "num-integer", + "num-traits", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.1", + "objc2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-video", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-video-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bf9a3c14831a7d9641b0d81d87dd913ee238a012b2fde27db5a84b56f5df3e" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openh264" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc9071a0b5f9c501ddd01066c2600d922cc799dc450a52975222bcda7755a08" +dependencies = [ + "openh264-sys2", + "wide", +] + +[[package]] +name = "openh264-sys2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ada29a4cadc13d4d326737af87c13e224210d7d99fe47594e9f3f9b0102fd70" +dependencies = [ + "cc", + "nasm-rs", + "walkdir", +] + [[package]] name = "opennow-streamer" -version = "0.1.0" +version = "0.2.0" +dependencies = [ + "opennow-streamer-core", + "opennow-streamer-platform", + "opennow-streamer-protocol", + "serde_json", + "tracing-subscriber", +] + +[[package]] +name = "opennow-streamer-core" +version = "0.2.0" dependencies = [ "base64", - "gst-plugin-rtp", - "gstreamer", - "gstreamer-sdp", - "gstreamer-video", - "gstreamer-webrtc", - "regex", + "opennow-streamer-platform", + "opennow-streamer-protocol", + "opennow-streamer-transport", + "serde_json", + "str0m", +] + +[[package]] +name = "opennow-streamer-platform" +version = "0.2.0" +dependencies = [ + "audiopus_sys", + "libc", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "openh264", + "opennow-streamer-platform-macos", + "opennow-streamer-protocol", + "opus", + "raw-window-handle", + "sdl2", + "windows-sys 0.61.2", + "x11-dl", +] + +[[package]] +name = "opennow-streamer-platform-macos" +version = "0.2.0" +dependencies = [ + "libc", + "objc2", + "objc2-app-kit", + "objc2-audio-toolbox", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation", + "objc2-metal", + "objc2-quartz-core", + "objc2-video-toolbox", + "opus", + "thiserror", +] + +[[package]] +name = "opennow-streamer-protocol" +version = "0.2.0" +dependencies = [ "serde", "serde_json", ] [[package]] -name = "option-operations" -version = "0.6.1" +name = "opennow-streamer-transport" +version = "0.2.0" +dependencies = [ + "aes", + "crc32fast", + "ctr", + "getrandom 0.3.4", + "ghash", + "hmac", + "opennow-streamer-protocol", + "serde_json", + "sha1", + "socket2", + "str0m", + "subtle", + "thiserror", +] + +[[package]] +name = "opus" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754" +checksum = "4d3809943dff6fbad5f0484449ea26bdb9cb7d8efdf26ed50d3c7f227f69eb5c" dependencies = [ - "pastey", + "audiopus_sys", ] [[package]] -name = "pastey" -version = "0.2.2" +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a797f0e07bdf071d15742978fc3128ec6c22891c31a3a931513263904c982a" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] [[package]] name = "pin-project-lite" @@ -811,11 +1268,44 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] [[package]] name = "powerfmt" @@ -823,6 +1313,24 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -841,6 +1349,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -849,38 +1363,66 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "chacha20", - "getrandom", - "rand_core", + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.10.1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] -name = "regex" -version = "1.12.3" +name = "rcgen" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "091e7a8e7d86e6feb87a27ce8e2cba29d49eff9507afeebefab7eeb2ca667fb4" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "aws-lc-rs", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", ] [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -889,34 +1431,153 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "safe_arch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42c6efa15875e6ecb39ca61fb0b0c1a40b84fac5a5ffe71eef7d1000c8eb3f5f" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sctp-proto" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f895c3c33ae20283f9129bd09db2ca69138798b372ef2b98bd2946d23ea4819b" +dependencies = [ + "bytes", + "crc", + "log", + "rand", + "rustc-hash", + "slab", + "thiserror", +] + +[[package]] +name = "sdl2" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "raw-window-handle", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" +dependencies = [ + "cfg-if", + "cmake", + "libc", + "version-compare", +] + +[[package]] +name = "sec1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] [[package]] -name = "rtcp-types" -version = "0.3.0" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c081c846edea632bb47332fada9d4ac2fdf54d84beaf547fc947b58489e5f619" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "thiserror 2.0.18", + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "rtp-types" -version = "0.1.2" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb90df8268abfe08452ef2dae9e867a54edfdaa71b3127ef47d8b031f77ac73" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ - "smallvec", - "thiserror 1.0.69", + "core-foundation-sys", + "libc", ] [[package]] -name = "rustversion" -version = "1.0.23" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -945,7 +1606,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -962,12 +1623,34 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "1.1.1" +name = "sha1" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ - "serde_core", + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", ] [[package]] @@ -976,6 +1659,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.12" @@ -984,15 +1677,123 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "str0m" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840d11fdd44459d71bc7b3dd6796f55e9ac86c8e3fa8678098a8642f5701c5ff" +dependencies = [ + "arrayvec", + "base64ct", + "combine", + "dimpl", + "fastrand", + "is", + "sctp-proto", + "serde", + "str0m-apple-crypto", + "str0m-proto", + "str0m-rust-crypto", + "str0m-wincrypto", + "subtle", + "time", + "tracing", +] + +[[package]] +name = "str0m-apple-crypto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0fe88675a99679ce154c87ee356eb133af5ade7657fb5fe2db97689b4c41eb" +dependencies = [ + "apple-cryptokit-rs", + "cc", + "core-foundation", + "der", + "dimpl", + "pkcs8", + "sec1", + "security-framework", + "spki", + "str0m-proto", + "subtle", + "time", + "x509-cert", +] + +[[package]] +name = "str0m-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f3d99f2cf6c76a502e45feb896ea71e54787ede8744bcdb215acfbfcb905dd" +dependencies = [ + "base64ct", + "dimpl", + "fastrand", + "serde", + "subtle", + "time", +] + +[[package]] +name = "str0m-rust-crypto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947c2417bf43e47504911f92f47c22b6125a57d162e1661f1c77b78ebb3aa9d3" +dependencies = [ + "aes", + "aes-gcm", + "ctr", + "dimpl", + "hmac", + "p256", + "sha1", + "sha2", + "str0m-proto", + "time", +] + +[[package]] +name = "str0m-wincrypto" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "2816a81106af3dfdc321b37da5148238759c900fda9e7f8c11ebe77f1b7a2b73" +dependencies = [ + "dimpl", + "str0m-proto", + "tracing", + "windows", +] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -1006,31 +1807,25 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "7.0.8" +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "thiserror" -version = "1.0.69" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "thiserror-impl 1.0.69", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1039,29 +1834,27 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "thread_local" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cfg-if", ] [[package]] @@ -1075,6 +1868,7 @@ dependencies = [ "powerfmt", "serde_core", "time-core", + "time-macros", ] [[package]] @@ -1084,77 +1878,81 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] -name = "tokio" -version = "1.53.1" +name = "time-macros" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ - "pin-project-lite", + "num-conv", + "time-core", ] [[package]] -name = "tokio-util" -version = "0.7.19" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "bytes", - "futures-core", - "futures-sink", "pin-project-lite", - "tokio", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" +name = "tracing-attributes" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "toml_writer", - "winnow", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ - "serde_core", + "once_cell", + "valuable", ] [[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", + "log", + "once_cell", + "tracing-core", ] [[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "winnow", + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" +name = "typenum" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -1162,55 +1960,103 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version-compare" -version = "0.2.1" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasm-bindgen" -version = "0.2.126" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "same-file", + "winapi-util", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" +name = "wide" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "de2aaf408e58689c2096682331b1f42bb2d9f2ed6b11560407d023cd0a6c634e" dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "bytemuck", + "safe_arch", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "unicode-ident", + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", ] [[package]] @@ -1226,6 +2072,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -1234,7 +2091,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1245,7 +2102,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1254,6 +2111,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -1272,6 +2139,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1282,12 +2158,184 @@ dependencies = [ ] [[package]] -name = "winnow" -version = "1.0.2" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "memchr", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec", + "time", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] diff --git a/native/opennow-streamer/Cargo.toml b/native/opennow-streamer/Cargo.toml index 229d7e2d8..9d2d8526f 100644 --- a/native/opennow-streamer/Cargo.toml +++ b/native/opennow-streamer/Cargo.toml @@ -1,29 +1,43 @@ -[package] -name = "opennow-streamer" -version = "0.1.0" -edition = "2021" -license = "MIT" - -[features] -default = [] -gstreamer = [ - "dep:gstreamer", - "dep:gstreamer-sdp", - "dep:gstreamer-video", - "dep:gstreamer-webrtc", - "dep:gst-plugin-rtp", - "gstreamer-webrtc/v1_22", +[workspace] +members = [ + "crates/opennow-streamer", + "crates/opennow-streamer-core", + "crates/opennow-streamer-platform", + "crates/opennow-streamer-platform-macos", + "crates/opennow-streamer-protocol", + "crates/opennow-streamer-transport", ] +default-members = ["crates/opennow-streamer"] +resolver = "2" + +[workspace.package] +version = "0.2.0" +edition = "2024" +license = "MIT" +rust-version = "1.85" -[dependencies] +[workspace.dependencies] +aes = "0.8" +aes-gcm = "0.10" +audiopus_sys = { version = "0.2.2", features = ["static"] } base64 = "0.22" -gstreamer = { version = "0.25.1", optional = true } -gstreamer-sdp = { version = "0.25.0", optional = true } -gstreamer-video = { version = "0.25.0", optional = true } -gstreamer-webrtc = { version = "0.25.0", optional = true } -regex = "1" +ctr = "0.9" +crc32fast = "1" +getrandom = "0.3" +ghash = "0.5" +hmac = "0.12" +openh264 = { version = "0.9.8", default-features = false, features = ["source"] } +opus = "0.3.1" +sdl2 = { version = "0.38.0", default-features = false, features = ["bundled", "raw-window-handle", "static-link", "unsafe_textures"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +sha1 = "0.10" +socket2 = { version = "0.5", features = ["all"] } +str0m = { version = "0.23", default-features = false } +subtle = "2" +thiserror = "2" -[target.'cfg(target_os = "linux")'.dependencies] -gst-plugin-rtp = { version = "0.15.3", optional = true } +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true diff --git a/native/opennow-streamer/README.md b/native/opennow-streamer/README.md index 43b66eb74..dac57b240 100644 --- a/native/opennow-streamer/README.md +++ b/native/opennow-streamer/README.md @@ -1,14 +1,51 @@ -# OpenNOW Native Streamer +# OpenNOW Native Streamer v2 -This crate contains OpenNOW's native Rust streaming infrastructure. +This workspace is the clean replacement for the former GStreamer-based native streamer. -> [!NOTE] -> Native streamer / native streaming is experimental. Issues can be platform-specific and users may see fallback to Chromium/WebRTC; report problems on [GitHub Issues](https://github.com/OpenCloudGaming/OpenNOW/issues) or [Discord](https://discord.gg/8EJYaJcNfD). +The workspace owns the local process protocol, lifecycle state machine, standards-based WebRTC transport, and media output path. The baseline backend links OpenH264, Opus, and SDL from source, so it does not require a GStreamer or FFmpeg runtime. -Canonical native streamer, WebRTC, GStreamer, packaging, and development documentation lives at [opennow.zortos.me](https://opennow.zortos.me). This README is intentionally only a pointer so repository docs do not drift from the site. +The executable retains the versioned JSON-lines process contract used by OpenNOW while the app shell is migrated away from Electron. It does not load or redistribute NVIDIA client libraries. -## Linux runtime requirements +## Crates -Linux native video is embedded into the Electron window through an X11 child surface and `GstVideoOverlay`. OpenNOW defaults its Linux Electron shell to X11, which also works on Wayland desktops through XWayland. An explicit pure-Wayland launch is rejected with a diagnostic instead of starting an unmanaged GStreamer window that can remain invisible behind Electron. +- `opennow-streamer-protocol`: versioned local IPC DTOs. +- `opennow-streamer-core`: session lifecycle and command routing. +- `opennow-streamer-transport`: ICE, DTLS-SRTP, RTP/RTCP, and SCTP data channels. +- `opennow-streamer-platform`: bounded media queues, OpenH264/Opus decode, SDL audio/video output, and platform-native Electron surface ownership. +- `opennow-streamer`: process entry point. -The distro GStreamer runtime must provide WebRTC, the selected codec parser/depayloader, a compatible X11 video sink, and at least one decoder. Hardware decode is preferred, but unavailable hardware plugins fall back to the native GStreamer software decoder. If a hardware decoder starts but produces no frames while audio and RTP remain active, OpenNOW reconnects the same native session once with software decoding. +## Checks + +```sh +cargo test --manifest-path native/opennow-streamer/Cargo.toml +cargo build --manifest-path native/opennow-streamer/Cargo.toml --release +``` + +The Electron app's build wrapper also checks protocol-version parity, copies the executable into the platform package directory, and runs a JSON-lines `hello`/`stop` process smoke test: + +```sh +npm --prefix opennow-stable run native:build +``` + +## Platform integration + +The SDL presentation window is created hidden on the process main thread. Windows and X11/XWayland reparent it as an input-transparent child of the Electron native window. macOS keeps it as a non-activating, mouse-ignoring child `NSWindow` and converts renderer-relative surface rectangles through the Electron `NSView`. The executable runs `MainThreadHost` on its real main thread; stdin, WebRTC, and codec work run on named workers. This is required by AppKit and is checked with `pthread_main_np()` on macOS. + +Wayland does not provide a portable foreign-surface parenting protocol, so native presentation requires Electron's X11/XWayland mode on Linux. Hardware decoder entries remain unavailable until their implementations are linked; the built software backend advertises H.264 only. + +## macOS validation + +Run the checks natively on each architecture rather than treating a Linux cross-check as macOS proof: + +```sh +rustup target add aarch64-apple-darwin x86_64-apple-darwin +OPENNOW_NATIVE_STREAMER_TARGET="$(rustc -vV | sed -n 's/^host: //p')" \ +OPENNOW_NATIVE_STREAMER_PLATFORM_KEY="darwin-$(uname -m | sed 's/arm64/arm64/;s/x86_64/x64/')" \ +npm --prefix opennow-stable run native:build +``` + +The CI package matrix runs this build on both Apple Silicon and Intel macOS runners before producing the DMG and ZIP. Release validation must additionally inspect the app bundle, verify its signature, launch the packaged child executable, and confirm fallback behavior on real hardware. + +## Runtime validation + +Unit tests synthesize and decode H.264 and Opus frames, verify drop-oldest queue behavior, and exercise pause/stop lifecycle. Release validation must additionally run an authorized live session on Windows, Linux/XWayland, Intel macOS, and Apple Silicon to validate WebRTC interoperability, A/V timing, Electron child-surface stacking, and device-specific audio output. diff --git a/native/opennow-streamer/bin/concrt140.dll b/native/opennow-streamer/bin/concrt140.dll deleted file mode 100644 index 830dfaeaf..000000000 Binary files a/native/opennow-streamer/bin/concrt140.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/gio-2.0-0.dll b/native/opennow-streamer/bin/gio-2.0-0.dll deleted file mode 100644 index 56235522e..000000000 Binary files a/native/opennow-streamer/bin/gio-2.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/glib-2.0-0.dll b/native/opennow-streamer/bin/glib-2.0-0.dll deleted file mode 100644 index a9e79cd53..000000000 Binary files a/native/opennow-streamer/bin/glib-2.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/gmodule-2.0-0.dll b/native/opennow-streamer/bin/gmodule-2.0-0.dll deleted file mode 100644 index f9712b7cd..000000000 Binary files a/native/opennow-streamer/bin/gmodule-2.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/gobject-2.0-0.dll b/native/opennow-streamer/bin/gobject-2.0-0.dll deleted file mode 100644 index 33f81a469..000000000 Binary files a/native/opennow-streamer/bin/gobject-2.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/gstreamer-1.0-0.dll b/native/opennow-streamer/bin/gstreamer-1.0-0.dll deleted file mode 100644 index 66afb1858..000000000 Binary files a/native/opennow-streamer/bin/gstreamer-1.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/gthread-2.0-0.dll b/native/opennow-streamer/bin/gthread-2.0-0.dll deleted file mode 100644 index e13a20a88..000000000 Binary files a/native/opennow-streamer/bin/gthread-2.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/intl-8.dll b/native/opennow-streamer/bin/intl-8.dll deleted file mode 100644 index d37f3a60f..000000000 Binary files a/native/opennow-streamer/bin/intl-8.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/msvcp140.dll b/native/opennow-streamer/bin/msvcp140.dll deleted file mode 100644 index 5153fb087..000000000 Binary files a/native/opennow-streamer/bin/msvcp140.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/msvcp140_1.dll b/native/opennow-streamer/bin/msvcp140_1.dll deleted file mode 100644 index fe6169ee5..000000000 Binary files a/native/opennow-streamer/bin/msvcp140_1.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/msvcp140_2.dll b/native/opennow-streamer/bin/msvcp140_2.dll deleted file mode 100644 index 967be8237..000000000 Binary files a/native/opennow-streamer/bin/msvcp140_2.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/msvcp140_atomic_wait.dll b/native/opennow-streamer/bin/msvcp140_atomic_wait.dll deleted file mode 100644 index 01f8e9a36..000000000 Binary files a/native/opennow-streamer/bin/msvcp140_atomic_wait.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/msvcp140_codecvt_ids.dll b/native/opennow-streamer/bin/msvcp140_codecvt_ids.dll deleted file mode 100644 index 63214b8a8..000000000 Binary files a/native/opennow-streamer/bin/msvcp140_codecvt_ids.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/orc-0.4-0.dll b/native/opennow-streamer/bin/orc-0.4-0.dll deleted file mode 100644 index 5270f9f69..000000000 Binary files a/native/opennow-streamer/bin/orc-0.4-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/pcre2-8-0.dll b/native/opennow-streamer/bin/pcre2-8-0.dll deleted file mode 100644 index 092953e23..000000000 Binary files a/native/opennow-streamer/bin/pcre2-8-0.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/vcruntime140.dll b/native/opennow-streamer/bin/vcruntime140.dll deleted file mode 100644 index c2f509d20..000000000 Binary files a/native/opennow-streamer/bin/vcruntime140.dll and /dev/null differ diff --git a/native/opennow-streamer/bin/vcruntime140_1.dll b/native/opennow-streamer/bin/vcruntime140_1.dll deleted file mode 100644 index 64cd24630..000000000 Binary files a/native/opennow-streamer/bin/vcruntime140_1.dll and /dev/null differ diff --git a/native/opennow-streamer/crates/opennow-streamer-core/Cargo.toml b/native/opennow-streamer/crates/opennow-streamer-core/Cargo.toml new file mode 100644 index 000000000..359127c2f --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-core/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "opennow-streamer-core" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +base64.workspace = true +opennow-streamer-platform = { path = "../opennow-streamer-platform" } +opennow-streamer-protocol = { path = "../opennow-streamer-protocol" } +opennow-streamer-transport = { path = "../opennow-streamer-transport" } +serde_json.workspace = true + +[dev-dependencies] +str0m.workspace = true diff --git a/native/opennow-streamer/crates/opennow-streamer-core/src/lib.rs b/native/opennow-streamer/crates/opennow-streamer-core/src/lib.rs new file mode 100644 index 000000000..3f68ca037 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-core/src/lib.rs @@ -0,0 +1,1795 @@ +use std::net::UdpSocket; +use std::sync::mpsc::{Receiver, Sender}; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use base64::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; +use opennow_streamer_platform::{ + EncodedFrame, MediaCodec, MediaFeedback, MediaRuntime, MediaSession, MediaSink, PushOutcome, + supports_audio_decode, supports_audio_output, video_backends, +}; +use opennow_streamer_protocol::{ + Capabilities, Command, PROTOCOL_VERSION, SessionContext, error, event, response, +}; +use opennow_streamer_transport::{ + NvstDropReason, NvstReceiveEvent, NvstReceiverState, NvstUdpReceiverSession, + PreferredVideoTransport, ReservedNvstBundle, TransportControl, TransportEvent, + TransportSession, negotiate, reserve_nvst_mjolnir_udp_socket, + select_preferred_video_transport, spawn_nvst_mjolnir_receiver, + spawn_nvst_udp_receiver_with_socket, +}; +use serde_json::{Value, json}; + +pub use opennow_streamer_transport::{EncodedMediaFrame, MediaConsumer}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum State { + Idle, + Prepared, + Negotiating, + Connected, +} + +const ENCODED_MEDIA_QUEUE_CAPACITY: usize = 8; + +pub struct Engine { + lifecycle: Arc>, + transport: Option, + nvst_transport: Option, + nvst_mjolnir_transport: Option, + reserved_nvst_bundle: Option, + nvst_hole_punch_socket: Option, + events: Sender, + media_consumer: Option, + media_runtime: Option, + media_session: Option, + media_worker: Option>, + media_feedback: Option>, + feedback_worker: Option>, +} + +#[derive(Debug)] +struct Lifecycle { + state: State, + context: Option, + generation: u64, +} + +impl Engine { + pub fn new(events: Sender) -> Self { + Self { + lifecycle: Arc::new(Mutex::new(Lifecycle { + state: State::Idle, + context: None, + generation: 0, + })), + transport: None, + nvst_transport: None, + nvst_mjolnir_transport: None, + reserved_nvst_bundle: None, + nvst_hole_punch_socket: None, + events, + media_consumer: None, + media_runtime: None, + media_session: None, + media_worker: None, + media_feedback: None, + feedback_worker: None, + } + } + + pub fn with_media_consumer(events: Sender, media_consumer: MediaConsumer) -> Self { + Self { + lifecycle: Arc::new(Mutex::new(Lifecycle { + state: State::Idle, + context: None, + generation: 0, + })), + transport: None, + nvst_transport: None, + nvst_mjolnir_transport: None, + reserved_nvst_bundle: None, + nvst_hole_punch_socket: None, + events, + media_consumer: Some(media_consumer), + media_runtime: None, + media_session: None, + media_worker: None, + media_feedback: None, + feedback_worker: None, + } + } + + pub fn with_media_runtime(events: Sender, media_runtime: MediaRuntime) -> Self { + Self { + lifecycle: Arc::new(Mutex::new(Lifecycle { + state: State::Idle, + context: None, + generation: 0, + })), + transport: None, + nvst_transport: None, + nvst_mjolnir_transport: None, + reserved_nvst_bundle: None, + nvst_hole_punch_socket: None, + events, + media_consumer: None, + media_runtime: Some(media_runtime), + media_session: None, + media_worker: None, + media_feedback: None, + feedback_worker: None, + } + } + + pub fn handle(&mut self, command: Command) -> (Vec, bool) { + let id = command.id.clone(); + let result = match command.kind.as_str() { + "hello" => self.hello(&command), + "nvst-bind" => self.nvst_bind(command), + "nvst-send" => self.nvst_send(command), + "start" => self.start(command), + "offer" => self.offer(command), + "remote-ice" => self.remote_ice(command), + "input" => self.input(command), + "input-paused" => self.set_paused(command), + "surface" => self.update_surface(command), + "bitrate" | "update-shortcuts" => Err(error( + Some(&id), + "unsupported-command", + format!( + "Native streamer v2 cannot apply the {} command", + command.kind + ), + )), + "stop" => { + self.stop(command.reason.as_deref().unwrap_or("stopped")); + Ok(vec![response(id, "ok")]) + } + other => Err(error( + Some(&id), + "unknown-command", + format!("Unknown command: {other}"), + )), + }; + + match result { + Ok(values) => (values, true), + Err(value) => (vec![value], true), + } + } + + fn hello(&self, command: &Command) -> Result, Value> { + if command.protocol_version != Some(PROTOCOL_VERSION) { + return Err(error( + Some(&command.id), + "protocol-version-mismatch", + format!("Native streamer v2 requires protocol {PROTOCOL_VERSION}"), + )); + } + let backends = video_backends(); + let media_ready = self.media_runtime.is_some(); + let video_ready = media_ready && backends.iter().any(|backend| backend.available); + let capabilities = Capabilities { + protocol_version: PROTOCOL_VERSION, + backend: "native", + fallback_reason: (!media_ready) + .then_some("Native streamer v2 requires an in-process decoded media runtime"), + supports_offer_answer: media_ready, + supports_remote_ice: media_ready, + supports_local_ice: media_ready, + supports_input: media_ready, + supports_video_decode: video_ready, + supports_video_present: video_ready, + supports_audio_decode: media_ready && supports_audio_decode(), + supports_audio_output: media_ready && supports_audio_output(), + video_backends: backends, + }; + Ok(vec![json!({ + "id": command.id, + "type": "ready", + "capabilities": capabilities, + })]) + } + + fn nvst_bind(&mut self, command: Command) -> Result, Value> { + if self.reserved_nvst_bundle.is_none() { + let bundle = ReservedNvstBundle::reserve().map_err(|bind_error| { + error( + Some(&command.id), + "nvst-bind-failed", + format!("failed to reserve NVST UDP socket: {bind_error}"), + ) + })?; + eprintln!( + "NVST reserved video UDP socket on {} (Mjolnir on {})", + bundle + .local_addr() + .map(|addr| addr.to_string()) + .unwrap_or_else(|_| "unknown".to_owned()), + bundle + .mjolnir_local_addr() + .map(|addr| addr.to_string()) + .unwrap_or_else(|_| "unknown".to_owned()), + ); + self.reserved_nvst_bundle = Some(bundle); + } + let bundle = self.reserved_nvst_bundle.as_mut().ok_or_else(|| { + error( + Some(&command.id), + "nvst-bind-failed", + "reserved NVST UDP socket has no local port", + ) + })?; + let local_addr = bundle.local_addr().map_err(|_| { + error( + Some(&command.id), + "nvst-bind-failed", + "reserved NVST UDP socket has no local port", + ) + })?; + let mjolnir_addr = bundle.mjolnir_local_addr().map_err(|_| { + error( + Some(&command.id), + "nvst-bind-failed", + "reserved NVST Mjolnir UDP socket has no local port", + ) + })?; + let port = local_addr.port(); + let local_address = bundle.advertised_local_address(); + let identity = bundle.identity(); + Ok(vec![json!({ + "id": command.id, + "type": "nvst-bound", + "port": port, + "mjolnirPort": mjolnir_addr.port(), + "localAddress": local_address, + "iceUsernameFragment": identity.ice_username_fragment, + "icePassword": identity.ice_password, + "dtlsFingerprint": identity.dtls_fingerprint, + })]) + } + + fn nvst_send(&mut self, command: Command) -> Result, Value> { + let host = command.host.ok_or_else(|| { + error( + Some(&command.id), + "nvst-send-failed", + "nvst-send requires host", + ) + })?; + let port = command.port.ok_or_else(|| { + error( + Some(&command.id), + "nvst-send-failed", + "nvst-send requires port", + ) + })?; + let payload = BASE64 + .decode(command.payload_base64.unwrap_or_default()) + .map_err(|decode_error| { + error( + Some(&command.id), + "nvst-send-failed", + format!("nvst-send payload is not valid base64: {decode_error}"), + ) + })?; + let send_result = if let Some(bundle) = self.reserved_nvst_bundle.as_ref() { + bundle.send_to(&payload, host.as_str(), port) + } else if let Some(socket) = self.nvst_hole_punch_socket.as_ref() { + socket.send_to(&payload, (host.as_str(), port)) + } else { + return Err(error( + Some(&command.id), + "nvst-send-failed", + "NVST UDP socket has not been reserved", + )); + }; + send_result.map_err(|send_error| { + error( + Some(&command.id), + "nvst-send-failed", + format!("failed to send NVST UDP datagram: {send_error}"), + ) + })?; + Ok(vec![response(command.id, "ok")]) + } + + fn start(&mut self, command: Command) -> Result, Value> { + let context = parse_context(command.context, &command.id)?; + validate_context(&context, &command.id)?; + { + let lifecycle = lock_lifecycle(&self.lifecycle); + if lifecycle.state != State::Idle { + return Err(invalid_state(&command.id, "start", lifecycle.state, "Idle")); + } + } + if self.media_runtime.is_some() + && context + .settings + .get("codec") + .and_then(Value::as_str) + .is_some_and(|codec| !codec.eq_ignore_ascii_case("h264")) + { + return Err(error( + Some(&command.id), + "unsupported-video-codec", + "Native streamer v2 was built with H.264 decode only", + )); + } + let transport_context = serde_json::to_value(&context).map_err(|context_error| { + error( + Some(&command.id), + "invalid-context", + format!("Session context is not serializable: {context_error}"), + ) + })?; + let (nvst_config, fallback_note) = + match select_preferred_video_transport(&transport_context) { + PreferredVideoTransport::Nvst(config) => (Some(config), None), + PreferredVideoTransport::WebRtcFallback(reason) => ( + None, + Some(format!( + "NVST unavailable; using WebRTC fallback: {reason:?}" + )), + ), + }; + + if let Some(transport) = self.transport.take() { + transport.stop(); + } + if let Some(transport) = self.nvst_transport.take() { + transport.stop(); + } + if let Some(transport) = self.nvst_mjolnir_transport.take() { + transport.stop(); + } + self.stop_media_resources(); + if let Some(runtime) = self.media_runtime.clone() { + let (feedback_sender, feedback_receiver) = std::sync::mpsc::channel(); + let session = runtime + .start(feedback_sender) + .map_err(|message| error(Some(&command.id), "media-output-unavailable", message))?; + let sink = session.sink(); + let (media_consumer, media_receiver) = + std::sync::mpsc::sync_channel(ENCODED_MEDIA_QUEUE_CAPACITY); + let output = self.events.clone(); + let media_worker = match thread::Builder::new() + .name("opennow-media-consumer".to_owned()) + .spawn(move || consume_encoded_media(&output, media_receiver, sink)) + { + Ok(worker) => worker, + Err(spawn_error) => { + session.stop(); + return Err(error( + Some(&command.id), + "media-worker-failed", + spawn_error.to_string(), + )); + } + }; + self.media_consumer = Some(media_consumer); + self.media_session = Some(session); + self.media_worker = Some(media_worker); + self.media_feedback = Some(feedback_receiver); + } + + let mut nvst_events = None; + if let Some(config) = nvst_config { + let Some(media_consumer) = self.media_consumer.clone() else { + self.stop_media_resources(); + return Err(error( + Some(&command.id), + "media-consumer-unavailable", + "NVST video requires an in-process encoded media consumer", + )); + }; + let (event_sender, event_receiver) = std::sync::mpsc::channel(); + let (reserved_socket, reserved_rtc, reserved_mjolnir) = + match self.reserved_nvst_bundle.take() { + Some(bundle) => { + self.nvst_hole_punch_socket = bundle.try_clone_socket().ok(); + let (socket, rtc, mjolnir_socket) = bundle.into_parts(); + (Some(socket), Some(rtc), Some(mjolnir_socket)) + } + None => (None, None, None), + }; + let mjolnir_udp_port = config.mjolnir_udp_port(); + let transport = spawn_nvst_udp_receiver_with_socket( + config.clone(), + media_consumer.clone(), + event_sender.clone(), + reserved_socket, + reserved_rtc, + ) + .map_err(|transport_error| { + self.stop_media_resources(); + error( + Some(&command.id), + "nvst-start-failed", + transport_error.to_string(), + ) + })?; + self.nvst_transport = Some(transport); + if let Some(expected_port) = mjolnir_udp_port { + // Official two-socket model: video RTP/SRTP arrives on the + // dedicated NATT-only Mjolnir socket, not on the ICE/DTLS bundle. + let mjolnir_socket = match reserved_mjolnir { + Some(socket) => { + let actual_port = + socket.local_addr().map(|addr| addr.port()).unwrap_or(0); + if actual_port != expected_port { + eprintln!( + "NVST Mjolnir socket port mismatch: reserved {actual_port}, handoff expects {expected_port}; NATT keepalive determines routing" + ); + } + socket + } + None => { + eprintln!( + "NVST Mjolnir reservation missing at start; binding a fresh video UDP socket" + ); + reserve_nvst_mjolnir_udp_socket().map_err(|bind_error| { + if let Some(transport) = self.nvst_transport.take() { + transport.stop(); + } + self.stop_media_resources(); + error( + Some(&command.id), + "nvst-start-failed", + format!("failed to reserve NVST Mjolnir UDP socket: {bind_error}"), + ) + })? + } + }; + let mjolnir = spawn_nvst_mjolnir_receiver( + mjolnir_socket, + config, + media_consumer, + event_sender, + ) + .map_err(|mjolnir_error| { + if let Some(transport) = self.nvst_transport.take() { + transport.stop(); + } + self.stop_media_resources(); + error( + Some(&command.id), + "nvst-start-failed", + mjolnir_error.to_string(), + ) + })?; + self.nvst_mjolnir_transport = Some(mjolnir); + } + nvst_events = Some(event_receiver); + } else { + self.reserved_nvst_bundle = None; + self.nvst_hole_punch_socket = None; + } + + let generation = { + let mut lifecycle = lock_lifecycle(&self.lifecycle); + lifecycle.generation = lifecycle.generation.wrapping_add(1); + lifecycle.context = Some(context); + lifecycle.state = if nvst_events.is_some() { + State::Connected + } else { + State::Prepared + }; + lifecycle.generation + }; + if let Some(nvst_events) = nvst_events { + let output = self.events.clone(); + let lifecycle = self.lifecycle.clone(); + let media_feedback = self.media_feedback.take(); + self.feedback_worker = thread::Builder::new() + .name("opennow-nvst-events".to_owned()) + .spawn(move || { + forward_nvst_session_events( + &output, + &lifecycle, + generation, + nvst_events, + media_feedback, + ); + }) + .ok(); + if self.feedback_worker.is_none() { + if let Some(transport) = self.nvst_transport.take() { + transport.stop(); + } + if let Some(transport) = self.nvst_mjolnir_transport.take() { + transport.stop(); + } + self.stop_media_resources(); + let mut lifecycle = lock_lifecycle(&self.lifecycle); + if lifecycle.generation == generation { + lifecycle.context = None; + lifecycle.state = State::Idle; + } + return Err(error( + Some(&command.id), + "media-worker-failed", + "Failed to start NVST lifecycle worker", + )); + } + } + let _ = self.events.send(event( + "status", + json!({ + "status": "ready", + "message": if self.nvst_transport.is_some() { + "NVST authenticated H.264 receive path initialized" + } else if self.media_runtime.is_some() { + "H.264 video and Opus audio media path initialized" + } else { + "Native WebRTC session prepared" + } + }), + )); + if let Some(note) = fallback_note { + let _ = self + .events + .send(event("log", json!({ "level": "debug", "message": note }))); + } + let mut start_response = response(command.id, "ok"); + start_response["transport"] = Value::String( + if self.nvst_transport.is_some() { + "nvst" + } else { + "webrtc" + } + .to_owned(), + ); + Ok(vec![start_response]) + } + + fn offer(&mut self, command: Command) -> Result, Value> { + if self.nvst_transport.is_some() { + return Err(error( + Some(&command.id), + "nvst-video-active", + "NVST video is active; do not negotiate a WebRTC media offer for this session", + )); + } + let offer_sdp = command.sdp.as_deref().ok_or_else(|| { + error( + Some(&command.id), + "missing-sdp", + "Offer command does not include SDP", + ) + })?; + let offered_context = command + .context + .map(|context| parse_context(Some(context), &command.id)) + .transpose()?; + if let Some(context) = &offered_context { + validate_context(context, &command.id)?; + } + let (context, generation) = { + let mut lifecycle = lock_lifecycle(&self.lifecycle); + if lifecycle.state == State::Idle { + return Err(error( + Some(&command.id), + "not-started", + "Start must be sent before offer", + )); + } + if lifecycle.state != State::Prepared { + return Err(invalid_state( + &command.id, + "offer", + lifecycle.state, + "Prepared", + )); + } + let Some(stored_context) = lifecycle.context.as_ref() else { + lifecycle.state = State::Idle; + return Err(error( + Some(&command.id), + "invalid-state", + "Prepared lifecycle is missing its session context", + )); + }; + let stored_session_id = stored_context.session.session_id.clone(); + if let Some(context) = offered_context { + if context.session.session_id != stored_session_id { + return Err(error( + Some(&command.id), + "session-mismatch", + "Offer context does not match the prepared session", + )); + } + lifecycle.context = Some(context); + } + let Some(context) = lifecycle.context.clone() else { + lifecycle.state = State::Idle; + return Err(error( + Some(&command.id), + "invalid-state", + "Prepared lifecycle is missing its session context", + )); + }; + lifecycle.state = State::Negotiating; + (context, lifecycle.generation) + }; + let Some(media_consumer) = self.media_consumer.clone() else { + let mut lifecycle = lock_lifecycle(&self.lifecycle); + if lifecycle.generation == generation && lifecycle.state == State::Negotiating { + lifecycle.state = State::Prepared; + } + return Err(error( + Some(&command.id), + "media-consumer-unavailable", + "No in-process encoded media consumer is configured", + )); + }; + let threshold = partial_reliable_threshold(offer_sdp).unwrap_or(300); + let (transport_events, receiver) = std::sync::mpsc::channel(); + let negotiated = negotiate( + offer_sdp, + &context.session, + threshold, + transport_events, + media_consumer, + ) + .map_err(|transport_error| { + let mut lifecycle = lock_lifecycle(&self.lifecycle); + if lifecycle.generation == generation && lifecycle.state == State::Negotiating { + lifecycle.state = State::Prepared; + } + error( + Some(&command.id), + transport_error.code(), + transport_error.to_string(), + ) + })?; + let output = self.events.clone(); + let lifecycle = self.lifecycle.clone(); + let transport_control = negotiated.session.control(); + let media_feedback = self.media_feedback.take(); + let feedback_worker = thread::Builder::new() + .name("opennow-media-events".to_owned()) + .spawn(move || { + forward_session_events( + &output, + &lifecycle, + generation, + receiver, + media_feedback, + transport_control, + ); + }); + self.feedback_worker = Some(match feedback_worker { + Ok(worker) => worker, + Err(spawn_error) => { + negotiated.session.stop(); + let mut lifecycle = lock_lifecycle(&self.lifecycle); + if lifecycle.generation == generation && lifecycle.state == State::Negotiating { + lifecycle.state = State::Prepared; + } + drop(lifecycle); + self.stop_media_resources(); + return Err(error( + Some(&command.id), + "media-worker-failed", + spawn_error.to_string(), + )); + } + }); + self.transport = Some(negotiated.session); + let _ = self.events.send(event( + "local-ice", + json!({ "candidate": negotiated.local_candidate }), + )); + Ok(vec![json!({ + "id": command.id, + "type": "answer", + "answer": { "sdp": negotiated.answer_sdp }, + })]) + } + + fn remote_ice(&self, command: Command) -> Result, Value> { + if self.nvst_transport.is_some() { + return Err(error( + Some(&command.id), + "nvst-input-unsupported", + "NVST video does not implement the WebRTC input/control channel", + )); + } + let state = lock_lifecycle(&self.lifecycle).state; + if !matches!(state, State::Negotiating | State::Connected) { + return Err(invalid_state( + &command.id, + "remote-ice", + state, + "Negotiating or Connected", + )); + } + let transport = self.transport.as_ref().ok_or_else(|| { + error( + Some(&command.id), + "transport-not-ready", + "No active WebRTC transport", + ) + })?; + let candidate = command.candidate.as_ref().ok_or_else(|| { + error( + Some(&command.id), + "missing-candidate", + "Remote ICE command is empty", + ) + })?; + transport + .add_remote_candidate(candidate) + .map_err(|transport_error| { + error( + Some(&command.id), + transport_error.code(), + transport_error.to_string(), + ) + })?; + Ok(vec![response(command.id, "ok")]) + } + + fn input(&self, command: Command) -> Result, Value> { + if self.nvst_transport.is_some() { + return Err(error( + Some(&command.id), + "nvst-input-unsupported", + "NVST video input is not implemented", + )); + } + let state = lock_lifecycle(&self.lifecycle).state; + if state != State::Connected { + return Err(invalid_state( + &command.id, + "input", + state, + "Connected with an initialized input channel", + )); + } + let transport = self.transport.as_ref().ok_or_else(|| { + error( + Some(&command.id), + "transport-not-ready", + "No active WebRTC transport", + ) + })?; + let input = command + .input + .as_ref() + .ok_or_else(|| error(Some(&command.id), "missing-input", "Input command is empty"))?; + let bytes = BASE64 + .decode(&input.payload_base64) + .map_err(|decode_error| { + error(Some(&command.id), "invalid-input", decode_error.to_string()) + })?; + transport + .send_input(bytes, input.partially_reliable) + .map_err(|transport_error| { + error( + Some(&command.id), + transport_error.code(), + transport_error.to_string(), + ) + })?; + Ok(Vec::new()) + } + + fn set_paused(&self, command: Command) -> Result, Value> { + let Some(runtime) = self.media_runtime.as_ref() else { + return Err(error( + Some(&command.id), + "unsupported-command", + "Native streamer has no media runtime for input-paused", + )); + }; + let paused = command.paused.ok_or_else(|| { + error( + Some(&command.id), + "missing-paused", + "Pause command does not include paused state", + ) + })?; + runtime + .set_paused(paused) + .map_err(|message| error(Some(&command.id), "media-host-unavailable", message))?; + if let Some(media) = self.media_session.as_ref() { + media.set_paused(paused); + } + if let Some(transport) = self.nvst_transport.as_ref() { + let result = if paused { + transport.pause() + } else { + transport.resume() + }; + result.map_err(|transport_error| { + error( + Some(&command.id), + "nvst-control-failed", + transport_error.to_string(), + ) + })?; + } + if let Some(transport) = self.nvst_mjolnir_transport.as_ref() { + let result = if paused { + transport.pause() + } else { + transport.resume() + }; + result.map_err(|transport_error| { + error( + Some(&command.id), + "nvst-control-failed", + transport_error.to_string(), + ) + })?; + } + Ok(vec![response(command.id, "ok")]) + } + + fn update_surface(&self, command: Command) -> Result, Value> { + let Some(runtime) = self.media_runtime.as_ref() else { + return Err(error( + Some(&command.id), + "unsupported-command", + "Native streamer has no media runtime for surface", + )); + }; + let surface = command.surface.ok_or_else(|| { + error( + Some(&command.id), + "missing-surface", + "Surface command does not include a render surface", + ) + })?; + runtime + .update_surface(surface) + .map_err(|message| error(Some(&command.id), "media-host-unavailable", message))?; + Ok(vec![response(command.id, "ok")]) + } + + fn stop(&mut self, reason: &str) { + let was_active = { + let mut lifecycle = lock_lifecycle(&self.lifecycle); + let was_active = lifecycle.state != State::Idle; + lifecycle.generation = lifecycle.generation.wrapping_add(1); + lifecycle.context = None; + lifecycle.state = State::Idle; + was_active + }; + if let Some(transport) = self.transport.take() { + transport.stop(); + } + if let Some(transport) = self.nvst_transport.take() { + transport.stop(); + } + if let Some(transport) = self.nvst_mjolnir_transport.take() { + transport.stop(); + } + self.reserved_nvst_bundle = None; + self.nvst_hole_punch_socket = None; + self.stop_media_resources(); + if was_active { + let _ = self.events.send(event( + "status", + json!({ "status": "stopped", "message": reason }), + )); + } + } + + fn stop_media_resources(&mut self) { + if self.media_runtime.is_some() { + self.media_consumer = None; + if let Some(session) = self.media_session.take() { + session.stop(); + } + if let Some(worker) = self.media_worker.take() { + let _ = worker.join(); + } + self.media_feedback = None; + } + if let Some(worker) = self.feedback_worker.take() { + let _ = worker.join(); + } + } +} + +impl Drop for Engine { + fn drop(&mut self) { + self.stop("process closed"); + } +} + +fn partial_reliable_threshold(sdp: &str) -> Option { + sdp.lines().find_map(|line| { + line.trim() + .strip_prefix("a=ri.partialReliableThresholdMs:") + .and_then(|value| value.trim().parse().ok()) + }) +} + +fn parse_context(context: Option, id: &str) -> Result { + let context = context.ok_or_else(|| { + error( + Some(id), + "missing-context", + "Command requires session context", + ) + })?; + serde_json::from_value(context).map_err(|context_error| { + error( + Some(id), + "invalid-context", + format!("Invalid session context: {context_error}"), + ) + }) +} + +fn validate_context(context: &SessionContext, id: &str) -> Result<(), Value> { + if context.session.session_id.trim().is_empty() { + return Err(error( + Some(id), + "invalid-context", + "Session context requires a non-empty sessionId", + )); + } + if context.session.server_ip.trim().is_empty() { + return Err(error( + Some(id), + "invalid-context", + "Session context requires a non-empty serverIp endpoint", + )); + } + if !context.settings.is_object() || !context.shortcuts.is_object() { + return Err(error( + Some(id), + "invalid-context", + "Session context settings and shortcuts must be objects", + )); + } + if context + .session + .ice_servers + .iter() + .any(|server| server.urls.is_empty() || server.urls.iter().any(|url| url.trim().is_empty())) + { + return Err(error( + Some(id), + "invalid-context", + "Every ICE server requires at least one non-empty URL", + )); + } + if let Some(endpoint) = &context.session.media_connection_info { + if endpoint.ip.trim().is_empty() || endpoint.port == 0 || endpoint.port > u16::MAX.into() { + return Err(error( + Some(id), + "invalid-context", + "mediaConnectionInfo requires a hostname and a port in 1..=65535", + )); + } + } + if context + .session + .connection_info + .as_ref() + .is_some_and(|connections| { + connections.iter().any(|connection| { + connection.port == 0 + || connection.port > u16::MAX.into() + || connection + .ip + .as_ref() + .is_some_and(|ip| ip.trim().is_empty()) + }) + }) + { + return Err(error( + Some(id), + "invalid-context", + "connectionInfo requires ports in 1..=65535 and non-empty hostnames when present", + )); + } + serde_json::to_value(context).map_err(|context_error| { + error( + Some(id), + "invalid-context", + format!("Session context is not serializable: {context_error}"), + ) + })?; + Ok(()) +} + +fn invalid_state(id: &str, command: &str, state: State, required: &str) -> Value { + error( + Some(id), + "invalid-state", + format!("Cannot apply {command} while lifecycle is {state:?}; required state: {required}"), + ) +} + +fn lock_lifecycle(lifecycle: &Mutex) -> MutexGuard<'_, Lifecycle> { + lifecycle + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +fn forward_transport_event( + output: &Sender, + lifecycle: &Mutex, + generation: u64, + transport_event: TransportEvent, +) { + { + let mut lifecycle = lock_lifecycle(lifecycle); + if lifecycle.generation != generation { + return; + } + match &transport_event { + TransportEvent::Connected => lifecycle.state = State::Connected, + TransportEvent::Disconnected(_) => { + lifecycle.context = None; + lifecycle.state = State::Idle; + } + _ => {} + } + } + let value = match transport_event { + TransportEvent::Connected => event( + "status", + json!({ "status": "streaming", "message": "ICE, DTLS-SRTP, and RTP connected" }), + ), + TransportEvent::Disconnected(message) => { + event("status", json!({ "status": "stopped", "message": message })) + } + TransportEvent::InputReady(protocol_version) => event( + "input-ready", + json!({ "protocolVersion": protocol_version }), + ), + TransportEvent::Log(message) => { + event("log", json!({ "level": "warn", "message": message })) + } + }; + let _ = output.send(value); +} + +fn forward_nvst_session_events( + output: &Sender, + lifecycle: &Mutex, + generation: u64, + nvst_events: Receiver, + media_feedback: Option>, +) { + let mut dropped = 0; + let mut last_drop_report = Instant::now(); + loop { + if let Some(feedback) = media_feedback.as_ref() { + while let Ok(feedback) = feedback.try_recv() { + forward_nvst_media_feedback( + output, + lifecycle, + generation, + feedback, + &mut dropped, + &mut last_drop_report, + ); + } + } + match nvst_events.recv_timeout(Duration::from_millis(5)) { + Ok(nvst_event) => { + let terminal = forward_nvst_event(output, lifecycle, generation, nvst_event); + if terminal { + return; + } + } + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return, + } + } +} + +fn forward_nvst_event( + output: &Sender, + lifecycle: &Mutex, + generation: u64, + nvst_event: NvstReceiveEvent, +) -> bool { + let terminal = matches!( + &nvst_event, + NvstReceiveEvent::Lifecycle(NvstReceiverState::Stopped) + | NvstReceiveEvent::Dropped(NvstDropReason::MediaConsumerBackpressured) + | NvstReceiveEvent::Dropped(NvstDropReason::MediaConsumerClosed) + ); + { + let mut lifecycle = lock_lifecycle(lifecycle); + if lifecycle.generation != generation { + return true; + } + match &nvst_event { + NvstReceiveEvent::Lifecycle(NvstReceiverState::Running) => { + lifecycle.state = State::Connected; + } + NvstReceiveEvent::Lifecycle(NvstReceiverState::Stopped) + | NvstReceiveEvent::Dropped(NvstDropReason::MediaConsumerBackpressured) + | NvstReceiveEvent::Dropped(NvstDropReason::MediaConsumerClosed) => { + lifecycle.context = None; + lifecycle.state = State::Idle; + } + _ => {} + } + } + let value = match nvst_event { + NvstReceiveEvent::Lifecycle(NvstReceiverState::Running) => event( + "status", + json!({ "status": "streaming", "message": "NVST SRTP video receiver is running" }), + ), + NvstReceiveEvent::Lifecycle(NvstReceiverState::Paused) => event( + "status", + json!({ "status": "paused", "message": "NVST SRTP video receiver is paused" }), + ), + NvstReceiveEvent::Lifecycle(NvstReceiverState::RecoveryRequired) => event( + "error", + json!({ + "code": "nvst-recovery-required", + "message": "NVST receiver needs an explicit recovery after a timeout" + }), + ), + NvstReceiveEvent::Lifecycle(NvstReceiverState::Stopped) => event( + "status", + json!({ "status": "stopped", "message": "NVST receiver stopped" }), + ), + NvstReceiveEvent::RecoveryNeeded(recovery) => event( + "error", + json!({ + "code": "nvst-recovery-required", + "message": format!("NVST receive recovery required: {recovery:?}") + }), + ), + NvstReceiveEvent::Dropped(NvstDropReason::MediaConsumerBackpressured) => event( + "error", + json!({ + "code": "media-consumer-backpressured", + "message": "NVST receiver stopped because the decoded media path is backpressured" + }), + ), + NvstReceiveEvent::Dropped(NvstDropReason::MediaConsumerClosed) => event( + "error", + json!({ + "code": "media-consumer-closed", + "message": "NVST receiver stopped because the decoded media path closed" + }), + ), + NvstReceiveEvent::Dropped(reason) => event( + "log", + json!({ "level": "debug", "message": format!("Dropped NVST datagram: {reason:?}") }), + ), + NvstReceiveEvent::Frame(_) => return terminal, + }; + let _ = output.send(value); + terminal +} + +fn forward_nvst_media_feedback( + output: &Sender, + lifecycle: &Mutex, + generation: u64, + feedback: MediaFeedback, + dropped: &mut usize, + last_drop_report: &mut Instant, +) { + if lock_lifecycle(lifecycle).generation != generation { + return; + } + match feedback { + MediaFeedback::PlaybackStarted { backend } => { + let _ = output.send(event( + "log", + json!({ + "level": "info", + "message": format!("{backend} presented the first video frame") + }), + )); + } + MediaFeedback::BackendFallback { from, to, reason } => { + let _ = output.send(event( + "log", + json!({ + "level": "warn", + "message": format!("{from} startup failed; using {to}: {reason}") + }), + )); + } + MediaFeedback::RequestKeyframe { reason, .. } => { + let _ = output.send(event( + "error", + json!({ + "code": "nvst-keyframe-request-unsupported", + "message": format!("NVST media path needs a keyframe ({reason}), but NVST control/NACK is not implemented") + }), + )); + } + MediaFeedback::DecoderError { codec, message } => { + let _ = output.send(event( + "error", + json!({ + "code": "media-decode-error", + "message": format!("{codec} decoder error: {message}") + }), + )); + } + MediaFeedback::OutputError { message } => { + let _ = output.send(event( + "error", + json!({ "code": "media-output-error", "message": message }), + )); + } + MediaFeedback::QueueDropped { media, count } => { + *dropped = dropped.saturating_add(count); + if last_drop_report.elapsed() >= Duration::from_secs(1) { + let _ = output.send(event( + "log", + json!({ + "level": "debug", + "message": format!("Low-latency {media} queues dropped {dropped} stale samples/frames") + }), + )); + *dropped = 0; + *last_drop_report = Instant::now(); + } + } + } +} + +fn consume_encoded_media( + output: &Sender, + receiver: Receiver, + sink: MediaSink, +) { + while let Ok(frame) = receiver.recv() { + let codec = if frame.codec.eq_ignore_ascii_case("h264") { + MediaCodec::H264 + } else if frame.codec.eq_ignore_ascii_case("opus") { + MediaCodec::Opus { channels: 2 } + } else { + MediaCodec::Unsupported(frame.codec) + }; + match sink.push(EncodedFrame { + mid: frame.mid, + codec, + data: frame.payload, + timestamp: frame.rtp_timestamp, + clock_rate_hz: frame.clock_rate_hz, + keyframe: frame.keyframe, + contiguous: frame.contiguous, + }) { + PushOutcome::Unsupported => { + let _ = output.send(event( + "log", + json!({ + "level": "warn", + "message": "Dropping a frame for a codec not built into native streamer v2" + }), + )); + } + PushOutcome::Closed => break, + PushOutcome::Queued | PushOutcome::DroppedOldest | PushOutcome::Paused => {} + } + } +} + +fn forward_session_events( + output: &Sender, + lifecycle: &Mutex, + generation: u64, + transport_events: Receiver, + media_feedback: Option>, + transport: TransportControl, +) { + let mut dropped = 0; + let mut last_drop_report = Instant::now(); + loop { + if let Some(feedback) = media_feedback.as_ref() { + while let Ok(feedback) = feedback.try_recv() { + forward_media_feedback( + output, + lifecycle, + generation, + &transport, + feedback, + &mut dropped, + &mut last_drop_report, + ); + } + } + match transport_events.recv_timeout(Duration::from_millis(5)) { + Ok(transport_event) => { + let disconnected = matches!(transport_event, TransportEvent::Disconnected(_)); + forward_transport_event(output, lifecycle, generation, transport_event); + if disconnected { + break; + } + } + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, + } + } +} + +fn forward_media_feedback( + output: &Sender, + lifecycle: &Mutex, + generation: u64, + transport: &TransportControl, + feedback: MediaFeedback, + dropped: &mut usize, + last_drop_report: &mut Instant, +) { + if lock_lifecycle(lifecycle).generation != generation { + return; + } + match feedback { + MediaFeedback::PlaybackStarted { backend } => { + let _ = output.send(event( + "log", + json!({ + "level": "info", + "message": format!("{backend} presented the first video frame") + }), + )); + } + MediaFeedback::BackendFallback { from, to, reason } => { + let _ = output.send(event( + "log", + json!({ + "level": "warn", + "message": format!("{from} startup failed; using {to}: {reason}") + }), + )); + } + MediaFeedback::RequestKeyframe { mid, reason } => { + let request_result = transport.request_keyframe(mid); + let _ = output.send(event( + "log", + json!({ + "level": if request_result.is_ok() { "info" } else { "warn" }, + "message": format!("Requested a video keyframe: {reason}") + }), + )); + } + MediaFeedback::DecoderError { codec, message } => { + let _ = output.send(event( + "error", + json!({ + "code": "media-decode-error", + "message": format!("{codec} decoder error: {message}") + }), + )); + } + MediaFeedback::OutputError { message } => { + let _ = output.send(event( + "error", + json!({ "code": "media-output-error", "message": message }), + )); + } + MediaFeedback::QueueDropped { media, count } => { + *dropped = dropped.saturating_add(count); + if last_drop_report.elapsed() >= Duration::from_secs(1) { + let _ = output.send(event( + "log", + json!({ + "level": "debug", + "message": format!( + "Low-latency {media} queues dropped {dropped} stale samples/frames" + ) + }), + )); + *dropped = 0; + *last_drop_report = Instant::now(); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::UdpSocket; + use std::time::Instant; + use str0m::media::{Direction, MediaKind}; + use str0m::{Candidate, RtcConfig}; + + fn command(value: Value) -> Command { + serde_json::from_value(value).expect("command") + } + + fn synthetic_context(session_id: &str, ice_servers: Value) -> Value { + json!({ + "session": { + "sessionId": session_id, + "serverIp": "127-0-0-1.synthetic.invalid", + "iceServers": ice_servers, + "mediaConnectionInfo": { + "ip": "127-0-0-1.media.synthetic.invalid", + "port": 18_784, + "usage": 17 + }, + "syntheticExtension": "preserved" + }, + "settings": { "codec": "H264", "fps": 60 }, + "shortcuts": { "stopStream": "Ctrl+Shift+Q" }, + "syntheticContextExtension": true + }) + } + + fn synthetic_offer() -> String { + opennow_streamer_transport::install_crypto(); + let mut offerer = RtcConfig::new().build(Instant::now()); + offerer.add_local_candidate( + Candidate::host("127.0.0.1:49152".parse().expect("candidate address"), "udp") + .expect("local candidate"), + ); + let mut change = offerer.sdp_api(); + change.add_media(MediaKind::Video, Direction::SendOnly, None, None, None); + let (offer, _pending) = change.apply().expect("synthetic offer"); + offer.to_sdp_string() + } + + fn lifecycle_state(engine: &Engine) -> State { + lock_lifecycle(&engine.lifecycle).state + } + + fn unused_udp_port() -> u16 { + let socket = UdpSocket::bind("127.0.0.1:0").expect("ephemeral UDP port"); + let port = socket.local_addr().expect("socket address").port(); + drop(socket); + port + } + + #[test] + fn hello_reports_honest_transport_only_capabilities() { + let (sender, _receiver) = std::sync::mpsc::channel(); + let mut engine = Engine::new(sender); + let command = command(json!({ + "id": "hello", + "type": "hello", + "protocolVersion": PROTOCOL_VERSION, + })); + let (responses, _) = engine.handle(command); + assert_eq!(responses[0]["type"], "ready"); + assert_eq!(responses[0]["capabilities"]["supportsOfferAnswer"], false); + assert_eq!(responses[0]["capabilities"]["supportsVideoPresent"], false); + } + + #[test] + fn extracts_partial_reliable_threshold() { + assert_eq!( + partial_reliable_threshold("v=0\r\na=ri.partialReliableThresholdMs:250\r\n"), + Some(250), + ); + } + + #[test] + fn start_validates_stores_context_and_prepares_session() { + let (sender, receiver) = std::sync::mpsc::channel(); + let mut engine = Engine::new(sender); + let context = synthetic_context("synthetic-session", json!([])); + let start = command(json!({ + "id": "start", + "type": "start", + "context": context, + })); + let (responses, _) = engine.handle(start); + + assert_eq!(responses[0]["type"], "ok"); + assert_eq!(responses[0]["transport"], "webrtc"); + let lifecycle = lock_lifecycle(&engine.lifecycle); + assert_eq!(lifecycle.state, State::Prepared); + let stored = serde_json::to_value(lifecycle.context.as_ref().expect("stored context")) + .expect("serializable stored context"); + assert_eq!(stored["session"]["sessionId"], "synthetic-session"); + assert_eq!(stored["session"]["syntheticExtension"], "preserved"); + assert_eq!(stored["syntheticContextExtension"], true); + drop(lifecycle); + let status = receiver.recv().expect("ready status"); + assert_eq!(status["status"], "ready"); + } + + #[test] + fn valid_nvst_handoff_starts_udp_video_and_bypasses_webrtc_offer_negotiation() { + let (sender, receiver) = std::sync::mpsc::channel(); + let (media_sender, _media_receiver) = std::sync::mpsc::sync_channel(4); + let mut engine = Engine::with_media_consumer(sender, media_sender); + let mut context = synthetic_context("nvst-session", json!([])); + context["nvstVideo"] = json!({ + "clientUdpPort": unused_udp_port(), + "videoPeerIp": "127.0.0.1", + "videoPeerPort": 5004, + "srtpAesKeyHex": "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "srtpSaltHex": "00000000000000009ECA935E", + "codec": "H264" + }); + let (responses, _) = engine.handle(command(json!({ + "id": "start", + "type": "start", + "context": context.clone(), + }))); + + assert_eq!(responses[0]["type"], "ok"); + assert_eq!(responses[0]["transport"], "nvst"); + assert_eq!(lifecycle_state(&engine), State::Connected); + assert!(engine.nvst_transport.is_some()); + assert!(engine.transport.is_none()); + assert!(receiver.try_iter().any(|message| { + message["type"] == "status" + && message["message"] + .as_str() + .is_some_and(|text| text.contains("NVST")) + })); + + let (responses, _) = engine.handle(command(json!({ + "id": "offer", + "type": "offer", + "context": context, + "sdp": synthetic_offer(), + }))); + assert_eq!(responses[0]["code"], "nvst-video-active"); + + let (responses, _) = engine.handle(command(json!({ + "id": "stop", + "type": "stop", + "reason": "test complete", + }))); + assert_eq!(responses[0]["type"], "ok"); + assert_eq!(lifecycle_state(&engine), State::Idle); + } + + #[test] + fn start_rejects_invalid_and_duplicate_sessions() { + let (sender, _receiver) = std::sync::mpsc::channel(); + let mut engine = Engine::new(sender); + let invalid = command(json!({ + "id": "invalid", + "type": "start", + "context": { + "session": { "sessionId": "", "serverIp": "host", "iceServers": [] }, + "settings": {}, + "shortcuts": {} + } + })); + let (responses, _) = engine.handle(invalid); + assert_eq!(responses[0]["code"], "invalid-context"); + assert_eq!(lifecycle_state(&engine), State::Idle); + + for id in ["first", "duplicate"] { + let start = command(json!({ + "id": id, + "type": "start", + "context": synthetic_context("synthetic-session", json!([])), + })); + let (responses, _) = engine.handle(start); + if id == "first" { + assert_eq!(responses[0]["type"], "ok"); + } else { + assert_eq!(responses[0]["code"], "invalid-state"); + } + } + } + + #[test] + fn offer_negotiates_directly_with_configured_ice_services() { + let (sender, _receiver) = std::sync::mpsc::channel(); + let (media_sender, _media_receiver) = std::sync::mpsc::sync_channel(4); + let mut engine = Engine::with_media_consumer(sender, media_sender); + let context = synthetic_context( + "synthetic-session", + json!([{ + "urls": ["stun:stun.synthetic.invalid:3478", "turn:turn.synthetic.invalid:3478"], + "username": "synthetic-user", + "credential": "synthetic-credential" + }]), + ); + let (responses, _) = engine.handle(command(json!({ + "id": "start", + "type": "start", + "context": context.clone(), + }))); + assert_eq!(responses[0]["type"], "ok"); + + let (responses, _) = engine.handle(command(json!({ + "id": "offer", + "type": "offer", + "context": context, + "sdp": synthetic_offer() + }))); + assert_eq!(responses[0]["type"], "answer"); + assert!( + responses[0]["answer"]["sdp"] + .as_str() + .is_some_and(|sdp| sdp.contains("m=video") && !sdp.contains("m=video 0")) + ); + assert_eq!(lifecycle_state(&engine), State::Negotiating); + } + + #[test] + fn offer_fails_typed_when_no_in_process_media_consumer_exists() { + let (sender, _receiver) = std::sync::mpsc::channel(); + let mut engine = Engine::new(sender); + let context = synthetic_context("synthetic-session", json!([])); + let (responses, _) = engine.handle(command(json!({ + "id": "start", + "type": "start", + "context": context.clone(), + }))); + assert_eq!(responses[0]["type"], "ok"); + + let (responses, _) = engine.handle(command(json!({ + "id": "offer", + "type": "offer", + "context": context, + "sdp": "v=0\r\n" + }))); + + assert_eq!(responses[0]["code"], "media-consumer-unavailable"); + assert_eq!(lifecycle_state(&engine), State::Prepared); + } + + #[test] + fn prepared_session_negotiates_synthetic_offer_for_typed_media_consumer() { + let (sender, receiver) = std::sync::mpsc::channel(); + let (media_sender, _media_receiver) = std::sync::mpsc::sync_channel(4); + let mut engine = Engine::with_media_consumer(sender, media_sender); + let context = synthetic_context("synthetic-session", json!([])); + let (responses, _) = engine.handle(command(json!({ + "id": "start", + "type": "start", + "context": context.clone(), + }))); + assert_eq!(responses[0]["type"], "ok"); + + let (responses, _) = engine.handle(command(json!({ + "id": "offer", + "type": "offer", + "context": context, + "sdp": synthetic_offer() + }))); + + assert_eq!(responses[0]["type"], "answer"); + assert!( + responses[0]["answer"]["sdp"] + .as_str() + .is_some_and(|sdp| sdp.contains("m=video") && !sdp.contains("m=video 0")) + ); + assert_eq!(lifecycle_state(&engine), State::Negotiating); + assert!( + receiver + .try_iter() + .any(|value| value["type"] == "local-ice") + ); + } + + #[test] + fn disconnect_clears_context_and_stale_disconnect_cannot_clear_new_session() { + let (sender, receiver) = std::sync::mpsc::channel(); + let mut engine = Engine::new(sender.clone()); + let (responses, _) = engine.handle(command(json!({ + "id": "first", + "type": "start", + "context": synthetic_context("first-session", json!([])), + }))); + assert_eq!(responses[0]["type"], "ok"); + let first_generation = lock_lifecycle(&engine.lifecycle).generation; + forward_transport_event( + &sender, + &engine.lifecycle, + first_generation, + TransportEvent::Disconnected("synthetic disconnect".to_owned()), + ); + { + let lifecycle = lock_lifecycle(&engine.lifecycle); + assert_eq!(lifecycle.state, State::Idle); + assert!(lifecycle.context.is_none()); + } + + let (responses, _) = engine.handle(command(json!({ + "id": "second", + "type": "start", + "context": synthetic_context("second-session", json!([])), + }))); + assert_eq!(responses[0]["type"], "ok"); + forward_transport_event( + &sender, + &engine.lifecycle, + first_generation, + TransportEvent::Disconnected("late stale disconnect".to_owned()), + ); + let lifecycle = lock_lifecycle(&engine.lifecycle); + assert_eq!(lifecycle.state, State::Prepared); + assert_eq!( + lifecycle + .context + .as_ref() + .map(|value| value.session.session_id.as_str()), + Some("second-session") + ); + drop(lifecycle); + + let events = receiver.try_iter().collect::>(); + assert!(events.iter().any(|value| value["status"] == "stopped")); + assert!( + !events + .iter() + .any(|value| value["message"] == "late stale disconnect") + ); + } + + #[test] + fn encoded_media_consumer_is_typed_in_process_and_preserves_arc_payload() { + let (sender, receiver) = std::sync::mpsc::channel(); + let (media_sender, media_receiver) = std::sync::mpsc::sync_channel(4); + let mut engine = Engine::with_media_consumer(sender, media_sender); + let (responses, _) = engine.handle(command(json!({ + "id": "start", + "type": "start", + "context": synthetic_context("synthetic-session", json!([])), + }))); + assert_eq!(responses[0]["type"], "ok"); + let payload: Arc<[u8]> = Arc::from([1_u8, 2, 3]); + engine + .media_consumer + .as_ref() + .expect("media consumer") + .send(EncodedMediaFrame { + mid: "video-0".to_owned(), + codec: "H264".to_owned(), + payload: payload.clone(), + rtp_timestamp: 90_000, + clock_rate_hz: 90_000, + received_at_us: 1_500, + keyframe: true, + contiguous: true, + }) + .expect("frame delivery"); + + let frame = media_receiver.recv().expect("encoded frame"); + assert!(Arc::ptr_eq(&frame.payload, &payload)); + assert_eq!(frame.rtp_timestamp, 90_000); + assert_eq!(frame.clock_rate_hz, 90_000); + assert_eq!(frame.received_at_us, 1_500); + assert!( + receiver + .try_iter() + .all(|value| value["type"] != "encoded-media") + ); + } + + #[test] + fn unapplied_commands_are_rejected_and_stop_clears_context() { + let (sender, _receiver) = std::sync::mpsc::channel(); + let mut engine = Engine::new(sender); + let (responses, _) = engine.handle(command(json!({ + "id": "surface", + "type": "surface", + "surface": {} + }))); + assert_eq!(responses[0]["code"], "unsupported-command"); + + let (responses, _) = engine.handle(command(json!({ + "id": "start", + "type": "start", + "context": synthetic_context("synthetic-session", json!([])), + }))); + assert_eq!(responses[0]["type"], "ok"); + let (responses, _) = engine.handle(command(json!({ + "id": "stop", + "type": "stop", + "reason": "synthetic test complete" + }))); + assert_eq!(responses[0]["type"], "ok"); + let lifecycle = lock_lifecycle(&engine.lifecycle); + assert_eq!(lifecycle.state, State::Idle); + assert!(lifecycle.context.is_none()); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/Cargo.toml b/native/opennow-streamer/crates/opennow-streamer-platform-macos/Cargo.toml new file mode 100644 index 000000000..332211c53 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "opennow-streamer-platform-macos" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "Native VideoToolbox, Metal, and CoreAudio backend for OpenNOW" +[dependencies] +thiserror.workspace = true + +[target.'cfg(target_os = "macos")'.dependencies] +libc = "0.2" +objc2 = "0.6.3" +objc2-app-kit = { version = "0.3.2", features = ["NSEvent"] } +objc2-audio-toolbox = "0.3.2" +objc2-core-audio-types = "0.3.2" +objc2-core-foundation = "0.3.2" +objc2-core-media = "0.3.2" +objc2-core-video = "0.3.2" +objc2-foundation = "0.3.2" +objc2-metal = "0.3.2" +objc2-quartz-core = "0.3.2" +objc2-video-toolbox = "0.3.2" +opus = "0.3.1" diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/README.md b/native/opennow-streamer/crates/opennow-streamer-platform-macos/README.md new file mode 100644 index 000000000..e252f646c --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/README.md @@ -0,0 +1,138 @@ +# OpenNOW native macOS platform backend + +This isolated crate implements the macOS media path without GStreamer or FFmpeg: + +- H.264 Annex B or four-byte AVCC access units are copied into CoreMedia sample buffers and + decoded asynchronously by VideoToolbox. +- VideoToolbox is asked for Metal-compatible, IOSurface-backed NV12 (`420v`) pixel buffers. A + `CVMetalTextureCache` maps both planes into Metal without a CPU pixel copy, and a + `CAMetalLayer` presents them through a small BT.601/BT.709 conversion shader. +- Opus packets are decoded to interleaved `f32` PCM by the reference libopus decoder. The + dependency builds libopus statically on macOS, so it adds no runtime media-framework + dependency. A default-output Audio Unit pulls PCM from a fixed-size SPSC ring in its real-time + callback. + +The crate is a member of the native-streamer workspace and is linked only on macOS. + +## Integration API + +Create `H264ParameterSets` from the current SPS and PPS, provide absolute Electron screen bounds, +and start the process-owned passive overlay on AppKit's main thread: + +```rust,no_run +use opennow_streamer_platform_macos::{ + AudioFormat, BackendConfig, H264Format, H264ParameterSets, MacOsBackend, + OwnedOverlayConfig, QueueLimits, ScreenRect, SurfaceTarget, VideoColorSpace, +}; + +let parameter_sets = H264ParameterSets::new(sps, pps)?; +let mut backend = MacOsBackend::start(BackendConfig { + surface: SurfaceTarget::OwnedOverlay(OwnedOverlayConfig::new( + ScreenRect::new(120.0, 80.0, 1280.0, 720.0), + true, + )), + video: H264Format::new(parameter_sets, VideoColorSpace::Bt709), + audio: AudioFormat::OPUS_STEREO_48KHZ, + queues: QueueLimits::default(), +})?; + +let sink = backend.sink(); +// Move `sink` to the transport thread and call submit_h264/submit_opus there. +// Keep `backend` on the AppKit main thread. + +backend.stop(); +# Ok::<(), Box>(()) +``` + +The native streamer never passes Electron's `NSView` or `NSWindow` address to this Rust child. +Live layout and visibility updates use only the absolute `screenRect` contract: + +```rust,no_run +use opennow_streamer_platform_macos::ScreenRect; + +backend.update_owned_overlay(ScreenRect::new(120.0, 80.0, 960.0, 540.0), true)?; +backend.update_owned_overlay(ScreenRect::new(120.0, 80.0, 960.0, 540.0), false)?; +# Ok::<(), Box>(()) +``` + +Screen rectangles use Electron's top-left device-independent coordinates. The backend converts +them to AppKit's bottom-left coordinate space. Its `NSPanel` is borderless and non-activating, +ignores mouse events, rejects key/main-window status, and orders without stealing focus. The +native streamer also compares the frontmost application with its Electron parent process on every +main-thread pump, ordering the panel out while another application is active. The separately owned +SDL window used by the software fallback applies the same parent/frontmost gate. + +`StreamSink` is `Send + Sync`; `MacOsBackend` is intentionally neither. Video and audio can be +reconfigured through `StreamSink` while running. Video reconfiguration constructs a new +VideoToolbox session before replacing the old one. Audio reconfiguration stops the old Audio Unit +and decoder worker before constructing the new format, so a failed audio reconfiguration leaves +audio stopped rather than running under an ambiguous format. + +Only H.264 is implemented. The workspace advertises this backend only when +`VTIsHardwareDecodeSupported` succeeds and Metal can create a device and command queue. This crate +makes no H.265, AV1, software-decoder, or non-macOS capability claim. + +The workspace performs full backend construction after the first SPS/PPS arrive. If VideoToolbox, +Metal, CoreAudio, or overlay construction fails at that point, the main-thread host destroys the +partial macOS output, initializes the existing SDL output, and hands the pending keyframe plus the +same bounded H.264/Opus queues to the OpenH264/software workers. Hardware selection is disabled for +later sessions in that process and later capability replies report VideoToolbox unavailable. + +## Queue and lifecycle behavior + +All buffering is explicitly bounded: + +- VideoToolbox admission returns `SubmitOutcome::Backpressured` when the in-flight decode limit is + reached; accepted decoder work is never silently invalidated. +- The decoded-frame queue drops its oldest frame when rendering falls behind, keeping latency + bounded. +- While a supplied-window child is hidden, decoded frames are discarded before requesting a + `CAMetalDrawable`; showing it resumes from the newest frame without accumulating hidden work. +- The Opus packet queue drops its oldest packet and reports `SubmitOutcome::ReplacedOldest`. +- The PCM ring never allocates or locks in the CoreAudio callback. If decoding outruns playback, + new samples are dropped and counted. If playback underruns, the callback emits silence and + counts the missing frames. + +`stop` is idempotent. It first rejects new submissions, waits for VideoToolbox's outstanding +callbacks, stops CoreAudio, discards queued work, joins both workers, and waits for submitted Metal +command buffers. A supplied view's previous backing layer is then restored; an owned window is +closed. A supplied window's passive child view is removed without modifying the BrowserWindow +content view or renderer layer. + +## Safety invariants + +`BorrowedNsView::from_raw` and `BorrowedNsWindow::from_raw` are the only public unsafe entry +points. Their pointers must be live objects of exactly the documented AppKit class and must be +created and consumed on AppKit's main thread. The backend retains the object for its lifetime. A +supplied view is treated as a dedicated presentation surface because its backing layer is replaced +until shutdown. A supplied window instead receives an owned child surface; geometry/visibility +updates, child insertion, and child removal all require AppKit's main thread. + +The `NativeSurfaceHandle` pointers borrow the running backend. For a supplied window, `ns_view` +identifies the owned passive child rather than the BrowserWindow content view. The pointers are +valid only until `stop` or `Drop`, may be used only on AppKit's main thread, and must never be +released by the caller. + +VideoToolbox and CoreAudio callback contexts are heap allocated at stable addresses and outlive +their registered sessions. The VideoToolbox session is accessed behind a mutex; decoded +`CVPixelBuffer`s are immutable after the callback and retained across the queue. The Metal worker +retains each `CVMetalTexture` until its command buffer completes. The CoreAudio callback has one +consumer and the Opus worker has one producer for the PCM ring. + +## Checks + +On macOS, run: + +```sh +cargo test +cargo clippy --all-targets -- -D warnings +``` + +Both Apple architectures can be type-checked from a non-macOS host when Rust's targets are +installed. A real build still requires the Apple SDK and a matching C compiler because vendored +libopus is compiled for the target: + +```sh +cargo check --target aarch64-apple-darwin +cargo check --target x86_64-apple-darwin +``` diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/format.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/format.rs new file mode 100644 index 000000000..2a7b3bbbb --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/format.rs @@ -0,0 +1,608 @@ +use std::ffi::c_void; +use std::ptr::NonNull; + +use thiserror::Error; + +const MAX_PARAMETER_SET_BYTES: usize = 64 * 1024; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum H264Framing { + AnnexB, + Avcc, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VideoColorSpace { + Bt601, + Bt709, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FrameTiming { + pub presentation_value: i64, + pub duration_value: i64, + pub timescale: i32, +} + +impl FrameTiming { + pub const fn new(presentation_value: i64, duration_value: i64, timescale: i32) -> Self { + Self { + presentation_value, + duration_value, + timescale, + } + } + + pub const fn from_90khz(presentation_value: i64, duration_value: i64) -> Self { + Self::new(presentation_value, duration_value, 90_000) + } + + pub(crate) fn validate(self) -> Result<(), FormatError> { + if self.timescale <= 0 { + return Err(FormatError::InvalidTimescale); + } + if self.duration_value < 0 { + return Err(FormatError::InvalidDuration); + } + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct H264ParameterSets { + sequence: Vec, + picture: Vec, +} + +impl H264ParameterSets { + pub fn new(sequence: impl AsRef<[u8]>, picture: impl AsRef<[u8]>) -> Result { + let sequence = normalize_parameter_set(sequence.as_ref(), 7)?; + let picture = normalize_parameter_set(picture.as_ref(), 8)?; + Ok(Self { sequence, picture }) + } + + pub fn sequence(&self) -> &[u8] { + &self.sequence + } + + pub fn picture(&self) -> &[u8] { + &self.picture + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct H264Format { + pub parameter_sets: H264ParameterSets, + pub color_space: VideoColorSpace, +} + +impl H264Format { + pub const fn new(parameter_sets: H264ParameterSets, color_space: VideoColorSpace) -> Self { + Self { + parameter_sets, + color_space, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AudioFormat { + pub sample_rate: u32, + pub channels: u8, +} + +impl AudioFormat { + pub const OPUS_STEREO_48KHZ: Self = Self { + sample_rate: 48_000, + channels: 2, + }; + + pub const fn new(sample_rate: u32, channels: u8) -> Self { + Self { + sample_rate, + channels, + } + } + + pub(crate) fn validate(self) -> Result<(), FormatError> { + if !matches!(self.sample_rate, 8_000 | 12_000 | 16_000 | 24_000 | 48_000) { + return Err(FormatError::UnsupportedOpusSampleRate(self.sample_rate)); + } + if !matches!(self.channels, 1 | 2) { + return Err(FormatError::UnsupportedChannelCount(self.channels)); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct QueueLimits { + pub video_frames_in_flight: usize, + pub decoded_video_frames: usize, + pub opus_packets: usize, + pub pcm_milliseconds: u32, + pub max_video_access_unit_bytes: usize, +} + +impl Default for QueueLimits { + fn default() -> Self { + Self { + video_frames_in_flight: 8, + decoded_video_frames: 3, + opus_packets: 12, + pcm_milliseconds: 120, + max_video_access_unit_bytes: 8 * 1024 * 1024, + } + } +} + +impl QueueLimits { + pub(crate) fn validate(self) -> Result<(), FormatError> { + if self.video_frames_in_flight == 0 + || self.decoded_video_frames == 0 + || self.opus_packets == 0 + || self.pcm_milliseconds == 0 + || self.max_video_access_unit_bytes == 0 + { + return Err(FormatError::ZeroQueueLimit); + } + let pcm_ms = + usize::try_from(self.pcm_milliseconds).map_err(|_| FormatError::QueueTooLarge)?; + if pcm_ms > 5_000 { + return Err(FormatError::QueueTooLarge); + } + Ok(()) + } +} + +/// Absolute screen bounds in Electron's top-left, device-independent coordinate space. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ScreenRect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl ScreenRect { + pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self { + Self { + x, + y, + width, + height, + } + } + + pub(crate) fn validate(self) -> Result<(), FormatError> { + if !self.x.is_finite() + || !self.y.is_finite() + || !self.width.is_finite() + || !self.height.is_finite() + || self.width <= 0.0 + || self.height <= 0.0 + { + return Err(FormatError::InvalidScreenRect); + } + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct OwnedOverlayConfig { + pub screen_rect: ScreenRect, + pub visible: bool, +} + +impl OwnedOverlayConfig { + pub const fn new(screen_rect: ScreenRect, visible: bool) -> Self { + Self { + screen_rect, + visible, + } + } + + pub(crate) fn validate(self) -> Result<(), FormatError> { + self.screen_rect.validate() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BorrowedNsView(NonNull); + +impl BorrowedNsView { + /// Creates a borrowed AppKit view handle. + /// + /// # Safety + /// + /// `view` must point to a live, dedicated `NSView` whose backing layer may be replaced for the + /// backend's lifetime. The object must belong to the current process, and creating the handle + /// and passing it to `MacOsBackend::start` must occur on AppKit's main thread. The backend + /// retains the view before this call's borrowed lifetime can end. + pub const unsafe fn from_raw(view: NonNull) -> Self { + Self(view) + } + + pub const fn as_ptr(self) -> NonNull { + self.0 + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BorrowedNsWindow(NonNull); + +impl BorrowedNsWindow { + /// Creates a borrowed AppKit window handle. + /// + /// # Safety + /// + /// `window` must point to a live `NSWindow`. Construction and backend startup must occur on + /// AppKit's main thread. The backend does not replace the window content view or its layer; it + /// inserts a passive child view for stream presentation. + pub const unsafe fn from_raw(window: NonNull) -> Self { + Self(window) + } + + pub const fn as_ptr(self) -> NonNull { + self.0 + } +} + +/// A rectangle in renderer-relative, top-left AppKit points. +/// +/// Points match Electron's device-independent coordinates. Negative origins are allowed for +/// clipping, while width and height must be finite and non-negative. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct RendererRect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +impl RendererRect { + pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self { + Self { + x, + y, + width, + height, + } + } + + pub(crate) fn validate(self) -> Result<(), FormatError> { + if !self.x.is_finite() + || !self.y.is_finite() + || !self.width.is_finite() + || !self.height.is_finite() + || self.width < 0.0 + || self.height < 0.0 + { + return Err(FormatError::InvalidRendererRect); + } + Ok(()) + } + + pub(crate) fn to_parent_coordinates( + self, + parent: RendererRect, + parent_is_flipped: bool, + ) -> Self { + let y = if parent_is_flipped { + parent.y + self.y + } else { + parent.y + parent.height - self.y - self.height + }; + Self::new(parent.x + self.x, y, self.width, self.height) + } +} + +/// Initial layout for a passive video child inside a supplied `NSWindow` content view. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct WindowSurfaceConfig { + pub window: BorrowedNsWindow, + pub bounds: RendererRect, + pub visible: bool, +} + +impl WindowSurfaceConfig { + pub const fn new(window: BorrowedNsWindow, bounds: RendererRect) -> Self { + Self { + window, + bounds, + visible: true, + } + } + + pub(crate) fn validate(self) -> Result<(), FormatError> { + self.bounds.validate() + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SurfaceTarget { + /// Creates a borderless, non-activating, mouse-ignoring overlay owned by this process. + OwnedOverlay(OwnedOverlayConfig), + /// Uses a caller-owned view that is explicitly dedicated to video presentation. Its backing + /// layer is replaced until backend shutdown and restored afterwards. + NsView(BorrowedNsView), + /// Adds an owned, passive child view to the supplied window's content view. The existing + /// content view and its backing layer are never replaced. + NsWindow(WindowSurfaceConfig), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct BackendConfig { + pub surface: SurfaceTarget, + pub video: H264Format, + pub audio: AudioFormat, + pub queues: QueueLimits, +} + +impl BackendConfig { + pub(crate) fn validate(&self) -> Result<(), FormatError> { + self.audio.validate()?; + self.queues.validate()?; + if let SurfaceTarget::OwnedOverlay(overlay) = self.surface { + overlay.validate()?; + } + if let SurfaceTarget::NsWindow(window) = &self.surface { + window.validate()?; + } + Ok(()) + } +} + +#[derive(Debug, Error, Eq, PartialEq)] +pub enum FormatError { + #[error("H.264 parameter set is empty")] + EmptyParameterSet, + #[error("H.264 parameter set exceeds the supported size")] + ParameterSetTooLarge, + #[error("expected H.264 NAL type {expected}, got {actual}")] + UnexpectedNalType { expected: u8, actual: u8 }, + #[error("multiple NAL units were supplied where one parameter set was expected")] + MultipleParameterSets, + #[error("H.264 access unit has no NAL units")] + EmptyAccessUnit, + #[error("H.264 access unit contains an empty NAL unit")] + EmptyNalUnit, + #[error("H.264 access unit has invalid AVCC framing")] + InvalidAvcc, + #[error("H.264 NAL unit is too large for AVCC framing")] + NalUnitTooLarge, + #[error("frame timescale must be positive")] + InvalidTimescale, + #[error("frame duration must not be negative")] + InvalidDuration, + #[error("unsupported Opus sample rate {0}")] + UnsupportedOpusSampleRate(u32), + #[error("unsupported Opus channel count {0}")] + UnsupportedChannelCount(u8), + #[error("queue limits must be non-zero")] + ZeroQueueLimit, + #[error("queue configuration is too large")] + QueueTooLarge, + #[error("absolute screen bounds must be finite with positive dimensions")] + InvalidScreenRect, + #[error("renderer-relative surface bounds must be finite with non-negative dimensions")] + InvalidRendererRect, +} + +pub(crate) fn access_unit_to_avcc( + bytes: &[u8], + framing: H264Framing, +) -> Result, FormatError> { + match framing { + H264Framing::AnnexB => annex_b_to_avcc(bytes), + H264Framing::Avcc => { + validate_avcc(bytes)?; + Ok(bytes.to_vec()) + } + } +} + +fn normalize_parameter_set(bytes: &[u8], expected_type: u8) -> Result, FormatError> { + let bytes = strip_start_code(bytes); + if bytes.is_empty() { + return Err(FormatError::EmptyParameterSet); + } + if bytes.len() > MAX_PARAMETER_SET_BYTES { + return Err(FormatError::ParameterSetTooLarge); + } + if find_start_code(bytes, 1).is_some() { + return Err(FormatError::MultipleParameterSets); + } + let actual = bytes[0] & 0x1f; + if actual != expected_type { + return Err(FormatError::UnexpectedNalType { + expected: expected_type, + actual, + }); + } + Ok(bytes.to_vec()) +} + +fn strip_start_code(bytes: &[u8]) -> &[u8] { + if bytes.starts_with(&[0, 0, 0, 1]) { + &bytes[4..] + } else if bytes.starts_with(&[0, 0, 1]) { + &bytes[3..] + } else { + bytes + } +} + +fn annex_b_to_avcc(bytes: &[u8]) -> Result, FormatError> { + let Some((mut start, prefix_len)) = find_start_code(bytes, 0) else { + return Err(FormatError::EmptyAccessUnit); + }; + if bytes[..start].iter().any(|byte| *byte != 0) { + return Err(FormatError::EmptyAccessUnit); + } + start += prefix_len; + let mut output = Vec::with_capacity(bytes.len()); + loop { + let next = find_start_code(bytes, start); + let end = next.map_or(bytes.len(), |(offset, _)| offset); + if end == start { + return Err(FormatError::EmptyNalUnit); + } + write_avcc_nal(&mut output, &bytes[start..end])?; + let Some((next_start, next_prefix)) = next else { + break; + }; + start = next_start + next_prefix; + } + if output.is_empty() { + return Err(FormatError::EmptyAccessUnit); + } + Ok(output) +} + +fn find_start_code(bytes: &[u8], from: usize) -> Option<(usize, usize)> { + let mut index = from; + while index + 3 <= bytes.len() { + if bytes[index..].starts_with(&[0, 0, 1]) { + return Some((index, 3)); + } + if bytes[index..].starts_with(&[0, 0, 0, 1]) { + return Some((index, 4)); + } + index += 1; + } + None +} + +fn write_avcc_nal(output: &mut Vec, nal: &[u8]) -> Result<(), FormatError> { + if nal.is_empty() { + return Err(FormatError::EmptyNalUnit); + } + let len = u32::try_from(nal.len()).map_err(|_| FormatError::NalUnitTooLarge)?; + output.extend_from_slice(&len.to_be_bytes()); + output.extend_from_slice(nal); + Ok(()) +} + +fn validate_avcc(bytes: &[u8]) -> Result<(), FormatError> { + if bytes.is_empty() { + return Err(FormatError::EmptyAccessUnit); + } + let mut offset = 0usize; + while offset < bytes.len() { + let header = bytes + .get(offset..offset + 4) + .ok_or(FormatError::InvalidAvcc)?; + let len = u32::from_be_bytes(header.try_into().expect("four-byte AVCC length")) as usize; + if len == 0 { + return Err(FormatError::EmptyNalUnit); + } + offset = offset.checked_add(4).ok_or(FormatError::InvalidAvcc)?; + offset = offset.checked_add(len).ok_or(FormatError::InvalidAvcc)?; + if offset > bytes.len() { + return Err(FormatError::InvalidAvcc); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn converts_mixed_annex_b_start_codes_to_avcc() { + let annex_b = [0, 0, 0, 1, 0x65, 0xaa, 0xbb, 0, 0, 1, 0x41, 0xcc]; + assert_eq!( + annex_b_to_avcc(&annex_b).unwrap(), + [0, 0, 0, 3, 0x65, 0xaa, 0xbb, 0, 0, 0, 2, 0x41, 0xcc] + ); + } + + #[test] + fn rejects_truncated_avcc_access_unit() { + assert_eq!( + access_unit_to_avcc(&[0, 0, 0, 3, 0x65], H264Framing::Avcc), + Err(FormatError::InvalidAvcc) + ); + } + + #[test] + fn normalizes_parameter_set_start_codes() { + let sets = H264ParameterSets::new( + [0, 0, 0, 1, 0x67, 0x64, 0x00, 0x29], + [0, 0, 1, 0x68, 0xee, 0x3c, 0x80], + ) + .unwrap(); + assert_eq!(sets.sequence(), &[0x67, 0x64, 0x00, 0x29]); + assert_eq!(sets.picture(), &[0x68, 0xee, 0x3c, 0x80]); + } + + #[test] + fn rejects_wrong_parameter_set_type() { + assert_eq!( + H264ParameterSets::new([0x68, 1], [0x68, 2]), + Err(FormatError::UnexpectedNalType { + expected: 7, + actual: 8 + }) + ); + } + + #[test] + fn validates_audio_and_queue_formats() { + assert!(AudioFormat::OPUS_STEREO_48KHZ.validate().is_ok()); + assert_eq!( + AudioFormat::new(44_100, 2).validate(), + Err(FormatError::UnsupportedOpusSampleRate(44_100)) + ); + let limits = QueueLimits { + opus_packets: 0, + ..QueueLimits::default() + }; + assert_eq!(limits.validate(), Err(FormatError::ZeroQueueLimit)); + } + + #[test] + fn converts_renderer_top_left_bounds_for_unflipped_appkit_parent() { + let parent = RendererRect::new(0.0, 0.0, 1280.0, 720.0); + let renderer = RendererRect::new(20.0, 30.0, 640.0, 360.0); + assert_eq!( + renderer.to_parent_coordinates(parent, false), + RendererRect::new(20.0, 330.0, 640.0, 360.0) + ); + } + + #[test] + fn preserves_renderer_y_for_flipped_parent_and_accounts_for_bounds_origin() { + let parent = RendererRect::new(5.0, 10.0, 1280.0, 720.0); + let renderer = RendererRect::new(20.0, 30.0, 640.0, 360.0); + assert_eq!( + renderer.to_parent_coordinates(parent, true), + RendererRect::new(25.0, 40.0, 640.0, 360.0) + ); + } + + #[test] + fn rejects_invalid_renderer_geometry() { + assert_eq!( + RendererRect::new(0.0, 0.0, -1.0, 10.0).validate(), + Err(FormatError::InvalidRendererRect) + ); + assert_eq!( + RendererRect::new(f64::NAN, 0.0, 10.0, 10.0).validate(), + Err(FormatError::InvalidRendererRect) + ); + } + + #[test] + fn recomputes_unflipped_child_position_after_parent_resize() { + let renderer = RendererRect::new(20.0, 30.0, 640.0, 360.0); + let initial = + renderer.to_parent_coordinates(RendererRect::new(0.0, 0.0, 1280.0, 720.0), false); + let resized = + renderer.to_parent_coordinates(RendererRect::new(0.0, 0.0, 1280.0, 920.0), false); + assert_eq!(initial.y, 330.0); + assert_eq!(resized.y, 530.0); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/lib.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/lib.rs new file mode 100644 index 000000000..e5cac89c4 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/lib.rs @@ -0,0 +1,51 @@ +//! Native macOS media backend for OpenNOW. +//! +//! `opennow-streamer-platform` conditionally uses this crate on macOS and passes encoded media +//! through [`StreamSink`]. Video is decoded by VideoToolbox into IOSurface +//! backed NV12 pixel buffers and sampled directly by Metal. Opus is decoded by the statically +//! built reference libopus implementation and written to a CoreAudio output unit as interleaved +//! `f32` PCM. +//! +//! # Threading and safety invariants +//! +//! - [`MacOsBackend::start`] must run on the AppKit main thread. `MacOsBackend` is deliberately +//! `!Send` and owns all AppKit objects, so its `stop` and `Drop` paths also run on that thread. +//! - The native streamer integration uses only the process-owned overlay target and never +//! dereferences an Electron AppKit pointer. A supplied [`BorrowedNsView`] must be dedicated to +//! video because its layer is temporarily +//! replaced. A supplied [`BorrowedNsWindow`] keeps its existing content view and renderer layer; +//! the backend inserts a passive, non-focusable child view and removes it on shutdown. +//! - Supplied-window geometry and visibility changes go through +//! [`MacOsBackend::update_window_surface`] and are applied only on the AppKit main thread. +//! - [`StreamSink`] is `Send + Sync`. VideoToolbox, CoreAudio, and Metal callbacks never borrow +//! caller memory. Encoded access units and packets are copied before a submit call returns. +//! - Every media queue is bounded. Video admission rejects new work at the configured in-flight +//! limit, decoded video and Opus queues drop their oldest item, and the PCM ring drops new +//! samples rather than blocking CoreAudio's real-time callback. +//! - VideoToolbox callbacks retain immutable `CVPixelBuffer`s before enqueueing them. The Metal +//! presenter retains `CVMetalTexture`s until their command buffer has completed. Shutdown waits +//! for VideoToolbox callbacks and GPU work before releasing either callback context. + +#![deny(unsafe_op_in_unsafe_fn)] +#![cfg_attr(not(target_os = "macos"), allow(dead_code))] + +mod format; +mod lifecycle; +mod queue; +mod ring; + +#[cfg(target_os = "macos")] +mod macos; + +pub use format::{ + AudioFormat, BackendConfig, BorrowedNsView, BorrowedNsWindow, FrameTiming, H264Format, + H264Framing, H264ParameterSets, OwnedOverlayConfig, QueueLimits, RendererRect, ScreenRect, + SurfaceTarget, VideoColorSpace, WindowSurfaceConfig, +}; +pub use lifecycle::BackendState; + +#[cfg(target_os = "macos")] +pub use macos::{ + BackendError, BackendStats, MacOsBackend, NativeSurfaceHandle, StreamSink, SubmitOutcome, + debug_show_overlay_window, probe_h264_hardware, pump_app_events, +}; diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/lifecycle.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/lifecycle.rs new file mode 100644 index 000000000..4603ddac5 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/lifecycle.rs @@ -0,0 +1,78 @@ +use std::sync::atomic::{AtomicU8, Ordering}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum BackendState { + Running = 1, + Stopping = 2, + Stopped = 3, +} + +pub(crate) struct Lifecycle(AtomicU8); + +pub(crate) struct AttachmentLifecycle { + attached: bool, +} + +impl AttachmentLifecycle { + pub(crate) const fn attached() -> Self { + Self { attached: true } + } + + pub(crate) fn begin_detach(&mut self) -> bool { + std::mem::replace(&mut self.attached, false) + } +} + +impl Lifecycle { + pub(crate) const fn running() -> Self { + Self(AtomicU8::new(BackendState::Running as u8)) + } + + pub(crate) fn state(&self) -> BackendState { + match self.0.load(Ordering::Acquire) { + 1 => BackendState::Running, + 2 => BackendState::Stopping, + _ => BackendState::Stopped, + } + } + + pub(crate) fn begin_stop(&self) -> bool { + self.0 + .compare_exchange( + BackendState::Running as u8, + BackendState::Stopping as u8, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + } + + pub(crate) fn finish_stop(&self) { + self.0.store(BackendState::Stopped as u8, Ordering::Release); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stop_transition_is_idempotent() { + let lifecycle = Lifecycle::running(); + assert_eq!(lifecycle.state(), BackendState::Running); + assert!(lifecycle.begin_stop()); + assert!(!lifecycle.begin_stop()); + assert_eq!(lifecycle.state(), BackendState::Stopping); + lifecycle.finish_stop(); + assert_eq!(lifecycle.state(), BackendState::Stopped); + assert!(!lifecycle.begin_stop()); + } + + #[test] + fn surface_attachment_detaches_exactly_once() { + let mut lifecycle = AttachmentLifecycle::attached(); + assert!(lifecycle.begin_detach()); + assert!(!lifecycle.begin_detach()); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/audio.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/audio.rs new file mode 100644 index 000000000..2ae372c54 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/audio.rs @@ -0,0 +1,331 @@ +use std::ffi::c_void; +use std::mem; +use std::ptr::{self, NonNull}; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::thread::{self, JoinHandle}; + +use objc2_audio_toolbox::{ + AURenderCallbackStruct, AudioComponentDescription, AudioComponentFindNext, + AudioComponentInstanceDispose, AudioComponentInstanceNew, AudioOutputUnitStart, + AudioOutputUnitStop, AudioUnit, AudioUnitInitialize, AudioUnitRenderActionFlags, + AudioUnitSetProperty, AudioUnitUninitialize, kAudioUnitManufacturer_Apple, + kAudioUnitProperty_SetRenderCallback, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, + kAudioUnitSubType_DefaultOutput, kAudioUnitType_Output, +}; +use objc2_core_audio_types::{ + AudioBufferList, AudioStreamBasicDescription, AudioTimeStamp, kAudioFormatFlagIsFloat, + kAudioFormatFlagIsPacked, kAudioFormatLinearPCM, +}; +use opus::{Channels, Decoder}; + +use crate::format::AudioFormat; +use crate::queue::{BoundedQueue, PushResult}; +use crate::ring::PcmRing; + +use super::{BackendError, Counters}; + +const MAX_OPUS_FRAME_SAMPLES_PER_CHANNEL: usize = 5_760; + +pub(super) struct AudioPipeline { + packets: Arc>>, + ring: Arc, + output: Option, + worker: Option>, +} + +impl AudioPipeline { + pub(super) fn start( + format: AudioFormat, + packet_capacity: usize, + pcm_milliseconds: u32, + counters: Arc, + ) -> Result { + format.validate()?; + let channels = usize::from(format.channels); + let pcm_capacity = usize::try_from(format.sample_rate) + .ok() + .and_then(|rate| rate.checked_mul(channels)) + .and_then(|samples| samples.checked_mul(pcm_milliseconds as usize)) + .map(|samples| samples / 1_000) + .filter(|samples| *samples > 0) + .ok_or(crate::format::FormatError::QueueTooLarge)?; + let ring = Arc::new(PcmRing::new(pcm_capacity)); + let packets: Arc>> = Arc::new(BoundedQueue::new(packet_capacity)); + let decoder_channels = if format.channels == 1 { + Channels::Mono + } else { + Channels::Stereo + }; + let mut decoder = Decoder::new(format.sample_rate, decoder_channels) + .map_err(|error| BackendError::Opus(error.to_string()))?; + let worker_packets = Arc::clone(&packets); + let worker_ring = Arc::clone(&ring); + let worker_counters = Arc::clone(&counters); + let worker = thread::Builder::new() + .name("opennow-opus-decode".into()) + .spawn(move || { + let mut pcm = vec![0.0; MAX_OPUS_FRAME_SAMPLES_PER_CHANNEL * channels]; + while let Some(packet) = worker_packets.pop_wait() { + match decoder.decode_float(&packet, &mut pcm, false) { + Ok(samples_per_channel) => { + let sample_count = samples_per_channel * channels; + let written = worker_ring.push(&pcm[..sample_count]); + if written != sample_count { + worker_counters + .pcm_samples_dropped + .fetch_add((sample_count - written) as u64, Ordering::Relaxed); + } + } + Err(_) => { + worker_counters + .opus_decode_errors + .fetch_add(1, Ordering::Relaxed); + } + } + } + }) + .map_err(|_| BackendError::Thread("Opus decoder"))?; + + let output = match AudioOutput::start(format, Arc::clone(&ring), counters) { + Ok(output) => output, + Err(error) => { + packets.close(); + let _ = worker.join(); + return Err(error); + } + }; + Ok(Self { + packets, + ring, + output: Some(output), + worker: Some(worker), + }) + } + + pub(super) fn submit(&self, packet: Vec) -> PushResult> { + self.packets.push_drop_oldest(packet) + } + + pub(super) fn set_paused(&mut self, paused: bool) -> Result<(), BackendError> { + if paused && let Some(output) = self.output.as_mut() { + output.set_paused(true)?; + } + self.packets.clear(); + self.ring.clear(); + if !paused && let Some(output) = self.output.as_mut() { + output.set_paused(false)?; + } + Ok(()) + } + + pub(super) fn stop(mut self) { + drop(self.output.take()); + self.packets.close_and_discard(); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +impl Drop for AudioPipeline { + fn drop(&mut self) { + drop(self.output.take()); + self.packets.close_and_discard(); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +struct AudioCallbackContext { + ring: Arc, + counters: Arc, + channels: usize, +} + +struct AudioOutput { + unit: AudioUnit, + initialized: bool, + started: bool, + callback_context: Box, +} + +// Audio Unit control calls have no thread affinity and AudioOutput is always owned behind Shared's +// audio Mutex. CoreAudio's concurrent render callback touches only the stable callback context. +unsafe impl Send for AudioOutput {} + +impl AudioOutput { + fn start( + format: AudioFormat, + ring: Arc, + counters: Arc, + ) -> Result { + let mut description = AudioComponentDescription { + componentType: kAudioUnitType_Output, + componentSubType: kAudioUnitSubType_DefaultOutput, + componentManufacturer: kAudioUnitManufacturer_Apple, + componentFlags: 0, + componentFlagsMask: 0, + }; + let component = + unsafe { AudioComponentFindNext(ptr::null_mut(), NonNull::from(&mut description)) }; + if component.is_null() { + return Err(BackendError::AppleApi { + api: "AudioComponentFindNext", + status: -1, + }); + } + let mut unit = ptr::null_mut(); + let status = unsafe { AudioComponentInstanceNew(component, NonNull::from(&mut unit)) }; + check_status("AudioComponentInstanceNew", status)?; + if unit.is_null() { + return Err(BackendError::AppleApi { + api: "AudioComponentInstanceNew", + status: -1, + }); + } + + let mut output = Self { + unit, + initialized: false, + started: false, + callback_context: Box::new(AudioCallbackContext { + ring, + counters, + channels: usize::from(format.channels), + }), + }; + let bytes_per_frame = u32::from(format.channels) * mem::size_of::() as u32; + let stream_format = AudioStreamBasicDescription { + mSampleRate: f64::from(format.sample_rate), + mFormatID: kAudioFormatLinearPCM, + mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, + mBytesPerPacket: bytes_per_frame, + mFramesPerPacket: 1, + mBytesPerFrame: bytes_per_frame, + mChannelsPerFrame: u32::from(format.channels), + mBitsPerChannel: 32, + mReserved: 0, + }; + let status = unsafe { + AudioUnitSetProperty( + output.unit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 0, + (&stream_format as *const AudioStreamBasicDescription).cast(), + mem::size_of_val(&stream_format) as u32, + ) + }; + check_status("AudioUnitSetProperty(StreamFormat)", status)?; + + let callback = AURenderCallbackStruct { + inputProc: Some(render_callback), + inputProcRefCon: (&mut *output.callback_context as *mut AudioCallbackContext).cast(), + }; + let status = unsafe { + AudioUnitSetProperty( + output.unit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + 0, + (&callback as *const AURenderCallbackStruct).cast(), + mem::size_of_val(&callback) as u32, + ) + }; + check_status("AudioUnitSetProperty(SetRenderCallback)", status)?; + + check_status("AudioUnitInitialize", unsafe { + AudioUnitInitialize(output.unit) + })?; + output.initialized = true; + check_status("AudioOutputUnitStart", unsafe { + AudioOutputUnitStart(output.unit) + })?; + output.started = true; + Ok(output) + } + + fn set_paused(&mut self, paused: bool) -> Result<(), BackendError> { + if paused && self.started { + check_status("AudioOutputUnitStop", unsafe { + AudioOutputUnitStop(self.unit) + })?; + self.started = false; + } else if !paused && !self.started { + check_status("AudioOutputUnitStart", unsafe { + AudioOutputUnitStart(self.unit) + })?; + self.started = true; + } + Ok(()) + } +} + +impl Drop for AudioOutput { + fn drop(&mut self) { + if self.started { + let _ = unsafe { AudioOutputUnitStop(self.unit) }; + self.started = false; + } + if self.initialized { + let _ = unsafe { AudioUnitUninitialize(self.unit) }; + self.initialized = false; + } + if !self.unit.is_null() { + let _ = unsafe { AudioComponentInstanceDispose(self.unit) }; + self.unit = ptr::null_mut(); + } + } +} + +unsafe extern "C-unwind" fn render_callback( + context: NonNull, + mut action_flags: NonNull, + _timestamp: NonNull, + _bus_number: u32, + frame_count: u32, + buffers: *mut AudioBufferList, +) -> i32 { + let context = unsafe { context.cast::().as_ref() }; + let Some(buffers) = NonNull::new(buffers) else { + return -50; + }; + let buffers = unsafe { buffers.as_ref() }; + if buffers.mNumberBuffers == 0 { + return -50; + } + let buffer = &buffers.mBuffers[0]; + let requested = frame_count as usize * context.channels; + let available_samples = buffer.mDataByteSize as usize / mem::size_of::(); + let sample_count = requested.min(available_samples); + let Some(data) = NonNull::new(buffer.mData.cast::()) else { + return -50; + }; + let output = unsafe { std::slice::from_raw_parts_mut(data.as_ptr(), sample_count) }; + let read = context.ring.pop_into(output); + output[read..].fill(0.0); + if read < requested { + context.counters.pcm_underrun_frames.fetch_add( + ((requested - read) / context.channels) as u64, + Ordering::Relaxed, + ); + } + if read == 0 { + unsafe { + action_flags + .as_mut() + .insert(AudioUnitRenderActionFlags::UnitRenderAction_OutputIsSilence) + }; + } + 0 +} + +fn check_status(api: &'static str, status: i32) -> Result<(), BackendError> { + if status == 0 { + Ok(()) + } else { + Err(BackendError::AppleApi { api, status }) + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/mod.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/mod.rs new file mode 100644 index 000000000..97ce03e17 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/mod.rs @@ -0,0 +1,551 @@ +mod audio; +mod presentation; +mod surface; +mod video; + +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; + +use objc2::MainThreadMarker; +use objc2_metal::{MTLCreateSystemDefaultDevice, MTLDevice}; +use thiserror::Error; + +use crate::format::{ + AudioFormat, BackendConfig, FormatError, FrameTiming, H264Format, H264Framing, RendererRect, + ScreenRect, access_unit_to_avcc, +}; +use crate::lifecycle::{BackendState, Lifecycle}; +use crate::queue::{BoundedQueue, PushResult}; + +use self::audio::AudioPipeline; +use self::presentation::PresenterHandle; +use self::surface::SurfaceOwner; +use self::video::{DecodedFrame, VideoDecoder}; + +const MAX_OPUS_PACKET_BYTES: usize = 1_275; + +/// Shows a standalone overlay window through the exact production creation path. +/// Debug-only aid for isolating window-server behavior without a streaming session. +pub fn debug_show_overlay_window() { + let Some(main_thread) = MainThreadMarker::new() else { + return; + }; + surface::debug_overlay_window(main_thread); +} + +/// Drains pending AppKit events and window-server work on the main thread. +/// +/// The streamer's main thread runs the host command loop instead of `NSApplication.run()`, so +/// window ordering, compositing, and window controls only make progress when this is called. +pub fn pump_app_events() { + use objc2_app_kit::{NSApplication, NSEventMask}; + + let Some(main_thread) = MainThreadMarker::new() else { + return; + }; + let application = NSApplication::sharedApplication(main_thread); + unsafe { + while let Some(event) = application.nextEventMatchingMask_untilDate_inMode_dequeue( + NSEventMask::Any, + None, + objc2_foundation::NSDefaultRunLoopMode, + true, + ) { + application.sendEvent(&event); + } + } + application.updateWindows(); +} + +#[derive(Debug, Error)] +pub enum BackendError { + #[error(transparent)] + Format(#[from] FormatError), + #[error("the operation must run on the AppKit main thread")] + MainThreadRequired, + #[error("the backend is stopping or stopped")] + Stopped, + #[error("H.264 access unit is {actual} bytes; configured maximum is {maximum}")] + AccessUnitTooLarge { actual: usize, maximum: usize }, + #[error("Opus packet is {0} bytes; the maximum is 1275")] + OpusPacketTooLarge(usize), + #[error("Opus packet is empty")] + EmptyOpusPacket, + #[error("{api} failed with OSStatus {status}")] + AppleApi { api: &'static str, status: i32 }, + #[error("{0}")] + Metal(String), + #[error("Opus decoder failed: {0}")] + Opus(String), + #[error("failed to start {0} worker thread")] + Thread(&'static str), + #[error("the supplied NSWindow has no content view")] + MissingContentView, + #[error("surface layout updates require a supplied NSWindow target")] + NotWindowSurface, + #[error("surface updates require an owned overlay target")] + NotOwnedOverlay, + #[error("macOS did not report a primary screen")] + MissingPrimaryScreen, +} + +#[link(name = "VideoToolbox", kind = "framework")] +unsafe extern "C" { + fn VTIsHardwareDecodeSupported(codec_type: u32) -> u8; +} + +pub fn probe_h264_hardware() -> bool { + const H264_CODEC_TYPE: u32 = u32::from_be_bytes(*b"avc1"); + if unsafe { VTIsHardwareDecodeSupported(H264_CODEC_TYPE) } == 0 { + return false; + } + MTLCreateSystemDefaultDevice().is_some_and(|device| device.newCommandQueue().is_some()) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SubmitOutcome { + Accepted, + ReplacedOldest, + Backpressured, + Paused, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct BackendStats { + pub video_submitted: u64, + pub video_backpressured: u64, + pub video_decoded: u64, + pub video_decode_errors: u64, + pub video_frames_dropped: u64, + pub video_presented: u64, + pub video_present_errors: u64, + pub opus_submitted: u64, + pub opus_packets_dropped: u64, + pub opus_decode_errors: u64, + pub pcm_samples_dropped: u64, + pub pcm_underrun_frames: u64, +} + +#[derive(Default)] +pub(super) struct Counters { + video_submitted: AtomicU64, + video_backpressured: AtomicU64, + video_decoded: AtomicU64, + video_decode_errors: AtomicU64, + video_frames_dropped: AtomicU64, + video_presented: AtomicU64, + video_present_errors: AtomicU64, + opus_submitted: AtomicU64, + opus_packets_dropped: AtomicU64, + opus_decode_errors: AtomicU64, + pcm_samples_dropped: AtomicU64, + pcm_underrun_frames: AtomicU64, +} + +impl Counters { + fn snapshot(&self) -> BackendStats { + BackendStats { + video_submitted: self.video_submitted.load(Ordering::Relaxed), + video_backpressured: self.video_backpressured.load(Ordering::Relaxed), + video_decoded: self.video_decoded.load(Ordering::Relaxed), + video_decode_errors: self.video_decode_errors.load(Ordering::Relaxed), + video_frames_dropped: self.video_frames_dropped.load(Ordering::Relaxed), + video_presented: self.video_presented.load(Ordering::Relaxed), + video_present_errors: self.video_present_errors.load(Ordering::Relaxed), + opus_submitted: self.opus_submitted.load(Ordering::Relaxed), + opus_packets_dropped: self.opus_packets_dropped.load(Ordering::Relaxed), + opus_decode_errors: self.opus_decode_errors.load(Ordering::Relaxed), + pcm_samples_dropped: self.pcm_samples_dropped.load(Ordering::Relaxed), + pcm_underrun_frames: self.pcm_underrun_frames.load(Ordering::Relaxed), + } + } +} + +/// Raw AppKit handles owned or retained by a running backend. +/// +/// Both pointers remain valid only until [`MacOsBackend::stop`] or `Drop`. Callers must use them +/// on AppKit's main thread and must not transfer ownership or release them. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct NativeSurfaceHandle { + pub ns_window: Option>, + /// The dedicated presentation view. For `SurfaceTarget::NsWindow`, this is the backend-owned + /// passive child view, never the supplied window's content view. + pub ns_view: std::ptr::NonNull, +} + +/// Thread-safe encoded media input for a running [`MacOsBackend`]. +#[derive(Clone)] +pub struct StreamSink { + shared: Arc, +} + +impl StreamSink { + pub fn submit_h264( + &self, + access_unit: &[u8], + framing: H264Framing, + timing: FrameTiming, + ) -> Result { + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + if self.shared.paused.load(Ordering::Acquire) { + return Ok(SubmitOutcome::Paused); + } + timing.validate()?; + if access_unit.len() > self.shared.max_video_access_unit_bytes { + return Err(BackendError::AccessUnitTooLarge { + actual: access_unit.len(), + maximum: self.shared.max_video_access_unit_bytes, + }); + } + let avcc = access_unit_to_avcc(access_unit, framing)?; + let decoder = self + .shared + .video + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + if self.shared.paused.load(Ordering::Acquire) { + return Ok(SubmitOutcome::Paused); + } + let decoder = decoder.as_ref().ok_or(BackendError::Stopped)?; + if !decoder.submit(&avcc, timing)? { + self.shared + .counters + .video_backpressured + .fetch_add(1, Ordering::Relaxed); + return Ok(SubmitOutcome::Backpressured); + } + self.shared + .counters + .video_submitted + .fetch_add(1, Ordering::Relaxed); + Ok(SubmitOutcome::Accepted) + } + + pub fn submit_opus(&self, packet: &[u8]) -> Result { + if packet.is_empty() { + return Err(BackendError::EmptyOpusPacket); + } + if packet.len() > MAX_OPUS_PACKET_BYTES { + return Err(BackendError::OpusPacketTooLarge(packet.len())); + } + self.submit_opus_owned(packet.to_vec()) + } + + fn submit_opus_owned(&self, packet: Vec) -> Result { + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + if self.shared.paused.load(Ordering::Acquire) { + return Ok(SubmitOutcome::Paused); + } + let audio = self + .shared + .audio + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + if self.shared.paused.load(Ordering::Acquire) { + return Ok(SubmitOutcome::Paused); + } + let audio = audio.as_ref().ok_or(BackendError::Stopped)?; + let outcome = match audio.submit(packet) { + PushResult::Pushed => SubmitOutcome::Accepted, + PushResult::Replaced(_) => { + self.shared + .counters + .opus_packets_dropped + .fetch_add(1, Ordering::Relaxed); + SubmitOutcome::ReplacedOldest + } + PushResult::Closed(_) => return Err(BackendError::Stopped), + }; + self.shared + .counters + .opus_submitted + .fetch_add(1, Ordering::Relaxed); + Ok(outcome) + } + + pub fn reconfigure_h264(&self, format: H264Format) -> Result<(), BackendError> { + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + let replacement = VideoDecoder::new( + &format, + self.shared.video_queue.clone(), + Arc::clone(&self.shared.counters), + self.shared.video_frames_in_flight, + )?; + let mut decoder = self + .shared + .video + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if self.shared.lifecycle.state() != BackendState::Running { + drop(replacement); + return Err(BackendError::Stopped); + } + let previous = decoder.replace(replacement); + drop(previous); + let discarded = self.shared.video_queue.clear(); + self.shared + .counters + .video_frames_dropped + .fetch_add(discarded as u64, Ordering::Relaxed); + drop(decoder); + Ok(()) + } + + pub fn reconfigure_audio(&self, format: AudioFormat) -> Result<(), BackendError> { + format.validate()?; + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + let mut audio = self + .shared + .audio + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + if let Some(previous) = audio.take() { + previous.stop(); + } + *audio = Some(AudioPipeline::start( + format, + self.shared.opus_packets, + self.shared.pcm_milliseconds, + Arc::clone(&self.shared.counters), + )?); + Ok(()) + } + + pub fn state(&self) -> BackendState { + self.shared.lifecycle.state() + } + + pub fn stats(&self) -> BackendStats { + self.shared.counters.snapshot() + } +} + +struct Shared { + lifecycle: Lifecycle, + paused: AtomicBool, + counters: Arc, + video_queue: Arc>, + video: Mutex>, + audio: Mutex>, + presenter: Mutex>, + video_frames_in_flight: usize, + opus_packets: usize, + pcm_milliseconds: u32, + max_video_access_unit_bytes: usize, +} + +impl Shared { + fn stop(&self) { + if !self.lifecycle.begin_stop() { + return; + } + let decoder = self + .video + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take(); + drop(decoder); + if let Some(audio) = self + .audio + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take() + { + audio.stop(); + } + if let Some(presenter) = self + .presenter + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .take() + { + presenter.stop(); + } + self.lifecycle.finish_stop(); + } +} + +/// Main-thread owner for the native macOS backend. +pub struct MacOsBackend { + shared: Arc, + surface: Option, + _main_thread_only: PhantomData>, +} + +impl MacOsBackend { + pub fn start(config: BackendConfig) -> Result { + let main_thread = MainThreadMarker::new().ok_or(BackendError::MainThreadRequired)?; + config.validate()?; + let surface = SurfaceOwner::attach(config.surface, main_thread)?; + let counters = Arc::new(Counters::default()); + let video_queue = Arc::new(BoundedQueue::new(config.queues.decoded_video_frames)); + let presenter = PresenterHandle::start( + surface.metal_layer(), + surface.presentation_visibility(), + Arc::clone(&video_queue), + Arc::clone(&counters), + )?; + let video = VideoDecoder::new( + &config.video, + Arc::clone(&video_queue), + Arc::clone(&counters), + config.queues.video_frames_in_flight, + )?; + let audio = AudioPipeline::start( + config.audio, + config.queues.opus_packets, + config.queues.pcm_milliseconds, + Arc::clone(&counters), + )?; + let shared = Arc::new(Shared { + lifecycle: Lifecycle::running(), + paused: AtomicBool::new(false), + counters, + video_queue, + video: Mutex::new(Some(video)), + audio: Mutex::new(Some(audio)), + presenter: Mutex::new(Some(presenter)), + video_frames_in_flight: config.queues.video_frames_in_flight, + opus_packets: config.queues.opus_packets, + pcm_milliseconds: config.queues.pcm_milliseconds, + max_video_access_unit_bytes: config.queues.max_video_access_unit_bytes, + }); + Ok(Self { + shared, + surface: Some(surface), + _main_thread_only: PhantomData, + }) + } + + pub fn sink(&self) -> StreamSink { + StreamSink { + shared: Arc::clone(&self.shared), + } + } + + pub fn native_surface(&self) -> Option { + self.surface.as_ref().map(SurfaceOwner::native_handle) + } + + /// Updates a supplied-window child surface in renderer-relative, top-left AppKit points. + /// + /// This method is available only for `SurfaceTarget::NsWindow` and returns + /// `MainThreadRequired` instead of dispatching implicitly when called off AppKit's main thread. + pub fn update_window_surface( + &mut self, + bounds: RendererRect, + visible: bool, + ) -> Result<(), BackendError> { + let main_thread = MainThreadMarker::new().ok_or(BackendError::MainThreadRequired)?; + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + bounds.validate()?; + self.surface + .as_mut() + .ok_or(BackendError::Stopped)? + .update_window_child(bounds, visible, main_thread) + } + + /// Repositions the process-owned passive overlay using absolute Electron screen coordinates. + pub fn update_owned_overlay( + &mut self, + screen_rect: ScreenRect, + visible: bool, + ) -> Result<(), BackendError> { + let main_thread = MainThreadMarker::new().ok_or(BackendError::MainThreadRequired)?; + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + screen_rect.validate()?; + self.surface + .as_mut() + .ok_or(BackendError::Stopped)? + .update_owned_overlay(screen_rect, visible, main_thread) + } + + pub fn refresh_overlay_ordering(&mut self) -> Result<(), BackendError> { + let _main_thread = MainThreadMarker::new().ok_or(BackendError::MainThreadRequired)?; + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + self.surface + .as_mut() + .ok_or(BackendError::Stopped)? + .refresh_overlay_ordering() + } + + pub fn state(&self) -> BackendState { + self.shared.lifecycle.state() + } + + pub fn stats(&self) -> BackendStats { + self.shared.counters.snapshot() + } + + pub fn set_paused(&mut self, paused: bool) -> Result<(), BackendError> { + let _main_thread = MainThreadMarker::new().ok_or(BackendError::MainThreadRequired)?; + if self.shared.lifecycle.state() != BackendState::Running { + return Err(BackendError::Stopped); + } + if paused { + self.shared.paused.store(true, Ordering::Release); + } + let discarded = self.shared.video_queue.clear(); + self.shared + .counters + .video_frames_dropped + .fetch_add(discarded as u64, Ordering::Relaxed); + if let Some(audio) = self + .shared + .audio + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_mut() + { + audio.set_paused(paused)?; + } + if !paused { + self.shared.paused.store(false, Ordering::Release); + } + Ok(()) + } + + pub fn stop(&mut self) { + self.shared.stop(); + if let Some(mut surface) = self.surface.take() { + surface.detach(); + } + } +} + +impl Drop for MacOsBackend { + fn drop(&mut self) { + self.stop(); + } +} + +#[allow(dead_code)] +fn assert_stream_sink_is_send_and_sync() { + fn assert_send_sync() {} + assert_send_sync::(); +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/presentation.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/presentation.rs new file mode 100644 index 000000000..d93b3f660 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/presentation.rs @@ -0,0 +1,405 @@ +use std::collections::VecDeque; +use std::ffi::c_void; +use std::ptr::{self, NonNull}; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::thread::{self, JoinHandle}; + +use objc2::rc::{Retained, autoreleasepool}; +use objc2::runtime::ProtocolObject; +use objc2_core_foundation::CFRetained; +use objc2_core_video::{ + CVMetalTexture, CVMetalTextureCache, CVMetalTextureGetTexture, CVPixelBufferGetHeightOfPlane, + CVPixelBufferGetPixelFormatType, CVPixelBufferGetPlaneCount, CVPixelBufferGetWidthOfPlane, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, +}; +use objc2_foundation::NSString; +use objc2_metal::{ + MTLClearColor, MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, + MTLCreateSystemDefaultDevice, MTLDevice, MTLDrawable, MTLLibrary, MTLLoadAction, + MTLPixelFormat, MTLPrimitiveType, MTLRenderCommandEncoder, MTLRenderPassDescriptor, + MTLRenderPipelineDescriptor, MTLRenderPipelineState, MTLStoreAction, MTLTexture, MTLViewport, +}; +use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; + +use crate::format::VideoColorSpace; +use crate::queue::BoundedQueue; + +use super::video::DecodedFrame; +use super::{BackendError, Counters}; + +const SHADER_SOURCE: &str = r#" +#include +using namespace metal; + +struct VertexOut { + float4 position [[position]]; + float2 texcoord; +}; + +vertex VertexOut video_vertex(uint vertex_id [[vertex_id]]) { + const float2 positions[3] = { float2(-1.0, -1.0), float2(3.0, -1.0), float2(-1.0, 3.0) }; + const float2 texcoords[3] = { float2(0.0, 1.0), float2(2.0, 1.0), float2(0.0, -1.0) }; + VertexOut out; + out.position = float4(positions[vertex_id], 0.0, 1.0); + out.texcoord = texcoords[vertex_id]; + return out; +} + +fragment float4 video_fragment( + VertexOut in [[stage_in]], + texture2d luma [[texture(0)]], + texture2d chroma [[texture(1)]], + constant uint &color_space [[buffer(0)]]) { + constexpr sampler linear_sampler(coord::normalized, address::clamp_to_edge, filter::linear); + float y = (luma.sample(linear_sampler, in.texcoord).r - (16.0 / 255.0)) * (255.0 / 219.0); + float2 cbcr = chroma.sample(linear_sampler, in.texcoord).rg - float2(0.5); + float3 rgb; + if (color_space == 0) { + rgb = float3( + y + 1.596027 * cbcr.y, + y - 0.391762 * cbcr.x - 0.812968 * cbcr.y, + y + 2.017232 * cbcr.x); + } else { + rgb = float3( + y + 1.792741 * cbcr.y, + y - 0.213249 * cbcr.x - 0.532909 * cbcr.y, + y + 2.112402 * cbcr.x); + } + return float4(saturate(rgb), 1.0); +} +"#; + +pub(super) struct PresenterHandle { + queue: Arc>, + worker: Option>, +} + +impl PresenterHandle { + pub(super) fn start( + layer: Retained, + visible: Arc, + queue: Arc>, + counters: Arc, + ) -> Result { + let mut presenter = MetalPresenter::new(layer)?; + let worker_queue = Arc::clone(&queue); + let worker = thread::Builder::new() + .name("opennow-metal-present".into()) + .spawn(move || { + while let Some(frame) = worker_queue.pop_wait() { + if !visible.load(Ordering::Acquire) { + counters + .video_frames_dropped + .fetch_add(1, Ordering::Relaxed); + continue; + } + let result = autoreleasepool(|_| presenter.present(frame)); + match result { + Ok(()) => { + counters.video_presented.fetch_add(1, Ordering::Relaxed); + } + Err(_) => { + counters + .video_present_errors + .fetch_add(1, Ordering::Relaxed); + } + } + } + presenter.finish(); + }) + .map_err(|_| BackendError::Thread("Metal presenter"))?; + Ok(Self { + queue, + worker: Some(worker), + }) + } + + pub(super) fn stop(mut self) { + self.queue.close_and_discard(); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +impl Drop for PresenterHandle { + fn drop(&mut self) { + self.queue.close_and_discard(); + if let Some(worker) = self.worker.take() { + let _ = worker.join(); + } + } +} + +struct PendingFrame { + command_buffer: Retained>, + _luma_cv_texture: CFRetained, + _chroma_cv_texture: CFRetained, + _luma_texture: Retained>, + _chroma_texture: Retained>, +} + +struct MetalPresenter { + layer: Retained, + command_queue: Retained>, + pipeline: Retained>, + texture_cache: CFRetained, + pending: VecDeque, +} + +// Metal objects are thread-safe. AppKit attaches and detaches the layer on the main thread; after +// construction this worker only uses CAMetalLayer's thread-safe drawable API. +unsafe impl Send for MetalPresenter {} + +impl MetalPresenter { + fn new(layer: Retained) -> Result { + let device = MTLCreateSystemDefaultDevice() + .ok_or_else(|| BackendError::Metal("Metal is unavailable on this Mac".into()))?; + layer.setDevice(Some(&device)); + layer.setPixelFormat(MTLPixelFormat::BGRA8Unorm); + layer.setFramebufferOnly(true); + layer.setMaximumDrawableCount(3); + layer.setPresentsWithTransaction(false); + layer.setDisplaySyncEnabled(true); + layer.setAllowsNextDrawableTimeout(true); + + let command_queue = device + .newCommandQueue() + .ok_or_else(|| BackendError::Metal("failed to create Metal command queue".into()))?; + let source = NSString::from_str(SHADER_SOURCE); + let library = device + .newLibraryWithSource_options_error(&source, None) + .map_err(|error| BackendError::Metal(error.localizedDescription().to_string()))?; + let vertex = library + .newFunctionWithName(&NSString::from_str("video_vertex")) + .ok_or_else(|| BackendError::Metal("video_vertex shader is missing".into()))?; + let fragment = library + .newFunctionWithName(&NSString::from_str("video_fragment")) + .ok_or_else(|| BackendError::Metal("video_fragment shader is missing".into()))?; + let descriptor = MTLRenderPipelineDescriptor::new(); + descriptor.setVertexFunction(Some(&vertex)); + descriptor.setFragmentFunction(Some(&fragment)); + let attachments = descriptor.colorAttachments(); + let attachment = unsafe { attachments.objectAtIndexedSubscript(0) }; + attachment.setPixelFormat(MTLPixelFormat::BGRA8Unorm); + let pipeline = device + .newRenderPipelineStateWithDescriptor_error(&descriptor) + .map_err(|error| BackendError::Metal(error.localizedDescription().to_string()))?; + + let mut cache_ptr = ptr::null_mut(); + let status = unsafe { + CVMetalTextureCache::create(None, None, &device, None, NonNull::from(&mut cache_ptr)) + }; + if status != 0 { + return Err(BackendError::AppleApi { + api: "CVMetalTextureCacheCreate", + status, + }); + } + let cache_ptr = NonNull::new(cache_ptr).ok_or(BackendError::AppleApi { + api: "CVMetalTextureCacheCreate", + status: -1, + })?; + let texture_cache = unsafe { CFRetained::from_raw(cache_ptr) }; + Ok(Self { + layer, + command_queue, + pipeline, + texture_cache, + pending: VecDeque::with_capacity(3), + }) + } + + fn present(&mut self, frame: DecodedFrame) -> Result<(), BackendError> { + if self.pending.len() >= 2 { + self.wait_for_oldest(); + } + if CVPixelBufferGetPixelFormatType(&frame.image) + != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + || CVPixelBufferGetPlaneCount(&frame.image) != 2 + { + return Err(BackendError::Metal( + "VideoToolbox returned a non-NV12 pixel buffer".into(), + )); + } + + let width = CVPixelBufferGetWidthOfPlane(&frame.image, 0); + let height = CVPixelBufferGetHeightOfPlane(&frame.image, 0); + let chroma_width = CVPixelBufferGetWidthOfPlane(&frame.image, 1); + let chroma_height = CVPixelBufferGetHeightOfPlane(&frame.image, 1); + let luma_cv_texture = + self.make_texture(&frame, MTLPixelFormat::R8Unorm, width, height, 0)?; + let chroma_cv_texture = self.make_texture( + &frame, + MTLPixelFormat::RG8Unorm, + chroma_width, + chroma_height, + 1, + )?; + let luma_texture = CVMetalTextureGetTexture(&luma_cv_texture) + .ok_or_else(|| BackendError::Metal("failed to get Metal luma texture".into()))?; + let chroma_texture = CVMetalTextureGetTexture(&chroma_cv_texture) + .ok_or_else(|| BackendError::Metal("failed to get Metal chroma texture".into()))?; + let drawable = self + .layer + .nextDrawable() + .ok_or_else(|| BackendError::Metal("CAMetalLayer has no drawable".into()))?; + let drawable_texture = drawable.texture(); + + let render_pass = MTLRenderPassDescriptor::renderPassDescriptor(); + let attachments = render_pass.colorAttachments(); + let attachment = unsafe { attachments.objectAtIndexedSubscript(0) }; + attachment.setTexture(Some(&drawable_texture)); + attachment.setLoadAction(MTLLoadAction::Clear); + attachment.setStoreAction(MTLStoreAction::Store); + attachment.setClearColor(MTLClearColor { + red: 0.0, + green: 0.0, + blue: 0.0, + alpha: 1.0, + }); + + let command_buffer = self + .command_queue + .commandBuffer() + .ok_or_else(|| BackendError::Metal("failed to create Metal command buffer".into()))?; + let encoder = command_buffer + .renderCommandEncoderWithDescriptor(&render_pass) + .ok_or_else(|| BackendError::Metal("failed to create Metal render encoder".into()))?; + encoder.setRenderPipelineState(&self.pipeline); + unsafe { + encoder.setFragmentTexture_atIndex(Some(&luma_texture), 0); + encoder.setFragmentTexture_atIndex(Some(&chroma_texture), 1); + } + let color_space = match frame.color_space { + VideoColorSpace::Bt601 => 0u32, + VideoColorSpace::Bt709 => 1u32, + }; + unsafe { + encoder.setFragmentBytes_length_atIndex( + NonNull::from(&color_space).cast::(), + std::mem::size_of_val(&color_space), + 0, + ) + }; + let destination_width = drawable_texture.width() as f64; + let destination_height = drawable_texture.height() as f64; + encoder.setViewport(aspect_fit_viewport( + width as f64, + height as f64, + destination_width, + destination_height, + )); + unsafe { encoder.drawPrimitives_vertexStart_vertexCount(MTLPrimitiveType::Triangle, 0, 3) }; + encoder.endEncoding(); + let drawable_ref: &ProtocolObject = &drawable; + let drawable_as_base: &ProtocolObject = drawable_ref.as_ref(); + command_buffer.presentDrawable(drawable_as_base); + command_buffer.commit(); + self.pending.push_back(PendingFrame { + command_buffer, + _luma_cv_texture: luma_cv_texture, + _chroma_cv_texture: chroma_cv_texture, + _luma_texture: luma_texture, + _chroma_texture: chroma_texture, + }); + Ok(()) + } + + fn make_texture( + &self, + frame: &DecodedFrame, + pixel_format: MTLPixelFormat, + width: usize, + height: usize, + plane: usize, + ) -> Result, BackendError> { + let mut texture_ptr = ptr::null_mut(); + let status = unsafe { + CVMetalTextureCache::create_texture_from_image( + None, + &self.texture_cache, + &frame.image, + None, + pixel_format, + width, + height, + plane, + NonNull::from(&mut texture_ptr), + ) + }; + if status != 0 { + return Err(BackendError::AppleApi { + api: "CVMetalTextureCacheCreateTextureFromImage", + status, + }); + } + let texture_ptr = NonNull::new(texture_ptr).ok_or(BackendError::AppleApi { + api: "CVMetalTextureCacheCreateTextureFromImage", + status: -1, + })?; + Ok(unsafe { CFRetained::from_raw(texture_ptr) }) + } + + fn wait_for_oldest(&mut self) { + if let Some(frame) = self.pending.pop_front() { + frame.command_buffer.waitUntilCompleted(); + } + } + + fn finish(&mut self) { + while !self.pending.is_empty() { + self.wait_for_oldest(); + } + self.texture_cache.flush(0); + } +} + +impl Drop for MetalPresenter { + fn drop(&mut self) { + self.finish(); + } +} + +fn aspect_fit_viewport( + source_width: f64, + source_height: f64, + destination_width: f64, + destination_height: f64, +) -> MTLViewport { + let source_aspect = source_width / source_height; + let destination_aspect = destination_width / destination_height; + let (width, height) = if source_aspect > destination_aspect { + (destination_width, destination_width / source_aspect) + } else { + (destination_height * source_aspect, destination_height) + }; + MTLViewport { + originX: (destination_width - width) * 0.5, + originY: (destination_height - height) * 0.5, + width, + height, + znear: 0.0, + zfar: 1.0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aspect_fit_letterboxes_without_distortion() { + let viewport = aspect_fit_viewport(1920.0, 1080.0, 1024.0, 768.0); + assert_eq!(viewport.width, 1024.0); + assert_eq!(viewport.height, 576.0); + assert_eq!(viewport.originY, 96.0); + + let portrait = aspect_fit_viewport(1080.0, 1920.0, 1024.0, 768.0); + assert_eq!(portrait.height, 768.0); + assert_eq!(portrait.width, 432.0); + assert_eq!(portrait.originX, 296.0); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/surface.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/surface.rs new file mode 100644 index 000000000..d9ef48bda --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/surface.rs @@ -0,0 +1,409 @@ +use std::ffi::c_void; +use std::ptr::NonNull; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +use objc2::rc::Retained; +use objc2::{MainThreadMarker, MainThreadOnly, define_class, msg_send}; +use objc2_app_kit::{ + NSApplication, NSBackingStoreType, NSColor, NSScreen, NSView, NSWindow, NSWindowStyleMask, + NSWorkspace, +}; +use objc2_core_foundation::{CGPoint, CGRect, CGSize}; +use objc2_quartz_core::{CALayer, CAMetalLayer}; + +use crate::format::{RendererRect, ScreenRect, SurfaceTarget}; +use crate::lifecycle::AttachmentLifecycle; + +use super::{BackendError, NativeSurfaceHandle}; + +define_class!( + // NSView has no additional initialization or destruction requirements for a subclass with no + // ivars. MainThreadOnly preserves AppKit's thread contract. + #[unsafe(super(NSView))] + #[thread_kind = MainThreadOnly] + struct PassiveSurfaceView; + + impl PassiveSurfaceView { + // Returning nil lets hit testing continue through the BrowserWindow content view instead + // of routing pointer, gesture, or drag input to the video surface. + #[unsafe(method(hitTest:))] + fn hit_test(&self, _point: CGPoint) -> Option<&NSView> { + None + } + + #[unsafe(method(acceptsFirstResponder))] + fn accepts_first_responder(&self) -> bool { + false + } + + #[unsafe(method(becomeFirstResponder))] + fn become_first_responder(&self) -> bool { + false + } + + #[unsafe(method(canBecomeKeyView))] + fn can_become_key_view(&self) -> bool { + false + } + } +); + +enum Attachment { + Dedicated { + previous_layer: Option>, + previous_wants_layer: bool, + }, + WindowChild { + parent: Retained, + }, +} + +/// Creates the overlay window exactly as `SurfaceOwner::attach` does for `OwnedOverlay`, +/// for isolating window-server behavior without a streaming session. +pub(super) fn debug_overlay_window(main_thread: MainThreadMarker) { + let application = NSApplication::sharedApplication(main_thread); + application.setActivationPolicy(objc2_app_kit::NSApplicationActivationPolicy::Accessory); + application.finishLaunching(); + application.activate(); + let rect = ScreenRect::new(200.0, 167.0, 1400.0, 868.0); + let styles = + NSWindowStyleMask::Titled | NSWindowStyleMask::Closable | NSWindowStyleMask::Resizable; + let window: Retained = unsafe { + msg_send![ + main_thread.alloc::(), + initWithContentRect: appkit_screen_frame(rect, main_thread).expect("screen frame"), + styleMask: styles, + backing: NSBackingStoreType::Buffered, + defer: false + ] + }; + window.setTitle(&objc2_foundation::NSString::from_str("OpenNOW Video")); + window.orderFrontRegardless(); + eprintln!("NVST debug-overlay-window created"); + std::mem::forget(window); +} + +pub(super) struct SurfaceOwner { + window: Option>, + view: Retained, + layer: Retained, + attachment: Attachment, + visible: Arc, + requested_visible: bool, + parent_pid: Option, + owns_window: bool, + overlay: bool, + lifecycle: AttachmentLifecycle, +} + +impl SurfaceOwner { + pub(super) fn attach( + target: SurfaceTarget, + main_thread: MainThreadMarker, + ) -> Result { + let (window, view, owns_window, overlay, child_parent, requested_visible, parent_pid) = + match target { + SurfaceTarget::OwnedOverlay(config) => { + let application = NSApplication::sharedApplication(main_thread); + // A bare executable defaults to NSApplicationActivationPolicyProhibited, + // which makes the window server refuse to show any of its windows. + // Accessory lets the overlay appear without a dock icon or menu bar. + application.setActivationPolicy( + objc2_app_kit::NSApplicationActivationPolicy::Accessory, + ); + application.finishLaunching(); + application.activate(); + config.validate()?; + let parent_pid = unsafe { libc::getppid() }; + let frontmost = application_is_frontmost(parent_pid); + // External-window mode: visibility is driven purely by the + // renderer's requested state; the frontmost-app gate is + // disabled so the video stays visible while debugging. + let visible = config.visible; + eprintln!( + "NVST overlay-attach rect=({},{} {}x{}) config.visible={} parent_pid={parent_pid} frontmost={frontmost} visible={visible}", + config.screen_rect.x, config.screen_rect.y, config.screen_rect.width, config.screen_rect.height, config.visible, + ); + let styles = NSWindowStyleMask::Titled + | NSWindowStyleMask::Closable + | NSWindowStyleMask::Resizable; + // A runtime-defined NSWindow/NSPanel subclass never composites in this + // process (the window server lists it but keeps it offscreen and + // unsynced); a plain NSWindow through the same init path works. + let window: Retained = unsafe { + msg_send![ + main_thread.alloc::(), + initWithContentRect: appkit_screen_frame(config.screen_rect, main_thread)?, + styleMask: styles, + backing: NSBackingStoreType::Buffered, + defer: false + ] + }; + unsafe { window.setReleasedWhenClosed(false) }; + window.setTitle(&objc2_foundation::NSString::from_str("OpenNOW Video")); + window.setIgnoresMouseEvents(false); + window.setAcceptsMouseMovedEvents(false); + window.setHasShadow(true); + window.setOpaque(true); + window.setBackgroundColor(Some(&NSColor::blackColor())); + let view = window + .contentView() + .ok_or(BackendError::MissingContentView)?; + if visible { + window.orderFrontRegardless(); + } else { + window.orderOut(None); + } + ( + Some(window), + view, + true, + true, + None, + config.visible, + Some(parent_pid), + ) + } + SurfaceTarget::NsView(view) => { + let view = unsafe { Retained::retain(view.as_ptr().as_ptr().cast::()) } + .ok_or(BackendError::MissingContentView)?; + let window = view.window(); + (window, view, false, false, None, true, None) + } + SurfaceTarget::NsWindow(config) => { + config.validate()?; + let window = unsafe { + Retained::retain(config.window.as_ptr().as_ptr().cast::()) + } + .ok_or(BackendError::MissingContentView)?; + let parent = window + .contentView() + .ok_or(BackendError::MissingContentView)?; + let frame = appkit_frame(&parent, config.bounds); + let child: Retained = unsafe { + msg_send![main_thread.alloc::(), initWithFrame: frame] + }; + child.setHidden(!config.visible); + ( + Some(window), + child.into_super(), + false, + false, + Some(parent), + config.visible, + None, + ) + } + }; + + let attachment = if let Some(parent) = child_parent { + Attachment::WindowChild { parent } + } else { + Attachment::Dedicated { + previous_layer: view.layer(), + previous_wants_layer: view.wantsLayer(), + } + }; + let layer = CAMetalLayer::new(); + layer.setFrame(view.bounds()); + let scale = window + .as_ref() + .map_or(1.0, |window| window.backingScaleFactor()); + layer.setContentsScale(scale); + view.setWantsLayer(true); + view.setLayer(Some(&layer)); + if let Attachment::WindowChild { parent } = &attachment { + parent.addSubview(&view); + } + + Ok(Self { + window, + view, + layer, + attachment, + visible: Arc::new(AtomicBool::new( + requested_visible && parent_pid.is_none_or(application_is_frontmost), + )), + requested_visible, + parent_pid, + owns_window, + overlay, + lifecycle: AttachmentLifecycle::attached(), + }) + } + + pub(super) fn metal_layer(&self) -> Retained { + self.layer.clone() + } + + pub(super) fn presentation_visibility(&self) -> Arc { + Arc::clone(&self.visible) + } + + pub(super) fn native_handle(&self) -> NativeSurfaceHandle { + NativeSurfaceHandle { + ns_window: self.window.as_ref().map(|window| { + NonNull::new(Retained::as_ptr(window).cast_mut().cast::()) + .expect("retained NSWindow is non-null") + }), + ns_view: NonNull::new(Retained::as_ptr(&self.view).cast_mut().cast::()) + .expect("retained NSView is non-null"), + } + } + + pub(super) fn update_window_child( + &mut self, + bounds: RendererRect, + visible: bool, + _main_thread: MainThreadMarker, + ) -> Result<(), BackendError> { + bounds.validate()?; + let Attachment::WindowChild { parent } = &self.attachment else { + return Err(BackendError::NotWindowSurface); + }; + self.view.setFrame(appkit_frame(parent, bounds)); + self.view.setHidden(!visible); + self.layer.setFrame(self.view.bounds()); + self.visible.store(visible, Ordering::Release); + Ok(()) + } + + pub(super) fn update_owned_overlay( + &mut self, + screen_rect: ScreenRect, + visible: bool, + main_thread: MainThreadMarker, + ) -> Result<(), BackendError> { + if !self.overlay { + return Err(BackendError::NotOwnedOverlay); + } + screen_rect.validate()?; + let window = self.window.as_ref().ok_or(BackendError::Stopped)?; + window.setFrame_display(appkit_screen_frame(screen_rect, main_thread)?, true); + self.layer.setFrame(self.view.bounds()); + self.layer.setContentsScale(window.backingScaleFactor()); + self.requested_visible = visible; + let frontmost = self.parent_pid.is_some_and(application_is_frontmost); + let ordered = visible; + eprintln!( + "NVST overlay-update rect=({},{} {}x{}) requested_visible={visible} frontmost={frontmost} ordered={ordered}", + screen_rect.x, screen_rect.y, screen_rect.width, screen_rect.height, + ); + self.visible.store(ordered, Ordering::Release); + if ordered { + window.orderFrontRegardless(); + } else { + window.orderOut(None); + } + Ok(()) + } + + pub(super) fn refresh_overlay_ordering(&mut self) -> Result<(), BackendError> { + if !self.overlay { + return Ok(()); + } + let window = self.window.as_ref().ok_or(BackendError::Stopped)?; + let raw_frontmost = NSWorkspace::sharedWorkspace() + .frontmostApplication() + .map(|application| application.processIdentifier()); + let frontmost = self.parent_pid.is_some_and(application_is_frontmost); + let ordered = self.requested_visible; + static POLL_LOG: AtomicU64 = AtomicU64::new(0); + let tick = POLL_LOG.fetch_add(1, Ordering::Relaxed); + if tick % 20 == 0 { + eprintln!( + "NVST overlay-poll parent={:?} raw_frontmost={raw_frontmost:?} requested_visible={} visible={}", + self.parent_pid, + self.requested_visible, + self.visible.load(Ordering::Acquire), + ); + } + // Re-assert ordering every poll: the initial orderFrontRegardless can be lost when it + // races the app's launch registration with the window server, and re-ordering is cheap. + if ordered { + window.orderFrontRegardless(); + } + if self.visible.swap(ordered, Ordering::AcqRel) == ordered { + return Ok(()); + } + eprintln!( + "NVST overlay-ordering-change requested_visible={} frontmost={frontmost} ordered={ordered}", + self.requested_visible, + ); + if !ordered { + window.orderOut(None); + } + Ok(()) + } + + pub(super) fn detach(&mut self) { + if !self.lifecycle.begin_detach() { + return; + } + match &self.attachment { + Attachment::Dedicated { + previous_layer, + previous_wants_layer, + } => { + if self.view.layer().is_some_and(|current| { + Retained::as_ptr(¤t) == Retained::as_ptr(&self.layer).cast() + }) { + self.view.setLayer(previous_layer.as_deref()); + self.view.setWantsLayer(*previous_wants_layer); + } + } + Attachment::WindowChild { .. } => self.view.removeFromSuperview(), + } + if self.owns_window { + if let Some(window) = &self.window { + window.close(); + } + } + } +} + +impl Drop for SurfaceOwner { + fn drop(&mut self) { + self.detach(); + } +} + +fn appkit_frame(parent: &NSView, bounds: RendererRect) -> CGRect { + let parent_bounds = parent.bounds(); + let parent_rect = RendererRect::new( + parent_bounds.origin.x, + parent_bounds.origin.y, + parent_bounds.size.width, + parent_bounds.size.height, + ); + let frame = bounds.to_parent_coordinates(parent_rect, parent.isFlipped()); + CGRect::new( + CGPoint::new(frame.x, frame.y), + CGSize::new(frame.width, frame.height), + ) +} + +fn appkit_screen_frame( + screen_rect: ScreenRect, + main_thread: MainThreadMarker, +) -> Result { + screen_rect.validate()?; + let primary = NSScreen::screens(main_thread) + .firstObject() + .ok_or(BackendError::MissingPrimaryScreen)?; + let primary_frame = primary.frame(); + Ok(CGRect::new( + CGPoint::new( + primary_frame.origin.x + screen_rect.x, + primary_frame.origin.y + primary_frame.size.height - screen_rect.y - screen_rect.height, + ), + CGSize::new(screen_rect.width, screen_rect.height), + )) +} + +fn application_is_frontmost(process_id: libc::pid_t) -> bool { + NSWorkspace::sharedWorkspace() + .frontmostApplication() + .is_some_and(|application| application.processIdentifier() == process_id) +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/video.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/video.rs new file mode 100644 index 000000000..61b89a2df --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/macos/video.rs @@ -0,0 +1,367 @@ +use std::ffi::c_void; +use std::ptr::{self, NonNull}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use objc2_core_foundation::{ + CFDictionary, CFNumber, CFNumberType, CFRetained, kCFBooleanTrue, + kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks, +}; +use objc2_core_media::{ + CMBlockBuffer, CMFormatDescription, CMSampleBuffer, CMSampleTimingInfo, CMTime, + CMVideoFormatDescriptionCreateFromH264ParameterSets, kCMTimeInvalid, +}; +use objc2_core_video::{ + CVImageBuffer, kCVPixelBufferIOSurfacePropertiesKey, kCVPixelBufferMetalCompatibilityKey, + kCVPixelBufferPixelFormatTypeKey, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, +}; +use objc2_video_toolbox::{ + VTDecodeFrameFlags, VTDecodeInfoFlags, VTDecompressionOutputCallbackRecord, + VTDecompressionSession, kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder, +}; + +use crate::format::{FrameTiming, H264Format, VideoColorSpace}; +use crate::queue::{BoundedQueue, PushResult}; + +use super::{BackendError, Counters}; + +pub(super) struct DecodedFrame { + pub(super) image: CFRetained, + pub(super) color_space: VideoColorSpace, +} + +// The callback retains the CVImageBuffer and no code mutates it after publication to the queue. +unsafe impl Send for DecodedFrame {} + +struct InFlight { + count: AtomicUsize, + maximum: usize, +} + +impl InFlight { + fn try_acquire(&self) -> bool { + self.count + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| { + (count < self.maximum).then_some(count + 1) + }) + .is_ok() + } + + fn release(&self) { + let previous = self.count.fetch_sub(1, Ordering::AcqRel); + debug_assert!(previous > 0); + } +} + +struct CallbackContext { + queue: Arc>, + counters: Arc, + in_flight: Arc, + color_space: VideoColorSpace, +} + +pub(super) struct VideoDecoder { + session: Option>, + format_description: CFRetained, + callback_context: Box, + in_flight: Arc, +} + +// VTDecompressionSession has no thread affinity. Shared owns VideoDecoder behind a Mutex, so +// decode, reconfiguration, and invalidation are serialized even when StreamSink moves threads. +unsafe impl Send for VideoDecoder {} + +impl VideoDecoder { + pub(super) fn new( + format: &H264Format, + queue: Arc>, + counters: Arc, + maximum_in_flight: usize, + ) -> Result { + let format_description = create_format_description(format)?; + let in_flight = Arc::new(InFlight { + count: AtomicUsize::new(0), + maximum: maximum_in_flight, + }); + let mut callback_context = Box::new(CallbackContext { + queue, + counters, + in_flight: Arc::clone(&in_flight), + color_space: format.color_space, + }); + let callback = VTDecompressionOutputCallbackRecord { + decompressionOutputCallback: Some(decompression_callback), + decompressionOutputRefCon: (&mut *callback_context as *mut CallbackContext).cast(), + }; + + let pixel_format = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange as i32; + let pixel_format_number = unsafe { + CFNumber::new( + None, + CFNumberType::SInt32Type, + (&pixel_format as *const i32).cast(), + ) + } + .ok_or_else(|| BackendError::Metal("failed to create CV pixel format number".into()))?; + let empty_properties = make_dictionary(&[])?; + let true_value = unsafe { kCFBooleanTrue }.ok_or_else(|| { + BackendError::Metal("CoreFoundation true value is unavailable".into()) + })?; + let pixel_format_key = unsafe { kCVPixelBufferPixelFormatTypeKey }; + let metal_compatibility_key = unsafe { kCVPixelBufferMetalCompatibilityKey }; + let io_surface_properties_key = unsafe { kCVPixelBufferIOSurfacePropertiesKey }; + let hardware_decoder_key = + unsafe { kVTVideoDecoderSpecification_RequireHardwareAcceleratedVideoDecoder }; + let destination_attributes = make_dictionary(&[ + (cf_ptr(pixel_format_key), cf_ptr(&*pixel_format_number)), + (cf_ptr(metal_compatibility_key), cf_ptr(true_value)), + ( + cf_ptr(io_surface_properties_key), + cf_ptr(&*empty_properties), + ), + ])?; + let decoder_specification = + make_dictionary(&[(cf_ptr(hardware_decoder_key), cf_ptr(true_value))])?; + + let mut session_ptr = ptr::null_mut(); + let status = unsafe { + VTDecompressionSession::create( + None, + &format_description, + Some(&decoder_specification), + Some(&destination_attributes), + &callback, + NonNull::from(&mut session_ptr), + ) + }; + check_status("VTDecompressionSessionCreate", status)?; + let session_ptr = NonNull::new(session_ptr).ok_or(BackendError::AppleApi { + api: "VTDecompressionSessionCreate", + status: -1, + })?; + let session = unsafe { CFRetained::from_raw(session_ptr) }; + + Ok(Self { + session: Some(session), + format_description, + callback_context, + in_flight, + }) + } + + pub(super) fn submit( + &self, + avcc_access_unit: &[u8], + timing: FrameTiming, + ) -> Result { + if !self.in_flight.try_acquire() { + return Ok(false); + } + let result = self.submit_acquired(avcc_access_unit, timing); + if result.is_err() { + self.in_flight.release(); + self.callback_context + .counters + .video_decode_errors + .fetch_add(1, Ordering::Relaxed); + } + result.map(|()| true) + } + + fn submit_acquired( + &self, + avcc_access_unit: &[u8], + timing: FrameTiming, + ) -> Result<(), BackendError> { + let mut block_ptr = ptr::null_mut(); + let status = unsafe { + CMBlockBuffer::create_with_memory_block( + None, + ptr::null_mut(), + avcc_access_unit.len(), + None, + ptr::null(), + 0, + avcc_access_unit.len(), + 0, + NonNull::from(&mut block_ptr), + ) + }; + check_status("CMBlockBufferCreateWithMemoryBlock", status)?; + let block_ptr = NonNull::new(block_ptr).ok_or(BackendError::AppleApi { + api: "CMBlockBufferCreateWithMemoryBlock", + status: -1, + })?; + let block = unsafe { CFRetained::from_raw(block_ptr) }; + let source = NonNull::new(avcc_access_unit.as_ptr().cast_mut().cast::()) + .expect("validated AVCC access unit is not empty"); + let status = + unsafe { CMBlockBuffer::replace_data_bytes(source, &block, 0, avcc_access_unit.len()) }; + check_status("CMBlockBufferReplaceDataBytes", status)?; + + let sample_timing = CMSampleTimingInfo { + duration: unsafe { CMTime::new(timing.duration_value, timing.timescale) }, + presentationTimeStamp: unsafe { + CMTime::new(timing.presentation_value, timing.timescale) + }, + decodeTimeStamp: unsafe { kCMTimeInvalid }, + }; + let sample_size = avcc_access_unit.len(); + let mut sample_ptr = ptr::null_mut(); + let status = unsafe { + CMSampleBuffer::create_ready( + None, + Some(&block), + Some(&self.format_description), + 1, + 1, + &sample_timing, + 1, + &sample_size, + NonNull::from(&mut sample_ptr), + ) + }; + check_status("CMSampleBufferCreateReady", status)?; + let sample_ptr = NonNull::new(sample_ptr).ok_or(BackendError::AppleApi { + api: "CMSampleBufferCreateReady", + status: -1, + })?; + let sample = unsafe { CFRetained::from_raw(sample_ptr) }; + let session = self.session.as_ref().ok_or(BackendError::Stopped)?; + let status = unsafe { + session.decode_frame( + &sample, + VTDecodeFrameFlags::Frame_EnableAsynchronousDecompression, + ptr::null_mut(), + ptr::null_mut(), + ) + }; + check_status("VTDecompressionSessionDecodeFrame", status) + } +} + +impl Drop for VideoDecoder { + fn drop(&mut self) { + if let Some(session) = self.session.take() { + let _ = unsafe { session.wait_for_asynchronous_frames() }; + unsafe { session.invalidate() }; + drop(session); + } + debug_assert_eq!(self.in_flight.count.load(Ordering::Acquire), 0); + let _ = &self.callback_context; + } +} + +unsafe extern "C-unwind" fn decompression_callback( + output_refcon: *mut c_void, + _source_refcon: *mut c_void, + status: i32, + _info_flags: VTDecodeInfoFlags, + image_buffer: *mut CVImageBuffer, + _presentation_time_stamp: CMTime, + _presentation_duration: CMTime, +) { + let Some(context) = NonNull::new(output_refcon.cast::()) else { + return; + }; + let context = unsafe { context.as_ref() }; + if status == 0 { + if let Some(image_buffer) = NonNull::new(image_buffer) { + let image = unsafe { CFRetained::retain(image_buffer) }; + let frame = DecodedFrame { + image, + color_space: context.color_space, + }; + context + .counters + .video_decoded + .fetch_add(1, Ordering::Relaxed); + if matches!( + context.queue.push_drop_oldest(frame), + PushResult::Replaced(_) | PushResult::Closed(_) + ) { + context + .counters + .video_frames_dropped + .fetch_add(1, Ordering::Relaxed); + } + } else { + context + .counters + .video_decode_errors + .fetch_add(1, Ordering::Relaxed); + } + } else { + context + .counters + .video_decode_errors + .fetch_add(1, Ordering::Relaxed); + } + context.in_flight.release(); +} + +fn create_format_description( + format: &H264Format, +) -> Result, BackendError> { + let mut pointers = [ + NonNull::new(format.parameter_sets.sequence().as_ptr().cast_mut()) + .expect("validated SPS is non-empty"), + NonNull::new(format.parameter_sets.picture().as_ptr().cast_mut()) + .expect("validated PPS is non-empty"), + ]; + let mut sizes = [ + format.parameter_sets.sequence().len(), + format.parameter_sets.picture().len(), + ]; + let mut description_ptr: *const CMFormatDescription = ptr::null(); + let status = unsafe { + CMVideoFormatDescriptionCreateFromH264ParameterSets( + None, + pointers.len(), + NonNull::new(pointers.as_mut_ptr()).expect("parameter set array is non-empty"), + NonNull::new(sizes.as_mut_ptr()).expect("parameter set size array is non-empty"), + 4, + NonNull::from(&mut description_ptr), + ) + }; + check_status( + "CMVideoFormatDescriptionCreateFromH264ParameterSets", + status, + )?; + let description_ptr = + NonNull::new(description_ptr.cast_mut()).ok_or(BackendError::AppleApi { + api: "CMVideoFormatDescriptionCreateFromH264ParameterSets", + status: -1, + })?; + Ok(unsafe { CFRetained::from_raw(description_ptr) }) +} + +fn make_dictionary( + entries: &[(*const c_void, *const c_void)], +) -> Result, BackendError> { + let mut keys: Vec<_> = entries.iter().map(|(key, _)| *key).collect(); + let mut values: Vec<_> = entries.iter().map(|(_, value)| *value).collect(); + unsafe { + CFDictionary::new( + None, + keys.as_mut_ptr(), + values.as_mut_ptr(), + entries.len() as isize, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ) + } + .ok_or_else(|| BackendError::Metal("failed to create CoreFoundation dictionary".into())) +} + +fn cf_ptr(value: &T) -> *const c_void { + (value as *const T).cast() +} + +fn check_status(api: &'static str, status: i32) -> Result<(), BackendError> { + if status == 0 { + Ok(()) + } else { + Err(BackendError::AppleApi { api, status }) + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/queue.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/queue.rs new file mode 100644 index 000000000..f4d87e544 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/queue.rs @@ -0,0 +1,155 @@ +use std::collections::VecDeque; +use std::sync::{Condvar, Mutex}; + +pub(crate) enum PushResult { + Pushed, + Replaced(T), + Closed(T), +} + +pub(crate) struct BoundedQueue { + capacity: usize, + state: Mutex>, + ready: Condvar, +} + +struct QueueState { + values: VecDeque, + closed: bool, +} + +impl BoundedQueue { + pub(crate) fn new(capacity: usize) -> Self { + assert!(capacity > 0); + Self { + capacity, + state: Mutex::new(QueueState { + values: VecDeque::with_capacity(capacity), + closed: false, + }), + ready: Condvar::new(), + } + } + + pub(crate) fn push_drop_oldest(&self, value: T) -> PushResult { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if state.closed { + return PushResult::Closed(value); + } + let replaced = if state.values.len() == self.capacity { + state.values.pop_front() + } else { + None + }; + state.values.push_back(value); + self.ready.notify_one(); + replaced.map_or(PushResult::Pushed, PushResult::Replaced) + } + + pub(crate) fn pop_wait(&self) -> Option { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + loop { + if let Some(value) = state.values.pop_front() { + return Some(value); + } + if state.closed { + return None; + } + state = self + .ready + .wait(state) + .unwrap_or_else(|poisoned| poisoned.into_inner()); + } + } + + pub(crate) fn close(&self) { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + state.closed = true; + self.ready.notify_all(); + } + + pub(crate) fn close_and_discard(&self) { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + state.closed = true; + state.values.clear(); + self.ready.notify_all(); + } + + pub(crate) fn clear(&self) -> usize { + let mut state = self + .state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let discarded = state.values.len(); + state.values.clear(); + discarded + } + + #[cfg(test)] + pub(crate) fn len(&self) -> usize { + self.state + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .values + .len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::thread; + + #[test] + fn replaces_oldest_at_capacity() { + let queue = BoundedQueue::new(2); + assert!(matches!(queue.push_drop_oldest(1), PushResult::Pushed)); + assert!(matches!(queue.push_drop_oldest(2), PushResult::Pushed)); + assert!(matches!(queue.push_drop_oldest(3), PushResult::Replaced(1))); + assert_eq!(queue.len(), 2); + assert_eq!(queue.pop_wait(), Some(2)); + assert_eq!(queue.pop_wait(), Some(3)); + } + + #[test] + fn close_wakes_waiter_and_rejects_pushes() { + let queue = Arc::new(BoundedQueue::::new(1)); + let waiter = { + let queue = Arc::clone(&queue); + thread::spawn(move || queue.pop_wait()) + }; + queue.close(); + assert_eq!(waiter.join().unwrap(), None); + assert!(matches!(queue.push_drop_oldest(7), PushResult::Closed(7))); + } + + #[test] + fn discard_close_drops_queued_work() { + let queue = BoundedQueue::new(2); + assert!(matches!(queue.push_drop_oldest(1), PushResult::Pushed)); + queue.close_and_discard(); + assert_eq!(queue.pop_wait(), None); + } + + #[test] + fn clear_keeps_queue_open() { + let queue = BoundedQueue::new(2); + assert!(matches!(queue.push_drop_oldest(1), PushResult::Pushed)); + assert_eq!(queue.clear(), 1); + assert!(matches!(queue.push_drop_oldest(2), PushResult::Pushed)); + assert_eq!(queue.pop_wait(), Some(2)); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/ring.rs b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/ring.rs new file mode 100644 index 000000000..0cd626cad --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform-macos/src/ring.rs @@ -0,0 +1,103 @@ +use std::cell::UnsafeCell; +use std::sync::atomic::{AtomicUsize, Ordering}; + +pub(crate) struct PcmRing { + samples: Box<[UnsafeCell]>, + read: AtomicUsize, + write: AtomicUsize, +} + +// Exactly one Opus worker calls push and exactly one CoreAudio callback calls pop_into. Acquire and +// release publication prevents either side from accessing a slot while the other side owns it. +unsafe impl Send for PcmRing {} +unsafe impl Sync for PcmRing {} + +impl PcmRing { + pub(crate) fn new(capacity: usize) -> Self { + assert!(capacity > 0 && capacity < usize::MAX / 2); + Self { + samples: (0..capacity).map(|_| UnsafeCell::new(0.0)).collect(), + read: AtomicUsize::new(0), + write: AtomicUsize::new(0), + } + } + + pub(crate) fn push(&self, input: &[f32]) -> usize { + let write = self.write.load(Ordering::Relaxed); + let read = self.read.load(Ordering::Acquire); + let available = self.samples.len().saturating_sub(write.wrapping_sub(read)); + let count = input.len().min(available); + for (offset, sample) in input[..count].iter().enumerate() { + let index = write.wrapping_add(offset) % self.samples.len(); + unsafe { *self.samples[index].get() = *sample }; + } + self.write + .store(write.wrapping_add(count), Ordering::Release); + count + } + + pub(crate) fn pop_into(&self, output: &mut [f32]) -> usize { + let read = self.read.load(Ordering::Relaxed); + let write = self.write.load(Ordering::Acquire); + let count = output.len().min(write.wrapping_sub(read)); + for (offset, sample) in output[..count].iter_mut().enumerate() { + let index = read.wrapping_add(offset) % self.samples.len(); + unsafe { *sample = *self.samples[index].get() }; + } + self.read.store(read.wrapping_add(count), Ordering::Release); + count + } + + pub(crate) fn clear(&self) { + let write = self.write.load(Ordering::Acquire); + self.read.store(write, Ordering::Release); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::thread; + + #[test] + fn wraps_and_preserves_pcm_order() { + let ring = PcmRing::new(4); + assert_eq!(ring.push(&[1.0, 2.0, 3.0]), 3); + let mut first = [0.0; 2]; + assert_eq!(ring.pop_into(&mut first), 2); + assert_eq!(first, [1.0, 2.0]); + assert_eq!(ring.push(&[4.0, 5.0, 6.0]), 3); + let mut second = [0.0; 4]; + assert_eq!(ring.pop_into(&mut second), 4); + assert_eq!(second, [3.0, 4.0, 5.0, 6.0]); + } + + #[test] + fn producer_and_consumer_can_run_concurrently() { + const COUNT: usize = 20_000; + let ring = Arc::new(PcmRing::new(256)); + let producer = { + let ring = Arc::clone(&ring); + thread::spawn(move || { + for value in 0..COUNT { + let sample = value as f32; + while ring.push(&[sample]) == 0 { + thread::yield_now(); + } + } + }) + }; + let mut expected = 0usize; + let mut sample = [0.0]; + while expected < COUNT { + if ring.pop_into(&mut sample) == 0 { + thread::yield_now(); + continue; + } + assert_eq!(sample[0], expected as f32); + expected += 1; + } + producer.join().unwrap(); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/Cargo.toml b/native/opennow-streamer/crates/opennow-streamer-platform/Cargo.toml new file mode 100644 index 000000000..e8374ee67 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "opennow-streamer-platform" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +audiopus_sys.workspace = true +openh264.workspace = true +opennow-streamer-protocol = { path = "../opennow-streamer-protocol" } +opus.workspace = true +sdl2.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies] +raw-window-handle = "0.6" +windows-sys = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", +] } + +[target.'cfg(target_os = "linux")'.dependencies] +raw-window-handle = "0.6" +x11-dl = "2.21" + +[target.'cfg(target_os = "macos")'.dependencies] +libc = "0.2" +objc2 = "0.6" +objc2-app-kit = { version = "0.3", default-features = false, features = ["NSGraphics", "NSResponder", "NSRunningApplication", "NSView", "NSWindow", "NSWorkspace"] } +objc2-foundation = { version = "0.3", default-features = false, features = ["NSGeometry"] } +opennow-streamer-platform-macos = { path = "../opennow-streamer-platform-macos" } +raw-window-handle = "0.6" diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/lib.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/lib.rs new file mode 100644 index 000000000..b8da9edd4 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/lib.rs @@ -0,0 +1,196 @@ +#[cfg(target_os = "macos")] +mod macos_backend; +mod media; +mod native_surface; +mod output; +mod queue; +mod runtime; + +pub use media::{EncodedFrame, MediaCodec, MediaFeedback, MediaSession, MediaSink, PushOutcome}; +pub use runtime::{MainThreadHost, MediaRuntime, create_runtime}; + +/// Shows a standalone overlay window through the exact production creation path. +/// Debug-only aid for isolating window-server behavior without a streaming session. +#[cfg(target_os = "macos")] +pub fn debug_show_overlay_window() { + opennow_streamer_platform_macos::debug_show_overlay_window(); +} + +use opennow_streamer_protocol::{CodecCapability, VideoBackendCapability}; + +pub fn video_backends() -> Vec { + vec![hardware_backend(), software_backend()] +} + +pub const fn supports_audio_decode() -> bool { + true +} + +pub const fn supports_audio_output() -> bool { + true +} + +#[cfg(target_os = "windows")] +fn hardware_backend() -> VideoBackendCapability { + unavailable_backend( + "d3d11", + "windows", + "D3D11 hardware decode is not built into this binary", + ) +} + +#[cfg(target_os = "macos")] +fn hardware_backend() -> VideoBackendCapability { + const UNAVAILABLE: &str = "VideoToolbox H.264 hardware decode or Metal is unavailable"; + let available = macos_backend::available(); + VideoBackendCapability { + backend: "videotoolbox", + platform: "macos", + codecs: vec![ + CodecCapability { + codec: "h264", + available, + reason: (!available).then_some(UNAVAILABLE), + }, + CodecCapability { + codec: "h265", + available: false, + reason: Some("H.265 VideoToolbox decode is not implemented"), + }, + CodecCapability { + codec: "av1", + available: false, + reason: Some("AV1 VideoToolbox decode is not implemented"), + }, + ], + zero_copy_modes: available + .then_some(vec!["cvpixelbuffer-iosurface-metal"]) + .unwrap_or_default(), + available, + reason: (!available).then_some(UNAVAILABLE), + } +} + +#[cfg(target_os = "linux")] +fn hardware_backend() -> VideoBackendCapability { + unavailable_backend( + "vaapi", + "linux", + "VA-API/V4L2 hardware decode is not built into this binary", + ) +} + +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +fn hardware_backend() -> VideoBackendCapability { + unavailable_backend("unsupported", "other", "Unsupported operating system") +} + +fn software_backend() -> VideoBackendCapability { + VideoBackendCapability { + backend: "software", + platform: "cross-platform", + codecs: vec![ + CodecCapability { + codec: "h264", + available: true, + reason: None, + }, + CodecCapability { + codec: "h265", + available: false, + reason: Some("H.265 decoder is not built into this binary"), + }, + CodecCapability { + codec: "av1", + available: false, + reason: Some("AV1 decoder is not built into this binary"), + }, + ], + zero_copy_modes: Vec::new(), + available: true, + reason: None, + } +} + +#[cfg(not(target_os = "macos"))] +fn unavailable_backend( + backend: &'static str, + platform: &'static str, + reason: &'static str, +) -> VideoBackendCapability { + VideoBackendCapability { + backend, + platform, + codecs: ["h264", "h265", "av1"] + .into_iter() + .map(|codec| CodecCapability { + codec, + available: false, + reason: Some(reason), + }) + .collect(), + zero_copy_modes: Vec::new(), + available: false, + reason: Some(reason), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn advertises_only_the_linked_software_codec() { + let backends = video_backends(); + let software = backends + .iter() + .find(|backend| backend.backend == "software") + .expect("software backend"); + assert!(software.available); + assert!( + software + .codecs + .iter() + .find(|codec| codec.codec == "h264") + .expect("h264") + .available + ); + assert!( + software + .codecs + .iter() + .filter(|codec| codec.codec != "h264") + .all(|codec| !codec.available) + ); + #[cfg(not(target_os = "macos"))] + assert!( + backends + .iter() + .filter(|backend| backend.backend != "software") + .all(|backend| !backend.available) + ); + #[cfg(target_os = "macos")] + { + let hardware = backends + .iter() + .find(|backend| backend.backend == "videotoolbox") + .expect("VideoToolbox backend"); + assert!( + hardware + .codecs + .iter() + .filter(|codec| codec.codec != "h264") + .all(|codec| !codec.available) + ); + assert_eq!( + hardware.available, + hardware + .codecs + .iter() + .find(|codec| codec.codec == "h264") + .expect("h264") + .available + ); + } + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/macos_backend.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/macos_backend.rs new file mode 100644 index 000000000..2c5926178 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/macos_backend.rs @@ -0,0 +1,265 @@ +use opennow_streamer_platform_macos::{ + AudioFormat, BackendConfig, H264Format, H264Framing, H264ParameterSets, MacOsBackend, + OwnedOverlayConfig, QueueLimits, ScreenRect, StreamSink, SurfaceTarget, VideoColorSpace, + probe_h264_hardware, +}; +use opennow_streamer_protocol::{RenderSurface, RenderSurfaceRect}; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +const HIDDEN_SURFACE: ScreenRect = ScreenRect::new(0.0, 0.0, 2.0, 2.0); +const ORDERING_POLL_INTERVAL: Duration = Duration::from_millis(100); + +pub(crate) fn available() -> bool { + availability().load(Ordering::Acquire) +} + +pub(crate) fn disable() { + availability().store(false, Ordering::Release); +} + +fn availability() -> &'static AtomicBool { + static AVAILABLE: OnceLock = OnceLock::new(); + AVAILABLE.get_or_init(|| AtomicBool::new(probe_h264_hardware())) +} + +pub(crate) struct MacOutput { + backend: Option, + screen_rect: ScreenRect, + visible: bool, + paused: bool, + last_ordering_check: Instant, +} + +impl MacOutput { + pub(crate) fn initialize() -> Self { + Self { + backend: None, + screen_rect: HIDDEN_SURFACE, + visible: false, + paused: false, + last_ordering_check: Instant::now(), + } + } + + pub(crate) fn start(&mut self, surface: Option<&RenderSurface>) -> Result<(), String> { + self.paused = false; + if let Some(surface) = surface { + self.update_surface(surface)?; + } + Ok(()) + } + + pub(crate) fn configure_h264( + &mut self, + parameter_sets: H264ParameterSets, + ) -> Result { + if self.backend.is_some() { + return Err("macOS VideoToolbox backend is already configured".to_owned()); + } + let mut backend = MacOsBackend::start(BackendConfig { + surface: SurfaceTarget::OwnedOverlay(OwnedOverlayConfig::new( + self.screen_rect, + self.visible && !self.paused, + )), + video: H264Format::new(parameter_sets, VideoColorSpace::Bt709), + audio: AudioFormat::OPUS_STEREO_48KHZ, + queues: QueueLimits::default(), + }) + .map_err(|error| format!("VideoToolbox backend initialization failed: {error}"))?; + backend + .set_paused(self.paused) + .map_err(|error| format!("VideoToolbox pause state failed: {error}"))?; + let sink = backend.sink(); + self.backend = Some(backend); + Ok(sink) + } + + pub(crate) fn set_paused(&mut self, paused: bool) -> Result<(), String> { + self.paused = paused; + if let Some(backend) = self.backend.as_mut() { + backend + .set_paused(paused) + .map_err(|error| format!("macOS media pause failed: {error}"))?; + backend + .update_owned_overlay(self.screen_rect, self.visible && !paused) + .map_err(|error| format!("macOS overlay pause failed: {error}"))?; + } + Ok(()) + } + + pub(crate) fn stop(&mut self) { + if let Some(mut backend) = self.backend.take() { + backend.stop(); + } + self.paused = false; + } + + pub(crate) fn update_surface(&mut self, surface: &RenderSurface) -> Result<(), String> { + if surface.visible { + self.screen_rect = surface.screen_rect.map(screen_rect).ok_or_else(|| { + "visible macOS overlay is missing absolute screen bounds".to_owned() + })?; + } + self.visible = surface.visible && surface.screen_rect.is_some(); + if let Some(backend) = self.backend.as_mut() { + backend + .update_owned_overlay(self.screen_rect, self.visible && !self.paused) + .map_err(|error| format!("macOS overlay update failed: {error}"))?; + } + Ok(()) + } + + pub(crate) fn pump(&mut self) -> Result<(), String> { + if self.last_ordering_check.elapsed() < ORDERING_POLL_INTERVAL { + return Ok(()); + } + self.last_ordering_check = Instant::now(); + if let Some(backend) = self.backend.as_mut() { + backend + .refresh_overlay_ordering() + .map_err(|error| format!("macOS overlay ordering refresh failed: {error}"))?; + } + Ok(()) + } +} + +fn screen_rect(rect: RenderSurfaceRect) -> ScreenRect { + ScreenRect::new( + f64::from(rect.x), + f64::from(rect.y), + f64::from(rect.width), + f64::from(rect.height), + ) +} + +#[derive(Default)] +pub(crate) struct H264ParameterSetTracker { + sequence: Option>, + picture: Option>, +} + +impl H264ParameterSetTracker { + pub(crate) fn observe(&mut self, access_unit: &[u8]) -> Result { + let framing = if find_start_code(access_unit, 0).is_some() { + self.observe_annex_b(access_unit)?; + H264Framing::AnnexB + } else { + self.observe_avcc(access_unit)?; + H264Framing::Avcc + }; + Ok(framing) + } + + pub(crate) fn parameter_sets(&self) -> Result, String> { + match (&self.sequence, &self.picture) { + (Some(sequence), Some(picture)) => H264ParameterSets::new(sequence, picture) + .map(Some) + .map_err(|error| format!("invalid H.264 parameter sets: {error}")), + _ => Ok(None), + } + } + + fn observe_annex_b(&mut self, access_unit: &[u8]) -> Result<(), String> { + let Some((mut start, prefix)) = find_start_code(access_unit, 0) else { + return Err("H.264 access unit has no Annex B start code".to_owned()); + }; + if access_unit[..start].iter().any(|byte| *byte != 0) { + return Err("H.264 access unit has invalid Annex B framing".to_owned()); + } + start += prefix; + loop { + let next = find_start_code(access_unit, start); + let end = next.map_or(access_unit.len(), |(offset, _)| offset); + self.observe_nal(&access_unit[start..end])?; + let Some((next_start, next_prefix)) = next else { + return Ok(()); + }; + start = next_start + next_prefix; + } + } + + fn observe_avcc(&mut self, access_unit: &[u8]) -> Result<(), String> { + let mut offset = 0usize; + while offset < access_unit.len() { + let length_bytes = access_unit + .get(offset..offset + 4) + .ok_or_else(|| "H.264 access unit has truncated AVCC framing".to_owned())?; + let length = u32::from_be_bytes( + length_bytes + .try_into() + .expect("AVCC length was checked as four bytes"), + ) as usize; + offset += 4; + let end = offset + .checked_add(length) + .filter(|end| *end <= access_unit.len()) + .ok_or_else(|| "H.264 access unit has invalid AVCC framing".to_owned())?; + self.observe_nal(&access_unit[offset..end])?; + offset = end; + } + if offset == 0 { + return Err("H.264 access unit is empty".to_owned()); + } + Ok(()) + } + + fn observe_nal(&mut self, nal: &[u8]) -> Result<(), String> { + let header = *nal + .first() + .ok_or_else(|| "H.264 access unit contains an empty NAL unit".to_owned())?; + match header & 0x1f { + 7 => self.sequence = Some(nal.to_vec()), + 8 => self.picture = Some(nal.to_vec()), + _ => {} + } + Ok(()) + } +} + +fn find_start_code(bytes: &[u8], from: usize) -> Option<(usize, usize)> { + let mut index = from; + while index + 3 <= bytes.len() { + if bytes[index..].starts_with(&[0, 0, 0, 1]) { + return Some((index, 4)); + } + if bytes[index..].starts_with(&[0, 0, 1]) { + return Some((index, 3)); + } + index += 1; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn extracts_parameter_sets_from_annex_b_access_unit() { + let mut tracker = H264ParameterSetTracker::default(); + let framing = tracker + .observe(&[ + 0, 0, 0, 1, 0x67, 0x64, 0, 0x29, 0, 0, 1, 0x68, 0xee, 0x3c, 0x80, 0, 0, 0, 1, 0x65, + 1, + ]) + .unwrap(); + assert_eq!(framing, H264Framing::AnnexB); + let parameters = tracker.parameter_sets().unwrap().unwrap(); + assert_eq!(parameters.sequence(), &[0x67, 0x64, 0, 0x29]); + assert_eq!(parameters.picture(), &[0x68, 0xee, 0x3c, 0x80]); + } + + #[test] + fn extracts_parameter_sets_across_avcc_access_units() { + let mut tracker = H264ParameterSetTracker::default(); + assert_eq!( + tracker.observe(&[0, 0, 0, 2, 0x67, 1]).unwrap(), + H264Framing::Avcc + ); + assert!(tracker.parameter_sets().unwrap().is_none()); + tracker.observe(&[0, 0, 0, 2, 0x68, 2]).unwrap(); + assert!(tracker.parameter_sets().unwrap().is_some()); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/media.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/media.rs new file mode 100644 index 000000000..9711cf140 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/media.rs @@ -0,0 +1,917 @@ +use std::sync::Arc; +#[cfg(target_os = "macos")] +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::Sender; +use std::thread::{self, JoinHandle}; + +use openh264::OpenH264API; +use openh264::decoder::{Decoder as OpenH264Decoder, DecoderConfig}; +use openh264::formats::YUVSource; +use opus::{Channels, Decoder as OpusNativeDecoder}; + +use crate::output::{DecodedVideoFrame, OutputBuffers}; +use crate::queue::{BoundedQueue, PushResult}; +use crate::runtime::HostCommand; + +const VIDEO_QUEUE_CAPACITY: usize = 3; +const AUDIO_QUEUE_CAPACITY: usize = 12; +const OPUS_SAMPLE_RATE: u32 = 48_000; +const MAX_OPUS_FRAME_SAMPLES_PER_CHANNEL: usize = 5_760; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MediaCodec { + H264, + Opus { channels: u8 }, + Unsupported(String), +} + +#[derive(Debug, Clone)] +pub struct EncodedFrame { + pub mid: String, + pub codec: MediaCodec, + pub data: Arc<[u8]>, + pub timestamp: u64, + pub clock_rate_hz: u32, + pub keyframe: bool, + pub contiguous: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MediaFeedback { + PlaybackStarted { + backend: &'static str, + }, + BackendFallback { + from: &'static str, + to: &'static str, + reason: String, + }, + RequestKeyframe { + mid: String, + reason: String, + }, + DecoderError { + codec: &'static str, + message: String, + }, + QueueDropped { + media: &'static str, + count: usize, + }, + OutputError { + message: String, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PushOutcome { + Queued, + DroppedOldest, + Paused, + Unsupported, + Closed, +} + +struct SharedPipeline { + video: Arc>, + audio: Arc>, + output: Arc, + feedback: Sender, + paused: AtomicBool, + video_desynced: AtomicBool, + keyframe_requested: AtomicBool, + stopped: AtomicBool, + #[cfg(target_os = "macos")] + mac_sink: Mutex>, + #[cfg(target_os = "macos")] + mac_software_fallback: AtomicBool, +} + +#[derive(Clone)] +pub struct MediaSink { + shared: Arc, +} + +impl MediaSink { + pub fn push(&self, frame: EncodedFrame) -> PushOutcome { + if self.shared.stopped.load(Ordering::Acquire) { + return PushOutcome::Closed; + } + if self.shared.paused.load(Ordering::Acquire) { + return PushOutcome::Paused; + } + match frame.codec { + MediaCodec::H264 => self.push_video(frame), + MediaCodec::Opus { .. } => self.push_audio(frame), + MediaCodec::Unsupported(_) => PushOutcome::Unsupported, + } + } + + fn push_video(&self, frame: EncodedFrame) -> PushOutcome { + if !frame.keyframe && self.shared.video_desynced.load(Ordering::Acquire) { + self.mark_video_desynced(&frame.mid, "waiting for a decodable H.264 keyframe"); + } else if !frame.contiguous { + self.mark_video_desynced(&frame.mid, "RTP video discontinuity"); + } + let mid = frame.mid.clone(); + match self.shared.video.push(frame) { + PushResult::Queued => PushOutcome::Queued, + PushResult::DroppedOldest => { + self.mark_video_desynced(&mid, "encoded video queue overflow"); + let _ = self.shared.feedback.send(MediaFeedback::QueueDropped { + media: "video", + count: 1, + }); + PushOutcome::DroppedOldest + } + PushResult::Closed => PushOutcome::Closed, + } + } + + fn push_audio(&self, frame: EncodedFrame) -> PushOutcome { + match self.shared.audio.push(frame) { + PushResult::Queued => PushOutcome::Queued, + PushResult::DroppedOldest => { + let _ = self.shared.feedback.send(MediaFeedback::QueueDropped { + media: "audio", + count: 1, + }); + PushOutcome::DroppedOldest + } + PushResult::Closed => PushOutcome::Closed, + } + } + + fn mark_video_desynced(&self, mid: &str, reason: &str) { + self.shared.video_desynced.store(true, Ordering::Release); + if !self.shared.keyframe_requested.swap(true, Ordering::AcqRel) { + let _ = self.shared.feedback.send(MediaFeedback::RequestKeyframe { + mid: mid.to_owned(), + reason: reason.to_owned(), + }); + } + } +} + +pub struct MediaSession { + sink: MediaSink, + video_worker: Option>, + audio_worker: Option>, + host_commands: Sender, +} + +impl MediaSession { + pub(crate) fn spawn( + output: Arc, + feedback: Sender, + host_commands: Sender, + use_macos_hardware: bool, + ) -> Result { + #[cfg(target_os = "macos")] + if use_macos_hardware { + return Self::spawn_macos(output, feedback, host_commands); + } + let _ = use_macos_hardware; + let video_decoder = H264Decoder::new()?; + let audio_decoder = OpusDecoder::new(2)?; + let shared = Arc::new(SharedPipeline { + video: Arc::new(BoundedQueue::new(VIDEO_QUEUE_CAPACITY)), + audio: Arc::new(BoundedQueue::new(AUDIO_QUEUE_CAPACITY)), + output, + feedback, + paused: AtomicBool::new(false), + video_desynced: AtomicBool::new(true), + keyframe_requested: AtomicBool::new(false), + stopped: AtomicBool::new(false), + #[cfg(target_os = "macos")] + mac_sink: Mutex::new(None), + #[cfg(target_os = "macos")] + mac_software_fallback: AtomicBool::new(false), + }); + let video_shared = Arc::clone(&shared); + let video_worker = thread::Builder::new() + .name("opennow-h264-decode".to_owned()) + .spawn(move || run_video_decoder(video_shared, video_decoder)) + .map_err(|error| format!("failed to start H.264 decoder worker: {error}"))?; + let audio_shared = Arc::clone(&shared); + let audio_worker = match thread::Builder::new() + .name("opennow-opus-decode".to_owned()) + .spawn(move || run_audio_decoder(audio_shared, audio_decoder)) + { + Ok(worker) => worker, + Err(error) => { + shared.video.close(); + let _ = video_worker.join(); + return Err(format!("failed to start Opus decoder worker: {error}")); + } + }; + Ok(Self { + sink: MediaSink { shared }, + video_worker: Some(video_worker), + audio_worker: Some(audio_worker), + host_commands, + }) + } + + #[cfg(target_os = "macos")] + fn spawn_macos( + output: Arc, + feedback: Sender, + host_commands: Sender, + ) -> Result { + let shared = Arc::new(SharedPipeline { + video: Arc::new(BoundedQueue::new(VIDEO_QUEUE_CAPACITY)), + audio: Arc::new(BoundedQueue::new(AUDIO_QUEUE_CAPACITY)), + output, + feedback, + paused: AtomicBool::new(false), + video_desynced: AtomicBool::new(true), + keyframe_requested: AtomicBool::new(false), + stopped: AtomicBool::new(false), + mac_sink: Mutex::new(None), + mac_software_fallback: AtomicBool::new(false), + }); + let video_shared = Arc::clone(&shared); + let video_commands = host_commands.clone(); + let video_worker = thread::Builder::new() + .name("opennow-videotoolbox-submit".to_owned()) + .spawn(move || run_macos_video(video_shared, video_commands)) + .map_err(|error| format!("failed to start VideoToolbox submit worker: {error}"))?; + let audio_shared = Arc::clone(&shared); + let audio_worker = match thread::Builder::new() + .name("opennow-coreaudio-submit".to_owned()) + .spawn(move || run_macos_audio(audio_shared)) + { + Ok(worker) => worker, + Err(error) => { + shared.video.close(); + let _ = video_worker.join(); + return Err(format!("failed to start CoreAudio submit worker: {error}")); + } + }; + Ok(Self { + sink: MediaSink { shared }, + video_worker: Some(video_worker), + audio_worker: Some(audio_worker), + host_commands, + }) + } + + pub fn sink(&self) -> MediaSink { + self.sink.clone() + } + + pub fn set_paused(&self, paused: bool) { + self.sink.shared.paused.store(paused, Ordering::Release); + self.sink.shared.video.clear(); + self.sink.shared.audio.clear(); + self.sink.shared.output.clear(); + if !paused { + self.sink + .shared + .video_desynced + .store(true, Ordering::Release); + self.sink + .shared + .keyframe_requested + .store(false, Ordering::Release); + } + let _ = self.host_commands.send(HostCommand::Pause { + paused, + reply: None, + }); + } + + pub fn stop(mut self) { + self.stop_inner(); + } + + fn stop_inner(&mut self) { + if self.sink.shared.stopped.swap(true, Ordering::AcqRel) { + return; + } + self.sink.shared.video.close(); + self.sink.shared.audio.close(); + if let Some(worker) = self.video_worker.take() { + let _ = worker.join(); + } + if let Some(worker) = self.audio_worker.take() { + let _ = worker.join(); + } + #[cfg(target_os = "macos")] + { + self.sink + .shared + .mac_sink + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take(); + } + self.sink.shared.output.clear(); + let _ = self.host_commands.send(HostCommand::Stop); + } +} + +impl Drop for MediaSession { + fn drop(&mut self) { + self.stop_inner(); + } +} + +struct H264Decoder { + decoder: OpenH264Decoder, +} + +impl H264Decoder { + fn new() -> Result { + OpenH264Decoder::with_api_config( + OpenH264API::from_source(), + DecoderConfig::new().debug(false), + ) + .map(|decoder| Self { decoder }) + .map_err(|error| format!("OpenH264 decoder initialization failed: {error}")) + } + + fn decode(&mut self, encoded: &[u8]) -> Result, String> { + let Some(yuv) = self + .decoder + .decode(encoded) + .map_err(|error| error.to_string())? + else { + return Ok(None); + }; + let (width, height) = yuv.dimensions(); + let mut rgb = vec![0; yuv.rgb8_len()]; + yuv.write_rgb8(&mut rgb); + Ok(Some(DecodedVideoFrame { + width: width as u32, + height: height as u32, + rgb, + })) + } +} + +struct OpusDecoder { + decoder: OpusNativeDecoder, + channels: u8, + scratch: Vec, +} + +impl OpusDecoder { + fn new(channels: u8) -> Result { + let opus_channels = match channels { + 1 => Channels::Mono, + 2 => Channels::Stereo, + other => return Err(format!("unsupported Opus channel count: {other}")), + }; + OpusNativeDecoder::new(OPUS_SAMPLE_RATE, opus_channels) + .map(|decoder| Self { + decoder, + channels, + scratch: vec![0.0; MAX_OPUS_FRAME_SAMPLES_PER_CHANNEL * channels as usize], + }) + .map_err(|error| format!("Opus decoder initialization failed: {error}")) + } + + fn decode(&mut self, encoded: &[u8]) -> Result<&[f32], String> { + let samples_per_channel = self + .decoder + .decode_float(encoded, &mut self.scratch, false) + .map_err(|error| error.to_string())?; + Ok(&self.scratch[..samples_per_channel * self.channels as usize]) + } +} + +fn run_video_decoder(shared: Arc, decoder: H264Decoder) { + run_video_decoder_from(shared, decoder, None); +} + +fn run_video_decoder_from( + shared: Arc, + mut decoder: H264Decoder, + mut pending: Option, +) { + loop { + let Some(frame) = pending.take().or_else(|| shared.video.pop()) else { + return; + }; + if shared.paused.load(Ordering::Acquire) { + continue; + } + if shared.video_desynced.load(Ordering::Acquire) { + if !frame.keyframe { + continue; + } + match H264Decoder::new() { + Ok(new_decoder) => decoder = new_decoder, + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message, + }); + continue; + } + } + shared.video_desynced.store(false, Ordering::Release); + shared.keyframe_requested.store(false, Ordering::Release); + } + match decoder.decode(&frame.data) { + Ok(Some(decoded)) => { + if shared.output.replace_video(decoded) { + let _ = shared.feedback.send(MediaFeedback::QueueDropped { + media: "present", + count: 1, + }); + } + } + Ok(None) => {} + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message, + }); + shared.video_desynced.store(true, Ordering::Release); + if !shared.keyframe_requested.swap(true, Ordering::AcqRel) { + let _ = shared.feedback.send(MediaFeedback::RequestKeyframe { + mid: frame.mid, + reason: "H.264 decoder rejected an access unit".to_owned(), + }); + } + } + } + } +} + +fn run_audio_decoder(shared: Arc, decoder: OpusDecoder) { + run_audio_decoder_from(shared, decoder, None); +} + +fn run_audio_decoder_from( + shared: Arc, + mut decoder: OpusDecoder, + mut pending: Option, +) { + let mut configured_channels = 2; + loop { + let Some(frame) = pending.take().or_else(|| shared.audio.pop()) else { + return; + }; + if shared.paused.load(Ordering::Acquire) { + continue; + } + let MediaCodec::Opus { channels } = frame.codec else { + continue; + }; + let channels = channels.clamp(1, 2); + if channels != configured_channels { + match OpusDecoder::new(channels) { + Ok(new_decoder) => { + decoder = new_decoder; + configured_channels = channels; + } + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "opus", + message, + }); + continue; + } + } + } + match decoder.decode(&frame.data) { + Ok(samples) => { + if configured_channels == 1 { + let mut stereo = Vec::with_capacity(samples.len() * 2); + for sample in samples { + stereo.extend([*sample, *sample]); + } + let dropped = shared.output.push_audio(&stereo); + if dropped > 0 { + let _ = shared.feedback.send(MediaFeedback::QueueDropped { + media: "audio-output", + count: dropped, + }); + } + } else { + let dropped = shared.output.push_audio(samples); + if dropped > 0 { + let _ = shared.feedback.send(MediaFeedback::QueueDropped { + media: "audio-output", + count: dropped, + }); + } + } + } + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "opus", + message, + }); + } + } + } +} + +#[cfg(target_os = "macos")] +fn run_macos_video(shared: Arc, host_commands: Sender) { + use std::sync::mpsc; + use std::time::Duration; + + use opennow_streamer_platform_macos::{ + FrameTiming, H264Format, SubmitOutcome, VideoColorSpace, + }; + + use crate::runtime::MacH264Configuration; + + let mut tracker = crate::macos_backend::H264ParameterSetTracker::default(); + let mut configured_parameter_sets = None; + let mut backend_sink = None; + let mut playback_started = false; + while let Some(frame) = shared.video.pop() { + if shared.paused.load(Ordering::Acquire) { + continue; + } + let framing = match tracker.observe(&frame.data) { + Ok(framing) => framing, + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message, + }); + mark_macos_video_desynced(&shared, &frame.mid, "invalid H.264 framing"); + continue; + } + }; + let parameter_sets = match tracker.parameter_sets() { + Ok(parameter_sets) => parameter_sets, + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message, + }); + mark_macos_video_desynced(&shared, &frame.mid, "invalid H.264 parameter sets"); + continue; + } + }; + if shared.video_desynced.load(Ordering::Acquire) && !frame.keyframe { + continue; + } + if backend_sink.is_none() { + let Some(parameter_sets) = parameter_sets.clone() else { + mark_macos_video_desynced( + &shared, + &frame.mid, + "VideoToolbox is waiting for H.264 SPS/PPS", + ); + continue; + }; + let (reply, response) = mpsc::channel(); + if host_commands + .send(HostCommand::ConfigureMacH264 { + parameter_sets: parameter_sets.clone(), + reply, + }) + .is_err() + { + return; + } + match response.recv_timeout(Duration::from_secs(10)) { + Ok(Ok(MacH264Configuration::Hardware(sink))) => { + *shared + .mac_sink + .lock() + .unwrap_or_else(|error| error.into_inner()) = Some(sink.clone()); + configured_parameter_sets = Some(parameter_sets); + backend_sink = Some(sink); + } + Ok(Ok(MacH264Configuration::SoftwareFallback { reason })) => { + shared.mac_software_fallback.store(true, Ordering::Release); + let _ = shared.feedback.send(MediaFeedback::BackendFallback { + from: "VideoToolbox/Metal", + to: "OpenH264/SDL", + reason, + }); + match H264Decoder::new() { + Ok(decoder) => run_video_decoder_from(shared, decoder, Some(frame)), + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message, + }); + } + } + return; + } + Ok(Err(message)) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message, + }); + return; + } + Err(_) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message: "VideoToolbox initialization timed out on the main thread" + .to_owned(), + }); + return; + } + } + } else if let Some(parameter_sets) = parameter_sets + && configured_parameter_sets.as_ref() != Some(¶meter_sets) + { + let format = H264Format::new(parameter_sets.clone(), VideoColorSpace::Bt709); + let Some(sink) = backend_sink.as_ref() else { + return; + }; + if let Err(error) = sink.reconfigure_h264(format) { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message: error.to_string(), + }); + mark_macos_video_desynced( + &shared, + &frame.mid, + "VideoToolbox reconfiguration failed", + ); + continue; + } + configured_parameter_sets = Some(parameter_sets); + } + + shared.video_desynced.store(false, Ordering::Release); + shared.keyframe_requested.store(false, Ordering::Release); + let timescale = i32::try_from(frame.clock_rate_hz) + .ok() + .filter(|timescale| *timescale > 0) + .unwrap_or(90_000); + let timing = FrameTiming::new( + i64::try_from(frame.timestamp).unwrap_or(i64::MAX), + 0, + timescale, + ); + let Some(sink) = backend_sink.as_ref() else { + return; + }; + match sink.submit_h264(&frame.data, framing, timing) { + Ok(SubmitOutcome::Accepted | SubmitOutcome::Paused) => {} + Ok(SubmitOutcome::Backpressured | SubmitOutcome::ReplacedOldest) => { + let _ = shared.feedback.send(MediaFeedback::QueueDropped { + media: "videotoolbox", + count: 1, + }); + mark_macos_video_desynced( + &shared, + &frame.mid, + "VideoToolbox decode queue backpressure", + ); + } + Err(error) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "h264", + message: error.to_string(), + }); + mark_macos_video_desynced( + &shared, + &frame.mid, + "VideoToolbox rejected an H.264 access unit", + ); + } + } + if !playback_started && sink.stats().video_presented > 0 { + playback_started = true; + let _ = shared.feedback.send(MediaFeedback::PlaybackStarted { + backend: "VideoToolbox/Metal", + }); + } + } +} + +#[cfg(target_os = "macos")] +fn run_macos_audio(shared: Arc) { + use opennow_streamer_platform_macos::{AudioFormat, SubmitOutcome}; + + let mut configured_channels = 2; + while let Some(frame) = shared.audio.pop() { + if shared.paused.load(Ordering::Acquire) { + continue; + } + if shared.mac_software_fallback.load(Ordering::Acquire) { + match OpusDecoder::new(2) { + Ok(decoder) => run_audio_decoder_from(shared, decoder, Some(frame)), + Err(message) => { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "opus", + message, + }); + } + } + return; + } + let MediaCodec::Opus { channels } = frame.codec else { + continue; + }; + let Some(sink) = shared + .mac_sink + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clone() + else { + continue; + }; + let channels = channels.clamp(1, 2); + if channels != configured_channels { + if let Err(error) = sink.reconfigure_audio(AudioFormat::new(48_000, channels)) { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "opus", + message: error.to_string(), + }); + continue; + } + configured_channels = channels; + } + match sink.submit_opus(&frame.data) { + Ok(SubmitOutcome::Accepted | SubmitOutcome::Backpressured | SubmitOutcome::Paused) => {} + Ok(SubmitOutcome::ReplacedOldest) => { + let _ = shared.feedback.send(MediaFeedback::QueueDropped { + media: "coreaudio", + count: 1, + }); + } + Err(error) => { + if !shared.stopped.load(Ordering::Acquire) { + let _ = shared.feedback.send(MediaFeedback::DecoderError { + codec: "opus", + message: error.to_string(), + }); + } + } + } + } +} + +#[cfg(target_os = "macos")] +fn mark_macos_video_desynced(shared: &SharedPipeline, mid: &str, reason: &str) { + shared.video_desynced.store(true, Ordering::Release); + if !shared.keyframe_requested.swap(true, Ordering::AcqRel) { + let _ = shared.feedback.send(MediaFeedback::RequestKeyframe { + mid: mid.to_owned(), + reason: reason.to_owned(), + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use openh264::encoder::Encoder; + use openh264::formats::{RgbSliceU8, YUVBuffer}; + use opus::{Application, Encoder as OpusEncoder}; + + #[test] + fn decodes_a_synthetic_h264_keyframe() { + let width = 32; + let height = 32; + let mut rgb = vec![0_u8; width * height * 3]; + for (index, pixel) in rgb.chunks_exact_mut(3).enumerate() { + pixel.copy_from_slice(&[(index % 255) as u8, 64, 192]); + } + let yuv = YUVBuffer::from_rgb_source(RgbSliceU8::new(&rgb, (width, height))); + let mut encoder = Encoder::new().expect("encoder"); + let encoded = encoder.encode(&yuv).expect("encode").to_vec(); + let mut decoder = H264Decoder::new().expect("decoder"); + let decoded = decoder + .decode(&encoded) + .expect("decode") + .expect("decoded frame"); + assert_eq!( + (decoded.width, decoded.height), + (width as u32, height as u32) + ); + assert_eq!(decoded.rgb.len(), width * height * 3); + } + + #[test] + fn software_handoff_decodes_the_pending_h264_keyframe() { + let width = 32; + let height = 32; + let rgb = vec![96_u8; width * height * 3]; + let yuv = YUVBuffer::from_rgb_source(RgbSliceU8::new(&rgb, (width, height))); + let mut encoder = Encoder::new().expect("encoder"); + let encoded: Arc<[u8]> = encoder.encode(&yuv).expect("encode").to_vec().into(); + let output = Arc::new(OutputBuffers::new()); + let (feedback, _receiver) = std::sync::mpsc::channel(); + let shared = Arc::new(SharedPipeline { + video: Arc::new(BoundedQueue::new(VIDEO_QUEUE_CAPACITY)), + audio: Arc::new(BoundedQueue::new(AUDIO_QUEUE_CAPACITY)), + output: Arc::clone(&output), + feedback, + paused: AtomicBool::new(false), + video_desynced: AtomicBool::new(true), + keyframe_requested: AtomicBool::new(false), + stopped: AtomicBool::new(false), + #[cfg(target_os = "macos")] + mac_sink: Mutex::new(None), + #[cfg(target_os = "macos")] + mac_software_fallback: AtomicBool::new(true), + }); + shared.video.close(); + run_video_decoder_from( + shared, + H264Decoder::new().expect("decoder"), + Some(EncodedFrame { + mid: "video".to_owned(), + codec: MediaCodec::H264, + data: encoded, + timestamp: 0, + clock_rate_hz: 90_000, + keyframe: true, + contiguous: true, + }), + ); + let decoded = output.take_video().expect("decoded pending frame"); + assert_eq!( + (decoded.width, decoded.height), + (width as u32, height as u32) + ); + } + + #[test] + fn decodes_synthetic_stereo_opus() { + let mut encoder = OpusEncoder::new(OPUS_SAMPLE_RATE, Channels::Stereo, Application::Audio) + .expect("encoder"); + let input: Vec = (0..960 * 2) + .map(|sample| ((sample as f32 / 24.0).sin()) * 0.25) + .collect(); + let mut packet = vec![0_u8; 4_000]; + let encoded_len = encoder.encode_float(&input, &mut packet).expect("encode"); + let mut decoder = OpusDecoder::new(2).expect("decoder"); + let decoded = decoder.decode(&packet[..encoded_len]).expect("decode"); + assert_eq!(decoded.len(), input.len()); + assert!(decoded.iter().any(|sample| sample.abs() > 0.001)); + } + + #[test] + fn paused_and_stopped_sessions_reject_frames() { + let (feedback, _receiver) = std::sync::mpsc::channel(); + let (commands, _host) = std::sync::mpsc::channel(); + let session = + MediaSession::spawn(Arc::new(OutputBuffers::new()), feedback, commands, false) + .expect("session"); + let sink = session.sink(); + session.set_paused(true); + assert_eq!( + sink.push(EncodedFrame { + mid: "video".to_owned(), + codec: MediaCodec::H264, + data: Arc::from([]), + timestamp: 0, + clock_rate_hz: 90_000, + keyframe: false, + contiguous: true, + }), + PushOutcome::Paused + ); + session.stop(); + assert_eq!( + sink.push(EncodedFrame { + mid: "video".to_owned(), + codec: MediaCodec::H264, + data: Arc::from([]), + timestamp: 0, + clock_rate_hz: 90_000, + keyframe: false, + contiguous: true, + }), + PushOutcome::Closed + ); + } + + #[test] + fn requests_a_keyframe_when_video_starts_mid_gop() { + let (feedback, receiver) = std::sync::mpsc::channel(); + let (commands, _host) = std::sync::mpsc::channel(); + let session = + MediaSession::spawn(Arc::new(OutputBuffers::new()), feedback, commands, false) + .expect("session"); + assert_eq!( + session.sink().push(EncodedFrame { + mid: "video".to_owned(), + codec: MediaCodec::H264, + data: Arc::from([0_u8, 0, 0, 1, 1]), + timestamp: 0, + clock_rate_hz: 90_000, + keyframe: false, + contiguous: true, + }), + PushOutcome::Queued + ); + assert!(matches!( + receiver.recv_timeout(std::time::Duration::from_secs(1)), + Ok(MediaFeedback::RequestKeyframe { mid, .. }) if mid == "video" + )); + session.stop(); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/native_surface.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/native_surface.rs new file mode 100644 index 000000000..9e16dc571 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/native_surface.rs @@ -0,0 +1,387 @@ +use opennow_streamer_protocol::RenderSurfaceRect; +use sdl2::video::Window; + +pub(crate) struct NativeSurface { + inner: platform::Surface, +} + +impl NativeSurface { + pub(crate) fn new(window: &Window) -> Result { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + platform::Surface::new(window) + })) + .map_err(|_| "SDL could not expose a native presentation handle".to_owned())? + .map(|inner| Self { inner }) + } + + pub(crate) fn attach_and_show( + &mut self, + parent_handle: &str, + rect: RenderSurfaceRect, + screen_rect: Option, + scale: f32, + ) -> Result<(), String> { + self.inner + .attach_and_show(parent_handle, rect, screen_rect, scale) + } + + pub(crate) fn hide(&mut self) { + self.inner.hide(); + } + + pub(crate) fn refresh_ordering(&mut self) -> Result<(), String> { + self.inner.refresh_ordering() + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn parse_handle(value: &str) -> Result { + let trimmed = value.trim(); + let parsed = if let Some(hex) = trimmed.strip_prefix("0x") { + usize::from_str_radix(hex, 16) + } else { + trimmed.parse() + }; + parsed + .ok() + .filter(|handle| *handle != 0) + .ok_or_else(|| format!("invalid Electron native window handle: {value}")) +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +fn physical_rect(rect: RenderSurfaceRect, scale: f32) -> (i32, i32, u32, u32) { + let scale = if scale.is_finite() { + scale.clamp(0.25, 8.0) + } else { + 1.0 + }; + ( + (rect.x as f32 * scale).round() as i32, + (rect.y as f32 * scale).round() as i32, + ((rect.width as f32 * scale).round() as u32).max(2), + ((rect.height as f32 * scale).round() as u32).max(2), + ) +} + +#[cfg(target_os = "windows")] +mod platform { + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use windows_sys::Win32::Foundation::{GetLastError, SetLastError}; + use windows_sys::Win32::UI::WindowsAndMessaging::{ + GWL_EXSTYLE, GWL_STYLE, GetWindowLongPtrW, HWND_BOTTOM, SW_HIDE, SWP_NOACTIVATE, + SWP_SHOWWINDOW, SetParent, SetWindowLongPtrW, SetWindowPos, ShowWindow, WS_CHILD, + WS_DISABLED, WS_EX_NOACTIVATE, WS_EX_TRANSPARENT, WS_VISIBLE, + }; + + use super::*; + + pub(crate) struct Surface { + child: windows_sys::Win32::Foundation::HWND, + parent: windows_sys::Win32::Foundation::HWND, + } + + impl Surface { + pub(crate) fn new(window: &Window) -> Result { + let child = match window + .window_handle() + .map_err(|error| format!("SDL Win32 handle unavailable: {error}"))? + .as_raw() + { + RawWindowHandle::Win32(handle) => handle.hwnd.get() as _, + _ => return Err("SDL did not create a Win32 presentation window".to_owned()), + }; + Ok(Self { + child, + parent: std::ptr::null_mut(), + }) + } + + pub(crate) fn attach_and_show( + &mut self, + parent_handle: &str, + rect: RenderSurfaceRect, + _screen_rect: Option, + scale: f32, + ) -> Result<(), String> { + let parent = parse_handle(parent_handle)? as _; + let (x, y, width, height) = physical_rect(rect, scale); + unsafe { + if self.parent != parent { + SetLastError(0); + if SetParent(self.child, parent).is_null() && GetLastError() != 0 { + return Err("failed to parent SDL video surface to Electron".to_owned()); + } + self.parent = parent; + let style = GetWindowLongPtrW(self.child, GWL_STYLE) as u32; + SetWindowLongPtrW( + self.child, + GWL_STYLE, + ((style & !WS_VISIBLE) | WS_CHILD | WS_DISABLED) as isize, + ); + let extended = GetWindowLongPtrW(self.child, GWL_EXSTYLE) as u32; + SetWindowLongPtrW( + self.child, + GWL_EXSTYLE, + (extended | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT) as isize, + ); + } + if SetWindowPos( + self.child, + HWND_BOTTOM, + x, + y, + width as i32, + height as i32, + SWP_NOACTIVATE | SWP_SHOWWINDOW, + ) == 0 + { + return Err("failed to position Electron child video surface".to_owned()); + } + } + Ok(()) + } + + pub(crate) fn hide(&mut self) { + unsafe { + ShowWindow(self.child, SW_HIDE); + } + } + + pub(crate) fn refresh_ordering(&mut self) -> Result<(), String> { + Ok(()) + } + } +} + +#[cfg(target_os = "linux")] +mod platform { + use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; + use x11_dl::xlib; + + use super::*; + + pub(crate) struct Surface { + xlib: xlib::Xlib, + display: *mut xlib::Display, + child: xlib::Window, + parent: xlib::Window, + } + + impl Surface { + pub(crate) fn new(window: &Window) -> Result { + let child = match window + .window_handle() + .map_err(|error| format!("SDL X11 window handle unavailable: {error}"))? + .as_raw() + { + RawWindowHandle::Xlib(handle) => handle.window, + RawWindowHandle::Wayland(_) => { + return Err( + "native Electron surface embedding requires an X11/XWayland session" + .to_owned(), + ); + } + _ => return Err("SDL did not create an X11 presentation window".to_owned()), + }; + let display = match window + .display_handle() + .map_err(|error| format!("SDL X11 display handle unavailable: {error}"))? + .as_raw() + { + RawDisplayHandle::Xlib(handle) => handle + .display + .map(|display| display.as_ptr()) + .unwrap_or(std::ptr::null_mut()), + _ => std::ptr::null_mut(), + }; + if display.is_null() { + return Err("SDL X11 display pointer is null".to_owned()); + } + Ok(Self { + xlib: xlib::Xlib::open() + .map_err(|error| format!("failed to load X11 embedding API: {error}"))?, + display: display.cast(), + child, + parent: 0, + }) + } + + pub(crate) fn attach_and_show( + &mut self, + parent_handle: &str, + rect: RenderSurfaceRect, + _screen_rect: Option, + scale: f32, + ) -> Result<(), String> { + let parent = parse_handle(parent_handle)? as xlib::Window; + let (x, y, width, height) = physical_rect(rect, scale); + unsafe { + if self.parent != parent { + (self.xlib.XUnmapWindow)(self.display, self.child); + (self.xlib.XReparentWindow)(self.display, self.child, parent, x, y); + (self.xlib.XSelectInput)(self.display, self.child, xlib::StructureNotifyMask); + self.parent = parent; + } + (self.xlib.XMoveResizeWindow)(self.display, self.child, x, y, width, height); + (self.xlib.XLowerWindow)(self.display, self.child); + (self.xlib.XMapWindow)(self.display, self.child); + (self.xlib.XFlush)(self.display); + } + Ok(()) + } + + pub(crate) fn hide(&mut self) { + unsafe { + (self.xlib.XUnmapWindow)(self.display, self.child); + (self.xlib.XFlush)(self.display); + } + } + + pub(crate) fn refresh_ordering(&mut self) -> Result<(), String> { + Ok(()) + } + } +} + +#[cfg(target_os = "macos")] +mod platform { + use std::time::{Duration, Instant}; + + use objc2::rc::Retained; + use objc2_app_kit::{NSView, NSWindow, NSWorkspace}; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + + use super::*; + + const ORDERING_POLL_INTERVAL: Duration = Duration::from_millis(100); + + pub(crate) struct Surface { + child: Retained, + raw_window: *mut sdl2::sys::SDL_Window, + parent_pid: libc::pid_t, + requested_visible: bool, + ordered: bool, + last_ordering_check: Option, + } + + impl Surface { + pub(crate) fn new(window: &Window) -> Result { + let child_view = match window + .window_handle() + .map_err(|error| format!("SDL AppKit handle unavailable: {error}"))? + .as_raw() + { + RawWindowHandle::AppKit(handle) => unsafe { + &*handle.ns_view.as_ptr().cast::() + }, + _ => return Err("SDL did not create an AppKit presentation window".to_owned()), + }; + let child = child_view + .window() + .ok_or_else(|| "SDL AppKit view has no window".to_owned())?; + child.setIgnoresMouseEvents(true); + Ok(Self { + child, + raw_window: window.raw(), + parent_pid: unsafe { libc::getppid() }, + requested_visible: false, + ordered: false, + last_ordering_check: None, + }) + } + + pub(crate) fn attach_and_show( + &mut self, + _parent_handle: &str, + _rect: RenderSurfaceRect, + screen_rect: Option, + _scale: f32, + ) -> Result<(), String> { + let screen_rect = screen_rect.ok_or_else(|| { + "macOS native surface is missing absolute screen bounds".to_owned() + })?; + let width = i32::try_from(screen_rect.width) + .map_err(|_| "macOS native surface width is out of range".to_owned())?; + let height = i32::try_from(screen_rect.height) + .map_err(|_| "macOS native surface height is out of range".to_owned())?; + unsafe { + sdl2::sys::SDL_SetWindowPosition(self.raw_window, screen_rect.x, screen_rect.y); + sdl2::sys::SDL_SetWindowSize(self.raw_window, width, height); + } + self.requested_visible = true; + self.last_ordering_check = None; + self.refresh_ordering() + } + + pub(crate) fn hide(&mut self) { + self.requested_visible = false; + if self.ordered { + unsafe { + sdl2::sys::SDL_HideWindow(self.raw_window); + } + self.ordered = false; + } + } + + pub(crate) fn refresh_ordering(&mut self) -> Result<(), String> { + if self + .last_ordering_check + .is_some_and(|last| last.elapsed() < ORDERING_POLL_INTERVAL) + { + return Ok(()); + } + self.last_ordering_check = Some(Instant::now()); + let should_order = self.requested_visible && self.parent_is_frontmost(); + if should_order == self.ordered { + return Ok(()); + } + unsafe { + if should_order { + sdl2::sys::SDL_ShowWindow(self.raw_window); + } else { + sdl2::sys::SDL_HideWindow(self.raw_window); + } + } + if should_order { + self.child.orderFrontRegardless(); + } + self.ordered = should_order; + Ok(()) + } + + fn parent_is_frontmost(&self) -> bool { + NSWorkspace::sharedWorkspace() + .frontmostApplication() + .is_some_and(|application| application.processIdentifier() == self.parent_pid) + } + } +} + +#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] +mod platform { + use super::*; + + pub(crate) struct Surface; + + impl Surface { + pub(crate) fn new(_window: &Window) -> Result { + Err("native presentation is unsupported on this operating system".to_owned()) + } + + pub(crate) fn attach_and_show( + &mut self, + _parent_handle: &str, + _rect: RenderSurfaceRect, + _screen_rect: Option, + _scale: f32, + ) -> Result<(), String> { + Err("native presentation is unsupported on this operating system".to_owned()) + } + + pub(crate) fn hide(&mut self) {} + + pub(crate) fn refresh_ordering(&mut self) -> Result<(), String> { + Ok(()) + } + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/output.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/output.rs new file mode 100644 index 000000000..5448012a9 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/output.rs @@ -0,0 +1,417 @@ +use std::collections::VecDeque; +use std::sync::{Arc, Mutex}; + +use opennow_streamer_protocol::RenderSurface; +use sdl2::audio::{AudioCallback, AudioDevice, AudioSpecDesired}; +use sdl2::pixels::{Color, PixelFormatEnum}; +use sdl2::rect::Rect; +use sdl2::render::{Texture, WindowCanvas}; + +use crate::native_surface::NativeSurface; + +const AUDIO_SAMPLE_RATE: i32 = 48_000; +const AUDIO_CHANNELS: u8 = 2; +const AUDIO_BUFFER_FRAMES: u16 = 480; +const MAX_AUDIO_LATENCY_MS: usize = 120; + +#[derive(Debug)] +pub(crate) struct DecodedVideoFrame { + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) rgb: Vec, +} + +#[derive(Debug)] +pub(crate) struct OutputBuffers { + video: Mutex>, + audio: Mutex>, + audio_capacity: usize, +} + +impl OutputBuffers { + pub(crate) fn new() -> Self { + Self { + video: Mutex::new(None), + audio: Mutex::new(VecDeque::with_capacity( + AUDIO_SAMPLE_RATE as usize * AUDIO_CHANNELS as usize * MAX_AUDIO_LATENCY_MS / 1_000, + )), + audio_capacity: AUDIO_SAMPLE_RATE as usize + * AUDIO_CHANNELS as usize + * MAX_AUDIO_LATENCY_MS + / 1_000, + } + } + + pub(crate) fn replace_video(&self, frame: DecodedVideoFrame) -> bool { + self.video + .lock() + .unwrap_or_else(|error| error.into_inner()) + .replace(frame) + .is_some() + } + + pub(crate) fn take_video(&self) -> Option { + self.video + .lock() + .unwrap_or_else(|error| error.into_inner()) + .take() + } + + pub(crate) fn push_audio(&self, samples: &[f32]) -> usize { + let mut audio = self.audio.lock().unwrap_or_else(|error| error.into_inner()); + let overflow = audio + .len() + .saturating_add(samples.len()) + .saturating_sub(self.audio_capacity); + if overflow > 0 { + let drain_count = overflow.min(audio.len()); + audio.drain(..drain_count); + } + if samples.len() >= self.audio_capacity { + audio.clear(); + audio.extend( + samples[samples.len() - self.audio_capacity..] + .iter() + .copied(), + ); + } else { + audio.extend(samples.iter().copied()); + } + overflow + } + + pub(crate) fn clear(&self) { + self.take_video(); + self.audio + .lock() + .unwrap_or_else(|error| error.into_inner()) + .clear(); + } + + fn fill_audio(&self, destination: &mut [f32]) { + let mut audio = self.audio.lock().unwrap_or_else(|error| error.into_inner()); + for sample in destination { + *sample = audio.pop_front().unwrap_or(0.0); + } + } +} + +struct StreamAudioCallback { + output: Arc, +} + +impl AudioCallback for StreamAudioCallback { + type Channel = f32; + + fn callback(&mut self, output: &mut [f32]) { + self.output.fill_audio(output); + } +} + +pub(crate) struct SoftwareOutput { + _sdl: sdl2::Sdl, + canvas: WindowCanvas, + texture: Option, + texture_size: Option<(u32, u32)>, + event_pump: sdl2::EventPump, + audio: AudioDevice, + output: Arc, + native_surface: Result, + visible: bool, + paused: bool, +} + +impl SoftwareOutput { + fn initialize(output: Arc) -> Result { + let sdl = sdl2::init().map_err(|error| format!("SDL initialization failed: {error}"))?; + let video = sdl + .video() + .map_err(|error| format!("SDL video initialization failed: {error}"))?; + let audio_subsystem = sdl + .audio() + .map_err(|error| format!("SDL audio initialization failed: {error}"))?; + let window = video + .window("OpenNOW Stream", 1280, 720) + .position_centered() + .resizable() + .borderless() + .hidden() + .metal_view() + .build() + .map_err(|error| format!("native video window creation failed: {error}"))?; + let mut canvas = window + .into_canvas() + .build() + .map_err(|error| format!("native video renderer creation failed: {error}"))?; + canvas.set_draw_color(Color::BLACK); + canvas.clear(); + canvas.present(); + + let desired = AudioSpecDesired { + freq: Some(AUDIO_SAMPLE_RATE), + channels: Some(AUDIO_CHANNELS), + samples: Some(AUDIO_BUFFER_FRAMES), + }; + let callback_output = Arc::clone(&output); + let audio = audio_subsystem + .open_playback(None, &desired, move |_| StreamAudioCallback { + output: callback_output, + }) + .map_err(|error| format!("native audio output creation failed: {error}"))?; + if audio.spec().freq != AUDIO_SAMPLE_RATE || audio.spec().channels != AUDIO_CHANNELS { + return Err(format!( + "native audio output returned unsupported format: {} Hz, {} channels", + audio.spec().freq, + audio.spec().channels + )); + } + let event_pump = sdl + .event_pump() + .map_err(|error| format!("native window event pump creation failed: {error}"))?; + let native_surface = if video.current_video_driver() == "dummy" { + Err("dummy SDL video driver has no native presentation handle".to_owned()) + } else { + NativeSurface::new(canvas.window()) + }; + + Ok(Self { + _sdl: sdl, + canvas, + texture: None, + texture_size: None, + event_pump, + audio, + output, + native_surface, + visible: false, + paused: false, + }) + } + + fn start(&mut self, surface: Option<&RenderSurface>) -> Result<(), String> { + self.paused = false; + self.output.clear(); + self.audio.resume(); + if let Some(surface) = surface { + self.update_surface(surface)?; + } + Ok(()) + } + + fn set_paused(&mut self, paused: bool) { + self.paused = paused; + if paused { + self.audio.pause(); + self.output.clear(); + } else { + self.audio.resume(); + } + } + + fn stop(&mut self) { + self.audio.pause(); + self.output.clear(); + if let Ok(surface) = self.native_surface.as_mut() { + surface.hide(); + } + self.visible = false; + self.paused = false; + self.texture = None; + self.texture_size = None; + } + + fn update_surface(&mut self, surface: &RenderSurface) -> Result<(), String> { + let Some(rect) = surface.rect.filter(|_| surface.visible) else { + if let Ok(native_surface) = self.native_surface.as_mut() { + native_surface.hide(); + } + self.visible = false; + return Ok(()); + }; + let parent_handle = surface + .window_handle + .as_deref() + .ok_or_else(|| "visible native surface is missing Electron windowHandle".to_owned())?; + self.native_surface + .as_mut() + .map_err(|error| error.clone())? + .attach_and_show( + parent_handle, + rect, + surface.screen_rect, + surface.device_scale_factor, + )?; + self.visible = true; + Ok(()) + } + + fn pump(&mut self) -> Result<(), String> { + if let Ok(surface) = self.native_surface.as_mut() { + surface.refresh_ordering()?; + } + for event in self.event_pump.poll_iter() { + if matches!(event, sdl2::event::Event::Quit { .. }) { + if let Ok(surface) = self.native_surface.as_mut() { + surface.hide(); + } + self.visible = false; + } + } + if self.paused || !self.visible { + self.output.take_video(); + return Ok(()); + } + let Some(frame) = self.output.take_video() else { + return Ok(()); + }; + if self.texture_size != Some((frame.width, frame.height)) { + self.texture = Some( + self.canvas + .texture_creator() + .create_texture_streaming(PixelFormatEnum::RGB24, frame.width, frame.height) + .map_err(|error| format!("video texture creation failed: {error}"))?, + ); + self.texture_size = Some((frame.width, frame.height)); + } + let texture = self.texture.as_mut().expect("texture was just created"); + texture + .update(None, &frame.rgb, frame.width as usize * 3) + .map_err(|error| format!("video texture upload failed: {error}"))?; + let (output_width, output_height) = self.canvas.output_size()?; + let target = aspect_fit(frame.width, frame.height, output_width, output_height); + self.canvas.set_draw_color(Color::BLACK); + self.canvas.clear(); + self.canvas.copy(texture, None, target)?; + self.canvas.present(); + Ok(()) + } +} + +pub(crate) enum ActiveOutput { + Software(SoftwareOutput), + #[cfg(target_os = "macos")] + Mac(crate::macos_backend::MacOutput), +} + +impl ActiveOutput { + pub(crate) fn initialize( + output: Arc, + use_macos_hardware: bool, + ) -> Result { + #[cfg(target_os = "macos")] + if use_macos_hardware { + return Ok(Self::Mac(crate::macos_backend::MacOutput::initialize())); + } + let _ = use_macos_hardware; + SoftwareOutput::initialize(output).map(Self::Software) + } + + pub(crate) fn start(&mut self, surface: Option<&RenderSurface>) -> Result<(), String> { + match self { + Self::Software(output) => output.start(surface), + #[cfg(target_os = "macos")] + Self::Mac(output) => output.start(surface), + } + } + + pub(crate) fn set_paused(&mut self, paused: bool) -> Result<(), String> { + match self { + Self::Software(output) => { + output.set_paused(paused); + Ok(()) + } + #[cfg(target_os = "macos")] + Self::Mac(output) => output.set_paused(paused), + } + } + + pub(crate) fn stop(&mut self) { + match self { + Self::Software(output) => output.stop(), + #[cfg(target_os = "macos")] + Self::Mac(output) => output.stop(), + } + } + + pub(crate) fn update_surface(&mut self, surface: &RenderSurface) -> Result<(), String> { + match self { + Self::Software(output) => output.update_surface(surface), + #[cfg(target_os = "macos")] + Self::Mac(output) => output.update_surface(surface), + } + } + + pub(crate) fn pump(&mut self) -> Result<(), String> { + match self { + Self::Software(output) => output.pump(), + #[cfg(target_os = "macos")] + Self::Mac(output) => output.pump(), + } + } + + #[cfg(target_os = "macos")] + pub(crate) fn configure_macos_h264( + &mut self, + parameter_sets: opennow_streamer_platform_macos::H264ParameterSets, + ) -> Result { + match self { + Self::Mac(output) => output.configure_h264(parameter_sets), + Self::Software(_) => Err("VideoToolbox is not the selected media backend".to_owned()), + } + } +} + +fn aspect_fit(source_width: u32, source_height: u32, width: u32, height: u32) -> Rect { + let scale = (width as f64 / source_width as f64).min(height as f64 / source_height as f64); + let target_width = (source_width as f64 * scale).round() as u32; + let target_height = (source_height as f64 * scale).round() as u32; + Rect::new( + ((width - target_width) / 2) as i32, + ((height - target_height) / 2) as i32, + target_width, + target_height, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn audio_buffer_drops_oldest_samples() { + let output = OutputBuffers { + video: Mutex::new(None), + audio: Mutex::new(VecDeque::new()), + audio_capacity: 4, + }; + assert_eq!(output.push_audio(&[1.0, 2.0, 3.0]), 0); + assert_eq!(output.push_audio(&[4.0, 5.0, 6.0]), 2); + let mut values = [0.0; 4]; + output.fill_audio(&mut values); + assert_eq!(values, [3.0, 4.0, 5.0, 6.0]); + } + + #[test] + fn video_slot_keeps_only_the_latest_frame() { + let output = OutputBuffers::new(); + assert!(!output.replace_video(DecodedVideoFrame { + width: 1, + height: 1, + rgb: vec![1, 2, 3], + })); + assert!(output.replace_video(DecodedVideoFrame { + width: 2, + height: 1, + rgb: vec![4; 6], + })); + assert_eq!(output.take_video().expect("frame").width, 2); + } + + #[test] + fn aspect_fit_letterboxes_without_stretching() { + assert_eq!( + aspect_fit(1920, 1080, 1000, 1000), + Rect::new(0, 218, 1000, 563) + ); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/queue.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/queue.rs new file mode 100644 index 000000000..89cd6a4a0 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/queue.rs @@ -0,0 +1,107 @@ +use std::collections::VecDeque; +use std::sync::{Condvar, Mutex}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PushResult { + Queued, + DroppedOldest, + Closed, +} + +#[derive(Debug)] +struct QueueState { + values: VecDeque, + closed: bool, +} + +#[derive(Debug)] +pub(crate) struct BoundedQueue { + capacity: usize, + state: Mutex>, + ready: Condvar, +} + +impl BoundedQueue { + pub(crate) fn new(capacity: usize) -> Self { + assert!(capacity > 0, "bounded queue capacity must be non-zero"); + Self { + capacity, + state: Mutex::new(QueueState { + values: VecDeque::with_capacity(capacity), + closed: false, + }), + ready: Condvar::new(), + } + } + + pub(crate) fn push(&self, value: T) -> PushResult { + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + if state.closed { + return PushResult::Closed; + } + let result = if state.values.len() == self.capacity { + state.values.pop_front(); + PushResult::DroppedOldest + } else { + PushResult::Queued + }; + state.values.push_back(value); + self.ready.notify_one(); + result + } + + pub(crate) fn pop(&self) -> Option { + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + loop { + if let Some(value) = state.values.pop_front() { + return Some(value); + } + if state.closed { + return None; + } + state = self + .ready + .wait(state) + .unwrap_or_else(|error| error.into_inner()); + } + } + + pub(crate) fn clear(&self) { + self.state + .lock() + .unwrap_or_else(|error| error.into_inner()) + .values + .clear(); + } + + pub(crate) fn close(&self) { + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + state.closed = true; + state.values.clear(); + self.ready.notify_all(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn drops_the_oldest_value_at_capacity() { + let queue = BoundedQueue::new(2); + assert_eq!(queue.push(1), PushResult::Queued); + assert_eq!(queue.push(2), PushResult::Queued); + assert_eq!(queue.push(3), PushResult::DroppedOldest); + assert_eq!(queue.pop(), Some(2)); + assert_eq!(queue.pop(), Some(3)); + } + + #[test] + fn close_wakes_and_rejects_producers() { + let queue = BoundedQueue::new(1); + queue.push(1); + queue.close(); + assert_eq!(queue.pop(), None); + assert_eq!(queue.push(2), PushResult::Closed); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-platform/src/runtime.rs b/native/opennow-streamer/crates/opennow-streamer-platform/src/runtime.rs new file mode 100644 index 000000000..05c7cdacf --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-platform/src/runtime.rs @@ -0,0 +1,340 @@ +use std::marker::PhantomData; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::time::Duration; + +use opennow_streamer_protocol::RenderSurface; + +use crate::media::{MediaFeedback, MediaSession}; +use crate::output::{ActiveOutput, OutputBuffers}; + +const HOST_POLL_INTERVAL: Duration = Duration::from_millis(2); +const HOST_START_TIMEOUT: Duration = Duration::from_secs(10); +const HOST_CONTROL_TIMEOUT: Duration = Duration::from_secs(2); + +#[cfg(target_os = "macos")] +pub(crate) enum MacH264Configuration { + Hardware(opennow_streamer_platform_macos::StreamSink), + SoftwareFallback { reason: String }, +} + +pub(crate) enum HostCommand { + Start { + reply: Sender>, + feedback: Sender, + }, + Pause { + paused: bool, + reply: Option>>, + }, + Surface { + surface: RenderSurface, + reply: Sender>, + }, + #[cfg(target_os = "macos")] + ConfigureMacH264 { + parameter_sets: opennow_streamer_platform_macos::H264ParameterSets, + reply: Sender>, + }, + Stop, + Shutdown, +} + +#[derive(Clone)] +pub struct MediaRuntime { + commands: Sender, + output: Arc, + paused: Arc, + use_macos_hardware: Arc, +} + +impl MediaRuntime { + pub fn start(&self, feedback: Sender) -> Result { + let (reply, response) = mpsc::channel(); + self.commands + .send(HostCommand::Start { + reply, + feedback: feedback.clone(), + }) + .map_err(|_| "native media host is no longer running".to_owned())?; + response + .recv_timeout(HOST_START_TIMEOUT) + .map_err(|_| "native media host did not start on the UI thread".to_owned())??; + match MediaSession::spawn( + Arc::clone(&self.output), + feedback, + self.commands.clone(), + self.use_macos_hardware.load(Ordering::Acquire), + ) { + Ok(session) => { + if self.paused.load(Ordering::Acquire) { + session.set_paused(true); + } + Ok(session) + } + Err(error) => { + let _ = self.commands.send(HostCommand::Stop); + Err(error) + } + } + } + + pub fn update_surface(&self, surface: RenderSurface) -> Result<(), String> { + let (reply, response) = mpsc::channel(); + self.commands + .send(HostCommand::Surface { surface, reply }) + .map_err(|_| "native media host is no longer running".to_owned())?; + response + .recv_timeout(HOST_CONTROL_TIMEOUT) + .map_err(|_| "native media host did not apply the Electron surface update".to_owned())? + } + + pub fn set_paused(&self, paused: bool) -> Result<(), String> { + self.paused.store(paused, Ordering::Release); + let (reply, response) = mpsc::channel(); + self.commands + .send(HostCommand::Pause { + paused, + reply: Some(reply), + }) + .map_err(|_| "native media host is no longer running".to_owned())?; + response + .recv_timeout(HOST_CONTROL_TIMEOUT) + .map_err(|_| "native media host did not apply the pause update".to_owned())? + } + + pub fn shutdown(&self) { + let _ = self.commands.send(HostCommand::Shutdown); + } +} + +pub struct MainThreadHost { + commands: Receiver, + output: Arc, + _not_send: PhantomData>, + use_macos_hardware: Arc, +} + +impl MainThreadHost { + pub fn run(self) { + let mut active: Option = None; + let mut surface: Option = None; + let mut paused = false; + let mut feedback: Option> = None; + loop { + match self.commands.recv_timeout(HOST_POLL_INTERVAL) { + Ok(HostCommand::Start { + reply, + feedback: session_feedback, + }) => { + if let Some(output) = active.as_mut() { + output.stop(); + } + match ActiveOutput::initialize( + Arc::clone(&self.output), + self.use_macos_hardware.load(Ordering::Acquire), + ) { + Ok(mut output) => { + if let Err(error) = output.start(surface.as_ref()) { + output.stop(); + active = None; + feedback = None; + let _ = reply.send(Err(error)); + continue; + } + if let Err(error) = output.set_paused(paused) { + output.stop(); + active = None; + feedback = None; + let _ = reply.send(Err(error)); + continue; + } + active = Some(output); + feedback = Some(session_feedback); + let _ = reply.send(Ok(())); + } + Err(error) => { + active = None; + feedback = None; + let _ = reply.send(Err(error)); + } + } + } + Ok(HostCommand::Pause { + paused: new_paused, + reply, + }) => { + let result = active + .as_mut() + .map_or(Ok(()), |output| output.set_paused(new_paused)); + if result.is_ok() { + paused = new_paused; + } + if let Some(reply) = reply { + let _ = reply.send(result); + } + } + Ok(HostCommand::Surface { + surface: new_surface, + reply, + }) => { + static SURFACE_LOG_REMAINING: AtomicU64 = AtomicU64::new(12); + if SURFACE_LOG_REMAINING + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |remaining| { + remaining.checked_sub(1) + }) + .is_ok() + { + eprintln!( + "NVST surface-update visible={} rect={:?}", + new_surface.visible, new_surface.screen_rect + ); + } + let result = if let Some(output) = active.as_mut() { + output.update_surface(&new_surface) + } else { + Ok(()) + }; + if let Err(message) = &result { + if let Some(output) = active.as_mut() { + if let Some(feedback) = feedback.as_ref() { + let _ = feedback.send(MediaFeedback::OutputError { + message: message.clone(), + }); + } + output.stop(); + active = None; + feedback = None; + } + } + surface = Some(new_surface); + let _ = reply.send(result); + } + #[cfg(target_os = "macos")] + Ok(HostCommand::ConfigureMacH264 { + parameter_sets, + reply, + }) => { + let hardware_result = active + .as_mut() + .ok_or_else(|| "native media output is not active".to_owned()) + .and_then(|output| output.configure_macos_h264(parameter_sets)); + match hardware_result { + Ok(sink) => { + let _ = reply.send(Ok(MacH264Configuration::Hardware(sink))); + } + Err(hardware_error) => { + self.use_macos_hardware.store(false, Ordering::Release); + crate::macos_backend::disable(); + if let Some(output) = active.as_mut() { + output.stop(); + } + let fallback = + ActiveOutput::initialize(Arc::clone(&self.output), false).and_then( + |mut output| { + output.start(surface.as_ref())?; + output.set_paused(paused)?; + Ok(output) + }, + ); + match fallback { + Ok(output) => { + active = Some(output); + let _ = + reply.send(Ok(MacH264Configuration::SoftwareFallback { + reason: hardware_error, + })); + } + Err(fallback_error) => { + active = None; + let _ = reply.send(Err(format!( + "VideoToolbox startup failed ({hardware_error}); software output fallback also failed: {fallback_error}" + ))); + } + } + } + } + } + Ok(HostCommand::Stop) => { + if let Some(output) = active.as_mut() { + output.stop(); + } + active = None; + feedback = None; + } + Ok(HostCommand::Shutdown) | Err(mpsc::RecvTimeoutError::Disconnected) => { + if let Some(output) = active.as_mut() { + output.stop(); + } + return; + } + Err(mpsc::RecvTimeoutError::Timeout) => {} + } + if let Some(output) = active.as_mut() + && let Err(message) = output.pump() + { + if let Some(feedback) = feedback.as_ref() { + let _ = feedback.send(MediaFeedback::OutputError { message }); + } + output.stop(); + active = None; + feedback = None; + } + // The host loop replaces `NSApplication.run()`; without draining AppKit's queue the + // overlay window never finishes ordering in and its controls stay dead. + #[cfg(target_os = "macos")] + opennow_streamer_platform_macos::pump_app_events(); + } + } +} + +pub fn create_runtime() -> Result<(MainThreadHost, MediaRuntime), String> { + ensure_macos_main_thread()?; + let use_macos_hardware = Arc::new(AtomicBool::new(use_macos_hardware())); + let (commands, receiver) = mpsc::channel(); + let output = Arc::new(OutputBuffers::new()); + let paused = Arc::new(AtomicBool::new(false)); + Ok(( + MainThreadHost { + commands: receiver, + output: Arc::clone(&output), + _not_send: PhantomData, + use_macos_hardware: Arc::clone(&use_macos_hardware), + }, + MediaRuntime { + commands, + output, + paused, + use_macos_hardware, + }, + )) +} + +#[cfg(target_os = "macos")] +fn use_macos_hardware() -> bool { + crate::macos_backend::available() +} + +#[cfg(not(target_os = "macos"))] +const fn use_macos_hardware() -> bool { + false +} + +#[cfg(target_os = "macos")] +fn ensure_macos_main_thread() -> Result<(), String> { + if unsafe { libc::pthread_main_np() } == 1 { + Ok(()) + } else { + Err( + "the macOS native media host must be created and run on the process main thread" + .to_owned(), + ) + } +} + +#[cfg(not(target_os = "macos"))] +fn ensure_macos_main_thread() -> Result<(), String> { + Ok(()) +} diff --git a/native/opennow-streamer/crates/opennow-streamer-protocol/Cargo.toml b/native/opennow-streamer/crates/opennow-streamer-protocol/Cargo.toml new file mode 100644 index 000000000..1a1670cbe --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-protocol/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "opennow-streamer-protocol" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true diff --git a/native/opennow-streamer/crates/opennow-streamer-protocol/src/lib.rs b/native/opennow-streamer/crates/opennow-streamer-protocol/src/lib.rs new file mode 100644 index 000000000..c6c5c7612 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-protocol/src/lib.rs @@ -0,0 +1,312 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; + +pub const PROTOCOL_VERSION: u64 = 4; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Command { + pub id: String, + #[serde(rename = "type")] + pub kind: String, + #[serde(default)] + pub protocol_version: Option, + #[serde(default)] + pub context: Option, + #[serde(default)] + pub sdp: Option, + #[serde(default)] + pub candidate: Option, + #[serde(default)] + pub input: Option, + #[serde(default)] + pub paused: Option, + #[serde(default)] + pub surface: Option, + #[serde(default)] + pub max_bitrate_kbps: Option, + #[serde(default)] + pub reason: Option, + #[serde(default)] + pub shortcuts: Option, + #[serde(default)] + pub host: Option, + #[serde(default)] + pub port: Option, + #[serde(default)] + pub payload_base64: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IceCandidate { + pub candidate: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sdp_mid: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sdp_m_line_index: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub username_fragment: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeInput { + #[serde(default)] + pub payload_base64: String, + #[serde(default)] + pub partially_reliable: bool, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenderSurface { + #[serde(default)] + pub rect: Option, + #[serde(default)] + pub visible: bool, + #[serde(default = "default_device_scale_factor")] + pub device_scale_factor: f32, + #[serde(default)] + pub window_handle: Option, + #[serde(default)] + pub screen_rect: Option, +} + +impl Default for RenderSurface { + fn default() -> Self { + Self { + rect: None, + visible: false, + device_scale_factor: default_device_scale_factor(), + window_handle: None, + screen_rect: None, + } + } +} + +fn default_device_scale_factor() -> f32 { + 1.0 +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenderSurfaceRect { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionContext { + pub session: Session, + pub settings: Value, + pub shortcuts: Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub nvst_video: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Session { + pub session_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sub_session_id: Option, + pub server_ip: String, + #[serde(default)] + pub ice_servers: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub media_connection_info: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub connection_info: Option>, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionInfo { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ip: Option, + pub port: u32, + pub usage: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub protocol: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub app_level_protocol: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resource_path: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IceServer { + pub urls: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub credential: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MediaConnectionInfo { + pub ip: String, + pub port: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub usage: Option, + #[serde(flatten)] + pub extra: Map, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Capabilities { + pub protocol_version: u64, + pub backend: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub fallback_reason: Option<&'static str>, + pub supports_offer_answer: bool, + pub supports_remote_ice: bool, + pub supports_local_ice: bool, + pub supports_input: bool, + pub supports_video_decode: bool, + pub supports_video_present: bool, + pub supports_audio_decode: bool, + pub supports_audio_output: bool, + pub video_backends: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VideoBackendCapability { + pub backend: &'static str, + pub platform: &'static str, + pub codecs: Vec, + pub zero_copy_modes: Vec<&'static str>, + pub available: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option<&'static str>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CodecCapability { + pub codec: &'static str, + pub available: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option<&'static str>, +} + +pub fn response(id: impl Into, kind: &str) -> Value { + serde_json::json!({ "id": id.into(), "type": kind }) +} + +pub fn error(id: Option<&str>, code: &str, message: impl Into) -> Value { + let mut value = serde_json::json!({ + "type": "error", + "code": code, + "message": message.into(), + }); + if let Some(id) = id { + value["id"] = Value::String(id.to_owned()); + } + value +} + +pub fn event(kind: &str, fields: Value) -> Value { + let mut object = fields.as_object().cloned().unwrap_or_default(); + object.insert("type".to_owned(), Value::String(kind.to_owned())); + Value::Object(object) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_forward_compatible_commands() { + let command: Command = serde_json::from_value(serde_json::json!({ + "id": "1", + "type": "start", + "context": { "session": { "sessionId": "session" } }, + "futureField": true + })) + .expect("command"); + assert_eq!(command.kind, "start"); + assert!(command.context.is_some()); + } + + #[test] + fn serializes_candidate_for_app_contract() { + let candidate = IceCandidate { + candidate: "candidate:1 1 udp 1 127.0.0.1 5000 typ host".to_owned(), + sdp_mid: Some("0".to_owned()), + sdp_m_line_index: Some(0), + username_fragment: None, + }; + let value = serde_json::to_value(candidate).expect("candidate"); + assert_eq!(value["sdpMLineIndex"], 0); + } + + #[test] + fn unsolicited_errors_do_not_serialize_a_null_request_id() { + let value = error(None, "invalid-command", "bad JSON"); + assert!(value.get("id").is_none()); + assert_eq!(value["type"], "error"); + } + + #[test] + fn session_context_round_trips_required_and_forward_compatible_fields() { + let fixture = serde_json::json!({ + "session": { + "sessionId": "synthetic-session", + "subSessionId": "synthetic-subsession", + "serverIp": "127-0-0-1.synthetic.invalid", + "iceServers": [], + "mediaConnectionInfo": { + "ip": "198.51.100.20", + "port": 18_784, + "usage": 17, + "futureEndpointField": true + }, + "connectionInfo": [ + { + "ip": "198.51.100.10", + "port": 443, + "usage": 14, + "protocol": 1, + "resourcePath": "/nvst/" + }, + { + "ip": "198.51.100.20", + "port": 48322, + "usage": 16, + "protocol": 1, + "appLevelProtocol": 6, + "resourcePath": "rtsps://198.51.100.20:48322/session", + "futureConnectionField": true + } + ], + "futureSessionField": "preserved" + }, + "settings": { "codec": "H264", "fps": 60 }, + "shortcuts": { "stopStream": "Ctrl+Shift+Q" }, + "futureContextField": 42 + }); + + let context: SessionContext = serde_json::from_value(fixture.clone()).expect("context"); + assert_eq!(context.session.session_id, "synthetic-session"); + assert_eq!( + serde_json::to_value(context).expect("serializable context"), + fixture + ); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-transport/Cargo.toml b/native/opennow-streamer/crates/opennow-streamer-transport/Cargo.toml new file mode 100644 index 000000000..ca2e3bc81 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-transport/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "opennow-streamer-transport" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +aes.workspace = true +ctr.workspace = true +crc32fast.workspace = true +getrandom.workspace = true +ghash.workspace = true +hmac.workspace = true +opennow-streamer-protocol = { path = "../opennow-streamer-protocol" } +serde_json.workspace = true +sha1.workspace = true +socket2.workspace = true +subtle.workspace = true +thiserror.workspace = true + +[target.'cfg(windows)'.dependencies] +str0m = { workspace = true, features = ["wincrypto-dimpl"] } + +[target.'cfg(target_os = "macos")'.dependencies] +str0m = { workspace = true, features = ["apple-crypto"] } + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +str0m = { workspace = true, features = ["rust-crypto"] } + +[dev-dependencies] +serde_json.workspace = true diff --git a/native/opennow-streamer/crates/opennow-streamer-transport/src/lib.rs b/native/opennow-streamer/crates/opennow-streamer-transport/src/lib.rs new file mode 100644 index 000000000..09f635644 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-transport/src/lib.rs @@ -0,0 +1,936 @@ +use std::io::ErrorKind; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs, UdpSocket}; +use std::ops::Range; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender, SyncSender, TryRecvError, TrySendError}; +use std::sync::{Arc, Once}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use opennow_streamer_protocol::{IceCandidate, IceServer, MediaConnectionInfo, Session}; +use str0m::change::SdpOffer; +use str0m::channel::{ChannelConfig, ChannelId, Reliability}; +use str0m::crypto::from_feature_flags; +use str0m::media::{KeyframeRequestKind, Mid}; +use str0m::net::{Protocol, Receive}; +use str0m::{Candidate, Event, IceConnectionState, Input, Output, Rtc, RtcConfig}; +use thiserror::Error; + +pub mod nvst; + +pub use nvst::{ + BoundedFrameQueue, EncodedH264Frame, NvstBundleIdentity, NvstConfigError, NvstDropReason, + NvstFallbackReason, NvstReceiveEvent, NvstReceiverState, NvstRecovery, NvstSrtpProfile, + NvstUdpReceiverError, NvstUdpReceiverSession, NvstUnsupportedFeature, NvstVideoCodec, + NvstVideoConfig, NvstVideoReceiver, PreferredVideoTransport, ReservedNvstBundle, + advertised_nvst_ipv4, nack_transmission_support, parse_nvst_video_handoff, + reserve_nvst_mjolnir_udp_socket, reserve_nvst_udp_socket, + select_preferred_video_transport, spawn_nvst_mjolnir_receiver, spawn_nvst_udp_receiver, + spawn_nvst_udp_receiver_with_socket, +}; + +static INSTALL_CRYPTO: Once = Once::new(); +const RELIABLE_INPUT_LABEL: &str = "input_channel_v1"; +const PARTIAL_INPUT_LABEL: &str = "input_channel_partially_reliable"; +const STATS_LABEL: &str = "stats_channel"; + +#[derive(Debug, Error)] +pub enum TransportError { + #[error("invalid server endpoint: {0}")] + InvalidServerEndpoint(String), + #[error("failed to resolve server endpoint {endpoint}: {source}")] + ResolveServerEndpoint { + endpoint: String, + #[source] + source: std::io::Error, + }, + #[error("invalid WebRTC media endpoint: {0}")] + InvalidMediaEndpoint(String), + #[error("failed to resolve WebRTC media endpoint {endpoint}: {source}")] + ResolveMediaEndpoint { + endpoint: String, + #[source] + source: std::io::Error, + }, + #[error("failed to bind media UDP socket: {0}")] + Bind(#[source] std::io::Error), + #[error("invalid WebRTC offer: {0}")] + Offer(String), + #[error("failed to configure local ICE candidate: {0}")] + LocalCandidate(String), + #[error("invalid remote ICE candidate: {0}")] + RemoteCandidate(String), + #[error("input channel is not ready")] + InputNotReady, + #[error("encoded media consumer is no longer running")] + MediaConsumerClosed, + #[error("encoded media consumer is backpressured")] + MediaConsumerBackpressured, + #[error("transport worker is no longer running")] + Closed, +} + +impl TransportError { + pub const fn code(&self) -> &'static str { + match self { + Self::InvalidServerEndpoint(_) | Self::ResolveServerEndpoint { .. } => { + "invalid-server-endpoint" + } + Self::InvalidMediaEndpoint(_) | Self::ResolveMediaEndpoint { .. } => { + "invalid-media-endpoint" + } + Self::Bind(_) | Self::LocalCandidate(_) => "local-transport-failed", + Self::Offer(_) => "invalid-offer", + Self::RemoteCandidate(_) => "invalid-remote-candidate", + Self::InputNotReady => "input-not-ready", + Self::MediaConsumerClosed => "media-consumer-closed", + Self::MediaConsumerBackpressured => "media-consumer-backpressured", + Self::Closed => "transport-closed", + } + } +} + +#[derive(Debug)] +pub enum TransportEvent { + Connected, + Disconnected(String), + InputReady(u16), + Log(String), +} + +#[derive(Debug, Clone)] +pub struct EncodedMediaFrame { + pub mid: String, + pub codec: String, + pub payload: Arc<[u8]>, + pub rtp_timestamp: u64, + pub clock_rate_hz: u32, + pub received_at_us: u64, + pub keyframe: bool, + pub contiguous: bool, +} + +pub type MediaConsumer = SyncSender; + +pub fn install_crypto() { + INSTALL_CRYPTO.call_once(|| from_feature_flags().install_process_default()); +} + +fn deliver_media_frame( + consumer: &MediaConsumer, + frame: EncodedMediaFrame, +) -> Result<(), TransportError> { + consumer.try_send(frame).map_err(|error| match error { + TrySendError::Full(_) => TransportError::MediaConsumerBackpressured, + TrySendError::Disconnected(_) => TransportError::MediaConsumerClosed, + }) +} + +enum TransportCommand { + AddRemoteCandidate(Candidate), + SendInput { + bytes: Vec, + partially_reliable: bool, + }, + RequestKeyframe { + mid: String, + }, + Stop, +} + +pub struct NegotiatedTransport { + pub answer_sdp: String, + pub local_candidate: IceCandidate, + pub session: TransportSession, +} + +pub struct TransportSession { + commands: Sender, + join: Option>, + media_endpoint: Option, + input_ready: Arc, +} + +#[derive(Clone)] +pub struct TransportControl { + commands: Sender, +} + +impl TransportSession { + pub fn control(&self) -> TransportControl { + TransportControl { + commands: self.commands.clone(), + } + } + + pub fn add_remote_candidate(&self, candidate: &IceCandidate) -> Result<(), TransportError> { + let candidate = normalize_remote_candidate(&candidate.candidate, self.media_endpoint); + let candidate = candidate.strip_prefix("a=").unwrap_or(&candidate); + let candidate = Candidate::from_sdp_string(candidate) + .map_err(|error| TransportError::RemoteCandidate(error.to_string()))?; + self.commands + .send(TransportCommand::AddRemoteCandidate(candidate)) + .map_err(|_| TransportError::Closed) + } + + pub fn send_input( + &self, + bytes: Vec, + partially_reliable: bool, + ) -> Result<(), TransportError> { + if !self.input_ready.load(Ordering::Acquire) { + return Err(TransportError::InputNotReady); + } + self.commands + .send(TransportCommand::SendInput { + bytes, + partially_reliable, + }) + .map_err(|_| TransportError::Closed) + } + + pub fn stop(mut self) { + let _ = self.commands.send(TransportCommand::Stop); + if let Some(join) = self.join.take() { + let _ = join.join(); + } + } +} + +impl TransportControl { + pub fn request_keyframe(&self, mid: impl Into) -> Result<(), TransportError> { + self.commands + .send(TransportCommand::RequestKeyframe { mid: mid.into() }) + .map_err(|_| TransportError::Closed) + } +} + +impl Drop for TransportSession { + fn drop(&mut self) { + let _ = self.commands.send(TransportCommand::Stop); + } +} + +pub fn negotiate( + offer_sdp: &str, + session: &Session, + partial_reliable_lifetime_ms: u16, + events: Sender, + media_consumer: MediaConsumer, +) -> Result { + install_crypto(); + + if let Some(schemes) = configured_ice_schemes(&session.ice_servers) { + let _ = events.send(TransportEvent::Log(format!( + "Configured ICE services ({schemes}) are not gathered locally; continuing with a direct host candidate for the ICE-lite GFN peer" + ))); + } + let normalized_offer = normalize_offer_endpoints(offer_sdp, session)?; + let server_ip = match normalized_offer.media_endpoint { + Some(endpoint) => endpoint.ip(), + None => resolve_server_endpoint(&session.server_ip)?, + }; + let socket = bind_routed_socket(server_ip).map_err(TransportError::Bind)?; + let local_addr = socket.local_addr().map_err(TransportError::Bind)?; + let local_candidate = Candidate::host(local_addr, "udp") + .map_err(|error| TransportError::LocalCandidate(error.to_string()))?; + + let mut rtc = RtcConfig::new().build(Instant::now()); + rtc.add_local_candidate(local_candidate.clone()); + let offer = SdpOffer::from_sdp_string(&normalized_offer.sdp) + .map_err(|error| TransportError::Offer(error.to_string()))?; + let answer = rtc + .sdp_api() + .accept_offer(offer) + .map_err(|error| TransportError::Offer(error.to_string()))?; + + let reliable = rtc.direct_api().create_data_channel(ChannelConfig { + label: RELIABLE_INPUT_LABEL.to_owned(), + ..Default::default() + }); + let partial = rtc.direct_api().create_data_channel(ChannelConfig { + label: PARTIAL_INPUT_LABEL.to_owned(), + ordered: false, + reliability: Reliability::MaxPacketLifetime { + lifetime: partial_reliable_lifetime_ms, + }, + ..Default::default() + }); + let stats = rtc.direct_api().create_data_channel(ChannelConfig { + label: STATS_LABEL.to_owned(), + ordered: false, + reliability: Reliability::MaxRetransmits { retransmits: 0 }, + ..Default::default() + }); + + let answer_sdp = answer.to_sdp_string(); + let candidate_text = local_candidate.to_sdp_string(); + let (command_tx, command_rx) = mpsc::channel(); + let input_ready = Arc::new(AtomicBool::new(false)); + let worker_input_ready = input_ready.clone(); + let transport_origin = Instant::now(); + let join = thread::Builder::new() + .name("opennow-webrtc".to_owned()) + .spawn(move || { + run_transport( + rtc, + socket, + command_rx, + TransportOutputs { + events, + media_consumer, + }, + TransportChannels { + reliable, + partial, + stats, + }, + worker_input_ready, + transport_origin, + ); + }) + .map_err(TransportError::Bind)?; + + Ok(NegotiatedTransport { + answer_sdp, + local_candidate: IceCandidate { + candidate: candidate_text, + sdp_mid: Some("0".to_owned()), + sdp_m_line_index: Some(0), + username_fragment: None, + }, + session: TransportSession { + commands: command_tx, + join: Some(join), + media_endpoint: normalized_offer.media_endpoint, + input_ready, + }, + }) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NormalizedOffer { + pub sdp: String, + pub replacements: usize, + pub media_endpoint: Option, +} + +pub fn normalize_offer_endpoints( + offer_sdp: &str, + session: &Session, +) -> Result { + let media_endpoint = resolve_media_endpoint(session.media_connection_info.as_ref())?; + let Some(endpoint) = media_endpoint else { + return Ok(NormalizedOffer { + sdp: offer_sdp.to_owned(), + replacements: 0, + media_endpoint: None, + }); + }; + + let mut sdp = String::with_capacity(offer_sdp.len()); + let mut replacements = 0; + for chunk in offer_sdp.split_inclusive('\n') { + let (line, ending) = chunk + .strip_suffix("\r\n") + .map(|line| (line, "\r\n")) + .or_else(|| chunk.strip_suffix('\n').map(|line| (line, "\n"))) + .unwrap_or((chunk, "")); + let rewritten = rewrite_candidate_endpoint(line, endpoint); + replacements += usize::from(rewritten != line); + sdp.push_str(&rewritten); + sdp.push_str(ending); + } + + Ok(NormalizedOffer { + sdp, + replacements, + media_endpoint: Some(endpoint), + }) +} + +pub fn resolve_server_endpoint(endpoint: &str) -> Result { + resolve_host(endpoint, 9).map_err(|source| { + if endpoint.trim().is_empty() { + TransportError::InvalidServerEndpoint("endpoint is empty".to_owned()) + } else { + TransportError::ResolveServerEndpoint { + endpoint: endpoint.to_owned(), + source, + } + } + }) +} + +fn resolve_media_endpoint( + endpoint: Option<&MediaConnectionInfo>, +) -> Result, TransportError> { + let Some(endpoint) = endpoint.filter(|endpoint| matches!(endpoint.usage, Some(2 | 17))) else { + return Ok(None); + }; + let port = u16::try_from(endpoint.port) + .ok() + .filter(|port| *port != 0) + .ok_or_else(|| { + TransportError::InvalidMediaEndpoint(format!( + "port {} is outside 1..=65535", + endpoint.port + )) + })?; + if endpoint.ip.trim().is_empty() { + return Err(TransportError::InvalidMediaEndpoint( + "hostname is empty".to_owned(), + )); + } + resolve_host(&endpoint.ip, port) + .map(|ip| Some(SocketAddr::new(ip, port))) + .map_err(|source| TransportError::ResolveMediaEndpoint { + endpoint: format!("{}:{port}", endpoint.ip), + source, + }) +} + +fn resolve_host(host: &str, port: u16) -> std::io::Result { + let host = host.trim(); + if host.is_empty() { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "endpoint is empty", + )); + } + if let Ok(ip) = host.parse() { + return Ok(ip); + } + if let Some(ip) = dashed_ipv4_prefix(host) { + return Ok(IpAddr::V4(ip)); + } + (host, port) + .to_socket_addrs()? + .next() + .map(|address| address.ip()) + .ok_or_else(|| std::io::Error::new(ErrorKind::AddrNotAvailable, "no addresses resolved")) +} + +fn dashed_ipv4_prefix(host: &str) -> Option { + let first_label = host.split('.').next()?; + let octets = first_label + .split('-') + .map(str::parse::) + .collect::, _>>() + .ok()?; + let octets: [u8; 4] = octets.try_into().ok()?; + Some(octets.into()) +} + +fn configured_ice_schemes(servers: &[IceServer]) -> Option { + let mut schemes = servers + .iter() + .flat_map(|server| &server.urls) + .map(|url| { + url.split_once(':') + .map_or("unknown", |(scheme, _)| scheme) + .to_ascii_lowercase() + }) + .collect::>(); + if schemes.is_empty() { + return None; + } + schemes.sort_unstable(); + schemes.dedup(); + Some(schemes.join(", ")) +} + +fn normalize_remote_candidate(candidate: &str, endpoint: Option) -> String { + endpoint.map_or_else( + || candidate.to_owned(), + |endpoint| rewrite_candidate_endpoint(candidate, endpoint), + ) +} + +fn rewrite_candidate_endpoint(candidate: &str, endpoint: SocketAddr) -> String { + let candidate_body = candidate.strip_prefix("a=").unwrap_or(candidate); + if !candidate_body.starts_with("candidate:") { + return candidate.to_owned(); + } + let Some(address_range) = token_range(candidate, 4) else { + return candidate.to_owned(); + }; + let Some(port_range) = token_range(candidate, 5) else { + return candidate.to_owned(); + }; + let address = endpoint.ip().to_string(); + let port = endpoint.port().to_string(); + if candidate[address_range.clone()] == address && candidate[port_range.clone()] == port { + return candidate.to_owned(); + } + + let mut rewritten = candidate.to_owned(); + rewritten.replace_range(port_range, &port); + rewritten.replace_range(address_range, &address); + rewritten +} + +fn token_range(value: &str, target: usize) -> Option> { + let mut token = 0; + let mut start = None; + for (index, character) in value.char_indices() { + if character.is_ascii_whitespace() { + if let Some(start) = start.take() { + if token == target { + return Some(start..index); + } + token += 1; + } + } else if start.is_none() { + start = Some(index); + } + } + start + .filter(|_| token == target) + .map(|start| start..value.len()) +} + +fn bind_routed_socket(server_ip: IpAddr) -> std::io::Result { + let unspecified = match server_ip { + IpAddr::V4(_) => "0.0.0.0:0", + IpAddr::V6(_) => "[::]:0", + }; + let route_probe = UdpSocket::bind(unspecified)?; + route_probe.connect(SocketAddr::new(server_ip, 9))?; + let local_ip = route_probe.local_addr()?.ip(); + UdpSocket::bind(SocketAddr::new(local_ip, 0)) +} + +struct TransportChannels { + reliable: ChannelId, + partial: ChannelId, + stats: ChannelId, +} + +struct TransportOutputs { + events: Sender, + media_consumer: MediaConsumer, +} + +fn run_transport( + mut rtc: Rtc, + socket: UdpSocket, + commands: Receiver, + outputs: TransportOutputs, + channels: TransportChannels, + input_ready_state: Arc, + transport_origin: Instant, +) { + struct ResetInputReady(Arc); + + impl Drop for ResetInputReady { + fn drop(&mut self) { + self.0.store(false, Ordering::Release); + } + } + + let _reset_input_ready = ResetInputReady(input_ready_state.clone()); + let mut receive_buffer = vec![0_u8; 65_536]; + let mut input_ready = false; + let mut next_heartbeat = Instant::now() + Duration::from_secs(2); + + loop { + loop { + match commands.try_recv() { + Ok(TransportCommand::AddRemoteCandidate(candidate)) => { + rtc.add_remote_candidate(candidate); + } + Ok(TransportCommand::SendInput { + bytes, + partially_reliable, + }) => { + if input_ready { + let channel_id = if partially_reliable { + channels.partial + } else { + channels.reliable + }; + if let Some(mut channel) = rtc.channel(channel_id) { + let _ = channel.write(true, &bytes); + } + } + } + Ok(TransportCommand::RequestKeyframe { mid }) => { + let mid = Mid::from(mid.as_str()); + let Some(mut writer) = rtc.writer(mid) else { + let _ = outputs.events.send(TransportEvent::Log(format!( + "Unable to request keyframe for unknown media id {mid}" + ))); + continue; + }; + let kind = if writer.is_request_keyframe_possible(KeyframeRequestKind::Pli) { + Some(KeyframeRequestKind::Pli) + } else if writer.is_request_keyframe_possible(KeyframeRequestKind::Fir) { + Some(KeyframeRequestKind::Fir) + } else { + None + }; + if let Some(kind) = kind + && let Err(error) = writer.request_keyframe(None, kind) + { + let _ = outputs.events.send(TransportEvent::Log(format!( + "Failed to request keyframe for {mid}: {error}" + ))); + } + } + Ok(TransportCommand::Stop) | Err(TryRecvError::Disconnected) => { + rtc.disconnect(); + let _ = outputs + .events + .send(TransportEvent::Disconnected("stopped".to_owned())); + return; + } + Err(TryRecvError::Empty) => break, + } + } + + if input_ready && Instant::now() >= next_heartbeat { + if let Some(mut channel) = rtc.channel(channels.reliable) { + let _ = channel.write(true, &[2, 0, 0, 0]); + } + next_heartbeat = Instant::now() + Duration::from_secs(2); + } + + let timeout = loop { + match rtc.poll_output() { + Ok(Output::Timeout(timeout)) => break timeout, + Ok(Output::Transmit(transmit)) => { + let _ = socket.send_to(&transmit.contents, transmit.destination); + } + Ok(Output::Event(event)) => match event { + Event::IceConnectionStateChange(IceConnectionState::Connected) => { + let _ = outputs.events.send(TransportEvent::Connected); + } + Event::IceConnectionStateChange(IceConnectionState::Disconnected) => { + let _ = outputs + .events + .send(TransportEvent::Disconnected("ICE disconnected".to_owned())); + return; + } + Event::ChannelData(data) if data.id == channels.reliable => { + if let Some(version) = input_protocol_version(&data.data) { + input_ready = true; + input_ready_state.store(true, Ordering::Release); + let _ = outputs.events.send(TransportEvent::InputReady(version)); + } + } + Event::ChannelData(data) if data.id == channels.stats => {} + Event::MediaData(data) => { + let keyframe = data.is_keyframe(); + let received_at_us = data + .network_time + .saturating_duration_since(transport_origin) + .as_micros() + .try_into() + .unwrap_or(u64::MAX); + let frame = EncodedMediaFrame { + mid: data.mid.to_string(), + codec: format!("{:?}", data.params.spec().codec), + payload: data.data, + rtp_timestamp: data.time.numer(), + clock_rate_hz: data.time.denom(), + received_at_us, + keyframe, + contiguous: data.contiguous, + }; + if let Err(error) = deliver_media_frame(&outputs.media_consumer, frame) { + let _ = outputs + .events + .send(TransportEvent::Disconnected(error.to_string())); + return; + } + } + _ => {} + }, + Err(error) => { + let _ = outputs + .events + .send(TransportEvent::Disconnected(error.to_string())); + return; + } + } + }; + + let wait = timeout + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(10)); + if wait.is_zero() { + let _ = rtc.handle_input(Input::Timeout(Instant::now())); + continue; + } + + let _ = socket.set_read_timeout(Some(wait)); + let input = match socket.recv_from(&mut receive_buffer) { + Ok((length, source)) => { + let destination = match socket.local_addr() { + Ok(value) => value, + Err(error) => { + let _ = outputs + .events + .send(TransportEvent::Disconnected(error.to_string())); + return; + } + }; + let contents = match receive_buffer[..length].try_into() { + Ok(value) => value, + Err(error) => { + let _ = outputs.events.send(TransportEvent::Log(format!( + "Dropping oversized UDP packet: {error}" + ))); + continue; + } + }; + Input::Receive( + Instant::now(), + Receive { + proto: Protocol::Udp, + source, + destination, + contents, + }, + ) + } + Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => { + Input::Timeout(Instant::now()) + } + Err(error) => { + let _ = outputs + .events + .send(TransportEvent::Disconnected(error.to_string())); + return; + } + }; + if let Err(error) = rtc.handle_input(input) { + let _ = outputs + .events + .send(TransportEvent::Disconnected(error.to_string())); + return; + } + } +} + +fn input_protocol_version(bytes: &[u8]) -> Option { + if bytes.len() < 2 { + return None; + } + let first = u16::from_le_bytes([bytes[0], bytes[1]]); + if first == 526 { + return Some(if bytes.len() >= 4 { + u16::from_le_bytes([bytes[2], bytes[3]]) + } else { + 2 + }); + } + (bytes[0] == 0x0e).then_some(first) +} + +#[cfg(test)] +mod tests { + use super::*; + use opennow_streamer_protocol::Session; + use serde_json::json; + use str0m::media::{Direction, MediaKind}; + + fn synthetic_session(media_connection_info: serde_json::Value) -> Session { + serde_json::from_value(json!({ + "sessionId": "synthetic-session", + "serverIp": "127-0-0-1.session.synthetic.invalid", + "iceServers": [], + "mediaConnectionInfo": media_connection_info, + })) + .expect("synthetic session") + } + + #[test] + fn parses_both_input_handshake_layouts() { + assert_eq!(input_protocol_version(&[0x0e, 0x02, 0x03, 0x00]), Some(3)); + assert_eq!(input_protocol_version(&[0x0e, 0x03]), Some(0x030e)); + assert_eq!(input_protocol_version(&[1]), None); + } + + #[test] + fn normalizes_synthetic_gfn_media_endpoint_candidates() { + let session = synthetic_session(json!({ + "ip": "198-51-100-42.media.synthetic.invalid", + "port": 18_784, + "usage": 17, + })); + let offer = [ + "v=0", + "c=IN IP4 0.0.0.0", + "a=candidate:udp 1 udp 2122260223 0.0.0.0 47998 typ host", + "a=candidate:tcp 1 tcp 1518214911 203.0.113.9 9 typ host tcptype active", + ] + .join("\r\n"); + + let normalized = normalize_offer_endpoints(&offer, &session).expect("normalized offer"); + + assert_eq!(normalized.replacements, 2); + assert_eq!( + normalized.media_endpoint, + Some("198.51.100.42:18784".parse().expect("socket address")) + ); + assert!(normalized.sdp.contains("c=IN IP4 0.0.0.0")); + assert!( + normalized + .sdp + .contains("a=candidate:udp 1 udp 2122260223 198.51.100.42 18784 typ host") + ); + assert!(normalized.sdp.contains( + "a=candidate:tcp 1 tcp 1518214911 198.51.100.42 18784 typ host tcptype active" + )); + assert!(normalized.sdp.contains("\r\n")); + } + + #[test] + fn rewrites_trickled_candidate_to_the_normalized_media_endpoint() { + let endpoint = Some("198.51.100.42:18784".parse().expect("socket address")); + let candidate = "candidate:remote 1 udp 2122260223 203.0.113.9 47998 typ host"; + + assert_eq!( + normalize_remote_candidate(candidate, endpoint), + "candidate:remote 1 udp 2122260223 198.51.100.42 18784 typ host" + ); + assert_eq!( + normalize_remote_candidate("candidate:malformed", endpoint), + "candidate:malformed" + ); + } + + #[test] + fn skips_non_webrtc_media_endpoint_usage() { + let session = synthetic_session(json!({ + "ip": "198.51.100.42", + "port": 18_784, + "usage": 14, + })); + let offer = "v=0\na=candidate:udp 1 udp 1 203.0.113.9 47998 typ host\n"; + + let normalized = normalize_offer_endpoints(offer, &session).expect("normalized offer"); + + assert_eq!(normalized.sdp, offer); + assert_eq!(normalized.replacements, 0); + assert_eq!(normalized.media_endpoint, None); + } + + #[test] + fn accepts_ip_encoded_and_dns_hostnames_as_server_endpoints() { + assert_eq!( + resolve_server_endpoint("127-0-0-1.session.synthetic.invalid") + .expect("encoded hostname"), + "127.0.0.1".parse::().expect("IP address") + ); + assert!( + resolve_server_endpoint("localhost") + .expect("DNS hostname") + .is_loopback() + ); + } + + #[test] + fn rejects_invalid_media_endpoint_port() { + let session = synthetic_session(json!({ + "ip": "198.51.100.42", + "port": 70_000, + "usage": 2, + })); + + let error = normalize_offer_endpoints("v=0\n", &session).expect_err("invalid endpoint"); + + assert_eq!(error.code(), "invalid-media-endpoint"); + assert!(error.to_string().contains("1..=65535")); + } + + #[test] + fn summarizes_configured_ice_schemes_without_credentials() { + let servers: Vec = serde_json::from_value(json!([ + { + "urls": ["stun:stun.synthetic.invalid:3478"], + }, + { + "urls": ["turns:turn.synthetic.invalid:5349"], + "username": "synthetic-user", + "credential": "synthetic-secret" + } + ])) + .expect("ICE servers"); + + let schemes = configured_ice_schemes(&servers).expect("configured schemes"); + + assert_eq!(schemes, "stun, turns"); + assert!(!schemes.contains("synthetic-secret")); + } + + #[test] + fn negotiates_a_synthetic_offer_with_a_hostname_server_endpoint() { + install_crypto(); + let mut offerer = RtcConfig::new().build(Instant::now()); + offerer.add_local_candidate( + Candidate::host("127.0.0.1:49152".parse().expect("candidate address"), "udp") + .expect("local candidate"), + ); + let mut change = offerer.sdp_api(); + change.add_media(MediaKind::Video, Direction::SendOnly, None, None, None); + let (offer, _pending) = change.apply().expect("offer"); + let offer_sdp = offer.to_sdp_string(); + let session = synthetic_session(serde_json::Value::Null); + let (events, _receiver) = mpsc::channel(); + let (media_consumer, _media_receiver) = mpsc::sync_channel(4); + + let negotiated = negotiate(&offer_sdp, &session, 300, events, media_consumer) + .expect("negotiated answer"); + + assert!(negotiated.answer_sdp.contains("m=video")); + assert!(!negotiated.answer_sdp.contains("m=video 0")); + negotiated.session.stop(); + } + + #[test] + fn delivers_encoded_payload_to_typed_consumer_without_copying_arc() { + let (consumer, receiver) = mpsc::sync_channel(1); + let payload: Arc<[u8]> = Arc::from([1_u8, 2, 3, 4]); + let frame = EncodedMediaFrame { + mid: "video-0".to_owned(), + codec: "H264".to_owned(), + payload: payload.clone(), + rtp_timestamp: 180_000, + clock_rate_hz: 90_000, + received_at_us: 2_500, + keyframe: true, + contiguous: true, + }; + + deliver_media_frame(&consumer, frame).expect("frame delivery"); + + let delivered = receiver.recv().expect("delivered frame"); + assert!(Arc::ptr_eq(&delivered.payload, &payload)); + assert_eq!(delivered.rtp_timestamp, 180_000); + assert_eq!(delivered.clock_rate_hz, 90_000); + assert_eq!(delivered.received_at_us, 2_500); + } + + #[test] + fn reports_media_consumer_backpressure_instead_of_growing_an_unbounded_queue() { + let (consumer, _receiver) = mpsc::sync_channel(1); + let frame = || EncodedMediaFrame { + mid: "video-0".to_owned(), + codec: "H264".to_owned(), + payload: Arc::from([1_u8, 2, 3, 4]), + rtp_timestamp: 180_000, + clock_rate_hz: 90_000, + received_at_us: 2_500, + keyframe: true, + contiguous: true, + }; + deliver_media_frame(&consumer, frame()).expect("first frame delivery"); + + let error = deliver_media_frame(&consumer, frame()).expect_err("bounded queue is full"); + + assert_eq!(error.code(), "media-consumer-backpressured"); + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer-transport/src/nvst.rs b/native/opennow-streamer/crates/opennow-streamer-transport/src/nvst.rs new file mode 100644 index 000000000..72665d028 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer-transport/src/nvst.rs @@ -0,0 +1,4388 @@ +//! Independently authored NVST video receive transport. +//! +//! This module only implements the receive side of the classic NVST video handoff: +//! authenticated SRTP video datagrams from the negotiated peer become bounded H.264 +//! Annex-B access units. It deliberately does not implement NVST audio, control, input, +//! FEC repair, or NACK transmission because the current handoff does not contain enough +//! wire information to implement those features safely. + +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::fmt; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +use aes::cipher::{BlockEncrypt, KeyIvInit, StreamCipher}; +use aes::{Aes128, Aes256}; +use crc32fast::hash as crc32; +use ctr::{Ctr128BE, Ctr32BE}; +use ghash::{GHash, universal_hash::UniversalHash}; +use hmac::{Hmac, Mac}; +use serde_json::Value; +use sha1::Sha1; +use socket2::{Domain, Protocol, Socket, Type}; +use subtle::ConstantTimeEq; +use thiserror::Error; + +use str0m::channel::{ChannelConfig, ChannelId, Reliability}; +use str0m::config::Fingerprint; +use str0m::media::{MediaKind, Mid}; +use str0m::net::{Protocol as RtcProtocol, Receive}; +use str0m::rtp::Ssrc; +use str0m::{Candidate, Event, IceCreds, Input, Output, Rtc, RtcConfig}; + +use super::{ + EncodedMediaFrame, MediaConsumer, TransportError, deliver_media_frame, install_crypto, +}; + +const RTP_FIXED_HEADER_LEN: usize = 12; +const SRTP_AES_CM_HMAC_SHA1_80_TAG_LEN: usize = 10; +/// RFC 7714 `AEAD_AES_*_GCM` profiles carry a 16-byte authentication tag. +const SRTP_AEAD_AES_GCM_TAG_LEN: usize = 16; +/// NVIDIA's `SecureRtp` (libBifrost2) maps `sec_serv_conf_and_auth` + 256-bit keys +/// to `srtp_crypto_policy_set_aes_gcm_256_8_auth` — an 8-byte tag, not RFC 7714's 16. +const SRTP_AEAD_AES_GCM_8_TAG_LEN: usize = 8; + +/// RFC 3711 / libsrtp SRTP labels. Hook captures of 0x03/0x05 were SRTCP (same +/// master key, separate session keys). Mjolnir video is RTP, so use 0x00/0x02. +const GFN_SRTP_KEY_LABEL: u8 = 0x00; +const GFN_SRTP_SALT_LABEL: u8 = 0x02; +/// RFC 3711 SRTCP KDF labels (same master key, separate session keys). +const GFN_SRTCP_KEY_LABEL: u8 = 0x03; +const GFN_SRTCP_SALT_LABEL: u8 = 0x05; +const SRTCP_ENCRYPTED_FLAG: u32 = 0x8000_0000; +const SRTCP_RR_INTERVAL: Duration = Duration::from_secs(1); +/// How many even SCTP stream ids to try for the `rtcp1` feedback channel. The +/// server resets the DCEP open on the wrong id, so we probe the low even ids. +const RTCP_STREAM_CANDIDATES: usize = 8; +const NV_VIDEO_PACKET_LEN: usize = 16; +const DEFAULT_REORDER_WINDOW: usize = 32; +const MAX_REORDER_WINDOW: usize = 128; +const DEFAULT_MAX_ACCESS_UNIT_BYTES: usize = 2 * 1024 * 1024; +const MAX_ACCESS_UNIT_BYTES: usize = 16 * 1024 * 1024; +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5); +const MIN_TIMEOUT: Duration = Duration::from_millis(250); +const MAX_TIMEOUT: Duration = Duration::from_secs(90); +const MAX_PING_BYTES: usize = 512; +const PING_INTERVAL_BEFORE_CONNECTION: Duration = Duration::from_millis(20); +const PING_INTERVAL_AFTER_CONNECTION: Duration = Duration::from_millis(100); +const UDP_RECEIVE_POLL_INTERVAL: Duration = Duration::from_millis(10); +const STUN_HEADER_LEN: usize = 20; +const STUN_MAGIC_COOKIE: u32 = 0x2112_a442; +const STUN_BINDING_REQUEST: u16 = 0x0001; +const STUN_BINDING_SUCCESS_RESPONSE: u16 = 0x0101; +const STUN_ATTR_USERNAME: u16 = 0x0006; +const STUN_ATTR_MESSAGE_INTEGRITY: u16 = 0x0008; +const STUN_ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020; +const STUN_ATTR_FINGERPRINT: u16 = 0x8028; +const STUN_FINGERPRINT_XOR: u32 = 0x5354_554e; +const MAX_ICE_CREDENTIAL_BYTES: usize = 256; + +/// The independently documented `NV_VIDEO_PACKET` flag values used by an earlier OpenNOW +/// implementation. This module does not borrow code or binaries from NVIDIA. +const FLAG_CONTAINS_PIC_DATA: u8 = 0x01; +const FLAG_EOF: u8 = 0x02; +const FLAG_SOF: u8 = 0x04; +const STREAM_PACKET_INDEX_MASK: u32 = 0x00ff_ffff; +/// Mjolnir video RTP packets carry the per-packet video metadata in a fixed +/// 16-byte RTP extension block with profile `0x4753` ("GS"), not in the payload. +const GS_VIDEO_EXTENSION_PROFILE: u16 = 0x4753; +const MAX_GS_FRAME_HEADER_BYTES: usize = 64; + +type Aes256Ctr = Ctr128BE; +type Aes128Ctr = Ctr128BE; +type HmacSha1 = Hmac; + +/// The SRTP profile must come from negotiated metadata. The legacy `nvstVideo` handoff has no +/// profile field; Bifrost's Mjolnir video path hardcodes `aes_gcm_256_8_auth`, so the legacy +/// default selects the 8-byte-tag GCM variant. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NvstSrtpProfile { + AeadAes128Gcm, + AeadAes256Gcm, + AeadAes128Gcm8, + AeadAes256Gcm8, + AesCm128HmacSha1_32, + AesCm128HmacSha1_80, + AesCm256HmacSha1_32, + AesCm256HmacSha1_80, +} + +impl NvstSrtpProfile { + fn parse(value: &str) -> Result { + match value.trim().to_ascii_uppercase().as_str() { + "AEAD_AES_128_GCM" | "SRTP_AEAD_AES_128_GCM" => Ok(Self::AeadAes128Gcm), + "AEAD_AES_256_GCM" | "SRTP_AEAD_AES_256_GCM" => Ok(Self::AeadAes256Gcm), + "AEAD_AES_128_GCM_8" | "SRTP_AEAD_AES_128_GCM_8" => Ok(Self::AeadAes128Gcm8), + "AEAD_AES_256_GCM_8" | "SRTP_AEAD_AES_256_GCM_8" => Ok(Self::AeadAes256Gcm8), + "AES_CM_128_HMAC_SHA1_32" | "SRTP_AES_CM_128_HMAC_SHA1_32" => { + Ok(Self::AesCm128HmacSha1_32) + } + "AES_CM_128_HMAC_SHA1_80" | "SRTP_AES_CM_128_HMAC_SHA1_80" => { + Ok(Self::AesCm128HmacSha1_80) + } + "AES_CM_256_HMAC_SHA1_32" | "SRTP_AES_CM_256_HMAC_SHA1_32" => { + Ok(Self::AesCm256HmacSha1_32) + } + "AES_CM_256_HMAC_SHA1_80" | "SRTP_AES_CM_256_HMAC_SHA1_80" => { + Ok(Self::AesCm256HmacSha1_80) + } + other => Err(NvstConfigError::UnsupportedSrtpProfile(other.to_owned())), + } + } +} + +/// The only media codec this receive path currently exposes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NvstVideoCodec { + H264, +} + +impl NvstVideoCodec { + fn parse(value: &str) -> Result { + match value.trim().to_ascii_uppercase().as_str() { + "H264" | "AVC" => Ok(Self::H264), + other => Err(NvstConfigError::UnsupportedCodec(other.to_owned())), + } + } +} + +/// Why an NVST handoff cannot be selected. Callers should retain their WebRTC fallback. +#[derive(Debug, Error)] +pub enum NvstConfigError { + #[error("nvstVideo must be an object")] + HandoffNotObject, + #[error("nvstVideo is missing {0}")] + MissingField(&'static str), + #[error("nvstVideo.{field} must be a {expected}")] + InvalidFieldType { + field: &'static str, + expected: &'static str, + }, + #[error("nvstVideo.{field} is out of range")] + OutOfRange { field: &'static str }, + #[error("nvstVideo.videoPeerIp is not a routable unicast IP address: {0}")] + InvalidPeerIp(String), + #[error("nvstVideo.srtpAesKeyHex is invalid for the selected SRTP profile")] + InvalidAesKey, + #[error("nvstVideo.srtpSaltHex is invalid for the selected SRTP profile")] + InvalidSrtpSalt, + #[error("nvstVideo.srtpProfile {0} is not implemented")] + UnsupportedSrtpProfile(String), + #[error("NVST video codec {0} is not implemented; only H264 Annex-B is available")] + UnsupportedCodec(String), + #[error("nvstTransport.tracks is not implemented yet; retain the legacy nvstVideo handoff")] + RichHandoffUnsupported, +} + +/// Explicitly records transport features that cannot be sent correctly from current wire data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NvstUnsupportedFeature { + Audio, + Input, + Nack, + FecRepair, +} + +impl fmt::Display for NvstUnsupportedFeature { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Self::Audio => "audio", + Self::Input => "input", + Self::Nack => "NACK transmission", + Self::FecRepair => "FEC repair", + }; + formatter.write_str(name) + } +} + +/// Feedback plane shared between the Mjolnir video receiver (which learns the +/// stream SSRC/sequence and detects unrecoverable loss) and the ICE/DTLS bundle +/// (which owns the `rtcp1` SCTP data channel used for RTCP feedback). +/// +/// The official client sends RTCP Receiver Reports / PLI over an SCTP data +/// channel on the bundle ("RTCP over SCTP is a must for One SDK video to +/// function"). Without it the server stops video after a short provisional +/// window. This state lets the bundle build accurate reports for the stream the +/// Mjolnir receiver is actually seeing. +#[derive(Debug, Default)] +pub struct NvstFeedbackState { + /// Bound video stream SSRC (0 until the first packet is authenticated). + video_ssrc: AtomicU32, + /// Highest extended sequence number received on the video stream. + highest_sequence: AtomicU32, + /// Set when the receiver hits unrecoverable loss and needs a fresh keyframe. + keyframe_needed: AtomicBool, +} + +impl NvstFeedbackState { + fn publish_stream(&self, ssrc: u32, highest_sequence: u32) { + self.video_ssrc.store(ssrc, Ordering::Release); + self.highest_sequence + .fetch_max(highest_sequence, Ordering::AcqRel); + } + + fn request_keyframe(&self) { + self.keyframe_needed.store(true, Ordering::Release); + } + + /// SSRC + highest sequence for the next Receiver Report, if a stream is bound. + fn stream_snapshot(&self) -> Option<(u32, u32)> { + let ssrc = self.video_ssrc.load(Ordering::Acquire); + (ssrc != 0).then(|| (ssrc, self.highest_sequence.load(Ordering::Acquire))) + } + + /// Atomically takes the pending keyframe request, returning true if one was set. + fn take_keyframe_request(&self) -> bool { + self.keyframe_needed.swap(false, Ordering::AcqRel) + } +} + +/// Shared handle to the NVST feedback plane (cheap to clone, shared across threads). +pub type SharedNvstFeedback = Arc; + +/// Legacy `nvstVideo` configuration normalized into the bounded receive transport. +/// +/// Secret material is never exposed through `Debug`. The legacy `nvstVideo` handoff defaults to +/// `AEAD_AES_256_GCM`; every SRTP profile requires an explicit `srtpSaltHex`. +#[derive(Clone)] +pub struct NvstVideoConfig { + client_udp_port: u16, + video_peer: SocketAddr, + srtp: NvstSrtpMaterial, + ping_payload: Vec, + ping_version: Option, + stun_credentials: Option, + remote_dtls_fingerprint: Option, + /// Dedicated NATT-only video (Mjolnir) socket port in the official two-socket + /// cloud model. When set, video RTP/SRTP arrives on this socket while the + /// ICE/DTLS bundle socket only carries control/audio keepalive traffic. + mjolnir_udp_port: Option, + codec: NvstVideoCodec, + expected_payload_type: Option, + expected_ssrc: Option, + reorder_window_packets: usize, + max_access_unit_bytes: usize, + timeout: Duration, + /// Feedback plane shared with the ICE/DTLS bundle (cloned configs share it). + feedback: SharedNvstFeedback, +} + +impl fmt::Debug for NvstVideoConfig { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("NvstVideoConfig") + .field("client_udp_port", &self.client_udp_port) + .field("video_peer", &self.video_peer) + .field("srtp", &self.srtp) + .field("ping_payload_len", &self.ping_payload.len()) + .field("ping_version", &self.ping_version) + .field("stun_credentials", &self.stun_credentials) + .field( + "remote_dtls_fingerprint_bytes", + &self.remote_dtls_fingerprint.as_ref().map(String::len), + ) + .field("mjolnir_udp_port", &self.mjolnir_udp_port) + .field("codec", &self.codec) + .field("expected_payload_type", &self.expected_payload_type) + .field("expected_ssrc", &self.expected_ssrc) + .field("reorder_window_packets", &self.reorder_window_packets) + .field("max_access_unit_bytes", &self.max_access_unit_bytes) + .field("timeout", &self.timeout) + .finish() + } +} + +#[derive(Clone)] +struct NvstStunCredentials { + local_username_fragment: String, + local_password: String, + remote_username_fragment: String, + remote_password: String, +} + +impl fmt::Debug for NvstStunCredentials { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("NvstStunCredentials") + .field("local_username_fragment", &"[redacted]") + .field("local_password", &"[redacted]") + .field("remote_username_fragment", &"[redacted]") + .field("remote_password", &"[redacted]") + .finish() + } +} + +#[derive(Clone)] +enum NvstSrtpMaterial { + AeadAes128Gcm { + master_key: [u8; 16], + master_salt: [u8; 12], + authentication_tag_len: usize, + }, + AeadAes256Gcm { + master_key: [u8; 32], + master_salt: [u8; 12], + authentication_tag_len: usize, + }, + AesCm128HmacSha1 { + master_key: [u8; 16], + master_salt: [u8; 14], + authentication_tag_len: usize, + }, + AesCm256HmacSha1 { + master_key: [u8; 32], + master_salt: [u8; 14], + authentication_tag_len: usize, + }, +} + +impl fmt::Debug for NvstSrtpMaterial { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("NvstSrtpMaterial") + .field("profile", &self.profile()) + .field("master_key", &"[redacted]") + .field("master_salt", &"[redacted]") + .finish() + } +} + +impl NvstSrtpMaterial { + fn profile(&self) -> NvstSrtpProfile { + match self { + Self::AeadAes128Gcm { + authentication_tag_len: SRTP_AEAD_AES_GCM_8_TAG_LEN, + .. + } => NvstSrtpProfile::AeadAes128Gcm8, + Self::AeadAes128Gcm { .. } => NvstSrtpProfile::AeadAes128Gcm, + Self::AeadAes256Gcm { + authentication_tag_len: SRTP_AEAD_AES_GCM_8_TAG_LEN, + .. + } => NvstSrtpProfile::AeadAes256Gcm8, + Self::AeadAes256Gcm { .. } => NvstSrtpProfile::AeadAes256Gcm, + Self::AesCm128HmacSha1 { + authentication_tag_len: SRTP_AES_CM_HMAC_SHA1_80_TAG_LEN, + .. + } => NvstSrtpProfile::AesCm128HmacSha1_80, + Self::AesCm128HmacSha1 { .. } => NvstSrtpProfile::AesCm128HmacSha1_32, + Self::AesCm256HmacSha1 { + authentication_tag_len: SRTP_AES_CM_HMAC_SHA1_80_TAG_LEN, + .. + } => NvstSrtpProfile::AesCm256HmacSha1_80, + Self::AesCm256HmacSha1 { .. } => NvstSrtpProfile::AesCm256HmacSha1_32, + } + } +} + +impl NvstVideoConfig { + /// Parses the stable legacy `nvstVideo` object. The parsing boundary is intentionally + /// isolated here so a future `nvstTransport.tracks[]` handoff can normalize into this + /// same configuration without changing the SRTP/RTP receiver. + pub fn from_legacy_handoff( + handoff: &Value, + settings_codec: Option<&str>, + ) -> Result { + let object = handoff + .as_object() + .ok_or(NvstConfigError::HandoffNotObject)?; + let client_udp_port = required_u16(object, "clientUdpPort")?; + if client_udp_port == 0 { + return Err(NvstConfigError::OutOfRange { + field: "clientUdpPort", + }); + } + + let peer_ip_text = required_string(object, "videoPeerIp")?; + let peer_ip: IpAddr = peer_ip_text + .parse() + .map_err(|_| NvstConfigError::InvalidPeerIp(peer_ip_text.to_owned()))?; + if !is_unicast_peer(peer_ip) { + return Err(NvstConfigError::InvalidPeerIp(peer_ip_text.to_owned())); + } + let video_peer_port = required_u16(object, "videoPeerPort")?; + if video_peer_port == 0 { + return Err(NvstConfigError::OutOfRange { + field: "videoPeerPort", + }); + } + + let master_key = required_string(object, "srtpAesKeyHex")?; + // Bifrost's SignalingHandler initializes Mjolnir video as + // sec_serv_conf_and_auth + 256-bit keys → AES-256-GCM with an 8-byte tag. + let srtp_profile = optional_string(object, "srtpProfile")? + .map(NvstSrtpProfile::parse) + .transpose()? + .unwrap_or(NvstSrtpProfile::AeadAes256Gcm8); + let srtp = match srtp_profile { + NvstSrtpProfile::AeadAes128Gcm | NvstSrtpProfile::AeadAes128Gcm8 => { + let master_salt = required_string(object, "srtpSaltHex").and_then(|salt| { + decode_fixed_hex::<12>(salt, NvstConfigError::InvalidSrtpSalt) + })?; + NvstSrtpMaterial::AeadAes128Gcm { + master_key: decode_fixed_hex::<16>(master_key, NvstConfigError::InvalidAesKey)?, + master_salt, + authentication_tag_len: match srtp_profile { + NvstSrtpProfile::AeadAes128Gcm8 => SRTP_AEAD_AES_GCM_8_TAG_LEN, + _ => SRTP_AEAD_AES_GCM_TAG_LEN, + }, + } + } + NvstSrtpProfile::AeadAes256Gcm | NvstSrtpProfile::AeadAes256Gcm8 => { + let master_salt = required_string(object, "srtpSaltHex").and_then(|salt| { + decode_fixed_hex::<12>(salt, NvstConfigError::InvalidSrtpSalt) + })?; + NvstSrtpMaterial::AeadAes256Gcm { + master_key: decode_fixed_hex::<32>(master_key, NvstConfigError::InvalidAesKey)?, + master_salt, + authentication_tag_len: match srtp_profile { + NvstSrtpProfile::AeadAes256Gcm8 => SRTP_AEAD_AES_GCM_8_TAG_LEN, + _ => SRTP_AEAD_AES_GCM_TAG_LEN, + }, + } + } + NvstSrtpProfile::AesCm128HmacSha1_32 | NvstSrtpProfile::AesCm128HmacSha1_80 => { + let master_salt = required_string(object, "srtpSaltHex").and_then(|salt| { + decode_salt_hex::<14>(salt, NvstConfigError::InvalidSrtpSalt) + })?; + NvstSrtpMaterial::AesCm128HmacSha1 { + master_key: decode_fixed_hex::<16>(master_key, NvstConfigError::InvalidAesKey)?, + master_salt, + authentication_tag_len: match srtp_profile { + NvstSrtpProfile::AesCm128HmacSha1_32 => 4, + NvstSrtpProfile::AesCm128HmacSha1_80 => SRTP_AES_CM_HMAC_SHA1_80_TAG_LEN, + _ => unreachable!("AES-CM profile selected above"), + }, + } + } + NvstSrtpProfile::AesCm256HmacSha1_32 | NvstSrtpProfile::AesCm256HmacSha1_80 => { + let master_salt = required_string(object, "srtpSaltHex").and_then(|salt| { + decode_salt_hex::<14>(salt, NvstConfigError::InvalidSrtpSalt) + })?; + NvstSrtpMaterial::AesCm256HmacSha1 { + master_key: decode_fixed_hex::<32>(master_key, NvstConfigError::InvalidAesKey)?, + master_salt, + authentication_tag_len: match srtp_profile { + NvstSrtpProfile::AesCm256HmacSha1_32 => 4, + NvstSrtpProfile::AesCm256HmacSha1_80 => SRTP_AES_CM_HMAC_SHA1_80_TAG_LEN, + _ => unreachable!("AES-CM profile selected above"), + }, + } + } + }; + + let codec_name = optional_string(object, "codec")? + .or(settings_codec) + .ok_or(NvstConfigError::MissingField("codec"))?; + let codec = NvstVideoCodec::parse(codec_name)?; + let ping_payload = optional_string(object, "pingPayload")? + .map_or_else(|| b"PING".to_vec(), |payload| payload.as_bytes().to_vec()); + if ping_payload.is_empty() || ping_payload.len() > MAX_PING_BYTES { + return Err(NvstConfigError::OutOfRange { + field: "pingPayload", + }); + } + let ping_version = optional_u8(object, "pingVersion")?; + let remote_dtls_fingerprint = + optional_string(object, "remoteDtlsFingerprint")?.map(str::to_owned); + let mjolnir_udp_port = optional_u16(object, "mjolnirUdpPort")?; + if mjolnir_udp_port == Some(0) { + return Err(NvstConfigError::OutOfRange { + field: "mjolnirUdpPort", + }); + } + let stun_credentials = if ping_version == Some(6) || remote_dtls_fingerprint.is_some() { + Some(NvstStunCredentials { + local_username_fragment: required_ice_credential( + object, + "localIceUsernameFragment", + )?, + local_password: required_ice_credential(object, "localIcePassword")?, + remote_username_fragment: required_ice_credential( + object, + "remoteIceUsernameFragment", + )?, + remote_password: required_ice_credential(object, "remoteIcePassword")?, + }) + } else { + None + }; + + let expected_payload_type = optional_u8(object, "rtpPayloadType")?; + let expected_ssrc = optional_u32(object, "rtpSsrc")?; + let reorder_window_packets = + optional_usize(object, "reorderWindowPackets")?.unwrap_or(DEFAULT_REORDER_WINDOW); + if !(1..=MAX_REORDER_WINDOW).contains(&reorder_window_packets) { + return Err(NvstConfigError::OutOfRange { + field: "reorderWindowPackets", + }); + } + let max_access_unit_bytes = + optional_usize(object, "maxAccessUnitBytes")?.unwrap_or(DEFAULT_MAX_ACCESS_UNIT_BYTES); + if !(1..=MAX_ACCESS_UNIT_BYTES).contains(&max_access_unit_bytes) { + return Err(NvstConfigError::OutOfRange { + field: "maxAccessUnitBytes", + }); + } + let timeout_ms = optional_u64(object, "timeoutMs")?; + let timeout = timeout_ms + .map(Duration::from_millis) + .unwrap_or(DEFAULT_TIMEOUT); + if !(MIN_TIMEOUT..=MAX_TIMEOUT).contains(&timeout) { + return Err(NvstConfigError::OutOfRange { field: "timeoutMs" }); + } + + Ok(Self { + client_udp_port, + video_peer: SocketAddr::new(peer_ip, video_peer_port), + srtp, + ping_payload, + ping_version, + stun_credentials, + remote_dtls_fingerprint, + mjolnir_udp_port, + codec, + expected_payload_type, + expected_ssrc, + reorder_window_packets, + max_access_unit_bytes, + timeout, + feedback: Arc::new(NvstFeedbackState::default()), + }) + } + + pub fn client_udp_port(&self) -> u16 { + self.client_udp_port + } + + /// Shared feedback plane (RTCP-over-SCTP state) for this session. + pub fn feedback(&self) -> SharedNvstFeedback { + self.feedback.clone() + } + + pub fn video_peer(&self) -> SocketAddr { + self.video_peer + } + + pub fn codec(&self) -> NvstVideoCodec { + self.codec + } + + pub fn srtp_profile(&self) -> NvstSrtpProfile { + self.srtp.profile() + } + + pub fn timeout(&self) -> Duration { + self.timeout + } + + pub fn remote_dtls_fingerprint(&self) -> Option<&str> { + self.remote_dtls_fingerprint.as_deref() + } + + pub fn mjolnir_udp_port(&self) -> Option { + self.mjolnir_udp_port + } +} + +fn required_ice_credential( + object: &serde_json::Map, + field: &'static str, +) -> Result { + let value = required_string(object, field)?; + if value.is_empty() || value.len() > MAX_ICE_CREDENTIAL_BYTES { + return Err(NvstConfigError::OutOfRange { field }); + } + Ok(value.to_owned()) +} + +/// Parses `context.nvstVideo` without tying the rest of the transport to JSON field names. +/// `None` means the legacy handoff was not supplied, which is a normal WebRTC fallback case. +pub fn parse_nvst_video_handoff( + context: &Value, +) -> Result, NvstConfigError> { + let Some(handoff) = context.get("nvstVideo") else { + if context.get("nvstTransport").is_some() { + return Err(NvstConfigError::RichHandoffUnsupported); + } + return Ok(None); + }; + let settings_codec = context.pointer("/settings/codec").and_then(Value::as_str); + NvstVideoConfig::from_legacy_handoff(handoff, settings_codec).map(Some) +} + +/// The transport selector always prefers a valid NVST video handoff, while making every +/// incomplete or unsupported handoff a typed WebRTC fallback instead of an optimistic start. +#[derive(Debug)] +pub enum PreferredVideoTransport { + Nvst(NvstVideoConfig), + WebRtcFallback(NvstFallbackReason), +} + +#[derive(Debug)] +pub enum NvstFallbackReason { + NoNvstHandoff, + InvalidNvstHandoff(NvstConfigError), +} + +pub fn select_preferred_video_transport(context: &Value) -> PreferredVideoTransport { + match parse_nvst_video_handoff(context) { + Ok(Some(config)) => PreferredVideoTransport::Nvst(config), + Ok(None) => PreferredVideoTransport::WebRtcFallback(NvstFallbackReason::NoNvstHandoff), + Err(error) => { + PreferredVideoTransport::WebRtcFallback(NvstFallbackReason::InvalidNvstHandoff(error)) + } + } +} + +fn required_string<'a>( + object: &'a serde_json::Map, + field: &'static str, +) -> Result<&'a str, NvstConfigError> { + let value = object + .get(field) + .ok_or(NvstConfigError::MissingField(field))?; + value.as_str().ok_or(NvstConfigError::InvalidFieldType { + field, + expected: "string", + }) +} + +fn optional_string<'a>( + object: &'a serde_json::Map, + field: &'static str, +) -> Result, NvstConfigError> { + match object.get(field) { + None | Some(Value::Null) => Ok(None), + Some(value) => value + .as_str() + .map(Some) + .ok_or(NvstConfigError::InvalidFieldType { + field, + expected: "string", + }), + } +} + +fn required_u16( + object: &serde_json::Map, + field: &'static str, +) -> Result { + let value = required_u64(object, field)?; + u16::try_from(value).map_err(|_| NvstConfigError::OutOfRange { field }) +} + +fn required_u64( + object: &serde_json::Map, + field: &'static str, +) -> Result { + let value = object + .get(field) + .ok_or(NvstConfigError::MissingField(field))?; + value.as_u64().ok_or(NvstConfigError::InvalidFieldType { + field, + expected: "unsigned integer", + }) +} + +fn optional_u64( + object: &serde_json::Map, + field: &'static str, +) -> Result, NvstConfigError> { + match object.get(field) { + None | Some(Value::Null) => Ok(None), + Some(value) => value + .as_u64() + .map(Some) + .ok_or(NvstConfigError::InvalidFieldType { + field, + expected: "unsigned integer", + }), + } +} + +fn optional_u16( + object: &serde_json::Map, + field: &'static str, +) -> Result, NvstConfigError> { + optional_u64(object, field)?.map_or(Ok(None), |value| { + u16::try_from(value) + .map(Some) + .map_err(|_| NvstConfigError::OutOfRange { field }) + }) +} + +fn optional_u8( + object: &serde_json::Map, + field: &'static str, +) -> Result, NvstConfigError> { + optional_u64(object, field)?.map_or(Ok(None), |value| { + u8::try_from(value) + .map(Some) + .map_err(|_| NvstConfigError::OutOfRange { field }) + }) +} + +fn optional_u32( + object: &serde_json::Map, + field: &'static str, +) -> Result, NvstConfigError> { + optional_u64(object, field)?.map_or(Ok(None), |value| { + u32::try_from(value) + .map(Some) + .map_err(|_| NvstConfigError::OutOfRange { field }) + }) +} + +fn optional_usize( + object: &serde_json::Map, + field: &'static str, +) -> Result, NvstConfigError> { + optional_u64(object, field)?.map_or(Ok(None), |value| { + usize::try_from(value) + .map(Some) + .map_err(|_| NvstConfigError::OutOfRange { field }) + }) +} + +fn decode_fixed_hex( + value: &str, + error: NvstConfigError, +) -> Result<[u8; N], NvstConfigError> { + if value.len() != N * 2 { + return Err(error); + } + let mut decoded = [0_u8; N]; + for (index, output) in decoded.iter_mut().enumerate() { + let offset = index * 2; + let pair = match value.get(offset..offset + 2) { + Some(pair) => pair, + None => return Err(error), + }; + *output = match u8::from_str_radix(pair, 16) { + Ok(byte) => byte, + Err(_) => return Err(error), + }; + } + Ok(decoded) +} + +/// GFN packs the keyId into the low 4 bytes of a 12-byte salt; libsrtp right-pads the +/// AES-CM master salt to 14 bytes. Accept the 12-byte probe form and right-pad with zeros. +fn decode_salt_hex( + value: &str, + error: NvstConfigError, +) -> Result<[u8; N], NvstConfigError> { + if value.len() % 2 != 0 || value.len() > N * 2 { + return Err(error); + } + let mut decoded = [0_u8; N]; + for (index, output) in decoded.iter_mut().take(value.len() / 2).enumerate() { + let offset = index * 2; + let pair = match value.get(offset..offset + 2) { + Some(pair) => pair, + None => return Err(error), + }; + *output = match u8::from_str_radix(pair, 16) { + Ok(byte) => byte, + Err(_) => return Err(error), + }; + } + Ok(decoded) +} + +fn is_unicast_peer(ip: IpAddr) -> bool { + match ip { + IpAddr::V4(ip) => !ip.is_unspecified() && !ip.is_multicast() && ip != Ipv4Addr::BROADCAST, + IpAddr::V6(ip) => !ip.is_unspecified() && !ip.is_multicast() && ip != Ipv6Addr::UNSPECIFIED, + } +} + +/// A received H.264 byte-stream access unit ready for a decoder queue. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncodedH264Frame { + pub timestamp: u32, + pub frame_index: u32, + pub first_stream_packet_index: u32, + pub keyframe: bool, + pub bytes: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NvstReceiverState { + Running, + Paused, + RecoveryRequired, + Stopped, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NvstDropReason { + UnexpectedSource { + expected: SocketAddr, + actual: SocketAddr, + }, + Paused, + Stopped, + RecoveryRequired, + MalformedRtp(RtpParseError), + AuthenticationFailed, + ReplayRejected, + UnexpectedPayloadType { + expected: u8, + actual: u8, + }, + UnexpectedSsrc { + expected: u32, + actual: u32, + }, + StaleRtpPacket { + index: u64, + }, + DuplicateRtpPacket { + index: u64, + }, + Unsupported(NvstUnsupportedFeature), + AwaitingStartOfFrame, + FrameDiscontinuity, + MissingAnnexBStartCode, + AccessUnitTooLarge { + limit: usize, + }, + MediaConsumerBackpressured, + MediaConsumerClosed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RtpParseError { + TooShort, + InvalidVersion, + InvalidCsrcLength, + InvalidExtensionLength, + MissingAuthenticationTag, + InvalidPadding, + MissingNvVideoHeader, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NvstRecovery { + PacketGap { + first_missing_index: u64, + last_missing_index: u64, + nack: NvstUnsupportedFeature, + }, + Timeout { + idle_for: Duration, + }, +} + +/// All receive decisions are explicit so callers can collect operational metrics without +/// treating malformed network traffic as a fatal thread error. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NvstReceiveEvent { + Frame(EncodedH264Frame), + Dropped(NvstDropReason), + RecoveryNeeded(NvstRecovery), + Lifecycle(NvstReceiverState), +} + +#[derive(Debug, Clone, Copy)] +struct RtpHeader { + payload_type: u8, + sequence_number: u16, + timestamp: u32, + ssrc: u32, + payload_offset: usize, + has_padding: bool, + gs_video_header: Option<[u8; NV_VIDEO_PACKET_LEN]>, +} + +impl RtpHeader { + fn parse(packet: &[u8]) -> Result { + if packet.len() < RTP_FIXED_HEADER_LEN { + return Err(RtpParseError::TooShort); + } + let first = packet[0]; + if first >> 6 != 2 { + return Err(RtpParseError::InvalidVersion); + } + let csrc_count = usize::from(first & 0x0f); + let mut payload_offset = RTP_FIXED_HEADER_LEN + .checked_add(csrc_count.saturating_mul(4)) + .ok_or(RtpParseError::InvalidCsrcLength)?; + if packet.len() < payload_offset { + return Err(RtpParseError::InvalidCsrcLength); + } + let mut gs_video_header = None; + if first & 0x10 != 0 { + if packet.len() < payload_offset + 4 { + return Err(RtpParseError::InvalidExtensionLength); + } + let profile = u16::from_be_bytes([packet[payload_offset], packet[payload_offset + 1]]); + let words = + u16::from_be_bytes([packet[payload_offset + 2], packet[payload_offset + 3]]); + let extension_len = usize::from(words) + .checked_mul(4) + .and_then(|length| length.checked_add(4)) + .ok_or(RtpParseError::InvalidExtensionLength)?; + if profile == GS_VIDEO_EXTENSION_PROFILE && extension_len >= NV_VIDEO_PACKET_LEN + 4 { + let body = &packet[payload_offset + 4..payload_offset + extension_len]; + gs_video_header = Some( + body[..NV_VIDEO_PACKET_LEN] + .try_into() + .expect("length checked"), + ); + } + payload_offset = payload_offset + .checked_add(extension_len) + .ok_or(RtpParseError::InvalidExtensionLength)?; + if packet.len() < payload_offset { + return Err(RtpParseError::InvalidExtensionLength); + } + } + Ok(Self { + payload_type: packet[1] & 0x7f, + sequence_number: u16::from_be_bytes([packet[2], packet[3]]), + timestamp: u32::from_be_bytes([packet[4], packet[5], packet[6], packet[7]]), + ssrc: u32::from_be_bytes([packet[8], packet[9], packet[10], packet[11]]), + payload_offset, + has_padding: first & 0x20 != 0, + gs_video_header, + }) + } + + fn payload<'a>(&self, packet: &'a [u8]) -> Result<&'a [u8], RtpParseError> { + if packet.len() < self.payload_offset { + return Err(RtpParseError::TooShort); + } + let mut end = packet.len(); + if self.has_padding { + let padding = usize::from(*packet.last().ok_or(RtpParseError::InvalidPadding)?); + if padding == 0 || padding > end.saturating_sub(self.payload_offset) { + return Err(RtpParseError::InvalidPadding); + } + end -= padding; + } + Ok(&packet[self.payload_offset..end]) + } +} + +#[derive(Debug)] +struct RtpPacket { + index: u64, + header: RtpHeader, + plaintext: Vec, +} + +#[derive(Clone)] +struct SrtpReceiver { + cipher: SrtpCipher, + replay: ReplayWindow, +} + +#[derive(Clone)] +enum SrtpCipher { + AeadAes128Gcm { + encryption_key: [u8; 16], + session_salt: [u8; 12], + authentication_tag_len: usize, + }, + AeadAes256Gcm { + encryption_key: [u8; 32], + session_salt: [u8; 12], + authentication_tag_len: usize, + }, + AesCm128HmacSha1 { + encryption_key: [u8; 16], + authentication_key: [u8; 20], + session_salt: [u8; 14], + authentication_tag_len: usize, + }, + AesCm256HmacSha1 { + encryption_key: [u8; 32], + authentication_key: [u8; 20], + session_salt: [u8; 14], + authentication_tag_len: usize, + }, +} + +impl SrtpReceiver { + fn from_material(material: &NvstSrtpMaterial) -> Self { + let cipher = match material { + NvstSrtpMaterial::AeadAes128Gcm { + master_key, + master_salt, + authentication_tag_len, + } => SrtpCipher::AeadAes128Gcm { + encryption_key: derive_aes128_cm_key::<16>(master_key, master_salt, GFN_SRTP_KEY_LABEL), + session_salt: derive_aes128_cm_key::<12>(master_key, master_salt, GFN_SRTP_SALT_LABEL), + authentication_tag_len: *authentication_tag_len, + }, + NvstSrtpMaterial::AeadAes256Gcm { + master_key, + master_salt, + authentication_tag_len, + } => SrtpCipher::AeadAes256Gcm { + encryption_key: derive_aes_cm_key::<32>(master_key, master_salt, GFN_SRTP_KEY_LABEL), + session_salt: derive_aes_cm_key::<12>(master_key, master_salt, GFN_SRTP_SALT_LABEL), + authentication_tag_len: *authentication_tag_len, + }, + NvstSrtpMaterial::AesCm128HmacSha1 { + master_key, + master_salt, + authentication_tag_len, + } => SrtpCipher::AesCm128HmacSha1 { + encryption_key: derive_aes128_cm_key::<16>(master_key, master_salt, 0x00), + authentication_key: derive_aes128_cm_key::<20>(master_key, master_salt, 0x01), + session_salt: derive_aes128_cm_key::<14>(master_key, master_salt, 0x02), + authentication_tag_len: *authentication_tag_len, + }, + NvstSrtpMaterial::AesCm256HmacSha1 { + master_key, + master_salt, + authentication_tag_len, + } => SrtpCipher::AesCm256HmacSha1 { + encryption_key: derive_aes_cm_key::<32>(master_key, master_salt, 0x00), + authentication_key: derive_aes_cm_key::<20>(master_key, master_salt, 0x01), + session_salt: derive_aes_cm_key::<14>(master_key, master_salt, 0x02), + authentication_tag_len: *authentication_tag_len, + }, + }; + Self { + cipher, + replay: ReplayWindow::default(), + } + } + + fn unprotect(&mut self, datagram: &[u8]) -> Result { + let header = RtpHeader::parse(datagram).map_err(NvstDropReason::MalformedRtp)?; + let packet_index = self.replay.guess_packet_index(header.sequence_number)?; + let roc = u32::try_from(packet_index >> 16).map_err(|_| NvstDropReason::ReplayRejected)?; + let plaintext = match &self.cipher { + SrtpCipher::AeadAes128Gcm { + encryption_key, + session_salt, + authentication_tag_len, + } => unprotect_aes_gcm( + datagram, + header, + roc, + encryption_key, + session_salt, + *authentication_tag_len, + )?, + SrtpCipher::AeadAes256Gcm { + encryption_key, + session_salt, + authentication_tag_len, + } => unprotect_aes_gcm( + datagram, + header, + roc, + encryption_key, + session_salt, + *authentication_tag_len, + )?, + SrtpCipher::AesCm128HmacSha1 { + encryption_key, + authentication_key, + session_salt, + authentication_tag_len, + } => unprotect_aes_cm_hmac_sha1( + datagram, + header, + packet_index, + roc, + encryption_key, + authentication_key, + session_salt, + *authentication_tag_len, + )?, + SrtpCipher::AesCm256HmacSha1 { + encryption_key, + authentication_key, + session_salt, + authentication_tag_len, + } => unprotect_aes_cm_hmac_sha1( + datagram, + header, + packet_index, + roc, + encryption_key, + authentication_key, + session_salt, + *authentication_tag_len, + )?, + }; + self.replay.check(packet_index)?; + header + .payload(&plaintext) + .map_err(NvstDropReason::MalformedRtp)?; + self.replay.commit(packet_index); + Ok(RtpPacket { + index: packet_index, + header, + plaintext, + }) + } +} + +/// AES-GCM with a truncated tag. `aes-gcm` 0.10 only seals 12–16 byte tags; +/// NVIDIA's Mjolnir path uses the 8-byte `aes_gcm_*_8_auth` libsrtp policy. +fn unprotect_aes_gcm( + datagram: &[u8], + header: RtpHeader, + roc: u32, + encryption_key: &[u8], + session_salt: &[u8; 12], + tag_len: usize, +) -> Result, NvstDropReason> { + let ciphertext_end = + datagram + .len() + .checked_sub(tag_len) + .ok_or(NvstDropReason::MalformedRtp( + RtpParseError::MissingAuthenticationTag, + ))?; + if ciphertext_end < header.payload_offset { + return Err(NvstDropReason::MalformedRtp( + RtpParseError::MissingAuthenticationTag, + )); + } + let iv = srtp_gcm_iv(*session_salt, header.ssrc, roc, header.sequence_number); + let aad = &datagram[..header.payload_offset]; + let ciphertext = &datagram[header.payload_offset..ciphertext_end]; + let received_tag = &datagram[ciphertext_end..]; + let (expected_tag, mut ctr) = aes_gcm_tag_and_ctr(encryption_key, &iv, aad, ciphertext); + if expected_tag[..tag_len].ct_eq(received_tag).unwrap_u8() != 1 { + return Err(NvstDropReason::AuthenticationFailed); + } + let mut plaintext = datagram[..ciphertext_end].to_vec(); + ctr.apply_keystream(&mut plaintext[header.payload_offset..]); + Ok(plaintext) +} + +enum GcmCtr { + Aes128(Ctr32BE), + Aes256(Ctr32BE), +} + +impl GcmCtr { + fn apply_keystream(&mut self, buffer: &mut [u8]) { + match self { + Self::Aes128(cipher) => cipher.apply_keystream(buffer), + Self::Aes256(cipher) => cipher.apply_keystream(buffer), + } + } +} + +fn aes_gcm_tag_and_ctr( + encryption_key: &[u8], + iv: &[u8; 12], + aad: &[u8], + ciphertext: &[u8], +) -> ([u8; 16], GcmCtr) { + let mut j0 = [0_u8; 16]; + j0[..12].copy_from_slice(iv); + j0[15] = 1; + let mut hash_key = [0_u8; 16]; + let mut tag_mask = [0_u8; 16]; + let ctr = match encryption_key.len() { + 16 => { + let key: &[u8; 16] = encryption_key.try_into().expect("16-byte GCM key"); + let aes = ::new(key.into()); + aes.encrypt_block((&mut hash_key).into()); + let mut ctr = Ctr32BE::::new(key.into(), (&j0).into()); + ctr.apply_keystream(&mut tag_mask); + GcmCtr::Aes128(ctr) + } + 32 => { + let key: &[u8; 32] = encryption_key.try_into().expect("32-byte GCM key"); + let aes = ::new(key.into()); + aes.encrypt_block((&mut hash_key).into()); + let mut ctr = Ctr32BE::::new(key.into(), (&j0).into()); + ctr.apply_keystream(&mut tag_mask); + GcmCtr::Aes256(ctr) + } + _ => unreachable!("AES-GCM key is 16 or 32 bytes"), + }; + let mut hasher = ::new((&hash_key).into()); + hasher.update_padded(aad); + hasher.update_padded(ciphertext); + let mut len_block = ghash::Block::default(); + len_block[..8].copy_from_slice(&((aad.len() as u64) * 8).to_be_bytes()); + len_block[8..].copy_from_slice(&((ciphertext.len() as u64) * 8).to_be_bytes()); + hasher.update(&[len_block]); + let mut tag = hasher.finalize(); + for (byte, mask) in tag.iter_mut().zip(tag_mask) { + *byte ^= mask; + } + let mut expected = [0_u8; 16]; + expected.copy_from_slice(&tag); + (expected, ctr) +} + +#[cfg(test)] +fn protect_aes_gcm( + packet: &mut Vec, + payload_offset: usize, + encryption_key: &[u8], + iv: &[u8; 12], + tag_len: usize, +) { + let aad_owned = packet[..payload_offset].to_vec(); + let (_, mut ctr) = aes_gcm_tag_and_ctr(encryption_key, iv, &aad_owned, &[]); + ctr.apply_keystream(&mut packet[payload_offset..]); + let (tag, _) = aes_gcm_tag_and_ctr( + encryption_key, + iv, + &aad_owned, + &packet[payload_offset..], + ); + packet.extend_from_slice(&tag[..tag_len]); +} + +#[allow(clippy::too_many_arguments)] +fn unprotect_aes_cm_hmac_sha1( + datagram: &[u8], + header: RtpHeader, + packet_index: u64, + roc: u32, + encryption_key: &[u8], + authentication_key: &[u8; 20], + session_salt: &[u8; 14], + authentication_tag_len: usize, +) -> Result, NvstDropReason> { + let authenticated_len = + datagram + .len() + .checked_sub(authentication_tag_len) + .ok_or(NvstDropReason::MalformedRtp( + RtpParseError::MissingAuthenticationTag, + ))?; + if authenticated_len < header.payload_offset { + return Err(NvstDropReason::MalformedRtp( + RtpParseError::MissingAuthenticationTag, + )); + } + let mut mac = + HmacSha1::new_from_slice(authentication_key).expect("HMAC-SHA1 accepts a fixed-size key"); + mac.update(&datagram[..authenticated_len]); + mac.update(&roc.to_be_bytes()); + let expected_tag = mac.finalize().into_bytes(); + let received_tag = &datagram[authenticated_len..]; + if expected_tag[..authentication_tag_len] + .ct_eq(received_tag) + .unwrap_u8() + != 1 + { + return Err(NvstDropReason::AuthenticationFailed); + } + let mut plaintext = datagram[..authenticated_len].to_vec(); + let iv = srtp_aes_cm_iv(session_salt, header.ssrc, packet_index); + match encryption_key.len() { + 16 => { + let key: &[u8; 16] = encryption_key.try_into().expect("16-byte AES-CM key"); + let mut cipher = Aes128Ctr::new(key.into(), (&iv).into()); + cipher.apply_keystream(&mut plaintext[header.payload_offset..]); + } + 32 => { + let key: &[u8; 32] = encryption_key.try_into().expect("32-byte AES-CM key"); + let mut cipher = Aes256Ctr::new(key.into(), (&iv).into()); + cipher.apply_keystream(&mut plaintext[header.payload_offset..]); + } + _ => unreachable!("AES-CM key is 16 or 32 bytes"), + } + Ok(plaintext) +} + +fn derive_aes_cm_key( + master_key: &[u8; 32], + master_salt: &[u8], + label: u8, +) -> [u8; N] { + let mut iv = [0_u8; 16]; + iv[..master_salt.len()].copy_from_slice(master_salt); + // RFC 3711 key_id = label * 2^48, then x = master_salt * 2^16 XOR key_id * 2^16. + iv[7] ^= label; + let mut output = [0_u8; N]; + let mut cipher = Aes256Ctr::new(master_key.into(), (&iv).into()); + cipher.apply_keystream(&mut output); + output +} + +fn derive_aes128_cm_key( + master_key: &[u8; 16], + master_salt: &[u8], + label: u8, +) -> [u8; N] { + let mut iv = [0_u8; 16]; + iv[..master_salt.len()].copy_from_slice(master_salt); + iv[7] ^= label; + let mut output = [0_u8; N]; + let mut cipher = Aes128Ctr::new(master_key.into(), (&iv).into()); + cipher.apply_keystream(&mut output); + output +} + +fn srtp_aes_cm_iv(session_salt: &[u8; 14], ssrc: u32, packet_index: u64) -> [u8; 16] { + let mut iv = [0_u8; 16]; + iv[..14].copy_from_slice(session_salt); + for (target, source) in iv[4..8].iter_mut().zip(ssrc.to_be_bytes()) { + *target ^= source; + } + let index_bytes = packet_index.to_be_bytes(); + for (target, source) in iv[8..14].iter_mut().zip(&index_bytes[2..]) { + *target ^= source; + } + iv +} + +fn srtp_gcm_iv(session_salt: [u8; 12], ssrc: u32, roc: u32, sequence_number: u16) -> [u8; 12] { + let mut iv = session_salt; + for (target, source) in iv[2..6].iter_mut().zip(ssrc.to_be_bytes()) { + *target ^= source; + } + for (target, source) in iv[6..10].iter_mut().zip(roc.to_be_bytes()) { + *target ^= source; + } + for (target, source) in iv[10..].iter_mut().zip(sequence_number.to_be_bytes()) { + *target ^= source; + } + iv +} + +/// RFC 7714 §9.2 SRTCP GCM IV: salt XOR (SSRC at bytes 2..6, SRTCP index at 6..10). +fn srtcp_gcm_iv(session_salt: [u8; 12], ssrc: u32, srtcp_index: u32) -> [u8; 12] { + let mut iv = session_salt; + for (target, source) in iv[2..6].iter_mut().zip(ssrc.to_be_bytes()) { + *target ^= source; + } + for (target, source) in iv[6..10].iter_mut().zip(srtcp_index.to_be_bytes()) { + *target ^= source; + } + iv +} + +/// Builds and SRTCP-protects an RTCP Receiver Report (RFC 3550 §6.4.1) carrying one +/// report block for `media_ssrc`. GCM layout (RFC 7714 §9): the first 8 bytes stay +/// cleartext, only the report block is encrypted, then the E|index word and the +/// truncated auth tag are appended. AAD = cleartext header || E|index. +#[allow(clippy::too_many_arguments)] +fn protect_srtcp_receiver_report_gcm( + sender_ssrc: u32, + media_ssrc: u32, + highest_sequence: u32, + srtcp_index: u32, + encryption_key: &[u8], + session_salt: &[u8; 12], + tag_len: usize, +) -> Vec { + let mut packet = Vec::with_capacity(32 + 4 + tag_len); + packet.push(0x81); // V=2, P=0, RC=1 (one report block) + packet.push(201); // PT=RR + packet.extend_from_slice(&7_u16.to_be_bytes()); // length: 8 words - 1 + packet.extend_from_slice(&sender_ssrc.to_be_bytes()); + packet.extend_from_slice(&media_ssrc.to_be_bytes()); + packet.push(0); // fraction lost + packet.extend_from_slice(&[0, 0, 0]); // cumulative packets lost (24-bit) + packet.extend_from_slice(&highest_sequence.to_be_bytes()); + packet.extend_from_slice(&0_u32.to_be_bytes()); // interarrival jitter + packet.extend_from_slice(&0_u32.to_be_bytes()); // LSR + packet.extend_from_slice(&0_u32.to_be_bytes()); // DLSR + + let e_index = SRTCP_ENCRYPTED_FLAG | (srtcp_index & !SRTCP_ENCRYPTED_FLAG); + let iv = srtcp_gcm_iv(*session_salt, sender_ssrc, srtcp_index); + let mut aad = packet[..8].to_vec(); + aad.extend_from_slice(&e_index.to_be_bytes()); + let (_, mut ctr) = aes_gcm_tag_and_ctr(encryption_key, &iv, &aad, &[]); + ctr.apply_keystream(&mut packet[8..]); + let (tag, _) = aes_gcm_tag_and_ctr(encryption_key, &iv, &aad, &packet[8..]); + packet.extend_from_slice(&e_index.to_be_bytes()); + packet.extend_from_slice(&tag[..tag_len]); + packet +} + +/// Builds a plain (unencrypted) RTCP Receiver Report (RFC 3550 §6.4.1) with one +/// report block for `media_ssrc`. Sent over the `rtcp1` SCTP data channel, which +/// is already encrypted by DTLS, so no SRTCP layer is applied. +fn build_rtcp_receiver_report(sender_ssrc: u32, media_ssrc: u32, highest_sequence: u32) -> Vec { + let mut packet = Vec::with_capacity(32); + packet.push(0x81); // V=2, P=0, RC=1 (one report block) + packet.push(201); // PT=RR + packet.extend_from_slice(&7_u16.to_be_bytes()); // length: 8 words - 1 + packet.extend_from_slice(&sender_ssrc.to_be_bytes()); + packet.extend_from_slice(&media_ssrc.to_be_bytes()); + packet.push(0); // fraction lost + packet.extend_from_slice(&[0, 0, 0]); // cumulative packets lost (24-bit) + packet.extend_from_slice(&highest_sequence.to_be_bytes()); + packet.extend_from_slice(&0_u32.to_be_bytes()); // interarrival jitter + packet.extend_from_slice(&0_u32.to_be_bytes()); // LSR + packet.extend_from_slice(&0_u32.to_be_bytes()); // DLSR + packet +} + +/// Builds a plain RTCP Picture Loss Indication (RFC 4585 §6.3.1) asking the +/// sender of `media_ssrc` for a fresh keyframe. +fn build_rtcp_pli(sender_ssrc: u32, media_ssrc: u32) -> Vec { + let mut packet = Vec::with_capacity(12); + packet.push(0x81); // V=2, P=0, FMT=1 (PLI) + packet.push(192); // PT=PSFB (payload-specific feedback) + packet.extend_from_slice(&2_u16.to_be_bytes()); // length: 3 words - 1 + packet.extend_from_slice(&sender_ssrc.to_be_bytes()); + packet.extend_from_slice(&media_ssrc.to_be_bytes()); + packet +} + +#[derive(Clone, Default)] +struct ReplayWindow { + highest_index: Option, + seen: u64, +} + +impl ReplayWindow { + fn guess_packet_index(&self, sequence_number: u16) -> Result { + let Some(highest_index) = self.highest_index else { + return Ok(u64::from(sequence_number)); + }; + let roc = highest_index >> 16; + let highest_sequence = (highest_index & 0xffff) as u16; + let delta = i32::from(sequence_number) - i32::from(highest_sequence); + let guessed_roc = if delta < -32_768 { + roc.checked_add(1).ok_or(NvstDropReason::ReplayRejected)? + } else if delta > 32_768 { + roc.checked_sub(1).ok_or(NvstDropReason::ReplayRejected)? + } else { + roc + }; + Ok((guessed_roc << 16) | u64::from(sequence_number)) + } + + fn check(&self, index: u64) -> Result<(), NvstDropReason> { + let Some(highest_index) = self.highest_index else { + return Ok(()); + }; + if index > highest_index { + return Ok(()); + } + let age = highest_index - index; + if age >= 64 || self.seen & (1_u64 << age) != 0 { + return Err(NvstDropReason::ReplayRejected); + } + Ok(()) + } + + fn commit(&mut self, index: u64) { + match self.highest_index { + None => { + self.highest_index = Some(index); + self.seen = 1; + } + Some(highest_index) if index > highest_index => { + let advance = index - highest_index; + self.seen = if advance >= 64 { + 1 + } else { + (self.seen << advance) | 1 + }; + self.highest_index = Some(index); + } + Some(highest_index) => { + self.seen |= 1_u64 << (highest_index - index); + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct NvVideoPacket { + stream_packet_index: u32, + frame_index: u32, + flags: u8, + is_fec: bool, +} + +impl NvVideoPacket { + /// Reads the Mjolnir video metadata from the `0x4753` ("GS") RTP extension: + /// a 16-byte little-endian block holding the stream packet index, frame index, + /// the packet-type nibble (picture data / SOF / EOF), and FEC group + /// coordinates. The RTP payload itself is pure H.264 access-unit data. + fn parse<'a>(header: &RtpHeader, payload: &'a [u8]) -> Result<(Self, &'a [u8]), RtpParseError> { + let Some(extension) = header.gs_video_header else { + return Err(RtpParseError::MissingNvVideoHeader); + }; + let packet_word = u32::from_le_bytes(extension[0..4].try_into().expect("length checked")); + let flags_word = u32::from_le_bytes(extension[8..12].try_into().expect("length checked")); + let fec_word = u32::from_le_bytes(extension[12..16].try_into().expect("length checked")); + let fec_index = (fec_word >> 12) & 0x3ff; + let fec_source_packets = (fec_word >> 22) & 0x3ff; + let packet = Self { + stream_packet_index: (packet_word >> 8) & STREAM_PACKET_INDEX_MASK, + frame_index: u32::from_le_bytes(extension[4..8].try_into().expect("length checked")), + flags: (flags_word & 0x0f) as u8, + is_fec: (fec_word >> 8) & 0xff != 0 && fec_index >= fec_source_packets, + }; + Ok((packet, payload)) + } + + fn contains_picture_data(self) -> bool { + self.flags & FLAG_CONTAINS_PIC_DATA != 0 + } + + fn is_start_of_frame(self) -> bool { + self.flags & FLAG_SOF != 0 + } + + fn is_end_of_frame(self) -> bool { + self.flags & FLAG_EOF != 0 + } +} + +struct H264AccessUnitAssembler { + current_frame: Option, + first_stream_packet_index: Option, + bytes: Vec, + max_access_unit_bytes: usize, +} + +impl H264AccessUnitAssembler { + fn new(max_access_unit_bytes: usize) -> Self { + Self { + current_frame: None, + first_stream_packet_index: None, + bytes: Vec::new(), + max_access_unit_bytes, + } + } + + fn reset(&mut self) { + self.current_frame = None; + self.first_stream_packet_index = None; + self.bytes.clear(); + } + + fn push( + &mut self, + header: NvVideoPacket, + timestamp: u32, + payload: &[u8], + ) -> Result, NvstDropReason> { + if header.is_fec || !header.contains_picture_data() { + return Err(NvstDropReason::Unsupported( + NvstUnsupportedFeature::FecRepair, + )); + } + if header.is_start_of_frame() { + self.reset(); + let Some(payload) = h264_picture_payload(payload) else { + return Err(NvstDropReason::MissingAnnexBStartCode); + }; + self.current_frame = Some(header.frame_index); + self.first_stream_packet_index = Some(header.stream_packet_index); + let remaining = self.max_access_unit_bytes; + if payload.len() > remaining { + self.reset(); + return Err(NvstDropReason::AccessUnitTooLarge { + limit: self.max_access_unit_bytes, + }); + } + self.bytes.extend_from_slice(payload); + } else if self.current_frame != Some(header.frame_index) { + self.reset(); + return Err(NvstDropReason::AwaitingStartOfFrame); + } else { + let remaining = self.max_access_unit_bytes.saturating_sub(self.bytes.len()); + if payload.len() > remaining { + self.reset(); + return Err(NvstDropReason::AccessUnitTooLarge { + limit: self.max_access_unit_bytes, + }); + } + self.bytes.extend_from_slice(payload); + } + if !header.is_end_of_frame() { + return Ok(None); + } + + let bytes = std::mem::take(&mut self.bytes); + self.current_frame = None; + let first_stream_packet_index = self + .first_stream_packet_index + .take() + .expect("start-of-frame initializes the packet index"); + Ok(Some(EncodedH264Frame { + timestamp, + frame_index: header.frame_index, + first_stream_packet_index, + keyframe: h264_access_unit_is_keyframe(&bytes), + bytes, + })) + } +} + +static NVST_DEBUG_DUMP_REMAINING: AtomicU64 = AtomicU64::new(96); + +/// Temporary ground-truth dump of decrypted Mjolnir video packets so the +/// RTP extension + NV_VIDEO_PACKET layout can be verified against live traffic. +fn debug_dump_nv_packet(path: &str, packet: &[u8], payload_offset: usize) { + use std::fmt::Write as _; + if NVST_DEBUG_DUMP_REMAINING + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |remaining| { + remaining.checked_sub(1) + }) + .is_err() + { + return; + } + let dump_len = packet.len().min(56); + let mut packet_hex = String::with_capacity(dump_len * 2 + 8); + for (index, byte) in packet[..dump_len].iter().enumerate() { + if index == 12 || index == payload_offset { + let _ = write!(packet_hex, "|"); + } + let _ = write!(packet_hex, "{byte:02x}"); + } + eprintln!( + "NVST pkt-dump[{path}] len={} payloadOff={payload_offset} bytes={packet_hex}", + packet.len(), + ); + } + + static NVST_DEBUG_FRAME_DUMP_REMAINING: AtomicU64 = AtomicU64::new(24); + + /// Temporary dump of assembled access units to verify NAL layout/keyframe + /// detection against live traffic. + fn debug_dump_frame(frame: &EncodedH264Frame) { + use std::fmt::Write as _; + if NVST_DEBUG_FRAME_DUMP_REMAINING + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |remaining| { + remaining.checked_sub(1) + }) + .is_err() + { + return; + } + let dump_len = frame.bytes.len().min(40); + let mut hex = String::with_capacity(dump_len * 2); + for byte in &frame.bytes[..dump_len] { + let _ = write!(hex, "{byte:02x}"); + } + eprintln!( + "NVST frame-dump len={} keyframe={} ts={} bytes={hex}", + frame.bytes.len(), + frame.keyframe, + frame.timestamp, + ); + } + +fn h264_picture_payload(payload: &[u8]) -> Option<&[u8]> { + let search_len = payload.len().min(MAX_GS_FRAME_HEADER_BYTES + 4); + let (offset, _) = find_annex_b_start_code(&payload[..search_len])?; + Some(&payload[offset..]) +} + +fn h264_access_unit_is_keyframe(bytes: &[u8]) -> bool { + let mut offset = 0; + while let Some((start, prefix_len)) = find_annex_b_start_code(&bytes[offset..]) { + let nal_start = offset + start + prefix_len; + if let Some(nal_header) = bytes.get(nal_start) + && nal_header & 0x1f == 5 + { + return true; + } + offset = nal_start; + } + false +} + +fn find_annex_b_start_code(bytes: &[u8]) -> Option<(usize, usize)> { + bytes + .windows(4) + .position(|window| window == [0, 0, 0, 1]) + .map_or_else( + || { + bytes + .windows(3) + .position(|window| window == [0, 0, 1]) + .map(|position| (position, 3)) + }, + |position| Some((position, 4)), + ) +} + +struct RtpReorderBuffer { + next_index: Option, + packets: BTreeMap, + max_packets: usize, +} + +struct ReorderResult { + ready: Vec, + recovery: Option, + dropped: Option, +} + +impl RtpReorderBuffer { + fn new(max_packets: usize) -> Self { + Self { + next_index: None, + packets: BTreeMap::new(), + max_packets, + } + } + + fn reset(&mut self) { + self.next_index = None; + self.packets.clear(); + } + + fn push(&mut self, packet: RtpPacket) -> ReorderResult { + let index = packet.index; + let next_index = *self.next_index.get_or_insert(index); + if index < next_index { + return ReorderResult { + ready: Vec::new(), + recovery: None, + dropped: Some(NvstDropReason::StaleRtpPacket { index }), + }; + } + if self.packets.contains_key(&index) { + return ReorderResult { + ready: Vec::new(), + recovery: None, + dropped: Some(NvstDropReason::DuplicateRtpPacket { index }), + }; + } + + let mut recovery = None; + if index.saturating_sub(next_index) >= self.max_packets as u64 { + recovery = Some(NvstRecovery::PacketGap { + first_missing_index: next_index, + last_missing_index: index - 1, + nack: NvstUnsupportedFeature::Nack, + }); + self.packets.clear(); + self.next_index = Some(index); + } + self.packets.insert(index, packet); + + if self.packets.len() >= self.max_packets { + let first_available = *self + .packets + .first_key_value() + .expect("non-empty after insertion") + .0; + let expected = self.next_index.expect("next index set above"); + if first_available > expected { + recovery = Some(NvstRecovery::PacketGap { + first_missing_index: expected, + last_missing_index: first_available - 1, + nack: NvstUnsupportedFeature::Nack, + }); + self.next_index = Some(first_available); + } + } + + let mut ready = Vec::new(); + while let Some(next) = self.next_index { + let Some(packet) = self.packets.remove(&next) else { + break; + }; + ready.push(packet); + self.next_index = Some(next + 1); + } + ReorderResult { + ready, + recovery, + dropped: None, + } + } +} + +/// Sends periodic SRTCP Receiver Reports so the peer keeps the video flowing. +/// The official client maintains an SRTCP session (hook captures show the 0x03/0x05 +/// KDF labels); without any receiver feedback the server stops video after an +/// initial burst. Only the GCM profiles are supported (the active Mjolnir policy). +struct SrtcpSender { + encryption_key: SrtcpKey, + session_salt: [u8; 12], + tag_len: usize, + sender_ssrc: u32, + next_index: u32, + last_sent: Option, +} + +enum SrtcpKey { + Aes128([u8; 16]), + Aes256([u8; 32]), +} + +impl SrtcpKey { + fn as_bytes(&self) -> &[u8] { + match self { + Self::Aes128(key) => key, + Self::Aes256(key) => key, + } + } +} + +impl SrtcpSender { + fn from_material(material: &NvstSrtpMaterial) -> Option { + let (encryption_key, session_salt, tag_len) = match material { + NvstSrtpMaterial::AeadAes128Gcm { + master_key, + master_salt, + authentication_tag_len, + } => ( + SrtcpKey::Aes128(derive_aes128_cm_key::<16>( + master_key, + master_salt, + GFN_SRTCP_KEY_LABEL, + )), + derive_aes128_cm_key::<12>(master_key, master_salt, GFN_SRTCP_SALT_LABEL), + *authentication_tag_len, + ), + NvstSrtpMaterial::AeadAes256Gcm { + master_key, + master_salt, + authentication_tag_len, + } => ( + SrtcpKey::Aes256(derive_aes_cm_key::<32>( + master_key, + master_salt, + GFN_SRTCP_KEY_LABEL, + )), + derive_aes_cm_key::<12>(master_key, master_salt, GFN_SRTCP_SALT_LABEL), + *authentication_tag_len, + ), + _ => return None, + }; + let mut sender_ssrc = [0_u8; 4]; + if getrandom::fill(&mut sender_ssrc).is_err() { + sender_ssrc = 0x4f4e_4f57_u32.to_be_bytes(); // "ONOW" + } + Some(Self { + encryption_key, + session_salt, + tag_len, + sender_ssrc: u32::from_be_bytes(sender_ssrc), + next_index: 0, + last_sent: None, + }) + } + + /// Returns an SRTCP Receiver Report to send once per interval, after the media + /// SSRC is known. Returns `None` when it is not yet time or nothing to report on. + fn poll_receiver_report( + &mut self, + media_ssrc: Option, + highest_sequence: u32, + now: Instant, + ) -> Option> { + let media_ssrc = media_ssrc?; + if let Some(last) = self.last_sent + && now.duration_since(last) < SRTCP_RR_INTERVAL + { + return None; + } + self.last_sent = Some(now); + let index = self.next_index; + self.next_index = self.next_index.wrapping_add(1); + Some(protect_srtcp_receiver_report_gcm( + self.sender_ssrc, + media_ssrc, + highest_sequence, + index, + self.encryption_key.as_bytes(), + &self.session_salt, + self.tag_len, + )) + } +} + +/// Stateful, non-blocking NVST video receiver. `process_datagram` is deterministic and testable; +/// `spawn_nvst_udp_receiver` below is a thin UDP/thread wrapper for the production path. +pub struct NvstVideoReceiver { + config: NvstVideoConfig, + srtp: SrtpReceiver, + srtcp: Option, + reorder: RtpReorderBuffer, + assembler: H264AccessUnitAssembler, + state: NvstReceiverState, + bound_ssrc: Option, + highest_sequence_received: u32, + authenticated_packets: u64, + fec_packets: u64, + frames_emitted: u64, + timeout_origin: Instant, + last_authenticated_packet: Option, +} + +impl NvstVideoReceiver { + pub fn new(config: NvstVideoConfig) -> Self { + let srtp = SrtpReceiver::from_material(&config.srtp); + let srtcp = SrtcpSender::from_material(&config.srtp); + let reorder = RtpReorderBuffer::new(config.reorder_window_packets); + let assembler = H264AccessUnitAssembler::new(config.max_access_unit_bytes); + Self { + config, + srtp, + srtcp, + reorder, + assembler, + state: NvstReceiverState::Running, + bound_ssrc: None, + highest_sequence_received: 0, + authenticated_packets: 0, + fec_packets: 0, + frames_emitted: 0, + timeout_origin: Instant::now(), + last_authenticated_packet: None, + } + } + + pub fn state(&self) -> NvstReceiverState { + self.state + } + + pub fn pause(&mut self) -> Option { + if self.state != NvstReceiverState::Running { + return None; + } + self.reset_media_state(); + self.state = NvstReceiverState::Paused; + Some(NvstReceiveEvent::Lifecycle(self.state)) + } + + pub fn resume(&mut self) -> Option { + if self.state != NvstReceiverState::Paused { + return None; + } + self.reset_media_state(); + self.timeout_origin = Instant::now(); + self.state = NvstReceiverState::Running; + Some(NvstReceiveEvent::Lifecycle(self.state)) + } + + pub fn recover(&mut self) -> Option { + if self.state != NvstReceiverState::RecoveryRequired { + return None; + } + self.reset_media_state(); + self.timeout_origin = Instant::now(); + self.state = NvstReceiverState::Running; + Some(NvstReceiveEvent::Lifecycle(self.state)) + } + + pub fn stop(&mut self) -> Option { + if self.state == NvstReceiverState::Stopped { + return None; + } + self.reset_media_state(); + self.state = NvstReceiverState::Stopped; + Some(NvstReceiveEvent::Lifecycle(self.state)) + } + + /// Returns a typed timeout only once. The caller must deliberately call `recover` before + /// more media is accepted, preventing a stale stream from silently resuming. + pub fn poll_timeout(&mut self, now: Instant) -> Option { + if self.state != NvstReceiverState::Running { + return None; + } + let last_packet = self + .last_authenticated_packet + .unwrap_or(self.timeout_origin); + let idle_for = now.saturating_duration_since(last_packet); + if idle_for < self.config.timeout { + return None; + } + self.reset_media_state(); + self.state = NvstReceiverState::RecoveryRequired; + Some(NvstReceiveEvent::RecoveryNeeded(NvstRecovery::Timeout { + idle_for, + })) + } + + pub fn process_datagram( + &mut self, + source: SocketAddr, + datagram: &[u8], + now: Instant, + ) -> Vec { + if source != self.config.video_peer { + return vec![NvstReceiveEvent::Dropped( + NvstDropReason::UnexpectedSource { + expected: self.config.video_peer, + actual: source, + }, + )]; + } + match self.state { + NvstReceiverState::Paused => { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::Paused)]; + } + NvstReceiverState::Stopped => { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::Stopped)]; + } + NvstReceiverState::RecoveryRequired => { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::RecoveryRequired)]; + } + NvstReceiverState::Running => {} + } + let packet = match self.srtp.unprotect(datagram) { + Ok(packet) => packet, + Err(reason) => return vec![NvstReceiveEvent::Dropped(reason)], + }; + debug_dump_nv_packet("raw", &packet.plaintext, packet.header.payload_offset); + if let Some(expected) = self.config.expected_payload_type + && packet.header.payload_type != expected + { + return vec![NvstReceiveEvent::Dropped( + NvstDropReason::UnexpectedPayloadType { + expected, + actual: packet.header.payload_type, + }, + )]; + } + let expected_ssrc = self.config.expected_ssrc.or(self.bound_ssrc); + if let Some(expected) = expected_ssrc + && packet.header.ssrc != expected + { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::UnexpectedSsrc { + expected, + actual: packet.header.ssrc, + })]; + } + self.bound_ssrc.get_or_insert(packet.header.ssrc); + self.last_authenticated_packet = Some(now); + self.authenticated_packets += 1; + let sequence = u32::try_from(packet.index & 0xffff_ffff).unwrap_or(u32::MAX); + self.highest_sequence_received = self.highest_sequence_received.max(sequence); + self.config + .feedback + .publish_stream(packet.header.ssrc, self.highest_sequence_received); + + let result = self.reorder.push(packet); + let mut events = Vec::new(); + if let Some(reason) = result.dropped { + events.push(NvstReceiveEvent::Dropped(reason)); + } + if let Some(recovery) = result.recovery { + self.assembler.reset(); + // A sequence gap breaks the decoder's reference chain; ask (via the + // bundle's rtcp1 channel) for a fresh keyframe to recover. + self.config.feedback.request_keyframe(); + events.push(NvstReceiveEvent::RecoveryNeeded(recovery)); + } + for packet in result.ready { + let payload = match packet.header.payload(&packet.plaintext) { + Ok(payload) => payload, + Err(error) => { + events.push(NvstReceiveEvent::Dropped(NvstDropReason::MalformedRtp( + error, + ))); + continue; + } + }; + let (nv_packet, media) = match NvVideoPacket::parse(&packet.header, payload) { + Ok(value) => value, + Err(error) => { + events.push(NvstReceiveEvent::Dropped(NvstDropReason::MalformedRtp( + error, + ))); + continue; + } + }; + match self + .assembler + .push(nv_packet, packet.header.timestamp, media) + { + Ok(Some(frame)) => { + self.frames_emitted += 1; + debug_dump_frame(&frame); + events.push(NvstReceiveEvent::Frame(frame)); + } + Ok(None) => {} + Err(reason) => { + if matches!(reason, NvstDropReason::Unsupported(NvstUnsupportedFeature::FecRepair)) + { + self.fec_packets += 1; + } + events.push(NvstReceiveEvent::Dropped(reason)); + } + } + } + events + } + + /// Returns an SRTCP Receiver Report to send to the video peer, at most once per + /// interval, once the media SSRC is known. Keeps the peer's video flowing. + pub fn poll_receiver_report(&mut self, now: Instant) -> Option> { + if self.state != NvstReceiverState::Running { + return None; + } + self.srtcp.as_mut()?.poll_receiver_report( + self.bound_ssrc, + self.highest_sequence_received, + now, + ) + } + + /// Elapsed-since-start receive counters for ground-truth timing that does not + /// depend on when buffered log lines happen to flush. + pub fn stats_line(&self, origin: Instant) -> String { + format!( + "elapsed={:.1}s auth={} fec={} frames={} ssrc={:?}", + origin.elapsed().as_secs_f64(), + self.authenticated_packets, + self.fec_packets, + self.frames_emitted, + self.bound_ssrc, + ) + } + + fn process_mjolnir_payload( + &mut self, + ssrc: u32, + rtp_timestamp: u32, + payload: &[u8], + now: Instant, + ) -> Vec { + match self.state { + NvstReceiverState::Paused => { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::Paused)]; + } + NvstReceiverState::Stopped => { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::Stopped)]; + } + NvstReceiverState::RecoveryRequired => { + return vec![NvstReceiveEvent::Dropped(NvstDropReason::RecoveryRequired)]; + } + NvstReceiverState::Running => {} + } + self.bound_ssrc.get_or_insert(ssrc); + self.last_authenticated_packet = Some(now); + debug_dump_nv_packet("bundle", payload, 0); + // The bundle path cannot assemble video: the Mjolnir frame metadata lives + // in the `0x4753` RTP extension, which str0m does not surface. The official + // cloud path delivers video exclusively on the raw Mjolnir socket, so bundle + // RTP (audio/control) is intentionally ignored here. + let _ = rtp_timestamp; + Vec::new() + } + + fn reset_media_state(&mut self) { + self.reorder.reset(); + self.assembler.reset(); + } +} + +enum StunDatagram { + NotStun, + Invalid, + Handled(Option>), +} + +fn append_stun_attribute(packet: &mut Vec, attribute_type: u16, value: &[u8]) { + packet.extend_from_slice(&attribute_type.to_be_bytes()); + packet.extend_from_slice(&(value.len() as u16).to_be_bytes()); + packet.extend_from_slice(value); + packet.resize(packet.len().next_multiple_of(4), 0); +} + +fn build_authenticated_stun_packet( + message_type: u16, + transaction_id: &[u8; 12], + key: &[u8], + attributes: &[(u16, Vec)], +) -> Vec { + let mut packet = Vec::with_capacity(128); + packet.extend_from_slice(&message_type.to_be_bytes()); + packet.extend_from_slice(&0_u16.to_be_bytes()); + packet.extend_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes()); + packet.extend_from_slice(transaction_id); + for (attribute_type, value) in attributes { + append_stun_attribute(&mut packet, *attribute_type, value); + } + + let length_through_integrity = packet.len() - STUN_HEADER_LEN + 24; + packet[2..4].copy_from_slice(&(length_through_integrity as u16).to_be_bytes()); + let mut mac = HmacSha1::new_from_slice(key).expect("HMAC accepts variable-size ICE passwords"); + mac.update(&packet); + append_stun_attribute( + &mut packet, + STUN_ATTR_MESSAGE_INTEGRITY, + &mac.finalize().into_bytes(), + ); + + let final_length = packet.len() - STUN_HEADER_LEN + 8; + packet[2..4].copy_from_slice(&(final_length as u16).to_be_bytes()); + let fingerprint = crc32(&packet) ^ STUN_FINGERPRINT_XOR; + append_stun_attribute( + &mut packet, + STUN_ATTR_FINGERPRINT, + &fingerprint.to_be_bytes(), + ); + packet +} + +fn build_stun_binding_request( + credentials: &NvstStunCredentials, + transaction_id: &[u8; 12], +) -> Vec { + let username = format!( + "{}:{}", + credentials.remote_username_fragment, credentials.local_username_fragment + ); + build_authenticated_stun_packet( + STUN_BINDING_REQUEST, + transaction_id, + credentials.remote_password.as_bytes(), + &[(STUN_ATTR_USERNAME, username.into_bytes())], + ) +} + +/// Official `NattHolePunch::SendPing` (pingVersion=6) encodes +/// `SetStunCredentials(local, pingPayload, …, describePassword)` as +/// USERNAME `pingPayload:localUfrag` and HMAC-SHA1 with the DESCRIBE password. +/// WebRtcTransport ICE uses the V2 ufrag via [`build_stun_binding_request`]. +fn build_natt_hole_punch_request( + local_username_fragment: &str, + ping_payload: &[u8], + remote_password: &str, + transaction_id: &[u8; 12], +) -> Vec { + let username = format!( + "{}:{local_username_fragment}", + String::from_utf8_lossy(ping_payload) + ); + build_authenticated_stun_packet( + STUN_BINDING_REQUEST, + transaction_id, + remote_password.as_bytes(), + &[(STUN_ATTR_USERNAME, username.into_bytes())], + ) +} + +fn xor_mapped_address(source: SocketAddr, transaction_id: &[u8; 12]) -> Vec { + let mut value = Vec::with_capacity(20); + value.push(0); + value.push(if source.is_ipv4() { 1 } else { 2 }); + value.extend_from_slice(&(source.port() ^ ((STUN_MAGIC_COOKIE >> 16) as u16)).to_be_bytes()); + match source.ip() { + IpAddr::V4(ip) => { + for (octet, mask) in ip.octets().into_iter().zip(STUN_MAGIC_COOKIE.to_be_bytes()) { + value.push(octet ^ mask); + } + } + IpAddr::V6(ip) => { + let mut mask = [0_u8; 16]; + mask[..4].copy_from_slice(&STUN_MAGIC_COOKIE.to_be_bytes()); + mask[4..].copy_from_slice(transaction_id); + for (octet, mask) in ip.octets().into_iter().zip(mask) { + value.push(octet ^ mask); + } + } + } + value +} + +fn find_stun_attribute(packet: &[u8], wanted_type: u16) -> Option<(usize, &[u8])> { + let mut offset = STUN_HEADER_LEN; + while offset + 4 <= packet.len() { + let attribute_type = u16::from_be_bytes([packet[offset], packet[offset + 1]]); + let length = usize::from(u16::from_be_bytes([packet[offset + 2], packet[offset + 3]])); + let value_start = offset + 4; + let value_end = value_start.checked_add(length)?; + if value_end > packet.len() { + return None; + } + if attribute_type == wanted_type { + return Some((offset, &packet[value_start..value_end])); + } + offset = value_end.next_multiple_of(4); + } + None +} + +fn valid_stun_fingerprint(packet: &[u8]) -> bool { + let Some((offset, value)) = find_stun_attribute(packet, STUN_ATTR_FINGERPRINT) else { + return false; + }; + if value.len() != 4 || offset + 8 != packet.len() { + return false; + } + let expected = crc32(&packet[..offset]) ^ STUN_FINGERPRINT_XOR; + expected.to_be_bytes().ct_eq(value).into() +} + +fn valid_stun_message_integrity(packet: &[u8], key: &[u8]) -> bool { + let Some((integrity_offset, integrity)) = + find_stun_attribute(packet, STUN_ATTR_MESSAGE_INTEGRITY) + else { + return false; + }; + if integrity.len() != 20 { + return false; + } + let fingerprint_bytes = if find_stun_attribute(packet, STUN_ATTR_FINGERPRINT).is_some() { + 8 + } else { + 0 + }; + let Some(adjusted_length) = packet + .len() + .checked_sub(STUN_HEADER_LEN + fingerprint_bytes) + else { + return false; + }; + let mut authenticated = packet[..integrity_offset].to_vec(); + authenticated[2..4].copy_from_slice(&(adjusted_length as u16).to_be_bytes()); + let mut mac = HmacSha1::new_from_slice(key).expect("HMAC accepts variable-size ICE passwords"); + mac.update(&authenticated); + mac.finalize().into_bytes().ct_eq(integrity).into() +} + +fn handle_stun_datagram( + datagram: &[u8], + source: SocketAddr, + credentials: &NvstStunCredentials, +) -> StunDatagram { + if datagram.len() < STUN_HEADER_LEN + || datagram[0] & 0xc0 != 0 + || u32::from_be_bytes([datagram[4], datagram[5], datagram[6], datagram[7]]) + != STUN_MAGIC_COOKIE + { + return StunDatagram::NotStun; + } + let message_length = usize::from(u16::from_be_bytes([datagram[2], datagram[3]])); + if STUN_HEADER_LEN + message_length != datagram.len() || !valid_stun_fingerprint(datagram) { + return StunDatagram::Invalid; + } + let message_type = u16::from_be_bytes([datagram[0], datagram[1]]); + let transaction_id: [u8; 12] = datagram[8..20] + .try_into() + .expect("STUN header length checked above"); + match message_type { + STUN_BINDING_REQUEST => { + let Some((_, username)) = find_stun_attribute(datagram, STUN_ATTR_USERNAME) else { + return StunDatagram::Invalid; + }; + let expected_username = format!( + "{}:{}", + credentials.local_username_fragment, credentials.remote_username_fragment + ); + if !bool::from(expected_username.as_bytes().ct_eq(username)) + || !valid_stun_message_integrity(datagram, credentials.local_password.as_bytes()) + { + return StunDatagram::Invalid; + } + let mapped_address = xor_mapped_address(source, &transaction_id); + StunDatagram::Handled(Some(build_authenticated_stun_packet( + STUN_BINDING_SUCCESS_RESPONSE, + &transaction_id, + credentials.local_password.as_bytes(), + &[(STUN_ATTR_XOR_MAPPED_ADDRESS, mapped_address)], + ))) + } + STUN_BINDING_SUCCESS_RESPONSE => { + if valid_stun_message_integrity(datagram, credentials.remote_password.as_bytes()) { + StunDatagram::Handled(None) + } else { + StunDatagram::Invalid + } + } + _ => StunDatagram::Invalid, + } +} + +enum UdpReceiverCommand { + Pause, + Resume, + Recover, + Stop, +} + +/// Owns the bounded UDP receive worker. Frames go through the same bounded `MediaConsumer` used +/// by WebRTC, so a slow decoder cannot make UDP receive unbounded. +pub struct NvstUdpReceiverSession { + commands: Sender, + join: Option>, +} + +#[derive(Debug, Error)] +pub enum NvstUdpReceiverError { + #[error("failed to bind NVST UDP socket: {0}")] + Bind(#[source] std::io::Error), + #[error("failed to configure NVST UDP socket: {0}")] + Configure(#[source] std::io::Error), + #[error("failed to start NVST receive worker: {0}")] + Spawn(#[source] std::io::Error), + #[error("NVST receive worker is no longer running")] + Closed, + #[error("failed to prepare NVST WebRTC bundle: {0}")] + WebrtcBundle(String), +} + +impl NvstUdpReceiverSession { + pub fn pause(&self) -> Result<(), NvstUdpReceiverError> { + self.send(UdpReceiverCommand::Pause) + } + + pub fn resume(&self) -> Result<(), NvstUdpReceiverError> { + self.send(UdpReceiverCommand::Resume) + } + + pub fn recover(&self) -> Result<(), NvstUdpReceiverError> { + self.send(UdpReceiverCommand::Recover) + } + + pub fn stop(mut self) { + let _ = self.commands.send(UdpReceiverCommand::Stop); + if let Some(join) = self.join.take() { + let _ = join.join(); + } + } + + fn send(&self, command: UdpReceiverCommand) -> Result<(), NvstUdpReceiverError> { + self.commands + .send(command) + .map_err(|_| NvstUdpReceiverError::Closed) + } +} + +impl Drop for NvstUdpReceiverSession { + fn drop(&mut self) { + let _ = self.commands.send(UdpReceiverCommand::Stop); + } +} + +fn discover_routed_ipv4() -> Option { + let probe = UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)).ok()?; + probe + .connect(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 9)) + .ok()?; + let addr = probe.local_addr().ok()?; + if addr.ip().is_unspecified() || addr.ip().is_loopback() { + return None; + } + Some(addr.ip()) +} + +pub fn reserve_nvst_udp_socket() -> std::io::Result { + // Official binds the bundle sockets on 0.0.0.0 and advertises the routed + // NIC IPv4 separately via clientPorts.localAddress + host a=candidate. + bind_nvst_udp(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) +} + +/// IPv4 OpenNOW should put in ANNOUNCE `localAddress` / host candidate. +/// Independent of the bind address: official listens on 0.0.0.0. +pub fn advertised_nvst_ipv4() -> Option { + discover_routed_ipv4() +} + +/// ICE + DTLS identity that already owns the reserved bundle socket. +#[derive(Debug, Clone)] +pub struct NvstBundleIdentity { + pub ice_username_fragment: String, + pub ice_password: String, + pub dtls_fingerprint: String, +} + +/// UDP socket plus the `Rtc` that will speak ICE/DTLS on it, plus the dedicated +/// NATT-only video (Mjolnir) socket the official two-socket cloud model uses for +/// raw-SRTP video. +pub struct ReservedNvstBundle { + socket: UdpSocket, + rtc: Rtc, + mjolnir_socket: UdpSocket, +} + +impl ReservedNvstBundle { + pub fn reserve() -> Result { + let socket = reserve_nvst_udp_socket().map_err(NvstUdpReceiverError::Bind)?; + let rtc = create_nvst_bundle_rtc(&socket)?; + let mjolnir_socket = + reserve_nvst_mjolnir_udp_socket().map_err(NvstUdpReceiverError::Bind)?; + Ok(Self { + socket, + rtc, + mjolnir_socket, + }) + } + + pub fn local_addr(&self) -> std::io::Result { + self.socket.local_addr() + } + + pub fn mjolnir_local_addr(&self) -> std::io::Result { + self.mjolnir_socket.local_addr() + } + + pub fn advertised_local_address(&self) -> Option { + match self.socket.local_addr().ok()?.ip() { + IpAddr::V4(ip) if !ip.is_unspecified() && !ip.is_loopback() => Some(ip.to_string()), + _ => advertised_nvst_ipv4().map(|ip| ip.to_string()), + } + } + + pub fn identity(&mut self) -> NvstBundleIdentity { + nvst_local_bundle_identity(&mut self.rtc) + } + + pub fn send_to(&self, payload: &[u8], host: &str, port: u16) -> std::io::Result { + self.socket.send_to(payload, (host, port)) + } + + pub fn try_clone_socket(&self) -> std::io::Result { + self.socket.try_clone() + } + + pub fn into_parts(self) -> (UdpSocket, Rtc, UdpSocket) { + (self.socket, self.rtc, self.mjolnir_socket) + } +} + +fn generate_gfn_local_ice_credentials() -> IceCreds { + const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"; + let mut random = [0_u8; 26]; + let _ = getrandom::fill(&mut random); + let encode = |start: usize, length: usize| { + random[start..start + length] + .iter() + .map(|value| ALPHABET[usize::from(*value) & 0x3f] as char) + .collect::() + }; + IceCreds { + ufrag: encode(0, 4), + pass: encode(4, 22), + } +} + +fn create_nvst_bundle_rtc(socket: &UdpSocket) -> Result { + install_crypto(); + let _ = socket; + // Official GenerateIceCredentials() is 4-char ufrag / 22-char password. + // str0m's default 16-char ufrag is rejected by Bifrost length checks. + let mut rtc = RtcConfig::new().set_rtp_mode(true).build(Instant::now()); + rtc.direct_api() + .set_local_ice_credentials(generate_gfn_local_ice_credentials()); + Ok(rtc) +} + +fn nvst_local_bundle_identity(rtc: &mut Rtc) -> NvstBundleIdentity { + let creds = rtc.direct_api().local_ice_credentials(); + let fingerprint = rtc.direct_api().local_dtls_fingerprint().clone(); + NvstBundleIdentity { + ice_username_fragment: creds.ufrag, + ice_password: creds.pass, + dtls_fingerprint: nvst_fingerprint_hex(&fingerprint), + } +} + +fn nvst_fingerprint_hex(fingerprint: &Fingerprint) -> String { + fingerprint + .bytes + .iter() + .map(|byte| format!("{byte:02X}")) + .collect::>() + .join(":") +} + +fn routed_host_addr(peer: Option, local: SocketAddr) -> SocketAddr { + if !local.ip().is_unspecified() && !local.ip().is_loopback() { + return local; + } + if let Some(peer) = peer + && let Ok(probe) = UdpSocket::bind(SocketAddr::new( + if peer.is_ipv4() { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + } else { + IpAddr::V6(Ipv6Addr::UNSPECIFIED) + }, + 0, + )) + { + let _ = probe.connect(peer); + if let Ok(addr) = probe.local_addr() + && !addr.ip().is_unspecified() + && !addr.ip().is_loopback() + { + return SocketAddr::new(addr.ip(), local.port()); + } + } + local +} + +fn parse_nvst_fingerprint(value: &str) -> Result { + let trimmed = value.trim(); + let sdp = if trimmed.contains(' ') { + trimmed.to_owned() + } else { + format!("sha-256 {trimmed}") + }; + sdp.parse() +} + +fn bind_nvst_udp(bind_ip: IpAddr, port: u16) -> std::io::Result { + #[cfg(unix)] + if let Ok(fd_text) = std::env::var("OPENNOW_NVST_VIDEO_UDP_FD") { + if let Ok(fd) = fd_text.parse::() { + use std::os::unix::io::FromRawFd; + // Electron dups the probe socket onto this fd so native never rebinds. + eprintln!("NVST inheriting video UDP socket from fd {fd}"); + return Ok(unsafe { UdpSocket::from_raw_fd(fd) }); + } + } + bind_nvst_udp_socket(bind_ip, port) +} + +fn bind_nvst_udp_socket(bind_ip: IpAddr, port: u16) -> std::io::Result { + let domain = if bind_ip.is_ipv4() { + Domain::IPV4 + } else { + Domain::IPV6 + }; + let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?; + socket.set_reuse_address(true)?; + #[cfg(unix)] + socket.set_reuse_port(true)?; + socket.bind(&SocketAddr::new(bind_ip, port).into())?; + Ok(socket.into()) +} + +/// Reserves the dedicated NATT-only video (Mjolnir) socket. The native streamer +/// always owns this socket outright, so it never inherits an Electron-owned fd. +pub fn reserve_nvst_mjolnir_udp_socket() -> std::io::Result { + bind_nvst_udp_socket(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) +} + +pub fn spawn_nvst_udp_receiver( + config: NvstVideoConfig, + media_consumer: MediaConsumer, + event_sender: Sender, +) -> Result { + spawn_nvst_udp_receiver_with_socket(config, media_consumer, event_sender, None, None) +} + +pub fn spawn_nvst_udp_receiver_with_socket( + config: NvstVideoConfig, + media_consumer: MediaConsumer, + event_sender: Sender, + reserved_socket: Option, + reserved_rtc: Option, +) -> Result { + let bind_ip = match config.video_peer.ip() { + IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::UNSPECIFIED), + }; + let socket = match reserved_socket { + Some(socket) => { + eprintln!( + "NVST using UDP socket reserved before ANNOUNCE ({})", + socket + .local_addr() + .map(|addr| addr.to_string()) + .unwrap_or_else(|_| "unknown".to_owned()) + ); + socket + } + None => { + bind_nvst_udp(bind_ip, config.client_udp_port).map_err(NvstUdpReceiverError::Bind)? + } + }; + socket + .set_read_timeout(Some(UDP_RECEIVE_POLL_INTERVAL)) + .map_err(NvstUdpReceiverError::Configure)?; + let rtc = if config.remote_dtls_fingerprint().is_some() { + let rtc = match reserved_rtc { + Some(rtc) => rtc, + None => create_nvst_bundle_rtc(&socket)?, + }; + Some(prepare_nvst_webrtc_bundle(&socket, &config, rtc)?) + } else { + None + }; + spawn_receiver_thread( + "opennow-nvst-video", + socket, + config, + media_consumer, + event_sender, + rtc, + ) +} + +/// Spawns the raw-SRTP NATT video receiver on the reserved Mjolnir socket. +/// +/// Official GFN cloud (`nativeRtcOnBundlePort=1`) delivers video RTP/SRTP to this +/// dedicated NATT-only socket while the ICE/DTLS bundle socket carries +/// control/audio. The raw receiver's NATT keepalive pings are what route video +/// here, and it decrypts with the runtime encryptionKey sent in ANNOUNCE. +pub fn spawn_nvst_mjolnir_receiver( + socket: UdpSocket, + config: NvstVideoConfig, + media_consumer: MediaConsumer, + event_sender: Sender, +) -> Result { + eprintln!( + "NVST Mjolnir raw-SRTP video receiver arming on {}", + socket + .local_addr() + .map(|addr| addr.to_string()) + .unwrap_or_else(|_| "unknown".to_owned()) + ); + spawn_receiver_thread( + "opennow-nvst-mjolnir", + socket, + config, + media_consumer, + event_sender, + None, + ) +} + +fn spawn_receiver_thread( + name: &str, + socket: UdpSocket, + config: NvstVideoConfig, + media_consumer: MediaConsumer, + event_sender: Sender, + rtc: Option, +) -> Result { + socket + .set_read_timeout(Some(UDP_RECEIVE_POLL_INTERVAL)) + .map_err(NvstUdpReceiverError::Configure)?; + let (commands, receiver) = mpsc::channel(); + let transport_origin = Instant::now(); + let join = thread::Builder::new() + .name(name.to_owned()) + .spawn(move || { + run_nvst_udp_receiver( + socket, + config, + receiver, + media_consumer, + event_sender, + transport_origin, + rtc, + ) + }) + .map_err(NvstUdpReceiverError::Spawn)?; + Ok(NvstUdpReceiverSession { + commands, + join: Some(join), + }) +} + +fn prepare_nvst_webrtc_bundle( + socket: &UdpSocket, + config: &NvstVideoConfig, + mut rtc: Rtc, +) -> Result { + let fingerprint = config.remote_dtls_fingerprint().ok_or_else(|| { + NvstUdpReceiverError::WebrtcBundle("missing remote DTLS fingerprint".into()) + })?; + let remote_fingerprint = + parse_nvst_fingerprint(fingerprint).map_err(NvstUdpReceiverError::WebrtcBundle)?; + let credentials = config.stun_credentials.as_ref().ok_or_else(|| { + NvstUdpReceiverError::WebrtcBundle( + "DTLS bundle requires local and remote ICE credentials".into(), + ) + })?; + let local_addr = socket + .local_addr() + .map_err(NvstUdpReceiverError::Configure)?; + let local_candidate = + Candidate::host(routed_host_addr(Some(config.video_peer), local_addr), "udp") + .map_err(|error| NvstUdpReceiverError::WebrtcBundle(error.to_string()))?; + let remote_candidate = Candidate::host(config.video_peer, "udp") + .map_err(|error| NvstUdpReceiverError::WebrtcBundle(error.to_string()))?; + rtc.add_local_candidate(local_candidate); + rtc.add_remote_candidate(remote_candidate); + { + let mut api = rtc.direct_api(); + api.set_ice_controlling(true); + api.set_local_ice_credentials(IceCreds { + ufrag: credentials.local_username_fragment.clone(), + pass: credentials.local_password.clone(), + }); + api.set_remote_ice_credentials(IceCreds { + ufrag: credentials.remote_username_fragment.clone(), + pass: credentials.remote_password.clone(), + }); + api.set_remote_fingerprint(remote_fingerprint); + api.declare_media(Mid::from("0"), MediaKind::Video); + api.start_dtls(true) + .map_err(|error| NvstUdpReceiverError::WebrtcBundle(error.to_string()))?; + } + eprintln!( + "NVST WebRTC bundle armed (local={}, peer={}, remoteFingerprintBytes={})", + routed_host_addr(Some(config.video_peer), local_addr), + config.video_peer, + fingerprint.len() + ); + Ok(rtc) +} + +fn looks_like_rtp(datagram: &[u8]) -> bool { + datagram.len() >= RTP_FIXED_HEADER_LEN + && datagram[0] >> 6 == 2 + && !looks_like_stun(datagram) + && !looks_like_dtls(datagram) +} + +fn looks_like_stun(datagram: &[u8]) -> bool { + datagram.len() >= STUN_HEADER_LEN && datagram[4..8] == STUN_MAGIC_COOKIE.to_be_bytes() +} + +#[cfg_attr(not(test), allow(dead_code))] +fn synthesize_ice_binding_success( + transaction_id: &[u8; 12], + mapped: SocketAddr, + remote_password: &str, +) -> Vec { + build_authenticated_stun_packet( + STUN_BINDING_SUCCESS_RESPONSE, + transaction_id, + remote_password.as_bytes(), + &[( + STUN_ATTR_XOR_MAPPED_ADDRESS, + xor_mapped_address(mapped, transaction_id), + )], + ) +} + +fn looks_like_dtls(datagram: &[u8]) -> bool { + matches!(datagram.first().copied(), Some(20..=63)) +} + +fn peek_rtp_ssrc(datagram: &[u8]) -> Option { + looks_like_rtp(datagram) + .then(|| u32::from_be_bytes([datagram[8], datagram[9], datagram[10], datagram[11]])) +} + +fn run_nvst_webrtc_bundle( + socket: UdpSocket, + config: NvstVideoConfig, + commands: Receiver, + media_consumer: MediaConsumer, + event_sender: Sender, + transport_origin: Instant, + mut rtc: Rtc, +) { + let video_peer = config.video_peer; + let stun_credentials = config.stun_credentials.clone(); + let ping_payload = config.ping_payload.clone(); + // Feedback plane shared with the Mjolnir video receiver: it publishes the + // stream SSRC/sequence and keyframe requests; this bundle sends the RTCP + // Receiver Reports / PLI over the `rtcp1` SCTP data channel. + let feedback = config.feedback(); + // With a dedicated Mjolnir video socket the bundle only carries + // control/audio keepalive traffic; the Mjolnir receiver owns the media + // timeout, so the bundle must not raise a spurious media recovery. + let owns_media_timeout = config.mjolnir_udp_port.is_none(); + let receive_destination = socket.local_addr().ok().map_or_else( + || video_peer, + |local| routed_host_addr(Some(video_peer), local), + ); + let mut receiver = NvstVideoReceiver::new(config); + let mut datagram = vec![0_u8; 65_536]; + let mut inbound_datagrams = 0_u64; + let mut outbound_datagrams = 0_u64; + let mut hole_punch_pings = 0_u64; + let mut last_hole_punch = Instant::now() - PING_INTERVAL_BEFORE_CONNECTION; + let mut seen_ssrcs = HashSet::new(); + let mut dtls_ready = false; + // RTCP-over-SCTP (`rtcp1`) feedback channel state. + let mut sctp_started = false; + let mut rtcp_channel: Option = None; + let mut rtcp_channel_open = false; + let mut rtcp_sender_ssrc = 0x4f4e_4f57_u32; // "ONOW" + rtcp_sender_ssrc ^= (transport_origin.elapsed().subsec_nanos()) & 0xffff; + let mut last_rtcp_send = Instant::now() - SRTCP_RR_INTERVAL; + let mut rtcp_reports_sent = 0_u64; + // The official client opens `rtcp1` once the RTSP session is established, + // not cold during the SCTP handshake (which gets the stream reset). Defer + // creation to shortly after the handshake. Do NOT gate on video flowing: + // the server may wait for rtcp1 before sending the keyframe, so gating on + // video would deadlock. + let mut rtcp_create_attempted = false; + let mut sctp_started_at: Option = None; + loop { + loop { + match commands.try_recv() { + Ok(UdpReceiverCommand::Pause) => forward_optional(&event_sender, receiver.pause()), + Ok(UdpReceiverCommand::Resume) => { + forward_optional(&event_sender, receiver.resume()) + } + Ok(UdpReceiverCommand::Recover) => { + forward_optional(&event_sender, receiver.recover()) + } + Ok(UdpReceiverCommand::Stop) | Err(TryRecvError::Disconnected) => { + rtc.disconnect(); + forward_optional(&event_sender, receiver.stop()); + return; + } + Err(TryRecvError::Empty) => break, + } + } + + // Official first burst is three ICE Binding Requests, plus NATT + // ping-string PING. After DTLS they keep pinging at 100ms. + let now = Instant::now(); + let ping_interval = if dtls_ready { + PING_INTERVAL_AFTER_CONNECTION + } else { + PING_INTERVAL_BEFORE_CONNECTION + }; + if now.duration_since(last_hole_punch) >= ping_interval + && let Some(credentials) = stun_credentials.as_ref() + { + let mut ice_bytes = 0_usize; + if !dtls_ready { + for _ in 0..3 { + let mut ice_tid = [0_u8; 12]; + if getrandom::fill(&mut ice_tid).is_ok() { + let ice = build_stun_binding_request(credentials, &ice_tid); + ice_bytes = ice.len(); + let _ = socket.send_to(&ice, video_peer); + } + } + } + let mut natt_tid = [0_u8; 12]; + let natt = if getrandom::fill(&mut natt_tid).is_ok() { + let natt = build_natt_hole_punch_request( + &credentials.local_username_fragment, + &ping_payload, + &credentials.remote_password, + &natt_tid, + ); + let _ = socket.send_to(&natt, video_peer); + Some(natt) + } else { + None + }; + hole_punch_pings += 1; + if hole_punch_pings == 1 || hole_punch_pings % 50 == 0 { + eprintln!( + "NVST hole-punch ping={hole_punch_pings} dest={video_peer} iceBurst={} iceBytes={ice_bytes} iceUsername={}:{} nattBytes={} nattUsername={}:{}", + if dtls_ready { 0 } else { 3 }, + credentials.remote_username_fragment, + credentials.local_username_fragment, + natt.as_ref().map_or(0, Vec::len), + String::from_utf8_lossy(&ping_payload), + credentials.local_username_fragment + ); + } + last_hole_punch = now; + } + + // Open the rtcp1 channel ~1s after the SCTP handshake, once the RTSP + // session has had time to establish on the server. + if sctp_started + && !rtcp_create_attempted + && rtcp_channel.is_none() + && sctp_started_at.is_some_and(|t| now.duration_since(t) >= Duration::from_secs(1)) + { + rtcp_create_attempted = true; + // The official client's RTCP feedback channel is labelled + // `rtcp_on_sctp_private` (not `rtcp1`). The server resets a DCEP open + // whose label it doesn't recognize — which is why `rtcp1` was reset on + // every stream id. Open candidates across the low even stream ids (client + // parity) with the correct label; the server ACKs one with ChannelOpen and + // resets the rest, and we adopt whichever opens. + for _ in 0..RTCP_STREAM_CANDIDATES { + let config = ChannelConfig { + label: "rtcp_on_sctp_private".to_string(), + negotiated: None, + reliability: Reliability::Reliable, + ordered: true, + protocol: String::new(), + }; + let _ = rtc.direct_api().create_data_channel(config); + } + eprintln!("NVST creating rtcp_on_sctp_private across {RTCP_STREAM_CANDIDATES} stream-id candidates"); + } + + // Send RTCP feedback over the rtcp1 SCTP channel once it is open and the + // Mjolnir receiver has bound the video stream. A Receiver Report goes out + // every second; a PLI goes out whenever the receiver flags it needs a + // keyframe (rate-limited to the same cadence). + if rtcp_channel_open + && now.duration_since(last_rtcp_send) >= SRTCP_RR_INTERVAL + && let Some(channel_id) = rtcp_channel + && let Some((media_ssrc, highest_sequence)) = feedback.stream_snapshot() + { + let mut channel = rtc.channel(channel_id); + if let Some(channel) = channel.as_mut() { + let report = + build_rtcp_receiver_report(rtcp_sender_ssrc, media_ssrc, highest_sequence); + if channel.write(true, &report).unwrap_or(false) { + rtcp_reports_sent += 1; + if rtcp_reports_sent == 1 || rtcp_reports_sent % 10 == 0 { + eprintln!( + "NVST rtcp1 RR sent={rtcp_reports_sent} mediaSsrc={media_ssrc} highestSeq={highest_sequence}" + ); + } + } + if feedback.take_keyframe_request() { + let pli = build_rtcp_pli(rtcp_sender_ssrc, media_ssrc); + if channel.write(true, &pli).unwrap_or(false) { + eprintln!("NVST rtcp1 PLI sent for mediaSsrc={media_ssrc}"); + } + } + } + last_rtcp_send = now; + } + + let timeout = loop { + match rtc.poll_output() { + Ok(Output::Timeout(timeout)) => break timeout, + Ok(Output::Transmit(transmit)) => { + outbound_datagrams += 1; + let kind = if looks_like_stun(&transmit.contents) { + "stun" + } else if looks_like_dtls(&transmit.contents) { + "dtls" + } else { + "other" + }; + if outbound_datagrams <= 8 || outbound_datagrams % 50 == 0 { + eprintln!( + "NVST WebRTC outbound={outbound_datagrams} kind={kind} dest={} bytes={}", + transmit.destination, + transmit.contents.len() + ); + } + let _ = socket.send_to(&transmit.contents, transmit.destination); + // Official ICE-on WebRtcTransport skips setupDtls until a real + // inbound STUN. Do not synthesize Binding Success — that only + // unblocks str0m and sends ClientHello before GFN has a pair. + } + Ok(Output::Event(event)) => match event { + Event::IceConnectionStateChange(state) => { + eprintln!("NVST ICE state: {state:?}"); + // Official GFN treats hole-punch / ICE receive failure as + // non-fatal. Media is gated on DTLS, not ICE success. + } + Event::Connected => { + dtls_ready = true; + eprintln!("NVST DTLS handshake complete; waiting for SRTP/Mjolnir"); + // Bring up SCTP over the established DTLS transport. The server + // opens the `rtcp1` channel itself (matching the official client, + // which receives server-created channels); we just listen for it. + if !sctp_started { + sctp_started = true; + sctp_started_at = Some(Instant::now()); + rtc.direct_api().start_sctp(true); + eprintln!("NVST SCTP started; will open rtcp1 after handshake settles"); + } + } + Event::ChannelOpen(id, label) => { + eprintln!("NVST data channel open: id={id:?} label={label}"); + if label.contains("rtcp") { + rtcp_channel = Some(id); + rtcp_channel_open = true; + // Ask for a keyframe immediately so the decoder can start. + feedback.request_keyframe(); + } + } + Event::ChannelData(data) => { + // The server may send Sender Reports / control on rtcp1; log to + // learn the exact on-wire format it expects back. + eprintln!( + "NVST rtcp1 inbound: id={:?} binary={} bytes={} data={:02x?}", + data.id, + data.binary, + data.data.len(), + &data.data[..data.data.len().min(16)] + ); + } + Event::RtpPacket(packet) => { + for event in receiver.process_mjolnir_payload( + *packet.header.ssrc, + packet.header.timestamp, + &packet.payload, + Instant::now(), + ) { + if !forward_receive_event( + &media_consumer, + &event_sender, + transport_origin, + event, + ) { + rtc.disconnect(); + forward_optional(&event_sender, receiver.stop()); + return; + } + } + } + _ => {} + }, + Err(error) => { + eprintln!("NVST WebRTC bundle failed: {error}"); + forward_optional(&event_sender, receiver.stop()); + return; + } + } + }; + + let wait = timeout + .saturating_duration_since(Instant::now()) + .min(UDP_RECEIVE_POLL_INTERVAL); + if wait.is_zero() { + let _ = rtc.handle_input(Input::Timeout(Instant::now())); + } else { + let _ = socket.set_read_timeout(Some(wait)); + match socket.recv_from(&mut datagram) { + Ok((length, source)) => { + inbound_datagrams += 1; + if inbound_datagrams == 1 || inbound_datagrams % 50 == 0 { + eprintln!( + "NVST WebRTC inbound={inbound_datagrams} source={source} bytes={length} dtlsReady={dtls_ready}" + ); + } + if datagram[..length] == *b"PING" { + let _ = socket.send_to(b"PONG", source); + continue; + } + if let Some(ssrc) = peek_rtp_ssrc(&datagram[..length]) + && seen_ssrcs.insert(ssrc) + { + rtc.direct_api().expect_stream_rx( + Ssrc::from(ssrc), + None, + Mid::from("0"), + None, + ); + eprintln!("NVST expecting SSRC {ssrc} on bundle mid=0"); + } + let destination = receive_destination; + let contents = match datagram[..length].try_into() { + Ok(value) => value, + Err(error) => { + eprintln!("NVST dropping oversized UDP packet: {error}"); + continue; + } + }; + if let Err(error) = rtc.handle_input(Input::Receive( + Instant::now(), + Receive { + proto: RtcProtocol::Udp, + source, + destination, + contents, + }, + )) { + eprintln!("NVST WebRTC handle_input failed: {error}"); + forward_optional(&event_sender, receiver.stop()); + return; + } + } + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + let _ = rtc.handle_input(Input::Timeout(Instant::now())); + } + Err(_) => { + forward_optional(&event_sender, receiver.stop()); + return; + } + } + } + + let timeout = if owns_media_timeout { + receiver.poll_timeout(Instant::now()) + } else { + None + }; + if timeout.is_some() { + eprintln!( + "NVST WebRTC timeout counters: inbound={inbound_datagrams}, dtlsReady={dtls_ready}" + ); + } + forward_optional(&event_sender, timeout); + } +} + +fn run_nvst_udp_receiver( + socket: UdpSocket, + config: NvstVideoConfig, + commands: Receiver, + media_consumer: MediaConsumer, + event_sender: Sender, + transport_origin: Instant, + rtc: Option, +) { + if let Some(rtc) = rtc { + run_nvst_webrtc_bundle( + socket, + config, + commands, + media_consumer, + event_sender, + transport_origin, + rtc, + ); + return; + } + let mut receiver = NvstVideoReceiver::new(config); + let stun_credentials = receiver.config.stun_credentials.clone(); + let mut datagram = vec![0_u8; 65_536]; + let mut peer_seen = false; + let mut last_ping = Instant::now() - PING_INTERVAL_BEFORE_CONNECTION; + let mut pings_sent = 0_u64; + let mut inbound_datagrams = 0_u64; + let mut handled_stun = 0_u64; + let mut invalid_stun = 0_u64; + let mut non_stun = 0_u64; + let mut wrong_source = 0_u64; + let mut receiver_reports_sent = 0_u64; + let stats_origin = Instant::now(); + let mut last_stats_log = Instant::now(); + loop { + loop { + match commands.try_recv() { + Ok(UdpReceiverCommand::Pause) => forward_optional(&event_sender, receiver.pause()), + Ok(UdpReceiverCommand::Resume) => { + forward_optional(&event_sender, receiver.resume()) + } + Ok(UdpReceiverCommand::Recover) => { + forward_optional(&event_sender, receiver.recover()) + } + Ok(UdpReceiverCommand::Stop) | Err(TryRecvError::Disconnected) => { + forward_optional(&event_sender, receiver.stop()); + return; + } + Err(TryRecvError::Empty) => break, + } + } + + let now = Instant::now(); + let ping_interval = if peer_seen { + PING_INTERVAL_AFTER_CONNECTION + } else { + PING_INTERVAL_BEFORE_CONNECTION + }; + if now.duration_since(last_ping) >= ping_interval { + if let Some(credentials) = stun_credentials.as_ref() { + let mut transaction_id = [0_u8; 12]; + if getrandom::fill(&mut transaction_id).is_ok() { + let ping = build_natt_hole_punch_request( + &credentials.local_username_fragment, + &receiver.config.ping_payload, + &credentials.remote_password, + &transaction_id, + ); + let _ = socket.send_to(&ping, receiver.config.video_peer); + pings_sent += 1; + } + } else { + let _ = socket.send_to(&receiver.config.ping_payload, receiver.config.video_peer); + pings_sent += 1; + } + last_ping = now; + } + + match socket.recv_from(&mut datagram) { + Ok((length, source)) => { + inbound_datagrams += 1; + if inbound_datagrams == 1 { + eprintln!( + "NVST raw-SRTP inbound first datagram: source={source} bytes={length} peer={}", + receiver.config.video_peer + ); + } + if source != receiver.config.video_peer { + wrong_source += 1; + } + if source == receiver.config.video_peer + && let Some(credentials) = stun_credentials.as_ref() + { + match handle_stun_datagram(&datagram[..length], source, credentials) { + StunDatagram::Handled(response) => { + handled_stun += 1; + peer_seen = true; + if let Some(response) = response { + let _ = socket.send_to(&response, source); + } + continue; + } + StunDatagram::Invalid => { + invalid_stun += 1; + continue; + } + StunDatagram::NotStun => non_stun += 1, + } + } + peer_seen |= source == receiver.config.video_peer; + for event in receiver.process_datagram(source, &datagram[..length], Instant::now()) + { + if !forward_receive_event( + &media_consumer, + &event_sender, + transport_origin, + event, + ) { + forward_optional(&event_sender, receiver.stop()); + return; + } + } + } + Err(error) if error.kind() == std::io::ErrorKind::TimedOut => {} + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {} + Err(_) => { + forward_optional(&event_sender, receiver.stop()); + return; + } + } + let now = Instant::now(); + if let Some(report) = receiver.poll_receiver_report(now) { + if socket.send_to(&report, receiver.config.video_peer).is_ok() { + receiver_reports_sent += 1; + } + } + if now.duration_since(last_stats_log) >= Duration::from_secs(2) { + last_stats_log = now; + eprintln!( + "NVST rx-stats {} inbound={inbound_datagrams} pings={pings_sent} rr={receiver_reports_sent}", + receiver.stats_line(stats_origin), + ); + } + let timeout = receiver.poll_timeout(now); + if timeout.is_some() { + eprintln!( + "NVST transport timeout counters: pings={pings_sent}, inbound={inbound_datagrams}, stunHandled={handled_stun}, stunInvalid={invalid_stun}, nonStun={non_stun}, wrongSource={wrong_source}" + ); + } + forward_optional(&event_sender, timeout); + } +} + +fn forward_optional(sender: &Sender, event: Option) { + if let Some(event) = event { + let _ = sender.send(event); + } +} + +fn forward_receive_event( + media_consumer: &MediaConsumer, + event_sender: &Sender, + transport_origin: Instant, + event: NvstReceiveEvent, +) -> bool { + if let NvstReceiveEvent::Frame(frame) = event { + let media_frame = EncodedMediaFrame { + mid: "nvst-video-0".to_owned(), + codec: "H264".to_owned(), + payload: Arc::from(frame.bytes), + rtp_timestamp: u64::from(frame.timestamp), + clock_rate_hz: 90_000, + received_at_us: transport_origin + .elapsed() + .as_micros() + .try_into() + .unwrap_or(u64::MAX), + keyframe: frame.keyframe, + contiguous: true, + }; + let result = match deliver_media_frame(media_consumer, media_frame) { + Ok(()) => return true, + Err(TransportError::MediaConsumerBackpressured) => { + NvstDropReason::MediaConsumerBackpressured + } + Err(TransportError::MediaConsumerClosed) => NvstDropReason::MediaConsumerClosed, + Err(_) => NvstDropReason::MediaConsumerClosed, + }; + let _ = event_sender.send(NvstReceiveEvent::Dropped(result)); + return false; + } + let _ = event_sender.send(event); + true +} + +/// There is no safe NVST NACK wire encoder in the current handoff. Expose this explicit answer +/// rather than emitting an invented control datagram. +pub fn nack_transmission_support() -> Result<(), NvstUnsupportedFeature> { + Err(NvstUnsupportedFeature::Nack) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + const TEST_KEY: &str = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"; + const TEST_SALT: &str = "000102030405060708090A0B0C0D"; + const TEST_PEER: &str = "192.0.2.20"; + + fn legacy_handoff() -> Value { + json!({ + "clientUdpPort": 49005, + "videoPeerIp": TEST_PEER, + "videoPeerPort": 5004, + "srtpAesKeyHex": TEST_KEY, + "srtpSaltHex": "00000000000000009ECA935E", + "codec": "H264", + "rtpPayloadType": 96, + "rtpSsrc": 0x11223344u32, + "reorderWindowPackets": 4, + "maxAccessUnitBytes": 4096, + "timeoutMs": 500 + }) + } + + fn config() -> NvstVideoConfig { + NvstVideoConfig::from_legacy_handoff(&legacy_handoff(), None).expect("valid config") + } + + fn peer() -> SocketAddr { + SocketAddr::new(TEST_PEER.parse().expect("test IP"), 5004) + } + + fn stun_credentials() -> NvstStunCredentials { + NvstStunCredentials { + local_username_fragment: "loc1".to_owned(), + local_password: "local-password-value-01".to_owned(), + remote_username_fragment: "remote01".to_owned(), + remote_password: "remote-password-with-36-byte-value-001".to_owned(), + } + } + + fn build_plaintext_rtp(sequence: u16, flags: u8, frame_index: u32, media: &[u8]) -> Vec { + let mut packet = vec![0x90, 0xe0]; + packet.extend_from_slice(&sequence.to_be_bytes()); + packet.extend_from_slice(&0x01020304u32.to_be_bytes()); + packet.extend_from_slice(&0x11223344u32.to_be_bytes()); + packet.extend_from_slice(&GS_VIDEO_EXTENSION_PROFILE.to_be_bytes()); + packet.extend_from_slice(&4_u16.to_be_bytes()); + packet.extend_from_slice(&(u32::from(sequence) << 8).to_le_bytes()); + packet.extend_from_slice(&frame_index.to_le_bytes()); + packet.push(flags); + packet.extend_from_slice(&[0, 0, 0]); + packet.extend_from_slice(&[0; 4]); + packet.extend_from_slice(media); + packet + } + + fn test_srtp(config: &NvstVideoConfig) -> SrtpReceiver { + SrtpReceiver::from_material(&config.srtp) + } + + fn protect_for_test(crypto: &SrtpReceiver, mut packet: Vec, roc: u32) -> Vec { + let header = RtpHeader::parse(&packet).expect("RTP header"); + let packet_index = (u64::from(roc) << 16) | u64::from(header.sequence_number); + match &crypto.cipher { + SrtpCipher::AeadAes128Gcm { + encryption_key, + session_salt, + authentication_tag_len, + } => { + let iv = srtp_gcm_iv(*session_salt, header.ssrc, roc, header.sequence_number); + protect_aes_gcm( + &mut packet, + header.payload_offset, + encryption_key, + &iv, + *authentication_tag_len, + ); + } + SrtpCipher::AeadAes256Gcm { + encryption_key, + session_salt, + authentication_tag_len, + } => { + let iv = srtp_gcm_iv(*session_salt, header.ssrc, roc, header.sequence_number); + protect_aes_gcm( + &mut packet, + header.payload_offset, + encryption_key, + &iv, + *authentication_tag_len, + ); + } + SrtpCipher::AesCm128HmacSha1 { + encryption_key, + authentication_key, + session_salt, + authentication_tag_len, + } => { + let iv = srtp_aes_cm_iv(session_salt, header.ssrc, packet_index); + let mut cipher = Aes128Ctr::new(encryption_key.into(), (&iv).into()); + cipher.apply_keystream(&mut packet[header.payload_offset..]); + let mut mac = HmacSha1::new_from_slice(authentication_key).expect("fixed key"); + mac.update(&packet); + mac.update(&roc.to_be_bytes()); + packet.extend_from_slice(&mac.finalize().into_bytes()[..*authentication_tag_len]); + } + SrtpCipher::AesCm256HmacSha1 { + encryption_key, + authentication_key, + session_salt, + authentication_tag_len, + } => { + let iv = srtp_aes_cm_iv(session_salt, header.ssrc, packet_index); + let mut cipher = Aes256Ctr::new(encryption_key.into(), (&iv).into()); + cipher.apply_keystream(&mut packet[header.payload_offset..]); + let mut mac = HmacSha1::new_from_slice(authentication_key).expect("fixed key"); + mac.update(&packet); + mac.update(&roc.to_be_bytes()); + packet.extend_from_slice(&mac.finalize().into_bytes()[..*authentication_tag_len]); + } + } + packet + } + + #[test] + fn decrypt_captured_official_packet() { + // Diagnostic: decrypt a real captured official GFN video packet with the + // session's master key/salt, comparing the KDF-derived session key (the + // current SrtpReceiver path) against using the master key/salt directly. + let master_key = decode_fixed_hex::<32>( + "D3CB0D52DC2D9CFFE439CB69DBDCEB8725D0F5230145D92D360F17505B7F0520", + NvstConfigError::InvalidAesKey, + ) + .expect("key"); + let master_salt = decode_fixed_hex::<12>( + "0000000000000000D4818BDE", + NvstConfigError::InvalidSrtpSalt, + ) + .expect("salt"); + + // Diagnostic harness for live captures; skip when no dump was captured this run. + let dump = match std::fs::read("/tmp/opennow-video-dump.bin") { + Ok(dump) => dump, + Err(_) => { + eprintln!("no /tmp/opennow-video-dump.bin capture; skipping"); + return; + } + }; + assert!(dump.len() > 12 && &dump[0..4] == b"NVST", "dump has a packet"); + let pkt_len = u16::from_be_bytes([dump[10], dump[11]]) as usize; + let packet = &dump[12..12 + pkt_len]; + let header = RtpHeader::parse(packet).expect("header"); + eprintln!( + "packet: len={} pt={} seq={} ssrc={:08x} payload_offset={}", + packet.len(), + header.payload_type, + header.sequence_number, + header.ssrc, + header.payload_offset + ); + + // Path A: KDF-derived session key (current SrtpReceiver code). + let material = NvstSrtpMaterial::AeadAes256Gcm { + master_key, + master_salt, + authentication_tag_len: SRTP_AEAD_AES_GCM_8_TAG_LEN, + }; + let mut receiver = SrtpReceiver::from_material(&material); + match receiver.unprotect(packet) { + Ok(p) => eprintln!( + "KDF-path DECRYPT OK: plaintext={} payload[0..16]={:02x?}", + p.plaintext.len(), + &p.plaintext[header.payload_offset..header.payload_offset + 16] + ), + Err(e) => eprintln!("KDF-path DECRYPT FAIL: {:?}", e), + } + + // Path B: master key/salt used directly as the session key/salt (no KDF). + match unprotect_aes_gcm( + packet, + header, + 0, + &master_key, + &master_salt, + SRTP_AEAD_AES_GCM_8_TAG_LEN, + ) { + Ok(pt) => eprintln!( + "NO-KDF-path DECRYPT OK: payload[0..16]={:02x?}", + &pt[header.payload_offset..header.payload_offset + 16] + ), + Err(error) => eprintln!("NO-KDF-path DECRYPT FAIL: {error:?}"), + } + } + + #[test] + fn legacy_schema_defaults_to_aes_256_gcm_8_with_explicit_salt() { + let config = config(); + assert_eq!(config.video_peer(), peer()); + assert_eq!(config.srtp_profile(), NvstSrtpProfile::AeadAes256Gcm8); + let NvstSrtpMaterial::AeadAes256Gcm { master_salt, .. } = config.srtp else { + panic!("legacy handoff must choose AES-256-GCM"); + }; + assert_eq!( + master_salt, + decode_fixed_hex::<12>("00000000000000009ECA935E", NvstConfigError::InvalidSrtpSalt) + .expect("salt"), + ); + } + + #[test] + fn ping_version_six_requires_all_ice_credentials() { + let mut handoff = legacy_handoff(); + handoff["pingVersion"] = json!(6); + assert!(matches!( + NvstVideoConfig::from_legacy_handoff(&handoff, None), + Err(NvstConfigError::MissingField("localIceUsernameFragment")) + )); + + handoff["localIceUsernameFragment"] = json!("loc1"); + handoff["localIcePassword"] = json!("local-password-value-01"); + handoff["remoteIceUsernameFragment"] = json!("remote01"); + handoff["remoteIcePassword"] = json!("remote-password-with-36-byte-value-001"); + let config = NvstVideoConfig::from_legacy_handoff(&handoff, None) + .expect("complete version-six credentials"); + assert_eq!(config.ping_version, Some(6)); + assert!(config.stun_credentials.is_some()); + assert!(!format!("{config:?}").contains("local-password-value-01")); + } + + #[test] + fn reserved_bundle_socket_binds_unspecified_ipv4() { + let socket = reserve_nvst_udp_socket().expect("reserve"); + let addr = socket.local_addr().expect("local"); + assert!( + addr.ip().is_unspecified(), + "official binds 0.0.0.0, advertised NIC IPv4 is separate" + ); + assert_ne!(addr.port(), 0); + } + + #[test] + fn natt_hole_punch_uses_setup_ping_payload_not_v2_ufrag() { + let credentials = stun_credentials(); + let packet = build_natt_hole_punch_request( + &credentials.local_username_fragment, + b"srv1", + &credentials.remote_password, + &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + ); + let (_, username) = + find_stun_attribute(&packet, STUN_ATTR_USERNAME).expect("USERNAME"); + assert_eq!(username, b"srv1:loc1"); + assert_ne!( + username, + format!( + "{}:{}", + credentials.remote_username_fragment, credentials.local_username_fragment + ) + .as_bytes() + ); + assert!(valid_stun_fingerprint(&packet)); + assert!(valid_stun_message_integrity( + &packet, + credentials.remote_password.as_bytes() + )); + } + + #[test] + fn version_six_binding_request_matches_known_answer() { + let packet = build_stun_binding_request( + &stun_credentials(), + &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + ); + assert_eq!( + packet, + hex_bytes( + "000100342112A442000102030405060708090A0B0006000D72656D6F746530313A6C6F633100000000080014B276DC1C7949494C7EF7EB226BE8BB5E0EE5AABD802800045A8349EF" + ), + ); + assert!(valid_stun_fingerprint(&packet)); + assert!(valid_stun_message_integrity( + &packet, + b"remote-password-with-36-byte-value-001" + )); + } + + #[test] + fn version_six_validates_requests_and_builds_authenticated_responses() { + let credentials = stun_credentials(); + let transaction_id = [0x11; 12]; + let request = build_authenticated_stun_packet( + STUN_BINDING_REQUEST, + &transaction_id, + credentials.local_password.as_bytes(), + &[(STUN_ATTR_USERNAME, b"loc1:remote01".to_vec())], + ); + let source: SocketAddr = "192.0.2.20:5004".parse().expect("source"); + let StunDatagram::Handled(Some(response)) = + handle_stun_datagram(&request, source, &credentials) + else { + panic!("authenticated binding request must produce a response"); + }; + assert_eq!( + u16::from_be_bytes([response[0], response[1]]), + STUN_BINDING_SUCCESS_RESPONSE + ); + assert_eq!(&response[8..20], &transaction_id); + assert!(valid_stun_fingerprint(&response)); + assert!(valid_stun_message_integrity( + &response, + credentials.local_password.as_bytes() + )); + let (_, mapped) = find_stun_attribute(&response, STUN_ATTR_XOR_MAPPED_ADDRESS) + .expect("XOR-MAPPED-ADDRESS"); + assert_eq!(mapped[1], 1); + assert_eq!( + u16::from_be_bytes([mapped[2], mapped[3]]) ^ ((STUN_MAGIC_COOKIE >> 16) as u16), + 5004 + ); + + let mut tampered = request; + tampered[24] ^= 1; + assert!(matches!( + handle_stun_datagram(&tampered, source, &credentials), + StunDatagram::Invalid + )); + } + + #[test] + fn gfn_local_ice_credentials_match_official_lengths() { + let creds = generate_gfn_local_ice_credentials(); + assert_eq!(creds.ufrag.len(), 4); + assert_eq!(creds.pass.len(), 22); + } + + #[test] + fn synthesized_ice_success_matches_the_request_transaction() { + let credentials = stun_credentials(); + let transaction_id = [7_u8; 12]; + let mapped = "192.168.1.104:54454".parse().expect("mapped"); + let response = + synthesize_ice_binding_success(&transaction_id, mapped, &credentials.remote_password); + assert_eq!( + u16::from_be_bytes([response[0], response[1]]), + STUN_BINDING_SUCCESS_RESPONSE + ); + assert_eq!(&response[8..20], &transaction_id); + assert!(valid_stun_fingerprint(&response)); + assert!(valid_stun_message_integrity( + &response, + credentials.remote_password.as_bytes() + )); + } + + #[test] + fn legacy_gcm_key_derivation_matches_known_answer() { + let config = config(); + let receiver = test_srtp(&config); + let SrtpCipher::AeadAes256Gcm { + encryption_key, + session_salt, + .. + } = receiver.cipher + else { + panic!("legacy handoff must derive an AES-256-GCM session"); + }; + assert_eq!( + encryption_key, + decode_fixed_hex::<32>( + "0E44A0B0E7F1BDBB298CBEE52C9F8AC1C37726768C946F59BDAAA099608CBF66", + NvstConfigError::InvalidAesKey, + ) + .expect("key"), + ); + assert_eq!( + session_salt, + decode_fixed_hex::<12>("78B9D97B7FFB37CAD539A29D", NvstConfigError::InvalidSrtpSalt) + .expect("salt"), + ); + } + + #[test] + fn selector_prefers_valid_legacy_nvst_and_falls_back_for_h265() { + let context = json!({ "nvstVideo": legacy_handoff() }); + assert!(matches!( + select_preferred_video_transport(&context), + PreferredVideoTransport::Nvst(_) + )); + assert!(matches!( + select_preferred_video_transport(&json!({ "nvstTransport": { "tracks": [] } })), + PreferredVideoTransport::WebRtcFallback(NvstFallbackReason::InvalidNvstHandoff( + NvstConfigError::RichHandoffUnsupported + )) + )); + + let mut missing_gcm_salt = legacy_handoff(); + missing_gcm_salt + .as_object_mut() + .expect("handoff object") + .remove("srtpSaltHex"); + assert!(matches!( + NvstVideoConfig::from_legacy_handoff(&missing_gcm_salt, None), + Err(NvstConfigError::MissingField("srtpSaltHex")) + )); + + let mut invalid = legacy_handoff(); + invalid["codec"] = json!("H265"); + let context = json!({ "nvstVideo": invalid }); + assert!(matches!( + select_preferred_video_transport(&context), + PreferredVideoTransport::WebRtcFallback(NvstFallbackReason::InvalidNvstHandoff( + NvstConfigError::UnsupportedCodec(_) + )) + )); + + let mut missing_cm_salt = legacy_handoff(); + missing_cm_salt + .as_object_mut() + .expect("handoff object") + .remove("srtpSaltHex"); + missing_cm_salt["srtpProfile"] = json!("AES_CM_128_HMAC_SHA1_80"); + missing_cm_salt["srtpAesKeyHex"] = json!("000102030405060708090A0B0C0D0E0F"); + assert!(matches!( + NvstVideoConfig::from_legacy_handoff(&missing_cm_salt, None), + Err(NvstConfigError::MissingField("srtpSaltHex")) + )); + missing_cm_salt["srtpSaltHex"] = json!(TEST_SALT); + let explicit_cm = NvstVideoConfig::from_legacy_handoff(&missing_cm_salt, None) + .expect("explicit AES-CM profile"); + assert_eq!( + explicit_cm.srtp_profile(), + NvstSrtpProfile::AesCm128HmacSha1_80 + ); + + let mut gcm_128 = legacy_handoff(); + gcm_128["srtpProfile"] = json!("AEAD_AES_128_GCM"); + gcm_128["srtpAesKeyHex"] = json!("000102030405060708090A0B0C0D0E0F"); + gcm_128["srtpSaltHex"] = json!("00000000000000009ECA935E"); + assert_eq!( + NvstVideoConfig::from_legacy_handoff(&gcm_128, None) + .expect("explicit AES-128-GCM profile") + .srtp_profile(), + NvstSrtpProfile::AeadAes128Gcm + ); + + let mut cm_32 = gcm_128; + cm_32["srtpProfile"] = json!("AES_CM_128_HMAC_SHA1_32"); + cm_32["srtpSaltHex"] = json!(TEST_SALT); + assert_eq!( + NvstVideoConfig::from_legacy_handoff(&cm_32, None) + .expect("explicit AES-CM-32 profile") + .srtp_profile(), + NvstSrtpProfile::AesCm128HmacSha1_32 + ); + } + + #[test] + fn rejects_invalid_peer_and_secret_material() { + let mut handoff = legacy_handoff(); + handoff["videoPeerIp"] = json!("0.0.0.0"); + assert!(matches!( + NvstVideoConfig::from_legacy_handoff(&handoff, None), + Err(NvstConfigError::InvalidPeerIp(_)) + )); + let mut handoff = legacy_handoff(); + handoff["srtpAesKeyHex"] = json!("not-a-key"); + assert!(matches!( + NvstVideoConfig::from_legacy_handoff(&handoff, None), + Err(NvstConfigError::InvalidAesKey) + )); + + let mut aes_128 = legacy_handoff(); + aes_128["srtpProfile"] = json!("AEAD_AES_128_GCM"); + aes_128["srtpAesKeyHex"] = json!("000102030405060708090A0B0C0D0E0G"); + assert!(matches!( + NvstVideoConfig::from_legacy_handoff(&aes_128, None), + Err(NvstConfigError::InvalidAesKey) + )); + } + + #[test] + fn srtp_aes_cm_hmac_sha1_known_answer_unprotects_packet_when_explicitly_selected() { + let key = decode_fixed_hex::<16>( + "000102030405060708090A0B0C0D0E0F", + NvstConfigError::InvalidAesKey, + ) + .expect("key"); + let salt = + decode_fixed_hex::<14>(TEST_SALT, NvstConfigError::InvalidSrtpSalt).expect("salt"); + let material = NvstSrtpMaterial::AesCm128HmacSha1 { + master_key: key, + master_salt: salt, + authentication_tag_len: SRTP_AES_CM_HMAC_SHA1_80_TAG_LEN, + }; + let crypto = SrtpReceiver::from_material(&material); + // Frozen plaintext so the crypto known-answer vector stays independent of + // the RTP packet-layout helper: 12-byte header + 16-byte inline metadata + // + 6 bytes of media. + let plaintext = hex_bytes("80E01234010203041122334434120000070000000700000000000000000000016588"); + let protected = protect_for_test(&crypto, plaintext.clone(), 0); + assert_eq!( + protected, + hex_bytes( + "80E01234010203041122334408BA995FCB62EA430BE6EDEF8D4DE06268F0D6D702702082DDFADA13AE83402B" + ), + "independent AES-CTR/HMAC known-answer vector", + ); + let mut receiver = SrtpReceiver::from_material(&material); + let unprotected = receiver.unprotect(&protected).expect("authenticated SRTP"); + assert_eq!(unprotected.plaintext, plaintext); + assert_eq!(unprotected.index, 0x1234); + + let mut tampered = protected; + let last = tampered.last_mut().expect("authentication tag"); + *last ^= 0x01; + let mut receiver = SrtpReceiver::from_material(&material); + assert!(matches!( + receiver.unprotect(&tampered), + Err(NvstDropReason::AuthenticationFailed) + )); + } + + #[test] + fn srtp_aead_aes_256_gcm_rfc_known_answer_unprotects_and_rejects_tampering_and_replay() { + let session_key = + decode_fixed_hex::<32>(TEST_KEY, NvstConfigError::InvalidAesKey).expect("key"); + let session_salt = + decode_fixed_hex::<12>("517569642070726F2071756F", NvstConfigError::InvalidSrtpSalt) + .expect("salt"); + let mut receiver = SrtpReceiver { + cipher: SrtpCipher::AeadAes256Gcm { + encryption_key: session_key, + session_salt, + authentication_tag_len: SRTP_AEAD_AES_GCM_TAG_LEN, + }, + replay: ReplayWindow::default(), + }; + let protected = hex_bytes( + "8040F17B8041F8D35501A0B232B1DE78A822FE12EF9F78FA332E33AAB18012389A58E2F3B50B2A0276FFAE0F1BA63799B87B7AA3DB36DFFFD6B0F9BB7878D7A76C13", + ); + let expected = hex_bytes( + "8040F17B8041F8D35501A0B247616C6C696120657374206F6D6E69732064697669736120696E207061727465732074726573", + ); + let unprotected = receiver.unprotect(&protected).expect("RFC 7714 packet"); + assert_eq!(unprotected.plaintext, expected); + assert!(matches!( + receiver.unprotect(&protected), + Err(NvstDropReason::ReplayRejected) + )); + + let mut tampered = protected; + tampered[20] ^= 0x01; + let mut receiver = SrtpReceiver { + cipher: SrtpCipher::AeadAes256Gcm { + encryption_key: session_key, + session_salt, + authentication_tag_len: SRTP_AEAD_AES_GCM_TAG_LEN, + }, + replay: ReplayWindow::default(), + }; + assert!(matches!( + receiver.unprotect(&tampered), + Err(NvstDropReason::AuthenticationFailed) + )); + } + + #[test] + fn srtp_aead_aes_128_gcm_rfc_known_answer_unprotects_and_rejects_tampering_and_replay() { + let session_key = decode_fixed_hex::<16>( + "000102030405060708090A0B0C0D0E0F", + NvstConfigError::InvalidAesKey, + ) + .expect("key"); + let session_salt = + decode_fixed_hex::<12>("517569642070726F2071756F", NvstConfigError::InvalidSrtpSalt) + .expect("salt"); + let mut receiver = SrtpReceiver { + cipher: SrtpCipher::AeadAes128Gcm { + encryption_key: session_key, + session_salt, + authentication_tag_len: SRTP_AEAD_AES_GCM_TAG_LEN, + }, + replay: ReplayWindow::default(), + }; + let protected = hex_bytes( + "8040F17B8041F8D35501A0B2F24DE3A3FB34DE6CACBA861C9D7E4BCABE633BD50D294E6F42A5F47A51C7D19B36DE3ADF8833899D7F27BEB16A9152CF765EE4390CCE", + ); + let expected = hex_bytes( + "8040F17B8041F8D35501A0B247616C6C696120657374206F6D6E69732064697669736120696E207061727465732074726573", + ); + let unprotected = receiver.unprotect(&protected).expect("RFC 7714 packet"); + assert_eq!(unprotected.plaintext, expected); + assert!(matches!( + receiver.unprotect(&protected), + Err(NvstDropReason::ReplayRejected) + )); + + let mut tampered = protected; + tampered[20] ^= 0x01; + let mut receiver = SrtpReceiver { + cipher: SrtpCipher::AeadAes128Gcm { + encryption_key: session_key, + session_salt, + authentication_tag_len: SRTP_AEAD_AES_GCM_TAG_LEN, + }, + replay: ReplayWindow::default(), + }; + assert!(matches!( + receiver.unprotect(&tampered), + Err(NvstDropReason::AuthenticationFailed) + )); + } + + #[test] + fn parses_rtp_extensions_and_padding_after_srtp_unprotect() { + let mut packet = vec![0xb0, 0x60, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]; + packet.extend_from_slice(&[0xbe, 0xde, 0, 1, 0xaa, 0xbb, 0xcc, 0xdd]); + packet.extend_from_slice(&[1, 2, 3, 4, 2, 2]); + let header = RtpHeader::parse(&packet).expect("extension parsed"); + assert_eq!(header.payload_offset, 20); + assert_eq!( + header.payload(&packet).expect("padding removed"), + &[1, 2, 3, 4] + ); + } + + #[test] + fn parses_the_wire_stream_packet_index_as_a_24_bit_value() { + let packet = build_plaintext_rtp( + 1, + FLAG_SOF | FLAG_EOF | FLAG_CONTAINS_PIC_DATA, + 7, + &[], + ); + // Point the packet index at a 24-bit value to verify the wire shift. + let mut packet = packet; + packet[16..20].copy_from_slice(&(0x12_34_56_u32 << 8).to_le_bytes()); + + let header = RtpHeader::parse(&packet).expect("RTP header"); + let payload = header.payload(&packet).expect("payload"); + let (video, media) = NvVideoPacket::parse(&header, payload).expect("NV video header"); + assert_eq!(video.stream_packet_index, 0x12_34_56); + assert_eq!(video.frame_index, 7); + assert!(!video.is_fec); + assert!(media.is_empty()); + } + + #[test] + fn classifies_fec_packets_from_the_extension_group_coordinates() { + let mut packet = build_plaintext_rtp(3, FLAG_CONTAINS_PIC_DATA, 7, &[0xaa]); + // FecId=3, SrcPkts=3 with the FEC-group marker bits set => correction packet. + packet[28..32].copy_from_slice(&0x00c0_3420_u32.to_le_bytes()); + let header = RtpHeader::parse(&packet).expect("RTP header"); + let payload = header.payload(&packet).expect("payload"); + let (video, _) = NvVideoPacket::parse(&header, payload).expect("NV video header"); + assert!(video.is_fec); + + // Source packet: FecId=2 < SrcPkts=3. + let mut packet = build_plaintext_rtp(2, FLAG_CONTAINS_PIC_DATA, 7, &[0xaa]); + packet[28..32].copy_from_slice(&0x00c0_2420_u32.to_le_bytes()); + let header = RtpHeader::parse(&packet).expect("RTP header"); + let payload = header.payload(&packet).expect("payload"); + let (video, _) = NvVideoPacket::parse(&header, payload).expect("NV video header"); + assert!(!video.is_fec); + } + + #[test] + fn strips_the_gamestream_frame_header_before_annex_b_video() { + let header = NvVideoPacket { + stream_packet_index: 1, + frame_index: 9, + flags: FLAG_SOF | FLAG_EOF | FLAG_CONTAINS_PIC_DATA, + is_fec: false, + }; + let mut payload = vec![0x01, 0, 0, 2, 0, 0, 0, 0]; + payload.extend_from_slice(&[0, 0, 0, 1, 0x67, 0xaa, 0, 0, 1, 0x65, 0xbb]); + + let mut assembler = H264AccessUnitAssembler::new(4096); + let frame = assembler + .push(header, 90_000, &payload) + .expect("valid frame") + .expect("complete frame"); + + assert_eq!(frame.bytes, [0, 0, 0, 1, 0x67, 0xaa, 0, 0, 1, 0x65, 0xbb]); + assert!(frame.keyframe); + } + + #[test] + fn rejects_a_start_packet_without_nearby_annex_b_video() { + let header = NvVideoPacket { + stream_packet_index: 1, + frame_index: 9, + flags: FLAG_SOF | FLAG_EOF | FLAG_CONTAINS_PIC_DATA, + is_fec: false, + }; + let payload = vec![0x81; MAX_GS_FRAME_HEADER_BYTES + 8]; + let mut assembler = H264AccessUnitAssembler::new(4096); + + assert!(matches!( + assembler.push(header, 90_000, &payload), + Err(NvstDropReason::MissingAnnexBStartCode) + )); + } + + #[test] + fn receiver_reorders_authenticated_packets_and_emits_annex_b_frame() { + let config = config(); + let crypto = test_srtp(&config); + let first = protect_for_test( + &crypto, + build_plaintext_rtp( + 10, + FLAG_SOF | FLAG_CONTAINS_PIC_DATA, + 9, + &[0, 0, 0, 1, 0x65], + ), + 0, + ); + let middle = protect_for_test( + &crypto, + build_plaintext_rtp(11, FLAG_CONTAINS_PIC_DATA, 9, &[0xaa]), + 0, + ); + let last = protect_for_test( + &crypto, + build_plaintext_rtp(12, FLAG_EOF | FLAG_CONTAINS_PIC_DATA, 9, &[0xbb]), + 0, + ); + let mut receiver = NvstVideoReceiver::new(config); + assert!( + receiver + .process_datagram(peer(), &first, Instant::now()) + .is_empty() + ); + assert!( + receiver + .process_datagram(peer(), &last, Instant::now()) + .is_empty() + ); + let events = receiver.process_datagram(peer(), &middle, Instant::now()); + assert_eq!(events.len(), 1); + let NvstReceiveEvent::Frame(frame) = &events[0] else { + panic!("expected frame, got {events:?}"); + }; + assert_eq!(frame.frame_index, 9); + assert!(frame.keyframe); + assert_eq!(frame.bytes, [0, 0, 0, 1, 0x65, 0xaa, 0xbb]); + } + + #[test] + fn receiver_rejects_wrong_peer_and_duplicate_authenticated_packet() { + let config = config(); + let crypto = test_srtp(&config); + let packet = protect_for_test( + &crypto, + build_plaintext_rtp( + 1, + FLAG_SOF | FLAG_EOF | FLAG_CONTAINS_PIC_DATA, + 1, + &[0, 0, 1, 0x65], + ), + 0, + ); + let mut receiver = NvstVideoReceiver::new(config); + let wrong_peer = SocketAddr::new("192.0.2.21".parse().expect("IP"), 5004); + assert!(matches!( + receiver + .process_datagram(wrong_peer, &packet, Instant::now()) + .as_slice(), + [NvstReceiveEvent::Dropped( + NvstDropReason::UnexpectedSource { .. } + )] + )); + assert!(matches!( + receiver + .process_datagram(peer(), &packet, Instant::now()) + .as_slice(), + [NvstReceiveEvent::Frame(_)] + )); + assert!(matches!( + receiver + .process_datagram(peer(), &packet, Instant::now()) + .as_slice(), + [NvstReceiveEvent::Dropped(NvstDropReason::ReplayRejected)] + )); + } + + #[test] + fn gaps_require_recovery_and_never_emit_an_incomplete_frame() { + let config = config(); + let crypto = test_srtp(&config); + let first = protect_for_test( + &crypto, + build_plaintext_rtp(1, FLAG_SOF | FLAG_CONTAINS_PIC_DATA, 1, &[0, 0, 1, 0x65]), + 0, + ); + let far = protect_for_test( + &crypto, + build_plaintext_rtp(6, FLAG_EOF | FLAG_CONTAINS_PIC_DATA, 1, &[0xaa]), + 0, + ); + let mut receiver = NvstVideoReceiver::new(config); + let _ = receiver.process_datagram(peer(), &first, Instant::now()); + let events = receiver.process_datagram(peer(), &far, Instant::now()); + assert!(events.iter().any(|event| matches!( + event, + NvstReceiveEvent::RecoveryNeeded(NvstRecovery::PacketGap { + nack: NvstUnsupportedFeature::Nack, + .. + }) + ))); + assert!( + !events + .iter() + .any(|event| matches!(event, NvstReceiveEvent::Frame(_))) + ); + assert_eq!( + nack_transmission_support(), + Err(NvstUnsupportedFeature::Nack) + ); + } + + #[test] + fn pause_timeout_recovery_and_stop_fail_closed() { + let mut receiver = NvstVideoReceiver::new(config()); + assert_eq!( + receiver.pause(), + Some(NvstReceiveEvent::Lifecycle(NvstReceiverState::Paused)) + ); + assert!(matches!( + receiver + .process_datagram(peer(), &[], Instant::now()) + .as_slice(), + [NvstReceiveEvent::Dropped(NvstDropReason::Paused)] + )); + assert_eq!( + receiver.resume(), + Some(NvstReceiveEvent::Lifecycle(NvstReceiverState::Running)) + ); + receiver.last_authenticated_packet = Some(Instant::now() - Duration::from_secs(1)); + assert!(matches!( + receiver.poll_timeout(Instant::now()), + Some(NvstReceiveEvent::RecoveryNeeded( + NvstRecovery::Timeout { .. } + )) + )); + assert_eq!(receiver.state(), NvstReceiverState::RecoveryRequired); + assert_eq!( + receiver.recover(), + Some(NvstReceiveEvent::Lifecycle(NvstReceiverState::Running)) + ); + assert_eq!( + receiver.stop(), + Some(NvstReceiveEvent::Lifecycle(NvstReceiverState::Stopped)) + ); + assert!(matches!( + receiver + .process_datagram(peer(), &[], Instant::now()) + .as_slice(), + [NvstReceiveEvent::Dropped(NvstDropReason::Stopped)] + )); + } + + #[test] + fn startup_without_authenticated_packets_times_out() { + let mut receiver = NvstVideoReceiver::new(config()); + receiver.timeout_origin = Instant::now() - Duration::from_secs(1); + + assert!(matches!( + receiver.poll_timeout(Instant::now()), + Some(NvstReceiveEvent::RecoveryNeeded( + NvstRecovery::Timeout { .. } + )) + )); + assert_eq!(receiver.state(), NvstReceiverState::RecoveryRequired); + } + + #[test] + fn udp_receiver_repeats_the_negotiated_ping_before_media_arrives() { + let server = UdpSocket::bind("127.0.0.1:0").expect("server socket"); + server + .set_read_timeout(Some(Duration::from_millis(250))) + .expect("server timeout"); + let client_reservation = UdpSocket::bind("127.0.0.1:0").expect("client reservation"); + let client_port = client_reservation + .local_addr() + .expect("client address") + .port(); + drop(client_reservation); + + let mut config = config(); + config.client_udp_port = client_port; + config.video_peer = server.local_addr().expect("server address"); + config.ping_payload = b"negotiated-ping".to_vec(); + let (media_consumer, _media_receiver) = mpsc::sync_channel(1); + let (event_sender, _event_receiver) = mpsc::channel(); + let session = + spawn_nvst_udp_receiver(config, media_consumer, event_sender).expect("UDP receiver"); + + let mut datagram = [0_u8; 64]; + let (first_len, _) = server.recv_from(&mut datagram).expect("first ping"); + assert_eq!(&datagram[..first_len], b"negotiated-ping"); + let (second_len, _) = server.recv_from(&mut datagram).expect("repeated ping"); + assert_eq!(&datagram[..second_len], b"negotiated-ping"); + + session.stop(); + } + + #[test] + fn h264_frame_queue_is_bounded_and_prefers_current_frames() { + let mut queue = BoundedFrameQueue::new(2); + for frame_index in 1..=3 { + queue.push(EncodedH264Frame { + timestamp: frame_index, + frame_index, + first_stream_packet_index: frame_index, + keyframe: false, + bytes: vec![frame_index as u8], + }); + } + assert_eq!(queue.dropped_frames(), 1); + assert_eq!(queue.pop().expect("frame").frame_index, 2); + assert_eq!(queue.pop().expect("frame").frame_index, 3); + } + + fn hex_bytes(value: &str) -> Vec { + value + .as_bytes() + .chunks(2) + .map(|chunk| { + u8::from_str_radix(std::str::from_utf8(chunk).expect("hex"), 16).expect("hex") + }) + .collect() + } +} + +/// In-process bounded queue for callers that drive `NvstVideoReceiver` directly rather than +/// using the UDP worker. It drops the oldest frame to keep interactive latency bounded. +#[derive(Debug)] +pub struct BoundedFrameQueue { + frames: VecDeque, + capacity: usize, + dropped_frames: u64, +} + +impl BoundedFrameQueue { + pub fn new(capacity: usize) -> Self { + Self { + frames: VecDeque::with_capacity(capacity), + capacity: capacity.max(1), + dropped_frames: 0, + } + } + + pub fn push(&mut self, frame: EncodedH264Frame) { + if self.frames.len() == self.capacity { + let _ = self.frames.pop_front(); + self.dropped_frames += 1; + } + self.frames.push_back(frame); + } + + pub fn pop(&mut self) -> Option { + self.frames.pop_front() + } + + pub fn dropped_frames(&self) -> u64 { + self.dropped_frames + } +} diff --git a/native/opennow-streamer/crates/opennow-streamer/Cargo.toml b/native/opennow-streamer/crates/opennow-streamer/Cargo.toml new file mode 100644 index 000000000..6a0d1fd76 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "opennow-streamer" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +opennow-streamer-core = { path = "../opennow-streamer-core" } +opennow-streamer-platform = { path = "../opennow-streamer-platform" } +opennow-streamer-protocol = { path = "../opennow-streamer-protocol" } +serde_json.workspace = true +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/native/opennow-streamer/crates/opennow-streamer/src/main.rs b/native/opennow-streamer/crates/opennow-streamer/src/main.rs new file mode 100644 index 000000000..4567da338 --- /dev/null +++ b/native/opennow-streamer/crates/opennow-streamer/src/main.rs @@ -0,0 +1,91 @@ +#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] + +use std::io::{self, BufRead, Write}; +use std::sync::mpsc; +use std::thread; + +use opennow_streamer_core::Engine; +use opennow_streamer_platform::{MediaRuntime, create_runtime}; +use opennow_streamer_protocol::{Command, error}; +use serde_json::Value; + +fn write_message(message: &Value) -> io::Result<()> { + let mut stdout = io::stdout().lock(); + serde_json::to_writer(&mut stdout, message)?; + writeln!(stdout)?; + stdout.flush() +} + +fn run_protocol(media_runtime: MediaRuntime) -> io::Result<()> { + let (event_tx, event_rx) = mpsc::channel::(); + let event_writer = thread::spawn(move || { + while let Ok(message) = event_rx.recv() { + if write_message(&message).is_err() { + break; + } + } + }); + let mut engine = Engine::with_media_runtime(event_tx, media_runtime); + + for line in io::stdin().lock().lines() { + let line = line?; + if line.trim().is_empty() { + continue; + } + let command: Command = match serde_json::from_str(&line) { + Ok(command) => command, + Err(parse_error) => { + write_message(&error(None, "invalid-command", parse_error.to_string()))?; + continue; + } + }; + let (responses, keep_running) = engine.handle(command); + for response in responses { + write_message(&response)?; + } + if !keep_running { + break; + } + } + + drop(engine); + let _ = event_writer.join(); + Ok(()) +} + +fn main() -> io::Result<()> { + // Optional verbose tracing of the str0m SCTP/DTLS stack to stderr (stdout is + // reserved for the JSON protocol). Enable with OPENNOW_STREAMER_TRACE=1. + if std::env::var_os("OPENNOW_STREAMER_TRACE").is_some() { + use tracing_subscriber::EnvFilter; + let _ = tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("str0m=debug,sctp_proto=debug")), + ) + .with_writer(std::io::stderr) + .with_ansi(false) + .try_init(); + } + #[cfg(target_os = "macos")] + if std::env::var_os("OPENNOW_DEBUG_WINDOW").is_some() { + opennow_streamer_platform::debug_show_overlay_window(); + } + let (host, media_runtime) = create_runtime().map_err(io::Error::other)?; + let shutdown_runtime = media_runtime.clone(); + let protocol = thread::Builder::new() + .name("opennow-protocol".to_owned()) + .spawn(move || { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + run_protocol(media_runtime) + })); + shutdown_runtime.shutdown(); + result + })?; + host.run(); + match protocol.join() { + Ok(Ok(result)) => result, + Ok(Err(payload)) => std::panic::resume_unwind(payload), + Err(payload) => std::panic::resume_unwind(payload), + } +} diff --git a/native/opennow-streamer/src/backend.rs b/native/opennow-streamer/src/backend.rs deleted file mode 100644 index 8e1d7e0b6..000000000 --- a/native/opennow-streamer/src/backend.rs +++ /dev/null @@ -1,790 +0,0 @@ -use crate::input::{PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL, PARTIALLY_RELIABLE_HID_DEVICE_MASK_ALL}; -use crate::protocol::{ - missing_field, ColorQuality, CommandEnvelope, Event, MediaConnectionInfo, - NativeStreamerCapabilities, NativeStreamerSessionContext, Response, VideoCodec, - PROTOCOL_VERSION, -}; -use crate::sdp::{ - duplicate_session_webrtc_attributes_to_media, extract_ice_credentials, fix_server_ip, - parse_resolution, prefer_codec, rewrite_sdp_ice_candidate_endpoints, - sanitize_ice_pwd_for_gstreamer, summarize_media_transport_attributes, NvstParams, - PreferCodecOptions, -}; -use std::env; -use std::sync::mpsc::Sender; - -pub trait NativeStreamerBackend { - fn capabilities(&self) -> NativeStreamerCapabilities; - fn start(&mut self, command: CommandEnvelope) -> BackendReply; - fn handle_offer(&mut self, command: CommandEnvelope) -> BackendReply; - fn add_remote_ice(&mut self, command: CommandEnvelope) -> BackendReply; - fn send_input(&mut self, command: CommandEnvelope) -> BackendReply; - fn set_input_paused(&mut self, command: CommandEnvelope) -> BackendReply; - fn update_render_surface(&mut self, command: CommandEnvelope) -> BackendReply; - fn update_bitrate_limit(&mut self, command: CommandEnvelope) -> BackendReply; - fn update_shortcuts(&mut self, command: CommandEnvelope) -> BackendReply; - fn stop(&mut self, command: CommandEnvelope) -> BackendReply; -} - -const BACKEND_ENV: &str = "OPENNOW_NATIVE_STREAMER_BACKEND"; -const NATIVE_CODEC_ENV: &str = "OPENNOW_NATIVE_CODEC"; -const MIN_BITRATE_KBPS: u32 = 5_000; -const MAX_BITRATE_KBPS: u32 = 150_000; - -pub fn create_backend(event_sender: Option>) -> Box { - let requested = env::var(BACKEND_ENV) - .ok() - .map(|value| value.trim().to_ascii_lowercase()) - .filter(|value| !value.is_empty()); - create_backend_for_name(requested.as_deref(), event_sender) -} - -fn create_backend_for_name( - requested: Option<&str>, - event_sender: Option>, -) -> Box { - #[cfg(not(feature = "gstreamer"))] - let _ = &event_sender; - - match requested.unwrap_or(default_backend_name()) { - "stub" => Box::::default(), - #[cfg(feature = "gstreamer")] - "gstreamer" => Box::new(crate::gstreamer_backend::GstreamerBackend::new( - event_sender, - )), - #[cfg(not(feature = "gstreamer"))] - "gstreamer" => Box::new(StubBackend::with_fallback( - "gstreamer", - "GStreamer backend was requested, but this binary was built without the gstreamer feature.", - )), - other => Box::new(StubBackend::with_fallback( - other, - format!("Unknown native streamer backend \"{other}\"; using stub."), - )), - } -} - -fn default_backend_name() -> &'static str { - #[cfg(feature = "gstreamer")] - { - "gstreamer" - } - #[cfg(not(feature = "gstreamer"))] - { - "stub" - } -} - -#[derive(Debug, Default)] -pub struct BackendReply { - pub events: Vec, - pub response: Option, - pub should_continue: bool, -} - -impl BackendReply { - pub fn response(response: Response) -> Self { - Self { - events: Vec::new(), - response: Some(response), - should_continue: true, - } - } - - pub fn continue_without_response() -> Self { - Self { - events: Vec::new(), - response: None, - should_continue: true, - } - } - - pub fn stop(id: String, message: String) -> Self { - Self { - events: vec![Event::Status { - status: "stopped", - message: Some(message), - }], - response: Some(Response::Ok { id }), - should_continue: false, - } - } -} - -#[derive(Debug, Clone)] -pub struct PreparedNativeOffer { - pub original_sdp_len: usize, - pub fixed_offer_sdp: String, - pub gstreamer_offer_sdp: String, - pub gstreamer_ice_pwd_replacements: usize, - pub gstreamer_framerate_adjusted: bool, - pub media_connection_ice_replacements: usize, - pub nvst_params: NvstParams, - pub media_connection_info: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PrepareNativeOfferError { - InvalidResolution { resolution: String }, -} - -impl PrepareNativeOfferError { - pub fn into_response(self, id: String) -> Response { - match self { - Self::InvalidResolution { resolution } => Response::Error { - id: Some(id), - code: "invalid-resolution".to_owned(), - message: format!("Invalid stream resolution: {resolution}"), - }, - } - } -} - -pub fn prepare_native_offer( - context: &NativeStreamerSessionContext, - offer_sdp: &str, -) -> Result { - let Some((width, height)) = parse_resolution(&context.settings.resolution) else { - return Err(PrepareNativeOfferError::InvalidResolution { - resolution: context.settings.resolution.clone(), - }); - }; - - let fixed_offer_sdp = fix_server_ip(offer_sdp, &context.session.server_ip); - let (fixed_offer_sdp, media_connection_ice_replacements) = - if let Some(media_connection_info) = web_rtc_media_connection_info(context) { - rewrite_sdp_ice_candidate_endpoints( - &fixed_offer_sdp, - &media_connection_info.ip, - media_connection_info.port, - ) - } else { - (fixed_offer_sdp, 0) - }; - let fixed_offer_sdp = duplicate_session_webrtc_attributes_to_media(&fixed_offer_sdp); - let codec = resolve_native_codec(context.settings.codec); - let fixed_offer_sdp = prefer_codec( - &fixed_offer_sdp, - codec, - PreferCodecOptions { - prefer_hevc_profile_id: Some(preferred_hevc_profile_id(context.settings.color_quality)), - }, - ); - let (gstreamer_framerate_offer_sdp, gstreamer_framerate_adjusted) = - align_video_sdp_framerate_for_gstreamer(&fixed_offer_sdp, context.settings.fps); - let (gstreamer_offer_sdp, gstreamer_ice_pwd_replacements) = - sanitize_ice_pwd_for_gstreamer(&gstreamer_framerate_offer_sdp); - let credentials = extract_ice_credentials(&fixed_offer_sdp); - let nvst_params = NvstParams { - width, - height, - fps: context.settings.fps, - max_bitrate_kbps: context.settings.max_bitrate_mbps.saturating_mul(1000), - partial_reliable_threshold_ms: 16, - codec, - color_quality: context.settings.color_quality, - credentials, - hid_device_mask: Some(PARTIALLY_RELIABLE_HID_DEVICE_MASK_ALL), - enable_partially_reliable_transfer_gamepad: Some(PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL), - enable_partially_reliable_transfer_hid: Some(PARTIALLY_RELIABLE_HID_DEVICE_MASK_ALL), - }; - - Ok(PreparedNativeOffer { - original_sdp_len: offer_sdp.len(), - fixed_offer_sdp, - gstreamer_offer_sdp, - gstreamer_ice_pwd_replacements, - gstreamer_framerate_adjusted, - media_connection_ice_replacements, - nvst_params, - media_connection_info: context.session.media_connection_info.clone(), - }) -} - -pub(crate) fn web_rtc_media_connection_info( - context: &NativeStreamerSessionContext, -) -> Option<&MediaConnectionInfo> { - let media_connection_info = context.session.media_connection_info.as_ref()?; - if matches!(media_connection_info.usage, Some(2 | 17)) - && !media_connection_info.ip.trim().is_empty() - && media_connection_info.port > 0 - { - Some(media_connection_info) - } else { - None - } -} - -fn align_video_sdp_framerate_for_gstreamer(sdp: &str, fps: u32) -> (String, bool) { - if fps == 0 { - return (sdp.to_owned(), false); - } - - let line_ending = if sdp.contains("\r\n") { "\r\n" } else { "\n" }; - let has_trailing_ending = sdp.ends_with(line_ending); - let mut lines: Vec = sdp - .split(line_ending) - .filter(|line| !line.is_empty() || !has_trailing_ending) - .map(ToOwned::to_owned) - .collect(); - let mut output = Vec::with_capacity(lines.len() + 1); - let mut in_video = false; - let mut video_has_framerate = false; - let mut changed = false; - let target = format!("a=framerate:{fps}"); - - for line in lines.drain(..) { - if line.starts_with("m=") { - if in_video && !video_has_framerate { - output.push(target.clone()); - changed = true; - } - in_video = line.starts_with("m=video"); - video_has_framerate = false; - output.push(line); - continue; - } - - if in_video && line.starts_with("a=framerate:") { - video_has_framerate = true; - if line != target { - output.push(target.clone()); - changed = true; - } else { - output.push(line); - } - continue; - } - - output.push(line); - } - - if in_video && !video_has_framerate { - output.push(target); - changed = true; - } - - let mut result = output.join(line_ending); - if has_trailing_ending { - result.push_str(line_ending); - } - (result, changed) -} - -fn resolve_native_codec(configured: VideoCodec) -> VideoCodec { - match env::var(NATIVE_CODEC_ENV) - .unwrap_or_else(|_| "auto".to_owned()) - .to_ascii_lowercase() - .as_str() - { - "h264" | "avc" => VideoCodec::H264, - "h265" | "hevc" => VideoCodec::H265, - "av1" => VideoCodec::AV1, - _ => configured, - } -} - -pub fn prepared_offer_events(prepared: &PreparedNativeOffer) -> Vec { - let nvst = &prepared.nvst_params; - let mut events = vec![Event::Log { - level: "info", - message: format!( - "Prepared native offer for {}x{}@{} {} {}bit; SDP {} -> {} bytes.", - nvst.width, - nvst.height, - nvst.fps, - codec_label(nvst.codec), - color_quality_bit_depth(nvst.color_quality), - prepared.original_sdp_len, - prepared.fixed_offer_sdp.len(), - ), - }]; - - if prepared.gstreamer_framerate_adjusted { - events.push(Event::Log { - level: "info", - message: format!( - "Aligned native WebRTC video SDP framerate to {} fps for GStreamer caps negotiation.", - nvst.fps - ), - }); - } - - if let Some(media_connection_info) = &prepared.media_connection_info { - events.push(Event::Log { - level: "debug", - message: format!( - "GFN media connection hint {}:{} (usage={}).", - media_connection_info.ip, - media_connection_info.port, - media_connection_info - .usage - .map(|usage| usage.to_string()) - .unwrap_or_else(|| "unknown".to_owned()) - ), - }); - } - if prepared.media_connection_ice_replacements > 0 { - if let Some(media_connection_info) = &prepared.media_connection_info { - events.push(Event::Log { - level: "info", - message: format!( - "Rewrote {} remote ICE candidate endpoint(s) to GFN media connection hint {}:{}.", - prepared.media_connection_ice_replacements, - media_connection_info.ip, - media_connection_info.port - ), - }); - } - } - events.push(Event::Log { - level: "debug", - message: format!( - "Prepared native WebRTC SDP transport summary: {}.", - summarize_media_transport_attributes(&prepared.fixed_offer_sdp) - ), - }); - if prepared.gstreamer_ice_pwd_replacements > 0 { - events.push(Event::Log { - level: "warn", - message: format!( - "GFN offer uses non-standard ICE password characters; sanitized {} ice-pwd line(s) for GStreamer validation only.", - prepared.gstreamer_ice_pwd_replacements - ), - }); - } - - events -} - -pub fn normalize_bitrate_kbps(value: u32) -> u32 { - value.clamp(MIN_BITRATE_KBPS, MAX_BITRATE_KBPS) -} - -pub fn bitrate_kbps_to_mbps(value: u32) -> u32 { - normalize_bitrate_kbps(value).div_ceil(1000) -} - -pub fn update_context_bitrate_limit( - context: &mut Option, - max_bitrate_kbps: u32, -) { - if let Some(context) = context { - context.settings.max_bitrate_mbps = bitrate_kbps_to_mbps(max_bitrate_kbps); - } -} - -#[derive(Debug, Default)] -pub struct StubBackend { - active_context: Option, - startup_warning: Option, - requested_backend: Option, - fallback_reason: Option, -} - -impl StubBackend { - fn with_fallback(requested_backend: impl Into, reason: impl Into) -> Self { - let requested_backend = requested_backend.into(); - let reason = reason.into(); - Self { - active_context: None, - startup_warning: Some(reason.clone()), - requested_backend: Some(requested_backend), - fallback_reason: Some(reason), - } - } -} - -impl NativeStreamerBackend for StubBackend { - fn capabilities(&self) -> NativeStreamerCapabilities { - NativeStreamerCapabilities { - protocol_version: PROTOCOL_VERSION, - backend: "stub", - requested_backend: self.requested_backend.clone(), - fallback_reason: self.fallback_reason.clone(), - supports_offer_answer: false, - supports_remote_ice: true, - supports_local_ice: false, - supports_input: false, - video_backends: Vec::new(), - } - } - - fn start(&mut self, command: CommandEnvelope) -> BackendReply { - let id = command.id; - let Some(context) = command.context else { - return BackendReply::response(missing_field(&id, "context")); - }; - let session_id = context.session.session_id.clone(); - self.active_context = Some(context); - let mut events = Vec::new(); - if let Some(message) = self.startup_warning.take() { - events.push(Event::Log { - level: "warn", - message, - }); - } - events.push(Event::Status { - status: "ready", - message: Some(format!( - "Native streamer process is running for session {session_id}; media backend is not enabled." - )), - }); - BackendReply { - events, - response: Some(Response::Ok { id }), - should_continue: true, - } - } - - fn handle_offer(&mut self, command: CommandEnvelope) -> BackendReply { - let id = command.id.clone(); - let Some(context) = command.context else { - return BackendReply::response(missing_field(&id, "context")); - }; - let Some(offer_sdp) = command.sdp else { - return BackendReply::response(missing_field(&id, "sdp")); - }; - - let prepared = match prepare_native_offer(&context, &offer_sdp) { - Ok(prepared) => prepared, - Err(error) => return BackendReply::response(error.into_response(id)), - }; - - BackendReply { - events: prepared_offer_events(&prepared), - response: Some(Response::Error { - id: Some(id), - code: "backend-unavailable".to_owned(), - message: "The native streamer parsed the GFN offer, but no WebRTC media backend is enabled yet.".to_owned(), - }), - should_continue: true, - } - } - - fn add_remote_ice(&mut self, command: CommandEnvelope) -> BackendReply { - if command.candidate.is_none() { - return BackendReply::response(missing_field(&command.id, "candidate")); - } - - BackendReply::response(Response::Ok { id: command.id }) - } - - fn send_input(&mut self, command: CommandEnvelope) -> BackendReply { - if let Some(packet) = command.input { - let _ = packet.payload_bytes(); - } - BackendReply::continue_without_response() - } - - fn set_input_paused(&mut self, command: CommandEnvelope) -> BackendReply { - if command.paused.is_none() { - return BackendReply::response(missing_field(&command.id, "paused")); - } - - BackendReply::response(Response::Ok { id: command.id }) - } - - fn update_render_surface(&mut self, command: CommandEnvelope) -> BackendReply { - if command.surface.is_none() { - return BackendReply::response(missing_field(&command.id, "surface")); - } - - BackendReply::response(Response::Ok { id: command.id }) - } - - fn update_bitrate_limit(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(max_bitrate_kbps) = command.max_bitrate_kbps else { - return BackendReply::response(missing_field(&command.id, "maxBitrateKbps")); - }; - - let max_bitrate_kbps = normalize_bitrate_kbps(max_bitrate_kbps); - update_context_bitrate_limit(&mut self.active_context, max_bitrate_kbps); - - BackendReply { - events: vec![Event::Log { - level: "info", - message: format!( - "Updated native bitrate limit to {max_bitrate_kbps} Kbps for the next native offer." - ), - }], - response: Some(Response::Ok { id: command.id }), - should_continue: true, - } - } - - fn update_shortcuts(&mut self, command: CommandEnvelope) -> BackendReply { - // Stub backend has no native window; accept the command without applying it. - BackendReply::response(Response::Ok { id: command.id }) - } - - fn stop(&mut self, command: CommandEnvelope) -> BackendReply { - self.active_context = None; - let message = command - .reason - .unwrap_or_else(|| "stop requested".to_owned()); - BackendReply::stop(command.id, message) - } -} - -fn codec_label(codec: VideoCodec) -> &'static str { - match codec { - VideoCodec::H264 => "H264", - VideoCodec::H265 => "H265", - VideoCodec::AV1 => "AV1", - } -} - -fn color_quality_bit_depth(color_quality: ColorQuality) -> u8 { - color_quality.bit_depth() -} - -fn preferred_hevc_profile_id(color_quality: ColorQuality) -> u8 { - if color_quality.bit_depth() >= 10 { - 2 - } else { - 1 - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::protocol::{ColorQuality, NativeStreamerShortcutBindings, SessionInfo, StreamSettings}; - - fn context(resolution: &str) -> NativeStreamerSessionContext { - NativeStreamerSessionContext { - session: SessionInfo { - session_id: "session-1".to_owned(), - server_ip: "80-250-97-40.cloudmatchbeta.nvidiagrid.net".to_owned(), - ice_servers: Vec::new(), - media_connection_info: Some(MediaConnectionInfo { - ip: "10.0.0.7".to_owned(), - port: 49003, - usage: Some(2), - }), - negotiated_stream_profile: None, - requested_streaming_features: None, - finalized_streaming_features: None, - }, - settings: StreamSettings { - resolution: resolution.to_owned(), - fps: 120, - max_bitrate_mbps: 75, - codec: VideoCodec::H265, - color_quality: ColorQuality::TenBit420, - enable_cloud_gsync: false, - native_transition_diagnostics: None, - }, - shortcuts: NativeStreamerShortcutBindings::default(), - nvst_video: None, - } - } - - #[test] - fn prepares_offer_once_for_all_backends() { - let offer = "v=0\nc=IN IP4 0.0.0.0\na=ice-ufrag:user\na=ice-pwd:pass\na=fingerprint:sha-256 AA:BB\n"; - let prepared = prepare_native_offer(&context("1920x1080"), offer).expect("valid offer"); - - assert!(prepared.fixed_offer_sdp.contains("c=IN IP4 80.250.97.40")); - assert!(prepared - .gstreamer_offer_sdp - .contains("c=IN IP4 80.250.97.40")); - assert_eq!(prepared.nvst_params.width, 1920); - assert_eq!(prepared.nvst_params.height, 1080); - assert_eq!(prepared.nvst_params.fps, 120); - assert_eq!(prepared.nvst_params.max_bitrate_kbps, 75_000); - assert_eq!(prepared.nvst_params.credentials.ufrag, "user"); - assert_eq!( - prepared - .media_connection_info - .as_ref() - .map(|info| info.port), - Some(49003), - ); - } - - #[test] - fn prepares_offer_rewrites_server_ice_candidates_to_media_connection_info() { - let offer = [ - "v=0", - "a=ice-ufrag:user", - "a=ice-pwd:pass", - "a=fingerprint:sha-256 AA:BB", - "m=audio 47998 UDP/TLS/RTP/SAVPF 111", - "a=candidate:1 1 udp 2122260223 203.0.113.10 47998 typ host", - "m=video 47998 UDP/TLS/RTP/SAVPF 96", - "a=candidate:2 1 udp 2122260223 203.0.113.10 47998 typ host", - "a=rtpmap:96 H265/90000", - ] - .join("\n"); - - let prepared = prepare_native_offer(&context("1920x1080"), &offer).expect("valid offer"); - - assert_eq!(prepared.media_connection_ice_replacements, 2); - assert!(prepared - .fixed_offer_sdp - .contains("a=candidate:1 1 udp 2122260223 10.0.0.7 49003 typ host")); - assert!(prepared - .fixed_offer_sdp - .contains("a=candidate:2 1 udp 2122260223 10.0.0.7 49003 typ host")); - } - - #[test] - fn prepares_offer_skips_non_webrtc_media_connection_info() { - let mut context = context("1920x1080"); - context - .session - .media_connection_info - .as_mut() - .expect("media connection") - .usage = Some(14); - let offer = [ - "v=0", - "a=ice-ufrag:user", - "a=ice-pwd:pass", - "a=fingerprint:sha-256 AA:BB", - "a=candidate:1 1 udp 2122260223 203.0.113.10 47998 typ host", - ] - .join("\n"); - - let prepared = prepare_native_offer(&context, &offer).expect("valid offer"); - - assert_eq!(prepared.media_connection_ice_replacements, 0); - assert!(prepared - .fixed_offer_sdp - .contains("a=candidate:1 1 udp 2122260223 203.0.113.10 47998 typ host")); - } - - #[test] - fn prepares_offer_filters_remote_video_to_requested_codec() { - let offer = [ - "v=0", - "a=group:BUNDLE 0 1", - "a=ice-ufrag:user", - "a=ice-pwd:pass", - "a=fingerprint:sha-256 AA:BB", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=rtpmap:111 OPUS/48000/2", - "m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100", - "a=rtpmap:96 AV1/90000", - "a=rtpmap:97 rtx/90000", - "a=fmtp:97 apt=96", - "a=rtpmap:98 H265/90000", - "a=fmtp:98 profile-id=2;level-id=186", - "a=rtpmap:99 rtx/90000", - "a=fmtp:99 apt=98", - "a=rtpmap:100 flexfec-03/90000", - ] - .join("\n"); - - let prepared = prepare_native_offer(&context("1920x1080"), &offer).expect("valid offer"); - - assert!(prepared - .gstreamer_offer_sdp - .contains("a=rtpmap:98 H265/90000")); - assert!(prepared - .gstreamer_offer_sdp - .contains("a=rtpmap:99 rtx/90000")); - assert!(!prepared - .gstreamer_offer_sdp - .contains("a=rtpmap:96 AV1/90000")); - assert!(!prepared - .gstreamer_offer_sdp - .contains("a=rtpmap:97 rtx/90000")); - assert!(prepared - .gstreamer_offer_sdp - .contains("m=video 9 UDP/TLS/RTP/SAVPF 98 99 100")); - assert!(prepared - .gstreamer_offer_sdp - .contains("a=rtpmap:100 flexfec-03/90000")); - } - - #[test] - fn aligns_gstreamer_video_sdp_framerate() { - let sdp = "v=0\nm=video 9 UDP/TLS/RTP/SAVPF 96\na=framerate:60\na=rtpmap:96 H265/90000\n"; - - let (aligned, changed) = align_video_sdp_framerate_for_gstreamer(sdp, 240); - - assert!(changed); - assert!(aligned.contains("a=framerate:240\n")); - assert!(!aligned.contains("a=framerate:60")); - } - - #[test] - fn inserts_gstreamer_video_sdp_framerate_when_absent() { - let sdp = "v=0\nm=audio 9 UDP/TLS/RTP/SAVPF 111\na=rtpmap:111 OPUS/48000/2\nm=video 9 UDP/TLS/RTP/SAVPF 96\na=rtpmap:96 H265/90000\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\n"; - - let (aligned, changed) = align_video_sdp_framerate_for_gstreamer(sdp, 120); - - assert!(changed); - assert!(aligned.contains( - "m=video 9 UDP/TLS/RTP/SAVPF 96\na=rtpmap:96 H265/90000\na=framerate:120\nm=application" - )); - } - - #[test] - fn preserves_gstreamer_video_sdp_framerate_line_endings() { - let sdp = "v=0\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\na=rtpmap:96 H265/90000\r\n"; - - let (aligned, changed) = align_video_sdp_framerate_for_gstreamer(sdp, 240); - - assert!(changed); - assert!(aligned.contains("a=framerate:240\r\n")); - assert!(!aligned.contains('\n') || aligned.contains("\r\n")); - } - - #[test] - fn rejects_invalid_resolution_during_offer_preparation() { - let error = prepare_native_offer(&context("bad"), "v=0").expect_err("invalid resolution"); - assert_eq!( - error, - PrepareNativeOfferError::InvalidResolution { - resolution: "bad".to_owned(), - }, - ); - } - - #[test] - fn normalizes_native_bitrate_limits_to_slider_bounds() { - assert_eq!(normalize_bitrate_kbps(1_000), 5_000); - assert_eq!(normalize_bitrate_kbps(75_000), 75_000); - assert_eq!(normalize_bitrate_kbps(250_000), 150_000); - assert_eq!(bitrate_kbps_to_mbps(75_500), 76); - } - - #[test] - fn creates_expected_default_backend_for_build_features() { - let backend = create_backend_for_name(None, None); - let capabilities = backend.capabilities(); - #[cfg(not(feature = "gstreamer"))] - assert_eq!(capabilities.backend, "stub"); - #[cfg(feature = "gstreamer")] - assert_eq!(capabilities.backend, "gstreamer"); - } - - #[test] - fn reports_unknown_backend_fallback_in_capabilities() { - let backend = create_backend_for_name(Some("missing"), None); - let capabilities = backend.capabilities(); - assert_eq!(capabilities.backend, "stub"); - assert_eq!(capabilities.requested_backend.as_deref(), Some("missing")); - assert!(capabilities - .fallback_reason - .as_deref() - .is_some_and(|reason| reason.contains("Unknown native streamer backend")),); - } - - #[cfg(not(feature = "gstreamer"))] - #[test] - fn reports_gstreamer_feature_fallback_in_capabilities() { - let backend = create_backend_for_name(Some("gstreamer"), None); - let capabilities = backend.capabilities(); - assert_eq!(capabilities.backend, "stub"); - assert_eq!(capabilities.requested_backend.as_deref(), Some("gstreamer")); - assert!(capabilities - .fallback_reason - .as_deref() - .is_some_and(|reason| reason.contains("without the gstreamer feature")),); - } -} diff --git a/native/opennow-streamer/src/gstreamer_backend.rs b/native/opennow-streamer/src/gstreamer_backend.rs deleted file mode 100644 index c72879b04..000000000 --- a/native/opennow-streamer/src/gstreamer_backend.rs +++ /dev/null @@ -1,1215 +0,0 @@ -use crate::backend::{ - normalize_bitrate_kbps, prepare_native_offer, prepared_offer_events, - update_context_bitrate_limit, BackendReply, NativeStreamerBackend, - web_rtc_media_connection_info, -}; -use crate::gstreamer_config::{ - resolve_d3d_fullscreen_sink, resolve_present_max_fps, use_internal_renderer, - NATIVE_D3D_FULLSCREEN_ENV, NATIVE_PRESENT_MAX_FPS_ENV, PRESENT_LIMITER_AUTO_SENTINEL, - PRESENT_LIMITER_VRR_SENTINEL, -}; -use crate::gstreamer_platform::{clear_native_shortcut_bindings, set_native_shortcut_bindings}; -use crate::gstreamer_pipeline::{ - current_platform_label, init_gstreamer, native_video_backend_capabilities, GstreamerPipeline, -}; -use crate::protocol::{ - missing_field, CommandEnvelope, Event, IceCandidatePayload, NativeRenderSurface, - NativeStreamerCapabilities, NativeStreamerSessionContext, NativeVideoBackendCapability, - Response, SendAnswerRequest, PROTOCOL_VERSION, -}; -use crate::sdp::{ - build_nvst_sdp_for_answer, extract_negotiated_video_codec, munge_answer_sdp, - rewrite_ice_candidate_endpoint, -}; -use std::sync::mpsc::Sender; - -pub(crate) fn send_log(event_sender: &Option>, level: &'static str, message: String) { - if let Some(event_sender) = event_sender { - let _ = event_sender.send(Event::Log { level, message }); - } else { - eprintln!("[NativeStreamer] {message}"); - } -} - -#[derive(Debug)] -pub struct GstreamerBackend { - active_context: Option, - pending_remote_ice: Vec, - pipeline: Option, - event_sender: Option>, - remote_description_set: bool, - render_surface: Option, -} - -impl GstreamerBackend { - pub fn new(event_sender: Option>) -> Self { - Self { - active_context: None, - pending_remote_ice: Vec::new(), - pipeline: None, - event_sender, - remote_description_set: false, - render_surface: None, - } - } - - fn replay_pending_remote_ice(&mut self) -> Vec { - let candidates = std::mem::take(&mut self.pending_remote_ice); - let Some(pipeline) = self.pipeline.as_mut() else { - self.pending_remote_ice = candidates; - return Vec::new(); - }; - - let mut events = Vec::new(); - for candidate in candidates { - if let Err(message) = pipeline.add_remote_ice(&candidate) { - events.push(Event::Error { - code: "remote-ice-failed".to_owned(), - message, - }); - } - } - events - } - - fn rewrite_remote_ice_candidate(&self, candidate: IceCandidatePayload) -> IceCandidatePayload { - let Some(context) = self.active_context.as_ref() else { - return candidate; - }; - let Some(media_connection_info) = web_rtc_media_connection_info(context) else { - return candidate; - }; - let (rewritten, changed) = rewrite_ice_candidate_endpoint( - &candidate.candidate, - &media_connection_info.ip, - media_connection_info.port, - ); - if changed { - send_log( - &self.event_sender, - "info", - format!( - "Rewrote remote ICE candidate endpoint to GFN media connection hint {}:{}.", - media_connection_info.ip, media_connection_info.port - ), - ); - IceCandidatePayload { - candidate: rewritten, - ..candidate - } - } else { - candidate - } - } -} - -impl NativeStreamerBackend for GstreamerBackend { - fn capabilities(&self) -> NativeStreamerCapabilities { - NativeStreamerCapabilities { - protocol_version: PROTOCOL_VERSION, - backend: "gstreamer", - requested_backend: None, - fallback_reason: None, - supports_offer_answer: true, - supports_remote_ice: true, - supports_local_ice: true, - supports_input: true, - video_backends: match init_gstreamer() { - Ok(()) => native_video_backend_capabilities(), - Err(error) => vec![NativeVideoBackendCapability { - backend: "gstreamer".to_owned(), - platform: current_platform_label().to_owned(), - codecs: Vec::new(), - zero_copy_modes: Vec::new(), - sink: None, - available: false, - reason: Some(error), - }], - }, - } - } - - fn start(&mut self, command: CommandEnvelope) -> BackendReply { - let id = command.id; - let Some(context) = command.context else { - return BackendReply::response(missing_field(&id, "context")); - }; - - let session_id = context.session.session_id.clone(); - let pipeline = match GstreamerPipeline::build( - self.event_sender.clone(), - &context.session.ice_servers, - ) { - Ok(pipeline) => pipeline, - Err(message) => { - return BackendReply { - events: vec![Event::Error { - code: "gstreamer-start-failed".to_owned(), - message: message.clone(), - }], - response: Some(Response::Error { - id: Some(id), - code: "gstreamer-start-failed".to_owned(), - message, - }), - should_continue: true, - }; - } - }; - - if let Some(old_pipeline) = self.pipeline.take() { - if let Err(message) = old_pipeline.stop() { - eprintln!("[NativeStreamer] {message}"); - } - } - - set_native_shortcut_bindings(&context.shortcuts); - self.active_context = Some(context); - self.pending_remote_ice.clear(); - self.remote_description_set = false; - let webrtc_name = pipeline.webrtc_name(); - self.pipeline = Some(pipeline); - - let mut events = vec![Event::Status { - status: "ready", - message: Some(format!( - "GStreamer backend selected for session {session_id}; {} pipeline is ready.", - webrtc_name - )), - }]; - - if let Some(nvst) = self - .active_context - .as_ref() - .and_then(|ctx| ctx.nvst_video.clone()) - { - let fallback_codec = self - .active_context - .as_ref() - .map(|ctx| ctx.settings.codec.as_str().to_owned()) - .unwrap_or_else(|| "H265".to_owned()); - let requested_fps = self - .active_context - .as_ref() - .map(|ctx| ctx.settings.fps); - let d3d_fullscreen = resolve_d3d_fullscreen_sink( - self.active_context - .as_ref() - .map(|ctx| ctx.settings.enable_cloud_gsync) - .unwrap_or(false), - ); - let cloud_gsync_enabled = self - .active_context - .as_ref() - .map(|ctx| ctx.settings.enable_cloud_gsync) - .unwrap_or(false); - let present_max_fps = resolve_present_max_fps(cloud_gsync_enabled); - if let Some(pipeline) = self.pipeline.as_mut() { - pipeline.set_present_max_fps(present_max_fps); - pipeline.set_d3d_fullscreen_sink(d3d_fullscreen); - if let Some(ctx) = self.active_context.as_ref() { - let bitrate_kbps = ctx.settings.max_bitrate_mbps.saturating_mul(1000); - pipeline.configure_stats(ctx, bitrate_kbps); - } - match pipeline.attach_nvst_video( - nvst, - &fallback_codec, - requested_fps.filter(|fps| *fps > 0), - d3d_fullscreen, - ) { - Ok(()) => events.push(Event::Log { - level: "info", - message: "NVST classic UDP video receive scaffold attached (hybrid WebRTC input)." - .to_owned(), - }), - Err(message) => { - events.push(Event::Error { - code: "nvst-video-attach-failed".to_owned(), - message: message.clone(), - }); - return BackendReply { - events, - response: Some(Response::Error { - id: Some(id), - code: "nvst-video-attach-failed".to_owned(), - message, - }), - should_continue: true, - }; - } - } - } - } - - if let (Some(surface), Some(pipeline)) = - (self.render_surface.clone(), self.pipeline.as_ref()) - { - if let Err(message) = pipeline.update_render_surface(surface) { - if let Some(pipeline) = self.pipeline.take() { - let _ = pipeline.stop(); - } - return BackendReply { - events: vec![Event::Error { - code: "native-render-surface-failed".to_owned(), - message: message.clone(), - }], - response: Some(Response::Error { - id: Some(id), - code: "native-render-surface-failed".to_owned(), - message, - }), - should_continue: true, - }; - } - } - - BackendReply { - events, - response: Some(Response::Ok { id }), - should_continue: true, - } - } - - fn handle_offer(&mut self, command: CommandEnvelope) -> BackendReply { - let id = command.id.clone(); - let Some(context) = command.context else { - return BackendReply::response(missing_field(&id, "context")); - }; - let Some(offer_sdp) = command.sdp else { - return BackendReply::response(missing_field(&id, "sdp")); - }; - - let prepared = match prepare_native_offer(&context, &offer_sdp) { - Ok(prepared) => prepared, - Err(error) => return BackendReply::response(error.into_response(id)), - }; - - let mut events = prepared_offer_events(&prepared); - let parsed_offer = match GstreamerPipeline::parse_offer_sdp(&prepared.gstreamer_offer_sdp) { - Ok(offer) => offer, - Err(message) => { - return BackendReply { - events, - response: Some(Response::Error { - id: Some(id), - code: "invalid-remote-sdp".to_owned(), - message, - }), - should_continue: true, - }; - } - }; - - let Some(pipeline) = self.pipeline.as_mut() else { - return BackendReply { - events, - response: Some(Response::Error { - id: Some(id), - code: "gstreamer-not-started".to_owned(), - message: "GStreamer pipeline is not started.".to_owned(), - }), - should_continue: true, - }; - }; - - let present_max_fps = resolve_present_max_fps(context.settings.enable_cloud_gsync); - // Internal child-surface mode never uses exclusive D3D fullscreen present. - let d3d_fullscreen_sink = resolve_d3d_fullscreen_sink(context.settings.enable_cloud_gsync); - set_native_shortcut_bindings(&context.shortcuts); - pipeline.set_present_max_fps(present_max_fps); - pipeline.set_d3d_fullscreen_sink(d3d_fullscreen_sink); - pipeline.configure_stats(&context, prepared.nvst_params.max_bitrate_kbps); - if present_max_fps > 0 - && present_max_fps != PRESENT_LIMITER_AUTO_SENTINEL - && present_max_fps != PRESENT_LIMITER_VRR_SENTINEL - { - events.push(Event::Log { - level: "info", - message: format!( - "Native present limiter enabled at {present_max_fps} fps for {} fps stream; set {NATIVE_PRESENT_MAX_FPS_ENV}=0 to disable.", - context.settings.fps - ), - }); - } else if present_max_fps == PRESENT_LIMITER_AUTO_SENTINEL { - events.push(Event::Log { - level: "info", - message: format!( - "Native present limiter auto mode for {} fps stream (D3D11 caps to display Hz when stream fps exceeds it); set {NATIVE_PRESENT_MAX_FPS_ENV}=0 to disable.", - context.settings.fps - ), - }); - } else if present_max_fps == PRESENT_LIMITER_VRR_SENTINEL { - events.push(Event::Log { - level: "info", - message: format!( - "Native VRR present limiter auto mode for {} fps stream (caps below the display refresh ceiling when needed).", - context.settings.fps - ), - }); - } else { - events.push(Event::Log { - level: "info", - message: "Native present limiter disabled for uncapped VSync-off presentation." - .to_owned(), - }); - } - if d3d_fullscreen_sink { - events.push(Event::Log { - level: "info", - message: format!( - "Native D3D fullscreen presentation is enabled for Cloud G-Sync/VRR; set {NATIVE_D3D_FULLSCREEN_ENV}=0 to disable." - ), - }); - } else if use_internal_renderer() { - events.push(Event::Log { - level: "info", - message: "Native Internal renderer keeps exclusive D3D fullscreen off (child HWND present; sync=false, depth-1 post-decode queue)." - .to_owned(), - }); - } - - let answer_sdp = match pipeline.negotiate_answer( - parsed_offer, - (prepared.gstreamer_ice_pwd_replacements > 0) - .then_some(&prepared.nvst_params.credentials), - prepared.nvst_params.partial_reliable_threshold_ms, - ) { - Ok(answer_sdp) => munge_answer_sdp(&answer_sdp, prepared.nvst_params.max_bitrate_kbps), - Err(message) => { - return BackendReply { - events, - response: Some(Response::Error { - id: Some(id), - code: "gstreamer-negotiation-failed".to_owned(), - message, - }), - should_continue: true, - }; - } - }; - self.remote_description_set = true; - events.extend(self.replay_pending_remote_ice()); - - events.push(Event::Log { - level: "info", - message: - "GStreamer created a local WebRTC answer and replayed queued remote ICE candidates." - .to_owned(), - }); - - if let Some(negotiated_codec) = extract_negotiated_video_codec(&answer_sdp) { - if negotiated_codec != prepared.nvst_params.codec { - events.push(Event::Log { - level: "warn", - message: format!( - "Negotiated video codec is {} while requested codec was {}; building NVST SDP for the negotiated codec to avoid server/client codec mismatch.", - negotiated_codec.as_str(), - prepared.nvst_params.codec.as_str(), - ), - }); - } else { - events.push(Event::Log { - level: "debug", - message: format!( - "Negotiated video codec confirmed as {}.", - negotiated_codec.as_str() - ), - }); - } - } - - let nvst_sdp = match build_nvst_sdp_for_answer(&prepared.nvst_params, &answer_sdp) { - Ok(nvst_sdp) => nvst_sdp, - Err(message) => { - return BackendReply { - events, - response: Some(Response::Error { - id: Some(id), - code: "invalid-local-answer-sdp".to_owned(), - message, - }), - should_continue: true, - }; - } - }; - - events.push(Event::Log { - level: "debug", - message: "Built native NVST SDP from the local WebRTC answer transport credentials." - .to_owned(), - }); - - BackendReply { - events, - response: Some(Response::Answer { - id, - answer: SendAnswerRequest { - sdp: answer_sdp, - nvst_sdp: Some(nvst_sdp), - }, - }), - should_continue: true, - } - } - - fn add_remote_ice(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(candidate) = command.candidate else { - return BackendReply::response(missing_field(&command.id, "candidate")); - }; - let candidate = self.rewrite_remote_ice_candidate(candidate); - - if self.remote_description_set { - if let Some(pipeline) = self.pipeline.as_mut() { - if let Err(message) = pipeline.add_remote_ice(&candidate) { - return BackendReply::response(Response::Error { - id: Some(command.id), - code: "remote-ice-failed".to_owned(), - message, - }); - } - } else { - self.pending_remote_ice.push(candidate); - } - } else { - self.pending_remote_ice.push(candidate); - } - BackendReply::response(Response::Ok { id: command.id }) - } - - fn send_input(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(packet) = command.input else { - return BackendReply::continue_without_response(); - }; - - let Ok(payload) = packet.payload_bytes() else { - return BackendReply::continue_without_response(); - }; - - if payload.is_empty() || payload.len() > 4096 { - return BackendReply::continue_without_response(); - } - - if let Some(pipeline) = self.pipeline.as_ref() { - let _ = pipeline.send_input_packet(&payload, packet.partially_reliable); - } - - BackendReply::continue_without_response() - } - - fn set_input_paused(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(paused) = command.paused else { - return BackendReply::response(missing_field(&command.id, "paused")); - }; - - if let Some(pipeline) = self.pipeline.as_ref() { - pipeline.set_input_paused(paused); - } - - BackendReply::response(Response::Ok { id: command.id }) - } - - fn update_render_surface(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(surface) = command.surface else { - return BackendReply::response(missing_field(&command.id, "surface")); - }; - - self.render_surface = Some(surface.clone()); - if let Some(pipeline) = self.pipeline.as_ref() { - if let Err(message) = pipeline.update_render_surface(surface) { - return BackendReply { - events: vec![Event::Error { - code: "native-render-surface-failed".to_owned(), - message: message.clone(), - }], - response: Some(Response::Error { - id: Some(command.id), - code: "native-render-surface-failed".to_owned(), - message, - }), - should_continue: true, - }; - } - } - - BackendReply::response(Response::Ok { id: command.id }) - } - - fn update_bitrate_limit(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(max_bitrate_kbps) = command.max_bitrate_kbps else { - return BackendReply::response(missing_field(&command.id, "maxBitrateKbps")); - }; - - let max_bitrate_kbps = normalize_bitrate_kbps(max_bitrate_kbps); - update_context_bitrate_limit(&mut self.active_context, max_bitrate_kbps); - - BackendReply { - events: vec![Event::Log { - level: "info", - message: format!( - "Updated native bitrate limit to {max_bitrate_kbps} Kbps. The active GFN server bitrate cap is negotiated in NVST SDP and will apply on the next native offer/reconnect." - ), - }], - response: Some(Response::Ok { id: command.id }), - should_continue: true, - } - } - - fn update_shortcuts(&mut self, command: CommandEnvelope) -> BackendReply { - let Some(shortcuts) = command.shortcuts else { - return BackendReply::response(missing_field(&command.id, "shortcuts")); - }; - set_native_shortcut_bindings(&shortcuts); - if let Some(context) = self.active_context.as_mut() { - context.shortcuts = shortcuts; - } - BackendReply::response(Response::Ok { id: command.id }) - } - - fn stop(&mut self, command: CommandEnvelope) -> BackendReply { - self.active_context = None; - self.pending_remote_ice.clear(); - self.remote_description_set = false; - clear_native_shortcut_bindings(); - if let Some(pipeline) = self.pipeline.take() { - if let Err(message) = pipeline.stop() { - return BackendReply { - events: vec![Event::Error { - code: "gstreamer-stop-failed".to_owned(), - message: message.clone(), - }], - response: Some(Response::Error { - id: Some(command.id), - code: "gstreamer-stop-failed".to_owned(), - message, - }), - should_continue: true, - }; - } - } - let message = command - .reason - .unwrap_or_else(|| "stop requested".to_owned()); - BackendReply::stop(command.id, message) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::gstreamer_config::PRESENT_LIMITER_AUTO_SENTINEL; - use crate::gstreamer_input::parse_input_handshake_version; - use crate::gstreamer_liveness::{ - caps_framerate_summary, classify_video_startup_failure, sink_stats_summary, - VideoStallAction, VideoStallTracker, - }; - use crate::gstreamer_pipeline::{ - backend_runs_on_platform, configure_stats_overlay_element, - default_rtp_video_api_priority, effective_present_max_fps, format_video_chain_selection, - init_gstreamer, preferred_rtp_video_apis_for, resolve_gstreamer_stun_server, - rtp_video_chain_definition, RtpVideoApi, RtpVideoChainRole, - }; - use crate::gstreamer_transitions::resolve_queue_mode; - use crate::protocol::{IceServer, NativeQueueMode, StreamSettings, VideoCodec}; - use crate::sdp::IceCredentials; - use gst::prelude::*; - use gstreamer as gst; - use gstreamer_webrtc as gst_webrtc; - - #[test] - fn builds_and_stops_webrtc_pipeline() { - let pipeline = GstreamerPipeline::build(None, &[]).expect("GStreamer webrtcbin pipeline"); - assert_eq!(pipeline.webrtc.name(), "opennow-webrtcbin"); - assert_eq!( - pipeline.webrtc.property::("stun-server"), - "stun://stun2.l.google.com:19302" - ); - pipeline.stop().expect("pipeline stops"); - } - - #[test] - fn invalid_internal_render_surface_fails_before_stream_negotiation() { - if crate::gstreamer_config::use_external_renderer_window() { - return; - } - - let pipeline = GstreamerPipeline::build(None, &[]).expect("GStreamer pipeline"); - let error = pipeline - .update_render_surface(NativeRenderSurface { - window_handle: Some("not-a-native-handle".to_owned()), - rect: Some(crate::protocol::NativeRenderRect { - x: 0, - y: 0, - width: 1280, - height: 720, - }), - visible: true, - device_scale_factor: 1.0, - show_stats: false, - }) - .expect_err("invalid native parent handle must fail"); - assert!(error.contains("Invalid native parent window handle")); - pipeline.stop().expect("pipeline stops"); - } - - #[test] - fn classifies_native_video_startup_failure_stage() { - assert_eq!( - classify_video_startup_failure(0, 0, 0), - ("native-video-input-startup-timeout", "RTP video input") - ); - assert_eq!( - classify_video_startup_failure(4096, 0, 0), - ( - "native-video-decoder-startup-timeout", - "video decoder output" - ) - ); - assert_eq!( - classify_video_startup_failure(4096, 10, 0), - ( - "native-video-renderer-startup-timeout", - "video renderer input" - ) - ); - } - - #[test] - fn configures_session_stun_server_for_gstreamer() { - let servers = vec![ - IceServer { - urls: vec!["turn:relay.example.test:3478".to_owned()], - username: None, - credential: None, - }, - IceServer { - urls: vec!["stun:192.0.2.10:19302".to_owned()], - username: None, - credential: None, - }, - ]; - - assert_eq!( - resolve_gstreamer_stun_server(&servers), - "stun://192.0.2.10:19302" - ); - let pipeline = - GstreamerPipeline::build(None, &servers).expect("GStreamer webrtcbin pipeline"); - assert_eq!( - pipeline.webrtc.property::("stun-server"), - "stun://192.0.2.10:19302" - ); - pipeline.stop().expect("pipeline stops"); - } - - #[test] - fn configures_dwrite_stats_overlay_without_type_panics() { - gst::init().expect("gstreamer init"); - let Some(overlay) = gst::ElementFactory::make("dwritetextoverlay").build().ok() else { - return; - }; - - configure_stats_overlay_element(&overlay); - overlay.set_property("text", "OpenNOW native stats"); - } - - #[test] - fn parses_basic_remote_offer_sdp() { - let sdp = "v=0\r\no=- 1 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 127.0.0.1\r\na=mid:0\r\na=sctp-port:5000\r\n"; - let parsed = GstreamerPipeline::parse_offer_sdp(sdp).expect("valid SDP"); - assert_eq!(parsed.medias_len(), 1); - } - - #[test] - fn defers_gfn_uuid_ice_password_until_actual_ice_stream_exists() { - let mut pipeline = - GstreamerPipeline::build(None, &[]).expect("GStreamer webrtcbin pipeline"); - let credentials = IceCredentials { - ufrag: "2efecf37".to_owned(), - pwd: "26b335b8-6cb2-4c18-96d0-963e5e586c9a".to_owned(), - fingerprint: String::new(), - }; - - pipeline.original_remote_ice_credentials = Some(credentials); - assert!(!pipeline - .try_restore_original_remote_ice_credentials("without negotiated streams") - .expect("remote ICE credential restoration can be deferred")); - pipeline.stop().expect("pipeline stops"); - } - - #[test] - fn remote_ice_credential_restore_after_remote_description_does_not_probe_fake_streams() { - let mut pipeline = - GstreamerPipeline::build(None, &[]).expect("GStreamer webrtcbin pipeline"); - let sdp = concat!( - "v=0\r\n", - "o=- 4373647202393833435 2 IN IP4 127.0.0.1\r\n", - "s=-\r\n", - "t=0 0\r\n", - "a=group:BUNDLE 0 1 2 3\r\n", - "a=ice-options:trickle\r\n", - "a=ice-lite\r\n", - "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n", - "c=IN IP4 0.0.0.0\r\n", - "a=mid:0\r\n", - "a=ice-ufrag:2efecf37\r\n", - "a=ice-pwd:26b335b899a84ffab9aaf38ddad1e2b4\r\n", - "a=fingerprint:sha-256 94:6C:60:66:35:B9:F6:B4:BC:46:60:EF:81:AC:AB:87:A9:45:4A:09:92:E4:3E:16:28:7E:BD:6D:8C:1A:7D:6B\r\n", - "a=setup:actpass\r\n", - "a=rtcp-mux\r\n", - "a=rtpmap:111 OPUS/48000/2\r\n", - "m=video 9 UDP/TLS/RTP/SAVPF 96\r\n", - "c=IN IP4 0.0.0.0\r\n", - "a=mid:1\r\n", - "a=ice-ufrag:2efecf37\r\n", - "a=ice-pwd:26b335b899a84ffab9aaf38ddad1e2b4\r\n", - "a=fingerprint:sha-256 94:6C:60:66:35:B9:F6:B4:BC:46:60:EF:81:AC:AB:87:A9:45:4A:09:92:E4:3E:16:28:7E:BD:6D:8C:1A:7D:6B\r\n", - "a=setup:actpass\r\n", - "a=rtcp-mux\r\n", - "a=rtpmap:96 H264/90000\r\n", - "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n", - "c=IN IP4 0.0.0.0\r\n", - "a=mid:2\r\n", - "a=ice-ufrag:2efecf37\r\n", - "a=ice-pwd:26b335b899a84ffab9aaf38ddad1e2b4\r\n", - "a=fingerprint:sha-256 94:6C:60:66:35:B9:F6:B4:BC:46:60:EF:81:AC:AB:87:A9:45:4A:09:92:E4:3E:16:28:7E:BD:6D:8C:1A:7D:6B\r\n", - "a=setup:actpass\r\n", - "a=sctp-port:5000\r\n", - "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n", - "c=IN IP4 0.0.0.0\r\n", - "a=mid:3\r\n", - "a=ice-ufrag:2efecf37\r\n", - "a=ice-pwd:26b335b899a84ffab9aaf38ddad1e2b4\r\n", - "a=fingerprint:sha-256 94:6C:60:66:35:B9:F6:B4:BC:46:60:EF:81:AC:AB:87:A9:45:4A:09:92:E4:3E:16:28:7E:BD:6D:8C:1A:7D:6B\r\n", - "a=setup:actpass\r\n", - "a=sctp-port:5000\r\n", - ); - let offer_sdp = GstreamerPipeline::parse_offer_sdp(sdp).expect("valid SDP"); - let offer = - gst_webrtc::WebRTCSessionDescription::new(gst_webrtc::WebRTCSDPType::Offer, offer_sdp); - pipeline - .pipeline - .set_state(gst::State::Playing) - .expect("pipeline plays"); - pipeline - .set_description("set-remote-description", &offer) - .expect("remote description"); - - let credentials = IceCredentials { - ufrag: "2efecf37".to_owned(), - pwd: "26b335b8-99a8-4ffa-b9aa-f38ddad1e2b4".to_owned(), - fingerprint: String::new(), - }; - pipeline.original_remote_ice_credentials = Some(credentials); - pipeline - .try_restore_original_remote_ice_credentials("after remote description") - .expect("remote ICE credential restoration does not fail without actual streams"); - pipeline.stop().expect("pipeline stops"); - } - - #[test] - fn reports_offer_answer_and_local_ice_capabilities() { - let backend = GstreamerBackend::new(None); - let capabilities = backend.capabilities(); - assert!(capabilities.supports_offer_answer); - assert!(capabilities.supports_local_ice); - assert!(capabilities.supports_input); - } - - #[test] - #[cfg(target_os = "linux")] - fn bundles_av1_rtp_depayloading() { - init_gstreamer().expect("GStreamer initializes"); - assert!(gst::ElementFactory::find("rtpav1depay").is_some()); - } - - #[test] - fn parses_input_handshake_versions() { - assert_eq!( - parse_input_handshake_version(&[0x0e, 0x02, 0x03, 0x00]), - Some(3) - ); - assert_eq!(parse_input_handshake_version(&[0x0e, 0x02]), Some(2)); - assert_eq!(parse_input_handshake_version(&[0x0e, 0x03]), Some(0x030e)); - assert_eq!(parse_input_handshake_version(&[0x01, 0x02, 0x03]), None); - assert_eq!(parse_input_handshake_version(&[0x0e]), None); - } - - #[test] - fn maps_rtp_video_codecs_to_explicit_gpu_decode_chains() { - let h265 = - rtp_video_chain_definition("H265", RtpVideoApi::D3D11).expect("H265 D3D11 chain"); - assert_eq!(h265[0].factory, "rtph265depay"); - assert_eq!(h265[3].factory, "d3d11h265dec"); - assert_eq!(h265[4].factory, "dwritetextoverlay"); - assert_eq!(h265[6].factory, "d3d11videosink"); - assert!(!h265 - .iter() - .any(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter)); - - let h264 = - rtp_video_chain_definition("h264", RtpVideoApi::D3D12).expect("H264 D3D12 chain"); - assert_eq!(h264[0].factory, "rtph264depay"); - assert_eq!(h264[3].factory, "d3d12h264dec"); - assert_eq!(h264[4].factory, "dwritetextoverlay"); - assert_eq!(h264[6].factory, "d3d12videosink"); - assert!(!h264 - .iter() - .any(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter)); - - let av1 = rtp_video_chain_definition("AV1", RtpVideoApi::D3D11).expect("AV1 D3D11 chain"); - assert_eq!(av1[0].factory, "rtpav1depay"); - assert_eq!(av1[3].factory, "d3d11av1dec"); - assert_eq!(av1[4].factory, "dwritetextoverlay"); - assert_eq!(av1[6].factory, "d3d11videosink"); - } - - #[test] - fn does_not_force_d3d_memory_caps_by_default() { - let d3d11 = - rtp_video_chain_definition("H265", RtpVideoApi::D3D11).expect("H265 D3D11 chain"); - let d3d12 = - rtp_video_chain_definition("H264", RtpVideoApi::D3D12).expect("H264 D3D12 chain"); - - assert!(!d3d11 - .iter() - .any(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter)); - assert!(!d3d12 - .iter() - .any(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter)); - } - - #[test] - fn maps_cross_platform_video_paths_to_expected_decoders() { - let vt = - rtp_video_chain_definition("H264", RtpVideoApi::VideoToolbox).expect("VideoToolbox"); - assert_eq!(vt[3].factory, "vtdec_hw"); - assert!(vt.iter().any(|spec| spec.factory == "videoconvert")); - assert_eq!(vt.last().map(|spec| spec.factory), Some("glimagesink")); - assert!(!vt.iter().any(|spec| spec.factory == "capsfilter")); - - let vaapi = rtp_video_chain_definition("AV1", RtpVideoApi::Vaapi).expect("VAAPI AV1"); - assert_eq!(vaapi[3].factory, "vaav1dec"); - assert!(vaapi.iter().any(|spec| spec.factory == "videoconvert")); - assert_eq!(vaapi.last().map(|spec| spec.factory), Some("glimagesink")); - - let nvdec = rtp_video_chain_definition("AV1", RtpVideoApi::Nvdec).expect("NVDEC AV1"); - assert_eq!(nvdec[3].factory, "nvav1dec"); - assert!(nvdec.iter().any(|spec| spec.factory == "videoconvert")); - assert_eq!(nvdec.last().map(|spec| spec.factory), Some("glimagesink")); - - let v4l2 = rtp_video_chain_definition("H265", RtpVideoApi::V4L2).expect("V4L2 H265"); - assert_eq!(v4l2[3].factory, "v4l2slh265dec"); - assert!(!v4l2.iter().any(|spec| spec.factory == "videoconvert")); - - let v4l2_av1 = - rtp_video_chain_definition("AV1", RtpVideoApi::V4L2).expect("V4L2 AV1"); - assert_eq!(v4l2_av1[3].factory, "v4l2slav1dec"); - - let vulkan = rtp_video_chain_definition("H265", RtpVideoApi::Vulkan).expect("Vulkan H265"); - #[cfg(target_os = "windows")] - { - assert_eq!(vulkan[3].factory, "d3d12h265dec"); - assert!(!vulkan.iter().any(|spec| spec.factory == "vulkanh265dec")); - // Default Internal renderer: Electron cannot composite vulkansink. - assert_eq!( - vulkan.last().map(|spec| spec.factory), - Some("d3d12videosink") - ); - assert!(vulkan.iter().any(|spec| { - spec.role == RtpVideoChainRole::StatsOverlay && spec.factory == "dwritetextoverlay" - })); - assert!(!vulkan.iter().any(|spec| spec.factory == "vulkanupload")); - } - #[cfg(not(target_os = "windows"))] - { - assert_eq!(vulkan[3].factory, "vulkanh265dec"); - assert!(vulkan - .iter() - .any(|spec| spec.factory == "vulkancolorconvert")); - assert_eq!(vulkan.last().map(|spec| spec.factory), Some("vulkansink")); - } - let vulkan_av1 = - rtp_video_chain_definition("AV1", RtpVideoApi::Vulkan).expect("Vulkan AV1"); - #[cfg(target_os = "windows")] - assert_eq!(vulkan_av1[3].factory, "d3d12av1dec"); - #[cfg(not(target_os = "windows"))] - assert_eq!(vulkan_av1[3].factory, "vulkanav1dec"); - - let software = - rtp_video_chain_definition("H264", RtpVideoApi::Software).expect("software H264"); - assert_eq!(software[3].factory, "avdec_h264"); - assert!(software.iter().any(|spec| spec.factory == "videoconvert")); - assert_eq!( - software.last().map(|spec| spec.factory), - Some("autovideosink") - ); - } - - #[test] - fn exposes_vulkan_on_windows_and_linux_only() { - assert!(backend_runs_on_platform(RtpVideoApi::Vulkan, "windows")); - assert!(backend_runs_on_platform(RtpVideoApi::Vulkan, "linux")); - assert!(!backend_runs_on_platform(RtpVideoApi::Vulkan, "macos")); - assert!(!backend_runs_on_platform(RtpVideoApi::Vulkan, "other")); - } - - #[test] - fn explicit_linux_backend_selection_retains_native_software_fallback() { - assert_eq!( - preferred_rtp_video_apis_for("nvdec", Some(120)), - vec![RtpVideoApi::Nvdec, RtpVideoApi::Software] - ); - assert_eq!( - preferred_rtp_video_apis_for("vaapi", Some(120)), - vec![RtpVideoApi::Vaapi, RtpVideoApi::Software] - ); - assert_eq!( - preferred_rtp_video_apis_for("v4l2", Some(120)), - vec![RtpVideoApi::V4L2, RtpVideoApi::Software] - ); - assert_eq!( - preferred_rtp_video_apis_for("vulkan", Some(240)), - vec![RtpVideoApi::Vulkan, RtpVideoApi::Software] - ); - assert_eq!( - preferred_rtp_video_apis_for("vk", Some(120)), - vec![RtpVideoApi::Vulkan, RtpVideoApi::Software] - ); - } - - #[test] - #[cfg(target_os = "windows")] - fn windows_default_video_api_prefers_d3d12_for_high_fps() { - assert_eq!( - default_rtp_video_api_priority(Some(240)), - vec![ - RtpVideoApi::D3D12, - RtpVideoApi::D3D11, - RtpVideoApi::Software - ] - ); - assert_eq!( - default_rtp_video_api_priority(Some(120)), - vec![ - RtpVideoApi::D3D11, - RtpVideoApi::D3D12, - RtpVideoApi::Software - ] - ); - } - - #[test] - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] - fn linux_arm64_prefers_v4l2_for_raspberry_pi_and_arm_devices() { - assert_eq!( - default_rtp_video_api_priority(Some(60)), - vec![ - RtpVideoApi::V4L2, - RtpVideoApi::Nvdec, - RtpVideoApi::Vaapi, - RtpVideoApi::Vulkan, - RtpVideoApi::Software, - ] - ); - } - - #[test] - #[cfg(all(target_os = "linux", not(target_arch = "aarch64")))] - fn linux_desktop_prefers_vendor_decoders_before_generic_paths() { - assert_eq!( - default_rtp_video_api_priority(Some(120)), - vec![ - RtpVideoApi::Nvdec, - RtpVideoApi::Vaapi, - RtpVideoApi::Vulkan, - RtpVideoApi::V4L2, - RtpVideoApi::Software, - ] - ); - } - - #[test] - fn automatic_present_limiter_targets_d3d_present_paths() { - assert_eq!( - effective_present_max_fps( - PRESENT_LIMITER_AUTO_SENTINEL, - Some(240), - RtpVideoApi::D3D11, - Some(165) - ), - 165 - ); - assert_eq!( - effective_present_max_fps( - PRESENT_LIMITER_AUTO_SENTINEL, - Some(240), - RtpVideoApi::D3D12, - Some(165) - ), - 165 - ); - assert_eq!( - effective_present_max_fps(144, Some(240), RtpVideoApi::D3D12, Some(165)), - 144 - ); - assert_eq!( - effective_present_max_fps(0, Some(240), RtpVideoApi::D3D11, Some(165)), - 0 - ); - assert_eq!( - effective_present_max_fps( - PRESENT_LIMITER_VRR_SENTINEL, - Some(240), - RtpVideoApi::D3D11, - Some(165) - ), - 162 - ); - assert_eq!( - effective_present_max_fps( - PRESENT_LIMITER_VRR_SENTINEL, - Some(120), - RtpVideoApi::D3D11, - Some(165) - ), - 0 - ); - } - - #[test] - fn formats_selected_video_chain_diagnostics() { - let specs = - rtp_video_chain_definition("H264", RtpVideoApi::Software).expect("software H264"); - let message = format_video_chain_selection("H264", RtpVideoApi::Software, &specs); - - assert!(message.contains("backend=software")); - assert!(message.contains("decoder=avdec_h264")); - assert!(message.contains("converter=videoconvert")); - assert!(message.contains("memory=system-memory")); - } - - #[test] - fn extracts_caps_framerate_summary() { - let caps = "video/x-raw(memory:D3D11Memory), format=(string)NV12, framerate=(fraction)240/1; zeroCopyD3D11=true"; - assert_eq!(caps_framerate_summary(caps).as_deref(), Some("240/1")); - assert_eq!(caps_framerate_summary("video/x-raw").as_deref(), None); - } - - #[test] - fn video_stall_tracker_waits_until_threshold() { - let mut tracker = VideoStallTracker::default(); - - assert_eq!(tracker.evaluate(2_499, 0), VideoStallAction::None); - } - - #[test] - fn video_stall_tracker_progresses_recovery_attempts() { - let mut tracker = VideoStallTracker::default(); - - assert_eq!( - tracker.evaluate(2_500, 0), - VideoStallAction::RequestKeyframe { - attempt: 1, - stall_ms: 2_500, - }, - ); - assert_eq!(tracker.evaluate(3_000, 0), VideoStallAction::None); - assert_eq!( - tracker.evaluate(5_000, 0), - VideoStallAction::RequestKeyframe { - attempt: 2, - stall_ms: 5_000, - }, - ); - assert_eq!( - tracker.evaluate(8_000, 0), - VideoStallAction::Resync { - attempt: 3, - stall_ms: 8_000, - }, - ); - assert_eq!( - tracker.evaluate(12_000, 0), - VideoStallAction::PartialFlush { - attempt: 4, - stall_ms: 12_000, - }, - ); - assert_eq!( - tracker.evaluate(16_000, 0), - VideoStallAction::CompleteFlush { - attempt: 5, - stall_ms: 16_000, - }, - ); - assert_eq!( - tracker.evaluate(20_000, 0), - VideoStallAction::Fatal { - attempt: 6, - stall_ms: 20_000, - }, - ); - } - - #[test] - fn video_stall_tracker_resets_after_recovery() { - let mut tracker = VideoStallTracker::default(); - - assert_eq!( - tracker.evaluate(2_500, 0), - VideoStallAction::RequestKeyframe { - attempt: 1, - stall_ms: 2_500, - }, - ); - assert_eq!( - tracker.evaluate(2_600, 2_600), - VideoStallAction::Recovered { stall_ms: 2_600 }, - ); - assert_eq!(tracker.evaluate(3_000, 2_600), VideoStallAction::None); - assert_eq!( - tracker.evaluate(5_100, 2_600), - VideoStallAction::RequestKeyframe { - attempt: 1, - stall_ms: 2_500, - }, - ); - } - - #[test] - fn resolve_queue_mode_prefers_adaptive_for_240_fps_and_vrr_for_cloud_gsync() { - let adaptive = resolve_queue_mode(&StreamSettings { - resolution: "2560x1440".to_owned(), - fps: 240, - max_bitrate_mbps: 75, - codec: VideoCodec::H265, - color_quality: crate::protocol::ColorQuality::TenBit420, - enable_cloud_gsync: false, - native_transition_diagnostics: None, - }); - assert_eq!(adaptive, NativeQueueMode::Adaptive); - - let vrr = resolve_queue_mode(&StreamSettings { - resolution: "2560x1440".to_owned(), - fps: 120, - max_bitrate_mbps: 75, - codec: VideoCodec::H265, - color_quality: crate::protocol::ColorQuality::TenBit420, - enable_cloud_gsync: true, - native_transition_diagnostics: None, - }); - assert_eq!(vrr, NativeQueueMode::Vrr); - } - - #[test] - fn reports_missing_sink_stats_as_unavailable() { - gst::init().expect("gstreamer init"); - let sink = gst::ElementFactory::make("fakesink") - .build() - .expect("fakesink"); - assert_eq!( - sink_stats_summary(&sink), - "sinkStats rendered=0 dropped=0 averageRate=0.0" - ); - } -} diff --git a/native/opennow-streamer/src/gstreamer_config.rs b/native/opennow-streamer/src/gstreamer_config.rs deleted file mode 100644 index 0b08f8284..000000000 --- a/native/opennow-streamer/src/gstreamer_config.rs +++ /dev/null @@ -1,175 +0,0 @@ -// Always compiled so present-policy unit tests run without the optional -// `gstreamer` feature; production callers live behind that feature. -#![allow(dead_code)] - -pub(crate) const EXTERNAL_RENDERER_ENV: &str = "OPENNOW_NATIVE_EXTERNAL_RENDERER"; -pub(crate) const NATIVE_VIDEO_API_ENV: &str = "OPENNOW_NATIVE_VIDEO_API"; -pub(crate) const NATIVE_VIDEO_BACKEND_ENV: &str = "OPENNOW_NATIVE_VIDEO_BACKEND"; -pub(crate) const NATIVE_ZERO_COPY_ENV: &str = "OPENNOW_NATIVE_ZERO_COPY"; -pub(crate) const NATIVE_PRESENT_MAX_FPS_ENV: &str = "OPENNOW_NATIVE_PRESENT_MAX_FPS"; -pub(crate) const NATIVE_D3D_FULLSCREEN_ENV: &str = "OPENNOW_NATIVE_D3D_FULLSCREEN"; -pub(crate) const PRESENT_LIMITER_AUTO_SENTINEL: u32 = u32::MAX; -pub(crate) const PRESENT_LIMITER_VRR_SENTINEL: u32 = u32::MAX - 1; -const VRR_REFRESH_HEADROOM_FPS: u32 = 3; - -pub(crate) fn use_external_renderer_window() -> bool { - std::env::var(EXTERNAL_RENDERER_ENV) - .map(|value| { - !matches!( - value.trim().to_ascii_lowercase().as_str(), - "0" | "false" | "no" | "off" - ) - }) - // Default to the internal child-surface renderer (single Electron window). - .unwrap_or(false) -} - -pub(crate) fn use_internal_renderer() -> bool { - !use_external_renderer_window() -} - -pub(crate) fn requested_video_backend() -> String { - std::env::var(NATIVE_VIDEO_BACKEND_ENV) - .or_else(|_| std::env::var(NATIVE_VIDEO_API_ENV)) - .unwrap_or_else(|_| "auto".to_owned()) - .to_ascii_lowercase() -} - -pub(crate) fn zero_copy_requested() -> bool { - matches!( - std::env::var(NATIVE_ZERO_COPY_ENV) - .unwrap_or_else(|_| "auto".to_owned()) - .to_ascii_lowercase() - .as_str(), - "1" | "true" | "yes" | "forced" - ) -} - -pub(crate) fn resolve_present_max_fps(cloud_gsync_enabled: bool) -> u32 { - if let Ok(value) = std::env::var(NATIVE_PRESENT_MAX_FPS_ENV) { - let value = value.trim().to_ascii_lowercase(); - if value == "0" || value == "off" || value == "false" || value == "unlimited" { - return 0; - } - if value == "auto" { - return PRESENT_LIMITER_AUTO_SENTINEL; - } - if let Ok(fps) = value.parse::() { - return fps; - } - } - if cloud_gsync_enabled { - PRESENT_LIMITER_VRR_SENTINEL - } else { - 0 - } -} - -pub(crate) fn automatic_present_max_fps(requested_fps: u32, display_hz: Option) -> u32 { - display_hz - .filter(|display_hz| *display_hz >= 30 && *display_hz < requested_fps) - .unwrap_or(0) -} - -pub(crate) fn vrr_present_max_fps(requested_fps: u32, display_hz: Option) -> u32 { - display_hz - .filter(|display_hz| *display_hz >= 30 && *display_hz <= requested_fps) - .map(|display_hz| display_hz.saturating_sub(VRR_REFRESH_HEADROOM_FPS)) - .unwrap_or(0) -} - -pub(crate) fn resolve_d3d_fullscreen_sink(cloud_gsync_enabled: bool) -> bool { - resolve_d3d_fullscreen_sink_for( - use_internal_renderer(), - cloud_gsync_enabled, - std::env::var(NATIVE_D3D_FULLSCREEN_ENV).ok(), - ) -} - -/// Pure policy for exclusive D3D fullscreen present. -/// -/// Internal (child HWND) always stays windowed — exclusive fullscreen fights -/// Electron parenting. External may enable it for Cloud G-Sync/VRR, or via -/// `OPENNOW_NATIVE_D3D_FULLSCREEN`. -pub(crate) fn resolve_d3d_fullscreen_sink_for( - internal_renderer: bool, - cloud_gsync_enabled: bool, - env_override: Option, -) -> bool { - if internal_renderer { - return false; - } - - if let Some(value) = env_override { - let value = value.trim().to_ascii_lowercase(); - if matches!(value.as_str(), "1" | "on" | "true" | "yes") { - return true; - } - if matches!(value.as_str(), "0" | "off" | "false" | "no") { - return false; - } - } - - cloud_gsync_enabled -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn automatic_present_limiter_uses_display_refresh_below_requested_fps() { - assert_eq!(automatic_present_max_fps(240, Some(165)), 165); - assert_eq!(automatic_present_max_fps(240, Some(240)), 0); - assert_eq!(automatic_present_max_fps(240, Some(1)), 0); - assert_eq!(automatic_present_max_fps(240, None), 0); - } - - #[test] - fn default_present_policy_is_uncapped_without_vrr() { - assert_eq!(resolve_present_max_fps(false), 0); - assert_eq!( - resolve_present_max_fps(true), - PRESENT_LIMITER_VRR_SENTINEL - ); - } - - #[test] - fn vrr_present_limiter_stays_below_refresh_ceiling() { - assert_eq!(vrr_present_max_fps(240, Some(165)), 162); - assert_eq!(vrr_present_max_fps(165, Some(165)), 162); - assert_eq!(vrr_present_max_fps(120, Some(165)), 0); - assert_eq!(vrr_present_max_fps(240, None), 0); - } - - #[test] - fn internal_renderer_never_enables_exclusive_d3d_fullscreen() { - assert!(!resolve_d3d_fullscreen_sink_for(true, true, None)); - assert!(!resolve_d3d_fullscreen_sink_for( - true, - true, - Some("1".to_owned()) - )); - assert!(!resolve_d3d_fullscreen_sink_for( - true, - false, - Some("on".to_owned()) - )); - } - - #[test] - fn external_renderer_follows_cloud_gsync_and_env_for_d3d_fullscreen() { - assert!(resolve_d3d_fullscreen_sink_for(false, true, None)); - assert!(!resolve_d3d_fullscreen_sink_for(false, false, None)); - assert!(resolve_d3d_fullscreen_sink_for( - false, - false, - Some("1".to_owned()) - )); - assert!(!resolve_d3d_fullscreen_sink_for( - false, - true, - Some("0".to_owned()) - )); - } -} diff --git a/native/opennow-streamer/src/gstreamer_input.rs b/native/opennow-streamer/src/gstreamer_input.rs deleted file mode 100644 index eaa129a98..000000000 --- a/native/opennow-streamer/src/gstreamer_input.rs +++ /dev/null @@ -1,1279 +0,0 @@ -use crate::gstreamer_backend::send_log; -#[cfg(target_os = "windows")] -use crate::gstreamer_platform::win32_renderer_window; -use crate::input::InputEncoder; -#[cfg(target_os = "windows")] -use crate::input::{ - finalize_reliable_single_input_packets, layout_mapped_keyboard_keycode, - layout_mapped_keyboard_scancode, restamp_protocol_v3_outer_timestamp, GamepadInput, - KeyboardPayload, MouseButtonPayload, MouseMovePayload, MouseWheelPayload, - GAMEPAD_MAX_CONTROLLERS, PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL, -}; -use crate::protocol::Event; -#[cfg(target_os = "windows")] -use crate::protocol::NativeStreamerShortcutAction; -use gst::glib; -use gst::prelude::*; -use gstreamer as gst; -use gstreamer_webrtc as gst_webrtc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::Sender; -#[cfg(target_os = "windows")] -use std::sync::mpsc::{self, RecvTimeoutError, TryRecvError}; -#[cfg(target_os = "windows")] -use std::sync::OnceLock; -use std::sync::{Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; -#[cfg(target_os = "windows")] -use std::time::Instant; - -const RELIABLE_INPUT_CHANNEL_LABEL: &str = "input_channel_v1"; -const PARTIALLY_RELIABLE_INPUT_CHANNEL_LABEL: &str = "input_channel_partially_reliable"; -const DEFAULT_PARTIAL_RELIABLE_THRESHOLD_MS: u32 = 300; -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2); -const HEARTBEAT_STOP_POLL_INTERVAL: Duration = Duration::from_millis(50); -#[cfg(target_os = "windows")] -const NATIVE_INPUT_BRIDGE_POLL_INTERVAL: Duration = Duration::from_millis(1); -#[cfg(target_os = "windows")] -const NATIVE_INPUT_DRAIN_MAX_EVENTS: usize = 512; -#[cfg(target_os = "windows")] -const NATIVE_GAMEPAD_POLL_INTERVAL: Duration = Duration::from_millis(4); -#[cfg(target_os = "windows")] -const NATIVE_GAMEPAD_KEEPALIVE_INTERVAL: Duration = Duration::from_millis(100); - -#[cfg(target_os = "windows")] -static NATIVE_INPUT_STARTED_AT: OnceLock = OnceLock::new(); - -#[derive(Clone)] -pub(crate) struct GstreamerInputState { - encoder: Arc>, - pub(crate) ready: Arc, - pub(crate) paused: Arc, - heartbeat_stop: Arc, - heartbeat_thread: Arc>>>, -} - -impl std::fmt::Debug for GstreamerInputState { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("GstreamerInputState") - .field("ready", &self.ready.load(Ordering::SeqCst)) - .field("paused", &self.paused.load(Ordering::SeqCst)) - .finish_non_exhaustive() - } -} - -impl Default for GstreamerInputState { - fn default() -> Self { - Self { - encoder: Arc::new(Mutex::new(InputEncoder::default())), - ready: Arc::new(AtomicBool::new(false)), - paused: Arc::new(AtomicBool::new(false)), - heartbeat_stop: Arc::new(AtomicBool::new(false)), - heartbeat_thread: Arc::new(Mutex::new(None)), - } - } -} - -impl GstreamerInputState { - pub(crate) fn reset(&self) { - self.ready.store(false, Ordering::SeqCst); - self.paused.store(false, Ordering::SeqCst); - if let Ok(mut encoder) = self.encoder.lock() { - encoder.set_protocol_version(2); - encoder.reset_gamepad_sequences(); - } - } - - pub(crate) fn stop_heartbeat(&self) { - self.heartbeat_stop.store(true, Ordering::SeqCst); - let Some(handle) = self - .heartbeat_thread - .lock() - .ok() - .and_then(|mut thread| thread.take()) - else { - return; - }; - - if let Err(error) = handle.join() { - eprintln!("[NativeStreamer] Input heartbeat thread panicked: {error:?}"); - } - } -} - -#[cfg(target_os = "windows")] -#[derive(Debug, Clone, Copy)] -pub(crate) enum NativeWindowInputEvent { - Shortcut { - action: NativeStreamerShortcutAction, - }, - ClipboardPaste, - InputCaptureChanged { - captured: bool, - }, - Key { - pressed: bool, - keycode: u16, - scancode: u16, - modifiers: u16, - timestamp_us: u64, - }, - MouseMove { - dx: i16, - dy: i16, - timestamp_us: u64, - }, - MouseButton { - pressed: bool, - button: u8, - timestamp_us: u64, - }, - MouseWheel { - delta: i16, - timestamp_us: u64, - }, - LockKeysSync { - state: u8, - }, -} - -#[cfg(target_os = "windows")] -enum EncodedNativeInputBatch { - ReliableSingles(Vec>), - MousePacket(Vec), -} - -#[cfg(target_os = "windows")] -mod win32_xinput { - use std::ffi::{c_char, c_void}; - - type Dword = u32; - type Hmodule = *mut c_void; - type XInputGetStateFn = unsafe extern "system" fn(Dword, *mut XInputStateRaw) -> Dword; - - const ERROR_SUCCESS: Dword = 0; - const XINPUT_DLLS: [&str; 3] = ["xinput1_4.dll", "xinput9_1_0.dll", "xinput1_3.dll"]; - - #[repr(C)] - #[derive(Clone, Copy, Default)] - struct XInputGamepadRaw { - buttons: u16, - left_trigger: u8, - right_trigger: u8, - thumb_lx: i16, - thumb_ly: i16, - thumb_rx: i16, - thumb_ry: i16, - } - - #[repr(C)] - #[derive(Clone, Copy, Default)] - struct XInputStateRaw { - packet_number: Dword, - gamepad: XInputGamepadRaw, - } - - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] - pub struct XInputGamepadSnapshot { - pub buttons: u16, - pub left_trigger: u8, - pub right_trigger: u8, - pub left_stick_x: i16, - pub left_stick_y: i16, - pub right_stick_x: i16, - pub right_stick_y: i16, - } - - #[derive(Clone, Copy)] - pub struct XInput { - get_state: XInputGetStateFn, - } - - #[link(name = "kernel32")] - unsafe extern "system" { - fn GetProcAddress(module: Hmodule, proc_name: *const c_char) -> *mut c_void; - fn LoadLibraryW(filename: *const u16) -> Hmodule; - } - - impl XInput { - pub unsafe fn load() -> Option { - for dll in XINPUT_DLLS { - let wide = wide_null(dll); - let module = LoadLibraryW(wide.as_ptr()); - if module.is_null() { - continue; - } - - let address = GetProcAddress(module, b"XInputGetState\0".as_ptr() as *const c_char); - if !address.is_null() { - return Some(Self { - get_state: std::mem::transmute::<*mut c_void, XInputGetStateFn>(address), - }); - } - } - - None - } - - pub unsafe fn get_state(self, controller_id: u32) -> Option { - let mut state = XInputStateRaw::default(); - if (self.get_state)(controller_id, &mut state) != ERROR_SUCCESS { - return None; - } - - Some(XInputGamepadSnapshot { - buttons: state.gamepad.buttons, - left_trigger: apply_trigger_deadzone(state.gamepad.left_trigger), - right_trigger: apply_trigger_deadzone(state.gamepad.right_trigger), - left_stick_x: apply_stick_deadzone(state.gamepad.thumb_lx, 7849), - left_stick_y: apply_stick_deadzone(state.gamepad.thumb_ly, 7849), - right_stick_x: apply_stick_deadzone(state.gamepad.thumb_rx, 8689), - right_stick_y: apply_stick_deadzone(state.gamepad.thumb_ry, 8689), - }) - } - } - - fn wide_null(value: &str) -> Vec { - value.encode_utf16().chain(std::iter::once(0)).collect() - } - - fn apply_trigger_deadzone(value: u8) -> u8 { - if value <= 30 { - 0 - } else { - value - } - } - - fn apply_stick_deadzone(value: i16, deadzone: i16) -> i16 { - if (value as i32).abs() <= deadzone as i32 { - 0 - } else { - value - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct GstreamerInputChannels { - reliable: gst_webrtc::WebRTCDataChannel, - partially_reliable: gst_webrtc::WebRTCDataChannel, -} - -impl GstreamerInputChannels { - pub(crate) fn labels(&self) -> (String, String) { - ( - channel_label(&self.reliable), - channel_label(&self.partially_reliable), - ) - } - - pub(crate) fn send_packet(&self, payload: &[u8], partially_reliable: bool) -> bool { - if payload.is_empty() { - return false; - } - - let channel = if partially_reliable { - if self.partially_reliable.ready_state() != gst_webrtc::WebRTCDataChannelState::Open { - return false; - } - &self.partially_reliable - } else { - &self.reliable - }; - - if channel.ready_state() != gst_webrtc::WebRTCDataChannelState::Open { - return false; - } - - let bytes = glib::Bytes::from_owned(payload.to_vec()); - channel.send_data_full(Some(&bytes)).is_ok() - } -} - -#[cfg(target_os = "windows")] -#[derive(Debug)] -pub(crate) struct NativeWindowInputBridge { - stop: Arc, - input_thread: Option>, - gamepad_thread: Option>, -} - -#[cfg(target_os = "windows")] -impl NativeWindowInputBridge { - pub(crate) fn start( - input_state: GstreamerInputState, - input_channels: GstreamerInputChannels, - event_sender: Option>, - ) -> Self { - let (sender, receiver) = mpsc::channel::(); - unsafe { - win32_renderer_window::set_input_event_sender(Some(sender)); - } - - let stop = Arc::new(AtomicBool::new(false)); - let thread_stop = stop.clone(); - let thread_sender = event_sender.clone(); - let input_thread_state = input_state.clone(); - let input_thread_channels = input_channels.clone(); - let input_thread = thread::spawn(move || { - let mut pending_events = Vec::with_capacity(NATIVE_INPUT_DRAIN_MAX_EVENTS); - send_log( - &thread_sender, - "info", - "Native DX11 window input capture bridge armed.".to_owned(), - ); - - while !thread_stop.load(Ordering::SeqCst) { - pending_events.clear(); - loop { - match receiver.try_recv() { - Ok(event) => pending_events.push(event), - Err(TryRecvError::Empty) => break, - Err(TryRecvError::Disconnected) => return, - } - if pending_events.len() >= NATIVE_INPUT_DRAIN_MAX_EVENTS { - break; - } - } - - if pending_events.is_empty() { - match receiver.recv_timeout(NATIVE_INPUT_BRIDGE_POLL_INTERVAL) { - Ok(event) => pending_events.push(event), - Err(RecvTimeoutError::Timeout) => continue, - Err(RecvTimeoutError::Disconnected) => break, - } - - while pending_events.len() < NATIVE_INPUT_DRAIN_MAX_EVENTS { - match receiver.try_recv() { - Ok(event) => pending_events.push(event), - Err(TryRecvError::Empty) => break, - Err(TryRecvError::Disconnected) => break, - } - } - } - - send_native_window_input_events( - &input_thread_state, - &input_thread_channels, - &thread_sender, - &pending_events, - ); - } - }); - let gamepad_thread = Some(spawn_native_gamepad_thread( - input_state, - input_channels, - event_sender, - stop.clone(), - )); - - Self { - stop, - input_thread: Some(input_thread), - gamepad_thread, - } - } - - pub(crate) fn stop(&mut self) { - self.stop.store(true, Ordering::SeqCst); - unsafe { - win32_renderer_window::release_current_input_capture(); - win32_renderer_window::set_input_event_sender(None); - } - - if let Some(thread) = self.input_thread.take() { - if let Err(error) = thread.join() { - eprintln!("[NativeStreamer] Native window input bridge thread panicked: {error:?}"); - } - } - if let Some(thread) = self.gamepad_thread.take() { - if let Err(error) = thread.join() { - eprintln!("[NativeStreamer] Native XInput gamepad thread panicked: {error:?}"); - } - } - } -} - -#[cfg(target_os = "windows")] -impl Drop for NativeWindowInputBridge { - fn drop(&mut self) { - self.stop(); - } -} - -#[cfg(target_os = "windows")] -fn send_native_window_input_events( - input_state: &GstreamerInputState, - input_channels: &GstreamerInputChannels, - event_sender: &Option>, - events: &[NativeWindowInputEvent], -) { - if events.is_empty() { - return; - } - - // Forward shortcuts and host bridge events immediately (before input readiness check) - // These are local control events and don't need the stream channel - let mut other_events = Vec::new(); - for event in events.iter().copied() { - match event { - NativeWindowInputEvent::Shortcut { action } => { - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::Shortcut { action }); - } - } - NativeWindowInputEvent::ClipboardPaste => { - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::ClipboardPaste); - } - } - NativeWindowInputEvent::InputCaptureChanged { captured } => { - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::InputCaptureChanged { captured }); - } - } - _ => { - other_events.push(event); - } - } - } - - // Only process non-shortcut events if input is ready. - if other_events.is_empty() || !input_state.ready.load(Ordering::SeqCst) { - return; - } - if input_state.paused.load(Ordering::SeqCst) { - other_events.retain(is_native_input_release_event); - if other_events.is_empty() { - return; - } - } - - let mut pending_mouse_move: Option<(i32, i32, u64)> = None; - let mut current_reliable_singles: Vec> = Vec::new(); - let mut input_batches: Vec = Vec::new(); - - { - let Ok(encoder) = input_state.encoder.lock() else { - return; - }; - - let mut flush_current_reliable_singles = |singles: &mut Vec>, - batches: &mut Vec| { - if singles.is_empty() { - return; - } - batches.push(EncodedNativeInputBatch::ReliableSingles(std::mem::take( - singles, - ))); - }; - - for event in other_events.iter().copied() { - if let NativeWindowInputEvent::MouseMove { - dx, - dy, - timestamp_us, - } = event - { - let (pending_dx, pending_dy, pending_timestamp_us) = - pending_mouse_move.get_or_insert((0, 0, timestamp_us)); - *pending_dx = pending_dx.saturating_add(i32::from(dx)); - *pending_dy = pending_dy.saturating_add(i32::from(dy)); - *pending_timestamp_us = timestamp_us; - continue; - } - - if pending_mouse_move.is_some() { - flush_current_reliable_singles( - &mut current_reliable_singles, - &mut input_batches, - ); - collect_pending_mouse_move_packets( - &encoder, - &mut pending_mouse_move, - &mut input_batches, - ); - } - if let Some(payload) = - encode_native_window_input_payload(&encoder, event_sender, event) - { - current_reliable_singles.push(payload); - } - } - - flush_current_reliable_singles( - &mut current_reliable_singles, - &mut input_batches, - ); - collect_pending_mouse_move_packets( - &encoder, - &mut pending_mouse_move, - &mut input_batches, - ); - } - - let send_timestamp_us = native_input_timestamp_us(); - for batch in input_batches { - match batch { - EncodedNativeInputBatch::ReliableSingles(reliable_singles) => { - for payload in finalize_reliable_single_input_packets(&reliable_singles, send_timestamp_us) { - let _ = input_channels.send_packet(&payload, false); - } - } - EncodedNativeInputBatch::MousePacket(mut payload) => { - restamp_protocol_v3_outer_timestamp(&mut payload, send_timestamp_us); - let _ = input_channels.send_packet(&payload, true); - } - } - } -} - -#[cfg(target_os = "windows")] -fn is_native_input_release_event(event: &NativeWindowInputEvent) -> bool { - matches!( - event, - NativeWindowInputEvent::Key { pressed: false, .. } - | NativeWindowInputEvent::MouseButton { pressed: false, .. } - ) -} - -#[cfg(target_os = "windows")] -fn collect_pending_mouse_move_packets( - encoder: &InputEncoder, - pending_mouse_move: &mut Option<(i32, i32, u64)>, - input_batches: &mut Vec, -) { - let Some((mut dx, mut dy, timestamp_us)) = pending_mouse_move.take() else { - return; - }; - - while dx != 0 || dy != 0 { - let chunk_dx = dx.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16; - let chunk_dy = dy.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16; - input_batches.push(EncodedNativeInputBatch::MousePacket(encoder.encode_mouse_move( - MouseMovePayload { - dx: chunk_dx, - dy: chunk_dy, - timestamp_us, - }, - ))); - dx = dx.saturating_sub(i32::from(chunk_dx)); - dy = dy.saturating_sub(i32::from(chunk_dy)); - } -} - -#[cfg(target_os = "windows")] -fn encode_native_window_input_payload( - encoder: &InputEncoder, - event_sender: &Option>, - event: NativeWindowInputEvent, -) -> Option> { - match event { - NativeWindowInputEvent::Shortcut { action } => { - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::Shortcut { action }); - } - None - } - NativeWindowInputEvent::ClipboardPaste => { - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::ClipboardPaste); - } - None - } - NativeWindowInputEvent::InputCaptureChanged { captured } => { - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::InputCaptureChanged { captured }); - } - None - } - NativeWindowInputEvent::Key { - pressed, - keycode, - scancode, - modifiers, - timestamp_us, - } => { - let payload = KeyboardPayload { - keycode: layout_mapped_keyboard_keycode(keycode, scancode), - scancode: layout_mapped_keyboard_scancode(scancode), - modifiers, - timestamp_us, - }; - Some(if pressed { - encoder.encode_key_down(payload) - } else { - encoder.encode_key_up(payload) - }) - } - NativeWindowInputEvent::MouseMove { - dx, - dy, - timestamp_us, - } => Some(encoder.encode_mouse_move(MouseMovePayload { - dx, - dy, - timestamp_us, - })), - NativeWindowInputEvent::MouseButton { - pressed, - button, - timestamp_us, - } => { - let payload = MouseButtonPayload { - button, - timestamp_us, - }; - Some(if pressed { - encoder.encode_mouse_button_down(payload) - } else { - encoder.encode_mouse_button_up(payload) - }) - } - NativeWindowInputEvent::MouseWheel { - delta, - timestamp_us, - } => Some(encoder.encode_mouse_wheel(MouseWheelPayload { - delta, - timestamp_us, - })), - NativeWindowInputEvent::LockKeysSync { state } => { - Some(encoder.encode_lock_keys_sync(state)) - } - } -} - -#[cfg(target_os = "windows")] -#[allow(dead_code)] -fn send_encoded_native_window_input_event( - encoder: &InputEncoder, - input_channels: &GstreamerInputChannels, - event_sender: &Option>, - event: NativeWindowInputEvent, -) { - let Some(payload) = encode_native_window_input_payload(encoder, event_sender, event) else { - return; - }; - - let partially_reliable = matches!( - event, - NativeWindowInputEvent::MouseMove { .. } - ); - let _ = input_channels.send_packet(&payload, partially_reliable); -} - -#[cfg(target_os = "windows")] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -struct NativeGamepadSnapshot { - connected: bool, - buttons: u16, - left_trigger: u8, - right_trigger: u8, - left_stick_x: i16, - left_stick_y: i16, - right_stick_x: i16, - right_stick_y: i16, -} - -#[cfg(target_os = "windows")] -impl NativeGamepadSnapshot { - fn from_xinput(snapshot: win32_xinput::XInputGamepadSnapshot) -> Self { - Self { - connected: true, - buttons: snapshot.buttons, - left_trigger: snapshot.left_trigger, - right_trigger: snapshot.right_trigger, - left_stick_x: snapshot.left_stick_x, - left_stick_y: snapshot.left_stick_y, - right_stick_x: snapshot.right_stick_x, - right_stick_y: snapshot.right_stick_y, - } - } - - fn is_neutral(self) -> bool { - self.buttons == 0 - && self.left_trigger == 0 - && self.right_trigger == 0 - && self.left_stick_x == 0 - && self.left_stick_y == 0 - && self.right_stick_x == 0 - && self.right_stick_y == 0 - } -} - -#[cfg(target_os = "windows")] -fn spawn_native_gamepad_thread( - input_state: GstreamerInputState, - input_channels: GstreamerInputChannels, - event_sender: Option>, - stop: Arc, -) -> JoinHandle<()> { - thread::spawn(move || { - let Some(xinput) = (unsafe { win32_xinput::XInput::load() }) else { - send_log( - &event_sender, - "warn", - "Native XInput gamepad bridge unavailable; controller input will require the web renderer fallback.".to_owned(), - ); - return; - }; - - send_log( - &event_sender, - "info", - "Native XInput gamepad bridge armed.".to_owned(), - ); - - let mut previous = [NativeGamepadSnapshot::default(); GAMEPAD_MAX_CONTROLLERS as usize]; - let mut last_sent = [Instant::now(); GAMEPAD_MAX_CONTROLLERS as usize]; - let mut suppress_until_neutral = [false; GAMEPAD_MAX_CONTROLLERS as usize]; - let mut was_paused = false; - - while !stop.load(Ordering::SeqCst) { - if input_state.ready.load(Ordering::SeqCst) { - let (snapshots, bitmap) = poll_xinput_gamepads(xinput); - - if input_state.paused.load(Ordering::SeqCst) { - if !was_paused { - send_neutral_gamepad_snapshots_for_pause( - &input_state, - &input_channels, - &previous, - &snapshots, - ); - } - previous = snapshots; - for controller_id in 0..GAMEPAD_MAX_CONTROLLERS as usize { - last_sent[controller_id] = Instant::now(); - } - was_paused = true; - thread::sleep(NATIVE_GAMEPAD_POLL_INTERVAL); - continue; - } - - if was_paused { - for controller_id in 0..GAMEPAD_MAX_CONTROLLERS as usize { - let snapshot = snapshots[controller_id]; - suppress_until_neutral[controller_id] = - snapshot.connected && !snapshot.is_neutral(); - previous[controller_id] = snapshot; - last_sent[controller_id] = Instant::now(); - } - } - was_paused = false; - - for controller_id in 0..GAMEPAD_MAX_CONTROLLERS as usize { - let snapshot = snapshots[controller_id]; - if suppress_until_neutral[controller_id] { - previous[controller_id] = snapshot; - last_sent[controller_id] = Instant::now(); - if !snapshot.connected || snapshot.is_neutral() { - suppress_until_neutral[controller_id] = false; - } - continue; - } - let state_changed = snapshot != previous[controller_id]; - let keepalive_due = snapshot.connected - && last_sent[controller_id].elapsed() >= NATIVE_GAMEPAD_KEEPALIVE_INTERVAL; - - if state_changed || keepalive_due { - send_native_gamepad_snapshot( - &input_state, - &input_channels, - controller_id as u8, - bitmap, - snapshot, - ); - last_sent[controller_id] = Instant::now(); - - if snapshot.connected != previous[controller_id].connected { - send_log( - &event_sender, - "info", - format!( - "Native XInput controller {controller_id} {}.", - if snapshot.connected { - "connected" - } else { - "disconnected" - } - ), - ); - } - } - - previous[controller_id] = snapshot; - } - } else { - was_paused = false; - } - - thread::sleep(NATIVE_GAMEPAD_POLL_INTERVAL); - } - }) -} - -#[cfg(target_os = "windows")] -fn poll_xinput_gamepads( - xinput: win32_xinput::XInput, -) -> ([NativeGamepadSnapshot; GAMEPAD_MAX_CONTROLLERS as usize], u16) { - let mut snapshots = [NativeGamepadSnapshot::default(); GAMEPAD_MAX_CONTROLLERS as usize]; - let mut bitmap = 0u16; - - for controller_id in 0..GAMEPAD_MAX_CONTROLLERS as usize { - if let Some(snapshot) = unsafe { xinput.get_state(controller_id as u32) } { - snapshots[controller_id] = NativeGamepadSnapshot::from_xinput(snapshot); - bitmap |= 1 << controller_id; - } - } - - (snapshots, bitmap) -} - -#[cfg(target_os = "windows")] -fn send_neutral_gamepad_snapshots_for_pause( - input_state: &GstreamerInputState, - input_channels: &GstreamerInputChannels, - previous: &[NativeGamepadSnapshot; GAMEPAD_MAX_CONTROLLERS as usize], - current: &[NativeGamepadSnapshot; GAMEPAD_MAX_CONTROLLERS as usize], -) { - let bitmap = previous - .iter() - .zip(current.iter()) - .enumerate() - .fold(0u16, |bitmap, (controller_id, (previous, current))| { - if previous.connected || current.connected { - bitmap | (1 << controller_id) - } else { - bitmap - } - }); - - if bitmap == 0 { - return; - } - - for controller_id in 0..GAMEPAD_MAX_CONTROLLERS as usize { - if (bitmap & (1 << controller_id)) == 0 { - continue; - } - - send_native_gamepad_snapshot( - input_state, - input_channels, - controller_id as u8, - bitmap, - NativeGamepadSnapshot { - connected: true, - ..NativeGamepadSnapshot::default() - }, - ); - } -} - -#[cfg(target_os = "windows")] -fn send_native_gamepad_snapshot( - input_state: &GstreamerInputState, - input_channels: &GstreamerInputChannels, - controller_id: u8, - bitmap: u16, - snapshot: NativeGamepadSnapshot, -) { - if !input_state.ready.load(Ordering::SeqCst) { - return; - } - - let use_partially_reliable = - (PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL & (1_u32 << u32::from(controller_id))) != 0; - let input = GamepadInput { - controller_id, - buttons: snapshot.buttons, - left_trigger: snapshot.left_trigger, - right_trigger: snapshot.right_trigger, - left_stick_x: snapshot.left_stick_x, - left_stick_y: snapshot.left_stick_y, - right_stick_x: snapshot.right_stick_x, - right_stick_y: snapshot.right_stick_y, - connected: snapshot.connected, - timestamp_us: native_input_timestamp_us(), - }; - - let Ok(mut encoder) = input_state.encoder.lock() else { - return; - }; - let mut payload = encoder.encode_gamepad_state(bitmap, input, use_partially_reliable); - drop(encoder); - - restamp_protocol_v3_outer_timestamp(&mut payload, native_input_timestamp_us()); - let _ = input_channels.send_packet(&payload, use_partially_reliable); -} - -#[cfg(target_os = "windows")] -fn native_input_timestamp_us() -> u64 { - NATIVE_INPUT_STARTED_AT - .get_or_init(Instant::now) - .elapsed() - .as_micros() - .min(u128::from(u64::MAX)) as u64 -} - -pub(crate) fn wire_remote_data_channels( - webrtc: &gst::Element, - event_sender: Option>, -) { - webrtc.connect("on-data-channel", false, move |values| { - let Some(channel) = values - .get(1) - .and_then(|value| value.get::().ok()) - else { - send_log( - &event_sender, - "warn", - "GStreamer emitted on-data-channel without a channel.".to_owned(), - ); - return None; - }; - - let label = channel_label(&channel); - send_log( - &event_sender, - "info", - format!( - "Remote WebRTC data channel received: label={}, ordered={}.", - label, - channel.is_ordered() - ), - ); - connect_remote_data_channel_callbacks(&label, &channel, event_sender.clone()); - None - }); -} - -pub(crate) fn create_input_data_channels( - webrtc: &gst::Element, - input_state: GstreamerInputState, - event_sender: Option>, - partial_reliable_threshold_ms: u32, -) -> Result { - let reliable = create_data_channel(webrtc, RELIABLE_INPUT_CHANNEL_LABEL, None)?; - connect_input_channel_callbacks( - RELIABLE_INPUT_CHANNEL_LABEL, - &reliable, - input_state.clone(), - event_sender.clone(), - ); - - let clamped_threshold_ms = if partial_reliable_threshold_ms == 0 { - DEFAULT_PARTIAL_RELIABLE_THRESHOLD_MS - } else { - partial_reliable_threshold_ms.clamp(1, 5000) - }; - let options = gst::Structure::builder("data-channel-options") - .field("ordered", false) - .field("max-packet-lifetime", clamped_threshold_ms as i32) - .build(); - let partially_reliable = create_data_channel( - webrtc, - PARTIALLY_RELIABLE_INPUT_CHANNEL_LABEL, - Some(options), - )?; - connect_input_channel_callbacks( - PARTIALLY_RELIABLE_INPUT_CHANNEL_LABEL, - &partially_reliable, - input_state, - event_sender.clone(), - ); - - send_log( - &event_sender, - "info", - format!( - "Created WebRTC input data channels ({}, {} maxPacketLifeTime={}ms).", - RELIABLE_INPUT_CHANNEL_LABEL, - PARTIALLY_RELIABLE_INPUT_CHANNEL_LABEL, - clamped_threshold_ms - ), - ); - - Ok(GstreamerInputChannels { - reliable, - partially_reliable, - }) -} - -fn create_data_channel( - webrtc: &gst::Element, - label: &'static str, - options: Option, -) -> Result { - let channel = match options { - Some(options) => { - let options = Some(options); - webrtc.emit_by_name::( - "create-data-channel", - &[&label, &options], - ) - } - None => webrtc.emit_by_name::( - "create-data-channel", - &[&label, &None::], - ), - }; - - let actual_label = channel_label(&channel); - if actual_label != label { - return Err(format!( - "GStreamer created data channel with unexpected label: expected {label}, got {actual_label}." - )); - } - - Ok(channel) -} - -fn connect_input_channel_callbacks( - label: &'static str, - channel: &gst_webrtc::WebRTCDataChannel, - input_state: GstreamerInputState, - event_sender: Option>, -) { - let open_sender = event_sender.clone(); - channel.connect_on_open(move |channel| { - send_log( - &open_sender, - "info", - format!( - "Input data channel open: label={}, id={}, ordered={}, maxPacketLifeTime={}.", - label, - channel.id(), - channel.is_ordered(), - channel.max_packet_lifetime() - ), - ); - }); - - let close_sender = event_sender.clone(); - let close_state = input_state.clone(); - channel.connect_on_close(move |_| { - if label == RELIABLE_INPUT_CHANNEL_LABEL { - close_state.ready.store(false, Ordering::SeqCst); - close_state.heartbeat_stop.store(true, Ordering::SeqCst); - } - send_log( - &close_sender, - "info", - format!("Input data channel closed: label={label}."), - ); - }); - - let error_sender = event_sender.clone(); - channel.connect_on_error(move |_, error| { - send_log( - &error_sender, - "warn", - format!("Input data channel error on {label}: {error}."), - ); - }); - - if label == RELIABLE_INPUT_CHANNEL_LABEL { - let data_sender = event_sender.clone(); - let data_state = input_state.clone(); - channel.connect_on_message_data(move |channel, data| { - let Some(bytes) = data else { - return; - }; - handle_input_handshake_message( - channel, - bytes.as_ref(), - data_state.clone(), - data_sender.clone(), - ); - }); - - let string_sender = event_sender.clone(); - let string_state = input_state; - channel.connect_on_message_string(move |channel, message| { - let Some(message) = message else { - return; - }; - handle_input_handshake_message( - channel, - message.as_bytes(), - string_state.clone(), - string_sender.clone(), - ); - }); - } -} - -fn connect_remote_data_channel_callbacks( - label: &str, - channel: &gst_webrtc::WebRTCDataChannel, - event_sender: Option>, -) { - let label = label.to_owned(); - let open_sender = event_sender.clone(); - let open_label = label.clone(); - channel.connect_on_open(move |_| { - send_log( - &open_sender, - "info", - format!("Remote data channel open: label={open_label}."), - ); - }); - - let close_sender = event_sender.clone(); - let close_label = label.clone(); - channel.connect_on_close(move |_| { - send_log( - &close_sender, - "info", - format!("Remote data channel closed: label={close_label}."), - ); - }); - - let error_sender = event_sender; - channel.connect_on_error(move |_, error| { - send_log( - &error_sender, - "warn", - format!("Remote data channel error on {label}: {error}."), - ); - }); -} - -fn handle_input_handshake_message( - channel: &gst_webrtc::WebRTCDataChannel, - bytes: &[u8], - input_state: GstreamerInputState, - event_sender: Option>, -) { - let Some(protocol_version) = parse_input_handshake_version(bytes) else { - return; - }; - - let encoder_version = protocol_version.min(u8::MAX as u16) as u8; - if let Ok(mut encoder) = input_state.encoder.lock() { - encoder.set_protocol_version(encoder_version); - } - let was_ready = input_state.ready.swap(true, Ordering::SeqCst); - if was_ready { - return; - } - - send_log( - &event_sender, - "info", - format!( - "Input handshake complete on {} (protocol v{}).", - channel_label(channel), - protocol_version - ), - ); - if let Some(sender) = event_sender.as_ref() { - let _ = sender.send(Event::InputReady { protocol_version }); - } - start_input_heartbeat(input_state, channel.clone(), event_sender); -} - -pub(crate) fn parse_input_handshake_version(bytes: &[u8]) -> Option { - if bytes.len() < 2 { - return None; - } - - let first_word = u16::from_le_bytes([bytes[0], bytes[1]]); - if first_word == 526 { - return Some(if bytes.len() >= 4 { - u16::from_le_bytes([bytes[2], bytes[3]]) - } else { - 2 - }); - } - - if bytes[0] == 0x0e { - return Some(first_word); - } - - None -} - -fn start_input_heartbeat( - input_state: GstreamerInputState, - channel: gst_webrtc::WebRTCDataChannel, - event_sender: Option>, -) { - let Ok(mut heartbeat_thread) = input_state.heartbeat_thread.lock() else { - send_log( - &event_sender, - "warn", - "Failed to acquire input heartbeat thread lock.".to_owned(), - ); - return; - }; - if heartbeat_thread - .as_ref() - .is_some_and(|thread| !thread.is_finished()) - { - return; - } - if let Some(thread) = heartbeat_thread.take() { - let _ = thread.join(); - } - - input_state.heartbeat_stop.store(false, Ordering::SeqCst); - let encoder = input_state.encoder.clone(); - let stop = input_state.heartbeat_stop.clone(); - let thread_sender = event_sender.clone(); - *heartbeat_thread = Some(thread::spawn(move || { - while !stop.load(Ordering::SeqCst) { - send_input_heartbeat(&channel, &encoder, &thread_sender); - - let mut slept = Duration::ZERO; - while slept < HEARTBEAT_INTERVAL { - if stop.load(Ordering::SeqCst) { - break; - } - let remaining = HEARTBEAT_INTERVAL.saturating_sub(slept); - let interval = remaining.min(HEARTBEAT_STOP_POLL_INTERVAL); - thread::sleep(interval); - slept += interval; - } - } - })); -} - -fn send_input_heartbeat( - channel: &gst_webrtc::WebRTCDataChannel, - encoder: &Arc>, - event_sender: &Option>, -) { - if channel.ready_state() != gst_webrtc::WebRTCDataChannelState::Open { - return; - } - - let Ok(encoder) = encoder.lock() else { - send_log( - event_sender, - "warn", - "Failed to acquire input encoder for heartbeat.".to_owned(), - ); - return; - }; - let bytes = glib::Bytes::from_owned(encoder.encode_heartbeat()); - if let Err(error) = channel.send_data_full(Some(&bytes)) { - send_log( - event_sender, - "warn", - format!("Failed to send input heartbeat: {error}."), - ); - } -} - -pub(crate) fn channel_label(channel: &gst_webrtc::WebRTCDataChannel) -> String { - channel - .label() - .map(|label| label.to_string()) - .unwrap_or_else(|| "".to_owned()) -} diff --git a/native/opennow-streamer/src/gstreamer_liveness.rs b/native/opennow-streamer/src/gstreamer_liveness.rs deleted file mode 100644 index bf6b72de7..000000000 --- a/native/opennow-streamer/src/gstreamer_liveness.rs +++ /dev/null @@ -1,1941 +0,0 @@ -use crate::gstreamer_backend::send_log; -use crate::gstreamer_config::use_external_renderer_window; -use crate::gstreamer_pipeline::{configure_queue, set_property_if_supported}; -use crate::gstreamer_transitions::{ - format_transition_summary, resolve_queue_mode, TransitionSnapshot, TransitionTelemetry, - DEFAULT_VIDEO_QUEUE_DEPTH, -}; -use crate::protocol::{Event, NativeQueueMode, NativeStreamerSessionContext, VideoStallEvent}; -use gst::prelude::*; -use gstreamer as gst; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; -use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::time::{Duration, Instant}; - -pub(crate) const VIDEO_SINK_RATE_LOG_INTERVAL: Duration = Duration::from_secs(1); -const VIDEO_STALL_WARNING_MS: u64 = 2_500; -const VIDEO_STALL_SECOND_ATTEMPT_MS: u64 = 5_000; -const VIDEO_STALL_RESYNC_MS: u64 = 8_000; -const VIDEO_STALL_PARTIAL_FLUSH_MS: u64 = 12_000; -const VIDEO_STALL_COMPLETE_FLUSH_MS: u64 = 16_000; -const VIDEO_STALL_FATAL_MS: u64 = 20_000; -const VIDEO_STALL_MIN_KEYFRAME_REQUEST_MS: u64 = 2_000; -const VIDEO_STARTUP_KEYFRAME_MS: u64 = 2_500; -const VIDEO_STARTUP_RESYNC_MS: u64 = 5_000; -const VIDEO_STARTUP_FATAL_MS: u64 = 8_000; -const VIDEO_LIVENESS_POLL_INTERVAL: Duration = Duration::from_millis(250); - -#[derive(Debug, Clone, Copy, PartialEq)] -pub(crate) struct VideoRateSnapshot { - encoded_kbps: f64, - decoded_fps: f64, - sink_fps: f64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum VideoStallAction { - None, - RequestKeyframe { attempt: u8, stall_ms: u64 }, - Resync { attempt: u8, stall_ms: u64 }, - PartialFlush { attempt: u8, stall_ms: u64 }, - CompleteFlush { attempt: u8, stall_ms: u64 }, - Fatal { attempt: u8, stall_ms: u64 }, - Recovered { stall_ms: u64 }, -} - -#[derive(Debug, Clone)] -pub(crate) struct VideoStallTracker { - in_stall: bool, - stall_started_ms: u64, - last_request_ms: Option, - next_attempt: u8, -} - -impl Default for VideoStallTracker { - fn default() -> Self { - Self { - in_stall: false, - stall_started_ms: 0, - last_request_ms: None, - next_attempt: 1, - } - } -} - -impl VideoStallTracker { - pub(crate) fn evaluate(&mut self, now_ms: u64, last_video_ms: u64) -> VideoStallAction { - let stall_ms = now_ms.saturating_sub(last_video_ms); - if stall_ms < VIDEO_STALL_WARNING_MS { - if self.in_stall { - let recovered_ms = now_ms.saturating_sub(self.stall_started_ms); - *self = Self::default(); - return VideoStallAction::Recovered { - stall_ms: recovered_ms, - }; - } - return VideoStallAction::None; - } - - if !self.in_stall { - self.in_stall = true; - self.stall_started_ms = last_video_ms; - self.next_attempt = 1; - } - - let next_due_ms = match self.next_attempt { - 1 => VIDEO_STALL_WARNING_MS, - 2 => VIDEO_STALL_SECOND_ATTEMPT_MS, - 3 => VIDEO_STALL_RESYNC_MS, - 4 => VIDEO_STALL_PARTIAL_FLUSH_MS, - 5 => VIDEO_STALL_COMPLETE_FLUSH_MS, - 6 => VIDEO_STALL_FATAL_MS, - _ => return VideoStallAction::None, - }; - if stall_ms < next_due_ms { - return VideoStallAction::None; - } - if self - .last_request_ms - .is_some_and(|last| now_ms.saturating_sub(last) < VIDEO_STALL_MIN_KEYFRAME_REQUEST_MS) - { - return VideoStallAction::None; - } - - let attempt = self.next_attempt; - self.next_attempt = self.next_attempt.saturating_add(1); - self.last_request_ms = Some(now_ms); - match attempt { - 1 | 2 => VideoStallAction::RequestKeyframe { attempt, stall_ms }, - 3 => VideoStallAction::Resync { attempt, stall_ms }, - 4 => VideoStallAction::PartialFlush { attempt, stall_ms }, - 5 => VideoStallAction::CompleteFlush { attempt, stall_ms }, - _ => VideoStallAction::Fatal { attempt, stall_ms }, - } - } -} - -#[derive(Debug)] -pub(crate) struct VideoLivenessState { - started_at: Instant, - codec: Mutex, - resolution: Mutex, - hardware_acceleration: Mutex, - memory_mode: Mutex, - caps_framerate: Mutex>, - requested_streaming_features_summary: Mutex, - finalized_streaming_features_summary: Mutex, - transition_telemetry: Mutex, - stats_overlay: Mutex>, - pre_decode_queue: Mutex>, - decoder: Mutex>, - post_decode_queue: Mutex>, - stats_overlay_visible: AtomicBool, - target_bitrate_kbps: AtomicU32, - encoded_bytes_total: AtomicU64, - last_encoded_ms: AtomicU64, - last_decoded_ms: AtomicU64, - last_sink_ms: AtomicU64, - last_audio_ms: AtomicU64, - first_startup_audio_ms: AtomicU64, - decoded_total: AtomicU64, - sink_total: AtomicU64, - zero_copy_d3d11: AtomicBool, - zero_copy_d3d12: AtomicBool, - rtp_video_src_pad: Mutex>, - requested_fps: AtomicU32, - framerate_mismatch_warned: AtomicBool, - transition_flush_escalation_enabled: AtomicBool, - first_encoded_logged: AtomicBool, - startup_keyframe_requested: AtomicBool, - startup_resync_requested: AtomicBool, - startup_fatal_reported: AtomicBool, -} - -impl VideoLivenessState { - fn new() -> Self { - Self { - started_at: Instant::now(), - codec: Mutex::new(String::new()), - resolution: Mutex::new(String::new()), - hardware_acceleration: Mutex::new(String::new()), - memory_mode: Mutex::new("system-memory".to_owned()), - caps_framerate: Mutex::new(None), - requested_streaming_features_summary: Mutex::new("none".to_owned()), - finalized_streaming_features_summary: Mutex::new("none".to_owned()), - transition_telemetry: Mutex::new(TransitionTelemetry::default()), - stats_overlay: Mutex::new(None), - pre_decode_queue: Mutex::new(None), - decoder: Mutex::new(None), - post_decode_queue: Mutex::new(None), - stats_overlay_visible: AtomicBool::new(false), - target_bitrate_kbps: AtomicU32::new(0), - encoded_bytes_total: AtomicU64::new(0), - last_encoded_ms: AtomicU64::new(0), - last_decoded_ms: AtomicU64::new(0), - last_sink_ms: AtomicU64::new(0), - last_audio_ms: AtomicU64::new(0), - first_startup_audio_ms: AtomicU64::new(0), - decoded_total: AtomicU64::new(0), - sink_total: AtomicU64::new(0), - zero_copy_d3d11: AtomicBool::new(false), - zero_copy_d3d12: AtomicBool::new(false), - rtp_video_src_pad: Mutex::new(None), - requested_fps: AtomicU32::new(0), - framerate_mismatch_warned: AtomicBool::new(false), - transition_flush_escalation_enabled: AtomicBool::new(true), - first_encoded_logged: AtomicBool::new(false), - startup_keyframe_requested: AtomicBool::new(false), - startup_resync_requested: AtomicBool::new(false), - startup_fatal_reported: AtomicBool::new(false), - } - } - - fn now_ms(&self) -> u64 { - self.started_at - .elapsed() - .as_millis() - .min(u128::from(u64::MAX)) as u64 - } - - pub(crate) fn configure( - &self, - context: &NativeStreamerSessionContext, - target_bitrate_kbps: u32, - ) { - let settings = &context.settings; - if let Ok(mut codec) = self.codec.lock() { - *codec = settings.codec.as_str().to_owned(); - } - if let Ok(mut resolution) = self.resolution.lock() { - *resolution = settings.resolution.clone(); - } - if let Ok(mut caps_framerate) = self.caps_framerate.lock() { - *caps_framerate = None; - } - if let Ok(mut requested_summary) = self.requested_streaming_features_summary.lock() { - *requested_summary = context - .session - .requested_streaming_features - .as_ref() - .map(|features| features.summary()) - .unwrap_or_else(|| "none".to_owned()); - } - if let Ok(mut finalized_summary) = self.finalized_streaming_features_summary.lock() { - *finalized_summary = context - .session - .finalized_streaming_features - .as_ref() - .map(|features| features.summary()) - .unwrap_or_else(|| "none".to_owned()); - } - if let Ok(mut telemetry) = self.transition_telemetry.lock() { - telemetry.queue_mode = resolve_queue_mode(settings); - telemetry.queue_depth = DEFAULT_VIDEO_QUEUE_DEPTH; - telemetry.queue_depth_changes = 0; - telemetry.present_pacing_changes = 0; - telemetry.partial_flush_count = 0; - telemetry.complete_flush_count = 0; - telemetry.last_transition = None; - } - self.target_bitrate_kbps - .store(target_bitrate_kbps, Ordering::Relaxed); - self.requested_fps.store(settings.fps, Ordering::Relaxed); - self.framerate_mismatch_warned - .store(false, Ordering::Relaxed); - self.first_encoded_logged.store(false, Ordering::Relaxed); - self.first_startup_audio_ms.store(0, Ordering::Relaxed); - self.transition_flush_escalation_enabled.store( - settings - .native_transition_diagnostics - .as_ref() - .map(|diagnostics| !diagnostics.disable_transition_flush_escalation) - .unwrap_or(true), - Ordering::Relaxed, - ); - self.startup_keyframe_requested - .store(false, Ordering::Relaxed); - self.startup_resync_requested - .store(false, Ordering::Relaxed); - self.startup_fatal_reported.store(false, Ordering::Relaxed); - } - - pub(crate) fn update_hardware_acceleration(&self, value: impl Into) { - if let Ok(mut hardware_acceleration) = self.hardware_acceleration.lock() { - *hardware_acceleration = value.into(); - } - } - - pub(crate) fn record_encoded_buffer(&self, size: usize) { - self.last_encoded_ms.store(self.now_ms(), Ordering::Relaxed); - self.encoded_bytes_total - .fetch_add(size as u64, Ordering::Relaxed); - } - - pub(crate) fn record_audio_buffer(&self) { - let now_ms = self.now_ms(); - self.last_audio_ms.store(now_ms, Ordering::Relaxed); - if self.last_sink_ms.load(Ordering::Relaxed) == 0 { - let _ = self.first_startup_audio_ms.compare_exchange( - 0, - now_ms, - Ordering::Relaxed, - Ordering::Relaxed, - ); - } - } - - fn log_first_encoded_once(&self) -> bool { - !self.first_encoded_logged.swap(true, Ordering::Relaxed) - } - - pub(crate) fn set_stats_overlay(&self, overlay: Option) { - if let Some(element) = overlay.as_ref() { - set_property_if_supported( - element, - "visible", - self.stats_overlay_visible.load(Ordering::Relaxed), - ); - } - if let Ok(mut current) = self.stats_overlay.lock() { - *current = overlay; - } - } - - pub(crate) fn set_stats_overlay_visible(&self, visible: bool) { - self.stats_overlay_visible.store(visible, Ordering::Relaxed); - if let Ok(current) = self.stats_overlay.lock() { - if let Some(overlay) = current.as_ref() { - set_property_if_supported(overlay, "visible", visible); - } - } - } - - fn update_stats_overlay_text(&self, text: &str) { - if let Ok(current) = self.stats_overlay.lock() { - if let Some(overlay) = current.as_ref() { - overlay.set_property("text", text); - set_property_if_supported( - overlay, - "visible", - self.stats_overlay_visible.load(Ordering::Relaxed) && !text.is_empty(), - ); - } - } - } - - pub(crate) fn record_decoded_buffer(&self) { - self.last_decoded_ms.store(self.now_ms(), Ordering::Relaxed); - self.decoded_total.fetch_add(1, Ordering::Relaxed); - } - - pub(crate) fn record_sink_buffer(&self) { - self.last_sink_ms.store(self.now_ms(), Ordering::Relaxed); - self.sink_total.fetch_add(1, Ordering::Relaxed); - } - - pub(crate) fn update_caps(&self, caps: &str) { - self.zero_copy_d3d11 - .store(caps.contains("memory:D3D11Memory"), Ordering::Relaxed); - self.zero_copy_d3d12 - .store(caps.contains("memory:D3D12Memory"), Ordering::Relaxed); - if let Ok(mut memory_mode) = self.memory_mode.lock() { - *memory_mode = memory_mode_from_caps(caps).to_owned(); - } - if let Ok(mut caps_framerate) = self.caps_framerate.lock() { - *caps_framerate = caps_framerate_summary(caps); - } - } - - fn zero_copy_d3d11(&self) -> bool { - self.zero_copy_d3d11.load(Ordering::Relaxed) - } - - fn zero_copy_d3d12(&self) -> bool { - self.zero_copy_d3d12.load(Ordering::Relaxed) - } - - fn memory_mode(&self) -> String { - self.memory_mode - .lock() - .map(|value| value.clone()) - .unwrap_or_else(|_| "unknown".to_owned()) - } - - fn zero_copy(&self) -> bool { - is_zero_copy_memory_mode(&self.memory_mode()) - } - - pub(crate) fn set_rtp_video_src_pad(&self, pad: &gst::Pad) { - if let Ok(mut current) = self.rtp_video_src_pad.lock() { - *current = Some(pad.clone()); - } - } - - fn requested_fps(&self) -> Option { - let fps = self.requested_fps.load(Ordering::Relaxed); - (fps > 0).then_some(fps) - } - - fn caps_framerate(&self) -> Option { - self.caps_framerate - .lock() - .ok() - .and_then(|value| value.clone()) - } - - fn warn_framerate_mismatch_once(&self) -> bool { - !self.framerate_mismatch_warned.swap(true, Ordering::Relaxed) - } - - fn rtp_video_src_pad(&self) -> Option { - self.rtp_video_src_pad - .lock() - .ok() - .and_then(|current| current.clone()) - } - - fn queue_mode(&self) -> NativeQueueMode { - self.transition_telemetry - .lock() - .map(|telemetry| telemetry.queue_mode) - .unwrap_or(NativeQueueMode::Auto) - } - - pub(crate) fn set_post_decode_queue(&self, queue: gst::Element) { - if let Ok(mut current) = self.post_decode_queue.lock() { - *current = Some(queue); - } - } - - pub(crate) fn set_pre_decode_queue(&self, queue: gst::Element) { - if let Ok(mut current) = self.pre_decode_queue.lock() { - *current = Some(queue); - } - } - - pub(crate) fn set_decoder(&self, decoder: gst::Element) { - if let Ok(mut current) = self.decoder.lock() { - *current = Some(decoder); - } - } - - fn pre_decode_queue(&self) -> Option { - self.pre_decode_queue - .lock() - .ok() - .and_then(|current| current.clone()) - } - - fn decoder(&self) -> Option { - self.decoder.lock().ok().and_then(|current| current.clone()) - } - - fn set_queue_depth( - &self, - max_buffers: u32, - reason: &str, - event_sender: &Option>, - ) { - let queue = self - .post_decode_queue - .lock() - .ok() - .and_then(|current| current.clone()); - if let Some(queue) = queue.as_ref() { - configure_queue(queue, max_buffers, true); - } - - let mut should_log = false; - if let Ok(mut telemetry) = self.transition_telemetry.lock() { - if telemetry.queue_depth != max_buffers { - telemetry.queue_depth = max_buffers; - telemetry.queue_depth_changes = telemetry.queue_depth_changes.saturating_add(1); - should_log = true; - } - } - - if should_log { - send_log( - event_sender, - "info", - format!("Adjusted native post-decode queue depth to {max_buffers} ({reason})."), - ); - } - } - - fn queue_depth(&self) -> u32 { - self.transition_telemetry - .lock() - .map(|telemetry| telemetry.queue_depth) - .unwrap_or(DEFAULT_VIDEO_QUEUE_DEPTH) - } - - fn record_present_pacing_change(&self) { - if let Ok(mut telemetry) = self.transition_telemetry.lock() { - telemetry.present_pacing_changes = telemetry.present_pacing_changes.saturating_add(1); - } - } - - fn transition_flush_escalation_enabled(&self) -> bool { - self.transition_flush_escalation_enabled - .load(Ordering::Relaxed) - } - - fn transition_telemetry_snapshot(&self) -> TransitionTelemetry { - self.transition_telemetry - .lock() - .map(|telemetry| telemetry.clone()) - .unwrap_or_default() - } - - fn requested_streaming_features_summary(&self) -> String { - self.requested_streaming_features_summary - .lock() - .map(|value| value.clone()) - .unwrap_or_else(|_| "none".to_owned()) - } - - fn finalized_streaming_features_summary(&self) -> String { - self.finalized_streaming_features_summary - .lock() - .map(|value| value.clone()) - .unwrap_or_else(|_| "none".to_owned()) - } - - fn record_transition( - &self, - transition_type: &str, - source: &str, - old_caps: Option, - new_caps: Option, - old_framerate: Option, - new_framerate: Option, - old_memory_mode: Option, - new_memory_mode: Option, - event_sender: &Option>, - ) { - let requested_fps = self.requested_fps(); - let queue_mode = self.queue_mode(); - let render_gap_ms = age_since_ms(self.now_ms(), self.last_sink_ms.load(Ordering::Relaxed)); - let high_fps_risk = requested_fps.is_some_and(|fps| fps >= 240) - && new_framerate - .as_deref() - .is_some_and(|value| value != format!("{}/1", requested_fps.unwrap_or_default())); - let summary = format_transition_summary( - transition_type, - source, - requested_fps, - old_framerate.as_deref(), - new_framerate.as_deref(), - high_fps_risk, - ); - let snapshot = TransitionSnapshot { - transition_type: transition_type.to_owned(), - source: source.to_owned(), - at_ms: self.now_ms(), - old_caps, - new_caps, - old_framerate, - new_framerate: new_framerate.clone(), - old_memory_mode, - new_memory_mode, - render_gap_ms, - requested_fps, - caps_framerate: new_framerate, - high_fps_risk, - queue_mode, - summary: summary.clone(), - }; - - if let Ok(mut telemetry) = self.transition_telemetry.lock() { - telemetry.last_transition = Some(snapshot.clone()); - } - - send_log( - event_sender, - "warn", - format!("Native video transition: {summary}"), - ); - if let Some(event_sender) = event_sender { - let _ = event_sender.send(Event::VideoTransition { - transition: snapshot.to_event(), - }); - } - } - - fn increment_partial_flush_count(&self) { - if let Ok(mut telemetry) = self.transition_telemetry.lock() { - telemetry.partial_flush_count = telemetry.partial_flush_count.saturating_add(1); - } - } - - fn increment_complete_flush_count(&self) { - if let Ok(mut telemetry) = self.transition_telemetry.lock() { - telemetry.complete_flush_count = telemetry.complete_flush_count.saturating_add(1); - } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct VideoLivenessMonitor { - state: Arc, - stop: Arc, - started: Arc, - thread: Arc>>>, -} - -impl Default for VideoLivenessMonitor { - fn default() -> Self { - Self { - state: Arc::new(VideoLivenessState::new()), - stop: Arc::new(AtomicBool::new(false)), - started: Arc::new(AtomicBool::new(false)), - thread: Arc::new(Mutex::new(None)), - } - } -} - -impl VideoLivenessMonitor { - pub(crate) fn configure( - &self, - context: &NativeStreamerSessionContext, - target_bitrate_kbps: u32, - ) { - self.state.configure(context, target_bitrate_kbps); - } - - pub(crate) fn update_hardware_acceleration(&self, value: impl Into) { - self.state.update_hardware_acceleration(value); - } - - pub(crate) fn record_encoded_buffer(&self, size: usize) { - self.state.record_encoded_buffer(size); - } - - pub(crate) fn record_audio_buffer(&self) { - self.state.record_audio_buffer(); - } - - pub(crate) fn set_stats_overlay(&self, overlay: Option) { - self.state.set_stats_overlay(overlay); - } - - pub(crate) fn set_stats_overlay_visible(&self, visible: bool) { - self.state.set_stats_overlay_visible(visible); - } - - pub(crate) fn record_decoded_buffer(&self) { - self.state.record_decoded_buffer(); - } - - pub(crate) fn record_sink_buffer(&self) { - self.state.record_sink_buffer(); - } - - pub(crate) fn update_caps(&self, caps: &str) { - self.state.update_caps(caps); - } - - pub(crate) fn set_rtp_video_src_pad(&self, pad: &gst::Pad) { - self.state.set_rtp_video_src_pad(pad); - } - - pub(crate) fn set_post_decode_queue(&self, queue: gst::Element) { - self.state.set_post_decode_queue(queue); - } - - pub(crate) fn set_pre_decode_queue(&self, queue: gst::Element) { - self.state.set_pre_decode_queue(queue); - } - - pub(crate) fn set_decoder(&self, decoder: gst::Element) { - self.state.set_decoder(decoder); - } - - pub(crate) fn log_first_encoded_once(&self) -> bool { - self.state.log_first_encoded_once() - } - - pub(crate) fn requested_fps(&self) -> Option { - self.state.requested_fps() - } - - pub(crate) fn warn_framerate_mismatch_once(&self) -> bool { - self.state.warn_framerate_mismatch_once() - } - - pub(crate) fn record_present_pacing_change(&self) { - self.state.record_present_pacing_change(); - } - - pub(crate) fn stop_flag(&self) -> Arc { - self.stop.clone() - } - - pub(crate) fn record_transition( - &self, - transition_type: &str, - source: &str, - old_caps: Option, - new_caps: Option, - old_framerate: Option, - new_framerate: Option, - old_memory_mode: Option, - new_memory_mode: Option, - event_sender: &Option>, - ) { - self.state.record_transition( - transition_type, - source, - old_caps, - new_caps, - old_framerate, - new_framerate, - old_memory_mode, - new_memory_mode, - event_sender, - ); - } - - pub(crate) fn start( - &self, - pipeline: gst::Pipeline, - sink: gst::Element, - event_sender: Option>, - ) { - if self.started.swap(true, Ordering::SeqCst) { - return; - } - - self.stop.store(false, Ordering::SeqCst); - let state = self.state.clone(); - let stop = self.stop.clone(); - let thread = thread::spawn(move || { - run_video_liveness_watchdog(state, stop, pipeline, sink, event_sender); - }); - if let Ok(mut slot) = self.thread.lock() { - *slot = Some(thread); - } - } - - pub(crate) fn stop(&self) { - self.stop.store(true, Ordering::SeqCst); - self.started.store(false, Ordering::SeqCst); - let handle = self.thread.lock().ok().and_then(|mut slot| slot.take()); - if let Some(handle) = handle { - let _ = handle.join(); - } - } -} - -fn run_video_liveness_watchdog( - state: Arc, - stop: Arc, - pipeline: gst::Pipeline, - sink: gst::Element, - event_sender: Option>, -) { - let mut tracker = VideoStallTracker::default(); - let mut last_rate_at = Instant::now(); - let mut last_encoded_bytes_total = state.encoded_bytes_total.load(Ordering::Relaxed); - let mut last_decoded_total = state.decoded_total.load(Ordering::Relaxed); - let mut last_sink_total = state.sink_total.load(Ordering::Relaxed); - let mut rates = VideoRateSnapshot { - encoded_kbps: 0.0, - decoded_fps: 0.0, - sink_fps: 0.0, - }; - - while !stop.load(Ordering::SeqCst) { - thread::sleep(VIDEO_LIVENESS_POLL_INTERVAL); - - let elapsed = last_rate_at.elapsed(); - if elapsed >= VIDEO_SINK_RATE_LOG_INTERVAL { - let encoded_bytes_total = state.encoded_bytes_total.load(Ordering::Relaxed); - let decoded_total = state.decoded_total.load(Ordering::Relaxed); - let sink_total = state.sink_total.load(Ordering::Relaxed); - let elapsed_secs = elapsed.as_secs_f64().max(0.001); - let bitrate_kbps = encoded_bytes_total - .saturating_sub(last_encoded_bytes_total) - .saturating_mul(8) as f64 - / elapsed_secs - / 1000.0; - rates = VideoRateSnapshot { - encoded_kbps: bitrate_kbps.max(0.0), - decoded_fps: decoded_total.saturating_sub(last_decoded_total) as f64 / elapsed_secs, - sink_fps: sink_total.saturating_sub(last_sink_total) as f64 / elapsed_secs, - }; - update_native_stats_overlay( - &sink, - &state, - rates.encoded_kbps.round() as u32, - rates, - decoded_total, - sink_total, - ); - emit_native_stats_event( - &event_sender, - &sink, - &state, - rates.encoded_kbps.round() as u32, - rates, - decoded_total, - sink_total, - ); - last_encoded_bytes_total = encoded_bytes_total; - last_decoded_total = decoded_total; - last_sink_total = sink_total; - last_rate_at = Instant::now(); - } - - let last_sink_ms = state.last_sink_ms.load(Ordering::Relaxed); - if last_sink_ms == 0 { - maybe_recover_video_startup(&state, &pipeline, &event_sender); - continue; - } - - let now_ms = state.now_ms(); - let encoded_age_ms = age_since_ms(now_ms, state.last_encoded_ms.load(Ordering::Relaxed)); - let decoded_age_ms = age_since_ms(now_ms, state.last_decoded_ms.load(Ordering::Relaxed)); - let sink_age_ms = age_since_ms(now_ms, last_sink_ms); - let likely_stage = classify_video_stall(encoded_age_ms, decoded_age_ms, sink_age_ms); - let transition_stall = likely_stage == "decode-chain-stalled" - && encoded_age_ms.is_some_and(|age| age <= 1_000); - - match tracker.evaluate(now_ms, last_sink_ms) { - VideoStallAction::None => {} - VideoStallAction::RequestKeyframe { attempt, stall_ms } => { - request_upstream_key_unit(&state, &event_sender); - emit_video_stall_event( - &event_sender, - &sink, - &state, - rates, - attempt, - stall_ms, - false, - ); - } - VideoStallAction::Resync { attempt, stall_ms } => { - request_upstream_key_unit(&state, &event_sender); - emit_video_stall_event( - &event_sender, - &sink, - &state, - rates, - attempt, - stall_ms, - true, - ); - match pipeline.recalculate_latency() { - Ok(()) => send_log( - &event_sender, - "warn", - "Requested GStreamer latency recalculation after native video stall.".to_owned(), - ), - Err(error) => send_log( - &event_sender, - "warn", - format!( - "Failed to request GStreamer latency recalculation after native video stall: {error}." - ), - ), - } - } - VideoStallAction::PartialFlush { attempt, stall_ms } => { - if transition_stall && state.transition_flush_escalation_enabled() { - request_upstream_key_unit(&state, &event_sender); - perform_transition_flush(&state, &event_sender, TransitionFlushKind::Partial); - } - emit_video_stall_event( - &event_sender, - &sink, - &state, - rates, - attempt, - stall_ms, - false, - ); - } - VideoStallAction::CompleteFlush { attempt, stall_ms } => { - if transition_stall && state.transition_flush_escalation_enabled() { - request_upstream_key_unit(&state, &event_sender); - perform_transition_flush(&state, &event_sender, TransitionFlushKind::Complete); - } - emit_video_stall_event( - &event_sender, - &sink, - &state, - rates, - attempt, - stall_ms, - false, - ); - } - VideoStallAction::Fatal { attempt, stall_ms } => { - emit_video_stall_event( - &event_sender, - &sink, - &state, - rates, - attempt, - stall_ms, - false, - ); - send_log( - &event_sender, - "error", - format!( - "Native video stall recovery exhausted after {stall_ms}ms; stage={likely_stage} queueMode={} transitionFlushEscalation={}.", - state.queue_mode().as_str(), - state.transition_flush_escalation_enabled(), - ), - ); - if let Some(event_sender) = &event_sender { - let _ = event_sender.send(Event::Error { - code: "native-video-stall-fatal".to_owned(), - message: format!( - "Native video stall recovery exhausted after {stall_ms}ms ({likely_stage})." - ), - }); - } - } - VideoStallAction::Recovered { stall_ms } => { - if state.queue_depth() > DEFAULT_VIDEO_QUEUE_DEPTH { - state.set_queue_depth( - DEFAULT_VIDEO_QUEUE_DEPTH, - "transition recovery completed", - &event_sender, - ); - } - send_log( - &event_sender, - "info", - format!("Native video recovered after {stall_ms} ms."), - ); - } - } - } -} - -fn maybe_recover_video_startup( - state: &VideoLivenessState, - pipeline: &gst::Pipeline, - event_sender: &Option>, -) { - let now_ms = state.now_ms(); - let last_audio_ms = state.last_audio_ms.load(Ordering::Relaxed); - let first_audio_ms = state.first_startup_audio_ms.load(Ordering::Relaxed); - let last_encoded_ms = state.last_encoded_ms.load(Ordering::Relaxed); - if first_audio_ms == 0 - || last_audio_ms == 0 - || now_ms.saturating_sub(last_audio_ms) > VIDEO_STARTUP_KEYFRAME_MS - { - return; - } - let audio_active_ms = now_ms.saturating_sub(first_audio_ms); - - let decoded_total = state.decoded_total.load(Ordering::Relaxed); - let sink_total = state.sink_total.load(Ordering::Relaxed); - let encoded_age = if last_encoded_ms == 0 { - "never".to_owned() - } else { - format!("{}ms", now_ms.saturating_sub(last_encoded_ms)) - }; - - if audio_active_ms >= VIDEO_STARTUP_KEYFRAME_MS - && !state - .startup_keyframe_requested - .swap(true, Ordering::Relaxed) - { - send_log( - event_sender, - "warn", - format!( - "Native video startup has no rendered frame after {audio_active_ms}ms of active audio; startupAge={now_ms}ms encodedAge={encoded_age} decoded={decoded_total} sink={sink_total}. Requesting keyframe." - ), - ); - request_upstream_key_unit(state, event_sender); - } - - if audio_active_ms >= VIDEO_STARTUP_RESYNC_MS - && !state.startup_resync_requested.swap(true, Ordering::Relaxed) - { - send_log( - event_sender, - "warn", - format!( - "Native video startup still has no rendered frame after {audio_active_ms}ms of active audio; startupAge={now_ms}ms encodedAge={encoded_age} decoded={decoded_total} sink={sink_total}. Requesting keyframe and GStreamer latency resync." - ), - ); - request_upstream_key_unit(state, event_sender); - if let Err(error) = pipeline.recalculate_latency() { - send_log( - event_sender, - "warn", - format!("Failed to resync GStreamer latency during native video startup recovery: {error}."), - ); - } - } - - if audio_active_ms >= VIDEO_STARTUP_FATAL_MS - && !state.startup_fatal_reported.swap(true, Ordering::Relaxed) - { - let (code, failure_stage) = classify_video_startup_failure( - state.encoded_bytes_total.load(Ordering::Relaxed), - decoded_total, - sink_total, - ); - send_log( - event_sender, - "error", - format!( - "Native video startup still has no rendered frame after {audio_active_ms}ms of active audio; stage={failure_stage} startupAge={now_ms}ms encodedAge={encoded_age} decoded={decoded_total} sink={sink_total}." - ), - ); - request_upstream_key_unit(state, event_sender); - if let Some(event_sender) = event_sender { - let _ = event_sender.send(Event::Error { - code: code.to_owned(), - message: format!( - "Native video startup timed out before the first rendered frame ({failure_stage})." - ), - }); - } - } -} - -pub(crate) fn classify_video_startup_failure( - encoded_bytes: u64, - decoded_frames: u64, - sink_frames: u64, -) -> (&'static str, &'static str) { - if encoded_bytes == 0 { - return ("native-video-input-startup-timeout", "RTP video input"); - } - if decoded_frames == 0 { - return ( - "native-video-decoder-startup-timeout", - "video decoder output", - ); - } - if sink_frames == 0 { - return ( - "native-video-renderer-startup-timeout", - "video renderer input", - ); - } - ( - "native-video-presentation-startup-timeout", - "native presentation", - ) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum TransitionFlushKind { - Partial, - Complete, -} - -fn perform_transition_flush( - state: &VideoLivenessState, - event_sender: &Option>, - flush_kind: TransitionFlushKind, -) { - let label = match flush_kind { - TransitionFlushKind::Partial => "partial", - TransitionFlushKind::Complete => "complete", - }; - let mut flushed = Vec::new(); - - if matches!( - flush_kind, - TransitionFlushKind::Partial | TransitionFlushKind::Complete - ) { - if let Some(queue) = state.pre_decode_queue() { - flush_element(&queue); - flushed.push("pre-decode queue"); - } - } - - if matches!(flush_kind, TransitionFlushKind::Complete) { - if let Some(decoder) = state.decoder() { - flush_element(&decoder); - flushed.push("decoder"); - } - } - - if let Some(queue) = state - .post_decode_queue - .lock() - .ok() - .and_then(|current| current.clone()) - { - flush_element(&queue); - flushed.push("post-decode queue"); - } - - if flushed.is_empty() { - send_log( - event_sender, - "warn", - "Cannot flush native transition path because no video branch elements are registered." - .to_owned(), - ); - return; - } - - match flush_kind { - TransitionFlushKind::Partial => { - state.increment_partial_flush_count(); - state.set_queue_depth(2, "transition partial flush", event_sender); - } - TransitionFlushKind::Complete => { - state.increment_complete_flush_count(); - state.set_queue_depth(2, "transition complete flush", event_sender); - } - } - - send_log( - event_sender, - "warn", - format!( - "Performed {label} native transition flush on {}.", - flushed.join(", ") - ), - ); -} - -fn flush_element(element: &gst::Element) { - let _ = element.send_event(gst::event::FlushStart::new()); - let _ = element.send_event(gst::event::FlushStop::new(false)); -} - -fn request_upstream_key_unit(state: &VideoLivenessState, event_sender: &Option>) { - let Some(src_pad) = state.rtp_video_src_pad() else { - send_log( - event_sender, - "warn", - "Unable to request upstream video key unit: no RTP video source pad registered." - .to_owned(), - ); - return; - }; - - let event = gst::event::CustomUpstream::builder( - gst::Structure::builder("GstForceKeyUnit") - .field("all-headers", true) - .build(), - ) - .build(); - - if src_pad.send_event(event) { - send_log( - event_sender, - "debug", - "Requested upstream video key unit via RTP source pad.".to_owned(), - ); - } else { - send_log( - event_sender, - "warn", - "Upstream video key-unit request was not accepted by the RTP source pad.".to_owned(), - ); - } -} - -fn emit_native_stats_event( - event_sender: &Option>, - sink: &gst::Element, - state: &VideoLivenessState, - bitrate_kbps: u32, - rates: VideoRateSnapshot, - frames_decoded: u64, - frames_rendered: u64, -) { - let Some(event_sender) = event_sender else { - return; - }; - - let target_bitrate_kbps = state.target_bitrate_kbps.load(Ordering::Relaxed); - let bitrate_performance_percent = if target_bitrate_kbps > 0 { - (f64::from(bitrate_kbps) / f64::from(target_bitrate_kbps)) * 100.0 - } else { - 0.0 - }; - let codec = state - .codec - .lock() - .map(|codec| codec.clone()) - .unwrap_or_default(); - let resolution = state - .resolution - .lock() - .map(|resolution| resolution.clone()) - .unwrap_or_default(); - let hardware_acceleration = state - .hardware_acceleration - .lock() - .map(|value| value.clone()) - .unwrap_or_default(); - let sink_stats = read_sink_stats(sink); - let telemetry = state.transition_telemetry_snapshot(); - let _ = event_sender.send(Event::Stats { - stats: crate::protocol::NativeStatsEvent { - codec, - resolution, - hardware_acceleration, - requested_fps: state.requested_fps(), - caps_framerate: state.caps_framerate(), - bitrate_kbps, - target_bitrate_kbps, - bitrate_performance_percent, - decoded_fps: rates.decoded_fps, - render_fps: rates.sink_fps, - frames_decoded, - frames_rendered, - frames_pending_to_present: frames_decoded.saturating_sub(frames_rendered), - sink_rendered: sink_stats.rendered, - sink_dropped: sink_stats.dropped, - memory_mode: state.memory_mode(), - zero_copy: state.zero_copy(), - queue_mode: telemetry.queue_mode.as_str().to_owned(), - queue_depth_changes: telemetry.queue_depth_changes, - present_pacing_changes: telemetry.present_pacing_changes, - partial_flush_count: telemetry.partial_flush_count, - complete_flush_count: telemetry.complete_flush_count, - last_transition_type: telemetry - .last_transition - .as_ref() - .map(|transition| transition.transition_type.clone()), - last_transition_at_ms: telemetry - .last_transition - .as_ref() - .map(|transition| transition.at_ms), - last_transition_summary: telemetry - .last_transition - .as_ref() - .map(|transition| transition.summary.clone()), - requested_streaming_features_summary: state.requested_streaming_features_summary(), - finalized_streaming_features_summary: state.finalized_streaming_features_summary(), - zero_copy_d3d11: state.zero_copy_d3d11(), - zero_copy_d3d12: state.zero_copy_d3d12(), - }, - }); -} - -fn update_native_stats_overlay( - sink: &gst::Element, - state: &VideoLivenessState, - bitrate_kbps: u32, - rates: VideoRateSnapshot, - _frames_decoded: u64, - frames_rendered: u64, -) { - let target_bitrate_kbps = state.target_bitrate_kbps.load(Ordering::Relaxed); - let bitrate_performance_percent = if target_bitrate_kbps > 0 { - (f64::from(bitrate_kbps) / f64::from(target_bitrate_kbps)) * 100.0 - } else { - 0.0 - }; - let codec = state - .codec - .lock() - .map(|codec| codec.clone()) - .unwrap_or_default(); - let resolution = state - .resolution - .lock() - .map(|resolution| resolution.clone()) - .unwrap_or_default(); - let hardware_acceleration = state - .hardware_acceleration - .lock() - .map(|value| value.clone()) - .unwrap_or_default(); - let sink_stats = read_sink_stats(sink); - let sink_dropped = sink_stats.dropped.unwrap_or(0); - let sink_rendered = sink_stats.rendered.unwrap_or(frames_rendered); - let sink_total = sink_rendered.saturating_add(sink_dropped); - let drop_percent = if sink_total > 0 { - (sink_dropped as f64 / sink_total as f64) * 100.0 - } else { - 0.0 - }; - let target_mbps = f64::from(target_bitrate_kbps) / 1000.0; - let bitrate_mbps = f64::from(bitrate_kbps) / 1000.0; - let memory_mode = state.memory_mode(); - let memory_path = if state.zero_copy() { - format!("{memory_mode} zero-copy") - } else { - memory_mode - }; - let text = format!( - "{} {} {:.1}/{:.1} Mbps Bit {:.0}%\nDecode {:.0}fps Render {:.0}fps Drop {:.2}% {}", - codec, - resolution, - bitrate_mbps, - target_mbps, - bitrate_performance_percent, - rates.decoded_fps, - rates.sink_fps, - drop_percent, - if hardware_acceleration.is_empty() { - memory_path - } else { - format!("{hardware_acceleration} {memory_path}") - }, - ); - state.update_stats_overlay_text(&text); -} - -fn emit_video_stall_event( - event_sender: &Option>, - sink: &gst::Element, - state: &VideoLivenessState, - rates: VideoRateSnapshot, - recovery_attempt: u8, - stall_ms: u64, - will_resync: bool, -) { - let stats = read_sink_stats(sink); - let now_ms = state.now_ms(); - let last_encoded_ms = state.last_encoded_ms.load(Ordering::Relaxed); - let last_decoded_ms = state.last_decoded_ms.load(Ordering::Relaxed); - let last_sink_ms = state.last_sink_ms.load(Ordering::Relaxed); - let encoded_age_ms = age_since_ms(now_ms, last_encoded_ms); - let decoded_age_ms = age_since_ms(now_ms, last_decoded_ms); - let sink_age_ms = age_since_ms(now_ms, last_sink_ms); - let likely_stage = classify_video_stall(encoded_age_ms, decoded_age_ms, sink_age_ms); - let memory_mode = state.memory_mode(); - let zero_copy = state.zero_copy(); - let telemetry = state.transition_telemetry_snapshot(); - let resync_suffix = if will_resync { - " Requesting keyframe and resyncing GStreamer latency." - } else { - " Requesting keyframe." - }; - send_log( - event_sender, - "warn", - format!( - "Native video stall detected: stall={stall_ms}ms stage={likely_stage} encoded={:.0}kbps decoded={:.1}fps sink={:.1}fps requestedFps={} capsFramerate={} queueMode={} partialFlushes={} completeFlushes={} lastTransition={} ages=encoded:{} decoded:{} sink:{} rendered={} dropped={} memoryMode={} zeroCopy={} zeroCopyD3D11={} zeroCopyD3D12={}. If decoded/sink/rendered counters are still flowing but the visible frame is stale, suspect a server-driven mid-stream transition the native decode/present chain failed to absorb rather than pure RTP loss.{}", - rates.encoded_kbps, - rates.decoded_fps, - rates.sink_fps, - state - .requested_fps() - .map(|value| value.to_string()) - .unwrap_or_else(|| "n/a".to_owned()), - state.caps_framerate().unwrap_or_else(|| "unknown".to_owned()), - telemetry.queue_mode.as_str(), - telemetry.partial_flush_count, - telemetry.complete_flush_count, - telemetry - .last_transition - .as_ref() - .map(|transition| transition.transition_type.as_str()) - .unwrap_or("none"), - format_age_ms(encoded_age_ms), - format_age_ms(decoded_age_ms), - format_age_ms(sink_age_ms), - stats - .rendered - .map(|value| value.to_string()) - .unwrap_or_else(|| "n/a".to_owned()), - stats - .dropped - .map(|value| value.to_string()) - .unwrap_or_else(|| "n/a".to_owned()), - memory_mode.as_str(), - zero_copy, - state.zero_copy_d3d11(), - state.zero_copy_d3d12(), - resync_suffix - ), - ); - if let Some(event_sender) = event_sender { - let _ = event_sender.send(Event::VideoStall(VideoStallEvent { - stall_ms, - encoded_kbps: rates.encoded_kbps, - decoded_fps: rates.decoded_fps, - sink_fps: rates.sink_fps, - encoded_age_ms, - decoded_age_ms, - sink_age_ms, - likely_stage: likely_stage.to_owned(), - sink_rendered: stats.rendered, - sink_dropped: stats.dropped, - memory_mode, - zero_copy, - requested_fps: state.requested_fps(), - caps_framerate: state.caps_framerate(), - queue_mode: telemetry.queue_mode.as_str().to_owned(), - partial_flush_count: telemetry.partial_flush_count, - complete_flush_count: telemetry.complete_flush_count, - last_transition_type: telemetry - .last_transition - .as_ref() - .map(|transition| transition.transition_type.clone()), - last_transition_at_ms: telemetry - .last_transition - .as_ref() - .map(|transition| transition.at_ms), - requested_streaming_features_summary: state.requested_streaming_features_summary(), - finalized_streaming_features_summary: state.finalized_streaming_features_summary(), - zero_copy_d3d11: state.zero_copy_d3d11(), - zero_copy_d3d12: state.zero_copy_d3d12(), - recovery_attempt, - })); - } -} - -fn age_since_ms(now_ms: u64, last_ms: u64) -> Option { - (last_ms != 0).then_some(now_ms.saturating_sub(last_ms)) -} - -fn format_age_ms(age_ms: Option) -> String { - age_ms - .map(|value| format!("{value}ms")) - .unwrap_or_else(|| "n/a".to_owned()) -} - -fn classify_video_stall( - encoded_age_ms: Option, - decoded_age_ms: Option, - sink_age_ms: Option, -) -> &'static str { - const ACTIVE_RECENT_MS: u64 = 1_000; - match (encoded_age_ms, decoded_age_ms, sink_age_ms) { - (Some(encoded), _, _) if encoded > VIDEO_STALL_WARNING_MS => "video-rtp-idle", - (Some(encoded), Some(decoded), _) - if encoded <= ACTIVE_RECENT_MS && decoded > VIDEO_STALL_WARNING_MS => - { - "decode-chain-stalled" - } - (_, Some(decoded), Some(sink)) - if decoded <= ACTIVE_RECENT_MS && sink > VIDEO_STALL_WARNING_MS => - { - "present-chain-stalled" - } - (None, _, _) => "video-rtp-not-observed", - _ => "video-output-stalled", - } -} - -pub(crate) fn watch_audio_activity(sink: &gst::Element, video_liveness: &VideoLivenessMonitor) { - let Some(sink_pad) = sink.static_pad("sink") else { - return; - }; - let monitor = video_liveness.clone(); - sink_pad.add_probe(gst::PadProbeType::BUFFER, move |_pad, _info| { - monitor.record_audio_buffer(); - gst::PadProbeReturn::Ok - }); -} - -pub(crate) fn watch_first_sink_buffer( - sink: &gst::Element, - media_label: &str, - event_sender: &Option>, - streaming_reported: &Arc, -) { - let Some(sink_pad) = sink.static_pad("sink") else { - return; - }; - let sender = event_sender.clone(); - let label = media_label.to_owned(); - let reported = streaming_reported.clone(); - sink_pad.add_probe(gst::PadProbeType::BUFFER, move |pad, _info| { - let caps = pad - .current_caps() - .map(|caps| caps.to_string()) - .unwrap_or_else(|| "unknown caps".to_owned()); - let zero_copy_d3d11 = caps.contains("memory:D3D11Memory"); - let zero_copy_d3d12 = caps.contains("memory:D3D12Memory"); - let memory_mode = memory_mode_from_caps(&caps); - let zero_copy = is_zero_copy_memory_mode(memory_mode); - send_log( - &sender, - "info", - format!( - "First decoded {label} buffer reached native sink; caps={caps}; memoryMode={memory_mode}; zeroCopy={zero_copy}; zeroCopyD3D11={zero_copy_d3d11}; zeroCopyD3D12={zero_copy_d3d12}." - ), - ); - - if label == "video" && !reported.swap(true, Ordering::SeqCst) { - if let Some(event_sender) = &sender { - let message = if use_external_renderer_window() { - "Native video frames reached the external low-latency GStreamer renderer window." - } else { - "Native video frames reached the internal child-surface GStreamer renderer." - }; - let _ = event_sender.send(Event::Status { - status: "streaming", - message: Some(message.to_owned()), - }); - } - } - - gst::PadProbeReturn::Remove - }); -} - -pub(crate) fn watch_rtp_video_bitrate( - pad: &gst::Pad, - video_liveness: VideoLivenessMonitor, - event_sender: &Option>, -) { - let sender = event_sender.clone(); - pad.add_probe(gst::PadProbeType::BUFFER, move |_pad, info| { - if let Some(buffer) = info.buffer() { - video_liveness.record_encoded_buffer(buffer.size()); - if video_liveness.log_first_encoded_once() { - send_log( - &sender, - "info", - format!( - "First encoded RTP video buffer arrived; size={} bytes.", - buffer.size() - ), - ); - } - } - gst::PadProbeReturn::Ok - }); -} - -pub(crate) fn watch_video_sink_rate( - sink: &gst::Element, - event_sender: &Option>, - video_liveness: Option, -) { - let Some(sink_pad) = sink.static_pad("sink") else { - return; - }; - let sink = sink.clone(); - watch_video_pad_rate( - &sink_pad, - "Native video sink rate", - Some(sink), - event_sender, - video_liveness.map(|monitor| (monitor, VideoLivenessPadKind::Sink)), - ); -} - -pub(crate) fn watch_video_decoded_rate( - queue: &gst::Element, - event_sender: &Option>, - video_liveness: Option, -) { - let Some(queue_sink_pad) = queue.static_pad("sink") else { - return; - }; - watch_video_pad_rate( - &queue_sink_pad, - "Native decoded video rate before present queue", - None, - event_sender, - video_liveness.map(|monitor| (monitor, VideoLivenessPadKind::Decoded)), - ); -} - -pub(crate) fn watch_video_caps_transitions( - element: &gst::Element, - source: &'static str, - event_sender: &Option>, - video_liveness: VideoLivenessMonitor, -) { - let Some(src_pad) = element.static_pad("src") else { - return; - }; - let sender = event_sender.clone(); - let monitor = video_liveness.clone(); - let last_caps = Arc::new(Mutex::new(None::)); - let last_framerate = Arc::new(Mutex::new(None::)); - let last_memory_mode = Arc::new(Mutex::new(None::)); - let last_caps_for_probe = last_caps.clone(); - let last_framerate_for_probe = last_framerate.clone(); - let last_memory_mode_for_probe = last_memory_mode.clone(); - - src_pad.add_probe(gst::PadProbeType::BUFFER, move |pad, _info| { - let caps = pad - .current_caps() - .map(|caps| caps.to_string()) - .unwrap_or_else(|| "unknown caps".to_owned()); - let framerate = caps_framerate_summary(&caps); - let memory_mode = Some(memory_mode_from_caps(&caps).to_owned()); - - let Ok(mut old_caps) = last_caps_for_probe.lock() else { - return gst::PadProbeReturn::Ok; - }; - let Ok(mut old_framerate) = last_framerate_for_probe.lock() else { - return gst::PadProbeReturn::Ok; - }; - let Ok(mut old_memory_mode) = last_memory_mode_for_probe.lock() else { - return gst::PadProbeReturn::Ok; - }; - - if old_caps.is_none() { - *old_caps = Some(caps); - *old_framerate = framerate; - *old_memory_mode = memory_mode; - return gst::PadProbeReturn::Ok; - } - - let caps_changed = old_caps.as_ref() != Some(&caps); - let framerate_changed = *old_framerate != framerate; - let memory_changed = *old_memory_mode != memory_mode; - if caps_changed || framerate_changed || memory_changed { - monitor.record_transition( - &format!("{source}-caps-change"), - source, - old_caps.clone(), - Some(caps.clone()), - old_framerate.clone(), - framerate.clone(), - old_memory_mode.clone(), - memory_mode.clone(), - &sender, - ); - *old_caps = Some(caps); - *old_framerate = framerate; - *old_memory_mode = memory_mode; - } - - gst::PadProbeReturn::Ok - }); -} - -pub(crate) fn watch_video_sink_caps_transitions( - sink: &gst::Element, - event_sender: &Option>, - video_liveness: Option, -) { - let Some(monitor) = video_liveness else { - return; - }; - let Some(sink_pad) = sink.static_pad("sink") else { - return; - }; - let sender = event_sender.clone(); - let last_caps = Arc::new(Mutex::new(None::)); - let last_framerate = Arc::new(Mutex::new(None::)); - let last_memory_mode = Arc::new(Mutex::new(None::)); - let last_caps_for_probe = last_caps.clone(); - let last_framerate_for_probe = last_framerate.clone(); - let last_memory_mode_for_probe = last_memory_mode.clone(); - - sink_pad.add_probe(gst::PadProbeType::BUFFER, move |pad, _info| { - let caps = pad - .current_caps() - .map(|caps| caps.to_string()) - .unwrap_or_else(|| "unknown caps".to_owned()); - let framerate = caps_framerate_summary(&caps); - let memory_mode = Some(memory_mode_from_caps(&caps).to_owned()); - - let Ok(mut old_caps) = last_caps_for_probe.lock() else { - return gst::PadProbeReturn::Ok; - }; - let Ok(mut old_framerate) = last_framerate_for_probe.lock() else { - return gst::PadProbeReturn::Ok; - }; - let Ok(mut old_memory_mode) = last_memory_mode_for_probe.lock() else { - return gst::PadProbeReturn::Ok; - }; - - if old_caps.is_none() { - *old_caps = Some(caps); - *old_framerate = framerate; - *old_memory_mode = memory_mode; - return gst::PadProbeReturn::Ok; - } - - let caps_changed = old_caps.as_ref() != Some(&caps); - let framerate_changed = *old_framerate != framerate; - let memory_changed = *old_memory_mode != memory_mode; - if caps_changed || framerate_changed || memory_changed { - monitor.record_transition( - "sink-caps-change", - "sink", - old_caps.clone(), - Some(caps.clone()), - old_framerate.clone(), - framerate.clone(), - old_memory_mode.clone(), - memory_mode.clone(), - &sender, - ); - *old_caps = Some(caps); - *old_framerate = framerate; - *old_memory_mode = memory_mode; - } - - gst::PadProbeReturn::Ok - }); -} - -pub(crate) fn install_present_limiter( - sink: &gst::Element, - present_max_fps: Arc, - event_sender: &Option>, - video_liveness: Option, -) { - let Some(sink_pad) = sink.static_pad("sink") else { - return; - }; - - let sender = event_sender.clone(); - let monitor = video_liveness.clone(); - let state = Arc::new(Mutex::new(PresentLimiterState { - next_present_at: Instant::now(), - last_log_at: Instant::now(), - passed: 0, - dropped: 0, - active_fps: 0, - })); - - sink_pad.add_probe(gst::PadProbeType::BUFFER, move |_pad, _info| { - let target_fps = present_max_fps.load(Ordering::Relaxed); - if target_fps == 0 { - return gst::PadProbeReturn::Ok; - } - - let Ok(mut state) = state.lock() else { - return gst::PadProbeReturn::Ok; - }; - - let now = Instant::now(); - if state.active_fps != target_fps { - state.active_fps = target_fps; - state.next_present_at = now; - state.last_log_at = now; - state.passed = 0; - state.dropped = 0; - if let Some(monitor) = &monitor { - monitor.record_present_pacing_change(); - } - } - - let frame_interval = Duration::from_secs_f64(1.0 / f64::from(target_fps.max(1))); - if now < state.next_present_at { - state.dropped = state.dropped.saturating_add(1); - return gst::PadProbeReturn::Drop; - } - - state.passed = state.passed.saturating_add(1); - while state.next_present_at <= now { - state.next_present_at += frame_interval; - } - let elapsed = state.last_log_at.elapsed(); - if elapsed >= VIDEO_SINK_RATE_LOG_INTERVAL { - let passed = state.passed; - let dropped = state.dropped; - send_log( - &sender, - "debug", - format!( - "Native present limiter: target={target_fps} fps; passed={passed}; dropped={dropped} over {:.1}s.", - elapsed.as_secs_f64() - ), - ); - state.last_log_at = now; - state.passed = 0; - state.dropped = 0; - } - - gst::PadProbeReturn::Ok - }); -} - -#[derive(Debug)] -struct PresentLimiterState { - next_present_at: Instant, - last_log_at: Instant, - passed: u32, - dropped: u32, - active_fps: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum VideoLivenessPadKind { - Decoded, - Sink, -} - -fn watch_video_pad_rate( - pad: &gst::Pad, - label: &'static str, - sink: Option, - event_sender: &Option>, - video_liveness: Option<(VideoLivenessMonitor, VideoLivenessPadKind)>, -) { - let sender = event_sender.clone(); - let state = Arc::new(Mutex::new((Instant::now(), 0u32))); - - pad.add_probe(gst::PadProbeType::BUFFER, move |pad, _info| { - if let Some((monitor, kind)) = &video_liveness { - match kind { - VideoLivenessPadKind::Decoded => monitor.record_decoded_buffer(), - VideoLivenessPadKind::Sink => monitor.record_sink_buffer(), - } - } - - let Ok(mut state) = state.lock() else { - return gst::PadProbeReturn::Ok; - }; - - state.1 = state.1.saturating_add(1); - let elapsed = state.0.elapsed(); - if elapsed >= VIDEO_SINK_RATE_LOG_INTERVAL { - let frames = state.1; - let fps = f64::from(frames) / elapsed.as_secs_f64(); - let caps = pad - .current_caps() - .map(|caps| caps.to_string()) - .unwrap_or_else(|| "unknown caps".to_owned()); - let zero_copy_d3d11 = caps.contains("memory:D3D11Memory"); - let zero_copy_d3d12 = caps.contains("memory:D3D12Memory"); - let memory_mode = memory_mode_from_caps(&caps); - let zero_copy = is_zero_copy_memory_mode(memory_mode); - if let Some((monitor, _)) = &video_liveness { - monitor.update_caps(&caps); - } - let caps_framerate = - caps_framerate_summary(&caps).unwrap_or_else(|| "unknown".to_owned()); - let requested_fps = video_liveness - .as_ref() - .and_then(|(monitor, _)| monitor.requested_fps()); - let requested_fps_summary = requested_fps - .map(|fps| format!("; requestedFps={fps}")) - .unwrap_or_default(); - if let (Some((monitor, _)), Some(requested_fps), Some(caps_framerate_value)) = ( - video_liveness.as_ref(), - requested_fps, - caps_framerate_summary(&caps), - ) { - let expected = format!("{requested_fps}/1"); - if caps_framerate_value != expected && monitor.warn_framerate_mismatch_once() { - monitor.record_transition( - "high-fps-transition-risk", - label, - None, - Some(caps.clone()), - None, - Some(caps_framerate_value.clone()), - None, - Some(memory_mode.to_owned()), - &sender, - ); - send_log( - &sender, - "warn", - format!( - "Native video caps framerate {caps_framerate_value} does not match requestedFps={requested_fps}; this can destabilize high-FPS native playback scheduling and buffer pools." - ), - ); - } - } - let sink_stats = sink - .as_ref() - .map(|sink| format!("; {}", sink_stats_summary(sink))) - .unwrap_or_default(); - - send_log( - &sender, - "debug", - format!( - "{label}: {fps:.1} fps; capsFramerate={caps_framerate}{requested_fps_summary}; memoryMode={memory_mode}; zeroCopy={zero_copy}; zeroCopyD3D11={zero_copy_d3d11}; zeroCopyD3D12={zero_copy_d3d12}{sink_stats}." - ), - ); - - *state = (Instant::now(), 0); - } - - gst::PadProbeReturn::Ok - }); -} - -pub(crate) fn sink_stats_summary(sink: &gst::Element) -> String { - let stats = read_sink_stats(sink); - if !stats.available { - return "sinkStats=unavailable".to_owned(); - } - - format!( - "sinkStats rendered={} dropped={} averageRate={}", - stats - .rendered - .map(|value| value.to_string()) - .unwrap_or_else(|| "n/a".to_owned()), - stats - .dropped - .map(|value| value.to_string()) - .unwrap_or_else(|| "n/a".to_owned()), - stats - .average_rate - .map(|value| format!("{value:.1}")) - .unwrap_or_else(|| "n/a".to_owned()) - ) -} - -#[derive(Debug, Clone, Copy, Default)] -struct VideoSinkStats { - available: bool, - rendered: Option, - dropped: Option, - average_rate: Option, -} - -fn read_sink_stats(sink: &gst::Element) -> VideoSinkStats { - if sink.find_property("stats").is_none() { - return VideoSinkStats::default(); - } - - let stats = sink.property::("stats"); - VideoSinkStats { - available: true, - rendered: stats.get::("rendered").ok(), - dropped: stats.get::("dropped").ok(), - average_rate: stats.get::("average-rate").ok(), - } -} - -pub(crate) fn caps_framerate_summary(caps: &str) -> Option { - let marker = "framerate=(fraction)"; - let start = caps.find(marker)? + marker.len(); - let rest = &caps[start..]; - let semicolon = rest.find(';'); - let comma = rest.find(','); - let end = match (semicolon, comma) { - (Some(left), Some(right)) => left.min(right), - (Some(index), None) | (None, Some(index)) => index, - (None, None) => rest.len(), - }; - Some(rest[..end].trim().to_owned()) -} - -pub(crate) fn memory_mode_from_caps(caps: &str) -> &'static str { - if caps.contains("memory:D3D12Memory") { - "D3D12Memory" - } else if caps.contains("memory:D3D11Memory") { - "D3D11Memory" - } else if caps.contains("memory:VulkanImage") { - "VulkanImage" - } else if caps.contains("memory:VAMemory") { - "VAMemory" - } else if caps.contains("memory:GLMemory") { - "GLMemory" - } else { - "system-memory" - } -} - -pub(crate) fn is_zero_copy_memory_mode(memory_mode: &str) -> bool { - matches!( - memory_mode, - "D3D12Memory" | "D3D11Memory" | "VulkanImage" | "VAMemory" | "GLMemory" - ) -} diff --git a/native/opennow-streamer/src/gstreamer_pipeline.rs b/native/opennow-streamer/src/gstreamer_pipeline.rs deleted file mode 100644 index 72ffe032f..000000000 --- a/native/opennow-streamer/src/gstreamer_pipeline.rs +++ /dev/null @@ -1,3062 +0,0 @@ -use crate::gstreamer_backend::send_log; -use crate::gstreamer_config::{ - automatic_present_max_fps, requested_video_backend, use_external_renderer_window, - use_internal_renderer, vrr_present_max_fps, zero_copy_requested, EXTERNAL_RENDERER_ENV, - NATIVE_D3D_FULLSCREEN_ENV, NATIVE_PRESENT_MAX_FPS_ENV, NATIVE_VIDEO_API_ENV, - NATIVE_VIDEO_BACKEND_ENV, PRESENT_LIMITER_AUTO_SENTINEL, PRESENT_LIMITER_VRR_SENTINEL, -}; -#[cfg(target_os = "windows")] -use crate::gstreamer_input::NativeWindowInputBridge; -use crate::gstreamer_input::{ - create_input_data_channels, wire_remote_data_channels, GstreamerInputChannels, - GstreamerInputState, -}; -use crate::gstreamer_liveness::{ - install_present_limiter, watch_audio_activity, watch_first_sink_buffer, - watch_rtp_video_bitrate, watch_video_caps_transitions, watch_video_decoded_rate, - watch_video_sink_caps_transitions, watch_video_sink_rate, VideoLivenessMonitor, -}; -use crate::gstreamer_platform::{ - primary_display_refresh_hz, release_native_input_capture, start_external_renderer_window_guard, - update_external_renderer_surface, -}; -#[cfg(target_os = "windows")] -use crate::gstreamer_platform::arm_internal_child_input; -use crate::gstreamer_transitions::DEFAULT_VIDEO_QUEUE_DEPTH; -use crate::internal_renderer::InternalRenderer; -use crate::nvst_video::{ - annexb_appsrc_caps, spawn_nvst_udp_receive, NvstVideoReceiveHandle, -}; -use crate::protocol::{ - Event, IceCandidatePayload, IceServer, NativeRenderSurface, NativeStreamerSessionContext, - NativeVideoBackendCapability, NativeVideoCodecCapability, NvstVideoSession, -}; -use crate::sdp::IceCredentials; -use gst::glib; -use gst::prelude::*; -use gstreamer as gst; -use gstreamer_sdp as gst_sdp; -use gstreamer_webrtc as gst_webrtc; -use std::collections::{HashMap, HashSet}; -use std::ffi::CString; -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use std::sync::mpsc::Sender; -use std::sync::{Arc, Mutex, OnceLock}; -use std::thread; - -const WEBRTC_LATENCY_MS: u32 = 2; -const DEFAULT_GFN_STUN_SERVER: &str = "stun://stun2.l.google.com:19302"; -const VIDEO_COMPRESSED_QUEUE_MAX_BUFFERS: u32 = 6; -pub(crate) const VIDEO_QUEUE_MAX_BUFFERS: u32 = DEFAULT_VIDEO_QUEUE_DEPTH; -const AUDIO_QUEUE_MAX_BUFFERS: u32 = 2; - -// gstreamer-rs exposes the generic ICE transport but not the NICE stream that -// owns remote credentials. GFN uses UUID ICE passwords, so we need the actual -// NICE stream after GStreamer's SDP parser validates a sanitized copy. -#[repr(C)] -struct GstWebRTCNiceTransportCompat { - parent: gst_webrtc::ffi::GstWebRTCICETransport, - stream: *mut gst_webrtc::ffi::GstWebRTCICEStream, - _priv: glib::ffi::gpointer, -} - -#[derive(Debug, Clone, Copy)] -struct ActualNiceIceStream { - ptr: *mut gst_webrtc::ffi::GstWebRTCICEStream, - stream_id: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum DecodedMediaKind { - Audio, - Video, - Unknown, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum RtpVideoChainRole { - Depayloader, - Parser, - PreDecodeQueue, - Decoder, - PostDecodeRateSetter, - PostDecodeConverter, - PostDecodeCapsFilter, - StatsOverlay, - PostDecodeQueue, - Sink, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum RtpVideoApi { - D3D11, - D3D12, - VideoToolbox, - Nvdec, - Vaapi, - V4L2, - Vulkan, - Software, -} - -impl RtpVideoApi { - fn label(self) -> &'static str { - match self { - Self::D3D11 => "D3D11", - Self::D3D12 => "D3D12", - Self::VideoToolbox => "VideoToolbox", - Self::Nvdec => "NVIDIA NVDEC", - Self::Vaapi => "VAAPI", - Self::V4L2 => "V4L2", - Self::Vulkan => "Vulkan", - Self::Software => "software", - } - } - - fn capability_id(self) -> &'static str { - match self { - Self::D3D11 => "d3d11", - Self::D3D12 => "d3d12", - Self::VideoToolbox => "videotoolbox", - Self::Nvdec => "nvdec", - Self::Vaapi => "vaapi", - Self::V4L2 => "v4l2", - Self::Vulkan => "vulkan", - Self::Software => "software", - } - } - - fn platform(self) -> &'static str { - match self { - Self::D3D11 | Self::D3D12 => "windows", - Self::VideoToolbox => "macos", - Self::Nvdec | Self::Vaapi | Self::V4L2 => "linux", - Self::Vulkan if current_platform_label() == "windows" => "windows", - Self::Vulkan => "linux", - Self::Software => "cross-platform", - } - } - - fn memory_caps(self) -> Option<&'static str> { - match self { - // D3D decoders and sinks can negotiate GPU memory directly. Keep - // the capsfilter opt-in so startup does not fail when a live RTP - // stream's raw caps are still settling. - Self::D3D11 => zero_copy_requested().then_some("video/x-raw(memory:D3D11Memory)"), - Self::D3D12 => zero_copy_requested().then_some("video/x-raw(memory:D3D12Memory)"), - Self::VideoToolbox => zero_copy_requested().then_some("video/x-raw(memory:GLMemory)"), - Self::Vaapi => zero_copy_requested().then_some("video/x-raw(memory:VAMemory)"), - // Linux: keep Vulkan images in-GPU. Windows uses a DXVA→upload hybrid - // (vulkanh264dec currently SIGSEGVs on NVIDIA Windows), so skip a hard - // VulkanImage capsfilter on that path. - Self::Vulkan if cfg!(target_os = "windows") => None, - Self::Vulkan => Some("video/x-raw(memory:VulkanImage)"), - _ => None, - } - } - - fn post_decode_converter_factory(self) -> Option<&'static str> { - match self { - Self::D3D11 | Self::D3D12 => None, - // Windows Vulkan present chain inserts download/convert/upload explicitly. - Self::Vulkan if cfg!(target_os = "windows") => None, - Self::Vulkan => Some("vulkancolorconvert"), - Self::VideoToolbox | Self::Vaapi if zero_copy_requested() => None, - // Non-D3D hardware decoders are not guaranteed to negotiate directly with every - // platform sink. Keep these paths reliable with an explicit raw-video conversion stage. - Self::VideoToolbox | Self::Nvdec | Self::Vaapi | Self::Software => Some("videoconvert"), - // V4L2 stateless decoders expose DMABuf on devices such as Raspberry Pi. - // Let glimagesink import it directly instead of forcing a CPU copy. - Self::V4L2 => None, - } - } - - fn stats_overlay_factory(self) -> Option<&'static str> { - match self { - Self::D3D11 | Self::D3D12 => Some("dwritetextoverlay"), - _ => None, - } - } - - fn sink_factory(self) -> &'static str { - match self { - Self::D3D11 => "d3d11videosink", - Self::D3D12 => "d3d12videosink", - Self::VideoToolbox => "glimagesink", - Self::Nvdec => "glimagesink", - Self::Vaapi => "glimagesink", - Self::V4L2 => "glimagesink", - Self::Vulkan => "vulkansink", - Self::Software => "autovideosink", - } - } - - fn decoder_factory(self, codec: &str) -> Option<&'static str> { - match (self, codec) { - (Self::D3D11, "H265" | "HEVC") => Some("d3d11h265dec"), - (Self::D3D11, "H264") => Some("d3d11h264dec"), - (Self::D3D11, "AV1") => Some("d3d11av1dec"), - (Self::D3D12, "H265" | "HEVC") => Some("d3d12h265dec"), - (Self::D3D12, "H264") => Some("d3d12h264dec"), - (Self::D3D12, "AV1") => Some("d3d12av1dec"), - (Self::VideoToolbox, "H265" | "HEVC" | "H264") => Some("vtdec_hw"), - (Self::Nvdec, "H265" | "HEVC") => Some("nvh265dec"), - (Self::Nvdec, "H264") => Some("nvh264dec"), - (Self::Nvdec, "AV1") => Some("nvav1dec"), - (Self::Vaapi, "H265" | "HEVC") => Some("vah265dec"), - (Self::Vaapi, "H264") => Some("vah264dec"), - (Self::Vaapi, "AV1") => Some("vaav1dec"), - (Self::V4L2, "H265" | "HEVC") => Some("v4l2slh265dec"), - (Self::V4L2, "H264") => Some("v4l2slh264dec"), - (Self::V4L2, "AV1") => Some("v4l2slav1dec"), - // vulkanh264dec/vulkanh265dec SIGSEGV on current NVIDIA Windows drivers; - // use DXVA decode (prefer D3D12) and either D3D present (Internal) or - // upload into Vulkan (External). - (Self::Vulkan, "H265" | "HEVC") if cfg!(target_os = "windows") => Some("d3d12h265dec"), - (Self::Vulkan, "H264") if cfg!(target_os = "windows") => Some("d3d12h264dec"), - (Self::Vulkan, "AV1") if cfg!(target_os = "windows") => Some("d3d12av1dec"), - (Self::Vulkan, "H265" | "HEVC") => Some("vulkanh265dec"), - (Self::Vulkan, "H264") => Some("vulkanh264dec"), - (Self::Vulkan, "AV1") => Some("vulkanav1dec"), - (Self::Software, "H265" | "HEVC") => Some("avdec_h265"), - (Self::Software, "H264") => Some("avdec_h264"), - (Self::Software, "AV1") => Some("avdec_av1"), - _ => None, - } - } - - fn fallback_decoder_factories(self, codec: &str) -> &'static [&'static str] { - match (self, codec) { - (Self::Vaapi, "H265" | "HEVC") => &["vaapih265dec"], - (Self::Vaapi, "H264") => &["vaapih264dec"], - (Self::Vaapi, "AV1") => &["vaapiav1dec"], - (Self::V4L2, "H265" | "HEVC") => &["v4l2h265dec"], - (Self::V4L2, "H264") => &["v4l2h264dec"], - (Self::V4L2, "AV1") => &["v4l2av1dec"], - (Self::VideoToolbox, "H265" | "HEVC" | "H264") => &["vtdec"], - (Self::Vulkan, "H265" | "HEVC") if cfg!(target_os = "windows") => { - &["d3d11h265dec", "nvh265dec"] - } - (Self::Vulkan, "H264") if cfg!(target_os = "windows") => { - &["d3d11h264dec", "nvh264dec"] - } - (Self::Vulkan, "AV1") if cfg!(target_os = "windows") => { - &["d3d11av1dec", "nvav1dec"] - } - (Self::Software, "AV1") => &["dav1ddec", "av1dec"], - _ => &[], - } - } - - fn sink_fallback_factories(self) -> &'static [&'static str] { - match self { - Self::VideoToolbox => &["osxvideosink", "autovideosink"], - // Prefer X11-capable sinks first: Internal Linux embeds via GstVideoOverlay - // into an X11 child. waylandsink cannot paint into that handle. - Self::Nvdec | Self::Vaapi | Self::V4L2 => { - &["ximagesink", "xvimagesink", "glimagesink", "waylandsink", "autovideosink"] - } - Self::Software => &["ximagesink", "xvimagesink", "glimagesink", "waylandsink"], - _ => &[], - } - } - - /// Sinks that can bind to the Internal X11 child via GstVideoOverlay. - fn internal_x11_sink_candidates(self) -> &'static [&'static str] { - match self { - Self::Nvdec | Self::Vaapi | Self::V4L2 | Self::Software => { - &["glimagesink", "ximagesink", "xvimagesink"] - } - // vulkansink implements GstVideoOverlay on Linux, so it can bind - // directly to the X11 child while retaining VulkanImage memory. - Self::Vulkan => &["vulkansink"], - _ => &[], - } - } - - fn is_gpu_path(self) -> bool { - !matches!(self, Self::Software) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct RtpVideoChainSpec { - pub(crate) factory: &'static str, - pub(crate) role: RtpVideoChainRole, - pub(crate) caps: Option, -} - -impl RtpVideoChainSpec { - fn new(factory: &'static str, role: RtpVideoChainRole) -> Self { - Self { - factory, - role, - caps: None, - } - } - - fn with_caps(factory: &'static str, role: RtpVideoChainRole, caps: impl Into) -> Self { - Self { - factory, - role, - caps: Some(caps.into()), - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct GstreamerRenderState { - surface: Arc>>, - video_sink: Arc>>, - internal_renderer: Arc, - external_renderer_logged: Arc, - internal_renderer_logged: Arc, - external_window_guard_started: Arc, - external_window_guard_stop: Arc, -} - -impl Default for GstreamerRenderState { - fn default() -> Self { - Self { - surface: Arc::new(Mutex::new(None)), - video_sink: Arc::new(Mutex::new(None)), - internal_renderer: Arc::new(InternalRenderer::new()), - external_renderer_logged: Arc::new(AtomicBool::new(false)), - internal_renderer_logged: Arc::new(AtomicBool::new(false)), - external_window_guard_started: Arc::new(AtomicBool::new(false)), - external_window_guard_stop: Arc::new(AtomicBool::new(false)), - } - } -} - -impl GstreamerRenderState { - fn set_surface( - &self, - surface: NativeRenderSurface, - event_sender: &Option>, - ) -> Result<(), String> { - if let Ok(mut current) = self.surface.lock() { - *current = Some(surface); - } - self.apply(event_sender) - } - - fn set_video_sink( - &self, - sink: gst::Element, - event_sender: &Option>, - ) -> Result<(), String> { - if let Ok(mut current) = self.video_sink.lock() { - *current = Some(sink.clone()); - } - if use_internal_renderer() { - self.internal_renderer.set_video_sink(sink)?; - } - self.apply(event_sender) - } - - fn apply(&self, event_sender: &Option>) -> Result<(), String> { - if use_external_renderer_window() { - let sink_ready = self.video_sink.lock().ok().and_then(|sink| sink.clone()); - let Some(_sink) = sink_ready else { - return Ok(()); - }; - - if let Some(surface) = self.surface.lock().ok().and_then(|surface| surface.clone()) { - update_external_renderer_surface(&surface); - } - if !self - .external_window_guard_started - .swap(true, Ordering::SeqCst) - { - self.external_window_guard_stop - .store(false, Ordering::SeqCst); - start_external_renderer_window_guard( - event_sender.clone(), - self.external_window_guard_stop.clone(), - ); - } - if !self.external_renderer_logged.swap(true, Ordering::SeqCst) { - send_log( - event_sender, - "info", - format!( - "Using external native GStreamer renderer window; set {EXTERNAL_RENDERER_ENV}=0 for the internal child-surface renderer." - ), - ); - } - return Ok(()); - } - - let surface = self.surface.lock().ok().and_then(|surface| surface.clone()); - let Some(surface) = surface else { - return Ok(()); - }; - - if !self.internal_renderer_logged.swap(true, Ordering::SeqCst) { - send_log( - event_sender, - "info", - format!( - "Using internal native child-surface renderer; set {EXTERNAL_RENDERER_ENV}=1 for the floating GStreamer window." - ), - ); - } - - self.internal_renderer.apply_surface(&surface)?; - - // Keep ClipCursor / capture rect aligned with the StreamView hole, and - // (re)arm RawInput if the child HWND was recreated on parent change. - #[cfg(target_os = "windows")] - { - update_external_renderer_surface(&surface); - let hwnd = self.internal_renderer.child_handle(); - if hwnd != 0 { - let _ = arm_internal_child_input(hwnd); - } - } - - Ok(()) - } - - fn stop_external_renderer_window_guard(&self) { - self.external_window_guard_stop - .store(true, Ordering::SeqCst); - self.external_window_guard_started - .store(false, Ordering::SeqCst); - } - - fn destroy_internal_renderer(&self) { - self.internal_renderer.destroy(); - self.internal_renderer_logged - .store(false, Ordering::SeqCst); - } -} - -#[derive(Debug)] -pub(crate) struct GstreamerPipeline { - pub(crate) pipeline: gst::Pipeline, - pub(crate) webrtc: gst::Element, - input_state: GstreamerInputState, - input_channels: Option, - #[cfg(target_os = "windows")] - native_window_input_bridge: Option, - render_state: GstreamerRenderState, - present_max_fps: Arc, - d3d_fullscreen_sink: Arc, - /// When true, WebRTC RTP video pads are ignored (classic NVST UDP owns video). - skip_webrtc_video: Arc, - nvst_receive: Option, - video_liveness: VideoLivenessMonitor, - event_sender: Option>, - pub(crate) original_remote_ice_credentials: Option, -} - -impl GstreamerPipeline { - pub(crate) fn build( - event_sender: Option>, - ice_servers: &[IceServer], - ) -> Result { - init_gstreamer()?; - - let pipeline = gst::Pipeline::new(); - let webrtc = gst::ElementFactory::make("webrtcbin") - .name("opennow-webrtcbin") - .property_from_str("bundle-policy", "max-bundle") - .build() - .map_err(|error| format!("Failed to create webrtcbin: {error}"))?; - configure_webrtc_low_latency(&webrtc); - let stun_server = resolve_gstreamer_stun_server(ice_servers); - webrtc.set_property("stun-server", &stun_server); - send_log( - &event_sender, - "info", - format!("Configured GStreamer ICE with STUN server {stun_server}."), - ); - - let input_state = GstreamerInputState::default(); - let render_state = GstreamerRenderState::default(); - let video_liveness = VideoLivenessMonitor::default(); - wire_local_ice_events(&webrtc, event_sender.clone())?; - wire_webrtc_state_events(&webrtc, event_sender.clone()); - wire_remote_data_channels(&webrtc, event_sender.clone()); - start_gstreamer_bus_diagnostics( - &pipeline, - event_sender.clone(), - video_liveness.stop_flag(), - video_liveness.clone(), - ); - let present_max_fps = Arc::new(AtomicU32::new(0)); - let d3d_fullscreen_sink = Arc::new(AtomicBool::new(false)); - let skip_webrtc_video = Arc::new(AtomicBool::new(false)); - wire_incoming_media_sink( - &pipeline, - &webrtc, - event_sender.clone(), - render_state.clone(), - present_max_fps.clone(), - d3d_fullscreen_sink.clone(), - skip_webrtc_video.clone(), - video_liveness.clone(), - ); - - pipeline - .add(&webrtc) - .map_err(|error| format!("Failed to add webrtcbin to pipeline: {error}"))?; - pipeline - .set_state(gst::State::Ready) - .map_err(|error| format!("Failed to set GStreamer pipeline to Ready: {error:?}"))?; - - Ok(Self { - pipeline, - webrtc, - input_state, - input_channels: None, - #[cfg(target_os = "windows")] - native_window_input_bridge: None, - render_state, - present_max_fps, - d3d_fullscreen_sink, - skip_webrtc_video, - nvst_receive: None, - video_liveness, - event_sender, - original_remote_ice_credentials: None, - }) - } - - pub(crate) fn parse_offer_sdp(sdp: &str) -> Result { - init_gstreamer()?; - gst_sdp::SDPMessage::parse_buffer(sdp.as_bytes()) - .map_err(|error| format!("GStreamer rejected the remote SDP offer: {error:?}")) - } - - pub(crate) fn webrtc_name(&self) -> String { - self.webrtc.name().to_string() - } - - pub(crate) fn set_present_max_fps(&self, fps: u32) { - self.present_max_fps.store(fps, Ordering::SeqCst); - } - - pub(crate) fn set_d3d_fullscreen_sink(&self, enabled: bool) { - self.d3d_fullscreen_sink.store(enabled, Ordering::SeqCst); - } - - pub(crate) fn configure_stats( - &self, - context: &NativeStreamerSessionContext, - target_bitrate_kbps: u32, - ) { - self.video_liveness.configure(context, target_bitrate_kbps); - } - - /// Attach classic NVST UDP video: appsrc → parse → decoder → sink, plus UDP recv thread. - /// Keeps webrtcbin for SCTP input; ignores WebRTC RTP video pads. - pub(crate) fn attach_nvst_video( - &mut self, - session: NvstVideoSession, - fallback_codec: &str, - requested_fps: Option, - d3d_fullscreen_sink: bool, - ) -> Result<(), String> { - if self.nvst_receive.is_some() { - return Ok(()); - } - - let codec = session - .codec - .as_deref() - .filter(|c| !c.is_empty()) - .unwrap_or(fallback_codec); - let codec_upper = codec.to_ascii_uppercase(); - let encoding = match codec_upper.as_str() { - "H264" => "H264", - "H265" | "HEVC" => "H265", - other => { - return Err(format!( - "NVST classic UDP video scaffold supports H264/H265, got {other}" - )); - } - }; - - self.skip_webrtc_video.store(true, Ordering::SeqCst); - - let (video_api, mut specs) = rtp_video_chain_specs(encoding, requested_fps).ok_or_else(|| { - format!( - "NVST Annex-B decode chain unavailable for {encoding}; install GStreamer plugins or set {NATIVE_VIDEO_BACKEND_ENV}=software." - ) - })?; - // Drop RTP depayloader — appsrc feeds assembled Annex-B AUs. - specs.retain(|spec| spec.role != RtpVideoChainRole::Depayloader); - if specs - .first() - .is_none_or(|spec| spec.role != RtpVideoChainRole::Parser) - { - return Err(format!( - "NVST video chain for {encoding} is missing a parser after depayloader removal." - )); - } - - let caps_str = annexb_appsrc_caps(encoding); - let appsrc = gst::ElementFactory::make("appsrc") - .name("nvst-annexb") - .build() - .map_err(|error| format!("Failed to create nvst-annexb appsrc: {error}"))?; - let caps = caps_str - .parse::() - .map_err(|error| format!("Invalid NVST appsrc caps: {error}"))?; - appsrc.set_property("caps", &caps); - set_property_if_supported(&appsrc, "is-live", true); - set_property_from_str_if_supported(&appsrc, "format", "time"); - set_property_if_supported(&appsrc, "block", false); - set_property_if_supported(&appsrc, "max-bytes", 0u64); - set_property_from_str_if_supported(&appsrc, "stream-type", "stream"); - - let streaming_reported = Arc::new(AtomicBool::new(false)); - let mut elements: Vec = Vec::with_capacity(specs.len() + 1); - - let result = (|| -> Result<(), String> { - send_log( - &self.event_sender, - "info", - format!( - "Attaching NVST classic UDP video ({encoding}) via appsrc Annex-B → {}; {}", - video_api.label(), - format_video_chain_selection(encoding, video_api, &specs) - ), - ); - - let configured_present_max_fps = self.present_max_fps.load(Ordering::SeqCst); - let effective = effective_present_max_fps( - configured_present_max_fps, - requested_fps, - video_api, - primary_display_refresh_hz(), - ); - self.present_max_fps.store(effective, Ordering::SeqCst); - - self.pipeline - .add(&appsrc) - .map_err(|error| format!("Failed to add NVST appsrc: {error}"))?; - elements.push(appsrc.clone()); - - for spec in &specs { - let element = make_element(spec.factory)?; - configure_rtp_video_chain_element( - &element, - spec.clone(), - video_api, - d3d_fullscreen_sink, - ); - if spec.role == RtpVideoChainRole::StatsOverlay { - self.video_liveness.set_stats_overlay(Some(element.clone())); - } - self.pipeline.add(&element).map_err(|error| { - format!( - "Failed to add {} for NVST {encoding} video chain: {error}", - spec.factory - ) - })?; - elements.push(element); - } - - for pair in elements.windows(2) { - pair[0].link(&pair[1]).map_err(|error| { - format!( - "Failed to link {} -> {} for NVST {encoding}: {error:?}", - element_factory_name(&pair[0]), - element_factory_name(&pair[1]) - ) - })?; - } - - let sink = elements - .last() - .ok_or_else(|| format!("NVST {encoding} video chain has no sink."))?; - if let Some(post_decode_queue) = - specs - .iter() - .zip(elements.iter().skip(1)) - .find_map(|(spec, element)| { - (spec.role == RtpVideoChainRole::PostDecodeQueue).then_some(element) - }) - { - self.video_liveness - .set_post_decode_queue(post_decode_queue.clone()); - watch_video_decoded_rate( - post_decode_queue, - &self.event_sender, - Some(self.video_liveness.clone()), - ); - } - if let Some(pre_decode_queue) = - specs - .iter() - .zip(elements.iter().skip(1)) - .find_map(|(spec, element)| { - (spec.role == RtpVideoChainRole::PreDecodeQueue).then_some(element) - }) - { - self.video_liveness - .set_pre_decode_queue(pre_decode_queue.clone()); - } - if let Some(parser) = specs.iter().zip(elements.iter().skip(1)).find_map( - |(spec, element)| (spec.role == RtpVideoChainRole::Parser).then_some(element), - ) { - watch_video_caps_transitions( - parser, - "parser", - &self.event_sender, - self.video_liveness.clone(), - ); - } - if let Some(decoder) = specs.iter().zip(elements.iter().skip(1)).find_map( - |(spec, element)| (spec.role == RtpVideoChainRole::Decoder).then_some(element), - ) { - self.video_liveness.set_decoder(decoder.clone()); - watch_video_caps_transitions( - decoder, - "decoder", - &self.event_sender, - self.video_liveness.clone(), - ); - } - - self.render_state - .set_video_sink(sink.clone(), &self.event_sender) - .map_err(|error| { - format!("Failed to attach NVST video sink to native surface: {error}") - })?; - install_present_limiter( - sink, - self.present_max_fps.clone(), - &self.event_sender, - Some(self.video_liveness.clone()), - ); - watch_video_sink_caps_transitions( - sink, - &self.event_sender, - Some(self.video_liveness.clone()), - ); - watch_first_sink_buffer(sink, "video", &self.event_sender, &streaming_reported); - watch_video_sink_rate( - sink, - &self.event_sender, - Some(self.video_liveness.clone()), - ); - - for element in &elements { - element.sync_state_with_parent().map_err(|error| { - format!("Failed to sync NVST {encoding} video-chain element state: {error}") - })?; - } - - self.video_liveness.update_hardware_acceleration(format!( - "GStreamer {} (NVST UDP)", - video_api.label() - )); - self.video_liveness.start( - self.pipeline.clone(), - sink.clone(), - self.event_sender.clone(), - ); - - let handle = spawn_nvst_udp_receive( - session, - appsrc, - self.event_sender.clone(), - )?; - self.nvst_receive = Some(handle); - - // Ensure pipeline can run the appsrc branch even before WebRTC offer. - let _ = self.pipeline.set_state(gst::State::Playing); - - Ok(()) - })(); - - if result.is_err() { - self.skip_webrtc_video.store(false, Ordering::SeqCst); - for element in &elements { - let _ = element.set_state(gst::State::Null); - let _ = self.pipeline.remove(element); - } - } - - result - } - - fn ensure_input_data_channels( - &mut self, - partial_reliable_threshold_ms: u32, - ) -> Result<(), String> { - if self.input_channels.is_some() { - return Ok(()); - } - - self.input_state.reset(); - let channels = create_input_data_channels( - &self.webrtc, - self.input_state.clone(), - self.event_sender.clone(), - partial_reliable_threshold_ms, - )?; - let _ = channels.labels(); - self.input_channels = Some(channels); - self.ensure_native_window_input_bridge(); - Ok(()) - } - - #[cfg(target_os = "windows")] - fn ensure_native_window_input_bridge(&mut self) { - // Win32 RawInput: floating external window OR internal child HWND. - // Electron click-through across a topmost D3D sibling is unreliable. - if self.native_window_input_bridge.is_some() { - return; - } - let Some(input_channels) = self.input_channels.clone() else { - return; - }; - - self.native_window_input_bridge = Some(NativeWindowInputBridge::start( - self.input_state.clone(), - input_channels, - self.event_sender.clone(), - )); - if use_internal_renderer() { - let hwnd = self.render_state.internal_renderer.child_handle(); - if hwnd != 0 && arm_internal_child_input(hwnd) { - send_log( - &self.event_sender, - "info", - "Armed RawInput capture on the internal child HWND.".to_owned(), - ); - } - } - } - - #[cfg(not(target_os = "windows"))] - fn ensure_native_window_input_bridge(&mut self) { - if use_internal_renderer() { - return; - } - send_log( - &self.event_sender, - "warn", - format!( - "Native OS-level input capture is not implemented for {}; Electron input forwarding remains active.", - std::env::consts::OS - ), - ); - } - - pub(crate) fn negotiate_answer( - &mut self, - offer_sdp: gst_sdp::SDPMessage, - original_remote_credentials: Option<&IceCredentials>, - partial_reliable_threshold_ms: u32, - ) -> Result { - let offer = - gst_webrtc::WebRTCSessionDescription::new(gst_webrtc::WebRTCSDPType::Offer, offer_sdp); - self.pipeline - .set_state(gst::State::Playing) - .map_err(|error| { - format!("Failed to set GStreamer pipeline to Playing before negotiation: {error:?}") - })?; - self.set_description("set-remote-description", &offer)?; - if let Some(credentials) = original_remote_credentials { - self.original_remote_ice_credentials = Some(credentials.clone()); - self.try_restore_original_remote_ice_credentials("after remote description")?; - } - self.ensure_input_data_channels(partial_reliable_threshold_ms)?; - let answer = self.create_answer()?; - let answer_sdp = answer - .sdp() - .as_text() - .map_err(|error| format!("Failed to serialize GStreamer answer SDP: {error}"))?; - self.set_description("set-local-description", &answer)?; - self.try_restore_original_remote_ice_credentials("after local description")?; - Ok(answer_sdp) - } - - pub(crate) fn try_restore_original_remote_ice_credentials( - &mut self, - stage: &str, - ) -> Result { - let Some(credentials) = self.original_remote_ice_credentials.clone() else { - return Ok(false); - }; - - if credentials.ufrag.is_empty() || credentials.pwd.is_empty() { - return Err( - "Cannot restore original remote ICE credentials: offer credentials are empty." - .to_owned(), - ); - } - - let Some(ice_agent) = self - .webrtc - .property::>("ice-agent") - else { - return Err( - "Cannot restore original remote ICE credentials: webrtcbin has no ICE agent." - .to_owned(), - ); - }; - let ice_agent_ptr = ice_agent.as_ptr() as *mut gst_webrtc::ffi::GstWebRTCICE; - let ufrag = CString::new(credentials.ufrag.as_str()) - .map_err(|_| "Cannot restore original remote ICE credentials: ufrag contains NUL.")?; - let pwd = CString::new(credentials.pwd.as_str()) - .map_err(|_| "Cannot restore original remote ICE credentials: pwd contains NUL.")?; - - let streams = self.negotiated_nice_streams(); - if streams.is_empty() { - send_log( - &self.event_sender, - "warn", - format!( - "GStreamer has not exposed actual NICE ICE streams {stage}; deferring GFN remote ICE credential restoration." - ), - ); - return Ok(false); - } - - let mut restored = 0usize; - let stream_ids = streams - .iter() - .map(|stream| stream.stream_id) - .collect::>(); - for stream in &streams { - let accepted = unsafe { - gst_webrtc::ffi::gst_webrtc_ice_set_remote_credentials( - ice_agent_ptr, - stream.ptr, - ufrag.as_ptr(), - pwd.as_ptr(), - ) != glib::ffi::GFALSE - }; - if accepted { - restored += 1; - } else { - send_log( - &self.event_sender, - "warn", - format!( - "GStreamer ICE agent rejected original remote credentials for actual stream {}.", - stream.stream_id - ), - ); - } - } - - if restored == 0 { - send_log( - &self.event_sender, - "warn", - format!( - "GStreamer rejected original GFN remote ICE credentials on all actual streams {stage}; ICE may fail." - ), - ); - return Ok(false); - } - - send_log( - &self.event_sender, - "info", - format!( - "Restored original GFN remote ICE credentials on {restored}/{} actual GStreamer NICE ICE stream(s) {stage}; streamIds={stream_ids:?}.", - streams.len() - ), - ); - Ok(true) - } - - fn negotiated_nice_streams(&self) -> Vec { - let mut streams = Vec::new(); - let mut seen_stream_pointers = HashSet::new(); - let mut seen_transport_summaries = Vec::new(); - for index in 0..8 { - let transceiver = self - .webrtc - .emit_by_name::>( - "get-transceiver", - &[&(index as i32)], - ); - let Some(transceiver) = transceiver else { - continue; - }; - - if let Some(receiver) = transceiver.receiver() { - if let Some(transport) = receiver.transport() { - self.collect_nice_stream_from_dtls_transport( - &transport, - index, - "receiver", - &mut streams, - &mut seen_stream_pointers, - &mut seen_transport_summaries, - ); - } - } - if let Some(sender) = transceiver.sender() { - if let Some(transport) = sender.transport() { - self.collect_nice_stream_from_dtls_transport( - &transport, - index, - "sender", - &mut streams, - &mut seen_stream_pointers, - &mut seen_transport_summaries, - ); - } - } - } - - if !seen_transport_summaries.is_empty() { - send_log( - &self.event_sender, - "debug", - format!( - "GStreamer negotiated ICE transports: {}.", - seen_transport_summaries.join(", ") - ), - ); - } - streams - } - - fn collect_nice_stream_from_dtls_transport( - &self, - dtls_transport: &gst_webrtc::WebRTCDTLSTransport, - transceiver_index: u32, - direction: &str, - streams: &mut Vec, - seen_stream_pointers: &mut HashSet, - seen_transport_summaries: &mut Vec, - ) { - let session_id = dtls_transport.session_id(); - let Some(ice_transport) = dtls_transport.transport() else { - seen_transport_summaries.push(format!( - "transceiver {transceiver_index} {direction} dtlsSession={session_id} iceTransport=none" - )); - return; - }; - - let transport_type = ice_transport.type_().name().to_owned(); - let component = ice_transport.component(); - let state = ice_transport.state(); - let Some(stream) = nice_stream_from_ice_transport(&ice_transport) else { - seen_transport_summaries.push(format!( - "transceiver {transceiver_index} {direction} dtlsSession={session_id} iceTransportType={transport_type} component={component:?} state={state:?} stream=none" - )); - return; - }; - - seen_transport_summaries.push(format!( - "transceiver {transceiver_index} {direction} dtlsSession={session_id} iceTransportType={transport_type} component={component:?} state={state:?} streamId={}", - stream.stream_id - )); - - let stream_pointer = stream.ptr as usize; - if seen_stream_pointers.insert(stream_pointer) { - streams.push(stream); - } - } - - pub(crate) fn set_description( - &self, - signal_name: &'static str, - description: &gst_webrtc::WebRTCSessionDescription, - ) -> Result<(), String> { - let promise = gst::Promise::new(); - self.webrtc - .emit_by_name::<()>(signal_name, &[description, &promise]); - wait_for_promise(&promise, signal_name) - } - - fn create_answer(&self) -> Result { - let promise = gst::Promise::new(); - self.webrtc - .emit_by_name::<()>("create-answer", &[&None::, &promise]); - wait_for_promise(&promise, "create-answer")?; - let reply = promise - .get_reply() - .ok_or_else(|| "GStreamer create-answer resolved without a reply.".to_owned())?; - reply - .get::("answer") - .map_err(|error| { - format!( - "GStreamer create-answer reply did not contain an answer: {error}; reply={}", - describe_structure(reply) - ) - }) - } - - pub(crate) fn add_remote_ice(&mut self, candidate: &IceCandidatePayload) -> Result<(), String> { - if candidate.candidate.trim().is_empty() { - return Err("Remote ICE candidate is empty.".to_owned()); - } - self.try_restore_original_remote_ice_credentials("before adding remote ICE candidate")?; - let sdp_m_line_index = candidate.sdp_m_line_index.unwrap_or(0); - self.webrtc.emit_by_name::<()>( - "add-ice-candidate", - &[&sdp_m_line_index, &candidate.candidate], - ); - Ok(()) - } - - pub(crate) fn send_input_packet(&self, payload: &[u8], partially_reliable: bool) -> bool { - if !self.input_state.ready.load(Ordering::SeqCst) - || self.input_state.paused.load(Ordering::SeqCst) - { - return false; - } - - let Some(input_channels) = &self.input_channels else { - return false; - }; - - input_channels.send_packet(payload, partially_reliable) - } - - pub(crate) fn set_input_paused(&self, paused: bool) { - self.input_state.paused.store(paused, Ordering::SeqCst); - if paused { - release_native_input_capture(); - } - } - - pub(crate) fn update_render_surface(&self, surface: NativeRenderSurface) -> Result<(), String> { - self.video_liveness - .set_stats_overlay_visible(surface.visible && surface.show_stats); - self.render_state.set_surface(surface, &self.event_sender) - } - - pub(crate) fn stop(mut self) -> Result<(), String> { - if let Some(handle) = self.nvst_receive.take() { - handle.stop(); - } - self.skip_webrtc_video.store(false, Ordering::SeqCst); - self.video_liveness.set_stats_overlay_visible(false); - self.render_state.stop_external_renderer_window_guard(); - self.render_state.destroy_internal_renderer(); - #[cfg(target_os = "windows")] - if let Some(mut bridge) = self.native_window_input_bridge.take() { - bridge.stop(); - } - self.input_state.stop_heartbeat(); - self.video_liveness.stop(); - self.pipeline - .set_state(gst::State::Null) - .map(|_| ()) - .map_err(|error| format!("Failed to stop GStreamer pipeline: {error:?}")) - } -} - -pub(crate) fn resolve_gstreamer_stun_server(ice_servers: &[IceServer]) -> String { - ice_servers - .iter() - .flat_map(|server| server.urls.iter()) - .find_map(|url| { - let url = url.trim(); - if url.starts_with("stun://") { - Some(url.to_owned()) - } else { - url.strip_prefix("stun:") - .map(|endpoint| format!("stun://{endpoint}")) - } - }) - .unwrap_or_else(|| DEFAULT_GFN_STUN_SERVER.to_owned()) -} - -fn nice_stream_from_ice_transport( - transport: &gst_webrtc::WebRTCICETransport, -) -> Option { - if transport.type_().name() != "GstWebRTCNiceTransport" { - return None; - } - - unsafe { - let transport_ptr = transport.as_ptr() as *mut GstWebRTCNiceTransportCompat; - if transport_ptr.is_null() { - return None; - } - - let stream_ptr = (*transport_ptr).stream; - if stream_ptr.is_null() { - return None; - } - - Some(ActualNiceIceStream { - ptr: stream_ptr, - stream_id: (*stream_ptr).stream_id, - }) - } -} - -pub(crate) fn init_gstreamer() -> Result<(), String> { - gst::init().map_err(|error| format!("Failed to initialize GStreamer: {error}"))?; - #[cfg(target_os = "linux")] - { - static RTP_PLUGIN_REGISTRATION: OnceLock> = OnceLock::new(); - RTP_PLUGIN_REGISTRATION - .get_or_init(|| { - if gst::ElementFactory::find("rtpav1depay").is_some() { - return Ok(()); - } - gstrsrtp::plugin_register_static().map_err(|error| { - format!("Failed to register the bundled AV1 RTP plugin: {error}") - }) - }) - .clone() - } - #[cfg(not(target_os = "linux"))] - { - Ok(()) - } -} - -pub(crate) fn set_property_if_supported>( - element: &gst::Element, - name: &str, - value: T, -) { - if let Some(property) = element.find_property(name) { - if !property.flags().contains(glib::ParamFlags::WRITABLE) { - return; - } - - let value = value.into(); - let value_type = value.type_(); - let property_type = property.value_type(); - if value_type == property_type || value_type.is_a(property_type) { - element.set_property_from_value(name, &value); - } - } -} - -pub(crate) fn set_property_from_str_if_supported(element: &gst::Element, name: &str, value: &str) { - if element.find_property(name).is_some() { - element.set_property_from_str(name, value); - } -} - -pub(crate) fn configure_webrtc_low_latency(webrtc: &gst::Element) { - set_property_if_supported(webrtc, "latency", WEBRTC_LATENCY_MS); -} - -pub(crate) fn configure_queue_for_low_latency(element: &gst::Element, media_label: &str) { - let max_buffers = if media_label == "video" { - VIDEO_QUEUE_MAX_BUFFERS - } else { - AUDIO_QUEUE_MAX_BUFFERS - }; - - configure_queue(element, max_buffers, true); -} - -pub(crate) fn configure_queue(element: &gst::Element, max_buffers: u32, leaky_downstream: bool) { - set_property_if_supported(element, "max-size-buffers", max_buffers); - set_property_if_supported(element, "max-size-bytes", 0u32); - set_property_if_supported(element, "max-size-time", 0u64); - if leaky_downstream { - set_property_from_str_if_supported(element, "leaky", "downstream"); - } else { - set_property_from_str_if_supported(element, "leaky", "no"); - } -} - -pub(crate) fn configure_sink_for_low_latency(element: &gst::Element) { - // GFN-aligned present: never clock-sync or QoS-throttle the sink. Latency - // comes from decode + a depth-1 leaky post-decode queue + optional present - // limiter, not from GstBaseSink pacing. - set_property_if_supported(element, "sync", false); - set_property_if_supported(element, "async", false); - set_property_if_supported(element, "qos", false); - set_property_if_supported(element, "max-lateness", -1i64); - set_property_if_supported(element, "processing-deadline", 0u64); - set_property_if_supported(element, "render-delay", 0u64); - set_property_if_supported(element, "throttle-time", 0u64); - set_property_if_supported(element, "enable-last-sample", false); - set_property_if_supported(element, "show-preroll-frame", false); - set_property_if_supported(element, "redraw-on-update", true); - set_property_if_supported(element, "force-aspect-ratio", true); -} - -/// Configure d3d11/d3d12videosink for low-latency Internal/External present. -/// -/// GStreamer docs: `fullscreen` is ignored unless `fullscreen-toggle-mode` -/// includes `property`. Internal always keeps exclusive fullscreen off (caller -/// passes `d3d_fullscreen_sink=false`); External + Cloud G-Sync may enable it. -pub(crate) fn configure_d3d_video_sink(element: &gst::Element, d3d_fullscreen_sink: bool) { - configure_sink_for_low_latency(element); - // d3d12 only: attaching the swapchain directly to an external HWND can turn - // a present stall into upstream decode backpressure on the child-surface path. - set_property_if_supported(element, "direct-swapchain", false); - set_property_if_supported(element, "error-on-closed", false); - // RawInput owns mouse/keyboard; do not let the sink emit GstNavigation events. - set_property_if_supported(element, "enable-navigation-events", false); - set_property_if_supported(element, "fullscreen-on-alt-enter", false); - if d3d_fullscreen_sink { - set_property_from_str_if_supported(element, "fullscreen-toggle-mode", "property"); - set_property_if_supported(element, "fullscreen", true); - } else { - set_property_from_str_if_supported(element, "fullscreen-toggle-mode", "none"); - set_property_if_supported(element, "fullscreen", false); - } -} - -pub(crate) fn configure_stats_overlay_element(element: &gst::Element) { - set_property_if_supported(element, "visible", false); - set_property_if_supported(element, "text", ""); - set_property_if_supported(element, "auto-resize", true); - set_property_if_supported(element, "layout-x", 0.018f64); - set_property_if_supported(element, "layout-y", 0.018f64); - set_property_if_supported(element, "layout-width", 0.55f64); - set_property_if_supported(element, "layout-height", 0.18f64); - set_property_if_supported(element, "font-family", "Cascadia Mono"); - set_property_if_supported(element, "font-size", 18f32); - set_property_from_str_if_supported(element, "text-alignment", "leading"); - set_property_from_str_if_supported(element, "paragraph-alignment", "near"); - set_property_if_supported(element, "foreground-color", 0xF2FF_FFFFu32); - set_property_if_supported(element, "outline-color", 0xD000_0000u32); -} - -pub(crate) fn wait_for_promise(promise: &gst::Promise, operation: &str) -> Result<(), String> { - match promise.wait() { - gst::PromiseResult::Replied => { - if let Some(reply) = promise.get_reply() { - if reply.has_field("error") { - return Err(format!( - "GStreamer promise returned an error during {operation}: {}", - describe_structure(reply) - )); - } - } - Ok(()) - } - gst::PromiseResult::Interrupted => { - Err(format!("GStreamer promise interrupted during {operation}.")) - } - gst::PromiseResult::Expired => { - Err(format!("GStreamer promise expired during {operation}.")) - } - gst::PromiseResult::Pending => Err(format!( - "GStreamer promise still pending during {operation}." - )), - other => Err(format!( - "GStreamer promise failed during {operation}: {other:?}" - )), - } -} - -pub(crate) fn describe_structure(structure: &gst::StructureRef) -> String { - let fields = structure - .iter() - .map(|(name, value)| { - let rendered = value - .get::<&glib::Error>() - .map(|error| format!("{error:?}")) - .unwrap_or_else(|_| format!("{value:?}")); - format!("{}={rendered}", name.as_str()) - }) - .collect::>(); - - format!("{} {{{}}}", structure.name().as_str(), fields.join(", ")) -} - -fn wire_local_ice_events( - webrtc: &gst::Element, - event_sender: Option>, -) -> Result<(), String> { - let Some(event_sender) = event_sender else { - return Ok(()); - }; - - webrtc.connect("on-ice-candidate", false, move |values| { - let sdp_m_line_index = values.get(1).and_then(glib_value_to_u32).unwrap_or(0); - let candidate = values - .get(2) - .and_then(|value| value.get::().ok()) - .unwrap_or_default(); - - if !candidate.trim().is_empty() { - let _ = event_sender.send(Event::LocalIce { - candidate: IceCandidatePayload { - candidate, - sdp_mid: Some(sdp_m_line_index.to_string()), - sdp_m_line_index: Some(sdp_m_line_index), - username_fragment: None, - }, - }); - } - - None - }); - Ok(()) -} - -fn glib_value_to_u32(value: &glib::Value) -> Option { - let value_type = value.type_(); - if value_type == u32::static_type() { - return value.get::().ok(); - } - if value_type == i32::static_type() { - return value - .get::() - .ok() - .and_then(|value| u32::try_from(value).ok()); - } - if value_type == u64::static_type() { - return value - .get::() - .ok() - .and_then(|value| u32::try_from(value).ok()); - } - if value_type == i64::static_type() { - return value - .get::() - .ok() - .and_then(|value| u32::try_from(value).ok()); - } - None -} - -fn wire_webrtc_state_events(webrtc: &gst::Element, event_sender: Option>) { - wire_webrtc_property_event( - webrtc, - event_sender.clone(), - "ice-connection-state", - "ICE connection state", - ); - wire_webrtc_property_event( - webrtc, - event_sender.clone(), - "ice-gathering-state", - "ICE gathering state", - ); - wire_webrtc_property_event( - webrtc, - event_sender, - "connection-state", - "peer connection state", - ); -} - -fn wire_webrtc_property_event( - webrtc: &gst::Element, - event_sender: Option>, - property_name: &'static str, - label: &'static str, -) { - if event_sender.is_none() || webrtc.find_property(property_name).is_none() { - return; - } - - webrtc.connect_notify(Some(property_name), move |element, _| { - let value = element.property_value(property_name); - send_log( - &event_sender, - "debug", - format!("GStreamer WebRTC {label}: {value:?}."), - ); - }); -} - -fn start_gstreamer_bus_diagnostics( - pipeline: &gst::Pipeline, - event_sender: Option>, - stop: Arc, - video_liveness: VideoLivenessMonitor, -) { - let Some(bus) = pipeline.bus() else { - send_log( - &event_sender, - "warn", - "GStreamer pipeline has no bus; native diagnostics will be limited.".to_owned(), - ); - return; - }; - - thread::spawn(move || { - while !stop.load(Ordering::SeqCst) { - let Some(message) = bus.timed_pop_filtered( - gst::ClockTime::from_mseconds(250), - &[ - gst::MessageType::Error, - gst::MessageType::Warning, - gst::MessageType::Qos, - gst::MessageType::Latency, - gst::MessageType::StateChanged, - gst::MessageType::Eos, - ], - ) else { - continue; - }; - - match message.view() { - gst::MessageView::Error(error) => send_log( - &event_sender, - "error", - format!( - "GStreamer bus error from {}: {}; debug={:?}.", - message_src_name(&message), - error.error(), - error.debug() - ), - ), - gst::MessageView::Warning(warning) => send_log( - &event_sender, - "warn", - format!( - "GStreamer bus warning from {}: {}; debug={:?}.", - message_src_name(&message), - warning.error(), - warning.debug() - ), - ), - gst::MessageView::Qos(_) => send_log( - &event_sender, - "debug", - format!( - "GStreamer bus QoS from {}: {}.", - message_src_name(&message), - message_structure_summary(&message) - ), - ), - gst::MessageView::Latency(_) => send_log( - &event_sender, - "debug", - format!( - "GStreamer bus latency update from {}.", - message_src_name(&message) - ), - ), - gst::MessageView::StateChanged(state) => { - if message - .src() - .and_then(|src| src.clone().downcast::().ok()) - .is_some() - { - send_log( - &event_sender, - "debug", - format!( - "GStreamer pipeline state changed: {:?} -> {:?} pending {:?}.", - state.old(), - state.current(), - state.pending() - ), - ); - video_liveness.record_transition( - "pipeline-state-change", - "pipeline", - Some(format!("{:?}", state.old())), - Some(format!("{:?}", state.current())), - None, - None, - None, - None, - &event_sender, - ); - } - } - gst::MessageView::Eos(_) => send_log( - &event_sender, - "warn", - format!("GStreamer bus EOS from {}.", message_src_name(&message)), - ), - _ => {} - } - } - }); -} - -fn message_src_name(message: &gst::Message) -> String { - message - .src() - .map(|src| src.path_string().to_string()) - .unwrap_or_else(|| "unknown".to_owned()) -} - -fn message_structure_summary(message: &gst::Message) -> String { - message - .structure() - .map(|structure| structure.to_string()) - .unwrap_or_else(|| "no structure".to_owned()) -} - -fn wire_incoming_media_sink( - pipeline: &gst::Pipeline, - webrtc: &gst::Element, - event_sender: Option>, - render_state: GstreamerRenderState, - present_max_fps: Arc, - d3d_fullscreen_sink: Arc, - skip_webrtc_video: Arc, - video_liveness: VideoLivenessMonitor, -) { - let pipeline = pipeline.downgrade(); - let streaming_reported = Arc::new(AtomicBool::new(false)); - webrtc.connect_pad_added(move |_webrtc, src_pad| { - let Some(pipeline) = pipeline.upgrade() else { - return; - }; - let event_sender = event_sender.clone(); - - if !is_rtp_pad(src_pad) { - send_log( - &event_sender, - "debug", - format!( - "Ignoring non-RTP WebRTC pad with caps {:?}.", - pad_caps_name(src_pad) - ), - ); - return; - } - - if let Some(encoding) = rtp_video_encoding(src_pad) { - if skip_webrtc_video.load(Ordering::SeqCst) { - send_log( - &event_sender, - "info", - format!( - "Ignoring WebRTC RTP video pad ({encoding}); NVST classic UDP owns video." - ), - ); - if let Err(error) = - link_decoded_media_to_fakesink(&pipeline, src_pad, "ignored webrtc video") - { - send_log(&event_sender, "debug", error); - } - return; - } - match link_rtp_video_pad( - &pipeline, - src_pad, - &encoding, - &render_state, - &event_sender, - &streaming_reported, - present_max_fps.clone(), - d3d_fullscreen_sink.load(Ordering::SeqCst), - video_liveness.clone(), - ) { - Ok(()) => return, - Err(error) => send_log( - &event_sender, - "warn", - format!("{error}; falling back to decodebin."), - ), - } - } - - let decodebin = match make_element("decodebin") { - Ok(decodebin) => decodebin, - Err(error) => { - send_log(&event_sender, "warn", error); - return; - } - }; - - let decode_pipeline = pipeline.downgrade(); - let decode_sender = event_sender.clone(); - let decode_render_state = render_state.clone(); - let decode_streaming_reported = streaming_reported.clone(); - let decode_video_liveness = video_liveness.clone(); - decodebin.connect_pad_added(move |_decodebin, decoded_pad| { - let Some(pipeline) = decode_pipeline.upgrade() else { - return; - }; - let media_kind = decoded_media_kind(decoded_pad); - if let Err(error) = link_decoded_media_pad( - &pipeline, - decoded_pad, - &decode_render_state, - &decode_sender, - &decode_streaming_reported, - &decode_video_liveness, - ) { - send_log(&decode_sender, "warn", error); - if let Err(fallback_error) = - link_decoded_media_to_fakesink(&pipeline, decoded_pad, "decoded media fallback") - { - send_log(&decode_sender, "warn", fallback_error); - } - return; - } - - send_log( - &decode_sender, - "info", - format!( - "Linked decoded {} stream to native sink chain.", - media_kind.label() - ), - ); - }); - - if let Err(error) = pipeline.add(&decodebin) { - send_log( - &event_sender, - "warn", - format!("Failed to add decodebin: {error}"), - ); - return; - } - if let Err(error) = decodebin.sync_state_with_parent() { - send_log( - &event_sender, - "warn", - format!("Failed to sync decodebin state: {error}"), - ); - return; - } - - let Some(sink_pad) = decodebin.static_pad("sink") else { - send_log( - &event_sender, - "warn", - "decodebin has no sink pad.".to_owned(), - ); - return; - }; - if let Err(error) = src_pad.link(&sink_pad) { - send_log( - &event_sender, - "warn", - format!("Failed to link WebRTC RTP pad to decodebin: {error:?}"), - ); - } else if rtp_video_encoding(src_pad).is_some() { - video_liveness.set_rtp_video_src_pad(src_pad); - } - }); -} - -impl DecodedMediaKind { - fn label(self) -> &'static str { - match self { - Self::Audio => "audio", - Self::Video => "video", - Self::Unknown => "unknown", - } - } -} - -fn is_rtp_pad(pad: &gst::Pad) -> bool { - pad_caps_name(pad) - .as_deref() - .is_some_and(|name| name == "application/x-rtp") -} - -fn pad_caps_name(pad: &gst::Pad) -> Option { - let caps = pad.current_caps().unwrap_or_else(|| pad.query_caps(None)); - caps.structure(0) - .map(|structure| structure.name().to_string()) -} - -fn decoded_media_kind(pad: &gst::Pad) -> DecodedMediaKind { - match pad_caps_name(pad).as_deref() { - Some(name) if name.starts_with("video/") => DecodedMediaKind::Video, - Some(name) if name.starts_with("audio/") => DecodedMediaKind::Audio, - _ => DecodedMediaKind::Unknown, - } -} - -fn rtp_video_encoding(pad: &gst::Pad) -> Option { - let caps = pad.current_caps().unwrap_or_else(|| pad.query_caps(None)); - let structure = caps.structure(0)?; - if structure.name() != "application/x-rtp" { - return None; - } - - let media = structure.get::("media").ok()?; - if media != "video" { - return None; - } - - structure - .get::("encoding-name") - .ok() - .map(|encoding| encoding.to_ascii_uppercase()) -} - -fn rtp_video_depayloader_factory(codec: &str) -> Option<&'static str> { - match codec { - "H265" | "HEVC" => Some("rtph265depay"), - "H264" => Some("rtph264depay"), - "AV1" => Some("rtpav1depay"), - _ => None, - } -} - -fn rtp_video_parser_factory(codec: &str) -> Option<&'static str> { - match codec { - "H265" | "HEVC" => Some("h265parse"), - "H264" => Some("h264parse"), - "AV1" => Some("av1parse"), - _ => None, - } -} - -pub(crate) fn rtp_video_chain_definition( - encoding: &str, - video_api: RtpVideoApi, -) -> Option> { - let codec = encoding.to_ascii_uppercase(); - - #[cfg(target_os = "windows")] - if video_api == RtpVideoApi::Vulkan { - return windows_vulkan_present_chain_definition(codec.as_str()); - } - - let mut specs = vec![ - RtpVideoChainSpec::new( - rtp_video_depayloader_factory(codec.as_str())?, - RtpVideoChainRole::Depayloader, - ), - RtpVideoChainSpec::new( - rtp_video_parser_factory(codec.as_str())?, - RtpVideoChainRole::Parser, - ), - RtpVideoChainSpec::new("queue", RtpVideoChainRole::PreDecodeQueue), - RtpVideoChainSpec::new( - video_api.decoder_factory(codec.as_str())?, - RtpVideoChainRole::Decoder, - ), - ]; - - if let Some(memory_caps) = video_api.memory_caps() { - specs.push(RtpVideoChainSpec::with_caps( - "capsfilter", - RtpVideoChainRole::PostDecodeCapsFilter, - memory_caps, - )); - } - if let Some(converter) = video_api.post_decode_converter_factory() { - specs.push(RtpVideoChainSpec::new( - converter, - RtpVideoChainRole::PostDecodeConverter, - )); - } - if let Some(overlay) = video_api.stats_overlay_factory() { - specs.push(RtpVideoChainSpec::new( - overlay, - RtpVideoChainRole::StatsOverlay, - )); - } - specs.push(RtpVideoChainSpec::new( - "queue", - RtpVideoChainRole::PostDecodeQueue, - )); - specs.push(RtpVideoChainSpec::new( - video_api.sink_factory(), - RtpVideoChainRole::Sink, - )); - - Some(specs) -} - -/// Windows Vulkan path. -/// -/// Electron Internal hole-punch only composites DXGI swapchains on the child HWND. -/// A Win32 Vulkan surface on that HWND (or a GSTVULKAN child of it) presents black -/// even though vulkansink reports rendered frames. So: -/// - Internal: DXVA decode + `d3d12videosink` (D3D11 fallback; visible in Electron) -/// - External: DXVA decode + convert/upload + `vulkansink` (true Vulkan present) -/// -/// Native `vulkanh264dec` currently access-violates under NVIDIA Windows drivers. -#[cfg(target_os = "windows")] -fn windows_vulkan_present_chain_definition(codec: &str) -> Option> { - if use_internal_renderer() { - windows_vulkan_internal_present_chain_definition(codec) - } else { - windows_vulkan_external_present_chain_definition(codec) - } -} - -/// Internal Electron path: DXVA + D3D12 present (D3D11 fallback; DXGI hole-punch). -#[cfg(target_os = "windows")] -fn windows_vulkan_internal_present_chain_definition(codec: &str) -> Option> { - let decoder = RtpVideoApi::Vulkan.decoder_factory(codec)?; - let prefer_d3d12 = decoder.starts_with("d3d12"); - let sink = if prefer_d3d12 { - "d3d12videosink" - } else { - "d3d11videosink" - }; - let memory_api = if prefer_d3d12 { - RtpVideoApi::D3D12 - } else { - RtpVideoApi::D3D11 - }; - let mut specs = vec![ - RtpVideoChainSpec::new( - rtp_video_depayloader_factory(codec)?, - RtpVideoChainRole::Depayloader, - ), - RtpVideoChainSpec::new(rtp_video_parser_factory(codec)?, RtpVideoChainRole::Parser), - RtpVideoChainSpec::new("queue", RtpVideoChainRole::PreDecodeQueue), - RtpVideoChainSpec::new(decoder, RtpVideoChainRole::Decoder), - ]; - if let Some(memory_caps) = memory_api.memory_caps() { - specs.push(RtpVideoChainSpec::with_caps( - "capsfilter", - RtpVideoChainRole::PostDecodeCapsFilter, - memory_caps, - )); - } - specs.push(RtpVideoChainSpec::new( - "dwritetextoverlay", - RtpVideoChainRole::StatsOverlay, - )); - specs.push(RtpVideoChainSpec::new( - "queue", - RtpVideoChainRole::PostDecodeQueue, - )); - specs.push(RtpVideoChainSpec::new(sink, RtpVideoChainRole::Sink)); - Some(specs) -} - -/// External / capability path: DXVA + vulkanupload + vulkansink. -#[cfg(target_os = "windows")] -fn windows_vulkan_external_present_chain_definition(codec: &str) -> Option> { - let decoder = RtpVideoApi::Vulkan.decoder_factory(codec)?; - Some(vec![ - RtpVideoChainSpec::new( - rtp_video_depayloader_factory(codec)?, - RtpVideoChainRole::Depayloader, - ), - RtpVideoChainSpec::new(rtp_video_parser_factory(codec)?, RtpVideoChainRole::Parser), - RtpVideoChainSpec::new("queue", RtpVideoChainRole::PreDecodeQueue), - RtpVideoChainSpec::new(decoder, RtpVideoChainRole::Decoder), - // Composite diagnostics while frames are still in the DXVA/D3D path. - // dwritetextoverlay cannot consume VulkanImage memory after upload. - RtpVideoChainSpec::new("dwritetextoverlay", RtpVideoChainRole::StatsOverlay), - RtpVideoChainSpec::new("d3d11download", RtpVideoChainRole::PostDecodeConverter), - RtpVideoChainSpec::new("videoconvert", RtpVideoChainRole::PostDecodeConverter), - RtpVideoChainSpec::with_caps( - "capsfilter", - RtpVideoChainRole::PostDecodeCapsFilter, - "video/x-raw,format=RGBA", - ), - RtpVideoChainSpec::new("vulkanupload", RtpVideoChainRole::PostDecodeConverter), - RtpVideoChainSpec::new("queue", RtpVideoChainRole::PostDecodeQueue), - RtpVideoChainSpec::new(RtpVideoApi::Vulkan.sink_factory(), RtpVideoChainRole::Sink), - ]) -} - -fn preferred_rtp_video_apis(requested_fps: Option) -> Vec { - let requested = requested_video_backend(); - preferred_rtp_video_apis_for(requested.as_str(), requested_fps) -} - -pub(crate) fn preferred_rtp_video_apis_for( - requested: &str, - requested_fps: Option, -) -> Vec { - match requested { - "d3d11" => vec![RtpVideoApi::D3D11], - "d3d12" => vec![RtpVideoApi::D3D12], - "videotoolbox" | "vt" => vec![RtpVideoApi::VideoToolbox], - "nvdec" | "nvcodec" | "nvidia" => { - vec![RtpVideoApi::Nvdec, RtpVideoApi::Software] - } - "vaapi" | "va" => vec![RtpVideoApi::Vaapi, RtpVideoApi::Software], - "v4l2" | "v4l2stateless" => vec![RtpVideoApi::V4L2, RtpVideoApi::Software], - "vulkan" | "vk" => vec![RtpVideoApi::Vulkan, RtpVideoApi::Software], - "software" | "sw" => vec![RtpVideoApi::Software], - _ => default_rtp_video_api_priority(requested_fps), - } -} - -pub(crate) fn effective_present_max_fps( - configured_present_max_fps: u32, - requested_fps: Option, - video_api: RtpVideoApi, - display_hz: Option, -) -> u32 { - if configured_present_max_fps == PRESENT_LIMITER_VRR_SENTINEL { - if !matches!(video_api, RtpVideoApi::D3D11 | RtpVideoApi::D3D12) { - return 0; - } - return requested_fps - .filter(|fps| *fps > 0) - .map(|fps| vrr_present_max_fps(fps, display_hz)) - .unwrap_or(0); - } - - if configured_present_max_fps != PRESENT_LIMITER_AUTO_SENTINEL { - return configured_present_max_fps; - } - - // D3D11/D3D12 present (and Internal Vulkan→D3D) need the auto limiter so - // stream fps above display Hz does not stall the DXGI present path. - if !matches!(video_api, RtpVideoApi::D3D11 | RtpVideoApi::D3D12) - && !(cfg!(target_os = "windows") - && video_api == RtpVideoApi::Vulkan - && use_internal_renderer()) - { - return 0; - } - - requested_fps - .filter(|fps| *fps > 0) - .map(|fps| automatic_present_max_fps(fps, display_hz)) - .unwrap_or(0) -} - -pub(crate) fn default_rtp_video_api_priority(requested_fps: Option) -> Vec { - #[cfg(target_os = "windows")] - { - if should_prefer_d3d12_for_high_fps(requested_fps) { - return vec![ - RtpVideoApi::D3D12, - RtpVideoApi::D3D11, - RtpVideoApi::Software, - ]; - } - vec![ - RtpVideoApi::D3D11, - RtpVideoApi::D3D12, - RtpVideoApi::Software, - ] - } - #[cfg(target_os = "macos")] - { - let _ = requested_fps; - vec![RtpVideoApi::VideoToolbox, RtpVideoApi::Software] - } - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] - { - let _ = requested_fps; - vec![ - RtpVideoApi::V4L2, - RtpVideoApi::Nvdec, - RtpVideoApi::Vaapi, - RtpVideoApi::Vulkan, - RtpVideoApi::Software, - ] - } - #[cfg(all(target_os = "linux", not(target_arch = "aarch64")))] - { - let _ = requested_fps; - vec![ - RtpVideoApi::Nvdec, - RtpVideoApi::Vaapi, - RtpVideoApi::Vulkan, - RtpVideoApi::V4L2, - RtpVideoApi::Software, - ] - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - let _ = requested_fps; - vec![RtpVideoApi::Software] - } -} - -fn should_prefer_d3d12_for_high_fps(requested_fps: Option) -> bool { - requested_fps.is_some_and(|fps| fps >= 200) -} - -fn rtp_video_chain_specs( - encoding: &str, - requested_fps: Option, -) -> Option<(RtpVideoApi, Vec)> { - preferred_rtp_video_apis(requested_fps) - .into_iter() - .find_map(|video_api| { - let codec = encoding.to_ascii_uppercase(); - let decoder = select_decoder_factory(video_api, codec.as_str())?; - let sink = select_sink_factory(video_api)?; - let mut specs = rtp_video_chain_definition(encoding, video_api)?; - for spec in &mut specs { - if spec.role == RtpVideoChainRole::Decoder { - spec.factory = decoder; - } else if spec.role == RtpVideoChainRole::Sink { - spec.factory = sink; - } - } - align_windows_vulkan_download_factory(&mut specs, decoder); - align_windows_vulkan_internal_present(&mut specs, decoder); - insert_requested_fps_capssetter(&mut specs, requested_fps); - specs.retain(|spec| { - spec.role != RtpVideoChainRole::StatsOverlay - || gst::ElementFactory::find(spec.factory).is_some() - }); - required_video_chain_elements_available(&specs).then_some((video_api, specs)) - }) -} - -fn align_windows_vulkan_download_factory(specs: &mut Vec, decoder: &str) { - #[cfg(target_os = "windows")] - { - let download = if decoder.starts_with("d3d12") { - Some("d3d12download") - } else if decoder.starts_with("d3d11") { - Some("d3d11download") - } else if decoder.starts_with("nv") { - // NVDEC Windows outputs system memory in our bundle; skip D3D download. - None - } else { - return; - }; - - match download { - Some(factory) => { - if let Some(spec) = specs.iter_mut().find(|spec| { - spec.role == RtpVideoChainRole::PostDecodeConverter - && (spec.factory == "d3d11download" || spec.factory == "d3d12download") - }) { - spec.factory = factory; - } - } - None => { - specs.retain(|spec| { - !(spec.role == RtpVideoChainRole::PostDecodeConverter - && (spec.factory == "d3d11download" || spec.factory == "d3d12download")) - }); - } - } - } - #[cfg(not(target_os = "windows"))] - { - let _ = (specs, decoder); - } -} - -/// Keep Internal Vulkan→D3D present matched to the selected DXVA decoder family. -#[cfg(target_os = "windows")] -fn align_windows_vulkan_internal_present(specs: &mut Vec, decoder: &str) { - if !use_internal_renderer() { - return; - } - let has_d3d_present = specs.iter().any(|spec| { - spec.role == RtpVideoChainRole::Sink - && (spec.factory == "d3d11videosink" || spec.factory == "d3d12videosink") - }); - if !has_d3d_present { - return; - } - - let (sink, memory_caps) = if decoder.starts_with("d3d12") - && gst::ElementFactory::find("d3d12videosink").is_some() - { - ("d3d12videosink", RtpVideoApi::D3D12.memory_caps()) - } else if decoder.starts_with("d3d11") - && gst::ElementFactory::find("d3d11videosink").is_some() - { - ("d3d11videosink", RtpVideoApi::D3D11.memory_caps()) - } else { - return; - }; - - if let Some(spec) = specs - .iter_mut() - .find(|spec| spec.role == RtpVideoChainRole::Sink) - { - spec.factory = sink; - } - if let Some(spec) = specs - .iter_mut() - .find(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter) - { - if let Some(caps) = memory_caps { - spec.caps = Some(caps.to_owned()); - } - } -} - -#[cfg(not(target_os = "windows"))] -fn align_windows_vulkan_internal_present(_specs: &mut Vec, _decoder: &str) {} - -fn insert_requested_fps_capssetter(specs: &mut Vec, requested_fps: Option) { - let Some(fps) = requested_fps.filter(|fps| *fps > 0) else { - return; - }; - if gst::ElementFactory::find("capssetter").is_none() { - return; - } - // Windows Vulkan hybrid / D3D present: forcing plain video/x-raw onto a D3D - // memory pad breaks caps negotiation. - if specs.iter().any(|spec| { - matches!( - spec.factory, - "d3d11download" - | "d3d12download" - | "vulkanupload" - | "d3d11videosink" - | "d3d12videosink" - | "d3d11h264dec" - | "d3d11h265dec" - | "d3d11av1dec" - | "d3d12h264dec" - | "d3d12h265dec" - | "d3d12av1dec" - ) - }) { - return; - } - let Some(decoder_index) = specs - .iter() - .position(|spec| spec.role == RtpVideoChainRole::Decoder) - else { - return; - }; - - specs.insert( - decoder_index + 1, - RtpVideoChainSpec::with_caps( - "capssetter", - RtpVideoChainRole::PostDecodeRateSetter, - format!("video/x-raw,framerate=(fraction){fps}/1"), - ), - ); -} - -fn select_decoder_factory(video_api: RtpVideoApi, codec: &str) -> Option<&'static str> { - let primary = video_api.decoder_factory(codec)?; - std::iter::once(primary) - .chain(video_api.fallback_decoder_factories(codec).iter().copied()) - .find(|factory| decoder_factory_usable(factory)) -} - -fn decoder_factory_usable(factory: &'static str) -> bool { - static DECODER_PROBES: OnceLock>> = OnceLock::new(); - let probes = DECODER_PROBES.get_or_init(|| Mutex::new(HashMap::new())); - if let Ok(probes) = probes.lock() { - if let Some(usable) = probes.get(factory) { - return *usable; - } - } - - let usable = gst::ElementFactory::make(factory) - .build() - .ok() - .is_some_and(|decoder| { - let usable = decoder.set_state(gst::State::Ready).is_ok(); - let _ = decoder.set_state(gst::State::Null); - usable - }); - if let Ok(mut probes) = probes.lock() { - probes.insert(factory, usable); - } - usable -} - -fn select_sink_factory(video_api: RtpVideoApi) -> Option<&'static str> { - // Internal Linux: never pick waylandsink for the X11 child overlay path. - #[cfg(target_os = "linux")] - if use_internal_renderer() { - let internal = video_api.internal_x11_sink_candidates(); - if let Some(factory) = internal - .iter() - .copied() - .find(|factory| gst::ElementFactory::find(factory).is_some()) - { - return Some(factory); - } - } - - // Internal Windows + Vulkan: Electron hole-punch cannot composite Win32 Vulkan - // swapchains; present with D3D12 (D3D11 fallback) VideoOverlay instead. - #[cfg(target_os = "windows")] - if use_internal_renderer() && video_api == RtpVideoApi::Vulkan { - return ["d3d12videosink", "d3d11videosink"] - .into_iter() - .find(|factory| gst::ElementFactory::find(factory).is_some()); - } - - select_capability_sink_factory(video_api) -} - -/// Sink advertised in capabilities / used when not overriding for Internal present. -fn select_capability_sink_factory(video_api: RtpVideoApi) -> Option<&'static str> { - std::iter::once(video_api.sink_factory()) - .chain(video_api.sink_fallback_factories().iter().copied()) - .find(|factory| gst::ElementFactory::find(factory).is_some()) -} - -fn required_video_chain_elements_available(specs: &[RtpVideoChainSpec]) -> bool { - specs - .iter() - .all(|spec| gst::ElementFactory::find(spec.factory).is_some()) -} - -fn all_rtp_video_apis() -> &'static [RtpVideoApi] { - &[ - RtpVideoApi::D3D12, - RtpVideoApi::D3D11, - RtpVideoApi::VideoToolbox, - RtpVideoApi::Nvdec, - RtpVideoApi::Vaapi, - RtpVideoApi::V4L2, - RtpVideoApi::Vulkan, - RtpVideoApi::Software, - ] -} - -fn all_video_codec_labels() -> &'static [&'static str] { - &["H264", "H265", "AV1"] -} - -pub(crate) fn current_platform_label() -> &'static str { - #[cfg(target_os = "windows")] - { - "windows" - } - #[cfg(target_os = "macos")] - { - "macos" - } - #[cfg(target_os = "linux")] - { - "linux" - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - "other" - } -} - -fn backend_runs_on_current_platform(video_api: RtpVideoApi) -> bool { - backend_runs_on_platform(video_api, current_platform_label()) -} - -pub(crate) fn backend_runs_on_platform(video_api: RtpVideoApi, platform: &str) -> bool { - match video_api { - RtpVideoApi::D3D11 | RtpVideoApi::D3D12 => platform == "windows", - RtpVideoApi::VideoToolbox => platform == "macos", - RtpVideoApi::Nvdec | RtpVideoApi::Vaapi | RtpVideoApi::V4L2 => platform == "linux", - RtpVideoApi::Vulkan => matches!(platform, "windows" | "linux"), - RtpVideoApi::Software => true, - } -} - -pub(crate) fn native_video_backend_capabilities() -> Vec { - all_rtp_video_apis() - .iter() - .copied() - .map(native_video_backend_capability) - .collect() -} - -fn native_video_backend_capability(video_api: RtpVideoApi) -> NativeVideoBackendCapability { - let platform_supported = backend_runs_on_current_platform(video_api); - // Advertise the true API sink (vulkansink). Internal Windows Vulkan may present - // via d3d12/d3d11videosink at session time for Electron hole-punch compatibility. - let sink_factory = platform_supported - .then(|| select_capability_sink_factory(video_api)) - .flatten(); - let codecs = all_video_codec_labels() - .iter() - .map(|codec| { - native_video_codec_capability(video_api, codec, platform_supported, sink_factory) - }) - .collect::>(); - let available = - platform_supported && sink_factory.is_some() && codecs.iter().any(|codec| codec.available); - let reason = if !platform_supported { - Some(format!( - "{} is a {} backend and does not run on {}.", - video_api.label(), - video_api.platform(), - current_platform_label() - )) - } else if sink_factory.is_none() { - Some(format!( - "{} sink is unavailable; install the platform GStreamer video sink plugins.", - video_api.label() - )) - } else if !available { - Some(format!( - "{} decoders are unavailable for H.264, H.265, and AV1.", - video_api.label() - )) - } else { - None - }; - - NativeVideoBackendCapability { - backend: video_api.capability_id().to_owned(), - platform: video_api.platform().to_owned(), - codecs, - zero_copy_modes: zero_copy_modes_for_backend(video_api), - sink: sink_factory.map(str::to_owned), - available, - reason, - } -} - -fn native_video_codec_capability( - video_api: RtpVideoApi, - codec: &str, - platform_supported: bool, - sink: Option<&'static str>, -) -> NativeVideoCodecCapability { - let depayloader = rtp_video_depayloader_factory(codec); - let parser = rtp_video_parser_factory(codec); - let decoder = platform_supported - .then(|| select_decoder_factory(video_api, codec)) - .flatten(); - // Capability checks the External Vulkan present chain so vulkansink/vulkanupload - // must be present even when Internal sessions present via D3D11. - let definition = { - #[cfg(target_os = "windows")] - { - if video_api == RtpVideoApi::Vulkan { - windows_vulkan_external_present_chain_definition(codec) - } else { - rtp_video_chain_definition(codec, video_api) - } - } - #[cfg(not(target_os = "windows"))] - { - rtp_video_chain_definition(codec, video_api) - } - }; - let available = platform_supported - && sink.is_some() - && decoder.is_some() - && depayloader.is_some_and(|factory| gst::ElementFactory::find(factory).is_some()) - && parser.is_some_and(|factory| gst::ElementFactory::find(factory).is_some()) - && definition.is_some_and(|mut specs| { - for spec in &mut specs { - if spec.role == RtpVideoChainRole::Decoder { - if let Some(decoder) = decoder { - spec.factory = decoder; - } - } else if spec.role == RtpVideoChainRole::Sink { - if let Some(sink) = sink { - spec.factory = sink; - } - } - } - specs.retain(|spec| { - spec.role != RtpVideoChainRole::StatsOverlay - || gst::ElementFactory::find(spec.factory).is_some() - }); - required_video_chain_elements_available(&specs) - }); - - let reason = if !platform_supported { - Some("Backend is not available on this platform.".to_owned()) - } else if depayloader.is_none() || parser.is_none() { - Some("RTP depayloader or parser is not mapped for this codec.".to_owned()) - } else if decoder.is_none() { - Some(format!( - "{} decoder for {codec} is not installed.", - video_api.label() - )) - } else if sink.is_none() { - Some(format!( - "{} video sink is not installed.", - video_api.label() - )) - } else if !available { - Some("Required GStreamer elements are not all available.".to_owned()) - } else { - None - }; - - NativeVideoCodecCapability { - codec: codec.to_ascii_lowercase(), - available, - decoder: decoder.map(str::to_owned), - parser: parser.map(str::to_owned), - depayloader: depayloader.map(str::to_owned), - reason, - } -} - -fn zero_copy_modes_for_backend(video_api: RtpVideoApi) -> Vec { - match video_api { - RtpVideoApi::D3D11 => vec!["D3D11Memory".to_owned()], - RtpVideoApi::D3D12 => vec!["D3D12Memory".to_owned()], - RtpVideoApi::VideoToolbox => vec!["GLMemory".to_owned()], - RtpVideoApi::Nvdec => Vec::new(), - RtpVideoApi::Vaapi => vec!["VAMemory".to_owned()], - // Linux keeps decoded frames as VulkanImage. Windows uses DXVA→upload, - // so there is no end-to-end VulkanImage zero-copy path yet. - RtpVideoApi::Vulkan if cfg!(target_os = "windows") => Vec::new(), - RtpVideoApi::Vulkan => vec!["VulkanImage".to_owned()], - RtpVideoApi::V4L2 => vec!["DMABuf".to_owned()], - RtpVideoApi::Software => Vec::new(), - } -} - -fn configure_rtp_video_chain_element( - element: &gst::Element, - spec: RtpVideoChainSpec, - video_api: RtpVideoApi, - d3d_fullscreen_sink: bool, -) { - match spec.role { - RtpVideoChainRole::Depayloader => { - set_property_if_supported(element, "request-keyframe", true); - // Hard-waiting after packet loss can freeze the visible frame while RTP is still flowing. - set_property_if_supported(element, "wait-for-keyframe", false); - } - RtpVideoChainRole::Parser => { - set_property_if_supported(element, "disable-passthrough", true); - set_property_if_supported(element, "config-interval", -1i32); - } - RtpVideoChainRole::PreDecodeQueue => { - configure_queue(element, VIDEO_COMPRESSED_QUEUE_MAX_BUFFERS, false); - } - RtpVideoChainRole::Decoder => { - set_property_if_supported(element, "automatic-request-sync-points", true); - set_property_if_supported(element, "discard-corrupted-frames", true); - set_property_if_supported(element, "min-force-key-unit-interval", 100_000_000u64); - set_property_if_supported(element, "qos", false); - } - RtpVideoChainRole::PostDecodeRateSetter => { - if let Some(caps) = spec - .caps - .as_deref() - .and_then(|caps| caps.parse::().ok()) - { - element.set_property("caps", &caps); - } - set_property_if_supported(element, "join", true); - set_property_if_supported(element, "replace", false); - set_property_if_supported(element, "qos", false); - } - RtpVideoChainRole::PostDecodeCapsFilter => { - if let Some(caps) = spec - .caps - .as_deref() - .and_then(|caps| caps.parse::().ok()) - { - element.set_property("caps", &caps); - } - } - RtpVideoChainRole::PostDecodeConverter => { - set_property_if_supported(element, "qos", false); - } - RtpVideoChainRole::StatsOverlay => { - configure_stats_overlay_element(element); - } - RtpVideoChainRole::PostDecodeQueue => { - configure_queue_for_low_latency(element, "video"); - } - RtpVideoChainRole::Sink => { - if matches!(video_api, RtpVideoApi::D3D11 | RtpVideoApi::D3D12) { - configure_d3d_video_sink(element, d3d_fullscreen_sink); - } else { - configure_sink_for_low_latency(element); - } - } - } -} - -fn link_rtp_video_pad( - pipeline: &gst::Pipeline, - src_pad: &gst::Pad, - encoding: &str, - render_state: &GstreamerRenderState, - event_sender: &Option>, - streaming_reported: &Arc, - present_max_fps: Arc, - d3d_fullscreen_sink: bool, - video_liveness: VideoLivenessMonitor, -) -> Result<(), String> { - if src_pad.is_linked() { - return Ok(()); - } - - let requested_fps = video_liveness.requested_fps(); - let (video_api, specs) = rtp_video_chain_specs(encoding, requested_fps).ok_or_else(|| { - format!( - "Explicit low-latency decode chain is unavailable for RTP {encoding}; install the platform GStreamer plugin packages or set {NATIVE_VIDEO_BACKEND_ENV}=software to force software decode." - ) - })?; - video_liveness.update_hardware_acceleration(format!("GStreamer {}", video_api.label())); - video_liveness.set_stats_overlay(None); - let mut elements = Vec::with_capacity(specs.len()); - - let result = (|| -> Result<(), String> { - send_log( - event_sender, - "info", - format_video_chain_selection(encoding, video_api, &specs), - ); - if video_api == RtpVideoApi::D3D12 { - send_log( - event_sender, - "info", - format_d3d12_selection_summary(requested_fps), - ); - } - let configured_present_max_fps = present_max_fps.load(Ordering::SeqCst); - let effective_present_max_fps = effective_present_max_fps( - configured_present_max_fps, - requested_fps, - video_api, - primary_display_refresh_hz(), - ); - present_max_fps.store(effective_present_max_fps, Ordering::SeqCst); - if effective_present_max_fps > 0 { - let reason = if configured_present_max_fps == PRESENT_LIMITER_AUTO_SENTINEL { - "auto-enabled for the D3D present path to prevent display-rate present backpressure" - .to_owned() - } else if configured_present_max_fps == PRESENT_LIMITER_VRR_SENTINEL { - "kept below the display refresh ceiling for VRR".to_owned() - } else { - format!("configured by {NATIVE_PRESENT_MAX_FPS_ENV}") - }; - send_log( - event_sender, - "info", - format!( - "Native present limiter enabled at {effective_present_max_fps} fps for {} video path; reason: {reason}.", - video_api.label() - ), - ); - } - if d3d_fullscreen_sink { - send_log( - event_sender, - "info", - format!( - "Native D3D sink fullscreen presentation enabled for Cloud G-Sync/VRR; set {NATIVE_D3D_FULLSCREEN_ENV}=0 to disable." - ), - ); - } - for spec in &specs { - let element = make_element(spec.factory)?; - configure_rtp_video_chain_element( - &element, - spec.clone(), - video_api, - d3d_fullscreen_sink, - ); - if spec.role == RtpVideoChainRole::StatsOverlay { - video_liveness.set_stats_overlay(Some(element.clone())); - } - pipeline.add(&element).map_err(|error| { - format!( - "Failed to add {} for RTP {encoding} video chain: {error}", - spec.factory - ) - })?; - elements.push(element); - } - - for pair in elements.windows(2) { - pair[0].link(&pair[1]).map_err(|error| { - format!( - "Failed to link {} -> {} for RTP {encoding} video chain: {error:?}", - element_factory_name(&pair[0]), - element_factory_name(&pair[1]) - ) - })?; - } - - let first = elements - .first() - .ok_or_else(|| format!("No elements created for RTP {encoding} video chain."))?; - let Some(first_sink_pad) = first.static_pad("sink") else { - return Err(format!( - "First RTP {encoding} video-chain element has no sink pad." - )); - }; - let sink = elements - .last() - .ok_or_else(|| format!("RTP {encoding} video chain has no sink element."))?; - if let Some(post_decode_queue) = - specs - .iter() - .zip(elements.iter()) - .find_map(|(spec, element)| { - (spec.role == RtpVideoChainRole::PostDecodeQueue).then_some(element) - }) - { - video_liveness.set_post_decode_queue(post_decode_queue.clone()); - watch_video_decoded_rate( - post_decode_queue, - event_sender, - Some(video_liveness.clone()), - ); - } - if let Some(pre_decode_queue) = - specs - .iter() - .zip(elements.iter()) - .find_map(|(spec, element)| { - (spec.role == RtpVideoChainRole::PreDecodeQueue).then_some(element) - }) - { - video_liveness.set_pre_decode_queue(pre_decode_queue.clone()); - } - if let Some(parser) = specs - .iter() - .zip(elements.iter()) - .find_map(|(spec, element)| (spec.role == RtpVideoChainRole::Parser).then_some(element)) - { - watch_video_caps_transitions(parser, "parser", event_sender, video_liveness.clone()); - } - if let Some(decoder) = specs - .iter() - .zip(elements.iter()) - .find_map(|(spec, element)| { - (spec.role == RtpVideoChainRole::Decoder).then_some(element) - }) - { - video_liveness.set_decoder(decoder.clone()); - watch_video_caps_transitions(decoder, "decoder", event_sender, video_liveness.clone()); - } - render_state - .set_video_sink(sink.clone(), event_sender) - .map_err(|error| { - format!("Failed to attach RTP {encoding} video sink to native surface: {error}") - })?; - install_present_limiter( - sink, - present_max_fps, - event_sender, - Some(video_liveness.clone()), - ); - watch_video_sink_caps_transitions(sink, event_sender, Some(video_liveness.clone())); - watch_first_sink_buffer(sink, "video", event_sender, streaming_reported); - watch_video_sink_rate(sink, event_sender, Some(video_liveness.clone())); - - for element in &elements { - element.sync_state_with_parent().map_err(|error| { - format!("Failed to sync RTP {encoding} video-chain element state: {error}") - })?; - } - src_pad - .link(&first_sink_pad) - .map_err(|error| format!("Failed to link RTP {encoding} video pad: {error:?}"))?; - video_liveness.set_rtp_video_src_pad(src_pad); - watch_rtp_video_bitrate(src_pad, video_liveness.clone(), event_sender); - video_liveness.start(pipeline.clone(), sink.clone(), event_sender.clone()); - - Ok(()) - })(); - - if result.is_err() { - for element in &elements { - let _ = element.set_state(gst::State::Null); - let _ = pipeline.remove(element); - } - } - - result?; - send_log( - event_sender, - "info", - format!( - "Linked RTP {encoding} video through explicit low-latency {} decode chain.", - video_api.label() - ), - ); - Ok(()) -} - -pub(crate) fn format_video_chain_selection( - encoding: &str, - video_api: RtpVideoApi, - specs: &[RtpVideoChainSpec], -) -> String { - let decoder = specs - .iter() - .find(|spec| spec.role == RtpVideoChainRole::Decoder) - .map(|spec| spec.factory) - .unwrap_or("unknown"); - let sink = specs - .iter() - .find(|spec| spec.role == RtpVideoChainRole::Sink) - .map(|spec| spec.factory) - .unwrap_or("unknown"); - let converter = specs - .iter() - .filter(|spec| spec.role == RtpVideoChainRole::PostDecodeConverter) - .map(|spec| spec.factory) - .collect::>() - .join("+"); - let converter = if converter.is_empty() { - "none".to_owned() - } else { - converter - }; - let memory = specs - .iter() - .find(|spec| spec.role == RtpVideoChainRole::PostDecodeCapsFilter) - .and_then(|spec| spec.caps.as_deref()) - .unwrap_or(if video_api.is_gpu_path() { - "auto-negotiated" - } else { - "system-memory" - }); - let acceleration = if video_api.is_gpu_path() { - "hardware" - } else { - "software" - }; - let path_note = if cfg!(target_os = "windows") && video_api == RtpVideoApi::Vulkan { - if sink == "d3d12videosink" { - " (DXVA decode + D3D12 present; Electron cannot composite Win32 vulkansink — use External for true Vulkan present)" - } else if sink == "d3d11videosink" { - " (DXVA decode + D3D11 present; Electron cannot composite Win32 vulkansink — use External for true Vulkan present)" - } else { - " (DXVA decode + Vulkan present; native vulkanh264dec is unstable on Windows)" - } - } else { - "" - }; - format!( - "Selected native {acceleration} video path for RTP {encoding}: backend={}, decoder={decoder}, converter={converter}, renderer={sink}, memory={memory}{path_note}.", - video_api.label() - ) -} - -fn format_d3d12_selection_summary(requested_fps: Option) -> String { - let backend_env = std::env::var(NATIVE_VIDEO_BACKEND_ENV).ok(); - let api_env = std::env::var(NATIVE_VIDEO_API_ENV).ok(); - let reason = if backend_env - .as_deref() - .is_some_and(|value| value.eq_ignore_ascii_case("d3d12")) - { - format!("forced by {NATIVE_VIDEO_BACKEND_ENV}=d3d12") - } else if api_env - .as_deref() - .is_some_and(|value| value.eq_ignore_ascii_case("d3d12")) - { - format!("forced by {NATIVE_VIDEO_API_ENV}=d3d12") - } else if should_prefer_d3d12_for_high_fps(requested_fps) { - format!( - "auto-selected for {} fps stream to avoid D3D11 display-rate present backpressure", - requested_fps - .map(|fps| fps.to_string()) - .unwrap_or_else(|| "high-FPS".to_owned()) - ) - } else { - "D3D11 was unavailable/probe failed".to_owned() - }; - - format!( - "Native D3D12 video path selected; reason: {reason}. env {NATIVE_VIDEO_BACKEND_ENV}={backend_env:?}, {NATIVE_VIDEO_API_ENV}={api_env:?}. If D3D12 stalls on a specific driver, force {NATIVE_VIDEO_BACKEND_ENV}=d3d11." - ) -} - -fn element_factory_name(element: &gst::Element) -> String { - element - .factory() - .map(|factory| factory.name().to_string()) - .unwrap_or_else(|| element.name().to_string()) -} - -fn link_decoded_media_pad( - pipeline: &gst::Pipeline, - src_pad: &gst::Pad, - render_state: &GstreamerRenderState, - event_sender: &Option>, - streaming_reported: &Arc, - video_liveness: &VideoLivenessMonitor, -) -> Result<(), String> { - if src_pad.is_linked() { - return Ok(()); - } - - match decoded_media_kind(src_pad) { - DecodedMediaKind::Video => link_media_chain( - pipeline, - src_pad, - &video_sink_factories(), - "video", - Some(render_state), - event_sender, - streaming_reported, - Some(video_liveness), - ), - DecodedMediaKind::Audio => link_media_chain( - pipeline, - src_pad, - &[ - ("queue", None), - ("audioconvert", None), - ("audioresample", None), - ("autoaudiosink", Some(false)), - ], - "audio", - None, - event_sender, - streaming_reported, - Some(video_liveness), - ), - DecodedMediaKind::Unknown => Err(format!( - "Unsupported decoded media caps {:?}; routing to fallback sink.", - pad_caps_name(src_pad) - )), - } -} - -fn video_sink_factories() -> Vec<(&'static str, Option)> { - #[cfg(target_os = "windows")] - { - let d3d_sink = ["d3d12videosink", "d3d11videosink"] - .into_iter() - .find(|factory| gst::ElementFactory::find(factory).is_some()); - if let Some(sink) = d3d_sink { - let mut factories = vec![("queue", None)]; - if gst::ElementFactory::find("dwritetextoverlay").is_some() { - factories.push(("dwritetextoverlay", None)); - } - factories.push((sink, Some(false))); - return factories; - } - } - - let mut factories = vec![("queue", None), ("videoconvert", None)]; - if gst::ElementFactory::find("dwritetextoverlay").is_some() { - factories.push(("dwritetextoverlay", None)); - } - factories.push(("autovideosink", Some(false))); - factories -} - -fn link_media_chain( - pipeline: &gst::Pipeline, - src_pad: &gst::Pad, - factories: &[(&str, Option)], - media_label: &str, - render_state: Option<&GstreamerRenderState>, - event_sender: &Option>, - streaming_reported: &Arc, - video_liveness: Option<&VideoLivenessMonitor>, -) -> Result<(), String> { - if media_label == "video" { - if let Some(video_liveness) = video_liveness { - video_liveness.set_stats_overlay(None); - } - } - - let mut elements = Vec::with_capacity(factories.len()); - for (factory, sync_property) in factories { - let factory = *factory; - let element = make_element(factory)?; - if factory == "queue" { - configure_queue_for_low_latency(&element, media_label); - } - if factory == "dwritetextoverlay" { - configure_stats_overlay_element(&element); - if media_label == "video" { - if let Some(video_liveness) = video_liveness { - video_liveness.set_stats_overlay(Some(element.clone())); - } - } - } - if sync_property.is_some() || factory.ends_with("sink") { - if factory == "d3d11videosink" || factory == "d3d12videosink" { - // Fallback decodebin path: never exclusive-fullscreen (Internal default). - configure_d3d_video_sink(&element, false); - } else { - configure_sink_for_low_latency(&element); - } - } - pipeline - .add(&element) - .map_err(|error| format!("Failed to add {factory} for {media_label}: {error}"))?; - elements.push(element); - } - - for pair in elements.windows(2) { - pair[0].link(&pair[1]).map_err(|error| { - format!( - "Failed to link {} -> {} for {media_label}: {error:?}", - pair[0] - .factory() - .map(|factory| factory.name()) - .unwrap_or_default(), - pair[1] - .factory() - .map(|factory| factory.name()) - .unwrap_or_default() - ) - })?; - } - - let first = elements - .first() - .ok_or_else(|| format!("No elements created for {media_label} sink chain."))?; - let Some(first_sink_pad) = first.static_pad("sink") else { - return Err(format!( - "First {media_label} sink-chain element has no sink pad." - )); - }; - src_pad - .link(&first_sink_pad) - .map_err(|error| format!("Failed to link decoded {media_label} pad: {error:?}"))?; - - if let Some(sink) = elements.last() { - if media_label == "video" { - if let Some(render_state) = render_state { - render_state - .set_video_sink(sink.clone(), event_sender) - .map_err(|error| { - format!("Failed to attach decoded video sink to native surface: {error}") - })?; - } - } - watch_first_sink_buffer(sink, media_label, event_sender, streaming_reported); - if media_label == "audio" { - if let Some(video_liveness) = video_liveness { - watch_audio_activity(sink, video_liveness); - } - } - if media_label == "video" { - if let Some(video_liveness) = video_liveness { - watch_video_sink_rate(sink, event_sender, Some(video_liveness.clone())); - video_liveness.start(pipeline.clone(), sink.clone(), event_sender.clone()); - } - } - } - - for element in &elements { - element.sync_state_with_parent().map_err(|error| { - format!("Failed to sync {media_label} sink-chain element state: {error}") - })?; - } - - Ok(()) -} - -fn link_decoded_media_to_fakesink( - pipeline: &gst::Pipeline, - src_pad: &gst::Pad, - label: &str, -) -> Result<(), String> { - if src_pad.is_linked() { - return Ok(()); - } - - let sink = gst::ElementFactory::make("fakesink") - .property("sync", false) - .property("async", false) - .build() - .map_err(|error| format!("Failed to create {label}: {error}"))?; - configure_sink_for_low_latency(&sink); - pipeline - .add(&sink) - .map_err(|error| format!("Failed to add {label}: {error}"))?; - sink.sync_state_with_parent() - .map_err(|error| format!("Failed to sync {label} state: {error}"))?; - - let Some(sink_pad) = sink.static_pad("sink") else { - return Err(format!("{label} has no sink pad.")); - }; - src_pad - .link(&sink_pad) - .map(|_| ()) - .map_err(|error| format!("Failed to link {label}: {error:?}")) -} - -fn make_element(factory: &str) -> Result { - gst::ElementFactory::make(factory) - .build() - .map_err(|error| format!("Failed to create GStreamer element {factory}: {error}")) -} diff --git a/native/opennow-streamer/src/gstreamer_platform.rs b/native/opennow-streamer/src/gstreamer_platform.rs deleted file mode 100644 index 1a936b760..000000000 --- a/native/opennow-streamer/src/gstreamer_platform.rs +++ /dev/null @@ -1,1705 +0,0 @@ -#[cfg(target_os = "windows")] -use crate::gstreamer_backend::send_log; -#[cfg(target_os = "windows")] -use crate::protocol::NativeRenderRect; -use crate::protocol::{Event, NativeRenderSurface, NativeStreamerShortcutBindings}; -#[cfg(target_os = "windows")] -use std::ffi::c_void; -use std::sync::atomic::AtomicBool; -#[cfg(target_os = "windows")] -use std::sync::atomic::Ordering; -use std::sync::mpsc::Sender; -use std::sync::Arc; -#[cfg(target_os = "windows")] -use std::thread; -#[cfg(target_os = "windows")] -use std::time::Duration; - -#[cfg(target_os = "windows")] -fn parse_window_handle(value: &str) -> Result { - let trimmed = value.trim(); - let hex = trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")); - let parsed = if let Some(hex) = hex { - usize::from_str_radix(hex, 16) - } else { - trimmed.parse::() - } - .map_err(|error| format!("Invalid native render window handle {value:?}: {error}"))?; - - if parsed == 0 { - return Err("Native render window handle is zero.".to_owned()); - } - - Ok(parsed) -} - -#[cfg(target_os = "windows")] -fn normalized_render_rect(rect: Option<&NativeRenderRect>) -> NativeRenderRect { - let Some(rect) = rect else { - return NativeRenderRect { - x: 0, - y: 0, - width: 2, - height: 2, - }; - }; - - NativeRenderRect { - x: rect.x.max(0), - y: rect.y.max(0), - width: rect.width.max(2), - height: rect.height.max(2), - } -} - -#[cfg(target_os = "windows")] -pub(crate) fn start_external_renderer_window_guard( - event_sender: Option>, - stop: Arc, -) { - thread::spawn(move || { - let mut logged = false; - while !stop.load(Ordering::SeqCst) { - if stop.load(Ordering::SeqCst) { - break; - } - - let configured = unsafe { win32_renderer_window::protect_process_renderer_window() }; - if configured && !logged { - send_log( - &event_sender, - "info", - "Configured external native renderer window for fullscreen DX11 input capture." - .to_owned(), - ); - logged = true; - } - thread::sleep(if logged { - Duration::from_millis(500) - } else { - Duration::from_millis(100) - }); - } - }); -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn start_external_renderer_window_guard( - _event_sender: Option>, - _stop: Arc, -) { -} - -#[cfg(target_os = "windows")] -pub(crate) fn set_native_shortcut_bindings(bindings: &NativeStreamerShortcutBindings) { - unsafe { - win32_renderer_window::set_shortcut_bindings(bindings.clone()); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn set_native_shortcut_bindings(_bindings: &NativeStreamerShortcutBindings) {} - -#[cfg(target_os = "windows")] -pub(crate) fn clear_native_shortcut_bindings() { - unsafe { - win32_renderer_window::clear_shortcut_bindings(); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn clear_native_shortcut_bindings() {} - -#[cfg(target_os = "windows")] -pub(crate) fn release_native_input_capture() { - unsafe { - win32_renderer_window::release_current_input_capture(); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn release_native_input_capture() {} - -#[cfg(target_os = "windows")] -pub(crate) fn arm_internal_child_input(hwnd: usize) -> bool { - unsafe { win32_renderer_window::arm_internal_child_input(hwnd) } -} - -#[cfg(target_os = "windows")] -pub(crate) fn update_external_renderer_surface(surface: &NativeRenderSurface) { - let target = surface - .window_handle - .as_deref() - .and_then(|window_handle| parse_window_handle(window_handle).ok()) - .and_then(|window_handle| { - surface - .visible - .then_some(()) - .and(surface.rect.as_ref()) - .map(|rect| (window_handle, normalized_render_rect(Some(rect)))) - }); - - unsafe { - win32_renderer_window::set_render_target_surface(target); - } -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn update_external_renderer_surface(_surface: &NativeRenderSurface) {} - -#[cfg(target_os = "windows")] -pub(crate) mod win32_renderer_window { - use crate::gstreamer_input::NativeWindowInputEvent; - use crate::protocol::NativeRenderRect; - use crate::protocol::{NativeStreamerShortcutAction, NativeStreamerShortcutBindings}; - use crate::shortcuts::NativeShortcutMatcher; - use std::collections::{HashMap, HashSet}; - use std::ffi::c_void; - use std::ptr::{null, null_mut}; - use std::sync::atomic::{AtomicU64, Ordering}; - use std::sync::mpsc::Sender; - use std::sync::{Mutex, OnceLock}; - use std::thread; - use std::time::{Duration, Instant}; - - type Bool = i32; - type Dword = u32; - type Hcursor = *mut c_void; - type Hmonitor = *mut c_void; - type Hrawinput = *mut c_void; - type Hwnd = *mut c_void; - type Lparam = isize; - type Lresult = isize; - type Uint = u32; - type Wparam = usize; - - const GWL_STYLE: i32 = -16; - const GWL_EXSTYLE: i32 = -20; - const GWLP_WNDPROC: i32 = -4; - const GW_OWNER: Uint = 4; - const HTCLIENT: isize = 1; - const HWND_NOTOPMOST: Hwnd = -2isize as Hwnd; - const MA_ACTIVATE: isize = 1; - const MONITOR_DEFAULTTONEAREST: Dword = 0x0000_0002; - const RID_INPUT: Uint = 0x1000_0003; - const RIDEV_REMOVE: Dword = 0x0000_0001; - const RIDEV_NOLEGACY: Dword = 0x0000_0030; - // Receive WM_INPUT even when this HWND is not foreground. Required for the - // internal child surface: Electron stays the top-level foreground window, - // so keyboard RawInput never arrives without INPUTSINK. Mouse still works - // via RIDEV_CAPTUREMOUSE alone. - const RIDEV_INPUTSINK: Dword = 0x0000_0100; - const RIDEV_CAPTUREMOUSE: Dword = 0x0000_0200; - const RIM_TYPEMOUSE: Dword = 0; - const RIM_TYPEKEYBOARD: Dword = 1; - const RI_KEY_BREAK: u16 = 0x0001; - const RI_KEY_E0: u16 = 0x0002; - const RI_KEY_E1: u16 = 0x0004; - const RI_MOUSE_LEFT_BUTTON_DOWN: u16 = 0x0001; - const RI_MOUSE_LEFT_BUTTON_UP: u16 = 0x0002; - const RI_MOUSE_RIGHT_BUTTON_DOWN: u16 = 0x0004; - const RI_MOUSE_RIGHT_BUTTON_UP: u16 = 0x0008; - const RI_MOUSE_MIDDLE_BUTTON_DOWN: u16 = 0x0010; - const RI_MOUSE_MIDDLE_BUTTON_UP: u16 = 0x0020; - const RI_MOUSE_BUTTON_4_DOWN: u16 = 0x0040; - const RI_MOUSE_BUTTON_4_UP: u16 = 0x0080; - const RI_MOUSE_BUTTON_5_DOWN: u16 = 0x0100; - const RI_MOUSE_BUTTON_5_UP: u16 = 0x0200; - const RI_MOUSE_WHEEL: u16 = 0x0400; - const VK_SHIFT: u16 = 0x10; - const VK_ESCAPE: u16 = 0x1B; - const VK_V: u16 = 0x56; - const VK_TAB: u16 = 0x09; - const VK_CONTROL: u16 = 0x11; - const VK_MENU: u16 = 0x12; - const VK_CAPITAL: i32 = 0x14; - const VK_NUMLOCK: i32 = 0x90; - const VK_SCROLL: i32 = 0x91; - const VK_LSHIFT: u16 = 0xA0; - const VK_RSHIFT: u16 = 0xA1; - const VK_LCONTROL: u16 = 0xA2; - const VK_RCONTROL: u16 = 0xA3; - const VK_LMENU: u16 = 0xA4; - const VK_RMENU: u16 = 0xA5; - const VK_LWIN: u16 = 0x5B; - const VK_RWIN: u16 = 0x5C; - const WM_INPUT: Uint = 0x00FF; - const WM_NCHITTEST: Uint = 0x0084; - const WM_MOUSEACTIVATE: Uint = 0x0021; - const WM_SETCURSOR: Uint = 0x0020; - const WM_KILLFOCUS: Uint = 0x0008; - const WM_ACTIVATE: Uint = 0x0006; - const WA_INACTIVE: usize = 0; - const WM_KEYDOWN: Uint = 0x0100; - const WM_KEYUP: Uint = 0x0101; - const WM_SYSKEYDOWN: Uint = 0x0104; - const WM_SYSKEYUP: Uint = 0x0105; - const WM_LBUTTONDOWN: Uint = 0x0201; - const WM_LBUTTONUP: Uint = 0x0202; - const WM_RBUTTONDOWN: Uint = 0x0204; - const WM_RBUTTONUP: Uint = 0x0205; - const WM_MBUTTONDOWN: Uint = 0x0207; - const WM_MBUTTONUP: Uint = 0x0208; - const WM_XBUTTONDOWN: Uint = 0x020B; - const WM_XBUTTONUP: Uint = 0x020C; - const XBUTTON1: u16 = 0x0001; - const XBUTTON2: u16 = 0x0002; - const WS_CAPTION: isize = 0x00C0_0000; - const WS_MAXIMIZEBOX: isize = 0x0001_0000; - const WS_MINIMIZEBOX: isize = 0x0002_0000; - const WS_SYSMENU: isize = 0x0008_0000; - const WS_THICKFRAME: isize = 0x0004_0000; - const WS_EX_NOACTIVATE: isize = 0x0800_0000; - const WS_EX_TOOLWINDOW: isize = 0x0000_0080; - const WS_EX_TRANSPARENT: isize = 0x0000_0020; - const SWP_NOSIZE: u32 = 0x0001; - const SWP_NOMOVE: u32 = 0x0002; - const SWP_NOACTIVATE: u32 = 0x0010; - const SWP_FRAMECHANGED: u32 = 0x0020; - const SW_MINIMIZE: i32 = 6; - const ESCAPE_SCANCODE: u16 = 0x0001; - const ESCAPE_HOLD_TO_MINIMIZE: Duration = Duration::from_secs(5); - - struct EnumState { - process_id: u32, - candidates: Vec, - } - - #[derive(Clone, Copy)] - struct WindowCandidate { - hwnd: Hwnd, - area: i64, - } - - #[derive(Clone, Copy)] - struct RenderTargetSurface { - hwnd: isize, - client_rect: Rect, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct Rect { - left: i32, - top: i32, - right: i32, - bottom: i32, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct Point { - x: i32, - y: i32, - } - - #[repr(C)] - struct MonitorInfo { - cb_size: Dword, - rc_monitor: Rect, - rc_work: Rect, - dw_flags: Dword, - } - - #[repr(C)] - struct RawInputDevice { - us_usage_page: u16, - us_usage: u16, - dw_flags: Dword, - hwnd_target: Hwnd, - } - - #[repr(C)] - struct RawInputHeader { - dw_type: Dword, - dw_size: Dword, - h_device: *mut c_void, - w_param: Wparam, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct RawMouse { - us_flags: u16, - buttons: u32, - ul_raw_buttons: u32, - l_last_x: i32, - l_last_y: i32, - ul_extra_information: u32, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct RawKeyboard { - make_code: u16, - flags: u16, - reserved: u16, - vkey: u16, - message: Uint, - extra_information: u32, - } - - #[derive(Clone, Copy)] - struct PressedKey { - keycode: u16, - scancode: u16, - suppressed: bool, - } - - #[derive(Clone, Copy)] - struct EscapeKeyPress { - scancode: u16, - hold_timer_armed: bool, - } - - static INPUT_EVENT_SENDER: OnceLock>>> = - OnceLock::new(); - static ORIGINAL_WNDPROCS: OnceLock>> = OnceLock::new(); - static CAPTURED_HWND: OnceLock>> = OnceLock::new(); - static PROTECTED_HWND: OnceLock>> = OnceLock::new(); - static PRESSED_KEYS: OnceLock>> = OnceLock::new(); - static LAST_LOCK_KEYS_STATE: OnceLock> = OnceLock::new(); - static LEGACY_SUPPRESSED_KEYS: OnceLock>> = OnceLock::new(); - static STARTED_AT: OnceLock = OnceLock::new(); - static ESCAPE_HOLD_HWND: OnceLock>> = OnceLock::new(); - static ESCAPE_HOLD_TOKEN: OnceLock = OnceLock::new(); - static ESCAPE_KEY_PRESS: OnceLock>> = OnceLock::new(); - static SHORTCUT_MATCHER: OnceLock> = OnceLock::new(); - static RENDER_TARGET_SURFACE: OnceLock>> = OnceLock::new(); - - #[link(name = "user32")] - unsafe extern "system" { - fn CallWindowProcW( - previous: isize, - hwnd: Hwnd, - message: Uint, - wparam: Wparam, - lparam: Lparam, - ) -> Lresult; - fn ClientToScreen(hwnd: Hwnd, point: *mut Point) -> Bool; - fn ClipCursor(rect: *const Rect) -> Bool; - fn DefWindowProcW(hwnd: Hwnd, message: Uint, wparam: Wparam, lparam: Lparam) -> Lresult; - fn EnumWindows( - callback: Option Bool>, - lparam: Lparam, - ) -> Bool; - fn GetMonitorInfoW(monitor: Hmonitor, info: *mut MonitorInfo) -> Bool; - fn GetRawInputData( - raw_input: Hrawinput, - command: Uint, - data: *mut c_void, - size: *mut u32, - header_size: u32, - ) -> u32; - fn GetKeyState(virtual_key: i32) -> i16; - fn GetWindow(hwnd: Hwnd, command: Uint) -> Hwnd; - fn GetWindowLongPtrW(hwnd: Hwnd, index: i32) -> isize; - fn GetWindowRect(hwnd: Hwnd, rect: *mut Rect) -> Bool; - fn GetWindowThreadProcessId(hwnd: Hwnd, process_id: *mut u32) -> u32; - fn IsIconic(hwnd: Hwnd) -> Bool; - fn IsWindowVisible(hwnd: Hwnd) -> Bool; - fn MonitorFromWindow(hwnd: Hwnd, flags: Dword) -> Hmonitor; - fn RegisterRawInputDevices(devices: *const RawInputDevice, count: u32, size: u32) -> Bool; - fn ReleaseCapture() -> Bool; - fn SetCapture(hwnd: Hwnd) -> Hwnd; - fn SetCursor(cursor: Hcursor) -> Hcursor; - fn SetFocus(hwnd: Hwnd) -> Hwnd; - fn SetForegroundWindow(hwnd: Hwnd) -> Bool; - fn SetWindowLongPtrW(hwnd: Hwnd, index: i32, new_long: isize) -> isize; - fn SetWindowPos( - hwnd: Hwnd, - insert_after: Hwnd, - x: i32, - y: i32, - cx: i32, - cy: i32, - flags: u32, - ) -> Bool; - fn ShowWindow(hwnd: Hwnd, command: i32) -> Bool; - fn ShowCursor(show: Bool) -> i32; - } - - #[link(name = "kernel32")] - unsafe extern "system" { - fn GetCurrentProcessId() -> u32; - } - - pub unsafe fn set_render_target_surface(target: Option<(usize, NativeRenderRect)>) { - let target_surface = target.map(|(window_handle, rect)| RenderTargetSurface { - hwnd: window_handle as isize, - client_rect: Rect { - left: rect.x, - top: rect.y, - right: rect.x.saturating_add(rect.width.max(2)), - bottom: rect.y.saturating_add(rect.height.max(2)), - }, - }); - let slot = RENDER_TARGET_SURFACE.get_or_init(|| Mutex::new(None)); - if let Ok(mut current) = slot.lock() { - *current = target_surface; - } - } - - pub unsafe fn set_input_event_sender(sender: Option>) { - let input_stopped = sender.is_none(); - let slot = INPUT_EVENT_SENDER.get_or_init(|| Mutex::new(None)); - if let Ok(mut current) = slot.lock() { - *current = sender; - } - if input_stopped { - unregister_raw_input_devices(); - } - } - - pub unsafe fn set_shortcut_bindings(bindings: NativeStreamerShortcutBindings) { - let matcher = SHORTCUT_MATCHER.get_or_init(|| Mutex::new(NativeShortcutMatcher::default())); - if let Ok(mut current) = matcher.lock() { - *current = NativeShortcutMatcher::from_bindings(&bindings); - } - } - - pub unsafe fn clear_shortcut_bindings() { - let matcher = SHORTCUT_MATCHER.get_or_init(|| Mutex::new(NativeShortcutMatcher::default())); - if let Ok(mut current) = matcher.lock() { - *current = NativeShortcutMatcher::default(); - } - } - - pub unsafe fn release_current_input_capture() { - let Some(captured) = CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - else { - if !crate::gstreamer_config::use_internal_renderer() { - unregister_raw_input_devices(); - } - return; - }; - - release_input_capture(captured as Hwnd); - } - - /// Arm RawInput on the internal child HWND (sibling of Intermediate D3D). - /// Chains over the child's existing wndproc so SET_BOUNDS still works. - pub unsafe fn arm_internal_child_input(hwnd: usize) -> bool { - if hwnd == 0 { - return false; - } - let hwnd = hwnd as Hwnd; - let protected_slot = PROTECTED_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut protected) = protected_slot.lock() { - *protected = Some(hwnd as isize); - } - let wndproc_installed = install_input_wndproc(hwnd); - let keyboard_registered = register_internal_raw_keyboard(hwnd); - wndproc_installed || keyboard_registered - } - - pub unsafe fn protect_process_renderer_window() -> bool { - let mut state = EnumState { - process_id: GetCurrentProcessId(), - candidates: Vec::new(), - }; - EnumWindows( - Some(collect_renderer_window_candidate), - &mut state as *mut EnumState as Lparam, - ); - - let Some(candidate) = state - .candidates - .into_iter() - .max_by_key(|candidate| candidate.area) - else { - return false; - }; - - protect_renderer_window(candidate.hwnd) - } - - unsafe extern "system" fn collect_renderer_window_candidate( - hwnd: Hwnd, - lparam: Lparam, - ) -> Bool { - let state = &mut *(lparam as *mut EnumState); - let mut process_id = 0; - GetWindowThreadProcessId(hwnd, &mut process_id); - if process_id != state.process_id || IsWindowVisible(hwnd) == 0 || IsIconic(hwnd) != 0 { - return 1; - } - - if !GetWindow(hwnd, GW_OWNER).is_null() { - return 1; - } - - let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - if (ex_style & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE)) != 0 { - return 1; - } - - let mut rect = Rect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if GetWindowRect(hwnd, &mut rect) == 0 { - return 1; - } - let width = rect.right.saturating_sub(rect.left); - let height = rect.bottom.saturating_sub(rect.top); - if width < 320 || height < 180 { - return 1; - } - - state.candidates.push(WindowCandidate { - hwnd, - area: i64::from(width) * i64::from(height), - }); - 1 - } - - unsafe fn protect_renderer_window(hwnd: Hwnd) -> bool { - let protected_slot = PROTECTED_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut protected) = protected_slot.lock() { - *protected = Some(hwnd as isize); - } - - let mut configured = false; - let current = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - let desired = current & !(WS_EX_NOACTIVATE | WS_EX_TRANSPARENT); - if desired != current { - SetWindowLongPtrW(hwnd, GWL_EXSTYLE, desired); - configured = true; - } - - let current_style = GetWindowLongPtrW(hwnd, GWL_STYLE); - let fullscreen_style = current_style - & !(WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); - if fullscreen_style != current_style { - SetWindowLongPtrW(hwnd, GWL_STYLE, fullscreen_style); - configured = true; - } - - if install_input_wndproc(hwnd) { - SetForegroundWindow(hwnd); - SetFocus(hwnd); - configured = true; - } - - if let Some(rect) = target_renderer_rect().or_else(|| monitor_rect_for_window(hwnd)) { - SetWindowPos( - hwnd, - HWND_NOTOPMOST, - rect.left, - rect.top, - rect.right.saturating_sub(rect.left).max(2), - rect.bottom.saturating_sub(rect.top).max(2), - SWP_NOACTIVATE | SWP_FRAMECHANGED, - ); - configured = true; - } else { - SetWindowPos( - hwnd, - HWND_NOTOPMOST, - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED, - ); - } - - configured - } - - unsafe fn render_rect_to_screen_rect(hwnd: Hwnd, rect: Rect) -> Option { - if hwnd.is_null() { - return None; - } - let mut origin = Point { - x: rect.left, - y: rect.top, - }; - if ClientToScreen(hwnd, &mut origin) == 0 { - return None; - } - let width = rect.right.saturating_sub(rect.left).max(2); - let height = rect.bottom.saturating_sub(rect.top).max(2); - - Some(Rect { - left: origin.x, - top: origin.y, - right: origin.x.saturating_add(width), - bottom: origin.y.saturating_add(height), - }) - } - - unsafe fn install_input_wndproc(hwnd: Hwnd) -> bool { - let key = hwnd as isize; - let map = ORIGINAL_WNDPROCS.get_or_init(|| Mutex::new(HashMap::new())); - let Ok(mut map) = map.lock() else { - return false; - }; - if map.contains_key(&key) { - return false; - } - - let previous = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, renderer_window_wndproc as isize); - if previous == 0 { - return false; - } - map.insert(key, previous); - true - } - - unsafe extern "system" fn renderer_window_wndproc( - hwnd: Hwnd, - message: Uint, - wparam: Wparam, - lparam: Lparam, - ) -> Lresult { - if message == WM_NCHITTEST { - return HTCLIENT; - } - if message == WM_MOUSEACTIVATE { - begin_input_capture(hwnd); - return MA_ACTIVATE; - } - if message == WM_SETCURSOR && is_input_captured(hwnd) { - SetCursor(null_mut()); - return 1; - } - if message == WM_INPUT { - handle_raw_input(lparam as Hrawinput); - return 0; - } - let handled_legacy_shortcut = is_keyboard_message(message) - && handle_legacy_shortcut_keyboard(message, wparam, lparam); - if handled_legacy_shortcut { - return 0; - } - if is_escape_keyboard_message(message, wparam) { - if !is_input_captured(hwnd) { - begin_input_capture(hwnd); - } - handle_legacy_escape_keyboard(message, lparam); - return 0; - } - if message == WM_KILLFOCUS || (message == WM_ACTIVATE && (wparam & 0xffff) == WA_INACTIVE) { - release_input_capture(hwnd); - } - if let Some((button, pressed)) = legacy_mouse_button(message, wparam) { - let was_captured = is_input_captured(hwnd); - if pressed && !was_captured { - begin_input_capture(hwnd); - emit_input_event(NativeWindowInputEvent::MouseButton { - pressed, - button, - timestamp_us: timestamp_us(), - }); - } - } - - let key = hwnd as isize; - let previous = ORIGINAL_WNDPROCS - .get() - .and_then(|map| map.lock().ok().and_then(|map| map.get(&key).copied())); - if let Some(previous) = previous { - return CallWindowProcW(previous, hwnd, message, wparam, lparam); - } - - DefWindowProcW(hwnd, message, wparam, lparam) - } - - unsafe fn monitor_rect_for_window(hwnd: Hwnd) -> Option { - let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - if monitor.is_null() { - return None; - } - - let mut info = MonitorInfo { - cb_size: std::mem::size_of::() as Dword, - rc_monitor: Rect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }, - rc_work: Rect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }, - dw_flags: 0, - }; - if GetMonitorInfoW(monitor, &mut info) == 0 { - return None; - } - - Some(info.rc_monitor) - } - - unsafe fn target_renderer_rect() -> Option { - let target = RENDER_TARGET_SURFACE - .get() - .and_then(|surface| surface.lock().ok().and_then(|surface| *surface))?; - let hwnd = target.hwnd as Hwnd; - render_rect_to_screen_rect(hwnd, target.client_rect) - .or_else(|| monitor_rect_for_window(hwnd)) - } - - unsafe fn begin_input_capture(hwnd: Hwnd) { - // External floating window: take OS focus so RawInput + ClipCursor work - // without INPUTSINK. Internal child: leave Electron as foreground so its - // shortcut keydown handlers keep working; keyboard arrives via INPUTSINK. - if !crate::gstreamer_config::use_internal_renderer() { - SetForegroundWindow(hwnd); - SetFocus(hwnd); - } - SetCapture(hwnd); - register_raw_input_devices(hwnd); - if let Some(rect) = target_renderer_rect().or_else(|| monitor_rect_for_window(hwnd)) { - ClipCursor(&rect); - } - hide_cursor(); - emit_input_capture_changed(true); - - let slot = CAPTURED_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut captured) = slot.lock() { - *captured = Some(hwnd as isize); - } - sync_lock_keys_state(true); - } - - unsafe fn release_input_capture(hwnd: Hwnd) { - cancel_escape_hold_to_minimize_timer(); - clear_escape_key_press(); - let slot = CAPTURED_HWND.get_or_init(|| Mutex::new(None)); - let mut should_release = false; - if let Ok(mut captured) = slot.lock() { - should_release = captured.is_some_and(|captured| captured == hwnd as isize); - if should_release { - *captured = None; - } - } - - if !should_release { - return; - } - - release_pressed_keys(); - ReleaseCapture(); - ClipCursor(null()); - show_cursor(); - emit_input_capture_changed(false); - if crate::gstreamer_config::use_internal_renderer() { - // F10/F8 release relative mouse capture, but the internal stream - // must keep receiving keyboard input (especially Escape) while the - // Electron window remains foreground. - unregister_raw_mouse_device(); - register_internal_raw_keyboard(hwnd); - } else { - unregister_raw_input_devices(); - } - SetWindowPos( - hwnd, - HWND_NOTOPMOST, - 0, - 0, - 0, - 0, - SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, - ); - } - - fn is_input_captured(hwnd: Hwnd) -> bool { - CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - .is_some_and(|captured| captured == hwnd as isize) - } - - fn captured_hwnd() -> Option { - CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - } - - fn protected_hwnd() -> Option { - PROTECTED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - } - - unsafe fn start_escape_hold_to_minimize_timer() { - let Some(hwnd) = captured_hwnd() else { - return; - }; - - let token = ESCAPE_HOLD_TOKEN - .get_or_init(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::SeqCst) - .wrapping_add(1); - let slot = ESCAPE_HOLD_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut held_hwnd) = slot.lock() { - *held_hwnd = Some(hwnd); - } - - thread::spawn(move || { - thread::sleep(ESCAPE_HOLD_TO_MINIMIZE); - unsafe { - minimize_window_if_escape_still_held(hwnd, token); - } - }); - } - - fn cancel_escape_hold_to_minimize_timer() { - ESCAPE_HOLD_TOKEN - .get_or_init(|| AtomicU64::new(0)) - .fetch_add(1, Ordering::SeqCst); - let slot = ESCAPE_HOLD_HWND.get_or_init(|| Mutex::new(None)); - if let Ok(mut held_hwnd) = slot.lock() { - *held_hwnd = None; - } - } - - unsafe fn minimize_window_if_escape_still_held(hwnd: isize, token: u64) { - let current_token = ESCAPE_HOLD_TOKEN - .get_or_init(|| AtomicU64::new(0)) - .load(Ordering::SeqCst); - if current_token != token { - return; - } - - let still_held = ESCAPE_HOLD_HWND - .get() - .and_then(|held_hwnd| held_hwnd.lock().ok().and_then(|held_hwnd| *held_hwnd)) - .is_some_and(|held_hwnd| held_hwnd == hwnd); - if !still_held { - return; - } - - // Consume the held Escape so key-up does not also send a tap to GFN. - clear_escape_key_press(); - cancel_escape_hold_to_minimize_timer(); - - let hwnd = hwnd as Hwnd; - release_input_capture(hwnd); - - ShowWindow(hwnd, SW_MINIMIZE); - } - - unsafe fn register_raw_input_devices(hwnd: Hwnd) -> bool { - let keyboard_flags = if crate::gstreamer_config::use_internal_renderer() { - // Internal: Electron stays foreground and must keep receiving legacy - // WM_KEYDOWN for UI shortcuts. Do NOT set RIDEV_NOLEGACY on keyboard - // or Electron shortcuts die. INPUTSINK delivers WM_INPUT while Electron - // remains the top-level foreground window. - RIDEV_INPUTSINK - } else { - RIDEV_NOLEGACY - }; - let mouse_flags = if crate::gstreamer_config::use_internal_renderer() { - RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE | RIDEV_INPUTSINK - } else { - RIDEV_NOLEGACY | RIDEV_CAPTUREMOUSE - }; - let devices = [ - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x02, - dw_flags: mouse_flags, - hwnd_target: hwnd, - }, - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x06, - dw_flags: keyboard_flags, - hwnd_target: hwnd, - }, - ]; - - RegisterRawInputDevices( - devices.as_ptr(), - devices.len() as u32, - std::mem::size_of::() as u32, - ) != 0 - } - - unsafe fn register_internal_raw_keyboard(hwnd: Hwnd) -> bool { - let device = RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x06, - dw_flags: RIDEV_INPUTSINK, - hwnd_target: hwnd, - }; - - RegisterRawInputDevices( - &device, - 1, - std::mem::size_of::() as u32, - ) != 0 - } - - unsafe fn unregister_raw_mouse_device() -> bool { - let device = RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x02, - dw_flags: RIDEV_REMOVE, - hwnd_target: null_mut(), - }; - - RegisterRawInputDevices( - &device, - 1, - std::mem::size_of::() as u32, - ) != 0 - } - - unsafe fn unregister_raw_input_devices() -> bool { - let devices = [ - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x02, - dw_flags: RIDEV_REMOVE, - hwnd_target: null_mut(), - }, - RawInputDevice { - us_usage_page: 0x01, - us_usage: 0x06, - dw_flags: RIDEV_REMOVE, - hwnd_target: null_mut(), - }, - ]; - - RegisterRawInputDevices( - devices.as_ptr(), - devices.len() as u32, - std::mem::size_of::() as u32, - ) != 0 - } - - unsafe fn handle_raw_input(raw_input: Hrawinput) { - let mut size = 0u32; - let header_size = std::mem::size_of::() as u32; - let query = GetRawInputData(raw_input, RID_INPUT, null_mut(), &mut size, header_size); - if query == u32::MAX || size < header_size { - return; - } - - let mut buffer = vec![0u8; size as usize]; - let read = GetRawInputData( - raw_input, - RID_INPUT, - buffer.as_mut_ptr() as *mut c_void, - &mut size, - header_size, - ); - if read == u32::MAX || read == 0 || buffer.len() < header_size as usize { - return; - } - - let header = &*(buffer.as_ptr() as *const RawInputHeader); - let data = buffer.as_ptr().add(std::mem::size_of::()); - match header.dw_type { - RIM_TYPEMOUSE => handle_raw_mouse(&*(data as *const RawMouse)), - RIM_TYPEKEYBOARD => handle_raw_keyboard(&*(data as *const RawKeyboard)), - _ => {} - } - } - - unsafe fn handle_raw_mouse(raw: &RawMouse) { - if CAPTURED_HWND - .get() - .and_then(|captured| captured.lock().ok().and_then(|captured| *captured)) - .is_none() - { - return; - } - - let timestamp_us = timestamp_us(); - let dx = clamp_i32_to_i16(raw.l_last_x); - let dy = clamp_i32_to_i16(raw.l_last_y); - if dx != 0 || dy != 0 { - emit_input_event(NativeWindowInputEvent::MouseMove { - dx, - dy, - timestamp_us, - }); - } - - let button_flags = (raw.buttons & 0xffff) as u16; - let button_data = (raw.buttons >> 16) as u16; - emit_raw_mouse_button_events(button_flags, timestamp_us); - if (button_flags & RI_MOUSE_WHEEL) != 0 { - emit_input_event(NativeWindowInputEvent::MouseWheel { - delta: button_data as i16, - timestamp_us, - }); - } - } - - unsafe fn emit_raw_mouse_button_events(flags: u16, timestamp_us: u64) { - let pairs = [ - (RI_MOUSE_LEFT_BUTTON_DOWN, 1, true), - (RI_MOUSE_LEFT_BUTTON_UP, 1, false), - (RI_MOUSE_MIDDLE_BUTTON_DOWN, 2, true), - (RI_MOUSE_MIDDLE_BUTTON_UP, 2, false), - (RI_MOUSE_RIGHT_BUTTON_DOWN, 3, true), - (RI_MOUSE_RIGHT_BUTTON_UP, 3, false), - (RI_MOUSE_BUTTON_4_DOWN, 4, true), - (RI_MOUSE_BUTTON_4_UP, 4, false), - (RI_MOUSE_BUTTON_5_DOWN, 5, true), - (RI_MOUSE_BUTTON_5_UP, 5, false), - ]; - - for (flag, button, pressed) in pairs { - if (flags & flag) != 0 { - emit_input_event(NativeWindowInputEvent::MouseButton { - pressed, - button, - timestamp_us, - }); - } - } - } - - unsafe fn handle_raw_keyboard(raw: &RawKeyboard) { - if raw.vkey == 0xff { - return; - } - - let pressed = match raw.message { - WM_KEYDOWN | WM_SYSKEYDOWN => true, - WM_KEYUP | WM_SYSKEYUP => false, - _ => (raw.flags & RI_KEY_BREAK) == 0, - }; - let keycode = normalize_virtual_key(raw.vkey, raw.make_code, raw.flags); - let mut scancode = normalize_scancode(raw.make_code, raw.flags); - if keycode == VK_ESCAPE && scancode == 0 { - scancode = ESCAPE_SCANCODE; - } - if keycode == 0 || scancode == 0 { - return; - } - handle_keyboard_state(keycode, scancode, pressed); - } - - unsafe fn handle_legacy_escape_keyboard(message: Uint, lparam: Lparam) { - let pressed = matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN); - let mut scancode = legacy_keyboard_scancode(lparam); - if scancode == 0 { - scancode = ESCAPE_SCANCODE; - } - handle_keyboard_state(VK_ESCAPE, scancode, pressed); - } - - fn is_escape_keyboard_message(message: Uint, wparam: Wparam) -> bool { - matches!(message, WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP) - && (wparam as u16) == VK_ESCAPE - } - - fn is_keyboard_message(message: Uint) -> bool { - matches!(message, WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP) - } - - fn legacy_keyboard_scancode(lparam: Lparam) -> u16 { - let scancode = ((lparam >> 16) & 0xff) as u16; - if scancode == 0 { - return 0; - } - if ((lparam >> 24) & 0x01) != 0 { - 0xe000 | scancode - } else { - scancode - } - } - - unsafe fn handle_legacy_shortcut_keyboard( - message: Uint, - wparam: Wparam, - lparam: Lparam, - ) -> bool { - let keycode = wparam as u16; - if keycode == VK_ESCAPE { - return false; - } - - let pressed = matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN); - let scancode = legacy_keyboard_scancode(lparam); - let key_id = if scancode == 0 { keycode } else { scancode }; - let suppressed_keys = LEGACY_SUPPRESSED_KEYS.get_or_init(|| Mutex::new(HashSet::new())); - let Ok(mut suppressed_keys) = suppressed_keys.lock() else { - return false; - }; - - if !pressed { - return suppressed_keys.remove(&key_id); - } - - if suppressed_keys.contains(&key_id) { - return true; - } - - let modifiers = current_legacy_modifier_flags(); - if pressed && is_clipboard_paste_shortcut(keycode, modifiers) { - suppressed_keys.insert(key_id); - drop(suppressed_keys); - emit_clipboard_paste_request(); - return true; - } - - let Some(action) = shortcut_action_for_keypress(keycode, scancode, modifiers) else { - return false; - }; - - suppressed_keys.insert(key_id); - drop(suppressed_keys); - handle_shortcut_action(action); - true - } - - unsafe fn handle_keyboard_state(keycode: u16, scancode: u16, pressed: bool) { - sync_lock_keys_state(false); - - let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); - let Ok(mut keys) = keys.lock() else { - return; - }; - if pressed && keycode == VK_TAB && is_alt_modifier_down(&keys) { - drop(keys); - release_current_input_capture(); - return; - } - if keycode == VK_ESCAPE { - drop(keys); - // In the internal renderer Electron must intercept the legacy Escape - // before Chromium exits fullscreen, then forward exactly one tap over - // IPC. RawInput still owns every other key in this mode. The external - // native window has no Electron interception and keeps this path. - if crate::gstreamer_config::use_internal_renderer() { - return; - } - handle_escape_keyboard_state(scancode, pressed); - return; - } - let previous = keys.get(&scancode).copied(); - if pressed { - if previous.is_some() { - return; - } - let modifiers = pressed_key_modifier_flags(&keys, keycode); - if is_clipboard_paste_shortcut(keycode, modifiers) { - keys.insert( - scancode, - PressedKey { - keycode, - scancode, - suppressed: true, - }, - ); - drop(keys); - emit_clipboard_paste_request(); - return; - } - if let Some(action) = shortcut_action_for_keypress(keycode, scancode, modifiers) { - keys.insert( - scancode, - PressedKey { - keycode, - scancode, - suppressed: true, - }, - ); - drop(keys); - handle_shortcut_action(action); - return; - } - keys.insert( - scancode, - PressedKey { - keycode, - scancode, - suppressed: false, - }, - ); - } else if let Some(previous) = keys.remove(&scancode) { - if previous.suppressed { - return; - } - } - let modifiers = pressed_key_modifier_flags(&keys, keycode); - drop(keys); - - emit_input_event(NativeWindowInputEvent::Key { - pressed, - keycode, - scancode, - modifiers, - timestamp_us: timestamp_us(), - }); - } - - unsafe fn handle_escape_keyboard_state(scancode: u16, pressed: bool) { - let slot = ESCAPE_KEY_PRESS.get_or_init(|| Mutex::new(None)); - let Ok(mut escape_press) = slot.lock() else { - return; - }; - - if pressed { - let should_start_hold_timer = if let Some(current) = escape_press.as_mut() { - let should_start = !crate::gstreamer_config::use_internal_renderer() - && !current.hold_timer_armed - && captured_hwnd().is_some(); - if should_start { - current.hold_timer_armed = true; - } - should_start - } else { - let hold_timer_armed = - !crate::gstreamer_config::use_internal_renderer() && captured_hwnd().is_some(); - *escape_press = Some(EscapeKeyPress { - scancode, - hold_timer_armed, - }); - hold_timer_armed - }; - drop(escape_press); - if should_start_hold_timer { - start_escape_hold_to_minimize_timer(); - } - return; - } - - let Some(escape_press) = escape_press.take() else { - cancel_escape_hold_to_minimize_timer(); - return; - }; - let scancode = escape_press.scancode; - - cancel_escape_hold_to_minimize_timer(); - send_escape_tap(scancode); - } - - fn clear_escape_key_press() { - let slot = ESCAPE_KEY_PRESS.get_or_init(|| Mutex::new(None)); - if let Ok(mut escape_press) = slot.lock() { - *escape_press = None; - } - } - - fn send_escape_tap(scancode: u16) { - let keydown_timestamp_us = timestamp_us(); - emit_input_event(NativeWindowInputEvent::Key { - pressed: true, - keycode: VK_ESCAPE, - scancode, - modifiers: 0, - timestamp_us: keydown_timestamp_us, - }); - emit_input_event(NativeWindowInputEvent::Key { - pressed: false, - keycode: VK_ESCAPE, - scancode, - modifiers: 0, - timestamp_us: timestamp_us(), - }); - } - - unsafe fn release_pressed_keys() { - let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); - let Ok(mut keys) = keys.lock() else { - return; - }; - let pressed = keys.values().copied().collect::>(); - keys.clear(); - drop(keys); - - let timestamp_us = timestamp_us(); - for key in pressed { - if key.suppressed { - continue; - } - emit_input_event(NativeWindowInputEvent::Key { - pressed: false, - keycode: key.keycode, - scancode: key.scancode, - modifiers: 0, - timestamp_us, - }); - } - } - - fn normalize_virtual_key(vkey: u16, make_code: u16, flags: u16) -> u16 { - match vkey { - VK_SHIFT => match make_code { - 0x36 => VK_RSHIFT, - _ => VK_LSHIFT, - }, - VK_CONTROL => { - if (flags & RI_KEY_E0) != 0 { - VK_RCONTROL - } else { - VK_LCONTROL - } - } - VK_MENU => { - if (flags & RI_KEY_E0) != 0 { - VK_RMENU - } else { - VK_LMENU - } - } - _ => vkey, - } - } - - fn normalize_scancode(make_code: u16, flags: u16) -> u16 { - if make_code == 0 { - return 0; - } - if (flags & RI_KEY_E0) != 0 { - 0xe000 | make_code - } else if (flags & RI_KEY_E1) != 0 { - 0xe100 | make_code - } else { - make_code - } - } - - /// Lock-key bitmask for INPUT_LOCK_KEYS_SYNC (official GFN iS() on desktop Windows). - unsafe fn lock_keys_sync_state() -> u8 { - let mut state = 0x10; - if (GetKeyState(VK_CAPITAL) & 0x0001) != 0 { - state |= 0x01; - } - state |= 0x20; - state |= 0x40; - if (GetKeyState(VK_NUMLOCK) & 0x0001) != 0 { - state |= 0x02; - } - if (GetKeyState(VK_SCROLL) & 0x0001) != 0 { - state |= 0x04; - } - state - } - - unsafe fn sync_lock_keys_state(force: bool) { - let state = lock_keys_sync_state(); - let slot = LAST_LOCK_KEYS_STATE.get_or_init(|| Mutex::new(0)); - let Ok(mut last) = slot.lock() else { - return; - }; - if !force && *last == state { - return; - } - *last = state; - drop(last); - emit_input_event(NativeWindowInputEvent::LockKeysSync { state }); - } - - /// Per-key modifier byte from tracked pressed keys (official GFN yS()/Cb()). - /// Lock keys sync separately via INPUT_LOCK_KEYS_SYNC, not here. - unsafe fn pressed_key_modifier_flags( - keys: &HashMap, - active_keycode: u16, - ) -> u16 { - let mut modifiers = 0u16; - let mut shift_tracked = false; - let mut control_tracked = false; - let mut alt_tracked = false; - let mut win_tracked = false; - - for key in keys.values() { - if key.keycode == active_keycode { - continue; - } - match key.keycode { - VK_LSHIFT | VK_RSHIFT | VK_SHIFT => { - shift_tracked = true; - modifiers |= 0x01; - } - VK_LCONTROL | VK_RCONTROL | VK_CONTROL => { - control_tracked = true; - modifiers |= 0x02; - } - VK_LMENU | VK_RMENU | VK_MENU => { - alt_tracked = true; - modifiers |= 0x04; - } - VK_LWIN | VK_RWIN => { - win_tracked = true; - modifiers |= 0x08; - } - _ => {} - } - } - - if !matches!(active_keycode, VK_LSHIFT | VK_RSHIFT | VK_SHIFT) - && !shift_tracked - && (is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT)) - { - modifiers |= 0x01; - } - if !matches!(active_keycode, VK_LCONTROL | VK_RCONTROL | VK_CONTROL) - && !control_tracked - && (is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL)) - { - modifiers |= 0x02; - } - if !matches!(active_keycode, VK_LMENU | VK_RMENU | VK_MENU) - && !alt_tracked - && (is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU)) - { - modifiers |= 0x04; - } - if !matches!(active_keycode, VK_LWIN | VK_RWIN) - && !win_tracked - && (is_key_down(VK_LWIN) || is_key_down(VK_RWIN)) - { - modifiers |= 0x08; - } - - modifiers - } - - /// Legacy fallback for shortcut matching before a key enters the pressed-key map. - unsafe fn keyboard_modifier_flags(active_keycode: u16) -> u16 { - let keys = PRESSED_KEYS.get_or_init(|| Mutex::new(HashMap::new())); - if let Ok(keys) = keys.lock() { - if !keys.is_empty() { - return pressed_key_modifier_flags(&keys, active_keycode); - } - } - - let mut modifiers = 0u16; - if !matches!(active_keycode, VK_LSHIFT | VK_RSHIFT | VK_SHIFT) - && (is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT)) - { - modifiers |= 0x01; - } - if !matches!(active_keycode, VK_LCONTROL | VK_RCONTROL | VK_CONTROL) - && (is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL)) - { - modifiers |= 0x02; - } - if !matches!(active_keycode, VK_LMENU | VK_RMENU | VK_MENU) - && (is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU)) - { - modifiers |= 0x04; - } - if !matches!(active_keycode, VK_LWIN | VK_RWIN) - && (is_key_down(VK_LWIN) || is_key_down(VK_RWIN)) - { - modifiers |= 0x08; - } - modifiers - } - - unsafe fn current_legacy_modifier_flags() -> u16 { - let mut modifiers = 0u16; - if is_key_down(VK_SHIFT) || is_key_down(VK_LSHIFT) || is_key_down(VK_RSHIFT) { - modifiers |= 0x01; - } - if is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL) { - modifiers |= 0x02; - } - if is_key_down(VK_MENU) || is_key_down(VK_LMENU) || is_key_down(VK_RMENU) { - modifiers |= 0x04; - } - if is_key_down(VK_LWIN) || is_key_down(VK_RWIN) { - modifiers |= 0x08; - } - if (GetKeyState(VK_CAPITAL) & 0x0001) != 0 { - modifiers |= 0x10; - } - if (GetKeyState(VK_NUMLOCK) & 0x0001) != 0 { - modifiers |= 0x20; - } - modifiers - } - - unsafe fn is_key_down(keycode: u16) -> bool { - ((GetKeyState(keycode as i32) as u16) & 0x8000) != 0 - } - - unsafe fn is_alt_modifier_down(keys: &HashMap) -> bool { - keys.values() - .any(|key| matches!(key.keycode, VK_LMENU | VK_RMENU | VK_MENU)) - || ((GetKeyState(VK_MENU as i32) as u16) & 0x8000) != 0 - } - - fn shortcut_action_for_keypress( - keycode: u16, - scancode: u16, - modifiers: u16, - ) -> Option { - SHORTCUT_MATCHER - .get() - .and_then(|matcher| matcher.lock().ok()) - .and_then(|matcher| matcher.match_keydown(keycode, scancode, modifiers)) - } - - unsafe fn handle_shortcut_action(action: NativeStreamerShortcutAction) { - match action { - NativeStreamerShortcutAction::TogglePointerLock => { - if let Some(hwnd) = captured_hwnd().or_else(protected_hwnd) { - let hwnd = hwnd as Hwnd; - if is_input_captured(hwnd) { - release_input_capture(hwnd); - } else { - begin_input_capture(hwnd); - } - } - // Internal: Electron's own keydown owns the UI shortcut; only - // toggle RawInput capture here and keep the key out of GFN. - if crate::gstreamer_config::use_internal_renderer() { - return; - } - } - _ => { - if shortcut_action_releases_input_capture(action) { - release_current_input_capture(); - } - // Internal: Electron already owns UI shortcuts via keydown. - // Suppress the key from GFN (caller marks suppressed) without - // emitting Shortcut, or Electron would double-fire. - if crate::gstreamer_config::use_internal_renderer() { - return; - } - emit_input_event(NativeWindowInputEvent::Shortcut { action }); - } - } - } - - fn shortcut_action_releases_input_capture(action: NativeStreamerShortcutAction) -> bool { - matches!( - action, - NativeStreamerShortcutAction::ToggleFullscreen - | NativeStreamerShortcutAction::StopStream - ) - } - - fn legacy_mouse_button(message: Uint, wparam: Wparam) -> Option<(u8, bool)> { - match message { - WM_LBUTTONDOWN => Some((1, true)), - WM_LBUTTONUP => Some((1, false)), - WM_MBUTTONDOWN => Some((2, true)), - WM_MBUTTONUP => Some((2, false)), - WM_RBUTTONDOWN => Some((3, true)), - WM_RBUTTONUP => Some((3, false)), - WM_XBUTTONDOWN | WM_XBUTTONUP => { - let xbutton = ((wparam >> 16) & 0xffff) as u16; - let button = match xbutton { - XBUTTON1 => 4, - XBUTTON2 => 5, - _ => return None, - }; - Some((button, message == WM_XBUTTONDOWN)) - } - _ => None, - } - } - - fn emit_input_event(event: NativeWindowInputEvent) { - let Some(sender) = INPUT_EVENT_SENDER - .get() - .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) - else { - return; - }; - let _ = sender.send(event); - } - - fn is_clipboard_paste_shortcut(keycode: u16, modifiers: u16) -> bool { - keycode == VK_V - && ((modifiers & 0x02) != 0 || unsafe { is_ctrl_modifier_down() }) - && (modifiers & 0x04) == 0 - } - - unsafe fn is_ctrl_modifier_down() -> bool { - is_key_down(VK_CONTROL) || is_key_down(VK_LCONTROL) || is_key_down(VK_RCONTROL) - } - - fn emit_clipboard_paste_request() { - let Some(sender) = INPUT_EVENT_SENDER - .get() - .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) - else { - return; - }; - let _ = sender.send(NativeWindowInputEvent::ClipboardPaste); - } - - fn emit_input_capture_changed(captured: bool) { - // Electron maps this to notifyPointerLockChange so main-process Escape - // interception stays in sync with RawInput capture (tap→GFN, hold→exit). - let Some(sender) = INPUT_EVENT_SENDER - .get() - .and_then(|sender| sender.lock().ok().and_then(|sender| sender.clone())) - else { - return; - }; - let _ = sender.send(NativeWindowInputEvent::InputCaptureChanged { captured }); - } - - fn clamp_i32_to_i16(value: i32) -> i16 { - value.clamp(i16::MIN as i32, i16::MAX as i32) as i16 - } - - fn timestamp_us() -> u64 { - STARTED_AT - .get_or_init(Instant::now) - .elapsed() - .as_micros() - .min(u128::from(u64::MAX)) as u64 - } - - unsafe fn hide_cursor() { - while ShowCursor(0) >= 0 {} - } - - unsafe fn show_cursor() { - while ShowCursor(1) < 0 {} - } -} - -// The old BrowserWindow-HWND GstVideoOverlay path was removed. Internal mode -// uses `crate::internal_renderer::InternalRenderer` (child surface owned by the -// streamer). External mode uses the floating GStreamer window + window guard. - -#[cfg(target_os = "windows")] -pub(crate) fn primary_display_refresh_hz() -> Option { - const VREFRESH: i32 = 116; - - #[link(name = "user32")] - extern "system" { - fn GetDC(hwnd: *mut c_void) -> *mut c_void; - fn ReleaseDC(hwnd: *mut c_void, hdc: *mut c_void) -> i32; - } - - #[link(name = "gdi32")] - extern "system" { - fn GetDeviceCaps(hdc: *mut c_void, index: i32) -> i32; - } - - let hdc = unsafe { GetDC(std::ptr::null_mut()) }; - if hdc.is_null() { - return None; - } - - let refresh = unsafe { GetDeviceCaps(hdc, VREFRESH) }; - unsafe { - ReleaseDC(std::ptr::null_mut(), hdc); - } - - (refresh > 1).then_some(refresh as u32) -} - -#[cfg(not(target_os = "windows"))] -pub(crate) fn primary_display_refresh_hz() -> Option { - None -} diff --git a/native/opennow-streamer/src/gstreamer_transitions.rs b/native/opennow-streamer/src/gstreamer_transitions.rs deleted file mode 100644 index 0392bc0a7..000000000 --- a/native/opennow-streamer/src/gstreamer_transitions.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::protocol::{NativeQueueMode, StreamSettings, VideoTransitionEvent}; - -pub(crate) const DEFAULT_VIDEO_QUEUE_DEPTH: u32 = 1; - -#[derive(Debug, Clone)] -pub(crate) struct TransitionSnapshot { - pub(crate) transition_type: String, - pub(crate) source: String, - pub(crate) at_ms: u64, - pub(crate) old_caps: Option, - pub(crate) new_caps: Option, - pub(crate) old_framerate: Option, - pub(crate) new_framerate: Option, - pub(crate) old_memory_mode: Option, - pub(crate) new_memory_mode: Option, - pub(crate) render_gap_ms: Option, - pub(crate) requested_fps: Option, - pub(crate) caps_framerate: Option, - pub(crate) high_fps_risk: bool, - pub(crate) queue_mode: NativeQueueMode, - pub(crate) summary: String, -} - -impl TransitionSnapshot { - pub(crate) fn to_event(&self) -> VideoTransitionEvent { - VideoTransitionEvent { - transition_type: self.transition_type.clone(), - source: self.source.clone(), - at_ms: self.at_ms, - old_caps: self.old_caps.clone(), - new_caps: self.new_caps.clone(), - old_framerate: self.old_framerate.clone(), - new_framerate: self.new_framerate.clone(), - old_memory_mode: self.old_memory_mode.clone(), - new_memory_mode: self.new_memory_mode.clone(), - render_gap_ms: self.render_gap_ms, - requested_fps: self.requested_fps, - caps_framerate: self.caps_framerate.clone(), - high_fps_risk: self.high_fps_risk, - queue_mode: self.queue_mode.as_str().to_owned(), - summary: self.summary.clone(), - } - } -} - -#[derive(Debug, Clone)] -pub(crate) struct TransitionTelemetry { - pub(crate) queue_mode: NativeQueueMode, - pub(crate) queue_depth: u32, - pub(crate) queue_depth_changes: u32, - pub(crate) present_pacing_changes: u32, - pub(crate) partial_flush_count: u32, - pub(crate) complete_flush_count: u32, - pub(crate) last_transition: Option, -} - -impl Default for TransitionTelemetry { - fn default() -> Self { - Self { - queue_mode: NativeQueueMode::Auto, - queue_depth: DEFAULT_VIDEO_QUEUE_DEPTH, - queue_depth_changes: 0, - present_pacing_changes: 0, - partial_flush_count: 0, - complete_flush_count: 0, - last_transition: None, - } - } -} - -pub(crate) fn resolve_queue_mode(settings: &StreamSettings) -> NativeQueueMode { - if let Some(force_queue_mode) = settings - .native_transition_diagnostics - .as_ref() - .and_then(|diagnostics| diagnostics.force_queue_mode) - { - return force_queue_mode; - } - - if settings.enable_cloud_gsync { - return NativeQueueMode::Vrr; - } - if settings.fps >= 240 { - return NativeQueueMode::Adaptive; - } - NativeQueueMode::Fixed -} - -pub(crate) fn format_transition_summary( - transition_type: &str, - source: &str, - requested_fps: Option, - old_framerate: Option<&str>, - new_framerate: Option<&str>, - high_fps_risk: bool, -) -> String { - let fps_summary = match (old_framerate, new_framerate) { - (Some(old), Some(new)) if old != new => format!("framerate {old} -> {new}"), - (_, Some(new)) => format!("framerate {new}"), - _ => "framerate unchanged/unknown".to_owned(), - }; - if high_fps_risk { - return format!( - "{transition_type} on {source}: {fps_summary} while requestedFps={} (high-fps transition risk).", - requested_fps - .map(|value| value.to_string()) - .unwrap_or_else(|| "unknown".to_owned()) - ); - } - format!("{transition_type} on {source}: {fps_summary}.") -} diff --git a/native/opennow-streamer/src/input.rs b/native/opennow-streamer/src/input.rs deleted file mode 100644 index 50a43cf22..000000000 --- a/native/opennow-streamer/src/input.rs +++ /dev/null @@ -1,775 +0,0 @@ -#![allow(dead_code)] - -use std::collections::HashMap; - -pub const INPUT_HEARTBEAT: u32 = 2; -pub const INPUT_KEY_DOWN: u32 = 3; -pub const INPUT_KEY_UP: u32 = 4; -pub const INPUT_MOUSE_REL: u32 = 7; -pub const INPUT_MOUSE_BUTTON_DOWN: u32 = 8; -pub const INPUT_MOUSE_BUTTON_UP: u32 = 9; -pub const INPUT_MOUSE_WHEEL: u32 = 10; -pub const INPUT_GAMEPAD: u32 = 12; -pub const INPUT_LOCK_KEYS_SYNC: u32 = 19; - -pub const GAMEPAD_MAX_CONTROLLERS: u8 = 4; -pub const GAMEPAD_PACKET_SIZE: usize = 38; -pub const PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL: u32 = (1 << GAMEPAD_MAX_CONTROLLERS) - 1; -pub const PARTIALLY_RELIABLE_HID_DEVICE_MASK_ALL: u32 = 0xFFFF_FFFF; - -const WRAPPER_LEGACY_INPUT: u8 = 0x21; -const WRAPPER_SINGLE_INPUT: u8 = 0x22; -const WRAPPER_PARTIALLY_RELIABLE_INPUT: u8 = 0x26; -const WRAPPER_VERSION_MARKER: u8 = 0x23; -const WRAPPER_VERSION_HEADER_BYTES: usize = 9; -const WRAPPER_SINGLE_BODY_OFFSET: usize = WRAPPER_VERSION_HEADER_BYTES + 1; -const GAMEPAD_PAYLOAD_SIZE: u16 = 26; -const GAMEPAD_INNER_SIZE: u16 = 20; -const GAMEPAD_RESERVED_MARKER: u16 = 85; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct KeyboardPayload { - pub keycode: u16, - pub scancode: u16, - pub modifiers: u16, - pub timestamp_us: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MouseMovePayload { - pub dx: i16, - pub dy: i16, - pub timestamp_us: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MouseButtonPayload { - pub button: u8, - pub timestamp_us: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MouseWheelPayload { - pub delta: i16, - pub timestamp_us: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct GamepadInput { - pub controller_id: u8, - pub buttons: u16, - pub left_trigger: u8, - pub right_trigger: u8, - pub left_stick_x: i16, - pub left_stick_y: i16, - pub right_stick_x: i16, - pub right_stick_y: i16, - pub connected: bool, - pub timestamp_us: u64, -} - -#[derive(Debug, Clone)] -pub struct InputEncoder { - protocol_version: u8, - gamepad_sequences: HashMap, -} - -impl Default for InputEncoder { - fn default() -> Self { - Self { - protocol_version: 2, - gamepad_sequences: HashMap::new(), - } - } -} - -impl InputEncoder { - pub fn new(protocol_version: u8) -> Self { - Self { - protocol_version, - gamepad_sequences: HashMap::new(), - } - } - - pub fn protocol_version(&self) -> u8 { - self.protocol_version - } - - pub fn set_protocol_version(&mut self, protocol_version: u8) { - self.protocol_version = protocol_version; - } - - pub fn reset_gamepad_sequences(&mut self) { - self.gamepad_sequences.clear(); - } - - pub fn encode_heartbeat(&self) -> Vec { - let mut payload = Vec::with_capacity(4); - put_u32_le(&mut payload, INPUT_HEARTBEAT); - payload - } - - pub fn encode_key_down(&self, payload: KeyboardPayload) -> Vec { - self.encode_keyboard(INPUT_KEY_DOWN, payload) - } - - pub fn encode_key_up(&self, payload: KeyboardPayload) -> Vec { - self.encode_keyboard(INPUT_KEY_UP, payload) - } - - pub fn encode_lock_keys_sync(&self, state: u8) -> Vec { - let mut bytes = Vec::with_capacity(5); - put_u32_le(&mut bytes, INPUT_LOCK_KEYS_SYNC); - bytes.push(state); - wrap_single_input(self.protocol_version, 0, &bytes) - } - - pub fn encode_mouse_move(&self, payload: MouseMovePayload) -> Vec { - let mut bytes = Vec::with_capacity(22); - put_u32_le(&mut bytes, INPUT_MOUSE_REL); - put_i16_be(&mut bytes, payload.dx); - put_i16_be(&mut bytes, payload.dy); - put_u16_be(&mut bytes, 0); - put_u32_be(&mut bytes, 0); - put_u64_be(&mut bytes, payload.timestamp_us); - wrap_legacy_input(self.protocol_version, payload.timestamp_us, &bytes) - } - - pub fn encode_mouse_button_down(&self, payload: MouseButtonPayload) -> Vec { - self.encode_mouse_button(INPUT_MOUSE_BUTTON_DOWN, payload) - } - - pub fn encode_mouse_button_up(&self, payload: MouseButtonPayload) -> Vec { - self.encode_mouse_button(INPUT_MOUSE_BUTTON_UP, payload) - } - - pub fn encode_mouse_wheel(&self, payload: MouseWheelPayload) -> Vec { - let mut bytes = Vec::with_capacity(22); - put_u32_le(&mut bytes, INPUT_MOUSE_WHEEL); - put_i16_be(&mut bytes, 0); - put_i16_be(&mut bytes, payload.delta); - put_u16_be(&mut bytes, 0); - put_u32_be(&mut bytes, 0); - put_u64_be(&mut bytes, payload.timestamp_us); - wrap_single_input(self.protocol_version, payload.timestamp_us, &bytes) - } - - pub fn encode_gamepad_state( - &mut self, - bitmap: u16, - input: GamepadInput, - use_partially_reliable: bool, - ) -> Vec { - let payload = encode_gamepad_payload(bitmap, input); - if !use_partially_reliable { - return wrap_legacy_input(self.protocol_version, input.timestamp_us, &payload); - } - - let sequence = self.next_gamepad_sequence(input.controller_id); - wrap_partially_reliable_input( - self.protocol_version, - input.timestamp_us, - input.controller_id, - sequence, - &payload, - ) - } - - fn encode_keyboard(&self, input_type: u32, payload: KeyboardPayload) -> Vec { - let mut bytes = Vec::with_capacity(18); - put_u32_le(&mut bytes, input_type); - put_u16_be(&mut bytes, payload.keycode); - put_u16_be(&mut bytes, payload.modifiers); - put_u16_be(&mut bytes, payload.scancode); - put_u64_be(&mut bytes, payload.timestamp_us); - wrap_single_input(self.protocol_version, payload.timestamp_us, &bytes) - } - - fn encode_mouse_button(&self, input_type: u32, payload: MouseButtonPayload) -> Vec { - let mut bytes = Vec::with_capacity(18); - put_u32_le(&mut bytes, input_type); - bytes.push(payload.button); - bytes.push(0); - put_u32_be(&mut bytes, 0); - put_u64_be(&mut bytes, payload.timestamp_us); - wrap_single_input(self.protocol_version, payload.timestamp_us, &bytes) - } - - fn next_gamepad_sequence(&mut self, controller_id: u8) -> u16 { - let current = *self.gamepad_sequences.get(&controller_id).unwrap_or(&1); - self.gamepad_sequences - .insert(controller_id, current.wrapping_add(1)); - current - } -} - -/// Rewrite the protocol v3 `[0x23][timestamp]` header to the send-time session clock. -pub fn restamp_protocol_v3_outer_timestamp(packet: &mut [u8], timestamp_us: u64) -> bool { - if packet.len() < WRAPPER_VERSION_HEADER_BYTES || packet[0] != WRAPPER_VERSION_MARKER { - return false; - } - - packet[1..WRAPPER_VERSION_HEADER_BYTES].copy_from_slice(×tamp_us.to_be_bytes()); - true -} - -/// Coalesce protocol v3 single-input packets into one datachannel payload. -/// Official GFN batches multiple `[0x22][body]` frames under one `[0x23][timestamp]` header -/// stamped with the send-time session clock (`ed()`), not per-event capture timestamps. -/// Returns `None` when payloads cannot be safely coalesced (for example protocol v2). -pub fn combine_single_input_packets( - payloads: &[Vec], - send_timestamp_us: u64, -) -> Option> { - if payloads.is_empty() { - return None; - } - if payloads.len() == 1 { - let mut packet = payloads[0].clone(); - restamp_protocol_v3_outer_timestamp(&mut packet, send_timestamp_us); - return Some(packet); - } - - let mut combined_bodies = Vec::new(); - - for payload in payloads { - if payload.len() >= WRAPPER_SINGLE_BODY_OFFSET - && payload[0] == WRAPPER_VERSION_MARKER - && payload[WRAPPER_VERSION_HEADER_BYTES] == WRAPPER_SINGLE_INPUT - { - combined_bodies.push(WRAPPER_SINGLE_INPUT); - combined_bodies.extend_from_slice(&payload[WRAPPER_SINGLE_BODY_OFFSET..]); - continue; - } - - return None; - } - - let mut bytes = Vec::with_capacity(WRAPPER_VERSION_HEADER_BYTES + combined_bodies.len()); - bytes.push(WRAPPER_VERSION_MARKER); - bytes.extend_from_slice(&send_timestamp_us.to_be_bytes()); - bytes.extend_from_slice(&combined_bodies); - Some(bytes) -} - -/// Finalize reliable keyboard/button packets for send, restamping v3 headers at send time. -pub fn finalize_reliable_single_input_packets( - payloads: &[Vec], - send_timestamp_us: u64, -) -> Vec> { - if payloads.is_empty() { - return Vec::new(); - } - - if let Some(combined) = combine_single_input_packets(payloads, send_timestamp_us) { - return vec![combined]; - } - - payloads - .iter() - .map(|payload| { - let mut packet = payload.clone(); - restamp_protocol_v3_outer_timestamp(&mut packet, send_timestamp_us); - packet - }) - .collect() -} - -pub fn partially_reliable_hid_mask_for_input_type(input_type: u32) -> u32 { - if input_type > 31 { - return 0; - } - 1_u32 << input_type -} - -pub fn is_partially_reliable_hid_transfer_eligible(input_type: u32) -> bool { - input_type == INPUT_MOUSE_REL -} - -pub(crate) fn layout_mapped_keyboard_scancode(physical_scancode: u16) -> u16 { - // GFN keyboard events use the selected remote layout plus VK; sending the local physical - // scancode makes QWERTZ-only keys such as Y/Z resolve as their US physical positions. - // Escape is the exception: Chromium's synthetic/pointer-lock path sends scan code 0x01, - // and GFN ignores native Escape taps when that scan code is discarded. - if physical_scancode == 0x0001 { - physical_scancode - } else { - 0 - } -} - -pub(crate) fn layout_mapped_keyboard_keycode(fallback_keycode: u16, physical_scancode: u16) -> u16 { - // Set 1 scancode -> official GFN browser position-dependent VK map (vendor Jr). - match physical_scancode { - 0x0001 => 0x1B, - 0x0002 => 0x31, - 0x0003 => 0x32, - 0x0004 => 0x33, - 0x0005 => 0x34, - 0x0006 => 0x35, - 0x0007 => 0x36, - 0x0008 => 0x37, - 0x0009 => 0x38, - 0x000A => 0x39, - 0x000B => 0x30, - 0x000C => 0xBD, - 0x000D => 0xBB, - 0x000E => 0x08, - 0x000F => 0x09, - 0x0010 => 0x51, - 0x0011 => 0x57, - 0x0012 => 0x45, - 0x0013 => 0x52, - 0x0014 => 0x54, - 0x0015 => 0x59, - 0x0016 => 0x55, - 0x0017 => 0x49, - 0x0018 => 0x4F, - 0x0019 => 0x50, - 0x001A => 0xDB, - 0x001B => 0xDD, - 0x001C => 0x0D, - 0x001D => 0xA2, - 0x001E => 0x41, - 0x001F => 0x53, - 0x0020 => 0x44, - 0x0021 => 0x46, - 0x0022 => 0x47, - 0x0023 => 0x48, - 0x0024 => 0x4A, - 0x0025 => 0x4B, - 0x0026 => 0x4C, - 0x0027 => 0xBA, - 0x0028 => 0xDE, - 0x0029 => 0xC0, - 0x002A => 0xA0, - 0x002B => 0xDC, - 0x002C => 0x5A, - 0x002D => 0x58, - 0x002E => 0x43, - 0x002F => 0x56, - 0x0030 => 0x42, - 0x0031 => 0x4E, - 0x0032 => 0x4D, - 0x0033 => 0xBC, - 0x0034 => 0xBE, - 0x0035 => 0xBF, - 0x0036 => 0xA1, - 0x0037 => 0x6A, - 0x0038 => 0xA4, - 0x0039 => 0x20, - 0x003A => 0x14, - 0x003B => 0x70, - 0x003C => 0x71, - 0x003D => 0x72, - 0x003E => 0x73, - 0x003F => 0x74, - 0x0040 => 0x75, - 0x0041 => 0x76, - 0x0042 => 0x77, - 0x0043 => 0x78, - 0x0044 => 0x79, - 0x0045 => fallback_keycode, - 0x0046 => 0x91, - 0x0047 => 0x67, - 0x0048 => 0x68, - 0x0049 => 0x69, - 0x004A => 0x6D, - 0x004B => 0x64, - 0x004C => 0x65, - 0x004D => 0x66, - 0x004E => 0x6B, - 0x004F => 0x61, - 0x0050 => 0x62, - 0x0051 => 0x63, - 0x0052 => 0x60, - 0x0053 => 0x6E, - 0x0056 => 0xE2, - 0x0057 => 0x7A, - 0x0058 => 0x7B, - 0x0059 => 0xBB, - 0x0064 => 0x7C, - 0x0070 => 0xE9, - 0x0073 => 0xC2, - 0x0079 => 0xEA, - 0x007B => 0xEB, - 0x007D => 0xC1, - 0x007E => 0xBC, - 0xE01C => 0x0D, - 0xE01D => 0xA3, - 0xE035 => 0x6F, - 0xE037 => 0x2A, - 0xE038 => 0xA5, - 0xE045 => 0x90, - 0xE047 => 0x24, - 0xE048 => 0x26, - 0xE049 => 0x21, - 0xE04B => 0x25, - 0xE04D => 0x27, - 0xE04F => 0x23, - 0xE050 => 0x28, - 0xE051 => 0x22, - 0xE052 => 0x2D, - 0xE053 => 0x2E, - 0xE05B => 0x5B, - 0xE05C => 0x5C, - 0xE05D => 0x5D, - _ => fallback_keycode, - } -} - -fn encode_gamepad_payload(bitmap: u16, input: GamepadInput) -> Vec { - let mut payload = Vec::with_capacity(GAMEPAD_PACKET_SIZE); - put_u32_le(&mut payload, INPUT_GAMEPAD); - put_u16_le(&mut payload, GAMEPAD_PAYLOAD_SIZE); - put_u16_le(&mut payload, input.controller_id as u16); - put_u16_le(&mut payload, bitmap); - put_u16_le(&mut payload, GAMEPAD_INNER_SIZE); - put_u16_le(&mut payload, input.buttons); - put_u16_le( - &mut payload, - input.left_trigger as u16 | ((input.right_trigger as u16) << 8), - ); - put_i16_le(&mut payload, input.left_stick_x); - put_i16_le(&mut payload, input.left_stick_y); - put_i16_le(&mut payload, input.right_stick_x); - put_i16_le(&mut payload, input.right_stick_y); - put_u16_le(&mut payload, 0); - put_u16_le(&mut payload, GAMEPAD_RESERVED_MARKER); - put_u16_le(&mut payload, 0); - put_u64_le(&mut payload, input.timestamp_us); - payload -} - -fn wrap_single_input(protocol_version: u8, timestamp_us: u64, payload: &[u8]) -> Vec { - if protocol_version < 3 { - return payload.to_vec(); - } - - let mut bytes = Vec::with_capacity(10 + payload.len()); - bytes.push(WRAPPER_VERSION_MARKER); - put_u64_be(&mut bytes, timestamp_us); - bytes.push(WRAPPER_SINGLE_INPUT); - bytes.extend_from_slice(payload); - bytes -} - -fn wrap_legacy_input(protocol_version: u8, timestamp_us: u64, payload: &[u8]) -> Vec { - if protocol_version < 3 { - return payload.to_vec(); - } - - let mut bytes = Vec::with_capacity(12 + payload.len()); - bytes.push(WRAPPER_VERSION_MARKER); - put_u64_be(&mut bytes, timestamp_us); - bytes.push(WRAPPER_LEGACY_INPUT); - put_u16_be(&mut bytes, payload.len() as u16); - bytes.extend_from_slice(payload); - bytes -} - -fn wrap_partially_reliable_input( - protocol_version: u8, - timestamp_us: u64, - controller_id: u8, - sequence: u16, - payload: &[u8], -) -> Vec { - if protocol_version < 3 { - return payload.to_vec(); - } - - let mut bytes = Vec::with_capacity(15 + payload.len()); - bytes.push(WRAPPER_VERSION_MARKER); - put_u64_be(&mut bytes, timestamp_us); - bytes.push(WRAPPER_PARTIALLY_RELIABLE_INPUT); - bytes.push(controller_id); - put_u16_be(&mut bytes, sequence); - bytes.push(WRAPPER_LEGACY_INPUT); - put_u16_be(&mut bytes, payload.len() as u16); - bytes.extend_from_slice(payload); - bytes -} - -fn put_u16_be(bytes: &mut Vec, value: u16) { - bytes.extend_from_slice(&value.to_be_bytes()); -} - -fn put_u16_le(bytes: &mut Vec, value: u16) { - bytes.extend_from_slice(&value.to_le_bytes()); -} - -fn put_i16_be(bytes: &mut Vec, value: i16) { - bytes.extend_from_slice(&value.to_be_bytes()); -} - -fn put_i16_le(bytes: &mut Vec, value: i16) { - bytes.extend_from_slice(&value.to_le_bytes()); -} - -fn put_u32_be(bytes: &mut Vec, value: u32) { - bytes.extend_from_slice(&value.to_be_bytes()); -} - -fn put_u32_le(bytes: &mut Vec, value: u32) { - bytes.extend_from_slice(&value.to_le_bytes()); -} - -fn put_u64_be(bytes: &mut Vec, value: u64) { - bytes.extend_from_slice(&value.to_be_bytes()); -} - -fn put_u64_le(bytes: &mut Vec, value: u64) { - bytes.extend_from_slice(&value.to_le_bytes()); -} - -#[cfg(test)] -mod combine_tests { - use super::*; - - #[test] - fn combines_protocol_v3_single_input_packets() { - let encoder = InputEncoder::new(3); - let first = encoder.encode_key_down(KeyboardPayload { - keycode: 0x0041, - scancode: 0, - modifiers: 0, - timestamp_us: 10, - }); - let second = encoder.encode_key_down(KeyboardPayload { - keycode: 0x0042, - scancode: 0, - modifiers: 0, - timestamp_us: 20, - }); - - let combined = combine_single_input_packets(&[first.clone(), second.clone()], 20) - .expect("v3 keyboard packets should combine"); - - assert_eq!(combined[0], WRAPPER_VERSION_MARKER); - assert_eq!(&combined[1..9], &20u64.to_be_bytes()); - assert_eq!(combined[9], WRAPPER_SINGLE_INPUT); - assert_eq!(&combined[10..14], &[0x03, 0, 0, 0]); - assert_eq!(combined[28], WRAPPER_SINGLE_INPUT); - assert_eq!(&combined[29..33], &[0x03, 0, 0, 0]); - } - - #[test] - fn leaves_protocol_v2_packets_uncombined() { - let encoder = InputEncoder::new(2); - let first = encoder.encode_key_down(KeyboardPayload { - keycode: 0x0041, - scancode: 0, - modifiers: 0, - timestamp_us: 10, - }); - let second = encoder.encode_key_up(KeyboardPayload { - keycode: 0x0041, - scancode: 0, - modifiers: 0, - timestamp_us: 11, - }); - - assert!(combine_single_input_packets(&[first, second], 11).is_none()); - } - - #[test] - fn restamps_single_v3_packet_at_send_time() { - let encoder = InputEncoder::new(3); - let mut packet = encoder.encode_key_down(KeyboardPayload { - keycode: 0x0041, - scancode: 0, - modifiers: 0, - timestamp_us: 10, - }); - - assert!(restamp_protocol_v3_outer_timestamp(&mut packet, 99)); - assert_eq!(&packet[1..9], &99u64.to_be_bytes()); - } - - #[test] - fn encodes_lock_keys_sync_as_single_input() { - let encoder = InputEncoder::new(3); - let payload = encoder.encode_lock_keys_sync(0x73); - - assert_eq!(payload.len(), 15); - assert_eq!(payload[0], WRAPPER_VERSION_MARKER); - assert_eq!(payload[9], WRAPPER_SINGLE_INPUT); - assert_eq!(&payload[10..14], &[0x13, 0, 0, 0]); - assert_eq!(payload[14], 0x73); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn encodes_heartbeat_as_raw_little_endian_type() { - let encoder = InputEncoder::default(); - assert_eq!(encoder.encode_heartbeat(), vec![2, 0, 0, 0]); - } - - #[test] - fn encodes_protocol_v2_keyboard_without_wrapper() { - let encoder = InputEncoder::new(2); - let payload = encoder.encode_key_down(KeyboardPayload { - keycode: 0x0041, - scancode: 0x001e, - modifiers: 0x0002, - timestamp_us: 0x0102_0304_0506_0708, - }); - - assert_eq!(payload.len(), 18); - assert_eq!( - payload, - vec![ - 0x03, 0x00, 0x00, 0x00, 0x00, 0x41, 0x00, 0x02, 0x00, 0x1e, 0x01, 0x02, 0x03, 0x04, - 0x05, 0x06, 0x07, 0x08, - ], - ); - } - - #[test] - fn wraps_protocol_v3_keyboard_as_single_input() { - let encoder = InputEncoder::new(3); - let payload = encoder.encode_key_up(KeyboardPayload { - keycode: 0x0041, - scancode: 0x001e, - modifiers: 0, - timestamp_us: 7, - }); - - assert_eq!(payload.len(), 28); - assert_eq!(payload[0], WRAPPER_VERSION_MARKER); - assert_eq!(&payload[1..9], &[0, 0, 0, 0, 0, 0, 0, 7]); - assert_eq!(payload[9], WRAPPER_SINGLE_INPUT); - assert_eq!(&payload[10..14], &[0x04, 0x00, 0x00, 0x00]); - } - - #[test] - fn wraps_protocol_v3_mouse_move_with_payload_size() { - let encoder = InputEncoder::new(3); - let payload = encoder.encode_mouse_move(MouseMovePayload { - dx: -2, - dy: 300, - timestamp_us: 9, - }); - - assert_eq!(payload.len(), 34); - assert_eq!(payload[0], WRAPPER_VERSION_MARKER); - assert_eq!(payload[9], WRAPPER_LEGACY_INPUT); - assert_eq!(&payload[10..12], &[0, 22]); - assert_eq!(&payload[12..16], &[0x07, 0x00, 0x00, 0x00]); - assert_eq!(&payload[16..18], &(-2_i16).to_be_bytes()); - assert_eq!(&payload[18..20], &300_i16.to_be_bytes()); - } - - #[test] - fn encodes_mouse_button_and_wheel_payloads() { - let encoder = InputEncoder::new(2); - let button = encoder.encode_mouse_button_down(MouseButtonPayload { - button: 1, - timestamp_us: 5, - }); - assert_eq!(button.len(), 18); - assert_eq!(&button[0..4], &[0x08, 0, 0, 0]); - assert_eq!(button[4], 1); - assert_eq!(&button[10..18], &[0, 0, 0, 0, 0, 0, 0, 5]); - - let wheel = encoder.encode_mouse_wheel(MouseWheelPayload { - delta: -120, - timestamp_us: 6, - }); - assert_eq!(wheel.len(), 22); - assert_eq!(&wheel[0..4], &[0x0a, 0, 0, 0]); - assert_eq!(&wheel[6..8], &(-120_i16).to_be_bytes()); - assert_eq!(&wheel[14..22], &[0, 0, 0, 0, 0, 0, 0, 6]); - } - - #[test] - fn encodes_gamepad_payload_and_wrappers() { - let mut encoder = InputEncoder::new(3); - let input = GamepadInput { - controller_id: 2, - buttons: 0x1001, - left_trigger: 12, - right_trigger: 34, - left_stick_x: -100, - left_stick_y: 100, - right_stick_x: -200, - right_stick_y: 200, - connected: true, - timestamp_us: 11, - }; - - let reliable = encoder.encode_gamepad_state(0x00ff, input, false); - assert_eq!(reliable.len(), 50); - assert_eq!(reliable[9], WRAPPER_LEGACY_INPUT); - assert_eq!(&reliable[10..12], &[0, GAMEPAD_PACKET_SIZE as u8]); - let raw = &reliable[12..]; - assert_eq!(&raw[0..4], &[0x0c, 0, 0, 0]); - assert_eq!(&raw[4..6], &GAMEPAD_PAYLOAD_SIZE.to_le_bytes()); - assert_eq!(&raw[6..8], &2_u16.to_le_bytes()); - assert_eq!(&raw[8..10], &0x00ff_u16.to_le_bytes()); - assert_eq!(&raw[12..14], &0x1001_u16.to_le_bytes()); - assert_eq!(&raw[14..16], &(12_u16 | (34_u16 << 8)).to_le_bytes()); - assert_eq!(&raw[16..18], &(-100_i16).to_le_bytes()); - assert_eq!(&raw[24..26], &0_u16.to_le_bytes()); - assert_eq!(&raw[26..28], &GAMEPAD_RESERVED_MARKER.to_le_bytes()); - assert_eq!(&raw[30..38], &11_u64.to_le_bytes()); - - let partially_reliable = encoder.encode_gamepad_state(0x00ff, input, true); - assert_eq!(partially_reliable.len(), 54); - assert_eq!(partially_reliable[9], WRAPPER_PARTIALLY_RELIABLE_INPUT); - assert_eq!(partially_reliable[10], 2); - assert_eq!(&partially_reliable[11..13], &1_u16.to_be_bytes()); - assert_eq!(partially_reliable[13], WRAPPER_LEGACY_INPUT); - assert_eq!(&partially_reliable[14..16], &[0, GAMEPAD_PACKET_SIZE as u8]); - - let next = encoder.encode_gamepad_state(0x00ff, input, true); - assert_eq!(&next[11..13], &2_u16.to_be_bytes()); - - encoder.reset_gamepad_sequences(); - let reset = encoder.encode_gamepad_state(0x00ff, input, true); - assert_eq!(&reset[11..13], &1_u16.to_be_bytes()); - } - - #[test] - fn computes_partially_reliable_hid_masks() { - assert_eq!( - partially_reliable_hid_mask_for_input_type(INPUT_MOUSE_REL), - 1 << 7 - ); - assert_eq!(partially_reliable_hid_mask_for_input_type(32), 0); - assert!(is_partially_reliable_hid_transfer_eligible(INPUT_MOUSE_REL)); - assert!(!is_partially_reliable_hid_transfer_eligible(INPUT_KEY_DOWN)); - } - - #[test] - fn layout_mapped_keyboard_input_omits_physical_scancodes() { - assert_eq!(layout_mapped_keyboard_scancode(0x0015), 0); - assert_eq!(layout_mapped_keyboard_scancode(0x002c), 0); - } - - #[test] - fn layout_mapped_keyboard_input_preserves_escape_scancode() { - assert_eq!(layout_mapped_keyboard_scancode(0x0001), 0x0001); - } - - #[test] - fn layout_mapped_keyboard_input_uses_official_position_keycodes() { - assert_eq!(layout_mapped_keyboard_keycode(0x0059, 0x002c), 0x005a); - assert_eq!(layout_mapped_keyboard_keycode(0x00ba, 0x001a), 0x00db); - assert_eq!(layout_mapped_keyboard_keycode(0x00c0, 0x0027), 0x00ba); - assert_eq!(layout_mapped_keyboard_keycode(0x00dc, 0x0029), 0x00c0); - assert_eq!(layout_mapped_keyboard_keycode(0x00bd, 0x0035), 0x00bf); - assert_eq!(layout_mapped_keyboard_keycode(0x0010, 0x0036), 0x00a1); - assert_eq!(layout_mapped_keyboard_keycode(0x0090, 0x0045), 0x0090); - assert_eq!(layout_mapped_keyboard_keycode(0x0013, 0x0045), 0x0013); - assert_eq!(layout_mapped_keyboard_keycode(0x00f2, 0x0070), 0x00e9); - assert_eq!(layout_mapped_keyboard_keycode(0x001c, 0x0079), 0x00ea); - assert_eq!(layout_mapped_keyboard_keycode(0x001d, 0x007b), 0x00eb); - assert_eq!(layout_mapped_keyboard_keycode(0x1234, 0xffff), 0x1234); - } -} diff --git a/native/opennow-streamer/src/internal_renderer.rs b/native/opennow-streamer/src/internal_renderer.rs deleted file mode 100644 index 4a15e2e0c..000000000 --- a/native/opennow-streamer/src/internal_renderer.rs +++ /dev/null @@ -1,1495 +0,0 @@ -//! Dedicated child native surface for the internal (single-window) renderer. -//! -//! GStreamer paints into a streamer-owned child surface that is parented into -//! the Electron window. The Electron BrowserWindow handle is never used as a -//! GstVideoOverlay target. - -use crate::protocol::{NativeRenderRect, NativeRenderSurface}; -use gstreamer as gst; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Mutex; - -#[cfg(any(target_os = "windows", target_os = "linux"))] -fn parse_native_handle(value: &str) -> Result { - let trimmed = value.trim(); - let hex = trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")); - let parsed = if let Some(hex) = hex { - usize::from_str_radix(hex, 16) - } else { - trimmed.parse::() - } - .map_err(|error| format!("Invalid native parent window handle {value:?}: {error}"))?; - - if parsed == 0 { - return Err("Native parent window handle is zero.".to_owned()); - } - - Ok(parsed) -} - -fn normalized_rect(rect: Option<&NativeRenderRect>) -> NativeRenderRect { - let Some(rect) = rect else { - return NativeRenderRect { - x: 0, - y: 0, - width: 2, - height: 2, - }; - }; - - NativeRenderRect { - x: rect.x.max(0), - y: rect.y.max(0), - width: rect.width.max(2), - height: rect.height.max(2), - } -} - -/// Platform child surface that GStreamer presents into. -#[derive(Debug)] -pub(crate) struct InternalRenderer { - inner: Mutex, - /// Child window handle for prepare-window-handle (avoids locking during sink setup). - child_handle: AtomicUsize, - child_width: std::sync::atomic::AtomicI32, - child_height: std::sync::atomic::AtomicI32, -} - -// Child window handles are owned exclusively by this process and only mutated -// under the mutex; GStreamer callbacks require Send + Sync on render state. -unsafe impl Send for InternalRenderer {} -unsafe impl Sync for InternalRenderer {} - -#[derive(Debug)] -struct InternalRendererState { - surface: Option, - last_parent: Option, - last_bounds: Option, - last_visible: bool, - video_sink: Option, -} - -impl InternalRenderer { - pub(crate) fn new() -> Self { - Self { - inner: Mutex::new(InternalRendererState { - surface: None, - last_parent: None, - last_bounds: None, - last_visible: false, - video_sink: None, - }), - child_handle: AtomicUsize::new(0), - child_width: std::sync::atomic::AtomicI32::new(2), - child_height: std::sync::atomic::AtomicI32::new(2), - } - } - - fn publish_child_handle(&self, handle: usize, bounds: &NativeRenderRect) { - self.child_handle.store(handle, Ordering::SeqCst); - self.child_width.store(bounds.width, Ordering::SeqCst); - self.child_height.store(bounds.height, Ordering::SeqCst); - } - - fn clear_child_handle(&self) { - self.child_handle.store(0, Ordering::SeqCst); - self.child_width.store(2, Ordering::SeqCst); - self.child_height.store(2, Ordering::SeqCst); - } - - #[cfg(target_os = "windows")] - pub(crate) fn child_handle(&self) -> usize { - self.child_handle.load(Ordering::SeqCst) - } - - pub(crate) fn set_video_sink(&self, sink: gst::Element) -> Result<(), String> { - let mut state = self - .inner - .lock() - .map_err(|_| "Internal renderer lock poisoned.".to_owned())?; - state.video_sink = Some(sink); - Self::rebind_overlay_locked(self, &mut state) - } - - pub(crate) fn apply_surface(&self, surface: &NativeRenderSurface) -> Result<(), String> { - let mut state = self - .inner - .lock() - .map_err(|_| "Internal renderer lock poisoned.".to_owned())?; - - let parent = match surface.window_handle.as_deref() { - Some(handle) => Some(parse_parent_handle(handle)?), - None => None, - }; - - if !surface.visible || parent.is_none() || surface.rect.is_none() { - if let Some(child) = state.surface.as_mut() { - child.set_visible(false)?; - } - state.last_visible = false; - return Ok(()); - } - - let parent = parent.expect("checked above"); - let bounds = normalized_rect(surface.rect.as_ref()); - - let needs_recreate = match state.last_parent { - Some(existing) => existing != parent || state.surface.is_none(), - None => true, - }; - - if needs_recreate { - if let Some(mut previous) = state.surface.take() { - previous.destroy(); - } - let child = PlatformChildSurface::create(parent, &bounds)?; - self.publish_child_handle(child.handle(), &bounds); - state.surface = Some(child); - state.last_parent = Some(parent); - state.last_bounds = Some(bounds.clone()); - state.last_visible = true; - Self::rebind_overlay_locked(self, &mut state)?; - } else { - let bounds_changed = state.last_bounds.as_ref() != Some(&bounds); - let was_visible = state.last_visible; - if let Some(child) = state.surface.as_mut() { - if bounds_changed { - child.set_bounds(&bounds)?; - } - if !was_visible { - child.set_visible(true)?; - } - self.publish_child_handle(child.handle(), &bounds); - } - state.last_bounds = Some(bounds); - state.last_visible = true; - if bounds_changed || !was_visible { - Self::rebind_overlay_locked(self, &mut state)?; - } - } - - Ok(()) - } - - pub(crate) fn destroy(&self) { - if let Ok(mut state) = self.inner.lock() { - if let Some(mut surface) = state.surface.take() { - surface.destroy(); - } - state.last_parent = None; - state.last_bounds = None; - state.last_visible = false; - state.video_sink = None; - } - self.clear_child_handle(); - } - - fn rebind_overlay_locked( - renderer: &InternalRenderer, - state: &mut InternalRendererState, - ) -> Result<(), String> { - let (Some(child), Some(sink)) = (state.surface.as_ref(), state.video_sink.as_ref()) else { - return Ok(()); - }; - let bounds = state.last_bounds.clone().unwrap_or_else(|| NativeRenderRect { - x: 0, - y: 0, - width: renderer.child_width.load(Ordering::SeqCst).max(2), - height: renderer.child_height.load(Ordering::SeqCst).max(2), - }); - bind_overlay_to_child(sink, child.handle(), Some(&bounds)) - } -} - -impl Drop for InternalRenderer { - fn drop(&mut self) { - self.destroy(); - } -} - -fn parse_parent_handle(value: &str) -> Result { - #[cfg(any(target_os = "windows", target_os = "linux"))] - { - parse_native_handle(value) - } - #[cfg(target_os = "macos")] - { - parse_native_handle_macos(value) - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - let _ = value; - Err("Internal renderer is not supported on this platform.".to_owned()) - } -} - -#[cfg(target_os = "macos")] -fn parse_native_handle_macos(value: &str) -> Result { - let trimmed = value.trim(); - let hex = trimmed - .strip_prefix("0x") - .or_else(|| trimmed.strip_prefix("0X")); - let parsed = if let Some(hex) = hex { - usize::from_str_radix(hex, 16) - } else { - trimmed.parse::() - } - .map_err(|error| format!("Invalid native parent view handle {value:?}: {error}"))?; - - if parsed == 0 { - return Err("Native parent view handle is zero.".to_owned()); - } - - Ok(parsed) -} - -fn bind_overlay_to_child( - sink: &gst::Element, - child_handle: usize, - bounds: Option<&NativeRenderRect>, -) -> Result<(), String> { - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - { - use gst_video::prelude::*; - use gstreamer_video as gst_video; - - let overlay = sink - .clone() - .dynamic_cast::() - .map_err(|_| { - format!( - "Native render sink {} does not implement GstVideoOverlay.", - sink.name() - ) - })?; - - // SAFETY: child_handle is a platform window/view created by this process - // (or an X11 window id we own) and remains valid while the surface lives. - // Win32 vulkansink: our patched gstvkwindow presents on the GSTVULKAN child - // hwnd while parenting that child under this overlay handle (no floating window). - unsafe { - overlay.set_window_handle(child_handle); - } - overlay.handle_events(false); - // Child surface is already sized to the StreamView rect; present into the - // full client area so D3D sinks do not wait on an empty render rectangle. - let rect = normalized_rect(bounds); - let _ = overlay.set_render_rectangle(0, 0, rect.width, rect.height); - overlay.expose(); - Ok(()) - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - let _ = (sink, child_handle, bounds); - Err("Internal renderer overlay binding is not supported on this platform.".to_owned()) - } -} - -#[derive(Debug)] -struct PlatformChildSurface { - #[cfg(target_os = "windows")] - win: windows_child::ChildWindow, - #[cfg(target_os = "macos")] - view: usize, - #[cfg(target_os = "linux")] - window: linux_child::XWindow, - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - _unused: (), -} - -impl PlatformChildSurface { - fn create(parent: usize, bounds: &NativeRenderRect) -> Result { - #[cfg(target_os = "windows")] - { - Ok(Self { - win: windows_child::ChildWindow::create(parent, bounds)?, - }) - } - #[cfg(target_os = "macos")] - { - let view = macos_child::create_child(parent, bounds)?; - Ok(Self { - view: view as usize, - }) - } - #[cfg(target_os = "linux")] - { - let window = linux_child::create_child(parent, bounds)?; - Ok(Self { window }) - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - let _ = (parent, bounds); - Err("Internal renderer child surfaces are not supported on this platform.".to_owned()) - } - } - - fn handle(&self) -> usize { - #[cfg(target_os = "windows")] - { - self.win.hwnd() - } - #[cfg(target_os = "macos")] - { - self.view - } - #[cfg(target_os = "linux")] - { - self.window.window as usize - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - 0 - } - } - - fn set_bounds(&mut self, bounds: &NativeRenderRect) -> Result<(), String> { - #[cfg(target_os = "windows")] - { - self.win.set_bounds(bounds) - } - #[cfg(target_os = "macos")] - { - macos_child::set_bounds(self.view as macos_child::NsViewPtr, bounds) - } - #[cfg(target_os = "linux")] - { - linux_child::set_bounds(&self.window, bounds) - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - let _ = bounds; - Ok(()) - } - } - - fn set_visible(&mut self, visible: bool) -> Result<(), String> { - #[cfg(target_os = "windows")] - { - self.win.set_visible(visible) - } - #[cfg(target_os = "macos")] - { - macos_child::set_visible(self.view as macos_child::NsViewPtr, visible) - } - #[cfg(target_os = "linux")] - { - linux_child::set_visible(&self.window, visible) - } - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - { - let _ = visible; - Ok(()) - } - } - - fn destroy(&mut self) { - #[cfg(target_os = "windows")] - { - self.win.destroy(); - } - #[cfg(target_os = "macos")] - { - macos_child::destroy(self.view as macos_child::NsViewPtr); - self.view = 0; - } - #[cfg(target_os = "linux")] - { - linux_child::destroy(&mut self.window); - } - } -} - -#[cfg(target_os = "windows")] -mod windows_child { - use crate::protocol::NativeRenderRect; - use std::ffi::c_void; - use std::ptr::{null, null_mut}; - use std::sync::atomic::{AtomicBool, Ordering}; - - pub(super) type Hwnd = *mut c_void; - - type Atom = u16; - type Bool = i32; - type Hinstance = *mut c_void; - type Hmenu = *mut c_void; - type Lparam = isize; - type Lresult = isize; - type Wparam = usize; - - const CS_HREDRAW: u32 = 0x0002; - const CS_OWNDC: u32 = 0x0020; - const CS_VREDRAW: u32 = 0x0001; - // Sibling of Chromium Intermediate D3D: sit on top for present, force - // Intermediate D3D WS_CLIPSIBLINGS so it punches a paint hole. Input is - // owned by RawInput on this HWND (not Electron click-through). - const HWND_TOP: Hwnd = std::ptr::null_mut(); - const GWL_STYLE: i32 = -16; - const GWL_EXSTYLE: i32 = -20; - const SWP_NOSIZE: u32 = 0x0001; - const SWP_NOMOVE: u32 = 0x0002; - const SWP_NOACTIVATE: u32 = 0x0010; - const SWP_FRAMECHANGED: u32 = 0x0020; - const SWP_SHOWWINDOW: u32 = 0x0040; - const SWP_HIDEWINDOW: u32 = 0x0080; - const SWP_NOCOPYBITS: u32 = 0x0100; - const SWP_NOZORDER: u32 = 0x0004; - const SWP_NOSENDCHANGING: u32 = 0x0400; - const SW_HIDE: i32 = 0; - const SW_SHOWNOACTIVATE: i32 = 4; - const WM_DESTROY: u32 = 0x0002; - const WM_ERASEBKGND: u32 = 0x0014; - const WM_MOUSEACTIVATE: u32 = 0x0021; - const WM_PAINT: u32 = 0x000F; - const WS_CHILD: u32 = 0x4000_0000; - const WS_CLIPCHILDREN: u32 = 0x0200_0000; - const WS_CLIPSIBLINGS: u32 = 0x0400_0000; - const WS_VISIBLE: u32 = 0x1000_0000; - const WS_POPUP: u32 = 0x8000_0000; - const WS_CAPTION: u32 = 0x00C0_0000; - const WS_THICKFRAME: u32 = 0x0004_0000; - const WS_MINIMIZEBOX: u32 = 0x0002_0000; - const WS_MAXIMIZEBOX: u32 = 0x0001_0000; - const WS_SYSMENU: u32 = 0x0008_0000; - const WS_BORDER: u32 = 0x0080_0000; - const WS_EX_APPWINDOW: u32 = 0x0004_0000; - const WS_EX_TOOLWINDOW: u32 = 0x0000_0080; - const WS_EX_NOACTIVATE: u32 = 0x0800_0000; - const MA_ACTIVATE: isize = 1; - const BLACK_BRUSH: i32 = 4; - - #[repr(C)] - struct WndClassExW { - cb_size: u32, - style: u32, - lpfn_wnd_proc: Option Lresult>, - cb_cls_extra: i32, - cb_wnd_extra: i32, - h_instance: Hinstance, - h_icon: *mut c_void, - h_cursor: *mut c_void, - h_br_background: *mut c_void, - lpsz_menu_name: *const u16, - lpsz_class_name: *const u16, - h_icon_sm: *mut c_void, - } - - #[link(name = "user32")] - extern "system" { - fn CreateWindowExW( - dw_ex_style: u32, - lp_class_name: *const u16, - lp_window_name: *const u16, - dw_style: u32, - x: i32, - y: i32, - n_width: i32, - n_height: i32, - h_wnd_parent: Hwnd, - h_menu: Hmenu, - h_instance: Hinstance, - lp_param: *mut c_void, - ) -> Hwnd; - fn DefWindowProcW(h_wnd: Hwnd, msg: u32, w_param: Wparam, l_param: Lparam) -> Lresult; - fn DestroyWindow(h_wnd: Hwnd) -> Bool; - fn EnumChildWindows( - h_wnd_parent: Hwnd, - lp_enum_func: Option Bool>, - l_param: Lparam, - ) -> Bool; - fn EnumWindows( - lp_enum_func: Option Bool>, - l_param: Lparam, - ) -> Bool; - fn GetClassNameW(h_wnd: Hwnd, lp_class_name: *mut u16, n_max_count: i32) -> i32; - fn GetModuleHandleW(lp_module_name: *const u16) -> Hinstance; - fn GetParent(h_wnd: Hwnd) -> Hwnd; - fn GetWindowLongPtrW(h_wnd: Hwnd, n_index: i32) -> isize; - fn GetWindowThreadProcessId(h_wnd: Hwnd, process_id: *mut u32) -> u32; - fn RegisterClassExW(class: *const WndClassExW) -> Atom; - fn SetParent(h_wnd_child: Hwnd, h_wnd_new_parent: Hwnd) -> Hwnd; - fn SetWindowLongPtrW(h_wnd: Hwnd, n_index: i32, dw_new_long: isize) -> isize; - fn SetWindowPos( - h_wnd: Hwnd, - h_wnd_insert_after: Hwnd, - x: i32, - y: i32, - cx: i32, - cy: i32, - flags: u32, - ) -> Bool; - fn ShowWindow(h_wnd: Hwnd, n_cmd_show: i32) -> Bool; - fn ValidateRect(h_wnd: Hwnd, lp_rect: *const c_void) -> Bool; - fn GetMessageW(lp_msg: *mut Msg, h_wnd: Hwnd, w_msg_filter_min: u32, w_msg_filter_max: u32) -> Bool; - fn TranslateMessage(lp_msg: *const Msg) -> Bool; - fn DispatchMessageW(lp_msg: *const Msg) -> Lresult; - fn PostMessageW(h_wnd: Hwnd, msg: u32, w_param: Wparam, l_param: Lparam) -> Bool; - fn PostThreadMessageW(id_thread: u32, msg: u32, w_param: Wparam, l_param: Lparam) -> Bool; - fn GetCurrentThreadId() -> u32; - } - - #[link(name = "kernel32")] - extern "system" { - fn GetCurrentProcessId() -> u32; - } - - #[repr(C)] - struct Msg { - hwnd: Hwnd, - message: u32, - w_param: Wparam, - l_param: Lparam, - time: u32, - pt_x: i32, - pt_y: i32, - } - - const WM_QUIT: u32 = 0x0012; - const WM_USER_SET_BOUNDS: u32 = 0x0400; - const WM_USER_SET_VISIBLE: u32 = 0x0401; - - #[link(name = "gdi32")] - extern "system" { - fn GetStockObject(index: i32) -> *mut c_void; - } - - static CLASS_REGISTERED: AtomicBool = AtomicBool::new(false); - const CLASS_NAME: &[u16] = &[ - b'O' as u16, b'p' as u16, b'e' as u16, b'n' as u16, b'N' as u16, b'O' as u16, b'W' as u16, - b'I' as u16, b'n' as u16, b't' as u16, b'e' as u16, b'r' as u16, b'n' as u16, b'a' as u16, - b'l' as u16, b'V' as u16, b'i' as u16, b'd' as u16, b'e' as u16, b'o' as u16, 0, - ]; - - unsafe extern "system" fn wnd_proc( - hwnd: Hwnd, - msg: u32, - w_param: Wparam, - l_param: Lparam, - ) -> Lresult { - match msg { - WM_USER_SET_BOUNDS => { - let x = (l_param & 0xFFFF) as i16 as i32; - let y = ((l_param >> 16) & 0xFFFF) as i16 as i32; - let w = (w_param & 0xFFFF) as i32; - let h = ((w_param >> 16) & 0xFFFF) as i32; - let parent = GetParent(hwnd); - if !parent.is_null() { - enable_clip_styles(parent, find_chromium_content_hwnd(parent)); - } - SetWindowPos( - hwnd, - HWND_TOP, - x, - y, - w.max(2), - h.max(2), - SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOCOPYBITS | SWP_NOSENDCHANGING, - ); - 0 - } - WM_USER_SET_VISIBLE => { - let visible = w_param != 0; - ShowWindow(hwnd, if visible { SW_SHOWNOACTIVATE } else { SW_HIDE }); - SetWindowPos( - hwnd, - HWND_TOP, - 0, - 0, - 0, - 0, - SWP_NOACTIVATE - | SWP_NOZORDER - | SWP_NOSIZE - | SWP_NOMOVE - | if visible { - SWP_SHOWWINDOW - } else { - SWP_HIDEWINDOW - }, - ); - 0 - } - // Activation / RawInput capture is handled by the chained platform - // wndproc installed after create (see arm_internal_child_input). - WM_MOUSEACTIVATE => MA_ACTIVATE, - WM_ERASEBKGND => 1, - WM_PAINT => { - ValidateRect(hwnd, null()); - 0 - } - WM_DESTROY => 0, - _ => DefWindowProcW(hwnd, msg, w_param, l_param), - } - } - - unsafe extern "system" fn collect_chromium_child(hwnd: Hwnd, l_param: Lparam) -> Bool { - let out = &mut *(l_param as *mut Hwnd); - if !out.is_null() { - return 1; - } - let mut class_name = [0u16; 64]; - let len = GetClassNameW(hwnd, class_name.as_mut_ptr(), class_name.len() as i32); - if len <= 0 { - return 1; - } - // Chromium's Intermediate D3D / content HWND class names. - let name: String = String::from_utf16_lossy(&class_name[..len as usize]); - if name.contains("Intermediate D3D") - || name.contains("Chrome_RenderWidgetHostHWND") - || name.contains("Chrome_WidgetWin_") - { - *out = hwnd; - return 0; - } - 1 - } - - unsafe fn find_chromium_content_hwnd(parent: Hwnd) -> Option { - let mut found: Hwnd = null_mut(); - EnumChildWindows( - parent, - Some(collect_chromium_child), - &mut found as *mut Hwnd as Lparam, - ); - if found.is_null() { - None - } else { - Some(found) - } - } - - unsafe extern "system" fn collect_gst_vulkan_child(hwnd: Hwnd, l_param: Lparam) -> Bool { - let out = &mut *(l_param as *mut Hwnd); - if !out.is_null() { - return 1; - } - if class_name_is_gst_vulkan(hwnd) { - *out = hwnd; - return 0; - } - 1 - } - - unsafe extern "system" fn collect_gst_vulkan_top_level(hwnd: Hwnd, l_param: Lparam) -> Bool { - let state = &mut *(l_param as *mut (u32, Hwnd)); - if !state.1.is_null() { - return 1; - } - let mut process_id = 0u32; - GetWindowThreadProcessId(hwnd, &mut process_id); - if process_id != state.0 { - return 1; - } - if class_name_is_gst_vulkan(hwnd) { - state.1 = hwnd; - return 0; - } - 1 - } - - unsafe fn class_name_is_gst_vulkan(hwnd: Hwnd) -> bool { - let mut class_name = [0u16; 64]; - let len = GetClassNameW(hwnd, class_name.as_mut_ptr(), class_name.len() as i32); - if len <= 0 { - return false; - } - String::from_utf16_lossy(&class_name[..len as usize]) == "GSTVULKAN" - } - - unsafe fn find_gst_vulkan_under_parent(parent: Hwnd) -> Option { - let mut found: Hwnd = null_mut(); - EnumChildWindows( - parent, - Some(collect_gst_vulkan_child), - &mut found as *mut Hwnd as Lparam, - ); - if found.is_null() { - None - } else { - Some(found) - } - } - - unsafe fn find_process_gst_vulkan_window() -> Option { - let mut state = (GetCurrentProcessId(), null_mut::()); - EnumWindows( - Some(collect_gst_vulkan_top_level), - &mut state as *mut (u32, Hwnd) as Lparam, - ); - if state.1.is_null() { - None - } else { - Some(state.1) - } - } - - /// Hide any top-level GSTVULKAN windows so they never float over Electron. - pub(super) fn suppress_top_level_gst_vulkan_windows() { - unsafe { - let mut state = (GetCurrentProcessId(), 0u32); - EnumWindows( - Some(suppress_top_level_gst_vulkan), - &mut state as *mut (u32, u32) as Lparam, - ); - } - } - - unsafe extern "system" fn suppress_top_level_gst_vulkan(hwnd: Hwnd, l_param: Lparam) -> Bool { - let state = &mut *(l_param as *mut (u32, u32)); - let mut process_id = 0u32; - GetWindowThreadProcessId(hwnd, &mut process_id); - if process_id != state.0 || !class_name_is_gst_vulkan(hwnd) { - return 1; - } - // Already embedded under some parent — leave alone. - if !GetParent(hwnd).is_null() { - return 1; - } - ShowWindow(hwnd, SW_HIDE); - let ex = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); - let desired_ex = (ex | WS_EX_TOOLWINDOW as isize | WS_EX_NOACTIVATE as isize) - & !(WS_EX_APPWINDOW as isize); - if desired_ex != ex { - SetWindowLongPtrW(hwnd, GWL_EXSTYLE, desired_ex); - } - SetWindowPos( - hwnd, - HWND_TOP, - -32_000, - -32_000, - 2, - 2, - SWP_NOACTIVATE | SWP_HIDEWINDOW | SWP_NOSENDCHANGING, - ); - state.1 = state.1.saturating_add(1); - 1 - } - - /// Reparent vulkansink's GSTVULKAN hwnd into our Internal child and size it. - /// Keeps the window hidden while top-level so nothing floats over Electron. - pub(super) fn embed_gst_vulkan_window(parent: Hwnd, width: i32, height: i32) -> bool { - if parent.is_null() { - return false; - } - let width = width.max(2); - let height = height.max(2); - unsafe { - suppress_top_level_gst_vulkan_windows(); - - let Some(vulkan) = find_gst_vulkan_under_parent(parent) - .or_else(|| find_process_gst_vulkan_window()) - else { - return false; - }; - - // Never show as a top-level window — hide first, then reparent, then show. - if GetParent(vulkan) != parent { - ShowWindow(vulkan, SW_HIDE); - SetParent(vulkan, parent); - } - - let style = GetWindowLongPtrW(vulkan, GWL_STYLE); - let cleared = (WS_POPUP - | WS_CAPTION - | WS_THICKFRAME - | WS_MINIMIZEBOX - | WS_MAXIMIZEBOX - | WS_SYSMENU - | WS_BORDER) as isize; - let desired = (style & !cleared) - | (WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) as isize; - if desired != style { - SetWindowLongPtrW(vulkan, GWL_STYLE, desired); - } - - let ex = GetWindowLongPtrW(vulkan, GWL_EXSTYLE); - let desired_ex = ex & !(WS_EX_APPWINDOW as isize | WS_EX_TOOLWINDOW as isize); - if desired_ex != ex { - SetWindowLongPtrW(vulkan, GWL_EXSTYLE, desired_ex); - } - - SetWindowPos( - vulkan, - HWND_TOP, - 0, - 0, - width, - height, - SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOCOPYBITS, - ); - ShowWindow(vulkan, SW_SHOWNOACTIVATE); - true - } - } - - unsafe fn enable_clip_styles(parent: Hwnd, chromium: Option) { - let parent_style = GetWindowLongPtrW(parent, GWL_STYLE); - let desired_parent = parent_style | (WS_CLIPCHILDREN as isize); - if desired_parent != parent_style { - SetWindowLongPtrW(parent, GWL_STYLE, desired_parent); - } - if let Some(chrome) = chromium { - let chrome_style = GetWindowLongPtrW(chrome, GWL_STYLE); - let desired_chrome = chrome_style | (WS_CLIPSIBLINGS as isize); - if desired_chrome != chrome_style { - SetWindowLongPtrW(chrome, GWL_STYLE, desired_chrome); - } - } - } - - fn ensure_class() -> Result<(), String> { - if CLASS_REGISTERED.load(Ordering::SeqCst) { - return Ok(()); - } - - unsafe { - let instance = GetModuleHandleW(null()); - if instance.is_null() { - return Err("GetModuleHandleW failed for internal renderer class.".to_owned()); - } - - let class = WndClassExW { - cb_size: std::mem::size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW | CS_OWNDC, - lpfn_wnd_proc: Some(wnd_proc), - cb_cls_extra: 0, - cb_wnd_extra: 0, - h_instance: instance, - h_icon: null_mut(), - h_cursor: null_mut(), - h_br_background: GetStockObject(BLACK_BRUSH), - lpsz_menu_name: null(), - lpsz_class_name: CLASS_NAME.as_ptr(), - h_icon_sm: null_mut(), - }; - - let atom = RegisterClassExW(&class); - if atom == 0 { - // Another thread may have won the race; treat as success if already registered. - if !CLASS_REGISTERED.load(Ordering::SeqCst) { - // ERROR_CLASS_ALREADY_EXISTS == 1410 - // Still mark registered so we do not loop. - } - } - CLASS_REGISTERED.store(true, Ordering::SeqCst); - } - - Ok(()) - } - - pub(super) struct ChildWindow { - hwnd: usize, - thread_id: u32, - join: Option>, - } - - impl std::fmt::Debug for ChildWindow { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChildWindow") - .field("hwnd", &self.hwnd) - .field("thread_id", &self.thread_id) - .finish() - } - } - - impl ChildWindow { - pub(super) fn create(parent: usize, bounds: &NativeRenderRect) -> Result { - ensure_class()?; - let parent_hwnd = parent as Hwnd; - if parent_hwnd.is_null() { - return Err("Internal renderer parent HWND is null.".to_owned()); - } - - let (tx, rx) = std::sync::mpsc::channel::>(); - let bounds = bounds.clone(); - let parent_handle = parent; - let join = std::thread::Builder::new() - .name("opennow-internal-video".to_owned()) - .spawn(move || { - let created = - unsafe { create_child_on_thread(parent_handle as Hwnd, &bounds) }; - match created { - Ok(hwnd) => { - let thread_id = unsafe { GetCurrentThreadId() }; - let _ = tx.send(Ok((hwnd as usize, thread_id))); - run_message_loop(hwnd); - } - Err(error) => { - let _ = tx.send(Err(error)); - } - } - }) - .map_err(|error| format!("Failed to spawn internal renderer UI thread: {error}"))?; - - let (hwnd, thread_id) = rx - .recv() - .map_err(|_| "Internal renderer UI thread exited before creating HWND.".to_owned())? - ?; - - Ok(Self { - hwnd, - thread_id, - join: Some(join), - }) - } - - pub(super) fn hwnd(&self) -> usize { - self.hwnd - } - - pub(super) fn set_bounds(&self, bounds: &NativeRenderRect) -> Result<(), String> { - if self.hwnd == 0 { - return Ok(()); - } - // Pack x/y into lParam (signed 16-bit each) and w/h into wParam. - let x = bounds.x.clamp(i16::MIN as i32, i16::MAX as i32) as u16 as u32; - let y = bounds.y.clamp(i16::MIN as i32, i16::MAX as i32) as u16 as u32; - let l_param = ((y as Lparam) << 16) | (x as Lparam); - let w = bounds.width.max(2).min(u16::MAX as i32) as u16 as usize; - let h = bounds.height.max(2).min(u16::MAX as i32) as u16 as usize; - let w_param = (h << 16) | w; - unsafe { - if PostMessageW( - self.hwnd as Hwnd, - WM_USER_SET_BOUNDS, - w_param, - l_param, - ) == 0 - { - return Err("PostMessageW(SET_BOUNDS) failed for internal renderer.".to_owned()); - } - } - Ok(()) - } - - pub(super) fn set_visible(&self, visible: bool) -> Result<(), String> { - if self.hwnd == 0 { - return Ok(()); - } - unsafe { - if PostMessageW( - self.hwnd as Hwnd, - WM_USER_SET_VISIBLE, - if visible { 1 } else { 0 }, - 0, - ) == 0 - { - return Err("PostMessageW(SET_VISIBLE) failed for internal renderer.".to_owned()); - } - } - Ok(()) - } - - pub(super) fn destroy(&mut self) { - if self.hwnd != 0 { - unsafe { - // DestroyWindow posts WM_DESTROY; then quit the UI thread loop. - DestroyWindow(self.hwnd as Hwnd); - PostThreadMessageW(self.thread_id, WM_QUIT, 0, 0); - } - self.hwnd = 0; - } - if let Some(join) = self.join.take() { - let _ = join.join(); - } - } - } - - impl Drop for ChildWindow { - fn drop(&mut self) { - self.destroy(); - } - } - - unsafe fn create_child_on_thread(parent_hwnd: Hwnd, bounds: &NativeRenderRect) -> Result { - let instance = GetModuleHandleW(null()); - let chromium = find_chromium_content_hwnd(parent_hwnd); - enable_clip_styles(parent_hwnd, chromium); - - // Sibling above Intermediate D3D for present. Clip styles punch a hole - // so Chromium does not paint over us. Input uses RawInput on this HWND - // (Electron click-through is unreliable across process boundaries). - // Do not use WS_EX_NOACTIVATE: the first click must activate so RawInput - // capture can arm (same as the floating external renderer window). - let hwnd = CreateWindowExW( - 0, - CLASS_NAME.as_ptr(), - null(), - WS_CHILD | WS_CLIPSIBLINGS | WS_VISIBLE, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - parent_hwnd, - null_mut(), - instance, - null_mut(), - ); - - if hwnd.is_null() { - return Err("CreateWindowExW failed for internal renderer child HWND.".to_owned()); - } - - SetWindowPos( - hwnd, - HWND_TOP, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOCOPYBITS | SWP_NOSENDCHANGING, - ); - - Ok(hwnd) - } - - fn run_message_loop(_hwnd: Hwnd) { - unsafe { - let mut msg = Msg { - hwnd: null_mut(), - message: 0, - w_param: 0, - l_param: 0, - time: 0, - pt_x: 0, - pt_y: 0, - }; - // D3D11 videosink present requires a live message pump on the - // thread that owns the child HWND. Without this, sink stays at 0 fps. - while GetMessageW(&mut msg, null_mut(), 0, 0) > 0 { - TranslateMessage(&msg); - DispatchMessageW(&msg); - } - } - } - - // ChildWindow owns create/bounds/visible/destroy + UI thread lifecycle. -} - -#[cfg(target_os = "macos")] -mod macos_child { - use crate::protocol::NativeRenderRect; - use std::ffi::c_void; - use std::sync::OnceLock; - - pub(super) type NsViewPtr = *mut c_void; - - #[link(name = "objc")] - extern "C" { - fn sel_registerName(name: *const i8) -> *const c_void; - fn objc_getClass(name: *const i8) -> *mut c_void; - fn objc_msgSend(); - } - - // objc_msgSend is variadic; call via transmute per selector signature. - type MsgSend0 = unsafe extern "C" fn(*mut c_void, *const c_void) -> *mut c_void; - type MsgSend1Ptr = unsafe extern "C" fn(*mut c_void, *const c_void, *mut c_void) -> *mut c_void; - type MsgSendRect = unsafe extern "C" fn(*mut c_void, *const c_void, NsRect) -> *mut c_void; - type MsgSendBool = unsafe extern "C" fn(*mut c_void, *const c_void, bool); - type MsgSendVoidPtr = unsafe extern "C" fn(*mut c_void, *const c_void, *mut c_void); - - #[repr(C)] - #[derive(Clone, Copy)] - struct NsPoint { - x: f64, - y: f64, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct NsSize { - width: f64, - height: f64, - } - - #[repr(C)] - #[derive(Clone, Copy)] - struct NsRect { - origin: NsPoint, - size: NsSize, - } - - struct Selectors { - alloc: *const c_void, - init_with_frame: *const c_void, - add_subview: *const c_void, - set_frame: *const c_void, - set_hidden: *const c_void, - set_wants_layer: *const c_void, - set_autoresizes_subviews: *const c_void, - release: *const c_void, - bounds: *const c_void, - } - - // Objective-C selectors are process-global, immutable runtime tokens returned by - // sel_registerName. Sharing these non-null tokens does not share pointed-to data. - unsafe impl Send for Selectors {} - unsafe impl Sync for Selectors {} - - fn selectors() -> &'static Selectors { - static SELECTORS: OnceLock = OnceLock::new(); - SELECTORS.get_or_init(|| unsafe { - Selectors { - alloc: sel_registerName(b"alloc\0".as_ptr().cast()), - init_with_frame: sel_registerName(b"initWithFrame:\0".as_ptr().cast()), - add_subview: sel_registerName(b"addSubview:\0".as_ptr().cast()), - set_frame: sel_registerName(b"setFrame:\0".as_ptr().cast()), - set_hidden: sel_registerName(b"setHidden:\0".as_ptr().cast()), - set_wants_layer: sel_registerName(b"setWantsLayer:\0".as_ptr().cast()), - set_autoresizes_subviews: sel_registerName( - b"setAutoresizesSubviews:\0".as_ptr().cast(), - ), - release: sel_registerName(b"release\0".as_ptr().cast()), - bounds: sel_registerName(b"bounds\0".as_ptr().cast()), - } - }) - } - - fn msg_send_0(obj: *mut c_void, sel: *const c_void) -> *mut c_void { - unsafe { - let f: MsgSend0 = std::mem::transmute(objc_msgSend as *const ()); - f(obj, sel) - } - } - - fn parent_bounds(parent: NsViewPtr) -> NsRect { - // bounds returns NSRect by value; on x86_64/arm64 macOS this is returned in registers / - // as a struct. Use a dedicated trampoline via objc_msgSend_stret is obsolete on arm64. - type MsgSendBounds = unsafe extern "C" fn(*mut c_void, *const c_void) -> NsRect; - unsafe { - let f: MsgSendBounds = std::mem::transmute(objc_msgSend as *const ()); - f(parent, selectors().bounds) - } - } - - fn to_ns_rect(bounds: &NativeRenderRect, parent: NsViewPtr) -> NsRect { - // Electron reports top-left client coords in device pixels. NSView is bottom-left - // in points; approximate by treating incoming coords as points relative to parent - // bounds (Electron already DPI-scales the rect we receive). - let parent_bounds = parent_bounds(parent); - let height = bounds.height as f64; - let y = parent_bounds.size.height - (bounds.y as f64) - height; - NsRect { - origin: NsPoint { - x: bounds.x as f64, - y: y.max(0.0), - }, - size: NsSize { - width: bounds.width as f64, - height, - }, - } - } - - pub(super) fn create_child(parent: usize, bounds: &NativeRenderRect) -> Result { - let parent_view = parent as NsViewPtr; - if parent_view.is_null() { - return Err("Internal renderer parent NSView is null.".to_owned()); - } - - unsafe { - let class = objc_getClass(b"NSView\0".as_ptr().cast()); - if class.is_null() { - return Err("objc_getClass(NSView) failed.".to_owned()); - } - - let sels = selectors(); - let alloc = msg_send_0(class, sels.alloc); - if alloc.is_null() { - return Err("NSView alloc failed.".to_owned()); - } - - let frame = to_ns_rect(bounds, parent_view); - let init: MsgSendRect = std::mem::transmute(objc_msgSend as *const ()); - let view = init(alloc, sels.init_with_frame, frame); - if view.is_null() { - return Err("NSView initWithFrame failed.".to_owned()); - } - - let set_bool: MsgSendBool = std::mem::transmute(objc_msgSend as *const ()); - set_bool(view, sels.set_wants_layer, true); - set_bool(view, sels.set_autoresizes_subviews, false); - set_bool(view, sels.set_hidden, false); - - let add: MsgSend1Ptr = std::mem::transmute(objc_msgSend as *const ()); - add(parent_view, sels.add_subview, view); - - Ok(view) - } - } - - pub(super) fn set_bounds(view: NsViewPtr, bounds: &NativeRenderRect) -> Result<(), String> { - if view.is_null() { - return Ok(()); - } - // Parent is needed for Y-flip; store is not available here — use frame in parent - // coordinates assuming the same parent. Read superview. - unsafe { - let sels = selectors(); - let superview_sel = sel_registerName(b"superview\0".as_ptr().cast()); - let parent = msg_send_0(view, superview_sel); - let frame = if parent.is_null() { - NsRect { - origin: NsPoint { - x: bounds.x as f64, - y: bounds.y as f64, - }, - size: NsSize { - width: bounds.width as f64, - height: bounds.height as f64, - }, - } - } else { - to_ns_rect(bounds, parent) - }; - let set_frame: MsgSendRect = std::mem::transmute(objc_msgSend as *const ()); - set_frame(view, sels.set_frame, frame); - } - Ok(()) - } - - pub(super) fn set_visible(view: NsViewPtr, visible: bool) -> Result<(), String> { - if view.is_null() { - return Ok(()); - } - unsafe { - let set_bool: MsgSendBool = std::mem::transmute(objc_msgSend as *const ()); - set_bool(view, selectors().set_hidden, !visible); - } - Ok(()) - } - - pub(super) fn destroy(view: NsViewPtr) { - if view.is_null() { - return; - } - unsafe { - let sels = selectors(); - let remove_sel = sel_registerName(b"removeFromSuperview\0".as_ptr().cast()); - let _ = msg_send_0(view, remove_sel); - let _ = msg_send_0(view, sels.release); - } - } -} - -#[cfg(target_os = "linux")] -mod linux_child { - use crate::protocol::NativeRenderRect; - use std::ffi::c_void; - use std::ptr::null_mut; - - #[derive(Debug)] - pub(super) struct XWindow { - pub(super) display: usize, - pub(super) window: u64, - parent: u64, - } - - // Minimal X11 FFI — enough to create a child window for GstVideoOverlay. - #[repr(C)] - struct XSetWindowAttributes { - background_pixmap: u64, - background_pixel: u64, - border_pixmap: u64, - border_pixel: u64, - bit_gravity: i32, - win_gravity: i32, - backing_store: i32, - backing_planes: u64, - backing_pixel: u64, - save_under: i32, - event_mask: i64, - do_not_propagate_mask: i64, - override_redirect: i32, - colormap: u64, - cursor: u64, - } - - #[link(name = "X11")] - extern "C" { - fn XOpenDisplay(display_name: *const i8) -> *mut c_void; - fn XDefaultScreen(display: *mut c_void) -> i32; - fn XDefaultVisual(display: *mut c_void, screen: i32) -> *mut c_void; - fn XDefaultDepth(display: *mut c_void, screen: i32) -> i32; - fn XDefaultColormap(display: *mut c_void, screen: i32) -> u64; - fn XBlackPixel(display: *mut c_void, screen: i32) -> u64; - fn XCreateWindow( - display: *mut c_void, - parent: u64, - x: i32, - y: i32, - width: u32, - height: u32, - border_width: u32, - depth: i32, - class: u32, - visual: *mut c_void, - valuemask: u64, - attributes: *mut XSetWindowAttributes, - ) -> u64; - fn XMapWindow(display: *mut c_void, window: u64) -> i32; - fn XUnmapWindow(display: *mut c_void, window: u64) -> i32; - fn XMoveResizeWindow( - display: *mut c_void, - window: u64, - x: i32, - y: i32, - width: u32, - height: u32, - ) -> i32; - fn XRaiseWindow(display: *mut c_void, window: u64) -> i32; - fn XClearWindow(display: *mut c_void, window: u64) -> i32; - fn XDestroyWindow(display: *mut c_void, window: u64) -> i32; - fn XFlush(display: *mut c_void) -> i32; - fn XSync(display: *mut c_void, discard: i32) -> i32; - fn XSelectInput(display: *mut c_void, window: u64, event_mask: i64) -> i32; - } - - const INPUT_OUTPUT: u32 = 1; - const CW_BACK_PIXEL: u64 = 0x0002; - const CW_EVENT_MASK: u64 = 0x0800; - const CW_COLORMAP: u64 = 0x2000; - // Exposure + StructureNotify so GstVideoOverlay can redraw after map/resize. - // No pointer/keyboard masks — Electron owns input in internal mode. - const EXPOSURE_MASK: i64 = 0x0000_8000; - const STRUCTURE_NOTIFY_MASK: i64 = 0x0002_0000; - const EVENT_MASK: i64 = EXPOSURE_MASK | STRUCTURE_NOTIFY_MASK; - - fn wayland_session_active() -> bool { - std::env::var_os("WAYLAND_DISPLAY") - .filter(|value| !value.is_empty()) - .is_some() - } - - fn x11_unavailable_message() -> String { - if wayland_session_active() { - "Native internal renderer requires X11 or XWayland. Pure Wayland embedding is not supported yet — launch under X11, or set GDK_BACKEND=x11 / use an XWayland session." - .to_owned() - } else { - "XOpenDisplay failed; native internal renderer requires a working X11 display (DISPLAY unset or unreachable)." - .to_owned() - } - } - - pub(super) fn create_child(parent: usize, bounds: &NativeRenderRect) -> Result { - unsafe { - let display = XOpenDisplay(null_mut()); - if display.is_null() { - return Err(x11_unavailable_message()); - } - - let screen = XDefaultScreen(display); - let visual = XDefaultVisual(display, screen); - let depth = XDefaultDepth(display, screen); - let mut attrs = XSetWindowAttributes { - background_pixmap: 0, - background_pixel: XBlackPixel(display, screen), - border_pixmap: 0, - border_pixel: 0, - bit_gravity: 0, - win_gravity: 0, - backing_store: 0, - backing_planes: 0, - backing_pixel: 0, - save_under: 0, - event_mask: EVENT_MASK, - do_not_propagate_mask: 0, - override_redirect: 0, - colormap: XDefaultColormap(display, screen), - cursor: 0, - }; - - let window = XCreateWindow( - display, - parent as u64, - bounds.x, - bounds.y, - bounds.width.max(2) as u32, - bounds.height.max(2) as u32, - 0, - depth, - INPUT_OUTPUT, - visual, - CW_BACK_PIXEL | CW_EVENT_MASK | CW_COLORMAP, - &mut attrs, - ); - - if window == 0 { - return Err("XCreateWindow failed for internal renderer child.".to_owned()); - } - - XSelectInput(display, window, EVENT_MASK); - XMapWindow(display, window); - XRaiseWindow(display, window); - // Ensure the child is mapped and sized before GstVideoOverlay binds. - XSync(display, 0); - XClearWindow(display, window); - XFlush(display); - - Ok(XWindow { - display: display as usize, - window, - parent: parent as u64, - }) - } - } - - pub(super) fn set_bounds(window: &XWindow, bounds: &NativeRenderRect) -> Result<(), String> { - if window.display == 0 || window.window == 0 { - return Ok(()); - } - let display = window.display as *mut c_void; - unsafe { - XMoveResizeWindow( - display, - window.window, - bounds.x, - bounds.y, - bounds.width.max(2) as u32, - bounds.height.max(2) as u32, - ); - XRaiseWindow(display, window.window); - XSync(display, 0); - XFlush(display); - } - let _ = window.parent; - Ok(()) - } - - pub(super) fn set_visible(window: &XWindow, visible: bool) -> Result<(), String> { - if window.display == 0 || window.window == 0 { - return Ok(()); - } - let display = window.display as *mut c_void; - unsafe { - if visible { - XMapWindow(display, window.window); - XRaiseWindow(display, window.window); - XClearWindow(display, window.window); - } else { - XUnmapWindow(display, window.window); - } - XSync(display, 0); - XFlush(display); - } - Ok(()) - } - - pub(super) fn destroy(window: &mut XWindow) { - if window.display == 0 { - return; - } - let display = window.display as *mut c_void; - unsafe { - if window.window != 0 { - XDestroyWindow(display, window.window); - window.window = 0; - } - // Intentionally keep the display open for process lifetime; closing here - // can race with other X users in the same process. - XFlush(display); - } - } -} diff --git a/native/opennow-streamer/src/main.rs b/native/opennow-streamer/src/main.rs deleted file mode 100644 index abf8216a9..000000000 --- a/native/opennow-streamer/src/main.rs +++ /dev/null @@ -1,178 +0,0 @@ -#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] - -mod backend; -#[cfg(feature = "gstreamer")] -mod gstreamer_backend; -// Present/input policy helpers are pure Rust and stay available for unit tests -// even when the optional GStreamer feature is off. -mod gstreamer_config; -#[cfg(feature = "gstreamer")] -mod gstreamer_input; -#[cfg(feature = "gstreamer")] -mod gstreamer_liveness; -#[cfg(feature = "gstreamer")] -mod gstreamer_pipeline; -#[cfg(feature = "gstreamer")] -mod gstreamer_platform; -#[cfg(feature = "gstreamer")] -mod gstreamer_transitions; -#[cfg(feature = "gstreamer")] -mod internal_renderer; -mod input; -mod nvst_video; -mod protocol; -mod shortcuts; -mod sdp; -#[cfg(target_os = "windows")] -mod windows_dpi; - -use serde::Serialize; -use serde_json::Value; -use std::io::{self, BufRead, Write}; -use std::sync::mpsc; -use std::thread; - -use backend::{create_backend, BackendReply, NativeStreamerBackend}; -use protocol::{parse_command, CommandEnvelope, Event, Response, PROTOCOL_VERSION}; - -fn write_json(value: &T) -> io::Result<()> { - let mut stdout = io::stdout().lock(); - serde_json::to_writer(&mut stdout, value)?; - writeln!(stdout)?; - stdout.flush() -} - -fn write_response(response: &Response) -> io::Result<()> { - write_json(response) -} - -fn write_event(event: &Event) -> io::Result<()> { - write_json(event) -} - -fn write_error(id: Option, code: &str, message: impl Into) -> io::Result<()> { - write_response(&Response::Error { - id, - code: code.to_owned(), - message: message.into(), - }) -} - -fn write_reply(reply: BackendReply) -> io::Result { - for event in &reply.events { - write_event(event)?; - } - if let Some(response) = &reply.response { - write_response(response)?; - } - Ok(reply.should_continue) -} - -fn handle_command( - command: CommandEnvelope, - backend: &mut dyn NativeStreamerBackend, -) -> io::Result { - match command.command_type.as_str() { - "hello" => { - let requested = command.protocol_version.unwrap_or(0); - if requested != PROTOCOL_VERSION { - write_error( - Some(command.id), - "protocol-version-mismatch", - "Unsupported native streamer protocol version.", - )?; - return Ok(true); - } - - write_response(&Response::Ready { - id: command.id, - capabilities: backend.capabilities(), - })?; - } - "start" => { - return write_reply(backend.start(command)); - } - "offer" => { - return write_reply(backend.handle_offer(command)); - } - "remote-ice" => { - return write_reply(backend.add_remote_ice(command)); - } - "input" => { - return write_reply(backend.send_input(command)); - } - "input-paused" => { - return write_reply(backend.set_input_paused(command)); - } - "surface" => { - return write_reply(backend.update_render_surface(command)); - } - "bitrate" => { - return write_reply(backend.update_bitrate_limit(command)); - } - "update-shortcuts" => { - return write_reply(backend.update_shortcuts(command)); - } - "stop" => { - return write_reply(backend.stop(command)); - } - other => { - write_error( - Some(command.id), - "unknown-command", - format!("Unknown command: {other}"), - )?; - } - } - - Ok(true) -} - -fn main() -> io::Result<()> { - #[cfg(target_os = "windows")] - windows_dpi::enable_per_monitor_awareness(); - - let stdin = io::stdin(); - let (event_sender, event_receiver) = mpsc::channel::(); - let event_writer = thread::spawn(move || { - for event in event_receiver { - if let Err(error) = write_event(&event) { - eprintln!("[NativeStreamer] Failed to write async event: {error}"); - break; - } - } - }); - let mut backend = create_backend(Some(event_sender)); - - for line in stdin.lock().lines() { - let line = line?; - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - - let value: Value = match serde_json::from_str(trimmed) { - Ok(value) => value, - Err(error) => { - write_error(None, "invalid-json", error.to_string())?; - continue; - } - }; - - let command = match parse_command(value) { - Ok(command) => command, - Err(error) => { - write_error(None, "invalid-command", error)?; - continue; - } - }; - - if !handle_command(command, backend.as_mut())? { - break; - } - } - - drop(backend); - let _ = event_writer.join(); - Ok(()) -} diff --git a/native/opennow-streamer/src/nvst_video.rs b/native/opennow-streamer/src/nvst_video.rs deleted file mode 100644 index 3415d24ab..000000000 --- a/native/opennow-streamer/src/nvst_video.rs +++ /dev/null @@ -1,537 +0,0 @@ -//! Classic NVST / Mjolnir UDP video receive scaffold (GO-with-Moonlight-hypothesis). -//! -//! Pipeline (intended): -//! UDP bind → hole-punch PING → (SRTP decrypt) → RTP(+ext) → NV_VIDEO_PACKET → -//! assemble AUs on FLAG_EOF → appsrc (Annex-B) → h265parse/h264parse → decoder → sink -//! -//! SRTP: master = AES-256 key[32] || salt[12] where salt = key_id as BE u32 zero-padded -//! to 12 bytes (`%024x`). Prefer AEAD_AES_256_GCM. This scaffold does **not** link -//! libsrtp; when decrypt is unavailable it logs once and parses cleartext RTP so the -//! receive/assemble/appsrc path can be exercised. A GStreamer `srtpdec` branch can be -//! added later once the wire profile is confirmed on a live pcap. - -#![cfg_attr(not(feature = "gstreamer"), allow(dead_code))] - -#[cfg(feature = "gstreamer")] -use crate::gstreamer_backend::send_log; -#[cfg(feature = "gstreamer")] -use crate::protocol::{Event, NvstVideoSession}; -#[cfg(feature = "gstreamer")] -use gstreamer as gst; -#[cfg(feature = "gstreamer")] -use gstreamer::prelude::*; -#[cfg(feature = "gstreamer")] -use std::io; -#[cfg(feature = "gstreamer")] -use std::net::{SocketAddr, UdpSocket}; -#[cfg(feature = "gstreamer")] -use std::sync::atomic::{AtomicBool, Ordering}; -#[cfg(feature = "gstreamer")] -use std::sync::mpsc::Sender; -#[cfg(feature = "gstreamer")] -use std::sync::Arc; -#[cfg(feature = "gstreamer")] -use std::thread::{self, JoinHandle}; -#[cfg(feature = "gstreamer")] -use std::time::Duration; - -/// Moonlight / GameStream `NV_VIDEO_PACKET` size (little-endian fields). -pub const NV_VIDEO_PACKET_LEN: usize = 16; -/// `FLAG_EOF` — last packet of a frame (assemble AU). -pub const FLAG_EOF: u8 = 0x02; -/// `FLAG_SOF` — first packet of a frame. -#[allow(dead_code)] -pub const FLAG_SOF: u8 = 0x04; -/// `FLAG_CONTAINS_PIC_DATA`. -#[allow(dead_code)] -pub const FLAG_CONTAINS_PIC_DATA: u8 = 0x01; - -/// Pack AES-256 key + key ID into the 44-byte libsrtp master key||salt. -/// -/// Salt is `key_id` as a big-endian u32, zero-padded to 12 bytes (`printf`-style `%024x`). -pub fn pack_srtp_master_key_salt(aes_key: &[u8; 32], key_id: u32) -> [u8; 44] { - let mut out = [0u8; 44]; - out[..32].copy_from_slice(aes_key); - out[40..44].copy_from_slice(&key_id.to_be_bytes()); - out -} - -/// Decode a 64-hex AES-256 key string into 32 bytes. -pub fn parse_aes_key_hex(hex: &str) -> Result<[u8; 32], String> { - let trimmed = hex.trim(); - if trimmed.len() != 64 { - return Err(format!( - "srtpAesKeyHex must be 64 hex chars, got {}", - trimmed.len() - )); - } - let mut out = [0u8; 32]; - for (i, chunk) in trimmed.as_bytes().chunks(2).enumerate() { - let s = std::str::from_utf8(chunk).map_err(|e| e.to_string())?; - out[i] = u8::from_str_radix(s, 16) - .map_err(|e| format!("Invalid hex in srtpAesKeyHex at byte {i}: {e}"))?; - } - Ok(out) -} - -/// Parsed Moonlight-hypothesis `NV_VIDEO_PACKET` (LE). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct NvVideoPacket { - pub stream_packet_index: u32, - pub frame_index: u32, - pub flags: u8, - pub extra_flags: u8, - pub multi_fec_flags: u8, - pub multi_fec_blocks: u8, - pub fec_info: u32, -} - -impl NvVideoPacket { - pub fn parse(bytes: &[u8]) -> Option { - if bytes.len() < NV_VIDEO_PACKET_LEN { - return None; - } - Some(Self { - stream_packet_index: u32::from_le_bytes(bytes[0..4].try_into().ok()?), - frame_index: u32::from_le_bytes(bytes[4..8].try_into().ok()?), - flags: bytes[8], - extra_flags: bytes[9], - multi_fec_flags: bytes[10], - multi_fec_blocks: bytes[11], - fec_info: u32::from_le_bytes(bytes[12..16].try_into().ok()?), - }) - } - - /// FEC / non-picture packets use `flags == 0` in the Moonlight hypothesis. - pub fn is_fec_or_empty(&self) -> bool { - self.flags == 0 - } - - pub fn is_eof(&self) -> bool { - self.flags & FLAG_EOF != 0 - } -} - -/// Strip standard RTP header (+ optional 4-byte extension pad when X is set) and -/// return the payload starting at `NV_VIDEO_PACKET`. -/// -/// Layout hypothesis (post-SRTP): `12B RTP + 4B ext pad if X + 16B NV_VIDEO_PACKET + payload`. -pub fn strip_rtp_header(packet: &[u8]) -> Option<&[u8]> { - if packet.len() < 12 { - return None; - } - let v_pxcc = packet[0]; - let version = v_pxcc >> 6; - if version != 2 { - return None; - } - let has_padding = (v_pxcc & 0x20) != 0; - let has_extension = (v_pxcc & 0x10) != 0; - let csrc_count = (v_pxcc & 0x0f) as usize; - let mut offset = 12 + csrc_count * 4; - if packet.len() < offset { - return None; - } - if has_extension { - // Moonlight-hypothesis: fixed 4-byte extension pad (not full RFC 5285 walk). - offset = offset.checked_add(4)?; - if packet.len() < offset { - return None; - } - } - - let mut payload = &packet[offset..]; - if has_padding { - let pad_len = *payload.last()? as usize; - if pad_len == 0 || pad_len > payload.len() { - return None; - } - payload = &payload[..payload.len() - pad_len]; - } - Some(payload) -} - -/// Parse RTP → NV_VIDEO_PACKET → media payload. Returns `None` for FEC (`flags==0`) -/// or malformed packets. -pub fn parse_nvst_rtp_payload(packet: &[u8]) -> Option<(NvVideoPacket, &[u8])> { - let after_rtp = strip_rtp_header(packet)?; - let header = NvVideoPacket::parse(after_rtp)?; - if header.is_fec_or_empty() { - return None; - } - let media = after_rtp.get(NV_VIDEO_PACKET_LEN..)?; - Some((header, media)) -} - -/// Assembles Annex-B access units from NVST RTP payloads (Moonlight hypothesis). -#[derive(Debug, Default)] -pub struct NvstFrameAssembler { - current_frame: Option, - buffer: Vec, -} - -impl NvstFrameAssembler { - pub fn new() -> Self { - Self::default() - } - - /// Push one media packet. Returns a completed AU when `FLAG_EOF` is set. - pub fn push(&mut self, header: &NvVideoPacket, payload: &[u8]) -> Option> { - if header.is_fec_or_empty() { - return None; - } - - match self.current_frame { - Some(frame) if frame != header.frame_index => { - self.buffer.clear(); - self.current_frame = Some(header.frame_index); - } - None => { - self.current_frame = Some(header.frame_index); - } - _ => {} - } - - self.buffer.extend_from_slice(payload); - - if header.is_eof() { - let au = std::mem::take(&mut self.buffer); - self.current_frame = None; - if au.is_empty() { - None - } else { - Some(au) - } - } else { - None - } - } -} - -/// Caps string for Annex-B appsrc feeding `h265parse` / `h264parse`. -pub fn annexb_appsrc_caps(codec: &str) -> &'static str { - match codec.to_ascii_uppercase().as_str() { - "H264" => "video/x-h264,stream-format=byte-stream,alignment=au", - _ => "video/x-h265,stream-format=byte-stream,alignment=au", - } -} - -/// Build description for a GStreamer branch that would decrypt SRTP then feed RTP -/// into an appsrc-style path. Documented for future `srtpdec` wiring; the live -/// scaffold currently decrypts (or skips) in the UDP thread and pushes Annex-B AUs. -pub fn srtpdec_pipeline_branch_hint(codec: &str) -> String { - let caps = annexb_appsrc_caps(codec); - let parser = match codec.to_ascii_uppercase().as_str() { - "H264" => "h264parse", - _ => "h265parse", - }; - format!( - "appsrc name=nvst-annexb caps=\"{caps}\" is-live=true format=time ! \ - {parser} ! …decoder… ! …sink… \ - (SRTP: prefer libsrtp AEAD_AES_256_GCM with pack_srtp_master_key_salt; \ - GStreamer srtpdec may be wired later — do not use appsrc caps=application/x-rtp \ - named nvst-rtp for assembled AUs)" - ) -} - -#[cfg(feature = "gstreamer")] -pub(crate) struct NvstVideoReceiveHandle { - stop: Arc, - join: Option>, -} - -#[cfg(feature = "gstreamer")] -impl std::fmt::Debug for NvstVideoReceiveHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NvstVideoReceiveHandle") - .field("stop", &self.stop.load(Ordering::SeqCst)) - .field("join", &self.join.as_ref().map(|_| "JoinHandle")) - .finish() - } -} - -#[cfg(feature = "gstreamer")] -impl NvstVideoReceiveHandle { - pub(crate) fn stop(mut self) { - self.stop.store(true, Ordering::SeqCst); - if let Some(join) = self.join.take() { - let _ = join.join(); - } - } -} - -#[cfg(feature = "gstreamer")] -impl Drop for NvstVideoReceiveHandle { - fn drop(&mut self) { - self.stop.store(true, Ordering::SeqCst); - if let Some(join) = self.join.take() { - let _ = join.join(); - } - } -} - -/// Spawn UDP bind → PING hole-punch → recv → parse → push Annex-B AUs into `appsrc`. -#[cfg(feature = "gstreamer")] -pub(crate) fn spawn_nvst_udp_receive( - session: NvstVideoSession, - appsrc: gst::Element, - event_sender: Option>, -) -> Result { - let aes_key = parse_aes_key_hex(&session.srtp_aes_key_hex)?; - let master = pack_srtp_master_key_salt(&aes_key, session.srtp_key_id); - let _ = master; // reserved for future libsrtp / srtpdec install - - let peer: SocketAddr = format!("{}:{}", session.video_peer_ip, session.video_peer_port) - .parse() - .map_err(|e| format!("Invalid nvstVideo peer address: {e}"))?; - - let bind_addr = SocketAddr::from(([0, 0, 0, 0], session.client_udp_port)); - let socket = UdpSocket::bind(bind_addr) - .map_err(|e| format!("Failed to bind NVST UDP {bind_addr}: {e}"))?; - socket - .set_read_timeout(Some(Duration::from_millis(250))) - .map_err(|e| format!("Failed to set NVST UDP read timeout: {e}"))?; - - let ping = session - .ping_payload - .as_deref() - .filter(|s| !s.is_empty()) - .unwrap_or("PING"); - match socket.send_to(ping.as_bytes(), peer) { - Ok(_) => send_log( - &event_sender, - "info", - format!( - "NVST video hole-punch sent ({ping_len} B) to {peer} from {bind_addr}.", - ping_len = ping.len() - ), - ), - Err(e) => send_log( - &event_sender, - "warn", - format!("NVST video hole-punch to {peer} failed: {e}"), - ), - } - - send_log( - &event_sender, - "info", - format!( - "NVST SRTP scaffold: master key/salt packed (44 B) for keyId {}; \ - libsrtp/srtpdec not linked — parsing cleartext RTP if packets arrive undecrypted. {}", - session.srtp_key_id, - srtpdec_pipeline_branch_hint(session.codec.as_deref().unwrap_or("H265")) - ), - ); - - let stop = Arc::new(AtomicBool::new(false)); - let stop_flag = stop.clone(); - let join = thread::Builder::new() - .name("nvst-udp-video".to_owned()) - .spawn(move || { - nvst_udp_recv_loop(socket, appsrc, event_sender, stop_flag); - }) - .map_err(|e| format!("Failed to spawn NVST UDP thread: {e}"))?; - - Ok(NvstVideoReceiveHandle { - stop, - join: Some(join), - }) -} - -#[cfg(feature = "gstreamer")] -fn nvst_udp_recv_loop( - socket: UdpSocket, - appsrc: gst::Element, - event_sender: Option>, - stop: Arc, -) { - let mut buf = [0u8; 2048]; - let mut assembler = NvstFrameAssembler::new(); - let mut first_packet_logged = false; - let mut first_au_logged = false; - let mut decrypt_fail_logged = false; - let mut cleartext_notice_logged = false; - - while !stop.load(Ordering::SeqCst) { - match socket.recv_from(&mut buf) { - Ok((len, _from)) => { - if !first_packet_logged { - first_packet_logged = true; - send_log( - &event_sender, - "info", - format!("NVST UDP received first packet ({len} B)."), - ); - } - if !cleartext_notice_logged { - cleartext_notice_logged = true; - send_log( - &event_sender, - "info", - "NVST SRTP decrypt unavailable in scaffold; attempting cleartext RTP parse." - .to_owned(), - ); - } - - let packet = &buf[..len]; - if packet.first().map(|b| b >> 6) != Some(2) { - if !decrypt_fail_logged { - decrypt_fail_logged = true; - send_log( - &event_sender, - "warn", - "NVST UDP packet does not look like cleartext RTP (version != 2); \ - SRTP decrypt required — continuing to listen." - .to_owned(), - ); - } - continue; - } - - let Some((header, payload)) = parse_nvst_rtp_payload(packet) else { - continue; - }; - let Some(au) = assembler.push(&header, payload) else { - continue; - }; - if !first_au_logged { - first_au_logged = true; - send_log( - &event_sender, - "info", - format!( - "NVST assembled first Annex-B AU ({} B, frameIndex={}).", - au.len(), - header.frame_index - ), - ); - } - if let Err(err) = push_au_to_appsrc(&appsrc, &au) { - send_log( - &event_sender, - "warn", - format!("NVST appsrc push failed: {err}"), - ); - } - } - Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => { - continue; - } - Err(e) => { - if !stop.load(Ordering::SeqCst) { - send_log( - &event_sender, - "warn", - format!("NVST UDP recv error: {e}"), - ); - } - break; - } - } - } -} - -#[cfg(feature = "gstreamer")] -fn push_au_to_appsrc(appsrc: &gst::Element, au: &[u8]) -> Result<(), String> { - let mut buffer = gst::Buffer::from_mut_slice(au.to_vec()); - { - let buffer = buffer - .get_mut() - .ok_or_else(|| "NVST appsrc buffer not writable".to_owned())?; - buffer.set_pts(gst::ClockTime::NONE); - buffer.set_dts(gst::ClockTime::NONE); - } - let flow = appsrc.emit_by_name::("push-buffer", &[&buffer]); - if flow != gst::FlowReturn::Ok { - return Err(format!("appsrc push-buffer returned {flow:?}")); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pack_srtp_master_key_salt_matches_docs_key_id() { - // Docs: key_id 2664076126 → salt ends 9ECA935E (`%024x` → 00000000000000009ECA935E). - let key = [0xABu8; 32]; - let packed = pack_srtp_master_key_salt(&key, 2664076126); - assert_eq!(&packed[..32], &key); - assert_eq!(&packed[32..40], &[0u8; 8]); - assert_eq!(&packed[40..44], &[0x9E, 0xCA, 0x93, 0x5E]); - } - - #[test] - fn pack_srtp_additional_doc_key_ids() { - let key = [0u8; 32]; - let a = pack_srtp_master_key_salt(&key, 1664590642); - assert_eq!(&a[40..44], &[0x63, 0x37, 0xA3, 0x32]); - let b = pack_srtp_master_key_salt(&key, 2478780175); - assert_eq!(&b[40..44], &[0x93, 0xBF, 0x2F, 0x0F]); - } - - #[test] - fn parse_nv_video_packet_synthetic() { - let mut bytes = [0u8; 16]; - bytes[0..4].copy_from_slice(&0x11223344u32.to_le_bytes()); - bytes[4..8].copy_from_slice(&7u32.to_le_bytes()); - bytes[8] = FLAG_SOF | FLAG_EOF | FLAG_CONTAINS_PIC_DATA; - bytes[9] = 0x10; - bytes[10] = 0x20; - bytes[11] = 0x30; - bytes[12..16].copy_from_slice(&0xAABBCCDDu32.to_le_bytes()); - - let pkt = NvVideoPacket::parse(&bytes).expect("parse"); - assert_eq!(pkt.stream_packet_index, 0x11223344); - assert_eq!(pkt.frame_index, 7); - assert!(pkt.is_eof()); - assert!(!pkt.is_fec_or_empty()); - assert_eq!(pkt.extra_flags, 0x10); - assert_eq!(pkt.fec_info, 0xAABBCCDD); - } - - #[test] - fn fec_flags_zero_ignored_by_parser() { - let mut packet = vec![0x80u8, 0x60, 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0]; - packet.extend_from_slice(&[0u8; 16]); - packet.extend_from_slice(&[0xDE, 0xAD]); - assert!(parse_nvst_rtp_payload(&packet).is_none()); - } - - #[test] - fn assemble_frame_on_eof() { - let mut asm = NvstFrameAssembler::new(); - let sof = NvVideoPacket { - stream_packet_index: 1, - frame_index: 3, - flags: FLAG_SOF | FLAG_CONTAINS_PIC_DATA, - extra_flags: 0, - multi_fec_flags: 0, - multi_fec_blocks: 0, - fec_info: 0, - }; - let eof = NvVideoPacket { - flags: FLAG_EOF | FLAG_CONTAINS_PIC_DATA, - stream_packet_index: 2, - ..sof - }; - assert!(asm.push(&sof, b"AA").is_none()); - let au = asm.push(&eof, b"BB").expect("AU"); - assert_eq!(au, b"AABB"); - } - - #[test] - fn strip_rtp_with_extension_pad() { - let mut packet = vec![0x90u8, 0x60, 0x00, 0x02, 0, 0, 0, 0, 0, 0, 0, 0]; - packet.extend_from_slice(&[0xBE, 0xDE, 0x00, 0x00]); - let mut nv = [0u8; 16]; - nv[8] = FLAG_EOF | FLAG_CONTAINS_PIC_DATA; - packet.extend_from_slice(&nv); - packet.extend_from_slice(b"NAL"); - let (hdr, payload) = parse_nvst_rtp_payload(&packet).expect("parse"); - assert!(hdr.is_eof()); - assert_eq!(payload, b"NAL"); - } -} diff --git a/native/opennow-streamer/src/protocol.rs b/native/opennow-streamer/src/protocol.rs deleted file mode 100644 index a3f79b572..000000000 --- a/native/opennow-streamer/src/protocol.rs +++ /dev/null @@ -1,739 +0,0 @@ -use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::borrow::Cow; - -pub const PROTOCOL_VERSION: u64 = 4; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CommandEnvelope { - pub id: String, - #[serde(rename = "type")] - pub command_type: String, - #[serde(default)] - pub protocol_version: Option, - #[serde(default)] - pub context: Option, - #[serde(default)] - pub sdp: Option, - #[serde(default)] - pub candidate: Option, - #[serde(default)] - pub input: Option, - #[serde(default)] - pub paused: Option, - #[serde(default)] - pub surface: Option, - #[serde(default)] - pub max_bitrate_kbps: Option, - #[serde(default)] - pub reason: Option, - #[serde(default)] - pub shortcuts: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeStreamerSessionContext { - pub session: SessionInfo, - pub settings: StreamSettings, - #[serde(default)] - pub shortcuts: NativeStreamerShortcutBindings, - /// Classic Mjolnir UDP video (post-SETUP peer + SRTP material). When set, - /// the GStreamer backend receives video over UDP while keeping webrtcbin - /// for SCTP datachannels / input. - #[serde(default, rename = "nvstVideo")] - pub nvst_video: Option, -} - -/// Classic NVST UDP video session parameters (Moonlight-hypothesis scaffold). -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] -pub struct NvstVideoSession { - pub client_udp_port: u16, - pub video_peer_ip: String, - pub video_peer_port: u16, - /// 64 hex chars = 32-byte AES-256 key. - pub srtp_aes_key_hex: String, - pub srtp_key_id: u32, - /// Optional SETUP `X-Nv-Ping-Payload` token; when absent, hole-punch sends `PING`. - #[serde(default)] - pub ping_payload: Option, - /// `H265` / `H264` (defaults to session settings codec when omitted). - #[serde(default)] - pub codec: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionInfo { - pub session_id: String, - pub server_ip: String, - #[serde(default)] - pub ice_servers: Vec, - #[serde(default)] - pub media_connection_info: Option, - #[allow(dead_code)] - #[serde(default)] - pub negotiated_stream_profile: Option, - #[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] - #[serde(default)] - pub requested_streaming_features: Option, - #[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] - #[serde(default)] - pub finalized_streaming_features: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct IceServer { - pub urls: Vec, - #[allow(dead_code)] - #[serde(default)] - pub username: Option, - #[allow(dead_code)] - #[serde(default)] - pub credential: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct MediaConnectionInfo { - pub ip: String, - pub port: u16, - #[serde(default)] - pub usage: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StreamSettings { - pub resolution: String, - pub fps: u32, - pub max_bitrate_mbps: u32, - pub codec: VideoCodec, - pub color_quality: ColorQuality, - #[serde(default)] - #[allow(dead_code)] - pub enable_cloud_gsync: bool, - #[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] - #[serde(default)] - pub native_transition_diagnostics: Option, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct StreamingFeatures { - #[serde(default)] - pub reflex: Option, - #[serde(default)] - pub bit_depth: Option, - #[serde(default)] - pub cloud_gsync: Option, - #[serde(default)] - pub chroma_format: Option, - #[serde(default)] - pub enabled_l4s: Option, - #[serde(default)] - pub true_hdr: Option, -} - -#[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] -impl StreamingFeatures { - pub fn summary(&self) -> String { - let mut parts = Vec::new(); - if let Some(reflex) = self.reflex { - parts.push(format!("reflex={reflex}")); - } - if let Some(bit_depth) = self.bit_depth { - parts.push(format!("bitDepth={bit_depth}")); - } - if let Some(cloud_gsync) = self.cloud_gsync { - parts.push(format!("cloudGsync={cloud_gsync}")); - } - if let Some(chroma_format) = self.chroma_format { - parts.push(format!("chroma={chroma_format}")); - } - if let Some(enabled_l4s) = self.enabled_l4s { - parts.push(format!("l4s={enabled_l4s}")); - } - if let Some(true_hdr) = self.true_hdr { - parts.push(format!("trueHdr={true_hdr}")); - } - if parts.is_empty() { - "none".to_owned() - } else { - parts.join(", ") - } - } -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NegotiatedStreamProfile { - #[serde(default)] - pub resolution: Option, - #[serde(default)] - pub fps: Option, - #[serde(default)] - pub codec: Option, -} - -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum NativeQueueMode { - #[default] - Auto, - Fixed, - Adaptive, - Vrr, -} - -#[cfg_attr(not(feature = "gstreamer"), allow(dead_code))] -impl NativeQueueMode { - pub fn as_str(self) -> &'static str { - match self { - Self::Auto => "auto", - Self::Fixed => "fixed", - Self::Adaptive => "adaptive", - Self::Vrr => "vrr", - } - } -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeTransitionDiagnosticsSettings { - #[serde(default)] - pub disable_dynamic_split_encode_updates: bool, - #[serde(default)] - pub force_queue_mode: Option, - #[serde(default)] - pub disable_transition_flush_escalation: bool, -} - -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] -pub enum VideoCodec { - H264, - H265, - AV1, -} - -impl VideoCodec { - #[allow(dead_code)] - pub fn as_str(self) -> &'static str { - match self { - Self::H264 => "H264", - Self::H265 => "H265", - Self::AV1 => "AV1", - } - } -} - -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] -pub enum ColorQuality { - #[serde(rename = "8bit_420")] - EightBit420, - #[serde(rename = "8bit_444")] - EightBit444, - #[serde(rename = "10bit_420")] - TenBit420, - #[serde(rename = "10bit_444")] - TenBit444, -} - -impl ColorQuality { - pub fn bit_depth(self) -> u8 { - match self { - Self::EightBit420 | Self::EightBit444 => 8, - Self::TenBit420 | Self::TenBit444 => 10, - } - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct IceCandidatePayload { - pub candidate: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub sdp_mid: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sdp_m_line_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub username_fragment: Option, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeInputPacket { - #[serde(default)] - pub payload: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub payload_base64: Option, - #[serde(default)] - pub partially_reliable: bool, -} - -impl NativeInputPacket { - pub fn payload_bytes(&self) -> Result, String> { - let Some(payload_base64) = self.payload_base64.as_deref() else { - return Ok(Cow::Borrowed(&self.payload)); - }; - - BASE64_STANDARD - .decode(payload_base64) - .map(Cow::Owned) - .map_err(|error| format!("Invalid base64 input payload: {error}")) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeRenderSurface { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub window_handle: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub rect: Option, - #[serde(default)] - pub visible: bool, - #[serde(default)] - pub device_scale_factor: f64, - #[serde(default)] - pub show_stats: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeRenderRect { - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeStreamerShortcutBindings { - #[serde(default)] - pub toggle_stats: String, - #[serde(default)] - pub toggle_pointer_lock: String, - #[serde(default)] - pub toggle_fullscreen: String, - #[serde(default)] - pub stop_stream: String, - #[serde(default)] - pub toggle_anti_afk: String, - #[serde(default)] - pub toggle_microphone: String, - #[serde(default)] - pub screenshot: String, - #[serde(default)] - pub toggle_recording: String, -} - -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum NativeStreamerShortcutAction { - ToggleStats, - TogglePointerLock, - ToggleFullscreen, - StopStream, - ToggleAntiAfk, - ToggleMicrophone, - Screenshot, - ToggleRecording, -} - -#[derive(Debug, Clone, Serialize)] -pub struct NativeStreamerCapabilities { - #[serde(rename = "protocolVersion")] - pub protocol_version: u64, - pub backend: &'static str, - #[serde(rename = "requestedBackend", skip_serializing_if = "Option::is_none")] - pub requested_backend: Option, - #[serde(rename = "fallbackReason", skip_serializing_if = "Option::is_none")] - pub fallback_reason: Option, - #[serde(rename = "supportsOfferAnswer")] - pub supports_offer_answer: bool, - #[serde(rename = "supportsRemoteIce")] - pub supports_remote_ice: bool, - #[serde(rename = "supportsLocalIce")] - pub supports_local_ice: bool, - #[serde(rename = "supportsInput")] - pub supports_input: bool, - #[serde( - rename = "videoBackends", - default, - skip_serializing_if = "Vec::is_empty" - )] - pub video_backends: Vec, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeVideoBackendCapability { - pub backend: String, - pub platform: String, - pub codecs: Vec, - #[serde(rename = "zeroCopyModes")] - pub zero_copy_modes: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub sink: Option, - pub available: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeVideoCodecCapability { - pub codec: String, - pub available: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub decoder: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub parser: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub depayloader: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[allow(dead_code)] -#[serde(tag = "type")] -pub enum Response { - #[serde(rename = "ready")] - Ready { - id: String, - capabilities: NativeStreamerCapabilities, - }, - #[serde(rename = "ok")] - Ok { id: String }, - #[serde(rename = "answer")] - Answer { - id: String, - answer: SendAnswerRequest, - }, - #[serde(rename = "error")] - Error { - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, - code: String, - message: String, - }, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SendAnswerRequest { - pub sdp: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub nvst_sdp: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VideoStallEvent { - pub stall_ms: u64, - pub encoded_kbps: f64, - pub decoded_fps: f64, - pub sink_fps: f64, - #[serde(skip_serializing_if = "Option::is_none")] - pub encoded_age_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub decoded_age_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sink_age_ms: Option, - pub likely_stage: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub sink_rendered: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sink_dropped: Option, - pub memory_mode: String, - pub zero_copy: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_fps: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub caps_framerate: Option, - pub queue_mode: String, - pub partial_flush_count: u32, - pub complete_flush_count: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_transition_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_transition_at_ms: Option, - pub requested_streaming_features_summary: String, - pub finalized_streaming_features_summary: String, - pub zero_copy_d3d11: bool, - pub zero_copy_d3d12: bool, - pub recovery_attempt: u8, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct VideoTransitionEvent { - pub transition_type: String, - pub source: String, - pub at_ms: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub old_caps: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub new_caps: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub old_framerate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub new_framerate: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub old_memory_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub new_memory_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub render_gap_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_fps: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub caps_framerate: Option, - pub high_fps_risk: bool, - pub queue_mode: String, - pub summary: String, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct NativeStatsEvent { - pub codec: String, - pub resolution: String, - pub hardware_acceleration: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub requested_fps: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub caps_framerate: Option, - pub bitrate_kbps: u32, - pub target_bitrate_kbps: u32, - pub bitrate_performance_percent: f64, - pub decoded_fps: f64, - pub render_fps: f64, - pub frames_decoded: u64, - pub frames_rendered: u64, - pub frames_pending_to_present: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub sink_rendered: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sink_dropped: Option, - pub memory_mode: String, - pub zero_copy: bool, - pub queue_mode: String, - pub queue_depth_changes: u32, - pub present_pacing_changes: u32, - pub partial_flush_count: u32, - pub complete_flush_count: u32, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_transition_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_transition_at_ms: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_transition_summary: Option, - pub requested_streaming_features_summary: String, - pub finalized_streaming_features_summary: String, - pub zero_copy_d3d11: bool, - pub zero_copy_d3d12: bool, -} - -#[derive(Debug, Clone, Serialize)] -#[allow(dead_code)] -#[serde(tag = "type")] -pub enum Event { - #[serde(rename = "log")] - Log { - level: &'static str, - message: String, - }, - #[serde(rename = "status")] - Status { - status: &'static str, - #[serde(skip_serializing_if = "Option::is_none")] - message: Option, - }, - #[serde(rename = "local-ice")] - LocalIce { candidate: IceCandidatePayload }, - #[serde(rename = "input-ready")] - InputReady { - #[serde(rename = "protocolVersion")] - protocol_version: u16, - }, - #[serde(rename = "shortcut")] - Shortcut { - action: NativeStreamerShortcutAction, - }, - #[serde(rename = "clipboard-paste")] - ClipboardPaste, - #[serde(rename = "input-capture-changed")] - InputCaptureChanged { - captured: bool, - }, - #[serde(rename = "video-stall")] - VideoStall(VideoStallEvent), - #[serde(rename = "video-transition")] - VideoTransition { transition: VideoTransitionEvent }, - #[serde(rename = "stats")] - Stats { stats: NativeStatsEvent }, - #[serde(rename = "error")] - Error { code: String, message: String }, -} - -pub fn parse_command(value: Value) -> Result { - serde_json::from_value(value).map_err(|error| error.to_string()) -} - -pub fn missing_field(id: &str, field: &str) -> Response { - Response::Error { - id: Some(id.to_owned()), - code: "missing-field".to_owned(), - message: format!("Command is missing required field: {field}"), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn input_packet_prefers_base64_payload() { - let packet = NativeInputPacket { - payload: vec![1, 2, 3], - payload_base64: Some("BAUG".to_owned()), - partially_reliable: true, - }; - - assert_eq!( - packet.payload_bytes().expect("valid base64").as_ref(), - &[4, 5, 6] - ); - } - - #[test] - fn input_packet_keeps_legacy_byte_array_payload() { - let packet = NativeInputPacket { - payload: vec![7, 8, 9], - payload_base64: None, - partially_reliable: false, - }; - - assert_eq!( - packet.payload_bytes().expect("legacy payload").as_ref(), - &[7, 8, 9] - ); - } - - #[test] - fn video_stall_event_serializes_as_flat_native_event() { - let event = Event::VideoStall(VideoStallEvent { - stall_ms: 2_500, - encoded_kbps: 0.0, - decoded_fps: 0.0, - sink_fps: 0.0, - encoded_age_ms: Some(2_500), - decoded_age_ms: Some(2_500), - sink_age_ms: Some(2_500), - likely_stage: "video-output-stalled".to_owned(), - sink_rendered: Some(42), - sink_dropped: Some(1), - memory_mode: "D3D11Memory".to_owned(), - zero_copy: true, - requested_fps: Some(240), - caps_framerate: Some("60/1".to_owned()), - queue_mode: "adaptive".to_owned(), - partial_flush_count: 1, - complete_flush_count: 0, - last_transition_type: Some("high-fps-transition-risk".to_owned()), - last_transition_at_ms: Some(2_250), - requested_streaming_features_summary: "reflex=true, bitDepth=10".to_owned(), - finalized_streaming_features_summary: "reflex=true, bitDepth=8".to_owned(), - zero_copy_d3d11: true, - zero_copy_d3d12: false, - recovery_attempt: 1, - }); - let value = serde_json::to_value(event).expect("serializes"); - - assert_eq!(value["type"], "video-stall"); - assert_eq!(value["stallMs"], 2_500); - assert_eq!(value["encodedAgeMs"], 2_500); - assert_eq!(value["likelyStage"], "video-output-stalled"); - assert_eq!(value["sinkRendered"], 42); - assert_eq!(value["recoveryAttempt"], 1); - assert_eq!(value["queueMode"], "adaptive"); - assert_eq!(value["lastTransitionType"], "high-fps-transition-risk"); - } - - #[test] - fn video_transition_event_serializes_as_nested_transition_payload() { - let event = Event::VideoTransition { - transition: VideoTransitionEvent { - transition_type: "sink-caps-change".to_owned(), - source: "sink".to_owned(), - at_ms: 3_100, - old_caps: Some( - "video/x-raw(memory:D3D11Memory),framerate=(fraction)240/1".to_owned(), - ), - new_caps: Some( - "video/x-raw(memory:D3D11Memory),framerate=(fraction)60/1".to_owned(), - ), - old_framerate: Some("240/1".to_owned()), - new_framerate: Some("60/1".to_owned()), - old_memory_mode: Some("D3D11Memory".to_owned()), - new_memory_mode: Some("D3D11Memory".to_owned()), - render_gap_ms: Some(900), - requested_fps: Some(240), - caps_framerate: Some("60/1".to_owned()), - high_fps_risk: true, - queue_mode: "adaptive".to_owned(), - summary: "sink caps moved from 240/1 to 60/1 while 240 FPS was requested" - .to_owned(), - }, - }; - let value = serde_json::to_value(event).expect("serializes"); - - assert_eq!(value["type"], "video-transition"); - assert_eq!(value["transition"]["transitionType"], "sink-caps-change"); - assert_eq!(value["transition"]["highFpsRisk"], true); - } - - #[test] - fn nvst_video_session_deserializes_camel_case() { - let value = serde_json::json!({ - "session": { - "sessionId": "s1", - "serverIp": "1.2.3.4" - }, - "settings": { - "resolution": "1920x1080", - "fps": 60, - "maxBitrateMbps": 50, - "codec": "H265", - "colorQuality": "8bit_420", - "enableCloudGsync": false - }, - "shortcuts": {}, - "nvstVideo": { - "clientUdpPort": 49005, - "videoPeerIp": "10.0.0.1", - "videoPeerPort": 5004, - "srtpAesKeyHex": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", - "srtpKeyId": 2664076126u32, - "pingPayload": "token", - "codec": "H265" - } - }); - let ctx: NativeStreamerSessionContext = - serde_json::from_value(value).expect("deserialize"); - let nvst = ctx.nvst_video.expect("nvstVideo present"); - assert_eq!(nvst.client_udp_port, 49005); - assert_eq!(nvst.video_peer_port, 5004); - assert_eq!(nvst.srtp_key_id, 2664076126); - assert_eq!(nvst.ping_payload.as_deref(), Some("token")); - } -} diff --git a/native/opennow-streamer/src/sdp.rs b/native/opennow-streamer/src/sdp.rs deleted file mode 100644 index 5517d27bf..000000000 --- a/native/opennow-streamer/src/sdp.rs +++ /dev/null @@ -1,1416 +0,0 @@ -#![allow(dead_code)] - -use regex::Regex; -use std::collections::{HashMap, HashSet}; - -use crate::input::{PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL, PARTIALLY_RELIABLE_HID_DEVICE_MASK_ALL}; -use crate::protocol::{ColorQuality, VideoCodec}; - -// Match the official web client's 240 FPS profile. Disabling split encode at -// this frame rate can leave H265 streams smeared because the server/client -// repair and frame-state assumptions no longer line up. -// Official Nvsc dumps set adjustStreamingFpsDuringOutOfFocus:1 (matches TS builder). -const ENABLE_OUT_OF_FOCUS_FPS_ADJUSTMENT: bool = true; -const ENABLE_240_FPS_SPLIT_ENCODE: bool = true; -const ENABLE_DYNAMIC_SPLIT_ENCODE_UPDATES: bool = true; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IceCredentials { - pub ufrag: String, - pub pwd: String, - pub fingerprint: String, -} - -#[derive(Debug, Clone)] -pub struct NvstParams { - pub width: u32, - pub height: u32, - pub fps: u32, - pub max_bitrate_kbps: u32, - pub partial_reliable_threshold_ms: u32, - pub codec: VideoCodec, - pub color_quality: ColorQuality, - pub credentials: IceCredentials, - pub hid_device_mask: Option, - pub enable_partially_reliable_transfer_gamepad: Option, - pub enable_partially_reliable_transfer_hid: Option, -} - -#[derive(Debug, Clone, Copy)] -pub struct PreferCodecOptions { - pub prefer_hevc_profile_id: Option, -} - -pub fn parse_resolution(value: &str) -> Option<(u32, u32)> { - let (width, height) = value.split_once('x')?; - let width = width.parse().ok()?; - let height = height.parse().ok()?; - Some((width, height)) -} - -pub fn extract_public_ip(host_or_ip: &str) -> Option { - if host_or_ip.is_empty() { - return None; - } - - let ipv4 = Regex::new(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$").expect("valid regex"); - if ipv4.is_match(host_or_ip) { - return Some(host_or_ip.to_owned()); - } - - let first_label = host_or_ip.split('.').next().unwrap_or_default(); - let parts: Vec<&str> = first_label.split('-').collect(); - if parts.len() == 4 - && parts.iter().all(|part| { - !part.is_empty() && part.len() <= 3 && part.as_bytes().iter().all(u8::is_ascii_digit) - }) - { - return Some(parts.join(".")); - } - - None -} - -pub fn fix_server_ip(sdp: &str, server_ip: &str) -> String { - let Some(ip) = extract_public_ip(server_ip) else { - return sdp.to_owned(); - }; - - let fixed = sdp.replace("c=IN IP4 0.0.0.0", &format!("c=IN IP4 {ip}")); - let candidate_re = - Regex::new(r"(a=candidate:\S+\s+\d+\s+\w+\s+\d+\s+)0\.0\.0\.0(\s+)").expect("valid regex"); - candidate_re - .replace_all(&fixed, format!("${{1}}{ip}${{2}}")) - .into_owned() -} - -pub fn rewrite_ice_candidate_endpoint( - candidate: &str, - ip: &str, - port: u16, -) -> (String, bool) { - let ip = ip.trim(); - if ip.is_empty() || port == 0 { - return (candidate.to_owned(), false); - } - - let mut parts: Vec = candidate.split_whitespace().map(ToOwned::to_owned).collect(); - if parts.len() < 6 - || !(parts[0].starts_with("candidate:") || parts[0].starts_with("a=candidate:")) - { - return (candidate.to_owned(), false); - } - - if parts[4] == ip && parts[5] == port.to_string() { - return (candidate.to_owned(), false); - } - - parts[4] = ip.to_owned(); - parts[5] = port.to_string(); - let rewritten = parts.join(" "); - (rewritten, true) -} - -pub fn rewrite_sdp_ice_candidate_endpoints( - sdp: &str, - ip: &str, - port: u16, -) -> (String, usize) { - let ending = line_ending(sdp); - let mut replacements = 0usize; - let lines = split_lines_lossless(sdp) - .into_iter() - .map(|line| { - if !line.starts_with("a=candidate:") { - return line.to_owned(); - } - let (rewritten, changed) = rewrite_ice_candidate_endpoint(line, ip, port); - if changed { - replacements += 1; - } - rewritten - }) - .collect::>(); - - (lines.join(ending), replacements) -} - -pub fn duplicate_session_webrtc_attributes_to_media(sdp: &str) -> String { - let ending = line_ending(sdp); - let lines = split_lines_lossless(sdp); - let first_media_index = lines.iter().position(|line| line.starts_with("m=")); - let Some(first_media_index) = first_media_index else { - return sdp.to_owned(); - }; - - let session_attributes: Vec<&str> = lines[..first_media_index] - .iter() - .copied() - .filter(|line| { - line.starts_with("a=ice-ufrag:") - || line.starts_with("a=ice-pwd:") - || line.starts_with("a=ice-options:") - || line.starts_with("a=fingerprint:") - || line.starts_with("a=setup:") - }) - .collect(); - - if session_attributes.is_empty() { - return sdp.to_owned(); - } - - let mut output: Vec = lines[..first_media_index] - .iter() - .filter(|line| !is_media_transport_attribute(line)) - .map(|line| (*line).to_owned()) - .collect(); - - let mut index = first_media_index; - while index < lines.len() { - let section_start = index; - index += 1; - while index < lines.len() && !lines[index].starts_with("m=") { - index += 1; - } - let section = &lines[section_start..index]; - let insert_index = section - .iter() - .position(|line| line.starts_with("a=")) - .unwrap_or(section.len()); - - for line in §ion[..insert_index] { - output.push((*line).to_owned()); - } - for attribute in &session_attributes { - let prefix = attribute - .split_once(':') - .map(|(prefix, _)| format!("{prefix}:")) - .unwrap_or_else(|| (*attribute).to_owned()); - if !section.iter().any(|line| line.starts_with(&prefix)) { - output.push((*attribute).to_owned()); - } - } - for line in §ion[insert_index..] { - output.push((*line).to_owned()); - } - } - - output.join(ending) -} - -pub fn summarize_media_transport_attributes(sdp: &str) -> String { - let lines = split_lines_lossless(sdp); - let session_has_fingerprint = lines - .iter() - .take_while(|line| !line.starts_with("m=")) - .any(|line| line.starts_with("a=fingerprint:")); - - let mut media_count = 0usize; - let mut fingerprint_count = 0usize; - let mut setup_count = 0usize; - let mut ice_ufrag_count = 0usize; - let mut ice_pwd_count = 0usize; - - let mut index = 0usize; - while index < lines.len() { - if !lines[index].starts_with("m=") { - index += 1; - continue; - } - - media_count += 1; - index += 1; - let mut has_fingerprint = false; - let mut has_setup = false; - let mut has_ice_ufrag = false; - let mut has_ice_pwd = false; - while index < lines.len() && !lines[index].starts_with("m=") { - has_fingerprint |= lines[index].starts_with("a=fingerprint:"); - has_setup |= lines[index].starts_with("a=setup:"); - has_ice_ufrag |= lines[index].starts_with("a=ice-ufrag:"); - has_ice_pwd |= lines[index].starts_with("a=ice-pwd:"); - index += 1; - } - - fingerprint_count += usize::from(has_fingerprint); - setup_count += usize::from(has_setup); - ice_ufrag_count += usize::from(has_ice_ufrag); - ice_pwd_count += usize::from(has_ice_pwd); - } - - format!( - "mediaSections={media_count}, mediaFingerprints={fingerprint_count}, mediaSetup={setup_count}, mediaIceUfrag={ice_ufrag_count}, mediaIcePwd={ice_pwd_count}, sessionFingerprint={session_has_fingerprint}" - ) -} - -pub fn sanitize_ice_pwd_for_gstreamer(sdp: &str) -> (String, usize) { - let ending = line_ending(sdp); - let mut replacements = 0usize; - let lines = split_lines_lossless(sdp) - .into_iter() - .map(|line| { - let Some(value) = line.strip_prefix("a=ice-pwd:") else { - return line.to_owned(); - }; - - let sanitized = sanitize_ice_pwd_value(value); - if sanitized == value { - return line.to_owned(); - } - - replacements += 1; - format!("a=ice-pwd:{sanitized}") - }) - .collect::>(); - - (lines.join(ending), replacements) -} - -fn sanitize_ice_pwd_value(value: &str) -> String { - value - .chars() - .filter(|character| { - character.is_ascii_alphanumeric() || *character == '+' || *character == '/' - }) - .collect() -} - -fn is_media_transport_attribute(line: &str) -> bool { - line.starts_with("a=ice-ufrag:") - || line.starts_with("a=ice-pwd:") - || line.starts_with("a=fingerprint:") - || line.starts_with("a=setup:") -} - -pub fn extract_ice_ufrag_from_offer(sdp: &str) -> String { - sdp.lines() - .find_map(|line| line.strip_prefix("a=ice-ufrag:")) - .map(str::trim) - .unwrap_or_default() - .to_owned() -} - -pub fn extract_ice_credentials(sdp: &str) -> IceCredentials { - let mut ufrag = String::new(); - let mut pwd = String::new(); - let mut fingerprint = String::new(); - - for line in sdp.lines() { - if ufrag.is_empty() { - if let Some(value) = line.strip_prefix("a=ice-ufrag:") { - ufrag = value.trim().to_owned(); - continue; - } - } - if pwd.is_empty() { - if let Some(value) = line.strip_prefix("a=ice-pwd:") { - pwd = value.trim().to_owned(); - continue; - } - } - if fingerprint.is_empty() { - if let Some(value) = extract_fingerprint_value(line) { - fingerprint = value.to_owned(); - } - } - - if !ufrag.is_empty() && !pwd.is_empty() && !fingerprint.is_empty() { - break; - } - } - - IceCredentials { - ufrag, - pwd, - fingerprint, - } -} - -fn extract_fingerprint_value(line: &str) -> Option<&str> { - let value = line.strip_prefix("a=fingerprint:")?; - let (_, fingerprint) = value.trim().split_once(' ')?; - let fingerprint = fingerprint.trim(); - if fingerprint.is_empty() { - None - } else { - Some(fingerprint) - } -} - -pub fn build_nvst_sdp_for_answer(params: &NvstParams, answer_sdp: &str) -> Result { - let credentials = extract_ice_credentials(answer_sdp); - if credentials.ufrag.is_empty() - || credentials.pwd.is_empty() - || credentials.fingerprint.is_empty() - { - return Err( - "Local answer SDP is missing ICE ufrag, ICE password, or DTLS fingerprint.".to_owned(), - ); - } - - let mut answer_params = params.clone(); - answer_params.credentials = credentials; - if let Some(codec) = extract_negotiated_video_codec(answer_sdp) { - answer_params.codec = codec; - } - Ok(build_nvst_sdp(&answer_params)) -} - -pub fn extract_negotiated_video_codec(sdp: &str) -> Option { - let lines = split_lines_lossless(sdp); - let mut in_video_section = false; - let mut video_payloads = Vec::new(); - let mut codec_by_payload_type: HashMap = HashMap::new(); - - for line in &lines { - if line.starts_with("m=video") { - in_video_section = true; - video_payloads = line.split_whitespace().skip(3).map(str::to_owned).collect(); - continue; - } - if line.starts_with("m=") && in_video_section { - in_video_section = false; - } - if !in_video_section || !line.starts_with("a=rtpmap:") { - continue; - } - - let rest = line.strip_prefix("a=rtpmap:").unwrap_or_default(); - let mut parts = rest.split_whitespace(); - let Some(pt) = parts.next() else { - continue; - }; - let Some(codec_part) = parts.next() else { - continue; - }; - let codec_name = normalize_codec(codec_part.split('/').next().unwrap_or_default()); - if !codec_name.is_empty() { - codec_by_payload_type.insert(pt.to_owned(), codec_name); - } - } - - video_payloads - .iter() - .filter_map(|pt| codec_by_payload_type.get(pt)) - .find_map(|codec| match codec.as_str() { - "H264" => Some(VideoCodec::H264), - "H265" => Some(VideoCodec::H265), - "AV1" => Some(VideoCodec::AV1), - _ => None, - }) -} - -fn line_ending(sdp: &str) -> &'static str { - if sdp.contains("\r\n") { - "\r\n" - } else { - "\n" - } -} - -fn normalize_codec(name: &str) -> String { - let upper = name.to_ascii_uppercase(); - if upper == "HEVC" { - "H265".to_owned() - } else { - upper - } -} - -fn split_lines_lossless(sdp: &str) -> Vec<&str> { - sdp.split(['\r', '\n']) - .filter(|line| !line.is_empty()) - .collect() -} - -pub fn prefer_codec(sdp: &str, codec: VideoCodec, options: PreferCodecOptions) -> String { - let ending = line_ending(sdp); - let lines = split_lines_lossless(sdp); - let target_codec = codec.as_str(); - let mut in_video_section = false; - let mut payload_types_by_codec: HashMap> = HashMap::new(); - let mut codec_by_payload_type: HashMap = HashMap::new(); - let mut rtx_apt_by_payload_type: HashMap = HashMap::new(); - let mut fmtp_by_payload_type: HashMap = HashMap::new(); - - for line in &lines { - if line.starts_with("m=video") { - in_video_section = true; - continue; - } - if line.starts_with("m=") && in_video_section { - in_video_section = false; - } - if !in_video_section || !line.starts_with("a=rtpmap:") { - continue; - } - - let rest = line.strip_prefix("a=rtpmap:").unwrap_or_default(); - let mut parts = rest.split_whitespace(); - let Some(pt) = parts.next() else { - continue; - }; - let Some(codec_part) = parts.next() else { - continue; - }; - let codec_name = normalize_codec(codec_part.split('/').next().unwrap_or_default()); - if codec_name.is_empty() { - continue; - } - payload_types_by_codec - .entry(codec_name.clone()) - .or_default() - .push(pt.to_owned()); - codec_by_payload_type.insert(pt.to_owned(), codec_name); - } - - in_video_section = false; - let apt_re = Regex::new(r"(?i)(?:^|;)\s*apt=(\d+)").expect("valid regex"); - for line in &lines { - if line.starts_with("m=video") { - in_video_section = true; - continue; - } - if line.starts_with("m=") && in_video_section { - in_video_section = false; - } - if !in_video_section || !line.starts_with("a=fmtp:") { - continue; - } - - let rest = line - .split_once(':') - .map(|(_, rest)| rest) - .unwrap_or_default(); - let mut parts = rest.splitn(2, char::is_whitespace); - let Some(pt) = parts.next() else { - continue; - }; - let params = parts.next().unwrap_or_default().trim(); - if pt.is_empty() || params.is_empty() { - continue; - } - - if let Some(captures) = apt_re.captures(params) { - if let Some(apt) = captures.get(1) { - rtx_apt_by_payload_type.insert(pt.to_owned(), apt.as_str().to_owned()); - } - } - fmtp_by_payload_type.insert(pt.to_owned(), params.to_owned()); - } - - let Some(preferred_payloads) = payload_types_by_codec.get(target_codec) else { - return sdp.to_owned(); - }; - if preferred_payloads.is_empty() { - return sdp.to_owned(); - } - - let mut ordered_preferred_payloads = preferred_payloads.clone(); - if codec == VideoCodec::H265 { - if let Some(preferred_profile) = options.prefer_hevc_profile_id { - ordered_preferred_payloads.sort_by_key(|pt| { - let fmtp = fmtp_by_payload_type - .get(pt) - .map(String::as_str) - .unwrap_or_default(); - let profile = capture_numeric_param(fmtp, "profile-id"); - if profile == Some(preferred_profile as u32) { - 0 - } else if profile.is_none() { - 1 - } else { - 2 - } - }); - } - } - - let preferred: HashSet = ordered_preferred_payloads.iter().cloned().collect(); - let mut allowed = preferred.clone(); - - for (rtx_pt, apt) in rtx_apt_by_payload_type { - if preferred.contains(&apt) - && codec_by_payload_type - .get(&rtx_pt) - .is_some_and(|name| name == "RTX") - { - allowed.insert(rtx_pt); - } - } - - for (pt, codec_name) in &codec_by_payload_type { - if matches!(codec_name.as_str(), "FLEXFEC-03") { - allowed.insert(pt.clone()); - } - } - - let mut filtered = Vec::new(); - in_video_section = false; - - for line in lines { - if line.starts_with("m=video") { - in_video_section = true; - let parts: Vec<&str> = line.split_whitespace().collect(); - let header = &parts[..parts.len().min(3)]; - let available: Vec<&str> = parts - .iter() - .skip(3) - .copied() - .filter(|pt| allowed.contains(*pt)) - .collect(); - let mut ordered = Vec::new(); - - for pt in &ordered_preferred_payloads { - if available.contains(&pt.as_str()) { - ordered.push(pt.as_str()); - } - } - for pt in available { - if !preferred.contains(pt) { - ordered.push(pt); - } - } - - if ordered.is_empty() { - filtered.push(line.to_owned()); - } else { - filtered.push( - header - .iter() - .chain(ordered.iter()) - .copied() - .collect::>() - .join(" "), - ); - } - continue; - } - - if line.starts_with("m=") && in_video_section { - in_video_section = false; - } - - if in_video_section - && (line.starts_with("a=rtpmap:") - || line.starts_with("a=fmtp:") - || line.starts_with("a=rtcp-fb:")) - { - let rest = line - .split_once(':') - .map(|(_, rest)| rest) - .unwrap_or_default(); - let pt = rest.split_whitespace().next().unwrap_or_default(); - if !pt.is_empty() && !allowed.contains(pt) { - continue; - } - } - - filtered.push(line.to_owned()); - } - - filtered.join(ending) -} - -fn capture_numeric_param(params: &str, key: &str) -> Option { - for part in params.split(';') { - let trimmed = part.trim(); - let Some((candidate_key, value)) = trimmed.split_once('=') else { - continue; - }; - if candidate_key.eq_ignore_ascii_case(key) { - return value.trim().parse().ok(); - } - } - None -} - -pub fn munge_answer_sdp(sdp: &str, max_bitrate_kbps: u32) -> String { - let ending = line_ending(sdp); - let lines = split_lines_lossless(sdp); - let mut result = Vec::new(); - - for (index, line) in lines.iter().enumerate() { - let mut current = (*line).to_owned(); - if current.starts_with("a=fmtp:") - && current.contains("minptime=") - && !current.contains("stereo=1") - { - current.push_str(";stereo=1"); - } - result.push(current); - - if line.starts_with("m=video") || line.starts_with("m=audio") { - let bitrate = if line.starts_with("m=video") { - max_bitrate_kbps - } else { - 128 - }; - let next_line = lines.get(index + 1).copied().unwrap_or_default(); - if !next_line.starts_with("b=") { - result.push(format!("b=AS:{bitrate}")); - } - } - } - - result.join(ending) -} - -pub fn build_nvst_sdp(params: &NvstParams) -> String { - // Align bitrate floor/startup with the TS WebRTC companion builder - // (official web client uses a 4 Mbps floor and ~max/4 startup). - const OFFICIAL_MIN_BITRATE_KBPS: u32 = 4000; - let max_bitrate = params.max_bitrate_kbps.max(OFFICIAL_MIN_BITRATE_KBPS); - let min_bitrate = OFFICIAL_MIN_BITRATE_KBPS; - let initial_bitrate = OFFICIAL_MIN_BITRATE_KBPS.max(max_bitrate / 4); - let is_high_fps = params.fps > 60; - let is_at_least_120_fps = params.fps >= 120; - let is_90_fps = params.fps == 90; - let is_120_fps = params.fps == 120; - let is_240_fps = params.fps == 240; - let is_av1 = params.codec == VideoCodec::AV1; - let bit_depth = params.color_quality.bit_depth(); - let hid_device_mask = params - .hid_device_mask - .unwrap_or(PARTIALLY_RELIABLE_HID_DEVICE_MASK_ALL); - let enable_partially_reliable_transfer_gamepad = params - .enable_partially_reliable_transfer_gamepad - .unwrap_or(PARTIALLY_RELIABLE_GAMEPAD_MASK_ALL); - let enable_partially_reliable_transfer_hid = params - .enable_partially_reliable_transfer_hid - .unwrap_or(hid_device_mask); - let min_target_frame_time_us = (1_000_000u32.saturating_mul(95) - / params.fps.max(1).saturating_mul(100)) - .max(1000); - - let mut lines = vec![ - "v=0".to_owned(), - "o=SdpTest test_id_13 14 IN IPv4 127.0.0.1".to_owned(), - "s=-".to_owned(), - "t=0 0".to_owned(), - format!("a=general.icePassword:{}", params.credentials.pwd), - format!("a=general.iceUserNameFragment:{}", params.credentials.ufrag), - format!( - "a=general.dtlsFingerprint:{}", - params.credentials.fingerprint - ), - "m=video 0 RTP/AVP".to_owned(), - "a=msid:fbc-video-0".to_owned(), - "a=vqos.fec.rateDropWindow:10".to_owned(), - "a=vqos.fec.minRequiredFecPackets:2".to_owned(), - "a=vqos.fec.repairMinPercent:5".to_owned(), - "a=vqos.fec.repairPercent:5".to_owned(), - "a=vqos.fec.repairMaxPercent:35".to_owned(), - "a=vqos.bllFec.enable:0".to_owned(), - "a=vqos.dynamicStreamingMode:0".to_owned(), - "a=vqos.drc.enable:0".to_owned(), - "a=vqos.calculateAvgVideoStreamingBitrate:1".to_owned(), - "a=video.dx9EnableNv12:1".to_owned(), - "a=video.dx9EnableHdr:1".to_owned(), - "a=vqos.qpg.enable:1".to_owned(), - "a=vqos.resControl.qp.qpg.featureSetting:7".to_owned(), - "a=video.adaptiveQuantization.spatialAQSetting:7".to_owned(), - "a=video.adaptiveQuantization.temporalAQSetting:0".to_owned(), - "a=video.adaptiveQuantization.spatialAQStrength:12".to_owned(), - "a=video.adaptiveQuantization.qpThresholdAdjPercent:2".to_owned(), - "a=video.adaptiveQuantization.saqAdaptMinQpThresholdPercent:40".to_owned(), - "a=video.adaptiveQuantization.saqAdaptMaxQpThresholdPercent:100".to_owned(), - "a=video.adaptiveQuantization.saqAdaptDecayStrengthX100:250".to_owned(), - "a=video.adaptiveQuantization.perfAdjEnablement:1".to_owned(), - "a=video.framePacing.mode:2".to_owned(), - format!("a=video.framePacing.pid.minTargetFrameTimeUs:{min_target_frame_time_us}"), - "a=bwe.useOwdCongestionControl:1".to_owned(), - "a=video.enableRtpNack:1".to_owned(), - "a=vqos.bw.txRxLag.minFeedbackTxDeltaMs:200".to_owned(), - "a=vqos.drc.bitrateIirFilterFactor:18".to_owned(), - "a=video.packetSize:1140".to_owned(), - "a=packetPacing.version:3".to_owned(), - "a=packetPacing.mode:1".to_owned(), - "a=packetPacing.minNumPacketsPerGroup:15".to_owned(), - "a=packetPacing.enableAccurateSleep:1".to_owned(), - "a=packetPacing.enableSmoothTransition:1".to_owned(), - "a=packetPacing.allowFpsBasedToggle:1".to_owned(), - "a=vqos.relaxMaxBitrate.overrideAvgBitrateThresholdPercent:4".to_owned(), - "a=vqos.relaxMaxBitrate.customAvgBitrateThresholdPercent:65".to_owned(), - "a=vqos.relaxMaxBitrate.overrideAvgQpThresholdPercent:7".to_owned(), - "a=vqos.relaxMaxBitrate.customAvgQpThresholdPercent:51".to_owned(), - "a=vqos.relaxMaxBitrate.iirFilterFactor:120".to_owned(), - "a=vqos.qpDelta.qpDeltaMaxPercent:10".to_owned(), - "a=vqos.qpDelta.qpDeltaSurfaceAdjustmentStrengthPercent:70".to_owned(), - "a=vqos.qpDelta.qpDeltaVbvUsageFactorPercentH264:100".to_owned(), - "a=vqos.qpDelta.qpDeltaVbvUsageFactorPercentH265:100".to_owned(), - "a=vqos.qpDelta.qpDeltaVbvUsageFactorPercentAv1:100".to_owned(), - "a=vqos.qpDelta.qpDeltaMinPercent:60".to_owned(), - "a=vqos.qpDelta.qpDeltaIirFactor:60".to_owned(), - "a=vqos.qpDelta.qpDeltaThrottlePercent:100".to_owned(), - ]; - - if is_high_fps { - lines.extend([ - "a=vqos.dfc.enable:1".to_owned(), - "a=vqos.dfc.decodeFpsAdjPercent:85".to_owned(), - "a=vqos.dfc.targetDownCooldownMs:250".to_owned(), - format!( - "a=vqos.dfc.dfcAlgoVersion:{}", - if is_at_least_120_fps { 2 } else { 1 } - ), - format!( - "a=vqos.dfc.minTargetFps:{}", - if is_at_least_120_fps { 100 } else { 60 } - ), - "a=vqos.resControl.dfc.useClientFpsPerf:0".to_owned(), - "a=vqos.dfc.adjustResAndFps:0".to_owned(), - ]); - lines.extend([ - "a=bwe.iirFilterFactor:8".to_owned(), - "a=video.encoderFeatureSetting:47".to_owned(), - "a=video.encoderPreset:6".to_owned(), - ]); - let fps_specific_capture_tuning = if is_90_fps { - Some((9, 11)) - } else if is_120_fps { - Some((6, 9)) - } else if is_240_fps { - Some((18, 9)) - } else { - None - }; - if let Some((grab_timeout_ms, decode_threshold_ms)) = fps_specific_capture_tuning { - lines.push(format!( - "a=video.fbcDynamicFpsGrabTimeoutMs:{grab_timeout_ms}" - )); - lines.push(format!( - "a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:{decode_threshold_ms}" - )); - } - } else { - lines.extend([ - "a=vqos.dfc.enable:0".to_owned(), - "a=vqos.dfc.adjustResAndFps:0".to_owned(), - ]); - } - - if is_240_fps { - lines.extend([ - "a=video.enableNextCaptureMode:1".to_owned(), - "a=vqos.maxStreamFpsEstimate:240".to_owned(), - ]); - if ENABLE_240_FPS_SPLIT_ENCODE { - let strips_per_frame = - if is_av1 && params.width.saturating_mul(params.height) >= 2_764_800 { - 63 - } else { - 3 - }; - lines.push(format!( - "a=video.videoSplitEncodeStripsPerFrame:{strips_per_frame}" - )); - lines.push(format!( - "a=video.updateSplitEncodeStateDynamically:{}", - if ENABLE_DYNAMIC_SPLIT_ENCODE_UPDATES { - 1 - } else { - 0 - } - )); - lines.push("a=vqos.rtcPreemptiveIdrSettings.minBurstNackSize:65535".to_owned()); - lines - .push("a=vqos.rtcPreemptiveIdrSettings.minNackPacketCaptureAgeMs:65535".to_owned()); - } - } - - lines.extend([ - format!( - "a=vqos.adjustStreamingFpsDuringOutOfFocus:{}", - if ENABLE_OUT_OF_FOCUS_FPS_ADJUSTMENT { - 1 - } else { - 0 - } - ), - "a=vqos.resControl.cpmRtc.ignoreOutOfFocusWindowState:1".to_owned(), - "a=vqos.resControl.perfHistory.rtcIgnoreOutOfFocusWindowState:1".to_owned(), - "a=vqos.resControl.cpmRtc.featureMask:0".to_owned(), - "a=vqos.resControl.cpmRtc.enable:0".to_owned(), - "a=vqos.resControl.cpmRtc.minResolutionPercent:100".to_owned(), - "a=vqos.resControl.cpmRtc.resolutionChangeHoldonMs:999999".to_owned(), - format!( - "a=packetPacing.numGroups:{}", - if is_120_fps { 3 } else { 5 } - ), - "a=packetPacing.maxDelayUs:1000".to_owned(), - "a=packetPacing.minNumPacketsFrame:10".to_owned(), - "a=video.rtpNackQueueLength:1024".to_owned(), - "a=video.rtpNackQueueMaxPackets:512".to_owned(), - "a=video.rtpNackMaxPacketCount:25".to_owned(), - "a=vqos.drc.qpMaxResThresholdAdj:4".to_owned(), - "a=vqos.grc.qpMaxResThresholdAdj:4".to_owned(), - "a=vqos.drc.iirFilterFactor:100".to_owned(), - ]); - - if is_av1 { - lines.extend([ - "a=vqos.drc.minQpHeadroom:20".to_owned(), - "a=vqos.drc.lowerQpThreshold:100".to_owned(), - "a=vqos.drc.upperQpThreshold:200".to_owned(), - "a=vqos.drc.minAdaptiveQpThreshold:180".to_owned(), - "a=vqos.drc.qpCodecThresholdAdj:0".to_owned(), - "a=vqos.drc.qpMaxResThresholdAdj:20".to_owned(), - "a=vqos.dfc.minQpHeadroom:20".to_owned(), - "a=vqos.dfc.qpLowerLimit:100".to_owned(), - "a=vqos.dfc.qpMaxUpperLimit:200".to_owned(), - "a=vqos.dfc.qpMinUpperLimit:180".to_owned(), - "a=vqos.dfc.qpMaxResThresholdAdj:20".to_owned(), - "a=vqos.dfc.qpCodecThresholdAdj:0".to_owned(), - "a=vqos.grc.minQpHeadroom:20".to_owned(), - "a=vqos.grc.lowerQpThreshold:100".to_owned(), - "a=vqos.grc.upperQpThreshold:200".to_owned(), - "a=vqos.grc.minAdaptiveQpThreshold:180".to_owned(), - "a=vqos.grc.qpMaxResThresholdAdj:20".to_owned(), - "a=vqos.grc.qpCodecThresholdAdj:0".to_owned(), - "a=video.minQp:25".to_owned(), - "a=video.enableAv1RcPrecisionFactor:1".to_owned(), - ]); - } - - lines.extend([ - format!("a=video.clientViewportWd:{}", params.width), - format!("a=video.clientViewportHt:{}", params.height), - format!("a=video.maxFPS:{}", params.fps), - format!("a=video.initialBitrateKbps:{initial_bitrate}"), - format!("a=video.initialPeakBitrateKbps:{initial_bitrate}"), - format!("a=vqos.bw.maximumBitrateKbps:{max_bitrate}"), - format!("a=vqos.bw.minimumBitrateKbps:{min_bitrate}"), - format!("a=vqos.bw.peakBitrateKbps:{max_bitrate}"), - format!("a=vqos.bw.serverPeakBitrateKbps:{max_bitrate}"), - "a=vqos.bw.enableBandwidthEstimation:1".to_owned(), - "a=vqos.bw.disableBitrateLimit:0".to_owned(), - format!("a=vqos.grc.maximumBitrateKbps:{max_bitrate}"), - "a=vqos.grc.enable:0".to_owned(), - "a=video.maxNumReferenceFrames:4".to_owned(), - "a=video.mapRtpTimestampsToFrames:1".to_owned(), - "a=video.encoderCscMode:3".to_owned(), - "a=video.dynamicRangeMode:0".to_owned(), - format!("a=video.bitDepth:{bit_depth}"), - format!("a=video.scalingFeature1:{}", if is_av1 { 1 } else { 0 }), - "a=video.prefilterParams.prefilterModel:0".to_owned(), - "m=audio 0 RTP/AVP".to_owned(), - "a=msid:audio".to_owned(), - "a=aqos.enableRedundancy:1".to_owned(), - "a=aqos.redundancyLevel:2".to_owned(), - "a=aqos.enableRedundancyForMic:1".to_owned(), - "a=aqos.redundancyLevelForMic:3".to_owned(), - "a=audio.enableDynamicAudioConfig:1".to_owned(), - "a=audio.enableTimestampAudioBuffer:1".to_owned(), - "m=mic 0 RTP/AVP".to_owned(), - "a=msid:mic".to_owned(), - "a=rtpmap:0 PCMU/8000".to_owned(), - "m=application 0 RTP/AVP".to_owned(), - "a=msid:input_1".to_owned(), - format!( - "a=ri.partialReliableThresholdMs:{}", - params.partial_reliable_threshold_ms - ), - format!("a=ri.hidDeviceMask:{hid_device_mask}"), - format!( - "a=ri.enablePartiallyReliableTransferGamepad:{enable_partially_reliable_transfer_gamepad}" - ), - format!( - "a=ri.enablePartiallyReliableTransferHid:{enable_partially_reliable_transfer_hid}" - ), - "a=ri.timestampsEnabled:1".to_owned(), - "a=ri.useMultipleGamepads:1".to_owned(), - String::new(), - ]); - - lines.join("\n") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn extracts_public_ip_from_host_or_ip() { - assert_eq!( - extract_public_ip("80-250-97-40.cloudmatchbeta.nvidiagrid.net").as_deref(), - Some("80.250.97.40"), - ); - assert_eq!( - extract_public_ip("161.248.11.132").as_deref(), - Some("161.248.11.132") - ); - assert_eq!(extract_public_ip("not-an-ip.example.com"), None); - } - - #[test] - fn fixes_connection_and_candidate_ips() { - let offer = "v=0\nc=IN IP4 0.0.0.0\na=candidate:1 1 udp 1 0.0.0.0 49000 typ host\n"; - let fixed = fix_server_ip(offer, "80-250-97-40.cloudmatchbeta.nvidiagrid.net"); - assert!(fixed.contains("c=IN IP4 80.250.97.40")); - assert!(fixed.contains("a=candidate:1 1 udp 1 80.250.97.40 49000 typ host")); - } - - #[test] - fn rewrites_sdp_ice_candidate_endpoints() { - let offer = [ - "v=0", - "c=IN IP4 0.0.0.0", - "a=candidate:1 1 udp 2122260223 203.0.113.10 47998 typ host", - "a=candidate:2 1 tcp 1518214911 203.0.113.10 9 typ host tcptype active", - ] - .join("\r\n"); - - let (rewritten, replacements) = - rewrite_sdp_ice_candidate_endpoints(&offer, "198.51.100.55", 18784); - - assert_eq!(replacements, 2); - assert!(rewritten - .contains("a=candidate:1 1 udp 2122260223 198.51.100.55 18784 typ host")); - assert!(rewritten.contains( - "a=candidate:2 1 tcp 1518214911 198.51.100.55 18784 typ host tcptype active" - )); - assert!(rewritten.contains("c=IN IP4 0.0.0.0")); - assert!(rewritten.contains("\r\n")); - } - - #[test] - fn rewrites_trickled_ice_candidate_endpoint() { - let candidate = "candidate:1 1 udp 2122260223 203.0.113.10 47998 typ host"; - - let (rewritten, changed) = - rewrite_ice_candidate_endpoint(candidate, "198.51.100.55", 18784); - - assert!(changed); - assert_eq!( - rewritten, - "candidate:1 1 udp 2122260223 198.51.100.55 18784 typ host" - ); - } - - #[test] - fn duplicates_session_webrtc_attributes_into_media_sections() { - let offer = [ - "v=0", - "a=ice-options:trickle", - "a=ice-ufrag:user", - "a=ice-pwd:pass", - "a=fingerprint:sha-256 AA:BB", - "a=setup:actpass", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "c=IN IP4 10.0.0.1", - "a=mid:0", - "m=video 9 UDP/TLS/RTP/SAVPF 96", - "c=IN IP4 10.0.0.1", - "a=mid:1", - ] - .join("\n"); - - let normalized = duplicate_session_webrtc_attributes_to_media(&offer); - let session_part = normalized.split("\nm=").next().expect("session section"); - let media_sections = normalized - .split("\nm=") - .skip(1) - .map(|section| format!("m={section}")) - .collect::>(); - - assert_eq!(media_sections.len(), 2); - for section in media_sections { - assert!(section.contains("a=ice-options:trickle")); - assert!(section.contains("a=ice-ufrag:user")); - assert!(section.contains("a=ice-pwd:pass")); - assert!(section.contains("a=fingerprint:sha-256 AA:BB")); - assert!(section.contains("a=setup:actpass")); - } - assert!(!session_part.contains("a=ice-ufrag:user")); - assert!(!session_part.contains("a=ice-pwd:pass")); - assert!(!session_part.contains("a=fingerprint:sha-256 AA:BB")); - assert!(!session_part.contains("a=setup:actpass")); - } - - #[test] - fn keeps_existing_media_webrtc_attributes() { - let offer = [ - "v=0", - "a=ice-ufrag:session", - "a=ice-pwd:session-pass", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=ice-ufrag:media", - "a=ice-pwd:media-pass", - "a=mid:0", - ] - .join("\n"); - - let normalized = duplicate_session_webrtc_attributes_to_media(&offer); - - assert!(normalized.contains("a=ice-ufrag:media")); - assert!(normalized.contains("a=ice-pwd:media-pass")); - assert_eq!(normalized.matches("a=ice-ufrag:session").count(), 0); - assert_eq!(normalized.matches("a=ice-pwd:session-pass").count(), 0); - } - - #[test] - fn summarizes_media_transport_attributes() { - let offer = [ - "v=0", - "a=group:BUNDLE 0 1", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=fingerprint:sha-256 AA:BB", - "a=setup:actpass", - "a=ice-ufrag:user", - "a=ice-pwd:pass", - "m=video 9 UDP/TLS/RTP/SAVPF 96", - "a=setup:actpass", - ] - .join("\n"); - - assert_eq!( - summarize_media_transport_attributes(&offer), - "mediaSections=2, mediaFingerprints=1, mediaSetup=2, mediaIceUfrag=1, mediaIcePwd=1, sessionFingerprint=false" - ); - } - - #[test] - fn sanitizes_nonstandard_ice_password_for_gstreamer() { - let sdp = [ - "v=0", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=ice-pwd:48ca4c4b-199a-454c-b58a-3d14739335a3", - "m=video 9 UDP/TLS/RTP/SAVPF 96", - "a=ice-pwd:alreadyValidPassword123456", - ] - .join("\n"); - - let (sanitized, replacements) = sanitize_ice_pwd_for_gstreamer(&sdp); - - assert_eq!(replacements, 1); - assert!(sanitized.contains("a=ice-pwd:48ca4c4b199a454cb58a3d14739335a3")); - assert!(sanitized.contains("a=ice-pwd:alreadyValidPassword123456")); - } - - #[test] - fn extracts_ice_credentials() { - let sdp = "a=ice-ufrag:user\r\na=ice-pwd:pass\r\na=fingerprint:sha-256 AA:BB\r\na=ice-ufrag:other\r\na=ice-pwd:other-pass\r\na=fingerprint:sha-256 CC:DD\r\n"; - assert_eq!(extract_ice_ufrag_from_offer(sdp), "user"); - assert_eq!( - extract_ice_credentials(sdp), - IceCredentials { - ufrag: "user".to_owned(), - pwd: "pass".to_owned(), - fingerprint: "AA:BB".to_owned(), - }, - ); - } - - #[test] - fn builds_nvst_sdp_with_local_answer_credentials() { - let params = NvstParams { - width: 1920, - height: 1080, - fps: 60, - max_bitrate_kbps: 75_000, - partial_reliable_threshold_ms: 16, - codec: VideoCodec::H265, - color_quality: ColorQuality::EightBit420, - credentials: IceCredentials { - ufrag: "remote-user".to_owned(), - pwd: "remote-password".to_owned(), - fingerprint: "AA:BB".to_owned(), - }, - hid_device_mask: None, - enable_partially_reliable_transfer_gamepad: None, - enable_partially_reliable_transfer_hid: None, - }; - let answer_sdp = [ - "v=0", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=ice-ufrag:local-user", - "a=ice-pwd:local-password", - "a=fingerprint:sha-256 CC:DD", - "m=video 9 UDP/TLS/RTP/SAVPF 96", - "a=ice-ufrag:video-user", - "a=ice-pwd:video-password", - "a=fingerprint:sha-256 EE:FF", - ] - .join("\n"); - - let nvst = build_nvst_sdp_for_answer(¶ms, &answer_sdp).expect("nvst sdp"); - - assert!(nvst.contains("a=general.icePassword:local-password")); - assert!(nvst.contains("a=general.iceUserNameFragment:local-user")); - assert!(nvst.contains("a=general.dtlsFingerprint:CC:DD")); - assert!(!nvst.contains("remote-password")); - assert!(!nvst.contains("video-password")); - } - - fn nvst_params_for_fps(fps: u32) -> NvstParams { - NvstParams { - width: 1920, - height: 1080, - fps, - max_bitrate_kbps: 75_000, - partial_reliable_threshold_ms: 16, - codec: VideoCodec::H265, - color_quality: ColorQuality::EightBit420, - credentials: IceCredentials { - ufrag: "user".to_owned(), - pwd: "password".to_owned(), - fingerprint: "AA:BB".to_owned(), - }, - hid_device_mask: None, - enable_partially_reliable_transfer_gamepad: None, - enable_partially_reliable_transfer_hid: None, - } - } - - #[test] - fn builds_nvst_sdp_disables_dynamic_streaming_for_normal_fps() { - let nvst = build_nvst_sdp(&nvst_params_for_fps(60)); - - assert!(nvst.contains("a=vqos.dynamicStreamingMode:0")); - assert!(nvst.contains("a=vqos.dfc.adjustResAndFps:0")); - assert!(nvst.contains("a=vqos.dfc.enable:0")); - assert!(!nvst.contains("a=vqos.dfc.decodeFpsAdjPercent:85")); - assert!(!nvst.contains("a=vqos.resControl.dfc.useClientFpsPerf:0")); - } - - #[test] - fn builds_nvst_sdp_disables_dynamic_streaming_for_high_fps() { - for fps in [90, 120, 144, 165, 240, 360] { - let nvst = build_nvst_sdp(&nvst_params_for_fps(fps)); - - assert!(nvst.contains("a=vqos.dynamicStreamingMode:0")); - assert!(nvst.contains("a=vqos.dfc.adjustResAndFps:0")); - assert!(nvst.contains("a=vqos.dfc.enable:1")); - assert!(nvst.contains("a=vqos.resControl.dfc.useClientFpsPerf:0")); - if fps >= 120 { - assert!(nvst.contains("a=vqos.dfc.dfcAlgoVersion:2")); - assert!(nvst.contains("a=vqos.dfc.minTargetFps:100")); - } else { - assert!(nvst.contains("a=vqos.dfc.dfcAlgoVersion:1")); - assert!(nvst.contains("a=vqos.dfc.minTargetFps:60")); - } - assert!(!nvst.contains("a=vqos.dfc.enable:0")); - } - } - - #[test] - fn applies_only_official_fps_specific_capture_tuning() { - let cases = [ - (90, Some((9, 11))), - (120, Some((6, 9))), - (144, None), - (165, None), - (240, Some((18, 9))), - (360, None), - ]; - - for (fps, expected) in cases { - let nvst = build_nvst_sdp(&nvst_params_for_fps(fps)); - match expected { - Some((grab_timeout_ms, decode_threshold_ms)) => { - assert!(nvst.contains(&format!( - "a=video.fbcDynamicFpsGrabTimeoutMs:{grab_timeout_ms}" - ))); - assert!(nvst.contains(&format!( - "a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:{decode_threshold_ms}" - ))); - } - None => { - assert!(!nvst.contains("a=video.fbcDynamicFpsGrabTimeoutMs:")); - assert!(!nvst.contains("a=vqos.resControl.cpmRtc.decodeTimeThresholdMs:")); - } - } - } - } - - #[test] - fn reserves_240_fps_capture_profile_for_exact_240_fps() { - let nvst_240 = build_nvst_sdp(&nvst_params_for_fps(240)); - let nvst_360 = build_nvst_sdp(&nvst_params_for_fps(360)); - - assert!(nvst_240.contains("a=vqos.maxStreamFpsEstimate:240")); - assert!(nvst_240.contains("a=video.enableNextCaptureMode:1")); - assert!(!nvst_360.contains("a=vqos.maxStreamFpsEstimate:240")); - assert!(!nvst_360.contains("a=video.enableNextCaptureMode:1")); - assert!(!nvst_360.contains("a=video.videoSplitEncodeStripsPerFrame:")); - } - - #[test] - fn extracts_negotiated_video_codec_from_answer_payload_order() { - let answer_sdp = [ - "v=0", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=rtpmap:111 OPUS/48000/2", - "m=video 9 UDP/TLS/RTP/SAVPF 101 102", - "a=rtpmap:101 AV1/90000", - "a=rtpmap:102 rtx/90000", - "a=fmtp:102 apt=101", - ] - .join("\n"); - - assert_eq!( - extract_negotiated_video_codec(&answer_sdp), - Some(VideoCodec::AV1) - ); - } - - #[test] - fn builds_nvst_sdp_for_answer_uses_negotiated_av1_codec() { - let params = NvstParams { - width: 2560, - height: 1440, - fps: 120, - max_bitrate_kbps: 150_000, - partial_reliable_threshold_ms: 16, - codec: VideoCodec::H265, - color_quality: ColorQuality::EightBit420, - credentials: IceCredentials { - ufrag: "remote-user".to_owned(), - pwd: "remote-password".to_owned(), - fingerprint: "AA:BB".to_owned(), - }, - hid_device_mask: None, - enable_partially_reliable_transfer_gamepad: None, - enable_partially_reliable_transfer_hid: None, - }; - let answer_sdp = [ - "v=0", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - "a=ice-ufrag:local-user", - "a=ice-pwd:local-password", - "a=fingerprint:sha-256 CC:DD", - "m=video 9 UDP/TLS/RTP/SAVPF 101", - "a=rtpmap:101 AV1/90000", - ] - .join("\n"); - - let nvst = build_nvst_sdp_for_answer(¶ms, &answer_sdp).expect("nvst sdp"); - - assert!(nvst.contains("a=video.scalingFeature1:1")); - assert!(nvst.contains("a=video.enableAv1RcPrecisionFactor:1")); - assert!(nvst.contains("a=vqos.drc.minQpHeadroom:20")); - } - - #[test] - fn munges_answer_bitrate_and_opus_stereo() { - let sdp = "m=video 9 UDP/TLS/RTP/SAVPF 96\nm=audio 9 UDP/TLS/RTP/SAVPF 111\na=fmtp:111 minptime=10;useinbandfec=1"; - let munged = munge_answer_sdp(sdp, 75000); - assert!(munged.contains("m=video 9 UDP/TLS/RTP/SAVPF 96\nb=AS:75000")); - assert!(munged.contains("m=audio 9 UDP/TLS/RTP/SAVPF 111\nb=AS:128")); - assert!(munged.contains("a=fmtp:111 minptime=10;useinbandfec=1;stereo=1")); - } - - #[test] - fn filters_video_codec_and_keeps_matching_rtx() { - let sdp = [ - "v=0", - "m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101", - "a=rtpmap:96 H264/90000", - "a=rtpmap:97 rtx/90000", - "a=fmtp:97 apt=96", - "a=rtpmap:98 H265/90000", - "a=fmtp:98 profile-id=1;level-id=186", - "a=rtpmap:99 rtx/90000", - "a=fmtp:99 apt=98", - "a=rtpmap:100 AV1/90000", - "a=rtpmap:101 flexfec-03/90000", - "m=audio 9 UDP/TLS/RTP/SAVPF 111", - ] - .join("\n"); - let filtered = prefer_codec( - &sdp, - VideoCodec::H265, - PreferCodecOptions { - prefer_hevc_profile_id: Some(1), - }, - ); - assert!(filtered.contains("m=video 9 UDP/TLS/RTP/SAVPF 98 99 101")); - assert!(!filtered.contains("a=rtpmap:96 H264/90000")); - assert!(filtered.contains("a=rtpmap:99 rtx/90000")); - assert!(filtered.contains("a=rtpmap:101 flexfec-03/90000")); - assert!(filtered.contains("m=audio 9 UDP/TLS/RTP/SAVPF 111")); - } - - #[test] - fn builds_nvst_sdp_with_core_attributes() { - let nvst = build_nvst_sdp(&NvstParams { - width: 1920, - height: 1080, - fps: 120, - max_bitrate_kbps: 75_000, - partial_reliable_threshold_ms: 16, - codec: VideoCodec::H265, - color_quality: ColorQuality::TenBit420, - credentials: IceCredentials { - ufrag: "ufrag".to_owned(), - pwd: "pwd".to_owned(), - fingerprint: "AA:BB".to_owned(), - }, - hid_device_mask: None, - enable_partially_reliable_transfer_gamepad: None, - enable_partially_reliable_transfer_hid: None, - }); - - assert!(nvst.contains("a=general.icePassword:pwd")); - assert!(nvst.contains("a=video.clientViewportWd:1920")); - assert!(nvst.contains("a=video.clientViewportHt:1080")); - assert!(nvst.contains("a=video.maxFPS:120")); - assert!(nvst.contains("a=video.bitDepth:10")); - assert!(nvst.contains("a=packetPacing.numGroups:3")); - assert!(nvst.contains("a=packetPacing.enableAccurateSleep:1")); - assert!(nvst.contains("a=packetPacing.minNumPacketsPerGroup:15")); - assert!(nvst.contains("a=video.framePacing.mode:2")); - assert!(nvst.contains("a=vqos.fec.repairPercent:5")); - assert!(nvst.contains("a=vqos.fec.repairMaxPercent:35")); - assert!(nvst.contains("a=vqos.bllFec.enable:0")); - assert!(nvst.contains("a=video.rtpNackQueueLength:1024")); - assert!(nvst.contains("a=video.rtpNackQueueMaxPackets:512")); - assert!(nvst.contains("a=video.rtpNackMaxPacketCount:25")); - assert!(nvst.contains("a=aqos.enableRedundancy:1")); - assert!(nvst.contains("a=vqos.adjustStreamingFpsDuringOutOfFocus:1")); - assert!(!nvst.contains("a=video.updateSplitEncodeStateDynamically:1")); - assert!(nvst.contains("a=ri.partialReliableThresholdMs:16")); - assert!(nvst.ends_with('\n')); - } - - #[test] - fn advertises_official_240_fps_split_encode_profile() { - let nvst = build_nvst_sdp(&NvstParams { - width: 1920, - height: 1080, - fps: 240, - max_bitrate_kbps: 75_000, - partial_reliable_threshold_ms: 16, - codec: VideoCodec::H265, - color_quality: ColorQuality::EightBit420, - credentials: IceCredentials { - ufrag: "ufrag".to_owned(), - pwd: "pwd".to_owned(), - fingerprint: "AA:BB".to_owned(), - }, - hid_device_mask: None, - enable_partially_reliable_transfer_gamepad: None, - enable_partially_reliable_transfer_hid: None, - }); - - assert!(nvst.contains("a=vqos.maxStreamFpsEstimate:240")); - assert!(nvst.contains("a=video.videoSplitEncodeStripsPerFrame:3")); - assert!(nvst.contains("a=video.updateSplitEncodeStateDynamically:1")); - assert!(nvst.contains("a=video.framePacing.pid.minTargetFrameTimeUs:3958")); - assert!(nvst.contains("a=vqos.rtcPreemptiveIdrSettings.minBurstNackSize:65535")); - assert!(nvst.contains("a=vqos.rtcPreemptiveIdrSettings.minNackPacketCaptureAgeMs:65535")); - } - - #[test] - fn uses_wide_split_encode_only_for_high_resolution_av1() { - let mut params = nvst_params_for_fps(240); - params.codec = VideoCodec::AV1; - params.width = 2560; - params.height = 1440; - - let nvst = build_nvst_sdp(¶ms); - - assert!(nvst.contains("a=video.videoSplitEncodeStripsPerFrame:63")); - } -} diff --git a/native/opennow-streamer/src/shortcuts.rs b/native/opennow-streamer/src/shortcuts.rs deleted file mode 100644 index 271b9e419..000000000 --- a/native/opennow-streamer/src/shortcuts.rs +++ /dev/null @@ -1,355 +0,0 @@ -use crate::protocol::{NativeStreamerShortcutAction, NativeStreamerShortcutBindings}; - -const MODIFIER_SHIFT: u16 = 0x01; -const MODIFIER_CTRL: u16 = 0x02; -const MODIFIER_ALT: u16 = 0x04; -const MODIFIER_META: u16 = 0x08; -const SHORTCUT_MODIFIER_MASK: u16 = MODIFIER_SHIFT | MODIFIER_CTRL | MODIFIER_ALT | MODIFIER_META; - -const VK_BACK: u16 = 0x08; -const VK_TAB: u16 = 0x09; -const VK_RETURN: u16 = 0x0D; -const VK_PAUSE: u16 = 0x13; -const VK_CAPITAL: u16 = 0x14; -const VK_ESCAPE: u16 = 0x1B; -const VK_SPACE: u16 = 0x20; -const VK_PRIOR: u16 = 0x21; -const VK_NEXT: u16 = 0x22; -const VK_END: u16 = 0x23; -const VK_HOME: u16 = 0x24; -const VK_LEFT: u16 = 0x25; -const VK_UP: u16 = 0x26; -const VK_RIGHT: u16 = 0x27; -const VK_DOWN: u16 = 0x28; -const VK_INSERT: u16 = 0x2D; -const VK_DELETE: u16 = 0x2E; -const VK_PRINT: u16 = 0x2C; -const VK_LWIN: u16 = 0x5B; -const VK_RWIN: u16 = 0x5C; -const VK_APPS: u16 = 0x5D; -const VK_NUMPAD0: u16 = 0x60; -const VK_MULTIPLY: u16 = 0x6A; -const VK_ADD: u16 = 0x6B; -const VK_SEPARATOR: u16 = 0x6C; -const VK_SUBTRACT: u16 = 0x6D; -const VK_DECIMAL: u16 = 0x6E; -const VK_DIVIDE: u16 = 0x6F; -const VK_F1: u16 = 0x70; -const VK_NUMLOCK: u16 = 0x90; -const VK_SCROLL: u16 = 0x91; -const VK_OEM_1: u16 = 0xBA; -const VK_OEM_PLUS: u16 = 0xBB; -const VK_OEM_COMMA: u16 = 0xBC; -const VK_OEM_MINUS: u16 = 0xBD; -const VK_OEM_PERIOD: u16 = 0xBE; -const VK_OEM_2: u16 = 0xBF; -const VK_OEM_3: u16 = 0xC0; -const VK_OEM_4: u16 = 0xDB; -const VK_OEM_5: u16 = 0xDC; -const VK_OEM_6: u16 = 0xDD; -const VK_OEM_7: u16 = 0xDE; -const SCANCODE_ENTER: u16 = 0x001C; -const SCANCODE_NUMPAD_ENTER: u16 = 0xE01C; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ShortcutBinding { - action: NativeStreamerShortcutAction, - keycode: u16, - scancode: Option, - modifiers: u16, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ParsedKey { - keycode: u16, - scancode: Option, -} - -#[derive(Debug, Clone, Default)] -pub(crate) struct NativeShortcutMatcher { - bindings: Vec, -} - -impl NativeShortcutMatcher { - pub(crate) fn from_bindings(bindings: &NativeStreamerShortcutBindings) -> Self { - let mut parsed = Vec::new(); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::ToggleStats, - &bindings.toggle_stats, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::TogglePointerLock, - &bindings.toggle_pointer_lock, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::ToggleFullscreen, - &bindings.toggle_fullscreen, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::StopStream, - &bindings.stop_stream, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::ToggleAntiAfk, - &bindings.toggle_anti_afk, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::ToggleMicrophone, - &bindings.toggle_microphone, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::Screenshot, - &bindings.screenshot, - ); - append_binding( - &mut parsed, - NativeStreamerShortcutAction::ToggleRecording, - &bindings.toggle_recording, - ); - Self { bindings: parsed } - } - - pub(crate) fn match_keydown( - &self, - keycode: u16, - scancode: u16, - modifiers: u16, - ) -> Option { - let modifiers = modifiers & SHORTCUT_MODIFIER_MASK; - self.bindings - .iter() - .find(|binding| { - binding.keycode == keycode - && binding.modifiers == modifiers - && binding.scancode.map_or(true, |expected| expected == scancode) - }) - .map(|binding| binding.action) - } -} - -fn append_binding( - bindings: &mut Vec, - action: NativeStreamerShortcutAction, - raw: &str, -) { - if let Some(binding) = parse_binding(action, raw) { - bindings.push(binding); - } -} - -fn parse_binding(action: NativeStreamerShortcutAction, raw: &str) -> Option { - let mut modifiers = 0u16; - let mut key = None; - - for token in raw.split('+').map(str::trim).filter(|token| !token.is_empty()) { - match token.to_ascii_uppercase().as_str() { - "CTRL" | "CONTROL" => modifiers |= MODIFIER_CTRL, - "ALT" | "OPTION" => modifiers |= MODIFIER_ALT, - "SHIFT" => modifiers |= MODIFIER_SHIFT, - "META" | "CMD" | "COMMAND" => modifiers |= MODIFIER_META, - _ => { - if key.is_some() { - return None; - } - key = parse_key_token(token); - } - } - } - - let key = key?; - Some(ShortcutBinding { - action, - keycode: key.keycode, - scancode: key.scancode, - modifiers, - }) -} - -fn parsed_key(keycode: u16) -> Option { - Some(ParsedKey { - keycode, - scancode: None, - }) -} - -fn parsed_scancode_key(keycode: u16, scancode: u16) -> Option { - Some(ParsedKey { - keycode, - scancode: Some(scancode), - }) -} - -fn parse_key_token(token: &str) -> Option { - let upper = token.trim().to_ascii_uppercase(); - if upper.len() == 1 { - let byte = upper.as_bytes()[0]; - return match byte { - b'A'..=b'Z' | b'0'..=b'9' => parsed_key(u16::from(byte)), - b',' => parsed_key(VK_OEM_COMMA), - b'.' => parsed_key(VK_OEM_PERIOD), - b'/' => parsed_key(VK_OEM_2), - b';' => parsed_key(VK_OEM_1), - b'\'' => parsed_key(VK_OEM_7), - b'[' => parsed_key(VK_OEM_4), - b']' => parsed_key(VK_OEM_6), - b'\\' => parsed_key(VK_OEM_5), - b'-' => parsed_key(VK_OEM_MINUS), - b'=' => parsed_key(VK_OEM_PLUS), - b'`' => parsed_key(VK_OEM_3), - _ => None, - }; - } - - if let Some(function_index) = upper - .strip_prefix('F') - .and_then(|value| value.parse::().ok()) - { - if (1..=24).contains(&function_index) { - return parsed_key(VK_F1 + function_index - 1); - } - } - - if let Some(numpad_index) = upper - .strip_prefix("NUMPAD") - .and_then(|value| value.parse::().ok()) - { - if numpad_index <= 9 { - return parsed_key(VK_NUMPAD0 + numpad_index); - } - } - - match upper.as_str() { - "BACKSPACE" => parsed_key(VK_BACK), - "TAB" => parsed_key(VK_TAB), - "ENTER" => parsed_scancode_key(VK_RETURN, SCANCODE_ENTER), - "NUMPADENTER" => parsed_scancode_key(VK_RETURN, SCANCODE_NUMPAD_ENTER), - "PAUSE" => parsed_key(VK_PAUSE), - "CAPSLOCK" => parsed_key(VK_CAPITAL), - "ESCAPE" => parsed_key(VK_ESCAPE), - "SPACE" => parsed_key(VK_SPACE), - "PAGEUP" => parsed_key(VK_PRIOR), - "PAGEDOWN" => parsed_key(VK_NEXT), - "END" => parsed_key(VK_END), - "HOME" => parsed_key(VK_HOME), - "ARROWLEFT" => parsed_key(VK_LEFT), - "ARROWUP" => parsed_key(VK_UP), - "ARROWRIGHT" => parsed_key(VK_RIGHT), - "ARROWDOWN" => parsed_key(VK_DOWN), - "INSERT" => parsed_key(VK_INSERT), - "DELETE" => parsed_key(VK_DELETE), - "PRINTSCREEN" => parsed_key(VK_PRINT), - "APPS" | "MENU" => parsed_key(VK_APPS), - "METALEFT" => parsed_key(VK_LWIN), - "METARIGHT" => parsed_key(VK_RWIN), - "NUMPADMULTIPLY" => parsed_key(VK_MULTIPLY), - "NUMPADADD" => parsed_key(VK_ADD), - "NUMPADSEPARATOR" => parsed_key(VK_SEPARATOR), - "NUMPADSUBTRACT" => parsed_key(VK_SUBTRACT), - "NUMPADDECIMAL" => parsed_key(VK_DECIMAL), - "NUMPADDIVIDE" => parsed_key(VK_DIVIDE), - "NUMLOCK" => parsed_key(VK_NUMLOCK), - "SCROLLLOCK" => parsed_key(VK_SCROLL), - "SEMICOLON" => parsed_key(VK_OEM_1), - "EQUAL" => parsed_key(VK_OEM_PLUS), - "COMMA" => parsed_key(VK_OEM_COMMA), - "MINUS" => parsed_key(VK_OEM_MINUS), - "PERIOD" => parsed_key(VK_OEM_PERIOD), - "SLASH" => parsed_key(VK_OEM_2), - "BACKQUOTE" => parsed_key(VK_OEM_3), - "BRACKETLEFT" => parsed_key(VK_OEM_4), - "BACKSLASH" => parsed_key(VK_OEM_5), - "BRACKETRIGHT" => parsed_key(VK_OEM_6), - "QUOTE" => parsed_key(VK_OEM_7), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn bindings() -> NativeStreamerShortcutBindings { - NativeStreamerShortcutBindings { - toggle_stats: "F3".to_owned(), - toggle_pointer_lock: "F8".to_owned(), - toggle_fullscreen: "F10".to_owned(), - stop_stream: "Ctrl+Shift+Q".to_owned(), - toggle_anti_afk: "Ctrl+Shift+K".to_owned(), - toggle_microphone: "Ctrl+Shift+M".to_owned(), - screenshot: "F11".to_owned(), - toggle_recording: "F12".to_owned(), - } - } - - #[test] - fn matches_function_key_shortcuts_without_modifiers() { - let matcher = NativeShortcutMatcher::from_bindings(&bindings()); - - assert_eq!( - matcher.match_keydown(VK_F1 + 2, 0, 0), - Some(NativeStreamerShortcutAction::ToggleStats) - ); - assert_eq!( - matcher.match_keydown(VK_F1 + 10, 0, 0), - Some(NativeStreamerShortcutAction::Screenshot) - ); - } - - #[test] - fn matches_exact_modifier_combinations() { - let matcher = NativeShortcutMatcher::from_bindings(&bindings()); - - assert_eq!( - matcher.match_keydown(u16::from(b'Q'), 0, MODIFIER_CTRL | MODIFIER_SHIFT), - Some(NativeStreamerShortcutAction::StopStream) - ); - assert_eq!(matcher.match_keydown(u16::from(b'Q'), 0, MODIFIER_CTRL), None); - assert_eq!( - matcher.match_keydown( - u16::from(b'Q'), - 0, - MODIFIER_CTRL | MODIFIER_SHIFT | MODIFIER_ALT - ), - None - ); - } - - #[test] - fn ignores_invalid_bindings() { - let matcher = NativeShortcutMatcher::from_bindings(&NativeStreamerShortcutBindings { - toggle_stats: "Ctrl+Shift".to_owned(), - ..bindings() - }); - - assert_eq!(matcher.match_keydown(VK_F1 + 2, 0, 0), None); - assert_eq!( - matcher.match_keydown(VK_F1 + 7, 0, 0), - Some(NativeStreamerShortcutAction::TogglePointerLock) - ); - } - - #[test] - fn keeps_regular_enter_and_numpad_enter_distinct() { - let matcher = NativeShortcutMatcher::from_bindings(&NativeStreamerShortcutBindings { - toggle_stats: "Enter".to_owned(), - toggle_pointer_lock: "NumpadEnter".to_owned(), - ..bindings() - }); - - assert_eq!( - matcher.match_keydown(VK_RETURN, SCANCODE_ENTER, 0), - Some(NativeStreamerShortcutAction::ToggleStats) - ); - assert_eq!( - matcher.match_keydown(VK_RETURN, SCANCODE_NUMPAD_ENTER, 0), - Some(NativeStreamerShortcutAction::TogglePointerLock) - ); - } -} diff --git a/native/opennow-streamer/src/windows_dpi.rs b/native/opennow-streamer/src/windows_dpi.rs deleted file mode 100644 index fdf2fcf16..000000000 --- a/native/opennow-streamer/src/windows_dpi.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Windows DPI policy for native renderer windows. -//! -//! Electron publishes native render-surface bounds in physical pixels. The -//! streamer must use the same coordinate space or Windows DPI virtualization -//! can offset or shrink the video window on scaled and mixed-DPI displays. - -use std::ffi::c_void; - -type DpiAwarenessContext = *mut c_void; - -// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 from winuser.h. -const PER_MONITOR_AWARE_V2: DpiAwarenessContext = -4_isize as DpiAwarenessContext; - -#[link(name = "user32")] -extern "system" { - fn SetProcessDpiAwarenessContext(value: DpiAwarenessContext) -> i32; - fn SetProcessDPIAware() -> i32; -} - -/// Opt the streamer into physical-pixel coordinates before GStreamer or any -/// renderer thread can create a window. The legacy fallback keeps coordinates -/// unvirtualized on Windows versions that reject the per-monitor-v2 context. -pub(crate) fn enable_per_monitor_awareness() { - unsafe { - if SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2) == 0 { - let _ = SetProcessDPIAware(); - } - } -} diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/OPENNOW-PATCH.txt b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/OPENNOW-PATCH.txt deleted file mode 100644 index 48d2a680d..000000000 --- a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/OPENNOW-PATCH.txt +++ /dev/null @@ -1 +0,0 @@ -Patched gstvkwindow_win32: VkSurface on internal GSTVULKAN hwnd (not Electron parent). diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/bin/gstvulkan-1.0-0.dll b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/bin/gstvulkan-1.0-0.dll deleted file mode 100644 index 542872401..000000000 Binary files a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/bin/gstvulkan-1.0-0.dll and /dev/null differ diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/lib/gstreamer-1.0/gstvulkan.dll b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/lib/gstreamer-1.0/gstvulkan.dll deleted file mode 100644 index 097b856a1..000000000 Binary files a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/1.28.3/lib/gstreamer-1.0/gstvulkan.dll and /dev/null differ diff --git a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/README.md b/native/opennow-streamer/vendor/gstreamer-vulkan-windows/README.md deleted file mode 100644 index a7dc26e69..000000000 --- a/native/opennow-streamer/vendor/gstreamer-vulkan-windows/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Windows GStreamer Vulkan plugins - -Official GStreamer Windows packages disable the Vulkan plugin in Cerbero -(`disable_plugin('vulkan', ...)` for Linux/Windows binary builds). - -OpenNOW vendors a matching `gstvulkan` build so the experimental Windows -Vulkan video backend can load `vulkansink` / `vulkanh264dec` / `vulkanh265dec`. - -Artifacts under `1.28.3/` target GStreamer 1.28.3 MSVC x86_64. - -## OpenNOW Win32 embed patch - -`gstvkwindow_win32.c` is patched so `vkCreateWin32SurfaceKHR` targets the -`GSTVULKAN` child hwnd (not the Electron/Chromium parent overlay hwnd). -Stock GStreamer presents onto the parent when `set_window_handle` is used, -which stays black under DirectComposition hole-punch. With this patch, -VideoOverlay parenting works: GSTVULKAN is a visible child of the Internal -surface and the swapchain presents there (no floating top-level window). diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/concrt140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/concrt140.dll deleted file mode 100644 index 830dfaeaf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/concrt140.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gio-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gio-2.0-0.dll deleted file mode 100644 index 56235522e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gio-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/glib-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/glib-2.0-0.dll deleted file mode 100644 index a9e79cd53..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/glib-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gmodule-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gmodule-2.0-0.dll deleted file mode 100644 index f9712b7cd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gmodule-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gobject-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gobject-2.0-0.dll deleted file mode 100644 index 33f81a469..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gobject-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer-1.0-0.dll deleted file mode 100644 index 66afb1858..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/OPENNOW-GSTREAMER-RUNTIME.txt b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/OPENNOW-GSTREAMER-RUNTIME.txt deleted file mode 100644 index 3135dca61..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/OPENNOW-GSTREAMER-RUNTIME.txt +++ /dev/null @@ -1,7 +0,0 @@ -OpenNOW private GStreamer runtime bundle -Source: C:\Program Files\gstreamer\1.0\msvc_x86_64 -Generated: 2026-06-30T13:34:35.902Z -Platform: win32 -Scope: native streamer child process only - -This directory is loaded only for the native streamer child process. Keep the private layout intact. diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/FLAC-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/FLAC-8.dll deleted file mode 100644 index 95227040b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/FLAC-8.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtAv1Enc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtAv1Enc.dll deleted file mode 100644 index 9dd3d0cfd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtAv1Enc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtJpegxs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtJpegxs.dll deleted file mode 100644 index bea4c0fee..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/SvtJpegxs.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/accesskit-c-0.17.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/accesskit-c-0.17.dll deleted file mode 100644 index bed220e35..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/accesskit-c-0.17.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ass-9.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ass-9.dll deleted file mode 100644 index 0fd9a5ceb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ass-9.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avcodec-61.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avcodec-61.dll deleted file mode 100644 index c949370bf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avcodec-61.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avfilter-10.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avfilter-10.dll deleted file mode 100644 index f98956b98..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avfilter-10.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avformat-61.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avformat-61.dll deleted file mode 100644 index 95cf742b2..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avformat-61.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avutil-59.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avutil-59.dll deleted file mode 100644 index c113530a2..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/avutil-59.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/bz2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/bz2.dll deleted file mode 100644 index d16161f30..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/bz2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-2.dll deleted file mode 100644 index 044112338..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-gobject-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-gobject-2.dll deleted file mode 100644 index 600f8f053..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-gobject-2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-script-interpreter-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-script-interpreter-2.dll deleted file mode 100644 index b850224af..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/cairo-script-interpreter-2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/concrt140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/concrt140.dll deleted file mode 100644 index 830dfaeaf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/concrt140.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dav1d.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dav1d.dll deleted file mode 100644 index 90618b088..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dav1d.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dca-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dca-0.dll deleted file mode 100644 index bffffe9fa..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dca-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dv-4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dv-4.dll deleted file mode 100644 index 95c0114d4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dv-4.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdnav-4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdnav-4.dll deleted file mode 100644 index ccecadc00..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdnav-4.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdread-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdread-8.dll deleted file mode 100644 index 935a9c546..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/dvdread-8.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/epoxy-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/epoxy-0.dll deleted file mode 100644 index 7d4b5aab6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/epoxy-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ffi-7.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ffi-7.dll deleted file mode 100644 index c443a4ac3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ffi-7.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fmt.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fmt.dll deleted file mode 100644 index eb77ff122..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fmt.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fontconfig-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fontconfig-1.dll deleted file mode 100644 index 614b988fb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fontconfig-1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/freetype-6.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/freetype-6.dll deleted file mode 100644 index 8323309db..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/freetype-6.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fribidi-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fribidi-0.dll deleted file mode 100644 index a831cbaeb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/fribidi-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-annotation-tool b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-annotation-tool deleted file mode 100644 index e6b73a389..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-annotation-tool +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env C:/Users/nirbheek/AppData/Local/Python/pythoncore-3.9-64/python.exe -# -*- Mode: Python -*- -# GObject-Introspection - a framework for introspecting GObject libraries -# Copyright (C) 2008 Johan Dahlin -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. -# - -import os -import sys -import sysconfig -import builtins - - -debug = os.getenv('GI_SCANNER_DEBUG') -if debug: - if 'pydevd' in debug.split(','): - # http://pydev.org/manual_adv_remote_debugger.html - pydevdpath = os.getenv('PYDEVDPATH', None) - if pydevdpath is not None and os.path.isdir(pydevdpath): - sys.path.insert(0, pydevdpath) - import pydevd - pydevd.settrace() - else: - def on_exception(exctype, value, tb): - print("Caught exception: %r %r" % (exctype, value)) - import pdb - pdb.pm() - sys.excepthook = on_exception - -# Detect and set datadir, pylibdir, etc as applicable -# Similar to the method used in gdbus-codegen -filedir = os.path.dirname(__file__) - -# Try using relative paths first so that the installation prefix is relocatable -datadir = os.path.abspath(os.path.join(filedir, '..', 'share')) -# Fallback to hard-coded paths if the relocatable paths are wrong -if not os.path.isdir(os.path.join(datadir, 'gir-1.0')): - datadir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share" - -builtins.__dict__['DATADIR'] = datadir - -gir_dir = os.path.abspath(os.path.join(filedir, '..', 'share', 'gir-1.0')) -# Fallback to hard-coded paths if the relocatable paths are wrong -if not os.path.isdir(gir_dir): - gir_dir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share/gir-1.0" - -builtins.__dict__['GIR_DIR'] = gir_dir - -# Again, relative paths first so that the installation prefix is relocatable -pylibdir = os.path.abspath(os.path.join(filedir, '..', 'lib', 'gobject-introspection')) - -# EXT_SUFFIX for py3 SO for py2 -py_mod_suffix = sysconfig.get_config_var('EXT_SUFFIX') or sysconfig.get_config_var('SO') - -if not os.path.isfile(os.path.join(pylibdir, 'giscanner', '_giscanner' + py_mod_suffix)): - # Running uninstalled? - builddir = os.getenv('UNINSTALLED_INTROSPECTION_BUILDDIR', None) - if builddir is not None: - # Autotools, most likely - builddir = os.path.abspath(builddir) - # For _giscanner.so - sys.path.insert(0, os.path.join(builddir, '.libs')) - srcdir = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None) - if srcdir: - # For the giscanner python files - pylibdir = srcdir - elif os.path.isdir(os.path.join(filedir, '..', 'giscanner')): - # We're running uninstalled inside meson - builddir = os.path.abspath(os.path.join(filedir, '..')) - pylibdir = builddir - - if 'GI_GIR_PATH' not in os.environ: - os.environ['GI_GIR_PATH'] = os.path.join(filedir, os.pardir, 'gir') - - gdump_path = os.path.join(builddir, 'giscanner', 'gdump.c') - if os.path.isfile(gdump_path): - builtins.__dict__['GDUMP_PATH'] = gdump_path - else: - # Okay, we're not running uninstalled and the prefix is not - # relocatable. Use hard-coded libdir. - pylibdir = os.path.join('C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib', 'gobject-introspection') - -sys.path.insert(0, pylibdir) - -from giscanner.utils import dll_dirs -dll_dirs = dll_dirs() -dll_dirs.add_dll_dirs(['gio-2.0']) - -def get_rspfile_args(rspfile): - ''' - Response files are useful on Windows where there is a command-line character - limit of 8191 because when passing sources as arguments to glib-mkenums this - limit can be exceeded in large codebases. - - There is no specification for response files and each tool that supports it - generally writes them out in slightly different ways, but some sources are: - https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files - https://docs.microsoft.com/en-us/windows/desktop/midl/the-response-file-command - ''' - import shlex - if not os.path.isfile(rspfile): - sys.exit('Response file {!r} does not exist'.format(rspfile)) - try: - with open(rspfile, 'r') as f: - cmdline = f.read() - except OSError as e: - sys.exit('Response file {!r} could not be read: {}' - .format(rspfile, e.strerror)) - return shlex.split(cmdline) - - -# Support reading an rspfile of the form @filename which contains the args -# to be parsed -if sys.argv[-1].startswith('@'): - args = sys.argv[0:-1] + get_rspfile_args(sys.argv[-1][1:]) -else: - args = sys.argv - -from giscanner.annotationmain import annotation_main -sys.exit(annotation_main(args)) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-compiler.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-compiler.exe deleted file mode 100644 index 39bbc175e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-compiler.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-generate.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-generate.exe deleted file mode 100644 index 5e046b3c9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-generate.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-inspect.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-inspect.exe deleted file mode 100644 index 718fcab38..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-inspect.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-scanner b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-scanner deleted file mode 100644 index ed58d9e1c..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/g-ir-scanner +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env C:/Users/nirbheek/AppData/Local/Python/pythoncore-3.9-64/python.exe -# -*- Mode: Python -*- -# GObject-Introspection - a framework for introspecting GObject libraries -# Copyright (C) 2008 Johan Dahlin -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. -# - -import os -import sys -import sysconfig -import builtins - - -debug = os.getenv('GI_SCANNER_DEBUG') -if debug: - if 'pydevd' in debug.split(','): - # http://pydev.org/manual_adv_remote_debugger.html - pydevdpath = os.getenv('PYDEVDPATH', None) - if pydevdpath is not None and os.path.isdir(pydevdpath): - sys.path.insert(0, pydevdpath) - import pydevd - pydevd.settrace() - else: - def on_exception(exctype, value, tb): - print("Caught exception: %r %r" % (exctype, value)) - import pdb - pdb.pm() - sys.excepthook = on_exception - -# Detect and set datadir, pylibdir, etc as applicable -# Similar to the method used in gdbus-codegen -filedir = os.path.dirname(__file__) - -# Try using relative paths first so that the installation prefix is relocatable -datadir = os.path.abspath(os.path.join(filedir, '..', 'share')) -# Fallback to hard-coded paths if the relocatable paths are wrong -if not os.path.isdir(os.path.join(datadir, 'gir-1.0')): - datadir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share" - -builtins.__dict__['DATADIR'] = datadir - -gir_dir = os.path.abspath(os.path.join(filedir, '..', 'share', 'gir-1.0')) -# Fallback to hard-coded paths if the relocatable paths are wrong -if not os.path.isdir(gir_dir): - gir_dir = "C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share/gir-1.0" - -builtins.__dict__['GIR_DIR'] = gir_dir - -# Again, relative paths first so that the installation prefix is relocatable -pylibdir = os.path.abspath(os.path.join(filedir, '..', 'lib', 'gobject-introspection')) - -# EXT_SUFFIX for py3 SO for py2 -py_mod_suffix = sysconfig.get_config_var('EXT_SUFFIX') or sysconfig.get_config_var('SO') - -if not os.path.isfile(os.path.join(pylibdir, 'giscanner', '_giscanner' + py_mod_suffix)): - # Running uninstalled? - builddir = os.getenv('UNINSTALLED_INTROSPECTION_BUILDDIR', None) - if builddir is not None: - # Autotools, most likely - builddir = os.path.abspath(builddir) - # For _giscanner.so - sys.path.insert(0, os.path.join(builddir, '.libs')) - srcdir = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None) - if srcdir: - # For the giscanner python files - pylibdir = srcdir - elif os.path.isdir(os.path.join(filedir, '..', 'giscanner')): - # We're running uninstalled inside meson - builddir = os.path.abspath(os.path.join(filedir, '..')) - pylibdir = builddir - - if 'GI_GIR_PATH' not in os.environ: - os.environ['GI_GIR_PATH'] = os.path.join(filedir, os.pardir, 'gir') - - gdump_path = os.path.join(builddir, 'giscanner', 'gdump.c') - if os.path.isfile(gdump_path): - builtins.__dict__['GDUMP_PATH'] = gdump_path - else: - # Okay, we're not running uninstalled and the prefix is not - # relocatable. Use hard-coded libdir. - pylibdir = os.path.join('C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib', 'gobject-introspection') - -sys.path.insert(0, pylibdir) - -from giscanner.utils import dll_dirs -dll_dirs = dll_dirs() -dll_dirs.add_dll_dirs(['gio-2.0']) - -def get_rspfile_args(rspfile): - ''' - Response files are useful on Windows where there is a command-line character - limit of 8191 because when passing sources as arguments to glib-mkenums this - limit can be exceeded in large codebases. - - There is no specification for response files and each tool that supports it - generally writes them out in slightly different ways, but some sources are: - https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files - https://docs.microsoft.com/en-us/windows/desktop/midl/the-response-file-command - ''' - import shlex - if not os.path.isfile(rspfile): - sys.exit('Response file {!r} does not exist'.format(rspfile)) - try: - with open(rspfile, 'r') as f: - cmdline = f.read() - except OSError as e: - sys.exit('Response file {!r} could not be read: {}' - .format(rspfile, e.strerror)) - return shlex.split(cmdline) - - -# Support reading an rspfile of the form @filename which contains the args -# to be parsed -if sys.argv[-1].startswith('@'): - args = sys.argv[0:-1] + get_rspfile_args(sys.argv[-1][1:]) -else: - args = sys.argv - -from giscanner.scannermain import scanner_main -sys.exit(scanner_main(args)) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus-codegen b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus-codegen deleted file mode 100644 index b41ed6b37..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus-codegen +++ /dev/null @@ -1,57 +0,0 @@ -#!C:\projects\repos\cerbero.git\1.28\build\build-tools\bin\python.exe - -# GDBus - GLib D-Bus Library -# -# Copyright (C) 2008-2011 Red Hat, Inc. -# -# SPDX-License-Identifier: LGPL-2.1-or-later -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General -# Public License along with this library; if not, see . -# -# Author: David Zeuthen - - -import os -import sys - -srcdir = os.getenv('UNINSTALLED_GLIB_SRCDIR', None) -filedir = os.path.dirname(__file__) - -if srcdir is not None: - path = os.path.join(srcdir, 'gio', 'gdbus-2.0') -elif os.path.basename(filedir) == 'bin': - # Make the prefix containing gdbus-codegen 'relocatable' at runtime by - # adding /some/prefix/bin/../share/glib-2.0 to the python path - path = os.path.join(filedir, '..', 'share', 'glib-2.0') -else: - # Assume that the modules we need are in the current directory and add the - # parent directory to the python path. - path = os.path.join(filedir, '..') - -# Canonicalize, then do further testing -path = os.path.abspath(path) - -# If the above path detection failed, use the hard-coded datadir. This can -# happen when, for instance, bindir and datadir are not in the same prefix or -# on Windows where we cannot make any guarantees about the directory structure. -# -# In these cases our installation cannot be relocatable, but at least we should -# be able to find the codegen module. -if not os.path.isfile(os.path.join(path, 'codegen', 'codegen_main.py')): - path = os.path.join('C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share', 'glib-2.0') - -sys.path.insert(0, path) -from codegen import codegen_main - -sys.exit(codegen_main.codegen_main()) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus.exe deleted file mode 100644 index 30163f69b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdbus.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-csource.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-csource.exe deleted file mode 100644 index b87483411..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-csource.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-query-loaders.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-query-loaders.exe deleted file mode 100644 index 1407281d8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk-pixbuf-query-loaders.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk_pixbuf-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk_pixbuf-2.0-0.dll deleted file mode 100644 index fa787363c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gdk_pixbuf-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-1.0-0.dll deleted file mode 100644 index 7a3f44e87..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-launch-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-launch-1.0.exe deleted file mode 100644 index ee2b8e9a8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/ges-launch-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-2.0-0.dll deleted file mode 100644 index 56235522e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-querymodules.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-querymodules.exe deleted file mode 100644 index ebc9a14cf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gio-querymodules.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/girepository-1.0-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/girepository-1.0-1.dll deleted file mode 100644 index ada1ff718..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/girepository-1.0-1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-2.0-0.dll deleted file mode 100644 index a9e79cd53..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-resources.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-resources.exe deleted file mode 100644 index 003d72a31..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-resources.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-schemas.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-schemas.exe deleted file mode 100644 index 5bfbd6396..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-compile-schemas.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-genmarshal b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-genmarshal deleted file mode 100644 index bc3fe55ac..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-genmarshal +++ /dev/null @@ -1,1080 +0,0 @@ -#!C:\projects\repos\cerbero.git\1.28\build\build-tools\bin\python.exe - -# pylint: disable=too-many-lines, missing-docstring, invalid-name - -# This file is part of GLib -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, see . - -import argparse -import os -import re -import sys - -VERSION_STR = '''glib-genmarshal version 2.82.4 -glib-genmarshal comes with ABSOLUTELY NO WARRANTY. -You may redistribute copies of glib-genmarshal under the terms of -the GNU General Public License which can be found in the -GLib source package. Sources, examples and contact -information are available at http://www.gtk.org''' - -GETTERS_STR = '''#ifdef G_ENABLE_DEBUG -#define g_marshal_value_peek_boolean(v) g_value_get_boolean (v) -#define g_marshal_value_peek_char(v) g_value_get_schar (v) -#define g_marshal_value_peek_uchar(v) g_value_get_uchar (v) -#define g_marshal_value_peek_int(v) g_value_get_int (v) -#define g_marshal_value_peek_uint(v) g_value_get_uint (v) -#define g_marshal_value_peek_long(v) g_value_get_long (v) -#define g_marshal_value_peek_ulong(v) g_value_get_ulong (v) -#define g_marshal_value_peek_int64(v) g_value_get_int64 (v) -#define g_marshal_value_peek_uint64(v) g_value_get_uint64 (v) -#define g_marshal_value_peek_enum(v) g_value_get_enum (v) -#define g_marshal_value_peek_flags(v) g_value_get_flags (v) -#define g_marshal_value_peek_float(v) g_value_get_float (v) -#define g_marshal_value_peek_double(v) g_value_get_double (v) -#define g_marshal_value_peek_string(v) (char*) g_value_get_string (v) -#define g_marshal_value_peek_param(v) g_value_get_param (v) -#define g_marshal_value_peek_boxed(v) g_value_get_boxed (v) -#define g_marshal_value_peek_pointer(v) g_value_get_pointer (v) -#define g_marshal_value_peek_object(v) g_value_get_object (v) -#define g_marshal_value_peek_variant(v) g_value_get_variant (v) -#else /* !G_ENABLE_DEBUG */ -/* WARNING: This code accesses GValues directly, which is UNSUPPORTED API. - * Do not access GValues directly in your code. Instead, use the - * g_value_get_*() functions - */ -#define g_marshal_value_peek_boolean(v) (v)->data[0].v_int -#define g_marshal_value_peek_char(v) (v)->data[0].v_int -#define g_marshal_value_peek_uchar(v) (v)->data[0].v_uint -#define g_marshal_value_peek_int(v) (v)->data[0].v_int -#define g_marshal_value_peek_uint(v) (v)->data[0].v_uint -#define g_marshal_value_peek_long(v) (v)->data[0].v_long -#define g_marshal_value_peek_ulong(v) (v)->data[0].v_ulong -#define g_marshal_value_peek_int64(v) (v)->data[0].v_int64 -#define g_marshal_value_peek_uint64(v) (v)->data[0].v_uint64 -#define g_marshal_value_peek_enum(v) (v)->data[0].v_long -#define g_marshal_value_peek_flags(v) (v)->data[0].v_ulong -#define g_marshal_value_peek_float(v) (v)->data[0].v_float -#define g_marshal_value_peek_double(v) (v)->data[0].v_double -#define g_marshal_value_peek_string(v) (v)->data[0].v_pointer -#define g_marshal_value_peek_param(v) (v)->data[0].v_pointer -#define g_marshal_value_peek_boxed(v) (v)->data[0].v_pointer -#define g_marshal_value_peek_pointer(v) (v)->data[0].v_pointer -#define g_marshal_value_peek_object(v) (v)->data[0].v_pointer -#define g_marshal_value_peek_variant(v) (v)->data[0].v_pointer -#endif /* !G_ENABLE_DEBUG */''' - -DEPRECATED_MSG_STR = 'The token "{}" is deprecated; use "{}" instead' - -VA_ARG_STR = \ - ' arg{:d} = ({:s}) va_arg (args_copy, {:s});' -STATIC_CHECK_STR = \ - '(param_types[{:d}] & G_SIGNAL_TYPE_STATIC_SCOPE) == 0 && ' -BOX_TYPED_STR = \ - ' arg{idx:d} = {box_func} (param_types[{idx:d}] & ~G_SIGNAL_TYPE_STATIC_SCOPE, arg{idx:d});' -BOX_UNTYPED_STR = \ - ' arg{idx:d} = {box_func} (arg{idx:d});' -UNBOX_TYPED_STR = \ - ' {unbox_func} (param_types[{idx:d}] & ~G_SIGNAL_TYPE_STATIC_SCOPE, arg{idx:d});' -UNBOX_UNTYPED_STR = \ - ' {unbox_func} (arg{idx:d});' - -STD_PREFIX = 'g_cclosure_marshal' - -# These are part of our ABI; keep this in sync with gmarshal.h -GOBJECT_MARSHALLERS = { - 'g_cclosure_marshal_VOID__VOID', - 'g_cclosure_marshal_VOID__BOOLEAN', - 'g_cclosure_marshal_VOID__CHAR', - 'g_cclosure_marshal_VOID__UCHAR', - 'g_cclosure_marshal_VOID__INT', - 'g_cclosure_marshal_VOID__UINT', - 'g_cclosure_marshal_VOID__LONG', - 'g_cclosure_marshal_VOID__ULONG', - 'g_cclosure_marshal_VOID__ENUM', - 'g_cclosure_marshal_VOID__FLAGS', - 'g_cclosure_marshal_VOID__FLOAT', - 'g_cclosure_marshal_VOID__DOUBLE', - 'g_cclosure_marshal_VOID__STRING', - 'g_cclosure_marshal_VOID__PARAM', - 'g_cclosure_marshal_VOID__BOXED', - 'g_cclosure_marshal_VOID__POINTER', - 'g_cclosure_marshal_VOID__OBJECT', - 'g_cclosure_marshal_VOID__VARIANT', - 'g_cclosure_marshal_VOID__UINT_POINTER', - 'g_cclosure_marshal_BOOLEAN__FLAGS', - 'g_cclosure_marshal_STRING__OBJECT_POINTER', - 'g_cclosure_marshal_BOOLEAN__BOXED_BOXED', -} - - -# pylint: disable=too-few-public-methods -class Color: - '''ANSI Terminal colors''' - GREEN = '\033[1;32m' - BLUE = '\033[1;34m' - YELLOW = '\033[1;33m' - RED = '\033[1;31m' - END = '\033[0m' - - -def print_color(msg, color=Color.END, prefix='MESSAGE'): - '''Print a string with a color prefix''' - if os.isatty(sys.stderr.fileno()): - real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END) - else: - real_prefix = prefix - sys.stderr.write('{prefix}: {msg}\n'.format(prefix=real_prefix, msg=msg)) - - -def print_error(msg): - '''Print an error, and terminate''' - print_color(msg, color=Color.RED, prefix='ERROR') - sys.exit(1) - - -def print_warning(msg, fatal=False): - '''Print a warning, and optionally terminate''' - if fatal: - color = Color.RED - prefix = 'ERROR' - else: - color = Color.YELLOW - prefix = 'WARNING' - print_color(msg, color, prefix) - if fatal: - sys.exit(1) - - -def print_info(msg): - '''Print a message''' - print_color(msg, color=Color.GREEN, prefix='INFO') - - -def generate_licensing_comment(outfile): - outfile.write('/* This file is generated by glib-genmarshal, do not ' - 'modify it. This code is licensed under the same license as ' - 'the containing project. Note that it links to GLib, so ' - 'must comply with the LGPL linking clauses. */\n') - - -def generate_header_preamble(outfile, prefix='', std_includes=True, use_pragma=False): - '''Generate the preamble for the marshallers header file''' - generate_licensing_comment(outfile) - - if use_pragma: - outfile.write('#pragma once\n') - outfile.write('\n') - else: - outfile.write('#ifndef __{}_MARSHAL_H__\n'.format(prefix.upper())) - outfile.write('#define __{}_MARSHAL_H__\n'.format(prefix.upper())) - outfile.write('\n') - # Maintain compatibility with the old C-based tool - if std_includes: - outfile.write('#include \n') - outfile.write('\n') - - outfile.write('G_BEGIN_DECLS\n') - outfile.write('\n') - - -def generate_header_postamble(outfile, prefix='', use_pragma=False): - '''Generate the postamble for the marshallers header file''' - outfile.write('\n') - outfile.write('G_END_DECLS\n') - - if not use_pragma: - outfile.write('\n') - outfile.write('#endif /* __{}_MARSHAL_H__ */\n'.format(prefix.upper())) - - -def generate_body_preamble(outfile, std_includes=True, include_headers=None, cpp_defines=None, cpp_undefines=None): - '''Generate the preamble for the marshallers source file''' - generate_licensing_comment(outfile) - - for header in (include_headers or []): - outfile.write('#include "{}"\n'.format(header)) - if include_headers: - outfile.write('\n') - - for define in (cpp_defines or []): - s = define.split('=') - symbol = s[0] - value = s[1] if len(s) > 1 else '1' - outfile.write('#define {} {}\n'.format(symbol, value)) - if cpp_defines: - outfile.write('\n') - - for undefine in (cpp_undefines or []): - outfile.write('#undef {}\n'.format(undefine)) - if cpp_undefines: - outfile.write('\n') - - if std_includes: - outfile.write('#include \n') - outfile.write('\n') - - outfile.write(GETTERS_STR) - outfile.write('\n\n') - - -# Marshaller arguments, as a dictionary where the key is the token used in -# the source file, and the value is another dictionary with the following -# keys: -# -# - signal: the token used in the marshaller prototype (mandatory) -# - ctype: the C type for the marshaller argument (mandatory) -# - getter: the function used to retrieve the argument from the GValue -# array when invoking the callback (optional) -# - promoted: the C type used by va_arg() to retrieve the argument from -# the va_list when invoking the callback (optional, only used when -# generating va_list marshallers) -# - box: an array of two elements, containing the boxing and unboxing -# functions for the given type (optional, only used when generating -# va_list marshallers) -# - static-check: a boolean value, if the given type should perform -# a static type check before boxing or unboxing the argument (optional, -# only used when generating va_list marshallers) -# - takes-type: a boolean value, if the boxing and unboxing functions -# for the given type require the type (optional, only used when -# generating va_list marshallers) -# - deprecated: whether the token has been deprecated (optional) -# - replaced-by: the token used to replace a deprecated token (optional, -# only used if deprecated is True) -IN_ARGS = { - 'VOID': { - 'signal': 'VOID', - 'ctype': 'void', - }, - 'BOOLEAN': { - 'signal': 'BOOLEAN', - 'ctype': 'gboolean', - 'getter': 'g_marshal_value_peek_boolean', - }, - 'CHAR': { - 'signal': 'CHAR', - 'ctype': 'gchar', - 'promoted': 'gint', - 'getter': 'g_marshal_value_peek_char', - }, - 'UCHAR': { - 'signal': 'UCHAR', - 'ctype': 'guchar', - 'promoted': 'guint', - 'getter': 'g_marshal_value_peek_uchar', - }, - 'INT': { - 'signal': 'INT', - 'ctype': 'gint', - 'getter': 'g_marshal_value_peek_int', - }, - 'UINT': { - 'signal': 'UINT', - 'ctype': 'guint', - 'getter': 'g_marshal_value_peek_uint', - }, - 'LONG': { - 'signal': 'LONG', - 'ctype': 'glong', - 'getter': 'g_marshal_value_peek_long', - }, - 'ULONG': { - 'signal': 'ULONG', - 'ctype': 'gulong', - 'getter': 'g_marshal_value_peek_ulong', - }, - 'INT64': { - 'signal': 'INT64', - 'ctype': 'gint64', - 'getter': 'g_marshal_value_peek_int64', - }, - 'UINT64': { - 'signal': 'UINT64', - 'ctype': 'guint64', - 'getter': 'g_marshal_value_peek_uint64', - }, - 'ENUM': { - 'signal': 'ENUM', - 'ctype': 'gint', - 'getter': 'g_marshal_value_peek_enum', - }, - 'FLAGS': { - 'signal': 'FLAGS', - 'ctype': 'guint', - 'getter': 'g_marshal_value_peek_flags', - }, - 'FLOAT': { - 'signal': 'FLOAT', - 'ctype': 'gfloat', - 'promoted': 'gdouble', - 'getter': 'g_marshal_value_peek_float', - }, - 'DOUBLE': { - 'signal': 'DOUBLE', - 'ctype': 'gdouble', - 'getter': 'g_marshal_value_peek_double', - }, - 'STRING': { - 'signal': 'STRING', - 'ctype': 'gpointer', - 'getter': 'g_marshal_value_peek_string', - 'box': ['g_strdup', 'g_free'], - 'static-check': True, - }, - 'PARAM': { - 'signal': 'PARAM', - 'ctype': 'gpointer', - 'getter': 'g_marshal_value_peek_param', - 'box': ['g_param_spec_ref', 'g_param_spec_unref'], - 'static-check': True, - }, - 'BOXED': { - 'signal': 'BOXED', - 'ctype': 'gpointer', - 'getter': 'g_marshal_value_peek_boxed', - 'box': ['g_boxed_copy', 'g_boxed_free'], - 'static-check': True, - 'takes-type': True, - }, - 'POINTER': { - 'signal': 'POINTER', - 'ctype': 'gpointer', - 'getter': 'g_marshal_value_peek_pointer', - }, - 'OBJECT': { - 'signal': 'OBJECT', - 'ctype': 'gpointer', - 'getter': 'g_marshal_value_peek_object', - 'box': ['g_object_ref', 'g_object_unref'], - }, - 'VARIANT': { - 'signal': 'VARIANT', - 'ctype': 'gpointer', - 'getter': 'g_marshal_value_peek_variant', - 'box': ['g_variant_ref_sink', 'g_variant_unref'], - 'static-check': True, - 'takes-type': False, - }, - - # Deprecated tokens - 'NONE': { - 'signal': 'VOID', - 'ctype': 'void', - 'deprecated': True, - 'replaced_by': 'VOID' - }, - 'BOOL': { - 'signal': 'BOOLEAN', - 'ctype': 'gboolean', - 'getter': 'g_marshal_value_peek_boolean', - 'deprecated': True, - 'replaced_by': 'BOOLEAN' - } -} - - -# Marshaller return values, as a dictionary where the key is the token used -# in the source file, and the value is another dictionary with the following -# keys: -# -# - signal: the token used in the marshaller prototype (mandatory) -# - ctype: the C type for the marshaller argument (mandatory) -# - setter: the function used to set the return value of the callback -# into a GValue (optional) -# - deprecated: whether the token has been deprecated (optional) -# - replaced-by: the token used to replace a deprecated token (optional, -# only used if deprecated is True) -OUT_ARGS = { - 'VOID': { - 'signal': 'VOID', - 'ctype': 'void', - }, - 'BOOLEAN': { - 'signal': 'BOOLEAN', - 'ctype': 'gboolean', - 'setter': 'g_value_set_boolean', - }, - 'CHAR': { - 'signal': 'CHAR', - 'ctype': 'gchar', - 'setter': 'g_value_set_char', - }, - 'UCHAR': { - 'signal': 'UCHAR', - 'ctype': 'guchar', - 'setter': 'g_value_set_uchar', - }, - 'INT': { - 'signal': 'INT', - 'ctype': 'gint', - 'setter': 'g_value_set_int', - }, - 'UINT': { - 'signal': 'UINT', - 'ctype': 'guint', - 'setter': 'g_value_set_uint', - }, - 'LONG': { - 'signal': 'LONG', - 'ctype': 'glong', - 'setter': 'g_value_set_long', - }, - 'ULONG': { - 'signal': 'ULONG', - 'ctype': 'gulong', - 'setter': 'g_value_set_ulong', - }, - 'INT64': { - 'signal': 'INT64', - 'ctype': 'gint64', - 'setter': 'g_value_set_int64', - }, - 'UINT64': { - 'signal': 'UINT64', - 'ctype': 'guint64', - 'setter': 'g_value_set_uint64', - }, - 'ENUM': { - 'signal': 'ENUM', - 'ctype': 'gint', - 'setter': 'g_value_set_enum', - }, - 'FLAGS': { - 'signal': 'FLAGS', - 'ctype': 'guint', - 'setter': 'g_value_set_flags', - }, - 'FLOAT': { - 'signal': 'FLOAT', - 'ctype': 'gfloat', - 'setter': 'g_value_set_float', - }, - 'DOUBLE': { - 'signal': 'DOUBLE', - 'ctype': 'gdouble', - 'setter': 'g_value_set_double', - }, - 'STRING': { - 'signal': 'STRING', - 'ctype': 'gchar*', - 'setter': 'g_value_take_string', - }, - 'PARAM': { - 'signal': 'PARAM', - 'ctype': 'GParamSpec*', - 'setter': 'g_value_take_param', - }, - 'BOXED': { - 'signal': 'BOXED', - 'ctype': 'gpointer', - 'setter': 'g_value_take_boxed', - }, - 'POINTER': { - 'signal': 'POINTER', - 'ctype': 'gpointer', - 'setter': 'g_value_set_pointer', - }, - 'OBJECT': { - 'signal': 'OBJECT', - 'ctype': 'GObject*', - 'setter': 'g_value_take_object', - }, - 'VARIANT': { - 'signal': 'VARIANT', - 'ctype': 'GVariant*', - 'setter': 'g_value_take_variant', - }, - - # Deprecated tokens - 'NONE': { - 'signal': 'VOID', - 'ctype': 'void', - 'setter': None, - 'deprecated': True, - 'replaced_by': 'VOID', - }, - 'BOOL': { - 'signal': 'BOOLEAN', - 'ctype': 'gboolean', - 'setter': 'g_value_set_boolean', - 'deprecated': True, - 'replaced_by': 'BOOLEAN', - }, -} - - -def check_args(retval, params, fatal_warnings=False): - '''Check the @retval and @params tokens for invalid and deprecated symbols.''' - if retval not in OUT_ARGS: - print_error('Unknown return value type "{}"'.format(retval)) - - if OUT_ARGS[retval].get('deprecated', False): - replaced_by = OUT_ARGS[retval]['replaced_by'] - print_warning(DEPRECATED_MSG_STR.format(retval, replaced_by), fatal_warnings) - - for param in params: - if param not in IN_ARGS: - print_error('Unknown parameter type "{}"'.format(param)) - else: - if IN_ARGS[param].get('deprecated', False): - replaced_by = IN_ARGS[param]['replaced_by'] - print_warning(DEPRECATED_MSG_STR.format(param, replaced_by), fatal_warnings) - - -def indent(text, level=0, fill=' '): - '''Indent @text by @level columns, using the @fill character''' - return ''.join([fill for x in range(level)]) + text - - -# pylint: disable=too-few-public-methods -class Visibility: - '''Symbol visibility options''' - NONE = 0 - INTERNAL = 1 - EXTERN = 2 - - -def generate_marshaller_name(prefix, retval, params, replace_deprecated=True): - '''Generate a marshaller name for the given @prefix, @retval, and @params. - If @replace_deprecated is True, the generated name will replace deprecated - tokens.''' - if replace_deprecated: - real_retval = OUT_ARGS[retval]['signal'] - real_params = [] - for param in params: - real_params.append(IN_ARGS[param]['signal']) - else: - real_retval = retval - real_params = params - return '{prefix}_{retval}__{args}'.format(prefix=prefix, - retval=real_retval, - args='_'.join(real_params)) - - -def generate_prototype(retval, params, - prefix='g_cclosure_user_marshal', - visibility=Visibility.NONE, - va_marshal=False): - '''Generate a marshaller declaration with the given @visibility. If @va_marshal - is True, the marshaller will use variadic arguments in place of a GValue array.''' - signature = [] - - if visibility == Visibility.INTERNAL: - signature += ['G_GNUC_INTERNAL'] - elif visibility == Visibility.EXTERN: - signature += ['extern'] - - function_name = generate_marshaller_name(prefix, retval, params) - - if not va_marshal: - signature += ['void ' + function_name + ' (GClosure *closure,'] - width = len('void ') + len(function_name) + 2 - - signature += [indent('GValue *return_value,', level=width, fill=' ')] - signature += [indent('guint n_param_values,', level=width, fill=' ')] - signature += [indent('const GValue *param_values,', level=width, fill=' ')] - signature += [indent('gpointer invocation_hint,', level=width, fill=' ')] - signature += [indent('gpointer marshal_data);', level=width, fill=' ')] - else: - signature += ['void ' + function_name + 'v (GClosure *closure,'] - width = len('void ') + len(function_name) + 3 - - signature += [indent('GValue *return_value,', level=width, fill=' ')] - signature += [indent('gpointer instance,', level=width, fill=' ')] - signature += [indent('va_list args,', level=width, fill=' ')] - signature += [indent('gpointer marshal_data,', level=width, fill=' ')] - signature += [indent('int n_params,', level=width, fill=' ')] - signature += [indent('GType *param_types);', level=width, fill=' ')] - - return signature - - -# pylint: disable=too-many-statements, too-many-locals, too-many-branches -def generate_body(retval, params, prefix, va_marshal=False): - '''Generate a marshaller definition. If @va_marshal is True, the marshaller - will use va_list and variadic arguments in place of a GValue array.''' - retval_setter = OUT_ARGS[retval].get('setter', None) - # If there's no return value then we can mark the retval argument as unused - # and get a minor optimisation, as well as avoid a compiler warning - if not retval_setter: - unused = ' G_GNUC_UNUSED' - else: - unused = '' - - body = ['void'] - - function_name = generate_marshaller_name(prefix, retval, params) - - if not va_marshal: - body += [function_name + ' (GClosure *closure,'] - width = len(function_name) + 2 - - body += [indent('GValue *return_value{},'.format(unused), level=width, fill=' ')] - body += [indent('guint n_param_values,', level=width, fill=' ')] - body += [indent('const GValue *param_values,', level=width, fill=' ')] - body += [indent('gpointer invocation_hint G_GNUC_UNUSED,', level=width, fill=' ')] - body += [indent('gpointer marshal_data)', level=width, fill=' ')] - else: - body += [function_name + 'v (GClosure *closure,'] - width = len(function_name) + 3 - - body += [indent('GValue *return_value{},'.format(unused), level=width, fill=' ')] - body += [indent('gpointer instance,', level=width, fill=' ')] - body += [indent('va_list args,', level=width, fill=' ')] - body += [indent('gpointer marshal_data,', level=width, fill=' ')] - body += [indent('int n_params,', level=width, fill=' ')] - body += [indent('GType *param_types)', level=width, fill=' ')] - - # Filter the arguments that have a getter - get_args = [x for x in params if IN_ARGS[x].get('getter', None) is not None] - - body += ['{'] - - # Generate the type of the marshaller function - typedef_marshal = generate_marshaller_name('GMarshalFunc', retval, params) - - typedef = ' typedef {ctype} (*{func_name}) ('.format(ctype=OUT_ARGS[retval]['ctype'], - func_name=typedef_marshal) - pad = len(typedef) - typedef += 'gpointer data1,' - body += [typedef] - - for idx, in_arg in enumerate(get_args): - body += [indent('{} arg{:d},'.format(IN_ARGS[in_arg]['ctype'], idx + 1), level=pad)] - - body += [indent('gpointer data2);', level=pad)] - - # Variable declarations - body += [' GCClosure *cc = (GCClosure *) closure;'] - body += [' gpointer data1, data2;'] - body += [' {} callback;'.format(typedef_marshal)] - - if retval_setter: - body += [' {} v_return;'.format(OUT_ARGS[retval]['ctype'])] - - if va_marshal: - for idx, arg in enumerate(get_args): - body += [' {} arg{:d};'.format(IN_ARGS[arg]['ctype'], idx)] - - if get_args: - body += [' va_list args_copy;'] - body += [''] - - body += [' va_copy (args_copy, args);'] - - for idx, arg in enumerate(get_args): - ctype = IN_ARGS[arg]['ctype'] - promoted_ctype = IN_ARGS[arg].get('promoted', ctype) - body += [VA_ARG_STR.format(idx, ctype, promoted_ctype)] - if IN_ARGS[arg].get('box', None): - box_func = IN_ARGS[arg]['box'][0] - if IN_ARGS[arg].get('static-check', False): - static_check = STATIC_CHECK_STR.format(idx) - else: - static_check = '' - arg_check = 'arg{:d} != NULL'.format(idx) - body += [' if ({}{})'.format(static_check, arg_check)] - if IN_ARGS[arg].get('takes-type', False): - body += [BOX_TYPED_STR.format(idx=idx, box_func=box_func)] - else: - body += [BOX_UNTYPED_STR.format(idx=idx, box_func=box_func)] - - body += [' va_end (args_copy);'] - - body += [''] - - # Preconditions check - if retval_setter: - body += [' g_return_if_fail (return_value != NULL);'] - - if not va_marshal: - body += [' g_return_if_fail (n_param_values == {:d});'.format(len(get_args) + 1)] - - body += [''] - - # Marshal instance, data, and callback set up - body += [' if (G_CCLOSURE_SWAP_DATA (closure))'] - body += [' {'] - body += [' data1 = closure->data;'] - if va_marshal: - body += [' data2 = instance;'] - else: - body += [' data2 = g_value_peek_pointer (param_values + 0);'] - body += [' }'] - body += [' else'] - body += [' {'] - if va_marshal: - body += [' data1 = instance;'] - else: - body += [' data1 = g_value_peek_pointer (param_values + 0);'] - body += [' data2 = closure->data;'] - body += [' }'] - # pylint: disable=line-too-long - body += [' callback = ({}) (marshal_data ? marshal_data : cc->callback);'.format(typedef_marshal)] - body += [''] - - # Marshal callback action - if retval_setter: - callback = ' {} callback ('.format(' v_return =') - else: - callback = ' callback (' - - pad = len(callback) - body += [callback + 'data1,'] - - if va_marshal: - for idx, arg in enumerate(get_args): - body += [indent('arg{:d},'.format(idx), level=pad)] - else: - for idx, arg in enumerate(get_args): - arg_getter = IN_ARGS[arg]['getter'] - body += [indent('{} (param_values + {:d}),'.format(arg_getter, idx + 1), level=pad)] - - body += [indent('data2);', level=pad)] - - if va_marshal: - boxed_args = [x for x in get_args if IN_ARGS[x].get('box', None) is not None] - if not boxed_args: - body += [''] - else: - for idx, arg in enumerate(get_args): - if not IN_ARGS[arg].get('box', None): - continue - unbox_func = IN_ARGS[arg]['box'][1] - if IN_ARGS[arg].get('static-check', False): - static_check = STATIC_CHECK_STR.format(idx) - else: - static_check = '' - arg_check = 'arg{:d} != NULL'.format(idx) - body += [' if ({}{})'.format(static_check, arg_check)] - if IN_ARGS[arg].get('takes-type', False): - body += [UNBOX_TYPED_STR.format(idx=idx, unbox_func=unbox_func)] - else: - body += [UNBOX_UNTYPED_STR.format(idx=idx, unbox_func=unbox_func)] - - if retval_setter: - body += [''] - body += [' {} (return_value, v_return);'.format(retval_setter)] - - body += ['}'] - - return body - - -def generate_marshaller_alias(outfile, marshaller, real_marshaller, - include_va=False, - source_location=None): - '''Generate an alias between @marshaller and @real_marshaller, including - an optional alias for va_list marshallers''' - if source_location: - outfile.write('/* {} */\n'.format(source_location)) - - outfile.write('#define {}\t{}\n'.format(marshaller, real_marshaller)) - - if include_va: - outfile.write('#define {}v\t{}v\n'.format(marshaller, real_marshaller)) - - outfile.write('\n') - - -def generate_marshallers_header(outfile, retval, params, - prefix='g_cclosure_user_marshal', - internal=False, - include_va=False, source_location=None): - '''Generate a declaration for a marshaller function, to be used in the header, - with the given @retval, @params, and @prefix. An optional va_list marshaller - for the same arguments is also generated. The generated buffer is written to - the @outfile stream object.''' - if source_location: - outfile.write('/* {} */\n'.format(source_location)) - - if internal: - visibility = Visibility.INTERNAL - else: - visibility = Visibility.EXTERN - - signature = generate_prototype(retval, params, prefix, visibility, False) - if include_va: - signature += generate_prototype(retval, params, prefix, visibility, True) - signature += [''] - - outfile.write('\n'.join(signature)) - outfile.write('\n') - - -def generate_marshallers_body(outfile, retval, params, - prefix='g_cclosure_user_marshal', - include_prototype=True, - internal=False, - include_va=False, source_location=None): - '''Generate a definition for a marshaller function, to be used in the source, - with the given @retval, @params, and @prefix. An optional va_list marshaller - for the same arguments is also generated. The generated buffer is written to - the @outfile stream object.''' - if source_location: - outfile.write('/* {} */\n'.format(source_location)) - - if include_prototype: - # Declaration visibility - if internal: - decl_visibility = Visibility.INTERNAL - else: - decl_visibility = Visibility.EXTERN - proto = ['/* Prototype for -Wmissing-prototypes */'] - # Add C++ guards in case somebody compiles the generated code - # with a C++ compiler - proto += ['G_BEGIN_DECLS'] - proto += generate_prototype(retval, params, prefix, decl_visibility, False) - proto += ['G_END_DECLS'] - outfile.write('\n'.join(proto)) - outfile.write('\n') - - body = generate_body(retval, params, prefix, False) - outfile.write('\n'.join(body)) - outfile.write('\n\n') - - if include_va: - if include_prototype: - # Declaration visibility - if internal: - decl_visibility = Visibility.INTERNAL - else: - decl_visibility = Visibility.EXTERN - proto = ['/* Prototype for -Wmissing-prototypes */'] - # Add C++ guards here as well - proto += ['G_BEGIN_DECLS'] - proto += generate_prototype(retval, params, prefix, decl_visibility, True) - proto += ['G_END_DECLS'] - outfile.write('\n'.join(proto)) - outfile.write('\n') - - body = generate_body(retval, params, prefix, True) - outfile.write('\n'.join(body)) - outfile.write('\n\n') - - -def parse_args(): - arg_parser = argparse.ArgumentParser(description='Generate signal marshallers for GObject') - arg_parser.add_argument('--prefix', metavar='STRING', - default='g_cclosure_user_marshal', - help='Specify marshaller prefix') - arg_parser.add_argument('--output', metavar='FILE', - type=argparse.FileType('w'), - default=sys.stdout, - help='Write output into the specified file') - arg_parser.add_argument('--skip-source', - action='store_true', - help='Skip source location comments') - arg_parser.add_argument('--internal', - action='store_true', - help='Mark generated functions as internal') - arg_parser.add_argument('--valist-marshallers', - action='store_true', - help='Generate va_list marshallers') - arg_parser.add_argument('-v', '--version', - action='store_true', - dest='show_version', - help='Print version information, and exit') - arg_parser.add_argument('--g-fatal-warnings', - action='store_true', - dest='fatal_warnings', - help='Make warnings fatal') - arg_parser.add_argument('--include-header', metavar='HEADER', nargs='?', - action='append', - dest='include_headers', - help='Include the specified header in the body') - arg_parser.add_argument('--pragma-once', - action='store_true', - help='Use "pragma once" as the inclusion guard') - arg_parser.add_argument('-D', - action='append', - dest='cpp_defines', - default=[], - help='Pre-processor define') - arg_parser.add_argument('-U', - action='append', - dest='cpp_undefines', - default=[], - help='Pre-processor undefine') - arg_parser.add_argument('files', metavar='FILE', nargs='*', - type=argparse.FileType('r'), - help='Files with lists of marshallers to generate, ' + - 'or "-" for standard input') - arg_parser.add_argument('--prototypes', - action='store_true', - help='Generate the marshallers prototype in the C code') - arg_parser.add_argument('--header', - action='store_true', - help='Generate C headers') - arg_parser.add_argument('--body', - action='store_true', - help='Generate C code') - - group = arg_parser.add_mutually_exclusive_group() - group.add_argument('--stdinc', - action='store_true', - dest='stdinc', default=True, - help='Include standard marshallers') - group.add_argument('--nostdinc', - action='store_false', - dest='stdinc', default=True, - help='Use standard marshallers') - - group = arg_parser.add_mutually_exclusive_group() - group.add_argument('--quiet', - action='store_true', - help='Only print warnings and errors') - group.add_argument('--verbose', - action='store_true', - help='Be verbose, and include debugging information') - - args = arg_parser.parse_args() - - if args.show_version: - print(VERSION_STR) - sys.exit(0) - - return args - - -def generate(args): - # Backward compatibility hack; some projects use both arguments to - # generate the marshallers prototype in the C source, even though - # it's not really a supported use case. We keep this behaviour by - # forcing the --prototypes and --body arguments instead. We make this - # warning non-fatal even with --g-fatal-warnings, as it's a deprecation - compatibility_mode = False - if args.header and args.body: - print_warning('Using --header and --body at the same time is deprecated; ' + - 'use --body --prototypes instead', False) - args.prototypes = True - args.header = False - compatibility_mode = True - - if args.header: - generate_header_preamble(args.output, - prefix=args.prefix, - std_includes=args.stdinc, - use_pragma=args.pragma_once) - elif args.body: - generate_body_preamble(args.output, - std_includes=args.stdinc, - include_headers=args.include_headers, - cpp_defines=args.cpp_defines, - cpp_undefines=args.cpp_undefines) - - seen_marshallers = set() - - for infile in args.files: - if not args.quiet: - print_info('Reading {}...'.format(infile.name)) - - line_count = 0 - for line in infile: - line_count += 1 - - if line == '\n' or line.startswith('#'): - continue - - matches = re.match(r'^([A-Z0-9]+)\s?:\s?([A-Z0-9,\s]+)$', line.strip()) - if not matches or len(matches.groups()) != 2: - print_warning('Invalid entry: "{}"'.format(line.strip()), args.fatal_warnings) - continue - - if not args.skip_source: - location = '{} ({}:{:d})'.format(line.strip(), infile.name, line_count) - else: - location = None - - retval = matches.group(1).strip() - params = [x.strip() for x in matches.group(2).split(',')] - check_args(retval, params, args.fatal_warnings) - - raw_marshaller = generate_marshaller_name(args.prefix, retval, params, False) - if raw_marshaller in seen_marshallers: - if args.verbose: - print_info('Skipping repeated marshaller {}'.format(line.strip())) - continue - - if args.header: - if args.verbose: - print_info('Generating declaration for {}'.format(line.strip())) - generate_std_alias = False - if args.stdinc: - std_marshaller = generate_marshaller_name(STD_PREFIX, retval, params) - if std_marshaller in GOBJECT_MARSHALLERS: - if args.verbose: - print_info('Skipping default marshaller {}'.format(line.strip())) - generate_std_alias = True - - marshaller = generate_marshaller_name(args.prefix, retval, params) - if generate_std_alias: - generate_marshaller_alias(args.output, marshaller, std_marshaller, - source_location=location, - include_va=args.valist_marshallers) - else: - generate_marshallers_header(args.output, retval, params, - prefix=args.prefix, - internal=args.internal, - include_va=args.valist_marshallers, - source_location=location) - # If the marshaller is defined using a deprecated token, we want to maintain - # compatibility and generate an alias for the old name pointing to the new - # one - if marshaller != raw_marshaller: - if args.verbose: - print_info('Generating alias for deprecated tokens') - generate_marshaller_alias(args.output, raw_marshaller, marshaller, - include_va=args.valist_marshallers) - elif args.body: - if args.verbose: - print_info('Generating definition for {}'.format(line.strip())) - generate_std_alias = False - if args.stdinc: - std_marshaller = generate_marshaller_name(STD_PREFIX, retval, params) - if std_marshaller in GOBJECT_MARSHALLERS: - if args.verbose: - print_info('Skipping default marshaller {}'.format(line.strip())) - generate_std_alias = True - marshaller = generate_marshaller_name(args.prefix, retval, params) - if generate_std_alias: - # We need to generate the alias if we are in compatibility mode - if compatibility_mode: - generate_marshaller_alias(args.output, marshaller, std_marshaller, - source_location=location, - include_va=args.valist_marshallers) - else: - generate_marshallers_body(args.output, retval, params, - prefix=args.prefix, - internal=args.internal, - include_prototype=args.prototypes, - include_va=args.valist_marshallers, - source_location=location) - if compatibility_mode and marshaller != raw_marshaller: - if args.verbose: - print_info('Generating alias for deprecated tokens') - generate_marshaller_alias(args.output, raw_marshaller, marshaller, - include_va=args.valist_marshallers) - - seen_marshallers.add(raw_marshaller) - - if args.header: - generate_header_postamble(args.output, prefix=args.prefix, use_pragma=args.pragma_once) - - -if __name__ == '__main__': - args = parse_args() - - with args.output: - generate(args) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-gettextize b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-gettextize deleted file mode 100644 index 9dc6f5d9b..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-gettextize +++ /dev/null @@ -1,189 +0,0 @@ -#! /bin/sh -# -# Copyright (C) 1995-1998, 2000, 2001 Free Software Foundation, Inc. -# -# SPDX-License-Identifier: GPL-2.0-or-later -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2, or (at your option) -# any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, see . -# - -# - Modified in October 2001 by jacob berkman to -# work with glib's Makefile.in.in and po2tbl.sed.in, to not copy in -# intl/, and to not add ChangeLog entries to po/ChangeLog - -# This file is meant for authors or maintainers which want to -# internationalize their package with the help of GNU gettext. For -# further information how to use it consult the GNU gettext manual. - -echo=echo -progname=$0 -force=0 -configstatus=0 -origdir=`pwd` -usage="\ -Usage: glib-gettextize [OPTION]... [package-dir] - --help print this help and exit - --version print version information and exit - -c, --copy copy files instead of making symlinks - -f, --force force writing of new files even if old exist -Report bugs to https://gitlab.gnome.org/GNOME/glib/issues/new." -package=glib -version=2.82.4 -try_ln_s=: - -# Directory where the sources are stored. -prefix=C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64 -case `uname` in -MINGW32*) - prefix="`dirname $0`/.." - ;; -esac - -datarootdir=C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share -datadir=C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share - -gettext_dir=$datadir/glib-2.0/gettext - -while test $# -gt 0; do - case "$1" in - -c | --copy | --c* ) - shift - try_ln_s=false ;; - -f | --force | --f* ) - shift - force=1 ;; - -r | --run | --r* ) - shift - configstatus=1 ;; - --help | --h* ) - $echo "$usage"; exit 0 ;; - --version | --v* ) - echo "$progname (GNU $package) $version" - $echo "Copyright (C) 1995-1998, 2000, 2001 Free Software Foundation, Inc. -This is free software; see the source for copying conditions. There is NO -warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." - $echo "Written by" "Ulrich Drepper" - exit 0 ;; - -- ) # Stop option processing - shift; break ;; - -* ) - $echo "glib-gettextize: unknown option $1" - $echo "Try \`glib-gettextize --help' for more information."; exit 1 ;; - * ) - break ;; - esac -done - -if test $# -gt 1; then - $echo "$usage" - exit 1 -fi - -# Fill in the command line options value. -if test $# -eq 1; then - srcdir=$1 - if cd "$srcdir"; then - srcdir=`pwd` - else - $echo "Cannot change directory to \`$srcdir'" - exit 1 - fi -else - srcdir=$origdir -fi - -test -f configure.in || test -f configure.ac || { - $echo "Missing configure.in or configure.ac, please cd to your package first." - exit 1 -} - -configure_in=NONE -if test -f configure.in; then - configure_in=configure.in -else - if test -f configure.ac; then - configure_in=configure.ac - fi -fi -# Check in which directory config.rpath, mkinstalldirs etc. belong. -auxdir=`cat "$configure_in" | grep '^AC_CONFIG_AUX_DIR' | sed -n -e 's/AC_CONFIG_AUX_DIR(\([^()]*\))/\1/p' | sed -e 's/^\[\(.*\)\]$/\1/' | sed -e 1q` -if test -n "$auxdir"; then - auxdir="$auxdir/" -fi - -if test -f po/Makefile.in.in && test $force -eq 0; then - $echo "\ -po/Makefile.in.in exists: use option -f if you really want to delete it." - exit 1 -fi - -test -d po || { - $echo "Creating po/ subdirectory" - mkdir po || { - $echo "failed to create po/ subdirectory" - exit 1 - } -} - -# For simplicity we changed to the gettext source directory. -cd $gettext_dir || { - $echo "gettext source directory '${gettext_dir}' doesn't exist" - exit 1 -} - -# Now copy all files. Take care for the destination directories. -for file in *; do - case $file in - intl | po) - ;; - mkinstalldirs) - rm -f "$srcdir/$auxdir$file" - ($try_ln_s && ln -s $gettext_dir/$file "$srcdir/$auxdir$file" && $echo "Symlinking file $file") 2>/dev/null || - { $echo "Copying file $file"; cp $file "$srcdir/$auxdir$file"; } - ;; - *) - rm -f "$srcdir/$file" - ($try_ln_s && ln -s $gettext_dir/$file "$srcdir/$file" && $echo "Symlinking file $file") 2>/dev/null || - { $echo "Copying file $file"; cp $file "$srcdir/$file"; } - ;; - esac -done - -# Copy files to po/ subdirectory. -cd po -for file in *; do - rm -f "$srcdir/po/$file" - ($try_ln_s && ln -s $gettext_dir/po/$file "$srcdir/po/$file" && $echo "Symlinking file po/$file") 2>/dev/null || - { $echo "Copying file po/$file"; cp $file "$srcdir/po/$file"; } -done -if test -f "$srcdir/po/cat-id-tbl.c"; then - $echo "Removing po/cat-id-tbl.c" - rm -f "$srcdir/po/cat-id-tbl.c" -fi -if test -f "$srcdir/po/stamp-cat-id"; then - $echo "Removing po/stamp-cat-id" - rm -f "$srcdir/po/stamp-cat-id" -fi - -echo -echo "Please add the files" -echo " codeset.m4 gettext.m4 glibc21.m4 iconv.m4 isc-posix.m4 lcmessage.m4" -echo " progtest.m4" -echo "from the $datadir/aclocal directory to your autoconf macro directory" -echo "or directly to your aclocal.m4 file." -echo "You will also need config.guess and config.sub, which you can get from" -echo "ftp://ftp.gnu.org/pub/gnu/config/." -echo - -exit 0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-mkenums b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-mkenums deleted file mode 100644 index 825003f1d..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/glib-mkenums +++ /dev/null @@ -1,816 +0,0 @@ -#!C:\projects\repos\cerbero.git\1.28\build\build-tools\bin\python.exe - -# If the code below looks horrible and unpythonic, do not panic. -# -# It is. -# -# This is a manual conversion from the original Perl script to -# Python. Improvements are welcome. -# -from __future__ import print_function, unicode_literals - -import argparse -import os -import re -import sys -import tempfile -import io -import errno -import codecs -import locale - -# Non-english locale systems might complain to unrecognized character -sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf-8') - -VERSION_STR = '''glib-mkenums version 2.82.4 -glib-mkenums comes with ABSOLUTELY NO WARRANTY. -You may redistribute copies of glib-mkenums under the terms of -the GNU General Public License which can be found in the -GLib source package. Sources, examples and contact -information are available at http://www.gtk.org''' - -# pylint: disable=too-few-public-methods -class Color: - '''ANSI Terminal colors''' - GREEN = '\033[1;32m' - BLUE = '\033[1;34m' - YELLOW = '\033[1;33m' - RED = '\033[1;31m' - END = '\033[0m' - - -def print_color(msg, color=Color.END, prefix='MESSAGE'): - '''Print a string with a color prefix''' - if os.isatty(sys.stderr.fileno()): - real_prefix = '{start}{prefix}{end}'.format(start=color, prefix=prefix, end=Color.END) - else: - real_prefix = prefix - print('{prefix}: {msg}'.format(prefix=real_prefix, msg=msg), file=sys.stderr) - - -def print_error(msg): - '''Print an error, and terminate''' - print_color(msg, color=Color.RED, prefix='ERROR') - sys.exit(1) - - -def print_warning(msg, fatal=False): - '''Print a warning, and optionally terminate''' - if fatal: - color = Color.RED - prefix = 'ERROR' - else: - color = Color.YELLOW - prefix = 'WARNING' - print_color(msg, color, prefix) - if fatal: - sys.exit(1) - - -def print_info(msg): - '''Print a message''' - print_color(msg, color=Color.GREEN, prefix='INFO') - - -def get_rspfile_args(rspfile): - ''' - Response files are useful on Windows where there is a command-line character - limit of 8191 because when passing sources as arguments to glib-mkenums this - limit can be exceeded in large codebases. - - There is no specification for response files and each tool that supports it - generally writes them out in slightly different ways, but some sources are: - https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-response-files - https://docs.microsoft.com/en-us/windows/desktop/midl/the-response-file-command - ''' - import shlex - if not os.path.isfile(rspfile): - sys.exit('Response file {!r} does not exist'.format(rspfile)) - try: - with open(rspfile, 'r') as f: - cmdline = f.read() - except OSError as e: - sys.exit('Response file {!r} could not be read: {}' - .format(rspfile, e.strerror)) - return shlex.split(cmdline) - - -def write_output(output): - global output_stream - print(output, file=output_stream) - - -# Python 2 defaults to ASCII in case stdout is redirected. -# This should make it match Python 3, which uses the locale encoding. -if sys.stdout.encoding is None: - output_stream = codecs.getwriter( - locale.getpreferredencoding())(sys.stdout) -else: - output_stream = sys.stdout - - -# Some source files aren't UTF-8 and the old perl version didn't care. -# Replace invalid data with a replacement character to keep things working. -# https://bugzilla.gnome.org/show_bug.cgi?id=785113#c20 -def replace_and_warn(err): - # 7 characters of context either side of the offending character - print_warning('UnicodeWarning: {} at {} ({})'.format( - err.reason, err.start, - err.object[err.start - 7:err.end + 7])) - return ('?', err.end) - -codecs.register_error('replace_and_warn', replace_and_warn) - - -# glib-mkenums.py -# Information about the current enumeration -flags = None # Is enumeration a bitmask? -option_underscore_name = '' # Overridden underscore variant of the enum name - # for example to fix the cases we don't get the - # mixed-case -> underscorized transform right. -option_lowercase_name = '' # DEPRECATED. A lower case name to use as part - # of the *_get_type() function, instead of the - # one that we guess. For instance, when an enum - # uses abnormal capitalization and we can not - # guess where to put the underscores. -option_since = '' # User provided version info for the enum. -seenbitshift = 0 # Have we seen bitshift operators? -seenprivate = False # Have we seen a private option? -enum_prefix = None # Prefix for this enumeration -enumname = '' # Name for this enumeration -enumshort = '' # $enumname without prefix -enumname_prefix = '' # prefix of $enumname -enumindex = 0 # Global enum counter -firstenum = 1 # Is this the first enumeration per file? -entries = [] # [ name, val ] for each entry -c_namespace = {} # C symbols namespace. - -output = '' # Filename to write result into - -def parse_trigraph(opts): - result = {} - for opt in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', opts): - opt = re.sub(r'^\s*', '', opt) - opt = re.sub(r'\s*$', '', opt) - m = re.search(r'(\w+)(?:=(.+))?', opt) - assert m is not None - groups = m.groups() - key = groups[0] - if len(groups) > 1: - val = groups[1] - else: - val = 1 - result[key] = val.strip('"') if val is not None else None - return result - -def parse_entries(file, file_name): - global entries, enumindex, enumname, seenbitshift, seenprivate, flags - looking_for_name = False - - while True: - line = file.readline() - if not line: - break - - line = line.strip() - - # read lines until we have no open comments - while re.search(r'/\*([^*]|\*(?!/))*$', line): - line += file.readline() - - # strip comments w/o options - line = re.sub(r'''/\*(?!<) - ([^*]+|\*(?!/))* - \*/''', '', line, flags=re.X) - - line = line.rstrip() - - # skip empty lines - if len(line.strip()) == 0: - continue - - if looking_for_name: - m = re.match(r'\s*(\w+)', line) - if m: - enumname = m.group(1) - return True - - # Handle include files - m = re.match(r'\#include\s*<([^>]*)>', line) - if m: - newfilename = os.path.join("..", m.group(1)) - newfile = io.open(newfilename, encoding="utf-8", - errors="replace_and_warn") - - if not parse_entries(newfile, newfilename): - return False - else: - continue - - m = re.match(r'\s*\}\s*(\w+)', line) - if m: - enumname = m.group(1) - enumindex += 1 - return 1 - - m = re.match(r'\s*\}', line) - if m: - enumindex += 1 - looking_for_name = True - continue - - m = re.match(r'''\s* - (\w+)\s* # name - (\s+[A-Z]+_(?:AVAILABLE|DEPRECATED)_ENUMERATOR_IN_[0-9_]+(?:_FOR\s*\(\s*\w+\s*\))?\s*)? # availability - (?:=( # value - \s*'[^']*'\s* # char - | # OR - \s*\w+\s*\(.*\)\s* # macro with multiple args - | # OR - (?:[^,/]|/(?!\*))* # anything but a comma or comment - ))?,?\s* - (?:/\*< # options - (([^*]|\*(?!/))*) - >\s*\*/)?,? - \s*$''', line, flags=re.X) - if m: - groups = m.groups() - name = groups[0] - availability = None - value = None - options = None - if len(groups) > 1: - availability = groups[1] - if len(groups) > 2: - value = groups[2] - if len(groups) > 3: - options = groups[3] - if flags is None and value is not None and '<<' in value: - seenbitshift = 1 - - if options is not None: - options = parse_trigraph(options) - if 'skip' not in options: - entries.append((name, value, seenprivate, options.get('nick'))) - else: - entries.append((name, value, seenprivate)) - else: - m = re.match(r'''\s* - /\*< (([^*]|\*(?!/))*) >\s*\*/ - \s*$''', line, flags=re.X) - if m: - options = m.groups()[0] - if options is not None: - options = parse_trigraph(options) - if 'private' in options: - seenprivate = True - continue - if 'public' in options: - seenprivate = False - continue - if re.match(r's*\#', line): - pass - else: - print_warning('Failed to parse "{}" in {}'.format(line, file_name)) - return False - -help_epilog = '''Production text substitutions: - \u0040EnumName\u0040 PrefixTheXEnum - \u0040enum_name\u0040 prefix_the_xenum - \u0040ENUMNAME\u0040 PREFIX_THE_XENUM - \u0040ENUMSHORT\u0040 THE_XENUM - \u0040ENUMPREFIX\u0040 PREFIX - \u0040enumsince\u0040 the user-provided since value given - \u0040VALUENAME\u0040 PREFIX_THE_XVALUE - \u0040valuenick\u0040 the-xvalue - \u0040valuenum\u0040 the integer value (limited support, Since: 2.26) - \u0040type\u0040 either enum or flags - \u0040Type\u0040 either Enum or Flags - \u0040TYPE\u0040 either ENUM or FLAGS - \u0040filename\u0040 name of current input file - \u0040basename\u0040 base name of the current input file (Since: 2.22) -''' - - -# production variables: -idprefix = "" # "G", "Gtk", etc -symprefix = "" # "g", "gtk", etc, if not just lc($idprefix) -fhead = "" # output file header -fprod = "" # per input file production -ftail = "" # output file trailer -eprod = "" # per enum text (produced prior to value itarations) -vhead = "" # value header, produced before iterating over enum values -vprod = "" # value text, produced for each enum value -vtail = "" # value tail, produced after iterating over enum values -comment_tmpl = "" # comment template - -def read_template_file(file): - global idprefix, symprefix, fhead, fprod, ftail, eprod, vhead, vprod, vtail, comment_tmpl - tmpl = {'file-header': fhead, - 'file-production': fprod, - 'file-tail': ftail, - 'enumeration-production': eprod, - 'value-header': vhead, - 'value-production': vprod, - 'value-tail': vtail, - 'comment': comment_tmpl, - } - in_ = 'junk' - - ifile = io.open(file, encoding="utf-8", errors="replace_and_warn") - for line in ifile: - m = re.match(r'\/\*\*\*\s+(BEGIN|END)\s+([\w-]+)\s+\*\*\*\/', line) - if m: - if in_ == 'junk' and m.group(1) == 'BEGIN' and m.group(2) in tmpl: - in_ = m.group(2) - continue - elif in_ == m.group(2) and m.group(1) == 'END' and m.group(2) in tmpl: - in_ = 'junk' - continue - else: - sys.exit("Malformed template file " + file) - - if in_ != 'junk': - tmpl[in_] += line - - if in_ != 'junk': - sys.exit("Malformed template file " + file) - - fhead = tmpl['file-header'] - fprod = tmpl['file-production'] - ftail = tmpl['file-tail'] - eprod = tmpl['enumeration-production'] - vhead = tmpl['value-header'] - vprod = tmpl['value-production'] - vtail = tmpl['value-tail'] - comment_tmpl = tmpl['comment'] - -parser = argparse.ArgumentParser(epilog=help_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter) - -parser.add_argument('--identifier-prefix', default='', dest='idprefix', - help='Identifier prefix') -parser.add_argument('--symbol-prefix', default='', dest='symprefix', - help='Symbol prefix') -parser.add_argument('--fhead', default=[], dest='fhead', action='append', - help='Output file header') -parser.add_argument('--ftail', default=[], dest='ftail', action='append', - help='Output file footer') -parser.add_argument('--fprod', default=[], dest='fprod', action='append', - help='Put out TEXT every time a new input file is being processed.') -parser.add_argument('--eprod', default=[], dest='eprod', action='append', - help='Per enum text, produced prior to value iterations') -parser.add_argument('--vhead', default=[], dest='vhead', action='append', - help='Value header, produced before iterating over enum values') -parser.add_argument('--vprod', default=[], dest='vprod', action='append', - help='Value text, produced for each enum value.') -parser.add_argument('--vtail', default=[], dest='vtail', action='append', - help='Value tail, produced after iterating over enum values') -parser.add_argument('--comments', default='', dest='comment_tmpl', - help='Comment structure') -parser.add_argument('--template', default='', dest='template', - help='Template file') -parser.add_argument('--output', default=None, dest='output') -parser.add_argument('--version', '-v', default=False, action='store_true', dest='version', - help='Print version information') -parser.add_argument('args', nargs='*', - help='One or more input files, or a single argument @rspfile_path ' - 'pointing to a file that contains the actual arguments') - -# Support reading an rspfile of the form @filename which contains the args -# to be parsed -if len(sys.argv) == 2 and sys.argv[1].startswith('@'): - args = get_rspfile_args(sys.argv[1][1:]) -else: - args = sys.argv[1:] - -options = parser.parse_args(args) - -if options.version: - print(VERSION_STR) - sys.exit(0) - -def unescape_cmdline_args(arg): - arg = arg.replace('\\n', '\n') - arg = arg.replace('\\r', '\r') - return arg.replace('\\t', '\t') - -if options.template != '': - read_template_file(options.template) - -idprefix += options.idprefix -symprefix += options.symprefix - -# This is a hack to maintain some semblance of backward compatibility with -# the old, Perl-based glib-mkenums. The old tool had an implicit ordering -# on the arguments and templates; each argument was parsed in order, and -# all the strings appended. This allowed developers to write: -# -# glib-mkenums \ -# --fhead ... \ -# --template a-template-file.c.in \ -# --ftail ... -# -# And have the fhead be prepended to the file-head stanza in the template, -# as well as the ftail be appended to the file-tail stanza in the template. -# Short of throwing away ArgumentParser and going over sys.argv[] element -# by element, we can simulate that behaviour by ensuring some ordering in -# how we build the template strings: -# -# - the head stanzas are always prepended to the template -# - the prod stanzas are always appended to the template -# - the tail stanzas are always appended to the template -# -# Within each instance of the command line argument, we append each value -# to the array in the order in which it appears on the command line. -fhead = ''.join([unescape_cmdline_args(x) for x in options.fhead]) + fhead -vhead = ''.join([unescape_cmdline_args(x) for x in options.vhead]) + vhead - -fprod += ''.join([unescape_cmdline_args(x) for x in options.fprod]) -eprod += ''.join([unescape_cmdline_args(x) for x in options.eprod]) -vprod += ''.join([unescape_cmdline_args(x) for x in options.vprod]) - -ftail = ftail + ''.join([unescape_cmdline_args(x) for x in options.ftail]) -vtail = vtail + ''.join([unescape_cmdline_args(x) for x in options.vtail]) - -if options.comment_tmpl != '': - comment_tmpl = unescape_cmdline_args(options.comment_tmpl) -elif comment_tmpl == "": - # default to C-style comments - comment_tmpl = "/* \u0040comment\u0040 */" - -output = options.output - -if output is not None: - (out_dir, out_fn) = os.path.split(options.output) - out_suffix = '_' + os.path.splitext(out_fn)[1] - if out_dir == '': - out_dir = '.' - fd, filename = tempfile.mkstemp(dir=out_dir) - os.close(fd) - tmpfile = io.open(filename, "w", encoding="utf-8") - output_stream = tmpfile -else: - tmpfile = None - -# put auto-generation comment -comment = comment_tmpl.replace('\u0040comment\u0040', - 'This file is generated by glib-mkenums, do ' - 'not modify it. This code is licensed under ' - 'the same license as the containing project. ' - 'Note that it links to GLib, so must comply ' - 'with the LGPL linking clauses.') -write_output("\n" + comment + '\n') - -def replace_specials(prod): - prod = prod.replace(r'\\a', r'\a') - prod = prod.replace(r'\\b', r'\b') - prod = prod.replace(r'\\t', r'\t') - prod = prod.replace(r'\\n', r'\n') - prod = prod.replace(r'\\f', r'\f') - prod = prod.replace(r'\\r', r'\r') - prod = prod.rstrip() - return prod - - -def warn_if_filename_basename_used(section, prod): - for substitution in ('\u0040filename\u0040', - '\u0040basename\u0040'): - if substitution in prod: - print_warning('{} used in {} section.'.format(substitution, - section)) - -if len(fhead) > 0: - prod = fhead - warn_if_filename_basename_used('file-header', prod) - prod = replace_specials(prod) - write_output(prod) - -def process_file(curfilename): - global entries, flags, seenbitshift, seenprivate, enum_prefix, c_namespace - firstenum = True - - try: - curfile = io.open(curfilename, encoding="utf-8", - errors="replace_and_warn") - except IOError as e: - if e.errno == errno.ENOENT: - print_warning('No file "{}" found.'.format(curfilename)) - return - raise - - while True: - line = curfile.readline() - if not line: - break - - line = line.strip() - - # read lines until we have no open comments - while re.search(r'/\*([^*]|\*(?!/))*$', line): - line += curfile.readline() - - # strip comments w/o options - line = re.sub(r'''/\*(?!<) - ([^*]+|\*(?!/))* - \*/''', '', line) - - # ignore forward declarations - if re.match(r'\s*typedef\s+enum.*;', line): - continue - - m = re.match(r'''\s*typedef\s+enum\s*[_A-Za-z]*[_A-Za-z0-9]*\s* - ({)?\s* - (?:/\*< - (([^*]|\*(?!/))*) - >\s*\*/)? - \s*({)?''', line, flags=re.X) - if m: - groups = m.groups() - if len(groups) >= 2 and groups[1] is not None: - options = parse_trigraph(groups[1]) - if 'skip' in options: - continue - enum_prefix = options.get('prefix', None) - flags = options.get('flags', None) - if 'flags' in options: - if flags is None: - flags = 1 - else: - flags = int(flags) - option_lowercase_name = options.get('lowercase_name', None) - option_underscore_name = options.get('underscore_name', None) - option_since = options.get('since', None) - else: - enum_prefix = None - flags = None - option_lowercase_name = None - option_underscore_name = None - option_since = None - - if option_lowercase_name is not None: - if option_underscore_name is not None: - print_warning("lowercase_name overridden with underscore_name") - option_lowercase_name = None - else: - print_warning("lowercase_name is deprecated, use underscore_name") - - # Didn't have trailing '{' look on next lines - if groups[0] is None and (len(groups) < 4 or groups[3] is None): - while True: - line = curfile.readline() - if not line: - print_error("Syntax error when looking for opening { in enum") - if re.match(r'\s*\{', line): - break - - seenbitshift = 0 - seenprivate = False - entries = [] - - # Now parse the entries - parse_entries(curfile, curfilename) - - # figure out if this was a flags or enums enumeration - if flags is None: - flags = seenbitshift - - # Autogenerate a prefix - if enum_prefix is None: - for entry in entries: - if not entry[2] and (len(entry) < 4 or entry[3] is None): - name = entry[0] - if enum_prefix is not None: - enum_prefix = os.path.commonprefix([name, enum_prefix]) - else: - enum_prefix = name - if enum_prefix is None: - enum_prefix = "" - else: - # Trim so that it ends in an underscore - enum_prefix = re.sub(r'_[^_]*$', '_', enum_prefix) - else: - # canonicalize user defined prefixes - enum_prefix = enum_prefix.upper() - enum_prefix = enum_prefix.replace('-', '_') - enum_prefix = re.sub(r'(.*)([^_])$', r'\1\2_', enum_prefix) - - fixed_entries = [] - for e in entries: - name = e[0] - num = e[1] - private = e[2] - if len(e) < 4 or e[3] is None: - nick = re.sub(r'^' + enum_prefix, '', name) - nick = nick.replace('_', '-').lower() - e = (name, num, private, nick) - fixed_entries.append(e) - entries = fixed_entries - - # Spit out the output - if option_underscore_name is not None: - enumlong = option_underscore_name.upper() - enumsym = option_underscore_name.lower() - enumshort = re.sub(r'^[A-Z][A-Z0-9]*_', '', enumlong) - - enumname_prefix = re.sub('_' + enumshort + '$', '', enumlong) - elif symprefix == '' and idprefix == '': - # enumname is e.g. GMatchType - enspace = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname) - - enumshort = re.sub(r'^[A-Z][a-z]*', '', enumname) - enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort) - enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort) - enumshort = enumshort.upper() - - enumname_prefix = re.sub(r'^([A-Z][a-z]*).*$', r'\1', enumname).upper() - - enumlong = enspace.upper() + "_" + enumshort - enumsym = enspace.lower() + "_" + enumshort.lower() - - if option_lowercase_name is not None: - enumsym = option_lowercase_name - else: - enumshort = enumname - if idprefix: - enumshort = re.sub(r'^' + idprefix, '', enumshort) - else: - enumshort = re.sub(r'/^[A-Z][a-z]*', '', enumshort) - - enumshort = re.sub(r'([^A-Z])([A-Z])', r'\1_\2', enumshort) - enumshort = re.sub(r'([A-Z][A-Z])([A-Z][0-9a-z])', r'\1_\2', enumshort) - enumshort = enumshort.upper() - - if symprefix: - enumname_prefix = symprefix.upper() - else: - enumname_prefix = idprefix.upper() - - enumlong = enumname_prefix + "_" + enumshort - enumsym = enumlong.lower() - - if option_since is not None: - enumsince = option_since - else: - enumsince = "" - - if firstenum: - firstenum = False - - if len(fprod) > 0: - prod = fprod - base = os.path.basename(curfilename) - - prod = prod.replace('\u0040filename\u0040', curfilename) - prod = prod.replace('\u0040basename\u0040', base) - prod = replace_specials(prod) - - write_output(prod) - - if len(eprod) > 0: - prod = eprod - - prod = prod.replace('\u0040enum_name\u0040', enumsym) - prod = prod.replace('\u0040EnumName\u0040', enumname) - prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) - prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) - prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) - prod = prod.replace('\u0040enumsince\u0040', enumsince) - if flags: - prod = prod.replace('\u0040type\u0040', 'flags') - else: - prod = prod.replace('\u0040type\u0040', 'enum') - if flags: - prod = prod.replace('\u0040Type\u0040', 'Flags') - else: - prod = prod.replace('\u0040Type\u0040', 'Enum') - if flags: - prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') - else: - prod = prod.replace('\u0040TYPE\u0040', 'ENUM') - prod = replace_specials(prod) - write_output(prod) - - if len(vhead) > 0: - prod = vhead - prod = prod.replace('\u0040enum_name\u0040', enumsym) - prod = prod.replace('\u0040EnumName\u0040', enumname) - prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) - prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) - prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) - prod = prod.replace('\u0040enumsince\u0040', enumsince) - if flags: - prod = prod.replace('\u0040type\u0040', 'flags') - else: - prod = prod.replace('\u0040type\u0040', 'enum') - if flags: - prod = prod.replace('\u0040Type\u0040', 'Flags') - else: - prod = prod.replace('\u0040Type\u0040', 'Enum') - if flags: - prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') - else: - prod = prod.replace('\u0040TYPE\u0040', 'ENUM') - prod = replace_specials(prod) - write_output(prod) - - if len(vprod) > 0: - prod = vprod - next_num = 0 - - prod = replace_specials(prod) - for name, num, private, nick in entries: - tmp_prod = prod - - if '\u0040valuenum\u0040' in prod: - # only attempt to eval the value if it is requested - # this prevents us from throwing errors otherwise - if num is not None: - # use sandboxed evaluation as a reasonable - # approximation to C constant folding - inum = eval(num, {}, c_namespace) - - # Support character literals - if isinstance(inum, str) and len(inum) == 1: - inum = ord(inum) - - # make sure it parsed to an integer - if not isinstance(inum, int): - sys.exit("Unable to parse enum value '%s'" % num) - num = inum - else: - num = next_num - - c_namespace[name] = num - tmp_prod = tmp_prod.replace('\u0040valuenum\u0040', str(num)) - next_num = int(num) + 1 - - if private: - continue - - tmp_prod = tmp_prod.replace('\u0040VALUENAME\u0040', name) - tmp_prod = tmp_prod.replace('\u0040valuenick\u0040', nick) - if flags: - tmp_prod = tmp_prod.replace('\u0040type\u0040', 'flags') - else: - tmp_prod = tmp_prod.replace('\u0040type\u0040', 'enum') - if flags: - tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Flags') - else: - tmp_prod = tmp_prod.replace('\u0040Type\u0040', 'Enum') - if flags: - tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'FLAGS') - else: - tmp_prod = tmp_prod.replace('\u0040TYPE\u0040', 'ENUM') - tmp_prod = tmp_prod.rstrip() - - write_output(tmp_prod) - - if len(vtail) > 0: - prod = vtail - prod = prod.replace('\u0040enum_name\u0040', enumsym) - prod = prod.replace('\u0040EnumName\u0040', enumname) - prod = prod.replace('\u0040ENUMSHORT\u0040', enumshort) - prod = prod.replace('\u0040ENUMNAME\u0040', enumlong) - prod = prod.replace('\u0040ENUMPREFIX\u0040', enumname_prefix) - prod = prod.replace('\u0040enumsince\u0040', enumsince) - if flags: - prod = prod.replace('\u0040type\u0040', 'flags') - else: - prod = prod.replace('\u0040type\u0040', 'enum') - if flags: - prod = prod.replace('\u0040Type\u0040', 'Flags') - else: - prod = prod.replace('\u0040Type\u0040', 'Enum') - if flags: - prod = prod.replace('\u0040TYPE\u0040', 'FLAGS') - else: - prod = prod.replace('\u0040TYPE\u0040', 'ENUM') - prod = replace_specials(prod) - write_output(prod) - -for fname in sorted(options.args): - process_file(fname) - -if len(ftail) > 0: - prod = ftail - warn_if_filename_basename_used('file-tail', prod) - prod = replace_specials(prod) - write_output(prod) - -# put auto-generation comment -comment = comment_tmpl -comment = comment.replace('\u0040comment\u0040', 'Generated data ends here') -write_output("\n" + comment + "\n") - -if tmpfile is not None: - tmpfilename = tmpfile.name - tmpfile.close() - - try: - os.unlink(options.output) - except OSError as error: - if error.errno != errno.ENOENT: - raise error - - os.rename(tmpfilename, options.output) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gmodule-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gmodule-2.0-0.dll deleted file mode 100644 index f9712b7cd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gmodule-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gobject-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gobject-2.0-0.dll deleted file mode 100644 index 33f81a469..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gobject-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/graphene-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/graphene-1.0-0.dll deleted file mode 100644 index 88b49fbda..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/graphene-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gresource.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gresource.exe deleted file mode 100644 index fa5efb09f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gresource.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsettings.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsettings.exe deleted file mode 100644 index fe48af7da..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsettings.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper-console.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper-console.exe deleted file mode 100644 index b8ae06f03..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper-console.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper.exe deleted file mode 100644 index b872dcebf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gspawn-win64-helper.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-device-monitor-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-device-monitor-1.0.exe deleted file mode 100644 index 7351b80ea..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-device-monitor-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-discoverer-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-discoverer-1.0.exe deleted file mode 100644 index 6edd577d5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-discoverer-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-dots-viewer.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-dots-viewer.exe deleted file mode 100644 index 9dedde707..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-dots-viewer.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-inspect-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-inspect-1.0.exe deleted file mode 100644 index ecf5887be..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-inspect-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-launch-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-launch-1.0.exe deleted file mode 100644 index a2cb81edc..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-launch-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-play-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-play-1.0.exe deleted file mode 100644 index 7a18dd03f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-play-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-shell b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-shell deleted file mode 100644 index 347bc9fa7..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-shell +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -export GSTREAMER_ROOT="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64" -export PATH="${GSTREAMER_ROOT}/bin${PATH:+:$PATH}" -export LD_LIBRARY_PATH="${GSTREAMER_ROOT}/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" -export PKG_CONFIG_PATH="${GSTREAMER_ROOT}/lib/pkgconfig:${GSTREAMER_ROOT}/share/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" -export XDG_DATA_DIRS="${GSTREAMER_ROOT}/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" -export XDG_CONFIG_DIRS="${GSTREAMER_ROOT}/etc/xdg${XDG_CONFIG_DIRS:+:$XDG_CONFIG_DIRS}" -export GST_REGISTRY_1_0="${HOME}/.cache/gstreamer-1.0/gstreamer-cerbero-registry" -export GST_PLUGIN_SCANNER_1_0="${GSTREAMER_ROOT}/libexec/gstreamer-1.0/gst-plugin-scanner" -export GST_PLUGIN_PATH_1_0="${GSTREAMER_ROOT}/lib/gstreamer-1.0" -export GST_PLUGIN_SYSTEM_PATH_1_0="${GSTREAMER_ROOT}/lib/gstreamer-1.0" -export PYTHONPATH="${GSTREAMER_ROOT}\Lib/site-packages${PYTHONPATH:+:$PYTHONPATH}" -export CFLAGS="-I${GSTREAMER_ROOT}/include ${CFLAGS}" -export CXXFLAGS="-I${GSTREAMER_ROOT}/include ${CXXFLAGS}" -export CPPFLAGS="-I${GSTREAMER_ROOT}/include ${CPPFLAGS}" -export LDFLAGS="-L${GSTREAMER_ROOT}/lib ${LDFLAGS}" -export GIO_EXTRA_MODULES="${GSTREAMER_ROOT}/lib/gio/modules" -export GI_TYPELIB_PATH="${GSTREAMER_ROOT}/lib/girepository-1.0" - - -$SHELL "$@" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-typefind-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-typefind-1.0.exe deleted file mode 100644 index 0c16b9a2b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-typefind-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-1.0.exe deleted file mode 100644 index e3ba43ebb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-launcher b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-launcher deleted file mode 100644 index 0267c71e3..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-launcher +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (c) 2014,Thibault Saunier -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, -# Boston, MA 02110-1301, USA. - -import os -import subprocess -import sys - -LIBDIR = r'C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib' -BUILDDIR = r'C:\projects\repos\cerbero.git\1.28\build\sources\msvc_x86_64\gst-devtools-1.0-1.28.3\b\validate\tools' -SRCDIR = r'C:\projects\repos\cerbero.git\1.28\build\sources\msvc_x86_64\gst-devtools-1.0-1.28.3\validate\tools' - - -def _add_gst_launcher_path(): - f = os.path.abspath(__file__) - if f.startswith(BUILDDIR): - # Make sure to have the configured config.py in the python path - sys.path.insert(0, os.path.abspath(os.path.join(BUILDDIR, ".."))) - root = os.path.abspath(os.path.join(SRCDIR, "../")) - else: - root = os.path.join(LIBDIR, 'gst-validate-launcher', 'python') - - sys.path.insert(0, root) - return os.path.join(root, "launcher") - - -if "__main__" == __name__: - libsdir = _add_gst_launcher_path() - from launcher.main import main - run_profile = os.environ.get('GST_VALIDATE_LAUNCHER_PROFILING', False) - if run_profile: - import cProfile - prof = cProfile.Profile() - try: - res = prof.runcall(main, libsdir) - finally: - prof.dump_stats('gst-validate-launcher-runstats') - exit(res) - - exit(main(libsdir)) diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-media-check-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-media-check-1.0.exe deleted file mode 100644 index 3e0b15675..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-media-check-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-rtsp-server-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-rtsp-server-1.0.exe deleted file mode 100644 index fdd76fa99..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-rtsp-server-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-transcoding-1.0.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-transcoding-1.0.exe deleted file mode 100644 index 8a4088e9c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gst-validate-transcoding-1.0.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstadaptivedemux-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstadaptivedemux-1.0-0.dll deleted file mode 100644 index 6ac5942e3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstadaptivedemux-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstallocators-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstallocators-1.0-0.dll deleted file mode 100644 index 273487df6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstallocators-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstanalytics-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstanalytics-1.0-0.dll deleted file mode 100644 index 23aa739e0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstanalytics-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstapp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstapp-1.0-0.dll deleted file mode 100644 index 1393c4247..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstapp-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstaudio-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstaudio-1.0-0.dll deleted file mode 100644 index c204b1a32..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstaudio-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbadaudio-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbadaudio-1.0-0.dll deleted file mode 100644 index f6969c535..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbadaudio-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbase-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbase-1.0-0.dll deleted file mode 100644 index f1c2eb367..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbase-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbasecamerabinsrc-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbasecamerabinsrc-1.0-0.dll deleted file mode 100644 index 887e3dd7e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstbasecamerabinsrc-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcheck-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcheck-1.0-0.dll deleted file mode 100644 index 22a046586..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcheck-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecparsers-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecparsers-1.0-0.dll deleted file mode 100644 index 3a22dee1f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecparsers-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecs-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecs-1.0-0.dll deleted file mode 100644 index a767b8270..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcodecs-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcontroller-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcontroller-1.0-0.dll deleted file mode 100644 index 5f2a5ac30..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcontroller-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcuda-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcuda-1.0-0.dll deleted file mode 100644 index 9fd1f6acd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstcuda-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d11-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d11-1.0-0.dll deleted file mode 100644 index 2311aee58..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d11-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d12-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d12-1.0-0.dll deleted file mode 100644 index 81bb65469..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3d12-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3dshader-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3dshader-1.0-0.dll deleted file mode 100644 index 0576eaca8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstd3dshader-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstdxva-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstdxva-1.0-0.dll deleted file mode 100644 index 016beed43..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstdxva-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstfft-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstfft-1.0-0.dll deleted file mode 100644 index 6ab13b9cf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstfft-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstgl-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstgl-1.0-0.dll deleted file mode 100644 index 5bc437dd4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstgl-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstinsertbin-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstinsertbin-1.0-0.dll deleted file mode 100644 index c1e6e1939..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstinsertbin-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstisoff-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstisoff-1.0-0.dll deleted file mode 100644 index 71b16a085..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstisoff-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmpegts-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmpegts-1.0-0.dll deleted file mode 100644 index cfdddb325..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmpegts-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmse-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmse-1.0-0.dll deleted file mode 100644 index 484f5d249..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstmse-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstnet-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstnet-1.0-0.dll deleted file mode 100644 index 1082e1e4a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstnet-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstpbutils-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstpbutils-1.0-0.dll deleted file mode 100644 index 4e6d938cd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstpbutils-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstphotography-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstphotography-1.0-0.dll deleted file mode 100644 index f6f7a7f0e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstphotography-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplay-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplay-1.0-0.dll deleted file mode 100644 index e970d5fb1..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplay-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplayer-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplayer-1.0-0.dll deleted file mode 100644 index 7dea461f3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstplayer-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstreamer-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstreamer-1.0-0.dll deleted file mode 100644 index 66afb1858..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstreamer-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstriff-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstriff-1.0-0.dll deleted file mode 100644 index 806f9bef4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstriff-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtp-1.0-0.dll deleted file mode 100644 index a55dd77e5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtp-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtsp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtsp-1.0-0.dll deleted file mode 100644 index f06a71924..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtsp-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtspserver-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtspserver-1.0-0.dll deleted file mode 100644 index e4e57589b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstrtspserver-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsctp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsctp-1.0-0.dll deleted file mode 100644 index af7691c83..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsctp-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsdp-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsdp-1.0-0.dll deleted file mode 100644 index bea49d060..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstsdp-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttag-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttag-1.0-0.dll deleted file mode 100644 index 2588224af..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttag-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttranscoder-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttranscoder-1.0-0.dll deleted file mode 100644 index 864ff64fe..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsttranscoder-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsturidownloader-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsturidownloader-1.0-0.dll deleted file mode 100644 index 425290fb6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gsturidownloader-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvalidate-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvalidate-1.0-0.dll deleted file mode 100644 index 4ebcf3356..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvalidate-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvideo-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvideo-1.0-0.dll deleted file mode 100644 index a10e1d149..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstvideo-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtc-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtc-1.0-0.dll deleted file mode 100644 index 4f80f3fb8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtc-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtcnice-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtcnice-1.0-0.dll deleted file mode 100644 index de461882e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwebrtcnice-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwinrt-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwinrt-1.0-0.dll deleted file mode 100644 index fa0a2b56d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gstwinrt-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gthread-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gthread-2.0-0.dll deleted file mode 100644 index e13a20a88..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gthread-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gtk-4-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gtk-4-1.dll deleted file mode 100644 index c3fd428f2..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/gtk-4-1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-cairo.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-cairo.dll deleted file mode 100644 index 66aa54cc7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-cairo.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-gobject.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-gobject.dll deleted file mode 100644 index 8104b7f8e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-gobject.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-subset.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-subset.dll deleted file mode 100644 index 834b8f14a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz-subset.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz.dll deleted file mode 100644 index 0d1243b68..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/harfbuzz.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/iconv-2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/iconv-2.dll deleted file mode 100644 index 9053a75a7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/iconv-2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/intl-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/intl-8.dll deleted file mode 100644 index d37f3a60f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/intl-8.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/jpeg8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/jpeg8.dll deleted file mode 100644 index 7a7bfab18..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/jpeg8.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-1.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-1.0-0.dll deleted file mode 100644 index 1efa5409e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-1.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-format.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-format.exe deleted file mode 100644 index 0cf0b1027..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-format.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-validate.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-validate.exe deleted file mode 100644 index 33d567da6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/json-glib-validate.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_api.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_api.dll deleted file mode 100644 index 6cd67a1e7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_api.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_legacy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_legacy.dll deleted file mode 100644 index 6277a45f4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_legacy.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_cpu.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_cpu.dll deleted file mode 100644 index d2daeeb8a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_cpu.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_legacy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_legacy.dll deleted file mode 100644 index 83d64911b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/lcevc_dec_pipeline_legacy.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcrypto-3-x64.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcrypto-3-x64.dll deleted file mode 100644 index 59688b42c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcrypto-3-x64.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcurl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcurl.dll deleted file mode 100644 index 8193459cb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libcurl.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libexpat.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libexpat.dll deleted file mode 100644 index 5e0ae1374..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libexpat.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libgcc_s_seh-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libgcc_s_seh-1.dll deleted file mode 100644 index ddf0e38ff..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libgcc_s_seh-1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libmpg123-1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libmpg123-1.dll deleted file mode 100644 index 51c3a9e56..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libmpg123-1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libpng16-config b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libpng16-config deleted file mode 100644 index 0068564c1..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/bin/libpng16-config +++ /dev/null @@ -1,127 +0,0 @@ -#! /bin/sh - -# libpng-config -# provides configuration info for libpng. - -# Copyright (C) 2002, 2004, 2006, 2007 Glenn Randers-Pehrson - -# This code is released under the libpng license. -# For conditions of distribution and use, see the disclaimer -# and license in png.h - -# Modeled after libxml-config. - -version="1.6.56" -prefix="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64" -exec_prefix="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64" -libdir="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/lib" -includedir="C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/include/libpng16" -libs="-lpng16" -all_libs="-lpng16 -lz -lm" -I_opts="-I${includedir}" -L_opts="-L${libdir}" -R_opts="" -cppflags="" -ccopts="" -ldopts="" - -usage() -{ - cat < - - - Set hintslight to hintstyle - - - - hintslight - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-scale-bitmap-fonts.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-scale-bitmap-fonts.conf deleted file mode 100644 index 0c3a2efc3..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-scale-bitmap-fonts.conf +++ /dev/null @@ -1,83 +0,0 @@ - - - - Bitmap scaling - - - - false - - - - pixelsize - pixelsize - - - - - - - false - - - false - - - true - - - - - pixelsizefixupfactor - 1.2 - - - pixelsizefixupfactor - 0.8 - - - - - - - true - - - 1.0 - - - - - - false - - - 1.0 - - - - matrix - - pixelsizefixupfactor 0 - 0 pixelsizefixupfactor - - - - - - size - pixelsizefixupfactor - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-sub-pixel-none.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-sub-pixel-none.conf deleted file mode 100644 index 1fb6c98af..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-sub-pixel-none.conf +++ /dev/null @@ -1,15 +0,0 @@ - - - - Disable sub-pixel rendering - - - - none - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-yes-antialias.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-yes-antialias.conf deleted file mode 100644 index 4451f6ed1..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/10-yes-antialias.conf +++ /dev/null @@ -1,8 +0,0 @@ - - - - Enable antialiasing - - true - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/11-lcdfilter-default.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/11-lcdfilter-default.conf deleted file mode 100644 index 602559783..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/11-lcdfilter-default.conf +++ /dev/null @@ -1,17 +0,0 @@ - - - - Use lcddefault as default for LCD filter - - - - - lcddefault - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/20-unhint-small-vera.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/20-unhint-small-vera.conf deleted file mode 100644 index e4e9c33cb..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/20-unhint-small-vera.conf +++ /dev/null @@ -1,49 +0,0 @@ - - - - Disable hinting for Bitstream Vera fonts when the size is less than 8ppem - - - - - Bitstream Vera Sans - - - 7.5 - - - false - - - - - - Bitstream Vera Serif - - - 7.5 - - - false - - - - - - Bitstream Vera Sans Mono - - - 7.5 - - - false - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/30-metric-aliases.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/30-metric-aliases.conf deleted file mode 100644 index edf4ef852..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/30-metric-aliases.conf +++ /dev/null @@ -1,658 +0,0 @@ - - - - Set substitutions for similar/metric-compatible families - - - - - - - - Helvetica LT Std - - Helvetica - - - - - Nimbus Sans L - - Helvetica - - - - - Nimbus Sans - - Helvetica - - - - - TeX Gyre Heros - - Helvetica - - - - - Nimbus Sans Narrow - - Helvetica Narrow - - - - - TeX Gyre Heros Cn - - Helvetica Narrow - - - - - Nimbus Roman No9 L - - Times - - - - - Nimbus Roman - - Times - - - - - TeX Gyre Termes - - Times - - - - - Courier Std - - Courier - - - - - Nimbus Mono L - - Courier - - - - - Nimbus Mono - - Courier - - - - - Nimbus Mono PS - - Courier - - - - - TeX Gyre Cursor - - Courier - - - - - Avant Garde - - ITC Avant Garde Gothic - - - - - URW Gothic L - - ITC Avant Garde Gothic - - - - - URW Gothic - - ITC Avant Garde Gothic - - - - - TeX Gyre Adventor - - ITC Avant Garde Gothic - - - - - Bookman - - ITC Bookman - - - - - URW Bookman L - - ITC Bookman - - - - - Bookman URW - - ITC Bookman - - - - - URW Bookman - - ITC Bookman - - - - - TeX Gyre Bonum - - ITC Bookman - - - - - Bookman Old Style - - ITC Bookman - - - - - Zapf Chancery - - ITC Zapf Chancery - - - - - URW Chancery L - - ITC Zapf Chancery - - - - - Chancery URW - - ITC Zapf Chancery - - - - - Z003 - - ITC Zapf Chancery - - - - - TeX Gyre Chorus - - ITC Zapf Chancery - - - - - URW Palladio L - - Palatino - - - - - Palladio URW - - Palatino - - - - - P052 - - Palatino - - - - - TeX Gyre Pagella - - Palatino - - - - - Palatino Linotype - - Palatino - - - - - Century Schoolbook L - - New Century Schoolbook - - - - - Century SchoolBook URW - - New Century Schoolbook - - - - - C059 - - New Century Schoolbook - - - - - TeX Gyre Schola - - New Century Schoolbook - - - - - Century Schoolbook - - New Century Schoolbook - - - - - - Arimo - - Arial - - - - - Liberation Sans - - Arial - - - - - Liberation Sans Narrow - - Arial Narrow - - - - - Albany - - Arial - - - - - Albany AMT - - Arial - - - - - Tinos - - Times New Roman - - - - - Liberation Serif - - Times New Roman - - - - - Thorndale - - Times New Roman - - - - - Thorndale AMT - - Times New Roman - - - - - Cousine - - Courier New - - - - - Liberation Mono - - Courier New - - - - - Cumberland - - Courier New - - - - - Cumberland AMT - - Courier New - - - - - Gelasio - - Georgia - - - - - Caladea - - Cambria - - - - - Carlito - - Calibri - - - - - SymbolNeu - - Symbol - - - - - - - - Helvetica - - Arial - - - - - Helvetica Narrow - - Arial Narrow - - - - - Times - - Times New Roman - - - - - Courier - - Courier New - - - - - - Arial - - Helvetica - - - - - Arial Narrow - - Helvetica Narrow - - - - - Times New Roman - - Times - - - - - Courier New - - Courier - - - - - - - - Helvetica - - Helvetica LT Std - - - - - Helvetica - - TeX Gyre Heros - - - - - Helvetica Narrow - - TeX Gyre Heros Cn - - - - - Times - - TeX Gyre Termes - - - - - Courier - - TeX Gyre Cursor - - - - - Courier - - Courier Std - - - - - ITC Avant Garde Gothic - - TeX Gyre Adventor - - - - - ITC Bookman - - Bookman Old Style - TeX Gyre Bonum - - - - - ITC Zapf Chancery - - TeX Gyre Chorus - - - - - Palatino - - Palatino Linotype - TeX Gyre Pagella - - - - - New Century Schoolbook - - Century Schoolbook - TeX Gyre Schola - - - - - - Arial - - Arimo - Liberation Sans - Albany - Albany AMT - - - - - Arial Narrow - - Liberation Sans Narrow - - - - - Times New Roman - - Tinos - Liberation Serif - Thorndale - Thorndale AMT - - - - - Courier New - - Cousine - Liberation Mono - Cumberland - Cumberland AMT - - - - - Georgia - - Gelasio - - - - - Cambria - - Caladea - - - - - Calibri - - Carlito - - - - - Symbol - - SymbolNeu - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/40-nonlatin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/40-nonlatin.conf deleted file mode 100644 index f8d96ce81..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/40-nonlatin.conf +++ /dev/null @@ -1,332 +0,0 @@ - - - - Set substitutions for non-Latin fonts - - - - - Nazli - serif - - - Lotoos - serif - - - Mitra - serif - - - Ferdosi - serif - - - Badr - serif - - - Zar - serif - - - Titr - serif - - - Jadid - serif - - - Kochi Mincho - serif - - - AR PL SungtiL GB - serif - - - AR PL Mingti2L Big5 - serif - - - MS 明朝 - serif - - - NanumMyeongjo - serif - - - UnBatang - serif - - - Baekmuk Batang - serif - - - MgOpen Canonica - serif - - - Sazanami Mincho - serif - - - AR PL ZenKai Uni - serif - - - ZYSong18030 - serif - - - FreeSerif - serif - - - SimSun - serif - - - - Arshia - sans-serif - - - Elham - sans-serif - - - Farnaz - sans-serif - - - Nasim - sans-serif - - - Sina - sans-serif - - - Roya - sans-serif - - - Koodak - sans-serif - - - Terafik - sans-serif - - - Kochi Gothic - sans-serif - - - AR PL KaitiM GB - sans-serif - - - AR PL KaitiM Big5 - sans-serif - - - MS ゴシック - sans-serif - - - NanumGothic - sans-serif - - - UnDotum - sans-serif - - - Baekmuk Dotum - sans-serif - - - MgOpen Modata - sans-serif - - - Sazanami Gothic - sans-serif - - - AR PL ShanHeiSun Uni - sans-serif - - - ZYSong18030 - sans-serif - - - FreeSans - sans-serif - - - - NSimSun - monospace - - - ZYSong18030 - monospace - - - NanumGothicCoding - monospace - - - FreeMono - monospace - - - - - Homa - fantasy - - - Kamran - fantasy - - - Fantezi - fantasy - - - Tabassom - fantasy - - - - - IranNastaliq - cursive - - - Nafees Nastaleeq - cursive - - - - - Noto Sans Arabic UI - system-ui - - - Noto Sans Bengali UI - system-ui - - - Noto Sans Devanagari UI - system-ui - - - Noto Sans Gujarati UI - system-ui - - - Noto Sans Gurmukhi UI - system-ui - - - Noto Sans Kannada UI - system-ui - - - Noto Sans Khmer UI - system-ui - - - Noto Sans Lao UI - system-ui - - - Noto Sans Malayalam UI - system-ui - - - Noto Sans Myanmar UI - system-ui - - - Noto Sans Oriya UI - system-ui - - - Noto Sans Sinhala UI - system-ui - - - Noto Sans Tamil UI - system-ui - - - Noto Sans Telugu UI - system-ui - - - Noto Sans Thai UI - system-ui - - - Leelawadee UI - system-ui - - - Nirmala UI - system-ui - - - Yu Gothic UI - system-ui - - - Meiryo UI - system-ui - - - MS UI Gothic - system-ui - - - Khmer UI - system-ui - - - Lao UI - system-ui - - - Microsoft JhengHei UI - system-ui - - - Microsoft YaHei UI - system-ui - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-generic.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-generic.conf deleted file mode 100644 index 5c1bd36b5..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-generic.conf +++ /dev/null @@ -1,136 +0,0 @@ - - - - Set substitutions for emoji/math fonts - - - - - - - - Noto Color Emoji - emoji - - - Apple Color Emoji - emoji - - - Segoe UI Emoji - emoji - - - Twitter Color Emoji - emoji - - - EmojiOne Mozilla - emoji - - - - Emoji Two - emoji - - - JoyPixels - emoji - - - Emoji One - emoji - - - - Noto Emoji - emoji - - - Android Emoji - emoji - - - - - - emoji - - - und-zsye - - - - - - und-zsye - - - emoji - - - - - emoji - - - - - - - - - XITS Math - math - - - STIX Two Math - math - - - Cambria Math - math - - - Latin Modern Math - math - - - Minion Math - math - - - Lucida Math - math - - - Asana Math - math - - - - - - math - - - und-zmth - - - - - - und-zmth - - - math - - - - - math - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-latin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-latin.conf deleted file mode 100644 index 53158c767..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/45-latin.conf +++ /dev/null @@ -1,309 +0,0 @@ - - - - Set substitutions for Latin fonts - - - - - Bitstream Vera Serif - serif - - - Cambria - serif - - - Constantia - serif - - - DejaVu Serif - serif - - - Elephant - serif - - - Garamond - serif - - - Georgia - serif - - - Liberation Serif - serif - - - Luxi Serif - serif - - - MS Serif - serif - - - Nimbus Roman No9 L - serif - - - Nimbus Roman - serif - - - Palatino Linotype - serif - - - Thorndale AMT - serif - - - Thorndale - serif - - - Times New Roman - serif - - - Times - serif - - - - Albany AMT - sans-serif - - - Albany - sans-serif - - - Arial Unicode MS - sans-serif - - - Arial - sans-serif - - - Bitstream Vera Sans - sans-serif - - - Britannic - sans-serif - - - Calibri - sans-serif - - - Candara - sans-serif - - - Century Gothic - sans-serif - - - Corbel - sans-serif - - - DejaVu Sans - sans-serif - - - Helvetica LT Std - sans-serif - - - Helvetica - sans-serif - - - Haettenschweiler - sans-serif - - - Liberation Sans - sans-serif - - - MS Sans Serif - sans-serif - - - Nimbus Sans L - sans-serif - - - Nimbus Sans - sans-serif - - - Luxi Sans - sans-serif - - - Tahoma - sans-serif - - - Trebuchet MS - sans-serif - - - Twentieth Century - sans-serif - - - Verdana - sans-serif - - - - Andale Mono - monospace - - - Bitstream Vera Sans Mono - monospace - - - Consolas - monospace - - - Courier New - monospace - - - Courier Std - monospace - - - Courier - monospace - - - Cumberland AMT - monospace - - - Cumberland - monospace - - - DejaVu Sans Mono - monospace - - - Fixedsys - monospace - - - Inconsolata - monospace - - - Liberation Mono - monospace - - - Luxi Mono - monospace - - - Nimbus Mono L - monospace - - - Nimbus Mono - monospace - - - Nimbus Mono PS - monospace - - - Terminal - monospace - - - - Bauhaus Std - fantasy - - - Cooper Std - fantasy - - - Copperplate Gothic Std - fantasy - - - Impact - fantasy - - - - Comic Sans MS - cursive - - - ITC Zapf Chancery Std - cursive - - - Zapfino - cursive - - - - Adwaita Sans - system-ui - - - Cantarell - system-ui - - - Noto Sans UI - system-ui - - - Segoe UI - system-ui - - - Segoe UI Historic - system-ui - - - Segoe UI Symbol - system-ui - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/48-spacing.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/48-spacing.conf deleted file mode 100644 index 6df5c1176..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/48-spacing.conf +++ /dev/null @@ -1,16 +0,0 @@ - - - - Add mono to the family when spacing is 100 - - - - 100 - - - monospace - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/49-sansserif.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/49-sansserif.conf deleted file mode 100644 index 6cc3a1c49..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/49-sansserif.conf +++ /dev/null @@ -1,22 +0,0 @@ - - - - Add sans-serif to the family when no generic name - - - - sans-serif - - - serif - - - monospace - - - sans-serif - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/50-user.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/50-user.conf deleted file mode 100644 index d019f4d4e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/50-user.conf +++ /dev/null @@ -1,16 +0,0 @@ - - - - Load per-user customization files - - fontconfig/conf.d - fontconfig/fonts.conf - - ~/.fonts.conf.d - ~/.fonts.conf - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/51-local.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/51-local.conf deleted file mode 100644 index 82e3c1b2f..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/51-local.conf +++ /dev/null @@ -1,7 +0,0 @@ - - - - Load local customization file - - local.conf - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-generic.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-generic.conf deleted file mode 100644 index 783150771..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-generic.conf +++ /dev/null @@ -1,64 +0,0 @@ - - - - Set preferable fonts for emoji/math fonts - - - - - - - - und-zsye - - - true - - - false - - - true - - - - - - emoji - - - Noto Color Emoji - Apple Color Emoji - Segoe UI Emoji - Twitter Color Emoji - EmojiOne Mozilla - - Emoji Two - JoyPixels - Emoji One - - Noto Emoji - Android Emoji - - - - - - - math - - XITS Math - STIX Two Math - Cambria Math - Latin Modern Math - Minion Math - Lucida Math - Asana Math - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-latin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-latin.conf deleted file mode 100644 index ae3be3c73..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/60-latin.conf +++ /dev/null @@ -1,89 +0,0 @@ - - - - Set preferable fonts for Latin - - serif - - Noto Serif - DejaVu Serif - Times New Roman - Thorndale AMT - Luxi Serif - Nimbus Roman No9 L - Nimbus Roman - Times - - - - sans-serif - - Noto Sans - DejaVu Sans - Verdana - Arial - Albany AMT - Luxi Sans - Nimbus Sans L - Nimbus Sans - Helvetica - Lucida Sans Unicode - BPG Glaho International - Tahoma - - - - monospace - - Noto Sans Mono - DejaVu Sans Mono - Inconsolata - Andale Mono - Courier New - Cumberland AMT - Luxi Mono - Nimbus Mono L - Nimbus Mono - Nimbus Mono PS - Courier - - - - - fantasy - - Impact - Copperplate Gothic Std - Cooper Std - Bauhaus Std - - - - - cursive - - ITC Zapf Chancery Std - Zapfino - Comic Sans MS - - - - - system-ui - - Adwaita Sans - Cantarell - Noto Sans UI - Segoe UI - Segoe UI Historic - Segoe UI Symbol - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-fonts-persian.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-fonts-persian.conf deleted file mode 100644 index 47da1bb09..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-fonts-persian.conf +++ /dev/null @@ -1,418 +0,0 @@ - - - - - - - - - - Nesf - Nesf2 - - - Nesf2 - Persian_sansserif_default - - - - - - Nazanin - Nazli - - - Lotus - Lotoos - - - Yaqut - Yaghoot - - - Yaghut - Yaghoot - - - Traffic - Terafik - - - Ferdowsi - Ferdosi - - - Fantezy - Fantezi - - - - - - - - Jadid - Persian_title - - - Titr - Persian_title - - - - - Kamran - - Persian_fantasy - Homa - - - - Homa - - Persian_fantasy - Kamran - - - - Fantezi - Persian_fantasy - - - Tabassom - Persian_fantasy - - - - - Arshia - Persian_square - - - Nasim - Persian_square - - - Elham - - Persian_square - Farnaz - - - - Farnaz - - Persian_square - Elham - - - - Sina - Persian_square - - - - - - - Persian_title - - Titr - Jadid - Persian_serif - - - - - - Persian_fantasy - - Homa - Kamran - Fantezi - Tabassom - Persian_square - - - - - - Persian_square - - Arshia - Elham - Farnaz - Nasim - Sina - Persian_serif - - - - - - - - Elham - - - farsiweb - - - - - - Homa - - - farsiweb - - - - - - Koodak - - - farsiweb - - - - - - Nazli - - - farsiweb - - - - - - Roya - - - farsiweb - - - - - - Terafik - - - farsiweb - - - - - - Titr - - - farsiweb - - - - - - - - - - TURNED-OFF - - - farsiweb - - - - roman - - - - roman - - - - - matrix - 1-0.2 - 01 - - - - - - oblique - - - - - - - - - farsiweb - - - false - - - false - - - false - - - - - - - - - serif - - Nazli - Lotoos - Mitra - Ferdosi - Badr - Zar - - - - - - sans-serif - - Roya - Koodak - Terafik - - - - - - monospace - - - Terafik - - - - - - fantasy - - Homa - Kamran - Fantezi - Tabassom - - - - - - cursive - - IranNastaliq - Nafees Nastaleeq - - - - - - - - - serif - - - 200 - - - 24 - - - Titr - - - - - - - sans-serif - - - 200 - - - 24 - - - Titr - - - - - - - Persian_sansserif_default - - - 200 - - - 24 - - - Titr - - - - - - - - - Persian_sansserif_default - - - Roya - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-nonlatin.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-nonlatin.conf deleted file mode 100644 index 3e5d1c7d8..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/65-nonlatin.conf +++ /dev/null @@ -1,240 +0,0 @@ - - - - Set preferable fonts for non-Latin - - serif - - Artsounk - BPG UTF8 M - Kinnari - Norasi - Frank Ruehl - Dror - JG LaoTimes - Saysettha Unicode - Pigiarniq - B Davat - B Compset - Kacst-Qr - Urdu Nastaliq Unicode - Raghindi - Mukti Narrow - malayalam - Sampige - padmaa - Hapax Berbère - MS Mincho - SimSun - PMingLiu - WenQuanYi Zen Hei - WenQuanYi Bitmap Song - AR PL ShanHeiSun Uni - AR PL New Sung - ZYSong18030 - HanyiSong - Hiragino Mincho ProN - Songti SC - Songti TC - SimSong - MgOpen Canonica - Sazanami Mincho - IPAMonaMincho - IPAMincho - Kochi Mincho - AR PL SungtiL GB - AR PL Mingti2L Big5 - AR PL Zenkai Uni - MS 明朝 - ZYSong18030 - NanumMyeongjo - UnBatang - Baekmuk Batang - AppleMyungjo - KacstQura - Frank Ruehl CLM - Lohit Bengali - Lohit Gujarati - Lohit Hindi - Lohit Marathi - Lohit Maithili - Lohit Kashmiri - Lohit Konkani - Lohit Nepali - Lohit Sindhi - Lohit Punjabi - Lohit Tamil - Rachana - Lohit Malayalam - Lohit Kannada - Lohit Telugu - Lohit Oriya - LKLUG - - - - sans-serif - - Nachlieli - Lucida Sans Unicode - Yudit Unicode - Kerkis - ArmNet Helvetica - Artsounk - BPG UTF8 M - Waree - Loma - Garuda - Umpush - Saysettha Unicode - JG Lao Old Arial - GF Zemen Unicode - Pigiarniq - B Davat - B Compset - Kacst-Qr - Urdu Nastaliq Unicode - Raghindi - Mukti Narrow - malayalam - Sampige - padmaa - Hapax Berbère - MS Gothic - UmePlus P Gothic - Microsoft YaHei - Microsoft JhengHei - WenQuanYi Zen Hei - WenQuanYi Bitmap Song - AR PL ShanHeiSun Uni - AR PL New Sung - Hiragino Sans - PingFang SC - PingFang TC - PingFang HK - Hiragino Sans CNS - Hiragino Sans GB - MgOpen Modata - VL Gothic - IPAMonaGothic - IPAGothic - Sazanami Gothic - Kochi Gothic - AR PL KaitiM GB - AR PL KaitiM Big5 - AR PL ShanHeiSun Uni - AR PL SungtiL GB - AR PL Mingti2L Big5 - MS ゴシック - ZYSong18030 - TSCu_Paranar - NanumGothic - UnDotum - Baekmuk Dotum - Baekmuk Gulim - Apple SD Gothic Neo - KacstQura - Lohit Bengali - Lohit Gujarati - Lohit Hindi - Lohit Marathi - Lohit Maithili - Lohit Kashmiri - Lohit Konkani - Lohit Nepali - Lohit Sindhi - Lohit Punjabi - Lohit Tamil - Meera - Lohit Malayalam - Lohit Kannada - Lohit Telugu - Lohit Oriya - LKLUG - - - - monospace - - Miriam Mono - VL Gothic - IPAMonaGothic - IPAGothic - Sazanami Gothic - Kochi Gothic - AR PL KaitiM GB - MS Gothic - UmePlus Gothic - NSimSun - MingLiu - AR PL ShanHeiSun Uni - AR PL New Sung Mono - HanyiSong - AR PL SungtiL GB - AR PL Mingti2L Big5 - ZYSong18030 - NanumGothicCoding - NanumGothic - UnDotum - Baekmuk Dotum - Baekmuk Gulim - TlwgTypo - TlwgTypist - TlwgTypewriter - TlwgMono - Hasida - GF Zemen Unicode - Hapax Berbère - Lohit Bengali - Lohit Gujarati - Lohit Hindi - Lohit Marathi - Lohit Maithili - Lohit Kashmiri - Lohit Konkani - Lohit Nepali - Lohit Sindhi - Lohit Punjabi - Lohit Tamil - Meera - Lohit Malayalam - Lohit Kannada - Lohit Telugu - Lohit Oriya - LKLUG - - - - - system-ui - - Noto Sans Arabic UI - Noto Sans Bengali UI - Noto Sans Devanagari UI - Noto Sans Gujarati UI - Noto Sans Gurmukhi UI - Noto Sans Kannada UI - Noto Sans Khmer UI - Noto Sans Lao UI - Noto Sans Malayalam UI - Noto Sans Myanmar UI - Noto Sans Oriya UI - Noto Sans Sinhala UI - Noto Sans Tamil UI - Noto Sans Telugu UI - Noto Sans Thai UI - Leelawadee UI - Nirmala UI - Yu Gothic UI - Meiryo UI - MS UI Gothic - Khmer UI - Lao UI - Microsoft YaHei UI - Microsoft JhengHei UI - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/69-unifont.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/69-unifont.conf deleted file mode 100644 index 02854ff9d..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/69-unifont.conf +++ /dev/null @@ -1,28 +0,0 @@ - - - - - serif - - FreeSerif - Code2000 - Code2001 - - - - sans-serif - - FreeSans - Arial Unicode MS - Arial Unicode - Code2000 - Code2001 - - - - monospace - - FreeMono - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/80-delicious.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/80-delicious.conf deleted file mode 100644 index d20990cd3..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/80-delicious.conf +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - Delicious - - - Heavy - - - heavy - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/90-synthetic.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/90-synthetic.conf deleted file mode 100644 index dfce674bb..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/90-synthetic.conf +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - roman - - - - roman - - - - - matrix - 10.2 - 01 - - - - - - oblique - - - - false - - - - - - - - - medium - - - - bold - - - - true - - - - bold - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/README b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/README deleted file mode 100644 index fd4f5b212..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/conf.d/README +++ /dev/null @@ -1,23 +0,0 @@ -conf.d/README - -Each file in this directory is a fontconfig configuration file. Fontconfig -scans this directory, loading all files of the form [0-9][0-9]*.conf. -These files are normally installed in C:/projects/repos/cerbero.git/1.28/build/dist/msvc_x86_64/share/fontconfig/conf.avail -and then symlinked here, allowing them to be easily installed and then -enabled/disabled by adjusting the symlinks. - -The files are loaded in numeric order, the structure of the configuration -has led to the following conventions in usage: - - Files beginning with: Contain: - - 00 through 09 Font directories - 10 through 19 system rendering defaults (AA, etc) - 20 through 29 font rendering options - 30 through 39 family substitution - 40 through 49 generic identification, map family->generic - 50 through 59 alternate config file loading - 60 through 69 generic aliases, map generic->family - 70 through 79 select font (adjust which fonts are available) - 80 through 89 match target="scan" (modify scanned patterns) - 90 through 99 font synthesis diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/fonts.conf b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/fonts.conf deleted file mode 100644 index c8fdfc24e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/fonts/fonts.conf +++ /dev/null @@ -1,103 +0,0 @@ - - - - - Default configuration file - - - - - - WINDOWSFONTDIR - WINDOWSUSERFONTDIR - - - fonts - - ~/.fonts - - - - - mono - - - monospace - - - - - - - sans serif - - - sans-serif - - - - - - - sans - - - sans-serif - - - - - - system ui - - - system-ui - - - - - conf.d - - - - LOCAL_APPDATA_FONTCONFIG_CACHE - fontconfig - - ~/.fontconfig - - - - - 30 - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/ssl/certs/ca-certificates.crt b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/ssl/certs/ca-certificates.crt deleted file mode 100644 index 9551dfd83..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/etc/ssl/certs/ca-certificates.crt +++ /dev/null @@ -1,3451 +0,0 @@ -## -## Bundle of CA Root Certificates -## -## Certificate data from Mozilla as of: Tue Aug 22 03:12:04 2023 GMT -## -## This is a bundle of X.509 certificates of public Certificate Authorities -## (CA). These were automatically extracted from Mozilla's root certificates -## file (certdata.txt). This file can be found in the mozilla source tree: -## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt -## -## It contains the certificates in PEM format and therefore -## can be directly used with curl / libcurl / php_curl, or with -## an Apache+mod_ssl webserver for SSL client authentication. -## Just configure this file as the SSLCACertificateFile. -## -## Conversion done with mk-ca-bundle.pl version 1.29. -## SHA256: 0ff137babc6a5561a9cfbe9f29558972e5b528202681b7d3803d03a3e82922bd -## - - -GlobalSign Root CA -================== ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx -GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds -b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV -BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD -VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa -DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc -THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb -Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP -c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX -gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF -AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj -Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG -j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH -hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC -X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- - -Entrust.net Premium 2048 Secure Server CA -========================================= ------BEGIN CERTIFICATE----- -MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u -ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp -bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV -BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx -NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 -d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl -MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u -ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL -Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr -hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW -nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi -VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ -KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy -T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf -zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT -J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e -nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= ------END CERTIFICATE----- - -Baltimore CyberTrust Root -========================= ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE -ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li -ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC -SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs -dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME -uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB -UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C -G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 -XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr -l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI -VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB -BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh -cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 -hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa -Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H -RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp ------END CERTIFICATE----- - -Entrust Root Certification Authority -==================================== ------BEGIN CERTIFICATE----- -MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV -BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw -b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG -A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 -MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu -MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu -Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v -dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz -A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww -Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 -j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN -rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw -DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 -MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH -hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA -A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM -Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa -v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS -W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 -tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 ------END CERTIFICATE----- - -Comodo AAA Services root -======================== ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS -R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg -TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw -MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl -c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV -BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG -C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs -i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW -Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH -Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK -Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f -BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl -cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz -LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm -7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz -Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z -8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C -12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- - -QuoVadis Root CA 2 -================== ------BEGIN CERTIFICATE----- -MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT -EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx -ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 -XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk -lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB -lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy -lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt -66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn -wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh -D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy -BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie -J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud -DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU -a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT -ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv -Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 -UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm -VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK -+JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW -IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 -WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X -f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II -4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 -VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u ------END CERTIFICATE----- - -QuoVadis Root CA 3 -================== ------BEGIN CERTIFICATE----- -MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT -EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx -OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg -DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij -KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K -DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv -BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp -p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 -nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX -MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM -Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz -uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT -BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj -YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 -aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB -BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD -VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 -ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE -AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV -qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s -hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z -POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 -Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp -8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC -bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu -g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p -vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr -qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= ------END CERTIFICATE----- - -Security Communication Root CA -============================== ------BEGIN CERTIFICATE----- -MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP -U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw -HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP -U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw -8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM -DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX -5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd -DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 -JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw -DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g -0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a -mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ -s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ -6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi -FL39vmwLAw== ------END CERTIFICATE----- - -XRamp Global CA Root -==================== ------BEGIN CERTIFICATE----- -MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE -BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj -dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx -HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg -U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu -IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx -foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE -zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs -AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry -xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap -oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC -AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc -/Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt -qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n -nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz -8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= ------END CERTIFICATE----- - -Go Daddy Class 2 CA -=================== ------BEGIN CERTIFICATE----- -MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY -VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG -A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g -RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD -ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv -2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 -qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j -YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY -vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O -BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o -atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu -MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG -A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim -PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt -I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ -HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI -Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b -vZ8= ------END CERTIFICATE----- - -Starfield Class 2 CA -==================== ------BEGIN CERTIFICATE----- -MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc -U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo -MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG -A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG -SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY -bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ -JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm -epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN -F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF -MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f -hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo -bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g -QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs -afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM -PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl -xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD -KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 -QBFGmh95DmK/D5fs4C8fF5Q= ------END CERTIFICATE----- - -DigiCert Assured ID Root CA -=========================== ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw -IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx -MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL -ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO -9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy -UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW -/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy -oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf -GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF -66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq -hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc -EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn -SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i -8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE----- - -DigiCert Global Root CA -======================= ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw -HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw -MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 -dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn -TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 -BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H -4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y -7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB -o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm -8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF -BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr -EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt -tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 -UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- - -DigiCert High Assurance EV Root CA -================================== ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw -KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw -MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ -MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu -Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t -Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS -OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 -MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ -NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe -h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB -Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY -JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ -V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp -myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK -mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K ------END CERTIFICATE----- - -SwissSign Gold CA - G2 -====================== ------BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw -EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN -MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp -c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq -t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C -jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg -vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF -ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR -AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend -jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO -peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR -7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi -GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 -OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov -L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm -5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr -44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf -Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m -Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp -mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk -vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf -KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br -NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj -viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ ------END CERTIFICATE----- - -SwissSign Silver CA - G2 -======================== ------BEGIN CERTIFICATE----- -MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT -BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X -DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 -aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG -9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 -N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm -+/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH -6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu -MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h -qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 -FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs -ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc -celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X -CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB -tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 -cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P -4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F -kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L -3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx -/uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa -DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP -e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu -WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ -DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub -DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u ------END CERTIFICATE----- - -SecureTrust CA -============== ------BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy -dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe -BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX -OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t -DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH -GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b -01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH -ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj -aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ -KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu -SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf -mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ -nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE----- - -Secure Global CA -================ ------BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH -bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg -MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg -Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx -YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ -bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g -8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV -HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi -0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn -oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA -MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ -OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn -CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 -3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE----- - -COMODO Certification Authority -============================== ------BEGIN CERTIFICATE----- -MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE -BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG -A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 -dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb -MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD -T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH -+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww -xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV -4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA -1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI -rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k -b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC -AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP -OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ -RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc -IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN -+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== ------END CERTIFICATE----- - -COMODO ECC Certification Authority -================================== ------BEGIN CERTIFICATE----- -MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC -R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE -ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix -GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR -Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo -b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X -4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni -wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG -FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA -U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= ------END CERTIFICATE----- - -Certigna -======== ------BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw -EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 -MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI -Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q -XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH -GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p -ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg -DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf -Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ -tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ -BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J -SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA -hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ -ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu -PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY -1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw -WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE----- - -ePKI Root Certification Authority -================================= ------BEGIN CERTIFICATE----- -MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG -EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg -Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx -MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq -MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs -IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi -lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv -qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX -12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O -WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ -ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao -lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ -vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi -Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi -MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH -ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 -1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq -KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV -xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP -NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r -GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE -xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx -gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy -sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD -BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= ------END CERTIFICATE----- - -certSIGN ROOT CA -================ ------BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD -VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa -Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE -CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I -JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH -rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 -ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD -0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 -AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B -Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB -AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 -SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 -x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt -vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz -TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD ------END CERTIFICATE----- - -NetLock Arany (Class Gold) Főtanúsítvány -======================================== ------BEGIN CERTIFICATE----- -MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G -A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 -dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB -cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx -MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO -ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv -biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 -c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu -0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw -/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk -H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw -fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 -neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB -BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW -qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta -YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC -bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna -NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu -dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= ------END CERTIFICATE----- - -SecureSign RootCA11 -=================== ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi -SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS -b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw -KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 -cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL -TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO -wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq -g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP -O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA -bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX -t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh -OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r -bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ -Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 -y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 -lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= ------END CERTIFICATE----- - -Microsec e-Szigno Root CA 2009 -============================== ------BEGIN CERTIFICATE----- -MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER -MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv -c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o -dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE -BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt -U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA -fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG -0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA -pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm -1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC -AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf -QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE -FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o -lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX -I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 -tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 -yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi -LXpUq3DDfSJlgnCW ------END CERTIFICATE----- - -GlobalSign Root CA - R3 -======================= ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv -YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh -bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT -aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln -bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt -iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ -0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 -rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl -OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 -xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE -FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 -lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 -EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E -bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 -YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r -kpeDMdmztcpHWD9f ------END CERTIFICATE----- - -Autoridad de Certificacion Firmaprofesional CIF A62634068 -========================================================= ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA -BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 -MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw -QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB -NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD -Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P -B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY -7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH -ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI -plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX -MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX -LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK -bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU -vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud -EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH -DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp -cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA -bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx -ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx -51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk -R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP -T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f -Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl -osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR -crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR -saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD -KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi -6Et8Vcad+qMUu2WFbm5PEn4KPJ2V ------END CERTIFICATE----- - -Izenpe.com -========== ------BEGIN CERTIFICATE----- -MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG -EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz -MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu -QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ -03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK -ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU -+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC -PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT -OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK -F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK -0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ -0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB -leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID -AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ -SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG -NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx -MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O -BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l -Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga -kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q -hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs -g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 -aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 -nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC -ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo -Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z -WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== ------END CERTIFICATE----- - -Go Daddy Root Certificate Authority - G2 -======================================== ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT -B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu -MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 -MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 -b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G -A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq -9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD -+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd -fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl -NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC -MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 -BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac -vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r -5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV -N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO -LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 ------END CERTIFICATE----- - -Starfield Root Certificate Authority - G2 -========================================= ------BEGIN CERTIFICATE----- -MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT -B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s -b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 -eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw -DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg -VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB -dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv -W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs -bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk -N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf -ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU -JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol -TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx -4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw -F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K -pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ -c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 ------END CERTIFICATE----- - -Starfield Services Root Certificate Authority - G2 -================================================== ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT -B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s -b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl -IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV -BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT -dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg -Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 -h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa -hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP -LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB -rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG -SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP -E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy -xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd -iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza -YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 ------END CERTIFICATE----- - -AffirmTrust Commercial -====================== ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw -MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly -bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb -DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV -C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 -BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww -MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV -HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG -hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi -qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv -0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh -sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= ------END CERTIFICATE----- - -AffirmTrust Networking -====================== ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw -MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly -bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE -Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI -dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 -/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb -h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV -HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu -UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 -12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 -WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 -/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= ------END CERTIFICATE----- - -AffirmTrust Premium -=================== ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy -OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy -dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn -BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV -5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs -+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd -GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R -p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI -S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 -6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 -/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo -+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv -MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg -Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC -6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S -L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK -+4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV -BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg -IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 -g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb -zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== ------END CERTIFICATE----- - -AffirmTrust Premium ECC -======================= ------BEGIN CERTIFICATE----- -MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV -BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx -MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U -cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ -N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW -BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK -BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X -57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM -eQ== ------END CERTIFICATE----- - -Certum Trusted Network CA -========================= ------BEGIN CERTIFICATE----- -MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK -ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy -MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU -ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC -l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J -J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 -fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 -cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB -Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw -DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj -jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 -mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj -Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI -03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= ------END CERTIFICATE----- - -TWCA Root Certification Authority -================================= ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ -VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG -EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB -IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK -AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx -QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC -oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP -4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r -y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB -BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG -9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC -mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW -QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY -T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny -Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== ------END CERTIFICATE----- - -Security Communication RootCA2 -============================== ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc -U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh -dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC -SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy -aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ -+T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R -3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV -spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K -EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 -QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB -CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj -u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk -3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q -tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 -mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 ------END CERTIFICATE----- - -Actalis Authentication Root CA -============================== ------BEGIN CERTIFICATE----- -MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM -BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE -AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky -MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz -IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 -IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ -wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa -by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 -zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f -YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 -oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l -EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 -hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 -EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 -jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY -iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt -ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI -WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 -JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx -K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ -Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC -4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo -2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz -lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem -OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 -vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== ------END CERTIFICATE----- - -Buypass Class 2 Root CA -======================= ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X -DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 -eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 -g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn -9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b -/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU -CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff -awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI -zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn -Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX -Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs -M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD -VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF -AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s -A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI -osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S -aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd -DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD -LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 -oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC -wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS -CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN -rJgWVqA= ------END CERTIFICATE----- - -Buypass Class 3 Root CA -======================= ------BEGIN CERTIFICATE----- -MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU -QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X -DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 -eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH -sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR -5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh -7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ -ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH -2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV -/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ -RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA -Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq -j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD -VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF -AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV -cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G -uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG -Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 -ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 -KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz -6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug -UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe -eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi -Cp/HuZc= ------END CERTIFICATE----- - -T-TeleSec GlobalRoot Class 3 -============================ ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM -IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU -cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx -MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz -dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD -ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK -9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU -NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF -iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W -0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr -AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb -fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT -ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h -P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml -e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== ------END CERTIFICATE----- - -D-TRUST Root Class 3 CA 2 2009 -============================== ------BEGIN CERTIFICATE----- -MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK -DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe -Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE -LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw -DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD -ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA -BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv -KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z -p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC -AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ -4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y -eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw -MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G -PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw -OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm -2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 -o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV -dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph -X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= ------END CERTIFICATE----- - -D-TRUST Root Class 3 CA 2 EV 2009 -================================= ------BEGIN CERTIFICATE----- -MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK -DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw -OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK -DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw -OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS -egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh -zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T -7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 -sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 -11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv -cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v -ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El -MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp -b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh -c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ -PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 -nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX -ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA -NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv -w9y4AyHqnxbxLFS1 ------END CERTIFICATE----- - -CA Disig Root R2 -================ ------BEGIN CERTIFICATE----- -MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw -EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp -ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx -EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp -c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC -w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia -xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 -A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S -GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV -g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa -5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE -koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A -Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i -Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV -HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u -Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM -tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV -sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je -dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 -1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx -mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 -utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 -sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg -UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV -7+ZtsH8tZ/3zbBt1RqPlShfppNcL ------END CERTIFICATE----- - -ACCVRAIZ1 -========= ------BEGIN CERTIFICATE----- -MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB -SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 -MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH -UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM -jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 -RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD -aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ -0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG -WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 -8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR -5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J -9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK -Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw -Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu -Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 -VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM -Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA -QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh -AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA -YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj -AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA -IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk -aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 -dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 -MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI -hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E -R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN -YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 -nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ -TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 -sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h -I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg -Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd -3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p -EfbRD0tVNEYqi4Y7 ------END CERTIFICATE----- - -TWCA Global Root CA -=================== ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT -CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD -QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK -EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg -Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C -nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV -r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR -Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV -tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W -KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 -sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p -yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn -kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI -zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC -AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g -cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn -LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M -8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg -/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg -lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP -A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m -i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 -EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 -zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= ------END CERTIFICATE----- - -TeliaSonera Root CA v1 -====================== ------BEGIN CERTIFICATE----- -MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE -CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 -MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW -VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ -6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA -3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k -B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn -Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH -oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 -F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ -oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 -gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc -TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB -AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW -DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm -zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx -0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW -pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV -G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc -c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT -JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 -qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 -Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems -WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= ------END CERTIFICATE----- - -T-TeleSec GlobalRoot Class 2 -============================ ------BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM -IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU -cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx -MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz -dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD -ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ -SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F -vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 -2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV -WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy -YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 -r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf -vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR -3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN -9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== ------END CERTIFICATE----- - -Atos TrustedRoot 2011 -===================== ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU -cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 -MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG -A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV -hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr -54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ -DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 -HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR -z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R -l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ -bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB -CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h -k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh -TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 -61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G -3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed ------END CERTIFICATE----- - -QuoVadis Root CA 1 G3 -===================== ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG -A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv -b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN -MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg -RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE -PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm -PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 -Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN -ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l -g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV -7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX -9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f -iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg -t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI -hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC -MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 -GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct -Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP -+V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh -3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa -wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 -O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 -FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV -hMJKzRwuJIczYOXD ------END CERTIFICATE----- - -QuoVadis Root CA 2 G3 -===================== ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG -A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv -b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN -MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg -RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh -ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY -NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t -oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o -MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l -V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo -L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ -sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD -6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh -lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI -hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 -AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K -pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 -x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz -dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X -U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw -mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD -zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN -JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr -O3jtZsSOeWmD3n+M ------END CERTIFICATE----- - -QuoVadis Root CA 3 G3 -===================== ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG -A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv -b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN -MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg -RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 -IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL -Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe -6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 -I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U -VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 -5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi -Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM -dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt -rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI -hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px -KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS -t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ -TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du -DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib -Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD -hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX -0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW -dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 -PpxxVJkES/1Y+Zj0 ------END CERTIFICATE----- - -DigiCert Assured ID Root G2 -=========================== ------BEGIN CERTIFICATE----- -MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw -IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw -MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL -ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH -35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq -bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw -VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP -YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn -lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO -w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv -0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz -d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW -hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M -jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo -IhNzbM8m9Yop5w== ------END CERTIFICATE----- - -DigiCert Assured ID Root G3 -=========================== ------BEGIN CERTIFICATE----- -MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD -VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 -MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ -BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb -RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs -KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF -UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy -YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy -1vUhZscv6pZjamVFkpUBtA== ------END CERTIFICATE----- - -DigiCert Global Root G2 -======================= ------BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw -HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx -MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 -dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ -kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO -3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV -BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM -UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB -o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu -5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr -F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U -WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH -QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ -iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE----- - -DigiCert Global Root G3 -======================= ------BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV -UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD -VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw -MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k -aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C -AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O -YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp -Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y -3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 -VOKa5Vt8sycX ------END CERTIFICATE----- - -DigiCert Trusted Root G4 -======================== ------BEGIN CERTIFICATE----- -MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw -HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 -MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp -pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o -k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa -vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY -QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 -MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm -mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 -f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH -dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 -oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud -DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD -ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY -ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr -yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy -7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah -ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN -5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb -/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa -5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK -G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP -82Z+ ------END CERTIFICATE----- - -COMODO RSA Certification Authority -================================== ------BEGIN CERTIFICATE----- -MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE -BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG -A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC -R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE -ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn -dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ -FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ -5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG -x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX -2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL -OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 -sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C -GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 -WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E -FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w -DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt -rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ -nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg -tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW -sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp -pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA -zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq -ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 -7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I -LaZRfyHBNVOFBkpdn627G190 ------END CERTIFICATE----- - -USERTrust RSA Certification Authority -===================================== ------BEGIN CERTIFICATE----- -MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE -BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK -ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE -BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK -ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh -dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz -0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j -Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn -RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O -+T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq -/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE -Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM -lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 -yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ -eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd -BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF -MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW -FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ -7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ -Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM -8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi -FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi -yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c -J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw -sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx -Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 ------END CERTIFICATE----- - -USERTrust ECC Certification Authority -===================================== ------BEGIN CERTIFICATE----- -MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC -VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC -VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU -aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 -0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez -nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV -HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB -HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu -9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= ------END CERTIFICATE----- - -GlobalSign ECC Root CA - R5 -=========================== ------BEGIN CERTIFICATE----- -MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb -R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD -EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb -R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD -EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 -SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS -h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd -BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx -uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 -yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 ------END CERTIFICATE----- - -IdenTrust Commercial Root CA 1 -============================== ------BEGIN CERTIFICATE----- -MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG -EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS -b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES -MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB -IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld -hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ -mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi -1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C -XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl -3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy -NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV -WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg -xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix -uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC -AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI -hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH -6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg -ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt -ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV -YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX -feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro -kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe -2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz -Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R -cGzM7vRX+Bi6hG6H ------END CERTIFICATE----- - -IdenTrust Public Sector Root CA 1 -================================= ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG -EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv -ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV -UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS -b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy -P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 -Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI -rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf -qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS -mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn -ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh -LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v -iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL -4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B -Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw -DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj -t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A -mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt -GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt -m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx -NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 -Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI -ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC -ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ -3Wl9af0AVqW3rLatt8o+Ae+c ------END CERTIFICATE----- - -Entrust Root Certification Authority - G2 -========================================= ------BEGIN CERTIFICATE----- -MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV -BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy -bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug -b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw -HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT -DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx -OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s -eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP -/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz -HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU -s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y -TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx -AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 -0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z -iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ -Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi -nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ -vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO -e4pIb4tF9g== ------END CERTIFICATE----- - -Entrust Root Certification Authority - EC1 -========================================== ------BEGIN CERTIFICATE----- -MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx -FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn -YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl -ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw -FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs -LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg -dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt -IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy -AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef -9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE -FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h -vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 -kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G ------END CERTIFICATE----- - -CFCA EV ROOT -============ ------BEGIN CERTIFICATE----- -MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE -CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB -IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw -MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD -DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV -BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD -7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN -uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW -ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 -xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f -py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K -gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol -hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ -tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf -BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB -/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB -ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q -ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua -4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG -E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX -BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn -aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy -PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX -kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C -ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su ------END CERTIFICATE----- - -OISTE WISeKey Global Root GB CA -=============================== ------BEGIN CERTIFICATE----- -MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG -EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl -ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw -MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD -VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds -b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX -scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP -rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk -9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o -Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg -GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI -hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD -dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 -VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui -HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic -Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= ------END CERTIFICATE----- - -SZAFIR ROOT CA2 -=============== ------BEGIN CERTIFICATE----- -MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG -A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV -BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ -BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD -VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q -qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK -DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE -2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ -ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi -ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P -AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC -AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 -O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 -oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul -4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 -+/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== ------END CERTIFICATE----- - -Certum Trusted Network CA 2 -=========================== ------BEGIN CERTIFICATE----- -MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE -BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 -bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y -ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ -TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB -IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 -7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o -CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b -Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p -uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 -GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ -9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB -Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye -hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM -BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI -hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW -Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA -L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo -clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM -pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb -w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo -J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm -ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX -is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 -zAYspsbiDrW5viSP ------END CERTIFICATE----- - -Hellenic Academic and Research Institutions RootCA 2015 -======================================================= ------BEGIN CERTIFICATE----- -MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT -BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 -aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl -YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx -MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg -QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV -BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw -MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv -bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh -iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ -6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd -FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr -i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F -GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 -fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu -iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc -Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD -AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI -hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ -D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM -d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y -d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn -82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb -davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F -Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt -J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa -JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q -p/UsQu0yrbYhnr68 ------END CERTIFICATE----- - -Hellenic Academic and Research Institutions ECC RootCA 2015 -=========================================================== ------BEGIN CERTIFICATE----- -MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 -aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u -cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj -aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw -MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj -IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD -VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 -Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP -dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK -Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O -BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA -GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn -dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR ------END CERTIFICATE----- - -ISRG Root X1 -============ ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE -BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD -EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG -EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT -DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r -Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 -3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K -b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN -Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ -4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf -1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu -hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH -usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r -OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G -A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY -9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV -0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt -hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw -TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx -e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA -JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD -YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n -JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ -m+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- - -AC RAIZ FNMT-RCM -================ ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT -AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw -MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD -TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf -qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr -btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL -j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou -08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw -WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT -tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ -47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC -ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa -i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE -FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o -dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD -nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s -D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ -j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT -Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW -+YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 -Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d -8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm -5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG -rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= ------END CERTIFICATE----- - -Amazon Root CA 1 -================ ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD -VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 -MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv -bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH -FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ -gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t -dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce -VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB -/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 -DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM -CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy -8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa -2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 -xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- - -Amazon Root CA 2 -================ ------BEGIN CERTIFICATE----- -MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD -VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 -MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv -bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC -ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 -kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp -N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 -AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd -fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx -kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS -btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 -Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN -c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ -3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw -DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA -A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY -+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE -YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW -xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ -gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW -aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV -Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 -KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi -JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= ------END CERTIFICATE----- - -Amazon Root CA 3 -================ ------BEGIN CERTIFICATE----- -MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG -EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy -NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ -MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB -f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr -Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 -rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc -eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== ------END CERTIFICATE----- - -Amazon Root CA 4 -================ ------BEGIN CERTIFICATE----- -MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG -EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy -NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ -MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN -/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri -83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV -HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA -MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 -AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== ------END CERTIFICATE----- - -TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 -============================================= ------BEGIN CERTIFICATE----- -MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT -D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr -IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g -TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp -ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD -VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt -c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth -bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 -IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 -6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc -wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 -3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 -WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU -ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ -KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh -AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc -lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R -e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j -q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= ------END CERTIFICATE----- - -GDCA TrustAUTH R5 ROOT -====================== ------BEGIN CERTIFICATE----- -MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw -BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD -DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow -YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ -IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs -AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p -OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr -pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ -9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ -xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM -R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ -D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 -oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx -9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg -p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 -H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 -6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd -+PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ -HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD -F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ -8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv -/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT -aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== ------END CERTIFICATE----- - -SSL.com Root Certification Authority RSA -======================================== ------BEGIN CERTIFICATE----- -MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM -BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x -MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw -MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx -EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM -LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C -Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 -P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge -oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp -k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z -fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ -gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 -UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 -1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s -bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV -HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr -dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf -ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl -u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq -erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj -MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ -vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI -Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y -wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI -WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= ------END CERTIFICATE----- - -SSL.com Root Certification Authority ECC -======================================== ------BEGIN CERTIFICATE----- -MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV -BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv -BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy -MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO -BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv -bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA -BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ -8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR -hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT -jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW -e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z -5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl ------END CERTIFICATE----- - -SSL.com EV Root Certification Authority RSA R2 -============================================== ------BEGIN CERTIFICATE----- -MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w -DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u -MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy -MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI -DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD -VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN -BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh -hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w -cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO -Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ -B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh -CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim -9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto -RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm -JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 -+qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV -HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp -qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 -++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx -Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G -guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz -OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 -CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq -lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR -rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 -hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX -9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== ------END CERTIFICATE----- - -SSL.com EV Root Certification Authority ECC -=========================================== ------BEGIN CERTIFICATE----- -MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV -BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy -BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw -MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx -EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM -LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB -BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy -3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O -BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe -5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ -N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm -m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== ------END CERTIFICATE----- - -GlobalSign Root CA - R6 -======================= ------BEGIN CERTIFICATE----- -MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX -R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds -b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i -YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs -U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss -grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE -3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF -vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM -PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ -azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O -WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy -CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP -0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN -b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE -AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV -HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN -nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 -lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY -BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym -Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr -3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 -0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T -uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK -oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t -JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= ------END CERTIFICATE----- - -OISTE WISeKey Global Root GC CA -=============================== ------BEGIN CERTIFICATE----- -MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD -SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo -MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa -Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL -ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh -bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr -VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab -NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd -BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E -AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk -AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 ------END CERTIFICATE----- - -UCA Global G2 Root -================== ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG -EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x -NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU -cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT -oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV -8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS -h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o -LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ -R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe -KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa -4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc -OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 -8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O -BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo -5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 -1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A -Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 -yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX -c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo -jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk -bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x -ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn -RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== ------END CERTIFICATE----- - -UCA Extended Validation Root -============================ ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG -EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u -IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G -A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs -iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF -Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu -eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR -59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH -0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR -el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv -B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth -WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS -NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS -3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL -BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR -ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM -aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 -dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb -+7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW -F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi -GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc -GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi -djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr -dhh2n1ax ------END CERTIFICATE----- - -Certigna Root CA -================ ------BEGIN CERTIFICATE----- -MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE -BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ -MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda -MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz -MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX -stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz -KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 -JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 -XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq -4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej -wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ -lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI -jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ -/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw -HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of -1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy -dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h -LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl -cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt -OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP -TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq -7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 -4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd -8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS -6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY -tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS -aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde -E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= ------END CERTIFICATE----- - -emSign Root CA - G1 -=================== ------BEGIN CERTIFICATE----- -MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET -MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl -ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx -ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk -aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN -LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 -cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW -DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ -6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH -hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG -MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 -vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q -NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q -+Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih -U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx -iN66zB+Afko= ------END CERTIFICATE----- - -emSign ECC Root CA - G3 -======================= ------BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG -A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg -MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 -MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 -ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g -RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc -58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr -MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC -AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D -CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 -jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE----- - -emSign Root CA - C1 -=================== ------BEGIN CERTIFICATE----- -MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx -EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp -Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE -BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD -ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up -ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ -Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX -OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V -I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms -lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ -XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD -ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp -/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 -NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 -wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ -BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= ------END CERTIFICATE----- - -emSign ECC Root CA - C3 -======================= ------BEGIN CERTIFICATE----- -MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG -A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF -Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE -BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD -ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd -6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 -SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA -B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA -MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU -ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== ------END CERTIFICATE----- - -Hongkong Post Root CA 3 -======================= ------BEGIN CERTIFICATE----- -MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG -A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK -Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 -MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv -bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX -SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz -iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf -jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim -5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe -sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj -0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ -JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u -y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h -+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG -xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID -AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e -i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN -AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw -W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld -y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov -+BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc -eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw -9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 -nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY -hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB -60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq -dBb9HxEGmpv0 ------END CERTIFICATE----- - -Entrust Root Certification Authority - G4 -========================================= ------BEGIN CERTIFICATE----- -MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV -BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu -bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 -dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 -dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT -AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 -L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv -cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D -umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV -3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds -8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ -e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 -ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X -xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV -7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 -dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW -Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T -AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n -MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q -jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht -7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK -YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt -jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ -m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW -RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA -JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G -+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT -kcpG2om3PVODLAgfi49T3f+sHw== ------END CERTIFICATE----- - -Microsoft ECC Root Certificate Authority 2017 -============================================= ------BEGIN CERTIFICATE----- -MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV -UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND -IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 -MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw -NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ -BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 -thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB -eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM -+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf -Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR -eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= ------END CERTIFICATE----- - -Microsoft RSA Root Certificate Authority 2017 -============================================= ------BEGIN CERTIFICATE----- -MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG -EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg -UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw -NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u -MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw -ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml -7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e -S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 -1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ -dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F -yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS -MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr -lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ -0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ -ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC -NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og -6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 -dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk -+ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex -/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy -AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW -ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE -7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT -c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D -5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E ------END CERTIFICATE----- - -e-Szigno Root CA 2017 -===================== ------BEGIN CERTIFICATE----- -MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw -DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt -MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa -Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE -CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp -Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx -s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G -A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv -vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA -tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO -svxyqltZ+efcMQ== ------END CERTIFICATE----- - -certSIGN Root CA G2 -=================== ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw -EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy -MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH -TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 -N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk -abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg -wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp -dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh -ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 -jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf -95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc -z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL -iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud -DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB -ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC -b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB -/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 -8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 -BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW -atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU -Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M -NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N -0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= ------END CERTIFICATE----- - -Trustwave Global Certification Authority -======================================== ------BEGIN CERTIFICATE----- -MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV -UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 -ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u -IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV -UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 -ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u -IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 -zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf -LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq -stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o -WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ -OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 -Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE -uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm -+9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj -ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB -BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H -PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H -ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla -4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R -vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd -zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O -856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH -Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu -3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP -29FpHOTKyeC2nOnOcXHebD8WpHk= ------END CERTIFICATE----- - -Trustwave Global ECC P256 Certification Authority -================================================= ------BEGIN CERTIFICATE----- -MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER -MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy -dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 -NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj -43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm -P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt -0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz -RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 ------END CERTIFICATE----- - -Trustwave Global ECC P384 Certification Authority -================================================= ------BEGIN CERTIFICATE----- -MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER -MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy -dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 -NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH -Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr -/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV -HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn -ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl -CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== ------END CERTIFICATE----- - -NAVER Global Root Certification Authority -========================================= ------BEGIN CERTIFICATE----- -MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG -A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD -DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4 -NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT -UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb -UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW -+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7 -XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2 -aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4 -Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z -VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B -A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai -cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy -YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV -HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB -Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK -21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB -jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx -hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg -E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH -D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ -A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY -qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG -I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg -kpzNNIaRkPpkUZ3+/uul9XXeifdy ------END CERTIFICATE----- - -AC RAIZ FNMT-RCM SERVIDORES SEGUROS -=================================== ------BEGIN CERTIFICATE----- -MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF -UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy -NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4 -MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt -UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB -QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA -BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2 -LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG -SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD -zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c= ------END CERTIFICATE----- - -GlobalSign Root R46 -=================== ------BEGIN CERTIFICATE----- -MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV -BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv -b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX -BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es -CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/ -r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje -2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt -bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj -K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4 -12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on -ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls -eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9 -vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM -BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg -JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy -gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92 -CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm -OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq -JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye -qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz -nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7 -DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3 -QEUxeCp6 ------END CERTIFICATE----- - -GlobalSign Root E46 -=================== ------BEGIN CERTIFICATE----- -MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT -AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg -RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV -BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq -hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB -jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj -QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL -gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk -vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ -CAezNIm8BZ/3Hobui3A= ------END CERTIFICATE----- - -GLOBALTRUST 2020 -================ ------BEGIN CERTIFICATE----- -MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx -IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT -VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh -BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy -MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi -D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO -VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM -CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm -fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA -A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR -JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG -DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU -clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ -mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud -IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA -VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw -4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9 -iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS -8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2 -HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS -vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918 -oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF -YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl -gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== ------END CERTIFICATE----- - -ANF Secure Server Root CA -========================= ------BEGIN CERTIFICATE----- -MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2MzI4 -NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lv -bjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNVBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3Qg -Q0EwHhcNMTkwOTA0MTAwMDM4WhcNMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEw -MQswCQYDVQQGEwJFUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQw -EgYDVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9vdCBDQTCC -AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCjcqQZAZ2cC4Ffc0m6p6zz -BE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9qyGFOtibBTI3/TO80sh9l2Ll49a2pcbnv -T1gdpd50IJeh7WhM3pIXS7yr/2WanvtH2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcv -B2VSAKduyK9o7PQUlrZXH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXse -zx76W0OLzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyRp1RM -VwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQzW7i1o0TJrH93PB0j -7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/SiOL9V8BY9KHcyi1Swr1+KuCLH5z -JTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJnLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe -8TZBAQIvfXOn3kLMTOmJDVb3n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVO -Hj1tyRRM4y5Bu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj -o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAOBgNVHQ8BAf8E -BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEATh65isagmD9uw2nAalxJ -UqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzx -j6ptBZNscsdW699QIyjlRRA96Gejrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDt -dD+4E5UGUcjohybKpFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM -5gf0vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjqOknkJjCb -5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ/zo1PqVUSlJZS2Db7v54 -EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ92zg/LFis6ELhDtjTO0wugumDLmsx2d1H -hk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGy -g77FGr8H6lnco4g175x2MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3 -r5+qPeoott7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= ------END CERTIFICATE----- - -Certum EC-384 CA -================ ------BEGIN CERTIFICATE----- -MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQswCQYDVQQGEwJQ -TDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy -dGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2 -MDcyNDU0WhcNNDMwMzI2MDcyNDU0WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERh -dGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx -GTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATEKI6rGFtq -vm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7TmFy8as10CW4kjPMIRBSqn -iBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68KjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD -VR0OBBYEFI0GZnQkdjrzife81r1HfS+8EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNo -ADBlAjADVS2m5hjEfO/JUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0 -QoSZ/6vnnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= ------END CERTIFICATE----- - -Certum Trusted Root CA -====================== ------BEGIN CERTIFICATE----- -MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQG -EwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0g -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0Ew -HhcNMTgwMzE2MTIxMDEzWhcNNDMwMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMY -QXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBB -dXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZn0EGze2jusDbCSzBfN8p -fktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/qp1x4EaTByIVcJdPTsuclzxFUl6s1wB52 -HO8AU5853BSlLCIls3Jy/I2z5T4IHhQqNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2 -fJmItdUDmj0VDT06qKhF8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGt -g/BKEiJ3HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGamqi4 -NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi7VdNIuJGmj8PkTQk -fVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSFytKAQd8FqKPVhJBPC/PgP5sZ0jeJ -P/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0PqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSY -njYJdmZm/Bo/6khUHL4wvYBQv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHK -HRzQ+8S1h9E6Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 -vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBAEii1QAL -LtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4WxmB82M+w85bj/UvXgF2Ez8s -ALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvozMrnadyHncI013nR03e4qllY/p0m+jiGPp2K -h2RX5Rc64vmNueMzeMGQ2Ljdt4NR5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8 -CYyqOhNf6DR5UMEQGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA -4kZf5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq0Uc9Nneo -WWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7DP78v3DSk+yshzWePS/Tj -6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTMqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmT -OPQD8rv7gmsHINFSH5pkAnuYZttcTVoP0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZck -bxJF0WddCajJFdr60qZfE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb ------END CERTIFICATE----- - -TunTrust Root CA -================ ------BEGIN CERTIFICATE----- -MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQELBQAwYTELMAkG -A1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUgQ2VydGlmaWNhdGlvbiBFbGVj -dHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJvb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQw -NDI2MDg1NzU2WjBhMQswCQYDVQQGEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBD -ZXJ0aWZpY2F0aW9uIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIw -DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZn56eY+hz -2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd2JQDoOw05TDENX37Jk0b -bjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgFVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7 -NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAd -gjH8KcwAWJeRTIAAHDOFli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViW -VSHbhlnUr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2eY8f -Tpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIbMlEsPvLfe/ZdeikZ -juXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISgjwBUFfyRbVinljvrS5YnzWuioYas -DXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwS -VXAkPcvCFDVDXSdOvsC9qnyW5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI -04Y+oXNZtPdEITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 -90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+zxiD2BkewhpMl -0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYuQEkHDVneixCwSQXi/5E/S7fd -Ao74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRY -YdZ2vyJ/0Adqp2RT8JeNnYA/u8EH22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJp -adbGNjHh/PqAulxPxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65x -xBzndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5Xc0yGYuP -jCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7bnV2UqL1g52KAdoGDDIzM -MEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9z -ZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZHu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3r -AZ3r2OvEhJn7wAzMMujjd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= ------END CERTIFICATE----- - -HARICA TLS RSA Root CA 2021 -=========================== ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQG -EwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u -cyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUz -OFoXDTQ1MDIxMzEwNTUzN1owbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRl -bWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNB -IFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569lmwVnlskN -JLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE4VGC/6zStGndLuwRo0Xu -a2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uva9of08WRiFukiZLRgeaMOVig1mlDqa2Y -Ulhu2wr7a89o+uOkXjpFc5gH6l8Cct4MpbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K -5FrZx40d/JiZ+yykgmvwKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEv -dmn8kN3bLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcYAuUR -0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqBAGMUuTNe3QvboEUH -GjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYqE613TBoYm5EPWNgGVMWX+Ko/IIqm -haZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHrW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQ -CPxrvrNQKlr9qEgYRtaQQJKQCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8G -A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAUX15QvWiWkKQU -EapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3f5Z2EMVGpdAgS1D0NTsY9FVq -QRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxajaH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxD -QpSbIPDRzbLrLFPCU3hKTwSUQZqPJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcR -j88YxeMn/ibvBZ3PzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5 -vZStjBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0/L5H9MG0 -qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pTBGIBnfHAT+7hOtSLIBD6 -Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79aPib8qXPMThcFarmlwDB31qlpzmq6YR/ -PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YWxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnn -kf3/W9b3raYvAwtt41dU63ZTGI0RmLo= ------END CERTIFICATE----- - -HARICA TLS ECC Root CA 2021 -=========================== ------BEGIN CERTIFICATE----- -MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQswCQYDVQQGEwJH -UjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBD -QTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoX -DTQ1MDIxMzExMDEwOVowbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWlj -IGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJv -b3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7KKrxcm1l -AEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9YSTHMmE5gEYd103KUkE+b -ECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW -0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAi -rcJRQO9gcS3ujwLEXQNwSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/Qw -CZ61IygNnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps ------END CERTIFICATE----- - -Autoridad de Certificacion Firmaprofesional CIF A62634068 -========================================================= ------BEGIN CERTIFICATE----- -MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCRVMxQjBA -BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 -MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIw -QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB -NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD -Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P -B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY -7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH -ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI -plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX -MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX -LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK -bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU -vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1Ud -DgQWBBRlzeurNR4APn7VdMActHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4w -gZswgZgGBFUdIAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j -b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABCAG8AbgBhAG4A -bwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAwADEANzAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9miWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL -4QjbEwj4KKE1soCzC1HA01aajTNFSa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDb -LIpgD7dvlAceHabJhfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1il -I45PVf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZEEAEeiGaP -cjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV1aUsIC+nmCjuRfzxuIgA -LI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2tCsvMo2ebKHTEm9caPARYpoKdrcd7b/+A -lun4jWq9GJAd/0kakFI3ky88Al2CdgtR5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH -9IBk9W6VULgRfhVwOEqwf9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpf -NIbnYrX9ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNKGbqE -ZycPvEJdvSRUDewdcAZfpLz6IHxV ------END CERTIFICATE----- - -vTrus ECC Root CA -================= ------BEGIN CERTIFICATE----- -MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMwRzELMAkGA1UE -BhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBS -b290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDczMTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAa -BgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYw -EAYHKoZIzj0CAQYFK4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+c -ToL0v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUde4BdS49n -TPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYDVR0TAQH/BAUwAwEB/zAO -BgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwV53dVvHH4+m4SVBrm2nDb+zDfSXkV5UT -QJtS0zvzQBm8JsctBp61ezaf9SXUY2sAAjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQL -YgmRWAD5Tfs0aNoJrSEGGJTO ------END CERTIFICATE----- - -vTrus Root CA -============= ------BEGIN CERTIFICATE----- -MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQELBQAwQzELMAkG -A1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xFjAUBgNVBAMTDXZUcnVzIFJv -b3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMxMDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoG -A1UEChMTaVRydXNDaGluYSBDby4sTHRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJ -KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZots -SKYcIrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykUAyyNJJrI -ZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+GrPSbcKvdmaVayqwlHeF -XgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z98Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KA -YPxMvDVTAWqXcoKv8R1w6Jz1717CbMdHflqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70 -kLJrxLT5ZOrpGgrIDajtJ8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2 -AXPKBlim0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZNpGvu -/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQUqqzApVg+QxMaPnu -1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHWOXSuTEGC2/KmSNGzm/MzqvOmwMVO -9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMBAAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYg -scasGrz2iTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOC -AgEAKbqSSaet8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd -nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1jbhd47F18iMjr -jld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvMKar5CKXiNxTKsbhm7xqC5PD4 -8acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIivTDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJn -xDHO2zTlJQNgJXtxmOTAGytfdELSS8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554Wg -icEFOwE30z9J4nfrI8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4 -sEb9b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNBUvupLnKW -nyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1PTi07NEPhmg4NpGaXutIc -SkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929vensBxXVsFy6K2ir40zSbofitzmdHxghm+H -l3s= ------END CERTIFICATE----- - -ISRG Root X2 -============ ------BEGIN CERTIFICATE----- -MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV -UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT -UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT -MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS -RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H -ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb -d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV -HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF -cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5 -U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn ------END CERTIFICATE----- - -HiPKI Root CA - G1 -================== ------BEGIN CERTIFICATE----- -MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQG -EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xGzAZBgNVBAMMEkhpUEtJ -IFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRaFw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYT -AlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kg -Um9vdCBDQSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0 -o9QwqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twvVcg3Px+k -wJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6lZgRZq2XNdZ1AYDgr/SE -YYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnzQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsA -GJZMoYFL3QRtU6M9/Aes1MU3guvklQgZKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfd -hSi8MEyr48KxRURHH+CKFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj -1jOXTyFjHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDry+K4 -9a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ/W3c1pzAtH2lsN0/ -Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgMa/aOEmem8rJY5AIJEzypuxC00jBF -8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYD -VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQD -AgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi -7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqcSE5XCV0vrPSl -tJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6FzaZsT0pPBWGTMpWmWSBUdGSquE -wx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9TcXzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07Q -JNBAsNB1CI69aO4I1258EHBGG3zgiLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv -5wiZqAxeJoBF1PhoL5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+Gpz -jLrFNe85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wrkkVbbiVg -hUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+vhV4nYWBSipX3tUZQ9rb -yltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQUYDksswBVLuT1sw5XxJFBAJw/6KXf6vb/ -yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== ------END CERTIFICATE----- - -GlobalSign ECC Root CA - R4 -=========================== ------BEGIN CERTIFICATE----- -MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYDVQQLExtHbG9i -YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds -b2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgwMTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9i -YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds -b2JhbFNpZ24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkW -ymOxuYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNVHQ8BAf8E -BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/+wpu+74zyTyjhNUwCgYI -KoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147bmF0774BxL4YSFlhgjICICadVGNA3jdg -UM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm ------END CERTIFICATE----- - -GTS Root R1 -=========== ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV -UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg -UjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE -ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM -f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0 -xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+w -B7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXW -nOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk -9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zq -kUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1A -K/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om3xPX -V2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDW -cfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQAD -ggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe -QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRi -ClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34ar -J45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYci -NuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5me -LMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJF -fbdT6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove+ -7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3 -FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3 -gm3c ------END CERTIFICATE----- - -GTS Root R2 -=========== ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV -UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg -UjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE -ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv -CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo7JUl -e3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWIm8Wb -a96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS -+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7M -kogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJG -r61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RWIr9q -S34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73VululycslaVNV -J1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy5okL -dWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQAD -ggIBAB/Kzt3HvqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 -0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyCB19m3H0Q/gxh -swWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2uNmSRXbBoGOqKYcl3qJfEycel -/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMgyALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVn -jWQye+mew4K6Ki3pHrTgSAai/GevHyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y5 -9PYjJbigapordwj6xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M -7YNRTOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924SgJPFI/2R8 -0L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV7LXTWtiBmelDGDfrs7vR -WGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjW -HYbL ------END CERTIFICATE----- - -GTS Root R3 -=========== ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi -MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMw -HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ -R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjO -PQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout -736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24CejQjBA -MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP0/Eq -Er24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azT -L818+FsuVbu/3ZL3pAzcMeGiAjEA/JdmZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV -11RZt+cRLInUue4X ------END CERTIFICATE----- - -GTS Root R4 -=========== ------BEGIN CERTIFICATE----- -MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi -MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQw -HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ -R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjO -PQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu -hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqjQjBA -MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV2Py1 -PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C -r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh -4rsUecrNIdSUtUlD ------END CERTIFICATE----- - -Telia Root CA v2 -================ ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQxCzAJBgNVBAYT -AkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2 -MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQK -DBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZI -hvcNAQEBBQADggIPADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ7 -6zBqAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9vVYiQJ3q -9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9lRdU2HhE8Qx3FZLgmEKn -pNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTODn3WhUidhOPFZPY5Q4L15POdslv5e2QJl -tI5c0BE0312/UqeBAMN/mUWZFdUXyApT7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW -5olWK8jjfN7j/4nlNW4o6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNr -RBH0pUPCTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6WT0E -BXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63RDolUK5X6wK0dmBR4 -M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZIpEYslOqodmJHixBTB0hXbOKSTbau -BcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGjYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7W -xy+G2CQ5MB0GA1UdDgQWBBRyrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYD -VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ -8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi0f6X+J8wfBj5 -tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMMA8iZGok1GTzTyVR8qPAs5m4H -eW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBSSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+C -y748fdHif64W1lZYudogsYMVoe+KTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygC -QMez2P2ccGrGKMOF6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15 -h2Er3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMtTy3EHD70 -sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pTVmBds9hCG1xLEooc6+t9 -xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAWysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQ -raVplI/owd8k+BsHMYeB2F326CjYSlKArBPuUBQemMc= ------END CERTIFICATE----- - -D-TRUST BR Root CA 1 2020 -========================= ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE -RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0EgMSAy -MDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNV -BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7 -dPYSzuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0QVK5buXu -QqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/VbNafAkl1bK6CKBrqx9t -MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu -bmV0L2NybC9kLXRydXN0X2JyX3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP -PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD -AwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFWwKrY7RjEsK70Pvom -AjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHVdWNbFJWcHwHP2NVypw87 ------END CERTIFICATE----- - -D-TRUST EV Root CA 1 2020 -========================= ------BEGIN CERTIFICATE----- -MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE -RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0EgMSAy -MDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNV -BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8 -ZRCC/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rDwpdhQntJ -raOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3OqQo5FD4pPfsazK2/umL -MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu -bmV0L2NybC9kLXRydXN0X2V2X3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj -dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP -PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD -AwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CAy/m0sRtW9XLS/BnR -AjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJbgfM0agPnIjhQW+0ZT0MW ------END CERTIFICATE----- - -DigiCert TLS ECC P384 Root G5 -============================= ------BEGIN CERTIFICATE----- -MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV -UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURpZ2lDZXJ0IFRMUyBFQ0MgUDM4 -NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQg -Um9vdCBHNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1Tzvd -lHJS7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp0zVozptj -n4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICISB4CIfBFqMA4GA1UdDwEB -/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCJao1H5+z8blUD2Wds -Jk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIx -AJSdYsiJvRmEFOml+wG4DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE----- - -DigiCert TLS RSA4096 Root G5 -============================ ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBNMQswCQYDVQQG -EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0 -MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcNNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJV -UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2 -IFJvb3QgRzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS8 -7IE+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG02C+JFvuU -AT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgpwgscONyfMXdcvyej/Ces -tyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZMpG2T6T867jp8nVid9E6P/DsjyG244gXa -zOvswzH016cpVIDPRFtMbzCe88zdH5RDnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnV -DdXifBBiqmvwPXbzP6PosMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9q -TXeXAaDxZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cdLvvy -z6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvXKyY//SovcfXWJL5/ -MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNeXoVPzthwiHvOAbWWl9fNff2C+MIk -wcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPLtgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4E -FgQUUTMc7TZArxfTJc1paPKvTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w -DQYJKoZIhvcNAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw -GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7HPNtQOa27PShN -lnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLFO4uJ+DQtpBflF+aZfTCIITfN -MBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/ -u4cnYiWB39yhL/btp/96j1EuMPikAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9G -OUrYU9DzLjtxpdRv/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh -47a+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilwMUc/dNAU -FvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WFqUITVuwhd4GTWgzqltlJ -yqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCKovfepEWFJqgejF0pW8hL2JpqA15w8oVP -bEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE----- - -Certainly Root R1 -================= ------BEGIN CERTIFICATE----- -MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAwPTELMAkGA1UE -BhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2VydGFpbmx5IFJvb3QgUjEwHhcN -MjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2Vy -dGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP -ADCCAgoCggIBANA21B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O -5MQTvqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbedaFySpvXl -8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b01C7jcvk2xusVtyWMOvwl -DbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGI -XsXwClTNSaa/ApzSRKft43jvRl5tcdF5cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkN -KPl6I7ENPT2a/Z2B7yyQwHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQ -AjeZjOVJ6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA2Cnb -rlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyHWyf5QBGenDPBt+U1 -VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMReiFPCyEQtkA6qyI6BJyLm4SGcprS -p6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud -DgQWBBTgqj8ljZ9EXME66C6ud0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAsz -HQNTVfSVcOQrPbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d -8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi1wrykXprOQ4v -MMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrdrRT90+7iIgXr0PK3aBLXWopB -GsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9ditaY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+ -gjwN/KUD+nsa2UUeYNrEjvn8K8l7lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgH -JBu6haEaBQmAupVjyTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7 -fpYnKx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLyyCwzk5Iw -x06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5nwXARPbv0+Em34yaXOp/S -X3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6OV+KmalBWQewLK8= ------END CERTIFICATE----- - -Certainly Root E1 -================= ------BEGIN CERTIFICATE----- -MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQswCQYDVQQGEwJV -UzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBFMTAeFw0yMTA0 -MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlu -bHkxGjAYBgNVBAMTEUNlcnRhaW5seSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4 -fxzf7flHh4axpMCK+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9 -YBk2QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8EBAMCAQYw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4hevIIgcwCgYIKoZIzj0E -AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 -rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR ------END CERTIFICATE----- - -Security Communication RootCA3 -============================== ------BEGIN CERTIFICATE----- -MIIFfzCCA2egAwIBAgIJAOF8N0D9G/5nMA0GCSqGSIb3DQEBDAUAMF0xCzAJBgNVBAYTAkpQMSUw -IwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMScwJQYDVQQDEx5TZWN1cml0eSBD -b21tdW5pY2F0aW9uIFJvb3RDQTMwHhcNMTYwNjE2MDYxNzE2WhcNMzgwMTE4MDYxNzE2WjBdMQsw -CQYDVQQGEwJKUDElMCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UE -AxMeU2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEA48lySfcw3gl8qUCBWNO0Ot26YQ+TUG5pPDXC7ltzkBtnTCHsXzW7OT4rCmDvu20r -hvtxosis5FaU+cmvsXLUIKx00rgVrVH+hXShuRD+BYD5UpOzQD11EKzAlrenfna84xtSGc4RHwsE -NPXY9Wk8d/Nk9A2qhd7gCVAEF5aEt8iKvE1y/By7z/MGTfmfZPd+pmaGNXHIEYBMwXFAWB6+oHP2 -/D5Q4eAvJj1+XCO1eXDe+uDRpdYMQXF79+qMHIjH7Iv10S9VlkZ8WjtYO/u62C21Jdp6Ts9EriGm -npjKIG58u4iFW/vAEGK78vknR+/RiTlDxN/e4UG/VHMgly1s2vPUB6PmudhvrvyMGS7TZ2crldtY -XLVqAvO4g160a75BflcJdURQVc1aEWEhCmHCqYj9E7wtiS/NYeCVvsq1e+F7NGcLH7YMx3weGVPK -p7FKFSBWFHA9K4IsD50VHUeAR/94mQ4xr28+j+2GaR57GIgUssL8gjMunEst+3A7caoreyYn8xrC -3PsXuKHqy6C0rtOUfnrQq8PsOC0RLoi/1D+tEjtCrI8Cbn3M0V9hvqG8OmpI6iZVIhZdXw3/JzOf -GAN0iltSIEdrRU0id4xVJ/CvHozJgyJUt5rQT9nO/NkuHJYosQLTA70lUhw0Zk8jq/R3gpYd0Vcw -CBEF/VfR2ccCAwEAAaNCMEAwHQYDVR0OBBYEFGQUfPxYchamCik0FW8qy7z8r6irMA4GA1UdDwEB -/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQDcAiMI4u8hOscNtybS -YpOnpSNyByCCYN8Y11StaSWSntkUz5m5UoHPrmyKO1o5yGwBQ8IibQLwYs1OY0PAFNr0Y/Dq9HHu -Tofjcan0yVflLl8cebsjqodEV+m9NU1Bu0soo5iyG9kLFwfl9+qd9XbXv8S2gVj/yP9kaWJ5rW4O -H3/uHWnlt3Jxs/6lATWUVCvAUm2PVcTJ0rjLyjQIUYWg9by0F1jqClx6vWPGOi//lkkZhOpn2ASx -YfQAW0q3nHE3GYV5v4GwxxMOdnE+OoAGrgYWp421wsTL/0ClXI2lyTrtcoHKXJg80jQDdwj98ClZ -XSEIx2C/pHF7uNkegr4Jr2VvKKu/S7XuPghHJ6APbw+LP6yVGPO5DtxnVW5inkYO0QR4ynKudtml -+LLfiAlhi+8kTtFZP1rUPcmTPCtk9YENFpb3ksP+MW/oKjJ0DvRMmEoYDjBU1cXrvMUVnuiZIesn -KwkK2/HmcBhWuwzkvvnoEKQTkrgc4NtnHVMDpCKn3F2SEDzq//wbEBrD2NCcnWXL0CsnMQMeNuE9 -dnUM/0Umud1RvCPHX9jYhxBAEg09ODfnRDwYwFMJZI//1ZqmfHAuc1Uh6N//g7kdPjIe1qZ9LPFm -6Vwdp6POXiUyK+OVrCoHzrQoeIY8LaadTdJ0MN1kURXbg4NR16/9M51NZg== ------END CERTIFICATE----- - -Security Communication ECC RootCA1 -================================== ------BEGIN CERTIFICATE----- -MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYTAkpQMSUwIwYD -VQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYDVQQDEyJTZWN1cml0eSBDb21t -dW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYxNjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTEL -MAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNV -BAMTIlNlY3VyaXR5IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+CnnfdldB9sELLo -5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpKULGjQjBAMB0GA1UdDgQW -BBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAK -BggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3L -snNdo4gIxwwCMQDAqy0Obe0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70e -N9k= ------END CERTIFICATE----- - -BJCA Global Root CA1 -==================== ------BEGIN CERTIFICATE----- -MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQG -EwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJK -Q0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAzMTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkG -A1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQD -DBRCSkNBIEdsb2JhbCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFm -CL3ZxRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZspDyRhyS -sTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O558dnJCNPYwpj9mZ9S1Wn -P3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgRat7GGPZHOiJBhyL8xIkoVNiMpTAK+BcW -yqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRj -eulumijWML3mG90Vr4TqnMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNn -MoH1V6XKV0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/pj+b -OT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZOz2nxbkRs1CTqjSSh -GL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXnjSXWgXSHRtQpdaJCbPdzied9v3pK -H9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMB -AAGjQjBAMB0GA1UdDgQWBBTF7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 -YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3KliawLwQ8hOnThJ -dMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u+2D2/VnGKhs/I0qUJDAnyIm8 -60Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuh -TaRjAv04l5U/BXCga99igUOLtFkNSoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW -4AB+dAb/OMRyHdOoP2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmp -GQrI+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRzznfSxqxx -4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9eVzYH6Eze9mCUAyTF6ps -3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4S -SPfSKcOYKMryMguTjClPPGAyzQWWYezyr/6zcCwupvI= ------END CERTIFICATE----- - -BJCA Global Root CA2 -==================== ------BEGIN CERTIFICATE----- -MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQswCQYDVQQGEwJD -TjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJKQ0Eg -R2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgyMVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UE -BhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRC -SkNBIEdsb2JhbCBSb290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jl -SR9BIgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK++kpRuDCK -/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJKsVF/BvDRgh9Obl+rg/xI -1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8 -W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8g -UXOQwKhbYdDFUDn9hf7B43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== ------END CERTIFICATE----- - -Sectigo Public Server Authentication Root E46 -============================================= ------BEGIN CERTIFICATE----- -MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH -QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2 -ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5 -WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0 -aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr -gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0 -NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud -DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB -/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH -lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U -SAGKcw== ------END CERTIFICATE----- - -Sectigo Public Server Authentication Root R46 -============================================= ------BEGIN CERTIFICATE----- -MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG -EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT -ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1 -OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T -ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3 -DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k -1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf -GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP -FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu -ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz -Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A -wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF -plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ -EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW -6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI -IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c -mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp -E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4 -exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M -0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI -84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m -pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd -Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b -E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm -J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL ------END CERTIFICATE----- - -SSL.com TLS RSA Root CA 2022 -============================ ------BEGIN CERTIFICATE----- -MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQG -EwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBSU0Eg -Um9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloXDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMC -VVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJv -b3QgQ0EgMjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u -9nTPL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OYt6/wNr/y -7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0insS657Lb85/bRi3pZ7Qcac -oOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3PnxEX4MN8/HdIGkWCVDi1FW24IBydm5M -R7d1VVm0U3TZlMZBrViKMWYPHqIbKUBOL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDG -D6C1vBdOSHtRwvzpXGk3R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEW -TO6Af77wdr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS+YCk -8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYSd66UNHsef8JmAOSq -g+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoGAtUjHBPW6dvbxrB6y3snm/vg1UYk -7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2fgTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1Ud -EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsu -N+7jhHonLs0ZNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt -hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsMQtfhWsSWTVTN -j8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvfR4iyrT7gJ4eLSYwfqUdYe5by -iB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjU -o3KUQyxi4U5cMj29TH0ZR6LDSeeWP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqo -ENjwuSfr98t67wVylrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7Egkaib -MOlqbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2wAgDHbICi -vRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3qr5nsLFR+jM4uElZI7xc7 -P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sjiMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB0 -9+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= ------END CERTIFICATE----- - -SSL.com TLS ECC Root CA 2022 -============================ ------BEGIN CERTIFICATE----- -MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV -UzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBFQ0MgUm9v -dCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMx -GDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3Qg -Q0EgMjAyMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWy -JGYmacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFNSeR7T5v1 -5wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSJjy+j6CugFFR7 -81a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NWuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGG -MAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w -7deedWo1dlJF4AIxAMeNb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5 -Zn6g6g== ------END CERTIFICATE----- - -Atos TrustedRoot Root CA ECC TLS 2021 -===================================== ------BEGIN CERTIFICATE----- -MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4wLAYDVQQDDCVB -dG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQswCQYD -VQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3Mg -VHJ1c3RlZFJvb3QgUm9vdCBDQSBFQ0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYT -AkRFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6K -DP/XtXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4AjJn8ZQS -b+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2KCXWfeBmmnoJsmo7jjPX -NtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwW5kp85wxtolrbNa9d+F851F+ -uDrNozZffPc8dz7kUK2o59JZDCaOMDtuCCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGY -a3cpetskz2VAv9LcjBHo9H1/IISpQuQo ------END CERTIFICATE----- - -Atos TrustedRoot Root CA RSA TLS 2021 -===================================== ------BEGIN CERTIFICATE----- -MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBMMS4wLAYDVQQD -DCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQsw -CQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0 -b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNV -BAYTAkRFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BB -l01Z4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYvYe+W/CBG -vevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZkmGbzSoXfduP9LVq6hdK -ZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDsGY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt -0xU6kGpn8bRrZtkh68rZYnxGEFzedUlnnkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVK -PNe0OwANwI8f4UDErmwh3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMY -sluMWuPD0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzygeBY -Br3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8ANSbhqRAvNncTFd+ -rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezBc6eUWsuSZIKmAMFwoW4sKeFYV+xa -fJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lIpw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUdEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0G -CSqGSIb3DQEBDAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS -4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPso0UvFJ/1TCpl -Q3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJqM7F78PRreBrAwA0JrRUITWX -AdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuywxfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9G -slA9hGCZcbUztVdF5kJHdWoOsAgMrr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2Vkt -afcxBPTy+av5EzH4AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9q -TFsR0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuYo7Ey7Nmj -1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5dDTedk+SKlOxJTnbPP/l -PqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcEoji2jbDwN/zIIX8/syQbPYtuzE2wFg2W -HYMfRsCbvUOZ58SWLs5fyQ== ------END CERTIFICATE----- diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/giolibproxy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/giolibproxy.dll deleted file mode 100644 index f348693df..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/giolibproxy.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/gioopenssl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/gioopenssl.dll deleted file mode 100644 index ca4c70fe4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gio/modules/gioopenssl.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsta52dec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsta52dec.dll deleted file mode 100644 index 56f8143d7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsta52dec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaccurip.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaccurip.dll deleted file mode 100644 index 9279d0825..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaccurip.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadaptivedemux2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadaptivedemux2.dll deleted file mode 100644 index fc9471412..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadaptivedemux2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadder.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadder.dll deleted file mode 100644 index ce1e34804..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadder.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmdec.dll deleted file mode 100644 index 52660069b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmdec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmenc.dll deleted file mode 100644 index d7d20bd7d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstadpcmenc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaes.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaes.dll deleted file mode 100644 index 95c0c7230..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaes.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaiff.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaiff.dll deleted file mode 100644 index af096de7b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaiff.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalaw.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalaw.dll deleted file mode 100644 index b7d82e831..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalaw.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalpha.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalpha.dll deleted file mode 100644 index 32a67ebb5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalpha.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalphacolor.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalphacolor.dll deleted file mode 100644 index c66cd8546..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstalphacolor.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamfcodec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamfcodec.dll deleted file mode 100644 index 2b00cba76..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamfcodec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrnb.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrnb.dll deleted file mode 100644 index c82a22128..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrnb.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrwbdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrwbdec.dll deleted file mode 100644 index b4eb803c8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstamrwbdec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstanalyticsoverlay.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstanalyticsoverlay.dll deleted file mode 100644 index 85714fcef..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstanalyticsoverlay.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapetag.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapetag.dll deleted file mode 100644 index d2c8b9213..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapetag.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapp.dll deleted file mode 100644 index 47ce4bde6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstapp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasf.dll deleted file mode 100644 index bcc0ba007..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasf.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasfmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasfmux.dll deleted file mode 100644 index 8b7aa0d18..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasfmux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasio.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasio.dll deleted file mode 100644 index 5b9e0581b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstasio.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstassrender.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstassrender.dll deleted file mode 100644 index 207a122d1..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstassrender.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiobuffersplit.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiobuffersplit.dll deleted file mode 100644 index cddfbd300..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiobuffersplit.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioconvert.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioconvert.dll deleted file mode 100644 index ef0ad8f85..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioconvert.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofx.dll deleted file mode 100644 index d8b96bd76..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofx.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofxbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofxbad.dll deleted file mode 100644 index afc595e91..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiofxbad.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiolatency.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiolatency.dll deleted file mode 100644 index b7e99c861..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiolatency.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixer.dll deleted file mode 100644 index 96a4cd146..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixer.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixmatrix.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixmatrix.dll deleted file mode 100644 index 59201861a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiomixmatrix.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioparsers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioparsers.dll deleted file mode 100644 index 6157b31b1..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioparsers.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiorate.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiorate.dll deleted file mode 100644 index 79beef0c7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiorate.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioresample.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioresample.dll deleted file mode 100644 index 3f2dd37a7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudioresample.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiotestsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiotestsrc.dll deleted file mode 100644 index 83135e18f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiotestsrc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiovisualizers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiovisualizers.dll deleted file mode 100644 index 3f96b340c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaudiovisualizers.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstauparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstauparse.dll deleted file mode 100644 index 9260fc8d5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstauparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautoconvert.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautoconvert.dll deleted file mode 100644 index 0988e2b29..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautoconvert.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautodetect.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautodetect.dll deleted file mode 100644 index 0cb2b3425..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstautodetect.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstavi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstavi.dll deleted file mode 100644 index e179a8d35..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstavi.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaws.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaws.dll deleted file mode 100644 index bbf50cc84..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstaws.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbayer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbayer.dll deleted file mode 100644 index 2b46908ef..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbayer.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstburn.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstburn.dll deleted file mode 100644 index 68d3a13fa..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstburn.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbz2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbz2.dll deleted file mode 100644 index e7e8e1757..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstbz2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcairo.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcairo.dll deleted file mode 100644 index d7bafe440..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcairo.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcamerabin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcamerabin.dll deleted file mode 100644 index 5144c1858..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcamerabin.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcdg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcdg.dll deleted file mode 100644 index abc9d9e86..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcdg.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclaxon.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclaxon.dll deleted file mode 100644 index 32cb88cc6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclaxon.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclosedcaption.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclosedcaption.dll deleted file mode 100644 index 431739792..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstclosedcaption.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodecalpha.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodecalpha.dll deleted file mode 100644 index 28c616ec3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodecalpha.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodectimestamper.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodectimestamper.dll deleted file mode 100644 index f352e7106..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcodectimestamper.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoloreffects.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoloreffects.dll deleted file mode 100644 index fa480f6ad..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoloreffects.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcompositor.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcompositor.dll deleted file mode 100644 index c46f422ba..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcompositor.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoreelements.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoreelements.dll deleted file mode 100644 index 1802d5631..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoreelements.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoretracers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoretracers.dll deleted file mode 100644 index a6e3ac676..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcoretracers.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcurl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcurl.dll deleted file mode 100644 index 38bbc20e7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcurl.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcutter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcutter.dll deleted file mode 100644 index 3f881d934..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstcutter.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d.dll deleted file mode 100644 index 312764312..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d11.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d11.dll deleted file mode 100644 index 6a1c00a27..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d11.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d12.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d12.dll deleted file mode 100644 index a192ebfa7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstd3d12.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdash.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdash.dll deleted file mode 100644 index 9e152f9bc..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdash.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdav1d.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdav1d.dll deleted file mode 100644 index 0e88f96f7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdav1d.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebug.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebug.dll deleted file mode 100644 index e2f1a7545..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebug.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebugutilsbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebugutilsbad.dll deleted file mode 100644 index 6a664162b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdebugutilsbad.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdecklink.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdecklink.dll deleted file mode 100644 index befc423f5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdecklink.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdeinterlace.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdeinterlace.dll deleted file mode 100644 index c9a56efde..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdeinterlace.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdemucs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdemucs.dll deleted file mode 100644 index fe4d85c9c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdemucs.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectshow.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectshow.dll deleted file mode 100644 index 09a2ad222..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectshow.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsound.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsound.dll deleted file mode 100644 index 5767cb631..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsound.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsoundsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsoundsrc.dll deleted file mode 100644 index 2b89e91a6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdirectsoundsrc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtls.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtls.dll deleted file mode 100644 index 75893711d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtls.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtmf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtmf.dll deleted file mode 100644 index 5cafa25f9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtmf.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtsdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtsdec.dll deleted file mode 100644 index 6d1155420..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdtsdec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdv.dll deleted file mode 100644 index 32f8c61d1..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdv.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsubenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsubenc.dll deleted file mode 100644 index 55a893db5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsubenc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsuboverlay.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsuboverlay.dll deleted file mode 100644 index 9ecb5ae5c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvbsuboverlay.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdlpcmdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdlpcmdec.dll deleted file mode 100644 index 36fb93c1a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdlpcmdec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdread.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdread.dll deleted file mode 100644 index a7d18edff..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdread.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdspu.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdspu.dll deleted file mode 100644 index 10cb9829f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdspu.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdsub.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdsub.dll deleted file mode 100644 index c4bb74344..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdvdsub.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdwrite.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdwrite.dll deleted file mode 100644 index 0c2247566..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstdwrite.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsteffectv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsteffectv.dll deleted file mode 100644 index b4fbc2350..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsteffectv.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstelevenlabs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstelevenlabs.dll deleted file mode 100644 index 0d95a678b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstelevenlabs.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstencoding.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstencoding.dll deleted file mode 100644 index 635c84ce8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstencoding.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstequalizer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstequalizer.dll deleted file mode 100644 index 2d0a59172..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstequalizer.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfallbackswitch.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfallbackswitch.dll deleted file mode 100644 index 76cb21f82..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfallbackswitch.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstffv1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstffv1.dll deleted file mode 100644 index 7dcad6261..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstffv1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfieldanalysis.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfieldanalysis.dll deleted file mode 100644 index e9a65cc20..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfieldanalysis.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflac.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflac.dll deleted file mode 100644 index ad616a6d4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflac.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflv.dll deleted file mode 100644 index 381f2e2d9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflv.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflxdec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflxdec.dll deleted file mode 100644 index a86d747d5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstflxdec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfreeverb.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfreeverb.dll deleted file mode 100644 index 2c7f2ec51..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfreeverb.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfrei0r.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfrei0r.dll deleted file mode 100644 index 4cc8db4f8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstfrei0r.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgaudieffects.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgaudieffects.dll deleted file mode 100644 index 9e0d542bd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgaudieffects.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdkpixbuf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdkpixbuf.dll deleted file mode 100644 index 16d488382..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdkpixbuf.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdp.dll deleted file mode 100644 index 487813af3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgdp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgeometrictransform.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgeometrictransform.dll deleted file mode 100644 index c5e06c016..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgeometrictransform.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstges.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstges.dll deleted file mode 100644 index 673e0c074..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstges.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgif.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgif.dll deleted file mode 100644 index fba7011cb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgif.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgio.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgio.dll deleted file mode 100644 index beccad521..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgio.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom.dll deleted file mode 100644 index 284483bcd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom2k1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom2k1.dll deleted file mode 100644 index 2c7a9f0de..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgoom2k1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgopbuffer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgopbuffer.dll deleted file mode 100644 index 018151490..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgopbuffer.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgtk4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgtk4.dll deleted file mode 100644 index 09086caa7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstgtk4.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthls.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthls.dll deleted file mode 100644 index 94a3a37a4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthls.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlsmultivariantsink.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlsmultivariantsink.dll deleted file mode 100644 index a65a2dd27..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlsmultivariantsink.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlssink3.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlssink3.dll deleted file mode 100644 index 9f6636013..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthlssink3.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthsv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthsv.dll deleted file mode 100644 index eac6704e3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsthsv.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticecast.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticecast.dll deleted file mode 100644 index a52fd7125..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticecast.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticydemux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticydemux.dll deleted file mode 100644 index a68b9e822..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsticydemux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3demux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3demux.dll deleted file mode 100644 index 6f8c4318c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3demux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3tag.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3tag.dll deleted file mode 100644 index 5ce38cf2b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstid3tag.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstimagefreeze.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstimagefreeze.dll deleted file mode 100644 index 91f1f433c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstimagefreeze.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinsertbin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinsertbin.dll deleted file mode 100644 index c8cdc7427..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinsertbin.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinter.dll deleted file mode 100644 index 959db4a00..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinter.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterlace.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterlace.dll deleted file mode 100644 index 8d26fa701..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterlace.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterleave.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterleave.dll deleted file mode 100644 index dd80a53fb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstinterleave.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstipcpipeline.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstipcpipeline.dll deleted file mode 100644 index c4f38ffe0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstipcpipeline.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisobmff.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisobmff.dll deleted file mode 100644 index 7ab299655..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisobmff.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisomp4.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisomp4.dll deleted file mode 100644 index 6ae077514..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstisomp4.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivfparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivfparse.dll deleted file mode 100644 index ba5983ec3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivfparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivtc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivtc.dll deleted file mode 100644 index cfd09cefe..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstivtc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjack.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjack.dll deleted file mode 100644 index d1a0e9705..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjack.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpeg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpeg.dll deleted file mode 100644 index a68709c9e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpeg.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpegformat.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpegformat.dll deleted file mode 100644 index 516ffa1c6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjpegformat.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjson.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjson.dll deleted file mode 100644 index d15db93d6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstjson.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstladspa.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstladspa.dll deleted file mode 100644 index 06c01d195..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstladspa.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlame.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlame.dll deleted file mode 100644 index a0c0ab0ea..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlame.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlcevcdecoder.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlcevcdecoder.dll deleted file mode 100644 index f33a18a8a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlcevcdecoder.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlegacyrawparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlegacyrawparse.dll deleted file mode 100644 index c85f14c9d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlegacyrawparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlevel.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlevel.dll deleted file mode 100644 index ad69fac2b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlevel.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlewton.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlewton.dll deleted file mode 100644 index a28c7d249..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlewton.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlibav.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlibav.dll deleted file mode 100644 index 10f7cd2bb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlibav.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlivesync.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlivesync.dll deleted file mode 100644 index ac97d2387..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstlivesync.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmatroska.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmatroska.dll deleted file mode 100644 index 7cd8cb841..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmatroska.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmediafoundation.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmediafoundation.dll deleted file mode 100644 index e8bfee4a9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmediafoundation.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmidi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmidi.dll deleted file mode 100644 index 8db2c5c37..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmidi.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsdemux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsdemux.dll deleted file mode 100644 index e68572021..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsdemux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsmux.dll deleted file mode 100644 index 3fb929d4d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegpsmux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsdemux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsdemux.dll deleted file mode 100644 index 8fbc69064..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsdemux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtslive.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtslive.dll deleted file mode 100644 index e544637bd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtslive.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsmux.dll deleted file mode 100644 index 0bd1d4874..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpegtsmux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpg123.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpg123.dll deleted file mode 100644 index 8da3ad136..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmpg123.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmse.dll deleted file mode 100644 index e0500b7b4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmulaw.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmulaw.dll deleted file mode 100644 index 336752161..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmulaw.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultifile.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultifile.dll deleted file mode 100644 index 27b23a115..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultifile.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultipart.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultipart.dll deleted file mode 100644 index bcdd9618d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmultipart.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmxf.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmxf.dll deleted file mode 100644 index 5b785c02a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstmxf.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstndi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstndi.dll deleted file mode 100644 index c994b0a59..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstndi.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnetsim.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnetsim.dll deleted file mode 100644 index 1161ae9b4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnetsim.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnice.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnice.dll deleted file mode 100644 index 728403147..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnice.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnle.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnle.dll deleted file mode 100644 index 018f55481..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnle.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnvcodec.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnvcodec.dll deleted file mode 100644 index 155b0d808..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstnvcodec.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstogg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstogg.dll deleted file mode 100644 index ed2dc9742..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstogg.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopengl.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopengl.dll deleted file mode 100644 index f62ec2e45..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopengl.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenh264.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenh264.dll deleted file mode 100644 index 27ce5d3a2..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenh264.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenjpeg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenjpeg.dll deleted file mode 100644 index 9b8126d72..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopenjpeg.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopus.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopus.dll deleted file mode 100644 index 7a2051205..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopus.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopusparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopusparse.dll deleted file mode 100644 index e12237366..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstopusparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoriginalbuffer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoriginalbuffer.dll deleted file mode 100644 index 61f8ead36..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoriginalbuffer.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoverlaycomposition.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoverlaycomposition.dll deleted file mode 100644 index 05821d373..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstoverlaycomposition.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpango.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpango.dll deleted file mode 100644 index ddfb532f9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpango.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpbtypes.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpbtypes.dll deleted file mode 100644 index ba4f6371f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpbtypes.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpcapparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpcapparse.dll deleted file mode 100644 index d2092e0fd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpcapparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstplayback.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstplayback.dll deleted file mode 100644 index 70a5b9309..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstplayback.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpng.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpng.dll deleted file mode 100644 index f7cd8ae8e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpng.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpnm.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpnm.dll deleted file mode 100644 index 8728b4125..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpnm.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstproxy.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstproxy.dll deleted file mode 100644 index 3e9539205..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstproxy.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpython.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpython.dll deleted file mode 100644 index 58afce20a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstpython.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqroverlay.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqroverlay.dll deleted file mode 100644 index ee849f272..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqroverlay.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqsv.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqsv.dll deleted file mode 100644 index 554cd2054..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstqsv.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstquinn.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstquinn.dll deleted file mode 100644 index f89a4576d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstquinn.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstraptorq.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstraptorq.dll deleted file mode 100644 index bc538755c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstraptorq.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrav1e.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrav1e.dll deleted file mode 100644 index 3741cfb50..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrav1e.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrawparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrawparse.dll deleted file mode 100644 index 804fedb09..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrawparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrealmedia.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrealmedia.dll deleted file mode 100644 index 71871f074..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrealmedia.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstregex.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstregex.dll deleted file mode 100644 index 66273d2bf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstregex.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstremovesilence.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstremovesilence.dll deleted file mode 100644 index 246bb3e5e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstremovesilence.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreplaygain.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreplaygain.dll deleted file mode 100644 index e60c5cdbd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreplaygain.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreqwest.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreqwest.dll deleted file mode 100644 index 2a0182eee..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstreqwest.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstresindvd.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstresindvd.dll deleted file mode 100644 index 4a69a9f47..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstresindvd.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrfbsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrfbsrc.dll deleted file mode 100644 index 0bb36a6c2..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrfbsrc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrist.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrist.dll deleted file mode 100644 index e65172e9c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrist.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsanalytics.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsanalytics.dll deleted file mode 100644 index 16e06fd3b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsanalytics.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudiofx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudiofx.dll deleted file mode 100644 index b70ee4263..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudiofx.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudioparsers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudioparsers.dll deleted file mode 100644 index 0f92b4a0a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsaudioparsers.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsclosedcaption.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsclosedcaption.dll deleted file mode 100644 index 748fdeabf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsclosedcaption.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsinter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsinter.dll deleted file mode 100644 index 04a8068c0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsinter.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsonvif.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsonvif.dll deleted file mode 100644 index 91523ee61..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsonvif.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrspng.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrspng.dll deleted file mode 100644 index cbf4a143b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrspng.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtp.dll deleted file mode 100644 index 71c9d0564..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtsp.dll deleted file mode 100644 index 2ad63642c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsrtsp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrstracers.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrstracers.dll deleted file mode 100644 index a8520c1f3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrstracers.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvg.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvg.dll deleted file mode 100644 index 3b28d819b..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvg.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvideofx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvideofx.dll deleted file mode 100644 index cb10e0c42..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrsvideofx.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrswebrtc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrswebrtc.dll deleted file mode 100644 index 4ed5830a0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrswebrtc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp.dll deleted file mode 100644 index 4284c1354..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp2.dll deleted file mode 100644 index ec8cca100..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtmp2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtp.dll deleted file mode 100644 index e706b9e72..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanager.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanager.dll deleted file mode 100644 index d43f1eb9a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanager.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanagerbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanagerbad.dll deleted file mode 100644 index 089f75fa8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtpmanagerbad.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtponvif.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtponvif.dll deleted file mode 100644 index 3fcd146c7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtponvif.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtsp.dll deleted file mode 100644 index 73d847477..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtsp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtspclientsink.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtspclientsink.dll deleted file mode 100644 index 268dd3853..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstrtspclientsink.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsbc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsbc.dll deleted file mode 100644 index 276be1fb8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsbc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsctp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsctp.dll deleted file mode 100644 index afbac24a3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsctp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsdpelem.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsdpelem.dll deleted file mode 100644 index 6568e4a5e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsdpelem.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsegmentclip.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsegmentclip.dll deleted file mode 100644 index 189c18a43..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsegmentclip.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstshapewipe.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstshapewipe.dll deleted file mode 100644 index f096d2ab3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstshapewipe.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsiren.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsiren.dll deleted file mode 100644 index 1b9ddee88..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsiren.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmooth.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmooth.dll deleted file mode 100644 index b98ad8835..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmooth.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmoothstreaming.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmoothstreaming.dll deleted file mode 100644 index 54d921b9e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmoothstreaming.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmpte.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmpte.dll deleted file mode 100644 index 8800a7779..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsmpte.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoundtouch.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoundtouch.dll deleted file mode 100644 index 05aacba61..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoundtouch.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoup.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoup.dll deleted file mode 100644 index 3edfd63b7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsoup.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspandsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspandsp.dll deleted file mode 100644 index 160f5b7bb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspandsp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspectrum.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspectrum.dll deleted file mode 100644 index 190a39313..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspectrum.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeechmatics.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeechmatics.dll deleted file mode 100644 index 08879d7e0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeechmatics.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeed.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeed.dll deleted file mode 100644 index e97009f3e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeed.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeex.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeex.dll deleted file mode 100644 index 7e8bdef65..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstspeex.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrt.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrt.dll deleted file mode 100644 index 7d0f61142..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrt.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrtp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrtp.dll deleted file mode 100644 index c8adbd37d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsrtp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gststreamgrouper.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gststreamgrouper.dll deleted file mode 100644 index 9b92949c8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gststreamgrouper.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubenc.dll deleted file mode 100644 index e78d0f454..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubenc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubparse.dll deleted file mode 100644 index 70825b328..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsubparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtav1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtav1.dll deleted file mode 100644 index 8cd564b34..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtav1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtjpegxs.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtjpegxs.dll deleted file mode 100644 index ee4518a97..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstsvtjpegxs.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstswitchbin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstswitchbin.dll deleted file mode 100644 index adbcf6ef5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstswitchbin.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttaglib.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttaglib.dll deleted file mode 100644 index 27906c1ff..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttaglib.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttcp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttcp.dll deleted file mode 100644 index 7a24d5655..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttcp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttensordecoders.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttensordecoders.dll deleted file mode 100644 index 99aa09cc5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttensordecoders.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextahead.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextahead.dll deleted file mode 100644 index 1b136d380..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextahead.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextwrap.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextwrap.dll deleted file mode 100644 index 8d9fcd0a4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttextwrap.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttheora.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttheora.dll deleted file mode 100644 index 556c18fc0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttheora.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstthreadshare.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstthreadshare.dll deleted file mode 100644 index 1ddfa58bd..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstthreadshare.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttimecode.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttimecode.dll deleted file mode 100644 index f4ae7a456..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttimecode.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttogglerecord.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttogglerecord.dll deleted file mode 100644 index ad95bc478..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttogglerecord.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttranscode.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttranscode.dll deleted file mode 100644 index fe481c7f7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttranscode.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttypefindfunctions.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttypefindfunctions.dll deleted file mode 100644 index 848b00da5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsttypefindfunctions.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstudp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstudp.dll deleted file mode 100644 index 562a170c9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstudp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsturiplaylistbin.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsturiplaylistbin.dll deleted file mode 100644 index 4f1ac143e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsturiplaylistbin.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideobox.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideobox.dll deleted file mode 100644 index d525fbe3a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideobox.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoconvertscale.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoconvertscale.dll deleted file mode 100644 index dce21665e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoconvertscale.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideocrop.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideocrop.dll deleted file mode 100644 index fdf32f8b6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideocrop.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofilter.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofilter.dll deleted file mode 100644 index 6d303ed4c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofilter.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofiltersbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofiltersbad.dll deleted file mode 100644 index cd37ef7e4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideofiltersbad.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoframe_audiolevel.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoframe_audiolevel.dll deleted file mode 100644 index 16e06e468..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoframe_audiolevel.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideomixer.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideomixer.dll deleted file mode 100644 index a2973a50c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideomixer.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoparsersbad.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoparsersbad.dll deleted file mode 100644 index 2d2ca9713..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideoparsersbad.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideorate.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideorate.dll deleted file mode 100644 index df5f884d0..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideorate.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideosignal.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideosignal.dll deleted file mode 100644 index 55fe45ef9..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideosignal.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideotestsrc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideotestsrc.dll deleted file mode 100644 index 3ef458028..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvideotestsrc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvoaacenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvoaacenc.dll deleted file mode 100644 index 36fe55096..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvoaacenc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvolume.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvolume.dll deleted file mode 100644 index 07bf135d3..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvolume.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvorbis.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvorbis.dll deleted file mode 100644 index 19c8f8115..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvorbis.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvpx.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvpx.dll deleted file mode 100644 index 96d842b7a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstvpx.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi.dll deleted file mode 100644 index fcc2e6ef4..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi2.dll deleted file mode 100644 index d280ca52d..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwasapi2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavenc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavenc.dll deleted file mode 100644 index 7c5e9b2cb..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavenc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavpack.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavpack.dll deleted file mode 100644 index 705af2ba6..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavpack.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavparse.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavparse.dll deleted file mode 100644 index 14e6c7c74..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwavparse.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtc.dll deleted file mode 100644 index a0cf7227a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtcdsp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtcdsp.dll deleted file mode 100644 index 4943eee5c..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtcdsp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtchttp.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtchttp.dll deleted file mode 100644 index 4480b8aaf..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebrtchttp.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebview2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebview2.dll deleted file mode 100644 index 16f77aaa7..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwebview2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwic.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwic.dll deleted file mode 100644 index 917a716ee..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwic.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwin32ipc.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwin32ipc.dll deleted file mode 100644 index 363639f95..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwin32ipc.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinks.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinks.dll deleted file mode 100644 index 4dcee3ade..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinks.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinscreencap.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinscreencap.dll deleted file mode 100644 index 2d5c4d897..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstwinscreencap.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx264.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx264.dll deleted file mode 100644 index 286a318ee..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx264.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx265.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx265.dll deleted file mode 100644 index c5558224a..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstx265.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstxingmux.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstxingmux.dll deleted file mode 100644 index 32f436056..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstxingmux.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsty4m.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsty4m.dll deleted file mode 100644 index 8e7c1e621..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gsty4m.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstzbar.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstzbar.dll deleted file mode 100644 index cec58449e..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/gstzbar.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/d3d11/gstd3d11config.h b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/d3d11/gstd3d11config.h deleted file mode 100644 index b8d917865..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/d3d11/gstd3d11config.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -#define GST_D3D11_WINAPI_ONLY_APP 0 -#define GST_D3D11_WINAPI_APP 1 - -G_END_DECLS diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/gl/gstglconfig.h b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/gl/gstglconfig.h deleted file mode 100644 index 3872f9b9a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/include/gst/gl/gstglconfig.h +++ /dev/null @@ -1,49 +0,0 @@ -/* gstglconfig.h - */ - -#ifndef __GST_GL_CONFIG_H__ -#define __GST_GL_CONFIG_H__ - -#include - -G_BEGIN_DECLS - - -#define GST_GL_HAVE_OPENGL 1 -#define GST_GL_HAVE_GLES2 0 -#define GST_GL_HAVE_GLES3 0 -#define GST_GL_HAVE_GLES3EXT3_H 0 - -#define GST_GL_HAVE_WINDOW_X11 0 -#define GST_GL_HAVE_WINDOW_COCOA 0 -#define GST_GL_HAVE_WINDOW_WIN32 1 -#define GST_GL_HAVE_WINDOW_WINRT 0 -#define GST_GL_HAVE_WINDOW_WAYLAND 0 -#define GST_GL_HAVE_WINDOW_ANDROID 0 -#define GST_GL_HAVE_WINDOW_DISPMANX 0 -#define GST_GL_HAVE_WINDOW_EAGL 0 -#define GST_GL_HAVE_WINDOW_VIV_FB 0 -#define GST_GL_HAVE_WINDOW_GBM 0 - -#define GST_GL_HAVE_PLATFORM_EGL 0 -#define GST_GL_HAVE_PLATFORM_GLX 0 -#define GST_GL_HAVE_PLATFORM_WGL 1 -#define GST_GL_HAVE_PLATFORM_CGL 0 -#define GST_GL_HAVE_PLATFORM_EAGL 0 - -#define GST_GL_HAVE_DMABUF 0 -#define GST_GL_HAVE_VIV_DIRECTVIV 0 - -#define GST_GL_HAVE_GLEGLIMAGEOES 1 -#define GST_GL_HAVE_GLCHAR 1 -#define GST_GL_HAVE_GLSIZEIPTR 1 -#define GST_GL_HAVE_GLINTPTR 1 -#define GST_GL_HAVE_GLSYNC 1 -#define GST_GL_HAVE_GLUINT64 1 -#define GST_GL_HAVE_GLINT64 1 -#define GST_GL_HAVE_EGLATTRIB 0 -#define GST_GL_HAVE_EGLUINT64KHR 0 - -G_END_DECLS - -#endif /* __GST_GL_CONFIG_H__ */ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstaws.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstaws.pc deleted file mode 100644 index 3d883264f..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstaws.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstaws -Description: GStreamer Amazon Web Services plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstaws -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, openssl - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstburn.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstburn.pc deleted file mode 100644 index c6f9b087d..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstburn.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstburn -Description: GStreamer Burn plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstburn -Cflags: -Libs.private: -lgstanalytics-1.0 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gstreamer-analytics-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstcdg.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstcdg.pc deleted file mode 100644 index cb104100d..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstcdg.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstcdg -Description: GStreamer CDG codec Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstcdg -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstclaxon.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstclaxon.pc deleted file mode 100644 index 155df58bd..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstclaxon.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstclaxon -Description: GStreamer Claxon FLAC Decoder Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstclaxon -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdav1d.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdav1d.pc deleted file mode 100644 index 8c06bbfa9..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdav1d.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstdav1d -Description: GStreamer dav1d AV1 decoder Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstdav1d -Cflags: -Libs.private: -ldav1d -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0, dav1d - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdemucs.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdemucs.pc deleted file mode 100644 index 94f3df756..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstdemucs.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstdemucs -Description: GStreamer Demucs Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstdemucs -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstelevenlabs.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstelevenlabs.pc deleted file mode 100644 index 1228bcdf8..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstelevenlabs.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstelevenlabs -Description: GStreamer ElevenLabs plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstelevenlabs -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstfallbackswitch.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstfallbackswitch.pc deleted file mode 100644 index 58fe8cf6f..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstfallbackswitch.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstfallbackswitch -Description: GStreamer Fallback Switcher and Source Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstfallbackswitch -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstffv1.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstffv1.pc deleted file mode 100644 index b1aa6defe..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstffv1.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstffv1 -Description: GStreamer FFV1 Decoder Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstffv1 -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgif.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgif.pc deleted file mode 100644 index cd88164d5..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgif.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstgif -Description: GStreamer GIF plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstgif -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgopbuffer.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgopbuffer.pc deleted file mode 100644 index b0739a245..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgopbuffer.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstgopbuffer -Description: Store complete groups of pictures at a time -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstgopbuffer -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgtk4.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgtk4.pc deleted file mode 100644 index 20a02a6d7..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstgtk4.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstgtk4 -Description: GStreamer GTK 4 sink element -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstgtk4 -Cflags: -Libs.private: -lgstgl-1.0 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgtk-4 -lpangowin32-1.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgtk-4 -lpangowin32-1.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgraphene-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgtk-4 -lpangowin32-1.0 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lgdk_pixbuf-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gtk4, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlsmultivariantsink.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlsmultivariantsink.pc deleted file mode 100644 index e88e3f41e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlsmultivariantsink.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsthlsmultivariantsink -Description: GStreamer HLS (HTTP Live Streaming) multi-variant sink Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsthlsmultivariantsink -Cflags: -Libs.private: -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlssink3.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlssink3.pc deleted file mode 100644 index bb3b226ed..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthlssink3.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsthlssink3 -Description: GStreamer HLS (HTTP Live Streaming) Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsthlssink3 -Cflags: -Libs.private: -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthsv.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthsv.pc deleted file mode 100644 index c540c12cc..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsthsv.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsthsv -Description: GStreamer plugin with HSV manipulation elements -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsthsv -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsticecast.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsticecast.pc deleted file mode 100644 index 84b36d3b7..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsticecast.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsticecast -Description: GStreamer Icecast Sink Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsticecast -Cflags: -Libs.private: -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstisobmff.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstisobmff.pc deleted file mode 100644 index 4c08f512a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstisobmff.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstisobmff -Description: GStreamer ISO Base Media File Format (MP4) Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstisobmff -Cflags: -Libs.private: -lgsttag-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstjson.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstjson.pc deleted file mode 100644 index b3d01c74f..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstjson.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstjson -Description: GStreamer JSON Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstjson -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlewton.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlewton.pc deleted file mode 100644 index dafb2d65e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlewton.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstlewton -Description: GStreamer lewton Vorbis Decoder Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstlewton -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlivesync.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlivesync.pc deleted file mode 100644 index 03f9b4658..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstlivesync.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstlivesync -Description: Livesync Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstlivesync -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstmpegtslive.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstmpegtslive.pc deleted file mode 100644 index 41df90db0..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstmpegtslive.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstmpegtslive -Description: GStreamer MPEG-TS Live sources -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstmpegtslive -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstndi.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstndi.pc deleted file mode 100644 index d16226280..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstndi.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstndi -Description: GStreamer NewTek NDI Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstndi -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstoriginalbuffer.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstoriginalbuffer.pc deleted file mode 100644 index 0426be761..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstoriginalbuffer.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstoriginalbuffer -Description: GStreamer Origin buffer meta Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstoriginalbuffer -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstquinn.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstquinn.pc deleted file mode 100644 index d5e9229de..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstquinn.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstquinn -Description: GStreamer Plugin for QUIC -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstquinn -Cflags: -Libs.private: -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstraptorq.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstraptorq.pc deleted file mode 100644 index 93ed49053..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstraptorq.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstraptorq -Description: GStreamer RaptorQ FEC Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstraptorq -Cflags: -Libs.private: -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-rtp-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrav1e.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrav1e.pc deleted file mode 100644 index 03d7a0419..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrav1e.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrav1e -Description: GStreamer rav1e AV1 Encoder Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrav1e -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstregex.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstregex.pc deleted file mode 100644 index 1c311847a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstregex.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstregex -Description: GStreamer Regular Expression Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstregex -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstreqwest.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstreqwest.pc deleted file mode 100644 index 90b9b8306..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstreqwest.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstreqwest -Description: GStreamer reqwest HTTP Source Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstreqwest -Cflags: -Libs.private: -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsanalytics.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsanalytics.pc deleted file mode 100644 index cf4b956b1..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsanalytics.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsanalytics -Description: GStreamer Rust Analytics Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsanalytics -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstanalytics-1.0 -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, gstreamer-analytics-1.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudiofx.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudiofx.pc deleted file mode 100644 index 1cdca409b..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudiofx.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsaudiofx -Description: GStreamer Rust Audio Effects Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsaudiofx -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudioparsers.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudioparsers.pc deleted file mode 100644 index 8e507bc4e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsaudioparsers.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsaudioparsers -Description: GStreamer Rust Audio Parsers Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsaudioparsers -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsclosedcaption.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsclosedcaption.pc deleted file mode 100644 index 717002645..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsclosedcaption.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsclosedcaption -Description: GStreamer Rust Closed Caption Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsclosedcaption -Cflags: -Libs.private: -lpangocairo-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lcairo -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0, pango, pangocairo, cairo-gobject - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsinter.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsinter.pc deleted file mode 100644 index e81fbe388..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsinter.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsinter -Description: GStreamer Inter Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsinter -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsonvif.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsonvif.pc deleted file mode 100644 index bbb266024..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsonvif.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsonvif -Description: GStreamer Rust ONVIF Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsonvif -Cflags: -Libs.private: -lpangocairo-1.0 -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lcairo -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lpango-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lharfbuzz -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0, pango, pangocairo - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrspng.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrspng.pc deleted file mode 100644 index d824c8415..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrspng.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrspng -Description: GStreamer Rust PNG encoder/decoder -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrspng -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtp.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtp.pc deleted file mode 100644 index d7be3a15c..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtp.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsrtp -Description: GStreamer Rust RTP Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsrtp -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-rtp-1.0, gstreamer-net-1.0, gstreamer-video-1.0 gobject-2.0, glib-2.0, gmodule-2.0, gio-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtsp.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtsp.pc deleted file mode 100644 index 42e8f1818..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsrtsp.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsrtsp -Description: GStreamer RTSP Client Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsrtsp -Cflags: -Libs.private: -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-net-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrstracers.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrstracers.pc deleted file mode 100644 index f5674318a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrstracers.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrstracers -Description: GStreamer Rust tracers plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrstracers -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsvideofx.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsvideofx.pc deleted file mode 100644 index 77048a43a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrsvideofx.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrsvideofx -Description: GStreamer Rust Video Effects Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrsvideofx -Cflags: -Libs.private: -lcairo-gobject -lcairo -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, cairo-gobject - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrswebrtc.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrswebrtc.pc deleted file mode 100644 index 525a2f524..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstrswebrtc.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstrswebrtc -Description: GStreamer plugin for high level WebRTC elements and a simple signaling server -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstrswebrtc -Cflags: -Libs.private: -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstpbutils-1.0 -lgstvideo-1.0 -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstwebrtc-1.0 -lgstsdp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstsdp-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstapp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstrtp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-rtp-1.0 >= 1.20, gstreamer-webrtc-1.0 >= 1.20, gstreamer-1.0 >= 1.20, gstreamer-app-1.0 >= 1.20, gstreamer-video-1.0 >= 1.20, gstreamer-sdp-1.0 >= 1.20, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstspeechmatics.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstspeechmatics.pc deleted file mode 100644 index c991cd528..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstspeechmatics.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstspeechmatics -Description: GStreamer Speechmatics plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstspeechmatics -Cflags: -Libs.private: -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gststreamgrouper.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gststreamgrouper.pc deleted file mode 100644 index 4cba88faf..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gststreamgrouper.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gststreamgrouper -Description: Filter element that makes all the incoming streams share a group-id -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgststreamgrouper -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextahead.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextahead.pc deleted file mode 100644 index 67836656d..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextahead.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsttextahead -Description: GStreamer Plugin for displaying upcoming text buffers ahead of time -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsttextahead -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextwrap.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextwrap.pc deleted file mode 100644 index f2bc6a06a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttextwrap.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsttextwrap -Description: GStreamer Text Wrap Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsttextwrap -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstthreadshare.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstthreadshare.pc deleted file mode 100644 index 9c208580e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstthreadshare.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstthreadshare -Description: GStreamer Threadshare Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstthreadshare -Cflags: -Libs.private: -lgstnet-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-net-1.0, gstreamer-rtp-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttogglerecord.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttogglerecord.pc deleted file mode 100644 index 5b37fed7e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsttogglerecord.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsttogglerecord -Description: GStreamer Toggle Record Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsttogglerecord -Cflags: -Libs.private: -lgstvideo-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstaudio-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsturiplaylistbin.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsturiplaylistbin.pc deleted file mode 100644 index de5d9ced1..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gsturiplaylistbin.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gsturiplaylistbin -Description: GStreamer Playlist Playback Plugin -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgsturiplaylistbin -Cflags: -Libs.private: -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gobject-2.0, glib-2.0, gmodule-2.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstwebrtchttp.pc b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstwebrtchttp.pc deleted file mode 100644 index 6bc030959..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/lib/gstreamer-1.0/pkgconfig/gstwebrtchttp.pc +++ /dev/null @@ -1,13 +0,0 @@ -prefix=${pcfiledir}/../../.. -exec_prefix=${prefix} -libdir=${prefix}/lib -includedir=${prefix}/include - -Name: gstwebrtchttp -Description: GStreamer WebRTC Plugin for WebRTC HTTP protocols (WHIP/WHEP) -Version: 0.15.2 -Libs: -L${libdir}/gstreamer-1.0 -lgstwebrtchttp -Cflags: -Libs.private: -lgstwebrtc-1.0 -lgstsdp-1.0 -lgstbase-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgstsdp-1.0 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lbcrypt -ladvapi32 -lgstreamer-1.0 -lgobject-2.0 -lglib-2.0 -lintl -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -lgobject-2.0 -lglib-2.0 -lintl -llegacy_stdio_definitions -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -Requires.private: gstrsworkspace, gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0, gstreamer-sdp-1.0, gstreamer-webrtc-1.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner.exe deleted file mode 100644 index 3dc07a298..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-ptp-helper.exe b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-ptp-helper.exe deleted file mode 100644 index 9194b1603..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/libexec/gstreamer-1.0/gst-ptp-helper.exe and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschema.dtd b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschema.dtd deleted file mode 100644 index 9d7482db7..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschema.dtd +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschemas.compiled b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschemas.compiled deleted file mode 100644 index b369f2b28..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/gschemas.compiled and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.Demo4.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.Demo4.gschema.xml deleted file mode 100644 index 3eaa6e8b4..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.Demo4.gschema.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - 'red' - - - (-1, -1) - - - false - - - false - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Inspector.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Inspector.gschema.xml deleted file mode 100644 index 28fa5dd6e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Inspector.gschema.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - false - Insert debug nodes - - If this setting is true, the recorder will insert debug nodes - into the recording. - - - - false - Record events - - If this setting is true, the recorder will include events - in the recording. - - - - false - Highlight sequences - - If this setting is true, the recorder will highlight events - that are part of an event sequence. - - - - false - Set the recorder to dark - - If this setting is true, the recorder will display render nodes - on a dark background. - - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.ColorChooser.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.ColorChooser.gschema.xml deleted file mode 100644 index bedc7030b..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.ColorChooser.gschema.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - [] - Custom colors - - An array of custom colors to show in the color chooser. Each color is - specified as a tuple of four doubles, specifying RGBA values between - 0 and 1. - - - - (false,1.0,1.0,1.0,1.0) - The selected color - - The selected color, described as a tuple whose first member is a - boolean that is true if a color was selected, and the remaining - four members are four doubles, specifying RGBA values between - 0 and 1. - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.Debug.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.Debug.gschema.xml deleted file mode 100644 index 89428d0c0..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.Debug.gschema.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - true - Enable inspector keybinding - - If this setting is true, GTK lets the user open an interactive - debugging window with a keybinding. The default shortcuts for - the keybinding are Control-Shift-I and Control-Shift-D. - - - - true - Inspector warning - - If this setting is true, GTK shows a warning before letting - the user use the interactive debugger. - - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.EmojiChooser.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.EmojiChooser.gschema.xml deleted file mode 100644 index 28c0a62c0..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.EmojiChooser.gschema.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - [] - Recently used Emoji - - An array of Emoji definitions to show in the Emoji chooser. Each Emoji is - specified as an array of codepoints, name and keywords. The extra - integer after this pair is the code of the Fitzpatrick modifier to use in - place of a modifier placeholder (0 or 0x1F3FB) in the codepoint array. - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.FileChooser.gschema.xml b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.FileChooser.gschema.xml deleted file mode 100644 index f1f0e054c..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/glib-2.0/schemas/org.gtk.gtk4.Settings.FileChooser.gschema.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 'path-bar' - Location mode - - Controls whether the file chooser shows just a path bar, or a visible entry - for the filename as well, for the benefit of typing-oriented users. The - possible values for these modes are "path-bar" and "filename-entry". - - - - false - Show hidden files - - Controls whether the file chooser shows hidden files or not. - - - - true - Show folders first - - If set to true, then folders are shown before files in the list. - - - - false - Expand folders - This key is deprecated; do not use it. - - - true - Show file sizes - - Controls whether the file chooser shows a column with file sizes. - - - - true - Show file types - - Controls whether the file chooser shows a column with file types. - - - - 'name' - Sort column - - Can be one of "name", "modified", or "size". It controls - which of the columns in the file chooser is used for sorting - the list of files. - - - - 'ascending' - Sort order - - Can be one of the strings "ascending" or "descending". - - - - (-1, -1) - Window position - - This key is ignored. - - - - (-1, -1) - Window size - - The size (width, height) of the GtkFileChooserDialog's window, in pixels. - - - - 'recent' - Startup mode - - Either "recent" or "cwd"; controls whether the file chooser - starts up showing the list of recently-used files, or the - contents of the current working directory. - - - - -1 - Sidebar width - - Width in pixels of the file chooser's places sidebar. - - - - '24h' - Time format - - Whether the time is shown in 24h or 12h format. - - - - 'regular' - Date format - - The amount of detail to show in the Modified column. - - - - 'category' - Type format - - Different ways to show the 'Type' column information. - Example outputs for a video mp4 file: - 'mime' -> 'video/mp4' - 'description' -> 'MPEG-4 video' - 'category' -> 'Video' - - - - 'list' - View type - - Whether the files are shown in a list or in a grid. - - - - - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate.scenario deleted file mode 100644 index a3043af08..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate.scenario +++ /dev/null @@ -1,5 +0,0 @@ -description, duration=15.0 -set-restriction, playback-time=5.0, restriction-caps="video/x-raw,framerate=(fraction)5/1" -set-restriction, playback-time=10.0, restriction-caps="video/x-raw,framerate=(fraction)30/1" -eos, playback-time=15.0 -stop, playback-time=15.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate_size.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate_size.scenario deleted file mode 100644 index d5cf5963a..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_framerate_size.scenario +++ /dev/null @@ -1,7 +0,0 @@ -description, duration=25.0 -set-restriction, playback-time=5.0, restriction-caps="video/x-raw,framerate=(fraction)5/1" -set-restriction, playback-time=10.0, restriction-caps="video/x-raw,height=20,width=20,framerate=(fraction)5/1" -set-restriction, playback-time=15.0, restriction-caps="video/x-raw,height=20,width=20,framerate=(fraction)30/1" -set-restriction, playback-time=20.0, restriction-caps="video/x-raw,height=720,width=1280,framerate=(fraction)30/1" -eos, playback-time=25.0 -stop, playback-time=25.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_size.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_size.scenario deleted file mode 100644 index c3b5d2ef9..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/adaptive_video_size.scenario +++ /dev/null @@ -1,5 +0,0 @@ -description, duration=15.0 -set-restriction, playback-time=5.0, restriction-caps="video/x-raw,height=480,width=854" -set-restriction, playback-time=10.0, restriction-caps="video/x-raw,height=720,width=1280" -eos, playback-time=15.0 -stop, playback-time=15.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/alternate_fast_backward_forward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/alternate_fast_backward_forward.scenario deleted file mode 100644 index 59138982e..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/alternate_fast_backward_forward.scenario +++ /dev/null @@ -1,14 +0,0 @@ -description, duration=55.0, min-media-duration=470.0, seek=true, reverse-playback=true -include,location=includes/default-seek-flags.scenario -seek, name=backward-seek, playback-time=0.0, rate=-1.0, start=0.0, stop=310.0, flags="$(default_flags)" -seek, name=forward-seek, playback-time=305.0, rate=1.0, start=305.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time=310.0, rate=2.0, start=310.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=320.0, rate=-2.0, start=0.0, stop=320.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time=310.0, rate=4.0, start=310.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=330.0, rate=-4.0, start=0.0, stop=330.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time=310.0, rate=8.0, start=310.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=350.0, rate=-8.0, start=0.0, stop=350.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time=310.0, rate=16.0, start=310.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=390.0, rate=-16.0, start=0.0, stop=390.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time=310.0, rate=32.0, start=310.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=470.0, rate=-32.0, start=310.0, stop=470.0, flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/change_state_intensive.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/change_state_intensive.scenario deleted file mode 100644 index 042d6fbc1..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/change_state_intensive.scenario +++ /dev/null @@ -1,8 +0,0 @@ -description, duration=0, summary="Set state to NULL->PLAYING->NULL 20 times", need-clock-sync=true, min-media-duration=1.0, live_content_compatible=True, handles-states=true, ignore-eos=true - -foreach, i=[0, 40], - actions = { - "set-state, state=playing", - "set-state, state=null", - } -stop; diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/disable_subtitle_track_while_paused.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/disable_subtitle_track_while_paused.scenario deleted file mode 100644 index 3c679c501..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/disable_subtitle_track_while_paused.scenario +++ /dev/null @@ -1,6 +0,0 @@ -description, summary="Disable subtitle track while pipeline is PAUSED", min-subtitle-track=2, duration=5.0, handles-states=true, needs_preroll=true -pause; -switch-track, name="Disable subtitle", type=text, disable=true -wait, duration=0.5 -play; -stop, playback-time=2.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_backward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_backward.scenario deleted file mode 100644 index f16072d50..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_backward.scenario +++ /dev/null @@ -1,9 +0,0 @@ -description, duration=30.0, minfo-media-duration=310.0, seek=true, reverse-playback=true, need-clock-sync=true, min-media-duration=310.0, ignore-eos=true -include,location=includes/default-seek-flags.scenario -seek, name=Fast-backward-seek, playback-time=0.0, rate=-2.0, start=0.0, stop=310.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=300.0, rate=-4.0, start=0.0, stop=300.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=280.0, rate=-8.0, start=0.0, stop=280.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=240.0, rate=-16.0, start=0.0, stop=240.0, flags="$(default_flags)" -seek, name=Fast-backward-seek, playback-time=160.0, rate=-32.0, start=0.0, stop=160.0, flags="$(default_flags)" -wait, message-type=eos -stop \ No newline at end of file diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_forward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_forward.scenario deleted file mode 100644 index 89855a609..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/fast_forward.scenario +++ /dev/null @@ -1,8 +0,0 @@ -description, duration=25.0, seek=true, need-clock-sync=true, min-media-duration=5.0, ignore-eos=true -include,location=includes/default-seek-flags.scenario -seek, name=Fast-forward-seek, playback-time=0.0, rate=2.0, start=0.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time="min(10.0, $(duration) * 0.0625)", rate=4.0, start=0.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time="min(20.0, $(duration) * 0.125)", rate=8.0, start=0.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time="min(40.0, $(duration) * 0.25)", rate=16.0, start=0.0, flags="$(default_flags)" -seek, name=Fast-forward-seek, playback-time="min(80.0, $(duration) * 0.50)", rate=32.0, start=0.0, flags="$(default_flags)" -stop, playback-time="min($(duration) - 0.3, 160.0)", on-message="eos" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_key_unit.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_key_unit.scenario deleted file mode 100644 index 2a9c8394d..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_key_unit.scenario +++ /dev/null @@ -1,4 +0,0 @@ -description, duration=2.0 -video-request-key-unit, playback-time=1.0, direction=upstream, running_time=-1.0, all-header=true, count=1 -eos, playback-time=2.0 -stop, playback-time=2.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_rtsp2.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_rtsp2.scenario deleted file mode 100644 index 0d957b6d2..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/force_rtsp2.scenario +++ /dev/null @@ -1 +0,0 @@ -set-property, target-element-factory-name="rtspsrc", property-name=default-rtsp-version, property-value=(string)"2-0" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/pause_resume.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/pause_resume.scenario deleted file mode 100644 index 27bfc1090..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/pause_resume.scenario +++ /dev/null @@ -1,6 +0,0 @@ -description, duration=14.0, min-media-duration=7.0 -pause, name=First-pause, playback-time=1.0, duration=1.0 -pause, name=Second-pause, playback-time=3.0, duration=5.0 -pause, name=Third-pause, playback-time=5.0, duration=1.0 -eos, name=Done-testing, playback-time=7.0 -stop, playback-time=7.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_15s.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_15s.scenario deleted file mode 100644 index af86cb35f..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_15s.scenario +++ /dev/null @@ -1,3 +0,0 @@ -description, duration=15.0 -eos, playback-time=15.0 -stop, playback-time=15.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_5s.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_5s.scenario deleted file mode 100644 index 40186e8f0..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/play_5s.scenario +++ /dev/null @@ -1,3 +0,0 @@ -description, duration=5.0 -eos, playback-time=5.0 -stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/reverse_playback.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/reverse_playback.scenario deleted file mode 100644 index 90e02cdce..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/reverse_playback.scenario +++ /dev/null @@ -1,3 +0,0 @@ -description, seek=true, reverse-playback=true -include,location=includes/default-seek-flags.scenario -seek, name=Reverse-seek, playback-time=0.0, rate=-1.0, start="max($(duration) - 15.0, 0.0)", stop="$(duration)", flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking.scenario deleted file mode 100644 index 3a8ff4784..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking.scenario +++ /dev/null @@ -1,8 +0,0 @@ -description, seek=true, handles-states=true, needs_preroll=true -include,location=includes/default-seek-flags.scenario -pause, playback-time=0.0 -seek, playback-time=0.0, start="$(duration) - 0.5", flags="$(default_flags)" -seek, playback-time=0.0, start=position-0.1, repeat="min(10, ($(duration) - 0.6))/0.1", flags="$(default_flags)" -play, playback-time=0.0 -stop, playback-time=1.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking_full.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking_full.scenario deleted file mode 100644 index 7cb1f7abe..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_backward_seeking_full.scenario +++ /dev/null @@ -1,8 +0,0 @@ -description, seek=true, handles-states=true, needs_preroll=true -include,location=includes/default-seek-flags.scenario -pause, playback-time=0.0 -seek, playback-time=0.0, start="$(duration) - 0.5", flags="$(default_flags)" -seek, playback-time=0.0, start=position-0.1, repeat="($(duration) - 0.6)/0.1", flags="$(default_flags)" -play, playback-time=0.0 -stop, playback-time=1.0 - diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking.scenario deleted file mode 100644 index 814dce4fe..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking.scenario +++ /dev/null @@ -1,6 +0,0 @@ -description, seek=true, handles-states=true, needs_preroll=true -include,location=includes/default-seek-flags.scenario -pause, playback-time=0.0 -seek, playback-time=0.0, start=position+0.1, repeat="min(10, ($(duration) - 0.5) / 0.1)", flags="$(default_flags)" -play, playback-time=0.0 -stop, playback-time=1.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking_full.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking_full.scenario deleted file mode 100644 index d83c8b1e9..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/scrub_forward_seeking_full.scenario +++ /dev/null @@ -1,6 +0,0 @@ -description, seek=true, handles-states=true, needs_preroll=true -include,location=includes/default-seek-flags.scenario -pause, playback-time=0.0 -seek, playback-time=0.0, start=position+0.1, repeat="($(duration) - 0.5)/0.1", flags="$(default_flags)" -play, playback-time=0.0 -stop, playback-time=1.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_backward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_backward.scenario deleted file mode 100644 index 66c9cd3dd..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_backward.scenario +++ /dev/null @@ -1,6 +0,0 @@ -description, seek=true, duration=30, need-clock-sync=true, ignore-eos=true -include,location=includes/default-seek-flags.scenario -seek, name=Backward-seek, playback-time="min(5.0, ($(duration) / 4))", rate=1.0, start=0.0, flags="$(default_flags)" -seek, name=Backward-seek, playback-time="min(10.0, 2*($(duration) / 4))", rate=1.0, start="min(5.0, $(duration) / 4)", flags="$(default_flags)" -seek, name=Backward-seek, playback-time="min(15.0, 3*($(duration) / 4))", rate=1.0, start="min(10.0, 2*($(duration) / 4))", flags="$(default_flags)" -stop, playback-time="min(15.0, 3*($(duration) / 4))" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward.scenario deleted file mode 100644 index 9949e4149..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward.scenario +++ /dev/null @@ -1,6 +0,0 @@ -description, seek=true, duration=20, need-clock-sync=true, ignore-eos=true -include,location=includes/default-seek-flags.scenario -seek, name=First-forward-seek, playback-time="min(5.0, ($(duration)/8))", start="min(10, 2*($(duration)/8))", flags="$(default_flags)" -seek, name=Second-forward-seek, playback-time="min(15.0, 3*($(duration)/8))", start="min(20, 4*($(duration)/8))", flags="$(default_flags)" -seek, name=Third-forward-seek, playback-time="min(25, 5*($(duration)/8))", start="min(30.0, 6*($(duration)/8))", flags="$(default_flags)" -stop, playback-time="min($(duration) - 1, 35)", on-message=eos \ No newline at end of file diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward_backward.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward_backward.scenario deleted file mode 100644 index 4b669aff7..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_forward_backward.scenario +++ /dev/null @@ -1,10 +0,0 @@ -description, seek=true, duration=40, min-media-duration=45.0 -include,location=includes/default-seek-flags.scenario -seek, name=Forward-seek, playback-time=0.0, rate=1.0, start=5.0, flags="$(default_flags)" -seek, name=Backward-seek, playback-time=10.0, rate=1.0, start=0.0, flags="$(default_flags)" -seek, name=Backward-seek, playback-time=5.0, rate=1.0, start=25.0, stop=-1, flags="$(default_flags)" -seek, name=Backward-seek, playback-time=30.0, rate=1.0, start=0.0, flags="$(default_flags)" -seek, name=Forward-seek, playback-time=5.0, rate=1.0, start=15.0, flags="$(default_flags)" -seek, name=Forward-seek, playback-time=20.0, rate=1.0, start=35.0, flags="$(default_flags)" -seek, name=Backward-seek, playback-time=40.0, rate=1.0, start=25.0, flags="$(default_flags)" -seek, name=Last-backward-seek, playback-time=30.0, rate=1.0, start=5.0, stop=10.0, flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_with_stop.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_with_stop.scenario deleted file mode 100644 index b4b7e3f60..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/seek_with_stop.scenario +++ /dev/null @@ -1,3 +0,0 @@ -description, seek=true, duration=5.0, need_clock_sync=true, min-media-duration=2 -include,location=includes/default-seek-flags.scenario -seek, playback-time=1.0, start=0.0, stop="min(5.0, duration-1.0)", flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/simple_seeks.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/simple_seeks.scenario deleted file mode 100644 index ca41f6ca2..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/simple_seeks.scenario +++ /dev/null @@ -1,5 +0,0 @@ -description, seek=true, duration=5.0 -include,location=includes/default-seek-flags.scenario -seek, playback-time=1.0, rate=1.0, start=2.0, flags="$(default_flags)" -seek, playback-time=3.0, rate=1.0, start=0.0, flags="$(default_flags)" -seek, playback-time=1.0, rate=1.0, start=2.0, stop=3.0, flags="$(default_flags)" diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track.scenario deleted file mode 100644 index b1a968b66..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track.scenario +++ /dev/null @@ -1,3 +0,0 @@ -description, summary="Change audio track at 5 second to the second audio track", min-audio-track=2, duration=10.0, min-media-duration=5.1 -switch-track, name=Next-audio-track, playback-time=5.0, type=audio, index=(string)+1 -stop, playback-time=10.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track_while_paused.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track_while_paused.scenario deleted file mode 100644 index fd4c36249..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_audio_track_while_paused.scenario +++ /dev/null @@ -1,11 +0,0 @@ -description, summary="Change audio track while pipeline is paused", min-audio-track=2, duration=6.0, need-clock-sync=true, needs_preroll=true -pause, playback-time=1.0; - -# Wait so that humans can see the pipeline is paused -wait, duration=0.5 -switch-track, name=Next-audio-track, type=audio, index=(string)+1 - -# Wait so that humans can see the pipeline is paused -wait, duration=0.5 -play; -stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track.scenario deleted file mode 100644 index 216e7ce91..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track.scenario +++ /dev/null @@ -1,3 +0,0 @@ -description, summary="Change subtitle track at 1 second while playing back", min-subtitle-track=2, duration=5.0, need-clock-sync=true -switch-track, playback-time=1.0, type=text, index=(string)+1 -stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track_while_paused.scenario b/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track_while_paused.scenario deleted file mode 100644 index 611914256..000000000 --- a/opennow-stable/native/opennow-streamer/bin/win32-x64/gstreamer/share/gstreamer-1.0/validate/scenarios/switch_subtitle_track_while_paused.scenario +++ /dev/null @@ -1,7 +0,0 @@ -description, summary="Change subtitle track while pipeline is PAUSED", min-subtitle-track=2, duration=5.0, handles-states=true, need-clock-sync=true, needs_preroll=true -pause; -wait, duration=0.5 -switch-track, type=text, index=(string)+1 -wait, duration=0.5 -play; -stop, playback-time=5.0 diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/gthread-2.0-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/gthread-2.0-0.dll deleted file mode 100644 index e13a20a88..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/gthread-2.0-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/intl-8.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/intl-8.dll deleted file mode 100644 index d37f3a60f..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/intl-8.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140.dll deleted file mode 100644 index 5153fb087..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_1.dll deleted file mode 100644 index fe6169ee5..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_1.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_2.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_2.dll deleted file mode 100644 index 967be8237..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_2.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_atomic_wait.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_atomic_wait.dll deleted file mode 100644 index 01f8e9a36..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_atomic_wait.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_codecvt_ids.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_codecvt_ids.dll deleted file mode 100644 index 63214b8a8..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/msvcp140_codecvt_ids.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/orc-0.4-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/orc-0.4-0.dll deleted file mode 100644 index 5270f9f69..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/orc-0.4-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/pcre2-8-0.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/pcre2-8-0.dll deleted file mode 100644 index 092953e23..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/pcre2-8-0.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140.dll deleted file mode 100644 index c2f509d20..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140.dll and /dev/null differ diff --git a/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140_1.dll b/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140_1.dll deleted file mode 100644 index 64cd24630..000000000 Binary files a/opennow-stable/native/opennow-streamer/bin/win32-x64/vcruntime140_1.dll and /dev/null differ diff --git a/opennow-stable/package.json b/opennow-stable/package.json index 954c4d572..f0f364726 100644 --- a/opennow-stable/package.json +++ b/opennow-stable/package.json @@ -26,7 +26,7 @@ "preview": "electron-vite preview", "dist": "npm run build && npm run native:build && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder", "dist:signed": "npm run build && npm run native:build && electron-builder", - "native:check": "cargo check --manifest-path ../native/opennow-streamer/Cargo.toml", + "native:check": "cargo check --manifest-path ../native/opennow-streamer/Cargo.toml --workspace", "native:build": "node scripts/build-native-streamer.mjs", "lint": "oxlint src", "locales:check": "node scripts/check-translations.mjs", @@ -174,27 +174,6 @@ "category": "Game", "maintainer": "zortos293 ", "artifactName": "OpenNOW-v${version}-linux-${arch}.${ext}" - }, - "deb": { - "depends": [ - "libgstreamer1.0-0", - "libgstreamer-plugins-base1.0-0", - "gstreamer1.0-tools", - "gstreamer1.0-libav", - "gstreamer1.0-plugins-base", - "gstreamer1.0-plugins-good", - "gstreamer1.0-plugins-bad", - "gstreamer1.0-plugins-ugly", - "gstreamer1.0-nice", - "gstreamer1.0-vaapi", - "gstreamer1.0-gl", - "gstreamer1.0-x", - "gstreamer1.0-alsa", - "libva2", - "libva-drm2", - "libvulkan1", - "mesa-vulkan-drivers" - ] } } } diff --git a/opennow-stable/scripts/build-native-streamer.mjs b/opennow-stable/scripts/build-native-streamer.mjs index cf19c93ff..8b64a0a55 100644 --- a/opennow-stable/scripts/build-native-streamer.mjs +++ b/opennow-stable/scripts/build-native-streamer.mjs @@ -1,533 +1,129 @@ -import { copyFileSync, chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs"; -import { delimiter, dirname, join, resolve } from "node:path"; +import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = dirname(fileURLToPath(import.meta.url)); const packageRoot = resolve(__dirname, ".."); const repoRoot = resolve(packageRoot, ".."); -const crateRoot = join(repoRoot, "native", "opennow-streamer"); -const manifestPath = join(crateRoot, "Cargo.toml"); -const protocolSourcePath = join(crateRoot, "src", "protocol.rs"); +const workspaceRoot = join(repoRoot, "native", "opennow-streamer"); +const manifestPath = join(workspaceRoot, "Cargo.toml"); +const protocolSourcePath = join( + workspaceRoot, + "crates", + "opennow-streamer-protocol", + "src", + "lib.rs", +); const appProtocolSourcePath = join(packageRoot, "src", "shared", "nativeStreamer.ts"); -const exeName = process.platform === "win32" ? "opennow-streamer.exe" : "opennow-streamer"; -const verifyCommandId = "verify"; -const nativeStreamerProtocolVersion = readVerifiedNativeStreamerProtocolVersion(); const nativeTarget = process.env.OPENNOW_NATIVE_STREAMER_TARGET?.trim() || ""; -const platformKey = process.env.OPENNOW_NATIVE_STREAMER_PLATFORM_KEY?.trim() || `${process.platform}-${process.arch}`; -const targetReleaseDir = nativeTarget - ? join(crateRoot, "target", nativeTarget, "release") - : join(crateRoot, "target", "release"); -const builtBinary = join(targetReleaseDir, exeName); -const packageBinaryDir = join(crateRoot, "bin"); -const packageBinary = join(packageBinaryDir, exeName); -const packagePlatformBinaryDir = join(packageBinaryDir, platformKey); -const packagePlatformBinary = join(packagePlatformBinaryDir, exeName); - -function hasFeature(features, feature) { - return features - .split(/[,\s]+/) - .map((value) => value.trim().toLowerCase()) - .filter(Boolean) - .includes(feature); -} - -function readProtocolVersion(sourcePath, pattern, label) { - const source = readFileSync(sourcePath, "utf8"); - const match = source.match(pattern); - if (!match) { - throw new Error(`Unable to read ${label} protocol version from ${sourcePath}`); - } +const platformKey = process.env.OPENNOW_NATIVE_STREAMER_PLATFORM_KEY?.trim() + || `${process.platform}-${process.arch}`; +const exeName = platformKey.startsWith("win32-") ? "opennow-streamer.exe" : "opennow-streamer"; +const releaseDir = nativeTarget + ? join(workspaceRoot, "target", nativeTarget, "release") + : join(workspaceRoot, "target", "release"); +const builtBinary = join(releaseDir, exeName); +const binRoot = join(workspaceRoot, "bin"); +const platformBinary = join(binRoot, platformKey, exeName); +rmSync(join(binRoot, exeName), { force: true }); + +function readVersion(path, pattern, label) { + const match = readFileSync(path, "utf8").match(pattern); + if (!match) throw new Error(`Unable to read ${label} protocol version from ${path}`); return Number.parseInt(match[1], 10); } -function readVerifiedNativeStreamerProtocolVersion() { - const appProtocolVersion = readProtocolVersion( - appProtocolSourcePath, - /export\s+const\s+NATIVE_STREAMER_PROTOCOL_VERSION\s*=\s*(\d+)\s*;/, - "app", - ); - const nativeProtocolVersion = readProtocolVersion( - protocolSourcePath, - /pub\s+const\s+PROTOCOL_VERSION\s*:\s*u64\s*=\s*(\d+)\s*;/, - "native", - ); - - if (appProtocolVersion !== nativeProtocolVersion) { - throw new Error( - `Native streamer protocol mismatch: app sends ${appProtocolVersion} from ${appProtocolSourcePath}, ` - + `native expects ${nativeProtocolVersion} from ${protocolSourcePath}.`, - ); - } - - return appProtocolVersion; -} - -function isWindowsBuild() { - return process.platform === "win32" || /windows-msvc$/i.test(nativeTarget); -} - -function isDarwinBuild() { - return process.platform === "darwin" || /apple-darwin$/i.test(nativeTarget); -} - -function shouldBundlePrivateGstreamerRuntime(nativeFeatures) { - if (!hasFeature(nativeFeatures, "gstreamer")) { - return false; - } - if (!isWindowsBuild() && !isDarwinBuild()) { - return false; - } - - const override = process.env.OPENNOW_BUNDLE_GSTREAMER_RUNTIME?.trim(); - if (override === "0") { - return false; - } - if (override === "1") { - return true; - } - return true; -} - -function prependEnvPath(env, directory) { - const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH"; - env[pathKey] = env[pathKey] ? `${directory}${delimiter}${env[pathKey]}` : directory; -} - -function appendEnvValue(env, key, value) { - env[key] = env[key]?.trim() ? `${env[key]} ${value}` : value; -} - -function configureDarwinLinkerPadding(env, nativeFeatures) { - if (!isDarwinBuild() || !shouldBundlePrivateGstreamerRuntime(nativeFeatures)) { - return; - } - appendEnvValue(env, "RUSTFLAGS", "-C link-arg=-Wl,-headerpad_max_install_names"); -} - -function brewPrefix() { - const result = spawnSync("brew", ["--prefix"], { encoding: "utf8" }); - return result.status === 0 ? result.stdout.trim() || null : null; -} - -function brewPrefixForPackage(packageName) { - const result = spawnSync("brew", ["--prefix", packageName], { encoding: "utf8" }); - return result.status === 0 ? result.stdout.trim() || null : null; -} - -function configuredCandidate(root, source) { - return root ? { root, source } : null; -} - -function existingConfiguredCandidates(candidates) { - return candidates.filter(Boolean); -} - -function formatCandidateSources(candidates) { - return candidates.map((candidate) => candidate.source).join(", ") || "none"; -} - -function configureGstreamerPluginDiscovery(env, sdkRoot) { - const pluginDir = join(sdkRoot, "lib", "gstreamer-1.0"); - const scanner = join( - sdkRoot, - "libexec", - "gstreamer-1.0", - process.platform === "win32" ? "gst-plugin-scanner.exe" : "gst-plugin-scanner", +const appProtocolVersion = readVersion( + appProtocolSourcePath, + /export\s+const\s+NATIVE_STREAMER_PROTOCOL_VERSION\s*=\s*(\d+)\s*;/, + "app", +); +const nativeProtocolVersion = readVersion( + protocolSourcePath, + /pub\s+const\s+PROTOCOL_VERSION\s*:\s*u64\s*=\s*(\d+)\s*;/, + "native", +); +if (appProtocolVersion !== nativeProtocolVersion) { + throw new Error( + `Native streamer protocol mismatch: app sends ${appProtocolVersion}, native expects ${nativeProtocolVersion}.`, ); - - if (isExistingDirectory(pluginDir)) { - env.GST_PLUGIN_PATH = pluginDir; - env.GST_PLUGIN_PATH_1_0 = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH_1_0 = pluginDir; - } - if (isExistingFile(scanner)) { - env.GST_PLUGIN_SCANNER = scanner; - env.GST_PLUGIN_SCANNER_1_0 = scanner; - } - env.GST_REGISTRY_REUSE_PLUGIN_SCANNER = "no"; -} - -function configureGstreamerSdk(env) { - if (process.platform === "win32") { - const candidates = existingConfiguredCandidates([ - configuredCandidate(env.GSTREAMER_1_0_ROOT_MSVC_X86_64, "GSTREAMER_1_0_ROOT_MSVC_X86_64"), - configuredCandidate("C:\\Program Files\\gstreamer\\1.0\\msvc_x86_64", "default Program Files"), - configuredCandidate("C:\\gstreamer\\1.0\\msvc_x86_64", "default C drive"), - ]); - const sdk = candidates - .map((candidate) => { - const pkgConfigFile = join(candidate.root, "lib", "pkgconfig", "gstreamer-1.0.pc"); - const pkgConfigBinary = ["pkg-config.exe", "pkgconf.exe"] - .map((name) => join(candidate.root, "bin", name)) - .find((path) => existsSync(path)); - return { ...candidate, pkgConfigBinary, pkgConfigFile }; - }) - .find((candidate) => candidate.pkgConfigBinary && existsSync(candidate.pkgConfigFile)); - if (!sdk) { - console.warn( - [ - "GStreamer SDK was not found automatically; relying on the current PKG_CONFIG environment.", - `Checked ${candidates.length} candidate source(s): ${formatCandidateSources(candidates)}.`, - "Expected relative files: bin/pkg-config.exe or bin/pkgconf.exe, and lib/pkgconfig/gstreamer-1.0.pc.", - ].join(" "), - ); - return null; - } - const pkgConfigDir = join(sdk.root, "lib", "pkgconfig"); - env.PKG_CONFIG = sdk.pkgConfigBinary; - env.PKG_CONFIG_PATH = env.PKG_CONFIG_PATH ? `${pkgConfigDir}${delimiter}${env.PKG_CONFIG_PATH}` : pkgConfigDir; - prependEnvPath(env, join(sdk.root, "bin")); - // The Windows MSI supports a custom INSTALLDIR, but a native executable outside - // the SDK cannot reliably infer that relocated plugin directory from its own path. - configureGstreamerPluginDiscovery(env, sdk.root); - console.log(`Configured GStreamer SDK from ${sdk.source}.`); - console.log("Configured pkg-config executable for GStreamer SDK."); - return sdk.root; - } - - if (process.platform === "darwin") { - const homebrewRoot = brewPrefix(); - const homebrewGStreamerRoot = brewPrefixForPackage("gstreamer"); - const candidates = existingConfiguredCandidates([ - configuredCandidate(env.GSTREAMER_1_0_ROOT_MACOS, "GSTREAMER_1_0_ROOT_MACOS"), - configuredCandidate("/Library/Frameworks/GStreamer.framework/Versions/1.0", "GStreamer framework version 1.0"), - configuredCandidate("/Library/Frameworks/GStreamer.framework/Versions/Current", "GStreamer framework current"), - configuredCandidate(homebrewGStreamerRoot, "Homebrew gstreamer prefix"), - configuredCandidate(homebrewRoot && join(homebrewRoot, "opt", "gstreamer"), "Homebrew opt gstreamer"), - configuredCandidate(homebrewRoot, "Homebrew prefix"), - configuredCandidate("/opt/homebrew", "default Homebrew Apple Silicon prefix"), - configuredCandidate("/usr/local", "default Homebrew Intel prefix"), - ]); - const sdk = candidates.find((candidate) => - existsSync(join(candidate.root, "lib", "pkgconfig", "gstreamer-1.0.pc")) - && existsSync(join(candidate.root, "lib", "libgstreamer-1.0.dylib")), - ); - if (!sdk) { - console.warn( - [ - "GStreamer macOS SDK was not found automatically; relying on the current PKG_CONFIG environment.", - `Checked ${candidates.length} candidate source(s): ${formatCandidateSources(candidates)}.`, - "Expected relative files: lib/pkgconfig/gstreamer-1.0.pc and lib/libgstreamer-1.0.dylib.", - ].join(" "), - ); - return null; - } - const sdkRoot = sdk.root; - const pkgConfigDir = join(sdkRoot, "lib", "pkgconfig"); - env.GSTREAMER_1_0_ROOT_MACOS = sdkRoot; - env.PKG_CONFIG_PATH = env.PKG_CONFIG_PATH ? `${pkgConfigDir}${delimiter}${env.PKG_CONFIG_PATH}` : pkgConfigDir; - prependEnvPath(env, join(sdkRoot, "bin")); - env.DYLD_LIBRARY_PATH = env.DYLD_LIBRARY_PATH ? `${join(sdkRoot, "lib")}${delimiter}${env.DYLD_LIBRARY_PATH}` : join(sdkRoot, "lib"); - env.DYLD_FALLBACK_LIBRARY_PATH = env.DYLD_FALLBACK_LIBRARY_PATH ? `${join(sdkRoot, "lib")}${delimiter}${env.DYLD_FALLBACK_LIBRARY_PATH}` : join(sdkRoot, "lib"); - if (/apple-darwin$/.test(nativeTarget)) { - env.PKG_CONFIG_ALLOW_CROSS = "1"; - env.PKG_CONFIG_SYSROOT_DIR = env.PKG_CONFIG_SYSROOT_DIR || "/"; - } - console.log(`Configured GStreamer SDK from ${sdk.source}.`); - return sdkRoot; - } - - return null; } -function bundleGstreamerRuntime(sdkRoot, nativeFeatures) { - if (!shouldBundlePrivateGstreamerRuntime(nativeFeatures)) { - return false; - } - - const args = [ - join(__dirname, "bundle-gstreamer-runtime.mjs"), - "--dest", - join(packagePlatformBinaryDir, "gstreamer"), - ]; - - if (sdkRoot) { - args.push("--sdk-root", sdkRoot); - } - args.push("--binary", packagePlatformBinary); +const cargoArgs = [ + "build", + "--locked", + "--release", + "--package", + "opennow-streamer", + "--manifest-path", + manifestPath, +]; +if (nativeTarget) cargoArgs.push("--target", nativeTarget); - const result = spawnSync(process.execPath, args, { +const build = spawnSync("cargo", cargoArgs, { + cwd: workspaceRoot, + stdio: "inherit", + env: process.env, +}); +if (build.status !== 0) process.exit(build.status ?? 1); +if (!existsSync(builtBinary)) throw new Error(`Native streamer build missing: ${builtBinary}`); + +mkdirSync(dirname(platformBinary), { recursive: true }); +copyFileSync(builtBinary, platformBinary); +if (!platformKey.startsWith("win32-")) { + chmodSync(platformBinary, 0o755); +} + +const hostPlatformKey = `${process.platform}-${process.arch}`; +if (!nativeTarget || platformKey === hostPlatformKey) { + const input = [ + JSON.stringify({ id: "verify", type: "hello", protocolVersion: nativeProtocolVersion }), + JSON.stringify({ + id: "verify-start", + type: "start", + context: { + session: { + sessionId: "build-verification", + serverIp: "127.0.0.1", + iceServers: [], + }, + settings: { codec: "H264" }, + shortcuts: {}, + }, + }), + JSON.stringify({ id: "stop", type: "stop", reason: "build verification" }), + "", + ].join("\n"); + const verify = spawnSync(platformBinary, [], { cwd: packageRoot, - stdio: "inherit", - env: process.env, - }); - - if (result.status !== 0) { - process.exit(result.status ?? 1); - } - - if (process.platform === "win32") { - injectWindowsVulkanPlugins(join(packagePlatformBinaryDir, "gstreamer")); - } - - return true; -} - -function injectWindowsVulkanPlugins(runtimeRoot) { - const result = spawnSync( - process.execPath, - [join(__dirname, "inject-gstreamer-vulkan-windows.mjs"), "--dest", runtimeRoot], - { - cwd: packageRoot, - stdio: "inherit", - env: process.env, - }, - ); - if (result.status !== 0) { - process.exit(result.status ?? 1); - } -} - -function isExistingFile(path) { - try { - return existsSync(path) && statSync(path).isFile(); - } catch { - return false; - } -} - -function isExistingDirectory(path) { - try { - return existsSync(path) && statSync(path).isDirectory(); - } catch { - return false; - } -} - -function buildBundledGstreamerEnv(baseEnv, binaryPath) { - const env = { SystemRoot: baseEnv.SystemRoot, WINDIR: baseEnv.WINDIR }; - const runtimeRoot = join(dirname(binaryPath), "gstreamer"); - const binDir = join(runtimeRoot, "bin"); - const libDir = join(runtimeRoot, "lib"); - const pluginDir = join(libDir, "gstreamer-1.0"); - const scanner = join(runtimeRoot, "libexec", "gstreamer-1.0", process.platform === "win32" ? "gst-plugin-scanner.exe" : "gst-plugin-scanner"); - const gioModulesDir = join(libDir, "gio", "modules"); - - if (!isExistingDirectory(runtimeRoot)) { - throw new Error(`Bundled GStreamer runtime was not found next to ${binaryPath}`); - } - if (process.platform === "win32") prependEnvPath(env, dirname(binaryPath)); - if (isExistingDirectory(binDir)) prependEnvPath(env, binDir); - if (isExistingDirectory(pluginDir)) { - env.GST_PLUGIN_PATH = pluginDir; - env.GST_PLUGIN_PATH_1_0 = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH_1_0 = pluginDir; - } - if (isExistingFile(scanner)) { - env.GST_PLUGIN_SCANNER = scanner; - env.GST_PLUGIN_SCANNER_1_0 = scanner; - } - env.GST_REGISTRY_REUSE_PLUGIN_SCANNER = "no"; - if (isExistingDirectory(gioModulesDir)) { - env.GIO_MODULE_DIR = gioModulesDir; - env.GIO_EXTRA_MODULES = gioModulesDir; - } - if (process.platform === "linux" && isExistingDirectory(libDir)) { - env.LD_LIBRARY_PATH = env.LD_LIBRARY_PATH ? `${libDir}${delimiter}${env.LD_LIBRARY_PATH}` : libDir; - } - if (process.platform === "darwin" && isExistingDirectory(libDir)) { - env.DYLD_LIBRARY_PATH = env.DYLD_LIBRARY_PATH ? `${libDir}${delimiter}${env.DYLD_LIBRARY_PATH}` : libDir; - env.DYLD_FALLBACK_LIBRARY_PATH = env.DYLD_FALLBACK_LIBRARY_PATH ? `${libDir}${delimiter}${env.DYLD_FALLBACK_LIBRARY_PATH}` : libDir; - } - if (process.platform === "win32" && isExistingDirectory(libDir)) { - prependEnvPath(env, libDir); - } - return env; -} - -function parseNativeStreamerResponse(stdout) { - const messages = []; - for (const rawLine of stdout.split(/\r?\n/)) { - const line = rawLine.trim(); - if (!line) { - continue; - } - - try { - messages.push(JSON.parse(line)); - } catch { - console.error(`Native streamer verification returned invalid JSON: ${line}`); - process.exit(1); - } - } - - const response = messages.find((message) => message?.id === verifyCommandId); - if (!response) { - console.error( - `Native streamer verification did not return a response to ${verifyCommandId}: ${JSON.stringify(messages)}`, - ); - process.exit(1); - } - - return response; -} - -function verifyGstreamerBinary(binaryPath, env) { - const result = spawnSync(binaryPath, { - input: `${JSON.stringify({ id: verifyCommandId, type: "hello", protocolVersion: nativeStreamerProtocolVersion })}\n`, - encoding: "utf8", - env: { - ...env, - OPENNOW_NATIVE_STREAMER_BACKEND: "gstreamer", - }, - }); - - if (result.status !== 0) { - console.error(result.stderr || result.stdout); - console.error(`Native streamer verification failed for ${binaryPath}`); - process.exit(result.status ?? 1); - } - - const response = parseNativeStreamerResponse(result.stdout); - if (response.type === "error") { - console.error( - `Native streamer verification failed: ${response.code ?? "error"}${response.message ? `: ${response.message}` : ""}`, - ); - process.exit(1); - } - - const capabilities = response.capabilities; - if ( - response.type !== "ready" || - capabilities?.backend !== "gstreamer" || - capabilities?.supportsOfferAnswer !== true || - capabilities?.supportsInput !== true - ) { - console.error( - `Native streamer verification expected a GStreamer backend, got: ${JSON.stringify( - capabilities, - )}`, - ); - process.exit(1); - } - - if (!Array.isArray(capabilities.videoBackends) || capabilities.videoBackends.length === 0) { - console.error( - `Native streamer verification expected video backend capabilities, got: ${JSON.stringify( - capabilities, - )}`, - ); - process.exit(1); - } - - const availableVideoBackends = capabilities.videoBackends - .filter((backend) => backend?.available) - .map((backend) => { - const codecs = Array.isArray(backend.codecs) - ? backend.codecs.filter((codec) => codec.available).map((codec) => codec.codec).join("/") - : ""; - return `${backend.backend}${codecs ? `(${codecs})` : ""}`; - }); - - if (availableVideoBackends.length === 0) { - console.error( - `Native streamer verification found no usable video backend: ${JSON.stringify( - capabilities.videoBackends, - )}`, - ); - process.exit(1); - } - - console.log(`Verified native streamer GStreamer capabilities: ${availableVideoBackends.join(", ")}.`); -} - -function verifyBundledWindowsLoader(binaryPath, baseEnv) { - const result = spawnSync(binaryPath, { - cwd: dirname(binaryPath), - input: `${JSON.stringify({ id: verifyCommandId, type: "hello", protocolVersion: nativeStreamerProtocolVersion })}\n`, encoding: "utf8", env: { - SystemRoot: baseEnv.SystemRoot, - WINDIR: baseEnv.WINDIR, - PATH: dirname(binaryPath), - OPENNOW_NATIVE_STREAMER_BACKEND: "gstreamer", + ...process.env, + SDL_AUDIODRIVER: "dummy", + SDL_VIDEODRIVER: "dummy", }, + input, + timeout: 15_000, }); - if (result.status !== 0) { - console.error(result.stderr || result.stdout); - console.error("Bundled native streamer could not start using only DLLs next to its executable."); - process.exit(result.status ?? 1); - } - parseNativeStreamerResponse(result.stdout); - console.log("Verified native streamer Windows loader dependency closure."); -} - -function verifyBundledWindowsVulkanPlugin(binaryPath, env) { - const gstInspect = join(dirname(binaryPath), "gstreamer", "bin", "gst-inspect-1.0.exe"); - const result = spawnSync(gstInspect, ["vulkanupload"], { - encoding: "utf8", - env, - }); - if (result.status !== 0) { - console.error(result.stderr || result.stdout); - console.error("Bundled GStreamer Vulkan plugin failed to load."); - process.exit(result.status ?? 1); + if (verify.status !== 0) { + throw new Error(`Native streamer verification failed: ${verify.stderr || verify.stdout}`); } - console.log("Verified bundled GStreamer Vulkan plugin and loader."); -} - -const cargoArgs = ["build", "--release", "--manifest-path", manifestPath]; -if (nativeTarget) { - cargoArgs.push("--target", nativeTarget); -} -const nativeFeatures = process.env.OPENNOW_NATIVE_STREAMER_FEATURES?.trim() || "gstreamer"; -if (nativeFeatures && nativeFeatures.toLowerCase() !== "none") { - cargoArgs.push("--features", nativeFeatures); -} -console.log( - nativeFeatures.toLowerCase() === "none" - ? "Building native streamer without optional features." - : `Building native streamer with features: ${nativeFeatures}`, -); - -const buildEnv = { ...process.env }; -let gstreamerSdkRoot = null; -if (hasFeature(nativeFeatures, "gstreamer")) { - gstreamerSdkRoot = configureGstreamerSdk(buildEnv); -} -configureDarwinLinkerPadding(buildEnv, nativeFeatures); - -const cargoCommand = process.platform === "win32" ? "cargo.exe" : "cargo"; -const result = spawnSync(cargoCommand, cargoArgs, { - cwd: repoRoot, - stdio: "inherit", - env: buildEnv, -}); - -if (result.status !== 0) { - process.exit(result.status ?? 1); -} - -if (!existsSync(builtBinary)) { - console.error(`Native streamer build did not produce ${builtBinary}`); - process.exit(1); -} - -mkdirSync(packageBinaryDir, { recursive: true }); -mkdirSync(packagePlatformBinaryDir, { recursive: true }); -copyFileSync(builtBinary, packageBinary); -copyFileSync(builtBinary, packagePlatformBinary); - -if (process.platform !== "win32") { - chmodSync(packageBinary, 0o755); - chmodSync(packagePlatformBinary, 0o755); -} - -if (hasFeature(nativeFeatures, "gstreamer")) { - verifyGstreamerBinary(packageBinary, buildEnv); - if (bundleGstreamerRuntime(gstreamerSdkRoot, nativeFeatures)) { - const bundledEnv = buildBundledGstreamerEnv(buildEnv, packagePlatformBinary); - if (process.platform === "win32") { - verifyBundledWindowsLoader(packagePlatformBinary, buildEnv); - verifyBundledWindowsVulkanPlugin(packagePlatformBinary, bundledEnv); - } - verifyGstreamerBinary(packagePlatformBinary, bundledEnv); + const messages = verify.stdout + .split(/\r?\n/) + .filter(Boolean) + .map((line) => JSON.parse(line)); + const ready = messages.find((message) => message.id === "verify" && message.type === "ready"); + const stopped = messages.find((message) => message.id === "stop" && message.type === "ok"); + const started = messages.find((message) => message.id === "verify-start" && message.type === "ok"); + const capabilitiesComplete = ready?.capabilities?.supportsOfferAnswer === true + && ready.capabilities.supportsVideoDecode === true + && ready.capabilities.supportsVideoPresent === true + && ready.capabilities.supportsAudioDecode === true + && ready.capabilities.supportsAudioOutput === true; + if (!capabilitiesComplete || !started || !stopped) { + throw new Error(`Native streamer verification returned an incomplete handshake: ${verify.stdout}`); } } -console.log(`Copied native streamer to ${packageBinary}`); -console.log(`Copied native streamer to ${packagePlatformBinary}`); +console.log(`Built native streamer v2: ${platformBinary}`); diff --git a/opennow-stable/scripts/build-patched-gstreamer-d3d11.ps1 b/opennow-stable/scripts/build-patched-gstreamer-d3d11.ps1 deleted file mode 100644 index abee94a9b..000000000 --- a/opennow-stable/scripts/build-patched-gstreamer-d3d11.ps1 +++ /dev/null @@ -1,91 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string] $GStreamerRoot, - [Parameter(Mandatory = $true)] - [string] $Version -) - -$ErrorActionPreference = "Stop" -$patch = Resolve-Path (Join-Path $PSScriptRoot "..\..\native\gstreamer-patches\0001-d3d11-enable-tearing-vrr.patch") -$workRoot = Join-Path ([System.IO.Path]::GetTempPath()) "opennow-gstreamer-$Version" -$archive = Join-Path $workRoot "gstreamer-$Version.tar.gz" -$sourceRoot = Join-Path $workRoot "gstreamer-$Version" -$buildRoot = Join-Path $workRoot "build" -$sourceUrl = "https://gitlab.freedesktop.org/gstreamer/gstreamer/-/archive/$Version/gstreamer-$Version.tar.gz" -$sourceMirrorUrl = "https://github.com/GStreamer/gstreamer/archive/refs/tags/$Version.tar.gz" - -Remove-Item -Recurse -Force $workRoot -ErrorAction SilentlyContinue -New-Item -ItemType Directory -Force -Path $workRoot | Out-Null - -& curl.exe --fail --location --retry 5 --retry-all-errors --output $archive $sourceUrl -if ($LASTEXITCODE -ne 0) { - & curl.exe --fail --location --retry 5 --retry-all-errors --output $archive $sourceMirrorUrl - if ($LASTEXITCODE -ne 0) { - throw "Failed to download GStreamer $Version source." - } -} - -& tar.exe -xzf $archive -C $workRoot -if ($LASTEXITCODE -ne 0) { - throw "Failed to extract GStreamer $Version source." -} - -& git.exe -C $sourceRoot apply --check $patch -if ($LASTEXITCODE -ne 0) { - throw "OpenNOW D3D11 tearing patch does not apply to GStreamer $Version." -} -& git.exe -C $sourceRoot apply $patch -if ($LASTEXITCODE -ne 0) { - throw "Failed to apply OpenNOW D3D11 tearing patch." -} - -python -m pip install --disable-pip-version-check --quiet meson ninja -if ($LASTEXITCODE -ne 0) { - throw "Failed to install Meson and Ninja." -} - -$env:PKG_CONFIG_PATH = "$(Join-Path $GStreamerRoot "lib\pkgconfig");$env:PKG_CONFIG_PATH" -$env:PATH = "$(Join-Path $GStreamerRoot "bin");$env:PATH" -$badPluginsSource = Join-Path $sourceRoot "subprojects\gst-plugins-bad" - -meson setup $buildRoot $badPluginsSource ` - --buildtype=release ` - --vsenv ` - -Dauto_features=disabled ` - -Dd3d11=enabled ` - -Dtests=disabled ` - -Dexamples=disabled ` - -Dtools=disabled ` - -Dintrospection=disabled ` - -Dgpl=disabled -if ($LASTEXITCODE -ne 0) { - throw "Failed to configure the patched GStreamer D3D11 plugin." -} - -meson compile -C $buildRoot gstd3d11 -if ($LASTEXITCODE -ne 0) { - throw "Failed to compile the patched GStreamer D3D11 plugin." -} - -$plugin = Get-ChildItem -Path $buildRoot -Filter "gstd3d11.dll" -Recurse | - Select-Object -First 1 -if (-not $plugin) { - throw "Patched gstd3d11.dll was not produced." -} - -$pluginDestination = Join-Path $GStreamerRoot "lib\gstreamer-1.0\gstd3d11.dll" -Copy-Item -Force $plugin.FullName $pluginDestination - -$d3d11Library = Get-ChildItem -Path $buildRoot -Filter "gstd3d11-1.0-0.dll" -Recurse | - Select-Object -First 1 -if ($d3d11Library) { - Copy-Item -Force $d3d11Library.FullName (Join-Path $GStreamerRoot "bin\gstd3d11-1.0-0.dll") -} - -$gstInspect = Join-Path $GStreamerRoot "bin\gst-inspect-1.0.exe" -& $gstInspect d3d11videosink -if ($LASTEXITCODE -ne 0) { - throw "Patched GStreamer D3D11 plugin failed its gst-inspect smoke check." -} - -Write-Host "Installed patched GStreamer D3D11 plugin: $pluginDestination" diff --git a/opennow-stable/scripts/bundle-gstreamer-runtime.mjs b/opennow-stable/scripts/bundle-gstreamer-runtime.mjs deleted file mode 100644 index 174cc4615..000000000 --- a/opennow-stable/scripts/bundle-gstreamer-runtime.mjs +++ /dev/null @@ -1,390 +0,0 @@ -import { - chmodSync, - copyFileSync, - cpSync, - existsSync, - mkdirSync, - readdirSync, - readFileSync, - rmSync, - statSync, - writeFileSync, -} from "node:fs"; -import { basename, delimiter, dirname, extname, join, relative, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; -import { collectBundledPeDependencies } from "./windows-pe-imports.mjs"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageRoot = resolve(__dirname, ".."); - -function parseArgs(argv) { - const parsed = new Map(); - for (let index = 0; index < argv.length; index += 1) { - const value = argv[index]; - if (!value.startsWith("--")) continue; - const key = value.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - parsed.set(key, "true"); - continue; - } - parsed.set(key, next); - index += 1; - } - return parsed; -} - -function isExistingFile(path) { - try { return existsSync(path) && statSync(path).isFile(); } catch { return false; } -} - -function isExistingDirectory(path) { - try { return existsSync(path) && statSync(path).isDirectory(); } catch { return false; } -} - -function run(command, args, options = {}) { - return spawnSync(command, args, { encoding: "utf8", ...options }); -} - -function commandAvailable(command) { - return run("/usr/bin/env", ["which", command]).status === 0; -} - -function brewPrefix() { - const result = run("brew", ["--prefix"]); - return result.status === 0 ? result.stdout.trim() || null : null; -} - -function copyPathIfPresent(source, destination) { - if (!existsSync(source)) return false; - const stats = statSync(source); - if (stats.isDirectory()) { - cpSync(source, destination, { - recursive: true, - force: true, - // Follow symlinks to copy actual files instead of broken links - // (Homebrew GStreamer installs some plugins as symlinks). - dereference: true, - filter: (entry) => { - const lower = entry.toLowerCase(); - return !lower.endsWith(".pdb") - && !lower.endsWith(".lib") - && !lower.endsWith(".a") - && !lower.includes(`${join("share", "doc").toLowerCase()}`); - }, - }); - return true; - } - mkdirSync(dirname(destination), { recursive: true }); - copyFileSync(source, destination); - return true; -} - -function copyMatchingFiles(sourceDir, destinationDir, pattern) { - if (!isExistingDirectory(sourceDir)) return; - mkdirSync(destinationDir, { recursive: true }); - for (const name of readdirSync(sourceDir)) { - if (pattern.test(name)) copyFileSync(join(sourceDir, name), join(destinationDir, name)); - } -} - -function windowsSdkCandidates(explicitSdkRoot) { - return [ - explicitSdkRoot, - process.env.GSTREAMER_1_0_ROOT_MSVC_X86_64, - "C:\\Program Files\\gstreamer\\1.0\\msvc_x86_64", - "C:\\gstreamer\\1.0\\msvc_x86_64", - ].filter(Boolean); -} - -function resolveWindowsSdkRoot(explicitSdkRoot) { - const sdkRoot = windowsSdkCandidates(explicitSdkRoot).find((candidate) => - isExistingFile(join(candidate, "bin", "gstreamer-1.0-0.dll")) - && isExistingDirectory(join(candidate, "lib", "gstreamer-1.0")), - ); - if (!sdkRoot) throw new Error("GStreamer MSVC x86_64 runtime was not found. Install the runtime/development MSI or pass --sdk-root."); - return sdkRoot; -} - -function macosCandidates(explicitSdkRoot) { - return [ - explicitSdkRoot, - process.env.GSTREAMER_1_0_ROOT_MACOS, - "/Library/Frameworks/GStreamer.framework/Versions/1.0", - "/Library/Frameworks/GStreamer.framework/Versions/Current", - brewPrefix(), - "/opt/homebrew", - "/usr/local", - ].filter(Boolean); -} - -function validateMacosRoot(candidate) { - return isExistingFile(join(candidate, "lib", "pkgconfig", "gstreamer-1.0.pc")) - && isExistingFile(join(candidate, "lib", "libgstreamer-1.0.dylib")) - && isExistingDirectory(join(candidate, "lib", "gstreamer-1.0")); -} - -function resolveMacosRuntimeRoot(explicitSdkRoot) { - const runtimeRoot = macosCandidates(explicitSdkRoot).find(validateMacosRoot); - if (!runtimeRoot) { - throw new Error("GStreamer macOS runtime was not found. Install the official runtime/devel .pkg packages or Homebrew packages, or pass --sdk-root."); - } - return runtimeRoot; -} - -function writeMetadata(destination, source, platform) { - writeFileSync( - join(destination, "OPENNOW-GSTREAMER-RUNTIME.txt"), - [ - "OpenNOW private GStreamer runtime bundle", - `Source: ${source}`, - `Generated: ${new Date().toISOString()}`, - `Platform: ${platform}`, - "Scope: native streamer child process only", - "", - "This directory is loaded only for the native streamer child process. Keep the private layout intact.", - "", - ].join("\n"), - ); -} - -const WINDOWS_VC_RUNTIME_DLLS = [ - "vcruntime140.dll", - "vcruntime140_1.dll", - "msvcp140.dll", - "msvcp140_1.dll", - "msvcp140_2.dll", - "msvcp140_atomic_wait.dll", - "msvcp140_codecvt_ids.dll", - "concrt140.dll", -]; - -function windowsVcRuntimeSearchDirs() { - return [ - process.env.VCToolsRedistDir ? join(process.env.VCToolsRedistDir, "x64", "Microsoft.VC143.CRT") : null, - process.env.VCToolsRedistDir ? join(process.env.VCToolsRedistDir, "x64", "Microsoft.VC142.CRT") : null, - process.env.VCToolsRedistDir ? join(process.env.VCToolsRedistDir, "x64", "Microsoft.VC141.CRT") : null, - process.env.SystemRoot ? join(process.env.SystemRoot, "System32") : null, - ...String(process.env.PATH ?? "").split(delimiter), - ].filter(Boolean); -} - -function copyWindowsLoaderDlls({ sdkRoot, destination, binary }) { - const executableDir = binary ? dirname(binary) : dirname(destination); - const sdkBin = join(sdkRoot, "bin"); - const copiedLoader = []; - const copiedVc = []; - - if (!binary) { - throw new Error("A native streamer executable is required to collect its Windows DLL dependencies."); - } - for (const source of collectBundledPeDependencies(binary, sdkBin)) { - const name = basename(source); - copyFileSync(source, join(executableDir, name)); - copiedLoader.push(name); - } - - const vcSearchDirs = windowsVcRuntimeSearchDirs(); - for (const name of WINDOWS_VC_RUNTIME_DLLS) { - const source = vcSearchDirs.map((dir) => join(dir, name)).find(isExistingFile); - if (!source) continue; - copyFileSync(source, join(executableDir, name)); - copyFileSync(source, join(destination, "bin", name)); - copiedVc.push(name); - } - - console.log(`Copied Windows loader DLLs next to native streamer: ${copiedLoader.length ? copiedLoader.join(", ") : "none"}.`); - console.log(`Copied Windows VC runtime DLLs: ${copiedVc.length ? copiedVc.join(", ") : "none found"}.`); -} - -function bundleWindowsRuntime({ sdkRoot, destination, binary }) { - const resolvedSdkRoot = resolveWindowsSdkRoot(sdkRoot); - rmSync(destination, { recursive: true, force: true }); - mkdirSync(destination, { recursive: true }); - const copied = [ - copyPathIfPresent(join(resolvedSdkRoot, "bin"), join(destination, "bin")), - copyPathIfPresent(join(resolvedSdkRoot, "lib", "gstreamer-1.0"), join(destination, "lib", "gstreamer-1.0")), - copyPathIfPresent(join(resolvedSdkRoot, "lib", "gio", "modules"), join(destination, "lib", "gio", "modules")), - copyPathIfPresent(join(resolvedSdkRoot, "libexec", "gstreamer-1.0"), join(destination, "libexec", "gstreamer-1.0")), - copyPathIfPresent(join(resolvedSdkRoot, "share", "gstreamer-1.0"), join(destination, "share", "gstreamer-1.0")), - copyPathIfPresent(join(resolvedSdkRoot, "share", "glib-2.0"), join(destination, "share", "glib-2.0")), - copyPathIfPresent(join(resolvedSdkRoot, "etc"), join(destination, "etc")), - ].filter(Boolean).length; - copyMatchingFiles(resolvedSdkRoot, destination, /^(copying|license|readme)/i); - copyWindowsLoaderDlls({ sdkRoot: resolvedSdkRoot, destination, binary }); - writeMetadata(destination, resolvedSdkRoot, "win32"); - console.log(`Bundled GStreamer runtime from ${resolvedSdkRoot} to ${destination} (${copied} paths).`); -} - -function walkFiles(root) { - if (!isExistingDirectory(root)) return []; - const out = []; - const stack = [root]; - while (stack.length > 0) { - const dir = stack.pop(); - for (const name of readdirSync(dir)) { - const path = join(dir, name); - const stats = statSync(path); - if (stats.isDirectory()) stack.push(path); - else if (stats.isFile()) out.push(path); - } - } - return out; -} - -function isMachO(path) { - try { - const buffer = readFileSync(path, { flag: "r" }); - if (buffer.length < 4) return false; - const magic = buffer.readUInt32BE(0); - return [0xfeedface, 0xfeedfacf, 0xcafebabe, 0xcafebabf, 0xbebafeca, 0xcffaedfe, 0xcefaedfe].includes(magic); - } catch { - return false; - } -} - -function dylibName(ref) { - return ref.split("/").pop(); -} - -function isPathInside(path, parent) { - const relativePath = relative(parent, path); - return relativePath === "" || (!relativePath.startsWith("..") && !relativePath.startsWith("/")); -} - -function posixPath(path) { - return path.split(/[\\/]+/).filter(Boolean).join("/"); -} - -function relocationTarget(file, destination, libDir, dep) { - const name = dylibName(dep); - if (!name) return null; - if (!isPathInside(file, destination)) return `@executable_path/gstreamer/lib/${name}`; - const relativeLibDir = posixPath(relative(dirname(file), libDir)); - return relativeLibDir ? `@loader_path/${relativeLibDir}/${name}` : `@loader_path/${name}`; -} - -function shouldRewriteDependency(dep, roots, bundledLibs) { - const name = dylibName(dep); - if (!name || !bundledLibs.has(name)) return false; - if (dep.startsWith("@rpath/") || dep.startsWith("@loader_path/") || dep.startsWith("@executable_path/")) return bundledLibs.has(name); - return roots.some((root) => dep === join(root, "lib", name) || dep.startsWith(`${root}/`) || dep.includes("GStreamer.framework")); -} - -function macosExternalRoots(sourceRoot) { - return [ - sourceRoot, - "/Library/Frameworks/GStreamer.framework/Versions/1.0", - "/Library/Frameworks/GStreamer.framework/Versions/Current", - "/Library/Frameworks/GStreamer.framework", - "/opt/homebrew", - "/usr/local", - ]; -} - -function runInstallNameTool(args, file) { - const result = run("install_name_tool", args, { stdio: "inherit" }); - if (result.status !== 0) { - throw new Error(`install_name_tool failed for ${file}: install_name_tool ${args.join(" ")}`); - } -} - -function patchMachO(file, destination, sourceRoot, libDir, bundledLibs) { - if (!isMachO(file) || !commandAvailable("otool") || !commandAvailable("install_name_tool")) return; - const output = run("otool", ["-L", file]); - if (output.status !== 0) return; - const roots = macosExternalRoots(sourceRoot); - for (const line of output.stdout.split(/\r?\n/).slice(1)) { - const dep = line.trim().split(/\s+/)[0]; - if (!dep || !shouldRewriteDependency(dep, roots, bundledLibs)) continue; - const target = relocationTarget(file, destination, libDir, dep); - if (target && target !== dep) runInstallNameTool(["-change", dep, target, file], file); - } - if (isPathInside(file, libDir) && extname(file) === ".dylib") { - runInstallNameTool(["-id", `@rpath/${dylibName(file)}`, file], file); - } -} - -function isExternalGstreamerDependency(dep, roots, bundledLibs) { - const name = dylibName(dep); - if (!name || !bundledLibs.has(name)) return false; - if (dep.startsWith("@")) return false; - return roots.some((root) => dep === join(root, "lib", name) || dep.startsWith(`${root}/`) || dep.includes("GStreamer.framework")); -} - -function validatePackagedBinaryRelocation(binary, sourceRoot, bundledLibs) { - if (!binary || !isMachO(binary) || !commandAvailable("otool")) return; - const output = run("otool", ["-L", binary]); - if (output.status !== 0) { - throw new Error(`otool -L failed for packaged native streamer: ${binary}`); - } - const roots = macosExternalRoots(sourceRoot); - const leakedDeps = output.stdout - .split(/\r?\n/) - .slice(1) - .map((line) => line.trim().split(/\s+/)[0]) - .filter((dep) => dep && isExternalGstreamerDependency(dep, roots, bundledLibs)); - if (leakedDeps.length > 0) { - throw new Error( - `Packaged native streamer still references external GStreamer dependencies after relocation: ${leakedDeps.join(", ")}`, - ); - } -} - -function bundleMacosRuntime({ sdkRoot, destination, binary }) { - const resolvedRuntimeRoot = resolveMacosRuntimeRoot(sdkRoot); - rmSync(destination, { recursive: true, force: true }); - mkdirSync(destination, { recursive: true }); - const copied = [ - copyPathIfPresent(join(resolvedRuntimeRoot, "bin"), join(destination, "bin")), - copyPathIfPresent(join(resolvedRuntimeRoot, "lib", "gstreamer-1.0"), join(destination, "lib", "gstreamer-1.0")), - copyPathIfPresent(join(resolvedRuntimeRoot, "lib", "gio", "modules"), join(destination, "lib", "gio", "modules")), - copyPathIfPresent(join(resolvedRuntimeRoot, "libexec", "gstreamer-1.0"), join(destination, "libexec", "gstreamer-1.0")), - copyPathIfPresent(join(resolvedRuntimeRoot, "share"), join(destination, "share")), - copyPathIfPresent(join(resolvedRuntimeRoot, "etc"), join(destination, "etc")), - ].filter(Boolean).length; - copyMatchingFiles(join(resolvedRuntimeRoot, "lib"), join(destination, "lib"), /\.(dylib|so)$/); - copyMatchingFiles(resolvedRuntimeRoot, destination, /^(copying|license|readme)/i); - const libDir = join(destination, "lib"); - const bundledLibs = new Set(readdirSync(libDir).filter((name) => name.endsWith(".dylib") || name.endsWith(".so"))); - for (const file of [...walkFiles(destination), binary].filter(Boolean)) { - try { chmodSync(file, statSync(file).mode | 0o200); } catch {} - patchMachO(file, destination, resolvedRuntimeRoot, libDir, bundledLibs); - } - validatePackagedBinaryRelocation(binary, resolvedRuntimeRoot, bundledLibs); - writeMetadata(destination, resolvedRuntimeRoot, "darwin"); - console.log(`Bundled GStreamer runtime from ${resolvedRuntimeRoot} to ${destination} (${copied} paths plus dylibs).`); -} - -const args = parseArgs(process.argv.slice(2)); -const destination = args.get("dest"); -if (!destination) { - console.error("Usage: node scripts/bundle-gstreamer-runtime.mjs --dest [--sdk-root ] [--binary ]"); - process.exit(1); -} - -try { - const resolvedDestination = resolve(packageRoot, destination); - if (process.platform === "win32") { - bundleWindowsRuntime({ - sdkRoot: args.get("sdk-root"), - destination: resolvedDestination, - binary: args.get("binary") ? resolve(packageRoot, args.get("binary")) : null, - }); - } else if (process.platform === "darwin") { - bundleMacosRuntime({ - sdkRoot: args.get("sdk-root"), - destination: resolvedDestination, - binary: args.get("binary") ? resolve(packageRoot, args.get("binary")) : null, - }); - } else { - throw new Error( - `Private GStreamer runtime collection is intentionally unsupported on Linux (${process.platform}). Linux builds use distro GStreamer packages because AppImage/private bundling is unreliable across glibc, libdrm/VAAPI/Vulkan, and GPU driver stacks.`, - ); - } -} catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); -} diff --git a/opennow-stable/scripts/dev.mjs b/opennow-stable/scripts/dev.mjs deleted file mode 100644 index 4412491be..000000000 --- a/opennow-stable/scripts/dev.mjs +++ /dev/null @@ -1,84 +0,0 @@ -import { spawn, spawnSync } from "node:child_process"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageRoot = resolve(__dirname, ".."); -const repoRoot = resolve(packageRoot, ".."); -const crateRoot = join(repoRoot, "native", "opennow-streamer"); -const exeName = process.platform === "win32" ? "opennow-streamer.exe" : "opennow-streamer"; -const nativeTarget = process.env.OPENNOW_NATIVE_STREAMER_TARGET?.trim() || ""; -const nativeProfile = process.env.OPENNOW_NATIVE_STREAMER_DEV_PROFILE?.trim() || "debug"; -const nativeFeatures = - process.env.OPENNOW_NATIVE_STREAMER_DEV_FEATURES?.trim() - ?? process.env.OPENNOW_NATIVE_STREAMER_FEATURES?.trim() - ?? "none"; -const targetDir = nativeTarget - ? join(crateRoot, "target", nativeTarget, nativeProfile) - : join(crateRoot, "target", nativeProfile); -const streamerBinary = join(targetDir, exeName); - -function runNativeBuild() { - if (process.env.OPENNOW_SKIP_NATIVE_STREAMER_BUILD === "1") { - console.log("Skipping native streamer build because OPENNOW_SKIP_NATIVE_STREAMER_BUILD=1."); - return; - } - - const args = [ - join(__dirname, "build-native-streamer.mjs"), - "--profile", - nativeProfile, - "--features", - nativeFeatures, - "--no-copy", - "--skip-verify", - ]; - const result = spawnSync(process.execPath, args, { - cwd: packageRoot, - stdio: "inherit", - env: process.env, - }); - - if (result.status !== 0) { - process.exit(result.status ?? 1); - } -} - -function runElectronVite() { - const child = spawn("electron-vite", ["dev"], { - cwd: packageRoot, - stdio: "inherit", - shell: process.platform === "win32", - env: { - ...process.env, - OPENNOW_NATIVE_STREAMER: process.env.OPENNOW_NATIVE_STREAMER?.trim() || streamerBinary, - }, - }); - - const forwardSignal = (signal) => { - if (!child.killed) { - child.kill(signal); - } - }; - process.once("SIGINT", forwardSignal); - process.once("SIGTERM", forwardSignal); - - child.once("exit", (code, signal) => { - process.off("SIGINT", forwardSignal); - process.off("SIGTERM", forwardSignal); - if (signal) { - process.kill(process.pid, signal); - return; - } - process.exit(code ?? 0); - }); - - child.once("error", (error) => { - console.error(`Failed to start electron-vite dev: ${error.message}`); - process.exit(1); - }); -} - -runNativeBuild(); -runElectronVite(); diff --git a/opennow-stable/scripts/inject-gstreamer-vulkan-windows.mjs b/opennow-stable/scripts/inject-gstreamer-vulkan-windows.mjs deleted file mode 100644 index 94146d122..000000000 --- a/opennow-stable/scripts/inject-gstreamer-vulkan-windows.mjs +++ /dev/null @@ -1,148 +0,0 @@ -import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageRoot = resolve(__dirname, ".."); -const repoRoot = resolve(packageRoot, ".."); -const vendorRoot = join(repoRoot, "native", "opennow-streamer", "vendor", "gstreamer-vulkan-windows"); - -function parseArgs(argv) { - const parsed = new Map(); - for (let index = 0; index < argv.length; index += 1) { - const value = argv[index]; - if (!value.startsWith("--")) continue; - const key = value.slice(2); - const next = argv[index + 1]; - if (!next || next.startsWith("--")) { - parsed.set(key, "true"); - continue; - } - parsed.set(key, next); - index += 1; - } - return parsed; -} - -function isExistingFile(path) { - try { - return existsSync(path) && statSync(path).isFile(); - } catch { - return false; - } -} - -function isExistingDirectory(path) { - try { - return existsSync(path) && statSync(path).isDirectory(); - } catch { - return false; - } -} - -function readRuntimeVersion(runtimeRoot) { - const metadataPath = join(runtimeRoot, "OPENNOW-GSTREAMER-RUNTIME.txt"); - if (!isExistingFile(metadataPath)) return null; - const text = readFileSync(metadataPath, "utf8"); - const sourceLine = text.split(/\r?\n/).find((line) => line.startsWith("Source:")); - // Prefer probing the bundled gst-inspect version when available. - const inspect = join(runtimeRoot, "bin", "gst-inspect-1.0.exe"); - if (isExistingFile(inspect)) { - const result = spawnSync(inspect, ["--version"], { encoding: "utf8" }); - const match = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/GStreamer\s+(\d+\.\d+\.\d+)/i); - if (match) return match[1]; - } - return sourceLine ? sourceLine.slice("Source:".length).trim() : null; -} - -function listVendorVersions() { - if (!isExistingDirectory(vendorRoot)) return []; - return readdirSync(vendorRoot) - .filter((name) => /^\d+\.\d+\.\d+$/.test(name) && isExistingDirectory(join(vendorRoot, name))) - .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })); -} - -function resolveVendorDir(runtimeVersion) { - const versions = listVendorVersions(); - if (versions.length === 0) return null; - if (runtimeVersion && versions.includes(runtimeVersion)) { - return join(vendorRoot, runtimeVersion); - } - // Fall back to newest vendored build; plugin ABI is usually compatible within a minor series. - if (runtimeVersion) { - const [major, minor] = runtimeVersion.split("."); - const sameSeries = versions.find((version) => version.startsWith(`${major}.${minor}.`)); - if (sameSeries) return join(vendorRoot, sameSeries); - return null; - } - return join(vendorRoot, versions[0]); -} - -function injectVulkanPlugins(runtimeRoot, vendorDir) { - const pluginSource = join(vendorDir, "lib", "gstreamer-1.0", "gstvulkan.dll"); - const librarySource = join(vendorDir, "bin", "gstvulkan-1.0-0.dll"); - const loaderSource = join(packageRoot, "node_modules", "electron", "dist", "vulkan-1.dll"); - if (!isExistingFile(pluginSource) || !isExistingFile(librarySource)) { - throw new Error(`Vendored Vulkan artifacts are incomplete under ${vendorDir}`); - } - if (!isExistingFile(loaderSource)) { - throw new Error(`Electron Vulkan loader was not found: ${loaderSource}`); - } - - const pluginDestDir = join(runtimeRoot, "lib", "gstreamer-1.0"); - const binDestDir = join(runtimeRoot, "bin"); - mkdirSync(pluginDestDir, { recursive: true }); - mkdirSync(binDestDir, { recursive: true }); - copyFileSync(pluginSource, join(pluginDestDir, "gstvulkan.dll")); - copyFileSync(librarySource, join(binDestDir, "gstvulkan-1.0-0.dll")); - copyFileSync(loaderSource, join(binDestDir, "vulkan-1.dll")); - - const metadataPath = join(runtimeRoot, "OPENNOW-GSTREAMER-RUNTIME.txt"); - if (isExistingFile(metadataPath)) { - const text = readFileSync(metadataPath, "utf8"); - if (!text.includes("Vulkan plugin: injected")) { - writeFileSync( - metadataPath, - `${text.trimEnd()}\nVulkan plugin: injected from ${vendorDir}\n`, - "utf8", - ); - } - } - - console.log(`Injected Windows GStreamer Vulkan plugins and loader into ${runtimeRoot}.`); -} - -const args = parseArgs(process.argv.slice(2)); -const destination = args.get("dest"); -if (!destination) { - console.error("Usage: node scripts/inject-gstreamer-vulkan-windows.mjs --dest "); - process.exit(1); -} - -if (process.platform !== "win32") { - console.log("Skipping Windows Vulkan plugin inject on non-Windows host."); - process.exit(0); -} - -try { - const runtimeRoot = resolve(packageRoot, destination); - if (!isExistingDirectory(runtimeRoot)) { - throw new Error(`GStreamer runtime directory was not found: ${runtimeRoot}`); - } - - const runtimeVersion = readRuntimeVersion(runtimeRoot); - const vendorDir = resolveVendorDir(runtimeVersion); - if (!vendorDir) { - throw new Error( - `No compatible vendored Windows GStreamer Vulkan plugins were found for runtime ${runtimeVersion ?? "unknown"}. ` - + `Expected artifacts under ${vendorRoot}//.`, - ); - } - - injectVulkanPlugins(runtimeRoot, vendorDir); -} catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); -} diff --git a/opennow-stable/scripts/run-tests.mjs b/opennow-stable/scripts/run-tests.mjs index c1a2c854b..c3b29f425 100644 --- a/opennow-stable/scripts/run-tests.mjs +++ b/opennow-stable/scripts/run-tests.mjs @@ -73,7 +73,7 @@ async function appendGitHubSummary({ tests, output, exitCode }) { const tests = (await discoverTests("src")) .map((path) => path.split(sep).join("/")) .sort(); -tests.push("scripts/after-sign-mac.test.mjs", "scripts/windows-pe-imports.test.mjs"); +tests.push("scripts/after-sign-mac.test.mjs"); if (tests.length === 0) { console.error("No test files found under src/**/*.test.ts"); diff --git a/opennow-stable/scripts/windows-pe-imports.mjs b/opennow-stable/scripts/windows-pe-imports.mjs deleted file mode 100644 index f00377934..000000000 --- a/opennow-stable/scripts/windows-pe-imports.mjs +++ /dev/null @@ -1,123 +0,0 @@ -import { readFileSync, readdirSync } from "node:fs"; -import { basename, join } from "node:path"; - -function checkedRead(buffer, offset, size, label) { - if (!Number.isInteger(offset) || offset < 0 || offset + size > buffer.length) { - throw new Error(`Invalid PE ${label} offset: ${offset}`); - } -} - -function readUInt16(buffer, offset, label) { - checkedRead(buffer, offset, 2, label); - return buffer.readUInt16LE(offset); -} - -function readUInt32(buffer, offset, label) { - checkedRead(buffer, offset, 4, label); - return buffer.readUInt32LE(offset); -} - -function readAsciiString(buffer, offset) { - checkedRead(buffer, offset, 1, "string"); - const end = buffer.indexOf(0, offset); - if (end < 0) { - throw new Error(`Unterminated PE import name at offset ${offset}`); - } - return buffer.toString("ascii", offset, end); -} - -export function readPeImportNames(buffer) { - if (buffer.length < 64 || buffer.toString("ascii", 0, 2) !== "MZ") { - throw new Error("File is not a PE executable"); - } - - const peOffset = readUInt32(buffer, 0x3c, "header"); - checkedRead(buffer, peOffset, 24, "signature"); - if (buffer.toString("ascii", peOffset, peOffset + 4) !== "PE\u0000\u0000") { - throw new Error("Invalid PE signature"); - } - - const sectionCount = readUInt16(buffer, peOffset + 6, "section count"); - const optionalHeaderSize = readUInt16(buffer, peOffset + 20, "optional header size"); - const optionalHeaderOffset = peOffset + 24; - const optionalHeaderMagic = readUInt16(buffer, optionalHeaderOffset, "optional header"); - const dataDirectoryOffset = optionalHeaderOffset + ( - optionalHeaderMagic === 0x10b ? 96 : optionalHeaderMagic === 0x20b ? 112 : 0 - ); - if (dataDirectoryOffset === optionalHeaderOffset) { - throw new Error(`Unsupported PE optional header: 0x${optionalHeaderMagic.toString(16)}`); - } - - checkedRead(buffer, optionalHeaderOffset, optionalHeaderSize, "optional header"); - const importTableRva = readUInt32(buffer, dataDirectoryOffset + 8, "import table"); - if (importTableRva === 0) { - return []; - } - - const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize; - const sections = []; - for (let index = 0; index < sectionCount; index += 1) { - const offset = sectionTableOffset + index * 40; - checkedRead(buffer, offset, 40, "section"); - sections.push({ - virtualSize: readUInt32(buffer, offset + 8, "section virtual size"), - virtualAddress: readUInt32(buffer, offset + 12, "section virtual address"), - rawSize: readUInt32(buffer, offset + 16, "section raw size"), - rawOffset: readUInt32(buffer, offset + 20, "section raw offset"), - }); - } - - const rvaToOffset = (rva) => { - const section = sections.find(({ virtualAddress, virtualSize, rawSize }) => - rva >= virtualAddress && rva < virtualAddress + Math.max(virtualSize, rawSize), - ); - if (!section) { - throw new Error(`PE import RVA 0x${rva.toString(16)} is outside every section`); - } - const offset = section.rawOffset + rva - section.virtualAddress; - checkedRead(buffer, offset, 1, "import"); - return offset; - }; - - const imports = []; - let descriptorOffset = rvaToOffset(importTableRva); - for (;;) { - checkedRead(buffer, descriptorOffset, 20, "import descriptor"); - const fields = Array.from({ length: 5 }, (_, index) => - readUInt32(buffer, descriptorOffset + index * 4, "import descriptor"), - ); - if (fields.every((value) => value === 0)) { - break; - } - imports.push(readAsciiString(buffer, rvaToOffset(fields[3]))); - descriptorOffset += 20; - } - return imports; -} - -export function collectBundledPeDependencies(binary, dependencyDirectory) { - const available = new Map( - readdirSync(dependencyDirectory) - .filter((name) => name.toLowerCase().endsWith(".dll")) - .map((name) => [name.toLowerCase(), join(dependencyDirectory, name)]), - ); - const dependencies = new Map(); - const pending = [binary]; - - while (pending.length > 0) { - const current = pending.pop(); - for (const importedName of readPeImportNames(readFileSync(current))) { - const normalizedName = importedName.toLowerCase(); - const dependency = available.get(normalizedName); - if (!dependency || dependencies.has(normalizedName)) { - continue; - } - dependencies.set(normalizedName, dependency); - pending.push(dependency); - } - } - - return [...dependencies.values()].sort((left, right) => - basename(left).localeCompare(basename(right), "en", { sensitivity: "base" }), - ); -} diff --git a/opennow-stable/scripts/windows-pe-imports.test.mjs b/opennow-stable/scripts/windows-pe-imports.test.mjs deleted file mode 100644 index c5e1a7ff0..000000000 --- a/opennow-stable/scripts/windows-pe-imports.test.mjs +++ /dev/null @@ -1,71 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import test from "node:test"; - -import { collectBundledPeDependencies, readPeImportNames } from "./windows-pe-imports.mjs"; - -function peFixture(importNames) { - const buffer = Buffer.alloc(0x800); - const peOffset = 0x80; - const optionalHeaderOffset = peOffset + 24; - const sectionTableOffset = optionalHeaderOffset + 0xf0; - const sectionRva = 0x1000; - const sectionOffset = 0x200; - const importTableOffset = sectionOffset; - - buffer.write("MZ", 0, "ascii"); - buffer.writeUInt32LE(peOffset, 0x3c); - buffer.write("PE\u0000\u0000", peOffset, "ascii"); - buffer.writeUInt16LE(0x8664, peOffset + 4); - buffer.writeUInt16LE(1, peOffset + 6); - buffer.writeUInt16LE(0xf0, peOffset + 20); - buffer.writeUInt16LE(0x20b, optionalHeaderOffset); - buffer.writeUInt32LE(sectionRva, optionalHeaderOffset + 112 + 8); - buffer.write(".rdata\u0000\u0000", sectionTableOffset, "ascii"); - buffer.writeUInt32LE(0x600, sectionTableOffset + 8); - buffer.writeUInt32LE(sectionRva, sectionTableOffset + 12); - buffer.writeUInt32LE(0x600, sectionTableOffset + 16); - buffer.writeUInt32LE(sectionOffset, sectionTableOffset + 20); - - let nameOffset = importTableOffset + (importNames.length + 1) * 20; - importNames.forEach((name, index) => { - buffer.writeUInt32LE(sectionRva + nameOffset - sectionOffset, importTableOffset + index * 20 + 12); - buffer.write(`${name}\u0000`, nameOffset, "ascii"); - nameOffset += Buffer.byteLength(name) + 1; - }); - return buffer; -} - -test("reads imported DLL names from a 64-bit PE image", () => { - assert.deepEqual( - readPeImportNames(peFixture(["gstvideo-1.0-0.dll", "KERNEL32.dll"])), - ["gstvideo-1.0-0.dll", "KERNEL32.dll"], - ); -}); - -test("collects only the recursive DLL closure available in the runtime", async () => { - const root = await mkdtemp(join(tmpdir(), "opennow-pe-imports-")); - const runtime = join(root, "runtime"); - const binary = join(root, "opennow-streamer.exe"); - - try { - await mkdir(runtime); - await writeFile(binary, peFixture(["gstvideo-1.0-0.dll", "KERNEL32.dll"])); - await writeFile( - join(runtime, "gstvideo-1.0-0.dll"), - peFixture(["gstreamer-1.0-0.dll", "ffi-7.dll"]), - ); - await writeFile(join(runtime, "gstreamer-1.0-0.dll"), peFixture(["glib-2.0-0.dll"])); - await writeFile(join(runtime, "glib-2.0-0.dll"), peFixture([])); - await writeFile(join(runtime, "unused.dll"), peFixture([])); - - assert.deepEqual( - collectBundledPeDependencies(binary, runtime).map((path) => path.slice(runtime.length + 1)), - ["glib-2.0-0.dll", "gstreamer-1.0-0.dll", "gstvideo-1.0-0.dll"], - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); diff --git a/opennow-stable/src/main/escapeFullscreenGuard.ts b/opennow-stable/src/main/escapeFullscreenGuard.ts index ae704b29b..a37f7d9cf 100644 --- a/opennow-stable/src/main/escapeFullscreenGuard.ts +++ b/opennow-stable/src/main/escapeFullscreenGuard.ts @@ -1,5 +1,5 @@ export const POINTER_LOCK_ESCAPE_FULLSCREEN_GRACE_MS = 1000; -/** Match native Internal Escape-hold timing (gstreamer_platform.rs). */ +/** Match native presenter's Escape-hold timing. */ export const ESCAPE_HOLD_TO_EXIT_FULLSCREEN_MS = 1500; export interface EscapeKeyInput { diff --git a/opennow-stable/src/main/index.ts b/opennow-stable/src/main/index.ts index 7ef82389b..fa9671bc5 100644 --- a/opennow-stable/src/main/index.ts +++ b/opennow-stable/src/main/index.ts @@ -81,7 +81,6 @@ import { getReleaseHighlightsPayload, shouldShowReleaseHighlights } from "./rele import { shutdownMainTelemetry, syncMainTelemetry } from "./telemetry/posthog"; import { createMainWindow } from "./window/mainWindow"; import { resolveAppInstanceProfile } from "./appInstance"; -import { shouldDefaultLinuxShellToX11 } from "./nativeStreamer/runtime"; import { DiagnosticHistoryController, DiagnosticHistoryStore, @@ -138,16 +137,6 @@ console.log( `[Main] Video acceleration preference: decode=${bootstrapChromiumPrefs.decoderPreference}, encode=${bootstrapChromiumPrefs.encoderPreference}`, ); -const explicitLinuxOzonePlatform = app.commandLine.getSwitchValue("ozone-platform") - || app.commandLine.getSwitchValue("ozone-platform-hint") - || process.env.ELECTRON_OZONE_PLATFORM_HINT; -if (shouldDefaultLinuxShellToX11(process.platform, explicitLinuxOzonePlatform)) { - app.commandLine.appendSwitch("ozone-platform", "x11"); - console.log( - "[Main] Linux display backend: X11/XWayland (required for embedded native GStreamer video).", - ); -} - const chromiumCommandLine = buildChromiumCommandLine( bootstrapChromiumPrefs, process.platform, diff --git a/opennow-stable/src/main/ipc/sessionHandlers.ts b/opennow-stable/src/main/ipc/sessionHandlers.ts index 2faf435d6..600c68e86 100644 --- a/opennow-stable/src/main/ipc/sessionHandlers.ts +++ b/opennow-stable/src/main/ipc/sessionHandlers.ts @@ -78,12 +78,12 @@ export function registerSessionIpcHandlers(deps: SessionIpcHandlerDeps): void { const streamingBaseUrl = payload.streamingBaseUrl ?? authService.getSelectedProvider().streamingServiceUrl; - const forceNewSession = shouldForceNewSession( - payload.existingSessionStrategy, - ); const resolvedSettings = await resolveSessionCloudGsyncSettings( payload.settings, ); + const forceNewSession = + shouldForceNewSession(payload.existingSessionStrategy) || + resolvedSettings.transportMode === "nvst"; const resolvedPayload: SessionCreateRequest = { ...payload, settings: resolvedSettings, diff --git a/opennow-stable/src/main/nativeStreamer/capabilities.test.ts b/opennow-stable/src/main/nativeStreamer/capabilities.test.ts index 19cea38a7..672892546 100644 --- a/opennow-stable/src/main/nativeStreamer/capabilities.test.ts +++ b/opennow-stable/src/main/nativeStreamer/capabilities.test.ts @@ -53,16 +53,18 @@ test("capability selection skips unavailable preferred and platform backends", ( test("status formatting reports selected video path and codec summary", () => { const status = createNativeStreamerStatus({ protocolVersion: 4, - backend: "gstreamer", + backend: "native", supportsOfferAnswer: true, supportsRemoteIce: true, supportsLocalIce: true, supportsInput: true, + supportsVideoDecode: true, + supportsVideoPresent: true, videoBackends: [backend("vaapi", "linux")], }, { - source: "system", - bundled: false, - message: "System runtime", + source: "self-contained", + selfContained: true, + message: "Self-contained runtime", }, "auto", "linux"); assert.equal(status.activeVideoBackend?.backend, "vaapi"); diff --git a/opennow-stable/src/main/nativeStreamer/capabilities.ts b/opennow-stable/src/main/nativeStreamer/capabilities.ts index b1324b3fe..9fcfe18a8 100644 --- a/opennow-stable/src/main/nativeStreamer/capabilities.ts +++ b/opennow-stable/src/main/nativeStreamer/capabilities.ts @@ -1,11 +1,10 @@ import type { - NativeGstreamerRuntimeStatus, + NativeStreamerRuntimeStatus, NativeStreamerStatus, NativeVideoBackendCapability, NativeVideoBackendPreference, } from "@shared/gfn"; import type { NativeStreamerCapabilities } from "@shared/nativeStreamer"; -import { linuxInstallInstructions } from "./runtime"; function formatVideoBackendName(backend: string | undefined): string { switch (backend) { @@ -86,63 +85,40 @@ export function formatError(error: unknown): string { return error instanceof Error ? error.message : String(error); } -function isWindowsDllLoadFailure(error: unknown, platform: NodeJS.Platform): boolean { - const message = formatError(error); - return platform === "win32" - && (message.includes("3221225781") || message.toLowerCase().includes("0xc0000135")); -} - function formatNativeStreamerDetectionFailure( error: unknown, - runtime: NativeGstreamerRuntimeStatus | null, - platform: NodeJS.Platform, + runtime: NativeStreamerRuntimeStatus | null, ): string { - if (isWindowsDllLoadFailure(error, platform)) { - return runtime?.bundled - ? `Native streamer could not load a required DLL even though bundled GStreamer was detected at ${runtime.path}. The packaged runtime may be incomplete or blocked. ${formatError(error)}` - : `Native streamer could not load a required DLL and no bundled GStreamer runtime was detected. ${formatError(error)}`; + const message = formatError(error); + if (message.includes("3221225781") || message.toLowerCase().includes("0xc0000135")) { + const location = runtime?.path ? ` at ${runtime.path}` : ""; + return `Native streamer could not load a required library${location}. The executable may be incomplete or blocked. ${message}`; } - return `Native streamer was not detected: ${formatError(error)}`; + return `Native streamer was not detected: ${message}`; } export function createNativeStreamerStatus( capabilities: NativeStreamerCapabilities | null, - runtimeStatus: NativeGstreamerRuntimeStatus | null, + runtimeStatus: NativeStreamerRuntimeStatus | null, preferredBackend: NativeVideoBackendPreference, platform = process.platform, ): NativeStreamerStatus { const backend = capabilities?.backend; - const gstreamerAvailable = backend === "gstreamer" && capabilities?.supportsOfferAnswer === true; + const available = backend === "native" + && capabilities?.supportsOfferAnswer === true + && capabilities?.supportsVideoDecode === true + && capabilities?.supportsVideoPresent === true; const videoBackends = capabilities?.videoBackends ?? []; const activeVideoBackend = resolveActiveVideoBackend(videoBackends, preferredBackend, platform); const runtime = runtimeStatus ?? { source: "unknown", - bundled: false, - message: "GStreamer runtime has not been checked yet.", - installInstructions: linuxInstallInstructions(platform), - } satisfies NativeGstreamerRuntimeStatus; - const effectiveRuntime: NativeGstreamerRuntimeStatus = gstreamerAvailable - ? runtime.bundled - ? runtime - : { - ...runtime, - source: "system", - message: "Using system GStreamer runtime; packaged Windows/macOS builds should use the bundled runtime.", - } - : { - ...runtime, - source: runtime.bundled ? "bundled" : platform === "linux" ? "missing" : runtime.source, - message: runtime.bundled - ? "Bundled GStreamer runtime was found, but the GStreamer backend is not ready." - : platform === "linux" - ? "GStreamer is not ready. Install distro GStreamer packages so plugins match the host GPU/driver stack." - : runtime.message, - installInstructions: runtime.installInstructions ?? linuxInstallInstructions(platform), - }; + selfContained: false, + message: "Native streamer runtime has not been checked yet.", + } satisfies NativeStreamerRuntimeStatus; return { detected: true, - gstreamerAvailable, + available, supportsOfferAnswer: capabilities?.supportsOfferAnswer === true, backend, fallbackReason: capabilities?.fallbackReason, @@ -150,31 +126,28 @@ export function createNativeStreamerStatus( activeVideoBackend, codecSummary: summarizeCodecs(activeVideoBackend), zeroCopySummary: summarizeZeroCopy(activeVideoBackend), - gstreamerRuntime: effectiveRuntime, - message: gstreamerAvailable - ? `${effectiveRuntime.message} Video path: ${formatVideoBackendName(activeVideoBackend?.backend)}.` - : capabilities?.fallbackReason ?? effectiveRuntime.message, + runtime, + message: available + ? `${runtime.message} Video path: ${formatVideoBackendName(activeVideoBackend?.backend)}.` + : capabilities?.fallbackReason ?? runtime.message, }; } export function createNativeStreamerDetectionFailureStatus( error: unknown, - runtimeStatus: NativeGstreamerRuntimeStatus | null, - platform = process.platform, + runtimeStatus: NativeStreamerRuntimeStatus | null, + _platform = process.platform, ): NativeStreamerStatus { const runtime = runtimeStatus ?? { - source: platform === "linux" ? "missing" : "unknown", - bundled: false, - message: platform === "linux" - ? "GStreamer is not ready. Linux uses distro packages because private AppImage GStreamer bundling is unreliable across glibc, libdrm/VAAPI/Vulkan, and GPU driver stacks." - : "GStreamer runtime could not be checked because the native streamer did not start.", - installInstructions: linuxInstallInstructions(platform), - } satisfies NativeGstreamerRuntimeStatus; + source: "unknown", + selfContained: false, + message: "Native streamer runtime could not be checked because the executable did not start.", + } satisfies NativeStreamerRuntimeStatus; return { detected: false, - gstreamerAvailable: false, + available: false, supportsOfferAnswer: false, - gstreamerRuntime: runtime, - message: formatNativeStreamerDetectionFailure(error, runtime, platform), + runtime, + message: formatNativeStreamerDetectionFailure(error, runtime), }; } diff --git a/opennow-stable/src/main/nativeStreamer/executableDiscovery.test.ts b/opennow-stable/src/main/nativeStreamer/executableDiscovery.test.ts new file mode 100644 index 000000000..e3f3b64e6 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/executableDiscovery.test.ts @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { resolveNativeStreamerExecutableCandidates } from "./executableDiscovery"; + +function options(root: string, configuredPath = "") { + return { + platform: "linux" as const, + arch: "x64", + resourcesPath: join(root, "resources"), + appPath: join(root, "app"), + mainDir: join(root, "app", "out", "main"), + envExecutablePath: undefined, + getConfiguredPath: () => configuredPath, + }; +} + +test("discovers the packaged self-contained executable", () => { + const root = mkdtempSync(join(tmpdir(), "opennow-native-discovery-")); + try { + const executable = join(root, "resources", "native", "opennow-streamer", "linux-x64", "opennow-streamer"); + mkdirSync(join(executable, ".."), { recursive: true }); + writeFileSync(executable, "native"); + chmodSync(executable, 0o755); + + assert.deepEqual(resolveNativeStreamerExecutableCandidates(options(root)), [executable]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("rejects a configured executable path that does not exist", () => { + const root = mkdtempSync(join(tmpdir(), "opennow-native-discovery-")); + try { + const missing = join(root, "missing-streamer"); + assert.throws( + () => resolveNativeStreamerExecutableCandidates(options(root, missing)), + /Configured native streamer executable was not found/, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts b/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts index 32987f8b5..c42f4421c 100644 --- a/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts +++ b/opennow-stable/src/main/nativeStreamer/executableDiscovery.ts @@ -1,16 +1,10 @@ import { join, resolve } from "node:path"; import { - hasBundledRuntimeNextToExecutable, isExistingFile, - isPathInside, nativeStreamerExecutableName, nativeStreamerPlatformKey, } from "./runtime"; -import { - materializePackagedNativeStreamerCache, - type PackagedNativeStreamerCacheContext, -} from "./runtimeCache"; export interface NativeStreamerExecutableDiscoveryOptions { platform: NodeJS.Platform; @@ -18,31 +12,8 @@ export interface NativeStreamerExecutableDiscoveryOptions { resourcesPath: string; appPath: string; mainDir: string; - isPackaged: boolean; envExecutablePath: string | undefined; getConfiguredPath(): string; - cacheContext: PackagedNativeStreamerCacheContext; -} - -export function shouldIgnorePackagedExecutableOverride( - configuredPath: string, - options: Pick< - NativeStreamerExecutableDiscoveryOptions, - "resourcesPath" | "appPath" | "mainDir" | "platform" - >, -): boolean { - if (hasBundledRuntimeNextToExecutable(configuredPath)) { - return false; - } - - const packagedRoots = [ - join(options.resourcesPath, "native", "opennow-streamer"), - resolve(options.appPath, "../native/opennow-streamer"), - resolve(options.mainDir, "../../../dist-release/win-unpacked/resources/native/opennow-streamer"), - resolve(options.mainDir, "../../../dist-release/win-unpacked/resources/app.asar.unpacked/native/opennow-streamer"), - ]; - - return packagedRoots.some((root) => isPathInside(root, configuredPath, options.platform)); } export function resolveNativeStreamerExecutableCandidates( @@ -50,83 +21,35 @@ export function resolveNativeStreamerExecutableCandidates( ): string[] { const exeName = nativeStreamerExecutableName(options.platform); const platformKey = nativeStreamerPlatformKey(options.platform, options.arch); - const bundledCandidates = [ - join(options.resourcesPath, "native", "opennow-streamer", platformKey, exeName), - join(options.resourcesPath, "native", "opennow-streamer", exeName), - ]; - const candidates: string[] = []; - const addCandidate = (candidate: string | undefined): void => { - if (!candidate || !isExistingFile(candidate) || candidates.includes(candidate)) { - return; - } - candidates.push(candidate); - }; - - if (options.isPackaged) { - for (const candidate of bundledCandidates) { - if (!isExistingFile(candidate) || !hasBundledRuntimeNextToExecutable(candidate)) { - continue; - } - addCandidate( - materializePackagedNativeStreamerCache( - candidate, - platformKey, - exeName, - options.cacheContext, - ) ?? undefined, - ); - } - } - bundledCandidates.forEach(addCandidate); - if (options.isPackaged && candidates.length > 0) { - const packagedBundledCandidates = candidates.filter((candidate) => - hasBundledRuntimeNextToExecutable(candidate), - ); - return packagedBundledCandidates.length > 0 ? packagedBundledCandidates : candidates; - } - const configuredPath = options.getConfiguredPath().trim(); - if (configuredPath) { - if (isExistingFile(configuredPath)) { - if (!shouldIgnorePackagedExecutableOverride(configuredPath, options)) { - addCandidate(configuredPath); - } else { - console.warn( - "[NativeStreamer] Ignoring packaged executable override without bundled runtime:", - configuredPath, - ); - } - } else { - throw new Error(`Configured native streamer executable was not found: ${configuredPath}`); - } + if (configuredPath && !isExistingFile(configuredPath)) { + throw new Error(`Configured native streamer executable was not found: ${configuredPath}`); } - - [ + // On macOS the streamer must run from inside an .app bundle: a bundle-less process is + // refused window compositing by the WindowServer, so the video overlay never appears. + const bundledMacBinary = + options.platform === "darwin" + ? resolve( + options.mainDir, + "../../../native/opennow-streamer/bin", + platformKey, + "OpenNOWStreamer.app/Contents/MacOS", + exeName, + ) + : undefined; + const checked = [ + configuredPath || undefined, options.envExecutablePath, - ...bundledCandidates, + bundledMacBinary, + join(options.resourcesPath, "native", "opennow-streamer", platformKey, exeName), resolve(options.mainDir, "../../../native/opennow-streamer/bin", platformKey, exeName), - resolve(options.mainDir, "../../../native/opennow-streamer/bin", exeName), - resolve(options.mainDir, "../../../native/opennow-streamer/dist", platformKey, exeName), - resolve(options.mainDir, "../../../native/opennow-streamer/dist", exeName), - resolve(options.mainDir, "../../../native/opennow-streamer/target/release", platformKey, exeName), resolve(options.mainDir, "../../../native/opennow-streamer/target/release", exeName), - resolve(options.mainDir, "../../../native/opennow-streamer/target/debug", platformKey, exeName), resolve(options.mainDir, "../../../native/opennow-streamer/target/debug", exeName), resolve(options.appPath, "../native/opennow-streamer/bin", platformKey, exeName), - resolve(options.appPath, "../native/opennow-streamer/bin", exeName), - resolve(options.appPath, "../native/opennow-streamer/dist", platformKey, exeName), - resolve(options.appPath, "../native/opennow-streamer/dist", exeName), - resolve(options.appPath, "../native/opennow-streamer/target/release", platformKey, exeName), - resolve(options.appPath, "../native/opennow-streamer/target/release", exeName), - resolve(options.appPath, "../native/opennow-streamer/target/debug", platformKey, exeName), - resolve(options.appPath, "../native/opennow-streamer/target/debug", exeName), - ] - .filter((candidate): candidate is string => Boolean(candidate)) - .forEach(addCandidate); - - if (candidates.length > 0) { - return candidates; - } - - throw new Error(`Native streamer binary not found. Checked: ${candidates.join(", ")}`); + ].filter((candidate): candidate is string => Boolean(candidate)); + const candidates = checked.filter((candidate, index) => + isExistingFile(candidate) && checked.indexOf(candidate) === index, + ); + if (candidates.length > 0) return candidates; + throw new Error(`Native streamer binary not found. Checked: ${checked.join(", ")}`); } diff --git a/opennow-stable/src/main/nativeStreamer/manager.test.ts b/opennow-stable/src/main/nativeStreamer/manager.test.ts index 24810889c..9e1fe253e 100644 --- a/opennow-stable/src/main/nativeStreamer/manager.test.ts +++ b/opennow-stable/src/main/nativeStreamer/manager.test.ts @@ -37,6 +37,7 @@ interface FakeChild { interface ManagerInternals { child: ChildProcessWithoutNullStreams | null; + stdoutBuffer: string; activeSessionId: string | null; capabilities: NativeStreamerCapabilities | null; pending: Map; @@ -45,9 +46,23 @@ interface ManagerInternals { timeoutMs: number, ): Promise; installStdinErrorHandler(child: ChildProcessWithoutNullStreams): void; + handleStdout(child: ChildProcessWithoutNullStreams, chunk: string): void; handleEvent(message: NativeStreamerEvent): void; } +test("stdout from a replaced native process cannot affect the current session", () => { + const { internals } = createManager(); + const current = createFakeChild(); + const stale = createFakeChild(); + internals.child = current.child; + + internals.handleStdout(stale.child, "stale partial output"); + assert.equal(internals.stdoutBuffer, ""); + + internals.handleStdout(current.child, "current partial output"); + assert.equal(internals.stdoutBuffer, "current partial output"); +}); + function createFakeChild(): FakeChild { const stdin = new FakeStdin(); let killed = false; @@ -74,7 +89,6 @@ function createManager(): { } { const manager = new NativeStreamerManager({ mainDir: "", - getBackendPreference: () => "auto", getVideoBackendPreference: () => "auto", getExecutablePathOverride: () => "", getCloudGsyncMode: () => "auto", @@ -98,7 +112,6 @@ test("Linux decoder startup timeout requests one native software retry", () => { let recoveryMessage = ""; const manager = new NativeStreamerManager({ mainDir: "", - getBackendPreference: () => "auto", getVideoBackendPreference: () => "nvdec", getExecutablePathOverride: () => "", getCloudGsyncMode: () => "auto", @@ -175,11 +188,13 @@ test("input writes tolerate a child exit race but still throw unrelated failures internals.activeSessionId = "session"; internals.capabilities = { protocolVersion: 4, - backend: "gstreamer", + backend: "native", supportsOfferAnswer: true, supportsRemoteIce: true, supportsLocalIce: true, supportsInput: true, + supportsVideoDecode: true, + supportsVideoPresent: true, }; fake.stdin.writeImpl = () => { throw writeError("ERR_STREAM_DESTROYED"); @@ -200,11 +215,13 @@ test("input writes tolerate a child exit race but still throw unrelated failures internals.activeSessionId = "session"; internals.capabilities = { protocolVersion: 4, - backend: "gstreamer", + backend: "native", supportsOfferAnswer: true, supportsRemoteIce: true, supportsLocalIce: true, supportsInput: true, + supportsVideoDecode: true, + supportsVideoPresent: true, }; assert.throws( diff --git a/opennow-stable/src/main/nativeStreamer/manager.ts b/opennow-stable/src/main/nativeStreamer/manager.ts index 887bae3c8..62f7729ac 100644 --- a/opennow-stable/src/main/nativeStreamer/manager.ts +++ b/opennow-stable/src/main/nativeStreamer/manager.ts @@ -1,6 +1,5 @@ import electron from "electron"; import { randomUUID } from "node:crypto"; -import { tmpdir } from "node:os"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { basename } from "node:path"; @@ -12,11 +11,10 @@ import { type IceCandidatePayload, type KeyframeRequest, type MainToRendererSignalingEvent, - type NativeStreamerBackendPreference, type NativeStreamerFeatureMode, type NativeVideoBackendPreference, type NativeStreamerStatus, - type NativeGstreamerRuntimeStatus, + type NativeStreamerRuntimeStatus, type NativeRenderSurface, type NativeStreamerSessionContext, type SendAnswerRequest, @@ -59,7 +57,6 @@ interface NativeStreamerCallbacks { interface NativeStreamerManagerOptions extends NativeStreamerCallbacks { mainDir: string; - getBackendPreference(): NativeStreamerBackendPreference; getVideoBackendPreference(): NativeVideoBackendPreference; getExecutablePathOverride(): string; getCloudGsyncMode(): NativeStreamerFeatureMode; @@ -74,7 +71,7 @@ interface PendingRequest { } const HELLO_TIMEOUT_MS = 10000; -const BUNDLED_GSTREAMER_HELLO_TIMEOUT_MS = process.platform === "win32" ? 120000 : 30000; +const BUNDLED_NATIVE_HELLO_TIMEOUT_MS = process.platform === "win32" ? 120000 : 30000; const CONTROL_TIMEOUT_MS = 8000; const SESSION_START_TIMEOUT_MS = process.platform === "win32" ? 90000 : 45000; const SURFACE_UPDATE_TIMEOUT_MS = 15000; @@ -104,10 +101,11 @@ export class NativeStreamerManager { private startupPromise: Promise | null = null; private stdoutBuffer = ""; private stderrTail: string[] = []; - private gstreamerRuntime: NativeGstreamerRuntimeStatus | null = null; + private runtimeStatus: NativeStreamerRuntimeStatus | null = null; private pending = new Map(); private capabilities: NativeStreamerCapabilities | null = null; private activeSessionId: string | null = null; + private activeTransport: "webrtc" | "nvst" | null = null; private inputBackpressureWarned = false; private answerInFlight = false; private queuedLocalIce: IceCandidatePayload[] = []; @@ -136,6 +134,10 @@ export class NativeStreamerManager { return this.activeSessionId !== null; } + isNvstSessionActive(sessionId: string): boolean { + return this.activeSessionId === sessionId && this.activeTransport === "nvst"; + } + private retainDiagnosticState(values: Record): void { this.diagnosticState = { ...this.diagnosticState, @@ -148,6 +150,56 @@ export class NativeStreamerManager { this.videoBackendOverride = value; } + async reserveNvstUdp(): Promise<{ + port: number; + mjolnirPort?: number; + localAddress?: string; + iceUsernameFragment?: string; + icePassword?: string; + dtlsFingerprint?: string; + send(payload: Buffer, host: string, port: number): Promise; + release(): Promise; + }> { + await this.ensureProcess(); + const response = await this.request({ type: "nvst-bind" }, CONTROL_TIMEOUT_MS); + if (response.type !== "nvst-bound" || !Number.isInteger(response.port) || response.port <= 0) { + throw new Error("Native streamer did not reserve an NVST UDP socket."); + } + const port = response.port; + const mjolnirPort = Number.isInteger(response.mjolnirPort) && (response.mjolnirPort ?? 0) > 0 + ? response.mjolnirPort + : undefined; + const localAddress = typeof response.localAddress === "string" && response.localAddress.length > 0 + ? response.localAddress + : undefined; + console.log( + `[NativeStreamer] Reserved NVST video UDP on port ${port} before RTSP ANNOUNCE` + + `${mjolnirPort ? ` mjolnirPort=${mjolnirPort}` : ""}` + + `${localAddress ? ` localAddress=${localAddress}` : ""}` + + `${response.dtlsFingerprint ? ` (dtlsFingerprintBytes=${response.dtlsFingerprint.length})` : ""}`, + ); + return { + port, + mjolnirPort, + localAddress, + iceUsernameFragment: response.iceUsernameFragment, + icePassword: response.icePassword, + dtlsFingerprint: response.dtlsFingerprint, + send: async (payload, host, peerPort) => { + const sent = await this.request({ + type: "nvst-send", + host, + port: peerPort, + payloadBase64: payload.toString("base64"), + }, CONTROL_TIMEOUT_MS); + if (sent.type !== "ok") { + throw new Error(`Native streamer returned ${sent.type} instead of ok for nvst-send.`); + } + }, + release: async () => undefined, + }; + } + async prepareForSession(context: NativeStreamerSessionContext): Promise { if (this.activeSessionId && this.activeSessionId !== context.session.sessionId) { await this.stop("new native streamer session"); @@ -174,11 +226,15 @@ export class NativeStreamerManager { ); } - await this.request({ + const response = await this.request({ type: "start", context, }, SESSION_START_TIMEOUT_MS); + if (response.type !== "ok") { + throw new Error(`Native streamer returned ${response.type} instead of ok.`); + } this.activeSessionId = context.session.sessionId; + this.activeTransport = response.transport === "nvst" ? "nvst" : "webrtc"; this.retainDiagnosticState({ sessionState: "ready" }); await this.flushQueuedRemoteIce(context.session.sessionId); } @@ -209,8 +265,8 @@ export class NativeStreamerManager { }); if (!this.capabilities?.supportsOfferAnswer) { - console.warn( - `[NativeStreamer] Backend "${this.capabilities?.backend ?? "unknown"}" reports offer/answer is not ready; forwarding offer for validation/fallback.`, + throw new Error( + `Native streamer backend "${this.capabilities?.backend ?? "unknown"}" does not support offer/answer.`, ); } @@ -261,14 +317,14 @@ export class NativeStreamerManager { await this.ensureProcess(); return createNativeStreamerStatus( this.capabilities, - this.gstreamerRuntime, + this.runtimeStatus, this.options.getVideoBackendPreference(), process.platform, ); } catch (error) { return createNativeStreamerDetectionFailureStatus( error, - this.gstreamerRuntime, + this.runtimeStatus, process.platform, ); } @@ -276,6 +332,9 @@ export class NativeStreamerManager { async addRemoteIce(candidate: IceCandidatePayload, context: NativeStreamerSessionContext): Promise { const sessionId = context.session.sessionId; + if (this.capabilities && !this.capabilities.supportsRemoteIce) { + return; + } if (!this.child || this.activeSessionId !== sessionId) { this.queueRemoteIce(sessionId, candidate); return; @@ -412,6 +471,7 @@ export class NativeStreamerManager { stopReason: reason, }); this.activeSessionId = null; + this.activeTransport = null; this.capabilities = null; this.surfaceUpdates.markNotReady(); this.clearQueuedRemoteIce(); @@ -448,6 +508,7 @@ export class NativeStreamerManager { stopReason: reason, }); this.activeSessionId = null; + this.activeTransport = null; this.capabilities = null; this.surfaceUpdates.markNotReady(); this.clearQueuedRemoteIce(); @@ -478,7 +539,6 @@ export class NativeStreamerManager { } const startupPromise = (async () => { - const backendPreference = this.options.getBackendPreference(); let lastError: Error | null = null; for (const executablePath of resolveNativeStreamerExecutableCandidates({ @@ -487,20 +547,11 @@ export class NativeStreamerManager { resourcesPath: process.resourcesPath, appPath: app.getAppPath(), mainDir: this.options.mainDir, - isPackaged: app.isPackaged, envExecutablePath: process.env.OPENNOW_NATIVE_STREAMER, getConfiguredPath: () => this.options.getExecutablePathOverride(), - cacheContext: { - appVersion: app.getVersion(), - isPackaged: app.isPackaged, - platform: process.platform, - resourcesPath: process.resourcesPath, - tempDirectory: tmpdir(), - userDataPath: app.getPath("userData"), - }, })) { try { - await this.startProcess(executablePath, backendPreference); + await this.startProcess(executablePath); return; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); @@ -533,17 +584,12 @@ export class NativeStreamerManager { } } - private async startProcess( - executablePath: string, - backendPreference: NativeStreamerBackendPreference, - ): Promise { + private async startProcess(executablePath: string): Promise { this.retainDiagnosticState({ processState: "starting", executable: basename(executablePath), - backendPreference, }); console.log("[NativeStreamer] Starting:", executablePath); - console.log("[NativeStreamer] Backend preference:", backendPreference); const videoBackendPreference = this.videoBackendOverride ?? this.options.getVideoBackendPreference(); this.retainDiagnosticState({ videoBackendPreference }); @@ -556,7 +602,6 @@ export class NativeStreamerManager { arch: process.arch, userDataPath: app.getPath("userData"), protocolVersion: NATIVE_STREAMER_PROTOCOL_VERSION, - backendPreference, videoBackendPreference, externalRendererEnabled: process.platform === "win32" ? this.options.getExternalRendererEnabled() @@ -566,22 +611,16 @@ export class NativeStreamerManager { cloudGsyncMode: this.options.getCloudGsyncMode(), d3dFullscreenMode: this.options.getD3dFullscreenMode(), }); - this.gstreamerRuntime = runtimeStatus; + this.runtimeStatus = runtimeStatus; this.retainDiagnosticState({ - runtimeBundled: runtimeStatus.bundled, + runtimeSelfContained: runtimeStatus.selfContained, runtimeState: runtimeStatus.message, }); - if (runtimeStatus.bundled) { - console.log("[NativeStreamer] Using bundled GStreamer runtime:", runtimeStatus.path); - } else { - console.log("[NativeStreamer]", runtimeStatus.message); - } + console.log("[NativeStreamer]", runtimeStatus.message, runtimeStatus.path); const child = spawn(executablePath, [], { stdio: "pipe", - // The default native path lets the GStreamer video sink create its own - // render window. Hiding the child process also hides that sink window on - // Windows, which leaves the Electron input placeholder black. + // The native presenter may own a top-level window on Windows. windowsHide: false, env: childEnv, }); @@ -592,9 +631,10 @@ export class NativeStreamerManager { this.inputBackpressureWarned = false; child.stdout.setEncoding("utf8"); - child.stdout.on("data", (chunk: string) => this.handleStdout(chunk)); + child.stdout.on("data", (chunk: string) => this.handleStdout(child, chunk)); child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk: string) => { + if (this.child !== child) return; for (const line of chunk.split(/\r?\n/)) { if (line.trim()) { this.appendStderr(line); @@ -614,7 +654,7 @@ export class NativeStreamerManager { this.handleProcessExit(child, reason); }); - const helloTimeoutMs = runtimeStatus.bundled ? BUNDLED_GSTREAMER_HELLO_TIMEOUT_MS : HELLO_TIMEOUT_MS; + const helloTimeoutMs = runtimeStatus.selfContained ? BUNDLED_NATIVE_HELLO_TIMEOUT_MS : HELLO_TIMEOUT_MS; const response = await this.request({ type: "hello", protocolVersion: NATIVE_STREAMER_PROTOCOL_VERSION, @@ -638,24 +678,9 @@ export class NativeStreamerManager { `Native streamer reported protocolVersion=${response.capabilities.protocolVersion}, expected ${NATIVE_STREAMER_PROTOCOL_VERSION}.`, ); } - this.assertBackendPreference(response.capabilities, backendPreference); await this.surfaceUpdates.markReady(); } - private assertBackendPreference( - capabilities: NativeStreamerCapabilities, - backendPreference: NativeStreamerBackendPreference, - ): void { - if (backendPreference === "auto" || capabilities.backend === backendPreference) { - return; - } - - const reason = capabilities.fallbackReason ? ` ${capabilities.fallbackReason}` : ""; - throw new Error( - `Native streamer backend "${backendPreference}" is unavailable; process selected "${capabilities.backend}".${reason}`, - ); - } - private request(input: NativeStreamerCommandInput, timeoutMs: number): Promise { const child = this.child; if ( @@ -707,7 +732,8 @@ export class NativeStreamerManager { }); } - private handleStdout(chunk: string): void { + private handleStdout(child: ChildProcessWithoutNullStreams, chunk: string): void { + if (this.child !== child) return; this.stdoutBuffer += chunk; const lines = this.stdoutBuffer.split(/\r?\n/); this.stdoutBuffer = lines.pop() ?? ""; @@ -771,6 +797,10 @@ export class NativeStreamerManager { } if (message.type === "local-ice") { + if (!this.capabilities?.supportsLocalIce) { + console.warn("[NativeStreamer] Ignoring local ICE from a backend that did not advertise it."); + return; + } if (this.answerInFlight) { this.queuedLocalIce.push(message.candidate); return; @@ -972,6 +1002,7 @@ export class NativeStreamerManager { this.stdoutBuffer = ""; this.stderrTail = []; this.activeSessionId = null; + this.activeTransport = null; this.capabilities = null; this.inputBackpressureWarned = false; this.surfaceUpdates.markNotReady(); @@ -1039,6 +1070,9 @@ export class NativeStreamerManager { private async flushQueuedRemoteIce(sessionId: string): Promise { const queued = this.drainQueuedRemoteIce(sessionId); + if (!this.capabilities?.supportsRemoteIce) { + return; + } for (const candidate of queued) { await this.sendRemoteIce(candidate); } diff --git a/opennow-stable/src/main/nativeStreamer/runtime.test.ts b/opennow-stable/src/main/nativeStreamer/runtime.test.ts index bad8d2652..c714beac8 100644 --- a/opennow-stable/src/main/nativeStreamer/runtime.test.ts +++ b/opennow-stable/src/main/nativeStreamer/runtime.test.ts @@ -1,106 +1,30 @@ import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import test from "node:test"; import { createNativeStreamerRuntimeEnvironment, - isNativeWaylandSession, - isPathInside, nativeStreamerExecutableName, nativeStreamerPlatformKey, - normalizePathForComparison, - shouldDefaultLinuxShellToX11, } from "./runtime"; -function createLinuxRuntimeEnvironment( - baseEnv: NodeJS.ProcessEnv, - linuxOzonePlatform?: string, -): NodeJS.ProcessEnv { - return createNativeStreamerRuntimeEnvironment({ +test("v2 runtime is self-contained and removes inherited GStreamer variables", () => { + const result = createNativeStreamerRuntimeEnvironment({ executablePath: "/tmp/opennow-streamer", - baseEnv, + baseEnv: { GST_PLUGIN_PATH: "/old/plugins", DISPLAY: ":1" }, platform: "linux", arch: "arm64", userDataPath: "/tmp/opennow-test", protocolVersion: 4, - backendPreference: "auto", videoBackendPreference: "auto", externalRendererEnabled: false, - linuxOzonePlatform, cloudGsyncMode: "auto", d3dFullscreenMode: "auto", - }).env; -} + }); -test("detects native Wayland sessions", () => { - assert.equal(isNativeWaylandSession({ WAYLAND_DISPLAY: "wayland-0" }), true); - assert.equal(isNativeWaylandSession({ XDG_SESSION_TYPE: "wayland" }), true); - assert.equal(isNativeWaylandSession({}, "wayland"), true); -}); - -test("explicit X11 keeps Linux child-surface embedding enabled", () => { - const waylandEnvironment = { - ELECTRON_OZONE_PLATFORM_HINT: "wayland", - WAYLAND_DISPLAY: "wayland-0", - XDG_SESSION_TYPE: "wayland", - }; - - assert.equal(isNativeWaylandSession(waylandEnvironment, "x11"), false); - assert.equal(isNativeWaylandSession({ XDG_SESSION_TYPE: "x11" }), false); - assert.equal( - isNativeWaylandSession({ ELECTRON_OZONE_PLATFORM_HINT: "x11" }), - false, - ); -}); - -test("Linux native runtime rejects unmanaged pure Wayland presentation", () => { - assert.throws( - () => createLinuxRuntimeEnvironment({ WAYLAND_DISPLAY: "wayland-0" }), - /requires Electron to run through X11\/XWayland/, - ); - assert.equal( - createLinuxRuntimeEnvironment( - { WAYLAND_DISPLAY: "wayland-0" }, - "x11", - ).OPENNOW_NATIVE_EXTERNAL_RENDERER, - "0", - ); -}); - -test("Linux shell defaults to X11 unless an Ozone backend was explicit", () => { - assert.equal(shouldDefaultLinuxShellToX11("linux", ""), true); - assert.equal(shouldDefaultLinuxShellToX11("linux", undefined), true); - assert.equal(shouldDefaultLinuxShellToX11("linux", "auto"), true); - assert.equal(shouldDefaultLinuxShellToX11("linux", "wayland"), false); - assert.equal(shouldDefaultLinuxShellToX11("win32", ""), false); -}); - -test("normalizes real and symlinked paths to the same comparison path", (t) => { - const root = mkdtempSync(join(tmpdir(), "opennow-native-path-")); - t.after(() => rmSync(root, { recursive: true, force: true })); - const runtime = join(root, "runtime"); - const alias = join(root, "runtime-alias"); - mkdirSync(runtime); - mkdirSync(join(runtime, "gstreamer")); - symlinkSync(runtime, alias, "dir"); - - assert.equal( - normalizePathForComparison(alias), - normalizePathForComparison(runtime), - ); - assert.equal(isPathInside(alias, join(runtime, "gstreamer")), true); -}); - -test("path containment rejects sibling names that only share a prefix", (t) => { - const root = mkdtempSync(join(tmpdir(), "opennow-native-path-")); - t.after(() => rmSync(root, { recursive: true, force: true })); - const runtime = join(root, "runtime"); - - assert.equal(isPathInside(runtime, runtime), true); - assert.equal(isPathInside(runtime, join(runtime, "cached", "streamer")), true); - assert.equal(isPathInside(runtime, `${runtime}-old/streamer`), false); + assert.equal(result.env.GST_PLUGIN_PATH, undefined); + assert.equal(result.env.OPENNOW_NATIVE_STREAMER_PROTOCOL, "4"); + assert.equal(result.runtimeStatus.selfContained, true); + assert.match(result.runtimeStatus.message, /self-contained/); }); test("runtime executable names and platform keys accept explicit platforms", () => { diff --git a/opennow-stable/src/main/nativeStreamer/runtime.ts b/opennow-stable/src/main/nativeStreamer/runtime.ts index 056a79917..57dd1514f 100644 --- a/opennow-stable/src/main/nativeStreamer/runtime.ts +++ b/opennow-stable/src/main/nativeStreamer/runtime.ts @@ -1,17 +1,10 @@ -import { - existsSync, - mkdirSync, - realpathSync, - statSync, -} from "node:fs"; -import { delimiter, dirname, join, resolve, sep } from "node:path"; +import { existsSync, realpathSync, statSync } from "node:fs"; +import { resolve, sep } from "node:path"; import { nativeStreamerFeatureModeToEnvValue, - type NativeGstreamerInstallInstruction, - type NativeGstreamerRuntimeStatus, - type NativeStreamerBackendPreference, type NativeStreamerFeatureMode, + type NativeStreamerRuntimeStatus, type NativeVideoBackendPreference, } from "@shared/gfn"; @@ -22,7 +15,6 @@ export interface NativeStreamerRuntimeEnvironmentOptions { arch: string; userDataPath: string; protocolVersion: number; - backendPreference: NativeStreamerBackendPreference; videoBackendPreference: NativeVideoBackendPreference; externalRendererEnabled: boolean; linuxOzonePlatform?: string; @@ -32,30 +24,9 @@ export interface NativeStreamerRuntimeEnvironmentOptions { export interface NativeStreamerRuntimeEnvironment { env: NodeJS.ProcessEnv; - runtimeStatus: NativeGstreamerRuntimeStatus; + runtimeStatus: NativeStreamerRuntimeStatus; } -const LINUX_GSTREAMER_INSTALL_INSTRUCTIONS: NativeGstreamerInstallInstruction[] = [ - { - distro: "Debian / Ubuntu / Mint / Pop!_OS / KDE neon", - command: "sudo apt update && sudo apt install libgstreamer1.0-0 libgstreamer-plugins-base1.0-0 gstreamer1.0-tools gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-nice gstreamer1.0-gl gstreamer1.0-vaapi gstreamer1.0-x gstreamer1.0-alsa libva2 libva-drm2 libvulkan1 mesa-vulkan-drivers", - }, - { - distro: "Fedora / RHEL / Nobara / Bazzite", - command: "sudo dnf install gstreamer1 gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-plugins-bad-free gstreamer1-plugins-bad-freeworld gstreamer1-plugins-ugly gstreamer1-libav gstreamer1-vaapi gstreamer1-plugin-openh264 libnice-gstreamer1 mesa-vulkan-drivers libva", - note: "RPM Fusion may be required for libav, ugly, or bad-freeworld packages.", - }, - { - distro: "Arch / Manjaro / EndeavourOS / SteamOS", - command: "sudo pacman -S --needed gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav gst-plugin-va libnice libva mesa vulkan-radeon", - note: "NVIDIA users should use their distro NVIDIA/Vulkan driver packages instead of vulkan-radeon.", - }, - { - distro: "openSUSE Tumbleweed / Leap", - command: "sudo zypper install gstreamer gstreamer-plugins-base gstreamer-plugins-good gstreamer-plugins-bad gstreamer-plugins-ugly gstreamer-plugins-libav gstreamer-plugins-vaapi gstreamer-libnice libva2 Mesa-vulkan-device-select", - }, -]; - export function nativeStreamerExecutableName(platform = process.platform): string { return platform === "win32" ? "opennow-streamer.exe" : "opennow-streamer"; } @@ -75,14 +46,6 @@ export function isExistingFile(path: string): boolean { } } -export function isExistingDirectory(path: string): boolean { - try { - return existsSync(path) && statSync(path).isDirectory(); - } catch { - return false; - } -} - export function normalizePathForComparison( path: string, platform = process.platform, @@ -91,7 +54,7 @@ export function normalizePathForComparison( try { resolvedPath = realpathSync.native(resolvedPath); } catch { - // Cache destinations and configured overrides may not exist yet. + // Paths selected in settings may not exist yet. } return platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath; } @@ -106,167 +69,35 @@ export function isPathInside( return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${sep}`); } -export function hasBundledRuntimeNextToExecutable(executablePath: string): boolean { - return isExistingDirectory(join(dirname(executablePath), "gstreamer")); -} - -export function linuxInstallInstructions( - platform = process.platform, -): NativeGstreamerInstallInstruction[] | undefined { - return platform === "linux" ? LINUX_GSTREAMER_INSTALL_INSTRUCTIONS : undefined; -} - -export function isNativeWaylandSession( - env: NodeJS.ProcessEnv, - ozonePlatform?: string, -): boolean { - const explicitPlatform = ozonePlatform?.trim().toLowerCase(); - if (explicitPlatform === "x11") return false; - if (explicitPlatform === "wayland") return true; - - const environmentHint = env.ELECTRON_OZONE_PLATFORM_HINT?.trim().toLowerCase(); - if (!explicitPlatform && environmentHint === "x11") return false; - if (!explicitPlatform && environmentHint === "wayland") return true; - - return env.XDG_SESSION_TYPE?.trim().toLowerCase() === "wayland" - || Boolean(env.WAYLAND_DISPLAY?.trim()); -} - -export function shouldDefaultLinuxShellToX11( - platform: NodeJS.Platform, - ozonePlatform?: string, -): boolean { - const normalized = ozonePlatform?.trim().toLowerCase(); - return platform === "linux" && (!normalized || normalized === "auto"); -} - -function prependEnvPath(env: NodeJS.ProcessEnv, key: string, directory: string): void { - env[key] = env[key] ? `${directory}${delimiter}${env[key]}` : directory; -} - -function prependProcessPath(env: NodeJS.ProcessEnv, directory: string): void { - const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH"; - prependEnvPath(env, pathKey, directory); -} - -function configureBundledGstreamerRuntime( - env: NodeJS.ProcessEnv, - executablePath: string, - platform: NodeJS.Platform, - arch: string, - userDataPath: string, -): NativeGstreamerRuntimeStatus { - const runtimeRoot = join(dirname(executablePath), "gstreamer"); - if (!isExistingDirectory(runtimeRoot)) { - return { - source: "system", - bundled: false, - message: platform === "linux" - ? "No bundled GStreamer runtime was found. Linux uses distro GStreamer packages so VAAPI/V4L2/Vulkan plugins match the host driver stack." - : "No bundled GStreamer runtime was found; using the system runtime if available.", - installInstructions: linuxInstallInstructions(platform), - }; - } - - const binDir = join(runtimeRoot, "bin"); - const libDir = join(runtimeRoot, "lib"); - const pluginDir = join(runtimeRoot, "lib", "gstreamer-1.0"); - const scanner = join( - runtimeRoot, - "libexec", - "gstreamer-1.0", - platform === "win32" ? "gst-plugin-scanner.exe" : "gst-plugin-scanner", - ); - const gioModulesDir = join(runtimeRoot, "lib", "gio", "modules"); - - if (platform === "win32") prependProcessPath(env, dirname(executablePath)); - if (isExistingDirectory(binDir)) prependProcessPath(env, binDir); - if (isExistingDirectory(pluginDir)) { - env.GST_PLUGIN_PATH = pluginDir; - env.GST_PLUGIN_PATH_1_0 = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH = pluginDir; - env.GST_PLUGIN_SYSTEM_PATH_1_0 = pluginDir; - } - if (isExistingFile(scanner)) { - env.GST_PLUGIN_SCANNER = scanner; - env.GST_PLUGIN_SCANNER_1_0 = scanner; - } - env.GST_REGISTRY_REUSE_PLUGIN_SCANNER = "no"; - if (isExistingDirectory(gioModulesDir)) { - env.GIO_MODULE_DIR = gioModulesDir; - env.GIO_EXTRA_MODULES = gioModulesDir; - } - const registryDir = join(userDataPath, "native-streamer", "gstreamer"); - const registryPath = join(registryDir, `${nativeStreamerPlatformKey(platform, arch)}-registry.bin`); - mkdirSync(registryDir, { recursive: true }); - env.GST_REGISTRY = registryPath; - if (platform === "linux") { - if (isExistingDirectory(libDir)) prependEnvPath(env, "LD_LIBRARY_PATH", libDir); - if (isExistingDirectory(binDir)) prependEnvPath(env, "LD_LIBRARY_PATH", binDir); - } - if (platform === "darwin") { - if (isExistingDirectory(libDir)) { - prependEnvPath(env, "DYLD_LIBRARY_PATH", libDir); - prependEnvPath(env, "DYLD_FALLBACK_LIBRARY_PATH", libDir); - } - if (isExistingDirectory(binDir)) { - prependEnvPath(env, "DYLD_LIBRARY_PATH", binDir); - prependEnvPath(env, "DYLD_FALLBACK_LIBRARY_PATH", binDir); - } - } - - return { - source: "bundled", - bundled: true, - path: runtimeRoot, - message: "Using bundled GStreamer runtime next to the native streamer executable.", - }; -} - export function createNativeStreamerRuntimeEnvironment( options: NativeStreamerRuntimeEnvironmentOptions, ): NativeStreamerRuntimeEnvironment { - if ( - options.platform === "linux" - && isNativeWaylandSession(options.baseEnv, options.linuxOzonePlatform) - ) { - throw new Error( - "Native Linux video requires Electron to run through X11/XWayland so GStreamer can embed frames in the OpenNOW window. Relaunch with --ozone-platform=x11; pure Wayland native embedding is not supported yet.", - ); - } - const env: NodeJS.ProcessEnv = { ...options.baseEnv, OPENNOW_NATIVE_STREAMER_PROTOCOL: String(options.protocolVersion), + OPENNOW_NATIVE_CLOUD_GSYNC: nativeStreamerFeatureModeToEnvValue(options.cloudGsyncMode), + OPENNOW_NATIVE_D3D_FULLSCREEN: nativeStreamerFeatureModeToEnvValue(options.d3dFullscreenMode), + OPENNOW_NATIVE_EXTERNAL_RENDERER: options.externalRendererEnabled ? "1" : "0", }; - delete env.OPENNOW_NATIVE_VIDEO_API; - delete env.OPENNOW_NATIVE_VIDEO_BACKEND; + delete env.GST_PLUGIN_PATH; + delete env.GST_PLUGIN_PATH_1_0; + delete env.GST_PLUGIN_SYSTEM_PATH; + delete env.GST_PLUGIN_SYSTEM_PATH_1_0; + delete env.GST_PLUGIN_SCANNER; + delete env.GST_PLUGIN_SCANNER_1_0; + delete env.GST_REGISTRY; + if (options.videoBackendPreference !== "auto") { env.OPENNOW_NATIVE_VIDEO_BACKEND = options.videoBackendPreference; } - if (options.platform === "linux") { - env.OPENNOW_NATIVE_EXTERNAL_RENDERER = "0"; - if ((options.arch === "arm64" || options.arch === "arm") && !env.GST_V4L2_ENABLE_PROBE) { - env.GST_V4L2_ENABLE_PROBE = "1"; - } - } else if (options.platform === "win32") { - env.OPENNOW_NATIVE_EXTERNAL_RENDERER = options.externalRendererEnabled ? "1" : "0"; - env.OPENNOW_NATIVE_D3D_ALLOW_TEARING = "1"; - } - env.OPENNOW_NATIVE_CLOUD_GSYNC = nativeStreamerFeatureModeToEnvValue(options.cloudGsyncMode); - env.OPENNOW_NATIVE_D3D_FULLSCREEN = nativeStreamerFeatureModeToEnvValue(options.d3dFullscreenMode); - if (options.backendPreference !== "auto") { - env.OPENNOW_NATIVE_STREAMER_BACKEND = options.backendPreference; - } return { env, - runtimeStatus: configureBundledGstreamerRuntime( - env, - options.executablePath, - options.platform, - options.arch, - options.userDataPath, - ), + runtimeStatus: { + source: "self-contained", + selfContained: true, + path: options.executablePath, + message: "Native streamer v2 is self-contained; no external media runtime is required.", + }, }; } diff --git a/opennow-stable/src/main/nativeStreamer/runtimeCache.test.ts b/opennow-stable/src/main/nativeStreamer/runtimeCache.test.ts deleted file mode 100644 index 899c32595..000000000 --- a/opennow-stable/src/main/nativeStreamer/runtimeCache.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import test from "node:test"; - -import { - buildPackagedNativeStreamerCacheMarker, - isSamePackagedNativeStreamerCacheMarker, - shouldUseStablePackagedNativeStreamerCache, -} from "./runtimeCache"; - -test("cache markers compare every executable and runtime identity field", (t) => { - const source = mkdtempSync(join(tmpdir(), "opennow-native-marker-")); - t.after(() => rmSync(source, { recursive: true, force: true })); - const runtime = join(source, "gstreamer"); - mkdirSync(runtime); - writeFileSync(join(source, "opennow-streamer.exe"), "streamer-v1"); - writeFileSync(join(runtime, "OPENNOW-GSTREAMER-RUNTIME.txt"), "runtime-v1"); - - const marker = buildPackagedNativeStreamerCacheMarker( - source, - "opennow-streamer.exe", - "win32-x64", - "1.2.3", - ); - const sameMarker = buildPackagedNativeStreamerCacheMarker( - source, - "opennow-streamer.exe", - "win32-x64", - "1.2.3", - ); - assert.equal(isSamePackagedNativeStreamerCacheMarker(marker, sameMarker), true); - - writeFileSync(join(runtime, "OPENNOW-GSTREAMER-RUNTIME.txt"), "runtime-v2"); - const changedRuntimeMarker = buildPackagedNativeStreamerCacheMarker( - source, - "opennow-streamer.exe", - "win32-x64", - "1.2.3", - ); - assert.equal(isSamePackagedNativeStreamerCacheMarker(marker, changedRuntimeMarker), false); - assert.equal(isSamePackagedNativeStreamerCacheMarker(null, marker), false); -}); - -test("stable packaged cache selection is constrained to Windows temporary resources", (t) => { - const temporaryRoot = mkdtempSync(join(tmpdir(), "opennow-native-cache-")); - t.after(() => rmSync(temporaryRoot, { recursive: true, force: true })); - const resourcesPath = join(temporaryRoot, "resources"); - mkdirSync(resourcesPath); - - assert.equal(shouldUseStablePackagedNativeStreamerCache({ - isPackaged: true, - platform: "win32", - resourcesPath, - tempDirectory: tmpdir(), - }), true); - assert.equal(shouldUseStablePackagedNativeStreamerCache({ - isPackaged: true, - platform: "linux", - resourcesPath, - tempDirectory: tmpdir(), - }), false); - assert.equal(shouldUseStablePackagedNativeStreamerCache({ - isPackaged: false, - platform: "win32", - resourcesPath, - tempDirectory: tmpdir(), - }), false); -}); diff --git a/opennow-stable/src/main/nativeStreamer/runtimeCache.ts b/opennow-stable/src/main/nativeStreamer/runtimeCache.ts deleted file mode 100644 index 572e998d2..000000000 --- a/opennow-stable/src/main/nativeStreamer/runtimeCache.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { createHash } from "node:crypto"; -import { - cpSync, - mkdirSync, - readFileSync, - renameSync, - rmSync, - writeFileSync, -} from "node:fs"; -import { dirname, join } from "node:path"; - -import { - hasBundledRuntimeNextToExecutable, - isExistingDirectory, - isExistingFile, - isPathInside, -} from "./runtime"; - -export interface PackagedNativeStreamerCacheMarker { - appVersion: string; - platformKey: string; - exeName: string; - exeSha256: string; - bundledRuntime: boolean; - runtimeManifestSha256?: string; -} - -export interface PackagedNativeStreamerCacheContext { - appVersion: string; - isPackaged: boolean; - platform: NodeJS.Platform; - resourcesPath: string; - tempDirectory: string; - userDataPath: string; -} - -function safePathSegment(value: string): string { - return value.replace(/[^a-zA-Z0-9._-]/g, "_") || "unknown"; -} - -function fileSha256(path: string): string { - return createHash("sha256").update(readFileSync(path)).digest("hex"); -} - -export function shouldUseStablePackagedNativeStreamerCache( - context: Pick< - PackagedNativeStreamerCacheContext, - "isPackaged" | "platform" | "resourcesPath" | "tempDirectory" - >, -): boolean { - return context.isPackaged - && context.platform === "win32" - && isPathInside(context.tempDirectory, context.resourcesPath, context.platform); -} - -export function buildPackagedNativeStreamerCacheMarker( - sourceDirectory: string, - exeName: string, - platformKey: string, - appVersion: string, -): PackagedNativeStreamerCacheMarker { - const runtimeManifest = join(sourceDirectory, "gstreamer", "OPENNOW-GSTREAMER-RUNTIME.txt"); - return { - appVersion, - platformKey, - exeName, - exeSha256: fileSha256(join(sourceDirectory, exeName)), - bundledRuntime: isExistingDirectory(join(sourceDirectory, "gstreamer")), - runtimeManifestSha256: isExistingFile(runtimeManifest) ? fileSha256(runtimeManifest) : undefined, - }; -} - -export function readPackagedNativeStreamerCacheMarker( - markerPath: string, -): PackagedNativeStreamerCacheMarker | null { - try { - return JSON.parse(readFileSync(markerPath, "utf8")) as PackagedNativeStreamerCacheMarker; - } catch { - return null; - } -} - -export function isSamePackagedNativeStreamerCacheMarker( - left: PackagedNativeStreamerCacheMarker | null, - right: PackagedNativeStreamerCacheMarker, -): boolean { - if (!left) { - return false; - } - - return left.appVersion === right.appVersion - && left.platformKey === right.platformKey - && left.exeName === right.exeName - && left.exeSha256 === right.exeSha256 - && left.bundledRuntime === right.bundledRuntime - && left.runtimeManifestSha256 === right.runtimeManifestSha256; -} - -export function materializePackagedNativeStreamerCache( - sourceExecutablePath: string, - platformKey: string, - exeName: string, - context: PackagedNativeStreamerCacheContext, -): string | null { - if (!shouldUseStablePackagedNativeStreamerCache(context)) { - return null; - } - - const sourceDirectory = dirname(sourceExecutablePath); - const cacheDirectory = join( - context.userDataPath, - "native-streamer", - "runtime", - safePathSegment(context.appVersion), - safePathSegment(platformKey), - ); - const cachedExecutablePath = join(cacheDirectory, exeName); - const markerPath = join(cacheDirectory, ".opennow-native-runtime.json"); - let stagingDirectory: string | null = null; - - try { - const expectedMarker = buildPackagedNativeStreamerCacheMarker( - sourceDirectory, - exeName, - platformKey, - context.appVersion, - ); - const cachedMarker = readPackagedNativeStreamerCacheMarker(markerPath); - if ( - isExistingFile(cachedExecutablePath) - && isSamePackagedNativeStreamerCacheMarker(cachedMarker, expectedMarker) - && (!expectedMarker.bundledRuntime || hasBundledRuntimeNextToExecutable(cachedExecutablePath)) - ) { - return cachedExecutablePath; - } - - stagingDirectory = `${cacheDirectory}.tmp-${process.pid}-${Date.now()}`; - rmSync(stagingDirectory, { recursive: true, force: true }); - mkdirSync(dirname(stagingDirectory), { recursive: true }); - cpSync(sourceDirectory, stagingDirectory, { - recursive: true, - force: true, - dereference: true, - filter: (entry) => { - const lower = entry.toLowerCase(); - return !lower.endsWith(".pdb") && !lower.endsWith(".lib") && !lower.endsWith(".a"); - }, - }); - writeFileSync( - join(stagingDirectory, ".opennow-native-runtime.json"), - `${JSON.stringify(expectedMarker, null, 2)}\n`, - "utf8", - ); - - if (!isExistingFile(join(stagingDirectory, exeName))) { - throw new Error(`Cached native streamer executable was not created: ${join(stagingDirectory, exeName)}`); - } - if (expectedMarker.bundledRuntime && !hasBundledRuntimeNextToExecutable(join(stagingDirectory, exeName))) { - throw new Error("Cached native streamer runtime is missing its bundled GStreamer directory."); - } - - rmSync(cacheDirectory, { recursive: true, force: true }); - renameSync(stagingDirectory, cacheDirectory); - stagingDirectory = null; - console.log("[NativeStreamer] Cached packaged native streamer in stable runtime path:", cachedExecutablePath); - return cachedExecutablePath; - } catch (error) { - console.warn("[NativeStreamer] Failed to prepare stable packaged runtime cache; using packaged resource path:", error); - return null; - } finally { - if (stagingDirectory) { - rmSync(stagingDirectory, { recursive: true, force: true }); - } - } -} diff --git a/opennow-stable/src/main/nativeStreamer/surface.test.ts b/opennow-stable/src/main/nativeStreamer/surface.test.ts new file mode 100644 index 000000000..47c403124 --- /dev/null +++ b/opennow-stable/src/main/nativeStreamer/surface.test.ts @@ -0,0 +1,31 @@ +/// + +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { BrowserWindow } from "electron"; +import { normalizeNativeRenderSurface } from "./surface"; + +test("normalizes renderer bounds into absolute screen coordinates", () => { + const handle = Buffer.alloc(8); + handle.writeBigUInt64LE(0x1234n); + const window = { + getNativeWindowHandle: () => handle, + getContentBounds: () => ({ x: 100, y: 200, width: 1280, height: 720 }), + } as unknown as BrowserWindow; + + const surface = normalizeNativeRenderSurface(window, { + rect: { x: 12.4, y: 24.6, width: 640.2, height: 360.4 }, + visible: true, + deviceScaleFactor: 2, + }); + + assert.deepEqual(surface, { + windowHandle: "0x1234", + deviceScaleFactor: 2, + visible: true, + showStats: false, + rect: { x: 12, y: 25, width: 640, height: 360 }, + screenRect: { x: 112, y: 225, width: 640, height: 360 }, + }); +}); diff --git a/opennow-stable/src/main/nativeStreamer/surface.ts b/opennow-stable/src/main/nativeStreamer/surface.ts index 0344956c2..3b29ecbf7 100644 --- a/opennow-stable/src/main/nativeStreamer/surface.ts +++ b/opennow-stable/src/main/nativeStreamer/surface.ts @@ -32,6 +32,7 @@ export function normalizeNativeRenderSurface( ? Math.min(8, Math.max(0.25, input.deviceScaleFactor)) : 1; const rect = input.rect; + const contentBounds = window.getContentBounds(); const visible = input.visible === true && rect !== null && @@ -55,5 +56,15 @@ export function normalizeNativeRenderSurface( height: Math.max(2, Math.round(rect.height)), } : null, + screenRect: visible + ? { + // The renderer reports rect in device pixels; AppKit screen + // coordinates are points, so convert before adding content bounds. + x: contentBounds.x + Math.round(rect.x / deviceScaleFactor), + y: contentBounds.y + Math.round(rect.y / deviceScaleFactor), + width: Math.max(2, Math.round(rect.width / deviceScaleFactor)), + height: Math.max(2, Math.round(rect.height / deviceScaleFactor)), + } + : undefined, }; } diff --git a/opennow-stable/src/main/platforms/gfn/clientHeaders.ts b/opennow-stable/src/main/platforms/gfn/clientHeaders.ts index 4314e588e..cbdc4625e 100644 --- a/opennow-stable/src/main/platforms/gfn/clientHeaders.ts +++ b/opennow-stable/src/main/platforms/gfn/clientHeaders.ts @@ -1,4 +1,6 @@ -import crypto from "node:crypto"; +import os from "node:os"; + +import { getCloudMatchDeviceHashId } from "./deviceId"; import { GFN_PLAY_ORIGIN as SHARED_GFN_PLAY_ORIGIN, GFN_PLAY_REFERER as SHARED_GFN_PLAY_REFERER } from "@shared/gfn/endpoints"; @@ -8,14 +10,48 @@ import { type GfnDeviceOs, } from "./deviceIdentity"; +/** Official mall `shared/assets/config/config.json` build.version / hash (2.0.87.131). */ +export const GFN_CLIENT_VERSION = "2.0.87.131"; +/** Official CEF host token: `HEAD/` + first 10 hex chars of mall `build.hash`. */ +const GFN_CEF_PRODUCT = "NVIDIACEFClient/HEAD/7b92719716"; + const GFN_WINDOWS_USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 NVIDIACEFClient/HEAD/debb5919f6 GFN-PC/2.0.80.173"; + `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ${GFN_CEF_PRODUCT} GFN-PC/${GFN_CLIENT_VERSION}`; const GFN_MACOS_USER_AGENT = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 GFN-PC/2.0.80.173"; + `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ${GFN_CEF_PRODUCT} GFN-PC/${GFN_CLIENT_VERSION}`; +const GFN_LINUX_USER_AGENT = + `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 ${GFN_CEF_PRODUCT} GFN-PC/${GFN_CLIENT_VERSION}`; + +export function gfnUserAgentForPlatform(platform: NodeJS.Platform = process.platform): string { + if (platform === "darwin") { + return GFN_MACOS_USER_AGENT; + } + if (platform === "linux") { + return GFN_LINUX_USER_AGENT; + } + return GFN_WINDOWS_USER_AGENT; +} + +export const GFN_USER_AGENT = gfnUserAgentForPlatform(); +/** Official mall JSON `clientVersion` token used in native Grid User-Agent. */ +export const GFN_BIFROST_CLIENT_VERSION = "30.0"; +/** Official Mac BifrostClientSDK token from Grid POST 2026-08-19. */ +const GFN_BIFROST_SDK = "4.9"; +const GFN_BIFROST_SDK_BUILD = "38495286"; + +export function gfnBifrostUserAgentForPlatform(platform: NodeJS.Platform = process.platform): string { + if (platform === "darwin") { + return `GFN-PC/${GFN_BIFROST_CLIENT_VERSION} (MacOSX ${os.release()}) BifrostClientSDK/${GFN_BIFROST_SDK} (${GFN_BIFROST_SDK_BUILD})`; + } + if (platform === "linux") { + return `GFN-PC/${GFN_BIFROST_CLIENT_VERSION} (Linux ${os.release()}) BifrostClientSDK/${GFN_BIFROST_SDK} (${GFN_BIFROST_SDK_BUILD})`; + } + return `GFN-PC/${GFN_BIFROST_CLIENT_VERSION} (Windows NT 10.0) BifrostClientSDK/${GFN_BIFROST_SDK} (${GFN_BIFROST_SDK_BUILD})`; +} -export const GFN_USER_AGENT = process.platform === "darwin" ? GFN_MACOS_USER_AGENT : GFN_WINDOWS_USER_AGENT; -export const GFN_CLIENT_VERSION = "2.0.80.173"; export const LCARS_CLIENT_ID = "ec7e38d4-03af-4b58-b131-cfb0495903ab"; +/** Official CloudMatch / Bifrost `clientIdentification` for the native PC client. */ +export const GFN_CLIENT_IDENTIFICATION = "GFN-PC"; export const GFN_PLAY_ORIGIN = SHARED_GFN_PLAY_ORIGIN; export const GFN_PLAY_REFERER = SHARED_GFN_PLAY_REFERER; @@ -45,6 +81,7 @@ function applyDeviceIdentityHeaders( ): void { headers["nv-device-os"] = identity.deviceOs; headers["nv-device-type"] = identity.deviceType; + headers["x-nv-client-identity"] = GFN_CLIENT_IDENTIFICATION; if (options?.includeMakeModel !== false) { headers["nv-device-make"] = identity.deviceMake; headers["nv-device-model"] = identity.deviceModel; @@ -148,50 +185,64 @@ export interface GfnCloudMatchHeadersOptions { function resolveCloudMatchIdentity(options: GfnCloudMatchHeadersOptions): { clientId: string; deviceId: string } { return { - clientId: options.clientId ?? crypto.randomUUID(), - deviceId: options.deviceId ?? crypto.randomUUID(), + clientId: options.clientId ?? LCARS_CLIENT_ID, + deviceId: options.deviceId ?? getCloudMatchDeviceHashId(), }; } export function buildGfnCloudMatchHeaders(options: GfnCloudMatchHeadersOptions): Record { const { clientId, deviceId } = resolveCloudMatchIdentity(options); const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: options.identifyAsSteamDeck }); - const headers: Record = { - "User-Agent": GFN_USER_AGENT, + const userAgent = gfnBifrostUserAgentForPlatform(); + void options.includeOrigin; + return { + "User-Agent": userAgent, Authorization: gfnJwtAuthorization(options.token), - "Content-Type": "application/json", - "nv-browser-type": "CHROME", - "nv-client-id": clientId, - "nv-client-streamer": "NVIDIA-CLASSIC", - "nv-client-type": "NATIVE", - "nv-client-version": GFN_CLIENT_VERSION, - "x-device-id": deviceId, + "Content-Type": "text/plain", + "NV-Client-ID": clientId, + "NV-Client-Streamer": "NVIDIA-CLASSIC", + "NV-Client-Type": "NATIVE", + "NV-Client-Version": GFN_CLIENT_VERSION, + "NV-Device-OS": identity.deviceOs, + "NV-Device-Type": identity.deviceType, + "NV-Device-Make": identity.deviceMake, + "NV-Device-Model": identity.deviceModel, + "X-Device-Id": deviceId, + "x-nv-client-identity": userAgent, }; - applyDeviceIdentityHeaders(headers, identity); - - if (options.includeOrigin !== false) { - headers.Origin = GFN_PLAY_ORIGIN; - headers.Referer = GFN_PLAY_REFERER; - } +} - return headers; +export interface GfnNvstClientHeadersOptions { + deviceId?: string; + identifyAsSteamDeck?: boolean; } -export function buildGfnCloudMatchClaimHeaders(options: GfnCloudMatchHeadersOptions): Record { - const { clientId, deviceId } = resolveCloudMatchIdentity(options); +/** + * Official Bifrost GridServer / CloudMatch HTTP identity. Not used on RTSP-over-WSS: + * that upgrade is `GET /rtsp` + `x-nv-sessionid` only. + */ +export function buildGfnNvstClientHeaders( + options: GfnNvstClientHeadersOptions = {}, +): Record { const identity = resolveGfnDeviceIdentity({ identifyAsSteamDeck: options.identifyAsSteamDeck }); + const userAgent = gfnBifrostUserAgentForPlatform(); const headers: Record = { - "User-Agent": GFN_USER_AGENT, - Authorization: gfnJwtAuthorization(options.token), - "Content-Type": "application/json", - Origin: GFN_PLAY_ORIGIN, - Referer: GFN_PLAY_REFERER, - "nv-client-id": clientId, - "nv-client-streamer": "NVIDIA-CLASSIC", - "nv-client-type": "NATIVE", - "nv-client-version": GFN_CLIENT_VERSION, - "x-device-id": deviceId, + "User-Agent": userAgent, + "x-nv-client-identity": userAgent, + "NV-Device-OS": identity.deviceOs, + "NV-Client-Streamer": "NVIDIA-CLASSIC", + "NV-Device-Type": identity.deviceType, + "NV-Client-Type": "NATIVE", + "NV-Device-Make": identity.deviceMake, + "NV-Device-Model": identity.deviceModel, + "NV-Client-Version": GFN_CLIENT_VERSION, }; - applyDeviceIdentityHeaders(headers, identity); + if (options.deviceId) { + headers["X-Device-Id"] = options.deviceId; + } return headers; } + +export function buildGfnCloudMatchClaimHeaders(options: GfnCloudMatchHeadersOptions): Record { + return buildGfnCloudMatchHeaders({ ...options, includeOrigin: false }); +} diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts b/opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts index a0cec4957..e3d746258 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatch.test.ts @@ -20,6 +20,8 @@ import { resolveRequestedCodecWireValue, } from "./cloudmatch"; import { buildSessionRequestBody } from "./cloudmatchSessionRequest"; +import { resolveNvstCreateStreamSku } from "./cloudmatchFeatures"; +import { resolveGfnDeviceIdentity } from "./deviceIdentity"; function makeSettings(overrides: Partial = {}): StreamSettings { return { @@ -191,6 +193,80 @@ test("CloudMatch session request body carries supported codecs into the wire cod assert.equal(body.sessionRequestData.requestedStreamingFeatures.codec, 2); }); +test("CloudMatch requests secure RTSPS for explicit classic native sessions", () => { + const nativeBody = buildSessionRequestBody( + { + appId: "1001", + internalTitle: "Test Game", + zone: "prod", + settings: makeSettings({ transportMode: "nvst" }), + }, + "device-id", + ); + const webRtcBody = buildSessionRequestBody( + { + appId: "1001", + internalTitle: "Test Game", + zone: "prod", + settings: makeSettings({ transportMode: "webrtc" }), + }, + "device-id", + ); + + assert.equal(nativeBody.sessionRequestData.secureRTSPSupported, true); + assert.equal(nativeBody.sessionRequestData.sdkVersion, "2.0"); + assert.equal(nativeBody.sessionRequestData.streamerVersion, "14"); + assert.equal(nativeBody.sessionRequestData.enhancedStreamMode, 0); + assert.deepEqual(nativeBody.sessionRequestData.availableSupportedControllers, [2]); + assert.equal(nativeBody.sessionRequestData.preferredController, 2); + assert.equal(nativeBody.sessionRequestData.requestedAudioFormat, 0); + assert.equal(nativeBody.sessionRequestData.partnerCustomData, null); + assert.equal(nativeBody.sessionRequestData.transport, null); + assert.equal(nativeBody.sessionRequestData.externalAppId, null); + assert.equal(nativeBody.sessionRequestData.appId, 1001); + assert.equal(nativeBody.sessionRequestData.internalTitle, null); + assert.equal(nativeBody.sessionRequestData.accountLinked, false); + assert.equal(nativeBody.sessionRequestData.deviceHashId, "device-id"); + assert.equal(nativeBody.sessionRequestData.userAge, 25); + assert.ok((nativeBody.sessionRequestData.clientRequestMonitorSettings[0]?.dpi ?? 0) > 0); + assert.equal( + nativeBody.sessionRequestData.clientPlatformName, + resolveGfnDeviceIdentity().clientPlatformName, + ); + if (process.platform === "darwin") { + assert.equal(nativeBody.sessionRequestData.clientPlatformName, "MacOSX"); + assert.equal(nativeBody.sessionRequestData.sdrHdrMode, 1); + assert.equal(nativeBody.sessionRequestData.clientDisplayHdrCapabilities?.version, 2); + assert.equal( + nativeBody.sessionRequestData.metaData.find((entry) => entry.key === "networkType")?.value, + "WiFi5.0", + ); + } + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.codec, undefined); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.maxBitrateKbps, undefined); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.dynamicStreamingMode, undefined); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.audioChannelCount, undefined); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.trueHdr, false); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.bitDepth, 1); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.chromaFormat, 0); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.reflex, true); + assert.equal(nativeBody.sessionRequestData.requestedStreamingFeatures.qosPolicy, 0); + assert.deepEqual(resolveNvstCreateStreamSku(makeSettings({ transportMode: "nvst" })), { + bitDepth: 1, + chromaFormat: 0, + reflex: true, + }); + assert.equal( + nativeBody.sessionRequestData.metaData.some((entry) => entry.key === "GSStreamerType"), + false, + ); + assert.equal(webRtcBody.sessionRequestData.secureRTSPSupported, false); + assert.deepEqual( + webRtcBody.sessionRequestData.metaData.find((entry) => entry.key === "GSStreamerType"), + { key: "GSStreamerType", value: "WebRTC" }, + ); +}); + test("CloudMatch extracts local serverInfo region before fallback regions", () => { const bases = extractServerInfoRegionBases({ metaData: [ @@ -209,12 +285,16 @@ test("CloudMatch extracts local serverInfo region before fallback regions", () = ]); }); -test("CloudMatch pins the resolved region with a network-test session before creating a session", async () => { +test("CloudMatch creates a session without a network-test pin", async () => { const originalFetch = globalThis.fetch; const originalWarn = console.warn; const calls: string[] = []; type CapturedSessionRequestBody = { sessionRequestData: { + appId?: string | number; + internalTitle?: string | null; + accountLinked?: boolean; + deviceHashId?: string; networkTestSessionId?: string | null; appLaunchMode?: number; enablePersistingInGameSettings?: boolean; @@ -232,15 +312,7 @@ test("CloudMatch pins the resolved region with a network-test session before cre }; }; }; - type CapturedNetworkTestRequestBody = { - netTestRequestData: { - netTestProfile: { - framesPerSecond: number; - }; - }; - }; let requestBody: CapturedSessionRequestBody | null = null; - let networkTestRequestBody: CapturedNetworkTestRequestBody | null = null; const expectedSessionUrl = `https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/session?${new URLSearchParams({ keyboardLayout: resolveGfnKeyboardLayout(DEFAULT_KEYBOARD_LAYOUT, process.platform), languageCode: "en_US", @@ -263,16 +335,6 @@ test("CloudMatch pins the resolved region with a network-test session before cre }), { status: 200 }); } - if (url === "https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/nettestsession") { - networkTestRequestBody = JSON.parse(String(init?.body)); - return new Response(JSON.stringify({ - requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS", serverId: "NP-LAX-01" }, - netTestSession: { - sessionId: "nettest-lax-1", - }, - }), { status: 200 }); - } - if (url === expectedSessionUrl) { requestBody = JSON.parse(String(init?.body)); const createdRequestBody = requestBody; @@ -317,12 +379,8 @@ test("CloudMatch pins the resolved region with a network-test session before cre assert.equal(session.enablePersistingInGameSettings, false); assert.deepEqual(calls, [ "https://prod.cloudmatchbeta.nvidiagrid.net/v2/serverInfo", - "https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/nettestsession", expectedSessionUrl, ]); - const capturedNetworkTestRequestBody = networkTestRequestBody as CapturedNetworkTestRequestBody | null; - assert.ok(capturedNetworkTestRequestBody); - assert.equal(capturedNetworkTestRequestBody.netTestRequestData.netTestProfile.framesPerSecond, 90); const capturedRequestBody = requestBody as CapturedSessionRequestBody | null; assert.ok(capturedRequestBody); assert.equal(capturedRequestBody.sessionRequestData.clientRequestMonitorSettings[0]?.framesPerSecond, 90); @@ -335,43 +393,133 @@ test("CloudMatch pins the resolved region with a network-test session before cre assert.equal(capturedRequestBody.sessionRequestData.requestedStreamingFeatures.audioChannelCount, 2); assert.equal(capturedRequestBody.sessionRequestData.appLaunchMode, 2); assert.equal(capturedRequestBody.sessionRequestData.enablePersistingInGameSettings, false); - assert.equal(capturedRequestBody.sessionRequestData.networkTestSessionId, "nettest-lax-1"); + assert.equal(capturedRequestBody.sessionRequestData.networkTestSessionId, null); + assert.equal(capturedRequestBody.sessionRequestData.appId, 1001); + assert.equal(capturedRequestBody.sessionRequestData.internalTitle, null); + assert.equal(capturedRequestBody.sessionRequestData.accountLinked, false); + assert.match(capturedRequestBody.sessionRequestData.deviceHashId ?? "", /^[0-9a-f]{64}$/); } finally { globalThis.fetch = originalFetch; console.warn = originalWarn; } }); -test("CloudMatch pins a manually selected zone before creating a session", async (t) => { +test("CloudMatch NVST create posts to regional host then sends official RESUME", async (t) => { const originalFetch = globalThis.fetch; const originalLog = console.log; + const originalWarn = console.warn; const calls: string[] = []; - let networkTestSessionId: string | null | undefined; - const base = "https://np-mia-04.cloudmatchbeta.nvidiagrid.net"; - const expectedSessionUrl = `${base}/v2/session?${new URLSearchParams({ + let resumeBodyJson: unknown = null; + + const regionalBase = "https://eu-netherlands-north.cloudmatchbeta.nvidiagrid.net"; + const query = new URLSearchParams({ keyboardLayout: resolveGfnKeyboardLayout(DEFAULT_KEYBOARD_LAYOUT, process.platform), languageCode: "en_US", - }).toString()}`; + }).toString(); + const expectedPostUrl = `${regionalBase}/v2/session?${query}`; + const expectedResumeUrl = `${regionalBase}/v2/session/session-nvst-1?${query}`; console.log = () => {}; + console.warn = () => {}; t.after(() => { globalThis.fetch = originalFetch; console.log = originalLog; + console.warn = originalWarn; }); globalThis.fetch = (async (input, init) => { const url = String(input); calls.push(url); - if (url === `${base}/v2/nettestsession`) { + if (url === "https://prod.cloudmatchbeta.nvidiagrid.net/v2/serverInfo") { return new Response(JSON.stringify({ - requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS", serverId: "NP-MIA-04" }, - netTestSession: { - sessionId: "nettest-mia-1", + requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS", serverId: "NP-AMS-06" }, + metaData: [ + { key: "local-region", value: "EU Northwest" }, + { key: "gfn-regions", value: "EU Northwest, Netherlands North" }, + { key: "EU Northwest", value: "https://np-ams-06.cloudmatchbeta.nvidiagrid.net/" }, + { key: "Netherlands North", value: "https://eu-netherlands-north.cloudmatchbeta.nvidiagrid.net/" }, + ], + }), { status: 200 }); + } + + if (url === expectedPostUrl && (init?.method ?? "GET") === "POST") { + return new Response(JSON.stringify({ + requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS" }, + session: { + sessionId: "session-nvst-1", + status: 1, + seatSetupInfo: { seatSetupStep: 0 }, + sessionControlInfo: { ip: "np-ams-06.cloudmatchbeta.nvidiagrid.net" }, + connectionInfo: [], + iceServerConfiguration: { + iceServers: [{ urls: "stun:127.0.0.1:19302" }], + }, }, }), { status: 200 }); } + if (url === expectedResumeUrl && init?.method === "PUT") { + resumeBodyJson = JSON.parse(String(init.body)); + return new Response(JSON.stringify({ + requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS" }, + session: { sessionId: "session-nvst-1", status: 1 }, + }), { status: 200 }); + } + + throw new Error(`Unexpected fetch: ${url} ${init?.method ?? "GET"}`); + }) as typeof fetch; + + const session = await createSession({ + token: "token", + streamingBaseUrl: "https://prod.cloudmatchbeta.nvidiagrid.net/", + appId: "1001", + internalTitle: "Test Game", + zone: "prod", + settings: makeSettings({ transportMode: "nvst", resolution: "2560x1600", fps: 120 }), + }); + + assert.deepEqual(calls, [ + "https://prod.cloudmatchbeta.nvidiagrid.net/v2/serverInfo", + expectedPostUrl, + expectedResumeUrl, + ]); + const resumeBody = resumeBodyJson as { + action?: number; + data?: string; + sessionRequestData?: { + requestedStreamingFeatures?: { bitDepth?: number; reflex?: boolean; chromaFormat?: number }; + }; + } | null; + assert.equal(resumeBody?.action, 2); + assert.equal(resumeBody?.data, "RESUME"); + assert.equal(resumeBody?.sessionRequestData?.requestedStreamingFeatures?.bitDepth, 1); + assert.equal(resumeBody?.sessionRequestData?.requestedStreamingFeatures?.reflex, true); + assert.equal(resumeBody?.sessionRequestData?.requestedStreamingFeatures?.chromaFormat, 0); + assert.equal(session.sessionId, "session-nvst-1"); +}); + +test("CloudMatch pins a manually selected zone before creating a session", async (t) => { + const originalFetch = globalThis.fetch; + const originalLog = console.log; + const calls: string[] = []; + let networkTestSessionId: string | null | undefined; + const base = "https://np-mia-04.cloudmatchbeta.nvidiagrid.net"; + const expectedSessionUrl = `${base}/v2/session?${new URLSearchParams({ + keyboardLayout: resolveGfnKeyboardLayout(DEFAULT_KEYBOARD_LAYOUT, process.platform), + languageCode: "en_US", + }).toString()}`; + + console.log = () => {}; + t.after(() => { + globalThis.fetch = originalFetch; + console.log = originalLog; + }); + + globalThis.fetch = (async (input, init) => { + const url = String(input); + calls.push(url); + if (url === expectedSessionUrl) { const body = JSON.parse(String(init?.body)) as { sessionRequestData: { @@ -412,10 +560,9 @@ test("CloudMatch pins a manually selected zone before creating a session", async }); assert.deepEqual(calls, [ - `${base}/v2/nettestsession`, expectedSessionUrl, ]); - assert.equal(networkTestSessionId, "nettest-mia-1"); + assert.equal(networkTestSessionId, null); }); test("CloudMatch retries transient serverInfo failures before creating a session", async () => { @@ -449,15 +596,6 @@ test("CloudMatch retries transient serverInfo failures before creating a session }), { status: 200 }); } - if (url === "https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/nettestsession") { - return new Response(JSON.stringify({ - requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS", serverId: "NP-LAX-01" }, - netTestSession: { - sessionId: "nettest-lax-retry", - }, - }), { status: 200 }); - } - if (url === expectedSessionUrl) { const body = JSON.parse(String(init?.body)) as { sessionRequestData: { @@ -500,7 +638,6 @@ test("CloudMatch retries transient serverInfo failures before creating a session assert.deepEqual(calls, [ "https://prod.cloudmatchbeta.nvidiagrid.net/v2/serverInfo", "https://prod.cloudmatchbeta.nvidiagrid.net/v2/serverInfo", - "https://np-lax-01.cloudmatchbeta.nvidiagrid.net/v2/nettestsession", expectedSessionUrl, ]); } finally { @@ -684,11 +821,21 @@ test("CloudMatch claim keeps the session-stable appLaunchMode over live settings requestStatus: { statusCode: 1, statusDescription: "SUCCESS_STATUS" }, session: { sessionId: "sess-1", + subSessionId: "subsess-1", status: 3, sessionControlInfo: { ip: "203.0.113.10" }, connectionInfo: [ { ip: "203.0.113.10", port: 443, usage: 14, resourcePath: "/nvst/" }, - { ip: "203.0.113.10", port: 49006, usage: 2 }, + { ip: "203.0.113.11", port: 49006, usage: 15, protocol: 2 }, + { ip: "203.0.113.13", port: 49007, usage: 17, protocol: 2 }, + { + ip: "203.0.113.12", + port: 48322, + usage: 16, + protocol: 1, + appLevelProtocol: 6, + resourcePath: "rtsps://203.0.113.12:48322/session", + }, ], iceServerConfiguration: { iceServers: [{ urls: "stun:127.0.0.1:19302" }], @@ -711,7 +858,7 @@ test("CloudMatch claim keeps the session-stable appLaunchMode over live settings try { // Session created as gamepad-friendly (wire 2) must stay gamepad-friendly on // resume even if the live settings toggles now say "default". - await claimSession({ + const claimed = await claimSession({ token: "token", sessionId: "sess-1", serverIp: "203.0.113.10", @@ -719,6 +866,26 @@ test("CloudMatch claim keeps the session-stable appLaunchMode over live settings appLaunchMode: 2, settings: makeSettings(), }); + assert.deepEqual(claimed.connectionInfo, [ + { ip: "203.0.113.10", port: 443, usage: 14, resourcePath: "/nvst/" }, + { ip: "203.0.113.11", port: 49006, usage: 15, protocol: 2 }, + { ip: "203.0.113.13", port: 49007, usage: 17, protocol: 2 }, + { + ip: "203.0.113.12", + port: 48322, + usage: 16, + protocol: 1, + appLevelProtocol: 6, + resourcePath: "rtsps://203.0.113.12:48322/session", + }, + ]); + assert.equal(claimed.subSessionId, "subsess-1"); + assert.deepEqual(claimed.mediaConnectionInfo, { + ip: "203.0.113.13", + port: 49007, + usage: 17, + }); + assert.deepEqual(claimed.rtspsEndpoints, ["rtsps://203.0.113.12:48322/session"]); // Without a session-stable value the claim falls back to the settings-derived mode. await claimSession({ diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatch.ts b/opennow-stable/src/main/platforms/gfn/cloudmatch.ts index 8b6d0e152..47e64889d 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatch.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatch.ts @@ -1,5 +1,3 @@ -import crypto from "node:crypto"; - import type { ActiveSessionInfo, SessionAdAction, @@ -21,8 +19,9 @@ import { SessionError } from "./errorCodes"; import { buildGfnCloudMatchClaimHeaders, buildGfnCloudMatchHeaders, + LCARS_CLIENT_ID, } from "./clientHeaders"; -import { getStableDeviceId } from "./deviceId"; +import { getCloudMatchDeviceHashId } from "./deviceId"; import { readCloudMatchJson, throwIfCloudMatchResponseError, @@ -49,7 +48,6 @@ import { buildClaimRequestBody, buildSessionRequestBody, } from "./cloudmatchSessionRequest"; -import { createNetworkTestSession } from "./networkTestSession"; import { echoedSessionAppLaunchMode, extractAdState, @@ -71,6 +69,7 @@ export { export { extractServerInfoRegionBases } from "./cloudmatchTransport"; const SESSION_MODIFY_ACTION_AD_UPDATE = 6; +const SESSION_MODIFY_ACTION_RESUME = 2; const AD_ACTION_CODES: Record = { start: 1, @@ -90,8 +89,8 @@ export async function createSession(input: SessionCreateRequest): Promise(response); + + // Official Bifrost follows every fresh create with an immediate + // PUT action=2 RESUME carrying the same full sessionRequestData (fresh + // SubSessionId). That explicit RESUME starts seat setup on the modern hex + // ping-hash streamer pool; sessions left to auto-start from POST alone land + // on the legacy literal-"PING" pool that never completes NVST hole-punch. + if (input.settings.transportMode === "nvst" && payload.session?.sessionId) { + await resumeFreshNvstSession({ + base, + sessionId: payload.session.sessionId, + input, + clientId, + deviceId, + keyboardLayout, + languageCode, + }); + } + return await toSessionInfo({ zone: input.zone, streamingBaseUrl: base, @@ -138,14 +161,63 @@ export async function createSession(input: SessionCreateRequest): Promise { - if (!input.token) { +/** + * Official fresh-create parity for the classic (NVST) streamer: immediately + * RESUME the just-created session with the full sessionRequestData. Failures + * are logged and ignored — the caller still polls to readiness as before. + */ +async function resumeFreshNvstSession(args: { + base: string; + sessionId: string; + input: SessionCreateRequest; + clientId: string; + deviceId: string; + keyboardLayout: string; + languageCode: string; +}): Promise { + const { base, sessionId, input, clientId, deviceId, keyboardLayout, languageCode } = args; + // Rebuild the body so SubSessionId is fresh, mirroring the official client. + const resumeBody = buildSessionRequestBody(input, deviceId, null); + const payload = { + action: SESSION_MODIFY_ACTION_RESUME, + data: "RESUME", + sessionRequestData: resumeBody.sessionRequestData, + metaData: null, + adUpdates: null, + }; + const url = `${base}/v2/session/${sessionId}?${new URLSearchParams({ keyboardLayout, languageCode }).toString()}`; + console.log(`[CloudMatch] createSession RESUME PUT ${url} (official fresh-create parity)`); + try { + const response = await fetchCloudMatch(url, { + method: "PUT", + headers: buildGfnCloudMatchHeaders({ token: input.token as string, clientId, deviceId, includeOrigin: false }), + body: JSON.stringify(payload), + }, { proxyUrl: input.proxyUrl }); + const text = await response.text(); + let statusCode = -1; + try { + statusCode = (JSON.parse(text) as CloudMatchResponse).requestStatus?.statusCode ?? -1; + } catch { + // Keep -1 for unparsable bodies; the warning below carries the HTTP status. + } + console.log(`[CloudMatch] createSession RESUME response: HTTP ${response.status}, requestStatus=${statusCode}`); + if (!response.ok || statusCode !== 1) { + console.warn( + `[CloudMatch] createSession RESUME not accepted (HTTP ${response.status}, status=${statusCode}); continuing with poll-based setup`, + ); + } + } catch (error) { + console.warn(`[CloudMatch] createSession RESUME failed: ${formatErrorForLog(error)}; continuing with poll-based setup`); + } +} + +export async function pollSession(input: SessionPollRequest): Promise { if (!input.token) { throw new Error("Missing token for session polling"); } // Use provided client/device IDs if available (should match session creation) - const clientId = input.clientId ?? crypto.randomUUID(); - const deviceId = input.deviceId ?? crypto.randomUUID(); + const clientId = input.clientId ?? LCARS_CLIENT_ID; + const deviceId = input.deviceId ?? getCloudMatchDeviceHashId(); const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); const baseHost = new URL(base).hostname; @@ -208,8 +280,8 @@ export async function reportSessionAd(input: SessionAdReportRequest): Promise { } // Use provided client/device IDs if available (should match session creation) - const clientId = input.clientId ?? crypto.randomUUID(); - const deviceId = input.deviceId ?? crypto.randomUUID(); + const clientId = input.clientId ?? LCARS_CLIENT_ID; + const deviceId = input.deviceId ?? getCloudMatchDeviceHashId(); const base = resolvePollStopBase(input.zone, input.streamingBaseUrl, input.serverIp); const url = `${base}/v2/session/${input.sessionId}`; @@ -303,7 +375,7 @@ export async function getActiveSessions( const base = normalizeTrustedCloudMatchBaseUrl(streamingBaseUrl); const headers = buildGfnCloudMatchHeaders({ token, - deviceId: getStableDeviceId(), + deviceId: getCloudMatchDeviceHashId(), includeOrigin: false, }); const primary = await fetchActiveSessionsFromBase(base, headers); @@ -435,6 +507,7 @@ async function fetchActiveSessionsFromBase( return { sessionId: s.sessionId, + subSessionId: s.subSessionId, appId, appLaunchMode, enablePersistingInGameSettings, @@ -462,8 +535,8 @@ export async function claimSession(input: SessionClaimRequest): Promise(response, { - onText: (text) => { - console.log(`[CloudMatch] claimSession response: HTTP ${response.status}`); - console.log(`[CloudMatch] claimSession response body FULL: ${text}`); - }, - }); + const { text, payload: apiResponse } = await readCloudMatchJson(response); + console.log( + `[CloudMatch] claimSession response: HTTP ${response.status}, requestStatus=${apiResponse.requestStatus.statusCode}, sessionStatus=${apiResponse.session?.status ?? "n/a"}, connectionInfo=${apiResponse.session?.connectionInfo?.length ?? 0}`, + ); if (apiResponse.requestStatus.statusCode !== 1) { throw SessionError.fromResponse(200, text); @@ -642,6 +712,7 @@ export async function claimSession(input: SessionClaimRequest): Promise ({ ...connection })), rtspsEndpoints: signaling.rtspsEndpoints.length > 0 ? signaling.rtspsEndpoints : undefined, iceServers: await normalizeIceServers(pollApiResponse), mediaConnectionInfo: signaling.mediaConnectionInfo, diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts index 201be03cf..64fae5219 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchFeatures.ts @@ -26,10 +26,11 @@ export function buildRequestedStreamingFeatures( chromaFormat: number, _hdrEnabled: boolean, supportedCodecs?: readonly VideoCodec[], + transportMode: StreamSettings["transportMode"] = settings.transportMode, ): CloudMatchRequest["sessionRequestData"]["requestedStreamingFeatures"] { const cloudGsync = settings.enableCloudGsync; - return { + const commonFeatures = { reflex: shouldRequestReflex(settings), bitDepth, cloudGsync, @@ -42,6 +43,25 @@ export function buildRequestedStreamingFeatures( prefilterSharpness: 0, prefilterNoiseReduction: 0, hudStreamingMode: 0, + }; + + if (transportMode === "nvst") { + const sku = resolveNvstCreateStreamSku(settings); + return { + ...commonFeatures, + reflex: sku.reflex, + bitDepth: sku.bitDepth, + chromaFormat: sku.chromaFormat, + mouseMovementFlags: 0, + trueHdr: false, + hidDevices: null, + qosPolicy: 0, + touchSupport: false, + }; + } + + return { + ...commonFeatures, maxBitrateKbps: Math.round(settings.maxBitrateMbps * 1000), codec: resolveRequestedCodecWireValue( codecWireValue(settings.codec), @@ -96,6 +116,16 @@ export function shouldRequestReflex(settings: StreamSettings): boolean { return settings.enableCloudGsync || settings.fps >= reflexMinimum; } +/** Official Mac Bifrost NVST create advertises 10-bit + reflex even at 8-bit UI quality. */ +export function resolveNvstCreateStreamSku(settings: StreamSettings): { + bitDepth: number; + chromaFormat: number; + reflex: boolean; +} { + void settings; + return { bitDepth: 1, chromaFormat: 0, reflex: true }; +} + export function shouldEnableInGameSettingsPersistence( input: Pick, ): boolean { diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts index f89ff887c..79f2cfefc 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionParsing.ts @@ -9,6 +9,7 @@ import type { import type { CloudMatchResponse, GetSessionsResponse } from "./types"; import { SessionError } from "./errorCodes"; +import { resolveSessionControlBaseUrl } from "./cloudmatchTransport"; import { normalizeIceServers, resolveSignaling, @@ -430,13 +431,14 @@ export async function toSessionInfo(options: ToSessionInfoOptions): Promise 0 ? connections.map((connection) => ({ ...connection })) : undefined, rtspsEndpoints: signaling.rtspsEndpoints.length > 0 ? signaling.rtspsEndpoints : undefined, iceServers: await normalizeIceServers(payload), mediaConnectionInfo: signaling.mediaConnectionInfo, diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts index 82e97c235..6ad2baae1 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSessionRequest.ts @@ -7,14 +7,33 @@ import { } from "@shared/gfn"; import type { CloudMatchRequest } from "./types"; +import { GFN_CLIENT_IDENTIFICATION } from "./clientHeaders"; import { resolveGfnDeviceIdentity } from "./deviceIdentity"; -import { getStableDeviceId } from "./deviceId"; +import { getCloudMatchDeviceHashId } from "./deviceId"; import { appLaunchModeWireValue, buildRequestedStreamingFeatures, + resolveNvstCreateStreamSku, shouldEnableInGameSettingsPersistence, } from "./cloudmatchFeatures"; +/** Official native Mac Bifrost `availableSupportedControllers` / `preferredController`. */ +const OFFICIAL_GAMEPAD_CONTROLLER = 2; + +const EMPTY_DISPLAY_DATA = { + displayPrimaryX0: 0, + displayPrimaryY0: 0, + displayPrimaryX1: 0, + displayPrimaryY1: 0, + displayPrimaryX2: 0, + displayPrimaryY2: 0, + displayWhitePointX: 0, + displayWhitePointY: 0, + desiredContentMaxLuminance: 0, + desiredContentMinLuminance: 0, + desiredContentMaxFrameAverageLuminance: 0, +} as const; + export function parseResolution(input: string): { width: number; height: number } { const [rawWidth, rawHeight] = input.split("x"); const width = Number.parseInt(rawWidth ?? "", 10); @@ -31,17 +50,68 @@ export function timezoneOffsetMs(): number { return -new Date().getTimezoneOffset() * 60 * 1000; } -export function webRtcSessionMetadata(width: number, height: number): Array<{ key: string; value: string }> { +function defaultMonitorDpi(): number { + return process.platform === "darwin" ? 144 : 96; +} + +function defaultNetworkType(): string { + return process.platform === "darwin" ? "WiFi5.0" : "Unknown"; +} + +function readPrimaryDisplayMetrics(): { + dpi: number; + horizontalPixels: number; + verticalPixels: number; +} | null { + try { + const electron = require("electron") as typeof import("electron"); + const display = electron.screen?.getPrimaryDisplay?.(); + if (!display) { + return null; + } + const scale = display.scaleFactor > 0 ? display.scaleFactor : 1; + const dpi = Math.round(scale * 72); + return { + dpi: dpi > 0 ? dpi : defaultMonitorDpi(), + horizontalPixels: Math.round(display.size.width * scale), + verticalPixels: Math.round(display.size.height * scale), + }; + } catch { + return null; + } +} + +function officialHdrCapabilities(): NonNullable< + CloudMatchRequest["sessionRequestData"]["clientDisplayHdrCapabilities"] +> { + return { + version: 2, + hdrEdrSupportedFlagsInUint32: 1, + static_metadata_descriptor_id: 0, + display_data: { ...EMPTY_DISPLAY_DATA }, + }; +} + +export function sessionMetadata( + width: number, + height: number, + transportMode: StreamSettings["transportMode"], +): Array<{ key: string; value: string }> { + const display = readPrimaryDisplayMetrics(); + const physical = { + horizontalPixels: display?.horizontalPixels ?? width, + verticalPixels: display?.verticalPixels ?? height, + }; return [ - { key: "SubSessionId", value: crypto.randomUUID() }, - { key: "wssignaling", value: "1" }, - { key: "GSStreamerType", value: "WebRTC" }, - { key: "networkType", value: "Unknown" }, { key: "ClientImeSupport", value: "0" }, + { key: "SubSessionId", value: crypto.randomUUID() }, { key: "clientPhysicalResolution", - value: JSON.stringify({ horizontalPixels: width, verticalPixels: height }), + value: JSON.stringify(physical), }, + { key: "networkType", value: defaultNetworkType() }, + { key: "wssignaling", value: "1" }, + ...(transportMode === "nvst" ? [] : [{ key: "GSStreamerType", value: "WebRTC" }]), { key: "surroundAudioInfo", value: "2" }, ]; } @@ -59,24 +129,38 @@ export function buildSessionRequestBody( // Conflating them caused the server to set up an HDR pipeline, which // dynamically downscaled resolution to ~540p. const hdrEnabled = false; // No HDR toggle implemented yet; hardcode off like claim body - const bitDepth = colorQualityBitDepth(cq); - const chromaFormat = colorQualityChromaFormat(cq); - const accountLinked = input.accountLinked ?? true; + const useClassicStreamer = input.settings.transportMode === "nvst"; + const streamSku = useClassicStreamer + ? resolveNvstCreateStreamSku(input.settings) + : { + bitDepth: colorQualityBitDepth(cq), + chromaFormat: colorQualityChromaFormat(cq), + }; + const bitDepth = streamSku.bitDepth; + const chromaFormat = streamSku.chromaFormat; + const accountLinked = false; + // Official Mac advertises HDR capability (sdrHdrMode 1 + caps v2) with zero luminance. + // Do not send desiredContentMaxLuminance>0 — that previously downscaled to ~540p. + const advertiseOfficialHdrCaps = useClassicStreamer && process.platform === "darwin"; + const sdrHdrMode = hdrEnabled || advertiseOfficialHdrCaps ? 1 : 0; + const display = readPrimaryDisplayMetrics(); return { sessionRequestData: { - appId: input.appId, - internalTitle: input.internalTitle || null, - availableSupportedControllers: [], + appId: parseInt(input.appId, 10), + externalAppId: null, + internalTitle: null, + availableSupportedControllers: [OFFICIAL_GAMEPAD_CONTROLLER], + preferredController: OFFICIAL_GAMEPAD_CONTROLLER, networkTestSessionId, parentSessionId: null, - clientIdentification: "GFN-PC", + clientIdentification: GFN_CLIENT_IDENTIFICATION, // Keep device identity stable across create -> reconnect/resume flows. // The official client preserves this identity, and resume reliability depends on it. deviceHashId, clientVersion: "30.0", - sdkVersion: "1.0", - streamerVersion: 1, + sdkVersion: "2.0", + streamerVersion: "14", clientPlatformName: resolveGfnDeviceIdentity().clientPlatformName, clientRequestMonitorSettings: [ { @@ -86,46 +170,39 @@ export function buildSessionRequestBody( widthInPixels: width, heightInPixels: height, framesPerSecond: input.settings.fps, - sdrHdrMode: hdrEnabled ? 1 : 0, - displayData: hdrEnabled - ? { - desiredContentMaxLuminance: 1000, - desiredContentMinLuminance: 0, - desiredContentMaxFrameAverageLuminance: 500, - } - : {}, + sdrHdrMode, + displayData: { ...EMPTY_DISPLAY_DATA }, hdr10PlusGamingData: null, - dpi: 0, + dpi: display?.dpi ?? defaultMonitorDpi(), }, ], useOps: true, audioMode: 2, - metaData: webRtcSessionMetadata(width, height), - sdrHdrMode: hdrEnabled ? 1 : 0, - clientDisplayHdrCapabilities: hdrEnabled - ? { - version: 1, - hdrEdrSupportedFlagsInUint32: 1, - staticMetadataDescriptorId: 0, - } + metaData: sessionMetadata(width, height, input.settings.transportMode), + sdrHdrMode, + clientDisplayHdrCapabilities: advertiseOfficialHdrCaps || hdrEnabled + ? officialHdrCapabilities() : null, surroundAudioInfo: 0, remoteControllersBitmap: 0, clientTimezoneOffset: timezoneOffsetMs(), - enhancedStreamMode: 1, + enhancedStreamMode: 0, appLaunchMode: appLaunchModeWireValue(input.settings.appLaunchMode), - secureRTSPSupported: false, - partnerCustomData: "", + secureRTSPSupported: useClassicStreamer, + partnerCustomData: null, accountLinked, enablePersistingInGameSettings: shouldEnableInGameSettingsPersistence(input), - userAge: 26, + requestedAudioFormat: 0, + userAge: 25, requestedStreamingFeatures: buildRequestedStreamingFeatures( input.settings, bitDepth, chromaFormat, hdrEnabled, input.supportedCodecs, + input.settings.transportMode, ), + transport: null, }, }; } @@ -144,9 +221,10 @@ export function buildClaimRequestBody( // The session is already configured on the server side. Sending different fps, resolution, // codec, etc. causes HTTP 400 from the server because those parameters are immutable for // an already-streaming session. Only send the action and minimal required fields. - const deviceId = getStableDeviceId(); + const deviceId = getCloudMatchDeviceHashId(); const subSessionId = crypto.randomUUID(); const timezoneMs = timezoneOffsetMs(); + const useClassicStreamer = settings.transportMode === "nvst"; return { action: 2, @@ -155,39 +233,44 @@ export function buildClaimRequestBody( // Minimal fields required for resume - NO streaming parameter renegotiation audioMode: 2, remoteControllersBitmap: 0, - sdrHdrMode: 0, + sdrHdrMode: useClassicStreamer && process.platform === "darwin" ? 1 : 0, networkTestSessionId: null, - availableSupportedControllers: [], + availableSupportedControllers: [OFFICIAL_GAMEPAD_CONTROLLER], + preferredController: OFFICIAL_GAMEPAD_CONTROLLER, clientVersion: "30.0", deviceHashId: deviceId, internalTitle: null, clientPlatformName: resolveGfnDeviceIdentity().clientPlatformName, metaData: [ + { key: "ClientImeSupport", value: "0" }, { key: "SubSessionId", value: subSessionId }, + { key: "networkType", value: defaultNetworkType() }, { key: "wssignaling", value: "1" }, - { key: "GSStreamerType", value: "WebRTC" }, - { key: "networkType", value: "Unknown" }, - { key: "ClientImeSupport", value: "0" }, + ...(useClassicStreamer ? [] : [{ key: "GSStreamerType", value: "WebRTC" }]), { key: "surroundAudioInfo", value: "2" }, ], surroundAudioInfo: 0, clientTimezoneOffset: timezoneMs, - clientIdentification: "GFN-PC", + clientIdentification: GFN_CLIENT_IDENTIFICATION, parentSessionId: null, appId: parseInt(appId, 10), - streamerVersion: 1, + streamerVersion: "14", // Resume must not renegotiate session parameters: prefer the wire value the // session was created with over whatever the UI toggles currently say. appLaunchMode: sessionAppLaunchMode ?? appLaunchModeWireValue(settings.appLaunchMode), - sdkVersion: "1.0", - enhancedStreamMode: 1, + sdkVersion: "2.0", + enhancedStreamMode: 0, useOps: true, - clientDisplayHdrCapabilities: null, - accountLinked: true, - partnerCustomData: "", + clientDisplayHdrCapabilities: useClassicStreamer && process.platform === "darwin" + ? officialHdrCapabilities() + : null, + accountLinked: false, + partnerCustomData: null, enablePersistingInGameSettings, - secureRTSPSupported: false, - userAge: 26, + requestedAudioFormat: 0, + secureRTSPSupported: useClassicStreamer, + userAge: 25, + transport: null, }, metaData: [], }; diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.test.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.test.ts new file mode 100644 index 000000000..7eaafff01 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.test.ts @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { CloudMatchResponse } from "./types"; +import { + buildSignalingUrl, + normalizeIceServers, + resolveMediaConnectionInfo, +} from "./cloudmatchSignaling"; + +test("normalizeIceServers preserves supplied hostnames, schemes, and credentials", async () => { + const response = { + session: { + iceServerConfiguration: { + iceServers: [{ + urls: ["turns:relay.example.test:443?transport=tcp"], + username: "synthetic-user", + credential: "synthetic-credential", + }], + }, + }, + } as CloudMatchResponse; + + assert.deepEqual(await normalizeIceServers(response), [{ + urls: ["turns:relay.example.test:443?transport=tcp"], + username: "synthetic-user", + credential: "synthetic-credential", + }]); +}); + +test("buildSignalingUrl preserves the supplied authority, path, and query", () => { + assert.deepEqual( + buildSignalingUrl( + "rtsps://signal.example.test:48322/custom/path?ticket=synthetic", + "198.51.100.1", + ), + { + signalingUrl: "wss://signal.example.test:48322/custom/path?ticket=synthetic", + signalingHost: "signal.example.test:48322", + }, + ); +}); + +test("WebRTC media projection ignores native MEDIA and prefers legacy VIDEO over BUNDLE", () => { + assert.deepEqual( + resolveMediaConnectionInfo([ + { ip: "198.51.100.15", port: 49015, usage: 15 }, + { ip: "198.51.100.17", port: 49017, usage: 17 }, + { ip: "198.51.100.2", port: 49002, usage: 2 }, + ], "198.51.100.1"), + { ip: "198.51.100.2", port: 49002, usage: 2 }, + ); + + assert.equal( + resolveMediaConnectionInfo([ + { ip: "198.51.100.14", port: 443, usage: 14 }, + { ip: "198.51.100.15", port: 49015, usage: 15 }, + { ip: "198.51.100.16", port: 48322, usage: 16 }, + ], "198.51.100.1", { logMissing: false }), + undefined, + ); +}); diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts index aaa27593e..6eb93c42c 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchSignaling.ts @@ -1,5 +1,3 @@ -import dns from "node:dns"; - import type { IceServer, MediaConnectionInfo } from "@shared/gfn"; import type { CloudMatchResponse } from "./types"; @@ -12,35 +10,6 @@ export function isReadySessionStatus(status: number): boolean { return READY_SESSION_STATUSES.has(status); } -async function resolveHostnameWithFallback(hostname: string): Promise { - // Try system resolver first, then fall back to Cloudflare (1.1.1.1) and Google (8.8.8.8) - try { - const r = await dns.promises.lookup(hostname); - if (r && (r as any).address) return (r as any).address; - } catch { - // ignore and try custom resolvers - } - - const fallbackServers = ["1.1.1.1", "8.8.8.8"]; - for (const server of fallbackServers) { - try { - const resolver = new dns.Resolver(); - resolver.setServers([server]); - const addrs: string[] = await new Promise((resolve, reject) => { - resolver.resolve4(hostname, (err, addresses) => { - if (err) reject(err); - else resolve(addresses); - }); - }); - if (addrs && addrs.length > 0) return addrs[0]; - } catch { - // try next fallback - } - } - - return null; -} - export async function normalizeIceServers(response: CloudMatchResponse): Promise { const raw = response.session.iceServerConfiguration?.iceServers ?? []; const servers = raw @@ -54,68 +23,13 @@ export async function normalizeIceServers(response: CloudMatchResponse): Promise }) .filter((entry) => entry.urls.length > 0); - if (servers.length > 0) { - // Attempt to resolve any hostnames in STUN/TURN URLs to IPs to avoid relying on the - // renderer's DNS resolution. This makes it possible to try alternate DNS servers - // when the system resolver fails. - const resolvedServers: IceServer[] = []; - for (const s of servers) { - const resolvedUrls: string[] = []; - for (const u of s.urls) { - try { - const m = u.match(/^([a-zA-Z0-9+.-]+):([^/]+)/); - if (m) { - const scheme = m[1]; - const hostPort = m[2]; - const host = hostPort.split(":")[0]; - const portPart = hostPort.includes(":") ? ":" + hostPort.split(":").slice(1).join(":") : ""; - - // Helper to bracket IPv6 literals when necessary - const bracketIfIpv6 = (h: string) => { - if (h.startsWith("[") && h.endsWith("]")) return h; - // Heuristic: contains ':' and is not an IPv4 dotted-quad - if (h.includes(":") && !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(h)) { - return `[${h}]`; - } - return h; - }; - - // If host already looks like an IPv4 or bracketed IPv6, keep original URL - if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || /^\[[0-9a-fA-F:]+\]$/.test(host)) { - resolvedUrls.push(u); - } else { - const ip = await resolveHostnameWithFallback(host); - const finalHost = ip ?? host; - const maybeBracketted = bracketIfIpv6(finalHost); - resolvedUrls.push(`${scheme}:${maybeBracketted}${portPart}`); - } - } else { - resolvedUrls.push(u); - } - } catch { - resolvedUrls.push(u); - } - } - resolvedServers.push({ urls: resolvedUrls, username: s.username, credential: s.credential }); - } - - return resolvedServers; - } + if (servers.length > 0) return servers; - // Default fallbacks — try to resolve known STUN hostnames to IPs as well - const defaults = ["s1.stun.gamestream.nvidia.com:19308", "stun.l.google.com:19302", "stun1.l.google.com:19302"]; - const out: IceServer[] = []; - for (const d of defaults) { - const parts = d.split(":"); - const host = parts[0]; - const port = parts.length > 1 ? `:${parts.slice(1).join(":")}` : ""; - const ip = await resolveHostnameWithFallback(host); - const bracketIfIpv6 = (h: string) => (h.includes(":") && !h.startsWith("[") ? `[${h}]` : h); - if (ip) out.push({ urls: [`stun:${bracketIfIpv6(ip)}${port}`] }); - else out.push({ urls: [`stun:${bracketIfIpv6(host)}${port}`] }); - } - - return out; + return [ + { urls: ["stun:s1.stun.gamestream.nvidia.com:19308"] }, + { urls: ["stun:stun.l.google.com:19302"] }, + { urls: ["stun:stun1.l.google.com:19302"] }, + ]; } /** @@ -208,9 +122,15 @@ export function resolveSignaling(response: CloudMatchResponse): { const rtspsHost = connections - .map((connection) => typeof connection.resourcePath === "string" - ? extractHostFromUrl(connection.resourcePath) - : null) + .filter((connection) => + connection.usage === 16 + || connection.appLevelProtocol === 6 + || (typeof connection.resourcePath === "string" && /^rtsps?:\/\//i.test(connection.resourcePath)), + ) + .map((connection) => connection.ip + ?? (typeof connection.resourcePath === "string" + ? extractHostFromUrl(connection.resourcePath) + : null)) .find((host): host is string => Boolean(host)) ?? signalingHost ?? (isZoneHostname(serverIp) ? null : serverIp); @@ -228,16 +148,18 @@ export function resolveSignaling(response: CloudMatchResponse): { /** * Resolve the media connection endpoint (IP + port) from the session's connectionInfo array. - * Matches Rust's media_connection_info() priority chain: - * 1. usage=2 (Primary media path, UDP) - * 2. usage=17 (Alternative media path) - * 3. usage=14 with highest port (Alliance fallback — distinguishes media port from signaling port) - * 4. Fallback: use serverIp with the highest port from any usage=14 entry + * This is the compatibility projection used by the WebRTC path: + * 1. usage=2 (legacy VIDEO) + * 2. usage=17 (BUNDLE) + * + * The native path receives the complete ordered connectionInfo array and must + * select current native transports such as usage=15 (MEDIA) itself. Signaling + * (14), MEDIA (15), and RTSPS (16) must not be repurposed as WebRTC ICE endpoints. * * For each entry, IP is extracted from: * a. The .ip field directly * b. The hostname in .resourcePath (e.g. rtsps://80-250-97-40.server.net:48322) - * c. Fallback to serverIp (only for usage=14 Alliance fallback) + * CloudMatch usage=14 is signaling and must never be repurposed as a media endpoint. */ export function resolveMediaConnectionInfo( connections: Array<{ ip?: string; port: number; usage: number; protocol?: number; resourcePath?: string }>, @@ -278,7 +200,7 @@ export function resolveMediaConnectionInfo( return 0; }; - // Priority 1: usage=2 (Primary media path, UDP) + // Priority 1: usage=2 (legacy VIDEO) const primary = connections.find((c) => c.usage === 2); if (primary) { const ip = extractIp(primary); @@ -287,7 +209,7 @@ export function resolveMediaConnectionInfo( if (ip && port > 0) return { ip, port, usage: primary.usage }; } - // Priority 2: usage=17 (Alternative media path) + // Priority 2: usage=17 (BUNDLE) const alt = connections.find((c) => c.usage === 17); if (alt) { const ip = extractIp(alt); @@ -296,18 +218,6 @@ export function resolveMediaConnectionInfo( if (ip && port > 0) return { ip, port, usage: alt.usage }; } - // Priority 3: usage=14 with highest port (Alliance fallback) - const alliance = connections - .filter((c) => c.usage === 14) - .sort((a, b) => b.port - a.port); - - for (const conn of alliance) { - const ip = extractIp(conn) ?? serverIp; - const port = extractPort(conn); - console.log(`[CloudMatch] resolveMediaConnectionInfo: usage=14 candidate: ip=${ip}, port=${port} (serverIp fallback=${serverIp})`); - if (ip && port > 0) return { ip, port, usage: conn.usage }; - } - if (options?.logMissing ?? true) { console.log("[CloudMatch] resolveMediaConnectionInfo: NO valid media connection info found"); } @@ -323,28 +233,29 @@ export function buildSignalingUrl( serverIp: string, ): { signalingUrl: string; signalingHost: string | null } { if (raw.startsWith("rtsps://") || raw.startsWith("rtsp://")) { - // Extract hostname from RTSP URL, convert to wss:// - const withoutScheme = raw.startsWith("rtsps://") - ? raw.slice("rtsps://".length) - : raw.slice("rtsp://".length); - const host = withoutScheme.split(":")[0]?.split("/")[0]; - if (host && host.length > 0 && !host.startsWith(".")) { + const signalingUrl = `wss://${raw.slice(raw.indexOf("://") + 3)}`; + try { + const parsed = new URL(signalingUrl); + if (!parsed.hostname || parsed.hostname.startsWith(".")) throw new Error("invalid host"); return { - signalingUrl: `wss://${host}/nvst/`, - signalingHost: host, + signalingUrl, + signalingHost: parsed.host, + }; + } catch { + return { + signalingUrl: `wss://${serverIp}:443/nvst/`, + signalingHost: null, }; } - return { - signalingUrl: `wss://${serverIp}:443/nvst/`, - signalingHost: null, - }; } if (raw.startsWith("wss://")) { // Already a full WSS URL, use as-is; extract host - const withoutScheme = raw.slice("wss://".length); - const host = withoutScheme.split("/")[0] ?? null; - return { signalingUrl: raw, signalingHost: host }; + try { + return { signalingUrl: raw, signalingHost: new URL(raw).host }; + } catch { + return { signalingUrl: raw, signalingHost: null }; + } } if (raw.startsWith("/")) { diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts index f97eba098..57ae94497 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.test.ts @@ -4,6 +4,9 @@ import test from "node:test"; import { isZoneHostname, normalizeTrustedCloudMatchBaseUrl, + resolvePollStopBase, + resolveSessionControlBaseUrl, + selectCreateSessionBase, } from "./cloudmatchTransport"; test("isZoneHostname accepts NVIDIA CloudMatch domains and their subdomains", () => { @@ -18,6 +21,50 @@ test("isZoneHostname rejects hostnames that only contain a CloudMatch domain sub assert.equal(isZoneHostname("cloudmatchbeta.nvidiagrid.net.evil.test"), false); }); +test("session control base follows official zone-LB poll host", () => { + assert.equal( + resolveSessionControlBaseUrl( + "np-ams-06.cloudmatchbeta.nvidiagrid.net", + "https://eu-netherlands-north.cloudmatchbeta.nvidiagrid.net", + ), + "https://np-ams-06.cloudmatchbeta.nvidiagrid.net", + ); + assert.equal( + resolveSessionControlBaseUrl("203.0.113.10", "https://np-lax-01.cloudmatchbeta.nvidiagrid.net"), + "https://np-lax-01.cloudmatchbeta.nvidiagrid.net", + ); +}); + +test("create host prefers regional metro URL over zone LB when available", () => { + assert.equal( + selectCreateSessionBase([ + "https://np-frk-08.cloudmatchbeta.nvidiagrid.net", + "https://eu-netherlands-north.cloudmatchbeta.nvidiagrid.net", + "https://np-ams-06.cloudmatchbeta.nvidiagrid.net", + ]), + "https://eu-netherlands-north.cloudmatchbeta.nvidiagrid.net", + ); + assert.equal( + selectCreateSessionBase(["https://np-lax-01.cloudmatchbeta.nvidiagrid.net"]), + "https://np-lax-01.cloudmatchbeta.nvidiagrid.net", + ); +}); + +test("poll/stop base uses assigned CloudMatch zone host or real seat IP", () => { + assert.equal( + resolvePollStopBase( + "prod", + "https://eu-netherlands-north.cloudmatchbeta.nvidiagrid.net", + "np-ams-06.cloudmatchbeta.nvidiagrid.net", + ), + "https://np-ams-06.cloudmatchbeta.nvidiagrid.net", + ); + assert.equal( + resolvePollStopBase("prod", "https://np-lax-01.cloudmatchbeta.nvidiagrid.net", "203.0.113.10"), + "https://203.0.113.10", + ); +}); + test("trusted CloudMatch endpoints require a clean NVIDIA HTTPS origin", () => { assert.equal( normalizeTrustedCloudMatchBaseUrl("https://NP-AMS-06.CLOUDMATCHBETA.NVIDIAGRID.NET./"), diff --git a/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts index 048eca54b..afd967783 100644 --- a/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts +++ b/opennow-stable/src/main/platforms/gfn/cloudmatchTransport.ts @@ -129,6 +129,26 @@ export function extractServerInfoRegionBases(payload: CloudMatchServerInfoRespon return bases; } +/** Official Bifrost POSTs to metro/regional hostnames (eu-*-*) rather than zone LBs (np-*-*). */ +export function selectCreateSessionBase(bases: readonly string[]): string | undefined { + if (bases.length === 0) { + return undefined; + } + + for (const base of bases) { + try { + const host = new URL(base).hostname.toLowerCase(); + if (!host.startsWith("np-")) { + return base; + } + } catch { + continue; + } + } + + return bases[0]; +} + export function isDefaultStreamingServiceBase(baseUrl: string): boolean { try { const hostname = new URL(baseUrl).hostname.toLowerCase(); @@ -145,6 +165,7 @@ export async function resolveCreateSessionBase( clientId: string, deviceId: string, proxyUrl?: string, + options: { preferRegionalHost?: boolean } = {}, ): Promise { if (!isDefaultStreamingServiceBase(base)) { return base; @@ -159,14 +180,19 @@ export async function resolveCreateSessionBase( return base; } - const [localRegionBase] = extractServerInfoRegionBases( + const regionBases = extractServerInfoRegionBases( (await response.json()) as CloudMatchServerInfoResponse, ); + const localRegionBase = options.preferRegionalHost + ? selectCreateSessionBase(regionBases) + : regionBases[0]; if (!localRegionBase || localRegionBase === base) { return base; } - console.log(`[CloudMatch] createSession resolved ${base} to local region ${localRegionBase}`); + console.log( + `[CloudMatch] createSession resolved ${base} to ${options.preferRegionalHost ? "regional create host" : "local region"} ${localRegionBase}`, + ); return localRegionBase; } catch (error) { console.warn(`[CloudMatch] createSession local-region discovery failed: ${formatErrorForLog(error)}`); @@ -202,14 +228,25 @@ export function isZoneHostname(ip: string): boolean { ); } +/** Official Bifrost polls GET /v2/session on sessionControlInfo.ip (zone LB), not the create host. */ +export function resolveSessionControlBaseUrl( + controlIp: string | string[] | undefined, + fallback: string, +): string { + const host = (Array.isArray(controlIp) ? controlIp[0] : controlIp)?.trim().replace(/\.$/, ""); + if (host && isZoneHostname(host)) { + return `https://${host.toLowerCase()}`; + } + return fallback; +} + export function resolvePollStopBase(zone: string, provided?: string, serverIp?: string): string { const base = resolveStreamingBaseUrl(zone, provided); - // Only use serverIp if it's a real server IP (not a zone hostname). - // The Rust version checks: if we're NOT an alliance partner AND we have a server_ip, use it. - // But if the "serverIp" is actually the zone hostname (from an early poll when connectionInfo - // was empty), using it is circular and doesn't help. - if (serverIp && shouldUseServerIp(base) && !isZoneHostname(serverIp)) { - return `https://${serverIp}`; + // Official Bifrost polls GET /v2/session on sessionControlInfo.ip, including zone LBs + // such as np-ams-06.cloudmatchbeta.nvidiagrid.net. Real seat IPs stay preferred once known. + const host = serverIp?.trim().replace(/\.$/, ""); + if (host && shouldUseServerIp(base)) { + return `https://${isZoneHostname(host) ? host.toLowerCase() : host}`; } return base; } diff --git a/opennow-stable/src/main/platforms/gfn/deviceId.test.ts b/opennow-stable/src/main/platforms/gfn/deviceId.test.ts new file mode 100644 index 000000000..a68c18542 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/deviceId.test.ts @@ -0,0 +1,20 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; + +import { toCloudMatchDeviceHashId } from "./deviceId"; + +test("CloudMatch device ids are SHA-256 hex of the stable UUID", () => { + const uuid = "11111111-1111-4111-8111-111111111111"; + const expected = createHash("sha256").update(uuid, "utf8").digest("hex"); + assert.equal(toCloudMatchDeviceHashId(uuid), expected); + assert.match(toCloudMatchDeviceHashId(uuid), /^[0-9a-f]{64}$/); +}); + +test("CloudMatch device ids pass through existing 64-char hashes", () => { + const hash = "eb3d00f59d2e42dafddbb00648b14be24a0f9e262bc5ba50d853a019301b03fc"; + assert.equal(toCloudMatchDeviceHashId(hash), hash); + assert.equal(toCloudMatchDeviceHashId(hash.toUpperCase()), hash); +}); diff --git a/opennow-stable/src/main/platforms/gfn/deviceId.ts b/opennow-stable/src/main/platforms/gfn/deviceId.ts index 1839aea0d..87efb51cd 100644 --- a/opennow-stable/src/main/platforms/gfn/deviceId.ts +++ b/opennow-stable/src/main/platforms/gfn/deviceId.ts @@ -15,6 +15,19 @@ function getElectronApp(): Electron.App | null { } } +/** Official Grid `X-Device-Id` / JSON `deviceHashId` is 64-char SHA-256 hex, not a UUID. */ +export function toCloudMatchDeviceHashId(deviceId: string): string { + const trimmed = deviceId.trim(); + if (/^[0-9a-f]{64}$/i.test(trimmed)) { + return trimmed.toLowerCase(); + } + return crypto.createHash("sha256").update(trimmed, "utf8").digest("hex"); +} + +export function getCloudMatchDeviceHashId(): string { + return toCloudMatchDeviceHashId(getStableDeviceId()); +} + export function getStableDeviceId(): string { if (cachedStableDeviceId) { return cachedStableDeviceId; diff --git a/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts b/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts index c4d0994d5..5cc9bf83b 100644 --- a/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts +++ b/opennow-stable/src/main/platforms/gfn/deviceIdentity.test.ts @@ -7,6 +7,12 @@ import { buildGfnCloudMatchHeaders, buildGfnGraphQlHeaders, buildGfnLcarsHeaders, + buildGfnNvstClientHeaders, + GFN_BIFROST_CLIENT_VERSION, + GFN_CLIENT_VERSION, + gfnBifrostUserAgentForPlatform, + gfnUserAgentForPlatform, + LCARS_CLIENT_ID, } from "./clientHeaders"; import { STEAM_DECK_DEVICE_IDENTITY, @@ -21,7 +27,62 @@ test("resolveGfnDeviceIdentity defaults to host desktop DESKTOP/UNKNOWN", () => assert.equal(identity.deviceType, "DESKTOP"); assert.equal(identity.deviceMake, "UNKNOWN"); assert.equal(identity.deviceModel, "UNKNOWN"); - assert.equal(identity.clientPlatformName, "windows"); + assert.equal(identity.clientPlatformName, "Windows"); +}); + +test("desktop identity uses native platform names", () => { + const darwin = resolveGfnDeviceIdentity({ platform: "darwin" }); + assert.equal(darwin.clientPlatformName, "MacOSX"); + assert.equal(darwin.deviceMake, "Apple"); + if (process.platform === "darwin") { + assert.notEqual(darwin.deviceModel, "UNKNOWN"); + } + assert.equal(resolveGfnDeviceIdentity({ platform: "linux" }).clientPlatformName, "Linux"); +}); + +test("Linux requests use the packaged native CEF product identity", () => { + const userAgent = gfnUserAgentForPlatform("linux"); + assert.match(userAgent, /\(X11; Linux x86_64\)/); + assert.match(userAgent, /NVIDIACEFClient\/HEAD\/7b92719716/); + assert.match(userAgent, new RegExp(`GFN-PC/${GFN_CLIENT_VERSION.replaceAll(".", "\\.")}`)); +}); + +test("macOS User-Agent identifies as the packaged GFN-PC CEF client", () => { + const userAgent = gfnUserAgentForPlatform("darwin"); + assert.match(userAgent, /\(Macintosh; Intel Mac OS X 10_15_7\)/); + assert.match(userAgent, /NVIDIACEFClient\/HEAD\/7b92719716/); + assert.match(userAgent, new RegExp(`GFN-PC/${GFN_CLIENT_VERSION.replaceAll(".", "\\.")}`)); +}); + +test("CloudMatch HTTP identity includes official Bifrost client identity", () => { + const headers = buildGfnCloudMatchHeaders({ token: "token", deviceId: "device" }); + const userAgent = gfnBifrostUserAgentForPlatform(); + assert.equal(headers["NV-Client-Type"], "NATIVE"); + assert.equal(headers["NV-Client-Streamer"], "NVIDIA-CLASSIC"); + assert.equal(headers["x-nv-client-identity"], userAgent); + assert.equal(headers["NV-Client-Version"], GFN_CLIENT_VERSION); + assert.equal(headers["Content-Type"], "text/plain"); + assert.equal(headers["User-Agent"], userAgent); + assert.match(headers["User-Agent"] ?? "", new RegExp(`GFN-PC/${GFN_BIFROST_CLIENT_VERSION}`)); + assert.equal(headers.Origin, undefined); + assert.equal(headers.Referer, undefined); + assert.equal(headers["nv-browser-type"], undefined); +}); + +test("Bifrost GridServer HTTP identity advertises the native PC client", () => { + const headers = buildGfnNvstClientHeaders({ deviceId: "device" }); + const userAgent = gfnBifrostUserAgentForPlatform(); + assert.equal(headers["NV-Client-Type"], "NATIVE"); + assert.equal(headers["NV-Client-Streamer"], "NVIDIA-CLASSIC"); + assert.equal(headers["x-nv-client-identity"], userAgent); + assert.equal(headers["X-Device-Id"], "device"); + assert.equal(headers["NV-Client-Version"], GFN_CLIENT_VERSION); + assert.equal(headers["User-Agent"], userAgent); +}); + +test("CloudMatch defaults to the stable LCARS client identity", () => { + const headers = buildGfnCloudMatchHeaders({ token: "token", deviceId: "device" }); + assert.equal(headers["NV-Client-ID"], LCARS_CLIENT_ID); }); test("resolveGfnDeviceIdentity Steam Deck profile matches official headers", () => { @@ -42,10 +103,10 @@ test("CloudMatch/LCARS/GraphQL headers honor Steam Deck identity", () => { clientId: "client", deviceId: "device", }); - assert.equal(cloudMatch["nv-device-os"], "STEAMOS"); - assert.equal(cloudMatch["nv-device-type"], "CONSOLE"); - assert.equal(cloudMatch["nv-device-make"], "VALVE"); - assert.equal(cloudMatch["nv-device-model"], "STEAMDECK"); + assert.equal(cloudMatch["NV-Device-OS"], "STEAMOS"); + assert.equal(cloudMatch["NV-Device-Type"], "CONSOLE"); + assert.equal(cloudMatch["NV-Device-Make"], "VALVE"); + assert.equal(cloudMatch["NV-Device-Model"], "STEAMDECK"); const lcars = buildGfnLcarsHeaders({ token: "token", @@ -74,6 +135,6 @@ test("explicit identifyAsSteamDeck option overrides settings reader", () => { deviceId: "device", identifyAsSteamDeck: true, }); - assert.equal(headers["nv-device-os"], "STEAMOS"); - assert.equal(headers["nv-device-type"], "CONSOLE"); + assert.equal(headers["NV-Device-OS"], "STEAMOS"); + assert.equal(headers["NV-Device-Type"], "CONSOLE"); }); diff --git a/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts b/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts index 32ad34bed..ae56e5574 100644 --- a/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts +++ b/opennow-stable/src/main/platforms/gfn/deviceIdentity.ts @@ -1,3 +1,5 @@ +import { execFileSync } from "node:child_process"; + /** * GFN device identity profiles. * @@ -18,29 +20,47 @@ export interface GfnDeviceIdentity { clientPlatformName: string; } -// OpenNOW historically always sent clientPlatformName "windows" on the desktop -// CloudMatch path (even on macOS/Linux). Keep that unless Steam Deck spoof is on. +let cachedDarwinHwModel: string | null = null; + +function readDarwinHwModel(): string { + if (cachedDarwinHwModel) { + return cachedDarwinHwModel; + } + try { + const model = execFileSync("sysctl", ["-n", "hw.model"], { + encoding: "utf8", + timeout: 500, + }).trim(); + cachedDarwinHwModel = model.length > 0 ? model : "UNKNOWN"; + } catch { + cachedDarwinHwModel = "UNKNOWN"; + } + return cachedDarwinHwModel; +} + const DESKTOP_IDENTITY_BY_PLATFORM: Record<"win32" | "darwin" | "linux", GfnDeviceIdentity> = { win32: { deviceOs: "WINDOWS", deviceType: "DESKTOP", deviceMake: "UNKNOWN", deviceModel: "UNKNOWN", - clientPlatformName: "windows", + clientPlatformName: "Windows", }, darwin: { deviceOs: "MACOS", deviceType: "DESKTOP", - deviceMake: "UNKNOWN", + deviceMake: "Apple", deviceModel: "UNKNOWN", - clientPlatformName: "windows", + // Native Bifrost / QUERY_GFN_START uses MacOSX. Mall JS Session Control + // falls back to OSName "MacOS", but official Mac never POSTs that body. + clientPlatformName: "MacOSX", }, linux: { deviceOs: "LINUX", deviceType: "DESKTOP", deviceMake: "UNKNOWN", deviceModel: "UNKNOWN", - clientPlatformName: "windows", + clientPlatformName: "Linux", }, }; @@ -67,7 +87,14 @@ export function isIdentifyAsSteamDeckEnabled(): boolean { export function resolveHostDesktopIdentity( platform: NodeJS.Platform = process.platform, ): GfnDeviceIdentity { - if (platform === "win32" || platform === "darwin" || platform === "linux") { + if (platform === "darwin") { + return { + ...DESKTOP_IDENTITY_BY_PLATFORM.darwin, + deviceMake: "Apple", + deviceModel: readDarwinHwModel(), + }; + } + if (platform === "win32" || platform === "linux") { return DESKTOP_IDENTITY_BY_PLATFORM[platform]; } return DESKTOP_IDENTITY_BY_PLATFORM.linux; diff --git a/opennow-stable/src/main/platforms/gfn/index.ts b/opennow-stable/src/main/platforms/gfn/index.ts index aa0fdafa0..146eeed94 100644 --- a/opennow-stable/src/main/platforms/gfn/index.ts +++ b/opennow-stable/src/main/platforms/gfn/index.ts @@ -28,7 +28,7 @@ export { } from "./games"; export { initSessionProxyAuth } from "./proxyFetch"; export { normalizeSessionProxyUrl, sessionProxyHasCredentials } from "./proxyUrl"; -export { getStableDeviceId } from "./deviceId"; +export { getCloudMatchDeviceHashId, getStableDeviceId, toCloudMatchDeviceHashId } from "./deviceId"; export { STEAM_DECK_DEVICE_IDENTITY, configureIdentifyAsSteamDeck, @@ -47,7 +47,10 @@ export { } from "./accountConnections"; export { GfnSignalingClient } from "./signaling"; export { + GFN_BIFROST_CLIENT_VERSION, + GFN_CLIENT_IDENTIFICATION, GFN_CLIENT_VERSION, + gfnBifrostUserAgentForPlatform, GFN_PLAY_ORIGIN, GFN_PLAY_REFERER, GFN_USER_AGENT, @@ -56,6 +59,7 @@ export { buildGfnCloudMatchHeaders, buildGfnGraphQlHeaders, buildGfnLcarsHeaders, + buildGfnNvstClientHeaders, buildNvidiaAuthHeaders, gfnJwtAuthorization, } from "./clientHeaders"; diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/owner.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/owner.test.ts new file mode 100644 index 000000000..cbcd66774 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/owner.test.ts @@ -0,0 +1,149 @@ +/// + +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { NativeStreamerSessionContext } from "@shared/gfn"; +import { + GfnNvstRtspSessionOwner, + GfnNvstUnavailableError, +} from "./owner"; +import type { NvstRtspSession } from "./probe"; + +function createContext( + sessionId: string, + transportMode: "nvst" | "webrtc" = "nvst", + withEndpoints = true, +): NativeStreamerSessionContext { + return { + session: { + sessionId, + status: 2, + zone: "test-zone", + serverIp: "192.0.2.10", + signalingServer: "signal.example", + signalingUrl: "wss://signal.example/session", + rtspsEndpoints: withEndpoints ? [`rtsps://rtsp.example:322/${sessionId}`] : undefined, + iceServers: [], + }, + settings: { + resolution: "1920x1080", + fps: 60, + codec: "H265", + transportMode, + } as NativeStreamerSessionContext["settings"], + shortcuts: {} as NativeStreamerSessionContext["shortcuts"], + }; +} + +function createRtspSession( + sessionId: string, + onRelease: (reason: string) => void, +): NvstRtspSession { + return { + endpoint: `rtsps://rtsp.example:322/${sessionId}`, + session: `rtsp-${sessionId}`, + hmacSeedPresent: true, + videoPeer: { ip: "192.0.2.20", port: 5004 }, + clientUdpPort: 45000, + srtp: { + aesKeyHex: "AA".repeat(32), + keyId: 42, + masterKeySaltHex: `${"AA".repeat(32)}${"00".repeat(11)}2A`, + saltHex: `${"00".repeat(11)}2A`, + clientGenerated: false, + }, + videoSession: { + clientUdpPort: 45000, + videoPeerIp: "192.0.2.20", + videoPeerPort: 5004, + srtpAesKeyHex: "AA".repeat(32), + srtpKeyId: 42, + srtpSaltHex: `${"00".repeat(11)}2A`, + codec: "H265", + }, + steps: ["wss-open", "options", "describe", "setup-video", "announce", "play"], + handoffVideoUdp: async () => undefined, + release: async (reason = "released") => onRelease(reason), + }; +} + +test("owner retains one negotiated control session and reuses its video handoff", async () => { + const releases: string[] = []; + let negotiations = 0; + const owner = new GfnNvstRtspSessionOwner({ + negotiate: async ({ sessionId }) => { + negotiations += 1; + return createRtspSession(sessionId, (reason) => releases.push(reason)); + }, + }); + + const first = await owner.prepare(createContext("same-session")); + const duplicate = await owner.prepare(createContext("same-session")); + + assert.equal(negotiations, 1); + assert.deepEqual(duplicate.nvstVideo, first.nvstVideo); + assert.deepEqual(releases, []); + + await owner.release("stream stopped"); + await owner.release("duplicate stop"); + assert.deepEqual(releases, ["stream stopped"]); +}); + +test("owner tears down the previous session before negotiating its replacement", async () => { + const events: string[] = []; + const owner = new GfnNvstRtspSessionOwner({ + negotiate: async ({ sessionId }) => { + events.push(`negotiate:${sessionId}`); + return createRtspSession(sessionId, (reason) => { + events.push(`release:${sessionId}:${reason}`); + }); + }, + }); + + await owner.prepare(createContext("first")); + await owner.prepare(createContext("second")); + + assert.deepEqual(events, [ + "negotiate:first", + "release:first:replaced by GFN session second", + "negotiate:second", + ]); + await owner.release("test complete"); +}); + +test("owner releases NVST when a WebRTC native context replaces it", async () => { + const releases: string[] = []; + const owner = new GfnNvstRtspSessionOwner({ + negotiate: async ({ sessionId }) => + createRtspSession(sessionId, (reason) => releases.push(reason)), + }); + + await owner.prepare(createContext("first")); + const webRtcContext = await owner.prepare(createContext("second", "webrtc")); + + assert.equal(webRtcContext.nvstVideo, undefined); + assert.deepEqual(releases, ["native transport is not NVST"]); +}); + +test("owner reports typed unavailability for missing endpoints and negotiation failure", async () => { + const owner = new GfnNvstRtspSessionOwner({ + negotiate: async () => { + throw new Error("synthetic RTSP failure"); + }, + }); + + await assert.rejects( + owner.prepare(createContext("missing", "nvst", false)), + (error: unknown) => + error instanceof GfnNvstUnavailableError + && error.code === "missing-rtsps-endpoints", + ); + await assert.rejects( + owner.prepare(createContext("failed")), + (error: unknown) => + error instanceof GfnNvstUnavailableError + && error.code === "negotiation-failed" + && /synthetic RTSP failure/.test(error.message), + ); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/owner.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/owner.ts new file mode 100644 index 000000000..572926694 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/owner.ts @@ -0,0 +1,208 @@ +import type { NativeStreamerSessionContext, NvstVideoSession } from "@shared/gfn"; + +import { + bindEphemeralUdp, + createNvstNegotiationDependencies, + negotiateNvstRtspSession, + NvstRtspNegotiationError, + type NvstRtspProbeInput, + type NvstRtspSession, + type NvstUdpPortReservation, +} from "./probe"; + +export type GfnNvstUnavailableCode = + | "missing-rtsps-endpoints" + | "negotiation-failed" + | "preparation-superseded"; + +export class GfnNvstUnavailableError extends Error { + constructor( + readonly code: GfnNvstUnavailableCode, + message: string, + options?: ErrorOptions, + ) { + super(message, options); + this.name = "GfnNvstUnavailableError"; + } +} + +export interface GfnNvstRtspOwner { + prepare(context: NativeStreamerSessionContext): Promise; + /** Unix fd of the still-bound video UDP socket, if the probe kept it open. */ + videoUdpFd(): number | undefined; + /** Releases the video UDP reservation after native has rebound the same port. */ + handoffVideoUdp(): Promise; + release(reason: string): Promise; +} + +export interface GfnNvstRtspSessionOwnerDependencies { + negotiate?(input: NvstRtspProbeInput): Promise; + /** Bind the video/bundle socket in native so ANNOUNCE never races a rebind. */ + reserveVideoUdp?(): Promise; + /** Start native receive as soon as video SETUP gives us a peer. */ + onVideoReady?(videoSession: NvstVideoSession): Promise; + /** Start ICE+DTLS after ANNOUNCE, before PLAY. */ + onAnnounceReady?(videoSession: NvstVideoSession): Promise; + onLog?(message: string): void; +} + +interface OwnedSession { + sessionId: string; + rtsp: NvstRtspSession; +} + +function withoutNvstVideo( + context: NativeStreamerSessionContext, +): NativeStreamerSessionContext { + const { nvstVideo: _nvstVideo, ...rest } = context; + return rest; +} + +export class GfnNvstRtspSessionOwner implements GfnNvstRtspOwner { + private active: OwnedSession | null = null; + private revision = 0; + private operation: Promise = Promise.resolve(); + + constructor( + private readonly dependencies: GfnNvstRtspSessionOwnerDependencies = {}, + ) {} + + prepare(context: NativeStreamerSessionContext): Promise { + const revision = ++this.revision; + return this.enqueue(async () => { + if (revision !== this.revision) { + throw new GfnNvstUnavailableError( + "preparation-superseded", + "NVST preparation was superseded before it started", + ); + } + + if (context.settings.transportMode !== "nvst") { + await this.releaseActive("native transport is not NVST"); + return withoutNvstVideo(context); + } + + const sessionId = context.session.sessionId; + if (this.active?.sessionId === sessionId) { + return { + ...withoutNvstVideo(context), + nvstVideo: this.active.rtsp.videoSession, + }; + } + + await this.releaseActive(`replaced by GFN session ${sessionId}`); + + const rtspsEndpoints = context.session.rtspsEndpoints ?? []; + if (rtspsEndpoints.length === 0) { + const message = `GFN session ${sessionId} did not provide RTSPS endpoints for explicit NVST mode`; + this.log(message); + throw new GfnNvstUnavailableError("missing-rtsps-endpoints", message); + } + + let rtsp: NvstRtspSession; + try { + rtsp = await this.negotiateRtsp({ + sessionId, + rtspsEndpoints, + resolution: context.settings.resolution, + fps: context.settings.fps, + codec: context.session.negotiatedStreamProfile?.codec ?? context.settings.codec, + onLog: this.dependencies.onLog, + onVideoReady: this.dependencies.onVideoReady, + onAnnounceReady: this.dependencies.onAnnounceReady, + }); + } catch (error) { + const detail = error instanceof NvstRtspNegotiationError + ? `${error.code}: ${error.message}` + : error instanceof Error + ? error.message + : String(error); + const message = `NVST is unavailable for GFN session ${sessionId}: ${detail}`; + this.log(message); + throw new GfnNvstUnavailableError("negotiation-failed", message, { + cause: error, + }); + } + + if (revision !== this.revision) { + await rtsp.release("superseded NVST preparation"); + throw new GfnNvstUnavailableError( + "preparation-superseded", + "NVST preparation was superseded before native startup", + ); + } + + this.active = { sessionId, rtsp }; + this.log( + `Retaining NVST RTSPS control session for GFN session ${sessionId}${rtsp.videoUdpFd !== undefined ? ` (videoUdpFd=${rtsp.videoUdpFd})` : ""}`, + ); + return { + ...withoutNvstVideo(context), + nvstVideo: rtsp.videoSession, + }; + }); + } + + videoUdpFd(): number | undefined { + return this.active?.rtsp.videoUdpFd; + } + + handoffVideoUdp(): Promise { + return this.enqueue(async () => { + const active = this.active; + if (!active) { + return; + } + await active.rtsp.handoffVideoUdp(); + }); + } + + release(reason: string): Promise { + ++this.revision; + return this.enqueue(() => this.releaseActive(reason)); + } + + private negotiateRtsp(input: NvstRtspProbeInput): Promise { + if (this.dependencies.negotiate) { + return this.dependencies.negotiate(input); + } + return negotiateNvstRtspSession( + input, + createNvstNegotiationDependencies({ + reserveUdpPort: bindEphemeralUdp, + reserveBundlePort: this.dependencies.reserveVideoUdp ?? bindEphemeralUdp, + }), + ); + } + + private enqueue(operation: () => Promise): Promise { + const result = this.operation.then(operation, operation); + this.operation = result.then( + () => undefined, + () => undefined, + ); + return result; + } + + private async releaseActive(reason: string): Promise { + const active = this.active; + this.active = null; + if (!active) { + return; + } + + this.log(`Releasing NVST RTSPS control session for GFN session ${active.sessionId} (${reason})`); + try { + await active.rtsp.release(reason); + } catch (error) { + this.log( + `Failed to release NVST RTSPS control session for GFN session ${active.sessionId}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + private log(message: string): void { + console.log(`[GfnNvstRtspOwner] ${message}`); + this.dependencies.onLog?.(message); + } +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts index ccd6e135a..657c33433 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.test.ts @@ -4,17 +4,149 @@ import test from "node:test"; import assert from "node:assert/strict"; import { + buildNvstStunBindingRequest, collectRtspsEndpoints, + negotiateNvstRtspSession, + NvstRtspNegotiationError, + officialVideoSetupControl, + incrementNvstPingUfrag, + resolveNvstIceRemoteUfrag, + resolveRtspControlUri, rtspsUrlToWssUrl, selectPrimaryRtspsEndpoint, + type NvstRtspClient, + type NvstRtspNegotiationDependencies, } from "./probe"; -test("selectPrimaryRtspsEndpoint prefers :322", () => { +test("version 6 STUN request matches the official RFC 5389 packet shape", () => { + assert.equal( + buildNvstStunBindingRequest( + "loc1", + "remote01", + "remote-password-with-36-byte-value-001", + Buffer.from("000102030405060708090A0B", "hex"), + ).toString("hex").toUpperCase(), + "000100342112A442000102030405060708090A0B0006000D72656D6F746530313A6C6F633100000000080014B276DC1C7949494C7EF7EB226BE8BB5E0EE5AABD802800045A8349EF", + ); +}); +import type { ParsedRtspResponse } from "./rtspClient"; + +function response( + headers: Record = {}, + body = "", + statusCode = 200, +): ParsedRtspResponse { + return { + statusCode, + statusText: statusCode === 200 ? "OK" : "Failed", + headers, + body, + }; +} + +class FakeRtspClient implements NvstRtspClient { + closed = false; + readonly requests: Array<{ + method: string; + uri: string; + headers: Record; + body: string; + }> = []; + + constructor( + private readonly onRequest: ( + method: string, + uri: string, + headers: Record, + body: string, + ) => ParsedRtspResponse | Promise, + private readonly events: string[], + ) {} + + async connect(sessionId?: string): Promise { + this.events.push(`connect:${sessionId}`); + } + + async request( + method: string, + uri: string, + headers: Record = {}, + body = "", + ): Promise { + this.requests.push({ method, uri, headers, body }); + this.events.push(`request:${method}`); + return this.onRequest(method, uri, headers, body); + } + + close(): void { + this.closed = true; + this.events.push("client-close"); + } +} + +const DESCRIBE_SDP = [ + "v=0", + "a=x-nv-runtime.encryptionKey:AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899", + "a=x-nv-runtime.encryptionKeyId:42", + "m=video 0 RTP/AVP 96", + "a=control:tracks/actual-video-track", + "m=audio 0 RTP/AVP 97", + "a=control:tracks/actual-audio-track", + "m=application 0 RTP/AVP 98", + "a=control:streamid=control/0", + "", +].join("\r\n"); + +function createNegotiationHarness( + events: string[], + override?: (method: string) => ParsedRtspResponse | undefined, +): { + client: FakeRtspClient; + dependencies: NvstRtspNegotiationDependencies; +} { + let nextUdpPort = 45678; + const client = new FakeRtspClient((method) => { + const overridden = override?.(method); + if (overridden) { + return overridden; + } + switch (method) { + case "DESCRIBE": + return response({ session: "rtsp-session;timeout=60" }, DESCRIBE_SDP); + case "SETUP": + return response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4", + "x-nv-ping-payload": "ping-data", + "x-nv-ping": "1", + }); + default: + return response(); + } + }, events); + return { + client, + dependencies: { + createClient: () => client, + reserveUdpPort: async () => { + const port = nextUdpPort; + nextUdpPort += 2; + return { + port, + release: async () => { + events.push("udp-release"); + }, + }; + }, + }, + }; +} + +test("selectPrimaryRtspsEndpoint preserves CloudMatch endpoint order", () => { const selected = selectPrimaryRtspsEndpoint([ "rtsps://host.example:48322", "rtsps://host.example:322", ]); - assert.equal(selected, "rtsps://host.example:322"); + assert.equal(selected, "rtsps://host.example:48322"); }); test("rtspsUrlToWssUrl is host:port with no path (empty upgrade path is manual)", () => { @@ -24,23 +156,29 @@ test("rtspsUrlToWssUrl is host:port with no path (empty upgrade path is manual)" ); }); -test("collectRtspsEndpoints keeps both usage=14 paths", () => { +test("collectRtspsEndpoints keeps current and legacy RTSPS descriptors and ignores signaling paths", () => { const endpoints = collectRtspsEndpoints( [ { - usage: 14, + usage: 16, port: 322, resourcePath: "rtsps://host.example:322", }, { - usage: 14, + usage: 16, port: 48322, resourcePath: "rtsps://host.example:48322", }, { - usage: 2, - port: 49006, - resourcePath: null, + usage: 14, + appLevelProtocol: 6, + port: 48323, + resourcePath: "rtsps://legacy.example:48323", + }, + { + usage: 14, + port: 443, + resourcePath: "/nvst/", }, ], "host.example", @@ -48,14 +186,15 @@ test("collectRtspsEndpoints keeps both usage=14 paths", () => { assert.deepEqual(endpoints, [ "rtsps://host.example:322", "rtsps://host.example:48322", + "rtsps://legacy.example:48323", ]); }); test("collectRtspsEndpoints synthesizes from port when resourcePath missing", () => { const endpoints = collectRtspsEndpoints( [ - { usage: 14, port: 322, resourcePath: null }, - { usage: 14, port: 48322, resourcePath: null }, + { usage: 16, port: 322, resourcePath: null }, + { usage: 16, port: 48322, resourcePath: null }, ], "host.example", ); @@ -64,3 +203,679 @@ test("collectRtspsEndpoints synthesizes from port when resourcePath missing", () "rtsps://host.example:48322", ]); }); + +test("officialVideoSetupControl appends the official video stream index", () => { + assert.equal(officialVideoSetupControl("streamid=video/0"), "streamid=video/0/0"); + assert.equal(officialVideoSetupControl("streamid=video/0/0"), "streamid=video/0/0"); + assert.equal(officialVideoSetupControl("tracks/actual-video-track"), "tracks/actual-video-track"); +}); + +test("incrementNvstPingUfrag matches official SETUP ping plus one", () => { + assert.equal(incrementNvstPingUfrag("2baae7cf47998"), "2baae7cf47999"); + assert.equal(incrementNvstPingUfrag("PING"), null); + assert.equal(incrementNvstPingUfrag("srv1"), null); +}); + +test("resolveNvstIceRemoteUfrag keeps SETUP PING as the keepalive ufrag", () => { + assert.equal(resolveNvstIceRemoteUfrag("2baae7cf47998", "5cace022", 6), "2baae7cf47999"); + assert.equal(resolveNvstIceRemoteUfrag("PING", "5cace022", 6), "PING"); + assert.equal(resolveNvstIceRemoteUfrag("srv1", "5cace022", 6), "srv1"); + assert.equal(resolveNvstIceRemoteUfrag("ping-data", "5cace022", 1), "5cace022"); + assert.equal(resolveNvstIceRemoteUfrag(undefined, "5cace022"), "5cace022"); +}); + +test("resolveRtspControlUri preserves server-advertised absolute and relative controls", () => { + assert.equal( + resolveRtspControlUri("rtsps://host.example:322/session/base", "tracks/video-main"), + "rtsps://host.example:322/session/base/tracks/video-main", + ); + assert.equal( + resolveRtspControlUri("rtsps://host.example:322/session/base", "/tracks/video-main"), + "rtsps://host.example:322/tracks/video-main", + ); + assert.equal( + resolveRtspControlUri( + "rtsps://host.example:322/session/base", + "rtsps://media.example:322/selected/video", + ), + "rtsps://media.example:322/selected/video", + ); +}); + +test("negotiation retains RTSPS control and video UDP until native rebind", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events); + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + codec: "H265", + }, dependencies); + events.push("native-start"); + await negotiated.handoffVideoUdp(); + + assert.deepEqual(events.slice(-3), [ + "request:ANNOUNCE", + "native-start", + "udp-release", + ]); + assert.equal(client.closed, false); + assert.equal(negotiated.videoSession.clientUdpPort, 45678); + assert.equal(negotiated.videoSession.videoPeerIp, "192.0.2.4"); + assert.equal(negotiated.videoSession.codec, "H265"); + assert.equal(negotiated.videoSession.srtpSaltHex, "00000000000000000000002A"); + assert.equal(negotiated.videoSession.srtpProfile, undefined); + assert.equal(negotiated.srtp.saltHex, "00000000000000000000002A"); + assert.equal(negotiated.srtp.profile, undefined); + assert.equal( + client.requests.find(({ method }) => method === "OPTIONS")?.uri, + "rtsps://host.example:322", + ); + assert.equal( + client.requests.find(({ method }) => method === "DESCRIBE")?.headers["x-nv-abtesting"], + "2", + ); + assert.equal( + client.requests.find(({ method, uri }) => method === "SETUP" && uri.includes("video"))?.headers["x-nv-ping"], + "6", + ); + assert.equal( + client.requests.find(({ method }) => method === "DESCRIBE")?.headers["x-nv-sessionid"], + "gfn-session", + ); + assert.equal( + client.requests.find(({ method, uri }) => method === "SETUP" && uri.includes("video"))?.uri, + "tracks/actual-video-track", + ); + assert.deepEqual( + client.requests.filter(({ method }) => method === "SETUP").map(({ uri }) => uri), + ["tracks/actual-video-track", "tracks/actual-audio-track", "streamid=control/0"], + ); + assert.deepEqual( + client.requests.filter(({ method }) => method === "SETUP").map(({ headers }) => headers.Session), + ["rtsp-session", "rtsp-session", "rtsp-session"], + ); + const announce = client.requests.find(({ method }) => method === "ANNOUNCE"); + assert.equal(announce?.uri, "/"); + assert.match(announce?.body ?? "", /m=video 5004/); + assert.match(announce?.body ?? "", /a=x-nv-general\.clientPorts\.video:45678/); + assert.equal(client.requests.some(({ method }) => method === "PLAY"), false); + + await negotiated.release("test stop"); + await negotiated.release("duplicate stop"); + + assert.equal(client.closed, true); + assert.equal(client.requests.filter(({ method }) => method === "TEARDOWN").length, 1); + assert.equal( + client.requests.find(({ method }) => method === "TEARDOWN")?.headers.Session, + "rtsp-session", + ); + assert.deepEqual(events.slice(-2), ["request:TEARDOWN", "client-close"]); +}); + +test("negotiation hands off an explicitly advertised DESCRIBE SRTP profile", async () => { + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => + method === "DESCRIBE" + ? response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "m=video", + "a=crypto:1 AEAD_AES_256_GCM inline:ignored\r\nm=video", + ), + ) + : undefined); + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + assert.equal(negotiated.srtp.profile, "AEAD_AES_256_GCM"); + assert.equal(negotiated.videoSession.srtpProfile, "AEAD_AES_256_GCM"); + assert.equal(negotiated.videoSession.srtpSaltHex, negotiated.srtp.saltHex); + await negotiated.release("test complete"); +}); + +test("ping version 6 hands off remote and generated local ICE credentials", async () => { + const events: string[] = []; + const remoteUsername = "remote01"; + const remotePassword = "remote-password-with-36-byte-value-001"; + const { client, dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "m=video", + `a=x-nv-general.iceUserNameFragmentV2:${remoteUsername}\r\na=x-nv-general.icePasswordV2:${remotePassword}\r\nm=video`, + ), + ); + } + if (method === "SETUP") { + return response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4", + "x-nv-ping": "6", + "x-nv-ping-payload": "srv1", + }); + } + return undefined; + }); + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + assert.equal(negotiated.videoSession.pingVersion, 6); + assert.equal(negotiated.videoSession.remoteIceUsernameFragment, "srv1"); + assert.equal(negotiated.videoSession.remoteIcePassword, remotePassword); + assert.match(negotiated.videoSession.localIceUsernameFragment ?? "", /^[A-Za-z0-9+/]{4}$/); + assert.match(negotiated.videoSession.localIcePassword ?? "", /^[A-Za-z0-9+/]{22}$/); + const announceBody = client.requests.find(({ method }) => method === "ANNOUNCE")?.body ?? ""; + assert.ok( + announceBody.includes( + `a=x-nv-general.iceUsernameFragment:${negotiated.videoSession.localIceUsernameFragment}`, + ), + ); + assert.ok( + announceBody.includes(`a=x-nv-general.iceUsernamePwd:${negotiated.videoSession.localIcePassword}`), + ); + assert.ok( + announceBody.includes( + `a=x-nv-general.iceUserNameFragmentV2:${negotiated.videoSession.localIceUsernameFragment}`, + ), + ); + assert.ok( + announceBody.includes(`a=x-nv-general.icePasswordV2:${negotiated.videoSession.localIcePassword}`), + ); + assert.match(announceBody, /m=video 5004/); + assert.match(announceBody, /a=x-nv-general\.clientPorts\.video:45678/); + assert.ok(announceBody.includes(`a=ice-ufrag:${negotiated.videoSession.localIceUsernameFragment}`)); + assert.ok(announceBody.includes(`a=ice-pwd:${negotiated.videoSession.localIcePassword}`)); + await negotiated.release("test complete"); +}); + +test("negotiation plays when DESCRIBE leaves disablePlay at 0", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + `${DESCRIBE_SDP.replace("m=video", "a=x-nv-general.disablePlay:0\r\nm=video")}`, + ); + } + if (method === "PLAY") { + return response({}, "", 455); + } + return undefined; + }); + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + assert.equal(client.requests.some(({ method, uri }) => method === "PLAY" && uri === "/"), true); + assert.equal(negotiated.steps.includes("play-455"), true); + await negotiated.release("test complete"); +}); + +test("negotiation arms native receive after video SETUP and before ANNOUNCE", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events); + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + onVideoReady: async (videoSession) => { + events.push(`native-armed:${videoSession.clientUdpPort}`); + }, + }, dependencies); + + const announceIndex = events.indexOf("request:ANNOUNCE"); + const armedIndex = events.indexOf("native-armed:45678"); + assert.ok(armedIndex >= 0); + assert.ok(announceIndex > armedIndex); + assert.equal(negotiated.steps.includes("native-receive-armed"), true); + assert.equal(client.requests.some(({ method }) => method === "ANNOUNCE"), true); + await negotiated.release("test complete"); +}); + +test("negotiation starts native WebRtcTransport after ANNOUNCE and before PLAY", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + `${DESCRIBE_SDP.replace("m=video", "a=x-nv-general.disablePlay:0\r\nm=video")}`, + ); + } + if (method === "PLAY") { + return response({}, "", 455); + } + return undefined; + }); + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + onAnnounceReady: async (videoSession) => { + events.push(`native-announce-armed:${videoSession.clientUdpPort}`); + }, + }, dependencies); + + const announceIndex = events.indexOf("request:ANNOUNCE"); + const armedIndex = events.indexOf("native-announce-armed:45678"); + const playIndex = events.indexOf("request:PLAY"); + assert.ok(announceIndex >= 0); + assert.ok(armedIndex > announceIndex); + assert.ok(playIndex > armedIndex); + assert.equal(negotiated.steps.includes("native-announce-armed"), true); + await negotiated.release("test complete"); +}); + +test("negotiation announces reserved WebRtcTransport ICE and DTLS fingerprint", async () => { + const events: string[] = []; + const fingerprint = "00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF"; + const { client, dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP + .replace("a=x-nv-runtime.encryptionKey:AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899\r\n", "") + .replace("a=x-nv-runtime.encryptionKeyId:42\r\n", "") + .replace( + "m=video", + [ + "a=x-nv-general.nativeRtcOnBundlePort:1", + "a=x-nv-general.useNewIceInfo:0", + "a=x-nv-general.dtlsFingerprintV2:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99", + "a=x-nv-general.iceUserNameFragmentV2:srvUfrag", + "a=x-nv-general.icePasswordV2:srv-password-with-22b-value", + "m=video", + ].join("\r\n"), + ), + ); + } + return undefined; + }); + // Native streamer owns the bundle socket (and the Mjolnir video socket), so the + // ICE/DTLS identity arrives on the bundle reservation. + dependencies.reserveBundlePort = async () => ({ + port: 45678, + mjolnirPort: 45680, + iceUsernameFragment: "locU", + icePassword: "local-password-22-chars!", + dtlsFingerprint: fingerprint, + localAddress: "192.0.2.8", + release: async () => { + events.push("udp-release"); + }, + }); + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + const announceBody = client.requests.find(({ method }) => method === "ANNOUNCE")?.body ?? ""; + assert.equal(announceBody.includes("a=x-nv-general.dtlsFingerprint:"), false); + assert.ok(announceBody.includes(`a=x-nv-general.dtlsFingerprintV2:${fingerprint}`)); + assert.equal(announceBody.includes("a=x-nv-general.iceUsernameFragment:locU"), false); + assert.ok(announceBody.includes("a=x-nv-general.iceUserNameFragmentV2:locU")); + assert.ok(announceBody.includes("a=ice-ufrag:locU")); + assert.ok(announceBody.includes("a=fingerprint:sha-256 " + fingerprint)); + assert.match(announceBody, /a=candidate:1 1 udp 2122260223 192\.0\.2\.8 45678 typ host/); + assert.match(announceBody, /a=x-nv-general\.clientBundlePort:45678/); + assert.match(announceBody, /a=x-nv-general\.clientPorts\.video:0/); + assert.match(announceBody, /a=x-nv-general\.rtcVideoOnNativeBundle:0/); + assert.doesNotMatch(announceBody, /clientTransport/); + assert.equal(negotiated.videoSession.localDtlsFingerprint, fingerprint); + assert.equal(negotiated.videoSession.remoteDtlsFingerprint?.length, 95); + assert.equal(negotiated.videoSession.remoteIceUsernameFragment, "srvUfrag"); + assert.equal(negotiated.videoSession.clientUdpPort, 45678); + assert.equal(negotiated.videoSession.mjolnirUdpPort, 45680); + // Official always sends a client-generated runtime.encryptionKey in ANNOUNCE (it keys the + // video SRTP on the separate non-DTLS socket), even when a DTLS fingerprint is present. + assert.ok(announceBody.includes("a=x-nv-runtime.encryptionKey:")); + assert.ok(announceBody.includes("a=x-nv-runtime.encryptionKeyId:")); + await negotiated.release("test complete"); +}); + +function stunUsername(packet: Buffer): string { + let offset = 20; + while (offset + 4 <= packet.length) { + const type = packet.readUInt16BE(offset); + const length = packet.readUInt16BE(offset + 2); + if (type === 0x0006) { + return packet.subarray(offset + 4, offset + 4 + length).toString("utf8"); + } + offset += 4 + length; + if (length % 4 !== 0) { + offset += 4 - (length % 4); + } + } + throw new Error("STUN packet missing USERNAME"); +} + +test("negotiation hole-punch uses SETUP PING as the keepalive ICE ufrag", async () => { + const sent: Buffer[] = []; + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "m=video", + [ + "a=x-nv-general.iceUserNameFragmentV2:5cace022", + "a=x-nv-general.icePasswordV2:srv-password-with-22b-value", + "m=video", + ].join("\r\n"), + ), + ); + } + if (method === "SETUP") { + return response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4", + "x-nv-ping": "6", + "x-nv-ping-payload": "PING", + }); + } + return undefined; + }); + const originalReserve = dependencies.reserveUdpPort; + dependencies.reserveUdpPort = async () => { + const reservation = await originalReserve(); + return { + ...reservation, + iceUsernameFragment: "18AU", + icePassword: "local-password-22-chars!", + send: async (payload) => { + sent.push(Buffer.from(payload)); + }, + }; + }; + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + const usernames = sent.map(stunUsername); + assert.ok(usernames.length >= 3, `expected ICE burst, got ${usernames.join(",")}`); + assert.deepEqual(usernames.slice(0, 3), ["PING:18AU", "PING:18AU", "PING:18AU"]); + assert.ok(usernames.includes("PING:18AU")); + assert.equal(negotiated.videoSession.remoteIceUsernameFragment, "PING"); + await negotiated.release("test complete"); +}); + +test("negotiation follows official video-only empty-Transport SETUP on the cloud path", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "a=control:tracks/actual-video-track", + "a=control:streamid=video/0", + ).replace("m=video", "a=x-nv-general.nativeRtcOnBundlePort:1\r\nm=video"), + ); + } + return undefined; + }); + // Native streamer owns both sockets: one nvst-bind returns the bundle port plus + // the dedicated Mjolnir video port. + dependencies.reserveBundlePort = async () => ({ + port: 45678, + mjolnirPort: 45680, + release: async () => { + events.push("udp-release"); + }, + }); + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + const setups = client.requests.filter(({ method }) => method === "SETUP"); + assert.deepEqual(setups.map(({ uri }) => uri), ["streamid=video/0/0"]); + assert.equal(setups[0]?.headers.Transport, ""); + assert.equal(setups[0]?.headers.Host, "host.example:322"); + const announceBody = client.requests.find(({ method }) => method === "ANNOUNCE")?.body ?? ""; + assert.match(announceBody, /nativeRtcOnBundlePort:1/); + assert.match(announceBody, /clientBundlePort:45678/); + assert.match(announceBody, /clientPorts\.video:0/); + assert.match(announceBody, /rtcVideoOnNativeBundle:0/); + assert.match(announceBody, /rtcAudioOnNativeBundle:1/); + assert.equal(negotiated.videoSession.clientUdpPort, 45678); + // The native-owned Mjolnir port is handed off so the native raw-SRTP receiver + // reads video from it; the probe must not bind/NATT its own Mjolnir socket. + assert.equal(negotiated.videoSession.mjolnirUdpPort, 45680); + assert.equal( + client.requests.find(({ method }) => method === "ANNOUNCE")?.uri, + "rtsps://host.example:322", + ); + await negotiated.release("test complete"); +}); + +test("official cloud ICE uses SETUP ping plus one, not DESCRIBE V2", async () => { + const sent: Buffer[] = []; + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "m=video", + [ + "a=x-nv-general.nativeRtcOnBundlePort:1", + "a=x-nv-general.iceUserNameFragmentV2:5cace022", + "a=x-nv-general.icePasswordV2:srv-password-with-22b-value", + "m=video", + ].join("\r\n"), + ), + ); + } + if (method === "SETUP") { + return response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4", + "x-nv-ping": "6", + "x-nv-ping-payload": "2baae7cf47998", + }); + } + return undefined; + }); + const originalReserve = dependencies.reserveUdpPort; + dependencies.reserveUdpPort = async () => { + const reservation = await originalReserve(); + return { + ...reservation, + iceUsernameFragment: "EF+W", + icePassword: "local-password-22-chars!", + send: async (payload) => { + sent.push(Buffer.from(payload)); + }, + }; + }; + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + const usernames = sent.map(stunUsername); + assert.ok(usernames.includes("2baae7cf47999:EF+W")); + assert.ok(usernames.includes("2baae7cf47998:EF+W")); + assert.ok(usernames.includes("PING:EF+W")); + assert.equal(usernames.includes("5cace022:EF+W"), false); + assert.equal(negotiated.videoSession.remoteIceUsernameFragment, "2baae7cf47999"); + await negotiated.release("test complete"); +}); + +test("official cloud STUN starts after ANNOUNCE, not after SETUP", async () => { + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "m=video", + [ + "a=x-nv-general.nativeRtcOnBundlePort:1", + "a=x-nv-general.iceUserNameFragmentV2:5cace022", + "a=x-nv-general.icePasswordV2:srv-password-with-22b-value", + "m=video", + ].join("\r\n"), + ), + ); + } + if (method === "SETUP") { + return response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4", + "x-nv-ping": "6", + "x-nv-ping-payload": "1d2fd28347998", + }); + } + return undefined; + }); + const originalReserve = dependencies.reserveUdpPort; + dependencies.reserveUdpPort = async () => { + const reservation = await originalReserve(); + return { + ...reservation, + iceUsernameFragment: "7m6V", + icePassword: "local-password-22-chars!", + send: async () => { + events.push("stun-send"); + }, + }; + }; + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + const setupIndex = events.indexOf("request:SETUP"); + const announceIndex = events.indexOf("request:ANNOUNCE"); + const firstStun = events.indexOf("stun-send"); + assert.ok(setupIndex >= 0); + assert.ok(announceIndex > setupIndex); + assert.ok(firstStun > announceIndex, `STUN at ${firstStun} must follow ANNOUNCE at ${announceIndex}`); + await negotiated.release("test complete"); +}); + +test("ping version 6 fails closed without DESCRIBE ICE credentials", async () => { + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => + method === "SETUP" + ? response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4", + "x-nv-ping": "6", + }) + : undefined); + + await assert.rejects( + negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies), + (error: unknown) => + error instanceof NvstRtspNegotiationError + && error.code === "missing-ice-credentials", + ); +}); + +test("negotiation hands off an explicitly advertised SETUP SRTP profile", async () => { + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => + method === "SETUP" + ? response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4;profile=AES_CM_128_HMAC_SHA1_80", + }) + : undefined); + + const negotiated = await negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies); + + assert.equal(negotiated.srtp.profile, "AES_CM_128_HMAC_SHA1_80"); + assert.equal(negotiated.videoSession.srtpProfile, "AES_CM_128_HMAC_SHA1_80"); + await negotiated.release("test complete"); +}); + +test("negotiation fails closed on conflicting explicit SRTP profiles", async () => { + const events: string[] = []; + const { dependencies } = createNegotiationHarness(events, (method) => { + if (method === "DESCRIBE") { + return response( + { session: "rtsp-session;timeout=60" }, + DESCRIBE_SDP.replace( + "m=video", + "a=crypto:1 AEAD_AES_256_GCM inline:ignored\r\nm=video", + ), + ); + } + if (method === "SETUP") { + return response({ + transport: "unicast;X-GS-ServerPort=5004-5005;source=192.0.2.4;profile=AEAD_AES_128_GCM", + }); + } + return undefined; + }); + + await assert.rejects( + negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322/session/base"], + }, dependencies), + (error: unknown) => + error instanceof NvstRtspNegotiationError + && error.code === "conflicting-srtp-profile", + ); + + assert.equal(events.includes("udp-release"), true); + assert.deepEqual(events.slice(-2), ["request:TEARDOWN", "client-close"]); +}); + +test("negotiation fails closed when DESCRIBE omits video control and tears down", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events, (method) => + method === "DESCRIBE" + ? response({ session: "rtsp-session" }, "v=0\r\nm=video 0 RTP/AVP 96\r\n") + : undefined); + + await assert.rejects( + negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322"], + }, dependencies), + (error: unknown) => + error instanceof NvstRtspNegotiationError + && error.code === "missing-video-control", + ); + + assert.equal(client.requests.some(({ method }) => method === "SETUP"), false); + assert.deepEqual(events.slice(-2), ["request:TEARDOWN", "client-close"]); +}); + +test("failed ANNOUNCE releases the UDP reservation and closes RTSPS control", async () => { + const events: string[] = []; + const { client, dependencies } = createNegotiationHarness(events, (method) => + method === "ANNOUNCE" ? response({}, "", 500) : undefined); + + await assert.rejects( + negotiateNvstRtspSession({ + sessionId: "gfn-session", + rtspsEndpoints: ["rtsps://host.example:322"], + }, dependencies), + (error: unknown) => + error instanceof NvstRtspNegotiationError + && error.code === "negotiation-failed" + && /ANNOUNCE failed/.test(error.message), + ); + + assert.equal(events.includes("udp-release"), true); + assert.equal(client.closed, true); + assert.deepEqual(events.slice(-3), ["udp-release", "request:TEARDOWN", "client-close"]); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts index da5da8264..cb6a16d52 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/probe.ts @@ -1,32 +1,42 @@ /** - * Classic NVST RTSPS-over-WSS handshake (GO-with-Moonlight-hypothesis). + * Classic NVST RTSPS-over-WSS handshake. * - * Runs OPTIONS → DESCRIBE → SETUP video/0/0 → ANNOUNCE → PLAY against `:322`. - * Extracts or generates runtime.encryptionKey for SRTP, then returns nvstVideo - * handoff fields for the native UDP receive scaffold. Does not keep the UDP - * socket open across the process boundary — native rebinds clientUdpPort. - * - * Evidence: docs/research/nvst-wire-format.md, nvst-srtp-key-derivation.md, - * nvst-announce-allowlist-1080p60.json. + * Runs OPTIONS → DESCRIBE → SETUP → ANNOUNCE. Official cloud (`nativeRtcOnBundlePort=1`) + * SETUPs video only with an empty Transport, binds Mjolnir before SETUP and the ICE + * bundle after SETUP, then ANNOUNCEs `clientPorts.*=0` + `clientBundlePort`. First + * bundle STUN is after ANNOUNCE; Mjolnir NATT starts just before PLAY. PLAY waits + * for WebRtcTransport. The legacy path still SETUPs every advertised stream. */ -import { createSocket, type Socket } from "node:dgram"; +import { createSocket } from "node:dgram"; +import { networkInterfaces } from "node:os"; +import { createHmac, randomBytes } from "node:crypto"; -import type { NvstVideoSession } from "@shared/gfn"; +import type { NvstSrtpProfile, NvstVideoSession } from "@shared/gfn"; import { extractVideoPeer, header, RtspOverWssClient, + type ParsedRtspResponse, } from "./rtspClient"; import { buildAnnounceSdp, extractHmacSeed, + extractNvstIceCredentials, + extractNvstSdpAttribute, + extractMediaControl, extractRuntimeEncryptionKey, generateClientEncryptionKey, + generateNvstIceCredentials, packSrtpMasterKeySalt, redactKey, } from "./sdp"; +import { + deriveSrtpSaltHex, + extractAdvertisedSrtpProfileFromHeaders, + extractAdvertisedSrtpProfileFromSdp, +} from "./srtp"; const DEFAULT_PROBE_TIMEOUT_MS = 20_000; @@ -38,6 +48,55 @@ export interface NvstRtspProbeInput { codec?: string; timeoutMs?: number; onLog?: (message: string) => void; + /** Official Bifrost has MjolnirVideoReceiver reading before later SETUP/ANNOUNCE/PLAY. */ + onVideoReady?(videoSession: NvstVideoSession): Promise; + /** Official starts WebRtcTransport after ANNOUNCE and waits for DTLS before PLAY. */ + onAnnounceReady?(videoSession: NvstVideoSession): Promise; +} + +export interface NvstRtspClient { + connect(sessionId?: string): Promise; + request( + method: string, + uri: string, + extraHeaders?: Record, + body?: string, + ): Promise; + close(): void; +} + +export interface NvstUdpPortReservation { + port: number; + /** + * Port of the dedicated NATT-only Mjolnir video socket reserved alongside the + * bundle. Set only when the native streamer owns both sockets (nvst-bind); the + * probe must not bind or NATT a separate Mjolnir socket in that case. + */ + mjolnirPort?: number; + localAddress?: string; + fd?: number; + iceUsernameFragment?: string; + icePassword?: string; + /** SHA-256 colon hex of the local DTLS cert that owns this socket. */ + dtlsFingerprint?: string; + send?(payload: Buffer, peerHost: string, peerPort: number): Promise; + onMessage?( + handler: (payload: Buffer, peer: { address: string; port: number }) => void, + ): void; + release(): Promise; +} + +export interface NvstRtspNegotiationDependencies { + createClient( + host: string, + port: number, + timeoutMs: number, + onLog?: (message: string) => void, + ): NvstRtspClient; + /** Mjolnir / extra SETUP sockets. Official cloud uses this for video NATT. */ + reserveUdpPort(peerHost?: string, peerPort?: number): Promise; + /** ICE/DTLS bundle socket. Official binds this after video SETUP. */ + reserveBundlePort?(peerHost?: string, peerPort?: number): Promise; } export interface NvstSrtpMaterial { @@ -47,6 +106,10 @@ export interface NvstSrtpMaterial { keyId: number; /** 88-char hex libsrtp master key||salt (AES-256 || 12-byte salt). */ masterKeySaltHex: string; + /** 24-char hex salt derived from runtime.encryptionKeyId. */ + saltHex: string; + /** Present only when DESCRIBE or SETUP explicitly advertises a known profile. */ + profile?: NvstSrtpProfile; /** True when OpenNOW generated the key for ANNOUNCE (DESCRIBE lacked it). */ clientGenerated: boolean; } @@ -67,20 +130,110 @@ export interface NvstRtspProbeResult { error?: string; } +export type NvstRtspNegotiationErrorCode = + | "missing-rtsps-endpoint" + | "missing-video-control" + | "missing-audio-control" + | "missing-control-stream" + | "missing-video-peer" + | "missing-ice-credentials" + | "conflicting-srtp-profile" + | "negotiation-failed"; + +export class NvstRtspNegotiationError extends Error { + constructor( + readonly code: NvstRtspNegotiationErrorCode, + message: string, + options?: ErrorOptions, + readonly steps: string[] = [], + ) { + super(message, options); + this.name = "NvstRtspNegotiationError"; + } +} + +export interface NvstRtspSession { + endpoint: string; + session: string; + hmacSeedPresent: boolean; + videoPeer: { ip: string; port: number }; + clientUdpPort: number; + srtp: NvstSrtpMaterial; + pingPayload?: string; + pingVersion?: number; + videoSession: NvstVideoSession; + steps: string[]; + videoUdpFd?: number; + /** Releases the video UDP reservation after native has rebound clientUdpPort. */ + handoffVideoUdp(): Promise; + release(reason?: string): Promise; +} + function log(onLog: NvstRtspProbeInput["onLog"], message: string): void { console.log(`[NvstRtspProbe] ${message}`); onLog?.(message); } +const CRC32_TABLE = Array.from({ length: 256 }, (_, value) => { + let crc = value; + for (let bit = 0; bit < 8; bit += 1) { + crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); + } + return crc >>> 0; +}); + +function appendStunAttribute(packet: Buffer[], type: number, value: Buffer): void { + const header = Buffer.alloc(4); + header.writeUInt16BE(type, 0); + header.writeUInt16BE(value.length, 2); + packet.push(header, value); + const padding = value.length % 4; + if (padding) { + packet.push(Buffer.alloc(4 - padding)); + } +} + +export function buildNvstStunBindingRequest( + localUsernameFragment: string, + remoteUsernameFragment: string, + remotePassword: string, + transactionId: Buffer = randomBytes(12), +): Buffer { + if (transactionId.length !== 12) { + throw new Error("STUN transaction ID must be 12 bytes"); + } + const header = Buffer.alloc(20); + header.writeUInt16BE(0x0001, 0); + header.writeUInt32BE(0x2112a442, 4); + transactionId.copy(header, 8); + const parts = [header]; + appendStunAttribute( + parts, + 0x0006, + Buffer.from(`${remoteUsernameFragment}:${localUsernameFragment}`, "utf8"), + ); + let packet = Buffer.concat(parts); + header.writeUInt16BE(packet.length - 20 + 24, 2); + packet = Buffer.concat(parts); + appendStunAttribute(parts, 0x0008, createHmac("sha1", remotePassword).update(packet).digest()); + packet = Buffer.concat(parts); + header.writeUInt16BE(packet.length - 20 + 8, 2); + packet = Buffer.concat(parts); + let crc = 0xffffffff; + for (const byte of packet) { + crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xff]!; + } + const fingerprint = Buffer.alloc(4); + fingerprint.writeUInt32BE(((crc ^ 0xffffffff) ^ 0x5354554e) >>> 0); + appendStunAttribute(parts, 0x8028, fingerprint); + return Buffer.concat(parts); +} + export function selectPrimaryRtspsEndpoint(endpoints: string[]): string | null { const normalized = endpoints .map((value) => value.trim()) .filter((value) => /^rtsps?:\/\//i.test(value)); - if (normalized.length === 0) { - return null; - } - const port322 = normalized.find((url) => /:322(?:\/|$)/.test(url)); - return port322 ?? normalized[0] ?? null; + return normalized[0] ?? null; } /** @@ -95,16 +248,18 @@ export function rtspsUrlToWssUrl(rtspsUrl: string): string { } export function collectRtspsEndpoints( - connections: Array<{ usage?: number; port?: number; resourcePath?: string | null }>, + connections: Array<{ + usage?: number; + port?: number; + appLevelProtocol?: number; + resourcePath?: string | null; + }>, fallbackHost?: string | null, ): string[] { const endpoints: string[] = []; const seen = new Set(); for (const conn of connections) { - if (conn.usage !== 14) { - continue; - } const resourcePath = typeof conn.resourcePath === "string" ? conn.resourcePath.trim() : ""; if (/^rtsps?:\/\//i.test(resourcePath)) { if (!seen.has(resourcePath)) { @@ -113,6 +268,9 @@ export function collectRtspsEndpoints( } continue; } + if (conn.usage !== 16 && conn.appLevelProtocol !== 6) { + continue; + } if (!fallbackHost || !conn.port) { continue; } @@ -126,181 +284,816 @@ export function collectRtspsEndpoints( return endpoints; } -async function bindEphemeralUdp(): Promise<{ socket: Socket; port: number }> { - const socket = createSocket("udp4"); - await new Promise((resolve, reject) => { - socket.once("error", reject); - socket.bind(0, "0.0.0.0", () => { - socket.off("error", reject); - resolve(); +export function resolveRtspControlUri(baseUri: string, control: string): string { + if (/^rtsps?:\/\//i.test(control)) { + return control; + } + + const base = new URL(baseUri.replace(/^rtsps:/i, "https:").replace(/^rtsp:/i, "http:")); + const scheme = baseUri.toLowerCase().startsWith("rtsp:") ? "rtsp:" : "rtsps:"; + if (control.startsWith("/")) { + return `${scheme}//${base.host}${control}`; + } + return `${baseUri.replace(/\/+$/, "")}/${control.replace(/^\/+/, "")}`; +} + +/** Official SETUP uses `streamid=video/0/0` when DESCRIBE advertised `streamid=video/0`. */ +export function officialVideoSetupControl(control: string): string { + if (/^streamid=video\/\d+$/i.test(control)) { + return `${control}/0`; + } + return control; +} + +/** Official bundle ICE remote ufrag is SETUP ping + 1 (`…998` → `…999`). */ +export function incrementNvstPingUfrag(payload: string): string | null { + if (!/^[0-9a-fA-F]+$/.test(payload) || payload.toUpperCase() === "PING") { + return null; + } + const next = (BigInt(`0x${payload}`) + 1n).toString(16); + return next.padStart(payload.length, "0"); +} + +/** + * Official NattHolePunch STUN remote ufrag: + * hex SETUP ping → ping+1 on the ICE bundle; otherwise the ping-string itself + * (`PING` when "Old server only supports PING"). Never fall back to DESCRIBE V2 + * while a SETUP ping payload is present — that keepalive identity is what the + * server answers. + */ +export function resolveNvstIceRemoteUfrag( + pingPayload: string | undefined, + describeUfrag?: string, + pingVersion?: number, +): string | undefined { + if (pingPayload) { + const incremented = incrementNvstPingUfrag(pingPayload); + if (incremented) { + return incremented; + } + if (pingPayload.toUpperCase() === "PING" || pingVersion === 6) { + return pingPayload; + } + } + return describeUfrag; +} + +function xorMappedIPv4(host: string, port: number): Buffer | null { + const parts = host.split(".").map((part) => Number.parseInt(part, 10)); + if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part) || part < 0 || part > 255)) { + return null; + } + const value = Buffer.alloc(8); + value.writeUInt8(0, 0); + value.writeUInt8(1, 1); + value.writeUInt16BE(port ^ 0x2112, 2); + value[4] = (parts[0] ?? 0) ^ 0x21; + value[5] = (parts[1] ?? 0) ^ 0x12; + value[6] = (parts[2] ?? 0) ^ 0xa4; + value[7] = (parts[3] ?? 0) ^ 0x42; + return value; +} + +/** Official ping-version 6 PONG: STUN Binding Success for an inbound request. */ +export function buildNvstStunBindingSuccess( + localPassword: string, + transactionId: Buffer, + mappedHost: string, + mappedPort: number, +): Buffer | null { + if (transactionId.length !== 12) { + return null; + } + const mapped = xorMappedIPv4(mappedHost, mappedPort); + if (!mapped) { + return null; + } + const header = Buffer.alloc(20); + header.writeUInt16BE(0x0101, 0); + header.writeUInt32BE(0x2112a442, 4); + transactionId.copy(header, 8); + const parts = [header]; + appendStunAttribute(parts, 0x0020, mapped); + let packet = Buffer.concat(parts); + header.writeUInt16BE(packet.length - 20 + 24, 2); + packet = Buffer.concat(parts); + appendStunAttribute(parts, 0x0008, createHmac("sha1", localPassword).update(packet).digest()); + packet = Buffer.concat(parts); + header.writeUInt16BE(packet.length - 20 + 8, 2); + packet = Buffer.concat(parts); + let crc = 0xffffffff; + for (const byte of packet) { + crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xff]!; + } + const fingerprint = Buffer.alloc(4); + fingerprint.writeUInt32BE(((crc ^ 0xffffffff) ^ 0x5354554e) >>> 0); + appendStunAttribute(parts, 0x8028, fingerprint); + return Buffer.concat(parts); +} + +function pickLocalIpv4(): string | undefined { + for (const addresses of Object.values(networkInterfaces())) { + for (const address of addresses ?? []) { + if (address.family === "IPv4" && !address.internal) { + return address.address; + } + } + } + return undefined; +} + +export function createNvstNegotiationDependencies( + overrides: Partial = {}, +): NvstRtspNegotiationDependencies { + return { + ...DEFAULT_NEGOTIATION_DEPENDENCIES, + ...overrides, + }; +} + +export async function bindEphemeralUdp(peerHost?: string, peerPort?: number): Promise { + const socket = createSocket({ type: "udp4", reuseAddr: true }); + try { + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.bind(0, "0.0.0.0", () => { + socket.off("error", reject); + resolve(); + }); }); - }); + } catch (error) { + try { + socket.close(); + } catch {} + throw error; + } + if (peerHost && peerPort) { + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.connect(peerPort, peerHost, () => { + socket.off("error", reject); + resolve(); + }); + }); + } const address = socket.address(); if (typeof address === "string") { socket.close(); throw new Error("Unexpected UDP socket address shape"); } - return { socket, port: address.port }; + let released = false; + if (peerHost && peerPort) { + socket.disconnect(); + } + const handle = (socket as unknown as { _handle?: { fd?: number } })._handle; + const fd = typeof handle?.fd === "number" && handle.fd >= 0 ? handle.fd : undefined; + return { + port: address.port, + localAddress: address.address === "0.0.0.0" ? pickLocalIpv4() : address.address, + fd, + send: async (payload, host, port) => { + await new Promise((resolve, reject) => { + socket.send(payload, port, host, (error) => error ? reject(error) : resolve()); + }); + }, + onMessage: (handler) => { + socket.on("message", (message, rinfo) => { + handler(Buffer.isBuffer(message) ? message : Buffer.from(message), { + address: rinfo.address, + port: rinfo.port, + }); + }); + }, + release: async () => { + if (released) { + return; + } + released = true; + await new Promise((resolve) => socket.close(resolve)); + }, + }; } -export async function runNvstRtspHandshakeProbe(input: NvstRtspProbeInput): Promise { +const DEFAULT_NEGOTIATION_DEPENDENCIES: NvstRtspNegotiationDependencies = { + createClient: (host, port, timeoutMs, onLog) => + new RtspOverWssClient(host, port, timeoutMs, onLog), + reserveUdpPort: bindEphemeralUdp, +}; + +async function teardownAndClose( + client: NvstRtspClient, + endpoint: string, + session: string | null, + reason: string, + onLog?: (message: string) => void, +): Promise { + try { + if (session) { + const response = await client.request("TEARDOWN", endpoint, { Session: session }); + if (response.statusCode === 200) { + log(onLog, `TEARDOWN ok (${reason})`); + } else { + log(onLog, `TEARDOWN returned ${response.statusCode} ${response.statusText} (${reason})`); + } + } + } catch (error) { + log(onLog, `TEARDOWN failed (${reason}): ${error instanceof Error ? error.message : String(error)}`); + } finally { + client.close(); + } +} + +export async function negotiateNvstRtspSession( + input: NvstRtspProbeInput, + dependencies: NvstRtspNegotiationDependencies = DEFAULT_NEGOTIATION_DEPENDENCIES, +): Promise { const steps: string[] = []; const endpoint = selectPrimaryRtspsEndpoint(input.rtspsEndpoints); if (!endpoint) { - return { - ok: false, - endpoint: "", - hmacSeedPresent: false, - steps, - error: "No rtsps:// endpoints available on the session", - }; + throw new NvstRtspNegotiationError( + "missing-rtsps-endpoint", + "No rtsps:// endpoints available on the session", + ); } const timeoutMs = input.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS; const wssUrl = rtspsUrlToWssUrl(endpoint); const parsedEndpoint = new URL(endpoint.replace(/^rtsps:/i, "https:").replace(/^rtsp:/i, "http:")); - const host = parsedEndpoint.hostname; - const port = Number(parsedEndpoint.port || "322"); - const client = new RtspOverWssClient( - host, - port, + const client = dependencies.createClient( + parsedEndpoint.hostname, + Number(parsedEndpoint.port || "322"), timeoutMs, (message) => log(input.onLog, message), ); - let udp: { socket: Socket; port: number } | null = null; + let udp: NvstUdpPortReservation | null = null; + let mjolnirUdp: NvstUdpPortReservation | null = null; + // Port of the native-owned Mjolnir video socket (undefined when the probe owns + // the fallback Mjolnir socket itself). + let nativeMjolnirPort: number | undefined; + let audioUdp: NvstUdpPortReservation | null = null; + const auxiliaryUdp: NvstUdpPortReservation[] = []; + const holePunchTimers: NodeJS.Timeout[] = []; + let videoHolePunchTimer: NodeJS.Timeout | null = null; + let session: string | null = null; + + const reserveBundle = (): Promise => + (dependencies.reserveBundlePort ?? dependencies.reserveUdpPort)(); try { log( input.onLog, - `Connecting RTSPS WSS ${wssUrl} via raw-TLS Bifrost-shaped upgrade (GET / then /v2/session/) (session ${input.sessionId})`, + `Connecting RTSPS WSS ${wssUrl} via raw-TLS Bifrost-shaped upgrade (GET /rtsp) (session ${input.sessionId})`, ); await client.connect(input.sessionId); steps.push("wss-open"); - const options = await client.request("OPTIONS", endpoint); + const rtspTarget = `rtsps://${parsedEndpoint.host}`; + const commonHeaders: Record = { + "X-GS-Version": "14.2", + Host: parsedEndpoint.host, + }; + if (input.sessionId.trim()) { + commonHeaders["x-nv-sessionid"] = input.sessionId.trim(); + } + const options = await client.request("OPTIONS", rtspTarget, commonHeaders); if (options.statusCode !== 200) { throw new Error(`OPTIONS failed: ${options.statusCode} ${options.statusText}`); } steps.push("options"); log(input.onLog, `OPTIONS ok (X-GS-Version=${header(options.headers, "x-gs-version") ?? "n/a"})`); - const describe = await client.request("DESCRIBE", endpoint, { + const describe = await client.request("DESCRIBE", rtspTarget, { + ...commonHeaders, Accept: "application/sdp", + // Official Bifrost sends x-nv-abtesting on DESCRIBE; the seat keys the modern + // ping/HMAC media context off it. Omitting it is treated as a legacy client. + "x-nv-abtesting": "2", }); if (describe.statusCode !== 200) { throw new Error(`DESCRIBE failed: ${describe.statusCode} ${describe.statusText}`); } steps.push("describe"); - const session = header(describe.headers, "session")?.split(";")[0]?.trim(); + session = header(describe.headers, "session")?.split(";")[0]?.trim() ?? null; if (!session) { throw new Error("DESCRIBE response missing Session header"); } + const videoControl = extractMediaControl(describe.body, "video"); + if (!videoControl) { + throw new NvstRtspNegotiationError( + "missing-video-control", + "DESCRIBE response did not advertise a video media control URI", + ); + } + const audioControl = extractMediaControl(describe.body, "audio"); + if (!audioControl) { + throw new NvstRtspNegotiationError( + "missing-audio-control", + "DESCRIBE response did not advertise an audio media control URI", + ); + } + const describedControls = [...describe.body.matchAll(/^a=control:(.+)$/gm)] + .map((match) => match[1]?.trim()) + .filter((control): control is string => Boolean(control)); + const controlStream = describedControls.find((control) => /(?:^|[=/])control\/0(?:\/|$)/i.test(control)); + if (!controlStream) { + throw new NvstRtspNegotiationError( + "missing-control-stream", + "DESCRIBE response did not advertise the primary control/0 stream", + ); + } + log(input.onLog, `DESCRIBE media controls: ${describedControls.join(", ") || "none"}`); + const videoControlUri = videoControl; const hmacSeed = extractHmacSeed(describe.body); + const iceCredentials = extractNvstIceCredentials(describe.body); + const legacyIceUsername = extractNvstSdpAttribute(describe.body, "general.iceUsernameFragment"); + const legacyIcePassword = extractNvstSdpAttribute(describe.body, "general.iceUsernamePwd"); + const v2IceUsername = extractNvstSdpAttribute(describe.body, "general.iceUserNameFragmentV2"); + const v2IcePassword = extractNvstSdpAttribute(describe.body, "general.icePasswordV2"); + const describedSrtpProfile = extractAdvertisedSrtpProfileFromSdp(describe.body); const describedKey = extractRuntimeEncryptionKey(describe.body); - let encryptionKeyHex: string; - let encryptionKeyId: number; + const dtlsFingerprint = extractNvstSdpAttribute(describe.body, "general.dtlsFingerprintV2") + ?? extractNvstSdpAttribute(describe.body, "general.dtlsFingerprint"); + const serverTransport = extractNvstSdpAttribute(describe.body, "general.serverTransport"); + const describedClientTransport = extractNvstSdpAttribute(describe.body, "general.clientTransport"); + const useNewIceInfo = extractNvstSdpAttribute(describe.body, "general.useNewIceInfo"); + const describedPingVersion = extractNvstSdpAttribute(describe.body, "general.pingVersion"); + const disablePlay = extractNvstSdpAttribute(describe.body, "general.disablePlay"); + const nativeRtcOnBundlePort = extractNvstSdpAttribute(describe.body, "general.nativeRtcOnBundlePort"); + log( + input.onLog, + `DESCRIBE transport metadata: dtlsFingerprintBytes=${dtlsFingerprint?.length ?? 0}, serverTransport=${serverTransport ?? "absent"}, clientTransport=${describedClientTransport ?? "absent"}, useNewIceInfo=${useNewIceInfo ?? "absent"}, pingVersion=${describedPingVersion ?? "absent"}, disablePlay=${disablePlay ?? "absent"}, nativeRtcOnBundlePort=${nativeRtcOnBundlePort ?? "absent"}, legacyIce=${legacyIceUsername?.length ?? 0}/${legacyIcePassword?.length ?? 0}, v2Ice=${v2IceUsername?.length ?? 0}/${v2IcePassword?.length ?? 0}, iceVariantsMatch=${legacyIceUsername === v2IceUsername && legacyIcePassword === v2IcePassword}`, + ); + let encryptionKeyHex: string | undefined; + let encryptionKeyId: number | undefined; let clientGenerated = false; if (describedKey) { encryptionKeyHex = describedKey.aesKeyHex; encryptionKeyId = describedKey.keyId; log( input.onLog, - `DESCRIBE ok (Session=${session}, HMAC ${hmacSeed ? "present" : "missing"}, encryptionKey ${redactKey(encryptionKeyHex)} from server)`, + `DESCRIBE ok (Session=${session}, videoControl=${videoControl}, HMAC ${hmacSeed ? "present" : "missing"}, ICE credentials ${iceCredentials ? `present (ufragBytes=${iceCredentials.usernameFragment.length}, pwdBytes=${iceCredentials.password.length})` : "missing"}, encryptionKey ${redactKey(encryptionKeyHex)} from server)`, ); } else { + // Official Bifrost ALWAYS client-generates runtime.encryptionKey and sends it in + // ANNOUNCE — even when a DTLS fingerprint is present. The video SRTP path (the + // separate non-DTLS socket) is keyed by this runtime key, not by DTLS-SRTP. If we + // skip generating it, the server never keys video for us and no video is sent. const generated = generateClientEncryptionKey(); encryptionKeyHex = generated.aesKeyHex; encryptionKeyId = generated.keyId; clientGenerated = true; log( input.onLog, - `DESCRIBE ok (Session=${session}, HMAC ${hmacSeed ? "present" : "missing"}, encryptionKey absent — client-generated ${redactKey(encryptionKeyHex)} for ANNOUNCE)`, + `DESCRIBE ok (Session=${session}, videoControl=${videoControl}, HMAC ${hmacSeed ? "present" : "missing"}, ICE credentials ${iceCredentials ? `present (ufragBytes=${iceCredentials.usernameFragment.length}, pwdBytes=${iceCredentials.password.length})` : "missing"}, encryptionKey absent — client-generated ${redactKey(encryptionKeyHex)} for ANNOUNCE)`, ); } - udp = await bindEphemeralUdp(); - const clientPort = udp.port; - // Transport uses X-GS-ClientPort (GameStream/Moonlight family). Official logs omit the - // client Transport summary string; server still returns X-GS-ServerPort + source. - const setup = await client.request("SETUP", `${endpoint}/streamid=video/0/0`, { + const officialCloudPath = nativeRtcOnBundlePort === "1"; + // Official Bifrost binds the ICE/bundle socket on 0.0.0.0 and never connects + // it to the RTSPS host. Connecting to :322 would create the wrong NAT mapping. + // Official: Mjolnir first (empty Transport line), then ICE bundle after SETUP. + const videoSetupUri = officialCloudPath + ? officialVideoSetupControl(videoControlUri) + : videoControlUri; + if (officialCloudPath) { + // Official two-socket model: the native streamer reserves BOTH the ICE/DTLS + // bundle and the dedicated NATT-only Mjolnir video socket in one nvst-bind. + // Reserve the bundle now so its mjolnirPort is known before video SETUP; the + // probe must not bind its own Mjolnir socket when the native streamer owns it. + udp = await reserveBundle(); + if (udp.mjolnirPort === undefined) { + // Older native streamer without a Mjolnir reservation: keep the probe-owned + // fallback socket so NATT keepalive still runs somewhere. + mjolnirUdp = await dependencies.reserveUdpPort(); + } else { + nativeMjolnirPort = udp.mjolnirPort; + } + } else { + udp = await reserveBundle(); + } + const setupHeaders: Record = { + ...commonHeaders, Session: session, - Transport: `unicast;X-GS-ClientPort=${clientPort}-${clientPort + 1}`, - }); + // Official Bifrost advertises its ping-protocol version on SETUP. The seat only + // returns a hex X-Nv-Ping-Payload (modern NATT/ICE identity) when this is present; + // without it the seat falls back to the literal "PING" keepalive and never arms + // the media relay to answer STUN. Echo the DESCRIBE-advertised version. + "x-nv-ping": describedPingVersion ?? "6", + }; + if (officialCloudPath) { + setupHeaders.Transport = ""; + } else if (udp) { + setupHeaders.Transport = `unicast;X-GS-ClientPort=${udp.port}-${udp.port + 1}`; + } + const setup = await client.request("SETUP", videoSetupUri, setupHeaders); if (setup.statusCode !== 200) { throw new Error(`SETUP video failed: ${setup.statusCode} ${setup.statusText}`); } steps.push("setup-video"); + session = header(setup.headers, "session")?.split(";")[0]?.trim() ?? session; + const setupSrtpProfile = extractAdvertisedSrtpProfileFromHeaders(setup.headers); + if ( + describedSrtpProfile + && setupSrtpProfile + && describedSrtpProfile !== setupSrtpProfile + ) { + throw new NvstRtspNegotiationError( + "conflicting-srtp-profile", + `DESCRIBE advertised ${describedSrtpProfile} but SETUP advertised ${setupSrtpProfile}`, + ); + } + const srtpProfile = setupSrtpProfile ?? describedSrtpProfile ?? undefined; const videoPeer = extractVideoPeer(header(setup.headers, "transport")); + if (!videoPeer) { + throw new NvstRtspNegotiationError( + "missing-video-peer", + "SETUP did not return video peer (X-GS-ServerPort/source)", + ); + } const pingPayload = header(setup.headers, "x-nv-ping-payload"); const pingVersionRaw = header(setup.headers, "x-nv-ping"); const pingVersion = pingVersionRaw ? Number(pingVersionRaw) : undefined; + if (pingVersion === 6 && (!iceCredentials || !pingPayload)) { + throw new NvstRtspNegotiationError( + "missing-ice-credentials", + "SETUP selected ping version 6 but its username payload or DESCRIBE password was missing", + ); + } + if (!udp) { + throw new NvstRtspNegotiationError( + "negotiation-failed", + "NVST ICE/bundle UDP socket was not reserved", + ); + } + const clientPort = udp.port; + const iceRemoteUfrag = resolveNvstIceRemoteUfrag( + pingPayload, + iceCredentials?.usernameFragment, + Number.isFinite(pingVersion) ? pingVersion : undefined, + ); + const reservedIce = udp.iceUsernameFragment && udp.icePassword + ? { usernameFragment: udp.iceUsernameFragment, password: udp.icePassword } + : null; + const localIceCredentials = iceCredentials + ? reservedIce ?? generateNvstIceCredentials() + : reservedIce; + const localDtlsFingerprint = udp.dtlsFingerprint; + const startAuthenticatedHolePunch = ( + reservation: NvstUdpPortReservation, + peer: { ip: string; port: number }, + options?: { nattUsername?: string; iceBurst?: boolean }, + ): NodeJS.Timeout | null => { + if (!localIceCredentials || !iceCredentials || !reservation.send) { + return null; + } + const iceBurst = options?.iceBurst !== false; + const nattUsername = options?.nattUsername; + let nonStunInbound = 0; + reservation.onMessage?.((payload, from) => { + if (payload.equals(Buffer.from("PING"))) { + void reservation.send?.(Buffer.from("PONG"), from.address, from.port).catch(() => undefined); + return; + } + if (payload.length >= 20 && payload.readUInt16BE(0) === 0x0001) { + const transactionId = payload.subarray(8, 20); + const pong = buildNvstStunBindingSuccess( + localIceCredentials.password, + transactionId, + from.address, + from.port, + ); + if (pong) { + void reservation.send?.(pong, from.address, from.port).catch(() => undefined); + } + return; + } + // Non-PING, non-STUN inbound is candidate video/SRTP. Log it (throttled) so we can + // confirm which socket the seat actually delivers video to (bundle vs Mjolnir). + nonStunInbound += 1; + if (nonStunInbound <= 5 || nonStunInbound % 200 === 0) { + log( + input.onLog, + `hole-punch inbound (non-STUN) port=${reservation.port} count=${nonStunInbound} bytes=${payload.length} firstByte=0x${payload.readUInt8(0).toString(16).padStart(2, "0")} from=${from.address}:${from.port}`, + ); + } + }); + const sendPing = (): void => { + // Official first burst is three ICE Binding Requests, then NATT + // ping-string PING ("Old server only supports PING") as keepalive. + if (iceBurst && iceRemoteUfrag) { + for (let burst = 0; burst < 3; burst += 1) { + const ice = buildNvstStunBindingRequest( + localIceCredentials.usernameFragment, + iceRemoteUfrag, + iceCredentials.password, + ); + void reservation.send?.(ice, peer.ip, peer.port).catch(() => undefined); + } + } + if (nattUsername) { + const natt = buildNvstStunBindingRequest( + localIceCredentials.usernameFragment, + nattUsername, + iceCredentials.password, + ); + void reservation.send?.(natt, peer.ip, peer.port).catch(() => undefined); + } + }; + sendPing(); + const timer = setInterval(sendPing, 20); + timer.unref(); + holePunchTimers.push(timer); + return timer; + }; + if (iceCredentials && localIceCredentials && !officialCloudPath) { + videoHolePunchTimer = startAuthenticatedHolePunch( + udp, + videoPeer, + { nattUsername: "PING" }, + ); + } + const setupHeaderNames = Object.keys(setup.headers).sort(); + const setupCredentialHeaders = setupHeaderNames + .filter((name) => /(ice|stun|credential|password|user)/i.test(name)) + .map((name) => `${name}[${header(setup.headers, name)?.length ?? 0}]`); log( input.onLog, - `SETUP video/0/0 ok (clientPort=${clientPort}, peer=${videoPeer ? `${videoPeer.ip}:${videoPeer.port}` : "unknown"})`, + `SETUP ${videoSetupUri} ok (official=${officialCloudPath}, bundlePort=${clientPort}${nativeMjolnirPort !== undefined ? `, mjolnirPort=${nativeMjolnirPort} (native)` : mjolnirUdp ? `, mjolnirPort=${mjolnirUdp.port}` : ""}, transport=${officialCloudPath ? "empty" : setupHeaders.Transport}, peer=${videoPeer.ip}:${videoPeer.port}, srtpProfile=${srtpProfile ?? "legacy-default"}, pingVersion=${Number.isFinite(pingVersion) ? pingVersion : "legacy"}, pingPayload=${pingPayload === undefined ? "absent" : JSON.stringify(pingPayload)}, iceRemote=${iceRemoteUfrag ?? "absent"}, pingPayloadBytes=${pingPayload ? Buffer.byteLength(pingPayload, "utf8") : 0}, headers=${setupHeaderNames.join(",")}, credentialHeaders=${setupCredentialHeaders.join(",") || "none"})`, ); + const handoffKeyHex = encryptionKeyHex ?? "00".repeat(32); + const handoffKeyId = encryptionKeyId ?? 0; + const saltHex = deriveSrtpSaltHex(handoffKeyId); const srtp: NvstSrtpMaterial = { - aesKeyHex: encryptionKeyHex, - keyId: encryptionKeyId, - masterKeySaltHex: packSrtpMasterKeySalt(encryptionKeyHex, encryptionKeyId), + aesKeyHex: handoffKeyHex, + keyId: handoffKeyId, + masterKeySaltHex: packSrtpMasterKeySalt(handoffKeyHex, handoffKeyId), + saltHex, + profile: srtpProfile, clientGenerated, }; + const videoSession: NvstVideoSession = { + clientUdpPort: clientPort, + // Native-owned Mjolnir video socket: the native streamer reads raw-SRTP video + // here while the bundle DTLS socket carries control/audio. + mjolnirUdpPort: nativeMjolnirPort, + videoPeerIp: videoPeer.ip, + videoPeerPort: videoPeer.port, + srtpAesKeyHex: srtp.aesKeyHex, + srtpKeyId: srtp.keyId, + srtpSaltHex: srtp.saltHex, + srtpProfile: srtp.profile, + pingPayload, + pingVersion: Number.isFinite(pingVersion) ? pingVersion : undefined, + localIceUsernameFragment: localIceCredentials?.usernameFragment, + localIcePassword: localIceCredentials?.password, + remoteIceUsernameFragment: iceRemoteUfrag + ?? (pingVersion === 6 ? pingPayload : undefined), + remoteIcePassword: iceCredentials?.password, + localDtlsFingerprint, + remoteDtlsFingerprint: dtlsFingerprint ?? undefined, + codec: input.codec, + timeoutMs: 60_000, + }; + if (input.onVideoReady) { + // Official binds Mjolnir before SETUP but does not send STUN until after ANNOUNCE. + log( + input.onLog, + officialCloudPath + ? `Video SETUP ready; deferring STUN until after ANNOUNCE (clientUdp ${clientPort})` + : `Video SETUP ready; keeping STUN hole-punch through remaining SETUP/ANNOUNCE (clientUdp ${clientPort})`, + ); + await input.onVideoReady(videoSession); + steps.push("native-receive-armed"); + } - const announceBody = buildAnnounceSdp({ - resolution: input.resolution, - fps: input.fps, - encryptionKeyHex, - encryptionKeyId, - }); + const setupTransport = (port: number): string => + `unicast;X-GS-ClientPort=${port}-${port + 1}`; + let audioClientPort = clientPort; + let controlClientPort: number | undefined; + if (!officialCloudPath) { + audioUdp = await dependencies.reserveUdpPort(); + audioClientPort = audioUdp.port; + const audioSetup = await client.request("SETUP", audioControl, { + ...commonHeaders, + Session: session, + Transport: setupTransport(audioClientPort), + }); + if (audioSetup.statusCode !== 200) { + throw new Error(`SETUP audio failed: ${audioSetup.statusCode} ${audioSetup.statusText}`); + } + steps.push("setup-audio"); + session = header(audioSetup.headers, "session")?.split(";")[0]?.trim() ?? session; + log(input.onLog, `SETUP ${audioControl} ok (clientPort=${audioClientPort})`); + const audioPeer = extractVideoPeer(header(audioSetup.headers, "transport")); + if (audioPeer && audioUdp && audioUdp !== udp) { + startAuthenticatedHolePunch(audioUdp, audioPeer, { + nattUsername: header(audioSetup.headers, "x-nv-ping-payload") ?? pingPayload, + }); + } + + for (const control of describedControls) { + if (control === videoControl || control === audioControl) { + continue; + } + const reservation = await dependencies.reserveUdpPort(); + if (reservation !== udp) { + auxiliaryUdp.push(reservation); + } + if (control === controlStream) { + controlClientPort = reservation.port; + } + const auxiliarySetup = await client.request("SETUP", control, { + ...commonHeaders, + Session: session, + Transport: setupTransport(reservation.port), + }); + if (auxiliarySetup.statusCode !== 200) { + throw new Error(`SETUP ${control} failed: ${auxiliarySetup.statusCode} ${auxiliarySetup.statusText}`); + } + steps.push(`setup-${control}`); + session = header(auxiliarySetup.headers, "session")?.split(";")[0]?.trim() ?? session; + const auxiliaryPeer = extractVideoPeer(header(auxiliarySetup.headers, "transport")); + if (auxiliaryPeer && reservation !== udp) { + startAuthenticatedHolePunch(reservation, auxiliaryPeer, { + nattUsername: header(auxiliarySetup.headers, "x-nv-ping-payload") ?? pingPayload, + }); + } + log( + input.onLog, + `SETUP ${control} ok (clientPort=${reservation.port}, peer=${auxiliaryPeer ? `${auxiliaryPeer.ip}:${auxiliaryPeer.port}` : "absent"}, pingVersion=${header(auxiliarySetup.headers, "x-nv-ping") ?? "legacy"})`, + ); + } + } else { + log( + input.onLog, + "Official cloud path: skipping audio/mic/control SETUP (WebRtcTransport owns those streams)", + ); + } + + const localIpv4 = udp.localAddress ?? pickLocalIpv4(); + const clientTransport = officialCloudPath + ? undefined + : (localIpv4 ? `${localIpv4}:${clientPort}` : undefined); const announce = await client.request( "ANNOUNCE", - endpoint, + officialCloudPath ? rtspTarget : "/", { + ...commonHeaders, Session: session, "Content-Type": "application/sdp", }, - announceBody, + buildAnnounceSdp(officialCloudPath + ? { + resolution: input.resolution, + fps: input.fps, + // Official always advertises the runtime encryptionKey in ANNOUNCE (it keys the + // video SRTP on the separate non-DTLS socket). Now that we always generate it, + // send it unconditionally rather than dropping it when a DTLS fingerprint exists. + encryptionKeyHex, + encryptionKeyId, + iceCredentials: localIceCredentials ?? undefined, + includeNvscLegacyIce: false, + includeNvscLegacyDtls: false, + videoPort: videoPeer.port, + clientPorts: { + video: 0, + audio: 0, + mic: 0, + control: 0, + bundle: 0, + session: 0, + localAddress: localIpv4, + }, + clientBundlePort: clientPort, + nativeRtcOnBundlePort: "1", + rtcVideoOnNativeBundle: false, + rtcAudioOnNativeBundle: true, + rtcMicOnNativeBundle: true, + rtcDataChannelOnNativeBundle: true, + enableUnifiedSocket: false, + // The native streamer opens the `rtcp1` SCTP data channel on the bundle and + // sends RTCP Receiver Reports / PLI over it, so advertise RTCP-over-SCTP. + rtcpOnSctp: true, + dtlsFingerprint: localDtlsFingerprint, + } + : { + resolution: input.resolution, + fps: input.fps, + encryptionKeyHex: describedKey || !dtlsFingerprint ? encryptionKeyHex : undefined, + encryptionKeyId: describedKey || !dtlsFingerprint ? encryptionKeyId : undefined, + iceCredentials: localIceCredentials ?? undefined, + videoPort: videoPeer.port, + clientPorts: { + video: clientPort, + audio: audioClientPort, + control: controlClientPort, + localAddress: localIpv4, + }, + clientTransport, + dtlsFingerprint: localDtlsFingerprint, + }), ); if (announce.statusCode !== 200) { throw new Error(`ANNOUNCE failed: ${announce.statusCode} ${announce.statusText}`); } steps.push("announce"); - log(input.onLog, "ANNOUNCE ok (allowlist + encryptionKey; ICE/DTLS omitted)"); - - const play = await client.request("PLAY", endpoint, { - Session: session, - Range: "npt=0.000-", - }); - if (play.statusCode !== 200) { - throw new Error(`PLAY failed: ${play.statusCode} ${play.statusText}`); + log( + input.onLog, + `ANNOUNCE ok (allowlist${encryptionKeyHex && (describedKey || !dtlsFingerprint) ? " + encryptionKey" : ""}${localIceCredentials ? ` + ICE V2 credentials (local ufragBytes=${localIceCredentials.usernameFragment.length}, pwdBytes=${localIceCredentials.password.length})` : ""}${localDtlsFingerprint ? ` + dtlsFingerprintBytes=${localDtlsFingerprint.length}` : ""}${localIpv4 ? ` + localAddress=${localIpv4}` : ""}${clientTransport ? ` + clientTransport=${clientTransport}` : ""}${localIpv4 && localIceCredentials ? ` + host candidate ${localIpv4}:${clientPort}` : ""})`, + ); + if (officialCloudPath && iceCredentials && localIceCredentials) { + log( + input.onLog, + `Starting official bundle STUN after ANNOUNCE (clientUdp ${clientPort}, iceRemote=${iceRemoteUfrag ?? "absent"})`, + ); + videoHolePunchTimer = startAuthenticatedHolePunch( + udp, + videoPeer, + { nattUsername: "PING" }, + ); } - steps.push("play"); - - // Close the probe UDP socket so the native streamer can rebind the same port. - udp.socket.close(); - udp = null; - - if (!videoPeer) { - throw new Error("SETUP did not return video peer (X-GS-ServerPort/source)"); + if (input.onAnnounceReady) { + log( + input.onLog, + `Starting native WebRtcTransport after ANNOUNCE (clientUdp ${clientPort})`, + ); + await input.onAnnounceReady(videoSession); + steps.push("native-announce-armed"); + } + if (officialCloudPath && iceCredentials && localIceCredentials && mjolnirUdp) { + // Probe-owned fallback only. When the native streamer owns the Mjolnir socket + // (nativeMjolnirPort set), its raw-SRTP receiver already runs this NATT + // keepalive — running a second one here would fight over the same socket. + // Official RtpSourceQueue on 49005 starts ~1ms before PLAY, after DTLS. + log( + input.onLog, + `Starting official Mjolnir NATT before PLAY (mjolnirPort=${mjolnirUdp.port})`, + ); + startAuthenticatedHolePunch(mjolnirUdp, videoPeer, { + iceBurst: false, + nattUsername: pingPayload && pingPayload !== "PING" ? pingPayload : "PING", + }); + } else if (officialCloudPath && nativeMjolnirPort !== undefined) { + log( + input.onLog, + `Mjolnir NATT owned by native streamer (mjolnirPort=${nativeMjolnirPort}); native raw-SRTP receiver keeps it alive`, + ); + } + if (officialCloudPath && disablePlay === "0") { + // Official waits for DTLS after setupWebRtcTransport, then PLAY returns 200. + await new Promise((resolve) => { + setTimeout(resolve, 400); + }); } - const videoSession: NvstVideoSession = { - clientUdpPort: clientPort, - videoPeerIp: videoPeer.ip, - videoPeerPort: videoPeer.port, - srtpAesKeyHex: srtp.aesKeyHex, - srtpKeyId: srtp.keyId, - pingPayload, - codec: input.codec, - }; - + if (disablePlay === "0") { + try { + const play = await client.request("PLAY", officialCloudPath ? rtspTarget : "/", { + ...commonHeaders, + Session: session, + }); + if (play.statusCode === 200) { + steps.push("play"); + log(input.onLog, "PLAY / ok"); + } else if (play.statusCode === 455) { + steps.push("play-455"); + log( + input.onLog, + `PLAY / returned 455 ${play.statusText} — treating as Bifrost ANNOUNCE-only`, + ); + } else { + steps.push("play-failed"); + log( + input.onLog, + `PLAY / returned ${play.statusCode} ${play.statusText}; continuing after ANNOUNCE`, + ); + } + } catch (error) { + steps.push("play-timeout"); + log( + input.onLog, + `PLAY / failed (${error instanceof Error ? error.message : String(error)}); continuing after ANNOUNCE`, + ); + } + } else { + steps.push("play-skipped"); + log(input.onLog, "PLAY skipped because DESCRIBE disabled it after ANNOUNCE"); + } log( input.onLog, - `PLAY ok — NVST video handoff ready (peer ${videoPeer.ip}:${videoPeer.port}, clientUdp ${clientPort}); WebRTC remains for SCTP input`, + `ANNOUNCE complete — NVST video handoff ready with video UDP still bound (peer ${videoPeer.ip}:${videoPeer.port}, clientUdp ${clientPort}, clientTransport=${clientTransport ?? "absent"})`, ); + let released = false; + const releaseVideoUdp = async (): Promise => { + // Keep STUN hole-punch on the native-owned bundle socket until the RTSP + // session is fully released. Official treats punch-receive failure as + // non-fatal; stopping sends as soon as native starts drops inbound DTLS. + await udp?.release().catch(() => undefined); + udp = null; + }; return { - ok: true, endpoint, session, hmacSeedPresent: Boolean(hmacSeed), @@ -311,7 +1104,78 @@ export async function runNvstRtspHandshakeProbe(input: NvstRtspProbeInput): Prom pingVersion: Number.isFinite(pingVersion) ? pingVersion : undefined, videoSession, steps, + videoUdpFd: udp.fd, + handoffVideoUdp: async () => { + if (released || !udp) { + return; + } + log(input.onLog, `Releasing Electron video UDP copy after native inherited the socket (clientUdp ${clientPort})`); + await releaseVideoUdp(); + }, + release: async (reason = "NVST session released") => { + if (released) { + return; + } + released = true; + for (const timer of holePunchTimers.splice(0)) { + clearInterval(timer); + } + videoHolePunchTimer = null; + await udp?.release().catch(() => undefined); + udp = null; + await mjolnirUdp?.release().catch(() => undefined); + mjolnirUdp = null; + await audioUdp?.release().catch(() => undefined); + audioUdp = null; + await Promise.all( + auxiliaryUdp.splice(0).map((reservation) => reservation.release().catch(() => undefined)), + ); + await teardownAndClose(client, endpoint, session, reason, input.onLog); + }, }; + } catch (error) { + for (const timer of holePunchTimers.splice(0)) { + clearInterval(timer); + } + await udp?.release().catch(() => undefined); + await mjolnirUdp?.release().catch(() => undefined); + await audioUdp?.release().catch(() => undefined); + await Promise.all(auxiliaryUdp.map((reservation) => reservation.release().catch(() => undefined))); + await teardownAndClose(client, endpoint, session, "failed negotiation", input.onLog); + if (error instanceof NvstRtspNegotiationError) { + throw new NvstRtspNegotiationError(error.code, error.message, { + cause: error, + }, [...steps]); + } + const message = error instanceof Error ? error.message : String(error); + throw new NvstRtspNegotiationError("negotiation-failed", message, { + cause: error, + }, [...steps]); + } +} + +export async function runNvstRtspHandshakeProbe( + input: NvstRtspProbeInput, + dependencies?: NvstRtspNegotiationDependencies, +): Promise { + const endpoint = selectPrimaryRtspsEndpoint(input.rtspsEndpoints) ?? ""; + try { + const negotiated = await negotiateNvstRtspSession(input, dependencies); + const result: NvstRtspProbeResult = { + ok: true, + endpoint: negotiated.endpoint, + session: negotiated.session, + hmacSeedPresent: negotiated.hmacSeedPresent, + videoPeer: negotiated.videoPeer, + clientUdpPort: negotiated.clientUdpPort, + srtp: negotiated.srtp, + pingPayload: negotiated.pingPayload, + pingVersion: negotiated.pingVersion, + videoSession: negotiated.videoSession, + steps: negotiated.steps, + }; + await negotiated.release("handshake probe complete"); + return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); log(input.onLog, `Probe failed: ${message}`); @@ -319,11 +1183,8 @@ export async function runNvstRtspHandshakeProbe(input: NvstRtspProbeInput): Prom ok: false, endpoint, hmacSeedPresent: false, - steps, + steps: error instanceof NvstRtspNegotiationError ? error.steps : [], error: message, }; - } finally { - client.close(); - udp?.socket.close(); } } diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts index a021bb644..53f1dd4d7 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.test.ts @@ -3,7 +3,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { extractVideoPeer, parseRtspResponse } from "./rtspClient"; +import { buildRtspRequest, extractVideoPeer, parseRtspResponse } from "./rtspClient"; test("extractVideoPeer prefers SETUP X-GS-ServerPort", () => { assert.deepEqual( @@ -12,6 +12,36 @@ test("extractVideoPeer prefers SETUP X-GS-ServerPort", () => { ); }); +test("buildRtspRequest keeps empty Transport and only sends Content-Length with a body", () => { + const setup = buildRtspRequest("SETUP", "streamid=video/0/0", { + "X-GS-Version": "14.2", + Host: "host.example:322", + Session: "XNV2060633119", + Transport: "", + }, "", 3); + assert.equal( + setup, + [ + "SETUP streamid=video/0/0 RTSP/1.0", + "CSeq: 3", + "Request-Id: 3", + "X-GS-Version: 14.2", + "Host: host.example:322", + "Session: XNV2060633119", + "Transport: ", + "", + "", + ].join("\r\n"), + ); + assert.match(setup, /^Transport: $/m); + assert.doesNotMatch(setup, /Content-Length/); + + const announce = buildRtspRequest("ANNOUNCE", "rtsps://host.example:322", { + "Content-Type": "application/sdp", + }, "v=0\r\n", 4); + assert.match(announce, /\r\nContent-Length: 5\r\n\r\nv=0\r\n$/); +}); + test("parseRtspResponse preserves status, normalized headers, and SDP body", () => { const response = parseRtspResponse( "RTSP/1.0 200 OK\r\nCSeq: 2\r\nSession: session-id;timeout=60\r\nContent-Length: 5\r\n\r\nv=0\r\n", diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts index 2136dfbc2..6ac500ab1 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/rtspClient.ts @@ -2,8 +2,6 @@ import type { Duplex } from "node:stream"; import { connectNvstWss, encodeWsTextFrame, WsFrameReader } from "./websocketTransport"; -const GS_VERSION = "14.2"; - export interface ParsedRtspResponse { statusCode: number; statusText: string; @@ -42,6 +40,33 @@ export function header(headers: Record, name: string): string | return headers[name.toLowerCase()]; } +/** Content-Length only when there is a body. Empty header values are kept (official SETUP sends `Transport: `). */ +export function buildRtspRequest( + method: string, + uri: string, + extraHeaders: Record = {}, + body = "", + cseq = 1, +): string { + const headers: Record = { + CSeq: String(cseq), + "Request-Id": String(cseq), + ...extraHeaders, + }; + if (body.length > 0) { + headers["Content-Length"] = String(Buffer.byteLength(body, "utf8")); + } + let message = `${method} ${uri} RTSP/1.0\r\n`; + for (const [key, value] of Object.entries(headers)) { + message += `${key}: ${value}\r\n`; + } + message += "\r\n"; + if (body.length > 0) { + message += body; + } + return message; +} + export function extractVideoPeer( transport: string | undefined, ): { ip: string; port: number } | undefined { @@ -124,24 +149,7 @@ export class RtspOverWssClient { } this.cseq += 1; - const headers: Record = { - CSeq: String(this.cseq), - "Request-Id": String(this.cseq), - "X-GS-Version": GS_VERSION, - ...extraHeaders, - }; - if (body.length > 0) { - headers["Content-Length"] = String(Buffer.byteLength(body, "utf8")); - } - - let message = `${method} ${uri} RTSP/1.0\r\n`; - for (const [key, value] of Object.entries(headers)) { - message += `${key}: ${value}\r\n`; - } - message += "\r\n"; - if (body.length > 0) { - message += body; - } + const message = buildRtspRequest(method, uri, extraHeaders, body, this.cseq); return await new Promise((resolve, reject) => { const timer = setTimeout(() => { diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts index d6900a87c..126bbd9b3 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.test.ts @@ -6,10 +6,34 @@ import assert from "node:assert/strict"; import { buildAnnounceSdp, extractHmacSeed, + extractNvstIceCredentials, + extractNvstSdpAttribute, + extractMediaControl, extractRuntimeEncryptionKey, + generateNvstIceCredentials, packSrtpMasterKeySalt, } from "./sdp"; +test("extractNvstIceCredentials reads native and standard SDP forms", () => { + assert.deepEqual( + extractNvstIceCredentials( + "a=x-nv-general.iceUsernameFragment:native-user\r\na=x-nv-general.iceUsernamePwd:native-password\r\n", + ), + { usernameFragment: "native-user", password: "native-password" }, + ); + assert.deepEqual( + extractNvstIceCredentials("a=ice-ufrag:standard-user\na=ice-pwd:standard-password\n"), + { usernameFragment: "standard-user", password: "standard-password" }, + ); + assert.deepEqual( + extractNvstIceCredentials( + "a=x-nv-general.iceUserNameFragmentV2:native-v2-user\r\na=x-nv-general.icePasswordV2:native-v2-password\r\n", + ), + { usernameFragment: "native-v2-user", password: "native-v2-password" }, + ); + assert.equal(extractNvstIceCredentials("a=ice-ufrag:incomplete\n"), null); +}); + test("extractHmacSeed reads DESCRIBE k= line", () => { const seed = extractHmacSeed( "v=0\r\nk=HMAC:76A28E94D8C07CB67C04C29CFAAAAF64BE4BA0899456217CB73D070E5060965F\r\na=x-nv-general.rtspWebSocketPerConnection:1\r\n", @@ -17,12 +41,157 @@ test("extractHmacSeed reads DESCRIBE k= line", () => { assert.equal(seed, "76A28E94D8C07CB67C04C29CFAAAAF64BE4BA0899456217CB73D070E5060965F"); }); -test("buildAnnounceSdp uses allowlist shape and omits ICE/DTLS", () => { - const sdp = buildAnnounceSdp({ resolution: "1920x1080", fps: 60 }); +test("extractNvstSdpAttribute reads DESCRIBE transport fields with or without x-nv-", () => { + assert.equal( + extractNvstSdpAttribute( + "a=x-nv-general.serverTransport:192.0.2.20:5004\r\na=x-nv-general.useNewIceInfo:0\r\n", + "general.serverTransport", + ), + "192.0.2.20:5004", + ); + assert.equal( + extractNvstSdpAttribute("a=general.useNewIceInfo:0\n", "general.useNewIceInfo"), + "0", + ); + assert.equal(extractNvstSdpAttribute("a=general.clientTransport:\n", "general.clientTransport"), null); +}); + +test("buildAnnounceSdp uses Bifrost session and attribute shape", () => { + const sdp = buildAnnounceSdp({ + resolution: "1920x1080", + fps: 60, + videoPort: 5004, + clientPorts: { video: 45000, audio: 45002, control: 45004 }, + }); + assert.match(sdp, /^o=unknown 0 14 IN IPv4 127\.0\.0\.1$/m); assert.match(sdp, /a=x-nv-video\[0\]\.clientViewportWd:1920/); assert.match(sdp, /a=x-nv-video\[0\]\.maxFPS:60/); - assert.match(sdp, /a=x-nv-general\.controlProtocol:udp_ag/); - assert.doesNotMatch(sdp, /iceUsernameFragment|dtlsFingerprint/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.video:45000/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.audio:45002/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.control:45004/); + assert.match(sdp, /m=video 5004\r\ni=DeviceString, DeviceName/); + assert.doesNotMatch(sdp, /RTP\/AVP|msid:video_0|clientTransport|nativeRtcOnBundlePort|iceUsernameFragment|dtlsFingerprint|controlProtocol/); +}); + +test("buildAnnounceSdp echoes nativeRtcOnBundlePort when the server advertised it", () => { + const sdp = buildAnnounceSdp({ + videoPort: 5004, + nativeRtcOnBundlePort: "1", + clientPorts: { video: 45000, audio: 45000, control: 45000, bundle: 45000 }, + }); + assert.match(sdp, /a=x-nv-general\.nativeRtcOnBundlePort:1/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.bundle:45000/); +}); + +test("buildAnnounceSdp marks rtc streams on the native bundle when unified", () => { + const sdp = buildAnnounceSdp({ + videoPort: 5004, + nativeRtcOnBundlePort: "1", + rtcOnNativeBundle: true, + }); + assert.match(sdp, /a=x-nv-general\.rtcVideoOnNativeBundle:1/); + assert.match(sdp, /a=x-nv-general\.rtcAudioOnNativeBundle:1/); + assert.match(sdp, /a=x-nv-general\.rtcDataChannelOnNativeBundle:1/); +}); + +test("buildAnnounceSdp matches official cloud bundle flags", () => { + const fingerprint = "00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF"; + const sdp = buildAnnounceSdp({ + videoPort: 5004, + iceCredentials: { usernameFragment: "Ab1+", password: "pwd0123456789abcdefABCD" }, + includeNvscLegacyIce: false, + includeNvscLegacyDtls: false, + dtlsFingerprint: fingerprint, + clientPorts: { + video: 0, + audio: 0, + mic: 0, + control: 0, + bundle: 0, + session: 0, + localAddress: "192.0.2.8", + }, + clientBundlePort: 49006, + nativeRtcOnBundlePort: "1", + rtcVideoOnNativeBundle: false, + rtcAudioOnNativeBundle: true, + rtcMicOnNativeBundle: true, + rtcDataChannelOnNativeBundle: true, + enableUnifiedSocket: false, + }); + assert.match(sdp, /a=x-nv-general\.clientPorts\.video:0/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.audio:0/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.mic:0/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.bundle:0/); + assert.match(sdp, /a=x-nv-general\.clientBundlePort:49006/); + assert.match(sdp, /a=x-nv-general\.rtcVideoOnNativeBundle:0/); + assert.match(sdp, /a=x-nv-general\.rtcAudioOnNativeBundle:1/); + assert.match(sdp, /a=x-nv-general\.rtcMicOnNativeBundle:1/); + assert.match(sdp, /a=x-nv-general\.enableUnifiedSocket:0/); + assert.doesNotMatch(sdp, /a=x-nv-general\.iceUsernameFragment:/); + assert.doesNotMatch(sdp, /a=x-nv-general\.iceUsernamePwd:/); + assert.doesNotMatch(sdp, /a=x-nv-general\.dtlsFingerprint:/); + assert.match(sdp, /a=x-nv-general\.iceUserNameFragmentV2:Ab1\+/); + assert.match(sdp, /a=x-nv-general\.dtlsFingerprintV2:/); + assert.match(sdp, /^a=ice-ufrag:Ab1\+$/m); + assert.match(sdp, /^a=candidate:1 1 udp 2122260223 192\.0\.2\.8 49006 typ host$/m); + assert.doesNotMatch(sdp, /clientTransport/); +}); + +test("buildAnnounceSdp includes official clientPorts.localAddress when provided", () => { + const sdp = buildAnnounceSdp({ + videoPort: 5004, + clientPorts: { video: 45000, localAddress: "192.0.2.8" }, + }); + assert.match(sdp, /a=x-nv-general\.clientPorts\.localAddress:192\.0\.2\.8/); + assert.match(sdp, /a=x-nv-general\.clientPorts\.video:45000/); +}); + +test("buildAnnounceSdp includes clientTransport only when provided", () => { + const sdp = buildAnnounceSdp({ + videoPort: 5004, + clientTransport: "192.0.2.8:45000", + }); + assert.match(sdp, /a=x-nv-general\.clientTransport:192\.0\.2\.8:45000/); +}); + +test("buildAnnounceSdp includes DTLS fingerprint V1 and V2 when provided", () => { + const fingerprint = "00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF"; + const sdp = buildAnnounceSdp({ dtlsFingerprint: fingerprint }); + assert.ok(sdp.includes(`a=x-nv-general.dtlsFingerprint:${fingerprint}`)); + assert.ok(sdp.includes(`a=x-nv-general.dtlsFingerprintV2:${fingerprint}`)); +}); + +test("buildAnnounceSdp includes generated ICE V2 credentials when negotiated", () => { + const credentials = generateNvstIceCredentials(); + assert.match(credentials.usernameFragment, /^[A-Za-z0-9+/]{4}$/); + assert.match(credentials.password, /^[A-Za-z0-9+/]{22}$/); + + const sdp = buildAnnounceSdp({ iceCredentials: credentials }); + assert.ok(sdp.includes(`a=x-nv-general.iceUsernameFragment:${credentials.usernameFragment}`)); + assert.ok(sdp.includes(`a=x-nv-general.iceUsernamePwd:${credentials.password}`)); + assert.ok(sdp.includes(`a=x-nv-general.iceUserNameFragmentV2:${credentials.usernameFragment}`)); + assert.ok(sdp.includes(`a=x-nv-general.icePasswordV2:${credentials.password}`)); + assert.ok(sdp.includes(`a=ice-ufrag:${credentials.usernameFragment}`)); + assert.ok(sdp.includes(`a=ice-pwd:${credentials.password}`)); +}); + +test("buildAnnounceSdp includes official WebRTC ICE/DTLS and host candidate", () => { + const fingerprint = "00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF"; + const sdp = buildAnnounceSdp({ + videoPort: 5004, + iceCredentials: { usernameFragment: "Ab1+", password: "pwd0123456789abcdefABCD" }, + dtlsFingerprint: fingerprint, + clientPorts: { video: 45000, bundle: 45000, localAddress: "192.0.2.8" }, + }); + assert.match(sdp, /^a=ice-options:trickle$/m); + assert.match(sdp, /^a=ice-ufrag:Ab1\+$/m); + assert.match(sdp, /^a=ice-pwd:pwd0123456789abcdefABCD$/m); + assert.ok(sdp.includes(`a=fingerprint:sha-256 ${fingerprint}`)); + assert.match(sdp, /^a=setup:actpass$/m); + assert.match(sdp, /^a=candidate:1 1 udp 2122260223 192\.0\.2\.8 45000 typ host$/m); + assert.match(sdp, /^c=IN IP4 0\.0\.0\.0$/m); + assert.match(sdp, /^m=video 5004$/m); }); test("packSrtpMasterKeySalt matches geronimo keyId packing", () => { @@ -44,3 +213,19 @@ test("extractRuntimeEncryptionKey reads DESCRIBE attrs", () => { assert.ok(parsed); assert.equal(parsed?.keyId, 2664076126); }); + +test("extractMediaControl reads the advertised video track instead of session control", () => { + const sdp = [ + "v=0", + "a=control:*", + "m=audio 0 RTP/AVP 97", + "a=control:streamid=audio/0/0", + "m=video 0 RTP/AVP 96", + "a=control:tracks/server-selected-video", + "", + ].join("\r\n"); + + assert.equal(extractMediaControl(sdp, "video"), "tracks/server-selected-video"); + assert.equal(extractMediaControl(sdp, "audio"), "streamid=audio/0/0"); + assert.equal(extractMediaControl(sdp, "control"), null); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts index 6f009571b..32fccdb62 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/sdp.ts @@ -1,5 +1,7 @@ import { randomBytes } from "node:crypto"; +import { deriveSrtpSaltHex } from "./srtp"; + /** Minimal ANNOUNCE attrs from docs/research/nvst-announce-allowlist-1080p60.json */ const ANNOUNCE_ALLOWLIST = { video: { @@ -35,8 +37,6 @@ const ANNOUNCE_ALLOWLIST = { "drc.enable": "0", "dfc.adjustResAndFps": "0", calculateAvgVideoStreamingBitrate: "1", - "bw.maximumBitrateKbps": "100000", - "bw.minimumBitrateKbps": "1000", }, packetPacing: { version: "3", @@ -81,23 +81,70 @@ function parseResolution(resolution: string | undefined): { width: number; heigh return { width: Number(match[1]), height: Number(match[2]) }; } +export function extractNvstSdpAttribute(sdp: string, name: string): string | null { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = new RegExp(`^a=(?:x-nv-)?${escaped}:([^\\r\\n]*)$`, "mi").exec(sdp); + const value = match?.[1]?.trim(); + return value ? value : null; +} + export function buildAnnounceSdp( options: { resolution?: string; fps?: number; encryptionKeyHex?: string; encryptionKeyId?: number; + iceCredentials?: { usernameFragment: string; password: string }; + /** Server video port from SETUP / DESCRIBE. Capture uses `m=video 5004`. */ + videoPort?: number; + clientPorts?: { + video: number; + audio?: number; + mic?: number; + control?: number; + bundle?: number; + session?: number; + /** Official `general.clientPorts.localAddress` — routable NIC IPv4. */ + localAddress?: string; + }; + /** Official `general.clientBundlePort` — ICE/DTLS socket, distinct from clientPorts.bundle. */ + clientBundlePort?: number; + /** Official `general.clientTransport` form is `ip:port`. */ + clientTransport?: string; + nativeRtcOnBundlePort?: string; + /** Official `general.rtc{Video,Audio,DataChannel}OnNativeBundle` when unified. */ + rtcOnNativeBundle?: boolean; + rtcVideoOnNativeBundle?: boolean; + rtcAudioOnNativeBundle?: boolean; + rtcMicOnNativeBundle?: boolean; + rtcDataChannelOnNativeBundle?: boolean; + enableUnifiedSocket?: boolean; + /** + * Official `general.rtcpOnSctp` gates RTCP feedback onto the `rtcp1` SCTP data + * channel. We do not bring that channel up, so advertise 0 to keep feedback on + * plain SRTCP over the Mjolnir socket (which the native receiver already sends). + */ + rtcpOnSctp?: boolean; + /** + * Official skips Nvsc V1 `iceUsernameFragment` / `iceUsernamePwd` / + * `dtlsFingerprint` and keeps V2 plus WebRTC `a=ice-*`. + */ + includeNvscLegacyIce?: boolean; + includeNvscLegacyDtls?: boolean; + /** SHA-256 colon hex (95 chars). Written as V1 + V2 to match Bifrost/mall. */ + dtlsFingerprint?: string; } = {}, ): string { const { width, height } = parseResolution(options.resolution); const fps = options.fps && options.fps > 0 ? Math.round(options.fps) : 60; const frameTimeUs = String(Math.round(1_000_000 / fps)); + const videoPort = options.videoPort && options.videoPort > 0 ? options.videoPort : 0; const lines: string[] = [ "v=0", - "o=- 0 0 IN IP4 127.0.0.1", - "s=OpenNOW NVST Handshake", - "t=0 0", + // Official macOS handshake origin username is "unknown", not "android". + "o=unknown 0 14 IN IPv4 127.0.0.1", + "s=NVIDIA Streaming Client", ]; const pushGroup = ( @@ -131,14 +178,102 @@ export function buildAnnounceSdp( lines.push("a=x-nv-runtime.videoSrtp:1"); if (options.encryptionKeyHex && options.encryptionKeyId !== undefined) { lines.push(`a=x-nv-runtime.encryptionKey:${options.encryptionKeyHex.toUpperCase()}`); - // Signed i32 form matches geronimo runtime.encryptionKeyId dumps. - const signedId = options.encryptionKeyId > 0x7fffffff - ? options.encryptionKeyId - 0x1_0000_0000 - : options.encryptionKeyId; - lines.push(`a=x-nv-runtime.encryptionKeyId:${signedId}`); - } - // Control protocol preference evidenced in geronimo: udp_ag (not encrypted). - lines.push("a=x-nv-general.controlProtocol:udp_ag"); + // Official sends the keyId unsigned (u32) on the wire, and our salt derivation uses the + // unsigned form too (deriveSrtpSaltHex does keyId >>> 0), so both ends derive the same salt. + lines.push(`a=x-nv-runtime.encryptionKeyId:${options.encryptionKeyId >>> 0}`); + } + if (options.iceCredentials) { + if (options.includeNvscLegacyIce !== false) { + lines.push(`a=x-nv-general.iceUsernameFragment:${options.iceCredentials.usernameFragment}`); + lines.push(`a=x-nv-general.iceUsernamePwd:${options.iceCredentials.password}`); + } + lines.push(`a=x-nv-general.iceUserNameFragmentV2:${options.iceCredentials.usernameFragment}`); + lines.push(`a=x-nv-general.icePasswordV2:${options.iceCredentials.password}`); + } + if (options.clientPorts) { + if (options.clientPorts.localAddress) { + lines.push(`a=x-nv-general.clientPorts.localAddress:${options.clientPorts.localAddress}`); + } + lines.push(`a=x-nv-general.clientPorts.video:${options.clientPorts.video}`); + if (options.clientPorts.audio !== undefined) { + lines.push(`a=x-nv-general.clientPorts.audio:${options.clientPorts.audio}`); + } + if (options.clientPorts.mic !== undefined) { + lines.push(`a=x-nv-general.clientPorts.mic:${options.clientPorts.mic}`); + } + if (options.clientPorts.control !== undefined) { + lines.push(`a=x-nv-general.clientPorts.control:${options.clientPorts.control}`); + } + if (options.clientPorts.bundle !== undefined) { + lines.push(`a=x-nv-general.clientPorts.bundle:${options.clientPorts.bundle}`); + } + if (options.clientPorts.session !== undefined) { + lines.push(`a=x-nv-general.clientPorts.session:${options.clientPorts.session}`); + } + } + if (options.clientBundlePort !== undefined) { + lines.push(`a=x-nv-general.clientBundlePort:${options.clientBundlePort}`); + } + if (options.clientTransport) { + lines.push(`a=x-nv-general.clientTransport:${options.clientTransport}`); + } + if (options.nativeRtcOnBundlePort) { + lines.push(`a=x-nv-general.nativeRtcOnBundlePort:${options.nativeRtcOnBundlePort}`); + } + const rtcVideo = options.rtcVideoOnNativeBundle ?? (options.rtcOnNativeBundle ? true : undefined); + const rtcAudio = options.rtcAudioOnNativeBundle ?? (options.rtcOnNativeBundle ? true : undefined); + const rtcMic = options.rtcMicOnNativeBundle; + const rtcData = options.rtcDataChannelOnNativeBundle ?? (options.rtcOnNativeBundle ? true : undefined); + if (rtcVideo !== undefined) { + lines.push(`a=x-nv-general.rtcVideoOnNativeBundle:${rtcVideo ? "1" : "0"}`); + } + if (rtcAudio !== undefined) { + lines.push(`a=x-nv-general.rtcAudioOnNativeBundle:${rtcAudio ? "1" : "0"}`); + } + if (rtcMic !== undefined) { + lines.push(`a=x-nv-general.rtcMicOnNativeBundle:${rtcMic ? "1" : "0"}`); + } + if (rtcData !== undefined) { + lines.push(`a=x-nv-general.rtcDataChannelOnNativeBundle:${rtcData ? "1" : "0"}`); + } + if (options.enableUnifiedSocket !== undefined) { + lines.push(`a=x-nv-general.enableUnifiedSocket:${options.enableUnifiedSocket ? "1" : "0"}`); + } + if (options.rtcpOnSctp !== undefined) { + lines.push(`a=x-nv-general.rtcpOnSctp:${options.rtcpOnSctp ? "1" : "0"}`); + } + if (options.dtlsFingerprint) { + if (options.includeNvscLegacyDtls !== false) { + lines.push(`a=x-nv-general.dtlsFingerprint:${options.dtlsFingerprint}`); + } + lines.push(`a=x-nv-general.dtlsFingerprintV2:${options.dtlsFingerprint}`); + } + // Official doAnnounce also emits CreateAnswer WebRTC ICE/DTLS (a=ice-ufrag, + // a=fingerprint, host a=candidate). NVST x-nv-general.* alone does not arm inbound UDP. + if (options.iceCredentials) { + lines.push("a=ice-options:trickle"); + lines.push(`a=ice-ufrag:${options.iceCredentials.usernameFragment}`); + lines.push(`a=ice-pwd:${options.iceCredentials.password}`); + } + if (options.dtlsFingerprint) { + lines.push(`a=fingerprint:sha-256 ${options.dtlsFingerprint}`); + lines.push("a=setup:actpass"); + } + const candidateAddress = options.clientPorts?.localAddress; + const candidatePort = options.clientBundlePort + ?? (options.clientPorts?.bundle && options.clientPorts.bundle > 0 ? options.clientPorts.bundle : undefined) + ?? (options.clientPorts?.video && options.clientPorts.video > 0 ? options.clientPorts.video : undefined); + if (candidateAddress && candidatePort) { + // Official CreateLocalCandidate format string: `a=candidate:1 1 udp 2122260223 ` + ` typ host`. + lines.push(`a=candidate:1 1 udp 2122260223 ${candidateAddress} ${candidatePort} typ host`); + } + lines.push("t=0 0"); + // Live capture ANNOUNCE uses the server video port, not SDP's "port 0 = unused". + lines.push(`m=video ${videoPort}`); + if (options.iceCredentials || options.dtlsFingerprint) { + lines.push("c=IN IP4 0.0.0.0"); + } + lines.push("i=DeviceString, DeviceName"); lines.push(""); return lines.join("\r\n"); } @@ -148,6 +283,23 @@ export function extractHmacSeed(sdp: string): string | null { return match?.[1] ?? null; } +export function extractNvstIceCredentials( + sdp: string, +): { usernameFragment: string; password: string } | null { + const usernameFragment = /^(?:a=(?:x-nv-)?general\.iceUsernameFragment:|a=ice-ufrag:)([^\r\n]+)\s*$/mi + .exec(sdp)?.[1]?.trim() + ?? /^(?:a=(?:x-nv-)?general\.iceUserNameFragmentV2:)([^\r\n]+)\s*$/mi + .exec(sdp)?.[1]?.trim(); + const password = /^(?:a=(?:x-nv-)?general\.iceUsernamePwd:|a=ice-pwd:)([^\r\n]+)\s*$/mi + .exec(sdp)?.[1]?.trim() + ?? /^(?:a=(?:x-nv-)?general\.icePasswordV2:)([^\r\n]+)\s*$/mi + .exec(sdp)?.[1]?.trim(); + if (!usernameFragment || !password) { + return null; + } + return { usernameFragment, password }; +} + /** * Pack AES-256 key + keyId into libsrtp master key||salt (88 hex). * Salt = keyId as `%024x` (12 bytes BE). See docs/research/nvst-srtp-key-derivation.md. @@ -157,9 +309,7 @@ export function packSrtpMasterKeySalt(aesKeyHex: string, keyId: number): string if (!/^[0-9A-F]{64}$/.test(key)) { throw new Error(`encryptionKey must be 64 hex chars, got length ${key.length}`); } - const id = keyId >>> 0; - const salt = id.toString(16).toUpperCase().padStart(24, "0"); - return `${key}${salt}`; + return `${key}${deriveSrtpSaltHex(keyId)}`; } export function extractRuntimeEncryptionKey( @@ -181,12 +331,47 @@ export function extractRuntimeEncryptionKey( return { aesKeyHex: keyMatch[1]!.toUpperCase(), keyId: keyId >>> 0 }; } +export function extractMediaControl(sdp: string, mediaType: string): string | null { + let currentMediaType: string | null = null; + + for (const rawLine of sdp.split(/\r?\n/)) { + const line = rawLine.trim(); + if (line.startsWith("m=")) { + currentMediaType = line.slice(2).split(/\s+/, 1)[0]?.toLowerCase() ?? null; + continue; + } + if (currentMediaType !== mediaType.toLowerCase() || !line.startsWith("a=control:")) { + continue; + } + + const control = line.slice("a=control:".length).trim(); + if (control && control !== "*") { + return control; + } + } + + return null; +} + export function generateClientEncryptionKey(): { aesKeyHex: string; keyId: number } { const aesKeyHex = randomBytes(32).toString("hex").toUpperCase(); const keyId = randomBytes(4).readUInt32BE(0); return { aesKeyHex, keyId }; } +export function generateNvstIceCredentials(): { usernameFragment: string; password: string } { + const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"; + const random = randomBytes(26); + const encode = (start: number, length: number): string => Array.from( + random.subarray(start, start + length), + (value) => alphabet[value & 0x3f], + ).join(""); + return { + usernameFragment: encode(0, 4), + password: encode(4, 22), + }; +} + export function redactKey(aesKeyHex: string): string { if (aesKeyHex.length < 8) { return "****"; diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/srtp.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/srtp.test.ts new file mode 100644 index 000000000..953f637bf --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/srtp.test.ts @@ -0,0 +1,77 @@ +/// + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + deriveSrtpSaltHex, + extractAdvertisedSrtpProfileFromHeaders, + extractAdvertisedSrtpProfileFromSdp, +} from "./srtp"; + +test("extractAdvertisedSrtpProfileFromSdp reads standard crypto suites", () => { + assert.equal( + extractAdvertisedSrtpProfileFromSdp([ + "v=0", + "m=video 0 RTP/SAVP 96", + "a=crypto:1 AEAD_AES_256_GCM inline:ignored", + "", + ].join("\r\n")), + "AEAD_AES_256_GCM", + ); +}); + +test("extractAdvertisedSrtpProfileFromSdp accepts an explicit SRTP-named attribute", () => { + assert.equal( + extractAdvertisedSrtpProfileFromSdp( + "v=0\r\na=x-provider-srtp-suite:AES_CM_128_HMAC_SHA1_80\r\n", + ), + "AES_CM_128_HMAC_SHA1_80", + ); +}); + +test("extractAdvertisedSrtpProfileFromHeaders reads explicit SETUP transport data", () => { + assert.equal( + extractAdvertisedSrtpProfileFromHeaders({ + transport: "RTP/SAVP;unicast;profile=AEAD_AES_128_GCM;X-GS-ServerPort=5004", + }), + "AEAD_AES_128_GCM", + ); + assert.equal( + extractAdvertisedSrtpProfileFromHeaders({ + "x-provider-srtp-suite": "AES_CM_128_HMAC_SHA1_32", + }), + "AES_CM_128_HMAC_SHA1_32", + ); +}); + +test("profile parser ignores unscoped and unknown profile text", () => { + assert.equal( + extractAdvertisedSrtpProfileFromSdp( + "v=0\r\na=x-note:prefer AEAD_AES_256_GCM\r\n", + ), + null, + ); + assert.equal( + extractAdvertisedSrtpProfileFromSdp( + "v=0\r\na=x-provider-srtp-suite:UNSUPPORTED_PROFILE\r\n", + ), + null, + ); + assert.equal( + extractAdvertisedSrtpProfileFromSdp( + "v=0\r\na=x-provider-srtp-supported-profiles:AEAD_AES_128_GCM AEAD_AES_256_GCM\r\n", + ), + null, + ); + assert.equal( + extractAdvertisedSrtpProfileFromHeaders({ + "x-note": "AEAD_AES_256_GCM", + }), + null, + ); +}); + +test("deriveSrtpSaltHex returns the direct 12-byte key-id salt", () => { + assert.equal(deriveSrtpSaltHex(2664076126), "00000000000000009ECA935E"); +}); diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/srtp.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/srtp.ts new file mode 100644 index 000000000..19fd82380 --- /dev/null +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/srtp.ts @@ -0,0 +1,77 @@ +import type { NvstSrtpProfile } from "@shared/gfn"; + +const SRTP_PROFILES: readonly NvstSrtpProfile[] = [ + "AEAD_AES_128_GCM", + "AEAD_AES_256_GCM", + "AES_CM_128_HMAC_SHA1_32", + "AES_CM_128_HMAC_SHA1_80", + "AES_CM_256_HMAC_SHA1_32", + "AES_CM_256_HMAC_SHA1_80", +]; +const SRTP_PROFILE_SET = new Set(SRTP_PROFILES); + +function findSrtpProfile(value: string): NvstSrtpProfile | null { + for (const token of value.toUpperCase().match(/[A-Z][A-Z0-9_]*/g) ?? []) { + if (SRTP_PROFILE_SET.has(token)) { + return token as NvstSrtpProfile; + } + } + return null; +} + +export function extractAdvertisedSrtpProfileFromSdp( + sdp: string, +): NvstSrtpProfile | null { + for (const rawLine of sdp.split(/\r?\n/)) { + const line = rawLine.trim(); + if (/^a=crypto:\d+\s+/i.test(line)) { + const profile = findSrtpProfile(line); + if (profile) { + return profile; + } + continue; + } + + const attribute = /^a=([^:]+):(.*)$/i.exec(line); + const attributeName = attribute?.[1] ?? ""; + if ( + !attribute + || !/(?:srtp|crypto)/i.test(attributeName) + || !/(?:profile|suite)/i.test(attributeName) + || /(?:supported|capabilit)/i.test(attributeName) + ) { + continue; + } + const profile = findSrtpProfile(attribute[2] ?? ""); + if (profile) { + return profile; + } + } + return null; +} + +export function extractAdvertisedSrtpProfileFromHeaders( + headers: Record, +): NvstSrtpProfile | null { + for (const [name, value] of Object.entries(headers)) { + if ( + name.toLowerCase() !== "transport" + && ( + !/(?:srtp|crypto)/i.test(name) + || !/(?:profile|suite)/i.test(name) + || /(?:supported|capabilit)/i.test(name) + ) + ) { + continue; + } + const profile = findSrtpProfile(value); + if (profile) { + return profile; + } + } + return null; +} + +export function deriveSrtpSaltHex(keyId: number): string { + return (keyId >>> 0).toString(16).toUpperCase().padStart(24, "0"); +} diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts index f6cf8004e..76925aa86 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.test.ts @@ -21,6 +21,7 @@ test("buildNvstWssUpgradeRequest uses Bifrost-shaped GET / by default", () => { assert.equal(requestLine, "GET / HTTP/1.1"); assert.equal(Buffer.from(requestLine, "utf8").toString("hex"), "474554202f20485454502f312e31"); assert.equal(buildNvstWssUpgradeRequestTarget("host.example", 322, "slash"), "/"); + assert.equal(buildNvstWssUpgradeRequestTarget("host.example", 322, "rtspPath"), "/rtsp"); assert.equal( buildNvstWssUpgradeRequestTarget("host.example", 322, "sessionPath", "abc-uuid"), "/v2/session/abc-uuid", @@ -36,6 +37,15 @@ test("buildNvstWssUpgradeRequest uses Bifrost-shaped GET / by default", () => { assert.doesNotMatch(request, /x-nv-sessionid/i); }); +test("buildNvstWssUpgradeRequest supports the /rtsp endpoint with session identity", () => { + const request = buildNvstWssUpgradeRequest("host.example", 322, "key", { + form: "rtspPath", + sessionId: "sess-uuid", + }); + assert.match(request, /^GET \/rtsp HTTP\/1\.1\r\n/); + assert.match(request, /\r\nx-nv-sessionid: sess-uuid\r\n/); +}); + test("buildEmptyPathUpgradeRequest keeps empty URI for research (live → 400)", () => { const request = buildEmptyPathUpgradeRequest( "80-250-97-40.cloudmatchbeta.nvidiagrid.net", @@ -47,7 +57,7 @@ test("buildEmptyPathUpgradeRequest keeps empty URI for research (live → 400)", assert.equal(Buffer.from(requestLine, "utf8").toString("hex"), "4745542020485454502f312e31"); }); -test("buildNvstWssUpgradeRequest can attach x-nv-sessionid for 403 retry", () => { +test("buildNvstWssUpgradeRequest attaches x-nv-sessionid to upgrade requests", () => { const request = buildNvstWssUpgradeRequest("host.example", 322, "abc", { form: "slash", sessionId: "sess-uuid", diff --git a/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts index 3d0f334f5..4976db25b 100644 --- a/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts +++ b/opennow-stable/src/main/platforms/gfn/nvstRtsp/websocketTransport.ts @@ -2,19 +2,9 @@ import { createHash, randomBytes } from "node:crypto"; import type { Duplex } from "node:stream"; import { connect as tlsConnect, type TLSSocket } from "node:tls"; -/** - * Official `:322` upgrade request-target is still under research. - * - * Live matrix (raw TLS unless noted): - * - empty (`GET HTTP/1.1`) → HTTP 400 - * - absolute `rtsps://` / `wss://` / `https://` → HTTP 404 - * - `/` via Node `ws` → HTTP 404 (extra Client headers; not Bifrost-shaped) - * - `/` via raw TLS Bifrost-shaped headers → **pending** (never tried) - * - * Poco 1.14.1 WebSocket::connect does not set method/URI; it only adds Upgrade - * headers. Default HTTPRequest is GET `/`. See docs/research/_tmp-bifrost2-ws-uri-resolved.txt. - */ +/** The official client upgrades GET /rtsp with x-nv-sessionid. */ export type NvstWssUpgradeTargetForm = + | "rtspPath" | "slash" | "sessionPath" | "rtsps" @@ -29,6 +19,8 @@ export function buildNvstWssUpgradeRequestTarget( sessionId?: string, ): string { switch (form) { + case "rtspPath": + return "/rtsp"; case "slash": return "/"; case "sessionPath": @@ -49,7 +41,6 @@ export function buildNvstWssUpgradeRequestTarget( /** * Raw TLS WebSocket upgrade for NVST `:322`. * Header order matches Poco WebSocket::connect + Bifrost Content-Length: 0. - * Optional `x-nv-sessionid` is only for a 403 retry. */ export function buildNvstWssUpgradeRequest( host: string, @@ -75,7 +66,6 @@ export function buildNvstWssUpgradeRequest( `Content-Length: 0\r\n`; const sessionId = options.sessionId?.trim(); if (sessionId && form !== "sessionPath") { - // Only attach as header on 403 retry paths; sessionPath already uses UUID in URI. request += `x-nv-sessionid: ${sessionId}\r\n`; } return `${request}\r\n`; @@ -100,7 +90,6 @@ function connectNvstWssOnce( timeoutMs: number, form: NvstWssUpgradeTargetForm, sessionId?: string, - attachSessionHeader = false, ): Promise { const key = randomBytes(16).toString("base64"); const expectedAccept = createHash("sha1") @@ -108,9 +97,7 @@ function connectNvstWssOnce( .digest("base64"); const request = buildNvstWssUpgradeRequest(host, port, key, { form, - // For slash/rtsps/… first attempt: no session header. - // For 403 retry or sessionPath: pass sessionId. - sessionId: attachSessionHeader || form === "sessionPath" ? sessionId : undefined, + sessionId, }); const requestLine = request.split("\r\n")[0] ?? ""; const requestLineHex = Buffer.from(requestLine, "utf8").toString("hex"); @@ -192,7 +179,7 @@ function connectNvstWssOnce( new Error( `WSS upgrade failed: HTTP ${statusCode || "unknown"} (${statusLine || "no status"}); ` + `form=${form} request-line=${requestLine} hex=${requestLineHex}` + - (attachSessionHeader ? " with x-nv-sessionid" : ""), + (sessionId && form !== "sessionPath" ? " with x-nv-sessionid" : ""), ), ); return; @@ -206,9 +193,6 @@ function connectNvstWssOnce( }); } -/** Primary: Bifrost-shaped GET /. Fallback: CloudMatch-style /v2/session/. */ -const UPGRADE_TARGET_FORMS: NvstWssUpgradeTargetForm[] = ["slash", "sessionPath"]; - export async function connectNvstWss( host: string, port: number, @@ -216,33 +200,10 @@ export async function connectNvstWss( sessionId?: string, onLog?: (message: string) => void, ): Promise { - let lastError: Error | null = null; - for (const form of UPGRADE_TARGET_FORMS) { - try { - const target = buildNvstWssUpgradeRequestTarget(host, port, form, sessionId); - onLog?.( - `Trying WSS upgrade form=${form} (GET ${target} HTTP/1.1) raw-TLS Bifrost headers`, - ); - return await connectNvstWssOnce(host, port, timeoutMs, form, sessionId, false); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - lastError = err; - const message = err.message; - if (sessionId && /\bHTTP 403\b/.test(message)) { - onLog?.(`Upgrade HTTP 403 on form=${form}; retrying with x-nv-sessionid`); - try { - return await connectNvstWssOnce(host, port, timeoutMs, form, sessionId, true); - } catch (retryError) { - lastError = retryError instanceof Error ? retryError : new Error(String(retryError)); - } - } - if (!/\bHTTP (400|404)\b/.test(message)) { - throw lastError; - } - onLog?.(message); - } - } - throw lastError ?? new Error("WSS upgrade failed for all request-target forms"); + const form = "rtspPath"; + const target = buildNvstWssUpgradeRequestTarget(host, port, form, sessionId); + onLog?.(`Trying WSS upgrade form=${form} (GET ${target} HTTP/1.1) with x-nv-sessionid`); + return connectNvstWssOnce(host, port, timeoutMs, form, sessionId); } export function encodeWsTextFrame(payload: Buffer): Buffer { diff --git a/opennow-stable/src/main/platforms/gfn/signaling.ts b/opennow-stable/src/main/platforms/gfn/signaling.ts index 9dc04b80e..d86b82162 100644 --- a/opennow-stable/src/main/platforms/gfn/signaling.ts +++ b/opennow-stable/src/main/platforms/gfn/signaling.ts @@ -58,7 +58,6 @@ export class GfnSignalingClient { signInUrl.protocol = "wss:"; signInUrl.pathname = `${signInUrl.pathname.replace(/\/?$/, "/")}sign_in`; - signInUrl.search = ""; signInUrl.searchParams.set("peer_id", this.peerName); signInUrl.searchParams.set("version", "2"); signInUrl.searchParams.set("peer_role", "1"); diff --git a/opennow-stable/src/main/platforms/gfn/types.ts b/opennow-stable/src/main/platforms/gfn/types.ts index 34fbb9dc8..77d5750b7 100644 --- a/opennow-stable/src/main/platforms/gfn/types.ts +++ b/opennow-stable/src/main/platforms/gfn/types.ts @@ -3,16 +3,20 @@ import type { SessionErrorInfo } from "@shared/sessionError"; export interface CloudMatchRequest { sessionRequestData: { - appId: string; + appId: string | number; internalTitle: string | null; availableSupportedControllers: number[]; + preferredController?: number; + requestedAudioFormat?: number; + externalAppId?: string | null; + transport?: null; networkTestSessionId: string | null; parentSessionId: string | null; clientIdentification: string; deviceHashId: string; clientVersion: string; sdkVersion: string; - streamerVersion: number; + streamerVersion: number | string; clientPlatformName: string; clientRequestMonitorSettings: Array<{ monitorId?: number; @@ -23,6 +27,14 @@ export interface CloudMatchRequest { framesPerSecond: number; sdrHdrMode: number; displayData: { + displayPrimaryX0?: number; + displayPrimaryY0?: number; + displayPrimaryX1?: number; + displayPrimaryY1?: number; + displayPrimaryX2?: number; + displayPrimaryY2?: number; + displayWhitePointX?: number; + displayWhitePointY?: number; desiredContentMaxLuminance?: number; desiredContentMinLuminance?: number; desiredContentMaxFrameAverageLuminance?: number; @@ -37,7 +49,9 @@ export interface CloudMatchRequest { clientDisplayHdrCapabilities: { version: number; hdrEdrSupportedFlagsInUint32: number; - staticMetadataDescriptorId: number; + staticMetadataDescriptorId?: number; + static_metadata_descriptor_id?: number; + display_data?: Record; } | null; surroundAudioInfo: number; remoteControllersBitmap: number; @@ -45,7 +59,7 @@ export interface CloudMatchRequest { enhancedStreamMode: number; appLaunchMode: number; secureRTSPSupported: boolean; - partnerCustomData: string; + partnerCustomData: string | null; accountLinked: boolean; enablePersistingInGameSettings: boolean; userAge: number; @@ -65,6 +79,8 @@ export interface CloudMatchRequest { prefilterSharpness?: number; prefilterNoiseReduction?: number; hudStreamingMode?: number; + qosPolicy?: number; + touchSupport?: boolean; sdrColorSpace?: number; hdrColorSpace?: number; maxBitrateKbps?: number; @@ -84,6 +100,7 @@ export interface CloudMatchResponse { }; session: { sessionId: string; + subSessionId?: string; status: number; queuePosition?: number; seatSetupInfo?: { @@ -146,7 +163,9 @@ export interface CloudMatchResponse { port: number; usage: number; protocol?: number; + appLevelProtocol?: number; resourcePath?: string; + [key: string]: unknown; }>; sessionControlInfo?: { ip?: string; @@ -195,6 +214,7 @@ export interface CloudMatchResponse { /** Session in the get sessions response */ export interface SessionEntry { sessionId: string; + subSessionId?: string; status: number; queuePosition?: number; seatSetupInfo?: { @@ -221,6 +241,9 @@ export interface SessionEntry { port: number; usage: number; protocol?: number; + appLevelProtocol?: number; + resourcePath?: string; + [key: string]: unknown; }>; monitorSettings?: Array<{ widthInPixels?: number; diff --git a/opennow-stable/src/main/settings.ts b/opennow-stable/src/main/settings.ts index b23b100c8..2f24f8f38 100644 --- a/opennow-stable/src/main/settings.ts +++ b/opennow-stable/src/main/settings.ts @@ -238,8 +238,8 @@ export class SettingsManager { migrated = true; } - if (settings.nativeStreamerBackend !== "gstreamer") { - settings.nativeStreamerBackend = "gstreamer"; + if ("nativeStreamerBackend" in settings) { + delete (settings as Settings & { nativeStreamerBackend?: unknown }).nativeStreamerBackend; migrated = true; } const appAccentColor = normalizeAppAccentColor(settings.appAccentColor); diff --git a/opennow-stable/src/main/signaling/signalingCoordinator.test.ts b/opennow-stable/src/main/signaling/signalingCoordinator.test.ts new file mode 100644 index 000000000..7ac7a4e50 --- /dev/null +++ b/opennow-stable/src/main/signaling/signalingCoordinator.test.ts @@ -0,0 +1,187 @@ +/// + +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { NativeStreamerSessionContext } from "@shared/gfn"; +import type { GfnNvstRtspOwner } from "../platforms/gfn/nvstRtsp/owner"; +import type { NativeStreamerManager } from "../nativeStreamer/manager"; +import type { SettingsManager } from "../settings"; +import { SignalingCoordinator } from "./signalingCoordinator"; + +function createContext(): NativeStreamerSessionContext { + return { + session: { + sessionId: "gfn-session", + status: 2, + zone: "test-zone", + serverIp: "192.0.2.10", + signalingServer: "signal.example", + signalingUrl: "wss://signal.example/session", + rtspsEndpoints: ["rtsps://rtsp.example:322/gfn-session"], + iceServers: [], + }, + settings: { + resolution: "1920x1080", + fps: 60, + codec: "H265", + transportMode: "nvst", + } as NativeStreamerSessionContext["settings"], + shortcuts: {} as NativeStreamerSessionContext["shortcuts"], + }; +} + +interface CoordinatorInternals { + nativeStreamerContext: NativeStreamerSessionContext | null; + nativeStreamerManager: NativeStreamerManager | null; + prepareNativeStreamerBeforeSignaling(): Promise; + routeSignalingEvent(event: { type: "offer"; sdp: string } | { type: "remote-ice"; candidate: string }): void; +} + +function createCoordinator( + owner: GfnNvstRtspOwner, +): { coordinator: SignalingCoordinator; internals: CoordinatorInternals } { + const coordinator = new SignalingCoordinator({ + ipcMain: {} as never, + mainDir: "", + settingsManager: { + get: (key: string) => key === "streamClientMode" ? "native" : undefined, + } as unknown as SettingsManager, + getMainWindow: () => null, + gfnNvstRtspOwner: owner, + }); + return { + coordinator, + internals: coordinator as unknown as CoordinatorInternals, + }; +} + +test("coordinator starts native after NVST prepare without dropping the reserved socket", async () => { + const events: string[] = []; + const owner: GfnNvstRtspOwner = { + prepare: async (context) => { + events.push("nvst-prepare"); + return { + ...context, + nvstVideo: { + clientUdpPort: 45000, + videoPeerIp: "192.0.2.20", + videoPeerPort: 5004, + srtpAesKeyHex: "AA".repeat(32), + srtpKeyId: 42, + srtpSaltHex: `${"00".repeat(11)}2A`, + }, + }; + }, + videoUdpFd: () => undefined, + handoffVideoUdp: async () => { + events.push("nvst-handoff-video-udp"); + }, + release: async (reason) => { + events.push(`nvst-release:${reason}`); + }, + }; + const { internals } = createCoordinator(owner); + internals.nativeStreamerContext = createContext(); + internals.nativeStreamerManager = { + prepareForSession: async (context: NativeStreamerSessionContext) => { + assert.equal(context.nvstVideo?.clientUdpPort, 45000); + events.push("native-prepare"); + }, + } as unknown as NativeStreamerManager; + + await internals.prepareNativeStreamerBeforeSignaling(); + + assert.deepEqual(events, ["nvst-prepare", "native-prepare", "nvst-handoff-video-udp"]); + assert.equal(internals.nativeStreamerContext?.nvstVideo?.videoPeerPort, 5004); +}); + +test("coordinator releases retained NVST control on explicit native stop", async () => { + const events: string[] = []; + const owner: GfnNvstRtspOwner = { + prepare: async (context) => context, + videoUdpFd: () => undefined, + handoffVideoUdp: async () => undefined, + release: async (reason) => { + events.push(`nvst-release:${reason}`); + }, + }; + const { coordinator, internals } = createCoordinator(owner); + internals.nativeStreamerManager = { + stop: async (reason: string) => { + events.push(`native-stop:${reason}`); + }, + } as unknown as NativeStreamerManager; + + await coordinator.stopNativeStreamer("test stop"); + + assert.deepEqual(events, ["native-stop:test stop", "nvst-release:test stop"]); +}); + +test("coordinator tears down explicit NVST without falling back to WebRTC", async () => { + const events: string[] = []; + const owner: GfnNvstRtspOwner = { + prepare: async (context) => { + events.push("nvst-prepare"); + return context; + }, + videoUdpFd: () => undefined, + handoffVideoUdp: async () => { + events.push("nvst-handoff-video-udp"); + }, + release: async (reason) => { + events.push(`nvst-release:${reason}`); + }, + }; + const { internals } = createCoordinator(owner); + internals.nativeStreamerContext = createContext(); + internals.nativeStreamerManager = { + prepareForSession: async () => { + events.push("native-prepare"); + throw new Error("synthetic native start failure"); + }, + stop: async (reason: string) => { + events.push(`native-stop:${reason}`); + }, + } as unknown as NativeStreamerManager; + + await assert.rejects( + internals.prepareNativeStreamerBeforeSignaling(), + /synthetic native start failure/, + ); + + assert.deepEqual(events, [ + "nvst-prepare", + "native-prepare", + "native-stop:explicit NVST pre-attach startup failed", + "nvst-release:explicit NVST pre-attach startup failed", + ]); +}); + +test("coordinator does not renegotiate WebRTC after native NVST starts", () => { + const owner: GfnNvstRtspOwner = { + prepare: async (context) => context, + videoUdpFd: () => undefined, + handoffVideoUdp: async () => undefined, + release: async () => undefined, + }; + const { internals } = createCoordinator(owner); + const context = createContext(); + context.nvstVideo = { + clientUdpPort: 45000, + videoPeerIp: "192.0.2.20", + videoPeerPort: 5004, + srtpAesKeyHex: "AA".repeat(32), + srtpKeyId: 42, + srtpSaltHex: `${"00".repeat(11)}2A`, + }; + internals.nativeStreamerContext = context; + internals.nativeStreamerManager = { + isNvstSessionActive: (sessionId: string) => sessionId === "gfn-session", + handleOffer: async () => assert.fail("NVST must not handle a WebRTC offer"), + addRemoteIce: async () => assert.fail("NVST must not handle WebRTC ICE"), + } as unknown as NativeStreamerManager; + + internals.routeSignalingEvent({ type: "offer", sdp: "v=0\r\n" }); + internals.routeSignalingEvent({ type: "remote-ice", candidate: "candidate:synthetic" }); +}); diff --git a/opennow-stable/src/main/signaling/signalingCoordinator.ts b/opennow-stable/src/main/signaling/signalingCoordinator.ts index 9b590c8f0..7e2a0a67c 100644 --- a/opennow-stable/src/main/signaling/signalingCoordinator.ts +++ b/opennow-stable/src/main/signaling/signalingCoordinator.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, type IpcMain } from "electron"; +import electron, { type BrowserWindow, type IpcMain } from "electron"; import { IPC_CHANNELS } from "@shared/ipc"; import type { IceCandidatePayload, @@ -15,6 +15,10 @@ import type { } from "@shared/gfn"; import { streamDiagnosticId } from "@shared/gfn"; import { setLogContext } from "@shared/logger"; +import { + GfnNvstRtspSessionOwner, + type GfnNvstRtspOwner, +} from "../platforms/gfn/nvstRtsp/owner"; import { GfnSignalingClient } from "../platforms/gfn/signaling"; import { NativeStreamerManager } from "../nativeStreamer/manager"; import { normalizeNativeInputPacket } from "../nativeStreamer/input"; @@ -22,11 +26,14 @@ import { normalizeNativeRenderSurface } from "../nativeStreamer/surface"; import { getNativeCloudGsyncCapabilities } from "../nativeCloudGsync"; import type { SettingsManager } from "../settings"; +const { BrowserWindow: ElectronBrowserWindow } = electron; + export interface SignalingCoordinatorDeps { ipcMain: IpcMain; mainDir: string; settingsManager: SettingsManager; getMainWindow(): BrowserWindow | null; + gfnNvstRtspOwner?: GfnNvstRtspOwner; } export class SignalingCoordinator { @@ -37,11 +44,39 @@ export class SignalingCoordinator { private nativeStreamerFallbackSessionId: string | null = null; private nativeSoftwareRetrySessionId: string | null = null; private lastSignalingPayload: SignalingConnectRequest | null = null; + private readonly gfnNvstRtspOwner: GfnNvstRtspOwner; private sessionDiagnosticState: Record = { phase: "idle", }; - constructor(private readonly deps: SignalingCoordinatorDeps) {} + constructor(private readonly deps: SignalingCoordinatorDeps) { + this.gfnNvstRtspOwner = deps.gfnNvstRtspOwner ?? new GfnNvstRtspSessionOwner({ + reserveVideoUdp: () => this.getNativeStreamerManager().reserveNvstUdp(), + onVideoReady: async (videoSession) => { + const current = this.nativeStreamerContext; + if (!current) { + throw new Error("Native streamer context missing while arming NVST receive"); + } + this.nativeStreamerContext = { + ...current, + nvstVideo: videoSession, + }; + }, + onAnnounceReady: async (videoSession) => { + const current = this.nativeStreamerContext; + if (!current) { + throw new Error("Native streamer context missing after NVST ANNOUNCE"); + } + const armed = { + ...current, + nvstVideo: videoSession, + }; + this.nativeStreamerContext = armed; + // Official doAnnounce: ANNOUNCE → setupWebRtcTransport → wait DTLS → PLAY. + await this.getNativeStreamerManager().prepareForSession(armed); + }, + }); + } private retainSessionState(values: Record): void { this.sessionDiagnosticState = { @@ -127,7 +162,7 @@ export class SignalingCoordinator { return; } - const window = BrowserWindow.fromWebContents(event.sender); + const window = ElectronBrowserWindow.fromWebContents(event.sender); if (!window || window.isDestroyed()) { return; } @@ -200,6 +235,7 @@ export class SignalingCoordinator { this.signalingClientKey = null; this.nativeStreamerManager?.setVideoBackendOverride(null); this.nativeStreamerManager?.dispose(options.reason); + void this.gfnNvstRtspOwner.release(options.reason); this.nativeStreamerManager = null; this.nativeStreamerContext = null; this.nativeStreamerFallbackSessionId = null; @@ -207,11 +243,15 @@ export class SignalingCoordinator { this.lastSignalingPayload = null; } - stopNativeStreamer(reason: string): void { - void this.nativeStreamerManager?.stop(reason); + async stopNativeStreamer(reason: string): Promise { + await Promise.all([ + this.nativeStreamerManager?.stop(reason), + this.gfnNvstRtspOwner.release(reason), + ]); } resetNativeStreamerContext(): void { + void this.gfnNvstRtspOwner.release("native streamer context reset"); this.nativeStreamerContext = null; this.nativeStreamerFallbackSessionId = null; this.nativeSoftwareRetrySessionId = null; @@ -248,27 +288,24 @@ export class SignalingCoordinator { ): void { if ( (key === "streamClientMode" && value !== "native") || - key === "nativeStreamerBackend" || key === "nativeStreamerExecutablePath" || key === "nativeCloudGsyncMode" || key === "nativeD3dFullscreenMode" || key === "nativeExternalRenderer" || key === "transportMode" ) { - this.stopNativeStreamer( - key === "nativeStreamerBackend" - ? "native streamer backend changed" - : key === "nativeStreamerExecutablePath" - ? "native streamer executable changed" - : key === "nativeCloudGsyncMode" - ? "native Cloud G-Sync mode changed" - : key === "nativeD3dFullscreenMode" - ? "native D3D fullscreen mode changed" - : key === "nativeExternalRenderer" - ? "native external renderer setting changed" - : key === "transportMode" - ? "native transport mode changed" - : "native streamer disabled", + void this.stopNativeStreamer( + key === "nativeStreamerExecutablePath" + ? "native streamer executable changed" + : key === "nativeCloudGsyncMode" + ? "native Cloud G-Sync mode changed" + : key === "nativeD3dFullscreenMode" + ? "native D3D fullscreen mode changed" + : key === "nativeExternalRenderer" + ? "native external renderer setting changed" + : key === "transportMode" + ? "native transport mode changed" + : "native streamer disabled", ); this.resetNativeStreamerContext(); } @@ -280,7 +317,7 @@ export class SignalingCoordinator { "[NativeStreamer] Native video backend changed; active session will keep its current backend until the next native streamer restart.", ); } else { - this.stopNativeStreamer("native video backend changed"); + void this.stopNativeStreamer("native video backend changed"); } } if (key === "maxBitrateMbps") { @@ -289,6 +326,7 @@ export class SignalingCoordinator { } private async connectSignaling(payload: SignalingConnectRequest): Promise { + const previousNativeStreamerContext = this.nativeStreamerContext; if ( this.lastSignalingPayload && this.lastSignalingPayload.sessionId !== payload.sessionId @@ -342,6 +380,16 @@ export class SignalingCoordinator { } if (this.signalingClient && this.signalingClientKey === nextKey) { + if ( + previousNativeStreamerContext?.session.sessionId === payload.sessionId + && previousNativeStreamerContext.nvstVideo + && this.nativeStreamerContext?.settings.transportMode === "nvst" + ) { + this.nativeStreamerContext = { + ...this.nativeStreamerContext, + nvstVideo: previousNativeStreamerContext.nvstVideo, + }; + } console.log( "[Signaling] Reuse existing signaling connection (duplicate connect request ignored)", ); @@ -369,9 +417,12 @@ export class SignalingCoordinator { phase: "signaling-connect-failed", lastError: error instanceof Error ? error.message : String(error), }); - await this.nativeStreamerManager - ?.stop("signaling connect failed") - .catch(() => undefined); + await Promise.all([ + this.nativeStreamerManager + ?.stop("signaling connect failed") + .catch(() => undefined), + this.gfnNvstRtspOwner.release("signaling connect failed"), + ]); this.signalingClient = null; this.signalingClientKey = null; throw error; @@ -383,7 +434,10 @@ export class SignalingCoordinator { phase: "disconnecting", stopReason: "renderer signaling disconnect", }); - await this.nativeStreamerManager?.stop("signaling disconnect"); + await Promise.all([ + this.nativeStreamerManager?.stop("signaling disconnect"), + this.gfnNvstRtspOwner.release("signaling disconnect"), + ]); this.nativeStreamerManager?.setVideoBackendOverride(null); this.nativeStreamerContext = null; this.nativeStreamerFallbackSessionId = null; @@ -421,7 +475,6 @@ export class SignalingCoordinator { private getNativeStreamerManager(): NativeStreamerManager { this.nativeStreamerManager ??= new NativeStreamerManager({ mainDir: this.deps.mainDir, - getBackendPreference: () => "gstreamer", getVideoBackendPreference: () => this.deps.settingsManager?.get("nativeVideoBackend") ?? "auto", getExecutablePathOverride: () => @@ -432,7 +485,16 @@ export class SignalingCoordinator { this.deps.settingsManager?.get("nativeD3dFullscreenMode") ?? "auto", getExternalRendererEnabled: () => this.deps.settingsManager?.get("nativeExternalRenderer") ?? false, - emit: (event) => this.emitToRenderer(event), + emit: (event) => { + if (event.type === "native-stream-stopped") { + void this.gfnNvstRtspOwner.release( + event.reason + ? `native streamer stopped: ${event.reason}` + : "native streamer stopped", + ); + } + this.emitToRenderer(event); + }, sendAnswer: async (payload) => { if (!this.signalingClient) { throw new Error("Signaling is not connected"); @@ -518,6 +580,9 @@ export class SignalingCoordinator { void this.nativeStreamerManager?.stop( `signaling disconnected: ${event.reason}`, ); + void this.gfnNvstRtspOwner.release( + `signaling disconnected: ${event.reason}`, + ); this.nativeStreamerContext = null; this.nativeStreamerFallbackSessionId = null; this.emitToRenderer(event); @@ -534,6 +599,21 @@ export class SignalingCoordinator { return; } + const nativeNvstActive = this.nativeStreamerManager + ?.isNvstSessionActive(context.session.sessionId) ?? false; + + if ( + nativeNvstActive + && (event.type === "offer" || event.type === "remote-ice") + ) { + console.log( + `[NativeStreamer] Explicit NVST is active; ignoring WebRTC ${event.type} (${ + event.type === "offer" ? `sdpBytes=${event.sdp.length}` : "candidate" + })`, + ); + return; + } + if (event.type === "offer") { void this.handleNativeStreamerOffer(event.sdp, context); return; @@ -562,6 +642,25 @@ export class SignalingCoordinator { await this.getNativeStreamerManager().handleOffer(sdp, context); } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (context.settings.transportMode === "nvst") { + console.warn("[NativeStreamer] Explicit NVST startup failed:", message); + this.retainSessionState({ + streamer: "native", + phase: "native-nvst-failed", + lastError: message, + }); + await Promise.all([ + this.nativeStreamerManager + ?.stop("explicit NVST startup failed") + .catch(() => undefined), + this.gfnNvstRtspOwner.release("explicit NVST startup failed"), + ]); + this.emitToRenderer({ + type: "error", + message: `Native NVST failed: ${message}. WebRTC media fallback is disabled for explicit NVST mode.`, + }); + return; + } console.warn("[NativeStreamer] Falling back to web streamer:", message); this.retainSessionState({ streamer: "web-fallback", @@ -573,9 +672,12 @@ export class SignalingCoordinator { this.nativeStreamerManager?.drainQueuedRemoteIce( context.session.sessionId, ) ?? []; - await this.nativeStreamerManager - ?.stop("native streamer fallback") - .catch(() => undefined); + await Promise.all([ + this.nativeStreamerManager + ?.stop("native streamer fallback") + .catch(() => undefined), + this.gfnNvstRtspOwner.release("native streamer fallback"), + ]); this.emitToRenderer({ type: "error", message: `Native streamer failed: ${message}. Falling back to web streamer.`, @@ -612,9 +714,43 @@ export class SignalingCoordinator { type: "log", message: "Preparing native streamer before signaling attach.", }); - await this.getNativeStreamerManager().prepareForSession(context); + const preparedContext = await this.gfnNvstRtspOwner.prepare(context); + if ( + this.nativeStreamerContext?.session.sessionId + !== preparedContext.session.sessionId + ) { + await this.gfnNvstRtspOwner.release( + "native streamer context changed during NVST preparation", + ); + throw new Error("Native streamer context changed during NVST preparation"); + } + this.nativeStreamerContext = preparedContext; + // Official Bifrost binds the ICE/bundle socket in-process before ANNOUNCE + // and never rebinds. Native reserved that socket during prepare(); start + // must reuse it on the same process. + await this.getNativeStreamerManager().prepareForSession(preparedContext); + await this.gfnNvstRtspOwner.handoffVideoUdp(); } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (context.settings.transportMode === "nvst") { + console.warn("[NativeStreamer] Explicit NVST pre-attach startup failed:", message); + this.retainSessionState({ + streamer: "native", + phase: "native-nvst-pre-attach-failed", + lastError: message, + }); + await Promise.all([ + this.nativeStreamerManager + ?.stop("explicit NVST pre-attach startup failed") + .catch(() => undefined), + this.gfnNvstRtspOwner.release("explicit NVST pre-attach startup failed"), + ]); + this.emitToRenderer({ + type: "error", + message: `Native NVST failed before signaling attach: ${message}. WebRTC media fallback is disabled for explicit NVST mode.`, + }); + throw error; + } console.warn( "[NativeStreamer] Pre-attach startup failed; falling back to web streamer:", message, @@ -625,9 +761,12 @@ export class SignalingCoordinator { lastError: message, }); this.nativeStreamerFallbackSessionId = context.session.sessionId; - await this.nativeStreamerManager - ?.stop("native streamer pre-attach fallback") - .catch(() => undefined); + await Promise.all([ + this.nativeStreamerManager + ?.stop("native streamer pre-attach fallback") + .catch(() => undefined), + this.gfnNvstRtspOwner.release("native streamer pre-attach fallback"), + ]); this.emitToRenderer({ type: "error", message: `Native streamer failed before signaling attach: ${message}. Falling back to web streamer.`, diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index 17b88f591..7365ebf8b 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -43,7 +43,8 @@ import { useGameLaunch } from "./hooks/streamSession/useGameLaunch"; import { useSignalingEvents } from "./hooks/streamSession/useSignalingEvents"; import { RECOVERABLE_STREAM_STATUSES, - SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS, + SIGNALING_RECOVERY_WINDOW_MS, + nextSignalingRecoveryPollDelayMs, remoteSessionEndCode, sendStreamClipboardPaste, sleep, @@ -781,6 +782,7 @@ export function App(): JSX.Element { resetRecoveryConnectionState(); discordStreamingActivitySessionRef.current = null; signalingRecoveryRef.current.attemptCount = 0; + signalingRecoveryRef.current.deadlineAtMs = null; signalingRecoveryRef.current.inFlight = null; signalingRecoveryRef.current.appId = null; setSession(null); @@ -879,8 +881,7 @@ export function App(): JSX.Element { enableL4S: settings.enableL4S, enableCloudGsync: settings.enableCloudGsync, clientMode: settings.streamClientMode, - nativeStreamerBackend: "gstreamer", - transportMode: "webrtc", + transportMode: settings.transportMode, nativeCloudGsyncMode: settings.nativeCloudGsyncMode, nativeTransitionDiagnostics: settings.nativeTransitionDiagnostics, appLaunchMode: resolveAppLaunchMode({ @@ -906,6 +907,7 @@ export function App(): JSX.Element { settings.nativeTransitionDiagnostics, settings.resolution, settings.streamClientMode, + settings.transportMode, subscriptionInfo?.entitledResolutions, ]); @@ -1850,8 +1852,10 @@ export function App(): JSX.Element { throw new Error("Connection to the running session was lost and your login token is no longer available for resume."); } - if (recoveryState.attemptCount >= SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS.length) { - console.warn("[Recovery] Recovery budget exhausted"); + const now = Date.now(); + recoveryState.deadlineAtMs ??= now + SIGNALING_RECOVERY_WINDOW_MS; + if (now >= recoveryState.deadlineAtMs) { + console.warn("[Recovery] Recovery window expired"); return false; } @@ -1862,23 +1866,29 @@ export function App(): JSX.Element { await disconnectSignalingControlled(); let lastError: Error | null = null; - while (recoveryState.attemptCount < SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS.length) { - const attemptIndex = recoveryState.attemptCount; - recoveryState.attemptCount += 1; - const attemptNumber = recoveryState.attemptCount; - const attemptDelayMs = SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS[attemptIndex] ?? 0; - - console.warn( - `[Recovery] Attempt ${attemptNumber}/${SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS.length} after signaling disconnect: ${reason}`, - ); - - if (attemptDelayMs > 0) { - await sleep(attemptDelayMs); - } + while (Date.now() < (recoveryState.deadlineAtMs ?? 0)) { + const pollDelayMs = nextSignalingRecoveryPollDelayMs({ + attemptCount: recoveryState.attemptCount, + online: navigator.onLine !== false, + nowMs: Date.now(), + deadlineAtMs: recoveryState.deadlineAtMs ?? 0, + }); + if (pollDelayMs === null) break; + if (pollDelayMs > 0) await sleep(pollDelayMs); if (!isRecoveryGenerationCurrent(recoveryGeneration)) { console.log("[Recovery] Aborting attempt after explicit shutdown"); return false; } + if (navigator.onLine === false) { + console.log("[Recovery] Network is offline; waiting before polling the session again"); + continue; + } + + recoveryState.attemptCount += 1; + const attemptNumber = recoveryState.attemptCount; + console.warn( + `[Recovery] Attempt ${attemptNumber} after signaling disconnect: ${reason}`, + ); try { const activeSessions = await window.openNow.getActiveSessions(token, effectiveStreamingBaseUrl); @@ -3095,7 +3105,7 @@ export function App(): JSX.Element { statsPosition={settings.statsOverlayPosition} showNativeStats={settings.showNativeStreamerStats} nativeInputCaptureActive={nativeInputCaptureActive} - gstreamerEnabled={settings.streamClientMode === "native"} + nativeStreamingEnabled={settings.streamClientMode === "native"} nativeExternalRenderer={settings.nativeExternalRenderer} shortcuts={{ toggleStats: formatShortcutForDisplay(settings.shortcutToggleStats, isMac), diff --git a/opennow-stable/src/renderer/src/components/StreamStatsHud.tsx b/opennow-stable/src/renderer/src/components/StreamStatsHud.tsx index 3ce270731..dd0dd4f29 100644 --- a/opennow-stable/src/renderer/src/components/StreamStatsHud.tsx +++ b/opennow-stable/src/renderer/src/components/StreamStatsHud.tsx @@ -46,7 +46,7 @@ export interface StreamStatsHudProps { diagnosticsStore: StreamDiagnosticsStore; mode: "compact" | "full"; position: StatsOverlayPosition; - gstreamerEnabled: boolean; + nativeStreamingEnabled: boolean; serverRegion?: string; sessionTimeRemainingText: string | null; hintsVisible?: boolean; @@ -57,7 +57,7 @@ export function StreamStatsHud({ diagnosticsStore, mode, position, - gstreamerEnabled, + nativeStreamingEnabled, serverRegion, sessionTimeRemainingText, hintsVisible = false, @@ -252,13 +252,13 @@ export function StreamStatsHud({ }), ); lines.push( - gstreamerEnabled - ? t("stream.stats.advancedGstreamerEnabled", { + nativeStreamingEnabled + ? t("stream.stats.advancedNativeEnabled", { state: stats.nativeRendererActive ? t("stream.stats.inUseValue") : t("stream.stats.notActiveValue"), }) - : t("stream.stats.advancedGstreamerDisabled"), + : t("stream.stats.advancedNativeDisabled"), ); if (!stats.nativeRendererActive && stats.transportType !== "unknown") { lines.push(t("stream.stats.advancedIceCandidate", { transport: transportText })); @@ -321,7 +321,7 @@ export function StreamStatsHud({ ); } return lines; - }, [gstreamerEnabled, hasLagIssue, shaderActive, stats, t, transportText]); + }, [nativeStreamingEnabled, hasLagIssue, shaderActive, stats, t, transportText]); const kpiRow = (
diff --git a/opennow-stable/src/renderer/src/components/StreamView.tsx b/opennow-stable/src/renderer/src/components/StreamView.tsx index 4656df56a..4009206ef 100644 --- a/opennow-stable/src/renderer/src/components/StreamView.tsx +++ b/opennow-stable/src/renderer/src/components/StreamView.tsx @@ -52,7 +52,7 @@ interface StreamViewProps { statsPosition: StatsOverlayPosition; showNativeStats?: boolean; nativeInputCaptureActive?: boolean; - gstreamerEnabled: boolean; + nativeStreamingEnabled: boolean; nativeExternalRenderer?: boolean; shortcuts: { toggleStats: string; @@ -135,7 +135,7 @@ export function StreamView({ statsPosition, showNativeStats = false, nativeInputCaptureActive = false, - gstreamerEnabled, + nativeStreamingEnabled, nativeExternalRenderer = false, shortcuts, serverRegion, @@ -444,7 +444,7 @@ export function StreamView({ useEffect(() => { const video = localVideoRef.current; if (!video) return; - const effective = nativeRendererActive || (gstreamerEnabled && isConnecting) + const effective = nativeRendererActive || (nativeStreamingEnabled && isConnecting) ? { ...videoShader, enabled: false } : videoShader; if (!shaderPipelineRef.current) { @@ -453,12 +453,12 @@ export function StreamView({ } else { shaderPipelineRef.current.updateSettings(effective); } - }, [videoShader, gstreamerEnabled, isConnecting, nativeRendererActive]); + }, [videoShader, nativeStreamingEnabled, isConnecting, nativeRendererActive]); useEffect(() => { const video = localVideoRef.current; if (!video) return; - const effective = gstreamerEnabled || nativeRendererActive + const effective = nativeStreamingEnabled || nativeRendererActive ? { ...frameInterpolation, enabled: false } : frameInterpolation; if (!frameInterpolationPipelineRef.current) { @@ -467,7 +467,7 @@ export function StreamView({ } else { frameInterpolationPipelineRef.current.updateSettings(effective); } - }, [frameInterpolation, gstreamerEnabled, nativeRendererActive]); + }, [frameInterpolation, nativeStreamingEnabled, nativeRendererActive]); useEffect(() => () => { shaderPipelineRef.current?.dispose(); @@ -700,7 +700,7 @@ export function StreamView({ const nativeInternalHole = usesNativeInternalSurface({ nativeRendererActive, - nativeStreamingEnabled: gstreamerEnabled, + nativeStreamingEnabled, connecting: isConnecting, externalRenderer: nativeExternalRenderer === true, }); @@ -787,7 +787,7 @@ export function StreamView({ onMouseSensitivityChange={onMouseSensitivityChange} mouseAcceleration={mouseAcceleration} onMouseAccelerationChange={onMouseAccelerationChange} - gstreamerEnabled={gstreamerEnabled} + nativeStreamingEnabled={nativeStreamingEnabled} videoShader={videoShader} onVideoShaderChange={onVideoShaderChange} frameInterpolation={frameInterpolation} @@ -876,7 +876,7 @@ export function StreamView({ diagnosticsStore={diagnosticsStore} mode={statsMode === "full" ? "full" : "compact"} position={statsPosition} - gstreamerEnabled={gstreamerEnabled} + nativeStreamingEnabled={nativeStreamingEnabled} serverRegion={serverRegion} sessionTimeRemainingText={showSessionTimeRemainingInStats ? sessionTimeRemainingText : null} hintsVisible={showHints} diff --git a/opennow-stable/src/renderer/src/components/settings/sections/SettingsNativeStreamerSection.tsx b/opennow-stable/src/renderer/src/components/settings/sections/SettingsNativeStreamerSection.tsx index 5e0bf3484..9bceab634 100644 --- a/opennow-stable/src/renderer/src/components/settings/sections/SettingsNativeStreamerSection.tsx +++ b/opennow-stable/src/renderer/src/components/settings/sections/SettingsNativeStreamerSection.tsx @@ -9,11 +9,11 @@ import { } from "@shared/gfn"; import { useTranslation } from "../../../i18n"; import { - formatGstreamerRuntimeLabel, + formatNativeRuntimeLabel, formatNativeVideoCodec, formatNativeVideoBackendName, getAvailableNativeCodecLabels, - getGstreamerRuntimeBadgeClass, + getNativeRuntimeBadgeClass, nativeVideoBackendOptions, } from "../settingsFormatters"; import { MotionSpinner } from "../../MotionSpinner"; @@ -68,11 +68,11 @@ export function SettingsNativeStreamerSection({ ).length; const activeVideoBackendId = nativeStreamerStatus?.activeVideoBackend?.backend; const primaryStatusMessage = nativeStreamerStatus?.message.trim() ?? ""; - const runtimeStatusMessage = nativeStreamerStatus?.gstreamerRuntime.message.trim() ?? ""; + const runtimeStatusMessage = nativeStreamerStatus?.runtime.message.trim() ?? ""; const distinctRuntimeStatusMessage = runtimeStatusMessage && runtimeStatusMessage !== primaryStatusMessage ? runtimeStatusMessage : ""; - const runtimePath = nativeStreamerStatus?.gstreamerRuntime.path?.trim() ?? ""; + const runtimePath = nativeStreamerStatus?.runtime.path?.trim() ?? ""; const refreshNativeStreamerStatus = useCallback(async () => { if (!isNativeStreamerPlatform) { @@ -88,12 +88,12 @@ export function SettingsNativeStreamerSection({ console.warn("[Settings] Failed to detect native streamer:", error); setNativeStreamerStatus({ detected: false, - gstreamerAvailable: false, + available: false, supportsOfferAnswer: false, - gstreamerRuntime: { + runtime: { source: "unknown", - bundled: false, - message: "GStreamer runtime could not be checked.", + selfContained: false, + message: "Native runtime could not be checked.", }, message: "Native streamer status could not be checked.", }); @@ -222,6 +222,33 @@ export function SettingsNativeStreamerSection({
+ {settings.streamClientMode === "native" && ( +
+ +
+ + +
+ + {t("settings.nativeStreamer.transportModeHint")} + +
+ )} +
@@ -287,27 +314,13 @@ export function SettingsNativeStreamerSection({ {runtimePath ? ( - {nativeStreamerStatus?.gstreamerRuntime.bundled - ? t("settings.nativeStreamer.bundledPathDetected") - : t("settings.nativeStreamer.gstreamerRuntime")} + {nativeStreamerStatus?.runtime.selfContained + ? t("settings.nativeStreamer.nativeExecutablePath") + : t("settings.nativeStreamer.nativeRuntime")} {runtimePath} ) : null} - {!nativeStreamerStatus?.gstreamerAvailable && nativeStreamerStatus?.gstreamerRuntime.installInstructions?.length ? ( -
- - {t("settings.nativeStreamer.linuxRuntimeHint")} - - {nativeStreamerStatus.gstreamerRuntime.installInstructions.map((instruction) => ( -
- {instruction.distro} - {instruction.command} - {instruction.note ? {instruction.note} : null} -
- ))} -
- ) : null}
@@ -395,7 +408,7 @@ export function SettingsNativeStreamerSection({ )} - {(!nativeStreamerStatus?.gstreamerAvailable || hostVideoBackends.length === 0) && ( + {(!nativeStreamerStatus?.available || hostVideoBackends.length === 0) && ( {nativeStreamerStatus?.activeVideoBackend?.reason ?? t("settings.nativeStreamer.capabilityProbeHint")} diff --git a/opennow-stable/src/renderer/src/components/settings/settingsFormatters.ts b/opennow-stable/src/renderer/src/components/settings/settingsFormatters.ts index 40c209667..4711cce4d 100644 --- a/opennow-stable/src/renderer/src/components/settings/settingsFormatters.ts +++ b/opennow-stable/src/renderer/src/components/settings/settingsFormatters.ts @@ -118,12 +118,10 @@ export function getAvailableNativeCodecLabels(backend: NativeVideoBackendCapabil .map((codec) => formatNativeVideoCodec(codec.codec)) ?? []; } -export function formatGstreamerRuntimeLabel(status: NativeStreamerStatus | null): string { - switch (status?.gstreamerRuntime.source) { - case "bundled": - return status.gstreamerAvailable ? "Bundled Runtime Used" : "Bundled Runtime Found"; - case "system": - return "System Runtime"; +export function formatNativeRuntimeLabel(status: NativeStreamerStatus | null): string { + switch (status?.runtime.source) { + case "self-contained": + return status.available ? "Native Runtime Ready" : "Native Runtime Found"; case "missing": return "Runtime Missing"; default: @@ -131,9 +129,8 @@ export function formatGstreamerRuntimeLabel(status: NativeStreamerStatus | null) } } -export function getGstreamerRuntimeBadgeClass(status: NativeStreamerStatus | null): string { - if (status?.gstreamerRuntime.source === "bundled" && status.gstreamerAvailable) return "settings-inline-badge--codec-gpu"; - if (status?.gstreamerRuntime.source === "system" && status.gstreamerAvailable) return "settings-inline-badge--codec-testing"; +export function getNativeRuntimeBadgeClass(status: NativeStreamerStatus | null): string { + if (status?.runtime.source === "self-contained" && status.available) return "settings-inline-badge--codec-gpu"; return "settings-inline-badge--updater-error"; } diff --git a/opennow-stable/src/renderer/src/components/settings/settingsTypes.ts b/opennow-stable/src/renderer/src/components/settings/settingsTypes.ts index 9c05dc9df..d9c2c73b2 100644 --- a/opennow-stable/src/renderer/src/components/settings/settingsTypes.ts +++ b/opennow-stable/src/renderer/src/components/settings/settingsTypes.ts @@ -154,7 +154,7 @@ export const SETTINGS_SCOPE_SEARCH_TERMS: Record void; mouseAcceleration: number; onMouseAccelerationChange: (value: number) => void; - gstreamerEnabled: boolean; + nativeStreamingEnabled: boolean; videoShader: VideoShaderSettings; onVideoShaderChange: (value: VideoShaderSettings) => void; frameInterpolation: FrameInterpolationSettings; @@ -104,7 +104,7 @@ export function StreamQuickMenu({ onMouseSensitivityChange, mouseAcceleration, onMouseAccelerationChange, - gstreamerEnabled, + nativeStreamingEnabled, videoShader, onVideoShaderChange, frameInterpolation, @@ -258,7 +258,7 @@ export function StreamQuickMenu({ onMouseSensitivityChange={onMouseSensitivityChange} mouseAcceleration={mouseAcceleration} onMouseAccelerationChange={onMouseAccelerationChange} - gstreamerEnabled={gstreamerEnabled} + nativeStreamingEnabled={nativeStreamingEnabled} videoShader={videoShader} onVideoShaderChange={onVideoShaderChange} frameInterpolation={frameInterpolation} diff --git a/opennow-stable/src/renderer/src/components/stream/quick-menu/StreamQuickMenuControlsPage.tsx b/opennow-stable/src/renderer/src/components/stream/quick-menu/StreamQuickMenuControlsPage.tsx index 925c2fea7..73d325c0c 100644 --- a/opennow-stable/src/renderer/src/components/stream/quick-menu/StreamQuickMenuControlsPage.tsx +++ b/opennow-stable/src/renderer/src/components/stream/quick-menu/StreamQuickMenuControlsPage.tsx @@ -32,7 +32,7 @@ interface StreamQuickMenuControlsPageProps { onMouseSensitivityChange: (value: number) => void; mouseAcceleration: number; onMouseAccelerationChange: (value: number) => void; - gstreamerEnabled: boolean; + nativeStreamingEnabled: boolean; videoShader: VideoShaderSettings; onVideoShaderChange: (value: VideoShaderSettings) => void; frameInterpolation: FrameInterpolationSettings; @@ -49,7 +49,7 @@ export function StreamQuickMenuControlsPage({ onMouseSensitivityChange, mouseAcceleration, onMouseAccelerationChange, - gstreamerEnabled, + nativeStreamingEnabled, videoShader, onVideoShaderChange, frameInterpolation, @@ -120,7 +120,7 @@ export function StreamQuickMenuControlsPage({ Frame Interpolation Experimental Framegen WebGPU processing. - {gstreamerEnabled ? ( + {nativeStreamingEnabled ? ( Frame interpolation is unavailable while the native streamer renders the video. @@ -196,7 +196,7 @@ export function StreamQuickMenuControlsPage({ Video Filters GPU shaders applied to the stream. - {gstreamerEnabled ? ( + {nativeStreamingEnabled ? ( Video filters are unavailable while the native streamer renders the video. ) : ( <> diff --git a/opennow-stable/src/renderer/src/hooks/streamSession/useGameLaunch.ts b/opennow-stable/src/renderer/src/hooks/streamSession/useGameLaunch.ts index c6b9bdc07..0f17484d9 100644 --- a/opennow-stable/src/renderer/src/hooks/streamSession/useGameLaunch.ts +++ b/opennow-stable/src/renderer/src/hooks/streamSession/useGameLaunch.ts @@ -247,9 +247,14 @@ export function useGameLaunch({ const otherSession = activeSessions.find((s) => s.status === 2 || s.status === 3) ?? null; if (matchingSession) { - await claimAndConnectSession(matchingSession); - setNavbarActiveSession(null); - return; + if (streamSettings.transportMode === "nvst") { + // Leftover NVST seats can carry the legacy "PING" streamer pool; always fresh-create. + existingSessionStrategy = "force-new"; + } else { + await claimAndConnectSession(matchingSession); + setNavbarActiveSession(null); + return; + } } if (otherSession) { diff --git a/opennow-stable/src/renderer/src/hooks/streamSession/useSessionRecoveryRuntime.ts b/opennow-stable/src/renderer/src/hooks/streamSession/useSessionRecoveryRuntime.ts index 76e80c594..b2fe2096c 100644 --- a/opennow-stable/src/renderer/src/hooks/streamSession/useSessionRecoveryRuntime.ts +++ b/opennow-stable/src/renderer/src/hooks/streamSession/useSessionRecoveryRuntime.ts @@ -72,6 +72,7 @@ export function useSessionRecoveryRuntime(runtime: RecoveryRuntime) { resetRecoveryConnectionState(); signalingRecoveryRef.current.generation += 1; signalingRecoveryRef.current.attemptCount = 0; + signalingRecoveryRef.current.deadlineAtMs = null; signalingRecoveryRef.current.inFlight = null; signalingRecoveryRef.current.appId = null; if (!options?.keepExplicitShutdown) { @@ -83,6 +84,7 @@ export function useSessionRecoveryRuntime(runtime: RecoveryRuntime) { resetRecoveryConnectionState(); signalingRecoveryRef.current.generation += 1; signalingRecoveryRef.current.explicitShutdown = true; + signalingRecoveryRef.current.deadlineAtMs = null; signalingRecoveryRef.current.inFlight = null; }, [resetRecoveryConnectionState, signalingRecoveryRef]); diff --git a/opennow-stable/src/renderer/src/hooks/streamSession/useStreamRuntimeState.ts b/opennow-stable/src/renderer/src/hooks/streamSession/useStreamRuntimeState.ts index 4e2874aec..92d4f14ac 100644 --- a/opennow-stable/src/renderer/src/hooks/streamSession/useStreamRuntimeState.ts +++ b/opennow-stable/src/renderer/src/hooks/streamSession/useStreamRuntimeState.ts @@ -78,6 +78,7 @@ export function useStreamRuntimeState() { const pendingControlledDisconnectsRef = useRef(0); const signalingRecoveryRef = useRef({ attemptCount: 0, + deadlineAtMs: null, inFlight: null, explicitShutdown: false, appId: null, diff --git a/opennow-stable/src/renderer/src/hooks/useStreamSession.ts b/opennow-stable/src/renderer/src/hooks/useStreamSession.ts index 4acefd423..75e5e61b4 100644 --- a/opennow-stable/src/renderer/src/hooks/useStreamSession.ts +++ b/opennow-stable/src/renderer/src/hooks/useStreamSession.ts @@ -12,10 +12,12 @@ export function useStreamSession() { export { ICE_DISCONNECTED_RECOVERY_GRACE_MS, RECOVERABLE_STREAM_STATUSES, - SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS, + SIGNALING_RECOVERY_POLL_INTERVAL_MS, SIGNALING_RECOVERY_STABLE_RESET_DELAY_MS, + SIGNALING_RECOVERY_WINDOW_MS, SIGNALING_REMOTE_ICE_GRACE_MS, isRemoteSessionEndReason, + nextSignalingRecoveryPollDelayMs, remoteSessionEndCode, readStreamClipboardText, sendStreamClipboardPaste, diff --git a/opennow-stable/src/renderer/src/lib/streamDiagnostics.ts b/opennow-stable/src/renderer/src/lib/streamDiagnostics.ts index a55c8c6ba..3ef6d2b15 100644 --- a/opennow-stable/src/renderer/src/lib/streamDiagnostics.ts +++ b/opennow-stable/src/renderer/src/lib/streamDiagnostics.ts @@ -84,7 +84,7 @@ export function mergeNativeStreamStats( const totalSinkFrames = sinkRendered + sinkDropped; const dropPercent = totalSinkFrames > 0 ? (sinkDropped / totalSinkFrames) * 100 : 0; const hardwareAcceleration = [ - stats.hardwareAcceleration || "GStreamer native decode", + stats.hardwareAcceleration || "Native decode", stats.zeroCopy && stats.memoryMode ? `${stats.memoryMode} zero-copy` : "", !stats.zeroCopy && stats.memoryMode ? stats.memoryMode : "", !stats.memoryMode && stats.zeroCopyD3D12 ? "D3D12 zero-copy" : "", diff --git a/opennow-stable/src/renderer/src/lib/streamSessionHelpers.test.ts b/opennow-stable/src/renderer/src/lib/streamSessionHelpers.test.ts index 11be1c812..2a07809f4 100644 --- a/opennow-stable/src/renderer/src/lib/streamSessionHelpers.test.ts +++ b/opennow-stable/src/renderer/src/lib/streamSessionHelpers.test.ts @@ -4,7 +4,10 @@ import test from "node:test"; import assert from "node:assert/strict"; import type { SessionInfo } from "@shared/gfn"; -import { disposeSessionCreatedAfterAbort } from "./streamSessionHelpers"; +import { + disposeSessionCreatedAfterAbort, + nextSignalingRecoveryPollDelayMs, +} from "./streamSessionHelpers"; const session = { sessionId: "late-session" } as SessionInfo; @@ -29,3 +32,30 @@ test("an active launch keeps its newly created session", async () => { assert.equal(disposed, false); assert.equal(stopCalls, 0); }); + +test("signaling recovery polls immediately online and waits without burning offline attempts", () => { + assert.equal(nextSignalingRecoveryPollDelayMs({ + attemptCount: 0, + online: true, + nowMs: 1_000, + deadlineAtMs: 301_000, + }), 0); + assert.equal(nextSignalingRecoveryPollDelayMs({ + attemptCount: 0, + online: false, + nowMs: 1_000, + deadlineAtMs: 301_000, + }), 5_000); + assert.equal(nextSignalingRecoveryPollDelayMs({ + attemptCount: 1, + online: true, + nowMs: 299_000, + deadlineAtMs: 301_000, + }), 2_000); + assert.equal(nextSignalingRecoveryPollDelayMs({ + attemptCount: 1, + online: true, + nowMs: 301_000, + deadlineAtMs: 301_000, + }), null); +}); diff --git a/opennow-stable/src/renderer/src/lib/streamSessionHelpers.ts b/opennow-stable/src/renderer/src/lib/streamSessionHelpers.ts index 7d1987347..431c7b65b 100644 --- a/opennow-stable/src/renderer/src/lib/streamSessionHelpers.ts +++ b/opennow-stable/src/renderer/src/lib/streamSessionHelpers.ts @@ -4,6 +4,7 @@ import type { GfnWebRtcClient } from "../platforms/gfn/webrtcClient"; export type SignalingRecoveryState = { attemptCount: number; + deadlineAtMs: number | null; inFlight: Promise | null; explicitShutdown: boolean; appId: number | null; @@ -11,11 +12,24 @@ export type SignalingRecoveryState = { }; export const RECOVERABLE_STREAM_STATUSES: readonly StreamStatus[] = ["streaming"]; -export const SIGNALING_RECOVERY_ATTEMPT_DELAYS_MS = [0, 3000] as const; +export const SIGNALING_RECOVERY_WINDOW_MS = 300_000; +export const SIGNALING_RECOVERY_POLL_INTERVAL_MS = 5_000; export const SIGNALING_RECOVERY_STABLE_RESET_DELAY_MS = 15000; export const SIGNALING_REMOTE_ICE_GRACE_MS = 5000; export const ICE_DISCONNECTED_RECOVERY_GRACE_MS = 7000; +export function nextSignalingRecoveryPollDelayMs(input: { + attemptCount: number; + online: boolean; + nowMs: number; + deadlineAtMs: number; +}): number | null { + const remainingMs = input.deadlineAtMs - input.nowMs; + if (remainingMs <= 0) return null; + if (input.attemptCount === 0 && input.online) return 0; + return Math.min(SIGNALING_RECOVERY_POLL_INTERVAL_MS, remainingMs); +} + export function isRemoteSessionEndReason(reason: string): boolean { const normalized = reason.trim().toLowerCase(); return normalized === "bye" || diff --git a/opennow-stable/src/renderer/src/platforms/gfn/webrtcClient.ts b/opennow-stable/src/renderer/src/platforms/gfn/webrtcClient.ts index 9f3470abf..bf156e00e 100644 --- a/opennow-stable/src/renderer/src/platforms/gfn/webrtcClient.ts +++ b/opennow-stable/src/renderer/src/platforms/gfn/webrtcClient.ts @@ -154,12 +154,12 @@ function describeColorQuality(colorQuality: ColorQuality): string { function describeNativeHardwareAcceleration(): string { const platform = navigator.platform.toLowerCase(); if (platform.includes("win")) { - return "GStreamer D3D11/DXVA"; + return "Native D3D11/DXVA"; } if (platform.includes("mac")) { - return "GStreamer VideoToolbox"; + return "Native VideoToolbox"; } - return "GStreamer NVDEC/VAAPI/V4L2/Vulkan"; + return "Native VA-API/V4L2/Vulkan"; } interface ClientOptions { @@ -299,7 +299,7 @@ export class GfnWebRtcClient { /** * When true, Electron captures keyboard/mouse/gamepad and forwards packets to * the native streamer over IPC (internal child-surface renderer). - * When false, the floating external GStreamer window owns OS-level input. + * When false, the external native presenter owns OS-level input. */ private nativeElectronInputBridge = false; private remoteIceEndpoint: SessionInfo["mediaConnectionInfo"] | null = null; diff --git a/opennow-stable/src/shared/gfn.test.ts b/opennow-stable/src/shared/gfn.test.ts index 8d349bae2..6b6caa36d 100644 --- a/opennow-stable/src/shared/gfn.test.ts +++ b/opennow-stable/src/shared/gfn.test.ts @@ -152,7 +152,6 @@ test("buildNativeStreamerSessionContext forwards requested/finalized streaming f enableL4S: true, enableCloudGsync: true, clientMode: "native", - nativeStreamerBackend: "gstreamer", nativeCloudGsyncMode: "auto", nativeTransitionDiagnostics: { forceQueueMode: "adaptive", @@ -249,14 +248,21 @@ test("isNativeExternalRendererSupported is Windows-only", () => { const status = createUnsupportedNativeStreamerStatus(); assert.equal(status.detected, false); - assert.equal(status.gstreamerAvailable, false); + assert.equal(status.available, false); assert.equal(status.supportsOfferAnswer, false); assert.equal(status.message, NATIVE_STREAMER_UNSUPPORTED_PLATFORM_MESSAGE); - assert.equal(status.gstreamerRuntime.message, NATIVE_STREAMER_UNSUPPORTED_PLATFORM_MESSAGE); + assert.equal(status.runtime.message, NATIVE_STREAMER_UNSUPPORTED_PLATFORM_MESSAGE); }); -test("NVST transport stays disabled and normalizes to WebRTC", () => { - assert.equal(isNvstTransportSupported("win32"), false); - assert.equal(normalizeTransportModeForPlatform("nvst", "win32", "native"), "webrtc"); +test("NVST transport is limited to native sessions on supported desktop platforms", () => { + assert.equal(isNvstTransportSupported("win32"), true); + assert.equal(isNvstTransportSupported("darwin"), true); + assert.equal(isNvstTransportSupported("linux"), true); + assert.equal(isNvstTransportSupported("android"), false); + assert.equal(normalizeTransportModeForPlatform("nvst", "win32", "native"), "nvst"); + assert.equal(normalizeTransportModeForPlatform("nvst", "darwin", "native"), "nvst"); + assert.equal(normalizeTransportModeForPlatform("nvst", "linux", "native"), "nvst"); + assert.equal(normalizeTransportModeForPlatform("nvst", "linux", "web"), "webrtc"); + assert.equal(normalizeTransportModeForPlatform("nvst", "android", "native"), "webrtc"); assert.equal(normalizeTransportModeForPlatform("webrtc", "win32", "native"), "webrtc"); }); diff --git a/opennow-stable/src/shared/gfn/nativeStreamer.ts b/opennow-stable/src/shared/gfn/nativeStreamer.ts index 4bdc0edb1..f41c8a181 100644 --- a/opennow-stable/src/shared/gfn/nativeStreamer.ts +++ b/opennow-stable/src/shared/gfn/nativeStreamer.ts @@ -1,7 +1,6 @@ import type { StreamClientMode } from "./stream"; -export type NativeStreamerBackend = "stub" | "gstreamer"; -export type NativeStreamerBackendPreference = "auto" | NativeStreamerBackend; +export type NativeStreamerBackend = "native"; export type NativeStreamerFeatureMode = "auto" | "disabled" | "forced"; export type NativeVideoBackendPreference = | "auto" @@ -34,9 +33,8 @@ export function isNativeExternalRendererSupported(platform: string): boolean { } export const isNativeDirectXBackendSupported = isNativeExternalRendererSupported; -/** NVST is intentionally disabled until the transport is complete. */ -export function isNvstTransportSupported(_platform: string): boolean { - return false; +export function isNvstTransportSupported(platform: string): boolean { + return isNativeStreamerSupportedPlatform(platform); } export function normalizeStreamClientModeForPlatform(mode: StreamClientMode, platform: string): StreamClientMode { @@ -48,11 +46,15 @@ export function normalizeNativeExternalRendererForPlatform(enabled: boolean, pla } export function normalizeTransportModeForPlatform( - _mode: StreamTransportMode, - _platform: string, - _streamClientMode: StreamClientMode = "native", + mode: StreamTransportMode, + platform: string, + streamClientMode: StreamClientMode = "native", ): StreamTransportMode { - return "webrtc"; + return mode === "nvst" + && streamClientMode === "native" + && isNvstTransportSupported(platform) + ? "nvst" + : "webrtc"; } export function nativeStreamerFeatureModeToEnvValue(mode: NativeStreamerFeatureMode): "auto" | "0" | "1" { @@ -66,25 +68,18 @@ export function nativeStreamerFeatureModeToEnvValue(mode: NativeStreamerFeatureM } } -export type NativeGstreamerRuntimeSource = "bundled" | "system" | "missing" | "unknown"; +export type NativeStreamerRuntimeSource = "self-contained" | "missing" | "unknown"; -export interface NativeGstreamerInstallInstruction { - distro: string; - command: string; - note?: string; -} - -export interface NativeGstreamerRuntimeStatus { - source: NativeGstreamerRuntimeSource; - bundled: boolean; +export interface NativeStreamerRuntimeStatus { + source: NativeStreamerRuntimeSource; + selfContained: boolean; path?: string; message: string; - installInstructions?: NativeGstreamerInstallInstruction[]; } export interface NativeStreamerStatus { detected: boolean; - gstreamerAvailable: boolean; + available: boolean; supportsOfferAnswer: boolean; backend?: NativeStreamerBackend; fallbackReason?: string; @@ -92,18 +87,18 @@ export interface NativeStreamerStatus { activeVideoBackend?: NativeVideoBackendCapability; codecSummary?: string; zeroCopySummary?: string; - gstreamerRuntime: NativeGstreamerRuntimeStatus; + runtime: NativeStreamerRuntimeStatus; message: string; } export function createUnsupportedNativeStreamerStatus(): NativeStreamerStatus { return { detected: false, - gstreamerAvailable: false, + available: false, supportsOfferAnswer: false, - gstreamerRuntime: { + runtime: { source: "unknown", - bundled: false, + selfContained: false, message: NATIVE_STREAMER_UNSUPPORTED_PLATFORM_MESSAGE, }, message: NATIVE_STREAMER_UNSUPPORTED_PLATFORM_MESSAGE, diff --git a/opennow-stable/src/shared/gfn/session.ts b/opennow-stable/src/shared/gfn/session.ts index e422f1dca..091b96fb6 100644 --- a/opennow-stable/src/shared/gfn/session.ts +++ b/opennow-stable/src/shared/gfn/session.ts @@ -8,7 +8,6 @@ import type { VideoCodec, } from "./stream"; import type { - NativeStreamerBackendPreference, NativeStreamerFeatureMode, StreamTransportMode, } from "./nativeStreamer"; @@ -34,7 +33,6 @@ export interface StreamSettings { /** Renderer-selected client path; main uses this to apply native-only Cloud G-Sync gating. */ clientMode?: StreamClientMode; /** Selected native streamer backend; stub cannot support Cloud G-Sync presentation. */ - nativeStreamerBackend?: NativeStreamerBackendPreference; /** Native media transport; legacy NVST values normalize to WebRTC. */ transportMode?: StreamTransportMode; /** Native-only override for Cloud G-Sync display detection. */ @@ -219,6 +217,7 @@ export function getSessionAdDurationMs(ad: SessionAdInfo | undefined): number | export interface SessionInfo { sessionId: string; + subSessionId?: string; appId?: string; status: number; queuePosition?: number; @@ -236,7 +235,9 @@ export interface SessionInfo { appLaunchMode?: number; /** Wire in-game settings persistence value the session was created with, kept session-stable for resumes */ enablePersistingInGameSettings?: boolean; - /** Classic NVST RTSPS endpoints from CloudMatch usage=14 connections. */ + /** Complete ordered CloudMatch transport list consumed by the native stream SDK. */ + connectionInfo?: SessionConnectionInfo[]; + /** Classic NVST RTSPS endpoints from CloudMatch usage=16 connections. */ rtspsEndpoints?: string[]; iceServers: IceServer[]; mediaConnectionInfo?: MediaConnectionInfo; @@ -247,9 +248,20 @@ export interface SessionInfo { deviceId?: string; } +export interface SessionConnectionInfo { + ip?: string; + port: number; + usage: number; + protocol?: number; + appLevelProtocol?: number; + resourcePath?: string; + [key: string]: unknown; +} + /** Information about an active session from getActiveSessions */ export interface ActiveSessionInfo { sessionId: string; + subSessionId?: string; appId: number; /** Wire appLaunchMode the session was created with, as echoed by the server */ appLaunchMode?: number; diff --git a/opennow-stable/src/shared/gfn/sessionProxy.test.ts b/opennow-stable/src/shared/gfn/sessionProxy.test.ts new file mode 100644 index 000000000..6ea84ec8e --- /dev/null +++ b/opennow-stable/src/shared/gfn/sessionProxy.test.ts @@ -0,0 +1,70 @@ +/// + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + INVALID_SESSION_PROXY_URL_MESSAGE, + isInvalidSessionProxyUrlError, + isValidSessionProxyUrl, + normalizeSessionProxyUrl, +} from "./sessionProxy"; + +test("normalizes scheme-less host:port values as http proxies", () => { + assert.equal(normalizeSessionProxyUrl("localhost:8080"), "http://localhost:8080"); + assert.equal(normalizeSessionProxyUrl("proxy.example.com:8080"), "http://proxy.example.com:8080"); + assert.equal(normalizeSessionProxyUrl("127.0.0.1:8080"), "http://127.0.0.1:8080"); +}); + +test("accepts http/https URLs with default ports stripped by WHATWG URL", () => { + assert.equal(normalizeSessionProxyUrl("http://proxy.example.com:80"), "http://proxy.example.com:80"); + assert.equal(normalizeSessionProxyUrl("https://proxy.example.com:443"), "https://proxy.example.com:443"); + assert.equal(normalizeSessionProxyUrl("http://proxy.example.com"), "http://proxy.example.com:80"); + assert.equal(normalizeSessionProxyUrl("https://proxy.example.com"), "https://proxy.example.com:443"); +}); + +test("accepts supported explicit session proxy schemes with ports", () => { + assert.equal(normalizeSessionProxyUrl("socks5://proxy.example.com:1080"), "socks5://proxy.example.com:1080"); + assert.equal(normalizeSessionProxyUrl("socks4://proxy.example.com:1080"), "socks4://proxy.example.com:1080"); +}); + +test("rejects socks proxies without an explicit port", () => { + assert.throws( + () => normalizeSessionProxyUrl("socks5://proxy.example.com"), + /Invalid session proxy URL/, + ); + assert.throws( + () => normalizeSessionProxyUrl("socks4://proxy.example.com"), + /Invalid session proxy URL/, + ); +}); + +test("rejects unsupported schemes and malformed credentials", () => { + assert.throws( + () => normalizeSessionProxyUrl("ftp://proxy.example.com:21"), + /Invalid session proxy URL/, + ); + assert.throws( + () => normalizeSessionProxyUrl("http://user%ZZ@proxy.example.com:8080"), + /Invalid session proxy URL/, + ); +}); + +test("isValidSessionProxyUrl mirrors normalize semantics", () => { + assert.equal(isValidSessionProxyUrl(""), true); + assert.equal(isValidSessionProxyUrl(" "), true); + assert.equal(isValidSessionProxyUrl("http://proxy.example.com:80"), true); + assert.equal(isValidSessionProxyUrl("socks5://proxy.example.com"), false); + assert.equal(isValidSessionProxyUrl("ftp://proxy.example.com:21"), false); +}); + +test("isInvalidSessionProxyUrlError detects wrapped IPC messages", () => { + assert.equal(isInvalidSessionProxyUrlError(new Error(INVALID_SESSION_PROXY_URL_MESSAGE)), true); + assert.equal( + isInvalidSessionProxyUrlError( + new Error(`Error invoking remote method 'games:browse-catalog': Error: ${INVALID_SESSION_PROXY_URL_MESSAGE}`), + ), + true, + ); + assert.equal(isInvalidSessionProxyUrlError(new Error("network down")), false); +}); diff --git a/opennow-stable/src/shared/gfn/sessionProxy.ts b/opennow-stable/src/shared/gfn/sessionProxy.ts new file mode 100644 index 000000000..2a6062787 --- /dev/null +++ b/opennow-stable/src/shared/gfn/sessionProxy.ts @@ -0,0 +1,89 @@ +/** + * Session-proxy URL normalization shared by main and renderer. + * Keep protocol/port rules here so settings UI and fetch paths cannot drift. + */ + +export const INVALID_SESSION_PROXY_URL_MESSAGE = + "Invalid session proxy URL. Use http://host:port, https://host:port, socks4://host:port, or socks5://host:port."; + +const SUPPORTED_PROXY_PROTOCOLS = new Set(["http:", "https:", "socks4:", "socks5:"]); + +function safeDecodeURIComponent(value: string): string { + try { + return decodeURIComponent(value); + } catch { + throw new Error(INVALID_SESSION_PROXY_URL_MESSAGE); + } +} + +function resolveExplicitProxyPort(protocol: string, parsedPort: string): string | null { + if (parsedPort) { + return parsedPort; + } + + // WHATWG URL strips default ports for http/https (`:80` / `:443` → ""), + // so accept those schemes with an implicit default port. + if (protocol === "http:") { + return "80"; + } + if (protocol === "https:") { + return "443"; + } + + // socks4/socks5 are non-special schemes and never strip ports; require an explicit one. + return null; +} + +/** + * Normalize a session proxy URL to `scheme://[user[:pass]@]host:port`. + * Returns null for empty/whitespace input. Throws for invalid values. + */ +export function normalizeSessionProxyUrl(raw?: string): string | null { + const trimmed = raw?.trim() ?? ""; + if (!trimmed) return null; + + const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`; + let parsed: URL; + try { + parsed = new URL(candidate); + } catch { + throw new Error(INVALID_SESSION_PROXY_URL_MESSAGE); + } + + if (!SUPPORTED_PROXY_PROTOCOLS.has(parsed.protocol) || !parsed.hostname) { + throw new Error(INVALID_SESSION_PROXY_URL_MESSAGE); + } + + const port = resolveExplicitProxyPort(parsed.protocol, parsed.port); + if (!port) { + throw new Error(INVALID_SESSION_PROXY_URL_MESSAGE); + } + + const username = parsed.username ? encodeURIComponent(safeDecodeURIComponent(parsed.username)) : ""; + const password = parsed.password ? encodeURIComponent(safeDecodeURIComponent(parsed.password)) : ""; + const credentials = username ? `${username}${password ? `:${password}` : ""}@` : ""; + return `${parsed.protocol}//${credentials}${parsed.hostname}:${port}`; +} + +/** True when the value is empty or a normalizeable session proxy URL. */ +export function isValidSessionProxyUrl(raw?: string): boolean { + try { + normalizeSessionProxyUrl(raw); + return true; + } catch { + return false; + } +} + +/** True when `error` (or its message) is the invalid session-proxy validation failure. */ +export function isInvalidSessionProxyUrlError(error: unknown): boolean { + if (error instanceof Error) { + return error.message.includes(INVALID_SESSION_PROXY_URL_MESSAGE) + || error.message.includes("Invalid session proxy URL"); + } + if (typeof error === "string") { + return error.includes(INVALID_SESSION_PROXY_URL_MESSAGE) + || error.includes("Invalid session proxy URL"); + } + return false; +} diff --git a/opennow-stable/src/shared/gfn/settings.ts b/opennow-stable/src/shared/gfn/settings.ts index a44c78079..5e1139c18 100644 --- a/opennow-stable/src/shared/gfn/settings.ts +++ b/opennow-stable/src/shared/gfn/settings.ts @@ -7,7 +7,6 @@ import type { VideoAccelerationPreference, } from "./stream"; import type { - NativeStreamerBackendPreference, NativeStreamerFeatureMode, NativeVideoBackendPreference, StreamTransportMode, @@ -71,7 +70,6 @@ export interface Settings { recordingResolution: RecordingResolution; recordingFps: RecordingFps; streamClientMode: StreamClientMode; - nativeStreamerBackend: NativeStreamerBackendPreference; nativeVideoBackend: NativeVideoBackendPreference; nativeStreamerExecutablePath: string; nativeCloudGsyncMode: NativeStreamerFeatureMode; @@ -284,7 +282,6 @@ export function createDefaultSettings(platform: string): Settings { recordingResolution: DEFAULT_RECORDING_RESOLUTION, recordingFps: DEFAULT_RECORDING_FPS, streamClientMode: "web", - nativeStreamerBackend: "gstreamer", nativeVideoBackend: "auto", nativeStreamerExecutablePath: "", nativeCloudGsyncMode: "auto", diff --git a/opennow-stable/src/shared/gfn/signaling.ts b/opennow-stable/src/shared/gfn/signaling.ts index 5cf1a4b21..4482b014a 100644 --- a/opennow-stable/src/shared/gfn/signaling.ts +++ b/opennow-stable/src/shared/gfn/signaling.ts @@ -53,14 +53,43 @@ export interface NativeStreamerSessionContext { nvstVideo?: NvstVideoSession; } +export type NvstSrtpProfile = + | "AEAD_AES_128_GCM" + | "AEAD_AES_256_GCM" + | "AEAD_AES_128_GCM_8" + | "AEAD_AES_256_GCM_8" + | "AES_CM_128_HMAC_SHA1_32" + | "AES_CM_128_HMAC_SHA1_80" + | "AES_CM_256_HMAC_SHA1_32" + | "AES_CM_256_HMAC_SHA1_80"; + export interface NvstVideoSession { clientUdpPort: number; + /** + * Dedicated NATT-only video (Mjolnir) socket port reserved by the native + * streamer. When present, the native streamer reads raw-SRTP video from this + * socket while the ICE/DTLS bundle socket carries control/audio. + */ + mjolnirUdpPort?: number; videoPeerIp: string; videoPeerPort: number; srtpAesKeyHex: string; srtpKeyId: number; + srtpSaltHex: string; + srtpProfile?: NvstSrtpProfile; pingPayload?: string; + pingVersion?: number; + localIceUsernameFragment?: string; + localIcePassword?: string; + remoteIceUsernameFragment?: string; + remoteIcePassword?: string; + /** SHA-256 colon hex from the local WebRtcTransport-equivalent cert. */ + localDtlsFingerprint?: string; + /** SHA-256 colon hex advertised by DESCRIBE (`dtlsFingerprint` / `V2`). */ + remoteDtlsFingerprint?: string; codec?: string; + /** Idle receive timeout. Handshake needs longer than the 5s media default. */ + timeoutMs?: number; } export function buildNativeStreamerSessionContext( @@ -130,6 +159,7 @@ export interface NativeRenderSurfaceUpdate { export interface NativeRenderSurface extends NativeRenderSurfaceUpdate { windowHandle?: string; + screenRect?: NativeRenderSurfaceRect; } export interface KeyframeRequest { diff --git a/opennow-stable/src/shared/nativeStreamer.ts b/opennow-stable/src/shared/nativeStreamer.ts index 830e629c3..5ce2f7689 100644 --- a/opennow-stable/src/shared/nativeStreamer.ts +++ b/opennow-stable/src/shared/nativeStreamer.ts @@ -23,6 +23,10 @@ export interface NativeStreamerCapabilities { supportsRemoteIce: boolean; supportsLocalIce: boolean; supportsInput: boolean; + supportsVideoDecode: boolean; + supportsVideoPresent: boolean; + supportsAudioDecode?: boolean; + supportsAudioOutput?: boolean; videoBackends?: NativeVideoBackendCapability[]; } @@ -42,6 +46,17 @@ export type NativeStreamerCommand = type: "start"; context: NativeStreamerSessionContext; } + | { + id: string; + type: "nvst-bind"; + } + | { + id: string; + type: "nvst-send"; + host: string; + port: number; + payloadBase64: string; + } | { id: string; type: "offer"; @@ -93,6 +108,17 @@ export type NativeStreamerResponse = | { id: string; type: "ok"; + transport?: "webrtc" | "nvst"; + } + | { + id: string; + type: "nvst-bound"; + port: number; + mjolnirPort?: number; + localAddress?: string; + iceUsernameFragment?: string; + icePassword?: string; + dtlsFingerprint?: string; } | { id: string; @@ -114,7 +140,7 @@ export type NativeStreamerEvent = } | { type: "status"; - status: "starting" | "ready" | "streaming" | "stopped"; + status: "starting" | "ready" | "streaming" | "paused" | "stopped"; message?: string; } | {