Skip to content

contrib/avcodec: in-process video decode, plus the CI wiring to actually gate it - #1472

Merged
paul-hammant merged 3 commits into
mainfrom
feat/contrib-avcodec
Aug 9, 2026
Merged

contrib/avcodec: in-process video decode, plus the CI wiring to actually gate it#1472
paul-hammant merged 3 commits into
mainfrom
feat/contrib-avcodec

Conversation

@paul-hammant

Copy link
Copy Markdown
Collaborator

contrib/avcodec — in-process FFmpeg video decode, so a renderer no longer has to transcode a whole clip to disk first. Aim #1 on aether-ui's video roadmap.

The module is Paul's work (commit 1); commits 2–3 are the shaping-up.

What the module does

A thin FFmpeg veneer following contrib/sqlite: C shim + module.ae + a catalogue entry with a pkg-config probe. Nothing vendored — user programs link -lavcodec -lavformat -lavutil -lswscale via their aether.toml.

Before, aether-ui's video_frame spawned ffmpeg to transcode a clip to raw RGBA on disk and fs.pread'd frames back: 27.6 MB for 6s of 320×240, ~1.5 GB per minute of 1080p, and no workaround at all for a live source (camera, network stream) since there is no file to pread. Now 27.6 MB → 0.

Surface is deliberately narrow — open, next frame as packed RGBA8888, close. Two ways to take a frame, mirroring sqlite's blob accessors: next_frame allocates a fresh owned string; next_frame_into writes into a caller-owned buffer, allocating nothing per frame (at 1080p30 that is 250 MB/s of churn avoided). fps() returns a ratio, not a float, because 30000/1001 does not survive a float round-trip.

The CI gate could never have passed

The module arrived with a good test — self-generating fixture via ffmpeg, clean SKIP when the binary is absent, and a sharp comment noting that the undersized-buffer assertion passed even with both capacity guards deleted (because the decoder was already at EOF), so it opens a fresh decoder to test a real refusal.

But its contrib_check.sh entry could not run. On a box with all four FFmpeg dev libraries installed:

FAIL  avcodec/decode  (build)
undefined reference to `avcodec_receive_frame'

The runner builds each test with ae build --extra <shim.c>, which compiles the shim but has no way to pass -l flags. So any module backed by a system library compiles and then dies at link. The entry was wired in but structurally incapable of running.

Fix: an optional fifth column naming the pkg-config modules a test must link against. When set, the runner stages an aether.toml workspace carrying link_flags — the same shape tests/integration/sqlite_roundtrip already uses, which is where ae's get_link_flags() picks them up — and SKIPs when pkg-config cannot find them, since an absent FFmpeg is a provisioning gap rather than a code defect.

I first tried @link(...) in module.ae (following sqlite's declaration). That does not work for this path and was reverted — // aether-link: is emitted by codegen as advice for a downstream build, not consumed by ae build.

Verification

Both paths, on this box:

with FFmpeg:     PASS  avcodec/decode  (run)      + all 6 existing contrib tests
without:         SKIP  avcodec/decode  (pkg-config: ... not found)

The module itself passes all six assertions — open + geometry, frame_bytes = w·h·4, fps ratio 10/1, a full RGBA frame, 20 frames decoded to EOF via the reused buffer, and the undersized-buffer refusal leaving the decoder usable.

make ci — C suite 230/230, .ae 975/977. Both failures are pre-existing and unrelated: integration_http_server_h2 (diagnosed in pesky_bug.md — an h2 connection-reuse bug, not the "50-stream" concurrency issue its name suggests) and http_middleware_d1, which passes in isolation and is the known load-sensitive flake.

Also

TODO.md gains a parked item: contrib/sqlite is covered by tests/integration/sqlite_roundtrip/ rather than contrib_check.sh, so contrib runtime coverage lives in two places with different shapes. The fix above means the table can now do generically what that shell driver does by hand — worth consolidating, but it rewrites a working test for no new coverage, so it is parked with the concrete next step written down.

🤖 Generated with Claude Code

paul-hammant and others added 3 commits August 9, 2026 17:37
Aim #1 on aether-ui's video roadmap. A thin FFmpeg veneer following
contrib/sqlite exactly: C shim + module.ae + a catalogue entry with a
pkg-config probe, nothing vendored, user programs link -lavcodec
-lavformat -lavutil -lswscale via aether.toml.

Before, aether-ui's video_frame spawned ffmpeg to transcode a whole clip
to raw RGBA on disk and fs.pread'd frames back: 27.6 MB for 6s of
320x240, ~1.5 GB per minute of 1080p, and NO workaround at all for a
live source (camera, network stream) since there is no file to pread.

Now: 27.6 MB -> 0. Measured 300 frames of 640x480 decoded in 0.426s
including compile time.

Surface is deliberately narrow -- open, next frame as packed RGBA8888,
close. Video only; audio, seeking and stream selection are future work
and none are needed to feed a renderer.

Two ways to take a frame, mirroring sqlite's blob accessors:
next_frame allocates a fresh owned string (simple, fine at small sizes);
next_frame_into writes into a caller-owned buffer, allocating nothing
per frame -- at 1080p30 that is 250 MB/s of churn avoided. fps() returns
a RATIO rather than a float because 30000/1001 does not survive a float
round-trip, and a presentation-timestamp model wants the exact value.

The test's undersized-buffer assertion took two goes, and the first
version is worth recording: it ran the decoder to EOF first, where
next_frame_into returns 0 whatever the capacity -- so it passed with
BOTH capacity guards deleted. Rewritten against a fresh decoder it
catches the real thing: the sabotaged build returned m2=12288, writing
12 KB into a 16-byte allocation. It now also checks the decoder still
works afterwards, so a refusal cannot silently consume the frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ly runs

The avcodec entry added alongside the module could never pass. `make
contrib-check` reported:

    FAIL  avcodec/decode         (build)
    collect2: error: ld returned 1 exit status
    undefined reference to `avcodec_receive_frame'

