Skip to content

Architecture

afmpeg is three layers. The middle one is the novel engineering — everything else is wiring around it.

   caller's afero.Fs                 pkg/afmpeg (the Go API)
   (MemMapFs / OsFs / …)        New → compile module once; Run/Probe per call
          │                                   │
          ▼                                   ▼
   internal/vfs  ────────────►  the wasm backend  ────────────►  ffmpeg.wasm
   afero.Fs → wazero            (pkg/afmpeg, wazero               (FFmpeg + openh264
   experimental/sys.FS           runtime + mounts)                  /x264, wasm32-wasi)
          ▲                                                              │
          └──────────────── WASI fs syscalls (path_open, fd_read, ──────┘
                            fd_write, fd_seek, …) routed to the afero.Fs

1. The FFmpeg-WASM module

FFmpeg and its dependencies (openh264/x264, …) compiled to wasm32-wasi, configured down to only the codecs/filters real workflows need. It is produced by a reproducible build pipeline and — per the licensing decision — shipped as a separate downloadable artifact, not //go:embed-ed, so the GPL obligation stays at arm's length from the permissively licensed Go package. See spec 0002.

2. The afero ↔ wazero vfs bridge (the heart)

ffmpeg-in-the-guest issues WASI filesystem syscalls. wazero routes them to a mounted experimental/sys.FS. afmpeg implements that sys.FS backed by an afero.Fs — so the guest's reads and writes hit the caller's filesystem (e.g. an in-memory MemMapFs) with no host disk touched. It also provides a writable /tmp and /dev/null the guest needs, and must handle seek-on-write (the mp4 muxer rewrites the moov atom under +faststart). This is what every other Go ffmpeg-wasm binding lacks. See spec 0003.

3. The Go API

New compiles the module once (the expensive step) into a reusable Runtime. Run mounts a caller-supplied afero.Fs, runs the module with the given args, and returns the exit code + captured stdout/stderr; RunJob/Command.JobSpec render a job for the ffmpeg-wasi engine, and Probe reports a file's container, duration, and streams over the same bridge. The use-case-agnostic command builder layers on top (a consumer's reel/timeline is built on it, in the consumer's code). See specs 0004 and 0005.

4. The backend seam (WASM default · native opt-in)

Run/Probe/Frames sit behind a small backend interface (spec 0028). The default backend is the WASM path above (wazero + the vfs bridge). An opt-in native backend (pkg/afmpeg/native, wired with WithBackend) satisfies the same seam differently: it spawns the same libav-direct engine compiled to a native ELF as a subprocess and serves the caller's afero.Fs to it over a Unix-socket IPC bridge — a framed read/write/seek protocol that carries even the muxer's backward seeks, so I/O still never touches host disk. It is CGO-free (a subprocess, not a linked library, so the licensing arm's-length holds) and gives threads + SIMD: native-speed software encode, and the full profile's HEVC/AV1 encoders that are impractical in WASM. The Go API, the job spec, and the results are identical — only the runtime underneath changes. The signed driver is acquired and verified with native.NewFromRelease, exactly as WithModuleRelease verifies a .wasm. See the native backend how-to.

Why this shape

The alternatives — purego/dlopen bindings (immature, still need host libav), CGO libav bindings (break a clean static cross-compile), and the stock wazero binding (missing filters/AAC, not filesystem-virtualised) — each failed at least one of pure-Go, in-memory, or has-the-codecs-we-need. afmpeg is the synthesis that holds all three. When native speed or HW-class codecs (HEVC/AV1) are required, the native subprocess backend (§4) is the sanctioned escape hatch — the same engine, out-of-process and signed, rather than reaching for the CGO libav binding this design set out to avoid. The full reasoning is in spec 0001 §11.