subtitles logic extracted in a separate component

This commit is contained in:
NikolaBorislavovHristov 2019-02-11 22:27:50 +02:00
parent 5b13e7a5cf
commit 03fa68ed0f
2 changed files with 220 additions and 111 deletions

View file

@ -0,0 +1,170 @@
var EventEmitter = require('events');
var subtitleUtils = require('./utils/subtitles');
var HTMLSubtitles = function(containerElement) {
var events = new EventEmitter();
var tracks = Object.freeze([]);
var cues = Object.freeze({});
var selectedTrackId = null;
var delay = 0;
var stylesElement = document.createElement('style');
var subtitlesElement = document.createElement('div');
containerElement.appendChild(stylesElement);
var subtitleStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles { position: absolute; right: 0; bottom: 0; left: 0; z-index: 0; font-size: 26pt; color: white; text-align: center; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue { display: inline-block; padding: 0.2em; text-shadow: #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles.dark-background .cue { text-shadow: none; background-color: #222222; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(subtitlesElement);
subtitlesElement.classList.add('subtitles');
Object.defineProperty(this, 'tracks', {
configurable: false,
enumerable: true,
get: function() { return Object.freeze(tracks.slice()); }
});
Object.defineProperty(this, 'selectedTrackId', {
configurable: false,
enumerable: true,
get: function() { return selectedTrackId; },
set: function(nextSelectedTrackId) {
cues = Object.freeze({});
selectedTrackId = null;
delay = 0;
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
if (track.id === nextSelectedTrackId) {
selectedTrackId = track.id;
fetch(track.url)
.then(function(resp) {
return resp.text();
})
.catch(function() {
events.emit('error', Object.freeze({
code: 70,
track: track
}));
})
.then(function(text) {
if (typeof text === 'string' && selectedTrackId === track.id) {
cues = subtitleUtils.parse(text);
events.emit('load', Object.freeze({
track: track
}));
}
})
.catch(function() {
events.emit('error', Object.freeze({
code: 71,
track: track
}));
});
break;
}
}
}
});
Object.defineProperty(this, 'delay', {
configurable: false,
enumerable: true,
get: function() { return delay; },
set: function(nextDelay) {
if (!isNaN(nextDelay)) {
delay = parseFloat(nextDelay);
}
}
});
Object.defineProperty(this, 'size', {
configurable: false,
enumerable: true,
get: function() { return parseFloat(stylesElement.sheet.cssRules[subtitleStylesIndex].style.fontSize); },
set: function(nextSize) {
if (!isNaN(nextSize)) {
stylesElement.sheet.cssRules[subtitleStylesIndex].style.fontSize = parseFloat(nextSize) + 'pt';
}
}
});
Object.defineProperty(this, 'darkBackground', {
configurable: false,
enumerable: true,
get: function() { return subtitlesElement.classList.contains('dark-background'); },
set: function(nextDarkBackground) {
if (!!nextDarkBackground) {
subtitlesElement.classList.add('dark-background');
} else {
subtitlesElement.classList.remove('dark-background');
}
}
});
this.addListener = function(eventName, listener) {
events.addListener(eventName, listener);
};
this.removeListener = function(eventName, listener) {
events.removeListener(eventName, listener);
};
this.addTracks = function(extraTracks) {
tracks = (Array.isArray(extraTracks) ? extraTracks : [])
.filter(function(track) {
return track &&
typeof track.url === 'string' &&
track.url.length > 0 &&
typeof track.origin === 'string' &&
track.origin.length > 0 &&
track.origin !== 'EMBEDDED';
})
.map(function(track) {
return Object.freeze(Object.assign({}, track, {
id: track.url
}));
})
.concat(tracks)
.filter(function(track, index, tracks) {
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].id === track.id) {
return i === index;
}
}
return false;
});
Object.freeze(tracks);
};
this.updateTextForTime = function(mediaTime) {
while (subtitlesElement.hasChildNodes()) {
subtitlesElement.removeChild(subtitlesElement.lastChild);
}
if (isNaN(mediaTime) || !Array.isArray(cues.times)) {
return;
}
var time = mediaTime + delay;
var cuesForTime = subtitleUtils.cuesForTime(cues, time);
for (var i = 0; i < cuesForTime.length; i++) {
var cueNode = subtitleUtils.render(cuesForTime[i]);
cueNode.classList.add('cue');
subtitlesElement.append(cueNode, document.createElement('br'));
}
};
this.clearTracks = function() {
tracks = Object.freeze([]);
cues = Object.freeze({});
selectedTrackId = null;
delay = 0;
};
this.detachElements = function() {
containerElement.removeChild(stylesElement);
containerElement.removeChild(subtitlesElement);
};
};
module.exports = HTMLSubtitles;

