mirror of
https://github.com/video-dev/hls.js.git
synced 2026-05-17 13:30:38 +00:00
Fixed issue where certain Tizen devices would get stuck seeking over buffer gaps (#7630)
Authored-by: Kyle Seager <kyle.seager@mydirectv.com>
This commit is contained in:
@@ -2011,6 +2011,7 @@ export type GapControllerConfig = {
|
||||
nudgeOffset: number;
|
||||
nudgeMaxRetry: number;
|
||||
nudgeOnVideoHole: boolean;
|
||||
skipBufferHolePadding: number;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "HdcpLevel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
||||
+17
-1
@@ -41,6 +41,7 @@ See [API Reference](https://hlsjs-dev.video-dev.org/api-docs/) for a complete li
|
||||
- [`nudgeOffset`](#nudgeoffset)
|
||||
- [`nudgeMaxRetry`](#nudgemaxretry)
|
||||
- [`nudgeOnVideoHole`](#nudgeonvideohole)
|
||||
- [`skipBufferHolePadding`](#skipbufferholepadding)
|
||||
- [`maxFragLookUpTolerance`](#maxfraglookuptolerance)
|
||||
- [`maxMaxBufferLength`](#maxmaxbufferlength)
|
||||
- [`liveSyncMode`](#livesyncmode)
|
||||
@@ -695,7 +696,10 @@ In case playback continues to stall after first playhead nudging, currentTime wi
|
||||
|
||||
(default: `3`)
|
||||
|
||||
Max number of playhead (`currentTime`) nudges before HLS.js raise a fatal BUFFER_STALLED_ERROR
|
||||
Maximum retry threshold used for both buffer hole skipping and playhead nudging:
|
||||
|
||||
- **Skip retries**: When jumping over buffer holes, if `skipRetry > nudgeMaxRetry`, a fatal `BUFFER_SEEK_OVER_HOLE` error is raised
|
||||
- **Nudge retries**: When nudging the playhead in buffered areas, if `nudgeRetry >= nudgeMaxRetry`, a fatal `BUFFER_STALLED_ERROR` is raised
|
||||
|
||||
### `nudgeOnVideoHole`
|
||||
|
||||
@@ -703,6 +707,18 @@ Max number of playhead (`currentTime`) nudges before HLS.js raise a fatal BUFFER
|
||||
|
||||
Whether or not HLS.js should perform a seek nudge to flush the rendering pipeline upon traversing a gap or hole in video SourceBuffer buffered time ranges. This is only performed when audio is buffered at the point where the hole is detected. For more information see `nudgeOnVideoHole` in gap-controller and issues https://issues.chromium.org/issues/40280613#comment10 and https://github.com/video-dev/hls.js/issues/5631.
|
||||
|
||||
### `skipBufferHolePadding`
|
||||
|
||||
(default: `0.1` seconds)
|
||||
|
||||
Workaround for some platforms where the video element often rounds the value we want to set as `currentTime`, preventing the player from jumping over buffer gaps.
|
||||
|
||||
Setting this to a higher value adds additional time to the skip buffer hole target time, which skips more media but mitigates infinite attempts to skip the same buffer hole.
|
||||
|
||||
Known to be helpful for platforms such as Xbox, Legacy Edge, and Tizen. Based on research on Tizen, the `skipBufferHolePadding` value should be greater than your GOP (Group of Pictures) length.
|
||||
|
||||
`media.currentTime = Math.max(nextBufferedRangeStartTime, media.currentTime) + skipBufferHolePadding`
|
||||
|
||||
### `maxFragLookUpTolerance`
|
||||
|
||||
(default 0.25s)
|
||||
|
||||
@@ -230,6 +230,7 @@ export type GapControllerConfig = {
|
||||
nudgeOffset: number;
|
||||
nudgeMaxRetry: number;
|
||||
nudgeOnVideoHole: boolean;
|
||||
skipBufferHolePadding: number;
|
||||
};
|
||||
|
||||
export type SelectionPreferences = {
|
||||
@@ -384,6 +385,7 @@ export const hlsDefaultConfig: HlsConfig = {
|
||||
nudgeOffset: 0.1, // used by gap-controller
|
||||
nudgeMaxRetry: 3, // used by gap-controller
|
||||
nudgeOnVideoHole: true, // used by gap-controller
|
||||
skipBufferHolePadding: 0.1, // used by gap-controller
|
||||
liveSyncMode: 'edge', // used by stream-controller
|
||||
liveSyncDurationCount: 3, // used by latency-controller
|
||||
liveSyncOnStallIncrease: 1, // used by latency-controller
|
||||
|
||||
@@ -24,8 +24,6 @@ import type { ErrorData } from '../types/events';
|
||||
import type { BufferInfo } from '../utils/buffer-helper';
|
||||
|
||||
export const MAX_START_GAP_JUMP = 2.0;
|
||||
export const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
|
||||
export const SKIP_BUFFER_RANGE_START = 0.05;
|
||||
const TICK_INTERVAL = 100;
|
||||
|
||||
export default class GapController extends TaskLoop {
|
||||
@@ -35,6 +33,7 @@ export default class GapController extends TaskLoop {
|
||||
private mediaSource?: MediaSource;
|
||||
|
||||
private nudgeRetry: number = 0;
|
||||
private skipRetry: number = 0;
|
||||
private stallReported: boolean = false;
|
||||
private stalled: number | null = null;
|
||||
private moved: boolean = false;
|
||||
@@ -178,7 +177,7 @@ export default class GapController extends TaskLoop {
|
||||
}
|
||||
this.moved = true;
|
||||
if (!seeking) {
|
||||
this.nudgeRetry = 0;
|
||||
this.skipRetry = this.nudgeRetry = 0;
|
||||
// When crossing between buffered video time ranges, but not audio, flush pipeline with seek (Chrome)
|
||||
if (
|
||||
config.nudgeOnVideoHole &&
|
||||
@@ -204,7 +203,7 @@ export default class GapController extends TaskLoop {
|
||||
|
||||
// The playhead should not be moving
|
||||
if (pausedEndedOrHalted) {
|
||||
this.nudgeRetry = 0;
|
||||
this.skipRetry = this.nudgeRetry = 0;
|
||||
this.stallResolved(currentTime);
|
||||
// Fire MEDIA_ENDED to workaround event not being dispatched by browser
|
||||
if (!this.ended && media.ended && this.hls) {
|
||||
@@ -217,7 +216,7 @@ export default class GapController extends TaskLoop {
|
||||
}
|
||||
|
||||
if (!BufferHelper.getBuffered(media).length) {
|
||||
this.nudgeRetry = 0;
|
||||
this.skipRetry = this.nudgeRetry = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -595,23 +594,27 @@ export default class GapController extends TaskLoop {
|
||||
}
|
||||
}
|
||||
}
|
||||
const targetTime = Math.max(
|
||||
startTime + SKIP_BUFFER_RANGE_START,
|
||||
currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS,
|
||||
);
|
||||
this.warn(
|
||||
`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`,
|
||||
);
|
||||
this.moved = true;
|
||||
media.currentTime = targetTime;
|
||||
if (!appended?.gap) {
|
||||
const { nudgeMaxRetry, skipBufferHolePadding } = config;
|
||||
const fatal = ++this.skipRetry > nudgeMaxRetry;
|
||||
const targetTime =
|
||||
Math.max(startTime, currentTime) + skipBufferHolePadding;
|
||||
if (!fatal) {
|
||||
this.warn(
|
||||
`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`,
|
||||
);
|
||||
this.moved = true;
|
||||
media.currentTime = targetTime;
|
||||
}
|
||||
if (!appended?.gap || fatal) {
|
||||
const error = new Error(
|
||||
`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`,
|
||||
fatal
|
||||
? `Playhead still not moving after seeking over buffer hole from ${currentTime} to ${targetTime} after ${config.nudgeMaxRetry} attempts.`
|
||||
: `fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`,
|
||||
);
|
||||
const errorData: ErrorData = {
|
||||
type: ErrorTypes.MEDIA_ERROR,
|
||||
details: ErrorDetails.BUFFER_SEEK_OVER_HOLE,
|
||||
fatal: false,
|
||||
fatal,
|
||||
error,
|
||||
reason: error.message,
|
||||
buffer: bufferInfo.len,
|
||||
|
||||
@@ -3,9 +3,7 @@ import sinon from 'sinon';
|
||||
import sinonChai from 'sinon-chai';
|
||||
import { State } from '../../../src/controller/base-stream-controller';
|
||||
import { FragmentTracker } from '../../../src/controller/fragment-tracker';
|
||||
import GapController, {
|
||||
SKIP_BUFFER_RANGE_START,
|
||||
} from '../../../src/controller/gap-controller';
|
||||
import GapController from '../../../src/controller/gap-controller';
|
||||
import { ErrorDetails, ErrorTypes } from '../../../src/errors';
|
||||
import { Events } from '../../../src/events';
|
||||
import Hls from '../../../src/hls';
|
||||
@@ -445,7 +443,9 @@ describe('GapController', function () {
|
||||
|
||||
expect(gapController.moved).to.equal(true);
|
||||
expect(gapController.stalled).to.equal(1234);
|
||||
expect(mockMedia.currentTime).to.equal(0.1 + SKIP_BUFFER_RANGE_START);
|
||||
expect(mockMedia.currentTime).to.equal(
|
||||
0.1 + config.skipBufferHolePadding,
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip any initial gap when not having played yet on second poll', function () {
|
||||
@@ -453,7 +453,9 @@ describe('GapController', function () {
|
||||
mockTimeRangesData = [[0.9, 10]];
|
||||
gapController.poll(0, mockMedia.currentTime);
|
||||
gapController.poll(0, mockMedia.currentTime);
|
||||
expect(mockMedia.currentTime).to.equal(0.9 + SKIP_BUFFER_RANGE_START);
|
||||
expect(mockMedia.currentTime).to.equal(
|
||||
0.9 + config.skipBufferHolePadding,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user