var EventEmitter = require('events'); var HTMLSubtitles = require('./HTMLSubtitles'); function YouTubeVideo(containerElement) { if (!(containerElement instanceof HTMLElement)) { throw new Error('Instance of HTMLElement required as a first argument'); } var self = this; var ready = false; var loaded = false; var destroyed = false; var events = new EventEmitter(); var dispatchArgsReadyQueue = []; var dispatchArgsLoadedQueue = []; var pausedObserved = false; var timeObserved = false; var durationObserved = false; var bufferingObserved = false; var volumeObserved = false; var timeChangedIntervalId = window.setInterval(onTimeChangedInterval, 100); var durationChangedIntervalId = window.setInterval(onDurationChangedInterval, 100); var volumeChangedIntervalId = window.setInterval(onVolumeChangedInterval, 100); var subtitles = new HTMLSubtitles(containerElement); var video = null; var scriptElement = document.createElement('script'); var stylesElement = document.createElement('style'); var videoContainer = document.createElement('div'); scriptElement.type = 'text/javascript'; scriptElement.src = 'https://www.youtube.com/iframe_api'; scriptElement.onload = onYouTubePlayerApiLoaded; scriptElement.onerror = onYouTubePlayerApiError; containerElement.appendChild(scriptElement); containerElement.appendChild(stylesElement); stylesElement.sheet.insertRule('#' + containerElement.id + ' .video { position: absolute; width: 100%; height: 100%; z-index: -1; }', stylesElement.sheet.cssRules.length); containerElement.appendChild(videoContainer); videoContainer.classList.add('video'); subtitles.addListener('error', onSubtitlesError); subtitles.addListener('load', updateSubtitleText); function getPaused() { if (!loaded) { return null; } return video.getPlayerState() !== YT.PlayerState.PLAYING; } function getTime() { if (!loaded) { return null; } return Math.floor(video.getCurrentTime() * 1000); } function getDuration() { if (!loaded) { return null; } return Math.floor(video.getDuration() * 1000); } function getBuffering() { if (!loaded) { return null; } return video.getPlayerState() === YT.PlayerState.BUFFERING; } function getVolume() { if (!ready || destroyed) { return null; } return video.isMuted() ? 0 : video.getVolume(); } function getSubtitleTracks() { if (!loaded) { return Object.freeze([]); } return subtitles.dispatch('getProp', 'tracks'); } function getSelectedSubtitleTrackId() { if (!loaded) { return null; } return subtitles.dispatch('getProp', 'selectedTrackId'); } function getSubtitleDelay() { if (!loaded) { return null; } return subtitles.dispatch('getProp', 'delay'); } function getSubtitleSize() { if (!ready || destroyed) { return null; } return subtitles.dispatch('getProp', 'size'); } function getSubtitleDarkBackground() { if (!ready || destroyed) { return null; } return subtitles.dispatch('getProp', 'darkBackground'); } function onEnded() { events.emit('ended'); } function onError(error) { Object.freeze(error); events.emit('error', error); if (error.critical) { self.dispatch('command', 'stop'); } } function onPausedChanged() { events.emit('propChanged', 'paused', getPaused()); } function onTimeChanged() { events.emit('propChanged', 'time', getTime()); } function onDurationChanged() { events.emit('propChanged', 'duration', getDuration()); } function onBufferingChanged() { events.emit('propChanged', 'buffering', getBuffering()); } function onVolumeChanged() { events.emit('propChanged', 'volume', getVolume()); } function onSubtitleTracksChanged() { events.emit('propChanged', 'subtitleTracks', getSubtitleTracks()); } function onSelectedSubtitleTrackIdChanged() { events.emit('propChanged', 'selectedSubtitleTrackId', getSelectedSubtitleTrackId()); } function onSubtitleDelayChanged() { events.emit('propChanged', 'subtitleDelay', getSubtitleDelay()); } function onSubtitleSizeChanged() { events.emit('propChanged', 'subtitleSize', getSubtitleSize()); } function onSubtitleDarkBackgroundChanged() { events.emit('propChanged', 'subtitleDarkBackground', getSubtitleDarkBackground()); } 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 onYouTubePlayerApiError() { onError({ code: 12, message: 'YouTube player API failed to load', critical: true }); } function onYouTubePlayerApiLoaded() { if (destroyed) { return; } if (!YT) { onYouTubePlayerApiError(); return; } YT.ready(() => { if (destroyed) { return; } video = new YT.Player(videoContainer, { height: '100%', width: '100%', playerVars: { autoplay: 1, cc_load_policy: 3, controls: 0, disablekb: 1, enablejsapi: 1, fs: 0, iv_load_policy: 3, loop: 0, modestbranding: 1, playsinline: 1, rel: 0 }, events: { onError: onVideoError, onReady: onVideoReady, onStateChange: onVideoStateChange } }); }); } function onVideoError(error) { var message; switch (error.data) { case 2: message = 'Invalid request'; break; case 5: message = 'The requested content cannot be played'; break; case 100: message = 'The video has been removed or marked as private'; break; case 101: case 150: message = 'The video cannot be played in embedded players'; break; default: message = 'Unknown error'; } onError({ code: error.data, message: message, critical: true }); } function onVideoReady() { ready = true; onVolumeChanged(); onSubtitleSizeChanged(); onSubtitleDarkBackgroundChanged(); flushDispatchArgsQueue(dispatchArgsReadyQueue); } function onVideoStateChange(state) { if (bufferingObserved) { onBufferingChanged(); } switch (state.data) { case YT.PlayerState.ENDED: onEnded(); break; case YT.PlayerState.PAUSED: case YT.PlayerState.PLAYING: if (pausedObserved) { onPausedChanged(); } if (timeObserved) { onTimeChanged(); } if (durationObserved) { onDurationChanged(); } break; case YT.PlayerState.UNSTARTED: if (pausedObserved) { onPausedChanged(); } break; } } function onTimeChangedInterval() { updateSubtitleText(); if (timeObserved) { onTimeChanged(); } } function onDurationChangedInterval() { if (durationObserved) { onDurationChanged(); } } function onVolumeChangedInterval() { if (volumeObserved) { onVolumeChanged(); } } function updateSubtitleText() { subtitles.dispatch('command', 'updateText', getTime()); } function flushDispatchArgsQueue(dispatchArgsQueue) { while (dispatchArgsQueue.length > 0) { var args = dispatchArgsQueue.shift(); self.dispatch.apply(self, args); } } this.addListener = function(eventName, listener) { if (destroyed) { throw new Error('Unable to add ' + eventName + ' listener'); } events.addListener(eventName, listener); }; this.removeListener = function(eventName, listener) { if (destroyed) { throw new Error('Unable to remove ' + eventName + ' listener'); } events.removeListener(eventName, listener); }; this.dispatch = function() { console.log(Array.from(arguments).map(String)) if (destroyed) { throw new Error('Unable to dispatch ' + arguments[0]); } switch (arguments[0]) { case 'observeProp': switch (arguments[1]) { case 'paused': events.emit('propValue', 'paused', getPaused()); pausedObserved = true; return; case 'time': events.emit('propValue', 'time', getTime()); timeObserved = true; return; case 'duration': events.emit('propValue', 'duration', getDuration()); durationObserved = true; return; case 'buffering': events.emit('propValue', 'duration', getBuffering()); bufferingObserved = true; return; case 'volume': events.emit('propValue', 'volume', getVolume()); volumeObserved = true; return; case 'subtitleTracks': events.emit('propValue', 'subtitleTracks', getSubtitleTracks()); return; case 'selectedSubtitleTrackId': events.emit('propValue', 'selectedSubtitleTrackId', getSelectedSubtitleTrackId()); return; case 'subtitleSize': events.emit('propValue', 'subtitleSize', getSubtitleSize()); return; case 'subtitleDelay': events.emit('propValue', 'subtitleDelay', getSubtitleDelay()); return; case 'subtitleDarkBackground': events.emit('propValue', 'subtitleDarkBackground', getSubtitleDarkBackground()); return; default: throw new Error('observeProp not supported: ' + arguments[1]); } case 'setProp': switch (arguments[1]) { case 'paused': if (loaded) { arguments[2] ? video.pauseVideo() : video.playVideo(); } else { dispatchArgsLoadedQueue.push(Array.from(arguments)); } return; case 'time': if (loaded) { if (!isNaN(arguments[2])) { video.seekTo(arguments[2] / 1000); } } else { dispatchArgsLoadedQueue.push(Array.from(arguments)); } return; case 'volume': if (ready) { if (!isNaN(arguments[2])) { video.unMute(); video.setVolume(Math.max(0, Math.min(100, arguments[2]))); } } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; case 'selectedSubtitleTrackId': if (loaded) { subtitles.dispatch('setProp', 'selectedTrackId', arguments[2]); onSubtitleDelayChanged(); onSelectedSubtitleTrackIdChanged(); updateSubtitleText(); } else { dispatchArgsLoadedQueue.push(Array.from(arguments)); } return; case 'subtitleSize': if (ready) { subtitles.dispatch('setProp', 'size', arguments[2]); onSubtitleSizeChanged(); } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; case 'subtitleDelay': if (loaded) { subtitles.dispatch('setProp', 'delay', arguments[2]); onSubtitleDelayChanged(); updateSubtitleText(); } else { dispatchArgsLoadedQueue.push(Array.from(arguments)); } return; case 'subtitleDarkBackground': if (ready) { subtitles.dispatch('setProp', 'darkBackground', arguments[2]); onSubtitleDarkBackgroundChanged(); } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; default: throw new Error('setProp not supported: ' + arguments[1]); } case 'command': switch (arguments[1]) { case 'addSubtitleTracks': if (loaded) { subtitles.dispatch('command', 'addTracks', arguments[2]); onSubtitleTracksChanged(); } else { dispatchArgsLoadedQueue.push(Array.from(arguments)); } return; case 'mute': if (ready) { video.mute(); } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; case 'unmute': if (ready) { video.unMute(); if (video.getVolume() === 0) { video.setVolume(50); } } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; case 'stop': loaded = false; dispatchArgsLoadedQueue = []; subtitles.dispatch('command', 'clearTracks'); if (ready) { video.stopVideo(); } onPausedChanged(); onTimeChanged(); onDurationChanged(); onBufferingChanged(); onSubtitleTracksChanged(); onSelectedSubtitleTrackIdChanged(); onSubtitleDelayChanged(); updateSubtitleText(); return; case 'load': if (ready) { debugger; var dispatchArgsLoadedQueueCopy = dispatchArgsLoadedQueue.slice(); self.dispatch('command', 'stop'); dispatchArgsLoadedQueue = dispatchArgsLoadedQueueCopy; var autoplay = typeof arguments[3].autoplay === 'boolean' ? arguments[3].autoplay : true; var time = !isNaN(arguments[3].time) ? arguments[3].time / 1000 : 0; if (autoplay) { video.loadVideoById({ videoId: arguments[2].ytId, startSeconds: time }); } else { video.cueVideoById({ videoId: arguments[2].ytId, startSeconds: time }); } loaded = true; onPausedChanged(); onTimeChanged(); onDurationChanged(); onBufferingChanged(); onSubtitleDelayChanged(); updateSubtitleText(); flushDispatchArgsQueue(dispatchArgsLoadedQueue); } else { dispatchArgsReadyQueue.push(Array.from(arguments)); } return; case 'destroy': self.dispatch('command', 'stop'); destroyed = true; onVolumeChanged(); onSubtitleSizeChanged(); onSubtitleDarkBackgroundChanged(); events.removeAllListeners(); clearInterval(timeChangedIntervalId); clearInterval(durationChangedIntervalId); clearInterval(volumeChangedIntervalId); video.destroy(); containerElement.removeChild(scriptElement); containerElement.removeChild(videoElement); containerElement.removeChild(stylesElement); subtitles.dispatch('command', 'destroy'); return; default: throw new Error('command not supported: ' + arguments[1]); } default: throw new Error('Invalid dispatch call: ' + Array.from(arguments).map(String)); } }; Object.freeze(this); }; YouTubeVideo.manifest = Object.freeze({ name: 'YouTubeVideo', embedded: true, props: Object.freeze(['paused', 'time', 'duration', 'volume', 'buffering', 'subtitleTracks', 'selectedSubtitleTrackId', 'subtitleSize', 'subtitleDelay', 'subtitleDarkBackground']) }); Object.freeze(YouTubeVideo); module.exports = YouTubeVideo;