mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 21:27:05 +00:00
312 lines
11 KiB
JavaScript
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;
|