mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
313 lines
10 KiB
TypeScript
313 lines
10 KiB
TypeScript
import { expect, use } from 'chai';
|
|
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 { SubtitleStreamController } from '../../../src/controller/subtitle-stream-controller';
|
|
import Decrypter from '../../../src/crypt/decrypter';
|
|
import { ErrorDetails } from '../../../src/errors';
|
|
import { Events } from '../../../src/events';
|
|
import Hls from '../../../src/hls';
|
|
import { Fragment } from '../../../src/loader/fragment';
|
|
import KeyLoader from '../../../src/loader/key-loader';
|
|
import { PlaylistLevelType } from '../../../src/types/loader';
|
|
import { AttrList } from '../../../src/utils/attr-list';
|
|
|
|
use(sinonChai);
|
|
|
|
const mediaMock = {
|
|
currentTime: 0,
|
|
addEventListener() {},
|
|
removeEventListener() {},
|
|
};
|
|
|
|
const tracksMock = [
|
|
{
|
|
id: 0,
|
|
details: { url: '', fragments: [] },
|
|
attrs: new AttrList({}),
|
|
},
|
|
{
|
|
id: 1,
|
|
attrs: new AttrList({}),
|
|
},
|
|
];
|
|
|
|
describe('SubtitleStreamController', function () {
|
|
let hls;
|
|
let fragmentTracker;
|
|
let keyLoader;
|
|
let subtitleStreamController;
|
|
|
|
beforeEach(function () {
|
|
hls = new Hls({});
|
|
mediaMock.currentTime = 0;
|
|
fragmentTracker = new FragmentTracker(hls);
|
|
keyLoader = new KeyLoader(hls.config, hls.logger);
|
|
|
|
subtitleStreamController = new SubtitleStreamController(
|
|
hls,
|
|
fragmentTracker,
|
|
keyLoader,
|
|
);
|
|
|
|
subtitleStreamController.onMediaAttached(Events.MEDIA_ATTACHED, {
|
|
media: mediaMock,
|
|
});
|
|
subtitleStreamController.state = State.IDLE;
|
|
});
|
|
|
|
afterEach(function () {
|
|
subtitleStreamController.onMediaDetaching(Events.MEDIA_DETACHING, {
|
|
media: mediaMock,
|
|
});
|
|
hls.destroy();
|
|
});
|
|
|
|
describe('onSubtitleTracksUpdate', function () {
|
|
beforeEach(function () {
|
|
hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, {
|
|
subtitleTracks: tracksMock,
|
|
});
|
|
});
|
|
|
|
it('should update tracks list', function () {
|
|
expect(subtitleStreamController.levels).to.have.lengthOf(2);
|
|
expect(subtitleStreamController.levels[0]).to.deep.include(tracksMock[0]);
|
|
expect(subtitleStreamController.levels[1]).to.deep.include(tracksMock[1]);
|
|
});
|
|
});
|
|
|
|
describe('onSubtitleTrackSwitch', function () {
|
|
beforeEach(function () {
|
|
subtitleStreamController.levels = tracksMock;
|
|
subtitleStreamController.clearInterval = sinon.spy();
|
|
subtitleStreamController.setInterval = sinon.spy();
|
|
|
|
hls.trigger(Events.SUBTITLE_TRACK_SWITCH, {
|
|
id: 0,
|
|
});
|
|
});
|
|
|
|
it('should call setInterval if details available', function () {
|
|
expect(subtitleStreamController.setInterval).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should call clearInterval if no tracks present', function () {
|
|
subtitleStreamController.levels = [];
|
|
hls.trigger(Events.SUBTITLE_TRACK_SWITCH, {
|
|
id: 0,
|
|
});
|
|
expect(subtitleStreamController.clearInterval).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should call clearInterval if new track id === -1', function () {
|
|
hls.trigger(Events.SUBTITLE_TRACK_SWITCH, {
|
|
id: -1,
|
|
});
|
|
expect(subtitleStreamController.clearInterval).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('onSubtitleTrackLoaded', function () {
|
|
beforeEach(function () {
|
|
subtitleStreamController.setInterval = sinon.spy();
|
|
subtitleStreamController.levels = tracksMock;
|
|
});
|
|
|
|
// Details are in subtitle-track-controller.js' onSubtitleTrackLoaded handler
|
|
it('should handle the event if the data matches the current track', function () {
|
|
const details = { foo: 'bar', fragments: [] };
|
|
subtitleStreamController.currentTrackId = 1;
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
|
id: 1,
|
|
details: details,
|
|
});
|
|
expect(subtitleStreamController.levels[1].details).to.equal(details);
|
|
});
|
|
|
|
it('should ignore the event if the data does not match the current track', function () {
|
|
const details = { foo: 'bar', fragments: [] };
|
|
subtitleStreamController.currentTrackId = 0;
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
|
id: 1,
|
|
details,
|
|
});
|
|
expect(subtitleStreamController.levels[0].details).to.not.equal(details);
|
|
expect(subtitleStreamController.setInterval).to.not.have.been.called;
|
|
});
|
|
|
|
it('should ignore the event if there are no tracks, or the id is not within the tracks array', function () {
|
|
subtitleStreamController.levels = [];
|
|
subtitleStreamController.trackId = 0;
|
|
const details = { foo: 'bar', fragments: [] };
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
|
id: 0,
|
|
details,
|
|
});
|
|
expect(subtitleStreamController.levels[0]).to.not.exist;
|
|
expect(subtitleStreamController.setInterval).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('onMediaSeeking', function () {
|
|
it('nulls fragPrevious when seeking away from fragCurrent', function () {
|
|
subtitleStreamController.fragCurrent = new Fragment(
|
|
PlaylistLevelType.MAIN,
|
|
'',
|
|
);
|
|
subtitleStreamController.fragCurrent.start = 1000;
|
|
subtitleStreamController.fragCurrent.duration = 10;
|
|
subtitleStreamController.fragPrevious = new Fragment(
|
|
PlaylistLevelType.MAIN,
|
|
'',
|
|
);
|
|
subtitleStreamController.onMediaSeeking();
|
|
expect(subtitleStreamController.fragPrevious).to.not.exist;
|
|
});
|
|
});
|
|
|
|
describe('_handleFragmentLoadProgress', function () {
|
|
let sandbox;
|
|
|
|
beforeEach(function () {
|
|
sandbox = sinon.createSandbox();
|
|
});
|
|
|
|
afterEach(function () {
|
|
sandbox.restore();
|
|
});
|
|
|
|
function buildEncryptedSubtitleFrag() {
|
|
const frag = new Fragment(PlaylistLevelType.SUBTITLE, '');
|
|
const decryptdata = {
|
|
encrypted: true,
|
|
method: 'AES-128',
|
|
key: new Uint8Array(16),
|
|
iv: new Uint8Array(16),
|
|
};
|
|
// Override Fragment's `decryptdata` and `encrypted` getters with a
|
|
// minimal LevelKey-like stub so we don't need to wire up `levelkeys`.
|
|
Object.defineProperty(frag, 'decryptdata', {
|
|
value: decryptdata,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(frag, 'encrypted', {
|
|
value: true,
|
|
configurable: true,
|
|
});
|
|
return frag;
|
|
}
|
|
|
|
function buildPart(frag: Fragment) {
|
|
return {
|
|
index: 0,
|
|
fragment: frag,
|
|
start: 0,
|
|
duration: 1,
|
|
} as any;
|
|
}
|
|
|
|
function flushPromises() {
|
|
// Drain the .then -> .catch -> .finally microtask chain before asserting.
|
|
let p = Promise.resolve();
|
|
for (let i = 0; i < 5; i++) {
|
|
p = p.then(() => undefined);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
function loadProgress(frag, part, payload) {
|
|
// fragContextChanged compares frag against fragCurrent; align them so the
|
|
// controller does not bail out of the post-decrypt continuation.
|
|
subtitleStreamController.fragCurrent = frag;
|
|
subtitleStreamController._handleFragmentLoadProgress({
|
|
frag,
|
|
part,
|
|
payload,
|
|
networkDetails: null,
|
|
});
|
|
}
|
|
|
|
it('does nothing when called without a part (full-segment progress)', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
const frag = buildEncryptedSubtitleFrag();
|
|
loadProgress(frag, null, new ArrayBuffer(16));
|
|
expect(triggerSpy).to.not.have.been.calledWith(Events.FRAG_DECRYPTED);
|
|
});
|
|
|
|
it('does nothing when the payload is empty', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
const frag = buildEncryptedSubtitleFrag();
|
|
loadProgress(frag, buildPart(frag), new ArrayBuffer(0));
|
|
expect(triggerSpy).to.not.have.been.calledWith(Events.FRAG_DECRYPTED);
|
|
});
|
|
|
|
it('does nothing when the fragment is not encrypted', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
const frag = new Fragment(PlaylistLevelType.SUBTITLE, '');
|
|
loadProgress(frag, buildPart(frag), new ArrayBuffer(16));
|
|
expect(triggerSpy).to.not.have.been.calledWith(Events.FRAG_DECRYPTED);
|
|
});
|
|
|
|
it('decrypts an encrypted VTT part and triggers FRAG_DECRYPTED with the part', async function () {
|
|
const plaintext = new ArrayBuffer(16);
|
|
const decryptStub = sandbox
|
|
.stub(Decrypter.prototype, 'decrypt')
|
|
.resolves(plaintext);
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
|
|
const frag = buildEncryptedSubtitleFrag();
|
|
const part = buildPart(frag);
|
|
loadProgress(frag, part, new ArrayBuffer(16));
|
|
|
|
await flushPromises();
|
|
|
|
expect(decryptStub).to.have.been.calledOnce;
|
|
const calls = triggerSpy.getCalls();
|
|
let decryptedCall;
|
|
for (let i = 0; i < calls.length; i++) {
|
|
if (calls[i].args[0] === Events.FRAG_DECRYPTED) {
|
|
decryptedCall = calls[i];
|
|
break;
|
|
}
|
|
}
|
|
expect(decryptedCall, 'FRAG_DECRYPTED should have been triggered').to
|
|
.exist;
|
|
expect(decryptedCall.args[1].frag).to.equal(frag);
|
|
expect(decryptedCall.args[1].part).to.equal(part);
|
|
expect(decryptedCall.args[1].payload).to.equal(plaintext);
|
|
});
|
|
|
|
it('emits ERROR when part decryption fails', async function () {
|
|
sandbox.stub(Decrypter.prototype, 'decrypt').rejects(new Error('boom'));
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
|
|
const frag = buildEncryptedSubtitleFrag();
|
|
const part = buildPart(frag);
|
|
loadProgress(frag, part, new ArrayBuffer(16));
|
|
|
|
await flushPromises();
|
|
|
|
const calls = triggerSpy.getCalls();
|
|
let errorCall;
|
|
for (let i = 0; i < calls.length; i++) {
|
|
if (
|
|
calls[i].args[0] === Events.ERROR &&
|
|
calls[i].args[1]?.details === ErrorDetails.FRAG_DECRYPT_ERROR
|
|
) {
|
|
errorCall = calls[i];
|
|
break;
|
|
}
|
|
}
|
|
expect(errorCall, 'a FRAG_DECRYPT_ERROR should have been emitted').to
|
|
.exist;
|
|
expect(errorCall.args[1].frag).to.equal(frag);
|
|
expect(errorCall.args[1].part).to.equal(part);
|
|
// Note: `fatal` is intentionally not asserted — the error-controller may
|
|
// mutate the event payload to escalate to fatal after our trigger.
|
|
expect(triggerSpy).to.not.have.been.calledWith(Events.FRAG_DECRYPTED);
|
|
});
|
|
});
|
|
});
|