Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix forward buffer length estimation and error handling #3961

Merged
merged 1 commit into from May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
81 changes: 45 additions & 36 deletions src/controller/audio-stream-controller.ts
Expand Up @@ -13,7 +13,6 @@ import type { TransmuxerResult } from '../types/transmuxer';
import { ChunkMetadata } from '../types/transmuxer';
import { fragmentWithinToleranceTest } from './fragment-finders';
import { alignPDT } from '../utils/discontinuities';
import { MAX_START_GAP_JUMP } from './gap-controller';
import { ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import type Hls from '../hls';
Expand Down Expand Up @@ -55,6 +54,7 @@ class AudioStreamController
private trackId: number = -1;
private waitingData: WaitingForPTSData | null = null;
private mainDetails: LevelDetails | null = null;
private bufferFlushed: boolean = false;

constructor(hls: Hls, fragmentTracker: FragmentTracker) {
super(hls, fragmentTracker, '[audio-stream-controller]');
Expand Down Expand Up @@ -270,11 +270,6 @@ class AudioStreamController
return;
}

const pos = this.getLoadPosition();
if (!Number.isFinite(pos)) {
return;
}

const levelInfo = levels[trackId];

const trackDetails = levelInfo.details;
Expand All @@ -287,25 +282,24 @@ class AudioStreamController
return;
}

let targetBufferTime = 0;
const mediaBuffer = this.mediaBuffer ? this.mediaBuffer : this.media;
const videoBuffer = this.videoBuffer ? this.videoBuffer : this.media;
const maxBufferHole =
pos < config.maxBufferHole
? Math.max(MAX_START_GAP_JUMP, config.maxBufferHole)
: config.maxBufferHole;
const bufferInfo = BufferHelper.bufferInfo(mediaBuffer, pos, maxBufferHole);
const mainBufferInfo = BufferHelper.bufferInfo(
videoBuffer,
pos,
maxBufferHole
if (this.bufferFlushed) {
this.bufferFlushed = false;
this.afterBufferFlushed(
this.mediaBuffer ? this.mediaBuffer : this.media,
ElementaryStreamTypes.AUDIO,
PlaylistLevelType.AUDIO
);
}

const bufferInfo = this.getFwdBufferInfo(
this.mediaBuffer ? this.mediaBuffer : this.media,
PlaylistLevelType.AUDIO
);
if (bufferInfo === null) {
return;
}
const bufferLen = bufferInfo.len;
const maxConfigBuffer = Math.min(
config.maxBufferLength,
config.maxMaxBufferLength
);
const maxBufLen = Math.max(maxConfigBuffer, mainBufferInfo.len);
const maxBufLen = this.getMaxBufferLength();
const audioSwitch = this.audioSwitch;

// if buffer length is less than maxBufLen try to load a new fragment
Expand All @@ -321,9 +315,10 @@ class AudioStreamController

const fragments = trackDetails.fragments;
const start = fragments[0].start;
targetBufferTime = bufferInfo.end;
let targetBufferTime = bufferInfo.end;

if (audioSwitch) {
const pos = this.getLoadPosition();
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) {
Expand All @@ -339,6 +334,7 @@ class AudioStreamController

const frag = this.getNextFragment(targetBufferTime, trackDetails);
if (!frag) {
this.bufferFlushed = true;
return;
}

Expand All @@ -349,6 +345,18 @@ class AudioStreamController
}
}

protected getMaxBufferLength(): number {
const maxConfigBuffer = super.getMaxBufferLength();
const mainBufferInfo = this.getFwdBufferInfo(
this.videoBuffer ? this.videoBuffer : this.media,
PlaylistLevelType.MAIN
);
if (mainBufferInfo === null) {
return maxConfigBuffer;
}
return Math.max(maxConfigBuffer, mainBufferInfo.len);
}

