Files
hls.js/tests/unit/controller/gap-controller.js
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Tom Jenkinson
359197f20b chore(deps): update dependency prettier to v3 (#5646)
* chore(deps): update dependency prettier to v3

* run prettier

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tom Jenkinson <tom@tjenkinson.me>
2023-08-22 11:24:59 +01:00

367 lines
12 KiB
JavaScript

import Hls from '../../../src/hls';
import GapController, {
SKIP_BUFFER_RANGE_START,
} from '../../../src/controller/gap-controller';
import { FragmentTracker } from '../../../src/controller/fragment-tracker';
import { Events } from '../../../src/events';
import { ErrorTypes, ErrorDetails } from '../../../src/errors';
describe('GapController', function () {
let hls;
let gapController;
let config;
let media;
let triggerSpy;
const sandbox = sinon.createSandbox();
beforeEach(function () {
hls = new Hls({});
hls.networkControllers.forEach((component) => component.destroy());
hls.networkControllers.length = 0;
hls.coreComponents.forEach((component) => component.destroy());
hls.coreComponents.length = 0;
media = {
currentTime: 0,
};
config = hls.config;
gapController = new GapController(
config,
media,
new FragmentTracker(hls),
hls,
);
triggerSpy = sinon.spy(hls, 'trigger');
});
afterEach(function () {
sandbox.restore();
hls.destroy();
});
describe('_tryNudgeBuffer', function () {
it('should increment the currentTime by a multiple of nudgeRetry and the configured nudge amount', function () {
for (let i = 0; i < config.nudgeMaxRetry; i++) {
triggerSpy.resetHistory();
const expected = media.currentTime + (i + 1) * config.nudgeOffset;
gapController._tryNudgeBuffer();
expect(media.currentTime).to.equal(expected);
expect(triggerSpy).to.have.been.calledWith(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
fatal: false,
error: triggerSpy.getCall(0).lastArg.error,
});
}
triggerSpy.resetHistory();
gapController._tryNudgeBuffer();
expect(triggerSpy).not.to.have.been.calledWith(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_NUDGE_ON_STALL,
fatal: false,
});
});
it('should not increment the currentTime if the max amount of nudges has been attempted', function () {
config.nudgeMaxRetry = 0;
gapController._tryNudgeBuffer();
expect(media.currentTime).to.equal(0);
expect(triggerSpy).to.have.been.calledWith(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_STALLED_ERROR,
fatal: true,
error: triggerSpy.getCall(0).lastArg.error,
});
});
});
describe('_reportStall', function () {
it('should report a stall with the current buffer length if it has not already been reported', function () {
gapController._reportStall({ len: 42 });
expect(triggerSpy).to.have.been.calledWith(Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.BUFFER_STALLED_ERROR,
fatal: false,
error: triggerSpy.getCall(0).lastArg.error,
buffer: 42,
});
});
it('should not report a stall if it was already reported', function () {
gapController.stallReported = true;
gapController._reportStall({ len: 42 });
expect(triggerSpy).to.not.have.been.called;
});
});
describe('_tryFixBufferStall', function () {
it('should nudge when stalling close to the buffer end', function () {
const mockBufferInfo = { len: 1 };
const mockStallDuration = (config.highBufferWatchdogPeriod + 1) * 1000;
const nudgeStub = sandbox.stub(gapController, '_tryNudgeBuffer');
gapController._tryFixBufferStall(mockBufferInfo, mockStallDuration);
expect(nudgeStub).to.have.been.calledOnce;
});
it('should not nudge when briefly stalling close to the buffer end', function () {
const mockBufferInfo = { len: 1 };
const mockStallDuration = (config.highBufferWatchdogPeriod / 2) * 1000;
const nudgeStub = sandbox.stub(gapController, '_tryNudgeBuffer');
gapController._tryFixBufferStall(mockBufferInfo, mockStallDuration);
expect(nudgeStub).to.have.not.been.called;
});
it('should not nudge when too far from the buffer end', function () {
const mockBufferInfo = { len: 0.09 };
const mockStallDuration = (config.highBufferWatchdogPeriod + 1) * 1000;
const nudgeStub = sandbox.stub(gapController, '_tryNudgeBuffer');
gapController._tryFixBufferStall(mockBufferInfo, mockStallDuration);
expect(nudgeStub).to.have.not.been.called;
});
it('should try to jump partial fragments when detected', function () {
sandbox
.stub(gapController.fragmentTracker, 'getPartialFragment')
.returns({});
const skipHoleStub = sandbox.stub(gapController, '_trySkipBufferHole');
gapController._tryFixBufferStall({ len: 0 });
expect(skipHoleStub).to.have.been.calledOnce;
});
it('should not try to jump partial fragments when none are detected', function () {
sandbox
.stub(gapController.fragmentTracker, 'getPartialFragment')
.returns(null);
const skipHoleStub = sandbox.stub(gapController, '_trySkipBufferHole');
gapController._tryFixBufferStall({ len: 0 });
expect(skipHoleStub).to.have.not.been.called;
});
});
describe('media clock polling', function () {
const TIMER_STEP_MS = 1234;
const STALL_HANDLING_RETRY_PERIOD_MS = 1000;
let mockMedia;
let mockTimeRanges;
let mockTimeRangesData;
let reportStallSpy;
let lastCurrentTime;
let isStalling;
let wallClock;
beforeEach(function () {
wallClock = sandbox.useFakeTimers(0);
isStalling = false;
mockTimeRangesData = [
[0.1, 0.2],
[0.4, 0.5],
];
mockTimeRanges = {
get length() {
return mockTimeRangesData.length;
},
start(index) {
return mockTimeRangesData[index][0];
},
end(index) {
return mockTimeRangesData[index][1];
},
};
// by default the media
// is setup in a "playable" state
// note that the initial current time
// is within the range of buffered data info
mockMedia = {
currentTime: 0,
paused: false,
seeking: false,
buffered: mockTimeRanges,
addEventListener() {},
};
gapController.media = mockMedia;
reportStallSpy = sandbox.spy(gapController, '_reportStall');
});
// tickMediaClock emulates the behavior
// of our external polling schedule
// which would progress as the media clock
// is altered (or not)
// when isStalling is false the media clock
// will not progress while the poll call is done
function tickMediaClock(incrementSec = 0.1) {
lastCurrentTime = mockMedia.currentTime;
if (!isStalling) {
mockMedia.currentTime += incrementSec;
}
gapController.poll(lastCurrentTime);
}
function setStalling() {
gapController.moved = true;
mockMedia.paused = false;
mockMedia.currentTime = 4;
mockTimeRangesData.length = 1;
mockTimeRangesData[0] = [0, 10];
lastCurrentTime = 4;
}
function setNotStalling() {
gapController.moved = true;
mockMedia.paused = false;
mockMedia.currentTime = 5;
mockTimeRangesData.length = 1;
mockTimeRangesData[0] = [0, 10];
lastCurrentTime = 4;
}
it('should try to fix a stall if expected to be playing', function () {
const fixStallStub = sandbox.stub(gapController, '_tryFixBufferStall');
setStalling();
gapController.poll(lastCurrentTime);
// The first poll call made while stalling just sets stall flags
expect(gapController.stalled).to.be.a('number');
expect(gapController.stallReported).to.be.false;
gapController.poll(lastCurrentTime);
expect(fixStallStub).to.have.been.calledOnce;
});
it('should reset stall flags when no longer stalling', function () {
setNotStalling();
gapController.stallReported = true;
gapController.nudgeRetry = 1;
gapController.stalled = 4200;
const fixStallStub = sandbox.stub(gapController, '_tryFixBufferStall');
gapController.poll(lastCurrentTime);
expect(gapController.stalled).to.not.exist;
expect(gapController.nudgeRetry).to.equal(0);
expect(gapController.stallReported).to.be.false;
expect(fixStallStub).to.not.have.been.called;
});
it('should not detect stalls when ended, unbuffered or seeking', function () {
wallClock.tick(TIMER_STEP_MS);
// we need to play a bit to get past the moved check
tickMediaClock();
isStalling = true;
mockMedia.ended = true;
tickMediaClock();
expect(gapController.stalled).to.equal(null, 'ended');
wallClock.tick(2 * STALL_HANDLING_RETRY_PERIOD_MS);
mockMedia.ended = false;
mockTimeRangesData.length = 0;
tickMediaClock();
expect(gapController.stalled).to.equal(null, 'empty buffer');
wallClock.tick(2 * STALL_HANDLING_RETRY_PERIOD_MS);
mockTimeRangesData = [
[0.1, 0.2],
[0.4, 0.5],
];
mockMedia.seeking = true;
// tickMediaClock(100)
expect(gapController.stalled).to.equal(null, 'seeking');
wallClock.tick(2 * STALL_HANDLING_RETRY_PERIOD_MS);
});
it('should not detect stalls when loading an earlier fragment while seeking', function () {
wallClock.tick(2 * STALL_HANDLING_RETRY_PERIOD_MS);
mockMedia.currentTime += 0.1;
gapController.poll(0);
expect(gapController.stalled).to.equal(null, 'buffered start');
wallClock.tick(2 * STALL_HANDLING_RETRY_PERIOD_MS);
mockMedia.currentTime += 5;
mockMedia.seeking = true;
mockTimeRangesData.length = 1;
mockTimeRangesData[0] = [5.5, 10];
gapController.poll(mockMedia.currentTime - 5);
expect(gapController.stalled).to.equal(null, 'new seek position');
wallClock.tick(2 * STALL_HANDLING_RETRY_PERIOD_MS);
gapController.poll(mockMedia.currentTime, {
start: 5,
});
expect(gapController.stalled).to.equal(
null,
'seeking while loading fragment',
);
});
it('should trigger reportStall when stalling for 250ms or longer', function () {
setStalling();
wallClock.tick(250);
gapController.stalled = 1;
gapController.poll(lastCurrentTime);
expect(reportStallSpy).to.not.have.been.called;
wallClock.tick(251);
gapController.poll(lastCurrentTime);
expect(reportStallSpy).to.have.been.calledOnce;
});
it('should not handle a stall (clock not advancing) when media has played before and is now paused', function () {
wallClock.tick(TIMER_STEP_MS);
tickMediaClock();
expect(gapController.moved).to.equal(true);
expect(gapController.stalled).to.equal(null);
mockMedia.paused = true;
isStalling = true;
tickMediaClock();
expect(gapController.stalled).to.equal(null);
mockMedia.paused = false;
tickMediaClock();
expect(gapController.stalled).to.equal(TIMER_STEP_MS);
});
it('should skip any initial gap before playing on the second poll (so that Chrome can jump the gap first)', function () {
wallClock.tick(TIMER_STEP_MS);
mockMedia.currentTime = 0;
isStalling = true;
tickMediaClock();
expect(gapController.moved).to.equal(false);
expect(gapController.stalled).to.equal(1234);
expect(mockMedia.currentTime).to.equal(0);
tickMediaClock();
expect(gapController.moved).to.equal(true);
expect(gapController.stalled).to.equal(null);
expect(mockMedia.currentTime).to.equal(0.1 + SKIP_BUFFER_RANGE_START);
});
it('should skip any initial gap when not having played yet on second poll', function () {
mockMedia.currentTime = 0;
mockTimeRangesData = [[0.9, 10]];
gapController.poll(0);
gapController.poll(0);
expect(mockMedia.currentTime).to.equal(0.9 + SKIP_BUFFER_RANGE_START);
});
});
});