diff --git a/src/index.html b/src/index.html index cbe96d57c..dcb80a620 100755 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@
+ \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 9246862c6..f7a189e01 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -1,6 +1,7 @@ import React, { Component, Fragment } from 'react'; import classnames from 'classnames'; import ReactHTMLVideo from './stremio-video/ReactHTMLVideo'; +import ReactYouTubeVideo from './stremio-video/ReactYouTubeVideo'; import ControlBar from './ControlBar'; import styles from './styles'; @@ -42,9 +43,13 @@ class Player extends Component { } prepareStream = () => { - return Promise.resolve({ - source: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', - videoComponent: ReactHTMLVideo + return new Promise((resolve, reject) => { + YT.ready(() => { + resolve({ + source: 'SEL97pn5aS0', + videoComponent: ReactYouTubeVideo + }); + }); }); } diff --git a/src/routes/Player/stremio-video/ReactYouTubeVideo.js b/src/routes/Player/stremio-video/ReactYouTubeVideo.js new file mode 100644 index 000000000..9866ff315 --- /dev/null +++ b/src/routes/Player/stremio-video/ReactYouTubeVideo.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import YouTubeVideo from './YouTubeVideo'; + +class ReactYouTubeVideo extends Component { + constructor(props) { + super(props); + + this.videoContainerRef = React.createRef(); + } + + componentDidMount() { + this.video = new YouTubeVideo(this.videoContainerRef.current); + this.video.on('ended', this.props.onEnded); + this.video.on('error', this.props.onError); + this.video.on('propValue', this.props.onPropValue); + this.video.on('propChanged', this.props.onPropChanged); + this.props.observedProps.forEach((propName) => { + this.dispatch('observeProp', propName); + }); + } + + componentWillUnmount() { + this.dispatch('stop'); + } + + shouldComponentUpdate() { + return false; + } + + dispatch = (...args) => { + try { + this.video && this.video.dispatch(...args); + } catch (e) { + console.error('YouTubeVideo', e); + } + } + + render() { + return ( +
+ ); + } +} + +ReactYouTubeVideo.manifest = YouTubeVideo.manifest; + +ReactYouTubeVideo.propTypes = { + className: PropTypes.string, + onEnded: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, + onPropValue: PropTypes.func.isRequired, + onPropChanged: PropTypes.func.isRequired, + observedProps: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export default ReactYouTubeVideo; diff --git a/src/routes/Player/stremio-video/YouTubeVideo.js b/src/routes/Player/stremio-video/YouTubeVideo.js new file mode 100644 index 000000000..ded1e9acf --- /dev/null +++ b/src/routes/Player/stremio-video/YouTubeVideo.js @@ -0,0 +1,189 @@ +var EventEmitter = require('events'); + +var YouTubeVideo = function(containerElement) { + var events = new EventEmitter(); + var ready = false; + var observedProps = {}; + var timeIntervalId = null; + var durationIntervalId = null; + var dispatchArgsQueue = []; + var onEnded = function() { + events.emit('ended'); + }; + var onError = function(event) { + var message; + var critical; + switch (event.data) { + case 2: + message = 'Invalid request'; + critical = true; + break; + case 5: + message = 'The requested content cannot be played'; + critical = true; + break; + case 100: + message = 'The video has been removed or marked as private'; + critical = true; + break; + case 101: + case 150: + message = 'The video cannot be played in embedded players'; + critical = true; + break; + default: + message = 'Unknown error'; + critical = true; + } + + events.emit('error', { + code: event.data, + message: message, + critical: critical + }); + }; + var onPausedChanged = function() { + events.emit('propChanged', 'paused', video.getPlayerState() === YT.PlayerState.PAUSED); + }; + var onTimeChanged = function() { + events.emit('propChanged', 'time', video.getCurrentTime() * 1000); + }; + var onDurationChanged = function() { + events.emit('propChanged', 'duration', video.getDuration() !== 0 ? video.getDuration() * 1000 : null); + }; + var onReady = function() { + ready = true; + dispatchArgsQueue.forEach(function(args) { + this.dispatch.apply(this, args); + }, this); + dispatchArgsQueue = []; + }; + var onStateChange = function(event) { + switch (event.data) { + case YT.PlayerState.ENDED: + onEnded(); + break; + case YT.PlayerState.PLAYING: + if (observedProps.paused) { + onPausedChanged(); + } + + if (observedProps.duration) { + onDurationChanged(); + } + break; + case YT.PlayerState.PAUSED: + if (observedProps.paused) { + onPausedChanged(); + } + break; + } + }; + var video = new YT.Player(containerElement, { + height: '100%', + width: '100%', + playerVars: { + autoplay: 1, + cc_load_policy: 0, + controls: 0, + disablekb: 1, + enablejsapi: 1, + fs: 0, + iv_load_policy: 3, + loop: 0, + modestbranding: 1, + playsinline: 1, + rel: 0 + }, + events: { + onError: onError.bind(this), + onReady: onReady.bind(this), + onStateChange: onStateChange.bind(this) + } + }); + + this.on = function(eventName, listener) { + events.on(eventName, listener); + }; + + this.dispatch = function() { + if (arguments[0] === 'observeProp') { + switch (arguments[1]) { + case 'paused': + if (ready) { + events.emit('propValue', 'paused', video.getPlayerState() === YT.PlayerState.PAUSED); + observedProps.paused = true; + } + break; + case 'time': + if (ready) { + events.emit('propValue', 'time', video.getCurrentTime() * 1000); + if (timeIntervalId === null) { + timeIntervalId = window.setInterval(onTimeChanged, 100); + } + } + break; + case 'duration': + if (ready) { + events.emit('propValue', 'duration', video.getDuration() !== 0 ? video.getDuration() * 1000 : null); + observedProps.duration = true; + if (durationIntervalId === null) { + durationIntervalId = window.setInterval(onDurationChanged, 5000); + } + } + break; + default: + throw new Error('observeProp not supported: ' + arguments[1]); + } + } else if (arguments[0] === 'setProp') { + switch (arguments[1]) { + case 'paused': + if (ready) { + arguments[2] ? video.pauseVideo() : video.playVideo(); + } + break; + case 'time': + if (ready) { + video.seekTo(arguments[2] / 1000); + } + break; + default: + throw new Error('setProp not supported: ' + arguments[1]); + } + } else if (arguments[0] === 'command') { + switch (arguments[1]) { + case 'load': + if (ready) { + video.loadVideoById({ + videoId: arguments[2].source, + startSeconds: isNaN(arguments[2].time) ? 0 : arguments[2].time / 1000 + }); + } + break; + case 'stop': + if (ready) { + events.removeAllListeners(); + observedProps = {}; + clearInterval(timeIntervalId); + clearInterval(durationIntervalId); + video.pauseVideo(); + } + break; + default: + throw new Error('command not supported: ' + arguments[1]); + } + } + + if (!ready) { + dispatchArgsQueue.push(Array.from(arguments)); + } + }; +}; + +YouTubeVideo.manifest = { + name: 'YouTubeVideo', + embedded: true, + props: ['paused', 'time', 'duration', 'volume'] +}; + +module.exports = YouTubeVideo;