Skip to content

Commit

Permalink
Improve GAP fragment picking, post gap buffering, and level switching…
Browse files Browse the repository at this point in the history
… error resolution

#2940
  • Loading branch information
robwalch committed Mar 1, 2023
1 parent 1f236db commit 938a05e
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 83 deletions.
6 changes: 5 additions & 1 deletion api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
// (undocumented)
protected getFwdBufferInfo(bufferable: Bufferable | null, type: PlaylistLevelType): BufferInfo | null;
// (undocumented)
protected getFwdBufferInfoAtPos(bufferable: Bufferable | null, pos: number, type: PlaylistLevelType): BufferInfo | null;
// (undocumented)
protected getInitialLiveFragment(levelDetails: LevelDetails, fragments: Array<Fragment>): Fragment | null;
// (undocumented)
protected getLevelDetails(): LevelDetails | undefined;
Expand Down Expand Up @@ -404,7 +406,9 @@ export class BaseStreamController extends TaskLoop implements NetworkComponentAP
// (undocumented)
protected onvseeking: EventListener | null;
// (undocumented)
protected reduceMaxBufferLength(threshold?: number): boolean;
protected reduceLengthAndFlushBuffer(data: ErrorData): boolean;
// (undocumented)
protected reduceMaxBufferLength(threshold: number): boolean;
// (undocumented)
protected resetFragmentErrors(filterType: PlaylistLevelType): void;
// (undocumented)
Expand Down
33 changes: 6 additions & 27 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,33 +671,12 @@ class AudioStreamController
}
break;
case ErrorDetails.BUFFER_FULL_ERROR:
// if in appending state
if (
data.parent === 'audio' &&
(this.state === State.PARSING || this.state === State.PARSED)
) {
let flushBuffer = true;
const bufferedInfo = this.getFwdBufferInfo(
this.mediaBuffer,
PlaylistLevelType.AUDIO
);
// 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end
// reduce max buf len if current position is buffered
if (bufferedInfo && bufferedInfo.len > 0.5) {
flushBuffer = !this.reduceMaxBufferLength(bufferedInfo.len);
}
if (flushBuffer) {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole audio buffer to recover
this.warn(
'Buffer full error also media.currentTime is not buffered, flush audio buffer'
);
this.fragCurrent = null;
this.bufferedTrack = null;
super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio');
}
this.resetLoadingState();
if (!data.parent || data.parent !== 'audio') {
return;
}
if (this.reduceLengthAndFlushBuffer(data)) {
this.bufferedTrack = null;
super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio');
}
break;
default:
Expand Down
57 changes: 48 additions & 9 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -899,24 +899,30 @@ export default class BaseStreamController
bufferable: Bufferable | null,
type: PlaylistLevelType
): BufferInfo | null {
const { config } = this;
const pos = this.getLoadPosition();
if (!Number.isFinite(pos)) {
return null;
}
const bufferInfo = BufferHelper.bufferInfo(
bufferable,
pos,
config.maxBufferHole
);
return this.getFwdBufferInfoAtPos(bufferable, pos, type);
}

protected getFwdBufferInfoAtPos(
bufferable: Bufferable | null,
pos: number,
type: PlaylistLevelType
): BufferInfo | null {
const {
config: { maxBufferHole },
} = this;
const bufferInfo = BufferHelper.bufferInfo(bufferable, pos, maxBufferHole);
// Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos
if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) {
const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type);
if (bufferedFragAtPos && bufferInfo.nextStart < bufferedFragAtPos.end) {
return BufferHelper.bufferInfo(
bufferable,
pos,
Math.max(bufferInfo.nextStart, config.maxBufferHole)
Math.max(bufferInfo.nextStart, maxBufferHole)
);
}
}
Expand All @@ -937,7 +943,7 @@ export default class BaseStreamController
return Math.min(maxBufLen, config.maxMaxBufferLength);
}

