stremio-web/src/video/HTMLVideo.js
2020-04-08 11:45:16 +03:00

460 lines
15 KiB
JavaScript

// Copyright (C) 2017-2020 Smart code 203358507
var EventEmitter = require('events');
var HTMLSubtitles = require('./HTMLSubtitles');
function HTMLVideo(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 loaded = false;
var events = new EventEmitter();
var observedProps = {};
var subtitles = new HTMLSubtitles({ containerElement: containerElement });
var stylesElement = document.createElement('style');
var videoElement = document.createElement('video');
events.on('error', function() { });
subtitles.on('propChanged', onSubtitlesPropChanged);
subtitles.on('trackLoaded', onSubtitlesTrackLoaded);
subtitles.on('error', onSubtitlesError);
containerElement.appendChild(stylesElement);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .video { position: absolute; width: 100%; height: 100%; z-index: -1; background-color: black; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(videoElement);
videoElement.classList.add('video');
videoElement.crossOrigin = 'anonymous';
videoElement.controls = false;
videoElement.onpause = function() {
onVideoPropChanged('paused');
};
videoElement.onplay = function() {
onVideoPropChanged('paused');
};
videoElement.ontimeupdate = function() {
onVideoPropChanged('currentTime');
};
videoElement.ondurationchange = function() {
onVideoPropChanged('duration');
};
videoElement.onwaiting = function() {
onVideoPropChanged('readyState');
};
videoElement.onplaying = function() {
onVideoPropChanged('readyState');
};
videoElement.onloadeddata = function() {
onVideoPropChanged('readyState');
};
videoElement.onvolumechange = function() {
onVideoPropChanged('volume');
onVideoPropChanged('muted');
};
videoElement.onended = function() {
onVideoEnded();
};
videoElement.onerror = function() {
onVideoError();
};
function onSubtitlesPropChanged(propName) {
switch (propName) {
case 'tracks': {
if (observedProps['subtitlesTracks']) {
events.emit('propChanged', 'subtitlesTracks', getProp('subtitlesTracks'));
}
break;
}
case 'selectedTrackId': {
if (observedProps['selectedSubtitlesTrackId']) {
events.emit('propChanged', 'selectedSubtitlesTrackId', getProp('selectedSubtitlesTrackId'));
}
break;
}
case 'delay': {
subtitles.updateText(getProp('time'));
if (observedProps['subtitlesDelay']) {
events.emit('propChanged', 'subtitlesDelay', getProp('subtitlesDelay'));
}
break;
}
case 'size': {
if (observedProps['subtitlesSize']) {
events.emit('propChanged', 'subtitlesSize', getProp('subtitlesSize'));
}
break;
}
case 'offset': {
if (observedProps['subtitlesOffset']) {
events.emit('propChanged', 'subtitlesOffset', getProp('subtitlesOffset'));
}
break;
}
case 'textColor': {
if (observedProps['subtitlesTextColor']) {
events.emit('propChanged', 'subtitlesTextColor', getProp('subtitlesTextColor'));
}
break;
}
case 'backgroundColor': {
if (observedProps['subtitlesBackgroundColor']) {
events.emit('propChanged', 'subtitlesBackgroundColor', getProp('subtitlesBackgroundColor'));
}
break;
}
case 'outlineColor': {
if (observedProps['subtitlesOutlineColor']) {
events.emit('propChanged', 'subtitlesOutlineColor', getProp('subtitlesOutlineColor'));
}
break;
}
}
}
function onSubtitlesTrackLoaded(track) {
subtitles.updateText(getProp('time'));
events.emit('subtitlesTrackLoaded', track);
}
function onSubtitlesError(error) {
onError(Object.assign({}, error, {
critical: false
}));
}
function onVideoPropChanged(propName) {
switch (propName) {
case 'paused': {
if (observedProps['paused']) {
events.emit('propChanged', 'paused', getProp('paused'));
}
break;
}
case 'currentTime': {
subtitles.updateText(getProp('time'));
if (observedProps['time']) {
events.emit('propChanged', 'time', getProp('time'));
}
break;
}
case 'duration': {
if (observedProps['duration']) {
events.emit('propChanged', 'duration', getProp('duration'));
}
break;
}
case 'readyState': {
if (observedProps['buffering']) {
events.emit('propChanged', 'buffering', getProp('buffering'));
}
break;
}
case 'volume': {
if (observedProps['volume']) {
events.emit('propChanged', 'volume', getProp('volume'));
}
break;
}
case 'muted': {
if (observedProps['muted']) {
events.emit('propChanged', 'muted', getProp('muted'));
}
break;
}
}
}
function onVideoEnded() {
events.emit('ended');
}
function onVideoError() {
onError({
code: videoElement.error.code,
message: videoElement.error.message,
critical: true
});
}
function onError(error) {
if (!error) {
return;
}
Object.freeze(error);
events.emit('error', error);
if (error.critical) {
command('stop');
}
}
function getProp(propName) {
switch (propName) {
case 'paused': {
if (!loaded) {
return null;
}
return !!videoElement.paused;
}
case 'time': {
if (!loaded || isNaN(videoElement.currentTime) || videoElement.currentTime === null) {
return null;
}
return Math.floor(videoElement.currentTime * 1000);
}
case 'duration': {
if (!loaded || isNaN(videoElement.duration) || videoElement.duration === null) {
return null;
}
return Math.floor(videoElement.duration * 1000);
}
case 'buffering': {
if (!loaded) {
return null;
}
return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
}
case 'volume': {
if (destroyed || isNaN(videoElement.volume) || videoElement.volume === null) {
return null;
}
return Math.floor(videoElement.volume * 100);
}
case 'muted': {
if (destroyed) {
return null;
}
return !!videoElement.muted;
}
case 'subtitlesTracks': {
return subtitles.tracks;
}
case 'selectedSubtitlesTrackId': {
return subtitles.selectedTrackId;
}
case 'subtitlesDelay': {
return subtitles.delay;
}
case 'subtitlesSize': {
return subtitles.size;
}
case 'subtitlesOffset': {
return subtitles.offset;
}
case 'subtitlesTextColor': {
return subtitles.textColor;
}
case 'subtitlesBackgroundColor': {
return subtitles.backgroundColor;
}
case 'subtitlesOutlineColor': {
return subtitles.outlineColor;
}
default: {
throw new Error('getProp not supported: ' + propName);
}
}
}
function observeProp(propName) {
if (HTMLVideo.manifest.props.indexOf(propName) === -1) {
throw new Error('observeProp not supported: ' + propName);
}
events.emit('propValue', propName, getProp(propName));
observedProps[propName] = true;
}
function setProp(propName, propValue) {
switch (propName) {
case 'paused': {
if (loaded) {
if (!!propValue) {
videoElement.pause();
} else {
videoElement.play();
}
}
break;
}
case 'time': {
if (loaded && !isNaN(propValue) && propValue !== null) {
videoElement.currentTime = parseInt(propValue) / 1000;
}
break;
}
case 'volume': {
if (!isNaN(propValue) && propValue !== null) {
videoElement.muted = false;
videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue))) / 100;
}
break;
}
case 'muted': {
videoElement.muted = !!propValue;
break;
}
case 'selectedSubtitlesTrackId': {
if (loaded) {
subtitles.selectedTrackId = propValue;
}
break;
}
case 'subtitlesDelay': {
if (loaded) {
subtitles.delay = propValue;
}
break;
}
case 'subtitlesSize': {
subtitles.size = propValue;
break;
}
case 'subtitlesOffset': {
subtitles.offset = propValue;
break;
}
case 'subtitlesTextColor': {
subtitles.textColor = propValue;
break;
}
case 'subtitlesBackgroundColor': {
subtitles.backgroundColor = propValue;
break;
}
case 'subtitlesOutlineColor': {
subtitles.outlineColor = propValue;
break;
}
default: {
throw new Error('setProp not supported: ' + propName);
}
}
}
function command(commandName, commandArgs) {
switch (commandName) {
case 'addSubtitlesTracks': {
if (loaded && commandArgs) {
subtitles.addTracks(commandArgs.tracks);
}
break;
}
case 'stop': {
loaded = false;
videoElement.removeAttribute('src');
videoElement.load();
videoElement.currentTime = 0;
onVideoPropChanged('paused');
onVideoPropChanged('currentTime');
onVideoPropChanged('duration');
onVideoPropChanged('readyState');
subtitles.clearTracks();
break;
}
case 'load': {
if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
command('stop');
videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
videoElement.currentTime = !isNaN(commandArgs.time) && commandArgs.time !== null ? parseInt(commandArgs.time) / 1000 : 0;
videoElement.src = commandArgs.stream.url;
loaded = true;
onVideoPropChanged('paused');
onVideoPropChanged('currentTime');
onVideoPropChanged('duration');
onVideoPropChanged('readyState');
}
break;
}
case 'destroy': {
command('stop');
destroyed = true;
onVideoPropChanged('volume');
onVideoPropChanged('muted');
subtitles.destroy();
events.removeAllListeners();
events.on('error', function() { });
videoElement.onpause = null;
videoElement.onplay = null;
videoElement.ontimeupdate = null;
videoElement.ondurationchange = null;
videoElement.onwaiting = null;
videoElement.onplaying = null;
videoElement.onloadeddata = null;
videoElement.onvolumechange = null;
videoElement.onended = null;
videoElement.onerror = null;
containerElement.removeChild(videoElement);
containerElement.removeChild(stylesElement);
break;
}
default: {
throw new Error('command not supported: ' + commandName);
}
}
}
function on(eventName, listener) {
if (destroyed) {
throw new Error('Video is destroyed');
}
events.on(eventName, listener);
}
function dispatch(args) {
if (destroyed) {
throw new Error('Video is destroyed');
}
if (args) {
if (typeof args.commandName === 'string') {
command(args.commandName, args.commandArgs);
return;
} else if (typeof args.propName === 'string') {
setProp(args.propName, args.propValue);
return;
} else if (typeof args.observedPropName === 'string') {
observeProp(args.observedPropName);
return;
}
}
throw new Error('Invalid dispatch call: ' + JSON.stringify(args));
}
this.on = on;
this.dispatch = dispatch;
Object.freeze(this);
};
HTMLVideo.manifest = Object.freeze({
name: 'HTMLVideo',
embedded: true,
props: Object.freeze(['paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesSize', 'subtitlesDelay', 'subtitlesOffset', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor'])
});
Object.freeze(HTMLVideo);
module.exports = HTMLVideo;