onMediaDetaching() {
this.videoBuffer = null;
super.onMediaDetaching();
Expand Down Expand Up @@ -399,6 +407,7 @@ class AudioStreamController
this.mainDetails = null;
this.fragmentTracker.removeAllFragments();
this.startPosition = this.lastCurrentTime = 0;
this.bufferFlushed = false;
}

onLevelLoaded(event: Events.LEVEL_LOADED, data: LevelLoadedData) {
Expand Down Expand Up @@ -625,17 +634,17 @@ class AudioStreamController
data.parent === 'audio' &&
(this.state === State.PARSING || this.state === State.PARSED)
) {
const media = this.mediaBuffer;
const currentTime = this.media.currentTime;
const mediaBuffered =
media &&
BufferHelper.isBuffered(media, currentTime) &&
BufferHelper.isBuffered(media, currentTime + 0.5);
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 (mediaBuffered) {
this.reduceMaxBufferLength();
this.state = State.IDLE;
} else {
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
Expand All @@ -645,6 +654,7 @@ class AudioStreamController
this.fragCurrent = null;
super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio');
}
this.resetLoadingState();
}
break;
default:
Expand All @@ -657,8 +667,7 @@ class AudioStreamController
{ type }: BufferFlushedData
) {
if (type === ElementaryStreamTypes.AUDIO) {
const media = this.mediaBuffer ? this.mediaBuffer : this.media;
this.afterBufferFlushed(media, type, PlaylistLevelType.AUDIO);
this.bufferFlushed = true;
}
}

Expand Down
49 changes: 48 additions & 1 deletion src/controller/base-stream-controller.ts
Expand Up @@ -729,6 +729,53 @@ export default class BaseStreamController
}
}

protected getFwdBufferInfo(
bufferable: Bufferable,
type: PlaylistLevelType
): {
len: number;
start: number;
end: number;
nextStart?: number;
} | null {
const { config } = this;
const pos = this.getLoadPosition();
if (!Number.isFinite(pos)) {
return null;
}
const bufferInfo = BufferHelper.bufferInfo(
bufferable,
pos,
config.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)
);
}
}
return bufferInfo;
}

protected getMaxBufferLength(levelBitrate?: number): number {
const { config } = this;
let maxBufLen;
if (levelBitrate) {
maxBufLen = Math.max(
(8 * config.maxBufferSize) / levelBitrate,
config.maxBufferLength
);
} else {
maxBufLen = config.maxBufferLength;
}
return Math.min(maxBufLen, config.maxMaxBufferLength);
}

