From f9eb212ab0d85144cb5945884efc7fa0f3f07a12 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Wed, 14 Oct 2026 16:46:36 -0700 Subject: [PATCH] VOD playlists list only the full mix Recordings keep every audio track, stems included, but the VOD master playlist listed them all as alternate mixes and defaulted to the first AAC by track id, which can be a stem. Carry each audio track's group in the metafile and list only the lowest group, as live HLS does. --- pkg/spxrpc/place_stream_playback_getvideo.go | 23 ++++++++++++++++--- .../place_stream_playback_getvideo_test.go | 16 +++++++++++++ pkg/vod/metafile.go | 4 ++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pkg/spxrpc/place_stream_playback_getvideo.go b/pkg/spxrpc/place_stream_playback_getvideo.go index eec769013..af3e6fd4a 100644 --- a/pkg/spxrpc/place_stream_playback_getvideo.go +++ b/pkg/spxrpc/place_stream_playback_getvideo.go @@ -639,9 +639,10 @@ func trackPlaylistURL(uri, track, sid string, startMS, endMS *int64) string { func masterPlaylist(meta *vod.Metafile, uri, sid string, startMS, endMS *int64) string { lines := []string{"#EXTM3U", "#EXT-X-VERSION:6", ""} - // Collect audio tracks in deterministic order, pick a default - // (prefer AAC since Safari is fussy about it). - audioIDs := tracksOfType(meta, "audio") + // Collect the full mix's audio tracks (stems are WebRTC-only, as in + // live HLS) in deterministic order, pick a default (prefer AAC since + // Safari is fussy about it). + audioIDs := mainAudioIDs(meta, tracksOfType(meta, "audio")) defaultAudio := pickDefaultAudio(meta, audioIDs) for _, tid := range audioIDs { t := meta.Tracks[tid] @@ -885,6 +886,22 @@ func tracksOfType(meta *vod.Metafile, t string) []string { return out } +// mainAudioIDs keeps the full mix's tracks among audioIDs: those in the +// lowest audio group. +func mainAudioIDs(meta *vod.Metafile, audioIDs []string) []string { + var main []string + for _, id := range audioIDs { + g := meta.Tracks[id].Group + switch { + case len(main) == 0 || g < meta.Tracks[main[0]].Group: + main = []string{id} + case g == meta.Tracks[main[0]].Group: + main = append(main, id) + } + } + return main +} + // pickDefaultAudio prefers an AAC track (Safari requires it) and // falls back to the first audio track. Returns "" if no audio. func pickDefaultAudio(meta *vod.Metafile, audioIDs []string) string { diff --git a/pkg/spxrpc/place_stream_playback_getvideo_test.go b/pkg/spxrpc/place_stream_playback_getvideo_test.go index cbd5046ef..a4859ba53 100644 --- a/pkg/spxrpc/place_stream_playback_getvideo_test.go +++ b/pkg/spxrpc/place_stream_playback_getvideo_test.go @@ -104,6 +104,22 @@ func TestMasterPlaylist_PropagatesTimeRange(t *testing.T) { require.Contains(t, pl, "end=2000") } +// A recording with stems lists only the full mix (the lowest audio group), +// even when a stem's AAC comes first by track id: HLS has no stem mixing. +func TestMasterPlaylist_FullMixOnly(t *testing.T) { + meta := fixtureMetafile() + stem := meta.Tracks["2"] + stem.Group = 2 + meta.Tracks["2"] = stem + mix := stem + mix.Group = 1 + meta.Tracks["3"] = mix + pl := masterPlaylist(meta, fixtureURI, fixtureSID, nil, nil) + require.Equal(t, 1, strings.Count(pl, "#EXT-X-MEDIA:TYPE=AUDIO"), pl) + require.Contains(t, pl, "track=3") + require.NotContains(t, pl, "track=2") +} + func TestMediaPlaylist_Video(t *testing.T) { pl, err := mediaPlaylist(fixtureMetafile(), "1", fixtureDID, fixtureSID, vodCDN{}, nil, nil) require.NoError(t, err) diff --git a/pkg/vod/metafile.go b/pkg/vod/metafile.go index 96bf46783..19bc661ee 100644 --- a/pkg/vod/metafile.go +++ b/pkg/vod/metafile.go @@ -93,6 +93,9 @@ type MetafileTrack struct { // Audio-only. Channels uint32 `json:"channels,omitempty"` SampleRate uint32 `json:"sampleRate,omitempty"` + // Group is the track's alternateGroup: the lowest is the full mix, + // higher ones are stems (absent on older metafiles: all one group). + Group uint32 `json:"group,omitempty"` } // MetafileSegment is one GOP-sized byte range within the blob. @@ -282,6 +285,7 @@ func (b *metafileBuilder) Finalize(cid string, size int64) *Metafile { track.Timescale = c.Timescale() track.Channels = c.NumberOfChannels track.SampleRate = c.SampleRate + track.Group = c.AlternateGroup break } } -- knotmirror 0.0.1