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 (
-
- )
+ return
}
}
diff --git a/src/players/YouTube.js b/src/players/YouTube.js
index b4e80f0b..49c24de0 100644
--- a/src/players/YouTube.js
+++ b/src/players/YouTube.js
@@ -1,13 +1,14 @@
import React from 'react'
import loadScript from 'load-script'
-import propTypes from '../propTypes'
+import { propTypes, defaultProps } from '../props'
import Base from './Base'
const SDK_URL = '//www.youtube.com/iframe_api'
const SDK_GLOBAL = 'YT'
const MATCH_URL = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/
const PLAYER_ID = 'youtube-player'
+const BLANK_VIDEO_URL = 'https://www.youtube.com/watch?v=GlCmAC4MHek'
const DEFAULT_PLAYER_VARS = {
autoplay: 0,
controls: 0,
@@ -16,14 +17,16 @@ const DEFAULT_PLAYER_VARS = {
export default class YouTube extends Base {
static propTypes = propTypes
- static defaultProps = {
- youtubeConfig: {}
- }
+ static defaultProps = defaultProps
static canPlay (url) {
return MATCH_URL.test(url)
}
- shouldComponentUpdate () {
- return false
+ componentDidMount () {
+ if (!this.props.url && this.props.youtubeConfig.preload) {
+ this.preloading = true
+ this.load(BLANK_VIDEO_URL)
+ }
+ super.componentDidMount()
}
getSDK () {
if (window[SDK_GLOBAL]) {
@@ -38,13 +41,14 @@ export default class YouTube extends Base {
})
})
}
- play (url) {
+ load (url, playing) {
const id = url && url.match(MATCH_URL)[1]
if (this.player) {
- if (id) {
+ this.stop()
+ if (playing) {
this.player.loadVideoById(id)
} else {
- this.player.playVideo()
+ this.player.cueVideoById(id)
}
return
}
@@ -69,6 +73,10 @@ export default class YouTube extends Base {
if (state.data === YT.PlayerState.BUFFERING) this.props.onBuffer()
if (state.data === YT.PlayerState.ENDED) this.props.onEnded()
}
+ play () {
+ if (!this.player) return
+ this.player.playVideo()
+ }
pause () {
if (!this.player) return
this.player.pauseVideo()
@@ -94,6 +102,7 @@ export default class YouTube extends Base {
return this.player.getVideoLoadedFraction()
}
render () {
- return
+ const style = { display: this.props.url ? 'block' : 'none' }
+ return
}
}
diff --git a/src/propTypes.js b/src/props.js
similarity index 55%
rename from src/propTypes.js
rename to src/props.js
index 178c746f..4d2f12f0 100644
--- a/src/propTypes.js
+++ b/src/props.js
@@ -1,6 +1,6 @@
import { PropTypes } from 'react'
-export default {
+export const propTypes = {
url: PropTypes.string,
playing: PropTypes.bool,
volume: PropTypes.number,
@@ -21,3 +21,27 @@ export default {
onEnded: PropTypes.func,
onError: PropTypes.func
}
+
+export const defaultProps = {
+ playing: false,
+ width: 640,
+ height: 360,
+ volume: 0.8,
+ soundcloudConfig: {
+ clientId: 'e8b6f84fbcad14c301ca1355cae1dea2'
+ },
+ youtubeConfig: {
+ playerVars: {},
+ preload: false
+ },
+ vimeoConfig: {
+ iframeParams: {},
+ preload: false
+ },
+ onPlay: function () {},
+ onPause: function () {},
+ onBuffer: function () {},
+ onEnded: function () {},
+ onError: function () {},
+ onProgress: function () {}
+}
diff --git a/test/ReactPlayer.js b/test/ReactPlayer.js
index 056b2683..3b938982 100644
--- a/test/ReactPlayer.js
+++ b/test/ReactPlayer.js
@@ -24,24 +24,32 @@ describe('ReactPlayer', () => {
it('renders YouTube player', () => {
shallowRenderer.render()
const result = shallowRenderer.getRenderOutput()
- expect(result.props.children.type).to.equal(YouTube)
+ const activePlayer = getActivePlayer(result)
+ expect(activePlayer.type).to.equal(YouTube)
})
it('renders SoundCloud player', () => {
shallowRenderer.render()
const result = shallowRenderer.getRenderOutput()
- expect(result.props.children.type).to.equal(SoundCloud)
+ const activePlayer = getActivePlayer(result)
+ expect(activePlayer.type).to.equal(SoundCloud)
})
it('renders Vimeo player', () => {
shallowRenderer.render()
const result = shallowRenderer.getRenderOutput()
- expect(result.props.children.type).to.equal(Vimeo)
+ const activePlayer = getActivePlayer(result)
+ expect(activePlayer.type).to.equal(Vimeo)
})
it('renders FilePlayer', () => {
shallowRenderer.render()
const result = shallowRenderer.getRenderOutput()
- expect(result.props.children.type).to.equal(FilePlayer)
+ const activePlayer = getActivePlayer(result)
+ expect(activePlayer.type).to.equal(FilePlayer)
})
})
+
+function getActivePlayer (result) {
+ return result.props.children.find(player => player.ref === 'player')
+}