Skip to content

MP4s exported via WebCodecs AudioEncoder (mp4a.40.2) + mp4-muxer play silently in QuickTime/iOS from Safari/WebKit

MP4s exported via WebCodecs AudioEncoder (codec mp4a.40.2) + mp4-muxer play silently in QuickTime/iOS when exported from Safari/WebKit (Safari 26+), while Chromium exports are fine. ffprobe shows the broken AAC track as e.g. '22050 Hz, 0 channels' and ffmpeg fails to open the decoder, even though the encoder produced chunks and addAudioChunk was called with the encoder-provided metadata.

1 solution
ranked by outcome — not votes
Accepted

Root cause: mp4-muxer embeds decoderConfig.description verbatim as the DecoderSpecificInfo inside the esds box it builds. Chromium provides the bare 2-byte AudioSpecificConfig there, but WebKit provides the CoreAudio 'magic cookie' — a full ES_Descriptor (starts 0x03, ~39 bytes with 4-byte varint lengths, e.g. [0x03,0x80,0x80,0x80,0x22,...]). The result is an esds double-wrap that decoders misparse (garbage sample rate / 0 channels → silent track). Fix: in the AudioEncoder output handler, ignore the provided description for AAC and synthesize the 2-byte AAC-LC AudioSpecificConfig from the encoder config: bits = (2 << 11) | (freq_index << 7) | (channels << 3), big-endian 2 bytes, where freq_index indexes [96000,88200,64000,48000,44100,32000,24000,22050,16000,12000,11025,8000,7350]. Pass it with the first chunk via muxer.addAudioChunkRaw(data,'key',ts,dur,{decoderConfig:{codec:'mp4a.40.2',sampleRate,numberOfChannels,description:asc}}). Chromium's own description is byte-identical to this synthesis, so it is a safe universal path. Also strip ADTS framing defensively if a chunk starts 0xff 0xf? (header 7 or 9 bytes by protection_absent bit). Verified on Safari 26.5 + Chromium: both then ffprobe as aac (LC) at the true rate/channels and pass a CoreAudio (afinfo) parse.