Watch job progress¶
Long jobs (a full-file transcode, a large remux) run for seconds or minutes. afmpeg.WithProgress
lets you receive live progress while the job runs — a completion fraction, byte counters, and
(on a v9+ engine) the frame count, media time, and encode speed — so you can drive a progress bar.
It works on any Run, RunJob, or Frames call, needs no special module, and adds nothing to a
job that doesn't ask for it.
How it works¶
afmpeg gathers progress two ways and merges both onto the one channel:
- Phase A — observed filesystem (any module, either backend). afmpeg implements the filesystem
the engine reads and writes through, so it watches bytes flow — input consumed, output produced —
without the engine emitting anything. The completion
Fractionis the input read position (bytes_read / input_size), which tracks a linear demuxer closely. Spec 0031 phase A. - Phase B — engine side-channel (a v9+ ffmpeg-wasi engine, WASM backend). When the module
supports it, setting
progress:truemakes the engine emit NDJSON records to a/dev/afmpeg-progressdevice afmpeg serves — fillingFrame,OutTime, and a host-derivedSpeed. When the engine also reports the media duration (n8.1.2-10+),Fractionis derived fromout_time / duration, accurate even for a generative input with no file to measure. Spec 0032.
Both feed the same Progress value: afmpeg turns on the engine channel automatically when you
attach WithProgress, phase-B records refine the phase-A samples as soon as the first one
arrives, and it falls back to byte progress on an older engine.
Attach a channel with WithProgress¶
Progress is per-invocation — a Runtime is shared and serialises its calls — so you attach the
channel to the call's context, not to New. afmpeg sends on the channel while the job runs and
stops when the call returns; it never closes your channel.
ch := make(chan afmpeg.Progress, 64) // buffered: see back-pressure below
// Drain in a goroutine while the (blocking) Run executes.
go func() {
for p := range ch {
if p.Frame > 0 { // phase B: a v9+ engine is reporting frame / time / speed
fmt.Printf("\rframe=%d t=%s %.1f×realtime", p.Frame, p.OutTime.Round(time.Second), p.Speed)
} else if p.Fraction >= 0 {
fmt.Printf("\r%.0f%% out=%d bytes", p.Fraction*100, p.OutputBytes)
}
}
}()
ctx := afmpeg.WithProgress(context.Background(), ch)
res, err := rt.Run(ctx, fs,
`{"op":"process","inputs":[{"path":"in.mov"}],"outputs":[{"path":"out.mp4","video_codec":"libx264","audio_codec":"aac"}]}`)
close(ch) // safe once Run has returned — afmpeg has stopped sending
if err != nil {
return err
}
The same ctx works with the typed builder and the frames op:
res, err := rt.RunJob(afmpeg.WithProgress(ctx, ch), fs, cmd)
out, err := rt.Frames(afmpeg.WithProgress(ctx, ch), fs, job)
The Progress value¶
type Progress struct {
Fraction float64 // completion in [0,1], or -1 when unknown
Elapsed time.Duration // since the invocation began
InputBytes int64 // bytes read from inputs so far
InputTotal int64 // total input size, 0 if unknown
OutputBytes int64 // bytes written to outputs so far
// Populated once a phase-B engine record has arrived (a v9+ engine on the WASM
// backend); zero before then, and on an engine that emits nothing:
Frame int64 // frames processed
OutTime time.Duration // media timestamp reached
Speed float64 // ×realtime encode speed (host-derived: OutTime/Elapsed)
}
Fraction is clamped to [0,1] and never decreases across a run, even when a demuxer seeks
backwards. Show a determinate bar when Fraction >= 0; fall back to an indeterminate spinner (and
use OutputBytes / Elapsed) when it is -1.
Back-pressure — you cannot slow the job down¶
Delivery is best-effort: afmpeg sends with a non-blocking send, so a slow or non-draining consumer simply misses intermediate samples — the job is never blocked or altered. Give the channel a modest buffer and drain it promptly for the smoothest bar. A job run with a channel nobody reads produces the identical result as one with no channel at all.
Limits¶
Fraction is a completion fraction, not a wall-clock ETA. Its quality depends on what the engine
tells afmpeg:
- Byte-only fallback. Without a phase-B duration (a pre-v9 engine, or the native backend below)
Fractionis byte progress (bytes_read / input_size). Seek-heavy inputs (e.g. an MP4 whose index sits at the end) make it lumpy — it is clamped so it never goes backwards, but it can jump — and sequential multi-input (concat) can plateau as each part completes, since the total to read grows as inputs open. - Generative inputs (a filter source with no input file) report
Fraction == -1on the byte-only path; on a v9 engine at n8.1.2-10+ the engine reports the media duration, soFractionis accurate even there (theout_time / durationderivation). - The native backend (use the native backend) reports phase-A byte
progress (
Fraction,InputBytes,OutputBytes— its IPC I/O crosses the same filesystem boundary), but not the phase-B engine record: itsFrame/OutTime/Speedstay zero, because the/dev/afmpeg-progressdevice is served by the WASM backend only.