src/controller/subtitle-track-controller.js
/*
* subtitle track controller
*/
import Event from '../events';
import EventHandler from '../event-handler';
import { logger } from '../utils/logger';
function filterSubtitleTracks (textTrackList) {
let tracks = [];
for (let i = 0; i < textTrackList.length; i++) {
if (textTrackList[i].kind === 'subtitles')
tracks.push(textTrackList[i]);
}
return tracks;
}
class SubtitleTrackController extends EventHandler {
constructor (hls) {
super(hls,
Event.MEDIA_ATTACHED,
Event.MEDIA_DETACHING,
Event.MANIFEST_LOADING,
Event.MANIFEST_LOADED,
Event.SUBTITLE_TRACK_LOADED);
this.tracks = [];
this.trackId = -1;
this.media = undefined;
/**
* @member {boolean} subtitleDisplay Enable/disable subtitle display rendering
*/
this.subtitleDisplay = false;
}
_onTextTracksChanged () {
// Media is undefined when switching streams via loadSource()
if (!this.media)
return;
let trackId = -1;
let tracks = filterSubtitleTracks(this.media.textTracks);
for (let id = 0; id < tracks.length; id++) {
if (tracks[id].mode === 'hidden') {
// Do not break in case there is a following track with showing.
trackId = id;
} else if (tracks[id].mode === 'showing') {
trackId = id;
break;
}
}
// Setting current subtitleTrack will invoke code.
this.subtitleTrack = trackId;
}
destroy () {
EventHandler.prototype.destroy.call(this);
}
// Listen for subtitle track change, then extract the current track ID.
onMediaAttached (data) {
this.media = data.media;
if (!this.media)
return;
if (this.queuedDefaultTrack !== undefined) {
this.subtitleTrack = this.queuedDefaultTrack;
delete this.queuedDefaultTrack;
}
this.trackChangeListener = this._onTextTracksChanged.bind(this);
this.useTextTrackPolling = !(this.media.textTracks && 'onchange' in this.media.textTracks);
if (this.useTextTrackPolling) {
this.subtitlePollingInterval = setInterval(() => {
this.trackChangeListener();
}, 500);
} else {
this.media.textTracks.addEventListener('change', this.trackChangeListener);
}
}
onMediaDetaching () {
if (!this.media)
return;
if (this.useTextTrackPolling)
clearInterval(this.subtitlePollingInterval);
else
this.media.textTracks.removeEventListener('change', this.trackChangeListener);
this.media = undefined;
}
// Reset subtitle tracks on manifest loading
onManifestLoading () {
this.tracks = [];
this.trackId = -1;
}
// Fired whenever a new manifest is loaded.
onManifestLoaded (data) {
let tracks = data.subtitles || [];
this.tracks = tracks;
this.trackId = -1;
this.hls.trigger(Event.SUBTITLE_TRACKS_UPDATED, { subtitleTracks: tracks });
// loop through available subtitle tracks and autoselect default if needed
// TODO: improve selection logic to handle forced, etc
tracks.forEach(track => {
if (track.default) {
// setting this.subtitleTrack will trigger internal logic
// if media has not been attached yet, it will fail
// we keep a reference to the default track id
// and we'll set subtitleTrack when onMediaAttached is triggered
if (this.media)
this.subtitleTrack = track.id;
else
this.queuedDefaultTrack = track.id;
}
});
}
// Trigger subtitle track playlist reload.
onTick () {
const trackId = this.trackId;
const subtitleTrack = this.tracks[trackId];
if (!subtitleTrack)
return;
const details = subtitleTrack.details;
// check if we need to load playlist for this subtitle Track
if (details === undefined || details.live === true) {
// track not retrieved yet, or live playlist we need to (re)load it
logger.log(`(re)loading playlist for subtitle track ${trackId}`);
this.hls.trigger(Event.SUBTITLE_TRACK_LOADING, { url: subtitleTrack.url, id: trackId });
}
}
onSubtitleTrackLoaded (data) {
if (data.id < this.tracks.length) {
logger.log(`subtitle track ${data.id} loaded`);
this.tracks[data.id].details = data.details;
// check if current playlist is a live playlist
if (data.details.live && !this.timer) {
// if live playlist we will have to reload it periodically
// set reload period to playlist target duration
this.timer = setInterval(() => {
this.onTick();
}, 1000 * data.details.targetduration, this);
}
if (!data.details.live && this.timer) {
// playlist is not live and timer is armed : stopping it
clearInterval(this.timer);
this.timer = null;
}
}
}
/** get alternate subtitle tracks list from playlist **/
get subtitleTracks () {
return this.tracks;
}
/** get index of the selected subtitle track (index in subtitle track lists) **/
get subtitleTrack () {
return this.trackId;
}
/** select a subtitle track, based on its index in subtitle track lists**/
set subtitleTrack (subtitleTrackId) {
if (this.trackId !== subtitleTrackId) { // || this.tracks[subtitleTrackId].details === undefined) {
this.setSubtitleTrackInternal(subtitleTrackId);
}
}
setSubtitleTrackInternal (newId) {
// check if level idx is valid
if (newId < -1 || newId >= this.tracks.length)
return;
// stopping live reloading timer if any
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
let textTracks = filterSubtitleTracks(this.media.textTracks);
// hide currently enabled subtitle track
if (this.trackId !== -1)
textTracks[this.trackId].mode = 'disabled';
this.trackId = newId;
logger.log(`switching to subtitle track ${newId}`);
this.hls.trigger(Event.SUBTITLE_TRACK_SWITCH, { id: newId });
if (newId === -1)
return;
const subtitleTrack = this.tracks[newId];
if (newId < textTracks.length)
textTracks[newId].mode = this.subtitleDisplay ? 'showing' : 'hidden';
// check if we need to load playlist for this subtitle Track
let details = subtitleTrack.details;
if (details === undefined || details.live === true) {
// track not retrieved yet, or live playlist we need to (re)load it
logger.log(`(re)loading playlist for subtitle track ${newId}`);
this.hls.trigger(Event.SUBTITLE_TRACK_LOADING, { url: subtitleTrack.url, id: newId });
}
}
}
export default SubtitleTrackController;