From 515e50e7e709d358525ce024ba652a8023ed802e Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 17:11:58 +0200 Subject: [PATCH 01/12] build(obs-irl-source): bundle a static ffmpeg, libsrt and librist The plugin linked whatever ffmpeg the host obs shipped, which caused two problems. obs-deps pins libsrt 1.5.2 (2022) and librist 0.2.7, so every transport fix since then was missing. And obs bumped ffmpeg 7 (avcodec-61) to 8.1 (avcodec-62) between the 32.1 and 32.2 lines, so a binary linked against one would not load where the other was present, forcing a separate release artifact per obs line. deps/build-deps.sh now builds a pinned decode-only ffmpeg 8.1.2 with libsrt 1.5.6, librist 0.2.18 and mbedtls 3.6.4, linked statically. libobs is the only version sensitive link left, and it gates modules on (major, minor) <= host, so one artifact per platform covers 32.1 and every newer line. Symbol isolation is load-bearing: obs has already loaded its own ffmpeg exporting the same names, so an exported avcodec_open2 here would resolve to obs's copy and run against another library's structs. The module is built with hidden visibility, --exclude-libs,ALL and a version script that exports only obs_module_*. scripts/verify-plugin.sh asserts this after every build, since a successful compile does not prove it. Two silent failures worth recording. ffmpeg's configure ignores unknown names in an --enable-* list, so --enable-protocol=srt (the component is libsrt) produced a build with CONFIG_LIBSRT=yes and no srt protocol at all; build-deps.sh now asserts every required component against config.mak. And librist's meson found the host's shared libmbedcrypto instead of ours, which fix_mbedtls_pc corrects for both librist and libsrt. --- .github/workflows/build.yml | 177 ++++++---- .github/workflows/release.yml | 40 +-- .gitignore | 1 + CLAUDE.md | 35 +- CMakeLists.txt | 125 ++++++-- README.md | 20 +- RELEASING.md | 8 +- cmake/module-symbols.exp | 7 + cmake/module-symbols.map | 14 + deps/README.md | 109 +++++++ deps/build-deps.sh | 588 ++++++++++++++++++++++++++++++++++ deps/versions.env | 26 ++ scripts/verify-plugin.sh | 83 +++++ 13 files changed, 1095 insertions(+), 138 deletions(-) create mode 100644 cmake/module-symbols.exp create mode 100644 cmake/module-symbols.map create mode 100644 deps/README.md create mode 100644 deps/build-deps.sh create mode 100644 deps/versions.env create mode 100644 scripts/verify-plugin.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 787e008..acf3d68 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,16 @@ on: pull_request: workflow_call: +# The plugin bundles its own static FFmpeg/libsrt, so the only remaining tie to +# a specific OBS release is libobs itself. libobs gates a module on +# (major, minor) <= host version and looks up nothing but obs_module_*, so +# building against the OLDEST supported line produces one artifact that loads +# on that line and every newer one. That is why there is no per-OBS-line matrix +# here any more. +env: + OBS_VERSION: "32.1.2" + OBS_DEPS_VERSION: "2025-08-23" + jobs: linux-x64: runs-on: ubuntu-26.04 @@ -16,10 +26,21 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - build-essential cmake pkg-config \ - libobs-dev \ - libavformat-dev libavcodec-dev libswresample-dev \ - libavfilter-dev libswscale-dev libavutil-dev + build-essential cmake pkg-config nasm meson ninja-build \ + libobs-dev libva-dev + + # Only the install prefix is cached. The source and object trees are an + # order of magnitude larger and the build-marker files live inside the + # prefix, so a restored prefix alone is enough to skip every dep build. + - name: Cache bundled deps + id: cache-deps + uses: actions/cache@v4 + with: + path: deps/.build/prefix + key: irl-deps-linux-x64-${{ hashFiles('deps/versions.env', 'deps/build-deps.sh') }} + + - name: Build bundled deps + run: ./deps/build-deps.sh - name: Configure run: cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo @@ -27,6 +48,9 @@ jobs: - name: Build run: cmake --build build --parallel + - name: Verify isolation + run: ./scripts/verify-plugin.sh build/obs-irl-source.so + - name: Upload artifact uses: actions/upload-artifact@v4 with: @@ -34,26 +58,7 @@ jobs: path: build/obs-irl-source.so windows-x64: - name: windows-x64 (obs ${{ matrix.obs_line }}) runs-on: windows-2025-vs2026 - strategy: - fail-fast: false - matrix: - include: - # OBS 32.1.x bundles FFmpeg 7 (avcodec-61) via obs-deps 2025-08-23. - # A plugin linked against this loads on 32.1.x and, thanks to the - # forward-compatible libobs module gate, on newer lines too, but only - # where FFmpeg 7 DLLs are present. - - obs_line: "32.1" - obs_version: "32.1.2" - obs_deps_version: "2025-08-23" - # OBS 32.2.x bundles FFmpeg 8.1 (avcodec-62) via obs-deps 2026-07-15. - # FFmpeg's major bump (obsproject/obs-deps 2025-12-23) changes the DLL - # sonames, so this artifact is a separate binary that only resolves - # against 32.2.x. Bump obs_version to 32.2.0 once the final tag ships. - - obs_line: "32.2" - obs_version: "32.2.0-rc2" - obs_deps_version: "2026-07-15" steps: - uses: actions/checkout@v4 @@ -62,6 +67,34 @@ jobs: with: arch: x64 + # librist builds with meson. It has to be the Windows-native meson (so it + # detects cl.exe rather than looking for a POSIX toolchain), which + # path-type: inherit below then makes visible inside MSYS2. + - name: Install meson + shell: pwsh + run: pip install --disable-pip-version-check meson ninja + + # FFmpeg's configure needs a POSIX shell even when it drives cl.exe. + # path-type: inherit keeps the MSVC environment visible inside MSYS2. + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MSYS + path-type: inherit + update: false + install: make nasm diffutils pkgconf + + - name: Cache bundled deps + id: cache-deps + uses: actions/cache@v4 + with: + path: deps/.build/prefix + key: irl-deps-windows-x64-${{ hashFiles('deps/versions.env', 'deps/build-deps.sh') }} + + - name: Build bundled deps + shell: msys2 {0} + run: ./deps/build-deps.sh + - name: Cache OBS build id: cache-obs uses: actions/cache@v4 @@ -69,20 +102,22 @@ jobs: path: | obs-src obs-deps - key: obs-build-${{ matrix.obs_version }}-${{ matrix.obs_deps_version }}-v2 + key: obs-build-${{ env.OBS_VERSION }}-${{ env.OBS_DEPS_VERSION }}-v3 + # obs-deps is still needed here, but only to build libobs itself. The + # plugin no longer links its FFmpeg. - name: Download OBS dependencies if: steps.cache-obs.outputs.cache-hit != 'true' shell: pwsh run: | Invoke-WebRequest ` - -Uri "https://github.com/obsproject/obs-deps/releases/download/${{ matrix.obs_deps_version }}/windows-deps-${{ matrix.obs_deps_version }}-x64.zip" ` + -Uri "https://github.com/obsproject/obs-deps/releases/download/${{ env.OBS_DEPS_VERSION }}/windows-deps-${{ env.OBS_DEPS_VERSION }}-x64.zip" ` -OutFile obs-deps.zip Expand-Archive obs-deps.zip -DestinationPath obs-deps - name: Clone OBS source if: steps.cache-obs.outputs.cache-hit != 'true' - run: git clone --depth 1 --branch ${{ matrix.obs_version }} https://github.com/obsproject/obs-studio.git obs-src + run: git clone --depth 1 --branch ${{ env.OBS_VERSION }} https://github.com/obsproject/obs-studio.git obs-src - name: Patch OBS to build only libobs if: steps.cache-obs.outputs.cache-hit != 'true' @@ -141,12 +176,36 @@ jobs: cmake -B build -G "Visual Studio 18 2026" -A x64 -DOBS_SOURCE_DIR=%CD%\obs-src - -DFFMPEG_DIR=%CD%\obs-deps - name: Build shell: cmd run: cmake --build build --config RelWithDebInfo --parallel + - name: Verify isolation + shell: pwsh + run: | + $dll = "build/RelWithDebInfo/obs-irl-source.dll" + $deps = (dumpbin /dependents $dll) -join "`n" + $exports = (dumpbin /exports $dll) -join "`n" + + $fail = $false + if ($deps -match 'av(codec|format|util)-\d+\.dll|sws(cale)?-\d+\.dll|swresample-\d+\.dll') { + Write-Host " FAIL plugin still imports a host FFmpeg DLL"; $fail = $true + } else { + Write-Host " ok no FFmpeg DLL imports (host FFmpeg not required)" + } + if ($exports -match '\bav_|\bavcodec_|\bsrt_|\bmbedtls_') { + Write-Host " FAIL bundled stack symbols are exported"; $fail = $true + } else { + Write-Host " ok bundled stack symbols not exported" + } + if ($exports -match 'obs_module_load') { + Write-Host " ok obs_module_load exported" + } else { + Write-Host " FAIL obs_module_load missing"; $fail = $true + } + if ($fail) { Write-Host $deps; Write-Host $exports; exit 1 } + - name: Collect runtime dependencies shell: pwsh run: | @@ -159,38 +218,33 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: obs-irl-source-windows-x64-obs${{ matrix.obs_line }} + name: obs-irl-source-windows-x64 path: | build/RelWithDebInfo/obs-irl-source.dll build/RelWithDebInfo/w32-pthreads.dll macos-arm64: - name: macos-arm64 (obs ${{ matrix.obs_line }}) - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - obs_line: "32.1" - obs_version: "32.1.2" - obs_deps_version: "2025-08-23" - runner: "macos-15" - # OBS 32.2 requires the macOS 26.5 SDK (Xcode 26.5); macos-15 ships - # only SDK 15.5, so this line needs the macos-26 image. - - obs_line: "32.2" - obs_version: "32.2.0-rc2" - obs_deps_version: "2026-07-15" - runner: "macos-26" + runs-on: macos-15 + env: + MACOSX_DEPLOYMENT_TARGET: "12.0" steps: - uses: actions/checkout@v4 - # ffmpeg is intentionally NOT installed from Homebrew. The plugin must - # link the same FFmpeg that OBS bundles (from obs-deps), so its runtime - # dylib references resolve against OBS.app's Frameworks. Homebrew ffmpeg - # has a different soname per OBS line and absolute (non-@rpath) install - # names, so a brew-linked plugin only loads where brew's FFmpeg matches. + # ffmpeg is intentionally NOT installed from Homebrew. The plugin links + # the bundled static stack instead, which is what removes its dependency + # on whatever FFmpeg the host OBS ships. - name: Install build tools - run: brew install cmake pkg-config simde uthash jansson + run: brew install cmake pkg-config nasm meson ninja simde uthash jansson + + - name: Cache bundled deps + id: cache-deps + uses: actions/cache@v4 + with: + path: deps/.build/prefix + key: irl-deps-macos-arm64-${{ hashFiles('deps/versions.env', 'deps/build-deps.sh') }} + + - name: Build bundled deps + run: ./deps/build-deps.sh - name: Cache OBS build id: cache-obs @@ -199,19 +253,19 @@ jobs: path: | obs-src obs-deps - key: obs-macos-arm64-${{ matrix.obs_version }}-${{ matrix.obs_deps_version }}-v2 + key: obs-macos-arm64-${{ env.OBS_VERSION }}-${{ env.OBS_DEPS_VERSION }}-v3 - name: Download OBS dependencies if: steps.cache-obs.outputs.cache-hit != 'true' run: | curl -L -o macos-deps.tar.xz \ - "https://github.com/obsproject/obs-deps/releases/download/${{ matrix.obs_deps_version }}/macos-deps-${{ matrix.obs_deps_version }}-arm64.tar.xz" + "https://github.com/obsproject/obs-deps/releases/download/${{ env.OBS_DEPS_VERSION }}/macos-deps-${{ env.OBS_DEPS_VERSION }}-arm64.tar.xz" mkdir -p obs-deps tar -xf macos-deps.tar.xz -C obs-deps - name: Clone OBS source if: steps.cache-obs.outputs.cache-hit != 'true' - run: git clone --depth 1 --branch ${{ matrix.obs_version }} https://github.com/obsproject/obs-studio.git obs-src + run: git clone --depth 1 --branch ${{ env.OBS_VERSION }} https://github.com/obsproject/obs-studio.git obs-src - name: Patch OBS to build only libobs if: steps.cache-obs.outputs.cache-hit != 'true' @@ -226,9 +280,8 @@ jobs: sed -i '' '/CMAKE_IGNORE_PREFIX_PATH/d' obs-src/cmake/macos/defaults.cmake # Relax OBS's SDK/Xcode floor. OBS gates the whole app on a very - # recent SDK (32.2 wants macOS 26.5 / Xcode 26.5), but we only build - # libobs with the UI off, which compiles on the runner's SDK. No-op - # on lines whose floor the runner already satisfies. + # recent SDK, but we only build libobs with the UI off, which + # compiles on the runner's SDK. sed -i '' -E 's/set\(obs_macos_minimum_sdk [0-9.]+\)/set(obs_macos_minimum_sdk 1.0)/' obs-src/cmake/macos/compilerconfig.cmake sed -i '' -E 's/set\(obs_macos_minimum_xcode [0-9.]+\)/set(obs_macos_minimum_xcode 1.0)/' obs-src/cmake/macos/compilerconfig.cmake @@ -245,24 +298,24 @@ jobs: if: steps.cache-obs.outputs.cache-hit != 'true' run: cmake --build obs-src/build --config Release --target libobs --parallel - # Force the manual FFmpeg discovery path (FFMPEG_DIR) instead of - # pkg-config. This links obs-deps' FFmpeg directly by library, preserving - # its @rpath install names, and avoids picking up any FFmpeg .pc files - # that the runner image or a relocated obs-deps prefix might expose. + # PkgConfig is disabled so a stray Homebrew or obs-deps FFmpeg .pc cannot + # shadow the bundled headers. - name: Configure run: > cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DOBS_SOURCE_DIR=$PWD/obs-src - -DFFMPEG_DIR=$PWD/obs-deps -DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=ON -DCMAKE_C_FLAGS="-I$(brew --prefix)/include" - name: Build run: cmake --build build --parallel + - name: Verify isolation + run: ./scripts/verify-plugin.sh build/obs-irl-source.so + - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: obs-irl-source-macos-arm64-obs${{ matrix.obs_line }} + name: obs-irl-source-macos-arm64 path: build/obs-irl-source.so diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 68e844d..ee4a9bc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,9 +40,8 @@ jobs: # Package layouts mirror where each platform expects plugins, so a # release archive is extracted straight into place with no renaming. - # The per-OBS-line loop globs the artifact names instead of hardcoding - # lines, so editing the matrix in build.yml is the only change needed - # to add or drop a line. + # One archive per platform: the plugin bundles its own FFmpeg, so there + # is no longer a per-OBS-line artifact to disambiguate. - name: Package run: | set -euo pipefail @@ -59,28 +58,23 @@ jobs: # Windows: extract into the OBS Studio install folder # (the DLLs land in obs-plugins\64bit) - for dir in artifacts/obs-irl-source-windows-x64-obs*; do - line="${dir##*-}" - staging="$(mktemp -d)" - mkdir -p "${staging}/obs-plugins/64bit" - cp "${dir}"/*.dll "${staging}/obs-plugins/64bit/" - (cd "${staging}" && zip -r \ - "${GITHUB_WORKSPACE}/dist/obs-irl-source-${version}-windows-x64-${line}.zip" \ - obs-plugins) - rm -rf "${staging}" - done + staging="$(mktemp -d)" + mkdir -p "${staging}/obs-plugins/64bit" + cp artifacts/obs-irl-source-windows-x64/*.dll "${staging}/obs-plugins/64bit/" + (cd "${staging}" && zip -r \ + "${GITHUB_WORKSPACE}/dist/obs-irl-source-${version}-windows-x64.zip" \ + obs-plugins) + rm -rf "${staging}" # macOS: extract into ~/Library/Application Support/obs-studio/plugins/ - for dir in artifacts/obs-irl-source-macos-arm64-obs*; do - line="${dir##*-}" - staging="$(mktemp -d)" - mkdir -p "${staging}/obs-irl-source/bin" - cp "${dir}/obs-irl-source.so" "${staging}/obs-irl-source/bin/" - (cd "${staging}" && zip -r \ - "${GITHUB_WORKSPACE}/dist/obs-irl-source-${version}-macos-arm64-${line}.zip" \ - obs-irl-source) - rm -rf "${staging}" - done + staging="$(mktemp -d)" + mkdir -p "${staging}/obs-irl-source/bin" + cp artifacts/obs-irl-source-macos-arm64/obs-irl-source.so \ + "${staging}/obs-irl-source/bin/" + (cd "${staging}" && zip -r \ + "${GITHUB_WORKSPACE}/dist/obs-irl-source-${version}-macos-arm64.zip" \ + obs-irl-source) + rm -rf "${staging}" (cd dist && sha256sum ./* > sha256sums.txt) ls -l dist diff --git a/.gitignore b/.gitignore index cb336ba..5c60d91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/ cmake-build-*/ .cache/ +deps/.build/ *.o *.so *.dll diff --git a/CLAUDE.md b/CLAUDE.md index 158b117..4730466 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,34 +8,43 @@ IRL Source is a third-party OBS Studio plugin (C11, AGPL-3.0) for receiving live ## Build commands +The plugin statically links its own FFmpeg, libsrt and mbedTLS (see `deps/README.md`), so the first step on every platform is building that stack. It is incremental, so this is a one time cost per version bump. + ### Linux ```bash -sudo apt install build-essential cmake pkg-config libobs-dev \ - libavformat-dev libavcodec-dev libswresample-dev libavfilter-dev \ - libswscale-dev libavutil-dev +sudo apt install build-essential cmake pkg-config nasm libobs-dev libva-dev +./deps/build-deps.sh cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build --parallel +./scripts/verify-plugin.sh build/obs-irl-source.so ``` ### Windows (MSVC) +`deps/build-deps.sh` runs inside MSYS2 with the MSVC environment active (FFmpeg's configure needs a POSIX shell even when driving `cl.exe`). See the `windows-x64` job in `.github/workflows/build.yml` for the exact setup. + ```powershell -cmake -B build -G "Visual Studio 18 2026" -A x64 -DOBS_SOURCE_DIR=obs-src -DFFMPEG_DIR=obs-deps +cmake -B build -G "Visual Studio 18 2026" -A x64 -DOBS_SOURCE_DIR=obs-src cmake --build build --config RelWithDebInfo ``` ### macOS (Apple Silicon) ```bash -brew install cmake pkg-config ffmpeg simde uthash jansson -cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DOBS_SOURCE_DIR=$PWD/obs-src +brew install cmake pkg-config nasm simde uthash jansson +./deps/build-deps.sh +cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DOBS_SOURCE_DIR=$PWD/obs-src \ + -DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=ON cmake --build build --parallel +./scripts/verify-plugin.sh build/obs-irl-source.so ``` Output: `build/obs-irl-source.so` (Linux/macOS) or `build/RelWithDebInfo/obs-irl-source.dll` (Windows). -These snippets compile the plugin, but a binary that actually loads in an installed OBS must link the same FFmpeg major that OBS bundles. The macOS snippet uses Homebrew FFmpeg for convenience, which is fine for a compile check but produces a binary that will not load inside OBS.app (wrong soname and non `@rpath` install names). For a distributable per OBS line build, follow what CI does (see the CI section): link obs-deps FFmpeg matching the target line. +`-DIRL_BUNDLED_FFMPEG=OFF` falls back to linking a system or obs-deps FFmpeg. That path still works for a quick compile check, but it reintroduces the per OBS line binding the bundled stack exists to remove, so it is not what releases use. + +`scripts/verify-plugin.sh` is not optional polish. It asserts the two properties that make the bundled stack correct and that a successful compile does not prove: that the binary carries no `libav*` dependency, and that it exports nothing but `obs_module_*`. CI runs it (and a `dumpbin` equivalent on Windows) on every build. There are no tests. @@ -106,13 +115,17 @@ Config fields marked `/* hot */` in `struct irl_config` are written by `irl_sour ## CI -GitHub Actions (`.github/workflows/build.yml`) builds on three platforms: Linux x64 (Ubuntu 26.04), Windows x64 (VS 2026), macOS ARM64 (macos-15). The Windows and macOS jobs clone OBS source, patch it to build only libobs, and link against that. The workflow also exposes `workflow_call` so the release workflow reuses it. +GitHub Actions (`.github/workflows/build.yml`) builds on three platforms: Linux x64 (Ubuntu 26.04), Windows x64 (VS 2026), macOS ARM64 (macos-15). Every job builds the bundled media stack first (cached on the hash of `deps/versions.env` plus `deps/build-deps.sh`), then the plugin, then runs the isolation checks. The Windows and macOS jobs also clone OBS source and patch it to build only libobs, which is the plugin's one remaining link against a specific OBS release. The workflow exposes `workflow_call` so the release workflow reuses it. + +Releases are tag driven (`.github/workflows/release.yml`, see `RELEASING.md`). Pushing `vX.Y.Z` verifies the tag against the `project(VERSION)` in CMakeLists.txt, runs the build workflow, repackages the artifacts into install layout archives (one per platform: Linux tar.gz, Windows zip, macOS zip), generates sha256sums.txt, and creates a draft GitHub release whose body is `.github/release-notes-header.md` plus commit generated notes. Publishing the draft is manual, after testing the artifacts. + +### One artifact per platform -Releases are tag driven (`.github/workflows/release.yml`, see `RELEASING.md`). Pushing `vX.Y.Z` verifies the tag against the `project(VERSION)` in CMakeLists.txt, runs the build workflow, repackages the artifacts into install layout archives (Linux tar.gz, per OBS line Windows and macOS zips, globbed from the artifact names so matrix edits flow through automatically), generates sha256sums.txt, and creates a draft GitHub release whose body is `.github/release-notes-header.md` plus commit generated notes. Publishing the draft is manual, after testing the artifacts. +There used to be a `matrix.include` over OBS lines, producing `-obs32.1` and `-obs32.2` binaries. That existed purely because the plugin dynamically linked obs-deps' FFmpeg, and OBS bumped FFmpeg 7 (`avcodec-61`) to 8.1 (`avcodec-62`) between those lines, so a binary linked against one would not load where the other was present. Bundling FFmpeg statically removed that constraint and the matrix with it. -The Windows and macOS jobs run a `matrix.include` over the supported OBS lines, one row per line. Each row pins both the OBS source tag (`obs_version`) and the matching obs-deps release (`obs_deps_version`). This split exists because the plugin dynamically links FFmpeg from obs-deps, and OBS bumped FFmpeg 7 (`avcodec-61`) to 8.1 (`avcodec-62`) between the 32.1 and 32.2 lines. A binary linked against one FFmpeg major will not load where the other is present, so each line produces its own artifact (suffixed `-obs32.1` / `-obs32.2`). The libobs module gate is forward compatible on its own (a plugin loads on its build version and any newer host), so it is FFmpeg, not libobs, that forces the per-line builds. macOS links obs-deps FFmpeg instead of Homebrew's (via `FFMPEG_DIR` plus `CMAKE_DISABLE_FIND_PACKAGE_PkgConfig`) so the plugin's dylib references carry `@rpath` install names that resolve inside OBS.app. +libobs itself was never the problem. `obs_init_module` gates a plugin on `(mod.ver() & 0xFFFF0000) <= LIBOBS_API_VER`, so major and minor only, and it looks up nothing but `obs_module_*` symbols. Building against the oldest supported line therefore yields one binary that loads on that line and every newer one. `OBS_VERSION` and `OBS_DEPS_VERSION` at the top of `build.yml` pin that oldest line; raise them only to drop support for older OBS releases, never to chase a newer one. -To add or move a supported line, edit the `matrix.include` rows: set `obs_version` to a tag on that line and `obs_deps_version` to the obs-deps release that the line's `CMakePresets.json` pins under `dependencies.prebuilt.version`. Verify the two FFmpeg majors differ by checking `avcodec-*.dll` (Windows) or `libavcodec.*.dylib` (macOS) in each target OBS install; if they match, one build covers both lines. +`OBS_DEPS_VERSION` still exists because libobs needs obs-deps to build. The plugin no longer touches it. ## Contributing diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c15ac0..99b2a0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,22 +159,49 @@ else() endif() # ── Find FFmpeg ────────────────────────────────────────────── -# Try pkg-config first (Linux, MSYS2, macOS with Homebrew). -# Fall back to find_path/find_library for MSVC + pre-built FFmpeg. +# Preferred: the bundled static stack from deps/build-deps.sh. It pins +# FFmpeg, libsrt and mbedTLS versions we control, so the plugin's media +# pipeline no longer depends on which FFmpeg major the host OBS happens to +# ship (that split is what forced per-OBS-line release artifacts) and no +# longer inherits obs-deps' 2022-era libsrt. +# +# Set IRL_BUNDLED_FFMPEG=OFF to link a system/obs-deps FFmpeg instead. +option(IRL_BUNDLED_FFMPEG "Link the bundled static FFmpeg/libsrt stack from deps/" ON) +set(IRL_DEPS_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/deps/.build/prefix" + CACHE PATH "Install prefix produced by deps/build-deps.sh") + set(_FFMPEG_FOUND OFF) +set(_IRL_BUNDLED OFF) -find_package(PkgConfig QUIET) -if(PkgConfig_FOUND) - pkg_check_modules(LIBAV QUIET IMPORTED_TARGET - libavformat - libavcodec - libavfilter - libswresample - libswscale - libavutil - ) - if(LIBAV_FOUND) +if(IRL_BUNDLED_FFMPEG) + if(EXISTS "${IRL_DEPS_PREFIX}/irl-deps.cmake") + include("${IRL_DEPS_PREFIX}/irl-deps.cmake") set(_FFMPEG_FOUND ON) + set(_IRL_BUNDLED ON) + else() + message(FATAL_ERROR + "Bundled FFmpeg requested but not built.\n" + " Run: ./deps/build-deps.sh\n" + " (or pass -DIRL_DEPS_PREFIX=/path/to/prefix if you built it elsewhere,\n" + " or -DIRL_BUNDLED_FFMPEG=OFF to link a system FFmpeg instead)") + endif() +endif() + +# Try pkg-config first (Linux, MSYS2, macOS with Homebrew). +# Fall back to find_path/find_library for MSVC + pre-built FFmpeg. +if(NOT _FFMPEG_FOUND) + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(LIBAV QUIET IMPORTED_TARGET + libavformat + libavcodec + libswresample + libswscale + libavutil + ) + if(LIBAV_FOUND) + set(_FFMPEG_FOUND ON) + endif() endif() endif() @@ -205,10 +232,6 @@ if(NOT _FFMPEG_FOUND) PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES include ) - find_path(AVFILTER_INCLUDE_DIR libavfilter/avfilter.h - PATHS ${_FFMPEG_SEARCH_PATHS} - PATH_SUFFIXES include - ) find_path(SWSCALE_INCLUDE_DIR libswscale/swscale.h PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES include @@ -221,17 +244,15 @@ if(NOT _FFMPEG_FOUND) find_library(AVFORMAT_LIB NAMES avformat PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES lib) find_library(AVCODEC_LIB NAMES avcodec PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES lib) find_library(SWRESAMPLE_LIB NAMES swresample PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES lib) - find_library(AVFILTER_LIB NAMES avfilter PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES lib) find_library(SWSCALE_LIB NAMES swscale PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES lib) find_library(AVUTIL_LIB NAMES avutil PATHS ${_FFMPEG_SEARCH_PATHS} PATH_SUFFIXES lib) - if(AVFORMAT_LIB AND AVCODEC_LIB AND SWRESAMPLE_LIB AND AVFILTER_LIB AND SWSCALE_LIB AND AVUTIL_LIB) + if(AVFORMAT_LIB AND AVCODEC_LIB AND SWRESAMPLE_LIB AND SWSCALE_LIB AND AVUTIL_LIB) set(_FFMPEG_FOUND ON) set(_FFMPEG_MANUAL_INCLUDE_DIRS ${AVFORMAT_INCLUDE_DIR} ${AVCODEC_INCLUDE_DIR} ${SWRESAMPLE_INCLUDE_DIR} - ${AVFILTER_INCLUDE_DIR} ${SWSCALE_INCLUDE_DIR} ${AVUTIL_INCLUDE_DIR} ) @@ -240,7 +261,6 @@ if(NOT _FFMPEG_FOUND) ${AVFORMAT_LIB} ${AVCODEC_LIB} ${SWRESAMPLE_LIB} - ${AVFILTER_LIB} ${SWSCALE_LIB} ${AVUTIL_LIB} ) @@ -250,8 +270,8 @@ endif() if(NOT _FFMPEG_FOUND) message(FATAL_ERROR "FFmpeg not found. Either:\n" - " - Install FFmpeg dev packages (apt install libavformat-dev libavfilter-dev ...)\n" - " - Install via MSYS2 (pacman -S mingw-w64-ucrt-x86_64-ffmpeg)\n" + " - Build the bundled stack: ./deps/build-deps.sh\n" + " - Install FFmpeg dev packages (apt install libavformat-dev libswscale-dev ...)\n" " - Pass -DFFMPEG_DIR=C:/path/to/ffmpeg (pre-built Windows SDK)\n" ) endif() @@ -294,7 +314,12 @@ if(_OBS_EXTRA_INCLUDE_DIRS) target_include_directories(obs-irl-source PRIVATE ${_OBS_EXTRA_INCLUDE_DIRS}) endif() -if(LIBAV_FOUND) +if(_IRL_BUNDLED) + # BEFORE, so the bundled headers win over any FFmpeg dev package that the + # OBS include paths might also drag in. Mismatched headers and archives is + # the one way a static link can still fail at runtime. + target_include_directories(obs-irl-source BEFORE PRIVATE ${IRL_DEPS_INCLUDE_DIRS}) +elseif(LIBAV_FOUND) target_include_directories(obs-irl-source PRIVATE ${LIBAV_INCLUDE_DIRS}) elseif(_FFMPEG_MANUAL_INCLUDE_DIRS) target_include_directories(obs-irl-source PRIVATE ${_FFMPEG_MANUAL_INCLUDE_DIRS}) @@ -316,12 +341,45 @@ else() set(_OBS_LINK_TARGET obs) endif() -if(LIBAV_FOUND) +if(_IRL_BUNDLED) + target_link_libraries(obs-irl-source PRIVATE ${_OBS_LINK_TARGET} + ${IRL_DEPS_STATIC_LIBRARIES} + ${IRL_DEPS_SYSTEM_LIBRARIES} + ) +elseif(LIBAV_FOUND) target_link_libraries(obs-irl-source PRIVATE ${_OBS_LINK_TARGET} PkgConfig::LIBAV) else() target_link_libraries(obs-irl-source PRIVATE ${_OBS_LINK_TARGET} ${_FFMPEG_MANUAL_LIBS}) endif() +# ── Symbol isolation for the bundled stack ─────────────────── +# +# Load-bearing, not cosmetic. The host OBS has already loaded its own +# libavcodec/libavformat, which export the exact same symbol names as the +# archives linked in here. On ELF, an exported avcodec_open2 in this module +# would be resolved through the global scope to *OBS's* copy, so our calls +# would run against a different library's structs. Hiding everything but the +# obs_module_* entry points (the only symbols libobs looks up) binds every +# internal call to our own copy at link time. +if(_IRL_BUNDLED) + set_target_properties(obs-irl-source PROPERTIES + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + if(APPLE) + target_link_options(obs-irl-source PRIVATE + "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/cmake/module-symbols.exp") + set_property(TARGET obs-irl-source APPEND PROPERTY + LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/module-symbols.exp") + elseif(UNIX) + target_link_options(obs-irl-source PRIVATE + "-Wl,--exclude-libs,ALL" + "-Wl,--version-script,${CMAKE_CURRENT_SOURCE_DIR}/cmake/module-symbols.map") + set_property(TARGET obs-irl-source APPEND PROPERTY + LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/module-symbols.map") + endif() +endif() + if(_OBS_EXTRA_LIBS) target_link_libraries(obs-irl-source PRIVATE ${_OBS_EXTRA_LIBS}) endif() @@ -353,8 +411,9 @@ endif() set_target_properties(obs-irl-source PROPERTIES PREFIX "") # ── Windows-specific ───────────────────────────────────────── -if(WIN32) - # Copy FFmpeg DLLs next to the plugin for convenience +if(WIN32 AND NOT _IRL_BUNDLED) + # Copy FFmpeg DLLs next to the plugin for convenience. The bundled stack + # is static, so there is nothing to copy there. if(FFMPEG_DIR AND EXISTS "${FFMPEG_DIR}/bin") add_custom_command(TARGET obs-irl-source POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory @@ -388,10 +447,16 @@ if(_OBS_USE_FIND_PACKAGE) else() message(STATUS " OBS dir: ${OBS_SOURCE_DIR}") endif() -if(LIBAV_FOUND) - message(STATUS " FFmpeg: pkg-config (${LIBAV_VERSION})") +if(_IRL_BUNDLED) + message(STATUS " FFmpeg: bundled static ${IRL_DEPS_FFMPEG_VERSION}") + message(STATUS " libsrt: bundled static ${IRL_DEPS_SRT_VERSION}") + message(STATUS " librist: bundled static ${IRL_DEPS_LIBRIST_VERSION}") + message(STATUS " mbedTLS: bundled static ${IRL_DEPS_MBEDTLS_VERSION}") + message(STATUS " Deps: ${IRL_DEPS_PREFIX}") +elseif(LIBAV_FOUND) + message(STATUS " FFmpeg: pkg-config (${LIBAV_VERSION}) — host-provided") else() - message(STATUS " FFmpeg: manual (${FFMPEG_DIR})") + message(STATUS " FFmpeg: manual (${FFMPEG_DIR}) — host-provided") endif() message(STATUS " Install: ${OBS_PLUGIN_DIR}") message(STATUS "") diff --git a/README.md b/README.md index 4f3c1e6..0213513 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ OBS ships with a Media Source that can play SRT. It works, but it was written fo ## Installation -Download from [Releases](../../releases). Windows and macOS archives are built per OBS release line (the file name says which, for example `obs32.1`). Pick the one matching your installed OBS version. +Download from [Releases](../../releases). There is one archive per platform and it works on OBS 32.1 and newer. Older releases of this plugin shipped a separate build per OBS version; that is no longer necessary. ### Windows @@ -58,7 +58,7 @@ Download from [Releases](../../releases). Windows and macOS archives are built p ### Linux -The release binary is built on Ubuntu against distribution libobs and FFmpeg. On other distributions, build from source instead (see [Building from source](#building-from-source)). +The release binary bundles its own media stack but still links your distribution's libobs, so it is built against Ubuntu's. On other distributions, build from source instead (see [Building from source](#building-from-source)). 1. Close OBS 2. Extract the tarball into `~/.config/obs-studio/plugins/` @@ -322,27 +322,29 @@ A healthy stream shows `speed=1.000`, `underruns=0`, `restarts=0`, and a constan ### Linux +The plugin statically links its own FFmpeg, libsrt, librist and mbedTLS rather than using OBS's. Building that stack is the first step, and it only has to happen again when a version in `deps/versions.env` changes. See `deps/README.md` for the details. + ```bash -sudo apt install build-essential cmake pkg-config libobs-dev \ - libavformat-dev libavcodec-dev libswresample-dev libavfilter-dev \ - libswscale-dev libavutil-dev +sudo apt install build-essential cmake pkg-config nasm meson ninja-build \ + libobs-dev libva-dev +./deps/build-deps.sh cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo cmake --build build --parallel ``` ### Windows (MSVC) -Requires Visual Studio 2026 and OBS source plus obs-deps (see `.github/workflows/build.yml` for the exact versions CI uses): +Requires Visual Studio 2026, plus OBS source and obs-deps to build libobs (see `.github/workflows/build.yml` for the exact versions CI uses). `deps/build-deps.sh` runs under MSYS2 with the MSVC environment active, because FFmpeg's configure needs a POSIX shell even when it is driving `cl.exe`. ```powershell -# Clone OBS and download pre-built dependencies +# Clone OBS and download pre-built dependencies (for libobs only) git clone --depth 1 --branch 32.1.2 https://github.com/obsproject/obs-studio.git obs-src # Download obs-deps from https://github.com/obsproject/obs-deps/releases # Install SIMDe headers or add them to CMAKE_PREFIX_PATH # Build -cmake -B build -G "Visual Studio 18 2026" -A x64 -DOBS_SOURCE_DIR=obs-src -DFFMPEG_DIR=obs-deps +cmake -B build -G "Visual Studio 18 2026" -A x64 -DOBS_SOURCE_DIR=obs-src cmake --build build --config RelWithDebInfo ``` -The plugin dynamically links the FFmpeg that OBS bundles, and OBS lines differ in FFmpeg major version, which is why release archives are built per OBS line. +Earlier versions dynamically linked the FFmpeg that OBS bundles, and OBS lines differ in FFmpeg major version, which is why release archives used to be built per OBS line. Bundling removed that constraint. `-DIRL_BUNDLED_FFMPEG=OFF` restores the old behaviour for a quick compile check against a system FFmpeg. diff --git a/RELEASING.md b/RELEASING.md index 9db9e62..8371632 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -22,8 +22,8 @@ Releases are tag driven. Pushing a tag `vX.Y.Z` runs `.github/workflows/release. 4. Wait for the Release workflow to finish. It creates a draft release containing: * `obs-irl-source-X.Y.Z-linux-x64.tar.gz` (extract into `~/.config/obs-studio/plugins/`) - * `obs-irl-source-X.Y.Z-windows-x64-obsNN.N.zip`, one per supported OBS line (extract into the OBS install folder) - * `obs-irl-source-X.Y.Z-macos-arm64-obsNN.N.zip`, one per supported OBS line (extract into `~/Library/Application Support/obs-studio/plugins/`) + * `obs-irl-source-X.Y.Z-windows-x64.zip` (extract into the OBS install folder) + * `obs-irl-source-X.Y.Z-macos-arm64.zip` (extract into `~/Library/Application Support/obs-studio/plugins/`) * `sha256sums.txt` The release body starts with install instructions (from `.github/release-notes-header.md`) followed by notes generated from the commit history. @@ -43,7 +43,9 @@ git push -f origin vX.Y.Z ## Supported OBS lines -The set of per line Windows and macOS packages comes from the `matrix.include` rows in `.github/workflows/build.yml`. The release packaging globs the artifact names, so adding or dropping a line there is the only change needed (see the CI section in `CLAUDE.md` for how to pick `obs_version` and `obs_deps_version`). +One archive per platform covers every supported OBS line. The plugin bundles its own media stack, so the only version sensitive link left is libobs, and libobs gates a plugin on `(major, minor) <= host`. Building against the oldest supported line therefore produces a binary that also loads on every newer one. + +`OBS_VERSION` at the top of `.github/workflows/build.yml` pins that oldest line. Raise it only to drop support for older OBS releases, never to chase a newer one. See the CI section in `CLAUDE.md`. ## Not automated (yet) diff --git a/cmake/module-symbols.exp b/cmake/module-symbols.exp new file mode 100644 index 0000000..7e43f87 --- /dev/null +++ b/cmake/module-symbols.exp @@ -0,0 +1,7 @@ +# Mach-O exported symbols list for the plugin module. +# +# See cmake/module-symbols.map for the rationale. macOS two-level namespace +# already prevents the bundled static FFmpeg from being rebound at load time, +# but keeping the export table to just the module entry points avoids leaking +# a second full set of av* symbols into the process. +_obs_module_* diff --git a/cmake/module-symbols.map b/cmake/module-symbols.map new file mode 100644 index 0000000..4002c94 --- /dev/null +++ b/cmake/module-symbols.map @@ -0,0 +1,14 @@ +/* + * ELF version script for the plugin module. + * + * libobs only ever os_dlsym()s obs_module_* out of a plugin, so nothing else + * needs to be visible. Hiding the rest is what keeps the bundled static FFmpeg + * from being interposed by (or interposing on) the host OBS's own FFmpeg, + * which shares every symbol name with ours. + */ +{ + global: + obs_module_*; + local: + *; +}; diff --git a/deps/README.md b/deps/README.md new file mode 100644 index 0000000..dec7da8 --- /dev/null +++ b/deps/README.md @@ -0,0 +1,109 @@ +# Bundled media stack + +The plugin statically links its own FFmpeg, libsrt, librist and mbedTLS instead +of using whatever the host OBS ships. `build-deps.sh` builds that stack; +`versions.env` pins the versions. + +## Why + +Two problems, one fix. + +**Stale transport libraries.** obs-deps has pinned libsrt 1.5.2 (September 2022) +and librist 0.2.7 for years. Every SRT and RIST fix since then is missing from +OBS's FFmpeg. + +**FFmpeg majors split the releases.** OBS 32.1 bundles FFmpeg 7 (`avcodec-61`), +OBS 32.2 bundles FFmpeg 8.1 (`avcodec-62`). A plugin linked against one will not +load where the other is present, so the project had to publish a separate +binary per OBS line. Linking statically removes the dependency entirely, and one +artifact per platform now covers every OBS line. + +## Usage + +```bash +./deps/build-deps.sh # builds into deps/.build/prefix +./deps/build-deps.sh --jobs 8 +./deps/build-deps.sh --clean # discard and rebuild everything +``` + +The build is incremental. Marker files inside the prefix record what finished, +so a second run is a no-op and CI only has to cache `deps/.build/prefix` (not +the much larger source and object trees). + +The script writes `deps/.build/prefix/irl-deps.cmake` describing the exact link +line. The plugin's CMakeLists includes that file; it does not rediscover the +stack on its own. + +### Platforms + +Needs a C/C++ compiler, CMake, nasm, and meson/ninja (librist builds with +meson, everything else with CMake or autotools). + +Linux and macOS run it directly. On Windows it runs inside MSYS2 with the MSVC +environment active, because FFmpeg's configure needs a POSIX shell even when it +is driving `cl.exe`. meson there must be the Windows-native one (installed with +pip) so it detects `cl.exe` rather than looking for a POSIX toolchain. See the +`windows-x64` job in `.github/workflows/build.yml`. + +Linux additionally needs `libva-dev` for VAAPI. The script refuses to build +without it rather than silently producing a software-only binary. For a local +compile check on a machine without it: + +```bash +IRL_DEPS_DISABLE_VAAPI=1 ./deps/build-deps.sh +``` + +Never ship that build. + +## What is in the FFmpeg build + +Decode only, `--disable-everything` plus an explicit component list: H.264, +HEVC, AV1, VP9, AAC, Opus, MP3, AC3/E-AC3 and PCM decoders; MPEG-TS, FLV, MP4, +Matroska, HLS and RTSP demuxers; SRT, RIST, RTMP(S), HTTP(S), TCP, UDP and RTP +protocols. Hardware decode is VAAPI and NVDEC on Linux, D3D11VA/DXVA2 and NVDEC +on Windows, VideoToolbox on macOS. + +The component list is what the README advertises, no more and no less. Trimming +it further is a user-visible feature removal, not a size optimisation. + +`configure` silently ignores unknown names in an `--enable-*` list, which makes +a typo invisible: `--enable-protocol=srt` (the component is `libsrt`) produced a +build with `CONFIG_LIBSRT=yes` and no SRT protocol at all. `build-deps.sh` +therefore asserts against the generated `ffbuild/config.mak` that every +component the plugin depends on actually landed, and fails the build otherwise. + +`fix_mbedtls_pc` exists for a related trap. libsrt and librist both record their +mbedTLS dependency as an absolute library path, which puts it in the wrong place +on a single-pass static link and, in librist's case, pinned the *host's* shared +`libmbedcrypto.so` instead of the copy in this prefix. The helper rewrites both +`.pc` files to plain `-l` flags resolved out of the prefix. + +## Symbol isolation + +The host OBS has already loaded its own FFmpeg, exporting the same symbol names +as the archives linked here. On ELF an exported `avcodec_open2` in the plugin +would resolve through the global scope to OBS's copy, so the plugin's calls +would run against a different library's structs. + +The plugin is therefore built with hidden visibility, `--exclude-libs,ALL` and a +version script (`cmake/module-symbols.map`, `cmake/module-symbols.exp`) so the +only exported symbols are the `obs_module_*` entry points libobs looks up. +`scripts/verify-plugin.sh` asserts this after every build, and CI fails if the +plugin gains an `libav*` dependency or leaks a bundled symbol. + +## Licensing + +FFmpeg is configured LGPLv3 (`--enable-version3`, no `--enable-gpl`), which is +compatible with the plugin's AGPL-3.0. Nothing here needs GPL components since +the plugin decodes and never encodes. libsrt is MPL-2.0, librist is BSD-2-Clause +and mbedTLS is Apache-2.0, all compatible with (A)GPLv3. + +Distributing a statically linked binary means distributing the corresponding +source. `versions.env` pins the exact upstream releases and this script is the +complete build recipe. + +## Bumping a version + +Edit `versions.env`, update the matching SHA256, and run the script. CI keys its +cache on the contents of `versions.env` and `build-deps.sh`, so a bump +invalidates it automatically. diff --git a/deps/build-deps.sh b/deps/build-deps.sh new file mode 100644 index 0000000..6cd85c6 --- /dev/null +++ b/deps/build-deps.sh @@ -0,0 +1,588 @@ +#!/usr/bin/env bash +# +# obs-irl-source — build the bundled media stack +# (mbedTLS, libsrt, librist, nv-codec-headers, FFmpeg). +# +# Copyright (C) 2026 Thomas Lekanger +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Produces static libraries plus a generated irl-deps.cmake describing the +# exact link line. On Windows this runs inside an MSYS2 bash with the MSVC +# environment already active (FFmpeg's configure needs a POSIX shell even when +# it drives cl.exe). +# +# Usage: +# deps/build-deps.sh [--prefix DIR] [--jobs N] [--clean] + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_dir="$(cd "${script_dir}/.." && pwd)" + +# shellcheck source=versions.env +source "${script_dir}/versions.env" + +prefix="${IRL_DEPS_PREFIX:-${repo_dir}/deps/.build/prefix}" +work="${IRL_DEPS_WORK:-${repo_dir}/deps/.build}" +jobs="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)" +clean=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) + prefix="$2" + shift 2 + ;; + --jobs) + jobs="$2" + shift 2 + ;; + --clean) + clean=1 + shift + ;; + -h | --help) + sed -n '2,20p' "${BASH_SOURCE[0]}" + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +case "$(uname -s)" in +Linux*) host=linux ;; +Darwin*) host=macos ;; +MINGW* | MSYS* | CYGWIN*) host=windows ;; +*) + echo "unsupported host: $(uname -s)" >&2 + exit 1 + ;; +esac + +downloads="${work}/downloads" +src="${work}/src" + +if [[ ${clean} -eq 1 ]]; then + rm -rf "${src}" "${prefix}" +fi +mkdir -p "${downloads}" "${src}" "${prefix}" + +# Absolute, forward-slash prefix. MSYS2 hands cl.exe/cmake native paths, so +# translate once here rather than at every use site. +prefix="$(cd "${prefix}" && pwd)" +if [[ ${host} == windows ]]; then + native_prefix="$(cygpath -m "${prefix}")" +else + native_prefix="${prefix}" +fi + +log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } + +# Path in the form the tool expects. FFmpeg's build is MSYS-native and takes +# MSYS paths, but cmake.exe and cl.exe are Windows binaries that cannot read +# them, so anything handed to CMake goes through here first. +npath() { + if [[ ${host} == windows ]]; then + cygpath -m "$1" + else + printf '%s' "$1" + fi +} + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi +} + +# fetch +fetch() { + local url="$1" file="$2" want="$3" path="${downloads}/$2" + + if [[ -f ${path} ]] && [[ "$(sha256_of "${path}")" == "${want}" ]]; then + echo "cached: ${file}" + return + fi + + echo "download: ${url}" + curl -fsSL --retry 3 --retry-delay 2 -o "${path}.tmp" "${url}" + + local got + got="$(sha256_of "${path}.tmp")" + if [[ ${got} != "${want}" ]]; then + rm -f "${path}.tmp" + echo "checksum mismatch for ${file}: got ${got}, expected ${want}" >&2 + exit 1 + fi + mv "${path}.tmp" "${path}" +} + +# extract +extract() { + local file="$1" dest="${src}/$2" + [[ -d ${dest} ]] && return + mkdir -p "${dest}" + tar -xf "${downloads}/${file}" -C "${dest}" --strip-components=1 +} + +# Marker files keep an interrupted or repeated run from redoing finished work. +# They live inside the prefix so CI can cache the prefix alone: restoring it +# without the (much larger) source and object trees still reads as "built". +built() { [[ -f "${prefix}/.built-$1-$2" ]]; } +mark_built() { touch "${prefix}/.built-$1-$2"; } + +export PKG_CONFIG_PATH="${prefix}/lib/pkgconfig:${prefix}/lib64/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" + +# Both libsrt and librist record their mbedTLS dependency in their .pc file as +# an absolute library path. That breaks a static link twice over: +# +# 1. FFmpeg's configure treats anything that is not -lfoo as a compiler flag +# and emits it *before* the -lsrt / -lrist it belongs to, which a +# single-pass linker cannot resolve. +# 2. Whichever mbedTLS the dependency's build system happened to find gets +# baked in by path. librist's meson picked up the host's shared +# libmbedcrypto.so, which would both reintroduce a runtime dependency the +# bundling exists to remove and mismatch the copy libsrt uses. +# +# Rewriting the reference to plain -l flags, appended after the library's own +# entry, fixes the ordering and pins it to the mbedTLS in this prefix. +fix_mbedtls_pc() { + local pc="$1" field="$2" + [[ -f ${pc} ]] || return 0 + sed -i.bak -E \ + -e 's![^[:space:]]*/lib(mbedtls|mbedx509|mbedcrypto|everest|p256m)\.(a|so[0-9.]*|dylib|lib)!!g' \ + -e 's!-l(mbedtls|mbedx509|mbedcrypto)([[:space:]]|$)!\2!g' \ + -e "s!^(${field}:.*)\$!\\1 -L\\\${libdir} -lmbedtls -lmbedx509 -lmbedcrypto!" \ + "${pc}" + rm -f "${pc}.bak" +} + +cmake_common=( + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX="${native_prefix}" + -DCMAKE_PREFIX_PATH="${native_prefix}" + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + -DBUILD_SHARED_LIBS=OFF +) +if [[ ${host} == macos ]]; then + cmake_common+=(-DCMAKE_OSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}") +fi +if [[ ${host} == windows ]]; then + # Ninja avoids the MSBuild/MSYS path translation mess entirely. + cmake_common+=(-G Ninja -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl) +fi + +# ── mbedTLS ────────────────────────────────────────────────────────────────── +# Supplies TLS for FFmpeg (https, rtmps) and AES for libsrt passphrases. +# Apache-2.0, which is compatible with the LGPLv3 FFmpeg build below. +build_mbedtls() { + built mbedtls "${MBEDTLS_VERSION}" && return + log "mbedTLS ${MBEDTLS_VERSION}" + + fetch "https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-${MBEDTLS_VERSION}/mbedtls-${MBEDTLS_VERSION}.tar.bz2" \ + "mbedtls-${MBEDTLS_VERSION}.tar.bz2" "${MBEDTLS_SHA256}" + extract "mbedtls-${MBEDTLS_VERSION}.tar.bz2" "mbedtls-${MBEDTLS_VERSION}" + + cmake -S "$(npath "${src}/mbedtls-${MBEDTLS_VERSION}")" \ + -B "$(npath "${src}/mbedtls-${MBEDTLS_VERSION}/build")" \ + "${cmake_common[@]}" \ + -DENABLE_TESTING=OFF \ + -DENABLE_PROGRAMS=OFF \ + -DUSE_STATIC_MBEDTLS_LIBRARY=ON \ + -DUSE_SHARED_MBEDTLS_LIBRARY=OFF \ + -DMBEDTLS_AS_SUBPROJECT=OFF + cmake --build "$(npath "${src}/mbedtls-${MBEDTLS_VERSION}/build")" --parallel "${jobs}" + cmake --install "$(npath "${src}/mbedtls-${MBEDTLS_VERSION}/build")" + + mark_built mbedtls "${MBEDTLS_VERSION}" +} + +# ── libsrt ─────────────────────────────────────────────────────────────────── +build_srt() { + built srt "${SRT_VERSION}" && return + log "libsrt ${SRT_VERSION}" + + fetch "https://github.com/Haivision/srt/archive/refs/tags/v${SRT_VERSION}.tar.gz" \ + "srt-${SRT_VERSION}.tar.gz" "${SRT_SHA256}" + extract "srt-${SRT_VERSION}.tar.gz" "srt-${SRT_VERSION}" + + cmake -S "$(npath "${src}/srt-${SRT_VERSION}")" \ + -B "$(npath "${src}/srt-${SRT_VERSION}/build")" \ + "${cmake_common[@]}" \ + -DENABLE_SHARED=OFF \ + -DENABLE_STATIC=ON \ + -DENABLE_APPS=OFF \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_UNITTESTS=OFF \ + -DENABLE_ENCRYPTION=ON \ + -DUSE_ENCLIB=mbedtls \ + -DENABLE_CXX11=ON + cmake --build "$(npath "${src}/srt-${SRT_VERSION}/build")" --parallel "${jobs}" + cmake --install "$(npath "${src}/srt-${SRT_VERSION}/build")" + + fix_mbedtls_pc "${prefix}/lib/pkgconfig/srt.pc" "Libs.private" + + mark_built srt "${SRT_VERSION}" +} + +# ── librist ────────────────────────────────────────────────────────────────── +# Meson, not CMake, which is why the deps build needs meson/ninja everywhere. +# lz4 and cJSON use librist's vendored copies so this pulls in no further +# system dependencies; mbedTLS is the one we already built, giving RIST its +# encryption support. +build_librist() { + built librist "${LIBRIST_VERSION}" && return + log "librist ${LIBRIST_VERSION}" + + fetch "https://code.videolan.org/rist/librist/-/archive/v${LIBRIST_VERSION}/librist-v${LIBRIST_VERSION}.tar.gz" \ + "librist-${LIBRIST_VERSION}.tar.gz" "${LIBRIST_SHA256}" + extract "librist-${LIBRIST_VERSION}.tar.gz" "librist-${LIBRIST_VERSION}" + + local rist="${src}/librist-${LIBRIST_VERSION}" + rm -rf "${rist}/build" + ( + cd "${rist}" + # librist locates mbedTLS with cc.find_library, which searches + # the default system paths and would happily bind the host's + # shared libmbedcrypto. Point the compiler at this prefix first + # so it finds the static copy libsrt is already using. + meson setup build \ + --prefix="${native_prefix}" \ + --buildtype=release \ + --default-library=static \ + -Dc_args="-I${native_prefix}/include" \ + -Dc_link_args="-L${native_prefix}/lib" \ + -Dbuilt_tools=false \ + -Dtest=false \ + -Dbuiltin_lz4=true \ + -Dbuiltin_cjson=true \ + -Dbuiltin_mbedtls=false \ + -Duse_mbedtls=true + meson compile -C build + meson install -C build + ) + + fix_mbedtls_pc "${prefix}/lib/pkgconfig/librist.pc" "Libs" + + mark_built librist "${LIBRIST_VERSION}" +} + +# ── nv-codec-headers ───────────────────────────────────────────────────────── +build_nvcodec() { + [[ ${host} == macos ]] && return 0 + built nvcodec "${NVCODEC_VERSION}" && return + log "nv-codec-headers ${NVCODEC_VERSION}" + + fetch "https://github.com/FFmpeg/nv-codec-headers/archive/refs/tags/n${NVCODEC_VERSION}.tar.gz" \ + "nv-codec-headers-${NVCODEC_VERSION}.tar.gz" "${NVCODEC_SHA256}" + extract "nv-codec-headers-${NVCODEC_VERSION}.tar.gz" "nv-codec-headers-${NVCODEC_VERSION}" + + make -C "${src}/nv-codec-headers-${NVCODEC_VERSION}" install PREFIX="${prefix}" + + mark_built nvcodec "${NVCODEC_VERSION}" +} + +# ── FFmpeg ─────────────────────────────────────────────────────────────────── +# +# Decode-only and deliberately narrow: --disable-everything, then only the +# components an IRL ingest actually touches. Keeping the surface small is what +# makes a static link viable size-wise. +# +# LGPLv3 (--enable-version3, no --enable-gpl). Nothing here needs GPL +# components: the plugin decodes, it never encodes. + +FFMPEG_DECODERS="h264,hevc,av1,vp9,aac,aac_latm,aac_fixed,opus,mp3,mp3float,ac3,ac3_fixed,eac3,pcm_s16le,pcm_s16be,pcm_s24le,pcm_s32le,pcm_u8,pcm_f32le,pcm_alaw,pcm_mulaw" +FFMPEG_PARSERS="h264,hevc,av1,vp9,aac,aac_latm,ac3,mpegaudio,opus" +FFMPEG_DEMUXERS="mpegts,mpegtsraw,flv,live_flv,mov,matroska,hls,rtsp,sdp,rtp,h264,hevc,av1,ivf,aac,mp3,ac3,wav,mpjpeg,data" +# SRT and RIST are "libsrt"/"librist" — FFmpeg names each protocol after the +# library. There is no "hls" protocol either; HLS is a demuxer that drives http. +FFMPEG_PROTOCOLS="file,pipe,data,cache,concat,tcp,udp,rtp,http,https,httpproxy,tls,crypto,libsrt,librist,rtmp,rtmpe,rtmps,rtmpt,rtmpte,rtmpts" +FFMPEG_BSFS="h264_mp4toannexb,hevc_mp4toannexb,av1_frame_merge,vp9_superframe,vp9_superframe_split,extract_extradata,aac_adtstoasc,null" + +# Components whose absence would silently degrade the plugin rather than fail +# the build. FFmpeg's configure ignores unknown names in an --enable-* list, so +# a typo here disables a feature without any diagnostic: --enable-protocol=srt +# happily produced a build with CONFIG_LIBSRT=yes and no SRT protocol at all. +FFMPEG_REQUIRED_CONFIG=" +LIBSRT_PROTOCOL +LIBRIST_PROTOCOL +MBEDTLS +TLS_PROTOCOL +RTMP_PROTOCOL +HTTPS_PROTOCOL +MPEGTS_DEMUXER +FLV_DEMUXER +MATROSKA_DEMUXER +H264_DECODER +HEVC_DECODER +AV1_DECODER +VP9_DECODER +AAC_DECODER +OPUS_DECODER +MP3_DECODER +AC3_DECODER +SWSCALE +SWRESAMPLE +" + +build_ffmpeg() { + built ffmpeg "${FFMPEG_VERSION}" && return + log "FFmpeg ${FFMPEG_VERSION}" + + fetch "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" \ + "ffmpeg-${FFMPEG_VERSION}.tar.xz" "${FFMPEG_SHA256}" + extract "ffmpeg-${FFMPEG_VERSION}.tar.xz" "ffmpeg-${FFMPEG_VERSION}" + + local ff="${src}/ffmpeg-${FFMPEG_VERSION}" + local args=( + --prefix="${prefix}" + --disable-shared + --enable-static + --enable-pic + --enable-version3 + --disable-programs + --disable-doc + --disable-avdevice + --disable-avfilter + --disable-everything + --disable-autodetect + --enable-swscale + --enable-swresample + --enable-network + --enable-zlib + --enable-mbedtls + --enable-libsrt + --enable-librist + --enable-decoder="${FFMPEG_DECODERS}" + --enable-parser="${FFMPEG_PARSERS}" + --enable-demuxer="${FFMPEG_DEMUXERS}" + --enable-protocol="${FFMPEG_PROTOCOLS}" + --enable-bsf="${FFMPEG_BSFS}" + --pkg-config-flags=--static + ) + + case "${host}" in + linux) + args+=( + --enable-ffnvcodec + --enable-cuda + --enable-nvdec + ) + # VAAPI links libva at build time, so a plugin built with it + # carries a hard DT_NEEDED on libva.so.2. That is the accepted + # trade for Intel/AMD hardware decode, but it must never be + # dropped silently: a release build without VAAPI would ship + # software-only decode with no visible failure. + if [[ ${IRL_DEPS_DISABLE_VAAPI:-0} == 1 ]]; then + echo "warning: VAAPI disabled by IRL_DEPS_DISABLE_VAAPI — do not ship this build" >&2 + args+=(--enable-hwaccel=h264_nvdec,hevc_nvdec,av1_nvdec,vp9_nvdec) + elif pkg-config --exists libva libva-drm; then + args+=( + --enable-vaapi + --enable-hwaccel=h264_vaapi,hevc_vaapi,av1_vaapi,vp9_vaapi,h264_nvdec,hevc_nvdec,av1_nvdec,vp9_nvdec + ) + else + echo "libva development files not found (install libva-dev)." >&2 + echo "Set IRL_DEPS_DISABLE_VAAPI=1 to build without hardware decode on Intel/AMD." >&2 + exit 1 + fi + ;; + macos) + args+=( + --enable-videotoolbox + --enable-hwaccel=h264_videotoolbox,hevc_videotoolbox,av1_videotoolbox,vp9_videotoolbox + ) + ;; + windows) + args+=( + --toolchain=msvc + --target-os=win64 + --arch=x86_64 + --enable-d3d11va + --enable-dxva2 + --enable-ffnvcodec + --enable-cuda + --enable-nvdec + --enable-hwaccel=h264_d3d11va,h264_d3d11va2,hevc_d3d11va,hevc_d3d11va2,av1_d3d11va,av1_d3d11va2,vp9_d3d11va,vp9_d3d11va2,h264_dxva2,hevc_dxva2,av1_dxva2,vp9_dxva2,h264_nvdec,hevc_nvdec,av1_nvdec,vp9_nvdec + ) + ;; + esac + + (cd "${ff}" && ./configure "${args[@]}") + + # config.mak marks a disabled component by prefixing it with '!'. + local missing=() + local component + for component in ${FFMPEG_REQUIRED_CONFIG}; do + if ! grep -qx "CONFIG_${component}=yes" "${ff}/ffbuild/config.mak"; then + missing+=("${component}") + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + echo "FFmpeg configure did not enable: ${missing[*]}" >&2 + echo "Check the --enable-* component names in ${BASH_SOURCE[0]}." >&2 + exit 1 + fi + + local hwaccels + hwaccels="$(grep -c '^CONFIG_[A-Z0-9_]*_HWACCEL=yes' "${ff}/ffbuild/config.mak" || true)" + echo "FFmpeg configured with ${hwaccels} hwaccel(s)" + if [[ ${hwaccels} -eq 0 ]]; then + echo "no hardware accelerators enabled — hardware decode would be dead" >&2 + exit 1 + fi + + make -C "${ff}" -j"${jobs}" + make -C "${ff}" install + + mark_built ffmpeg "${FFMPEG_VERSION}" +} + +# ── Generated CMake description ────────────────────────────────────────────── +# +# The plugin's CMakeLists includes this file rather than rediscovering the +# stack. Our own libraries go in by absolute path (no -l name resolution to get +# wrong); everything else comes from pkg-config --static, which is the only +# thing that knows the full transitive system-library set for a static FFmpeg. +emit_cmake() { + log "generating irl-deps.cmake" + + local libdir="${prefix}/lib" + local out="${prefix}/irl-deps.cmake" + + # Link order matters for a single-pass static link. + local ordered=(avformat avcodec swscale swresample avutil srt rist mbedtls mbedx509 mbedcrypto) + + local own=() + local name path + for name in "${ordered[@]}"; do + path="" + # FFmpeg's msvc toolchain keeps the .a suffix; CMake-built deps + # use .lib, and libsrt may keep its _static target name. + for candidate in \ + "${libdir}/lib${name}.a" \ + "${libdir}/${name}.lib" \ + "${libdir}/lib${name}.lib" \ + "${libdir}/${name}_static.lib" \ + "${libdir}/lib${name}_static.a"; do + [[ -f ${candidate} ]] && path="${candidate}" && break + done + if [[ -z ${path} ]]; then + echo "missing static library: ${name} (looked in ${libdir})" >&2 + exit 1 + fi + if [[ ${host} == windows ]]; then + path="$(cygpath -m "${path}")" + fi + own+=("${path}") + done + + # Everything pkg-config reports that is not one of ours is a system + # dependency (-lm, -lva, -framework VideoToolbox, ws2_32.lib, ...). + # + # srt is queried explicitly: FFmpeg's .pc files do not propagate it, and + # libsrt is C++, so its .pc is the only place the C++ runtime + # (-lstdc++ / -lc++) appears. + local raw + raw="$(pkg-config --static --libs libavformat libavcodec libswscale libswresample libavutil srt librist 2>/dev/null || true)" + + local system=() + local pending_framework=0 + local tok + for tok in ${raw}; do + if [[ ${pending_framework} -eq 1 ]]; then + system+=("-Wl,-framework,${tok}") + pending_framework=0 + continue + fi + case "${tok}" in + -framework) + pending_framework=1 + ;; + -L* | -Wl,-rpath*) + # Our libraries are absolute paths; search paths that + # point back into the prefix would only add ambiguity. + ;; + -l*) + name="${tok#-l}" + # Already listed by absolute path above. + if [[ " ${ordered[*]} " == *" ${name} "* ]]; then + continue + fi + # The compiler adds these itself, and naming them + # explicitly only creates link-order hazards. + case "${name}" in + c | gcc | gcc_s) continue ;; + esac + # A prefix-local library we did not anticipate: keep it, + # by absolute path, rather than silently dropping it. + if [[ -f "${libdir}/lib${name}.a" ]]; then + own+=("${libdir}/lib${name}.a") + continue + elif [[ -f "${libdir}/${name}.lib" ]]; then + own+=("$(cygpath -m "${libdir}/${name}.lib")") + continue + fi + if [[ ${host} == windows ]]; then + system+=("${name}.lib") + else + system+=("${tok}") + fi + ;; + *.lib) + name="$(basename "${tok}")" + [[ -f "${libdir}/${name}" ]] && continue + system+=("${name}") + ;; + -*) + system+=("${tok}") + ;; + esac + done + + # Order within the system list does not matter (these resolve against + # shared libraries), so collapse the duplicates pkg-config emits. + local deduped=() + for tok in "${system[@]:-}"; do + [[ -z ${tok} ]] && continue + [[ " ${deduped[*]:-} " == *" ${tok} "* ]] && continue + deduped+=("${tok}") + done + system=("${deduped[@]:-}") + + { + echo "# Generated by deps/build-deps.sh — do not edit." + echo "set(IRL_DEPS_FOUND TRUE)" + echo "set(IRL_DEPS_HOST \"${host}\")" + echo "set(IRL_DEPS_FFMPEG_VERSION \"${FFMPEG_VERSION}\")" + echo "set(IRL_DEPS_SRT_VERSION \"${SRT_VERSION}\")" + echo "set(IRL_DEPS_LIBRIST_VERSION \"${LIBRIST_VERSION}\")" + echo "set(IRL_DEPS_MBEDTLS_VERSION \"${MBEDTLS_VERSION}\")" + echo "set(IRL_DEPS_INCLUDE_DIRS \"${native_prefix}/include\")" + printf 'set(IRL_DEPS_STATIC_LIBRARIES\n' + printf ' "%s"\n' "${own[@]}" + printf ')\n' + printf 'set(IRL_DEPS_SYSTEM_LIBRARIES\n' + if [[ ${#system[@]} -gt 0 ]]; then + printf ' "%s"\n' "${system[@]}" + fi + printf ')\n' + } >"${out}" + + echo "wrote ${out}" + cat "${out}" +} + +log "building bundled deps for ${host} into ${prefix}" +build_mbedtls +build_srt +build_librist +build_nvcodec +build_ffmpeg +emit_cmake +log "done" diff --git a/deps/versions.env b/deps/versions.env new file mode 100644 index 0000000..dc3c5e4 --- /dev/null +++ b/deps/versions.env @@ -0,0 +1,26 @@ +# Pinned versions for the bundled media stack. +# +# The plugin links these statically so its media pipeline is independent of +# whatever FFmpeg the host OBS happens to ship. See deps/README.md. +# +# Bumping a version here also requires updating the matching SHA256. + +FFMPEG_VERSION=8.1.2 +FFMPEG_SHA256=464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c + +# obs-deps still pins libsrt 1.5.2 (September 2022). This is the main reason +# the plugin carries its own stack. +SRT_VERSION=1.5.6 +SRT_SHA256=2c4980c2c4cfd142d21b829d939dc51db9c6628af5967fff62fd7290769569c7 + +MBEDTLS_VERSION=3.6.4 +MBEDTLS_SHA256=ec35b18a6c593cf98c3e30db8b98ff93e8940a8c4e690e66b41dfc011d678110 + +# RIST ingest. obs-deps pins 0.2.7; this is the same story as libsrt. +LIBRIST_VERSION=0.2.18 +LIBRIST_SHA256=9a2d16dcdb9fb067b7ba4259a3976ff6f8df9a62dbec7f32f19a0b60ec0c114a + +# Headers only. FFmpeg loads nvcuda/nvcuvid at runtime, so this adds no +# build-time or load-time dependency on a CUDA install. +NVCODEC_VERSION=13.0.19.0 +NVCODEC_SHA256=86d15d1a7c0ac73a0eafdfc57bebfeba7da8264595bf531cf4d8db1c22940116 diff --git a/scripts/verify-plugin.sh b/scripts/verify-plugin.sh new file mode 100644 index 0000000..03430a6 --- /dev/null +++ b/scripts/verify-plugin.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# obs-irl-source — post-link checks for a bundled-stack build (Linux, macOS). +# +# Copyright (C) 2026 Thomas Lekanger +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# The whole point of bundling FFmpeg is that the plugin stops depending on the +# host OBS's copy, and that its own copy stays invisible to the rest of the +# process. Both properties are invisible in a successful compile and easy to +# lose to a stray link flag, so they get asserted here instead. +# +# Usage: scripts/verify-plugin.sh path/to/obs-irl-source.so + +set -euo pipefail + +module="${1:?usage: verify-plugin.sh }" +[[ -f ${module} ]] || { + echo "no such file: ${module}" >&2 + exit 1 +} + +fail=0 +check() { + if [[ $1 -eq 0 ]]; then + printf ' ok %s\n' "$2" + else + printf ' FAIL %s\n' "$2" + fail=1 + fi +} + +echo "verifying ${module}" + +case "$(uname -s)" in +Linux*) + needed="$(readelf -d "${module}" | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p')" + exports="$(nm -D --defined-only "${module}" | awk '$2 != "A" && $2 != "a" {print $3}')" + + echo "${needed}" | grep -qE '^lib(av|sw)' && r=1 || r=0 + check ${r} "no libav*/libsw* in DT_NEEDED (host FFmpeg not required)" + + echo "${needed}" | grep -q '^libva\.so' && r=0 || r=1 + check ${r} "libva present (VAAPI hardware decode compiled in)" + + echo "${exports}" | grep -vqE '^obs_module_' && r=1 || r=0 + check ${r} "exports limited to obs_module_*" + + echo "${exports}" | grep -q '^obs_module_load$' && r=0 || r=1 + check ${r} "obs_module_load exported" + ;; + +Darwin*) + needed="$(otool -L "${module}" | tail -n +2 | awk '{print $1}')" + exports="$(nm -gU "${module}" | awk '{print $3}' | grep -v '^$')" + + echo "${needed}" | grep -qE '/lib(av|sw)[a-z]*\.' && r=1 || r=0 + check ${r} "no libav*/libsw* in load commands (host FFmpeg not required)" + + echo "${exports}" | grep -vqE '^_obs_module_' && r=1 || r=0 + check ${r} "exports limited to _obs_module_*" + + echo "${exports}" | grep -q '^_obs_module_load$' && r=0 || r=1 + check ${r} "_obs_module_load exported" + ;; + +*) + echo "unsupported platform for this script (Windows uses dumpbin in CI)" >&2 + exit 1 + ;; +esac + +echo +if [[ ${fail} -ne 0 ]]; then + echo "verification FAILED" >&2 + echo + echo "dependencies:" + echo "${needed}" | sed 's/^/ /' + echo "exports:" + echo "${exports}" | sed 's/^/ /' + exit 1 +fi +echo "verification passed" From 477126fe2fe25856a9ba6832c3bde06467309045 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 17:16:08 +0200 Subject: [PATCH 02/12] fix(obs-irl-source): mark deps and verify scripts executable --- deps/build-deps.sh | 0 scripts/verify-plugin.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 deps/build-deps.sh mode change 100644 => 100755 scripts/verify-plugin.sh diff --git a/deps/build-deps.sh b/deps/build-deps.sh old mode 100644 new mode 100755 diff --git a/scripts/verify-plugin.sh b/scripts/verify-plugin.sh old mode 100644 new mode 100755 From f5e1ddd468db793f36928eed7c85218ff440fb8b Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 17:27:17 +0200 Subject: [PATCH 03/12] fix(obs-irl-source): unbreak the windows and macos bundled builds Three findings from the first CI run. meson aborts on Windows with "Found GNU link.exe instead of MSVC link.exe": MSYS2 ships a coreutils /usr/bin/link.exe that shadows MSVC's linker on PATH. CMake is unaffected because it resolves the linker next to the compiler, but meson probes PATH. cl.exe and link.exe share a directory, so build-deps.sh now puts that directory first rather than modifying MSYS2. The macOS export check misparsed nm output. nm renders an indirect symbol with an empty address column, which shifts every field and made "(indirect" look like an exported symbol. Use nm -gUj and drop the column parsing; the build was correct, the assertion was not. The macOS artifact carried an absolute /opt/homebrew/opt/jansson/lib/libjansson.4.dylib load command, so the shipped plugin required Homebrew on the user's machine. The plugin never calls jansson; it is only reachable from the OBS headers, so the include path is needed and the library is not. verify-plugin.sh now also rejects any load command outside @rpath, /usr/lib and /System so this cannot regress. --- CMakeLists.txt | 12 +++++------- deps/build-deps.sh | 15 +++++++++++++++ scripts/verify-plugin.sh | 11 ++++++++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 99b2a0f..19a704e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,22 +126,20 @@ else() endif() endif() +# Headers only. The plugin never calls jansson; it is reachable from the OBS +# headers, so the include path is needed and the library is not. Linking it +# baked an absolute /opt/homebrew/opt/jansson/... path into the macOS artifact, +# which made the shipped plugin require Homebrew on the user's machine. find_package(jansson QUIET) if(jansson_FOUND) list(APPEND _OBS_EXTRA_INCLUDE_DIRS "${jansson_INCLUDE_DIR}") - list(APPEND _OBS_EXTRA_LIBS jansson::jansson) else() find_path(JANSSON_FALLBACK_INCLUDE_DIR NAMES jansson.h PATHS /usr/include /usr/local/include ) - find_library(JANSSON_FALLBACK_LIBRARY - NAMES jansson - PATHS /usr/lib /usr/lib/x86_64-linux-gnu /usr/local/lib - ) - if(JANSSON_FALLBACK_INCLUDE_DIR AND JANSSON_FALLBACK_LIBRARY) + if(JANSSON_FALLBACK_INCLUDE_DIR) list(APPEND _OBS_EXTRA_INCLUDE_DIRS "${JANSSON_FALLBACK_INCLUDE_DIR}") - list(APPEND _OBS_EXTRA_LIBS "${JANSSON_FALLBACK_LIBRARY}") endif() endif() diff --git a/deps/build-deps.sh b/deps/build-deps.sh index 6cd85c6..f149468 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -62,6 +62,21 @@ MINGW* | MSYS* | CYGWIN*) host=windows ;; ;; esac +if [[ ${host} == windows ]]; then + # MSYS2 ships a coreutils /usr/bin/link.exe that shadows MSVC's linker. + # CMake is unaffected (it resolves the linker next to the compiler), but + # meson probes PATH and aborts with "Found GNU link.exe instead of MSVC + # link.exe". cl.exe and link.exe live in the same directory, so putting + # that directory first fixes it without modifying the MSYS2 install. + if ! command -v cl >/dev/null 2>&1; then + echo "cl.exe is not on PATH. Run this from an MSVC environment" >&2 + echo "(the CI job uses ilammy/msvc-dev-cmd plus msys2 path-type: inherit)." >&2 + exit 1 + fi + PATH="$(dirname "$(command -v cl)"):${PATH}" + export PATH +fi + downloads="${work}/downloads" src="${work}/src" diff --git a/scripts/verify-plugin.sh b/scripts/verify-plugin.sh index 03430a6..28c312c 100755 --- a/scripts/verify-plugin.sh +++ b/scripts/verify-plugin.sh @@ -52,11 +52,20 @@ Linux*) Darwin*) needed="$(otool -L "${module}" | tail -n +2 | awk '{print $1}')" - exports="$(nm -gU "${module}" | awk '{print $3}' | grep -v '^$')" + # -j prints just the symbol name. Column parsing is not viable here: + # nm renders an indirect symbol with an empty address column, which + # shifts every field and turns "(indirect" into an apparent export. + exports="$(nm -gUj "${module}" | grep -v '^$')" echo "${needed}" | grep -qE '/lib(av|sw)[a-z]*\.' && r=1 || r=0 check ${r} "no libav*/libsw* in load commands (host FFmpeg not required)" + # Everything must resolve on a stock Mac: @rpath for libobs, /usr/lib + # and /System for the OS. An absolute path anywhere else (a Homebrew + # prefix, say) is a dependency the user does not have. + echo "${needed}" | grep -qvE '^(@rpath/|/usr/lib/|/System/Library/)' && r=1 || r=0 + check ${r} "no non-system absolute paths in load commands" + echo "${exports}" | grep -vqE '^_obs_module_' && r=1 || r=0 check ${r} "exports limited to _obs_module_*" From 9e438eb20331c4452bda68700e0a1efcb0636ecc Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 17:36:22 +0200 Subject: [PATCH 04/12] fix(obs-irl-source): give librist an msvc compatible static library name meson names its static library librist.a even under MSVC, but FFmpeg's msvc toolchain rewrites the -lrist in librist.pc into rist.lib, so configure failed with "librist >= 0.2.7 not found using pkg-config" even though the library had just built and installed. Install the .lib name alongside it. Patching the .pc back to an absolute path would also work, but that is the link-ordering trap fix_mbedtls_pc exists to undo. FFmpeg's configure only reports "X not found using pkg-config" on failure, which does not distinguish a missing library from one whose name or link order the toolchain got wrong. Dump the tail of config.log so the next failure of this class is diagnosable from the CI log alone. --- deps/build-deps.sh | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index f149468..270baab 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -284,6 +284,15 @@ build_librist() { fix_mbedtls_pc "${prefix}/lib/pkgconfig/librist.pc" "Libs" + # meson names its static library librist.a even under MSVC, but FFmpeg's + # msvc toolchain rewrites the -lrist from librist.pc into rist.lib. + # Providing that name is less invasive than patching the .pc back to an + # absolute path, which is exactly the link-ordering trap fix_mbedtls_pc + # exists to undo. + if [[ ${host} == windows && -f "${prefix}/lib/librist.a" ]]; then + cp "${prefix}/lib/librist.a" "${prefix}/lib/rist.lib" + fi + mark_built librist "${LIBRIST_VERSION}" } @@ -428,7 +437,16 @@ build_ffmpeg() { ;; esac - (cd "${ff}" && ./configure "${args[@]}") + # configure prints only "X not found using pkg-config" on failure; the + # actual compiler and linker invocation lives in config.log, which is + # the only thing that distinguishes a missing library from one whose + # name or link order the toolchain got wrong. + if ! (cd "${ff}" && ./configure "${args[@]}"); then + echo + echo "---- tail of ffbuild/config.log ----" >&2 + tail -60 "${ff}/ffbuild/config.log" >&2 || true + exit 1 + fi # config.mak marks a disabled component by prefixing it with '!'. local missing=() From 18f97a6aa89c2eb5962ae20c24c70d5749a186b6 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 17:47:34 +0200 Subject: [PATCH 05/12] fix(obs-irl-source): pin the dynamic crt across the windows deps build FFmpeg's configure failed to link against librist and mbedcrypto with unresolved __imp_strdup, __imp_fopen and friends. Not a missing library: a C runtime mismatch. cl.exe defaults to the static CRT when given no flag, while CMake and meson both emit /MD, so FFmpeg silently built /MT against /MD dependencies and the __imp_* import thunks had nothing to resolve to. Mixing them is worse than a link error. /MD and /MT binaries get separate heaps and separate stdio state, so a buffer allocated in one and freed in the other corrupts the process. The plugin DLL is /MD (CMake's default, and what libobs uses), so /MD is the target and all three build systems are now pinned to it explicitly instead of left to their defaults. CMP0091 has to be forced too, or a dependency declaring cmake_minimum_required below 3.15 gets the old policy and ignores the setting. Also two latent bugs in the same area. meson_common is seeded with the options every platform needs so it is never empty, because macOS still ships bash 3.2 where expanding an empty array under set -u aborts. And the system library dedupe no longer uses the "${arr[@]:-}" idiom to assign, which yields a single empty element on an empty array and would have written an empty entry into the CMake link list. --- deps/build-deps.sh | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index 270baab..cbc2dd2 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -190,6 +190,33 @@ fi if [[ ${host} == windows ]]; then # Ninja avoids the MSBuild/MSYS path translation mess entirely. cmake_common+=(-G Ninja -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl) + # Pin the dynamic CRT everywhere. See IRL_MSVC_CRT below; CMP0091 has to + # be forced because a dependency declaring cmake_minimum_required below + # 3.15 gets the old policy and silently ignores the runtime setting. + cmake_common+=( + -DCMAKE_POLICY_DEFAULT_CMP0091=NEW + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL + ) +fi + +# Everything that ends up in one DLL must agree on the C runtime. Mixing them +# is not merely a link error: /MD and /MT binaries get separate heaps and +# separate stdio state, so a buffer allocated in one and freed in the other +# corrupts the process. +# +# cl.exe defaults to /MT when given no flag, while CMake and meson both emit +# /MD. FFmpeg's configure passes nothing, so it silently built /MT against /MD +# dependencies and failed with unresolved __imp_* CRT imports. The plugin DLL +# itself is /MD (CMake's default, and what libobs uses), so /MD is the target +# and all three build systems are pinned to it explicitly rather than left to +# their defaults. +# +# Seeded with the options every platform needs, so the array is never empty: +# macOS still ships bash 3.2, where expanding an empty array under `set -u` is +# an unbound-variable error. +meson_common=(--buildtype=release --default-library=static) +if [[ ${host} == windows ]]; then + meson_common+=(-Db_vscrt=md) fi # ── mbedTLS ────────────────────────────────────────────────────────────────── @@ -267,9 +294,8 @@ build_librist() { # shared libmbedcrypto. Point the compiler at this prefix first # so it finds the static copy libsrt is already using. meson setup build \ + "${meson_common[@]}" \ --prefix="${native_prefix}" \ - --buildtype=release \ - --default-library=static \ -Dc_args="-I${native_prefix}/include" \ -Dc_link_args="-L${native_prefix}/lib" \ -Dbuilt_tools=false \ @@ -427,6 +453,9 @@ build_ffmpeg() { --toolchain=msvc --target-os=win64 --arch=x86_64 + # cl.exe defaults to the static CRT; everything else here + # is /MD. See the meson_common comment above. + --extra-cflags=-MD --enable-d3d11va --enable-dxva2 --enable-ffnvcodec @@ -586,7 +615,13 @@ emit_cmake() { [[ " ${deduped[*]:-} " == *" ${tok} "* ]] && continue deduped+=("${tok}") done - system=("${deduped[@]:-}") + # Not `system=("${deduped[@]:-}")`: on an empty array that idiom yields a + # single empty element, and an empty entry in the CMake link list is an + # error rather than a no-op. + system=() + if [[ ${#deduped[@]} -gt 0 ]]; then + system=("${deduped[@]}") + fi { echo "# Generated by deps/build-deps.sh — do not edit." From 49ae92579d211bf6afdacac42ec5ba431a107136 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 18:10:03 +0200 Subject: [PATCH 06/12] fix(obs-irl-source): provide msvc library names for libsrt and librist libsrt builds srt_static.lib and meson builds librist.a, but FFmpeg's msvc toolchain rewrites the -lsrt and -lrist it reads from their .pc files into srt.lib and rist.lib. The mismatch surfaces as "not found using pkg-config" from a library that installed successfully seconds earlier, which is a misleading enough error to be worth handling once rather than per library. ensure_msvc_lib_name replaces the librist specific copy added earlier and now covers both, failing loudly with a directory listing if it cannot find any candidate rather than leaving configure to misreport it. --- deps/build-deps.sh | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index cbc2dd2..6d93ea3 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -166,6 +166,39 @@ export PKG_CONFIG_PATH="${prefix}/lib/pkgconfig:${prefix}/lib64/pkgconfig${PKG_C # # Rewriting the reference to plain -l flags, appended after the library's own # entry, fixes the ordering and pins it to the mbedTLS in this prefix. +# FFmpeg's msvc toolchain rewrites the -lfoo it reads from a .pc file into +# foo.lib, but neither CMake nor meson necessarily names its static output that +# way: libsrt builds srt_static.lib, meson builds librist.a. The mismatch shows +# up as "X not found using pkg-config" from a library that just installed +# successfully. Provide the name the linker will ask for. +# +# Patching the .pc to an absolute path would also resolve it, and would also +# reintroduce the link-ordering trap fix_mbedtls_pc exists to undo. +ensure_msvc_lib_name() { + local want="$1" + [[ ${host} == windows ]] || return 0 + + local dst="${prefix}/lib/${want}.lib" + [[ -f ${dst} ]] && return 0 + + local cand + for cand in \ + "${prefix}/lib/${want}_static.lib" \ + "${prefix}/lib/lib${want}.a" \ + "${prefix}/lib/lib${want}.lib" \ + "${prefix}/lib/${want}.a"; do + if [[ -f ${cand} ]]; then + cp "${cand}" "${dst}" + echo "provided $(basename "${dst}") from $(basename "${cand}")" + return 0 + fi + done + + echo "no static library found for -l${want} in ${prefix}/lib" >&2 + ls -1 "${prefix}/lib" >&2 || true + exit 1 +} + fix_mbedtls_pc() { local pc="$1" field="$2" [[ -f ${pc} ]] || return 0 @@ -268,6 +301,7 @@ build_srt() { cmake --install "$(npath "${src}/srt-${SRT_VERSION}/build")" fix_mbedtls_pc "${prefix}/lib/pkgconfig/srt.pc" "Libs.private" + ensure_msvc_lib_name srt mark_built srt "${SRT_VERSION}" } @@ -310,14 +344,7 @@ build_librist() { fix_mbedtls_pc "${prefix}/lib/pkgconfig/librist.pc" "Libs" - # meson names its static library librist.a even under MSVC, but FFmpeg's - # msvc toolchain rewrites the -lrist from librist.pc into rist.lib. - # Providing that name is less invasive than patching the .pc back to an - # absolute path, which is exactly the link-ordering trap fix_mbedtls_pc - # exists to undo. - if [[ ${host} == windows && -f "${prefix}/lib/librist.a" ]]; then - cp "${prefix}/lib/librist.a" "${prefix}/lib/rist.lib" - fi + ensure_msvc_lib_name rist mark_built librist "${LIBRIST_VERSION}" } From 75d5e8edb4a642feeb5a5c1e3dbf1a97fc3f5222 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 18:19:28 +0200 Subject: [PATCH 07/12] fix(obs-irl-source): declare mbedtls's bcrypt dependency on windows mbedTLS draws entropy from BCryptGenRandom on Windows but its generated .pc files do not declare bcrypt, so a static link through pkg-config fails on that single symbol. FFmpeg reports it as "ERROR: mbedTLS not found", which points at the wrong thing entirely. Also give configure an explicit include and library search path. Not every probe goes through pkg-config; when FFmpeg falls back to linking a bare -lmbedtls it has no search path and fails with "cannot open input file mbedtls.lib" for a library that is sitting right there. --- deps/build-deps.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index 6d93ea3..a362742 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -274,6 +274,21 @@ build_mbedtls() { cmake --build "$(npath "${src}/mbedtls-${MBEDTLS_VERSION}/build")" --parallel "${jobs}" cmake --install "$(npath "${src}/mbedtls-${MBEDTLS_VERSION}/build")" + # mbedTLS draws entropy from BCryptGenRandom on Windows but its generated + # .pc files do not declare bcrypt, so anything linking it statically via + # pkg-config fails on that one symbol. FFmpeg reports it as the wholly + # misleading "ERROR: mbedTLS not found". + if [[ ${host} == windows ]]; then + local pc + for pc in "${prefix}/lib/pkgconfig/mbedcrypto.pc" \ + "${prefix}/lib/pkgconfig/mbedtls.pc"; do + [[ -f ${pc} ]] || continue + grep -q -- '-lbcrypt' "${pc}" && continue + sed -i.bak -E 's!^(Libs:.*)$!\1 -lbcrypt!' "${pc}" + rm -f "${pc}.bak" + done + fi + mark_built mbedtls "${MBEDTLS_VERSION}" } @@ -483,6 +498,11 @@ build_ffmpeg() { # cl.exe defaults to the static CRT; everything else here # is /MD. See the meson_common comment above. --extra-cflags=-MD + # Not every configure probe goes through pkg-config; the + # fallbacks link a bare -lmbedtls with no search path and + # fail with "cannot open input file mbedtls.lib". + --extra-cflags=-I"${native_prefix}/include" + --extra-ldflags=-libpath:"${native_prefix}/lib" --enable-d3d11va --enable-dxva2 --enable-ffnvcodec From f15322cd6f758f457911a5940af406e4387e1590 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 18:31:00 +0200 Subject: [PATCH 08/12] build(obs-irl-source): build zlib on windows FFmpeg's configure reached the zlib check for the first time and failed with "zlib requested but not found". Linux and macOS supply zlib as a system library; Windows has none. Building it is preferable to dropping --enable-zlib on Windows: a codec or container feature that silently differs per platform is a worse outcome than one more short build step. ZLIB joins the asserted component list so it cannot go missing quietly either. ensure_msvc_lib_name now takes extra candidate basenames, since zlib's CMake names its static output zlibstatic while FFmpeg asks for z.lib. The stdbit.h error just above the failure in the log is unrelated: that is FFmpeg probing for a C23 header MSVC does not have, and it falls back. --- deps/build-deps.sh | 57 +++++++++++++++++++++++++++++++++++++--------- deps/versions.env | 4 ++++ 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index a362742..763be56 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -174,24 +174,29 @@ export PKG_CONFIG_PATH="${prefix}/lib/pkgconfig:${prefix}/lib64/pkgconfig${PKG_C # # Patching the .pc to an absolute path would also resolve it, and would also # reintroduce the link-ordering trap fix_mbedtls_pc exists to undo. +# ensure_msvc_lib_name <-l name> [extra basename ...] ensure_msvc_lib_name() { local want="$1" + shift [[ ${host} == windows ]] || return 0 local dst="${prefix}/lib/${want}.lib" [[ -f ${dst} ]] && return 0 - local cand - for cand in \ - "${prefix}/lib/${want}_static.lib" \ - "${prefix}/lib/lib${want}.a" \ - "${prefix}/lib/lib${want}.lib" \ - "${prefix}/lib/${want}.a"; do - if [[ -f ${cand} ]]; then - cp "${cand}" "${dst}" - echo "provided $(basename "${dst}") from $(basename "${cand}")" - return 0 - fi + local cand base + local candidates=( + "${want}_static" + "lib${want}" + "$@" + ) + for base in "${candidates[@]}"; do + for cand in "${prefix}/lib/${base}.lib" "${prefix}/lib/${base}.a"; do + if [[ -f ${cand} ]]; then + cp "${cand}" "${dst}" + echo "provided $(basename "${dst}") from $(basename "${cand}")" + return 0 + fi + done done echo "no static library found for -l${want} in ${prefix}/lib" >&2 @@ -252,6 +257,34 @@ if [[ ${host} == windows ]]; then meson_common+=(-Db_vscrt=md) fi +# ── zlib ───────────────────────────────────────────────────────────────────── +# Windows only. Linux and macOS supply zlib as a system library, which the +# builds there already link. Building it here rather than dropping +# --enable-zlib on Windows keeps the FFmpeg feature set identical on all three +# platforms; a capability that silently differs per platform is worse than a +# short extra build step. +build_zlib() { + [[ ${host} == windows ]] || return 0 + built zlib "${ZLIB_VERSION}" && return + log "zlib ${ZLIB_VERSION}" + + fetch "https://github.com/madler/zlib/releases/download/v${ZLIB_VERSION}/zlib-${ZLIB_VERSION}.tar.gz" \ + "zlib-${ZLIB_VERSION}.tar.gz" "${ZLIB_SHA256}" + extract "zlib-${ZLIB_VERSION}.tar.gz" "zlib-${ZLIB_VERSION}" + + cmake -S "$(npath "${src}/zlib-${ZLIB_VERSION}")" \ + -B "$(npath "${src}/zlib-${ZLIB_VERSION}/build")" \ + "${cmake_common[@]}" \ + -DZLIB_BUILD_EXAMPLES=OFF + cmake --build "$(npath "${src}/zlib-${ZLIB_VERSION}/build")" --parallel "${jobs}" + cmake --install "$(npath "${src}/zlib-${ZLIB_VERSION}/build")" + + # zlib's CMake calls its static output zlibstatic; FFmpeg asks for z.lib. + ensure_msvc_lib_name z zlibstatic zlib + + mark_built zlib "${ZLIB_VERSION}" +} + # ── mbedTLS ────────────────────────────────────────────────────────────────── # Supplies TLS for FFmpeg (https, rtmps) and AES for libsrt passphrases. # Apache-2.0, which is compatible with the LGPLv3 FFmpeg build below. @@ -404,6 +437,7 @@ FFMPEG_REQUIRED_CONFIG=" LIBSRT_PROTOCOL LIBRIST_PROTOCOL MBEDTLS +ZLIB TLS_PROTOCOL RTMP_PROTOCOL HTTPS_PROTOCOL @@ -694,6 +728,7 @@ emit_cmake() { } log "building bundled deps for ${host} into ${prefix}" +build_zlib build_mbedtls build_srt build_librist diff --git a/deps/versions.env b/deps/versions.env index dc3c5e4..202cf95 100644 --- a/deps/versions.env +++ b/deps/versions.env @@ -5,6 +5,10 @@ # # Bumping a version here also requires updating the matching SHA256. +# Windows only. Linux and macOS use the system zlib; Windows has none. +ZLIB_VERSION=1.3.1 +ZLIB_SHA256=9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23 + FFMPEG_VERSION=8.1.2 FFMPEG_SHA256=464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c From 0d854930edcb8f73429a500c5f017dbecc48cf8a Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 18:46:28 +0200 Subject: [PATCH 09/12] fix(obs-irl-source): stop zlib's zconf.h including unistd.h under msvc FFmpeg now configures on Windows (16 hwaccels, every asserted component present) and fails in its own compile instead: zconf.h(486): fatal error C1083: Cannot open include file: 'unistd.h' zconf.h.cmakein still carries an autoconf-era block that defines Z_HAVE_UNISTD_H from #ifdef HAVE_UNISTD_H. CMake probes for unistd.h, correctly does not find it under MSVC, and leaves Z_HAVE_UNISTD_H undefined earlier in the file, but that block then defines it anyway: FFmpeg's config.h contains "#define HAVE_UNISTD_H 0" and #ifdef is true for a value of zero. The header goes on to include a file MSVC does not have. zlib's own ./configure rewrites this line for exactly this reason; its CMake path does not, so patch it after install. The two remaining paths that could redefine Z_HAVE_UNISTD_H are gated on __WATCOMC__ and on _LARGEFILE64_SOURCE && !_WIN32, neither of which can fire here. --- deps/build-deps.sh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index 763be56..c91530d 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -282,6 +282,33 @@ build_zlib() { # zlib's CMake calls its static output zlibstatic; FFmpeg asks for z.lib. ensure_msvc_lib_name z zlibstatic zlib + # zconf.h.cmakein still carries an autoconf-era block: + # + # #ifdef HAVE_UNISTD_H + # # define Z_HAVE_UNISTD_H + # #endif + # + # CMake probes for unistd.h, does not find it under MSVC, and correctly + # leaves Z_HAVE_UNISTD_H undefined earlier in the file. That block then + # defines it anyway, because FFmpeg's config.h contains + # "#define HAVE_UNISTD_H 0" and #ifdef is true for a value of zero. The + # header goes on to include , which MSVC does not have, and + # every FFmpeg source that touches zlib fails to compile. + # + # zlib's own ./configure rewrites this line for exactly this reason. The + # CMake path does not, so do it here. + local zconf="${prefix}/include/zconf.h" + if [[ -f ${zconf} ]]; then + sed -i.bak \ + 's!^#ifdef HAVE_UNISTD_H.*!#if 0 /* patched: MSVC has no unistd.h, and FFmpeg defines HAVE_UNISTD_H to 0 */!' \ + "${zconf}" + rm -f "${zconf}.bak" + if grep -q '^#ifdef HAVE_UNISTD_H' "${zconf}"; then + echo "failed to patch HAVE_UNISTD_H out of ${zconf}" >&2 + exit 1 + fi + fi + mark_built zlib "${ZLIB_VERSION}" } From 1a1146122e30c2f7cb8eaa19af19489ef016fc65 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 19:07:24 +0200 Subject: [PATCH 10/12] fix(obs-irl-source): correct msvc flag parsing in the generated link line The deps build now completes on Windows, FFmpeg included, and the plugin link exposed two problems in emit_cmake. LNK1104: cannot open file 'ibpath:...lib.lib.lib'. The -l* arm is greedy enough to read the -libpath:C:/x that --extra-ldflags puts into FFmpeg's .pc files as a library named "ibpath:C:/x". Match the MSVC search-path spellings alongside -L, above that arm. zlib reached neither list. The Windows .pc chain never surfaces it as a -l flag, so it was dropped entirely and would have surfaced as unresolved inflate/deflate once the link got that far. It is ours only on Windows, so list it explicitly there, after its consumers. Static library candidates are now probed .lib first so a Windows link uses one naming convention rather than mixing librist.a with the rist.lib beside it. On Linux only the lib*.a form exists, so the order changes nothing. --- deps/build-deps.sh | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index c91530d..0509825 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -628,17 +628,29 @@ emit_cmake() { # Link order matters for a single-pass static link. local ordered=(avformat avcodec swscale swresample avutil srt rist mbedtls mbedx509 mbedcrypto) + # zlib is ours only on Windows; Linux and macOS pull the system one in + # through pkg-config as -lz. Listing it explicitly guarantees it reaches + # the link line: the Windows .pc chain does not surface it as a -l flag, + # so it would otherwise be dropped and show up as unresolved inflate and + # deflate at plugin link time. It goes last, after its consumers. + if [[ ${host} == windows ]]; then + ordered+=(z) + fi + local own=() local name path for name in "${ordered[@]}"; do path="" - # FFmpeg's msvc toolchain keeps the .a suffix; CMake-built deps - # use .lib, and libsrt may keep its _static target name. + # .lib first so a Windows build picks the name the MSVC linker + # expects: meson emits librist.a even under MSVC, and both that + # and the rist.lib beside it would link, but mixing conventions + # in one link line is needless room for surprise. On Linux only + # the lib*.a form exists, so the order costs nothing there. for candidate in \ - "${libdir}/lib${name}.a" \ "${libdir}/${name}.lib" \ "${libdir}/lib${name}.lib" \ "${libdir}/${name}_static.lib" \ + "${libdir}/lib${name}.a" \ "${libdir}/lib${name}_static.a"; do [[ -f ${candidate} ]] && path="${candidate}" && break done @@ -674,9 +686,14 @@ emit_cmake() { -framework) pending_framework=1 ;; - -L* | -Wl,-rpath*) + -L* | -Wl,-rpath* | -libpath:* | -LIBPATH:* | /libpath:* | /LIBPATH:*) # Our libraries are absolute paths; search paths that # point back into the prefix would only add ambiguity. + # + # The MSVC spellings must be matched here rather than + # left to the -l* arm below, which is greedy enough to + # read "-libpath:C:/x" as a library named "ibpath:C:/x" + # and emit "ibpath:C:/x.lib". ;; -l*) name="${tok#-l}" @@ -691,11 +708,12 @@ emit_cmake() { esac # A prefix-local library we did not anticipate: keep it, # by absolute path, rather than silently dropping it. - if [[ -f "${libdir}/lib${name}.a" ]]; then - own+=("${libdir}/lib${name}.a") + # .lib first, matching the candidate order above. + if [[ -f "${libdir}/${name}.lib" ]]; then + own+=("$(npath "${libdir}/${name}.lib")") continue - elif [[ -f "${libdir}/${name}.lib" ]]; then - own+=("$(cygpath -m "${libdir}/${name}.lib")") + elif [[ -f "${libdir}/lib${name}.a" ]]; then + own+=("${libdir}/lib${name}.a") continue fi if [[ ${host} == windows ]]; then From 6861d14fea4be6866000ed6e041cef5f6ca2c240 Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 19:22:56 +0200 Subject: [PATCH 11/12] fix(obs-irl-source): patch ffmpeg's missing gmtime_r include for msvc The plugin link came down to a single unresolved external: avformat.lib(tls_mbedtls.o): unresolved external symbol gmtime_r referenced in function mbedtls_gen_x509_cert Upstream bug rather than anything about this build. tls_mbedtls.c calls gmtime_r without including libavutil/time_internal.h, which is where FFmpeg keeps the ff_gmtime_r fallback for platforms lacking the POSIX function. Every target except MSVC resolves the real gmtime_r, so the missing include never shows up there. Applied on all platforms rather than gated on Windows: the include is a no-op where HAVE_GMTIME_R is set, and one source state everywhere beats a platform-specific divergence. The patch verifies its own anchor and fails loudly if a future FFmpeg bump moves it. --- deps/build-deps.sh | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/deps/build-deps.sh b/deps/build-deps.sh index 0509825..70ec620 100755 --- a/deps/build-deps.sh +++ b/deps/build-deps.sh @@ -492,6 +492,31 @@ build_ffmpeg() { extract "ffmpeg-${FFMPEG_VERSION}.tar.xz" "ffmpeg-${FFMPEG_VERSION}" local ff="${src}/ffmpeg-${FFMPEG_VERSION}" + + # Upstream bug. libavformat/tls_mbedtls.c calls gmtime_r without + # including libavutil/time_internal.h, which is where FFmpeg keeps the + # ff_gmtime_r fallback for platforms that lack the POSIX function. + # Everywhere except MSVC the real gmtime_r resolves and the missing + # include is invisible, so it surfaces only as a lone unresolved + # external when the plugin links. + # + # Applied on every platform: the include is a no-op where HAVE_GMTIME_R + # is set, and keeping one source state across platforms beats carrying a + # Windows-only divergence. + local tls="${ff}/libavformat/tls_mbedtls.c" + if [[ -f ${tls} ]] && ! grep -q 'libavutil/time_internal.h' "${tls}"; then + sed -i.bak \ + 's!^#include "libavutil/random_seed.h"!&\n#include "libavutil/time_internal.h"!' \ + "${tls}" + rm -f "${tls}.bak" + if ! grep -q 'libavutil/time_internal.h' "${tls}"; then + echo "failed to patch gmtime_r include into ${tls}" >&2 + echo "check whether the anchor include still exists upstream." >&2 + exit 1 + fi + echo "patched gmtime_r include into libavformat/tls_mbedtls.c" + fi + local args=( --prefix="${prefix}" --disable-shared From f8eae553a1fcec449e36f3f68187d18cc0faec6b Mon Sep 17 00:00:00 2001 From: datagutt Date: Thu, 23 Jul 2026 19:54:25 +0200 Subject: [PATCH 12/12] build(obs-irl-source): ship the windows pdb in the ci artifact A field crash on Windows resolved obs-irl-source.dll frames only to hex offsets because the plugin's PDB was never uploaded. Ship it alongside the DLL in the CI artifact (not the release zip). OBS's crash handler reads a PDB sitting next to the module, so the next crash log names the faulting function instead of a base+offset. --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acf3d68..c00be2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -222,6 +222,10 @@ jobs: path: | build/RelWithDebInfo/obs-irl-source.dll build/RelWithDebInfo/w32-pthreads.dll + # PDB ships in the CI artifact (not the release zip) so a crash + # log resolves obs-irl-source.dll frames to function names. + # OBS's crash handler reads it if it sits next to the DLL. + build/RelWithDebInfo/obs-irl-source.pdb macos-arm64: runs-on: macos-15