protected reduceMaxBufferLength(threshold?: number) {
const config = this.config;
const minLength = threshold || config.maxBufferLength;
Expand Down Expand Up @@ -1105,7 +1152,7 @@ export default class BaseStreamController
const { media } = this;
// if we have not yet loaded any fragment, start loading from start position
let pos = 0;
if (this.loadedmetadata) {
if (this.loadedmetadata && media) {
pos = media.currentTime;
} else if (this.nextLoadPosition) {
pos = this.nextLoadPosition;
Expand Down
1 change: 1 addition & 0 deletions src/controller/buffer-operation-queue.ts
Expand Up @@ -67,6 +67,7 @@ export default class BufferOperationQueue {
// Only shift the current operation off, otherwise the updateend handler will do this for us
if (!sb || !sb.updating) {
queue.shift();
this.executeNext(type);
}
}
}
Expand Down
89 changes: 39 additions & 50 deletions src/controller/stream-controller.ts
Expand Up @@ -7,33 +7,33 @@ import type { FragmentTracker } from './fragment-tracker';
import { FragmentState } from './fragment-tracker';
import type { Level } from '../types/level';
import { PlaylistLevelType } from '../types/loader';
import { Fragment, ElementaryStreamTypes } from '../loader/fragment';
import { ElementaryStreamTypes, Fragment } from '../loader/fragment';
import TransmuxerInterface from '../demux/transmuxer-interface';
import type { TransmuxerResult } from '../types/transmuxer';
import { ChunkMetadata } from '../types/transmuxer';
import GapController, { MAX_START_GAP_JUMP } from './gap-controller';
import GapController from './gap-controller';
import { ErrorDetails } from '../errors';
import { logger } from '../utils/logger';
import type Hls from '../hls';
import type { LevelDetails } from '../loader/level-details';
import type { TrackSet } from '../types/track';
import type { SourceBufferName } from '../types/buffer';
import type {
MediaAttachedData,
BufferCreatedData,
ManifestParsedData,
LevelLoadingData,
LevelLoadedData,
LevelsUpdatedData,
AudioTrackSwitchingData,
AudioTrackSwitchedData,
AudioTrackSwitchingData,
BufferCreatedData,
BufferEOSData,
BufferFlushedData,
ErrorData,
FragBufferedData,
FragLoadedData,
FragParsingMetadataData,
FragParsingUserdataData,
FragBufferedData,
BufferFlushedData,
ErrorData,
BufferEOSData,
LevelLoadedData,
LevelLoadingData,
LevelsUpdatedData,
ManifestParsedData,
MediaAttachedData,
} from '../types/events';

const TICK_INTERVAL = 100; // how often to tick in ms
Expand Down Expand Up @@ -244,37 +244,18 @@ export default class StreamController
return;
}

const pos = this.getLoadPosition();
if (!Number.isFinite(pos)) {
const bufferInfo = this.getFwdBufferInfo(
this.mediaBuffer ? this.mediaBuffer : media,
PlaylistLevelType.MAIN
);
if (bufferInfo === null) {
return;
}
const bufferLen = bufferInfo.len;

let targetBufferTime = 0;
// compute max Buffer Length that we could get from this load level, based on level bitrate. don't buffer more than 60 MB and more than 30s
const levelBitrate = levelInfo.maxBitrate;
let maxBufLen;
if (levelBitrate) {
maxBufLen = Math.max(
(8 * config.maxBufferSize) / levelBitrate,
config.maxBufferLength
);
} else {
maxBufLen = config.maxBufferLength;
}
maxBufLen = Math.min(maxBufLen, config.maxMaxBufferLength);
const maxBufLen = this.getMaxBufferLength(levelInfo.maxBitrate);

// determine next candidate fragment to be loaded, based on current position and end of buffer position
// ensure up to `config.maxMaxBufferLength` of buffer upfront
const maxBufferHole =
pos < config.maxBufferHole
? Math.max(MAX_START_GAP_JUMP, config.maxBufferHole)
: config.maxBufferHole;
const bufferInfo = BufferHelper.bufferInfo(
this.mediaBuffer ? this.mediaBuffer : media,
pos,
maxBufferHole
);
const bufferLen = bufferInfo.len;
// Stay idle if we are still with buffer margins
if (bufferLen >= maxBufLen) {
return;
Expand All @@ -291,7 +272,7 @@ export default class StreamController
return;
}

targetBufferTime = bufferInfo.end;
const targetBufferTime = bufferInfo.end;
let frag = this.getNextFragment(targetBufferTime, levelDetails);
// Avoid backtracking after seeking or switching by loading an earlier segment in streams that could backtrack
if (
Expand All @@ -312,6 +293,12 @@ export default class StreamController
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;
this.afterBufferFlushed(media, type, PlaylistLevelType.MAIN);
frag = this.getNextFragment(this.nextLoadPosition, levelDetails);
}
if (!frag) {
Expand Down Expand Up @@ -885,25 +872,27 @@ export default class StreamController
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
const mediaBuffered =
!!this.media &&
BufferHelper.isBuffered(this.media, this.media.currentTime) &&
BufferHelper.isBuffered(this.media, this.media.currentTime + 0.5);
// reduce max buf len if current position is buffered
if (mediaBuffered) {
this.reduceMaxBufferLength();
this.state = State.IDLE;
} else {
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 everything'
'buffer full error also media.currentTime is not buffered, flush main'
);
// flush everything
// flush main buffer
this.immediateLevelSwitch();
}
this.resetLoadingState();
}
break;
default:
Expand Down