# CI for ez-ffmpeg.
#
# Linux build and test jobs run on both native GitHub-hosted architectures
# (x64 and ARM64) in a two-lane FFmpeg matrix, because the crate supports
# linking either major and the two lanes are provisioned differently:
#
# * ffmpeg 8.1 lane — `--features ffmpeg-sys-next/build`: the sys crate's
# build script git-clones FFmpeg `release/8.1` (the branch is hardcoded to
# the crate's own major.minor) and compiles/statically links it. This is
# the same mechanism the pre-8 CI used, which then produced FFmpeg 7.1 —
# after the dependency bump the SAME flag silently produces 8.1, which is
# exactly why the 7.x lane below must NOT use it.
#
# * ffmpeg 7.1 lane — explicit from-source build of FFmpeg `release/7.1`
# (shared libs, default components, no external deps) installed under
# $HOME/ffmpeg71 and linked via pkg-config. This proves the bump keeps
# FFmpeg 7.x users working: ffmpeg-sys-next 8.1 probes the installed
# headers and only enables cfg flags up to ffmpeg_7_1.
#
# Ubuntu's apt FFmpeg (6.1 on 24.04) matches neither lane, so it is never
# installed; the apt packages below are just the FFmpeg build toolchain
# (nasm/yasm/clang/pkg-config/zlib). The 7.1 install is cached; the 8.1 lane
# rebuilds FFmpeg per job because its source-build helper tunes for the current
# runner CPU.
#
# Cargo caches deliberately keep only registry and git sources, never `target`.
# ffmpeg-sys-next's native source build adds `-march=native -mtune=native`, and
# GitHub can schedule the same runner architecture on different CPU generations;
# restoring those compiled artifacts can otherwise terminate tests with SIGILL.
#
# Native platform coverage outside Linux runs both FFmpeg lanes on every
# GitHub-hosted architecture: Apple Silicon macOS, Windows x64, and Windows
# ARM64. macOS builds FFmpeg 8.1 from source and uses Homebrew's versioned
# FFmpeg 7 formula; Windows checks out pinned vcpkg revisions whose ports carry
# FFmpeg 8.1.2 and 7.1.2. These jobs build every functional non-static feature,
# compile the corresponding test trees, and run the CPU-only library suite.
#
# GPU-dependent code: the `opengl` frame filter needs a real GPU/display and
# its tests fail headless ("Failed to create Surfman connection"), so the
# `test` job omits that feature; the `build` job still compiles it (via
# `--all-features`) so it stays type-checked. The `wgpu` feature runs in the
# test job: its GPU oracles self-skip without an adapter while its CPU-only
# gates (offline naga shader validation, layout pins, builders) run for real.
# The macOS-only `videotoolbox` scheduler tests are
# `#[cfg(target_os = "macos")]`, so they do not run on Linux.
#
# The `build` job (all-features build + a default-features build + clippy) and
# the `test` job run per Linux architecture and FFmpeg lane. Two specialized
# jobs run once on Linux x64:
#
# * `docs` — compiles the crate under `--cfg docsrs` (`DOCS_RS=1`), which stubs
# ez-ffmpeg's own FFI call sites; the gate catches docsrs-stub compile gaps.
# ffmpeg-sys-next still needs a real FFmpeg for its bindings, so this job builds
# one via `ffmpeg-sys-next/build` (not a literal docs.rs run — docs.rs has no
# network to clone FFmpeg — but it exercises the same `--cfg docsrs` paths).
# * `asan` — AddressSanitizer over the library unit tests and the
# lifecycle/teardown integration tests, guarding the use-after-free fixes.
# FFmpeg is statically linked via ffmpeg-sys-next/build; ASan's interceptors
# see its alloc/free, so a UAF where instrumented Rust touches FFmpeg-freed
# memory is caught (one entirely inside un-instrumented FFmpeg C is not
# reliably covered — those regressions are Rust-side here).
#
# All jobs are enforced gates.
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
build:
name: Build & Clippy (${{ matrix.arch }}, FFmpeg ${{ matrix.ffmpeg }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
arch: x64
ffmpeg: "8.1"
ffmpeg-features: "--features ffmpeg-sys-next/build"
- runner: ubuntu-24.04
arch: x64
ffmpeg: "7.1"
ffmpeg-features: ""
- runner: ubuntu-24.04-arm
arch: arm64
ffmpeg: "8.1"
ffmpeg-features: "--features ffmpeg-sys-next/build"
- runner: ubuntu-24.04-arm
arch: arm64
ffmpeg: "7.1"
ffmpeg-features: ""
steps:
- uses: actions/checkout@v7
# The 8.1 lane git-clones and statically builds FFmpeg inside target/
# alongside the crate's own build tree, and the sum has exhausted the
# stock image's free disk. Drop preinstalled toolchains this job never
# touches (~25 GiB) before the first build.
- name: Free disk space (from-source FFmpeg lane)
if: matrix.ffmpeg == '8.1'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force >/dev/null 2>&1 || true
df -h /
- name: Install FFmpeg build toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
nasm yasm clang pkg-config make git zlib1g-dev
echo "nasm: $(nasm --version | head -1)"
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache FFmpeg 7.1 install
if: matrix.ffmpeg == '7.1'
uses: actions/cache@v6
with:
path: ~/ffmpeg71
key: ${{ runner.os }}-${{ runner.arch }}-ffmpeg71-shared-v1
- name: Build FFmpeg 7.1 from source (if not cached)
if: matrix.ffmpeg == '7.1'
run: |
if [ ! -f "$HOME/ffmpeg71/lib/pkgconfig/libavcodec.pc" ]; then
git clone --depth=1 -b release/7.1 https://github.com/FFmpeg/FFmpeg ffmpeg-71-src
cd ffmpeg-71-src
./configure --prefix="$HOME/ffmpeg71" \
--disable-programs --disable-doc \
--disable-static --enable-shared
make -j"$(nproc)"
make install
fi
- name: Point pkg-config at FFmpeg 7.1
if: matrix.ffmpeg == '7.1'
run: |
echo "PKG_CONFIG_PATH=$HOME/ffmpeg71/lib/pkgconfig" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$HOME/ffmpeg71/lib" >> "$GITHUB_ENV"
- name: Cache cargo sources
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}
- name: Build
run: cargo build --all-features ${{ matrix.ffmpeg-features }} --verbose
# Compiles every #[cfg(test)] tree under --all-features WITHOUT running:
# the opengl/wgpu suites need a GPU/display to RUN, but their tests carry
# compile-time signature pins (e.g. the opengl typed-error fn-pointer
# bindings) that only a test build type-checks. `--no-run` is headless-safe.
- name: Build tests (all features, no run)
run: cargo test --lib --tests --all-features ${{ matrix.ffmpeg-features }} --no-run
# The crate's default feature set is empty (`default = []`), yet every job
# here builds `--all-features`; this proves the no-feature build and its cfg
# gates still compile. The ffmpeg-features flag only controls how FFmpeg is
# provisioned, so "default features" still carries it.
- name: Build (default features)
run: cargo build ${{ matrix.ffmpeg-features }} --verbose
# Safety-hygiene lints surface here. Not `-D warnings` yet: the crate has a
# pre-existing unused-import / undocumented-unsafe backlog to clear first.
- name: Clippy
run: cargo clippy --all-features ${{ matrix.ffmpeg-features }} --lib
test:
name: Test (${{ matrix.arch }}, FFmpeg ${{ matrix.ffmpeg }})
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
arch: x64
ffmpeg: "8.1"
ffmpeg-features: "--features ffmpeg-sys-next/build"
- runner: ubuntu-24.04
arch: x64
ffmpeg: "7.1"
ffmpeg-features: ""
- runner: ubuntu-24.04-arm
arch: arm64
ffmpeg: "8.1"
ffmpeg-features: "--features ffmpeg-sys-next/build"
- runner: ubuntu-24.04-arm
arch: arm64
ffmpeg: "7.1"
ffmpeg-features: ""
# Runs the full library suite MINUS the opengl feature, whose tests need a
# GPU/display (surfman connection creation fails headless). The wgpu
# feature IS included: its GPU oracles self-skip when no adapter exists,
# while its CPU-only gates — offline naga shader validation, uniform
# layout pins, builder checks — run for real. Media assets are committed
# as test.mp4.
steps:
- uses: actions/checkout@v7
# The 8.1 lane git-clones and statically builds FFmpeg inside target/
# alongside the crate's own build tree, and the sum has exhausted the
# stock image's free disk. Drop preinstalled toolchains this job never
# touches (~25 GiB) before the first build.
- name: Free disk space (from-source FFmpeg lane)
if: matrix.ffmpeg == '8.1'
run: |
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force >/dev/null 2>&1 || true
df -h /
- name: Install FFmpeg build toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
nasm yasm clang pkg-config make git zlib1g-dev
- uses: dtolnay/rust-toolchain@stable
- name: Cache FFmpeg 7.1 install
if: matrix.ffmpeg == '7.1'
uses: actions/cache@v6
with:
path: ~/ffmpeg71
key: ${{ runner.os }}-${{ runner.arch }}-ffmpeg71-shared-v1
- name: Build FFmpeg 7.1 from source (if not cached)
if: matrix.ffmpeg == '7.1'
run: |
if [ ! -f "$HOME/ffmpeg71/lib/pkgconfig/libavcodec.pc" ]; then
git clone --depth=1 -b release/7.1 https://github.com/FFmpeg/FFmpeg ffmpeg-71-src
cd ffmpeg-71-src
./configure --prefix="$HOME/ffmpeg71" \
--disable-programs --disable-doc \
--disable-static --enable-shared
make -j"$(nproc)"
make install
fi
- name: Point pkg-config at FFmpeg 7.1
if: matrix.ffmpeg == '7.1'
run: |
echo "PKG_CONFIG_PATH=$HOME/ffmpeg71/lib/pkgconfig" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$HOME/ffmpeg71/lib" >> "$GITHUB_ENV"
- name: Cache cargo sources
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}
- name: Test
run: cargo test --lib --test avoption_warnings --test build_probe --test builder_option_validation --test frame_export_test --test frame_export_uniform_test --test frame_export_audio_test --test hls_fmp4 --test video_writer --test rawvideo_seek_gating --test per_output_video_filter --test packet_sink --test packet_sink_negative --test packet_sink_teardown --test packet_sink_unwind --test cli_emit_hardening --test hdr_to_sdr_detection --test logger_reentrancy --test packet_sink_job_failure --features async,rtmp,flv,subtitle,wgpu,cli ${{ matrix.ffmpeg-features }}
# The packet-sink examples are API surface: keep them compiling on the
# runtime lane (native-AAC cases in the suites above do real work even
# without libx264; x264 cases self-skip at runtime).
- name: Check packet-sink examples
run: cargo check --example packet_sink_avc --example packet_sink_aac_channel --example packet_sink_av_transport --example hdr_to_sdr ${{ matrix.ffmpeg-features }}
- name: Doctests
run: cargo test --doc --features async,rtmp,flv,subtitle,wgpu,cli ${{ matrix.ffmpeg-features }}
# aarch64 second-stage confirmation for the NAL start-code scan
# refactor (perf/nal-stride3-scan, C2). Its x86-64 win is established
# (paired same-round benchmark: 19 corpora, medians 1.01-3.3x, no pair
# below parity); ARM throughput was the one open item in the
# SHIP-TWO-STAGE disposition. This runs the in-repo #[ignore]
# micro-benchmark on the arm64 runner, prints the full same-round
# paired table (parent byte scan vs shipped stride scan) to the CI log
# for review via `gh run view --log`, then applies a lenient
# net-regression gate: nal_scan_parity_gate.py requires each corpus's
# pooled median paired ratio >= 0.85 on both walks. That floor sits
# below the x86 win (near-parity 1-2 byte-NAL corpora hover around 1.0
# even unpinned) so it will not flake, yet a breach means the stride
# scan is a material per-corpus loss on ARM and the aarch64 byte-scan
# fallback (#[cfg(target_arch = "aarch64")] over the parent byte
# finder) should be gated in before release tagging.
#
# Pinned to the 7.1 arm64 lane only: 7.1 links a cached shared FFmpeg,
# so the release build recompiles Rust only (8.1 rebuilds FFmpeg
# statically in release); the scanner is core and feature-independent,
# so no extra cargo features are needed; and x64 needs no run (the x86
# evidence is complete on the branch). Three invocations pool fifteen
# same-round rounds per corpus so the median stays stable on a shared
# runner (the near-parity tiny-NAL corpora are the noise-sensitive
# ones and this keeps their pooled median clear of the floor).
- name: NAL start-code scan aarch64 parity benchmark
if: matrix.arch == 'arm64' && matrix.ffmpeg == '7.1'
run: |
set -o pipefail
: > nal_scan_bench.log
for i in 1 2 3; do
cargo test --release --lib bench_nal_startcode_scan ${{ matrix.ffmpeg-features }} \
-- --ignored --nocapture | tee -a nal_scan_bench.log
done
python3 .github/scripts/nal_scan_parity_gate.py nal_scan_bench.log
cli-goldens:
# Strict semantic-golden lane for the cli feature: builds a
# version-matched FFmpeg WITH programs and libx264 (GPL is fine for CI
# testing; nothing GPL ships in the crate) and runs the golden suite with
# EZ_FFMPEG_CLI pointing at that binary, so a skip IS a failure. The
# verified-profile table only lists lines whose lane has passed here:
# 7.1 today. An 8.1 row is a matrix addition (same recipe, release/8.1
# branch and an ffmpeg81full cache key) once the build matrix carries it.
name: CLI goldens (FFmpeg ${{ matrix.ffmpeg }}, strict)
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- ffmpeg: "7.1"
branch: release/7.1
prefix: ffmpeg71full
steps:
- uses: actions/checkout@v7
- name: Install FFmpeg build toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
nasm yasm clang pkg-config make git zlib1g-dev libx264-dev
- uses: dtolnay/rust-toolchain@stable
- name: Cache FFmpeg ${{ matrix.ffmpeg }} full install
uses: actions/cache@v6
with:
path: ~/${{ matrix.prefix }}
key: ${{ runner.os }}-${{ runner.arch }}-${{ matrix.prefix }}-shared-v1
- name: Build FFmpeg ${{ matrix.ffmpeg }} with programs (if not cached)
run: |
if [ ! -x "$HOME/${{ matrix.prefix }}/bin/ffmpeg" ]; then
git clone --depth=1 -b ${{ matrix.branch }} https://github.com/FFmpeg/FFmpeg ffmpeg-golden-src
cd ffmpeg-golden-src
./configure --prefix="$HOME/${{ matrix.prefix }}" \
--disable-doc \
--disable-static --enable-shared \
--enable-gpl --enable-libx264
make -j"$(nproc)"
make install
fi
- name: Point pkg-config at the golden FFmpeg
run: |
echo "PKG_CONFIG_PATH=$HOME/${{ matrix.prefix }}/lib/pkgconfig" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$HOME/${{ matrix.prefix }}/lib" >> "$GITHUB_ENV"
- name: Cache cargo sources
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}
- name: Build the pinned emitted examples
run: cargo build --features cli --examples
- name: Run the cli suite with strict goldens
run: |
EZ_FFMPEG_CLI="$HOME/${{ matrix.prefix }}/bin/ffmpeg" cargo test --features cli --lib cli::
# The `cli::` filter above cannot reach the frame-export byte-parity
# golden (`core::frame_export::color_goldens::default_tier_matches_cli_bytes`),
# whose gate compares libswscale builds and treats a skip as a failure
# under EZ_FFMPEG_CLI. The crate links the same install the strict
# binary comes from, so the parity comparison actually executes here.
- name: Run the frame-export goldens with the strict CLI
run: |
EZ_FFMPEG_CLI="$HOME/${{ matrix.prefix }}/bin/ffmpeg" cargo test --features cli --lib core::frame_export::color_goldens
- name: Emit-hardening integration tests
run: cargo test --features cli --test cli_emit_hardening
# RTMP loopback suite: real-TCP rml_rtmp watchers against the embedded
# RTMP server (late-joiner GOP replay, EOF status delivery, StreamBuilder
# port release, AVCC parser hardening). The publish leg re-encodes
# test.mp4 to H.264 because create_rtmp_input pins h264/aac, so it needs
# an FFmpeg with a working H.264 encoder — this lane's libx264 build is
# the only one in the matrix that has one: the from-source and vcpkg
# lanes carry no H.264 encoder, and the runners expose no V4L2 device or
# VideoToolbox session for the hardware fallbacks. The load-harness
# measurements in the same file stay #[ignore]d.
- name: RTMP loopback tests
run: cargo test --features rtmp --test rtmp_loopback
platform:
name: Platform (${{ matrix.platform }}, FFmpeg ${{ matrix.ffmpeg }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15
platform: macos-15-arm64
os: macos
target: aarch64-apple-darwin
ffmpeg: "8.1"
build_features: ffmpeg-sys-next/build-videotoolbox,async,opengl,wgpu,rtmp,flv,subtitle,cli
test_features: ffmpeg-sys-next/build-videotoolbox,async,rtmp,flv,subtitle,wgpu,cli
default_ffmpeg_features: --features ffmpeg-sys-next/build-videotoolbox
test_args: >-
--
--skip core::scheduler::ffmpeg_scheduler::tests::test_hwaccel
--skip core::scheduler::ffmpeg_scheduler::tests::test_to_stdout
- runner: macos-15
platform: macos-15-arm64
os: macos
target: aarch64-apple-darwin
ffmpeg: "7.1"
build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
test_features: async,rtmp,flv,subtitle,wgpu,cli
default_ffmpeg_features: ""
test_args: >-
--
--skip core::scheduler::ffmpeg_scheduler::tests::test_hwaccel
--skip core::scheduler::ffmpeg_scheduler::tests::test_to_stdout
- runner: windows-2025
platform: windows-2025-x64
os: windows
target: x86_64-pc-windows-msvc
ffmpeg: "8.1"
vcpkg_triplet: x64-windows
vcpkg_ref: b5229343b4b80264ed51e89c6a7dcd0cbe85e9cc
vcpkg_cache: b5229343-ffmpeg-8.1.2-v1
build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
test_features: async,rtmp,flv,subtitle,wgpu,cli
default_ffmpeg_features: ""
test_args: ""
- runner: windows-2025
platform: windows-2025-x64
os: windows
target: x86_64-pc-windows-msvc
ffmpeg: "7.1"
vcpkg_triplet: x64-windows
vcpkg_ref: 6070d5cb2f818921c10ccdf6f35f3415a2503369
vcpkg_cache: 6070d5cb-ffmpeg-7.1.2-p5-v1
build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
test_features: async,rtmp,flv,subtitle,wgpu,cli
default_ffmpeg_features: ""
test_args: ""
- runner: windows-11-arm
platform: windows-11-arm64
os: windows
target: aarch64-pc-windows-msvc
ffmpeg: "8.1"
vcpkg_triplet: arm64-windows
vcpkg_ref: b5229343b4b80264ed51e89c6a7dcd0cbe85e9cc
vcpkg_cache: b5229343-ffmpeg-8.1.2-v1
build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
test_features: async,rtmp,flv,subtitle,wgpu,cli
default_ffmpeg_features: ""
test_args: -- --test-threads=1
- runner: windows-11-arm
platform: windows-11-arm64
os: windows
target: aarch64-pc-windows-msvc
ffmpeg: "7.1"
vcpkg_triplet: arm64-windows
vcpkg_ref: 6070d5cb2f818921c10ccdf6f35f3415a2503369
vcpkg_cache: 6070d5cb-ffmpeg-7.1.2-p5-v1
build_features: async,opengl,wgpu,rtmp,flv,subtitle,cli
test_features: async,rtmp,flv,subtitle,wgpu,cli
default_ffmpeg_features: ""
test_args: -- --test-threads=1
env:
CARGO_BUILD_JOBS: 2
CARGO_INCREMENTAL: 0
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- name: Verify native Rust target
shell: bash
run: |
rustc -vV
test "$(rustc -vV | sed -n 's/^host: //p')" = "${{ matrix.target }}"
- name: Cache cargo sources
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}
# ffmpeg@7 is keg-only and uses absolute Homebrew install names, so its
# shared libraries remain discoverable without DYLD_LIBRARY_PATH. The
# versioned formula tracks the latest 7.1 patch release.
- name: Install FFmpeg 7.1 with Homebrew
if: matrix.os == 'macos' && matrix.ffmpeg == '7.1'
shell: bash
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_NO_INSTALL_CLEANUP: "1"
run: |
# GitHub's image may carry an unrelated untrusted aws/tap. Remove it
# before Homebrew runs diagnostics so the job stays annotation-free.
brew untap aws/tap >/dev/null 2>&1 || true
brew install ffmpeg@7
prefix="$(brew --prefix ffmpeg@7)"
version="$("$prefix/bin/ffmpeg" -version | sed -n 's/^ffmpeg version \([^ ]*\).*/\1/p')"
case "$version" in
7.1*) ;;
*) echo "Expected FFmpeg 7.1.x, found $version" >&2; exit 1 ;;
esac
echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig" >> "$GITHUB_ENV"
echo "$prefix/bin" >> "$GITHUB_PATH"
PKG_CONFIG_PATH="$prefix/lib/pkgconfig" pkg-config --modversion libavcodec
- name: Configure vcpkg
if: matrix.os == 'windows'
shell: pwsh
run: |
$root = $env:VCPKG_INSTALLATION_ROOT
$cache = Join-Path $env:GITHUB_WORKSPACE "vcpkg-bincache"
New-Item -ItemType Directory -Force -Path $cache | Out-Null
"VCPKG_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VCPKG_DEFAULT_TRIPLET=${{ matrix.vcpkg_triplet }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VCPKGRS_TRIPLET=${{ matrix.vcpkg_triplet }}" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VCPKGRS_DYNAMIC=1" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VCPKG_DEFAULT_BINARY_CACHE=$cache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"$root\installed\${{ matrix.vcpkg_triplet }}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Cache vcpkg binary packages
if: matrix.os == 'windows'
uses: actions/cache@v6
with:
path: vcpkg-bincache
key: ${{ runner.os }}-${{ runner.arch }}-vcpkg-${{ matrix.vcpkg_cache }}
- name: Install FFmpeg ${{ matrix.ffmpeg }} with vcpkg
if: matrix.os == 'windows'
shell: pwsh
run: |
git config --global --add safe.directory $env:VCPKG_ROOT
if ($LASTEXITCODE -ne 0) { throw "git config exited with code $LASTEXITCODE" }
git -C $env:VCPKG_ROOT fetch --depth=1 origin ${{ matrix.vcpkg_ref }}
if ($LASTEXITCODE -ne 0) { throw "git fetch exited with code $LASTEXITCODE" }
git -C $env:VCPKG_ROOT checkout --force FETCH_HEAD
if ($LASTEXITCODE -ne 0) { throw "git checkout exited with code $LASTEXITCODE" }
& "$env:VCPKG_ROOT\bootstrap-vcpkg.bat" -disableMetrics
if ($LASTEXITCODE -ne 0) { throw "bootstrap-vcpkg exited with code $LASTEXITCODE" }
& "$env:VCPKG_ROOT\vcpkg.exe" install "ffmpeg:${{ matrix.vcpkg_triplet }}" --clean-after-build
if ($LASTEXITCODE -ne 0) { throw "vcpkg install exited with code $LASTEXITCODE" }
$ffmpeg = & "$env:VCPKG_ROOT\vcpkg.exe" list | Select-String "^ffmpeg:"
if ($LASTEXITCODE -ne 0) { throw "vcpkg list exited with code $LASTEXITCODE" }
if (-not $ffmpeg) { throw "The FFmpeg vcpkg package was not installed" }
$expectedVersion = [regex]::Escape("${{ matrix.ffmpeg }}")
if ("$ffmpeg" -notmatch "\s$expectedVersion\.") {
throw "Expected FFmpeg ${{ matrix.ffmpeg }}.x, found: $ffmpeg"
}
$ffmpeg
# The crate-level `static` feature is intentionally omitted on Windows:
# these vcpkg triplets provide shared FFmpeg libraries. macOS uses the
# dependency's source-build feature for 8.1 and Homebrew's shared 7.1
# libraries for the compatibility lane. Every functional optional feature
# is type-checked on each native platform.
- name: Build all non-static features
run: cargo build --features "${{ matrix.build_features }}" --verbose
- name: Build all non-static feature tests
run: cargo test --lib --tests --features "${{ matrix.build_features }}" --no-run
# This keeps the crate's own default feature set empty while carrying only
# the dependency feature needed to provision source-built FFmpeg 8.1 on
# macOS. The system/vcpkg lanes pass an empty string here.
- name: Build default crate features
run: cargo build ${{ matrix.default_ffmpeg_features }} --verbose
# GPU-dependent features are compile-only above. GitHub's macOS runner
# also cannot create a VideoToolbox compression session, so its two
# hardware-encoder tests stay compile-only. The remaining suite exercises
# the platform scheduler, device, socket, RTMP, subtitle, and async paths.
# The cli suite is profile-aware: on linked 8.1 its execution tests
# assert the typed UnverifiedRuntimeProfile refusal, on the vcpkg 7.1
# lanes the semantic goldens skip deterministically (today no
# version-matched ffmpeg binary is on PATH, and even if one ever lands
# there the goldens' lenient gate skips every shape whose canonical
# command names an encoder this libx264-less link lacks; the
# in-process gates accept EncoderUnavailable on that link), and on the
# Homebrew 7.1 lane they run in full against the matching CLI. The
# emit-hardening tests type-check emitted programs with
# `rustc --emit=metadata`, needing no native linking.
#
# xnu regrows an unread TCP receive window past an explicitly pinned
# SO_RCVBUF (observed on these runners: pinned to 16384 at accept,
# grown back past 500k by the packet-sink wedge test's park — the
# test's own drain probes feed the autotuner), so a never-reading peer
# cannot hold a sender blocked under the 4 MiB default ceiling. Cap
# the ceilings to a bound the stream saturates within the test's
# first second. The bound must stay comfortably above the loopback
# MSS (16 KiB): at 16384 a drained window cannot re-announce itself
# and the wedge stages starve waiting for a refill that never comes.
- name: Cap TCP buffer autotuning (macOS)
if: matrix.os == 'macos'
run: |
sudo sysctl -w net.inet.tcp.autorcvbufmax=131072
sudo sysctl -w net.inet.tcp.autosndbufmax=131072
- name: Run CPU-only library tests
run: cargo test --lib --test avoption_warnings --test build_probe --test builder_option_validation --test frame_export_test --test frame_export_uniform_test --test frame_export_audio_test --test hls_fmp4 --test video_writer --test rawvideo_seek_gating --test per_output_video_filter --test packet_sink --test packet_sink_negative --test packet_sink_teardown --test packet_sink_unwind --test cli_emit_hardening --test hdr_to_sdr_detection --test logger_reentrancy --test packet_sink_job_failure --features "${{ matrix.test_features }}" ${{ matrix.test_args }}
- name: Check packet-sink examples
run: cargo check --example packet_sink_avc --example packet_sink_aac_channel --example packet_sink_av_transport --example hdr_to_sdr --features "${{ matrix.test_features }}"
docs:
name: Docs (docsrs cfg compile)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Install FFmpeg build toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
nasm yasm clang pkg-config make git zlib1g-dev
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo sources
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}
# `DOCS_RS=1` makes ez-ffmpeg's build.rs pass `--cfg docsrs`, compiling its FFI
# call sites as stubs; this gate catches a docsrs-stub compile gap (a fn that
# takes a `#[cfg(not(docsrs))]` type but is itself ungated). ffmpeg-sys-next's
# build.rs does NOT honor DOCS_RS, so it still needs a real FFmpeg for its
# bindings; `ffmpeg-sys-next/build` compiles one from source (the runner has
# the network docs.rs lacks), keeping the job self-contained. The features
# otherwise match `[package.metadata.docs.rs]`. Not `-D warnings`: docs.rs does
# not, and the crate has a pre-existing intra-doc-link backlog (a separate
# task); this lane is the stub-compile gate, not doc-link cleanup.
- name: cargo doc (docsrs cfg)
env:
DOCS_RS: "1"
run: cargo doc --no-deps --features ffmpeg-sys-next/build,async,opengl,rtmp,flv,subtitle,wgpu
asan:
name: ASAN (UAF, FFmpeg 8.1)
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- name: Install FFmpeg build toolchain
run: |
sudo apt-get update
sudo apt-get install -y \
nasm yasm clang pkg-config make git zlib1g-dev
- uses: dtolnay/rust-toolchain@nightly
- name: Cache cargo sources
uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-sources-v1-${{ hashFiles('**/Cargo.toml') }}
# `-Zsanitizer=address` instruments the target crates' Rust code. The explicit
# `--target` scopes the sanitizer to those crates and off the host build
# scripts / proc-macros; std is reused un-instrumented from the sysroot (so no
# `-Zbuild-std`), which suffices because the UAF regressions here are detected
# on the Rust side. `detect_leaks=0` because FFmpeg's one-time global
# allocations are not leaks. `--lib` runs the library unit tests under the
# same instrumentation as the lifecycle/teardown integration suites. GPU
# features are omitted here (the test job runs wgpu's self-skipping suite;
# opengl needs a display either way) — the tests under ASAN do not need
# them.
- name: ASAN UAF tests
env:
RUSTFLAGS: "-Zsanitizer=address"
RUSTDOCFLAGS: "-Zsanitizer=address"
ASAN_OPTIONS: "detect_leaks=0"
run: |
cargo +nightly test --target x86_64-unknown-linux-gnu \
--features ffmpeg-sys-next/build,async,rtmp,flv,subtitle \
--lib \
--test bsf --test mux_teardown --test lifecycle \
--test start_failure_join --test start_panic_unwind \
--test shortest --test frame_pipeline_hardening \
--test build_probe --test frame_export_test \
--test frame_export_uniform_test --test frame_export_audio_test \
--test video_writer --test packet_sink_teardown --test packet_sink_unwind \
--test logger_reentrancy