Skip to content

Development

afmpeg follows spec-driven development: no implementation change without a spec it implements. The specs are the authoritative decision record; the code is downstream of them.

Picking up implementation? Start at the implementation roadmap: the phased build order across all specs (0013–0034) with dependencies and prerequisites, and its Pick-up menu of the remaining trigger-gated work. Phases 0–4 are shipped (through vocab v9, job progress), and Phase 5 (the native backend + matrix, HEVC/AV1, perf) is largely shipped too. Current anchors: afmpeg v0.16.1, ffmpeg-wasi n9.0.1-8, job-spec vocab v10. What remains is all optional/trigger-gated: 0009 CLI, 0030 WASM threads, arm64/darwin native, HW-accel encode, 0025/0026.

Contributor docs

  • CI security scanning: how the MR security gate works (govulncheck / osv-scanner / trivy / gitleaks), and the recorded decisions behind the osv-scanner ignore + job overrides (incl. the unfixable, unreachable x/crypto advisory).

Specs

The source of truth. They live in the project wiki, see the register for all of them, in number order, with their current status.

Start with 0001-afmpeg, the thesis; it decomposes into the component specs.

They moved out of docs/ because a spec is a point-in-time decision record: read for the conclusion it reached, not kept true as the code changes. docs/ holds only what does change with the code.

Also Scope
external review: validation and disposition The commissioned external review + our per-finding validation verdicts and spec mapping

Method

  • Spec first. Get a spec to APPROVED, then implement against it test-first. New specs claim the next number from the register and are published to the wiki as specs/NNNN-<slug>, not committed to this repository.
  • Library before CLI. Logic lives in pkg/; any command layer is a thin adapter.
  • Test-first from the spec's contracts, table-driven with t.Parallel(); the per-package coverage bar is ≥90% on new pkg/ code.
  • Every package carries a doc.go. The package-level documentation lives in a dedicated doc.go (not scattered above a random file's package clause), so the package's purpose is discoverable in one place and on pkg.go.dev.
  • Docs land with the code, not after. A change that adds or reshapes a component ships its Diátaxis documentation in the same MR, an explanation page for a new component, a how-to for a new task, reference for a new config/CLI surface. Docs are part of "done", never an afterthought.
  • Verify before PR: just ci (tidy, generate, test, race, lint).

Local workflow

just            # build (tidy + generate + CGO_ENABLED=0 build)
just test       # unit tests with coverage
just test-race  # race detector
just lint       # golangci-lint
just ci         # the full local gate
just docs-serve # preview this site

Integration test (real ffmpeg)

The runtime has a gated integration suite that loads real ffmpeg-wasi modules and transcodes in memory. It skips unless pointed at a module, and there is one variable per capability profile (spec 0022):

Variable Module
AFMPEG_TEST_FFMPEG_WASI a lean build (or richer)
AFMPEG_TEST_FFMPEG_WASI_INTERMEDIATE an intermediate build

Profiles are cumulative but not interchangeable. Roughly a third of the suite exercises mpegts, HLS, libopus/libmp3lame/libvpx, yadif, loudnorm, libass burn-in or AV1 decode, none of which a lean build carries, so those tests need the intermediate module and skip without it, naming the variable to set. An intermediate build satisfies a lean test, so setting only the intermediate variable runs everything too.

just test-integration /path/to/ffmpeg-wasi-lgpl.wasm /path/to/ffmpeg-wasi-intermediate-lgpl.wasm

or directly:

AFMPEG_TEST_FFMPEG_WASI=/path/to/ffmpeg-wasi-lgpl.wasm \
AFMPEG_TEST_FFMPEG_WASI_INTERMEDIATE=/path/to/ffmpeg-wasi-intermediate-lgpl.wasm \
  go test ./pkg/afmpeg/ -run Integration -v

Both are published on every ffmpeg-wasi release.

Backend B (the native driver)

The pkg/afmpeg/native tests drive a native driver binary rather than a WASM module, and a driver varies on two independent axes: the capability profile and the licence variant:

Variable Driver
AFMPEG_TEST_NATIVE_DRIVER lean / lgpl
AFMPEG_TEST_NATIVE_DRIVER_GPL lean / gpl
AFMPEG_TEST_NATIVE_DRIVER_INTERMEDIATE intermediate / lgpl
AFMPEG_TEST_NATIVE_DRIVER_INTERMEDIATE_GPL intermediate / gpl
AFMPEG_TEST_NATIVE_DRIVER_FULL full / lgpl
AFMPEG_TEST_NATIVE_DRIVER_FULL_GPL full / gpl

Both axes are ordered, so a richer driver satisfies a poorer requirement and the resolver picks the least-rich adequate one supplied. In practice one intermediate/gpl driver runs every native test, because gpl is a superset of lgpl and intermediate of lean.

The variant axis is not cosmetic: cropdetect carries cropdetect_filter_deps="gpl" upstream, so it is absent from every lgpl build no matter how rich the profile, as is the libx264 encoder. A test needing either skips on an lgpl driver rather than failing with a message about a missing filter.

Put these in a .env (the justfile sets dotenv-load) and just test-integration picks them up. Full is native-only, per spec 0022 §4: there is no WASM-full module.

The runtime provides the env setjmp/longjmp host module and the WebAssembly feature set a real FFmpeg build needs (spec 0004 R-0004-9), so a released ffmpeg-wasi engine (spec 0007) loads and runs.

Option names in the docs

A wrong option name in an example compiles, lints clean and fails only at runtime, so two checks cover the names docs/ and the README teach (afmpeg#11):

  • The lexical check in internal/docsoptions reads every key set through EncoderOption, VideoOption, AudioOption, SubtitleOption, FormatOption, DemuxerOption or one of the Options maps, and fails on a key with a : in it (b:v) or on a muxer-only name in an encoder dictionary and the reverse (movflags). It runs in just test, just check (as just lint-docs-options) and in every MR pipeline that touches the docs. It cannot see a name that is merely wrong, such as crff.
  • The driver check, TestIntegration_DocumentedOptionsAreAccepted in pkg/afmpeg/native, offers each key to the encoder, muxer or demuxer the docs set it on and requires the job to succeed, which catches crff too. It is gated like the other native tests, so the MR pipeline never runs it. One full/gpl driver covers every example:
AFMPEG_TEST_NATIVE_DRIVER_FULL_GPL=/path/to/ffmpeg-wasi-driver-linux-amd64-full-gpl \
  go test ./pkg/afmpeg/native/ -run DocumentedOptions -v

A poorer driver runs what it can and skips the rest, naming the driver each example needs.