├── .npmignore ├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── .storybook └── config.js ├── .eslintrc ├── src ├── stories │ └── player.js └── index.js ├── LICENSE ├── package.json ├── README.md └── test └── reactYoutubePlayer.js /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | src 3 | test 4 | .* 5 | *.log 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-flow-comments" 4 | ], 5 | "presets": [ 6 | "react", 7 | "es2015", 8 | "stage-0" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | .* 6 | !.babelrc 7 | !.editorconfig 8 | !.eslintrc 9 | !.flowconfig 10 | !.gitignore 11 | !.npmignore 12 | !.scripts 13 | !.travis.yml 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | - 6 5 | - 4 6 | before_install: 7 | - npm config set depth 0 8 | - npm install --global npm@4 9 | script: 10 | - npm run test 11 | - npm run lint 12 | notifications: 13 | email: false 14 | sudo: false 15 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { 2 | configure 3 | } from '@kadira/storybook'; 4 | 5 | const loadStories = () => { 6 | // eslint-disable-next-line import/no-unassigned-import global-require 7 | require('../dist/stories/player'); 8 | }; 9 | 10 | configure(loadStories, module); 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "canonical", 4 | "canonical/flowtype", 5 | "canonical/react" 6 | ], 7 | "root": true, 8 | "rules": { 9 | "class-methods-use-this": 0, 10 | "filenames/match-exported": 0, 11 | "flowtype/no-weak-types": 0, 12 | "react/no-unused-prop-types": 0, 13 | "react/sort-comp": 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/stories/player.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | storiesOf 4 | } from '@kadira/storybook'; 5 | import YouTubePlayer from '..'; 6 | 7 | storiesOf('react-youtube-player', module) 8 | .add('component mounted with playbackState set to "unstarted"', () => { 9 | return ; 13 | }) 14 | .add('component mounted with playbackState set to "playing"', () => { 15 | return ; 19 | }) 20 | .add('component mounted with playbackState set to "paused"', () => { 21 | return ; 25 | }); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Gajus Kuizinas (http://gajus.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Gajus Kuizinas (http://gajus.com/) nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL ANUARY BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "email": "gajus@gajus.com", 4 | "name": "Gajus Kuizinas", 5 | "url": "http://gajus.com" 6 | }, 7 | "dependencies": { 8 | "youtube-player": "^5.1.0" 9 | }, 10 | "description": "React component that encapsulates YouTube IFrame Player API and exposes player controls using the component properties.", 11 | "devDependencies": { 12 | "@kadira/storybook": "^2.35.3", 13 | "babel-cli": "^6.24.1", 14 | "babel-core": "^6.24.1", 15 | "babel-loader": "^7.0.0", 16 | "babel-plugin-transform-flow-comments": "^6.22.0", 17 | "babel-plugin-transform-react-jsx": "^6.24.1", 18 | "babel-plugin-transform-runtime": "^6.23.0", 19 | "babel-preset-es2015": "^6.24.1", 20 | "babel-preset-stage-0": "^6.24.1", 21 | "chai": "^3.5.0", 22 | "eslint": "^3.19.0", 23 | "eslint-config-canonical": "^8.2.1", 24 | "jsdom": "^10.1.0", 25 | "react": "^15.5.4", 26 | "react-addons-test-utils": "^15.5.1", 27 | "react-dom": "^15.5.4" 28 | }, 29 | "keywords": [ 30 | "react-component", 31 | "react", 32 | "youtube", 33 | "video", 34 | "player" 35 | ], 36 | "license": "BSD-3-Clause", 37 | "main": "./dist/index.js", 38 | "name": "react-youtube-player", 39 | "peerDepdendencies": { 40 | "react": "^0.14.0", 41 | "react-dom": "^0.14.0" 42 | }, 43 | "repository": { 44 | "type": "git", 45 | "url": "https://github.com/gajus/react-youtube-player" 46 | }, 47 | "scripts": { 48 | "build": "NODE_ENV=production babel ./src --out-dir ./dist --source-maps --copy-files", 49 | "demo": "start-storybook -p 9001", 50 | "lint": "eslint src test", 51 | "test": "echo 'Please add tests. :-('" 52 | }, 53 | "version": "2.0.1" 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-youtube-player 2 | 3 | [![Travis build status](http://img.shields.io/travis/gajus/react-youtube-player/master.svg?style=flat-square)](https://travis-ci.org/gajus/react-youtube-player) 4 | [![NPM version](http://img.shields.io/npm/v/react-youtube-player.svg?style=flat-square)](https://www.npmjs.org/package/react-youtube-player) 5 | [![js-canonical-style](https://img.shields.io/badge/code%20style-canonical-blue.svg?style=flat-square)](https://github.com/gajus/canonical) 6 | 7 | React component that encapsulates [YouTube IFrame Player API](https://developers.google.com/youtube/iframe_api_reference) and exposes player controls using the component properties. 8 | 9 | ## Implementation 10 | 11 | `componentDidMount` callback is used to replace the rendered element with an `iframe` that loads a YouTube Player. 12 | 13 | `componentWillReceiveProps` is used to detect when component properties change, compare them with the state of the YouTube Player and call YouTube IFrame Player API when necessary. 14 | 15 | ## Usage 16 | 17 | ```js 18 | import YoutubePlayer from 'react-youtube-player'; 19 | 20 | /** 21 | * @typedef {string} YoutubePlayer~playbackState 22 | * @value 'unstarted' Stops and cancels loading of the current video. [stopVideo]{@link https://developers.google.com/youtube/iframe_api_reference#stopVideo} 23 | * @value 'playing' Plays the currently cued/loaded video. [playVideo]{@link https://developers.google.com/youtube/iframe_api_reference#playVideo} 24 | * @value 'paused' Pauses the currently playing video. [pauseVideo]{@link https://developers.google.com/youtube/iframe_api_reference#pauseVideo} 25 | */ 26 | 27 | /** 28 | * @property {string} videoId 29 | * @property {string|number} width (default: '100%'). 30 | * @property {string|number} height (default: '100%'). 31 | * @property {YoutubePlayer~playbackState} playbackState 32 | * @property {Object} configuration Configuration parameters to be passed to the YouTube Player (known as `playerVars` in the YouTube Player API for iframe Embeds, https://developers.google.com/youtube/player_parameters?playerVersion=HTML5#Parameters). 33 | */ 34 | 44 | ``` 45 | 46 | ## Demo 47 | 48 | To run the demo: 49 | 50 | ``` 51 | npm install 52 | npm run build 53 | npm run demo 54 | ``` 55 | -------------------------------------------------------------------------------- /test/reactYoutubePlayer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /* eslint-disable max-nested-callbacks */ 4 | 5 | // import assert from 'assert'; 6 | // import React from 'react'; 7 | // import ReactDOM from 'react-dom'; 8 | // import ReactYoutubePlayer from '../../src'; 9 | // 10 | // let player, playerComponent; 11 | // 12 | // const dom = document.createElement('div'); 13 | // 14 | // dom.id = 'dom'; 15 | // 16 | // describe('creating the player', () => { 17 | // before(() => { 18 | // document.body.appendChild(dom); 19 | // }); 20 | // 21 | // it('should render a Youtube player', (done) => { 22 | // playerComponent = ReactDOM.render( 23 | // , 27 | // dom, done 28 | // ); 29 | // player = playerComponent.player; 30 | // }); 31 | // 32 | // it('should properly create the player', function (done) { 33 | // this.slow(2000); 34 | // 35 | // setTimeout(() => { 36 | // const iframes = Array.from(document.getElementsByTagName('iframe')) 37 | // .filter((iframe) => { 38 | // return iframe.src.indexOf('youtube') !== -1; 39 | // }); 40 | // 41 | // assert.equal(iframes.length, 1); 42 | // done(); 43 | // }, 500); 44 | // }); 45 | // }); 46 | // 47 | // describe('using the player', () => { 48 | // it('should allow for playing the video', function (done) { 49 | // this.slow(3000); 50 | // this.timeout(5000); 51 | // playerComponent.setPlaybackState('playing'); 52 | // player.on('stateChange', () => { 53 | // player.getPlayerState().then((state) => { 54 | // // 1 == 'playing' 55 | // if (state === 1) { 56 | // done(); 57 | // } 58 | // 59 | // return state; 60 | // }) 61 | // .catch((err) => { 62 | // done(null, err); 63 | // }); 64 | // }); 65 | // }); 66 | // 67 | // it('should allow for pausing the video', (done) => { 68 | // playerComponent.setPlaybackState('paused'); 69 | // player.on('stateChange', () => { 70 | // player.getPlayerState().then((state) => { 71 | // // 1 == 'playing' 72 | // if (state === 2) { 73 | // done(); 74 | // } 75 | // 76 | // return state; 77 | // }) 78 | // .catch((err) => { 79 | // done(null, err); 80 | // }); 81 | // }); 82 | // }); 83 | // 84 | // it('should allow for changing the currently playing videos', function (done) { 85 | // this.slow(200); 86 | // playerComponent.cueVideoId('M7lc1UVf-VE'); 87 | // let finished; 88 | // 89 | // finished = false; 90 | // player.on('stateChange', () => { 91 | // player.getVideoUrl().then((url) => { 92 | // if (url.indexOf('M7lc1UVf-VE') !== -1 && !finished) { 93 | // finished = true; 94 | // done(); 95 | // } 96 | // 97 | // return url; 98 | // }) 99 | // .catch((err) => { 100 | // done(null, err); 101 | // }); 102 | // }); 103 | // }); 104 | // }); 105 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { 4 | PureComponent 5 | } from 'react'; 6 | import YoutubePlayer from 'youtube-player'; 7 | 8 | type PlaybackStateNameType = 'playing' | 'paused' | 'unstarted'; 9 | 10 | type ConfigurationType = {| 11 | +autoplay: 0 | 1, 12 | +cc_load_policy: 0 | 1, 13 | +color: 'red' | 'white', 14 | +controls: 0 | 1 | 2, 15 | +disablekb: 0 | 1, 16 | +enablejsapi: 0 | 1, 17 | +end: number, 18 | +fs: 0 | 1, 19 | +hl: string, 20 | +iv_load_policy: 1 | 3, 21 | +list: 'search' | 'user_uploads' | 'playlist', 22 | +listType: 'playlist' | 'search' | 'user_uploads', 23 | +loop: 0 | 1, 24 | +modestbranding: 0 | 1, 25 | +origin: string, 26 | +playlist: string, 27 | +playsinline: 0 | 1, 28 | +rel: 0 | 1, 29 | +showinfo: 0 | 1, 30 | +start: number, 31 | +theme: 'dark' | 'light' 32 | |}; 33 | 34 | type PropsType = {| 35 | +videoId: string, 36 | 37 | /** 38 | * @property configuration Configuration parameters to be passed to the YouTube Player (known as `playerVars` in the YouTube Player API for iframe Embeds, https://developers.google.com/youtube/player_parameters?playerVersion=HTML5#Parameters). 39 | */ 40 | +configuration: ConfigurationType, 41 | 42 | /** 43 | * @value 'unstarted' Stops and cancels loading of the current video. [stopVideo]{@link https://developers.google.com/youtube/iframe_api_reference#stopVideo} 44 | * @value 'playing' Plays the currently cued/loaded video. [playVideo]{@link https://developers.google.com/youtube/iframe_api_reference#playVideo} 45 | * @value 'paused' Pauses the currently playing video. [pauseVideo]{@link https://developers.google.com/youtube/iframe_api_reference#pauseVideo} 46 | */ 47 | +playbackState: string, 48 | 49 | // https://developers.google.com/youtube/iframe_api_reference#onReady 50 | // onReady: React.PropTypes.func, 51 | 52 | // https://developers.google.com/youtube/iframe_api_reference#onStateChange 53 | // onStateChange: React.PropTypes.func, 54 | 55 | // https://developers.google.com/youtube/iframe_api_reference#onPlaybackQualityChange 56 | // onPlaybackQualityChange: React.PropTypes.func, 57 | 58 | // https://developers.google.com/youtube/iframe_api_reference#onPlaybackRateChange 59 | // onPlaybackRateChange: React.PropTypes.func, 60 | 61 | // https://developers.google.com/youtube/iframe_api_reference#onApiChange 62 | // onApiChange: React.PropTypes.func, 63 | onBuffer: Function, 64 | 65 | // https://developers.google.com/youtube/iframe_api_reference#onStateChange 66 | onEnd: Function, 67 | 68 | // https://developers.google.com/youtube/iframe_api_reference#onError 69 | onError: Function, 70 | onPause: Function, 71 | onPlay: Function, 72 | onCued: Function, 73 | onUnstarted: Function 74 | |}; 75 | 76 | class ReactYoutubePlayer extends PureComponent { 77 | props: PropsType; 78 | 79 | // eslint-disable-next-line no-useless-constructor 80 | constructor (props: PropsType) { 81 | super(props); 82 | } 83 | 84 | static stateNames = { 85 | '-1': 'unstarted', 86 | 0: 'ended', 87 | 1: 'playing', 88 | 2: 'paused', 89 | 3: 'buffering', 90 | 5: 'cued' 91 | }; 92 | 93 | static defaultProps = { 94 | configuration: {}, 95 | onBuffer: () => {}, 96 | onCued: () => {}, 97 | onEnd: () => {}, 98 | onError: () => {}, 99 | onPause: () => {}, 100 | onPlay: () => {}, 101 | onUnstarted: () => {}, 102 | playbackState: 'unstarted' 103 | }; 104 | 105 | componentDidMount () { 106 | this.player = YoutubePlayer(this.refPlayer, { 107 | height: '100%', 108 | playerVars: this.props.configuration, 109 | width: '100%' 110 | }); 111 | 112 | this.bindEvent(); 113 | 114 | this.diffState({}, this.props); 115 | } 116 | 117 | componentWillUnmount () { 118 | this.player.destroy(); 119 | } 120 | 121 | componentWillReceiveProps (nextProps) { 122 | this.diffState(this.props, nextProps); 123 | } 124 | 125 | shouldComponentUpdate (): boolean { 126 | return false; 127 | } 128 | 129 | /** 130 | * State set using 'state' property can change, e.g. 131 | * 'playing' will change to 'ended' at the end of the video. 132 | * Read playback state reflects the current player state 133 | * and is used to compare against the video player properties. 134 | */ 135 | setRealPlaybackState = (stateName: string): void => { 136 | this.realPlaybackState = stateName; 137 | }; 138 | 139 | getRealPlaybackState = (): string => { 140 | return this.realPlaybackState; 141 | }; 142 | 143 | /** 144 | * Used to map YouTube IFrame Player API events to the callbacks 145 | * defined using the component instance properties. 146 | */ 147 | bindEvent = (): void => { 148 | this.player.on('stateChange', (event) => { 149 | this.setRealPlaybackState(ReactYoutubePlayer.stateNames[event.data]); 150 | 151 | const realPlaybackState = this.getRealPlaybackState(); 152 | 153 | if (realPlaybackState === 'ended') { 154 | this.props.onEnd(event); 155 | } else if (realPlaybackState === 'playing') { 156 | this.props.onPlay(event); 157 | } else if (realPlaybackState === 'paused') { 158 | this.props.onPause(event); 159 | } else if (realPlaybackState === 'buffering') { 160 | this.props.onBuffer(event); 161 | } else if (realPlaybackState === 'cued') { 162 | this.props.onCued(event); 163 | } else if (realPlaybackState === 'unstarted') { 164 | this.props.onUnstarted(event); 165 | } 166 | }); 167 | 168 | this.player.on('error', (event) => { 169 | this.props.onError(event); 170 | }); 171 | }; 172 | 173 | /** 174 | * The complexity of the ReactYoutubePlayer is that it attempts to combine 175 | * stateless properties with stateful player. This function is comparing 176 | * the last known property value of a state with the last known state of the player. 177 | * When these are different, it initiates an action that changes the player state, e.g. 178 | * when the current "state" property is "play" and the last known player state is "pause", 179 | * then setPlaybackState method will be called. 180 | */ 181 | diffState = (prevProps: $Shape, nextProps: $Shape): void => { 182 | if (prevProps.videoId !== nextProps.videoId && nextProps.videoId) { 183 | this.cueVideoId(nextProps.videoId); 184 | } 185 | 186 | if (this.realPlaybackState !== nextProps.playbackState && nextProps.playbackState) { 187 | this.setPlaybackState(nextProps.playbackState); 188 | } 189 | }; 190 | 191 | setPlaybackState = (stateName: PlaybackStateNameType): void => { 192 | if (stateName === 'playing') { 193 | this.player.playVideo(); 194 | } else if (stateName === 'paused') { 195 | this.player.pauseVideo(); 196 | } else if (stateName === 'unstarted') { 197 | this.player.stopVideo(); 198 | } else { 199 | throw new Error('Invalid playback state ("' + stateName + '").'); 200 | } 201 | }; 202 | 203 | cueVideoId = (videoId: string): void => { 204 | this.player.cueVideoById(videoId); 205 | }; 206 | 207 | render () { 208 | const style = { 209 | display: 'block', 210 | height: '100%', 211 | width: '100%' 212 | }; 213 | 214 | return
{ 216 | this.refViewport = element; 217 | }} style={style}> 218 |
{ 220 | this.refPlayer = element; 221 | }} style={style} 222 | /> 223 |
; 224 | } 225 | } 226 | 227 | export default ReactYoutubePlayer; 228 | --------------------------------------------------------------------------------