Skip to content

Commit

Permalink
Add config flag for maximum front buffer frontBufferFlushThreshold (#…
Browse files Browse the repository at this point in the history
…5761)

Fixes #5479

Co-authored-by: Evan Burton <eburton2@apple.com>
  • Loading branch information
robwalch and iamboorrito committed Aug 31, 2023
1 parent f27c436 commit 301be58
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 106 deletions.
7 changes: 7 additions & 0 deletions MIGRATING.md
Expand Up @@ -26,6 +26,13 @@ Set `backBufferLength` to `Infinity` and `liveBackBufferLength` to `90` if you w
eviction for Live and VOD streams as older versions did. While `liveBackBufferLength` can still be used, it has been
marked deprecated and may be removed in an upcoming minor release.

### Front Buffer Eviction

The new `frontBufferFlushThreshold` setting defaults to Infinity seconds and governs active eviction of buffered ranges outside of
the current contiguous front buffer. For example, given currentTime=0 and bufferedRanges=[[0, 100], [150, 200]] with
a configured frontBufferFlushThreshold=60, we will only remove the range from [150, 200] as it lies outside of the target buffer length
and is not contiguous with the forward buffer from the currentTime of 0.

### Low Latency Streams

The new `lowLatencyMode` setting is enabled by default. Set to `false` to disable Low-latency part loading and target
Expand Down
7 changes: 6 additions & 1 deletion api-extractor/report/hls.js.api.md
Expand Up @@ -555,7 +555,9 @@ export class BufferController implements ComponentAPI {
// (undocumented)
protected error: (msg: any, obj?: any) => void;
// (undocumented)
flushBackBuffer(): void;
flushBackBuffer(currentTime: number, targetDuration: number, targetBackBufferPosition: number): void;
// (undocumented)
flushFrontBuffer(currentTime: number, targetDuration: number, targetFrontBufferPosition: number): void;
// (undocumented)
hasSourceTypes(): boolean;
// (undocumented)
Expand Down Expand Up @@ -595,6 +597,8 @@ export class BufferController implements ComponentAPI {
// (undocumented)
tracks: TrackSet;
// (undocumented)
trimBuffers(): void;
// (undocumented)
protected unregisterListeners(): void;
// (undocumented)
updateSeekableRange(levelDetails: any): void;
Expand All @@ -608,6 +612,7 @@ export class BufferController implements ComponentAPI {
export type BufferControllerConfig = {
appendErrorMaxRetry: number;
backBufferLength: number;
frontBufferFlushThreshold: number;
liveDurationInfinity: boolean;
liveBackBufferLength: number | null;
};
Expand Down
8 changes: 8 additions & 0 deletions docs/API.md
Expand Up @@ -28,6 +28,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
- [`initialLiveManifestSize`](#initiallivemanifestsize)
- [`maxBufferLength`](#maxbufferlength)
- [`backBufferLength`](#backbufferlength)
- [`frontBufferFlushThreshold`](#frontbufferflushthreshold)
- [`maxBufferSize`](#maxbuffersize)
- [`maxBufferHole`](#maxbufferhole)
- [`maxStarvationDelay`](#maxstarvationdelay)
Expand Down Expand Up @@ -364,6 +365,7 @@ var config = {
maxBufferLength: 30,
maxMaxBufferLength: 600,
backBufferLength: Infinity,
frontBufferFlushThreshold: Infinity,
maxBufferSize: 60 * 1000 * 1000,
maxBufferHole: 0.5,
highBufferWatchdogPeriod: 2,
Expand Down Expand Up @@ -518,6 +520,12 @@ This is the guaranteed buffer length hls.js will try to reach, regardless of max

The maximum duration of buffered media to keep once it has been played, in seconds. Any video buffered past this duration will be evicted. `Infinity` means no restriction on back buffer length; `0` keeps the minimum amount. The minimum amount is equal to the target duration of a segment to ensure that current playback is not interrupted.

### `frontBufferFlushThreshold`

(default: `Infinity`)

The maximum duration of buffered media, in seconds, from the play position to keep before evicting non-contiguous forward ranges. A value of `Infinity` means no active eviction will take place; This value will always be at least the `maxBufferLength`.

### `maxBufferSize`

(default: 60 MB)
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Expand Up @@ -50,6 +50,7 @@ export type ABRControllerConfig = {
export type BufferControllerConfig = {
appendErrorMaxRetry: number;
backBufferLength: number;
frontBufferFlushThreshold: number;
liveDurationInfinity: boolean;
/**
* @deprecated use backBufferLength
Expand Down Expand Up @@ -330,6 +331,7 @@ export const hlsDefaultConfig: HlsConfig = {
initialLiveManifestSize: 1, // used by stream-controller
maxBufferLength: 30, // used by stream-controller
backBufferLength: Infinity, // used by buffer-controller
frontBufferFlushThreshold: Infinity,
maxBufferSize: 60 * 1000 * 1000, // used by stream-controller
maxBufferHole: 0.1, // used by stream-controller
highBufferWatchdogPeriod: 2, // used by stream-controller
Expand Down
115 changes: 97 additions & 18 deletions src/controller/buffer-controller.ts
Expand Up @@ -33,6 +33,7 @@ import type { ComponentAPI } from '../types/component-api';
import type { ChunkMetadata } from '../types/transmuxer';
import type Hls from '../hls';
import type { LevelDetails } from '../loader/level-details';
import type { HlsConfig } from '../hls';

const VIDEO_CODEC_PROFILE_REPLACE =
/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/;
Expand Down Expand Up @@ -611,7 +612,7 @@ export default class BufferController implements ComponentAPI {
}

private onFragChanged(event: Events.FRAG_CHANGED, data: FragChangedData) {
this.flushBackBuffer();
this.trimBuffers();
}

// on BUFFER_EOS mark matching sourcebuffer(s) as ended and trigger checkEos()
Expand Down Expand Up @@ -670,8 +671,8 @@ export default class BufferController implements ComponentAPI {
}
}

flushBackBuffer() {
const { hls, details, media, sourceBuffer } = this;
trimBuffers() {
const { hls, details, media } = this;
if (!media || details === null) {
return;
}
Expand All @@ -681,22 +682,59 @@ export default class BufferController implements ComponentAPI {
return;
}

const config: Readonly<HlsConfig> = hls.config;
const currentTime = media.currentTime;
const targetDuration = details.levelTargetDuration;

// Support for deprecated liveBackBufferLength
const backBufferLength =
details.live && hls.config.liveBackBufferLength !== null
? hls.config.liveBackBufferLength
: hls.config.backBufferLength;
details.live && config.liveBackBufferLength !== null
? config.liveBackBufferLength
: config.backBufferLength;

if (Number.isFinite(backBufferLength) && backBufferLength > 0) {
const maxBackBufferLength = Math.max(backBufferLength, targetDuration);
const targetBackBufferPosition =
Math.floor(currentTime / targetDuration) * targetDuration -
maxBackBufferLength;

this.flushBackBuffer(
currentTime,
targetDuration,
targetBackBufferPosition,
);
}

if (!Number.isFinite(backBufferLength) || backBufferLength < 0) {
return;
if (
Number.isFinite(config.frontBufferFlushThreshold) &&
config.frontBufferFlushThreshold > 0
) {
const frontBufferLength = Math.max(
config.maxBufferLength,
config.frontBufferFlushThreshold,
);

const maxFrontBufferLength = Math.max(frontBufferLength, targetDuration);
const targetFrontBufferPosition =
Math.floor(currentTime / targetDuration) * targetDuration +
maxFrontBufferLength;

this.flushFrontBuffer(
currentTime,
targetDuration,
targetFrontBufferPosition,
);
}
}

flushBackBuffer(
currentTime: number,
targetDuration: number,
targetBackBufferPosition: number,
) {
const { details, sourceBuffer } = this;
const sourceBufferTypes = this.getSourceBufferTypes();

const currentTime = media.currentTime;
const targetDuration = details.levelTargetDuration;
const maxBackBufferLength = Math.max(backBufferLength, targetDuration);
const targetBackBufferPosition =
Math.floor(currentTime / targetDuration) * targetDuration -
maxBackBufferLength;
sourceBufferTypes.forEach((type: SourceBufferName) => {
const sb = sourceBuffer[type];
if (sb) {
Expand All @@ -706,13 +744,13 @@ export default class BufferController implements ComponentAPI {
buffered.length > 0 &&
targetBackBufferPosition > buffered.start(0)
) {
hls.trigger(Events.BACK_BUFFER_REACHED, {
this.hls.trigger(Events.BACK_BUFFER_REACHED, {
bufferEnd: targetBackBufferPosition,
});

// Support for deprecated event:
if (details.live) {
hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
if (details?.live) {
this.hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, {
bufferEnd: targetBackBufferPosition,
});
} else if (
Expand All @@ -725,7 +763,7 @@ export default class BufferController implements ComponentAPI {
return;
}

hls.trigger(Events.BUFFER_FLUSHING, {
this.hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: 0,
endOffset: targetBackBufferPosition,
type,
Expand All @@ -735,6 +773,47 @@ export default class BufferController implements ComponentAPI {
});
}

flushFrontBuffer(
currentTime: number,
targetDuration: number,
targetFrontBufferPosition: number,
) {
const { sourceBuffer } = this;
const sourceBufferTypes = this.getSourceBufferTypes();

sourceBufferTypes.forEach((type: SourceBufferName) => {
const sb = sourceBuffer[type];
if (sb) {
const buffered = BufferHelper.getBuffered(sb);
const numBufferedRanges = buffered.length;
// The buffer is either empty or contiguous
if (numBufferedRanges < 2) {
return;
}
const bufferStart = buffered.start(numBufferedRanges - 1);
const bufferEnd = buffered.end(numBufferedRanges - 1);
// No flush if we can tolerate the current buffer length or the current buffer range we would flush is contiguous with current position
if (
targetFrontBufferPosition > bufferStart ||
(currentTime >= bufferStart && currentTime <= bufferEnd)
) {
return;
} else if (sb.ended && currentTime - bufferEnd < 2 * targetDuration) {
this.log(
`Cannot flush ${type} front buffer while SourceBuffer is in ended state`,
);
return;
}

this.hls.trigger(Events.BUFFER_FLUSHING, {
startOffset: bufferStart,
endOffset: Infinity,
type,
});
}
});
}

/**
* Update Media Source duration to current level duration or override to Infinity if configuration parameter
* 'liveDurationInfinity` is set to `true`
Expand Down

0 comments on commit 301be58

Please sign in to comment.