Skip to content

Commit

Permalink
Merge pull request #3961 from video-dev/bugfix/buffer-length-limit-wi…
Browse files Browse the repository at this point in the history
…th-gaps-and-reduce

Fix forward buffer length estimation and error handling
  • Loading branch information
robwalch committed May 28, 2021
2 parents bc370f9 + 1a8dd71 commit 40353e6
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 95 deletions.
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

0 comments on commit 40353e6

Please sign in to comment.