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 } }