0020 — metadata & chapters¶
Status: IMPLEMENTED (Phase 3 of the implementation roadmap.
Read side: probe (ffmpeg-wasi driver.c) now reports container tags/chapters and per-stream
tags/disposition/language, additively. Write side: process (process.c) accepts
outputs[].metadata (container tags), outputs[].chapters ("copy"/index passthrough deep-copy),
and outputs[].stream_metadata (per-map language/disposition/tags), threaded into the muxer
before write_header; the copy path now carries source disposition + tags, so attached_pic cover
art survives (D-0020-C). Shared glue in a new src/meta.c (disposition table, tag/disposition ↔
JSON) used by both ops. afmpeg's Probe/ProbeStream gain Tags/Chapters/Disposition/
Language; Output gains Metadata/Chapters/StreamMetadata (+ the StreamMeta type). Bumps
vocab to v7. Proven by TestIntegration_Metadata (container tags + per-stream language/
disposition write→read) + emitter unit tests. Chapter authoring (Q2) stays deferred, and the
chapter-copy round-trip is emission-tested only — no self-bootstrappable chaptered fixture, so its
end-to-end verification is corpus-gated like the 0016 decode-only codecs.)
Date: 2026-06-30
Parent: 0012 §4F (the metadata row) / §5 (Tier 1–2); 0007 (the engine + the job-spec contract)
Owns: R-PARITY-METADATA
1. Why¶
The engine moves pixels and samples but drops everything about them. probe reports codecs
and duration but not the title/artist/album tags, the chapter list, per-stream language, or the
disposition flags (default, forced, attached_pic) a consumer needs to make decisions; and
process writes output with no way to set tags, carry chapters across a transcode, or preserve a
stream's language/role. A podcast loses its chapters, an MP3 loses its cover art, a multi-language
file loses which audio track is the default. Every piece of this is a native libav metadata API
(av_dict_*, AVChapter, AVStream->disposition) — zero new library, near-zero .wasm cost —
so it is the cheapest "feels complete" jump in the batch. This spec owns the read extension to
probe and the write vocabulary on process for tags, chapters, disposition, language, and
cover art.
2. Scope¶
In — read side (extend op:"probe"):
- Container + per-stream metadata tags —
AVFormatContext->metadataand eachAVStream->metadata(av_dict), surfaced as a JSON tag map. - Chapters —
AVFormatContext->chapters[]:start/end(in seconds, viatime_base) + the chapter'stitletag. - Per-stream disposition — the
AV_DISPOSITION_*bitfield decoded to named booleans (default,forced,attached_pic, …) and language (thelanguagetag), on each stream.
In — write side (op:"process"):
- Set the output container tags and per-stream tags from the job-spec before
avformat_write_header. - Copy chapters through a transcode (a passthrough directive — read input chapters, write them
on the output
AVFormatContext). - Preserve / override disposition + language on output streams.
- Attached pictures / cover art — an
attached_picimage stream (album art in mp3/mp4) carried through as a copied image stream, not run through the filtergraph.
Out (referenced, not owned):
- Stream-copy mechanics (
-c copy, packet passthrough, bitstream filters) → 0013. Cover-art passthrough and chapter carry rely on 0013's copy path; we own the metadata intent, 0013 owns the byte-copy engine. - Subtitle-stream specifics (subtitle codecs/muxers, language on subtitle tracks) → 0019.
We define the generic per-stream
language/dispositionfields; subtitle plumbing is theirs. - Redesigning
probe— the op already exists (job-spec ref); we extenddescribe_stream/probe_input, not rebuild them.
3. Approach / design¶
Read. probe_input already walks AVFormatContext; extend it. Add a tags object built by
iterating av_dict_get(fmt->metadata, "", t, AV_DICT_IGNORE_SUFFIX); add a chapters array over
fmt->chapters[] (rescale start/end by ch->time_base, read the title tag). In
describe_stream, add the stream's own tags, a decoded disposition object (named flags from
st->disposition), and language (the language tag, hoisted for convenience). All read-only,
all native — the demuxer already populated these dicts during avformat_find_stream_info.
Write. process.c builds each output ofmt and its streams, then calls
avformat_write_header. Thread metadata in before that call: av_dict_set the container tags
onto ofmt->metadata, and per-stream tags / language / disposition onto each ost. Chapters
copy by allocating ofmt->chapters from the chosen input's AVChapter array (deep-copy
start/end/time_base + the title dict). Cover art is the wrinkle: an attached_pic input
stream is a single packet, not a continuous track — it must bypass the decode→filter→encode loop
and be stream-copied (0013) onto an output stream that keeps the AV_DISPOSITION_ATTACHED_PIC
flag, or the muxer rejects it / re-encodes a still as video. So cover art = "copy this image stream
+ set its disposition," sitting squarely on 0013's passthrough path.
4. Job-spec & build impact¶
- Build: none —
av_dict_*,AVChapter, disposition flags are corelibavformat/libavutil, always linked. No allowlist change, no flag, no lib. outputs[].metadata(vocabulary, mine). A{key: value}tag map →ofmt->metadata({"title":"…","artist":"…","comment":"…"}).av_dict_seteach beforewrite_header.outputs[].chapters(vocabulary, mine). A passthrough directive —"copy"(or an input index, e.g."0") to carry that input's chapters onto the output; absent/"none"→ drop. Keep it a directive, not an inline chapter-authoring DSL (authoring is an open question, Q2).- Per-stream
metadata/language/disposition(vocabulary, mine). On the output-pad / stream-routing level: a tag map, alanguagestring, and adispositionset (e.g.["default"],["attached_pic"]) applied to the correspondingost. These attach to whicheveroutputs[].mappad they belong to; shape pins in impl (a parallel array vs a per-map object). - Probe-result extension (mine):
inputs[].tags,inputs[].chapters[](start/end/title), and per-streamtags/disposition/language. Purely additive — existing fields unchanged, so an old afmpeg ignores them. - afmpeg side: the
Probe/ProbeStreamstructs (pkg/afmpeg/runtime.go) gainTags,Chapters,Disposition,Language;Command'sOutput(pkg/afmpeg/command.go) gainsMetadata,Chapters, and per-stream metadata fields, serialised injobSpec.
5. Licensing & size impact¶
- Licence: none. All-native, both variants identically. Pure 0012-§3 "expands the default variant" work — proprietary-compatible.
- Size: negligible. The dict/chapter/disposition code is already in the linked
libavformat; this is driver glue (a few KB ofdriver.c/process.c), not new object code. Not a lean/full axis concern — it ships in every build.
6. Decisions & open questions¶
- D-0020-A — metadata is fields on existing ops, not a new op. Read extends
probe; write extendsprocessoutputs[]. Noop:"metadata". Keeps the two-op contract (0007 §4) intact. - D-0020-B — chapters are passthrough-only in v1.
outputs[].chapters: "copy"carries input chapters; authoring chapters from the spec is deferred (Q2). Covers the common case (transcode keeps chapters) cheaply. - D-0020-C — cover art rides 0013's stream-copy path, it is not filtered. An
attached_picstream is copied + flagged, never decoded into the graph. This spec owns the intent (preserve cover art + its disposition); 0013 owns the copy mechanism. Hard dependency on 0013. - D-0020-D —
language/dispositionare generic per-stream fields. Defined here for all stream types; 0019 consumes them for subtitle tracks without redefining them. - Q1 — per-stream metadata shape: a
disposition/language/tagsblock keyed bymappad vs a parallelstreams[]array on the output. Pin in impl. - Q2 — chapter authoring (define chapters inline in the spec, not just copy) — deferred; promote if a consumer needs it.
- Q3 — tag key normalisation: libav normalises some keys per-muxer (e.g.
titlevsTITLEin matroska). Pass keys through verbatim and let the muxer map them, or normalise? Lean to verbatim (libav already maps on write).
7. Requirements¶
- R-PARITY-METADATA —
probesurfaces container + per-stream tags, chapters, disposition, and language;processsets output tags (container + per-stream), copies chapters through, preserves or overrides disposition/language, and carriesattached_piccover art — all native, both variants. - R-0020-1 — probe-result JSON gains
tags/chapters/ per-streamtags/disposition/language, purely additively (old consumers unaffected). - R-0020-2 —
outputs[].metadata+outputs[].chapters+ per-stream metadata are routed intoofmt->metadata/ofmt->chapters/ostbeforeavformat_write_header. - R-0020-3 — cover art (
attached_pic) survives a transcode as a copied, correctly-disposed image stream (via 0013). - R-0020-4 — afmpeg's
Probe/Commandmodels expose the new fields.
8. Test surface¶
- Read (integration, real driver): probe a tagged file (title/artist), a chaptered file, and a
multi-stream file; assert
tags,chapters(start/end/title), per-streamdisposition+language. Probe an MP3 with embedded art → anattached_picstream is reported. - Write round-trips:
processwithoutputs[].metadata→ probe back, assert the tags landed; transcode a chaptered input withchapters:"copy"→ probe back, assert chapter count + titles; set a streamlanguage/disposition:["default"]→ probe back, assert it. - Cover art: transcode an audio file carrying cover art → the output retains the
attached_picimage stream (pairs with the 0013 copy tests). - Fixtures: the 0007 dependency-free corpus (synthetic WAV/PNG) plus a small tags/chapters fixture authored by the engine itself (write tags → read them) — no licensed media needed for the tag/chapter path; cover-art tests reuse a generated PNG as the attached picture.
9. Dependencies & sequencing¶
- Hard dependency on 0013 for the cover-art /
attached_picpassthrough (and any chapter-only remux). The read side and tag/chapter write on a re-encode are independent of 0013 — build read + tag-write first, fold in cover art when 0013 lands. - Feeds 0019 — it reuses the generic
language/dispositionper-stream fields defined here for subtitle tracks. - Independent of the codec/container/filter batches (0015/0016/0017) — metadata is orthogonal to which codecs ship; build in any order.
10. Definition of done¶
probe reports container + per-stream tags, chapters, disposition, and language (additively);
process accepts outputs[].metadata, outputs[].chapters: "copy", and per-stream
language/disposition/tags, routing them into ofmt/ost before write_header; cover art
survives a transcode as a copied attached_pic stream over 0013's path; afmpeg's Probe and
Command models expose the new fields; tests cover read + write round-trips on the dependency-free
corpus. Decisions D-0020-A…D are recorded; the open questions (Q1–Q3) are resolved in
implementation. Stream-copy mechanics and subtitle-stream specifics remain with 0013 and 0019.