Skip to content

Commit dc2cd5e

Browse files
authored
Fix Live Interstitial midroll buffering and playback (#7913)
* Fix midroll buffering and playback progression from preroll at live edge Fixes #7912 * Fix live midroll interstitial resumption (low-latency and short primary breaks) #7912 * Fix appendInPlace stall handling #7912 * Replace `advanceInPlace` with `hls.startLoad` * Fix live midroll interstitial resumption (low-latency and short primary breaks) - Ensure seek to start on midroll join - Prevent lastCurrentTime > startPosition lock-in before segments are parsed - Improve live primary resumption estimate at end of interstitial playback Resolves #7912
1 parent 3f7c9d3 commit dc2cd5e

13 files changed

Lines changed: 238 additions & 103 deletions

api-extractor/report/hls.js.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2419,7 +2419,7 @@ export class HlsAssetPlayer {
24192419
// (undocumented)
24202420
get bufferedEnd(): number;
24212421
// (undocumented)
2422-
bufferedInPlaceToEnd(media?: HTMLMediaElement | null): boolean;
2422+
bufferedInPlaceToEnd(media?: HTMLMediaElement | null, fromTime?: number): boolean;
24232423
// (undocumented)
24242424
get currentTime(): number;
24252425
// (undocumented)

src/controller/audio-stream-controller.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,20 +222,29 @@ class AudioStreamController
222222
const lastCurrentTime = this.lastCurrentTime;
223223
this.stopLoad();
224224
this.setInterval(TICK_INTERVAL);
225-
if (lastCurrentTime > 0 && startPosition === -1) {
225+
if (
226+
lastCurrentTime > 0 &&
227+
startPosition === -1 &&
228+
!skipSeekToStartPosition &&
229+
this.initPTS.length
230+
) {
226231
this.log(
227232
`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(
228233
3,
229234
)}`,
230235
);
231236
startPosition = lastCurrentTime;
232-
this.state = State.IDLE;
233-
} else {
234-
this.state = State.WAITING_TRACK;
235237
}
238+
this.state = State.IDLE;
236239
this.nextLoadPosition = this.lastCurrentTime =
237240
startPosition + this.timelineOffset;
238241
this.startPosition = skipSeekToStartPosition ? -1 : startPosition;
242+
if (
243+
!skipSeekToStartPosition &&
244+
!this.fragmentTracker.hasFragments(this.playlistType)
245+
) {
246+
this.startFragRequested = false;
247+
}
239248
this.tick();
240249
}
241250

src/controller/base-stream-controller.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
findFragmentByPDT,
44
findFragmentByPTS,
55
findNearestWithCC,
6+
getNextFrag,
67
} from './fragment-finders';
78
import { FragmentState } from './fragment-tracker';
89
import Decrypter from '../crypt/decrypter';
@@ -1603,7 +1604,10 @@ export default class BaseStreamController
16031604
}
16041605
let programFrag = this.filterReplacedPrimary(frag, levelDetails);
16051606
if (!programFrag && frag) {
1606-
programFrag = fragments[1 + frag.sn - levelDetails.startSN] || null;
1607+
programFrag = getNextFrag(levelDetails, frag.sn, this.loadingParts);
1608+
if (programFrag) {
1609+
this.nextLoadPosition = programFrag.start;
1610+
}
16071611
}
16081612
return programFrag;
16091613
}
@@ -1696,9 +1700,9 @@ export default class BaseStreamController
16961700
if (bufferingInterstitial) {
16971701
// Do not stream fragments while buffering Interstitial Events (except for overlap at the start)
16981702
if (
1699-
bufferingInterstitial.appendInPlace ||
1700-
Math.abs(frag.start - bufferingItem.start) > 1 ||
1701-
bufferingItem.start === 0
1703+
!bufferingInterstitial.appendInPlace &&
1704+
(Math.abs(frag.start - bufferingItem.start) > 1 ||
1705+
bufferingItem.start === 0)
17021706
) {
17031707
return null;
17041708
}
@@ -1713,7 +1717,8 @@ export default class BaseStreamController
17131717
// fragment is past schedule item end
17141718
// allow some overflow when not appending in place to prevent stalls
17151719
if (
1716-
bufferingItem.nextEvent.appendInPlace ||
1720+
(bufferingItem.nextEvent.appendInPlace &&
1721+
!bufferingItem.nextEvent.hasPlayed) ||
17171722
frag.start - bufferingItem.end > 1
17181723
) {
17191724
return null;

src/controller/buffer-controller.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -837,12 +837,10 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
837837
const videoAppendEnd = partOrFrag.end;
838838
const diff = this.lastVideoAppendEnd - videoAppendEnd;
839839
this.lastVideoAppendEnd = videoAppendEnd;
840-
if (this.isAudioBlocked()) {
841-
if (diff < 0 || diff > partOrFrag.duration) {
842-
this.unblockAudio();
843-
} else {
844-
this.executeNext('audio');
845-
}
840+
if (diff < 0 || diff > partOrFrag.duration) {
841+
this.unblockAudio();
842+
} else if (this.isAudioBlocked()) {
843+
this.executeNext('audio');
846844
}
847845
}
848846
}

src/controller/fragment-finders.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,13 @@ export function findNearestWithCC(
239239
}
240240
return null;
241241
}
242+
243+
export function getNextFrag(
244+
details: LevelDetails,
245+
sn: number,
246+
fragmentHintForParts?: boolean,
247+
): MediaFragment | null {
248+
return fragmentHintForParts && sn === details.endSN && details.fragmentHint
249+
? details.fragmentHint
250+
: details.fragments[1 + sn - details.startSN] || null;
251+
}

