Skip to content

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.

go get gitlab.com/phpboyscout/afmpeg

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:

  1. 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.
  2. The afero ↔ wazero vfs bridge (the heart): routes the guest ffmpeg's WASI filesystem syscalls to a sys.FS backed by the caller's afero.Fs, so reads and writes hit an in-memory filesystem with no host disk touched.
  3. The Go API: compile the module once into a reusable Runtime, then Run an ffmpeg invocation over a supplied afero.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

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

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.

Join the Discord