fix(MSF): prevent video segment eviction during initial buffering (#9809)

## 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 <erik@vivoh.com>
This commit is contained in:
Erik Herz
2026-03-10 05:35:31 -04:00
committed by GitHub
parent 0db1349898
commit 04eab23047
+46 -7
View File
@@ -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<string, number>}
*/
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 &&