fix(spf): retain the muxed audio codec on video tracks without an AUDIO group

When an EXT-X-STREAM-INF lists an audio codec (e.g. CODECS="avc1,mp4a")
but declares no AUDIO group, the audio is muxed into that rendition's
segments. The parser was discarding the audio codec and keeping only the
video codec, so the SourceBuffer mimetype omitted audio and the muxed
segment failed to append (CHUNK_DEMUXER_ERROR_APPEND_FAILED: audio object
type does not match the mimetype).

Retain both codecs in that case so the mimetype matches the muxed media.
The guard keys on an audio codec actually being present in CODECS, not on
the mere absence of an AUDIO group — audioless video (CODECS="avc1")
stays video-only, and demuxed sources (with an AUDIO group) are unchanged.

Verified live: a muxed CMAF source (mediastreamsegmenter) now appends and
plays in SPF where it previously errored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 14:54:40 -07:00
co-authored by Claude Opus 4.8
parent d4b21954bc
commit 434390916f
2 changed files with 30 additions and 5 deletions
@@ -222,7 +222,13 @@ export function parseMultivariantPlaylist(text: string, unresolved: AddressableO
track.height = stream.resolution.height;
}
if (codecs?.video) {
track.codecs = [codecs.video];
// When the STREAM-INF lists an audio codec but declares no AUDIO group,
// the audio is muxed into this rendition's segments — keep both codecs so
// the SourceBuffer mimetype matches the muxed media (otherwise the muxed
// audio fails to append: "audio object type does not match the mimetype").
// With an AUDIO group the audio is a separate rendition, so only the video
// codec belongs here.
track.codecs = codecs.audio && !stream.audioGroupId ? [codecs.video, codecs.audio] : [codecs.video];
}
if (stream.frameRate) {
track.frameRate = stream.frameRate;
@@ -31,7 +31,9 @@ video-1080p.m3u8`;
const result = parseMultivariantPlaylist(text, { url: baseUrl });
// Should have video selection set
// These STREAM-INFs list an audio codec (mp4a) with no AUDIO group, so the
// audio is muxed into each rendition — both codecs are retained on the track
// (the SourceBuffer mimetype must cover the muxed media).
const videoSet = result.selectionSets.find((s) => s.type === 'video');
expect(videoSet).toBeDefined();
expect(videoSet?.switchingSets).toHaveLength(1);
@@ -45,7 +47,7 @@ video-1080p.m3u8`;
bandwidth: 800000,
width: 640,
height: 360,
codecs: ['avc1.4d401e'],
codecs: ['avc1.4d401e', 'mp4a.40.2'],
});
expect(typeof videoTracks?.[0]?.id).toBe('string');
@@ -55,7 +57,7 @@ video-1080p.m3u8`;
bandwidth: 1400000,
width: 1280,
height: 720,
codecs: ['avc1.4d401f'],
codecs: ['avc1.4d401f', 'mp4a.40.2'],
mimeType: 'video/mp4',
});
expect(typeof videoTracks?.[1]?.id).toBe('string');
@@ -68,7 +70,7 @@ video-1080p.m3u8`;
bandwidth: 2800000,
width: 1920,
height: 1080,
codecs: ['avc1.640028'],
codecs: ['avc1.640028', 'mp4a.40.2'],
mimeType: 'video/mp4',
});
expect(typeof track1080p.id).toBe('string');
@@ -77,6 +79,23 @@ video-1080p.m3u8`;
expect(track1080p.audioGroupIds).toBeUndefined();
});
it('keeps an audioless video rendition video-only (no audio codec in CODECS, no AUDIO group)', () => {
// The inverse of the muxed case: no audio codec listed and no AUDIO group
// means the rendition is genuinely video-only — the muxed-codec retention
// must key on an audio codec actually being present, not fabricate one from
// the mere absence of an AUDIO group.
const text = `#EXTM3U
#EXT-X-VERSION:7
#EXT-X-STREAM-INF:BANDWIDTH=900000,RESOLUTION=1280x720,CODECS="avc1.640020"
video-720p.m3u8`;
const result = parseMultivariantPlaylist(text, { url: baseUrl });
const videoTracks = result.selectionSets.find((s) => s.type === 'video')?.switchingSets[0]?.tracks;
expect(videoTracks).toHaveLength(1);
expect(videoTracks?.[0]?.codecs).toEqual(['avc1.640020']);
});
it('de-duplicates the EXT-X-STREAM-INF cross-product: one track per video URI, accumulating audio groups', () => {
// The same two video renditions are each listed twice — once paired with a
// 5.1 group (higher BANDWIDTH, ac-3) and once with a stereo group (lower,