From 04eab2304723fa052a2cc2bc7c4e367d5d85371d Mon Sep 17 00:00:00 2001 From: Erik Herz Date: Tue, 10 Mar 2026 05:35:31 -0400 Subject: [PATCH] fix(MSF): prevent video segment eviction during initial buffering (#9809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - In MSF live streams, audio and video segments arrive at different wall-clock times over WebTransport (audio frames are ~8KB vs ~125KB video keyframes, so audio arrives first). - The first stream type to deliver a segment calls `lockStartTime()` and `setSegmentAvailabilityDuration(0.5)`, creating a tight availability window. When the other type's segments arrive even slightly later, `mergeAndEvict` immediately evicts them because they fall before `getSegmentAvailabilityStart()`. - This causes video to never render — StreamingEngine repeatedly logs `(video:N) cannot find segment` because every video segment is evicted on arrival. ### Fix 1. Added `firstSegmentStartTimes_` map to track the first segment start time per content type. 2. Before the timeline is locked, pass `0` as the eviction start to `mergeAndEvict` to prevent premature segment eviction. 3. Only call `lockStartTime()` once ALL expected stream types (audio AND video) have received at least one segment. 4. Set `segmentAvailabilityDuration` wide enough to cover the gap between the earliest and latest stream start times (`gap + duration`), so the stream whose segments arrived first isn't continuously evicted. Co-authored-by: Erik Herz --- lib/msf/msf_parser.js | 53 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/msf/msf_parser.js b/lib/msf/msf_parser.js index 7d13538e8..4ffd44830 100644 --- a/lib/msf/msf_parser.js +++ b/lib/msf/msf_parser.js @@ -94,6 +94,15 @@ shaka.msf.MSFParser = class { /** @private {boolean} */ this.isFirstVideoSegment_ = true; + + /** + * Tracks the first segment start time for each content type. + * Used to delay locking the presentation timeline until all expected + * stream types have received their first segment, preventing premature + * eviction of segments from streams with earlier timestamps. + * @private {!Map} + */ + this.firstSegmentStartTimes_ = new Map(); } /** @@ -637,15 +646,45 @@ shaka.msf.MSFParser = class { reference.setSegmentData(obj.data); - stream.segmentIndex.mergeAndEvict( - [reference], - this.presentationTimeline_.getSegmentAvailabilityStart()); + // Track the first segment start time per content type. + // We must not lock the presentation timeline until all expected + // stream types have received at least one segment, otherwise + // streams with earlier timestamps get their segments evicted + // immediately by mergeAndEvict (e.g. video starts 4s before audio). + if (!this.firstSegmentStartTimes_.has(type)) { + this.firstSegmentStartTimes_.set(type, startTime); + } + + const timelineLocked = this.presentationTimeline_.isStartTimeLocked(); + + // Before the timeline is locked, pass 0 as the eviction start to + // prevent mergeAndEvict from discarding segments prematurely. + const evictionStart = timelineLocked ? + this.presentationTimeline_.getSegmentAvailabilityStart() : 0; + stream.segmentIndex.mergeAndEvict([reference], evictionStart); this.presentationTimeline_.notifySegments([reference]); - if (!this.presentationTimeline_.isStartTimeLocked()) { - this.presentationTimeline_.lockStartTime(); - this.presentationTimeline_.setSegmentAvailabilityDuration( - Math.max(0.5, duration)); + + if (!timelineLocked) { + // Only lock once we have first segments from all expected types. + const hasAudio = this.audioStreams_.length > 0; + const hasVideo = this.videoStreams_.length > 0; + const gotAudio = this.firstSegmentStartTimes_.has( + ContentType.AUDIO); + const gotVideo = this.firstSegmentStartTimes_.has( + ContentType.VIDEO); + const allReady = (!hasAudio || gotAudio) && (!hasVideo || gotVideo); + + if (allReady) { + // Set availability duration wide enough to cover the gap + // between the earliest and latest stream start times, so + // segments from streams with earlier timestamps aren't evicted. + const starts = [...this.firstSegmentStartTimes_.values()]; + const gap = Math.max(...starts) - Math.min(...starts); + this.presentationTimeline_.lockStartTime(); + this.presentationTimeline_.setSegmentAvailabilityDuration( + Math.max(0.5, duration, gap + duration)); + } } if (!this.config_.disableText &&