0033 — native progress side-channel (0031 phase B for Backend B)¶
Status: DEFERRED (draft retained as the decision record) — considered 2026-07-16 and not
scheduled: the value for the native backend is marginal and the cost (engine change + a new signed
native-driver release) is disproportionate. Trigger to revive in §6. Do not implement until it is
revived and moved to approved.
Date: 2026-07-16
Parent: 0032 (this completes its deferred native half — 0032
§Scope "Native backend parity … the record schema is shared so native can adopt it later", D7);
0028 (the native driver + IPC host this extends);
0031 (the Progress stream both phases feed)
Owns: R-PROGRESS-B-NATIVE — engine-emitted frame/time/speed progress for the native backend,
on the same WithProgress channel as WASM.
1. Why¶
0032 shipped phase B for the WASM backend: the engine
writes NDJSON {frame,out_time_us,total_size,duration_us} records to a /dev/afmpeg-progress device
that afmpeg's vfs serves, and the host merges them onto WithProgress. It explicitly deferred the
native backend (0032 §Scope, D7): "this spec is the WASM side. The record schema is shared so
native can adopt it later."
Today the native backend delivers phase A only (byte progress observed at the afero.Fs
boundary). Frame/OutTime/Speed stay zero. The reason is structural, not a bug:
ffmpeg-wasi/src/progress.copens the device with a raw POSIXopen("/dev/afmpeg-progress"), not through libav's AVIO.- The native IPC bridge (
nativeio.c) only wraps AVIO (media file I/O). A rawopen()in the native driver therefore hits the real host filesystem, finds no such file, and the emitter goes inert (fd < 0, best-effort no-op).
So the native driver emits nothing over any host-visible channel. Closing the gap needs a native transport for the records — a cross-repo change (engine + host), which is why 0032 deferred it.
2. Scope¶
In:
- ffmpeg-wasi (engine): in the native build, progress.c emits its existing records over a
host-provided transport instead of the raw device path. No record-schema change (0032's schema is
reused verbatim). Needs a native-driver rebuild + a new release tag.
- afmpeg (host): the native backend provisions that transport when WithProgress is active,
reads the records, and feeds the existing progressReporter engine sink — the same merge path
and Progress fields as WASM.
Out:
- The record schema, the Progress public type, and the WithProgress API — all unchanged (0031
§6.1 / 0032). This spec adds a transport, not a surface.
- linux/arm64 / darwin/arm64 native drivers (still gated on the cross-build toolchains, 0022).
- Any change to WASM phase B.
3. Decisions¶
- D1 — transport (OPEN). The native driver emits records over a dedicated Unix socket whose
path afmpeg passes in the env var
AFMPEG_NATIVE_PROGRESS_SOCKET, mirroringAFMPEG_NATIVE_SOCKET.progress.c's native branch connects once and writes the NDJSON lines; the host reads lines and feeds the sink. - Rejected — multiplex over the existing
io.sock: that socket is one-connection-per-opened-file and framed for AVIO (O/R/W/S/Z/C); interleaving a byte-stream of progress lines would complicate the framing and couple a best-effort channel to the critical I/O path. A dedicated, independent socket keeps progress strictly best-effort and out of the I/O framing. - Rejected — a second framed protocol: progress is a one-way append stream; newline-delimited JSON over a bare socket is sufficient and matches the WASM device's line semantics exactly.
- D2 — opt-in & best-effort. afmpeg provisions the socket (and sets the env) only when
WithProgressis active (the same condition that already stampsprogress:true). With the env unset,progress.c's native branch is inert. Any socket error — connect, write, host gone — is swallowed; progress I/O must never block, slow, or fail the encode (identical contract to the WASM device). - D3 — host seam (OPEN). The per-invocation sink lives in
ctx(attached bystartProgress→withProgressSink, unexported in packageafmpeg). The native backend is a separate package (pkg/afmpeg/native), so it needs an exported accessor —afmpeg.ProgressSinkFromContext(ctx) func([]byte)(returns nil when no channel is attached). The nativeBackend.Invokereads it, and when non-nil provisions the progress socket for that run. - Rejected — a method on the
Backendinterface: the sink is per-invocation, not per-construction; threading it throughInvoke's existingctxavoids widening the interface and matches how the WASM backend already discovers the sink. - D4 — schema reuse. The records are byte-for-byte 0032's
{frame,out_time_us,total_size,duration_us}, parsed by the sameengineRecord/noteEnginepath. Native therefore gains identicalFrame/OutTime/host-derivedSpeed, and the accurateout_time/durationFractionon drivers built from theduration_us-emitting engine. - D5 — driver version floor. Native phase-B records only flow from a driver built after this lands. Older native drivers stay phase-A-only (the env is simply ignored). afmpeg does not hard-gate on it — absence degrades cleanly to byte progress, exactly like a pre-v9 WASM engine.
4. Open questions¶
- Q1 (D1): dedicated socket vs. reusing
io.sockwith a distinguished sentinel name (e.g. the host special-cases an Open of/dev/afmpeg-progressand turns that one connection into the progress stream). The sentinel route reuses the existing dial/serve loop and needs no new env var — but it does requireprogress.c's native branch to route through AVIO rather than rawopen(). Decide before implementing. - Q2 (D3): exact exported name/location of the sink accessor (
ProgressSinkFromContextvs. folding it into a smallbackendContexthelper). Cosmetic; settle at review. - Q3: does native
Speedwant the driver's own wall-clock (it has a real monotonic clock, unlike the clockless WASI engine) instead of the host-derivedOutTime/Elapsed? Cheap to add; decide whether it's worth diverging the two backends'Speedderivation.
5. Resolved¶
- 2026-07-16 — deferred, not scheduled (the whole spec). Reviewed the value/cost with the user.
The native backend already delivers phase-A byte progress (
Fraction/InputBytes/OutputBytes) entirely host-side, no engine change — that is the progress native was designed to have, and it ships today. Phase B adds onlyFrame/OutTime/Speedand an accurateFractionfor generative/lavfi inputs. Because native runs 48–58× faster than WASM, fine-grained live progress matters far less, and the sole real gain (a realFractionon long generative native jobs) is narrow. Weighed against an engine change toprogress.c, a native-driver rebuild, and a new signed ffmpeg-wasi release, the cost is disproportionate. Kept as the decision record so the option isn't re-litigated from scratch. D1/Q1 (transport) left open — decide only if revived.
6. Revival trigger¶
Pull this only if a real consumer need appears — specifically a long-running generative
(lavfi, no input file) job on the native backend where the phase-A byte Fraction is -1 and
the user needs a determinate bar or a Frame/OutTime/Speed readout. If that surfaces: resolve
D1/Q1 (transport), move to approved, then implement (engine + host + a new native-driver release).
Absent that trigger, phase-A byte progress remains the native contract (documented in
watch-job-progress §Limits).