stremio-web/src/video/HTMLSubtitles.js
2020-03-24 14:05:11 +02:00

312 lines
11 KiB
JavaScript

var EventEmitter = require('events');
var subtitlesParser = require('./subtitlesParser');
var subtitlesRenderer = require('./subtitlesRenderer');
var colorConverter = require('./colorConverter');
var COLOR_REGEX = /^#[A-Fa-f0-9]{8}$/;
var ERROR_CODE = Object.freeze({
FETCH_FAILED: 70,
PARSE_FAILED: 71
});
var SIZE_COEF = 25;
function HTMLSubtitles(options) {
var containerElement = options && options.containerElement;
if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) {
throw new Error('Instance of HTMLElement with id attribute required');
}
if (!document.body.contains(containerElement)) {
throw new Error('Container element not attached to body');
}
var destroyed = false;
var events = new EventEmitter();
var cuesByTime = null;
var tracks = Object.freeze([]);
var selectedTrackId = null;
var delay = null;
var stylesElement = document.createElement('style');
var subtitlesElement = document.createElement('div');
events.on('error', function() { });
containerElement.appendChild(stylesElement);
var subtitlesContainerStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles { position: absolute; right: 0; bottom: 0; left: 0; z-index: 0; text-align: center; }', stylesElement.sheet.cssRules.length);
var subtitlesCueStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue { display: inline-block; padding: 0.2em; text-shadow: 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff; background-color: #00000000; color: #ffffffff; font-size: 4vmin; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue * { font-size: inherit; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(subtitlesElement);
subtitlesElement.classList.add('subtitles');
function on(eventName, listener) {
if (destroyed) {
return;
}
events.on(eventName, listener);
}
function addTracks(extraTracks) {
if (destroyed || !Array.isArray(extraTracks)) {
return;
}
tracks = 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);
events.emit('propChanged', 'tracks');
}
function updateText(mediaTime) {
while (subtitlesElement.hasChildNodes()) {
subtitlesElement.removeChild(subtitlesElement.lastChild);
}
if (cuesByTime === null || isNaN(mediaTime) || mediaTime === null) {
return;
}
var time = mediaTime + delay;
subtitlesRenderer.render(cuesByTime, time)
.forEach(function(cueNode) {
cueNode.classList.add('cue');
subtitlesElement.append(cueNode, document.createElement('br'));
});
}
function clearTracks() {
updateText(NaN);
cuesByTime = null;
tracks = Object.freeze([]);
selectedTrackId = null;
delay = null;
events.emit('propChanged', 'tracks');
events.emit('propChanged', 'selectedTrackId');
events.emit('propChanged', 'delay');
}
function destroy() {
destroyed = true;
clearTracks();
events.emit('propChanged', 'size');
events.emit('propChanged', 'textColor');
events.emit('propChanged', 'backgroundColor');
events.emit('propChanged', 'outlineColor');
events.emit('propChanged', 'offset');
events.removeAllListeners();
events.on('error', function() { });
containerElement.removeChild(stylesElement);
containerElement.removeChild(subtitlesElement);
}
Object.defineProperties(this, {
tracks: {
configurable: false,
enumerable: true,
get: function() {
return Object.freeze(tracks.slice());
}
},
selectedTrackId: {
configurable: false,
enumerable: true,
get: function() {
return selectedTrackId;
},
set: function(value) {
if (destroyed) {
return;
}
cuesByTime = null;
selectedTrackId = null;
delay = null;
updateText(NaN);
var selecterdTrack = tracks.find(function(track) {
return track.id === value;
});
if (selecterdTrack) {
selectedTrackId = selecterdTrack.id;
delay = 0;
fetch(selecterdTrack.url)
.then(function(resp) {
return resp.text();
})
.catch(function(error) {
events.emit('error', Object.freeze({
code: ERROR_CODE.FETCH_FAILED,
message: 'Failed to fetch subtitles from ' + selecterdTrack.origin,
track: selecterdTrack,
error: error
}));
})
.then(function(text) {
if (typeof text === 'string' && selectedTrackId === selecterdTrack.id) {
cuesByTime = subtitlesParser.parse(text);
if (cuesByTime.times.length === 0) {
throw new Error('parse failed');
}
events.emit('trackLoaded', selecterdTrack);
}
})
.catch(function(error) {
events.emit('error', Object.freeze({
code: ERROR_CODE.PARSE_FAILED,
message: 'Failed to parse subtitles from ' + selecterdTrack.origin,
track: selecterdTrack,
error: error
}));
});
}
events.emit('propChanged', 'selectedTrackId');
events.emit('propChanged', 'delay');
}
},
delay: {
configurable: false,
enumerable: true,
get: function() {
return delay;
},
set: function(value) {
if (destroyed || isNaN(value) || value === null || selectedTrackId === null) {
return;
}
delay = parseInt(value);
updateText(NaN);
events.emit('propChanged', 'delay');
}
},
size: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return parseInt(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.fontSize) * SIZE_COEF
},
set: function(value) {
if (destroyed || isNaN(value) || value === null) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.fontSize = Math.floor(value / SIZE_COEF) + 'vmin';
events.emit('propChanged', 'size');
}
},
offset: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return parseInt(stylesElement.sheet.cssRules[subtitlesContainerStylesIndex].style.bottom);
},
set: function(value) {
if (destroyed || isNaN(value) || value === null) {
return;
}
stylesElement.sheet.cssRules[subtitlesContainerStylesIndex].style.bottom = Math.max(0, Math.min(100, parseInt(value))) + '%';
events.emit('propChanged', 'offset');
}
},
textColor: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.color);
},
set: function(value) {
if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.color = value;
events.emit('propChanged', 'textColor');
}
},
backgroundColor: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.backgroundColor);
},
set: function(value) {
if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.backgroundColor = value;
events.emit('propChanged', 'backgroundColor');
}
},
outlineColor: {
configurable: false,
enumerable: false,
get: function() {
if (destroyed) {
return null;
}
return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.textShadow);
},
set: function(value) {
if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.textShadow =
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em';
events.emit('propChanged', 'outlineColor');
}
}
});
this.on = on;
this.addTracks = addTracks;
this.updateText = updateText;
this.clearTracks = clearTracks;
this.destroy = destroy;
Object.freeze(this);
};
Object.freeze(HTMLSubtitles);
module.exports = HTMLSubtitles;