Do not start loading on level update from interstitials-controller when autoStartLoad is false

Fix start on jagged audio/video segment boundaries before interstitial break
This commit is contained in:
Rob Walch
2026-06-11 16:34:42 -07:00
parent 619559cfc4
commit df4158658e
4 changed files with 62 additions and 23 deletions
+5 -2
View File
@@ -810,15 +810,18 @@ transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSe
const videoTrack = tracks.video;
const videoSb = videoTrack?.buffer;
if (videoSb && sn !== 'initSegment' && offset !== undefined) {
const audioSb = audioTrack?.buffer;
const partOrFrag = part || (frag as MediaFragment);
if (
type === 'audio' &&
parent !== 'main' &&
!(videoTrack.ending || videoTrack.ended)
!(videoTrack.ending || videoTrack.ended) &&
audioSb &&
BufferHelper.getBuffered(audioSb).length
) {
const pStart = partOrFrag.start;
const pTime = pStart + partOrFrag.duration * 0.05;
const vbuffered = videoSb.buffered;
const vbuffered = BufferHelper.getBuffered(videoSb);
const vappending = this.currentOp('video');
if (!vbuffered.length && !vappending) {
// wait for video before appending audio
+8 -3
View File
@@ -1031,7 +1031,8 @@ export default class InterstitialsController
private checkStart() {
const schedule = this.schedule;
const interstitialEvents = schedule?.events;
if (!interstitialEvents || this.playbackDisabled || !this.media) {
const hls = this.hls;
if (!interstitialEvents || this.playbackDisabled || !this.media || !hls) {
return;
}
// Check buffered to pre-roll
@@ -1042,7 +1043,11 @@ export default class InterstitialsController
const timelinePos = this.timelinePos;
const effectivePlayingItem = this.effectivePlayingItem;
if (timelinePos === -1) {
const startPosition = this.hls.startPosition;
if (!hls.loadingEnabled) {
this.log(`waiting for startLoad( <startPosition> )`);
return;
}
const startPosition = hls.startPosition;
this.timelinePos = startPosition;
if (interstitialEvents.length === 0) {
this.setSchedulePosition(0);
@@ -1056,7 +1061,7 @@ export default class InterstitialsController
startPosition > 0 ? startPosition : 0);
const index = schedule.findItemIndexAtTime(start);
this.setSchedulePosition(index);
} else if (this.hls.liveSyncPosition === 0) {
} else if (hls.liveSyncPosition === 0) {
this.setSchedulePosition(0);
} else {
this.log('[checkStart] waiting for live start');
@@ -1161,8 +1161,17 @@ describe('BufferController with attached media', function () {
it('blocks audio append when video buffer is empty', function () {
const audioQueue = getAudioQueue();
// Audio arrives with no video buffered - should be blocked
// Audio arrives with no video buffered - first append is not blocked
triggerAudioAppend(0, 2);
// Queue should have block-audio op followed by the audio append
expect(audioQueue.length).to.equal(1);
expect(audioQueue[0].label).to.equal('append-audio');
audioQueue.length = 0;
setSourceBufferBufferedRange(bufferController, 'audio', 0, 2);
// Morem audio arrives with no video buffered - blocked
triggerAudioAppend(2, 4);
// Queue should have block-audio op followed by the audio append
expect(audioQueue.length).to.equal(2);
@@ -1185,6 +1194,7 @@ describe('BufferController with attached media', function () {
it('blocks audio when audio is ahead of video', function () {
// Video has buffered 0-5, but audio segment starts at 6 (ahead of video)
setSourceBufferBufferedRange(bufferController, 'video', 0, 5);
setSourceBufferBufferedRange(bufferController, 'audio', 0, 6);
(bufferController as any).lastVideoAppendEnd = 5;
const audioQueue = getAudioQueue();
@@ -1197,8 +1207,9 @@ describe('BufferController with attached media', function () {
it('unblocks audio when video append advances past blocked audio position', function () {
const audioQueue = getAudioQueue();
// Audio arrives first, gets blocked (no video buffered)
triggerAudioAppend(0, 2);
// Audio arrives first, second append gets blocked (no video buffered)
setSourceBufferBufferedRange(bufferController, 'audio', 0, 1);
triggerAudioAppend(1, 1);
expect(audioQueue[0].label).to.equal('block-audio');
// Video append arrives — advances lastVideoAppendEnd past audio pTime
@@ -1234,11 +1245,11 @@ describe('BufferController with attached media', function () {
)?.buffer;
const audioQueue = getAudioQueue();
// First audio triggers block (no video buffered)
triggerAudioAppend(0, 2);
expect(audioQueue[0].label).to.equal('block-audio');
// First audio is appended
setSourceBufferBufferedRange(bufferController, 'audio', 0, 2);
// Second audio also triggers its own block (guard removed)
// Second audio also triggers block (guard removed)
triggerAudioAppend(2, 2);
triggerAudioAppend(2, 2);
expect(audioQueue.length).to.equal(4);
expect(audioQueue[0].label).to.equal('block-audio');
@@ -1273,6 +1284,7 @@ describe('BufferController with attached media', function () {
expect(audioQueue.length).to.equal(1);
expect(audioQueue[0].label).to.equal('append-audio');
expect(audioBuffer!.appendBuffer).to.have.been.calledOnce;
setSourceBufferBufferedRange(bufferController, 'audio', 0, 2);
// While audio[0] is still in-flight (no updateend yet), second audio
// arrives ahead of video — block-audio lands behind the in-flight append
@@ -167,6 +167,25 @@ describe('InterstitialsController', function () {
}
function setLoadedLevelDetails(playlist: string) {
const attrs = new AttrList({});
const level = new Level({
name: '',
url: '',
attrs,
bitrate: 0,
});
(hls.streamController as any).levels = [level];
(hls.levelController as any)._levels[0] = level;
// onManifestLoaded with autoStartLoaded kicks off loading
if (!hls.loadingEnabled) {
hls.startLoad(-1);
}
(hls.levelController as any).currentLevelIndex = 0;
(hls.levelController as any).currentLevel = level;
const details = M3U8Parser.parseLevelPlaylist(
playlist,
'http://example.com/playlist.m3u8',
@@ -178,18 +197,9 @@ describe('InterstitialsController', function () {
const timeSinceLoadedStub = sinon.stub(details, 'age');
timeSinceLoadedStub.get(() => 0);
expect(details.playlistParsingError).to.equal(null);
const attrs = new AttrList({});
const level = new Level({
name: '',
url: '',
attrs,
bitrate: 0,
});
level.details = details;
(hls.streamController as any).levels = [level];
(hls.levelController as any)._levels[0] = level;
(hls.levelController as any).currentLevelIndex = 0;
(hls.levelController as any).currentLevel = level;
hls.trigger(Events.LEVEL_LOADED, {
details,
levelInfo: level,
@@ -297,6 +307,7 @@ fileSequence4.ts
const eventsTriggered = getTriggerCalls();
expect(eventsTriggered).to.deep.equal(
[
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -1145,6 +1156,7 @@ fileSequence5.mp4`;
const callsWithPrerollBeforeAttach = getTriggerCalls();
expect(callsWithPrerollBeforeAttach).to.deep.equal(
[
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -1215,6 +1227,7 @@ fileSequence3.mp4
[
Events.MEDIA_ATTACHING,
Events.MEDIA_ATTACHED,
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -1283,6 +1296,7 @@ fileSequence3.mp4
[
Events.MEDIA_ATTACHING,
Events.MEDIA_ATTACHED,
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -1357,6 +1371,7 @@ fileSequence6.mp4`;
const eventsBeforeAttach = getTriggerCalls();
expect(eventsBeforeAttach).to.deep.equal(
[
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -1489,6 +1504,7 @@ fileSequence6.mp4`;
const eventsAfterPlaylist = getTriggerCalls();
expect(eventsAfterPlaylist).to.deep.equal(
[
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -1820,6 +1836,7 @@ fileSequence6.mp4
const eventsBeforeAttach = getTriggerCalls();
expect(eventsBeforeAttach).to.deep.equal(
[
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,
@@ -2139,6 +2156,7 @@ fileSequence3.mp4`;
const expectedEvents = [
Events.MEDIA_ATTACHING,
Events.MEDIA_ATTACHED,
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY,
@@ -2182,6 +2200,7 @@ media_w507366714_268.ts`;
const expectedEvents = [
Events.MEDIA_ATTACHING,
Events.MEDIA_ATTACHED,
Events.LEVEL_SWITCHING,
Events.LEVEL_LOADED,
Events.LEVEL_UPDATED,
Events.INTERSTITIALS_UPDATED,