Repository navigation
Description
Problem
detectMediaType in packages/ai/src/utils/media-type.ts:20 treats any ISO-BMFF file as video:
if (bytes.length >= 12 && ascii(bytes, 4, 8) === "ftyp") return "video/mp4"It never reads the major brand at bytes 8–12. That brand is what distinguishes the containers that share the ftyp box:
M4A/M4B— audio-only MP4 → should beaudio/mp4avif/avis— AVIF image → should beimage/avifheic/heix/mif1(HEIF) — HEIC/HEIF image → should beimage/heic/image/heifisom/mp41/mp42/avc1/dashetc. — genuine video MP4 →video/mp43gp*— 3GPP video →video/3gpp
Because sniffing wins over the file extension (fileMediaType, media-type.ts:57: detectMediaType(bytes) ?? extensionMediaType(path)), even a correctly named song.m4a or photo.avif file is published with the wrong media type.
Evidence (verified this run, main 87c05e11db)
Probe probe-media-ftyp.ts (laptop ~/git/fleet/probe-2026-11-08-creator-media-ftyp/, run with bun against the real packages/ai/src/utils/media-type.ts on the laptop primary at 87c05e11db), constructing 12-byte ftyp headers per brand:
M4A audio brand detect= video/mp4 file= video/mp4 (path song.m4a)
M4B audiobook brand detect= video/mp4 file= video/mp4 (path book.m4b)
avif image brand detect= video/mp4 file= video/mp4 (path photo.avif)
heic image brand detect= video/mp4 file= video/mp4 (path photo.heic)
isom video brand detect= video/mp4 file= video/mp4 (path clip.mp4) ← correct
mp42 video brand detect= video/mp4 file= video/mp4 (path clip.mp4) ← correct
Note the extension table cannot rescue the audio/image cases even where it knows them: EXTENSIONS in the same file has no avif/heic entry, and the sniff result pre-empts the m4a → audio/mp4 entry that does exist.
Why it matters
The sniffed type is authoritative at every call site:
packages/ai/src/media.ts:140— bytes-source assets takedetectMediaType(...)firstpackages/ai/src/media.ts:323—Media.bytesdefaults to the sniffed typepackages/ai/src/media.ts:361andpackages/ai/src/promise.ts:116— file loads viafileMediaTypepackages/ai/src/protocols/meta-responses.ts:130— the sniffed type (or octet-stream) is what is sent on the wire
So an M4A voice note / audiobook or an iPhone HEIC / AVIF photo handed to a provider is labelled video/mp4: audio routes can reject or mishandle it, image routes never see it as an image, and downstream mediaTypeExtension / kind logic inherits the wrong kind. This is the media façade's own sniffing contract — the doc comment at media-type.ts:8-9 promises the type "of a file's contents", and for these brands it returns a type the contents are not.
Acceptance criteria
-
detectMediaTypedispatches on theftypmajor brand (bytes 8–12): at minimumM4A/M4B→audio/mp4,avif/avis→image/avif,heic/heix(and HEIFmif1if claimed) → the matching image type; genuine MP4 brands (isom,mp41,mp42,avc1, …) still →video/mp4. - An unrecognized
ftypbrand does not silently becomevideo/mp4— it falls through toundefined(sofileMediaTypecan use the extension) or the chosen fallback is stated in the code comment and pinned by a test. -
fileMediaType(m4aBytes, "song.m4a")returnsaudio/mp4andfileMediaType(avifBytes, "photo.avif")returnsimage/avif(addavif/heictoEXTENSIONSif the brand dispatch does not already cover extension-only cases). - Tests in
packages/ai/test/media.test.tspin each brand family above plus the unknown-brand case, alongside the existing magic-byte cases; existing media tests still pass. - No other sniffer branch (PNG/JPEG/GIF/RIFF/EBML/frame-sync/…) changes behaviour.
Dedup: searched open + closed issues for ftyp / detectMediaType / media-type sniffing on 2026-11-08 — the only hit is closed #906 (prototype-chain lookups in the same file, fixed with Object.hasOwn); no open or closed issue covers the ftyp brand dispatch.
Activity
developing-today commentedon Nov 8, 2026
🤖 Agent note · filed
detectMediaType(packages/ai/src/utils/media-type.ts:20) returnsvideo/mp4for anyftypbox and never reads the major brand at bytes 8–12, so M4A/M4B audio and AVIF/HEIC images are published as video, and the sniff pre-empts the correct extension infileMediaType. Decide the brand table (audio/image/video families + unknown-brand fallback) and pin it with tests.ftypbrands no longer silently becomevideo/mp4,fileMediaTypereturnsaudio/mp4for an .m4a andimage/aviffor an .avif, and media.test.ts pins each brand family (see acceptance criteria on issue fix(ai): media-type sniffer labels every ISO-BMFF file video/mp4 — the ftyp major brand is never read, so M4A/M4B audio and AVIF/HEIC images are published as video #2127).developing-today commentedon Nov 8, 2026
Triage verdict (task 13770, lane opencode-a): REAL on current main de7b444. Probe in packages/ai: a 12-byte ftyp header with the M4A major brand returns detectMediaType = video/mp4 (expected audio/mp4), and fileMediaType lets the sniff pre-empt the m4a extension entry; media-type.ts has a single ftyp branch and no avif/heic EXTENSIONS entries, exactly as filed. Fix enqueued as task 13884 (small_fix, acceptance per this issue).
agent-note:v1 lane=opencode-a task=13770 state=triaged follow_on=13884
dezren39 commentedon Nov 8, 2026
🤖 Agent note · claimed
dezren39 commentedon Nov 8, 2026
🤖 Agent note · parked
Merge pull request #2144 from developing-today-forks/queue/13884-2127…
dezren39 commentedon Nov 8, 2026
🤖 Agent note · done — ftyp major-brand dispatch merged
391e43eb3675fb967b0ee89a86a161f222287553; this issue auto-closed COMPLETED by the PR.detectMediaTypenow dispatches on the ftyp major brand (bytes 8–12):M4A /M4B /M4P→audio/mp4,avif/avis→image/avif,heic/heix→image/heic,mif1/msf1→image/heif, genuine MP4 brands (isom/iso2/iso5/iso6/mp41/mp42/avc1/dash/M4V) →video/mp4; an unrecognized brand falls through toundefinedinstead of silently claimingvideo/mp4.EXTENSIONSgainedavif/heic/heif. No other sniffer branch changed.bun test test/media.test.ts(packages/ai) — 12 pass, new brand test 14 expects (each family + unknown brand +fileMediaTypeforsong.m4a/photo.avif/photo.heic); prettier clean; no Codex findings on PR fix(ai): dispatch media-type sniffer on ftyp major brand (#2127) #2144 (no reviews posted).