Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/actions/static-ffmpeg/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Static FFmpeg (decode-only)
description: >-
Builds a minimal static FFmpeg — H.264/HEVC decode plus the platform's
GPU decode API, nothing else — into ffmpeg-static/ at the workspace
root, cached between runs. Plugin binaries link this instead of the
FFmpeg inside OBS: FFmpeg's library names change with its major
version (avcodec-61.dll, libavcodec.so.60, ...), so a plugin linked
against OBS's copy stops loading the moment an OBS update ships a
newer FFmpeg (issues #65, #91, #93). With the decoder built in, one
plugin release keeps working across OBS updates.

inputs:
version:
description: FFmpeg release to build
required: false
default: "7.1"

runs:
using: composite
steps:
# hashFiles of this action: editing any configure flag or build step
# invalidates the cache automatically — no manual key bump to forget.
- name: Cache static FFmpeg
id: cache
uses: actions/cache@v6
with:
path: ffmpeg-static
key: ffmpeg-static-${{ runner.os }}-${{ inputs.version }}-${{ hashFiles('.github/actions/static-ffmpeg/**') }}

# Unconditional (not gated on the cache): libva-dev is needed again
# later when the plugin links the cached static libavutil, whose
# VAAPI hwcontext references libva symbols.
- name: Install Linux build dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y nasm libva-dev libdrm-dev

# H.264/HEVC decode + VAAPI, nothing else; the only FFmpeg-related
# runtime needs left are libva/libva-drm, whose soname has been
# stable since 2017 and which every VAAPI-capable system ships.
- name: Build (Linux)
if: runner.os == 'Linux' && steps.cache.outputs.cache-hit != 'true'
shell: bash
env:
FFMPEG_VERSION: ${{ inputs.version }}
run: |
curl -LO "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz"
tar xf "ffmpeg-${FFMPEG_VERSION}.tar.xz"
cd "ffmpeg-${FFMPEG_VERSION}"
./configure --prefix="$GITHUB_WORKSPACE/ffmpeg-static" \
--disable-everything --disable-programs --disable-doc \
--disable-avformat --disable-avfilter --disable-swscale \
--disable-swresample --disable-avdevice --disable-network \
--disable-debug --disable-autodetect \
--enable-decoder=h264,hevc --enable-parser=h264,hevc \
--enable-vaapi --enable-hwaccel=h264_vaapi,hevc_vaapi \
--enable-pic --disable-shared --enable-static \
|| { tail -n 50 ffbuild/config.log; exit 1; }
make -j"$(nproc)"
make install

# Same shape with VideoToolbox, built once per slice and merged with
# lipo to match the universal plugin. The linking side of this lives
# in CMakeLists.txt: the plugin links the CoreMedia/VideoToolbox
# frameworks the static hwaccel resolves against.
- name: Build (macOS, universal)
if: runner.os == 'macOS' && steps.cache.outputs.cache-hit != 'true'
shell: bash
env:
FFMPEG_VERSION: ${{ inputs.version }}
run: |
brew install nasm
curl -LO "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz"
tar xf "ffmpeg-${FFMPEG_VERSION}.tar.xz"
# --enable-cross-compile on the native slice too: it only turns
# off configure-time runtime probing, and keeps both invocations
# identical.
for arch in arm64 x86_64; do
mkdir -p "ffmpeg-build-$arch"
(cd "ffmpeg-build-$arch" && \
"../ffmpeg-${FFMPEG_VERSION}/configure" \
--prefix="$GITHUB_WORKSPACE/ffmpeg-arch-$arch" \
--enable-cross-compile --target-os=darwin \
--arch="$arch" --cc="clang -arch $arch" \
--extra-cflags="-mmacosx-version-min=13.0" \
--extra-ldflags="-mmacosx-version-min=13.0" \
--disable-everything --disable-programs --disable-doc \
--disable-avformat --disable-avfilter --disable-swscale \
--disable-swresample --disable-avdevice --disable-network \
--disable-debug --disable-autodetect \
--enable-decoder=h264,hevc --enable-parser=h264,hevc \
--enable-videotoolbox \
--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox \
--enable-pic --disable-shared --enable-static \
|| { tail -n 50 ffbuild/config.log; exit 1; } && \
make -j"$(sysctl -n hw.ncpu)" && make install)
done
# One header set is enough: avconfig.h is identical for both
# slices (same endianness and word size). No pkgconfig dir on
# purpose — the per-arch .pc files carry the wrong prefix, and
# the workflows pass explicit library paths instead.
mkdir -p ffmpeg-static/lib
cp -R ffmpeg-arch-arm64/include ffmpeg-static/include
for lib in libavcodec libavutil; do
lipo -create "ffmpeg-arch-arm64/lib/$lib.a" \
"ffmpeg-arch-x86_64/lib/$lib.a" \
-output "ffmpeg-static/lib/$lib.a"
done

# FFmpeg's build system is a shell script, but the compiler must be
# MSVC (cl.exe) so the archives link into the plugin's Visual Studio
# build: import the vcvars64 environment into this process, then run
# the build under the runner's stock MSYS2 with that environment
# inherited (build-windows.sh).
- name: Build (Windows)
if: runner.os == 'Windows' && steps.cache.outputs.cache-hit != 'true'
shell: pwsh
env:
FFMPEG_VERSION: ${{ inputs.version }}
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vs = & $vswhere -latest -products * `
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
-property installationPath
cmd /s /c "`"$vs\VC\Auxiliary\Build\vcvars64.bat`" > nul 2>&1 && set" |
ForEach-Object {
$name, $value = $_ -split '=', 2
if ($name -and $value) { Set-Item -Path "env:$name" -Value $value }
}
$env:MSYS2_PATH_TYPE = "inherit"
$env:CHERE_INVOKING = "1"
$script = "${env:GITHUB_ACTION_PATH}\build-windows.sh" -replace '\\', '/'
C:\msys64\usr\bin\bash -l $script
if ($LASTEXITCODE -ne 0) { throw "static FFmpeg build failed" }
58 changes: 58 additions & 0 deletions .github/actions/static-ffmpeg/build-windows.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Runs under the runner's stock MSYS2 with the MSVC (vcvars64)
# environment inherited — see action.yml. Produces static
# avcodec.lib/avutil.lib in $GITHUB_WORKSPACE/ffmpeg-static.
set -euxo pipefail

# MSYS2's coreutils /usr/bin/link shadows MSVC's link.exe, and FFmpeg's
# configure --toolchain=msvc needs the latter.
rm -f /usr/bin/link.exe

pacman -Sy --noconfirm --needed make nasm diffutils xz

curl -LO "https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz"
tar xf "ffmpeg-${FFMPEG_VERSION}.tar.xz"
cd "ffmpeg-${FFMPEG_VERSION}"

prefix="$(cygpath -u "$GITHUB_WORKSPACE")/ffmpeg-static"

# Same minimal decode-only shape as the Linux/macOS builds, with the
# Windows GPU decode APIs (D3D11VA + the DXVA2 fallback) in place of
# VAAPI. The *_d3d11va2 hwaccels are the AV_HWDEVICE_TYPE_D3D11VA +
# hw_device_ctx path the decoder actually uses; the *_d3d11va ones are
# the legacy API, enabled alongside for completeness. -MD matches the
# /MD CRT of the plugin's Release build. The linking side lives in
# CMakeLists.txt: bcrypt/ole32/user32 resolve the static libavutil's
# RNG and DXVA2 device setup.
./configure --prefix="$prefix" --toolchain=msvc \
--extra-cflags="-MD" \
--disable-everything --disable-programs --disable-doc \
--disable-avformat --disable-avfilter --disable-swscale \
--disable-swresample --disable-avdevice --disable-network \
--disable-debug --disable-autodetect \
--enable-decoder=h264,hevc --enable-parser=h264,hevc \
--enable-d3d11va --enable-dxva2 \
--enable-hwaccel=h264_d3d11va,h264_d3d11va2,h264_dxva2 \
--enable-hwaccel=hevc_d3d11va,hevc_d3d11va2,hevc_dxva2 \
--disable-shared --enable-static \
|| { tail -n 50 ffbuild/config.log; exit 1; }

make -j"$(nproc)"
make install

# The archive names configure emits have varied (libavcodec.a vs
# avcodec.lib); pin one canonical name the workflows can rely on, and
# fail loudly if neither shows up.
cd "$prefix/lib"
for lib in avcodec avutil; do
if [ ! -f "$lib.lib" ]; then
for cand in "lib$lib.a" "$lib.a" "lib$lib.lib"; do
if [ -f "$cand" ]; then
mv "$cand" "$lib.lib"
break
fi
done
fi
[ -f "$lib.lib" ]
done
ls -l
89 changes: 74 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ jobs:
git fetch --depth=1 origin "$BASE"
CHANGED=$(git diff --name-only "$BASE" HEAD)
# A workflow edit rebuilds everything: it may change any job.
# .github/actions/ is in the plugin set: the static-ffmpeg
# composite action feeds the plugin builds.
if echo "$CHANGED" | \
grep -qE '^(obs-plugin/|installer/|\.github/workflows/build\.yml)'; then
grep -qE '^(obs-plugin/|installer/|\.github/workflows/build\.yml|\.github/actions/)'; then
echo "plugin=true" >> "$GITHUB_OUTPUT"
else
echo "plugin=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -130,9 +132,12 @@ jobs:
if: needs.changes.outputs.plugin == 'true'
runs-on: windows-2022
env:
# Keep in sync with the OBS release users run: the plugin links the
# FFmpeg DLLs from this deps bundle, and OBS must ship the same ones.
# OBS 32.1.2 pins obs-deps 2025-08-23 (see its CMakePresets.json).
# Pins the libobs the plugin compiles/links against, and the Qt6
# bundle for the status-bar/dock UI. FFmpeg deliberately does NOT
# come from this bundle: the plugin links its own static
# decode-only FFmpeg (.github/actions/static-ffmpeg), so an OBS
# update swapping its bundled FFmpeg DLLs can't break plugin
# loading (issues #91, #93).
OBS_REF: "32.1.2"
DEPS_TAG: "2025-08-23"
steps:
Expand All @@ -148,7 +153,7 @@ jobs:
ref: ${{ env.OBS_REF }}
path: obs-studio

- name: Download OBS prebuilt dependencies (FFmpeg, Qt6)
- name: Download OBS prebuilt dependencies (for libobs; Qt6)
run: |
curl -L -o deps.zip "https://github.com/obsproject/obs-deps/releases/download/${{ env.DEPS_TAG }}/windows-deps-${{ env.DEPS_TAG }}-x64.zip"
7z x deps.zip -oobs-deps
Expand All @@ -159,6 +164,9 @@ jobs:
curl -L -o deps-qt6.zip "https://github.com/obsproject/obs-deps/releases/download/${{ env.DEPS_TAG }}/windows-deps-qt6-${{ env.DEPS_TAG }}-x64.zip"
7z x -y deps-qt6.zip -oobs-deps

- name: Build static FFmpeg (H.264/HEVC decode only)
uses: ./.github/actions/static-ffmpeg

- name: Cache libobs build
id: cache-libobs
uses: actions/cache@v6
Expand All @@ -177,18 +185,43 @@ jobs:
-DENABLE_AJA=OFF
cmake --build obs-build --target libobs --config Release

# DISABLE_FIND_PACKAGE_PkgConfig: if a pkg-config on the runner
# ever resolved FFmpeg from the obs-deps bundle, the DLL would
# silently regress to importing OBS's avcodec-NN.dll; force the
# explicit static paths instead. The guard below backstops it.
- name: Build plugin
run: |
cmake -S obs-plugin -B plugin-build -G "Visual Studio 17 2022" -A x64 `
-DCMAKE_PREFIX_PATH="${{ github.workspace }}\obs-deps" `
-DLIBOBS_INCLUDE_DIR="${{ github.workspace }}\obs-studio\libobs;${{ github.workspace }}\obs-build\config;${{ github.workspace }}\obs-studio\deps\w32-pthreads" `
-DLIBOBS_LIBRARY="${{ github.workspace }}\obs-build\libobs\Release\obs.lib;${{ github.workspace }}\obs-build\deps\w32-pthreads\Release\w32-pthreads.lib" `
-DOBS_FRONTEND_API_HINT="${{ github.workspace }}\obs-studio\frontend\api;${{ github.workspace }}\obs-studio\UI\obs-frontend-api" `
-DFFMPEG_INCLUDE_DIR="${{ github.workspace }}\obs-deps\include" `
-DAVCODEC_LIBRARY="${{ github.workspace }}\obs-deps\lib\avcodec.lib" `
-DAVUTIL_LIBRARY="${{ github.workspace }}\obs-deps\lib\avutil.lib"
-DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=ON `
-DFFMPEG_INCLUDE_DIR="${{ github.workspace }}\ffmpeg-static\include" `
-DAVCODEC_LIBRARY="${{ github.workspace }}\ffmpeg-static\lib\avcodec.lib" `
-DAVUTIL_LIBRARY="${{ github.workspace }}\ffmpeg-static\lib\avutil.lib"
cmake --build plugin-build --config Release

# A lenslink.dll that imports avcodec-NN.dll/avutil-NN.dll only
# loads while OBS ships that exact FFmpeg major — the next OBS
# update breaks it with LoadLibrary error 126 (issues #91, #93).
# The obs.dll check keeps the negative check honest: an empty or
# misread dependents dump would otherwise pass it vacuously.
- name: Verify FFmpeg linked statically
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$dumpbin = & $vswhere -latest -products * `
-find "VC\Tools\MSVC\**\bin\Hostx64\x64\dumpbin.exe" |
Select-Object -First 1
$deps = & $dumpbin /dependents plugin-build\Release\lenslink.dll | Out-String
Write-Host $deps
if ($deps -match "avcodec|avutil") {
throw "lenslink.dll imports OBS's FFmpeg DLLs (breaks on OBS updates)"
}
if ($deps -notmatch "obs\.dll") {
throw "obs.dll import missing - dependents dump looks wrong"
}

- name: Package (drop into C:\Program Files\obs-studio)
run: |
New-Item -ItemType Directory -Force -Path stage\obs-plugins\64bit | Out-Null
Expand Down Expand Up @@ -219,9 +252,13 @@ jobs:
if: needs.changes.outputs.plugin == 'true'
runs-on: macos-15
env:
# Keep in sync with the OBS release users run (same pins as the
# Windows job): the plugin links the FFmpeg dylibs from this deps
# bundle, and OBS must ship the same ones.
# Pins the libobs the plugin compiles/links against, and the Qt6
# bundle for the status-bar/dock UI (same pins as the Windows
# job). FFmpeg deliberately does NOT come from this bundle: the
# plugin links its own static decode-only FFmpeg
# (.github/actions/static-ffmpeg), so an OBS update swapping its
# bundled FFmpeg dylibs can't break plugin loading (issues #91,
# #93 hit exactly that on Windows).
OBS_REF: "32.1.2"
DEPS_TAG: "2025-08-23"
steps:
Expand All @@ -237,7 +274,7 @@ jobs:
ref: ${{ env.OBS_REF }}
path: obs-studio

- name: Download OBS prebuilt dependencies (FFmpeg, Qt6)
- name: Download OBS prebuilt dependencies (for libobs; Qt6)
run: |
curl -L -o deps.tar.xz "https://github.com/obsproject/obs-deps/releases/download/${{ env.DEPS_TAG }}/macos-deps-${{ env.DEPS_TAG }}-universal.tar.xz"
mkdir obs-deps
Expand All @@ -246,6 +283,9 @@ jobs:
curl -L -o deps-qt6.tar.xz "https://github.com/obsproject/obs-deps/releases/download/${{ env.DEPS_TAG }}/macos-deps-qt6-${{ env.DEPS_TAG }}-universal.tar.xz"
tar -xJf deps-qt6.tar.xz -C obs-deps

- name: Build static FFmpeg (H.264/HEVC decode only)
uses: ./.github/actions/static-ffmpeg

# "macos" in the key: cache keys are not runner-scoped, and the
# Windows job already uses libobs-<ref>-<tag> for its MSVC build.
- name: Cache libobs build
Expand Down Expand Up @@ -274,6 +314,10 @@ jobs:
-DENABLE_BROWSER=OFF -DENABLE_SCRIPTING=OFF
cmake --build obs-build --target libobs --config Release

# DISABLE_FIND_PACKAGE_PkgConfig: the runner ships a pkg-config,
# and CMake feeds it CMAKE_PREFIX_PATH (obs-deps) — without this
# the build could silently resolve OBS's shared FFmpeg instead of
# the static one. The guard below backstops it.
- name: Build plugin
run: |
cmake -S obs-plugin -B plugin-build \
Expand All @@ -285,11 +329,26 @@ jobs:
-DOBS_FRONTEND_API_HINT="$PWD/obs-studio/frontend/api;$PWD/obs-studio/UI/obs-frontend-api" \
-DLIBOBS_INCLUDE_DIR="$PWD/obs-studio/libobs;$PWD/obs-build/config" \
-DLIBOBS_LIBRARY="$PWD/obs-build/libobs/Release/libobs.framework" \
-DFFMPEG_INCLUDE_DIR="$PWD/obs-deps/include" \
-DAVCODEC_LIBRARY="$PWD/obs-deps/lib/libavcodec.dylib" \
-DAVUTIL_LIBRARY="$PWD/obs-deps/lib/libavutil.dylib"
-DCMAKE_DISABLE_FIND_PACKAGE_PkgConfig=ON \
-DFFMPEG_INCLUDE_DIR="$PWD/ffmpeg-static/include" \
-DAVCODEC_LIBRARY="$PWD/ffmpeg-static/lib/libavcodec.a" \
-DAVUTIL_LIBRARY="$PWD/ffmpeg-static/lib/libavutil.a"
cmake --build plugin-build

# A lenslink.so referencing libavcodec.NN.dylib only loads while
# OBS ships that exact FFmpeg major — the next OBS update breaks
# it (the macOS twin of Windows issues #91, #93). The libobs check
# keeps the negative check honest. Plain `if grep`, not `! grep`:
# a `!` line that isn't last is non-fatal under bash -e.
- name: Verify FFmpeg linked statically
run: |
otool -L plugin-build/lenslink.so | tee deps.log
if grep -E "libav(codec|util)" deps.log; then
echo "::error::lenslink.so links OBS's shared FFmpeg dylibs"
exit 1
fi
grep -q "libobs" deps.log

# Assemble the .plugin bundle on PRs too, so a broken Info.plist or
# layout change can't reach a release build. Takes seconds.
- name: Package (drop into ~/Library/Application Support/obs-studio/plugins)
Expand Down
Loading
Loading