Skip to content

Commit

Permalink
Only allow large gaps to be skipped if start gap or all fragments in …
Browse files Browse the repository at this point in the history
…range are partial (#5366)

* Only allow large gaps to be skipped if start gap or all fragments in range are partial
Fixes #5360

* Fix adaptation and fragment tracking after fragment errors (gap tags)
#2940
  • Loading branch information
robwalch committed Apr 6, 2023
1 parent 0b5ce01 commit a9982f7
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 53 deletions.
8 changes: 7 additions & 1 deletion api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export class BaseSegment {
//
// @public (undocumented)
export class BaseStreamController extends TaskLoop implements NetworkComponentAPI {
constructor(hls: Hls, fragmentTracker: FragmentTracker, keyLoader: KeyLoader, logPrefix: string);
constructor(hls: Hls, fragmentTracker: FragmentTracker, keyLoader: KeyLoader, logPrefix: string, playlistType: PlaylistLevelType);
// (undocumented)
protected afterBufferFlushed(media: Bufferable, bufferType: SourceBufferName, playlistType: PlaylistLevelType): void;
// (undocumented)
Expand Down Expand Up @@ -412,12 +412,16 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
// (undocumented)
protected onvseeking: EventListener | null;
// (undocumented)
protected playlistType: PlaylistLevelType;
// (undocumented)
protected recoverWorkerError(data: ErrorData): void;
// (undocumented)
protected reduceLengthAndFlushBuffer(data: ErrorData): boolean;
// (undocumented)
protected reduceMaxBufferLength(threshold: number): boolean;
// (undocumented)
protected removeUnbufferedFrags(start?: number): void;
// (undocumented)
protected resetFragmentErrors(filterType: PlaylistLevelType): void;
// (undocumented)
protected resetFragmentLoading(frag: Fragment): void;
Expand All @@ -428,6 +432,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
// (undocumented)
protected resetTransmuxer(): void;
// (undocumented)
protected resetWhenMissingContext(chunkMeta: ChunkMetadata): void;
// (undocumented)
protected retryDate: number;
// (undocumented)
protected seekToStartPos(): void;
Expand Down
10 changes: 8 additions & 2 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,14 @@ class AbrController implements AbrComponentAPI {
// compute next level using ABR logic
let nextABRAutoLevel = this.getNextABRAutoLevel();
// use forced auto level when ABR selected level has errored
if (forcedAutoLevel !== -1 && this.hls.levels[nextABRAutoLevel].loadError) {
return forcedAutoLevel;
if (forcedAutoLevel !== -1) {
const levels = this.hls.levels;
if (
levels.length > Math.max(forcedAutoLevel, nextABRAutoLevel) &&
levels[forcedAutoLevel].loadError <= levels[nextABRAutoLevel].loadError
) {
return forcedAutoLevel;
}
}
// if forced auto level has been defined, use it to cap ABR computed quality level
if (forcedAutoLevel !== -1) {
Expand Down
30 changes: 18 additions & 12 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ class AudioStreamController
fragmentTracker: FragmentTracker,
keyLoader: KeyLoader
) {
super(hls, fragmentTracker, keyLoader, '[audio-stream-controller]');
super(
hls,
fragmentTracker,
keyLoader,
'[audio-stream-controller]',
PlaylistLevelType.AUDIO
);
this._registerListeners();
}

Expand Down Expand Up @@ -309,9 +315,9 @@ class AudioStreamController
if (bufferInfo === null) {
return;
}
const audioSwitch = !!this.switchingTrack;
const { bufferedTrack, switchingTrack } = this;

if (!audioSwitch && this._streamEnded(bufferInfo, trackDetails)) {
if (!switchingTrack && this._streamEnded(bufferInfo, trackDetails)) {
hls.trigger(Events.BUFFER_EOS, { type: 'audio' });
this.state = State.ENDED;
return;
Expand All @@ -325,16 +331,18 @@ class AudioStreamController
const maxBufLen = this.getMaxBufferLength(mainBufferInfo?.len);

// if buffer length is less than maxBufLen try to load a new fragment
if (bufferLen >= maxBufLen && !audioSwitch) {
if (bufferLen >= maxBufLen && !switchingTrack) {
return;
}
const fragments = trackDetails.fragments;
const start = fragments[0].start;
let targetBufferTime = bufferInfo.end;

if (audioSwitch && media) {
if (switchingTrack && media) {
const pos = this.getLoadPosition();
targetBufferTime = pos;
if (bufferedTrack && switchingTrack.attrs !== bufferedTrack.attrs) {
targetBufferTime = pos;
}
// if currentTime (pos) is less than alt audio playlist start time, it means that alt audio is ahead of currentTime
if (trackDetails.PTSKnown && pos < start) {
// if everything is buffered from pos to start or if audio buffer upfront, let's seek to start
Expand Down Expand Up @@ -431,7 +439,7 @@ class AudioStreamController

if (fragCurrent) {
fragCurrent.abortRequests();
this.fragmentTracker.removeFragment(fragCurrent);
this.removeUnbufferedFrags(fragCurrent.start);
}
this.resetLoadingState();
// destroy useless transmuxer when switching audio to main
Expand Down Expand Up @@ -541,12 +549,13 @@ class AudioStreamController

const track = levels[trackId] as Level;
if (!track) {
this.warn('Audio track is defined on fragment load progress');
this.warn('Audio track is undefined on fragment load progress');
return;
}
const details = track.details as LevelDetails;
if (!details) {
this.warn('Audio track details undefined on fragment load progress');
this.removeUnbufferedFrags(frag.start);
return;
}
const audioCodec =
Expand Down Expand Up @@ -731,10 +740,7 @@ class AudioStreamController

const context = this.getCurrentContext(chunkMeta);
if (!context) {
this.warn(
`The loading context changed while buffering fragment ${chunkMeta.sn} of level ${chunkMeta.level}. This chunk will not be buffered.`
);
this.resetStartWhenNotLoaded(chunkMeta.level);
this.resetWhenMissingContext(chunkMeta);
return;
}
const { frag, part, level } = context;
Expand Down
32 changes: 31 additions & 1 deletion src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default class BaseStreamController
protected fragmentTracker: FragmentTracker;
protected transmuxer: TransmuxerInterface | null = null;
protected _state: string = State.STOPPED;
protected playlistType: PlaylistLevelType;
protected media: HTMLMediaElement | null = null;
protected mediaBuffer: Bufferable | null = null;
protected config: HlsConfig;
Expand Down Expand Up @@ -107,9 +108,11 @@ export default class BaseStreamController
hls: Hls,
fragmentTracker: FragmentTracker,
keyLoader: KeyLoader,
logPrefix: string
logPrefix: string,
playlistType: PlaylistLevelType
) {
super();
this.playlistType = playlistType;
this.logPrefix = logPrefix;
this.log = logger.log.bind(logger, `${logPrefix}:`);
this.warn = logger.warn.bind(logger, `${logPrefix}:`);
Expand Down Expand Up @@ -270,6 +273,14 @@ export default class BaseStreamController
}

if (media) {
// Remove gap fragments
this.fragmentTracker.removeFragmentsInRange(
currentTime,
Infinity,
this.playlistType,
true
);

this.lastCurrentTime = currentTime;
}

Expand Down Expand Up @@ -1587,6 +1598,25 @@ export default class BaseStreamController
}
}

protected resetWhenMissingContext(chunkMeta: ChunkMetadata) {
this.warn(
`The loading context changed while buffering fragment ${chunkMeta.sn} of level ${chunkMeta.level}. This chunk will not be buffered.`
);
this.removeUnbufferedFrags();
this.resetStartWhenNotLoaded(chunkMeta.level);
this.resetLoadingState();
}

protected removeUnbufferedFrags(start: number = 0) {
this.fragmentTracker.removeFragmentsInRange(
start,
Infinity,
this.playlistType,
false,
true
);
}

private updateLevelTiming(
frag: Fragment,
part: Part | null,
Expand Down
15 changes: 8 additions & 7 deletions src/controller/error-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default class ErrorController implements NetworkComponentAPI {
this.unregisterListeners();
// @ts-ignore
this.hls = null;
this.penalizedRenditions = {};
}

startLoad(startPosition: number): void {
Expand Down Expand Up @@ -328,10 +329,7 @@ export default class ErrorController implements NetworkComponentAPI {
}
const level = this.hls.levels[levelIndex];
if (level) {
// No penalty for GAP tags so that player can switch back when GAPs are found in other levels
if (data.details !== ErrorDetails.FRAG_GAP) {
level.loadError++;
}
level.loadError++;
if (hls.autoLevelEnabled) {
// Search for next level to retry
let nextLevel = -1;
Expand Down Expand Up @@ -476,14 +474,17 @@ export default class ErrorController implements NetworkComponentAPI {
: hls.loadLevel;
const level = hls.levels[levelIndex];
const redundantLevels = level.url.length;
this.penalizeRendition(level, data);
const errorUrlId = data.frag ? data.frag.urlId : level.urlId;
if (level.urlId === errorUrlId && (!data.frag || level.details)) {
this.penalizeRendition(level, data);
}
for (let i = 1; i < redundantLevels; i++) {
const newUrlId = (level.urlId + i) % redundantLevels;
const newUrlId = (errorUrlId + i) % redundantLevels;
const penalizedRendition = penalizedRenditions[newUrlId];
// Check if rendition is penalized and skip if it is a bad fit for failover
if (
!penalizedRendition ||
checkExpired(penalizedRendition, data, penalizedRenditions[level.urlId])
checkExpired(penalizedRendition, data, penalizedRenditions[errorUrlId])
) {
// delete penalizedRenditions[newUrlId];
// Update the url id of all levels so that we stay on the same set of variants when level switching
Expand Down
33 changes: 22 additions & 11 deletions src/controller/fragment-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class FragmentTracker implements ComponentAPI {

private bufferPadding: number = 0.2;
private hls: Hls;
private hasGaps: boolean = false;

constructor(hls: Hls) {
this.hls = hls;
Expand Down Expand Up @@ -220,7 +221,7 @@ export class FragmentTracker implements ComponentAPI {
}
}

public fragBuffered(frag: Fragment, force?: boolean) {
public fragBuffered(frag: Fragment, force?: true) {
const fragKey = getFragmentKey(frag);
let fragmentEntity = this.fragments[fragKey];
if (!fragmentEntity && force) {
Expand All @@ -231,6 +232,9 @@ export class FragmentTracker implements ComponentAPI {
buffered: false,
range: Object.create(null),
};
if (frag.gap) {
this.hasGaps = true;
}
}
if (fragmentEntity) {
fragmentEntity.loaded = null;
Expand Down Expand Up @@ -447,22 +451,28 @@ export class FragmentTracker implements ComponentAPI {
public removeFragmentsInRange(
start: number,
end: number,
playlistType: PlaylistLevelType
playlistType: PlaylistLevelType,
withGapOnly?: boolean,
unbufferedOnly?: boolean
) {
if (withGapOnly && !this.hasGaps) {
return;
}
Object.keys(this.fragments).forEach((key) => {
const fragmentEntity = this.fragments[key];
if (!fragmentEntity) {
return;
}
if (fragmentEntity.buffered) {
const frag = fragmentEntity.body;
if (
frag.type === playlistType &&
frag.start < end &&
frag.end > start
) {
this.removeFragment(frag);
}
const frag = fragmentEntity.body;
if (frag.type !== playlistType || (withGapOnly && !frag.gap)) {
return;
}
if (
frag.start < end &&
frag.end > start &&
(fragmentEntity.buffered || unbufferedOnly)
) {
this.removeFragment(frag);
}
});
}
Expand All @@ -485,6 +495,7 @@ export class FragmentTracker implements ComponentAPI {
this.endListFragments = Object.create(null);
this.mainFragEntity = null;
this.activeParts = null;
this.hasGaps = false;
}
}

Expand Down
42 changes: 41 additions & 1 deletion src/controller/gap-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { BufferInfo } from '../utils/buffer-helper';
import { BufferHelper } from '../utils/buffer-helper';
import { ErrorTypes, ErrorDetails } from '../errors';
import { PlaylistLevelType } from '../types/loader';
import { Events } from '../events';
import { logger } from '../utils/logger';
import type Hls from '../hls';
Expand Down Expand Up @@ -256,7 +257,46 @@ export default class GapController {
const bufferStarved = bufferInfo.len <= config.maxBufferHole;
const waiting =
bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3;
if (currentTime < startTime && (bufferStarved || waiting)) {
const gapLength = startTime - currentTime;
if (gapLength > 0 && (bufferStarved || waiting)) {
// Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial
if (gapLength > config.maxBufferHole) {
const { fragmentTracker } = this;
let startGap = false;
if (currentTime === 0) {
const startFrag = fragmentTracker.getAppendedFrag(
0,
PlaylistLevelType.MAIN
);
if (startFrag && startTime < startFrag.end) {
startGap = true;
}
}
if (!startGap) {
const startProvisioned =
partial ||
fragmentTracker.getAppendedFrag(
currentTime,
PlaylistLevelType.MAIN
);
if (startProvisioned) {
let moreToLoad = false;
let pos = startProvisioned.end;
while (pos < startTime) {
const provisioned = fragmentTracker.getPartialFragment(pos);
if (provisioned) {
pos += provisioned.duration;
} else {
moreToLoad = true;
break;
}
}
if (moreToLoad) {
return 0;
}
}
}
}
const targetTime = Math.max(
startTime + SKIP_BUFFER_RANGE_START,
currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS
Expand Down
7 changes: 5 additions & 2 deletions src/controller/level-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,13 @@ export function updateFragPTSDTS(
endPTS = Math.max(endPTS, fragEndPts);
endDTS = Math.max(endDTS, frag.endDTS);
}
frag.duration = endPTS - startPTS;

const drift = startPTS - frag.start;
frag.start = frag.startPTS = startPTS;
if (frag.start !== 0) {
frag.start = startPTS;
}
frag.duration = endPTS - frag.start;
frag.startPTS = startPTS;
frag.maxStartPTS = maxStartPTS;
frag.startDTS = startDTS;
frag.endPTS = endPTS;
Expand Down

0 comments on commit a9982f7

Please sign in to comment.