View file

@ -1,5 +1,5 @@
var EventEmitter = require('events');
var subtitleUtils = require('./utils/subtitles');
var HTMLSubtitles = require('./HTMLSubtitles');
var HTMLVideo = function(containerElement) {
if (!(containerElement instanceof HTMLElement)) {
@ -11,24 +11,15 @@ var HTMLVideo = function(containerElement) {
var loaded = false;
var destroyed = false;
var dispatchArgsQueue = [];
var subtitleCues = {};
var subtitleTracks = [];
var selectedSubtitleTrackId = null;
var subtitleDelay = 0;
var subtitles = new HTMLSubtitles(containerElement);
var stylesElement = document.createElement('style');
var videoElement = document.createElement('video');
var subtitlesElement = document.createElement('div');
containerElement.appendChild(stylesElement);
stylesElement.sheet.insertRule('#' + containerElement.id + ' video { position: absolute; width: 100%; height: 100%; z-index: -1; }', stylesElement.sheet.cssRules.length);
var subtitleStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles { position: absolute; right: 0; bottom: 0; left: 0; z-index: 0; font-size: 26pt; color: white; text-align: center; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue { display: inline-block; padding: 0.2em; text-shadow: #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px, #222222 0px 0px 1.8px; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles.dark-background .cue { text-shadow: none; background-color: #222222; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(videoElement);
videoElement.crossOrigin = 'anonymous';
videoElement.controls = false;
containerElement.appendChild(subtitlesElement);
subtitlesElement.classList.add('subtitles');
function getPaused() {
if (!loaded) {
@ -70,40 +61,66 @@ var HTMLVideo = function(containerElement) {
return [];
}
return subtitleTracks.slice();
return Object.freeze(subtitles.tracks.slice());
}
function getSelectedSubtitleTrackId() {
if (!loaded) {
return null;
}
return selectedSubtitleTrackId;
return subtitles.selectedTrackId;
}
function getSubtitleDelay() {
if (!loaded) {
return null;
}
return subtitleDelay;
return subtitles.delay;
}
function getSubtitleSize() {
if (destroyed) {
return null;
}
return parseFloat(stylesElement.sheet.cssRules[subtitleStylesIndex].style.fontSize);
return subtitles.size;
}
function getSubtitleDarkBackground() {
if (destroyed) {
return null;
}
return subtitlesElement.classList.contains('dark-background');
return subtitles.darkBackground;
}
function onError(error) {
Object.freeze(error)
events.emit('error', error);
if (error.critical) {
self.dispatch('command', 'stop');
}
}
function onEnded() {
events.emit('ended');
}
function onError() {
function onSubtitlesError(error) {
var message;
switch (error.code) {
case 70:
message = 'Failed to fetch subtitles from ' + error.track.origin;
break;
case 71:
message = 'Failed to parse subtitles from ' + error.track.origin;
break;
default:
message = 'Unknown subtitles error';
}
onError({
code: error.code,
message: message,
critical: false
});
}
function onVideoError() {
var message;
var critical;
switch (videoElement.error.code) {
@ -128,15 +145,11 @@ var HTMLVideo = function(containerElement) {
critical = true;
}
events.emit('error', {
onError({
code: videoElement.error.code,
message: message,
critical: critical
});
if (critical) {
self.dispatch('command', 'stop');
}
}
function onPausedChanged() {
events.emit('propChanged', 'paused', getPaused());
@ -169,21 +182,8 @@ var HTMLVideo = function(containerElement) {
events.emit('propChanged', 'subtitleDarkBackground', getSubtitleDarkBackground());
}
function updateSubtitleText() {
while (subtitlesElement.hasChildNodes()) {
subtitlesElement.removeChild(subtitlesElement.lastChild);
}
if (!loaded || !Array.isArray(subtitleCues.times)) {
return;
}
var time = getTime() + getSubtitleDelay();
var cuesForTime = subtitleUtils.cuesForTime(subtitleCues, time);
for (var i = 0; i < cuesForTime.length; i++) {
var cueNode = subtitleUtils.render(cuesForTime[i]);
cueNode.classList.add('cue');
subtitlesElement.append(cueNode, document.createElement('br'));
}
var time = getTime();
subtitles.updateTextForTime(time);
}
function flushArgsQueue() {
for (var i = 0; i < dispatchArgsQueue.length; i++) {
@ -274,41 +274,7 @@ var HTMLVideo = function(containerElement) {
break;
case 'selectedSubtitleTrackId':
if (loaded) {
selectedSubtitleTrackId = null;
subtitleDelay = 0;
subtitleCues = {};
for (var i = 0; i < subtitleTracks.length; i++) {
var subtitleTrack = subtitleTracks[i];
if (subtitleTrack.id === arguments[2]) {
selectedSubtitleTrackId = subtitleTrack.id;
fetch(subtitleTrack.url)
.then(function(resp) {
return resp.text();
})
.catch(function() {
events.emit('error', {
code: 70,
message: 'Failed to fetch subtitles from ' + subtitleTrack.origin,
critical: false
});
})
.then(function(text) {
if (selectedSubtitleTrackId === subtitleTrack.id) {
subtitleCues = subtitleUtils.parse(text);
updateSubtitleText();
}
})
.catch(function() {
events.emit('error', {
code: 71,
message: 'Failed to parse subtitles from ' + subtitleTrack.origin,
critical: false
});
});
break;
}
}
subtitles.selectedTrackId = arguments[2];
onSubtitleDelayChanged();
onSelectedSubtitleTrackIdChanged();
updateSubtitleText();
@ -317,7 +283,7 @@ var HTMLVideo = function(containerElement) {
case 'subtitleDelay':
if (loaded) {
if (!isNaN(arguments[2])) {
subtitleDelay = parseFloat(arguments[2]);
subtitles.delay = arguments[2];
onSubtitleDelayChanged();
updateSubtitleText();
}
@ -325,17 +291,12 @@ var HTMLVideo = function(containerElement) {
break;
case 'subtitleSize':
if (!isNaN(arguments[2])) {
stylesElement.sheet.cssRules[subtitleStylesIndex].style.fontSize = parseFloat(arguments[2]) + 'pt';
subtitles.size = arguments[2];
onSubtitleSizeChanged();
}
return;
case 'subtitleDarkBackground':
if (arguments[2]) {
subtitlesElement.classList.add('dark-background');
} else {
subtitlesElement.classList.remove('dark-background');
}
subtitles.darkBackground = arguments[2];
onSubtitleDarkBackgroundChanged();
return;
case 'volume':
@ -352,30 +313,7 @@ var HTMLVideo = function(containerElement) {
switch (arguments[1]) {
case 'addSubtitleTracks':
if (loaded) {
var extraSubtitleTracks = (Array.isArray(arguments[2]) ? arguments[2] : [])
.filter(function(track) {
return track &&
typeof track.url === 'string' &&
track.url.length > 0 &&
typeof track.origin === 'string' &&
track.origin.length > 0 &&
track.origin !== 'EMBEDDED';
})
.map(function(track) {
return Object.freeze(Object.assign({}, track, {
id: track.url
}));
});
subtitleTracks = subtitleTracks.concat(extraSubtitleTracks)
.filter(function(track, index, tracks) {
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].id === track.id) {
return i === index;
}
}
return false;
});
subtitles.addTracks(arguments[2]);
onSubtitleTracksChanged();
}
break;
@ -388,14 +326,13 @@ var HTMLVideo = function(containerElement) {
return;
case 'stop':
videoElement.removeEventListener('ended', onEnded);
videoElement.removeEventListener('error', onError);
videoElement.removeEventListener('error', onVideoError);
videoElement.removeEventListener('timeupdate', updateSubtitleText);
subtitles.removeListener('error', onSubtitlesError);
subtitles.removeListener('load', updateSubtitleText);
loaded = false;
dispatchArgsQueue = [];
subtitleCues = {};
subtitleTracks = [];
selectedSubtitleTrackId = null;
subtitleDelay = 0;
subtitles.clearTracks();
videoElement.removeAttribute('src');
videoElement.load();
videoElement.currentTime = 0;
@ -413,8 +350,10 @@ var HTMLVideo = function(containerElement) {
self.dispatch('command', 'stop');
dispatchArgsQueue = dispatchArgsQueueCopy;
videoElement.addEventListener('ended', onEnded);
videoElement.addEventListener('error', onError);
videoElement.addEventListener('error', onVideoError);
videoElement.addEventListener('timeupdate', updateSubtitleText);
subtitles.addListener('error', onSubtitlesError);
subtitles.addListener('load', updateSubtitleText);
videoElement.autoplay = typeof arguments[3].autoplay === 'boolean' ? arguments[3].autoplay : true;
videoElement.currentTime = !isNaN(arguments[3].time) ? arguments[3].time / 1000 : 0;
videoElement.src = arguments[2].url;
@ -444,7 +383,7 @@ var HTMLVideo = function(containerElement) {
videoElement.removeEventListener('loadeddata', onBufferingChanged);
containerElement.removeChild(videoElement);
containerElement.removeChild(stylesElement);
containerElement.removeChild(subtitlesElement);
subtitles.detachElements();
return;
default:
throw new Error('command not supported: ' + arguments[1]);