diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fe1e89..2e75e6da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. This projec ### Unreleased +* Fix [YouTube and Vimeo autoplay bug](https://github.com/CookPete/react-player/issues/7) * [Full commit list](https://github.com/CookPete/react-player/compare/v0.2.1...master) diff --git a/README.md b/README.md index 25f63b2f..1a4e2d49 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,13 @@ These props allow you to override the parameters for the various players Prop | Description ---- | ----------- -soundcloudConfig | An object containing configuration for the SoundCloud player. Includes `clientId`, which can be used to override the default `client_id` -vimeoConfig | An object containing configuration for the Vimeo player. Includes `iframeParams`, which maps to the [parameters accepted by the Vimeo iframe player](https://developer.vimeo.com/player/embedding#universal-parameters) -youtubeConfig | An object containing configuration for the YouTube player. Includes `playerVars`, which maps to the [parameters accepted by the YouTube iframe player](https://developers.google.com/youtube/player_parameters?playerVersion=HTML5) +soundcloudConfig | Configuration object for the SoundCloud player. Set `clientId`, to your own SoundCloud app [client ID](https://soundcloud.com/you/apps) +vimeoConfig | Configuration object for the Vimeo player. Set `iframeParams`, to override the [default params](https://developer.vimeo.com/player/embedding#universal-parameters). Set `preload` for [preloading](#preloading) +youtubeConfig | Configuration object for the YouTube player. Set `playerVars`, to override the [default player vars](https://developers.google.com/youtube/player_parameters?playerVersion=HTML5). Set `preload` for [preloading](#preloading) + +##### Preloading + +Both `youtubeConfig` and `vimeoConfig` props can take a `preload` value. Setting this to `true` will play a short, silent video in the background when `ReactPlayer` first mounts. This fixes a [bug](https://github.com/CookPete/react-player/issues/7) where videos would not play when loaded in a background browser tab. ### Methods diff --git a/src/ReactPlayer.js b/src/ReactPlayer.js index 64879f9c..0c74a291 100644 --- a/src/ReactPlayer.js +++ b/src/ReactPlayer.js @@ -1,35 +1,29 @@ import React, { Component } from 'react' import 'array.prototype.find' -import propTypes from './propTypes' +import { propTypes, defaultProps } from './props' import players from './players' +const PROGRESS_FREQUENCY = 500 + export default class ReactPlayer extends Component { static propTypes = propTypes - static defaultProps = { - volume: 0.8, - width: 640, - height: 360, - onPlay: function () {}, // TODO: Empty func var in react? - onPause: function () {}, - onBuffer: function () {}, - onEnded: function () {} - } + static defaultProps = defaultProps static canPlay (url) { return players.some(player => player.canPlay(url)) } - state = { - Player: this.getPlayer(this.props.url) + componentDidMount () { + this.progress() } - componentWillReceiveProps (nextProps) { - if (this.props.url !== nextProps.url) { - this.setState({ - Player: this.getPlayer(nextProps.url) - }) - } + componentWillUnmount () { + clearTimeout(this.progressTimeout) } - getPlayer (url) { - return players.find(Player => Player.canPlay(url)) + shouldComponentUpdate (nextProps) { + return ( + this.props.url !== nextProps.url || + this.props.playing !== nextProps.playing || + this.props.volume !== nextProps.volume + ) } seekTo = fraction => { const player = this.refs.player @@ -37,15 +31,45 @@ export default class ReactPlayer extends Component { player.seekTo(fraction) } } + progress = () => { + if (this.props.url && this.refs.player) { + let progress = {} + const loaded = this.refs.player.getFractionLoaded() + const played = this.refs.player.getFractionPlayed() + if (!this.prevLoaded || loaded !== this.prevLoaded) { + progress.loaded = this.prevLoaded = loaded + } + if (!this.prevPlayed || played !== this.prevPlayed) { + progress.played = this.prevPlayed = played + } + if (progress.loaded || progress.played) { + this.props.onProgress(progress) + } + } + this.progressTimeout = setTimeout(this.progress, PROGRESS_FREQUENCY) + } + renderPlayer = Player => { + const active = Player.canPlay(this.props.url) + const { youtubeConfig, soundcloudConfig, vimeoConfig, ...activeProps } = this.props + const props = active ? { ...activeProps, ref: 'player' } : {} + return ( + + ) + } render () { - const Player = this.state.Player const style = { width: this.props.width, height: this.props.height } return (
- { Player && } + { players.map(this.renderPlayer) }
) } diff --git a/src/players/Base.js b/src/players/Base.js index 7e584955..ec99f79e 100644 --- a/src/players/Base.js +++ b/src/players/Base.js @@ -1,27 +1,26 @@ import { Component } from 'react' -import propTypes from '../propTypes' - -const UPDATE_FREQUENCY = 500 +import { propTypes, defaultProps } from '../props' export default class Base extends Component { static propTypes = propTypes - static defaultProps = { - onProgress: function () {} - } + static defaultProps = defaultProps componentDidMount () { - this.play(this.props.url) - this.update() + if (this.props.url) { + this.load(this.props.url) + } } componentWillUnmount () { this.stop() - clearTimeout(this.updateTimeout) } componentWillReceiveProps (nextProps) { // Invoke player methods based on incoming props - if (this.props.url !== nextProps.url) { - this.play(nextProps.url) - this.props.onProgress({ played: 0, loaded: 0 }) + if (this.props.url !== nextProps.url && nextProps.url) { + this.load(nextProps.url, nextProps.playing) + this.props.onProgress({ played: 0, loaded: 0 }) // Needed? + } else if (this.props.url && !nextProps.url) { + this.stop() + clearTimeout(this.updateTimeout) } else if (!this.props.playing && nextProps.playing) { this.play() } else if (this.props.playing && !nextProps.playing) { @@ -30,24 +29,13 @@ export default class Base extends Component { this.setVolume(nextProps.volume) } } - update = () => { - let progress = {} - const loaded = this.getFractionLoaded() - const played = this.getFractionPlayed() - if (!this.prevLoaded || loaded !== this.prevLoaded) { - progress.loaded = this.prevLoaded = loaded - } - if (!this.prevPlayed || played !== this.prevPlayed) { - progress.played = this.prevPlayed = played - } - if (progress.loaded || progress.played) { - this.props.onProgress(progress) - } - this.updateTimeout = setTimeout(this.update, UPDATE_FREQUENCY) + shouldComponentUpdate (nextProps) { + return this.props.url !== nextProps.url } onReady = () => { this.setVolume(this.props.volume) - if (this.props.playing) { + if (this.props.playing || this.preloading) { + this.preloading = false this.play() } } diff --git a/src/players/FilePlayer.js b/src/players/FilePlayer.js index 8396655c..3f0a11d5 100644 --- a/src/players/FilePlayer.js +++ b/src/players/FilePlayer.js @@ -1,6 +1,6 @@ import React from 'react' -import propTypes from '../propTypes' +import { propTypes, defaultProps } from '../props' import Base from './Base' const VIDEO_EXTENSIONS = /\.(mp4|og[gv]|webm)$/ @@ -8,6 +8,7 @@ const AUDIO_EXTENSIONS = /\.(mp3|wav)$/ export default class FilePlayer extends Base { static propTypes = propTypes + static defaultProps = defaultProps static canPlay (url) { return VIDEO_EXTENSIONS.test(url) || AUDIO_EXTENSIONS.test(url) } @@ -18,19 +19,18 @@ export default class FilePlayer extends Base { this.player.onpause = this.props.onPause this.player.onended = this.props.onEnded this.player.onerror = this.props.onError - super.componentDidMount() } - shouldComponentUpdate (nextProps) { - return this.props.url !== nextProps + load (url) { + this.player.src = url } - play (url) { + play () { this.player.play() } pause () { this.player.pause() } stop () { - // No need to stop + this.player.src = '' } seekTo (fraction) { this.player.currentTime = this.player.duration * fraction @@ -48,10 +48,11 @@ export default class FilePlayer extends Base { } render () { const Media = AUDIO_EXTENSIONS.test(this.props.url) ? 'audio' : 'video' + const style = { display: this.props.url ? 'block' : 'none' } return ( diff --git a/src/players/SoundCloud.js b/src/players/SoundCloud.js index 1f5fc617..cb7fcb23 100644 --- a/src/players/SoundCloud.js +++ b/src/players/SoundCloud.js @@ -1,10 +1,9 @@ import React from 'react' import loadScript from 'load-script' -import propTypes from '../propTypes' +import { propTypes, defaultProps } from '../props' import Base from './Base' -const DEFAULT_CLIENT_ID = 'e8b6f84fbcad14c301ca1355cae1dea2' const SDK_URL = '//connect.soundcloud.com/sdk-2.0.0.js' const SDK_GLOBAL = 'SC' const RESOLVE_URL = '//api.soundcloud.com/resolve.json' @@ -12,11 +11,7 @@ const MATCH_URL = /^https?:\/\/(soundcloud.com|snd.sc)\/([a-z0-9-]+\/[a-z0-9-]+) export default class SoundCloud extends Base { static propTypes = propTypes - static defaultProps = { - soundcloudConfig: { - clientId: DEFAULT_CLIENT_ID - } - } + static defaultProps = defaultProps static canPlay (url) { return MATCH_URL.test(url) } @@ -24,7 +19,10 @@ export default class SoundCloud extends Base { image: null } shouldComponentUpdate (nextProps, nextState) { - return this.state.image !== nextState.image + return ( + super.shouldComponentUpdate(nextProps, nextState) || + this.state.image !== nextState.image + ) } getSDK () { if (window[SDK_GLOBAL]) { @@ -45,11 +43,7 @@ export default class SoundCloud extends Base { return fetch(RESOLVE_URL + '?url=' + url + '&client_id=' + this.props.soundcloudConfig.clientId) .then(response => response.json()) } - play (url) { - if (!url && this.player) { - this.player.play() - return - } + load (url) { this.stop() this.getSDK().then(SC => { this.getSongData(url).then(data => { @@ -81,6 +75,10 @@ export default class SoundCloud extends Base { onfinish: this.props.onFinish, ondataerror: this.props.onError } + play () { + if (!this.player) return + this.player.play() + } pause () { if (!this.player) return this.player.pause() @@ -107,6 +105,7 @@ export default class SoundCloud extends Base { } render () { const style = { + display: this.props.url ? 'block' : 'none', height: '100%', backgroundImage: this.state.image ? 'url(' + this.state.image + ')' : null, backgroundSize: 'cover', diff --git a/src/players/Vimeo.js b/src/players/Vimeo.js index 219ef08f..9daa3e0e 100644 --- a/src/players/Vimeo.js +++ b/src/players/Vimeo.js @@ -1,12 +1,13 @@ import React from 'react' -import queryString from 'query-string' +import { stringify } from 'query-string' -import propTypes from '../propTypes' +import { propTypes, defaultProps } from '../props' import Base from './Base' const IFRAME_SRC = 'https://player.vimeo.com/video/' const MATCH_URL = /https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/ const MATCH_MESSAGE_ORIGIN = /^https?:\/\/player.vimeo.com/ +const BLANK_VIDEO_URL = 'https://vimeo.com/127250231' const DEFAULT_IFRAME_PARAMS = { api: 1, autoplay: 0, @@ -18,30 +19,37 @@ const DEFAULT_IFRAME_PARAMS = { export default class Vimeo extends Base { static propTypes = propTypes - static defaultProps = { - vimeoConfig: {} - } + static defaultProps = defaultProps static canPlay (url) { return MATCH_URL.test(url) } componentDidMount () { window.addEventListener('message', this.onMessage, false) this.iframe = this.refs.iframe + + if (!this.props.url && this.props.vimeoConfig.preload) { + this.preloading = true + this.load(BLANK_VIDEO_URL) + } + super.componentDidMount() } - shouldComponentUpdate (nextProps) { - return this.props.url !== nextProps.url - } - play (url) { - if (!url) { - this.postMessage('play') + load (url) { + const id = url.match(MATCH_URL)[3] + const iframeParams = { + ...DEFAULT_IFRAME_PARAMS, + ...this.props.vimeoConfig.iframeParams } + this.iframe.src = IFRAME_SRC + id + '?' + stringify(iframeParams) + } + play () { + this.postMessage('play') } pause () { this.postMessage('pause') } stop () { - // No need + this.iframe.src = '' } seekTo (fraction) { this.postMessage('seekTo', this.duration * fraction) @@ -81,19 +89,11 @@ export default class Vimeo extends Base { return this.iframe.contentWindow && this.iframe.contentWindow.postMessage(data, this.origin) } render () { - const id = this.props.url.match(MATCH_URL)[3] const style = { + display: this.props.url ? 'block' : 'none', width: '100%', height: '100%' } - const iframeParams = { ...DEFAULT_IFRAME_PARAMS, ...this.props.vimeoConfig.iframeParams } - return ( -