├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── AudioPlayer.jsx ├── CirclePlayer.jsx ├── CircleProgress.js ├── Fullscreen.jsx ├── MediaPlayer.jsx ├── MuteUnmute.jsx ├── PlayPause.jsx ├── Repeat.jsx ├── VideoPlayer.jsx ├── audio │ ├── armstrong.mp3 │ └── podcast.mp3 ├── favicon.png ├── index.html ├── index.jsx ├── main.scss ├── playlist.js └── range-mixins.scss ├── package.json ├── src ├── Media.jsx ├── Player.jsx ├── context-types.js ├── controls │ ├── CurrentTime.jsx │ ├── Duration.jsx │ ├── Fullscreen.jsx │ ├── MuteUnmute.jsx │ ├── PlayPause.jsx │ ├── Progress.jsx │ ├── SeekBar.jsx │ ├── Volume.jsx │ └── index.js ├── decorators │ └── with-media-props.jsx ├── react-media-player.js ├── utils │ ├── exit-fullscreen.js │ ├── format-time.js │ ├── fullscreen-change.js │ ├── get-vendor.js │ ├── get-vimeo-id.js │ ├── get-youtube-id.js │ ├── index.js │ ├── keyboard-controls.jsx │ ├── load-api.js │ ├── request-fullscreen.js │ └── youtube-api-loader.js └── vendors │ ├── HTML5.jsx │ ├── Vimeo.jsx │ ├── Youtube.jsx │ └── vendor-prop-types.js ├── webpack.banner.js ├── webpack.config.js ├── webpack.prod.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "add-module-exports" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OSX Files 2 | .DS_Store 3 | .Trashes 4 | .Spotlight-V100 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # NPM 9 | node_modules 10 | npm-debug.log 11 | /dist/ 12 | /lib/ 13 | 14 | # General Files 15 | .sass-cache 16 | .hg 17 | .idea 18 | .svn 19 | .cache 20 | .project 21 | .tmp 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## CHANGELOG 2 | 3 | ### 0.7.9 4 | 5 | Fix build 6 | 7 | ### 0.7.8 8 | 9 | ### Features 10 | 11 | Add config prop [#75](https://github.com/souporserious/react-media-player/pull/75) 12 | 13 | Export `getVendor` utility [da19f297](https://github.com/souporserious/react-media-player/commit/da19f297) 14 | 15 | ### 0.7.7 16 | 17 | Add support for `isLoading` prop for Youtube vendor 18 | [#59](https://github.com/souporserious/react-media-player/pull/59) 19 | 20 | ### 0.7.6 21 | 22 | Create `AudioContext` in HTML5 vendor on `componentDidMount` 23 | [#53](https://github.com/souporserious/react-media-player/pull/53) 24 | 25 | ### 0.7.5 26 | 27 | Only use `AudioContext` in supported browsers 28 | 29 | ### 0.7.4 30 | 31 | Remove `AudioObject` vendor 32 | [#51](https://github.com/souporserious/react-media-player/pull/51) 33 | 34 | Add `connectSource` prop to Player 35 | [#50](https://github.com/souporserious/react-media-player/pull/50) 36 | 37 | Fix `formateTime` missing a `0` before minutes if it contains an hour 38 | [#49](https://github.com/souporserious/react-media-player/pull/49) 39 | 40 | ### 0.7.3 41 | 42 | Fix stale `Media` prop callback state 43 | 44 | ### 0.7.2 45 | 46 | Fix volume not being set correctly across vendors 47 | 48 | ### 0.7.1 49 | 50 | Allow React 16 on peerDependencies 51 | [#47](https://github.com/souporserious/react-media-player/pull/47) 52 | 53 | ### 0.7.0 54 | 55 | Break AudioObject into its own vendor 56 | [#44](https://github.com/souporserious/react-media-player/pull/44) 57 | 58 | ### 0.6.7 59 | 60 | Do not set ref when using audio object 61 | [#43](https://github.com/souporserious/react-media-player/pull/43) 62 | 63 | Fix for Firefox throwing an out of bounds error 64 | [#42](https://github.com/souporserious/react-media-player/pull/42) 65 | 66 | Workaround to cancel outstanding buffering network requests 67 | [#41](https://github.com/souporserious/react-media-player/pull/41) 68 | 69 | ### 0.6.6 70 | 71 | Check if `document` is defined (SSR support) 72 | [#40](https://github.com/souporserious/react-media-player/pull/40) 73 | 74 | ### 0.6.5 75 | 76 | Make sure to return `play` from `HTML5` component 77 | [#38](https://github.com/souporserious/react-media-player/pull/38) 78 | 79 | ### 0.6.4 80 | 81 | Return `play` in `Media` method so we can react to any promises that may have 82 | been returned 83 | 84 | ### 0.6.3 85 | 86 | Update ALL imports of PropTypes 87 | [#37](https://github.com/souporserious/react-media-player/pull/37) 88 | 89 | ### 0.6.2 90 | 91 | Migrate from React.PropTypes to prop-types package 92 | [#35](https://github.com/souporserious/react-media-player/pull/35) 93 | 94 | ### 0.6.1 95 | 96 | Fix `fullscreen` for Youtube videos 97 | 98 | Fix for durations over an hour 99 | [#20](https://github.com/souporserious/react-media-player/pull/20) 100 | 101 | ### 0.6.0 102 | 103 | `withMediaPlayer` has been removed in favor of a smart `Player` component that 104 | can be passed as a child in the `Media` component 105 | 106 | Each callback now gets the full state of the player passed into it as an object 107 | 108 | play, pause, playPause, stop, seekTo, mute, muteUnmute, setVolume, and 109 | fullscreen are all passed into a child function as well as available as public 110 | methods on the `Media` component 111 | 112 | Added ability to use an audio object to store audio in memory rather than a DOM 113 | node 114 | 115 | Moved keyboard controls into a function see example `MediaPlayer` component for 116 | usage 117 | 118 | Player will default to an empty video player instead of erroring when no `src` 119 | is provided. 120 | 121 | Made `isLoading` state better 122 | 123 | Fix Safari crashing when using Audio Object 124 | 125 | ### 0.5.1 126 | 127 | Pass `vendor` to `Media` child function 128 | 129 | Makes sure Youtube player is loaded before switching sources 130 | 131 | ### 0.5.0 132 | 133 | Media component now accepts `autoPlay`, `loop`, and event callback props 134 | 135 | Removed `get-file-extension` in favor of using a regex and test 136 | 137 | Fixed API loader to not call callback until script is loaded 138 | 139 | ### 0.4.0 140 | 141 | Renamed `KeyboardControls` -> `withKeyboardControls` 142 | 143 | `withKeyboardControls` exposed on `ReactMediaPlayer` rather than utils 144 | 145 | Context not meant to be used publicly now, everything should be ran through 146 | decorator functions 147 | 148 | ### 0.3.3 149 | 150 | Fixed seekbar not updating while scrubbing 151 | 152 | Fixed range inputs showing full progress on initial load 153 | 154 | Now checks if `onChange` is used in Seekbar and Volume components 155 | 156 | Patched Youtube not getting proper duration when loading a new video 157 | 158 | Reset duration when loading a new Youtube video 159 | 160 | ### 0.3.2 161 | 162 | Allow better styling of `SeekBar` and `Volume` controls by passing 163 | background-size. Specifically for styling back fill color in Chrome 164 | 165 | Workaround for [know bug](https://github.com/facebook/react/issues/554) with 166 | input ranges in <= IE11 167 | 168 | ### 0.3.1 169 | 170 | Fixes bad reference to main file in package.json 171 | 172 | ### 0.3.0 173 | 174 | Added `vendor` prop to allow explicitly choosing which component to render for 175 | the player. Useful for cases where we can't determine what type of file is 176 | trying to be played. 177 | 178 | ### 0.2.0 179 | 180 | Complete rewrite, better API, use of context in place of spreading props. 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Travis Arnold 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React Media Player 2 | 3 | Components and decorators to help build video & audio players in React. Supports 4 | HTML5, Youtube, and Vimeo media types. 5 | 6 | ## Install 7 | 8 | `npm install react-media-player --save` 9 | 10 | ```html 11 | 12 | (UMD library exposed as `ReactMediaPlayer`) 13 | ``` 14 | 15 |
16 | 17 | ## `Media` component 18 | 19 | A special wrapper component that collects information from the `Player` 20 | component and passes down the proper props to `withMediaProps` decorator. 21 | 22 | ## `Player` component 23 | 24 | This is another special component that renders your player and communicates with 25 | the `Media` wrapper. 26 | 27 | #### `src`: PropTypes.string.isRequired 28 | 29 | This is the source of the media you want to play. Currently supporting Youtube, 30 | Vimeo, and any HTML5 compatible video or audio. 31 | 32 | #### `vendor`: PropTypes.oneOf(['youtube', 'vimeo', 'audio', 'video']) 33 | 34 | Explicitly choose which internal component to render for the player. If nothing 35 | is set, the library does its best to determine what player to render based on 36 | the source passed in. 37 | 38 | #### `autoPlay`: PropTypes.bool 39 | 40 | Autoplay media when the component is mounted or `src` changes. 41 | 42 | #### `loop`: PropTypes.bool 43 | 44 | Loop the current `src` indefinitely. 45 | 46 | #### `useAudioObject`: PropTypes.bool 47 | 48 | When playing HTML5 `audio`, it will construct audio using the 49 | [`Audio`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement#Constructor) 50 | class instead of rendering an element to the page. 51 | 52 | #### `connectSource`: PropTypes.func(source, audioContext) 53 | 54 | A chance to connect a series of 55 | [`AudioNode`[s]](https://developer.mozilla.org/en-US/docs/Web/API/AudioNode) 56 | when using the `audio` vendor. Must return a new audio node that will be 57 | connected to `audioContext.destination` internally. 58 | 59 | #### `onPlay`: PropTypes.func 60 | 61 | Callback when media starts playing. 62 | 63 | #### `onPause`: PropTypes.func 64 | 65 | Callback when media has been paused. 66 | 67 | #### `onError`:PropTypes.func 68 | 69 | Callback when an error occurs. 70 | 71 | #### `onDuration`: PropTypes.func 72 | 73 | Callback when the duration of the media has been calculated. 74 | 75 | #### `onProgress`: PropTypes.func 76 | 77 | Callback when media starts downloading. 78 | 79 | #### `onTimeUpdate`: PropTypes.func 80 | 81 | Callback when media time has changed. 82 | 83 | #### `onMute`: PropTypes.func 84 | 85 | Callback when the player has been muted. 86 | 87 | #### `onVolumeChange`: PropTypes.func 88 | 89 | Callback when the player volume has changed. 90 | 91 | ```js 92 | import React, { Component } from 'react' 93 | import { Media, Player, controls } from 'react-media-player' 94 | const { PlayPause, MuteUnmute } = controls 95 | 96 | class MediaPlayer extends Component { 97 | render() { 98 | return ( 99 | 100 |
101 |
102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 |
110 | ) 111 | } 112 | } 113 | ``` 114 | 115 |
116 | 117 | ## `withMediaProps` decorator props exposed under `this.props.media` 118 | 119 | Passes down helpful state information and methods to build custom media player 120 | controls. Please note that children must be wrapped in the `Media` component. 121 | 122 | #### `currentTime`: PropTypes.number 123 | 124 | #### `progress`: PropTypes.number 125 | 126 | #### `duration`: PropTypes.number 127 | 128 | #### `volume`: PropTypes.number 129 | 130 | #### `isLoading`: PropTypes.bool 131 | 132 | #### `isPlaying`: PropTypes.bool 133 | 134 | #### `isMuted`: PropTypes.bool 135 | 136 | #### `isFullscreen`: PropTypes.bool 137 | 138 | #### `play`: PropTypes.func 139 | 140 | #### `pause`: PropTypes.func 141 | 142 | #### `playPause`: PropTypes.func 143 | 144 | #### `stop`: PropTypes.func 145 | 146 | #### `seekTo`: PropTypes.func 147 | 148 | #### `mute`: PropTypes.func 149 | 150 | #### `muteUnmute`: PropTypes.func 151 | 152 | #### `setVolume`: PropTypes.func 153 | 154 | #### `fullscreen`: PropTypes.func 155 | 156 | ```js 157 | import React, { Component } from 'react' 158 | import { withMediaProps } from 'react-media-player' 159 | 160 | class CustomPlayPause extends Component { 161 | shouldComponentUpdate({ media }) { 162 | return this.props.media.isPlaying !== media.isPlaying 163 | } 164 | 165 | _handlePlayPause = () => { 166 | this.props.media.playPause() 167 | } 168 | 169 | render() { 170 | const { className, style, media } = this.props 171 | return ( 172 | 180 | ) 181 | } 182 | } 183 | 184 | export default withMediaProps(CustomPlayPause) 185 | ``` 186 | 187 | ```js 188 | import React from 'react' 189 | import CustomPlayPause from './CustomPlayPause' 190 | 191 | function App() { 192 | return ( 193 | 194 | 195 | 196 | 197 | ) 198 | } 199 | 200 | export default App 201 | ``` 202 | 203 |
204 | 205 | ## `utils.keyboardControls` 206 | 207 | A special function that will provide keyboard support to the media player. 208 | 209 | ```js 210 | import React, { Component } from 'react' 211 | import { Media, Player, controls, utils } from 'react-media-player' 212 | const { 213 | PlayPause, 214 | CurrentTime, 215 | Progress, 216 | SeekBar, 217 | Duration, 218 | MuteUnmute, 219 | Volume, 220 | Fullscreen, 221 | } = controls 222 | const { keyboardControls } = utils 223 | 224 | class MediaPlayer extends Component { 225 | render() { 226 | const { Player, keyboardControls } = this.props 227 | return ( 228 | 229 | {mediaProps => ( 230 |
234 | 235 |
236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 |
245 |
246 | )} 247 |
248 | ) 249 | } 250 | } 251 | ``` 252 | 253 |
254 | 255 | ## `utils.formatTime` 256 | 257 | A helper function to format time. 258 | 259 | ```js 260 | import React, { Component } from 'react' 261 | import { withMediaProps, utils } from 'react-media-player' 262 | const { formatTime } = utils 263 | 264 | class CurrentTime extends Component { 265 | shouldComponentUpdate({ media }) { 266 | return this.props.media.currentTime !== media.currentTime 267 | } 268 | 269 | render() { 270 | const { className, style, media } = this.props 271 | return ( 272 | 275 | ) 276 | } 277 | } 278 | 279 | export default withMediaProps(CurrentTime) 280 | ``` 281 | 282 |
283 | 284 | ## Running Locally 285 | 286 | clone repo 287 | 288 | `git clone git@github.com:souporserious/react-media-player.git` 289 | 290 | move into folder 291 | 292 | `cd ~/react-media-player` 293 | 294 | install dependencies 295 | 296 | `npm install` 297 | 298 | run dev mode 299 | 300 | `npm run dev` 301 | 302 | open your browser and visit: `http://localhost:8080/` 303 | -------------------------------------------------------------------------------- /example/AudioPlayer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM, { findDOMNode } from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import { Media, Player, controls, utils } from '../src/react-media-player' 5 | import PlayPause from './PlayPause' 6 | import MuteUnmute from './MuteUnmute' 7 | 8 | const { 9 | CurrentTime, 10 | Progress, 11 | SeekBar, 12 | Duration, 13 | Volume, 14 | Fullscreen, 15 | } = controls 16 | const { formatTime } = utils 17 | 18 | class Panner { 19 | constructor({ source, audioContext, panningAmount = 0 }) { 20 | this._source = source 21 | this._audioContext = audioContext 22 | this._initialPanningAmount = panningAmount 23 | } 24 | 25 | connect() { 26 | this._splitter = this._audioContext.createChannelSplitter(2) 27 | this._gainLeft = this._audioContext.createGain() 28 | this._gainRight = this._audioContext.createGain() 29 | this._merger = this._audioContext.createChannelMerger(2) 30 | this._source.connect( 31 | this._splitter, 32 | 0, 33 | 0 34 | ) 35 | this._splitter.connect( 36 | this._gainLeft, 37 | 0 38 | ) 39 | this._splitter.connect( 40 | this._gainRight, 41 | 1 42 | ) 43 | this._gainLeft.connect( 44 | this._merger, 45 | 0, 46 | 0 47 | ) 48 | this._gainRight.connect( 49 | this._merger, 50 | 0, 51 | 1 52 | ) 53 | return this._merger 54 | } 55 | 56 | setPosition(amount) { 57 | this._gainLeft.gain.value = amount <= 0 ? 1 : 1 - amount 58 | this._gainRight.gain.value = amount >= 0 ? 1 : 1 + amount 59 | } 60 | } 61 | 62 | const tracks = ['podcast', 'armstrong'] 63 | 64 | class AudioPlayer extends Component { 65 | state = { 66 | currentTrack: tracks[0], 67 | } 68 | 69 | _handlePannerChange = ({ target }) => { 70 | const x = +target.value 71 | const y = 0 72 | const z = 1 - Math.abs(x) 73 | this.panner.setPosition(x, y, z) 74 | } 75 | 76 | _connectSource = (source, audioContext) => { 77 | this.panner = new Panner({ source, audioContext }) 78 | return this.panner.connect() 79 | } 80 | 81 | render() { 82 | return ( 83 | (this.media = c)}> 84 |
85 | {tracks.map(track => ( 86 | 92 | ))} 93 | (this._player = c)} 95 | src={`/audio/${this.state.currentTrack}.mp3`} 96 | connectSource={this._connectSource} 97 | useAudioObject 98 | // autoPlay 99 | /> 100 |
101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 116 |
117 |
118 | ) 119 | } 120 | } 121 | 122 | export default AudioPlayer 123 | -------------------------------------------------------------------------------- /example/CirclePlayer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import CircleProgress from './CircleProgress' 4 | import { Media, Player } from '../src/react-media-player' 5 | 6 | class CircleMediaPlayer extends Component { 7 | componentDidMount() { 8 | this._circle = new CircleProgress(this._svg) 9 | } 10 | 11 | renderPlay() { 12 | return ( 13 | 17 | ) 18 | } 19 | 20 | renderPause() { 21 | return ( 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | _handleTimeUpdate = ({ currentTime, duration }) => { 30 | this._circle.setProgress(currentTime / duration * 100) 31 | } 32 | 33 | render() { 34 | return ( 35 | 36 | {({ isPlaying, playPause }) => 37 | 52 | } 53 | 54 | ) 55 | } 56 | } 57 | 58 | export default CircleMediaPlayer 59 | -------------------------------------------------------------------------------- /example/CircleProgress.js: -------------------------------------------------------------------------------- 1 | class CircleProgress { 2 | constructor(el) { 3 | this.el = el; 4 | this.r = el.getAttribute("r"); 5 | this.c = Math.PI * (this.r * 2); 6 | this._init(); 7 | } 8 | 9 | _init() { 10 | this.el.style.strokeDasharray = this.c; 11 | this.setProgress(0); 12 | } 13 | 14 | setProgress(amount) { 15 | const dashoffset = Math.abs(amount * this.c / 100 - this.c); 16 | this.el.style.strokeDashoffset = dashoffset; 17 | } 18 | } 19 | 20 | export default CircleProgress; 21 | -------------------------------------------------------------------------------- /example/Fullscreen.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withMediaProps } from '../src/react-media-player' 4 | 5 | class Fullscreen extends Component { 6 | static contextTypes = { 7 | fullscreen: PropTypes.func, 8 | isFullscreen: PropTypes.bool 9 | } 10 | 11 | _handleFullscreen = () => { 12 | this.props.media.fullscreen() 13 | } 14 | 15 | render() { 16 | const { isFullscreen, fullscreen } = this.context 17 | return ( 18 | this._handleFullscreen()}> 19 | 20 | { 21 | !isFullscreen ? 22 | 23 | 24 | 25 | 26 | 27 | : 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default withMediaProps(Fullscreen) 41 | -------------------------------------------------------------------------------- /example/MediaPlayer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Media, Player, controls, utils } from '../src/react-media-player' 4 | import PlayPause from './PlayPause' 5 | import MuteUnmute from './MuteUnmute' 6 | import Repeat from './Repeat' 7 | 8 | const { 9 | CurrentTime, 10 | Progress, 11 | SeekBar, 12 | Duration, 13 | Volume, 14 | Fullscreen, 15 | } = controls 16 | const { keyboardControls } = utils 17 | 18 | const PrevTrack = props => ( 19 | 20 | 21 | 22 | ) 23 | 24 | const NextTrack = props => ( 25 | 26 | 27 | 28 | ) 29 | 30 | class MediaPlayer extends Component { 31 | _handlePrevTrack = () => { 32 | this.props.onPrevTrack() 33 | } 34 | 35 | _handleNextTrack = () => { 36 | this.props.onNextTrack() 37 | } 38 | 39 | _handleRepeatTrack = () => { 40 | this.props.onRepeatTrack() 41 | } 42 | 43 | _handleEnded = () => { 44 | this.props.onNextTrack() 45 | } 46 | 47 | render() { 48 | const { src, currentTrack, repeatTrack, autoPlay } = this.props 49 | return ( 50 | 51 | {mediaProps => ( 52 |
60 |
mediaProps.playPause()} 63 | > 64 | 73 |
74 |
75 |
76 | 77 | {currentTrack} 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 | 87 |
88 |
89 | 93 | 94 | 98 |
99 |
100 | 105 | 106 |
107 |
108 |
109 |
110 | )} 111 |
112 | ) 113 | } 114 | } 115 | 116 | export default MediaPlayer 117 | -------------------------------------------------------------------------------- /example/MuteUnmute.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withMediaProps } from '../src/react-media-player' 4 | import Transition from 'react-motion-ui-pack' 5 | 6 | class Scale extends Component { 7 | render() { 8 | return ( 9 | 14 | {this.props.children} 15 | 16 | ) 17 | } 18 | } 19 | 20 | class MuteUnmute extends Component { 21 | _handleMuteUnmute = () => { 22 | this.props.media.muteUnmute() 23 | } 24 | 25 | render() { 26 | const { media: { isMuted, volume }, className } = this.props 27 | return ( 28 | 29 | 30 | 31 | 32 | { volume >= 0.5 && 33 | 34 | } 35 | 36 | 37 | { volume > 0 && 38 | 39 | } 40 | 41 | 42 | { volume === 0 && 43 | 44 | } 45 | 46 | 47 | ) 48 | } 49 | } 50 | 51 | export default withMediaProps(MuteUnmute) 52 | -------------------------------------------------------------------------------- /example/PlayPause.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withMediaProps } from '../src/react-media-player' 4 | import Transition from 'react-motion-ui-pack' 5 | 6 | class ScaleX extends Component { 7 | render() { 8 | return ( 9 | 14 | {this.props.children} 15 | 16 | ) 17 | } 18 | } 19 | 20 | class PlayPause extends Component { 21 | _handlePlayPause = () => { 22 | this.props.media.playPause() 23 | } 24 | 25 | render() { 26 | const { media: { isPlaying }, className } = this.props 27 | return ( 28 | 36 | 37 | 38 | { isPlaying && 39 | 40 | 41 | 42 | 43 | } 44 | 45 | 46 | { !isPlaying && 47 | 53 | } 54 | 55 | 56 | ) 57 | } 58 | } 59 | 60 | export default withMediaProps(PlayPause) 61 | -------------------------------------------------------------------------------- /example/Repeat.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Repeat extends Component { 5 | render() { 6 | const { isActive, ...props } = this.props 7 | const fill = isActive ? '#8bb955' : '#CDD7DB' 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | } 17 | 18 | export default Repeat 19 | -------------------------------------------------------------------------------- /example/VideoPlayer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Media, Player, withMediaProps, withKeyboardControls, controls } from '../src/react-media-player' 4 | import PlayPause from './PlayPause' 5 | import MuteUnmute from './MuteUnmute' 6 | import Fullscreen from './Fullscreen' 7 | 8 | const { CurrentTime, Progress, SeekBar, Duration, Volume } = controls 9 | 10 | export default function VideoPlayer({ src }) { 11 | return ( 12 | 13 | {({ isFullscreen, playPause }) => 14 |
18 | playPause()} 21 | /> 22 |
23 | 24 | 25 |
26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 |
34 |
35 | } 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /example/audio/armstrong.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/souporserious/react-media-player/dced754b58151349e78979ebca3b3aab9f8f3e82/example/audio/armstrong.mp3 -------------------------------------------------------------------------------- /example/audio/podcast.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/souporserious/react-media-player/dced754b58151349e78979ebca3b3aab9f8f3e82/example/audio/podcast.mp3 -------------------------------------------------------------------------------- /example/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/souporserious/react-media-player/dced754b58151349e78979ebca3b3aab9f8f3e82/example/favicon.png -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Component 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /example/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createElement } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import MediaPlayer from './MediaPlayer' 5 | import VideoPlayer from './VideoPlayer' 6 | import AudioPlayer from './AudioPlayer' 7 | import CirclePlayer from './CirclePlayer' 8 | 9 | import { Media, Player, controls } from '../src/react-media-player' 10 | import playlist from './playlist' 11 | 12 | import './main.scss' 13 | 14 | const { PlayPause } = controls 15 | const mod = (num, max) => ((num % max) + max) % max 16 | 17 | class Playlist extends Component { 18 | _handleTrackClick(track) { 19 | this.props.onTrackClick(track) 20 | } 21 | 22 | render() { 23 | const { tracks, currentTrack } = this.props 24 | return ( 25 | 43 | ) 44 | } 45 | } 46 | 47 | class App extends Component { 48 | state = { 49 | currentTrack: { src: null, label: 'No media loaded' }, 50 | showMediaPlayer: true, 51 | repeatTrack: false, 52 | autoPlay: true, 53 | } 54 | 55 | _handleTrackClick = track => { 56 | this.setState({ currentTrack: track }) 57 | } 58 | 59 | _navigatePlaylist = direction => { 60 | const newIndex = mod( 61 | playlist.indexOf(this.state.currentTrack) + direction, 62 | playlist.length 63 | ) 64 | this.setState({ currentTrack: playlist[newIndex] }) 65 | } 66 | 67 | render() { 68 | const { showMediaPlayer, currentTrack, repeatTrack, autoPlay } = this.state 69 | return ( 70 |
71 | 76 | {showMediaPlayer && ( 77 |
78 | (this._mediaPlayer = c)} 80 | src={currentTrack.src} 81 | autoPlay={autoPlay} 82 | loop={repeatTrack} 83 | currentTrack={currentTrack.label} 84 | repeatTrack={repeatTrack} 85 | onPrevTrack={() => this._navigatePlaylist(-1)} 86 | onNextTrack={() => this._navigatePlaylist(1)} 87 | onRepeatTrack={() => { 88 | this.setState({ repeatTrack: !repeatTrack }) 89 | }} 90 | onPlay={() => !autoPlay && this.setState({ autoPlay: true })} 91 | onPause={() => this.setState({ autoPlay: false })} 92 | onEnded={() => !repeatTrack && this._navigatePlaylist(1)} 93 | /> 94 | 99 |
100 | )} 101 | 102 | 103 | 104 |
105 | ) 106 | } 107 | } 108 | 109 | ReactDOM.render(, document.getElementById('app')) 110 | -------------------------------------------------------------------------------- /example/main.scss: -------------------------------------------------------------------------------- 1 | @import './range-mixins.scss'; 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Lato', sans-serif; 9 | } 10 | 11 | // Circle Media Player 12 | .circle-media-player { 13 | display: inline-block; 14 | padding: 0; 15 | margin: 0; 16 | border: 0; 17 | line-height: 0; 18 | background-color: transparent; 19 | 20 | fill: none; 21 | stroke: #d7dbdc; 22 | stroke-width: 3; 23 | 24 | cursor: pointer; 25 | outline: 0; 26 | } 27 | 28 | .circle-media-player__foreground { 29 | stroke: #6e9541; 30 | transition: 350ms stroke-dashoffset; 31 | 32 | // position the start of the circle at the top 33 | transform: rotate(-90deg); 34 | transform-origin: 50% 50%; 35 | } 36 | 37 | .circle-media-player__background { 38 | } 39 | 40 | .circle-media-player__play, 41 | .circle-media-player__pause { 42 | fill: #6e9541; 43 | stroke-width: 0; 44 | } 45 | 46 | .media-player-wrapper { 47 | display: flex; 48 | flex-direction: column; 49 | width: 100%; 50 | max-width: 640px; 51 | margin-bottom: 24px; 52 | } 53 | 54 | .media-player { 55 | width: 100%; 56 | max-width: 640px; 57 | position: relative; 58 | 59 | // hide native controls 60 | video::-webkit-media-controls { 61 | display: none !important; 62 | } 63 | } 64 | 65 | .media-player-element { 66 | max-width: 100%; 67 | height: 0; 68 | padding-bottom: 56.25%; // 16:9 69 | position: relative; 70 | overflow: hidden; 71 | background-color: #d4d4d4; 72 | 73 | video, 74 | iframe, 75 | object, 76 | embed { 77 | display: block; 78 | width: 100%; 79 | height: 100%; 80 | border: 0; 81 | position: absolute; 82 | top: 0; 83 | left: 0; 84 | } 85 | } 86 | 87 | .media-controls { 88 | display: flex; 89 | align-items: center; 90 | padding: 12px; 91 | background-color: #282f31; 92 | color: #fff; 93 | 94 | svg, 95 | path, 96 | polygon { 97 | transform-origin: 50% 50%; 98 | } 99 | 100 | .media-player--fullscreen & { 101 | width: 100%; 102 | position: absolute; 103 | bottom: 0; 104 | left: 0; 105 | 106 | // push controls above fullscreen video 107 | z-index: 2147483647; 108 | } 109 | } 110 | 111 | .media-controls--full { 112 | flex-direction: column; 113 | 114 | .media-control-group--seek { 115 | width: 100%; 116 | margin: 12px 0; 117 | } 118 | } 119 | 120 | .media-row { 121 | display: flex; 122 | justify-content: space-between; 123 | width: 100%; 124 | } 125 | 126 | .media-control-group { 127 | display: flex; 128 | align-items: center; 129 | position: relative; 130 | } 131 | 132 | .media-control-group--seek { 133 | flex: 1; 134 | } 135 | 136 | .media-control { 137 | margin: 0 12px; 138 | } 139 | 140 | .media-control--progress { 141 | -webkit-appearance: none; 142 | width: calc(100% - 24px); 143 | height: 3px; 144 | margin: 0 12px; 145 | border: 0; 146 | position: absolute; 147 | top: 7px; 148 | left: 12px; 149 | 150 | // bar 151 | background-color: #373d3f; 152 | &::-webkit-progress-bar { 153 | background-color: #373d3f; 154 | } 155 | 156 | // progress 157 | color: lighten(#373d3f, 5%); // IE 158 | &::-moz-progress-bar { 159 | background-color: lighten(#373d3f, 5%); 160 | } 161 | &::-webkit-progress-value { 162 | background-color: lighten(#373d3f, 5%); 163 | } 164 | } 165 | 166 | .media-control--seekbar { 167 | position: relative; 168 | z-index: 5; 169 | 170 | @include -range-track(webkit, moz, ms) { 171 | background-color: transparent !important; 172 | } 173 | } 174 | 175 | .media-control--volume { 176 | max-width: 120px; 177 | } 178 | 179 | .media-control--prev-track { 180 | margin-right: 6px; 181 | } 182 | 183 | .media-control--next-track { 184 | margin-left: 6px; 185 | } 186 | 187 | input[type='range'] { 188 | // reset inputs to a plain state 189 | @include -range__reset(webkit, moz, ms); 190 | 191 | $track-height: 3px; 192 | $track-lower-color: #86b350; 193 | $track-upper-color: #373d3f; 194 | 195 | $thumb-height: 8px; 196 | $thumb-width: 8px; 197 | $thumb-color: #cdd7db; 198 | 199 | width: 100%; 200 | height: 12px; 201 | padding: 0 12px; 202 | margin: 0; 203 | background-color: transparent; 204 | 205 | &:hover, 206 | &:active { 207 | @include -range-thumb(webkit, moz, ms) { 208 | transform: scale(1.25); 209 | } 210 | } 211 | 212 | @include -range-track(webkit, moz, ms) { 213 | width: 100%; 214 | height: $track-height; 215 | border: 0; 216 | cursor: pointer; 217 | background-color: $track-upper-color; 218 | 219 | &:active { 220 | cursor: grabbing; 221 | } 222 | } 223 | 224 | // so we can style the lower progress 225 | &::-webkit-slider-container { 226 | background-size: inherit; 227 | } 228 | 229 | @include -range-track(webkit) { 230 | background: { 231 | image: linear-gradient($track-lower-color, $track-lower-color); 232 | size: inherit; 233 | repeat: no-repeat; 234 | } 235 | } 236 | 237 | @include -range-fill-lower(webkit, moz, ms) { 238 | background-color: $track-lower-color; 239 | } 240 | 241 | @include -range-thumb(webkit, moz, ms) { 242 | width: $thumb-width; 243 | height: $thumb-height; 244 | border: 0; 245 | border-radius: 50%; 246 | background-color: $thumb-color; 247 | cursor: grab; 248 | transform: scale(1); 249 | transform-origin: 50% 50%; 250 | transition: transform 150ms ease-out; 251 | 252 | &:active { 253 | cursor: grabbing; 254 | } 255 | } 256 | 257 | @include -range-thumb(webkit) { 258 | position: relative; 259 | top: -2.5px; 260 | } 261 | } 262 | 263 | .media-playlist-header { 264 | padding: 2px; 265 | background-color: #373d3f; 266 | } 267 | 268 | .media-playlist-title { 269 | font-size: 14px; 270 | text-align: center; 271 | } 272 | 273 | .media-playlist { 274 | background-color: #282f31; 275 | color: #fff; 276 | } 277 | 278 | .media-playlist-tracks { 279 | padding: 0; 280 | margin: 0; 281 | list-style: none; 282 | border: 1px solid #373d3f; 283 | } 284 | 285 | .media-playlist-track { 286 | padding: 12px; 287 | cursor: pointer; 288 | 289 | & + & { 290 | border-top: 1px solid #373d3f; 291 | } 292 | 293 | &.is-active { 294 | color: #8bb955; 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /example/playlist.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | src: "http://www.youtube.com/embed/h3YVKTxTOgU", 4 | label: "Brand New (Youtube)" 5 | }, 6 | { 7 | src: "https://youtu.be/VOyYwzkQB98", 8 | label: "Neck Deep (Youtube)" 9 | }, 10 | { 11 | src: "https://player.vimeo.com/video/156147818", 12 | label: "Pump (Vimeo)" 13 | }, 14 | { 15 | src: "https://vimeo.com/channels/staffpicks/150734165", 16 | label: "Lesley (Vimeo)" 17 | }, 18 | { 19 | src: `http://a1083.phobos.apple.com/us/r1000/014/Music/v4/4e/44/b7/4e44b7dc-aaa2-c63b-fb38-88e1635b5b29/mzaf_1844128138535731917.plus.aac.p.m4a`, 20 | label: "iTunes Preview" 21 | }, 22 | { 23 | src: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4", 24 | label: "Big Buck Bunny" 25 | }, 26 | { 27 | src: "https://vid4u.org/ninja/5/dev/assets/madmax-intro.mp4", 28 | label: "Mad Max Intro" 29 | }, 30 | { 31 | src: "http://www.w3schools.com/html/movie.mp4", 32 | label: "Bear" 33 | }, 34 | { 35 | src: "http://jelmerdemaat.nl/online-demos/conexus/video/small.mp4", 36 | label: "Lego Robot" 37 | }, 38 | { 39 | src: "http://shapeshed.com/examples/HTML5-video-element/video/320x240.m4v", 40 | label: "iPod Help" 41 | }, 42 | { 43 | src: "http://html5demos.com/assets/dizzy.mp4", 44 | label: "Dizzy Kitty" 45 | }, 46 | { 47 | src: "http://www.noiseaddicts.com/samples_1w72b820/3890.mp3", 48 | label: "Noise Addicts" 49 | } 50 | ]; 51 | -------------------------------------------------------------------------------- /example/range-mixins.scss: -------------------------------------------------------------------------------- 1 | /* INPUT[type='range'] Shadow Elements Selectors * 2 | * ============================================= */ 3 | $defaultBrowserList : webkit, moz, ms; 4 | 5 | // this function will be used in all this script. 6 | @function _contains(/* List */ $haystack, /* Any */ $needle) { 7 | @if index($haystack, $needle){ 8 | @return true; 9 | }@else{ 10 | @return false; 11 | } 12 | } 13 | 14 | // UGLY Browsers hacks here : 15 | // ( is there a better use for them ? ^_- ) 16 | @mixin browser-restrict-to ( $browserList... ){ 17 | @if( length($browserList) == 0 ){ 18 | @content; 19 | } 20 | @else{ 21 | @if ( _contains($browserList, webkit)){ 22 | // 23 | // http://browserhacks.com/#hack-f4ade0540d8e891e8190065f75acb186 24 | &:not(*:root) { @content; } 25 | } 26 | @if ( _contains($browserList, moz)){ 27 | // 28 | // http://browserhacks.com/#hack-8e9b5504d9fda44ec75169381b3c3157 29 | @supports (-moz-appearance:meterbar) { @content; } 30 | } 31 | @if ( _contains($browserList, ms)){ 32 | // http://browserhacks.com/#hack-f1070533535a12744a0381a75087a915 33 | @at-root { 34 | _:-ms-input-placeholder, 35 | :root & { @content; } 36 | } 37 | } 38 | } 39 | } 40 | 41 | 42 | @mixin -range-track ( $browserList... ) { 43 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 44 | 45 | @if ( _contains($browserList, webkit)){ 46 | &::-webkit-slider-runnable-track { @content; } 47 | } 48 | @if ( _contains($browserList, moz)){ 49 | &::-moz-range-track { @content; } 50 | } 51 | @if ( _contains($browserList, ms)){ 52 | &::-ms-track { @content; } 53 | } 54 | 55 | } 56 | 57 | 58 | @mixin -range-thumb ( $browserList... ) { 59 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 60 | 61 | @if ( _contains($browserList, webkit)){ 62 | &::-webkit-slider-thumb { @content; } 63 | } 64 | @if ( _contains($browserList, moz)){ 65 | &::-moz-range-thumb { @content; } 66 | } 67 | @if ( _contains($browserList, ms)){ 68 | &::-ms-thumb { @content; } 69 | } 70 | } 71 | 72 | @mixin -range-fill-lower ( $browserList... ) { 73 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 74 | 75 | @if ( _contains($browserList, webkit)){ 76 | &::-webkit-slider-thumb::before { @content; } 77 | } 78 | @if ( _contains($browserList, moz)){ 79 | &::-moz-range-progress { @content; } 80 | } 81 | @if ( _contains($browserList, ms)){ 82 | &::-ms-fill-lower { @content; } 83 | } 84 | } 85 | 86 | // Usefull only on webkit. 87 | @mixin -range-track-outline ( $browserList... ) { 88 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 89 | 90 | @if ( _contains($browserList, webkit)){ 91 | &::-webkit-slider-runnable-track::after { @content; } 92 | } 93 | } 94 | 95 | // I still didn't use them : 96 | @mixin -range-ticks ( $browserList... ) { 97 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 98 | 99 | @if ( _contains($browserList, ms)){ 100 | &::-ms-ticks-before { @content; } 101 | &::-ms-ticks-after { @content; } 102 | } 103 | } 104 | 105 | @mixin -range-tooltip ( $browserList... ) { 106 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 107 | 108 | @if ( _contains($browserList, ms)){ 109 | &::-ms-tooltip { @content; } 110 | } 111 | } 112 | 113 | 114 | @mixin -range-thumb__margin-top($margin){ 115 | @include -range-thumb( 'webkit' ) { 116 | margin-top: $margin; 117 | &::before { 118 | margin-top: ( $margin * -1 ); 119 | } 120 | } 121 | } 122 | 123 | @mixin -range__reset ( $browserList... ) { 124 | 125 | @include browser-restrict-to($browserList...){ 126 | $deep: "/deep/"; 127 | &, & #{unquote($deep)} * { 128 | &, &::before, &::after { 129 | box-sizing: border-box; 130 | } 131 | } 132 | overflow: hidden; 133 | } 134 | 135 | @if( length($browserList) == 0 ){ $browserList: $defaultBrowserList} 136 | 137 | @if ( _contains($browserList, webkit)) or ( _contains($browserList, moz)) { 138 | // Webkit/Gecko : 139 | @include browser-restrict-to($browserList...){ 140 | font-size: 1em; 141 | } 142 | } 143 | @if ( _contains($browserList, webkit)) { 144 | // Webkit reset : 145 | @include browser-restrict-to(webkit){ 146 | -webkit-appearance: none; 147 | &:focus { 148 | outline: none; 149 | } 150 | } 151 | } 152 | 153 | @include -range-track($browserList) { 154 | overflow: visible; 155 | } 156 | 157 | @if ( _contains($browserList, webkit)) { 158 | &::-webkit-slider-runnable-track { 159 | -webkit-appearance: none; 160 | position: relative; 161 | z-index: 1; 162 | 163 | &::after { 164 | content: ""; 165 | display: block; 166 | position: absolute; 167 | top: 0; left: 0; 168 | width: inherit; height: inherit; 169 | border-radius: inherit; 170 | z-index: -1; 171 | } 172 | } 173 | &::-webkit-slider-thumb { 174 | -webkit-appearance: none; 175 | position: relative; 176 | z-index: -1; 177 | &::before { 178 | content: ""; 179 | transform: translateX(-100%); 180 | display: block; 181 | z-index: -1; 182 | } 183 | } 184 | } 185 | 186 | @if ( _contains($browserList, moz)) { 187 | // Gecko reset : 188 | &::-moz-range-track { 189 | z-index: -1; 190 | } 191 | 192 | // remove dotted outline 193 | &::-moz-focus-outer { 194 | border: 0; 195 | } 196 | } 197 | 198 | @if ( _contains($browserList, ms)) { 199 | // IE reset : 200 | &::-ms-track { 201 | border: none; 202 | color: transparent; 203 | } 204 | 205 | // hide lower color 206 | &::-ms-fill-lower { 207 | background: transparent; 208 | } 209 | 210 | // remove tooltip 211 | &::-ms-tooltip { 212 | display: none; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-media-player", 3 | "version": "0.7.9", 4 | "description": "React media player.", 5 | "main": "lib/react-media-player.js", 6 | "files": [ 7 | "dist", 8 | "lib" 9 | ], 10 | "scripts": { 11 | "build:lib": "babel src --out-dir lib", 12 | "build": "npm run build:lib && NODE_ENV=production webpack --config webpack.prod.config.js", 13 | "dev": "webpack-dev-server --inline --hot --progress --colors --host 0.0.0.0 --devtool eval", 14 | "postbuild": "NODE_ENV=production TARGET=minify webpack --config webpack.prod.config.js", 15 | "prebuild": "rm -rf dist && mkdir dist", 16 | "prepublish": "npm run build", 17 | "deploy": "NODE_ENV=production TARGET=minify webpack && git-directory-deploy --directory example --branch gh-pages" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/souporserious/react-media-player" 22 | }, 23 | "keywords": [ 24 | "react", 25 | "component", 26 | "media", 27 | "audio", 28 | "video", 29 | "player", 30 | "playlist" 31 | ], 32 | "author": "Travis Arnold (http://souporserious.com)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/souporserious/react-media-player/issues" 36 | }, 37 | "homepage": "https://github.com/souporserious/react-media-player", 38 | "peerDependencies": { 39 | "react": "0.14.x || ^15.0.0 || ^16.0.0", 40 | "react-dom": "0.14.x || ^15.0.0 || ^16.0.0" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "^6.16.0", 44 | "babel-core": "^6.17.0", 45 | "babel-loader": "^6.2.5", 46 | "babel-plugin-add-module-exports": "^0.2.1", 47 | "babel-preset-es2015": "^6.16.0", 48 | "babel-preset-react": "^6.16.0", 49 | "babel-preset-stage-0": "^6.16.0", 50 | "chokidar": "^1.6.1", 51 | "css-loader": "^0.25.0", 52 | "git-directory-deploy": "^1.5.1", 53 | "http-server": "^0.9.0", 54 | "node-libs-browser": "^1.0.0", 55 | "node-sass": "^4.13.1", 56 | "postcss-loader": "^0.13.0", 57 | "react": "15.3.2", 58 | "react-dom": "15.3.2", 59 | "react-motion": "^0.4.2", 60 | "react-motion-ui-pack": "^0.10.2", 61 | "sass-loader": "^4.0.2", 62 | "style-loader": "^0.13.1", 63 | "webpack": "^1.13.2", 64 | "webpack-dev-server": "^1.9.0" 65 | }, 66 | "dependencies": { 67 | "prop-types": "^15.5.10" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Media.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Children } from 'react' 2 | import PropTypes from 'prop-types' 3 | import ReactDOM, { findDOMNode } from 'react-dom' 4 | import contextTypes from './context-types' 5 | import requestFullscreen from './utils/request-fullscreen' 6 | import exitFullscreen from './utils/exit-fullscreen' 7 | import fullscreenChange from './utils/fullscreen-change' 8 | 9 | const MEDIA_EVENTS = { 10 | onPlay: 'isPlaying', 11 | onPause: 'isPlaying', 12 | onDuration: 'duration', 13 | onProgress: 'progress', 14 | onTimeUpdate: 'currentTime', 15 | onMute: 'isMuted', 16 | onVolumeChange: 'volume', 17 | onError: null, 18 | } 19 | const MEDIA_EVENTS_KEYS = Object.keys(MEDIA_EVENTS) 20 | 21 | class Media extends Component { 22 | static propTypes = { 23 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, 24 | } 25 | 26 | static childContextTypes = contextTypes 27 | 28 | state = { 29 | currentTime: 0, 30 | progress: 0, 31 | duration: 0.1, 32 | volume: 1, 33 | isLoading: true, 34 | isPlaying: false, 35 | isMuted: false, 36 | isFullscreen: false, 37 | } 38 | 39 | _isMounted = false 40 | _playerProps = {} 41 | _lastVolume = 0 42 | 43 | getChildContext() { 44 | return { 45 | media: this._getPublicMediaProps(), 46 | _mediaSetters: { 47 | setPlayer: this._setPlayer, 48 | setPlayerProps: this._setPlayerProps, 49 | setPlayerState: this._setPlayerState, 50 | }, 51 | _mediaGetters: { 52 | getPlayerEvents: this._getPlayerEvents(), 53 | }, 54 | } 55 | } 56 | 57 | componentDidMount() { 58 | this._isMounted = true 59 | fullscreenChange('add', this._handleFullscreenChange) 60 | } 61 | 62 | componentWillUnmount() { 63 | this._isMounted = false 64 | fullscreenChange('remove', this._handleFullscreenChange) 65 | } 66 | 67 | _getPublicMediaProps() { 68 | return { 69 | ...this.state, 70 | play: this.play, 71 | pause: this.pause, 72 | playPause: this.playPause, 73 | stop: this.stop, 74 | seekTo: this.seekTo, 75 | skipTime: this.skipTime, 76 | mute: this.mute, 77 | muteUnmute: this.muteUnmute, 78 | setVolume: this.setVolume, 79 | addVolume: this.addVolume, 80 | fullscreen: this.fullscreen, 81 | } 82 | } 83 | 84 | _getPlayerEvents() { 85 | const events = {} 86 | MEDIA_EVENTS_KEYS.forEach(key => { 87 | const stateKey = MEDIA_EVENTS[key] 88 | const handlePropCallback = () => { 89 | const propCallback = this._playerProps[key] 90 | if (typeof propCallback === 'function') { 91 | propCallback(this.state) 92 | } 93 | } 94 | events[key] = val => { 95 | if (stateKey) { 96 | if (this._isMounted) { 97 | this.setState({ [stateKey]: val }, handlePropCallback) 98 | } 99 | } else { 100 | handlePropCallback() 101 | } 102 | } 103 | }) 104 | return events 105 | } 106 | 107 | _setPlayer = component => { 108 | this._player = component 109 | } 110 | 111 | _setPlayerProps = props => { 112 | this._playerProps = props 113 | } 114 | 115 | _setPlayerState = state => { 116 | this.setState(state) 117 | } 118 | 119 | play = () => { 120 | return this._player.play() 121 | } 122 | 123 | pause = () => { 124 | this._player.pause() 125 | } 126 | 127 | playPause = () => { 128 | if (!this.state.isPlaying) { 129 | return this.play() 130 | } else { 131 | this.pause() 132 | } 133 | } 134 | 135 | stop = () => { 136 | this._player.stop() 137 | } 138 | 139 | seekTo = currentTime => { 140 | this._player.seekTo(currentTime) 141 | this.setState({ currentTime }) 142 | } 143 | 144 | skipTime = amount => { 145 | const { currentTime, duration } = this.state 146 | let newTime = currentTime + amount 147 | if (newTime < 0) { 148 | newTime = 0 149 | } else if (newTime > duration) { 150 | newTime = duration 151 | } 152 | this.seekTo(newTime) 153 | } 154 | 155 | mute = isMuted => { 156 | if (isMuted) { 157 | this._lastVolume = this.state.volume 158 | this._player.setVolume(0) 159 | } else { 160 | const volume = this._lastVolume > 0 ? this._lastVolume : 0.1 161 | this._player.setVolume(volume) 162 | } 163 | this._player.mute(isMuted) 164 | } 165 | 166 | muteUnmute = () => { 167 | this.mute(!this.state.isMuted) 168 | } 169 | 170 | setVolume = volume => { 171 | const isMuted = volume <= 0 172 | if (isMuted !== this.state.isMuted) { 173 | this.mute(isMuted) 174 | } else { 175 | this._lastVolume = volume 176 | } 177 | this._player.setVolume(volume) 178 | } 179 | 180 | addVolume = amount => { 181 | let newVolume = this.state.volume + amount * 0.01 182 | if (newVolume < 0) { 183 | newVolume = 0 184 | } else if (newVolume > 1) { 185 | newVolume = 1 186 | } 187 | this.setVolume(newVolume) 188 | } 189 | 190 | fullscreen = () => { 191 | if (!this.state.isFullscreen) { 192 | this._player.node[requestFullscreen]() 193 | } else { 194 | document[exitFullscreen]() 195 | } 196 | } 197 | 198 | _handleFullscreenChange = ({ target }) => { 199 | if (target === this._player.node) { 200 | this.setState({ isFullscreen: !this.state.isFullscreen }) 201 | } 202 | } 203 | 204 | render() { 205 | const { children } = this.props 206 | if (typeof children === 'function') { 207 | return children(this._getPublicMediaProps()) 208 | } 209 | return Children.only(children) 210 | } 211 | } 212 | 213 | export default Media 214 | -------------------------------------------------------------------------------- /src/Player.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createElement } from 'react' 2 | import PropTypes from 'prop-types' 3 | import contextTypes from './context-types' 4 | import getVendor from './utils/get-vendor' 5 | 6 | const defaultConfig = { 7 | youtube: {}, 8 | vimeo: {}, 9 | html5: {}, 10 | } 11 | 12 | class Player extends Component { 13 | static propTypes = { 14 | vendor: PropTypes.oneOf(['video', 'audio', 'youtube', 'vimeo']), 15 | defaultCurrentTime: PropTypes.number, 16 | defaultVolume: PropTypes.number, 17 | defaultMuted: PropTypes.bool, 18 | } 19 | 20 | static defaultProps = { 21 | defaultCurrentTime: 0, 22 | defaultVolume: 1, 23 | defaultMuted: false, 24 | } 25 | 26 | static contextTypes = contextTypes 27 | 28 | _defaultsSet = false 29 | 30 | componentWillMount() { 31 | const { 32 | defaultCurrentTime, 33 | defaultMuted, 34 | defaultVolume, 35 | ...restProps 36 | } = this.props 37 | 38 | this._setPlayerProps({ volume: defaultVolume, ...restProps }) 39 | 40 | this._setPlayerState({ 41 | currentTime: defaultCurrentTime, 42 | volume: defaultMuted ? 0 : defaultVolume, 43 | }) 44 | 45 | // we need to unset the loading state if no source was loaded 46 | if (!this.props.src) { 47 | this._setLoading(false) 48 | } 49 | } 50 | 51 | componentWillUpdate(nextProps) { 52 | this._setPlayerProps(nextProps) 53 | 54 | // clean state if the media source has changed 55 | if (this.props.src !== nextProps.src) { 56 | this._setPlayerState({ 57 | currentTime: 0, 58 | progress: 0, 59 | duration: 0, 60 | isLoading: true, 61 | isPlaying: false, 62 | }) 63 | } 64 | } 65 | 66 | get instance() { 67 | return this._component && this._component.instance 68 | } 69 | 70 | _setPlayer = component => { 71 | this.context._mediaSetters.setPlayer(component) 72 | this._component = component 73 | } 74 | 75 | _setPlayerProps(props) { 76 | this.context._mediaSetters.setPlayerProps(props) 77 | } 78 | 79 | _setPlayerState(state) { 80 | this.context._mediaSetters.setPlayerState(state) 81 | } 82 | 83 | _setDefaults() { 84 | const { media } = this.context 85 | const { defaultCurrentTime, defaultVolume, defaultMuted } = this.props 86 | if (defaultCurrentTime > 0) { 87 | media.seekTo(defaultCurrentTime) 88 | } 89 | if (defaultMuted) { 90 | media.mute(defaultMuted) 91 | } else if (defaultVolume !== 1) { 92 | media.setVolume(defaultVolume) 93 | } 94 | this._defaultsSet = true 95 | } 96 | 97 | _setLoading = isLoading => { 98 | this.context._mediaSetters.setPlayerState({ isLoading }) 99 | } 100 | 101 | _handleOnReady = () => { 102 | const { media, _mediaSetters } = this.context 103 | const { autoPlay, onReady } = this.props 104 | 105 | if (!this._defaultsSet) { 106 | this._setDefaults() 107 | } else { 108 | media.mute(media.isMuted) 109 | media.setVolume(media.volume) 110 | } 111 | 112 | if (autoPlay) { 113 | media.play() 114 | } 115 | 116 | this._setLoading(false) 117 | 118 | if (typeof onReady === 'function') { 119 | onReady(media) 120 | } 121 | } 122 | 123 | _handleOnEnded = () => { 124 | const { media, _mediaSetters } = this.context 125 | const { loop, onEnded } = this.props 126 | if (loop) { 127 | media.seekTo(0) 128 | media.play() 129 | } else { 130 | _mediaSetters.setPlayerState({ isPlaying: false }) 131 | } 132 | if (typeof onEnded === 'function') { 133 | onEnded(media) 134 | } 135 | } 136 | 137 | render() { 138 | const { 139 | src, 140 | vendor: _vendor, 141 | config, 142 | autoPlay, 143 | onReady, 144 | onEnded, 145 | defaultCurrentTime, 146 | defaultVolume, 147 | defaultMuted, 148 | ...extraProps 149 | } = this.props 150 | const { vendor, component } = getVendor(src, _vendor) 151 | return createElement(component, { 152 | src, 153 | vendor, 154 | autoPlay, 155 | config: { ...defaultConfig, ...config }, 156 | extraProps, 157 | ref: this._setPlayer, 158 | isLoading: this._setLoading, 159 | onReady: this._handleOnReady, 160 | onEnded: this._handleOnEnded, 161 | ...this.context._mediaGetters.getPlayerEvents, 162 | }) 163 | } 164 | } 165 | 166 | export default Player 167 | -------------------------------------------------------------------------------- /src/context-types.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | export default { 4 | media: PropTypes.object, 5 | _mediaSetters: PropTypes.object, 6 | _mediaGetters: PropTypes.object 7 | }; 8 | -------------------------------------------------------------------------------- /src/controls/CurrentTime.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | import formatTime from '../utils/format-time' 5 | 6 | class CurrentTime extends Component { 7 | shouldComponentUpdate({ media }) { 8 | return this.props.media.currentTime !== media.currentTime 9 | } 10 | 11 | render() { 12 | const { className, style, media } = this.props 13 | return ( 14 | 17 | ) 18 | } 19 | } 20 | 21 | export default withMediaProps(CurrentTime) 22 | -------------------------------------------------------------------------------- /src/controls/Duration.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | import formatTime from '../utils/format-time' 5 | 6 | class Duration extends Component { 7 | shouldComponentUpdate({ media }) { 8 | return this.props.media.duration !== media.duration 9 | } 10 | 11 | render() { 12 | const { className, style, media } = this.props 13 | return ( 14 | 17 | ) 18 | } 19 | } 20 | 21 | export default withMediaProps(Duration) 22 | -------------------------------------------------------------------------------- /src/controls/Fullscreen.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | 5 | class Fullscreen extends Component { 6 | shouldComponentUpdate({ media }) { 7 | return this.props.media.isFullscreen !== media.isFullscreen 8 | } 9 | 10 | _handleFullscreen = () => { 11 | this.props.media.fullscreen() 12 | } 13 | 14 | render() { 15 | const { className, style, media } = this.props 16 | return ( 17 | 25 | ) 26 | } 27 | } 28 | 29 | export default withMediaProps(Fullscreen) 30 | -------------------------------------------------------------------------------- /src/controls/MuteUnmute.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | 5 | class MuteUnmute extends Component { 6 | shouldComponentUpdate({ media }) { 7 | return this.props.media.isMuted !== media.isMuted 8 | } 9 | 10 | _handleMuteUnmute = () => { 11 | this.props.media.muteUnmute() 12 | } 13 | 14 | render() { 15 | const { className, style, media } = this.props 16 | return ( 17 | 25 | ) 26 | } 27 | } 28 | 29 | export default withMediaProps(MuteUnmute) 30 | -------------------------------------------------------------------------------- /src/controls/PlayPause.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | 5 | class PlayPause extends Component { 6 | shouldComponentUpdate({ media }) { 7 | return this.props.media.isPlaying !== media.isPlaying 8 | } 9 | 10 | _handlePlayPause = () => { 11 | this.props.media.playPause() 12 | } 13 | 14 | render() { 15 | const { className, style, media } = this.props 16 | return ( 17 | 25 | ) 26 | } 27 | } 28 | 29 | export default withMediaProps(PlayPause) 30 | -------------------------------------------------------------------------------- /src/controls/Progress.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | 5 | class Progress extends Component { 6 | shouldComponentUpdate({ media }) { 7 | return this.props.media.progress !== media.progress 8 | } 9 | 10 | render() { 11 | const { className, style, media } = this.props 12 | return ( 13 | 19 | ) 20 | } 21 | } 22 | 23 | export default withMediaProps(Progress) 24 | -------------------------------------------------------------------------------- /src/controls/SeekBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | 5 | class SeekBar extends Component { 6 | _isPlayingOnMouseDown = false 7 | _onChangeUsed = false 8 | 9 | shouldComponentUpdate({ media }) { 10 | return ( 11 | this.props.media.currentTime !== media.currentTime || 12 | this.props.media.duration !== media.duration 13 | ) 14 | } 15 | 16 | _handleMouseDown = () => { 17 | this._isPlayingOnMouseDown = this.props.media.isPlaying 18 | this.props.media.pause() 19 | } 20 | 21 | _handleMouseUp = ({ target: { value } }) => { 22 | // seek on mouseUp as well because of this bug in <= IE11 23 | // https://github.com/facebook/react/issues/554 24 | if (!this._onChangeUsed) { 25 | this.props.media.seekTo(+value) 26 | } 27 | 28 | // only play if media was playing prior to mouseDown 29 | if (this._isPlayingOnMouseDown) { 30 | this.props.media.play() 31 | } 32 | } 33 | 34 | _handleChange = ({ target: { value } }) => { 35 | this.props.media.seekTo(+value) 36 | this._onChangeUsed = true 37 | } 38 | 39 | render() { 40 | const { className, style, media } = this.props 41 | const { duration, currentTime } = media 42 | return ( 43 | 57 | ) 58 | } 59 | } 60 | 61 | export default withMediaProps(SeekBar) 62 | -------------------------------------------------------------------------------- /src/controls/Volume.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import withMediaProps from '../decorators/with-media-props' 4 | 5 | class Volume extends Component { 6 | _onChangeUsed = false 7 | 8 | shouldComponentUpdate({ media }) { 9 | return this.props.media.volume !== media.volume 10 | } 11 | 12 | _handleMouseUp = ({ target: { value } }) => { 13 | // set volume on mouseUp as well because of this bug in <= IE11 14 | // https://github.com/facebook/react/issues/554 15 | if (!this._onChangeUsed) { 16 | this.props.media.setVolume((+value).toFixed(4)) 17 | } 18 | } 19 | 20 | _handleChange = ({ target: { value } }) => { 21 | this.props.media.setVolume((+value).toFixed(4)) 22 | this._onChangeUsed = true 23 | } 24 | 25 | render() { 26 | const { className, style, media } = this.props 27 | const { volume } = media 28 | return ( 29 | 43 | ) 44 | } 45 | } 46 | 47 | export default withMediaProps(Volume) 48 | -------------------------------------------------------------------------------- /src/controls/index.js: -------------------------------------------------------------------------------- 1 | export PlayPause from "./PlayPause"; 2 | export CurrentTime from "./CurrentTime"; 3 | export Progress from "./Progress"; 4 | export SeekBar from "./SeekBar"; 5 | export Duration from "./Duration"; 6 | export MuteUnmute from "./MuteUnmute"; 7 | export Volume from "./Volume"; 8 | export Fullscreen from "./Fullscreen"; 9 | -------------------------------------------------------------------------------- /src/decorators/with-media-props.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import contextTypes from '../context-types' 3 | 4 | export default function withMediaProps(MediaComponent) { 5 | return class extends Component { 6 | static displayName = 'withMediaProps' 7 | 8 | static contextTypes = contextTypes 9 | 10 | render() { 11 | return 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/react-media-player.js: -------------------------------------------------------------------------------- 1 | export Media from "./Media"; 2 | export Player from "./Player"; 3 | export withMediaProps from "./decorators/with-media-props"; 4 | export * as controls from "./controls"; 5 | export * as utils from "./utils"; 6 | -------------------------------------------------------------------------------- /src/utils/exit-fullscreen.js: -------------------------------------------------------------------------------- 1 | export default (() => { 2 | if (typeof document === "undefined") { 3 | return () => {}; 4 | } 5 | 6 | const names = [ 7 | "exitFullscreen", 8 | "mozCancelFullScreen", 9 | "msExitFullscreen", 10 | "webkitExitFullscreen" 11 | ]; 12 | return names.reduce((prev, curr) => (document[curr] ? curr : prev)); 13 | })(); 14 | -------------------------------------------------------------------------------- /src/utils/format-time.js: -------------------------------------------------------------------------------- 1 | export default function formatTime(current) { 2 | let h = Math.floor(current / 3600); 3 | let m = Math.floor((current - h * 3600) / 60); 4 | let s = Math.floor(current % 60); 5 | 6 | if (s < 10) { 7 | s = "0" + s; 8 | } 9 | 10 | if (h > 0) { 11 | m = m < 10 ? `0${m}` : m; 12 | return h + ":" + m + ":" + s; 13 | } else { 14 | return m + ":" + s; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/fullscreen-change.js: -------------------------------------------------------------------------------- 1 | export default function fullscreenChange(type, callback) { 2 | const vendors = [ 3 | "fullscreenchange", 4 | "mozfullscreenchange", 5 | "MSFullscreenChange", 6 | "webkitfullscreenchange" 7 | ]; 8 | vendors.forEach(vendor => document[`${type}EventListener`](vendor, callback)); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/get-vendor.js: -------------------------------------------------------------------------------- 1 | import HTML5 from "../vendors/HTML5"; 2 | import Vimeo from "../vendors/Vimeo"; 3 | import Youtube from "../vendors/Youtube"; 4 | 5 | export default function getVendor(src, vendor) { 6 | src = src || ""; 7 | if (vendor === "youtube" || /youtube.com|youtu.be/.test(src)) { 8 | return { vendor: "youtube", component: Youtube }; 9 | } else if (vendor === "vimeo" || /vimeo.com/.test(src)) { 10 | return { vendor: "vimeo", component: Vimeo }; 11 | } else { 12 | const isAudio = vendor === "audio" || /\.(mp3|wav|m4a)($|\?)/i.test(src); 13 | return { 14 | vendor: isAudio ? "audio" : "video", 15 | component: HTML5 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/get-vimeo-id.js: -------------------------------------------------------------------------------- 1 | // Thanks to: http://stackoverflow.com/a/13286930/1461204 2 | export default function getVimeoId(url) { 3 | const regExp = /https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/; 4 | const match = url.match(regExp); 5 | 6 | if (match) { 7 | return match[3]; 8 | } else { 9 | throw "Invalid Vimeo ID provided"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/get-youtube-id.js: -------------------------------------------------------------------------------- 1 | export default function getYoutubeId(url) { 2 | const regExp = /.*(?:youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=)([^#\&\?]*).*/; 3 | const match = url.match(regExp); 4 | 5 | if (match && match[1].length === 11) { 6 | return match[1]; 7 | } else { 8 | throw "Invalid Youtube ID provided"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export formatTime from './format-time' 2 | export getVendor from './get-vendor' 3 | export keyboardControls from './keyboard-controls' 4 | -------------------------------------------------------------------------------- /src/utils/keyboard-controls.jsx: -------------------------------------------------------------------------------- 1 | const MEDIA_KEYS = [ 2 | 0, 3 | 'f', 4 | 'j', 5 | 'k', 6 | 'l', 7 | ',', 8 | '.', 9 | ' ', 10 | 'Home', 11 | 'End', 12 | 'ArrowLeft', 13 | 'ArrowTop', 14 | 'ArrowRight', 15 | 'ArrowDown', 16 | ] 17 | 18 | export default function keyboardControls(mediaProps, e) { 19 | const { 20 | duration, 21 | playPause, 22 | seekTo, 23 | skipTime, 24 | addVolume, 25 | fullscreen, 26 | } = mediaProps 27 | const { key, shiftKey } = e 28 | 29 | // prevent default on any media keys 30 | MEDIA_KEYS.some(_key => _key === key && e.preventDefault()) 31 | 32 | // handle respective key controls 33 | switch (key) { 34 | // Play/Pause 35 | case ' ': 36 | case 'k': 37 | playPause() 38 | break 39 | 40 | // Seeking Backwards 41 | case 'ArrowLeft': 42 | skipTime(shiftKey ? -10 : -5) 43 | break 44 | case 'j': 45 | skipTime(shiftKey ? -10 : -5) 46 | break 47 | case ',': 48 | skipTime(-1) 49 | break 50 | 51 | // Seeking Forwards 52 | case 'ArrowRight': 53 | skipTime(shiftKey ? 10 : 5) 54 | break 55 | case 'l': 56 | skipTime(shiftKey ? 10 : 5) 57 | break 58 | case '.': 59 | skipTime(1) 60 | break 61 | case 0: 62 | case 'Home': 63 | seekTo(0) 64 | break 65 | case 'End': 66 | seekTo(duration) 67 | break 68 | 69 | // Volume 70 | case 'ArrowUp': 71 | addVolume(shiftKey ? 10 : 5) 72 | break 73 | case 'ArrowDown': 74 | addVolume(shiftKey ? -10 : -5) 75 | break 76 | 77 | // Fullscreen 78 | case 'f': 79 | fullscreen() 80 | break 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/load-api.js: -------------------------------------------------------------------------------- 1 | // load api asynchronously 2 | export default function loadAPI(url, cb) { 3 | // create script to be injected 4 | const script = document.createElement("script"); 5 | 6 | // load async 7 | script.async = true; 8 | 9 | // set source to vendors api 10 | script.src = url; 11 | 12 | // callback after load 13 | script.onload = () => typeof cb === "function" && cb(); 14 | 15 | // append script to document head 16 | document.head.appendChild(script); 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/request-fullscreen.js: -------------------------------------------------------------------------------- 1 | export default (() => { 2 | if (typeof document === "undefined") { 3 | return () => {}; 4 | } 5 | 6 | const names = [ 7 | "requestFullscreen", 8 | "mozRequestFullScreen", 9 | "msRequestFullscreen", 10 | "webkitRequestFullscreen" 11 | ]; 12 | return names.reduce( 13 | (prev, curr) => (document.documentElement[curr] ? curr : prev) 14 | ); 15 | })(); 16 | -------------------------------------------------------------------------------- /src/utils/youtube-api-loader.js: -------------------------------------------------------------------------------- 1 | import loadAPI from "./load-api"; 2 | 3 | export default { 4 | _queue: [], 5 | _isLoaded: false, 6 | 7 | load: function(component) { 8 | // if the API is loaded just create the player 9 | if (this._isLoaded) { 10 | component._createPlayer(); 11 | } else { 12 | this._queue.push(component); 13 | 14 | // load the Youtube API if this was the first component added 15 | if (this._queue.length === 1) { 16 | this._loadAPI(); 17 | } 18 | } 19 | }, 20 | 21 | _loadAPI: function() { 22 | loadAPI("//youtube.com/player_api"); 23 | 24 | window.onYouTubeIframeAPIReady = () => { 25 | this._isLoaded = true; 26 | for (let i = this._queue.length; i--; ) { 27 | this._queue[i]._createPlayer(); 28 | } 29 | this._queue = []; 30 | }; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/vendors/HTML5.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, createElement } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { findDOMNode } from 'react-dom' 4 | import vendorPropTypes from './vendor-prop-types' 5 | 6 | const AudioContext = typeof window === 'undefined' ? false : (window.AudioContext || window.webkitAudioContext) 7 | let audioContext 8 | 9 | if (AudioContext) { 10 | audioContext = new AudioContext() 11 | } 12 | 13 | class HTML5 extends Component { 14 | static propTypes = { 15 | ...vendorPropTypes, 16 | useAudioObject: PropTypes.bool, 17 | } 18 | 19 | get instance() { 20 | return this._player 21 | } 22 | 23 | get node() { 24 | return findDOMNode(this._player) 25 | } 26 | 27 | componentDidMount() { 28 | const { connectSource, useAudioObject } = this.props.extraProps 29 | if (this.props.vendor === 'audio') { 30 | if (useAudioObject) { 31 | this._createAudioObject() 32 | this._bindAudioObjectEvents() 33 | } 34 | if (audioContext && connectSource) { 35 | this._connectAudioContext() 36 | } 37 | } 38 | } 39 | 40 | componentDidUpdate(lastProps) { 41 | const { connectSource, useAudioObject } = this.props.extraProps 42 | const vendorChanged = this.props.vendor !== lastProps.vendor 43 | const sourceChanged = this.props.src !== lastProps.src 44 | if (useAudioObject) { 45 | if (vendorChanged) { 46 | this._createAudioObject() 47 | } else if (sourceChanged) { 48 | this._destroyAudioObject() 49 | this._createAudioObject() 50 | } 51 | this._bindAudioObjectEvents() 52 | } 53 | if (this.props.vendor === 'audio' && audioContext && connectSource) { 54 | if (vendorChanged) { 55 | this._connectAudioContext() 56 | } else if (sourceChanged) { 57 | this._disconnectAudioContext() 58 | this._connectAudioContext() 59 | } 60 | } 61 | } 62 | 63 | componentWillUnmount() { 64 | const { connectSource, useAudioObject } = this.props.extraProps 65 | if (audioContext && connectSource) { 66 | this._disconnectAudioContext() 67 | } 68 | if (useAudioObject) { 69 | this._destroyAudioObject() 70 | } 71 | } 72 | 73 | play() { 74 | if (audioContext && audioContext.state === 'suspended') { 75 | audioContext.resume() 76 | } 77 | return this._player.play() 78 | } 79 | 80 | pause() { 81 | this._player.pause() 82 | } 83 | 84 | stop() { 85 | this._player.pause() 86 | this._player.currentTime = 0 87 | } 88 | 89 | seekTo(currentTime) { 90 | if (this._player.readyState > 0) { 91 | this._player.currentTime = currentTime 92 | } 93 | } 94 | 95 | mute(muted) { 96 | const nextVolume = muted ? 0 : 1 97 | this._player.muted = muted 98 | this.setVolume(nextVolume) 99 | this.props.onMute(muted) 100 | this.props.onVolumeChange(nextVolume) 101 | } 102 | 103 | setVolume(volume) { 104 | if (this._gain) { 105 | this._gain.gain.value = volume 106 | } else { 107 | this._player.volume = volume 108 | } 109 | this.props.onVolumeChange(volume) 110 | } 111 | 112 | get _playerEvents() { 113 | return { 114 | onCanPlay: this._handleCanPlay, 115 | onPlay: this._handlePlay, 116 | onPlaying: this._isNotLoading, 117 | onPause: this._handlePause, 118 | onEnded: this._handleEnded, 119 | onWaiting: this._isLoading, 120 | onError: this._handleError, 121 | onProgress: this._handleProgress, 122 | onLoadedMetadata: this._handleDuration, 123 | onTimeUpdate: this._handleTimeUpdate, 124 | } 125 | } 126 | 127 | _createAudioObject() { 128 | this._player = new Audio(this.props.src) 129 | } 130 | 131 | _destroyAudioObject() { 132 | this.stop() 133 | this._player.src = 'about:blank' 134 | this._playerStopped = true 135 | } 136 | 137 | _bindAudioObjectEvents() { 138 | const { 139 | connectSource, 140 | useAudioObject, 141 | ...playerProps 142 | } = this.props.extraProps 143 | const playerEvents = this._playerEvents 144 | Object.keys(playerProps).forEach(key => { 145 | this._player[key] = playerProps[key] 146 | }) 147 | Object.keys(playerEvents).forEach(key => { 148 | this._player[key.toLowerCase()] = playerEvents[key] 149 | }) 150 | } 151 | 152 | _connectAudioContext() { 153 | const { connectSource, useAudioObject } = this.props.extraProps 154 | if (useAudioObject || !this._source) { 155 | this._source = audioContext.createMediaElementSource( 156 | useAudioObject ? this.instance : this.node 157 | ) 158 | } 159 | this._gain = audioContext.createGain() 160 | connectSource(this._source, audioContext).connect(this._gain) 161 | this._gain.connect(audioContext.destination) 162 | } 163 | 164 | _disconnectAudioContext() { 165 | this._source.disconnect(0) 166 | } 167 | 168 | _isLoading = () => { 169 | this.props.isLoading(true) 170 | } 171 | 172 | _isNotLoading = () => { 173 | this.props.isLoading(false) 174 | } 175 | 176 | _handleCanPlay = () => { 177 | this.props.onReady() 178 | } 179 | 180 | _handlePlay = () => { 181 | this.props.onPlay(true) 182 | this._isNotLoading() 183 | } 184 | 185 | _handlePause = () => { 186 | this.props.onPause(false) 187 | } 188 | 189 | _handleEnded = () => { 190 | this.props.onEnded(false) 191 | } 192 | 193 | _handleError = e => { 194 | if (this._playerStopped) { 195 | this._playerStopped = false 196 | } else { 197 | this.props.onError(e) 198 | this._isNotLoading() 199 | } 200 | } 201 | 202 | _handleProgress = ({ target: { buffered, duration } }) => { 203 | let progress = 0 204 | 205 | if (duration > 0 && buffered.length) { 206 | progress = buffered.end(buffered.length - 1) / duration 207 | } 208 | 209 | this.props.onProgress(progress) 210 | } 211 | 212 | _handleDuration = ({ target: { duration } }) => { 213 | this.props.onDuration(duration) 214 | } 215 | 216 | _handleTimeUpdate = ({ target: { currentTime } }) => { 217 | this.props.onTimeUpdate(currentTime) 218 | } 219 | 220 | render() { 221 | const { 222 | vendor, 223 | src, 224 | extraProps: { connectSource, useAudioObject, ...playerProps }, 225 | } = this.props 226 | return useAudioObject 227 | ? null 228 | : createElement(vendor, { 229 | ref: c => (this._player = c), 230 | src, 231 | ...playerProps, 232 | ...this._playerEvents, 233 | }) 234 | } 235 | } 236 | 237 | export default HTML5 238 | -------------------------------------------------------------------------------- /src/vendors/Vimeo.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { findDOMNode } from 'react-dom' 3 | import getVimeoId from '../utils/get-vimeo-id' 4 | import vendorPropTypes from './vendor-prop-types' 5 | 6 | class Vimeo extends Component { 7 | static propTypes = vendorPropTypes 8 | 9 | _vimeoId = getVimeoId(this.props.src) 10 | _origin = '*' 11 | 12 | componentDidMount() { 13 | window.addEventListener('message', this._onMessage) 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | if (nextProps.src !== this.props.src) { 18 | this._vimeoId = getVimeoId(nextProps.src) 19 | } 20 | } 21 | 22 | componentWillUnmount() { 23 | window.removeEventListener('message', this._onMessage) 24 | } 25 | 26 | get instance() { 27 | return this._iframe 28 | } 29 | 30 | get node() { 31 | return findDOMNode(this._iframe) 32 | } 33 | 34 | _onMessage = e => { 35 | let data 36 | 37 | // allow messages from the Vimeo player only 38 | if (!/^https?:\/\/player.vimeo.com/.test(e.origin)) { 39 | return false 40 | } 41 | 42 | if (this._origin === '*') { 43 | this._origin = e.origin 44 | } 45 | 46 | try { 47 | data = JSON.parse(e.data) 48 | } catch (err) { 49 | this.props.onError(err) 50 | } 51 | 52 | if (data) { 53 | switch (data.event) { 54 | case 'ready': 55 | this._postOnReadyMessages() 56 | break 57 | case 'loadProgress': 58 | this.props.onProgress(data.data.percent) 59 | break 60 | case 'playProgress': 61 | this.props.onTimeUpdate(data.data.seconds) 62 | break 63 | case 'play': 64 | this.props.onPlay(true) 65 | break 66 | case 'pause': 67 | this.props.onPause(false) 68 | break 69 | case 'finish': 70 | this.props.onEnded(false) 71 | break 72 | } 73 | if (data.method === 'getDuration') { 74 | this.props.onDuration(data.value) 75 | } 76 | } 77 | } 78 | 79 | _postMessage(method, value) { 80 | const data = { method } 81 | 82 | if (value) { 83 | data.value = value 84 | } 85 | 86 | this._iframe.contentWindow.postMessage(JSON.stringify(data), this._origin) 87 | } 88 | 89 | _postOnReadyMessages() { 90 | ;['loadProgress', 'playProgress', 'play', 'pause', 'finish'].forEach( 91 | listener => this._postMessage('addEventListener', listener) 92 | ) 93 | this._postMessage('getDuration') 94 | this.props.onReady() 95 | } 96 | 97 | play() { 98 | this._postMessage('play') 99 | } 100 | 101 | pause() { 102 | this._postMessage('pause') 103 | } 104 | 105 | stop() { 106 | this._postMessage('unload') 107 | } 108 | 109 | seekTo(currentTime) { 110 | this._postMessage('seekTo', currentTime) 111 | } 112 | 113 | mute(muted) { 114 | this._postMessage('setVolume', muted ? '0' : '1') 115 | this.props.onMute(muted) 116 | this.props.onVolumeChange(muted ? 0 : 1) 117 | } 118 | 119 | setVolume(volume) { 120 | this._postMessage('setVolume', volume) 121 | this.props.onVolumeChange(+volume) 122 | } 123 | 124 | render() { 125 | return ( 126 |