src/controller/gap-controller.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
interstitialsEnabled,
44
State,
55
} from './base-stream-controller';
6+
import { getNextFrag } from './fragment-finders';
67
import { ErrorDetails, ErrorTypes } from '../errors';
78
import { Events } from '../events';
89
import TaskLoop from '../task-loop';
@@ -344,6 +345,15 @@ export default class GapController extends TaskLoop {
344345
if (!this.media || (!this.hls as any)) {
345346
return;
346347
}
348+
if (media.currentTime !== currentTime) {
349+
const currentTimeBeforeStallEvent = currentTime;
350+
currentTime = media.currentTime;
351+
this.moved = true;
352+
this.log(
353+
`currentTime changed ${currentTimeBeforeStallEvent} > ${currentTime}`,
354+
);
355+
return;
356+
}
347357
}
348358

349359
const bufferedWithHoles = BufferHelper.bufferInfo(
@@ -763,7 +773,7 @@ function withInterstitialBoundary(
763773
hls.interstitialsManager
764774
) {
765775
const frag = 'type' in appended ? appended : appended.fragment;
766-
const nextFrag = levelDetails.fragments[1 + frag.sn - levelDetails.startSN];
776+
const nextFrag = getNextFrag(levelDetails, frag.sn, true);
767777
if (
768778
nextFrag?.level === frag.level &&
769779
fragOverlapsQueuedInterstitial(

src/controller/interstitial-player.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class HlsAssetPlayer {
9292
}
9393
}
9494

95-
bufferedInPlaceToEnd(media?: HTMLMediaElement | null) {
95+
bufferedInPlaceToEnd(media?: HTMLMediaElement | null, fromTime?: number) {
9696
if (!this.appendInPlace) {
9797
return false;
9898
}
@@ -103,8 +103,8 @@ export class HlsAssetPlayer {
103103
return false;
104104
}
105105
const duration = Math.min(this._bufferedEosTime || Infinity, this.duration);
106-
const start = this.timelineOffset;
107-
const bufferInfo = BufferHelper.bufferInfo(media, start, 0);
106+
const start = this.timelineOffset + (fromTime || 0);
107+
const bufferInfo = BufferHelper.bufferInfo(media, start, 0.02);
108108
const bufferedEnd = this.getAssetTime(bufferInfo.end);
109109
return bufferedEnd >= duration - 0.02;
110110
}

0 commit comments

Comments
 (0)