mirror of
https://github.com/video-dev/hls.js.git
synced 2026-06-16 13:34:44 +00:00
* Do not enable subtitle options with AUTOSELECT=YES attribute * Update and add initial selection tests for subtitle-controller * Only pick forced subtitle option if it is the only one Add default field to audio and subtitle selection options and forced field to subtitle selection option * Address TextTrack change event overriding subtitle preference Fix _TRACKS_UPDATED and _TRACK_SWITCH event order when preference is selected * Do not auto select subtitle options with FORCED=YES attribute
580 lines
19 KiB
TypeScript
580 lines
19 KiB
TypeScript
import SubtitleTrackController from '../../../src/controller/subtitle-track-controller';
|
|
import Hls from '../../../src/hls';
|
|
import { LoadStats } from '../../../src/loader/load-stats';
|
|
import { LevelDetails } from '../../../src/loader/level-details';
|
|
import { Events } from '../../../src/events';
|
|
import { AttrList } from '../../../src/utils/attr-list';
|
|
import type {
|
|
MediaAttributes,
|
|
MediaPlaylist,
|
|
} from '../../../src/types/media-playlist';
|
|
import type { Level } from '../../../src/types/level';
|
|
import type {
|
|
ComponentAPI,
|
|
NetworkComponentAPI,
|
|
} from '../../../src/types/component-api';
|
|
|
|
import sinon from 'sinon';
|
|
import chai from 'chai';
|
|
import sinonChai from 'sinon-chai';
|
|
|
|
chai.use(sinonChai);
|
|
const expect = chai.expect;
|
|
|
|
type HlsTestable = Omit<
|
|
Hls,
|
|
'levelController' | 'networkControllers' | 'coreComponents'
|
|
> & {
|
|
levelController: {
|
|
levels: Pick<Level, 'subtitleGroups'>[];
|
|
};
|
|
coreComponents: ComponentAPI[];
|
|
networkControllers: NetworkComponentAPI[];
|
|
};
|
|
|
|
describe('SubtitleTrackController', function () {
|
|
let hls: HlsTestable;
|
|
let subtitleTrackController: SubtitleTrackController;
|
|
let subtitleTracks: MediaPlaylist[];
|
|
let switchLevel: () => void;
|
|
let videoElement;
|
|
let sandbox;
|
|
|
|
beforeEach(function () {
|
|
hls = new Hls() as unknown as HlsTestable;
|
|
hls.networkControllers.forEach((component) => component.destroy());
|
|
hls.networkControllers.length = 0;
|
|
hls.coreComponents.forEach((component) => component.destroy());
|
|
hls.coreComponents.length = 0;
|
|
subtitleTrackController = new SubtitleTrackController(
|
|
hls as unknown as Hls,
|
|
);
|
|
hls.networkControllers.push(subtitleTrackController);
|
|
hls.levelController = {
|
|
levels: [
|
|
{
|
|
subtitleGroups: ['default-text-group'],
|
|
},
|
|
],
|
|
};
|
|
|
|
videoElement = document.createElement('video');
|
|
hls.trigger(Events.MEDIA_ATTACHED, { media: videoElement });
|
|
|
|
subtitleTracks = [
|
|
{
|
|
attrs: new AttrList({}) as MediaAttributes,
|
|
autoselect: true,
|
|
bitrate: 0,
|
|
default: false,
|
|
forced: false,
|
|
id: 0,
|
|
groupId: 'default-text-group',
|
|
lang: 'en-US',
|
|
name: 'English',
|
|
type: 'SUBTITLES',
|
|
url: 'baz',
|
|
// details: { live: false },
|
|
},
|
|
{
|
|
attrs: new AttrList({}) as MediaAttributes,
|
|
autoselect: true,
|
|
bitrate: 0,
|
|
default: false,
|
|
forced: false,
|
|
id: 1,
|
|
groupId: 'default-text-group',
|
|
lang: 'sv',
|
|
name: 'Swedish',
|
|
type: 'SUBTITLES',
|
|
url: 'bar',
|
|
},
|
|
{
|
|
attrs: new AttrList({}) as MediaAttributes,
|
|
autoselect: true,
|
|
bitrate: 0,
|
|
default: false,
|
|
forced: false,
|
|
id: 2,
|
|
groupId: 'default-text-group',
|
|
lang: 'en-US',
|
|
name: 'Untitled CC',
|
|
type: 'SUBTITLES',
|
|
url: 'foo',
|
|
// details: { live: true },
|
|
},
|
|
];
|
|
const levels = [
|
|
{
|
|
subtitleGroups: ['default-text-group'],
|
|
},
|
|
] as any;
|
|
hls.trigger(Events.MANIFEST_PARSED, {
|
|
subtitleTracks,
|
|
levels,
|
|
audioTracks: [],
|
|
sessionData: null,
|
|
sessionKeys: null,
|
|
firstLevel: 0,
|
|
stats: new LoadStats(),
|
|
audio: true,
|
|
video: true,
|
|
altAudio: true,
|
|
});
|
|
|
|
switchLevel = () => {
|
|
hls.trigger(Events.LEVEL_LOADING, {
|
|
id: 0,
|
|
level: 0,
|
|
pathwayId: undefined,
|
|
url: '',
|
|
deliveryDirectives: null,
|
|
});
|
|
};
|
|
|
|
const textTrack1 = videoElement.addTextTrack(
|
|
'subtitles',
|
|
'English',
|
|
'en-US',
|
|
);
|
|
const textTrack2 = videoElement.addTextTrack('subtitles', 'Swedish', 'sv');
|
|
const textTrack3 = videoElement.addTextTrack(
|
|
'captions',
|
|
'Untitled CC',
|
|
'en-US',
|
|
);
|
|
|
|
textTrack1.groupId = 'default-text-group';
|
|
textTrack2.groupId = 'default-text-group';
|
|
textTrack3.groupId = 'default-text-group';
|
|
|
|
textTrack1.mode = 'disabled';
|
|
textTrack2.mode = 'disabled';
|
|
textTrack3.mode = 'disabled';
|
|
sandbox = sinon.createSandbox();
|
|
});
|
|
|
|
afterEach(function () {
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe('onTextTracksChanged', function () {
|
|
beforeEach(function () {
|
|
switchLevel();
|
|
});
|
|
it('should set subtitleTrack to -1 if disabled', function () {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
|
|
const onTextTracksChanged = sinon.spy(
|
|
subtitleTrackController,
|
|
'onTextTracksChanged' as any,
|
|
);
|
|
|
|
videoElement.textTracks[0].mode = 'showing';
|
|
|
|
return new Promise((resolve) => {
|
|
self.setTimeout(() => {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(0);
|
|
expect(onTextTracksChanged).to.have.been.calledOnce;
|
|
videoElement.textTracks[0].mode = 'disabled';
|
|
self.setTimeout(() => {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
expect(onTextTracksChanged).to.have.been.calledTwice;
|
|
resolve(true);
|
|
}, 500);
|
|
}, 500);
|
|
});
|
|
});
|
|
|
|
it('should set subtitleTrack to 0 if hidden', function () {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
|
|
videoElement.textTracks[0].mode = 'hidden';
|
|
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.SUBTITLE_TRACK_SWITCH, () => {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(0);
|
|
resolve(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should set subtitleTrack to 0 if showing', function () {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
|
|
videoElement.textTracks[0].mode = 'showing';
|
|
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.SUBTITLE_TRACK_SWITCH, () => {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(0);
|
|
resolve(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should set subtitleTrack id captions track is showing', function () {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
|
|
videoElement.textTracks[2].mode = 'showing';
|
|
|
|
return new Promise((resolve) => {
|
|
hls.on(Events.SUBTITLE_TRACK_SWITCH, () => {
|
|
expect(videoElement.textTracks[2].kind).to.equal('captions');
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(2);
|
|
resolve(true);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('initial track selection', function () {
|
|
it('should not select any tracks if there are no default of forces tracks (ignoring autoselect)', function () {
|
|
switchLevel();
|
|
expect(subtitleTracks[0].autoselect).to.equal(true);
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
});
|
|
|
|
it('should not select forced tracks', function () {
|
|
subtitleTracks[1].forced = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
});
|
|
|
|
it('should select the default track when there are no forced tracks', function () {
|
|
subtitleTracks[2].default = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(2);
|
|
});
|
|
|
|
it('should select the first default track when there are no forced tracks', function () {
|
|
subtitleTracks[0].default = true;
|
|
subtitleTracks[1].default = true;
|
|
subtitleTracks[2].default = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(0);
|
|
});
|
|
|
|
it('should not select forced tracks over the default tracks (one forced track)', function () {
|
|
subtitleTracks[1].default = true;
|
|
subtitleTracks[2].forced = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(1);
|
|
});
|
|
|
|
it('should not select forced tracks over the default tracks (two forced track)', function () {
|
|
subtitleTracks[0].forced = true;
|
|
subtitleTracks[1].forced = true;
|
|
subtitleTracks[2].default = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(2);
|
|
});
|
|
|
|
describe('with subtitlePreference', function () {
|
|
it('should select the first track with matching lang', function () {
|
|
hls.config.subtitlePreference = {
|
|
lang: 'en-US',
|
|
};
|
|
subtitleTracks[2].default = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(0);
|
|
});
|
|
it('should select the first track with matching properties', function () {
|
|
hls.config.subtitlePreference = {
|
|
lang: 'en-US',
|
|
default: true,
|
|
};
|
|
subtitleTracks[2].default = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(2);
|
|
});
|
|
it('should not select default track if an unmatched preference is present', function () {
|
|
hls.config.subtitlePreference = {
|
|
lang: 'none',
|
|
};
|
|
subtitleTracks[2].default = true;
|
|
switchLevel();
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('set subtitleTrack', function () {
|
|
beforeEach(function () {
|
|
switchLevel();
|
|
});
|
|
it('should set active text track mode to showing', function () {
|
|
videoElement.textTracks[0].mode = 'disabled';
|
|
|
|
subtitleTrackController.subtitleDisplay = true;
|
|
subtitleTrackController.subtitleTrack = 0;
|
|
|
|
expect(videoElement.textTracks[0].mode).to.equal('showing');
|
|
});
|
|
|
|
it('should set active text track mode to hidden', function () {
|
|
videoElement.textTracks[0].mode = 'disabled';
|
|
subtitleTrackController.subtitleDisplay = false;
|
|
subtitleTrackController.subtitleTrack = 0;
|
|
|
|
expect(videoElement.textTracks[0].mode).to.equal('hidden');
|
|
});
|
|
|
|
it('should disable previous track', function () {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
|
|
const onTextTracksChanged = sinon.spy(
|
|
subtitleTrackController,
|
|
'onTextTracksChanged' as any,
|
|
);
|
|
|
|
videoElement.textTracks[0].mode = 'showing';
|
|
|
|
return new Promise((resolve) => {
|
|
self.setTimeout(() => {
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(0);
|
|
expect(videoElement.textTracks[0].mode).to.equal('showing');
|
|
expect(onTextTracksChanged).to.have.been.calledOnce;
|
|
subtitleTrackController.subtitleTrack = 1;
|
|
self.setTimeout(() => {
|
|
expect(videoElement.textTracks[0].mode).to.equal('disabled');
|
|
expect(videoElement.textTracks[1].mode).to.equal('showing');
|
|
expect(onTextTracksChanged).to.have.been.calledTwice;
|
|
resolve(true);
|
|
}, 500);
|
|
}, 500);
|
|
});
|
|
});
|
|
|
|
it('should disable all textTracks when set to -1', function () {
|
|
[].slice.call(videoElement.textTracks).forEach((t) => {
|
|
t.mode = 'showing';
|
|
});
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
subtitleTrackController.subtitleTrack = -1;
|
|
[].slice.call(videoElement.textTracks).forEach((t) => {
|
|
expect(t.mode).to.equal('disabled');
|
|
});
|
|
});
|
|
|
|
it('should trigger SUBTITLE_TRACK_SWITCH', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTrackController.startLoad();
|
|
subtitleTrackController.subtitleTrack = 1;
|
|
|
|
expect(triggerSpy).to.have.been.calledTwice;
|
|
expect(triggerSpy.firstCall).to.have.been.calledWith(
|
|
'hlsSubtitleTrackSwitch',
|
|
{
|
|
id: 1,
|
|
groupId: 'default-text-group',
|
|
name: 'Swedish',
|
|
type: 'SUBTITLES',
|
|
url: 'bar',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should trigger SUBTITLE_TRACK_LOADING if the track has no details', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTrackController.startLoad();
|
|
subtitleTrackController.subtitleTrack = 1;
|
|
|
|
expect(triggerSpy).to.have.been.calledTwice;
|
|
expect(triggerSpy.secondCall).to.have.been.calledWith(
|
|
'hlsSubtitleTrackLoading',
|
|
{
|
|
url: 'bar',
|
|
id: 1,
|
|
groupId: 'default-text-group',
|
|
deliveryDirectives: null,
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should not trigger SUBTITLE_TRACK_LOADING if the track has details and is not live', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTracks[0].details = { live: false } as any;
|
|
subtitleTrackController.startLoad();
|
|
subtitleTrackController.subtitleTrack = 0;
|
|
|
|
expect(triggerSpy).to.have.been.calledOnce;
|
|
expect(triggerSpy.firstCall).to.have.been.calledWith(
|
|
'hlsSubtitleTrackSwitch',
|
|
{
|
|
id: 0,
|
|
groupId: 'default-text-group',
|
|
name: 'English',
|
|
type: 'SUBTITLES',
|
|
url: 'baz',
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should trigger SUBTITLE_TRACK_SWITCH if passed -1', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTrackController.subtitleTrack = -1;
|
|
expect(triggerSpy.firstCall).to.have.been.calledWith(
|
|
'hlsSubtitleTrackSwitch',
|
|
{ id: -1 },
|
|
);
|
|
});
|
|
|
|
it('should trigger SUBTITLE_TRACK_LOADING if the track is live, even if it has details', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTracks[2].details = { live: true } as any;
|
|
subtitleTrackController.startLoad();
|
|
subtitleTrackController.subtitleTrack = 2;
|
|
|
|
expect(triggerSpy).to.have.been.calledTwice;
|
|
expect(triggerSpy.secondCall).to.have.been.calledWith(
|
|
'hlsSubtitleTrackLoading',
|
|
{
|
|
url: 'foo',
|
|
id: 2,
|
|
groupId: 'default-text-group',
|
|
deliveryDirectives: null,
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should do nothing if called with out of bound indices', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTrackController.subtitleTrack = 5;
|
|
subtitleTrackController.subtitleTrack = -2;
|
|
expect(triggerSpy).to.have.callCount(0);
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
});
|
|
|
|
it('should do nothing if called with a invalid index', function () {
|
|
const triggerSpy = sandbox.spy(hls, 'trigger');
|
|
subtitleTrackController.subtitleTrack = undefined as any;
|
|
subtitleTrackController.subtitleTrack = null as any;
|
|
expect(triggerSpy).to.have.callCount(0);
|
|
expect(subtitleTrackController.subtitleTrack).to.equal(-1);
|
|
});
|
|
});
|
|
|
|
describe('toggleTrackModes', function () {
|
|
// This can be the case when setting the subtitleTrack before Hls.js attaches to the mediaElement
|
|
it('should not throw an exception if trackId is out of the mediaElement text track bounds', function () {
|
|
switchLevel();
|
|
hls.detachMedia();
|
|
const toggleTrackModesSpy = sandbox.spy(
|
|
subtitleTrackController,
|
|
'toggleTrackModes',
|
|
);
|
|
(subtitleTrackController as any).trackId = 3;
|
|
hls.trigger(Events.MEDIA_ATTACHED, { media: videoElement });
|
|
subtitleTrackController.subtitleDisplay = true; // setting subtitleDisplay invokes `toggleTrackModes`
|
|
expect(toggleTrackModesSpy).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('onSubtitleTrackLoaded', function () {
|
|
beforeEach(function () {
|
|
switchLevel();
|
|
});
|
|
it('exits early if the loaded track does not match the requested track', function () {
|
|
const playlistLoadedSpy = sandbox.spy(
|
|
subtitleTrackController,
|
|
'playlistLoaded',
|
|
);
|
|
subtitleTrackController.startLoad();
|
|
(subtitleTrackController as any).trackId = 1;
|
|
(subtitleTrackController as any).currentTrack = subtitleTracks[1];
|
|
|
|
const mockLoadedEvent = {
|
|
id: 999,
|
|
groupId: 'default-text-group',
|
|
details: { foo: 'bar' } as any,
|
|
stats: new LoadStats(),
|
|
networkDetails: {},
|
|
deliveryDirectives: null,
|
|
};
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent);
|
|
expect((subtitleTrackController as any).timer).to.equal(-1);
|
|
expect(playlistLoadedSpy).to.have.not.been.called;
|
|
|
|
mockLoadedEvent.id = 0;
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent);
|
|
expect((subtitleTrackController as any).timer).to.equal(-1);
|
|
expect(playlistLoadedSpy).to.have.not.been.called;
|
|
|
|
mockLoadedEvent.id = 1;
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent);
|
|
expect((subtitleTrackController as any).timer).to.equal(-1);
|
|
expect(playlistLoadedSpy).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('does not set the reload timer if loading has not started', function () {
|
|
const details = new LevelDetails('');
|
|
subtitleTrackController.stopLoad();
|
|
(subtitleTrackController as any).trackId = 1;
|
|
(subtitleTrackController as any).currentTrack = subtitleTracks[1];
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
|
id: 1,
|
|
groupId: 'default-text-group',
|
|
details,
|
|
stats: new LoadStats(),
|
|
networkDetails: {},
|
|
deliveryDirectives: null,
|
|
});
|
|
expect((subtitleTrackController as any).timer).to.equal(-1);
|
|
});
|
|
|
|
it('sets the live reload timer if the level is live', function () {
|
|
const details = new LevelDetails('');
|
|
subtitleTrackController.startLoad();
|
|
(subtitleTrackController as any).trackId = 1;
|
|
(subtitleTrackController as any).currentTrack = subtitleTracks[1];
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
|
id: 1,
|
|
groupId: 'default-text-group',
|
|
details,
|
|
stats: new LoadStats(),
|
|
networkDetails: {},
|
|
deliveryDirectives: null,
|
|
});
|
|
expect((subtitleTrackController as any).timer).to.exist;
|
|
});
|
|
|
|
it('stops the live reload timer if the level is not live', function () {
|
|
const details = new LevelDetails('');
|
|
details.live = false;
|
|
(subtitleTrackController as any).trackId = 1;
|
|
(subtitleTrackController as any).currentTrack = subtitleTracks[1];
|
|
(subtitleTrackController as any).timer = self.setTimeout(() => {}, 0);
|
|
hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
|
|
id: 1,
|
|
groupId: 'default-text-group',
|
|
details,
|
|
stats: new LoadStats(),
|
|
networkDetails: {},
|
|
deliveryDirectives: null,
|
|
});
|
|
expect((subtitleTrackController as any).timer).to.equal(-1);
|
|
});
|
|
});
|
|
|
|
describe('stopLoad', function () {
|
|
it('stops loading', function () {
|
|
const clearReloadSpy = sandbox.spy(subtitleTrackController, 'clearTimer');
|
|
subtitleTrackController.stopLoad();
|
|
expect((subtitleTrackController as any).canLoad).to.be.false;
|
|
expect(clearReloadSpy).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('startLoad', function () {
|
|
it('starts loading', function () {
|
|
const loadCurrentTrackSpy = sandbox.spy(
|
|
subtitleTrackController,
|
|
'loadPlaylist',
|
|
);
|
|
subtitleTrackController.startLoad();
|
|
expect((subtitleTrackController as any).canLoad).to.be.true;
|
|
expect(loadCurrentTrackSpy).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
});
|