From a4b54af482b1b593ca60ca3d1aabaefdff08f6a9 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sat, 19 Sep 2026 15:23:24 -0700 Subject: [PATCH] live HLS: no pre-live segment is ever served as part of the public stream The window's published flag is per stream, not per segment: the moment the first published segment arrived, everything already in the window, the streamer's pre-live preview segments included, was served to anyone and offered to the CDN. Going public now starts the window over at that first published segment. Reported by Greptile on #1282. --- pkg/media/live_window.go | 18 +++++++++++++----- pkg/media/validate_bare_test.go | 8 ++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/media/live_window.go b/pkg/media/live_window.go index 794b23886..1ac0d29a6 100644 --- a/pkg/media/live_window.go +++ b/pkg/media/live_window.go @@ -86,16 +86,24 @@ func (mm *MediaManager) LiveWindowPublished(did string) bool { // errors are logged, never fatal to ingest. // // Unpublished (pre-live) segments are folded in too, and the window remembers -// whether its latest segment was published. Live HLS requests carry no -// session, so the getLive* handlers keep an unpublished window to callers -// holding a playback token the streamer minted for themselves (see -// spxrpc's getLiveToken) — the HLS counterpart of WebRTC's viewer == streamer -// gate — and answer StreamNotLive to everyone else. +// whether its latest segment was published. The getLive* handlers keep an +// unpublished window to the streamer's own playback session (see spxrpc's +// getPlaybackSession) — the HLS counterpart of WebRTC's viewer == streamer +// gate — and answer StreamNotLive to everyone else. When the stream goes +// public the window is started over, so no preview segment is ever served +// as part of the public stream. func (mm *MediaManager) feedLiveWindow(ctx context.Context, did string, segment []byte, published bool) { mm.liveWindowsMut.Lock() if mm.liveWindowPublished == nil { mm.liveWindowPublished = map[string]bool{} } + if published && !mm.liveWindowPublished[did] && mm.liveWindows[did] != nil { + // The stream just went public. The window's flag is per stream, not + // per segment, so everything in it is about to be served to anyone; + // the pre-live preview segments still sitting in it must not be. The + // public window starts at this segment. + delete(mm.liveWindows, did) + } mm.liveWindowPublished[did] = published mm.liveWindowsMut.Unlock() eventCh := make(chan *muxl.MuxlEvent, 8) diff --git a/pkg/media/validate_bare_test.go b/pkg/media/validate_bare_test.go index aef07d9a7..a90a78ac1 100644 --- a/pkg/media/validate_bare_test.go +++ b/pkg/media/validate_bare_test.go @@ -139,13 +139,21 @@ func TestFeedLiveWindow(t *testing.T) { require.NotNil(t, mm.GetLiveWindow("did:test:streamer"), "pre-live segments make a window") require.False(t, mm.LiveWindowPublished("did:test:streamer"), "…but it is not public") + preLive := mm.GetLiveWindow("did:test:streamer") mm.feedLiveWindow(ctx, "did:test:streamer", m4s, true) require.True(t, mm.LiveWindowPublished("did:test:streamer")) w := mm.GetLiveWindow("did:test:streamer") require.NotNil(t, w, "window created on feed") + require.NotSame(t, preLive, w, "going public starts the window over: the preview segments are not served to the public") tids := w.TrackIDs() require.NotEmpty(t, tids, "window has tracks") + for _, tid := range tids { + require.Len(t, w.Track(tid).Segments, len(preLive.Track(tid).Segments), "track %s: only the published segment, none of the pre-live ones", tid) + } + // Once public, further segments extend the same window. + mm.feedLiveWindow(ctx, "did:test:streamer", m4s, true) + require.Same(t, w, mm.GetLiveWindow("did:test:streamer")) for _, tid := range tids { require.NotEmpty(t, w.InitSegment(tid), "track %s has an init segment", tid) tr := w.Track(tid) -- 2.51.2