// 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;