mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
Thank you to Tom Jenkinson for acting as a reviewer for this work, including writing some suggestions via the GitHub PR github.com/video-dev/hls.js/pull/2073. The commit history has been squashed as it was a fairly lengthly process to get everything working to support the conversion to TypeScript. Early in the history of this commit, we converted properties to be initialized to `null`. This was incorrect, as it changes the behaviour of code that uses `Object.keys` calls to check if an object has any keys on it. Before, they would be empty, but if the properties are set to null, this check is no longer valid and it changes the behaviour. This is something to look out for in the future of other conversions, as the values themselves are often times reset to the `null`, but begin as undefined. Added a common `types` folder for interfaces where the ownership of the type wasn't clear for the individual file. The default no-op logger which is the inferred usage did not have any arguments in it's method signature. By adding an argument, TypeScript is able to infer the correct interface. As well the Karma and Webpack configuration had to be updated to enable building TypeScript, and getting testing coverage information from Istanbul. Define lib "es2015" and "dom" to add HTMLMediaElement, SourceBuffer and Number.isFinite definitions to the TypeScript binary when it is doing type-checking. When attempting to lint the older version of `typescript-eslint-parser` would emit `no-undef` errors for TypeScript interfaces. Updating the package fixes this issue. Tom pointed out that we should not use global module declarations to extend default HTML types since users will eventually be importing *.d.ts files from the hls.js project and this would also extend their types. Due to this feedback switched to defining a module local type `ExtendedSourceBuffer`. Which instead extends with the `ended` prop. Event Handlers were set to be part of the private scope. While this has no effect now, it will be important to ensure other modules do not directly invoke the event handlers. Other methods that were prefixed with a `_` were also set into the private scope. Where possible, code was switched to early-exit to lower the cognitive load in conditional statements, an example of this was the doAppend method. Another cleanup statement was switching handling of error codes and the comments to be closer to the conditional to which they applied. Switched to using the Record<Key, Value> type to help support access via the index signature [key: T] syntax in the future. An issue that was fixed in this commit was that The controller previous exposed the local `pendingTracks` property through the event and did not set the buffer on `this.tracks` instead opting to set it on the individual track, which bubbled up to the pendingTracks object, whose reference was lost in the buffer-controller module. This commit changes this behaviour so that the controller publishes `tracks` from the public variable on the controller rather than the pendingTracks which are passed into createSourceBuffers. This also means that this fixes the `.buffer.buffered` being undefined on the demo page. Added unit tests to cover the new behaviour of throwing an error if createSourceBuffer is called without an attached media element.
219 lines
7.5 KiB
JavaScript
219 lines
7.5 KiB
JavaScript
import assert from 'assert';
|
|
import sinon from 'sinon';
|
|
import Hls from '../../../src/hls';
|
|
import BufferController from '../../../src/controller/buffer-controller';
|
|
|
|
describe('BufferController tests', function () {
|
|
let hls;
|
|
let bufferController;
|
|
let flushSpy;
|
|
let removeStub;
|
|
const sandbox = sinon.sandbox.create();
|
|
|
|
beforeEach(function () {
|
|
hls = new Hls({});
|
|
bufferController = new BufferController(hls);
|
|
flushSpy = sandbox.spy(bufferController, 'flushLiveBackBuffer');
|
|
removeStub = sandbox.stub(bufferController, 'removeBufferRange');
|
|
});
|
|
|
|
afterEach(function () {
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe('Live back buffer enforcement', function () {
|
|
let mockMedia;
|
|
let mockSourceBuffer;
|
|
let bufStart;
|
|
|
|
beforeEach(function () {
|
|
bufStart = 0;
|
|
bufferController._levelTargetDuration = 10;
|
|
bufferController.media = mockMedia = {
|
|
currentTime: 0
|
|
};
|
|
bufferController.sourceBuffer = mockSourceBuffer = {
|
|
video: {
|
|
buffered: {
|
|
start () {
|
|
return bufStart;
|
|
},
|
|
length: 1
|
|
}
|
|
}
|
|
};
|
|
bufferController._live = true;
|
|
hls.config.liveBackBufferLength = 10;
|
|
});
|
|
|
|
it('exits early if not live', function () {
|
|
bufferController.flushLiveBackBuffer();
|
|
assert(removeStub.notCalled);
|
|
});
|
|
|
|
it('exits early if liveBackBufferLength is not a finite number, or is less than 0', function () {
|
|
hls.config.liveBackBufferLength = 'foo';
|
|
bufferController.flushLiveBackBuffer();
|
|
|
|
hls.config.liveBackBufferLength = -1;
|
|
bufferController.flushLiveBackBuffer();
|
|
|
|
assert(removeStub.notCalled);
|
|
});
|
|
|
|
it('does not flush if nothing is buffered', function () {
|
|
delete mockSourceBuffer.buffered;
|
|
bufferController.flushLiveBackBuffer();
|
|
|
|
mockSourceBuffer = null;
|
|
bufferController.flushLiveBackBuffer();
|
|
|
|
assert(removeStub.notCalled);
|
|
});
|
|
|
|
it('does not flush if no buffered range intersects with back buffer limit', function () {
|
|
bufStart = 5;
|
|
mockMedia.currentTime = 10;
|
|
bufferController.flushLiveBackBuffer();
|
|
assert(removeStub.notCalled);
|
|
});
|
|
|
|
it('does not flush if the liveBackBufferLength is Infinity', function () {
|
|
hls.config.liveBackBufferLength = Infinity;
|
|
mockMedia.currentTime = 15;
|
|
bufferController.flushLiveBackBuffer();
|
|
assert(removeStub.notCalled);
|
|
});
|
|
|
|
it('flushes up to the back buffer limit if the buffer intersects with that point', function () {
|
|
mockMedia.currentTime = 15;
|
|
bufferController.flushLiveBackBuffer();
|
|
assert(removeStub.calledOnce);
|
|
assert(!bufferController.flushBufferCounter, 'Should reset the flushBufferCounter');
|
|
assert(removeStub.calledWith('video', mockSourceBuffer.video, 0, 5));
|
|
});
|
|
|
|
it('flushes to a max of one targetDuration from currentTime, regardless of liveBackBufferLength', function () {
|
|
mockMedia.currentTime = 15;
|
|
bufferController._levelTargetDuration = 5;
|
|
hls.config.liveBackBufferLength = 0;
|
|
bufferController.flushLiveBackBuffer();
|
|
assert(removeStub.calledWith('video', mockSourceBuffer.video, 0, 10));
|
|
});
|
|
|
|
it('should trigger clean back buffer when there are no pending appends', function () {
|
|
bufferController.parent = {};
|
|
bufferController.segments = [{ parent: bufferController.parent }];
|
|
|
|
sandbox.stub(bufferController, 'doAppending');
|
|
|
|
bufferController._onSBUpdateEnd();
|
|
|
|
assert(flushSpy.notCalled, 'clear live back buffer was called');
|
|
|
|
bufferController.segments = [];
|
|
bufferController._onSBUpdateEnd();
|
|
|
|
assert(flushSpy.calledOnce, 'clear live back buffer was not called once');
|
|
});
|
|
});
|
|
|
|
describe('sourcebuffer creation', function () {
|
|
let createSbStub;
|
|
let checkPendingTracksSpy;
|
|
beforeEach(function () {
|
|
createSbStub = sandbox.stub(bufferController, 'createSourceBuffers');
|
|
checkPendingTracksSpy = sandbox.spy(bufferController, 'checkPendingTracks');
|
|
sandbox.stub(bufferController, 'doAppending');
|
|
});
|
|
|
|
it('initializes with zero expected BUFFER_CODEC events', function () {
|
|
assert.strictEqual(bufferController.bufferCodecEventsExpected, 0);
|
|
});
|
|
|
|
it('should throw if no media element has been attached', function () {
|
|
bufferController.createSourceBuffers.restore();
|
|
bufferController.pendingTracks = { video: {} };
|
|
|
|
assert.throws(bufferController.checkPendingTracks);
|
|
});
|
|
|
|
it('exposes tracks from buffer controller through BUFFER_CREATED event', function (done) {
|
|
bufferController.createSourceBuffers.restore();
|
|
|
|
let video = document.createElement('video');
|
|
bufferController.onMediaAttaching({ media: video });
|
|
|
|
hls.on(Hls.Events.BUFFER_CREATED, (_, data) => {
|
|
const tracks = data.tracks;
|
|
assert.notStrictEqual(bufferController.pendingTracks, tracks);
|
|
assert.strictEqual(bufferController.tracks, tracks);
|
|
done();
|
|
});
|
|
|
|
bufferController.pendingTracks = { video: { codec: 'testing' } };
|
|
bufferController.checkPendingTracks();
|
|
|
|
video = null;
|
|
});
|
|
|
|
it('expects one bufferCodec event by default', function () {
|
|
bufferController.onManifestParsed({});
|
|
assert.strictEqual(bufferController.bufferCodecEventsExpected, 1);
|
|
});
|
|
|
|
it('expects two bufferCodec events if altAudio is signaled', function () {
|
|
bufferController.onManifestParsed({ altAudio: true });
|
|
assert.strictEqual(bufferController.bufferCodecEventsExpected, 2);
|
|
});
|
|
|
|
it('creates sourceBuffers when no more BUFFER_CODEC events are expected', function () {
|
|
bufferController.pendingTracks = { video: {} };
|
|
|
|
bufferController.checkPendingTracks();
|
|
assert.strictEqual(createSbStub.calledOnce, true);
|
|
});
|
|
|
|
it('does not create sourceBuffers when BUFFER_CODEC events are expected', function () {
|
|
bufferController.pendingTracks = { video: {} };
|
|
bufferController.bufferCodecEventsExpected = 1;
|
|
|
|
bufferController.checkPendingTracks();
|
|
assert.strictEqual(createSbStub.notCalled, true);
|
|
assert.strictEqual(bufferController.bufferCodecEventsExpected, 1);
|
|
});
|
|
|
|
it('checks pending tracks in onMediaSourceOpen', function () {
|
|
bufferController._onMediaSourceOpen();
|
|
assert.strictEqual(checkPendingTracksSpy.calledOnce, true);
|
|
});
|
|
|
|
it('does not check pending tracks in onBufferCodecs until called for the expected amount of times', function () {
|
|
bufferController.sourceBuffer = {};
|
|
bufferController.mediaSource = { readyState: 'open' };
|
|
bufferController.bufferCodecEventsExpected = 2;
|
|
|
|
bufferController.onBufferCodecs({});
|
|
assert.strictEqual(checkPendingTracksSpy.calledOnce, true);
|
|
assert.strictEqual(bufferController.bufferCodecEventsExpected, 1);
|
|
|
|
bufferController.onBufferCodecs({});
|
|
assert.strictEqual(checkPendingTracksSpy.calledTwice, true);
|
|
assert.strictEqual(bufferController.bufferCodecEventsExpected, 0);
|
|
});
|
|
|
|
it('creates the expected amount of sourceBuffers given the standard event flow', function () {
|
|
bufferController.sourceBuffer = {};
|
|
bufferController.mediaSource = { readyState: 'open', removeEventListener: sandbox.stub() };
|
|
|
|
bufferController.onManifestParsed({ altAudio: true });
|
|
bufferController._onMediaSourceOpen();
|
|
bufferController.onBufferCodecs({ audio: {} });
|
|
bufferController.onBufferCodecs({ video: {} });
|
|
|
|
assert.strictEqual(createSbStub.calledOnce, true);
|
|
assert.strictEqual(createSbStub.calledWith({ audio: {}, video: {} }), true);
|
|
});
|
|
});
|
|
});
|