src/controller/subtitle-stream-controller.js
/*
* Subtitle Stream Controller
*/
import Event from '../events';
import { logger } from '../utils/logger';
import Decrypter from '../crypt/decrypter';
import TaskLoop from '../task-loop';
const State = {
STOPPED: 'STOPPED',
IDLE: 'IDLE',
KEY_LOADING: 'KEY_LOADING',
FRAG_LOADING: 'FRAG_LOADING'
};
class SubtitleStreamController extends TaskLoop {
constructor (hls) {
super(hls,
Event.MEDIA_ATTACHED,
Event.ERROR,
Event.KEY_LOADED,
Event.FRAG_LOADED,
Event.SUBTITLE_TRACKS_UPDATED,
Event.SUBTITLE_TRACK_SWITCH,
Event.SUBTITLE_TRACK_LOADED,
Event.SUBTITLE_FRAG_PROCESSED);
this.config = hls.config;
this.vttFragSNsProcessed = {};
this.vttFragQueues = undefined;
this.currentlyProcessing = null;
this.state = State.STOPPED;
this.currentTrackId = -1;
this.decrypter = new Decrypter(hls.observer, hls.config);
}
onHandlerDestroyed () {
this.state = State.STOPPED;
}
// Remove all queued items and create a new, empty queue for each track.
clearVttFragQueues () {
this.vttFragQueues = {};
this.tracks.forEach(track => {
this.vttFragQueues[track.id] = [];
});
}
// If no frag is being processed and queue isn't empty, initiate processing of next frag in line.
nextFrag () {
if (this.currentlyProcessing === null && this.currentTrackId > -1 && this.vttFragQueues[this.currentTrackId].length) {
let frag = this.currentlyProcessing = this.vttFragQueues[this.currentTrackId].shift();
this.fragCurrent = frag;
this.hls.trigger(Event.FRAG_LOADING, { frag: frag });
this.state = State.FRAG_LOADING;
}
}
// When fragment has finished processing, add sn to list of completed if successful.
onSubtitleFragProcessed (data) {
if (data.success)
this.vttFragSNsProcessed[data.frag.trackId].push(data.frag.sn);
this.currentlyProcessing = null;
this.state = State.IDLE;
this.nextFrag();
}
onMediaAttached () {
this.state = State.IDLE;
}
// If something goes wrong, procede to next frag, if we were processing one.
onError (data) {
let frag = data.frag;
// don't handle frag error not related to subtitle fragment
if (frag && frag.type !== 'subtitle')
return;
if (this.currentlyProcessing) {
this.currentlyProcessing = null;
this.nextFrag();
}
}
doTick () {
switch (this.state) {
case State.IDLE:
const tracks = this.tracks;
let trackId = this.currentTrackId;
const processedFragSNs = this.vttFragSNsProcessed[trackId],
fragQueue = this.vttFragQueues[trackId],
currentFragSN = this.currentlyProcessing ? this.currentlyProcessing.sn : -1;
const alreadyProcessed = function (frag) {
return processedFragSNs.indexOf(frag.sn) > -1;
};
const alreadyInQueue = function (frag) {
return fragQueue.some(fragInQueue => { return fragInQueue.sn === frag.sn; });
};
// exit if tracks don't exist
if (!tracks)
break;
var trackDetails;
if (trackId < tracks.length)
trackDetails = tracks[trackId].details;
if (typeof trackDetails === 'undefined')
break;
// Add all fragments that haven't been, aren't currently being and aren't waiting to be processed, to queue.
trackDetails.fragments.forEach(frag => {
if (!(alreadyProcessed(frag) || frag.sn === currentFragSN || alreadyInQueue(frag))) {
// Load key if subtitles are encrypted
if ((frag.decryptdata && frag.decryptdata.uri != null) && (frag.decryptdata.key == null)) {
logger.log(`Loading key for ${frag.sn}`);
this.state = State.KEY_LOADING;
this.hls.trigger(Event.KEY_LOADING, { frag: frag });
} else {
// Frags don't know their subtitle track ID, so let's just add that...
frag.trackId = trackId;
fragQueue.push(frag);
this.nextFrag();
}
}
});
}
}
// Got all new subtitle tracks.
onSubtitleTracksUpdated (data) {
logger.log('subtitle tracks updated');
this.tracks = data.subtitleTracks;
this.clearVttFragQueues();
this.vttFragSNsProcessed = {};
this.tracks.forEach(track => {
this.vttFragSNsProcessed[track.id] = [];
});
}
onSubtitleTrackSwitch (data) {
this.currentTrackId = data.id;
if (this.currentTrackId === -1)
return;
// Check if track was already loaded and if so make sure we finish
// downloading its frags, if not all have been downloaded yet
const currentTrack = this.tracks[this.currentTrackId];
let details = currentTrack.details;
if (details !== undefined)
this.tick();
}
// Got a new set of subtitle fragments.
onSubtitleTrackLoaded () {
this.tick();
}
onKeyLoaded () {
if (this.state === State.KEY_LOADING) {
this.state = State.IDLE;
this.tick();
}
}
onFragLoaded (data) {
let fragCurrent = this.fragCurrent,
decryptData = data.frag.decryptdata;
let fragLoaded = data.frag,
hls = this.hls;
if (this.state === State.FRAG_LOADING &&
fragCurrent &&
data.frag.type === 'subtitle' &&
fragCurrent.sn === data.frag.sn) {
// check to see if the payload needs to be decrypted
if ((data.payload.byteLength > 0) && (decryptData != null) && (decryptData.key != null) && (decryptData.method === 'AES-128')) {
let startTime;
try {
startTime = performance.now();
} catch (error) {
startTime = Date.now();
}
// decrypt the subtitles
this.decrypter.decrypt(data.payload, decryptData.key.buffer, decryptData.iv.buffer, function (decryptedData) {
let endTime;
try {
endTime = performance.now();
} catch (error) {
endTime = Date.now();
}
hls.trigger(Event.FRAG_DECRYPTED, { frag: fragLoaded, payload: decryptedData, stats: { tstart: startTime, tdecrypt: endTime } });
});
}
}
}
}
export default SubtitleStreamController;