pure go · in memory · no cgo
afmpeg
FFmpeg from Go, and the disk never finds out.
A pure-Go FFmpeg library that runs on a virtual filesystem. FFmpeg arrives
as a separate WebAssembly module, runs under wazero,
and every file it opens is answered out of an afero.Fs you hand it.
So inputs and outputs live in memory (or any afero backend), nothing is
installed on the host, no temp files are written, and the whole program
cross-compiles to one static binary.
Runs where the files are not
The vfs bridge routes the guest's filesystem calls to a sys.FS backed by your afero.Fs. An in-memory project, a remote cloned into RAM, an S3 bucket: FFmpeg reads and writes it without a byte touching host disk.
No CGO, no install
The engine is a WebAssembly module fetched at first use and verified against a signed release. Your binary stays statically linked and cross-compiles as it always did.
A typed command builder
Compose inputs, filters, outputs, seeks and metadata as Go values with Command, and hand the resulting job spec to the engine. No argument strings to get subtly wrong.
Progress on a channel
A running job reports completion, frames, media time and encode speed as it goes, so a long transcode can drive a progress bar or a cancellation instead of going quiet.
Capped, deadlined, serialised
A Runtime is bounded by default: memory capped, calls deadlined, invocations serialised. Media from strangers runs inside a sandbox with the limits already set.
A native backend when speed wins
The same engine as a signed native subprocess, behind the same API and the same afero I/O, for native-speed encode and the full profile's HEVC and AV1. Opt in per Runtime.
Why it exists¶
It was extracted from a need in keryx: keryx renders short reels by shelling out to
the ffmpeg binary, which needs real files on disk, so it can't render an in-memory
project (a remote cloned into RAM, no local checkout). Every existing Go option was
rejected: purego bindings are immature and still need host libav; CGO bindings break a
clean static cross-compile; and the spike found the stock wazero/WASM builds short of the
filters and codecs keryx needed (it names xfade and AAC), with their I/O going to the
host filesystem rather than to one you supply. afmpeg closes both gaps: an FFmpeg-WASM
build we maintain and configure (xfade and aac are in the lean profile), and an I/O
layer that answers the guest's filesystem calls out of an afero.Fs you hand it. keryx
now renders its reels through afmpeg, in-memory and pure Go, with no local checkout.
How it works¶
Three layers. The middle one is where the work is:
- The FFmpeg-WASM module: current FFmpeg compiled to
wasm32-wasi, configured to only the codecs/filters needed; shipped as a separate artifact by ffmpeg-wasi, never embedded. - The afero ↔ wazero vfs bridge (the heart): routes the guest ffmpeg's WASI
filesystem syscalls to a
sys.FSbacked by the caller'safero.Fs, so reads and writes hit an in-memory filesystem with no host disk touched. - The Go API: compile the module once into a reusable
Runtime, thenRunan ffmpeg invocation over a suppliedafero.Fs; a general command builder layers on top.
The runtime sits behind a backend seam (spec 0028): the WASM module is the default, and an opt-in native backend runs the same engine as a native subprocess for native-speed encode and HEVC/AV1. Same API, same afero I/O, no CGO.
See the architecture explainer for the full flow, and the roadmap for how the specs decompose the build.
Where to go next¶
- Tutorials: a working program in about fifteen minutes.
- How-to guides: solve a specific task.
- Explanation: the architecture, and why it is that shape.
- Reference: every option, field, default and limit.
- Development: contributor docs.
Status¶
Released. afmpeg runs real FFmpeg over a virtual filesystem today: the
vfs bridge, the runtime
(New/Run/RunJob/Probe/Close), the Command builder (JobSpec()/RunJob for
the ffmpeg-wasi engine), and both
signature-verified WithModuleRelease and bring-your-own
WithModuleURL module acquisition. Pair it with a released
ffmpeg-wasi module to transcode,
remux, clip, filter, burn in subtitles, edit metadata, extract frames, and read
analysis-filter measurements (ProcessResult.Analysis), entirely in memory. The current
version is whatever is at the top of
the releases page; design rationale
is in the specs under Development, starting with
0001.
Further reading¶
Everything written about the estate, including the curated guides, is on the blog.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.