Skip to content

build: bundle a static ffmpeg, libsrt and librist (closes #6)#4

Open
datagutt wants to merge 12 commits into
masterfrom
feat/bundled-ffmpeg
Open

build: bundle a static ffmpeg, libsrt and librist (closes #6)#4
datagutt wants to merge 12 commits into
masterfrom
feat/bundled-ffmpeg

Conversation

@datagutt

@datagutt datagutt commented Jul 23, 2026

Copy link
Copy Markdown
Member

Links a pinned FFmpeg, libsrt, librist and mbedTLS into the plugin statically instead of using whatever the host OBS ships.

Why

Two problems, one fix.

Stale transport libraries. obs-deps pins libsrt 1.5.2 (September 2022) and librist 0.2.7. Every SRT and RIST fix since then was missing.

bundled obs-deps
FFmpeg 8.1.2 7.x / 8.1.2 (varies by OBS line)
libsrt 1.5.6 1.5.2
librist 0.2.18 0.2.7
mbedTLS 3.6.4 3.6.4

The FFmpeg major split the releases. OBS 32.1 bundles FFmpeg 7 (avcodec-61), 32.2 bundles 8.1 (avcodec-62). A binary linked against one will not load where the other is present, so every release shipped a separate Windows and macOS artifact per OBS line.

One artifact per platform

libobs was never the constraint. obs_init_module gates a plugin on (mod.ver() & 0xFFFF0000) <= LIBOBS_API_VER, so major and minor only, with no equality check, and it looks up nothing but obs_module_*. Building against the oldest supported line yields one binary that loads on that line and every newer one.

OBS_VERSION at the top of build.yml pins that oldest line. OBS_DEPS_VERSION survives only because libobs itself needs obs-deps to build; the plugin no longer touches it.

Symbol isolation

This is load-bearing, not cosmetic. OBS has already loaded its own FFmpeg, exporting the same symbol names as the archives linked here. 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 module is built with hidden visibility, --exclude-libs,ALL and a version script exporting only the obs_module_* entry points. scripts/verify-plugin.sh asserts this after every build, with a dumpbin equivalent in the Windows job, because a successful compile does not prove it.

Verified against a harness using the identical link line, with a system FFmpeg loaded RTLD_GLOBAL first to simulate OBS:

host   avcodec 62.28.101  (loaded first, RTLD_GLOBAL)
module avcodec 62.28.102  <- its own static copy
protocols: srt=ok rist=ok rtmp=ok rtmps=ok https=ok udp=ok rtp=ok
decoders:  h264=ok hevc=ok av1=ok vp9=ok aac=ok opus=ok
module leaks avcodec_version? no (good)

DT_NEEDED reduces to libstdc++, libm, libz, libgcc_s, libc. No libav*, and no libmbedcrypto, which confirms librist bound the static copy. Stripped size roughly 12 MB.

Two silent failures found on the way

Both now guarded, and worth knowing about if anyone touches the component lists.

--enable-protocol=srt is a no-op: FFmpeg names the component libsrt. Configure ignores unknown names in an --enable-* list without a diagnostic, and produced a build with CONFIG_LIBSRT=yes next to !CONFIG_LIBSRT_PROTOCOL=yes, meaning the library was linked in and SRT was completely dead. build-deps.sh now asserts every required component against the generated ffbuild/config.mak.

librist's meson located mbedTLS with cc.find_library and bound the host's shared libmbedcrypto.so, which would have reintroduced a runtime dependency and mismatched the copy libsrt uses. fix_mbedtls_pc rewrites both librist.pc and srt.pc to resolve out of the prefix.

Component scope

The bundled FFmpeg is decode only (--disable-everything plus an explicit list) and carries exactly what the README advertises: H.264, HEVC, AV1, VP9, AAC, Opus, MP3, AC3/E-AC3 and PCM; MPEG-TS, FLV, MP4, Matroska, HLS and RTSP; SRT, RIST, RTMP(S), HTTP(S), TCP, UDP and RTP. Hardware decode is VAAPI and NVDEC on Linux, D3D11VA/DXVA2 and NVDEC on Windows, VideoToolbox on macOS.

Trimming that list further is a user-visible feature removal, not a size optimisation. deps/README.md says so.

Licensing

FFmpeg is configured LGPLv3 (--enable-version3, no --enable-gpl), 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 BSD-2-Clause, mbedTLS Apache-2.0, all compatible with (A)GPLv3.

Verification status

CI is green on all three platforms (run 30029171320): Linux x64, Windows x64, macOS ARM64. Each job builds the bundled stack, builds the plugin, and runs the isolation checks. Windows reports 16 hwaccels compiled in, no FFmpeg DLL imports, no leaked bundled symbols, and obs_module_load exported.

Artifact sizes: Linux 16 MB (unstripped, with debug info), Windows 4 MB, macOS 3 MB.

The isolation guarantee was also validated locally against a harness using the identical link line, with a system FFmpeg loaded RTLD_GLOBAL first to simulate OBS:

host   avcodec 62.28.101  (loaded first, RTLD_GLOBAL)
module avcodec 62.28.102  <- its own static copy
protocols: srt=ok rist=ok rtmp=ok rtmps=ok https=ok udp=ok rtp=ok
decoders:  h264=ok hevc=ok av1=ok vp9=ok aac=ok opus=ok
module leaks avcodec_version? no (good)

Not yet done: loading the artifacts in a real OBS install on each platform (the RELEASING.md smoke test). CI proves the binary builds, isolates, and exports correctly; it does not prove a stream plays. That is the manual pre-publish step.

Commit history

The first commit is the feature; the remaining ten are toolchain fixes that CI surfaced, almost all Windows (MSVC/MSYS2 CRT, static library naming, pkg-config quirks, one upstream FFmpeg gmtime_r include bug). Happy to squash to a single commit on merge if you would rather not carry the fix-forward trail.

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.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@datagutt, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: bf7da16c-e632-4f45-bf2b-14efef7be6d9

📥 Commits

Reviewing files that changed from the base of the PR and between 6861d14 and f8eae55.

📒 Files selected for processing (1)
  • .github/workflows/build.yml

Walkthrough

The plugin now builds a pinned static media dependency stack, links it through CMake with symbol isolation, verifies platform binaries, and publishes one artifact per platform. Build, release, and project documentation reflect the bundled stack and OBS compatibility model.

Changes

Bundled media stack

Layer / File(s) Summary
Dependency stack build
deps/build-deps.sh, deps/versions.env, deps/README.md, .gitignore
Builds pinned media components and emits CMake link metadata with checksum validation and incremental build support.
CMake integration and isolation
CMakeLists.txt, cmake/module-symbols.exp, scripts/verify-plugin.sh
Adds bundled-dependency linking, host-FFmpeg fallback behavior, restricted exports, and platform-specific binary verification.
Platform CI artifacts
.github/workflows/build.yml
Builds dependencies and the plugin on Linux, Windows, and macOS, verifies isolation, and uploads one artifact per platform.
Release packaging
.github/workflows/release.yml
Packages fixed Windows and macOS artifact paths into single platform archives.

Project documentation

Layer / File(s) Summary
Build and release guidance
README.md, CLAUDE.md, RELEASING.md
Documents bundled dependencies, build commands, isolation checks, OBS compatibility, and single-platform release archives.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CI as Build workflow
  participant Deps as deps/build-deps.sh
  participant CMake as CMake
  participant Verify as verify-plugin.sh
  participant Release as Release workflow
  CI->>Deps: Build bundled static dependencies
  CI->>CMake: Configure and build plugin
  CI->>Verify: Check dependencies and exports
  Verify-->>CI: Return verification status
  CI->>Release: Provide platform artifact
  Release->>Release: Package platform archive
Loading

Poem

A rabbit hops through static streams,
With bundled code and build-time dreams.
Symbols hide, checks softly glow,
One zip per platform hops below.
CI thumps its paws: “All clear!”


Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands.

datagutt added 11 commits July 23, 2026 17:16
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.
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.
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.
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.
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.
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.
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.
…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.
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.
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.
@datagutt datagutt changed the title build: bundle a static ffmpeg, libsrt and librist build: bundle a static ffmpeg, libsrt and librist (closes #6) Jul 23, 2026
@datagutt datagutt linked an issue Jul 23, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bundle a static ffmpeg, SRT and librist

1 participant