Static FFmpeg 9.0.2 libavcodec + libavutil, configured down to the HEVC / H.265 decoder and the HEVC parser, built once so that nothing which links it needs FFmpeg, nasm, a C toolchain or a system package at all.
avcodec-hevc-sys = { package = "libavcodec-hevc-prebuilt-sys", git = "https://github.com/andrewtheguy/libavcodec-hevc-prebuilt", tag = "v9.0.2-…" }The crate's [lib] name is avcodec_hevc_sys, so the calls read
avcodec_hevc_sys::avcodec_send_packet(…). The tag pins the crate; the archives come from the
latest release of the private archive repository.
The archives are not public — the model fdk-aac-prebuilt uses. This repository publishes the
source of the build and no binary of it: the archives live in the releases of a private
repository, andrewtheguy/libavcodec-hevc-prebuilt-archives,
and build.rs reads them through gh, for whoever is logged in (gh auth login, or
GH_TOKEN) to an account that can see it. Anyone else builds the archives with ./build.sh and
points LIBAVCODEC_HEVC_PREBUILT_DIR at them (below).
With access, there is no configure, no nasm, no C compiler, no pkg-config and no libclang:
build.rs downloads the archives for its target, checks them, and emits the link flags.
This is the FFmpeg twin of libde265-prebuilt and libvpx-prebuilt, and follows them closely: same layout, same chain of checks, same release model. It exists because libde265 needed a fork to decode multi-slice streams with SAO correctly; FFmpeg's HEVC decoder is the most widely used and most fuzzed one there is, and this repository builds an unpatched release of it.
This repository is not a fork of FFmpeg and carries none of its source: source.sh fetches the
pinned commit into build/ (gitignored) at build time.
ffmpeg.env the pin: version, commit, source repo, private archive repo
source.sh fetch the pinned commit, assert it; the feature switches
build.sh <target> configure, make, verify, write dist/<target>/MANIFEST
sync-prebuilt.sh dist/ -> the crate's cache; --headers; --check; --fetch
check-static.sh <binary> assert a finished binary carries libavcodec and links none
publish-private.sh build all four targets on the operator's machines, release them
crates/libavcodec-hevc-prebuilt-sys/ the FFI crate: committed headers, committed bindings, build.rs
crates/libavcodec-hevc-e2e/ a consumer that decodes committed HEVC streams bit-exactly
ci/unix/, ci/windows/ the build.yml rows, runnable here and on the remote builders
Targets: macos-arm64, linux-x86_64, linux-aarch64, windows-x86_64-msvc. The Windows
archives are avcodec.lib and avutil.lib, MSVC against the dynamic CRT (/MD), for
x86_64-pc-windows-msvc only. No musl — see below.
FFmpeg configured with --disable-everything --disable-autodetect, and then two components
back: --enable-decoder=hevc --enable-parser=hevc. That is Main, Main 10, Main 12 and the
range extensions FFmpeg implements, with frame and slice (WPP) threading; and the parser, which
turns an Annex B byte stream in arbitrary chunks into the packets avcodec_send_packet wants.
Only libavcodec and libavutil are built. No other codec, no bitstream filter, no demuxer, no
zlib/iconv/VAAPI/CUDA — nothing autodetected, so nothing on the build machine can leak into the
archive as a link-time dependency.
One hwaccel, on macOS: VideoToolbox's HEVC one (--enable-videotoolbox --enable-hwaccel=hevc_videotoolbox). A consumer that gives the context a VideoToolbox device
(av_hwdevice_ctx_create, hw_device_ctx) and picks AV_PIX_FMT_VIDEOTOOLBOX in get_format
gets VideoToolbox pictures, and av_hwframe_transfer_data copies them out as NV12, P010 or NV24.
For HEVC, FFmpeg asks VideoToolbox to enable its hardware decoder, not to require it, so such a
picture is not by itself proof the Mac's media engine decoded it. One that sets no device decodes
on the CPU exactly as before. When the hwaccel fails to start, FFmpeg calls get_format again
without AV_PIX_FMT_VIDEOTOOLBOX, and the CPU decodes only if the callback picks one of the software
formats still offered; a picture VideoToolbox fails to decode once started is returned as an error,
not decoded on the CPU instead. VideoToolbox is part of every macOS,
so it adds Apple's own frameworks to the link and nothing else. No other target has a hwaccel.
No encoder. FFmpeg has no HEVC encoder of its own: hevc encoding in FFmpeg means libx265
(GPL, C++, its own cmake build), or a hardware encoder (NVENC, QSV, VideoToolbox, VAAPI, AMF,
MediaCodec) that needs a vendor SDK and a GPU. Either would turn this LGPL, C-only, dependency-free
archive into something else, and the end-to-end test does not need one — see
below.
build.sh then asserts what it configured, rather than trusting it:
- the licence configure reports is LGPL version 2.1 or later — nothing GPL or non-free got in;
config_components.henables exactly the HEVC decoder and parser, no encoder or bsf, and no hwaccel buthevc_videotoolboxon macOS, withCONFIG_VIDEOTOOLBOXon; and the archive's codec registry, read withnm, holds exactlyff_hevc_decoderandff_hevc_parser;- the 32 entry points the crates call are defined, each in the library it must be in;
CONFIG_RUNTIME_CPUDETECTis on, and the SIMD configuration it needs is too;- the SIMD kernels are in the archive, counted (below);
- the system libraries and Apple frameworks the archives need are measured from their
undefined symbols, and
build.rsemits link flags from the measurement; and no symbol reaches for a C++ runtime; - on Windows, every member is a real COFF object (no
/GLblobs), the objects name the dynamic CRT and not the static one, and none names an unshipped PDB; - on macOS the deployment target is read back off every member (
minos 14.0), and no member calls compiler-rt's__isPlatformVersionAtLeast, which a Rust link does not reliably carry.
av_version_info() returns 9.0.2: build.sh passes the release number as REVISION, where
FFmpeg's build would otherwise ask git describe in a one-commit-deep checkout and get a hash.
x86_64: no floor, deliberately — the same argument as libvpx-prebuilt and libde265-prebuilt.
FFmpeg's x86 kernels are nasm assembly, one function per instruction set, and
ff_hevc_dsp_init_x86 installs whichever the CPU supports from av_get_cpu_flags() at run time.
A -march floor could not decide which runs; it could only cost the archive every machine below
it. The linux-x86_64 archive holds 440 HEVC kernels (SSE2 66, SSSE3 12, SSE4.1 234, AVX 38,
AVX2 90); build.sh fails below 200, or below 40 AVX2.
arm64: NEON is the ARMv8-A baseline, and FFmpeg's HEVC NEON kernels are aarch64 assembly the C compiler assembles — no nasm. linux-aarch64 holds 430 (NEON 322, i8mm 108); the i8mm ones are chosen at run time. build.sh fails below 50 NEON kernels.
The e2e binary then checks the other half: that FFmpeg detects the architecture's baseline at
run time (SSE2, NEON), and that every stream decodes bit-exactly with the kernels and with
av_force_cpu_flags(0). It prints the speed-up too, as evidence the kernels are dispatched.
Measured (8-bit / 10-bit): 1.7× / 1.45× on Linux x86_64, 1.9× / 1.5× on Windows, 1.6× / 1.1× on
the arm64 Linux build box (no i8mm), 1.5× / 1.1× on Apple silicon. FFmpeg's 10-bit NEON coverage
is thinner than its x86 coverage, which is what the arm64 10-bit number shows.
FFmpeg is C, so unlike libde265-prebuilt there is no C++ runtime to carry. What the archives
need is measured into the MANIFEST as system_libs and emitted by build.rs: m pthread on
Linux, nothing on macOS (libSystem), and on Windows what FFmpeg's own configure tested and
recorded — ole32 user32 bcrypt, all of them part of every Windows install.
The Apple frameworks are measured into a second line, frameworks: each framework FFmpeg's
EXTRALIBS names is kept when the SDK's .tbd export list for it holds a symbol the archives
leave undefined. On macOS that is CoreFoundation CoreMedia CoreVideo VideoToolbox, for the
hwaccel (CoreServices, which FFmpeg also names, is referenced by nothing and dropped); elsewhere
none. build.rs refuses a MANIFEST without the line: it is an archive built before the hwaccel,
of the same FFmpeg commit, which the identity check would otherwise pass.
No musl mapping. The Linux archives are compiled against glibc and reference glibc-only
names — __isoc99_sscanf, __xpg_strerror_r, the LFS64 aliases (open64, fstat64) that musl
1.2.4 stopped exporting. Set LIBAVCODEC_HEVC_PREBUILT_DIR to your own musl build instead.
ffmpeg.env pins a commit
-> source.sh fetches that commit, asserts HEAD and its RELEASE file, refuses a dirty tree
-> build.sh compiles that tree and writes sha256 of both libraries into a MANIFEST
-> publish-private.sh releases the archives plus SHA256SUMS, privately
-> build.rs downloads them with gh and verifies them against SHA256SUMS
-> and the extracted libraries against the MANIFEST beside them, on every path
and, separately, the part a reviewer can read:
include/ is what FFmpeg's own `make install-headers` installs from the pinned commit,
with the same feature switches (sync-prebuilt.sh --check)
and what `make install` produced in each real build (the same, with dist/ present)
src/bindings.rs is what bindgen 0.72.1 makes of those headers (gen-bindings.sh --check)
SHA256SUMS is a corruption check, not a tamper check: it lives on the same release as the files
it covers. The pin that constrains someone other than this repository is FFMPEG_COMMIT,
asserted against the fetched tree before a compiler runs.
On linux-x86_64 the pipeline also builds the libraries twice, the second time from a clean
tree, and requires the checksums to match. Only there: GNU ar zeroes member mtimes and uids
(FFmpeg's configure asks for rcD), but Apple's ar and lib.exe stamp their members.
bindgen's output over the committed headers, one file for macOS and both Linux architectures and
one for Windows (MSVC types every C enum int). Two deliberate gaps:
- the five functions taking a
va_list(av_vlog,av_log_set_callback,av_log_default_callback,av_log_format_line{,2}) are left out, becauseva_listis a different type on each target one file covers.av_log_set_levelandav_logare there; - function-like macros cannot be bound, so
lib.rsdefines the ones a decode loop needs:AVERROR_EOF,AVERROR_EAGAIN(whose value differs per platform),AVERROR_INVALIDDATA,averror()andAV_NOPTS_VALUE. The e2e binary checks the error constants against what libavcodec actually returns on each target.
No encoder means libavcodec-hevc-e2e cannot make its own input, and it does not need to. Its
two streams in crates/libavcodec-hevc-e2e/testdata/ are the same bytes libde265-prebuilt
tests with, encoded by libx265 from ffmpeg's synthetic testsrc2; beside each is one SHA-256
per frame of a separate FFmpeg's decode (the distribution's, not this build). HEVC decoding is
exactly specified: libde265 reproduces those hashes in its repository, and this binary requires
the pinned libavcodec to reproduce them here. gen-testdata.sh records how both were made, and
CI re-derives the hashes with whichever ffmpeg the runner has.
On every target, the binary checks the version and licence, that the registry holds exactly the
HEVC decoder and parser, and the CPU flags; then decodes both streams — 8-bit with B-frames, and
10-bit with three slices per picture and SAO (the stream that exposed libde265's bug) — through
the parser, with SIMD on, with SIMD off, with four frame threads and with four slice threads, and
requires every frame to match, with the stream's in-band MD5 picture hashes verified
(AV_EF_CRCCHECK | AV_EF_EXPLODE). It proves that check is live (skipping the loop filters must
fail it), and that a stream cut at 60% drains to EOF with at most one error and outputs reference
pictures in display order — every one but the picture the cut landed in, which FFmpeg conceals.
On macOS it decodes both streams once more through the VideoToolbox hwaccel, and requires every
picture to be a VideoToolbox one — so FFmpeg's own decoder cannot pass for it — and, copied out and
unpacked from NV12 or P010, to match the same reference to the bit. That proves the hwaccel path,
not the hardware: VideoToolbox is free to decode HEVC in software, and the test does not ask. On a
virtual Mac (kern.hv_vmm_present) VideoToolbox's result is reported and not checked: it may have
no decoder to reach, and a macOS 26 guest of an M2 Max decoded the 10-bit stream exactly and not
one picture of the 8-bit stream, which its host decodes exactly. On any other Mac a shortfall
fails.
./build.sh <target> # -> dist/<target>/{lib,include,MANIFEST}
./sync-prebuilt.sh # -> crates/libavcodec-hevc-prebuilt-sys/prebuilt/, what cargo links
cargo run --release -p libavcodec-hevc-e2e
./check-static.sh target/release/libavcodec-hevc-e2eBuilding needs a C compiler, make, and on x86_64 nasm. <target> is the one this machine
is — ./build.sh does not cross-compile. The whole gate, as build.yml runs it, is
ci/unix/ci.sh; through ../devtools, ci/unix/remote.sh runs it here, ci/unix/remote.sh -a ci on the linux/arm64 build box and ci/unix/remote.sh -H macvm ci on the Mac, and
ci/windows/remote.ps1 ci on the Windows box. Or a workflow_dispatch on Build
libavcodec-hevc.
./sync-prebuilt.sh --fetch pulls the latest private release's archives instead (through gh).
LIBAVCODEC_HEVC_PREBUILT_DIR points build.rs at a prefix you built yourself — the escape hatch
for an unsupported target, musl, or more of FFmpeg — and build.rs warns that nothing about it
was checked.
./publish-private.sh, run by hand on a Linux x86_64 machine, from a commit that is pushed. It
builds all four targets on machines of the operator's own, at once, each passing the gate
build.yml applies — build.sh's own verification, the link, clippy, the e2e binary and
check-static.sh — on the machine that built it:
| builder | target | how |
|---|---|---|
| the machine running it | linux-x86_64 |
natively, ci/unix/ci.sh |
| the devtools arm64 builder (remote-lxc) | linux-aarch64 |
ci/unix/remote.sh -a |
$LIBAVCODEC_HEVC_PREBUILT_MACOS_HOST (default macvm) |
macos-arm64 |
ci/unix/remote.sh -H |
| the Windows CI box | windows-x86_64-msvc |
MSVC, ci/windows/ci.ps1 |
The sibling devtools checkout does the travelling.
What travels is git archive HEAD, so nothing uncommitted or ignored can reach an archive. Then
the archives and their SHA256SUMS go to a release of the private archive repository — draft
first, and published only after this repository's tag is pushed, so a failed upload or push leaves
a deletable draft rather than a latest with half its files or no source tag. All four or no
release.
Not a workflow, for two reasons: a public repository's workflow artifacts can be downloaded by
anyone with a GitHub account, which is a way of publishing the binaries; and a private
repository's runners are billed by the minute. build.yml still runs every target on GitHub
(workflow_dispatch), as a test that uploads nothing.
The tag is computed, never typed: v<ffmpeg>-<YYYYMMDDHHMMSS>-<short sha>. It is created twice —
on the archive repository, as the release, and here, as a plain git tag with no release, which is
what a consumer's manifest names.
cargo build -vv 2>&1 | grep -E 'libavcodec|FFmpeg'build.rs emits the provenance, the version, the checksum results, the CPU floor and the SIMD
evidence as cargo:info lines. At run time avcodec_hevc_sys::version() is what the archive
reports and avcodec_hevc_sys::PREBUILT_VERSION is what this repository pinned; the e2e binary
asserts they agree.
This FFmpeg configuration is LGPL-2.1-or-later (LICENSE.md and COPYING.LGPLv2.1, at the
root and in every archive), and these are static archives. That combination is allowed, and
it has conditions a BSD library does not: whoever distributes a program that links them must let
recipients relink it against a modified FFmpeg — in practice, ship your object files or your
source alongside the binary — and must pass on the licence and FFmpeg's source (the pinned
commit, unmodified). If that does not suit a closed-source product, the options are dynamic
linking against an FFmpeg shared library (not what this repository builds) or a different
decoder.
HEVC is also covered by patent pools. Nothing in FFmpeg's licence, or in this repository, grants patent rights; whether a product needs a patent licence is a question for whoever ships it.