protected reduceMaxBufferLength(threshold?: number) {
protected reduceMaxBufferLength(threshold: number) {
const config = this.config;
const minLength = threshold || config.maxBufferLength;
if (config.maxMaxBufferLength >= minLength) {
Expand Down Expand Up @@ -1177,7 +1183,7 @@ export default class BaseStreamController
this.fragmentTracker.getState(nextFrag) !== FragmentState.OK
) {
this.log(
`SN ${frag.sn} just loaded, load next one: ${nextFrag.sn}`
`Skipping loaded ${frag.type} SN ${frag.sn} at buffer end`
);
frag = nextFrag;
} else {
Expand Down Expand Up @@ -1413,6 +1419,39 @@ export default class BaseStreamController
} else {
this.state = State.ERROR;
}
// Perform next async tick sooner to speed up error action resolution
this.tickImmediate();
}

protected reduceLengthAndFlushBuffer(data: ErrorData): boolean {
// if in appending state
if (this.state === State.PARSING || this.state === State.PARSED) {
const playlistType = data.parent as PlaylistLevelType;
const bufferedInfo = this.getFwdBufferInfo(
this.mediaBuffer,
playlistType
);
// 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end
// reduce max buf len if current position is buffered
let flushBuffer = true;
if (bufferedInfo && bufferedInfo.len > 0.5) {
flushBuffer = !this.reduceMaxBufferLength(bufferedInfo.len);
}
if (flushBuffer) {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole audio buffer to recover
this.warn(
`Buffer full error while media.currentTime is not buffered, flush ${playlistType} buffer`
);
}
if (data.frag) {
this.nextLoadPosition = data.frag.start;
}
this.resetLoadingState();
return flushBuffer;
}
return false;
}

protected resetFragmentErrors(filterType: PlaylistLevelType) {
Expand Down
22 changes: 20 additions & 2 deletions src/controller/error-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isTimeoutError,
shouldRetry,
} from '../utils/error-helper';
import { findFragmentByPTS } from './fragment-finders';
import { HdcpLevels } from '../types/level';
import { logger } from '../utils/logger';
import type Hls from '../hls';
Expand Down Expand Up @@ -284,7 +285,10 @@ export default class ErrorController implements NetworkComponentAPI {
}
const level = this.hls.levels[levelIndex];
if (level) {
level.loadError++;
// 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++;
}
const redundantLevels = level.url.length;
// Try redundant fail-over until level.loadError reaches redundantLevels
if (redundantLevels > 1 && level.loadError < redundantLevels) {
Expand All @@ -299,6 +303,20 @@ export default class ErrorController implements NetworkComponentAPI {
candidate !== hls.loadLevel &&
levels[candidate].loadError === 0
) {
// Skip level switch if GAP tag is found in next level
if (data.details === ErrorDetails.FRAG_GAP && data.frag) {
const levelDetails = hls.levels[candidate].details;
if (levelDetails) {
const fragCandidate = findFragmentByPTS(
data.frag,
levelDetails.fragments,
data.frag.start
);
if (fragCandidate?.gap) {
continue;
}
}
}
nextLevel = candidate;
break;
}
Expand Down Expand Up @@ -384,7 +402,7 @@ export default class ErrorController implements NetworkComponentAPI {

private switchLevel(data: ErrorData, levelIndex: number | undefined) {
if (levelIndex !== undefined && data.errorAction) {
this.warn(`${data.details}: switching to level ${levelIndex}`);
this.warn(`switching to level ${levelIndex} after ${data.details}`);
this.hls.nextAutoLevel = levelIndex;
data.errorAction.resolved = true;
// Stream controller is responsible for this but won't switch on false start
Expand Down
84 changes: 40 additions & 44 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,25 +302,42 @@ export default class StreamController
} else if (this.backtrackFragment && bufferInfo.len) {
this.backtrackFragment = null;
}
// Avoid loop loading by using nextLoadPosition set for backtracking
if (
frag &&
this.fragmentTracker.getState(frag) === FragmentState.OK &&
this.nextLoadPosition > targetBufferTime
) {
// Cleanup the fragment tracker before trying to find the next unbuffered fragment
const type =
this.audioOnly && !this.altAudio
? ElementaryStreamTypes.AUDIO
: ElementaryStreamTypes.VIDEO;
const mediaBuffer =
(type === ElementaryStreamTypes.VIDEO
? this.videoBuffer
: this.mediaBuffer) || this.media;
if (mediaBuffer) {
this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
if (frag) {
// Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags
const trackerState = this.fragmentTracker.getState(frag);
if (
(trackerState === FragmentState.OK ||
(trackerState === FragmentState.PARTIAL && frag.gap)) &&
this.nextLoadPosition > targetBufferTime
) {
const gapStart = frag.gap;
if (!gapStart) {
// Cleanup the fragment tracker before trying to find the next unbuffered fragment
const type =
this.audioOnly && !this.altAudio
? ElementaryStreamTypes.AUDIO
: ElementaryStreamTypes.VIDEO;
const mediaBuffer =
(type === ElementaryStreamTypes.VIDEO
? this.videoBuffer
: this.mediaBuffer) || this.media;
if (mediaBuffer) {
this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN);
}
}
frag = this.getNextFragment(this.nextLoadPosition, levelDetails);
if (gapStart && frag && !frag.gap && bufferInfo.nextStart) {
// Make sure this doesn't make the next buffer timerange exceed forward buffer length after a gap
const nextbufferInfo = this.getFwdBufferInfoAtPos(
this.mediaBuffer ? this.mediaBuffer : this.media,
bufferInfo.nextStart,
PlaylistLevelType.MAIN
);
if (nextbufferInfo !== null && nextbufferInfo.len > maxBufLen) {
return;
}
}
}
frag = this.getNextFragment(this.nextLoadPosition, levelDetails);
}
if (!frag) {
return;
Expand Down Expand Up @@ -877,32 +894,11 @@ export default class StreamController
}
break;
case ErrorDetails.BUFFER_FULL_ERROR:
// if in appending state
if (
data.parent === 'main' &&
(this.state === State.PARSING || this.state === State.PARSED)
) {
let flushBuffer = true;
const bufferedInfo = this.getFwdBufferInfo(
this.media,
PlaylistLevelType.MAIN
);
// 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end
// reduce max buf len if current position is buffered
if (bufferedInfo && bufferedInfo.len > 0.5) {
flushBuffer = !this.reduceMaxBufferLength(bufferedInfo.len);
}
if (flushBuffer) {
// current position is not buffered, but browser is still complaining about buffer full error
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
// in that case flush the whole buffer to recover
this.warn(
'buffer full error also media.currentTime is not buffered, flush main'
);
// flush main buffer
this.immediateLevelSwitch();
}
this.resetLoadingState();
if (!data.parent || data.parent !== 'main') {
return;
}
if (this.reduceLengthAndFlushBuffer(data)) {
this.flushMainBuffer(0, Number.POSITIVE_INFINITY);
}
break;
default:
Expand Down

0 comments on commit 938a05e

Please sign in to comment.