...on a box with all four FFmpeg dev libraries installed. The runner builds each
test with `ae build --extra <shim.c>`, which compiles the shim but has NO way to
pass -l flags, so any module backed by a system library compiles and then dies
at link. The entry was wired in but structurally incapable of running.

The module itself is fine — this is purely the CI wiring. Proven by building the
same test through an aether.toml workspace, where all six assertions pass:
open + geometry, frame_bytes, fps ratio 10/1, a full RGBA frame, 20 frames
decoded to EOF via the reused buffer, and the undersized-buffer refusal leaving
the decoder usable.

Added an optional fifth column naming the pkg-config modules a test must link
against. When set, the runner stages an aether.toml workspace carrying
link_flags — the same shape tests/integration/sqlite_roundtrip already uses,
which is where ae's get_link_flags() picks them up — and SKIPS the entry when
pkg-config cannot find the modules, since an absent FFmpeg is a provisioning gap
on the box rather than a code defect.

Both paths verified here:
  with FFmpeg:    PASS  avcodec/decode  (run)   + all six existing tests
  without:        SKIP  avcodec/decode  (pkg-config: ... not found)

Note contrib/sqlite has the same shape and is NOT in this table; it is covered
by tests/integration/sqlite_roundtrip instead. Worth folding in later so contrib
runtime coverage lives in one place, but that is a separate change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… avcodec

contrib/sqlite is covered by tests/integration/sqlite_roundtrip/, not by
.github/scripts/contrib_check.sh — so contrib runtime coverage lives in two
places with different shapes and nothing says which a new module should use.

This surfaced fixing the avcodec entry: it had been added to contrib_check.sh
but could never pass, because the runner builds with `ae build --extra shim.c`,
which compiles a C shim but cannot pass -l flags. Any module backed by a system
library compiled and then died at link. The fix taught contrib_check.sh a fifth
column naming pkg-config modules, staging an aether.toml workspace with
link_flags — which turns out to be exactly what sqlite_roundtrip had been doing
by hand in its own shell driver all along.

So the two mechanisms now overlap, and the table can do generically what the
shell driver does specifically. Parked rather than done: sqlite IS tested today,
consolidating means rewriting a working test for no new coverage, and it needs a
contrib/sqlite/test_sqlite.ae that does not exist yet (probe.ae is written for
the workspace harness). Worth doing when someone next adds a native-backed
contrib module and has to pick between the two patterns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@paul-hammant
paul-hammant merged commit 1cbe421 into main Aug 9, 2026
24 of 25 checks passed
@paul-hammant
paul-hammant deleted the feat/contrib-avcodec branch August 9, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant