Skip to content

Commit fecd4bf

Browse files
committed
Handle forward buffering past GAP tags in alt audio Playlists
1 parent 9d33020 commit fecd4bf

File tree

5 files changed

+111
-56
lines changed

5 files changed

+111
-56
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
342342
// (undocumented)
343343
protected getNextFragment(pos: number, levelDetails: LevelDetails): Fragment | null;
344344
// (undocumented)
345+
protected getNextFragmentLoopLoading(frag: Fragment, levelDetails: LevelDetails, bufferInfo: BufferInfo, playlistType: PlaylistLevelType, maxBufLen: number): Fragment | null;
346+
// (undocumented)
345347
getNextPart(partList: Part[], frag: Fragment, targetBufferTime: number): number;
346348
// Warning: (ae-forgotten-export) The symbol "PartsLoadedData" needs to be exported by the entry point hls.d.ts
347349
//
@@ -358,6 +360,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
358360
// (undocumented)
359361
protected initPTS: RationalTimestamp[];
360362
// (undocumented)
363+
protected isLoopLoading(frag: Fragment, targetBufferTime: number): boolean;
364+
// (undocumented)
361365
protected keyLoader: KeyLoader;
362366
// (undocumented)
363367
protected lastCurrentTime: number;

src/controller/audio-stream-controller.ts

+38-16
Original file line numberDiff line numberDiff line change
@@ -347,30 +347,52 @@ class AudioStreamController
347347
}
348348
}
349349

350-
// buffer audio up to one target duration ahead of main buffer
351-
if (
352-
mainBufferInfo &&
353-
targetBufferTime > mainBufferInfo.end + trackDetails.targetduration
354-
) {
350+
let frag = this.getNextFragment(targetBufferTime, trackDetails);
351+
let atGap = false;
352+
// Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags
353+
if (frag && this.isLoopLoading(frag, targetBufferTime)) {
354+
atGap = !!frag.gap;
355+
frag = this.getNextFragmentLoopLoading(
356+
frag,
357+
trackDetails,
358+
bufferInfo,
359+
PlaylistLevelType.MAIN,
360+
maxBufLen
361+
);
362+
}
363+
if (!frag) {
364+
this.bufferFlushed = true;
355365
return;
356366
}
357-
// wait for main buffer after buffing some audio
358-
if (!mainBufferInfo?.len && bufferInfo.len) {
367+
368+
// Buffer audio up to one target duration ahead of main buffer
369+
const atBufferSyncLimit =
370+
mainBufferInfo &&
371+
frag.start > mainBufferInfo.end + trackDetails.targetduration;
372+
if (
373+
atBufferSyncLimit ||
374+
// Or wait for main buffer after buffing some audio
375+
(!mainBufferInfo?.len && bufferInfo.len)
376+
) {
377+
// Check fragment-tracker for main fragments since GAP segments do not show up in bufferInfo
378+
const mainFrag = this.fragmentTracker.getBufferedFrag(
379+
frag.start,
380+
PlaylistLevelType.MAIN
381+
);
382+
if (mainFrag === null) {
383+
return;
384+
}
385+
// Bridge gaps in main buffer
386+
atGap ||=
387+
!!mainFrag.gap || (!!atBufferSyncLimit && mainBufferInfo.len === 0);
359388
if (
360-
!mainBufferInfo?.nextStart ||
361-
mainBufferInfo.nextStart >
362-
bufferInfo.end + trackDetails.targetduration * 2
389+
(atBufferSyncLimit && !atGap) ||
390+
(atGap && bufferInfo.nextStart && bufferInfo.nextStart < mainFrag.end)
363391
) {
364392
return;
365393
}
366394
}
367395

368-
const frag = this.getNextFragment(targetBufferTime, trackDetails);
369-
if (!frag) {
370-
this.bufferFlushed = true;
371-
return;
372-
}
373-
374396
this.loadFragment(frag, levelInfo, targetBufferTime);
375397
}
376398

src/controller/base-stream-controller.ts

+46-3
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,52 @@ export default class BaseStreamController
10101010
return this.mapToInitFragWhenRequired(frag);
10111011
}
10121012

1013+
protected isLoopLoading(frag: Fragment, targetBufferTime: number): boolean {
1014+
const trackerState = this.fragmentTracker.getState(frag);
1015+
return (
1016+
(trackerState === FragmentState.OK ||
1017+
(trackerState === FragmentState.PARTIAL && !!frag.gap)) &&
1018+
this.nextLoadPosition > targetBufferTime
1019+
);
1020+
}
1021+
1022+
protected getNextFragmentLoopLoading(
1023+
frag: Fragment,
1024+
levelDetails: LevelDetails,
1025+
bufferInfo: BufferInfo,
1026+
playlistType: PlaylistLevelType,
1027+
maxBufLen: number
1028+
): Fragment | null {
1029+
const gapStart = frag.gap;
1030+
const nextFragment = this.getNextFragment(
1031+
this.nextLoadPosition,
1032+
levelDetails
1033+
);
1034+
if (nextFragment === null) {
1035+
return nextFragment;
1036+
}
1037+
frag = nextFragment;
1038+
if (gapStart && frag && !frag.gap && bufferInfo.nextStart) {
1039+
// Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length
1040+
const nextbufferInfo = this.getFwdBufferInfoAtPos(
1041+
this.mediaBuffer ? this.mediaBuffer : this.media,
1042+
bufferInfo.nextStart,
1043+
playlistType
1044+
);
1045+
if (
1046+
nextbufferInfo !== null &&
1047+
bufferInfo.len + nextbufferInfo.len >= maxBufLen
1048+
) {
1049+
// Returning here might result in not finding an audio and video candiate to skip to
1050+
this.log(
1051+
`buffer full after gaps in "${playlistType}" playlist starting at sn: ${frag.sn}`
1052+
);
1053+
return null;
1054+
}
1055+
}
1056+
return frag;
1057+
}
1058+
10131059
mapToInitFragWhenRequired(frag: Fragment | null): typeof frag {
10141060
// If an initSegment is present, it must be buffered first
10151061
if (frag?.initSegment && !frag?.initSegment.data && !this.bitrateTest) {
@@ -1184,9 +1230,6 @@ export default class BaseStreamController
11841230
frag.sn < endSN &&
11851231
this.fragmentTracker.getState(nextFrag) !== FragmentState.OK
11861232
) {
1187-
this.log(
1188-
`Skipping loaded ${frag.type} SN ${frag.sn} at buffer end`
1189-
);
11901233
frag = nextFrag;
11911234
} else {
11921235
frag = null;

src/controller/stream-controller.ts

+22-37
Original file line numberDiff line numberDiff line change
@@ -302,45 +302,30 @@ export default class StreamController
302302
} else if (this.backtrackFragment && bufferInfo.len) {
303303
this.backtrackFragment = null;
304304
}
305-
if (frag) {
306-
// Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags
307-
const trackerState = this.fragmentTracker.getState(frag);
308-
if (
309-
(trackerState === FragmentState.OK ||
310-
(trackerState === FragmentState.PARTIAL && frag.gap)) &&
311-
this.nextLoadPosition > targetBufferTime
312-
) {
313-
const gapStart = frag.gap;
314-
if (!gapStart) {
315-
// Cleanup the fragment tracker before trying to find the next unbuffered fragment
316-
const type =
317-
this.audioOnly && !this.altAudio
318-
? ElementaryStreamTypes.AUDIO
319-
: ElementaryStreamTypes.VIDEO;
320-
const mediaBuffer =
321-
(type === ElementaryStreamTypes.VIDEO
322-
? this.videoBuffer
323-
: this.mediaBuffer) || this.media;
324-
if (mediaBuffer) {
325-
this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
326-
}
327-
}
328-
frag = this.getNextFragment(this.nextLoadPosition, levelDetails);
329-
if (gapStart && frag && !frag.gap && bufferInfo.nextStart) {
330-
// Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length
331-
const nextbufferInfo = this.getFwdBufferInfoAtPos(
332-
this.mediaBuffer ? this.mediaBuffer : this.media,
333-
bufferInfo.nextStart,
334-
PlaylistLevelType.MAIN
335-
);
336-
if (
337-
nextbufferInfo !== null &&
338-
bufferLen + nextbufferInfo.len >= maxBufLen
339-
) {
340-
return;
341-
}
305+
// Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags
306+
if (frag && this.isLoopLoading(frag, targetBufferTime)) {
307+
const gapStart = frag.gap;
308+
if (!gapStart) {
309+
// Cleanup the fragment tracker before trying to find the next unbuffered fragment
310+
const type =
311+
this.audioOnly && !this.altAudio
312+
? ElementaryStreamTypes.AUDIO
313+
: ElementaryStreamTypes.VIDEO;
314+
const mediaBuffer =
315+
(type === ElementaryStreamTypes.VIDEO
316+
? this.videoBuffer
317+
: this.mediaBuffer) || this.media;
318+
if (mediaBuffer) {
319+
this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
342320
}
343321
}
322+
frag = this.getNextFragmentLoopLoading(
323+
frag,
324+
levelDetails,
325+
bufferInfo,
326+
PlaylistLevelType.MAIN,
327+
maxBufLen
328+
);
344329
}
345330
if (!frag) {
346331
return;

src/loader/fragment-loader.ts

+1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ function createGapLoadError(frag: Fragment, part?: Part): LoadError {
363363
if (part) {
364364
errorData.part = part;
365365
}
366+
(part ? part : frag).stats.aborted = true;
366367
return new LoadError(errorData);
367368
}
368369

0 commit comments

Comments
 (0)