From 91d15424065575d759885e9b0f8969c8b1f4a7b4 Mon Sep 17 00:00:00 2001 From: Pete Cook Date: Tue, 24 Oct 2017 22:30:41 +0100 Subject: [PATCH] Refactor player rendering --- src/Player.js | 168 +++++++++++++++++++++++++++++++++++++ src/ReactPlayer.js | 92 ++++++-------------- src/players/Base.js | 130 ---------------------------- src/players/DailyMotion.js | 55 ++++-------- src/players/Facebook.js | 38 ++++----- src/players/FilePlayer.js | 65 +++++++------- src/players/SoundCloud.js | 54 ++++-------- src/players/Streamable.js | 25 ++---- src/players/Twitch.js | 39 +++------ src/players/Vidme.js | 6 +- src/players/Vimeo.js | 41 +++------ src/players/Wistia.js | 28 +++---- src/players/YouTube.js | 55 ++++-------- src/players/index.js | 23 +++++ src/preload.js | 44 ++++++++++ src/utils.js | 21 +++++ 16 files changed, 420 insertions(+), 464 deletions(-) create mode 100644 src/Player.js delete mode 100644 src/players/Base.js create mode 100644 src/players/index.js create mode 100644 src/preload.js diff --git a/src/Player.js b/src/Player.js new file mode 100644 index 00000000..60e80dd1 --- /dev/null +++ b/src/Player.js @@ -0,0 +1,168 @@ +import React, { Component } from 'react' + +import { propTypes, defaultProps } from './props' + +const SEEK_ON_PLAY_EXPIRY = 5000 + +export default class Player extends Component { + static displayName = 'Player' + static propTypes = propTypes + static defaultProps = defaultProps + mounted = false + isReady = false + isPlaying = false // Track playing state internally to prevent bugs + startOnPlay = true + seekOnPlay = null + componentDidMount () { + this.mounted = true + this.player.load(this.props.url) + } + componentWillUnmount () { + if (this.isReady) { + this.player.stop() + } + this.mounted = false + } + componentDidUpdate (prevProps) { + const { activePlayer, url } = this.props + if (prevProps.activePlayer !== activePlayer) { + this.isReady = false + this.seekOnPlay = null + this.startOnPlay = true + this.player.load(url, this.isReady) + } + } + componentWillReceiveProps (nextProps) { + // Invoke player methods based on incoming props + const { activePlayer, url, playing, volume, muted, playbackRate } = this.props + if (activePlayer !== nextProps.activePlayer) { + this.player.stop() + return // A new player is coming, so don't invoke any other methods + } + if (url !== nextProps.url) { + this.player.load(nextProps.url, this.isReady) + } + if (url && !nextProps.url) { + this.player.stop() + } + if (!playing && nextProps.playing && !this.isPlaying) { + this.player.play() + } + if (playing && !nextProps.playing && this.isPlaying) { + this.player.pause() + } + if (volume !== nextProps.volume && !nextProps.muted) { + this.player.setVolume(nextProps.volume) + } + if (muted !== nextProps.muted) { + this.player.setVolume(nextProps.muted ? 0 : nextProps.volume) + } + if (playbackRate !== nextProps.playbackRate && this.player.setPlaybackRate) { + this.player.setPlaybackRate(nextProps.playbackRate) + } + } + getCurrentTime () { + if (!this.isReady) return null + return this.player.getCurrentTime() + } + getSecondsLoaded () { + if (!this.isReady) return null + return this.player.getSecondsLoaded() + } + getDuration () { + if (!this.isReady) return null + return this.player.getDuration() + } + seekTo (amount) { + // When seeking before player is ready, store value and seek later + if (!this.isReady && amount !== 0) { + this.seekOnPlay = amount + setTimeout(() => { + this.seekOnPlay = null + }, SEEK_ON_PLAY_EXPIRY) + return + } + if (amount > 0 && amount < 1) { + // Convert fraction to seconds based on duration + const duration = this.player.getDuration() + if (!duration) { + console.warn('ReactPlayer: could not seek using fraction – duration not yet available') + return + } + this.player.seekTo(duration * amount) + return + } + this.player.seekTo(amount) + } + onReady = () => { + if (!this.mounted) return + const { onReady, playing } = this.props + this.isReady = true + this.loadingSDK = false + onReady() + if (playing) { + if (this.loadOnReady) { + this.player.load(this.loadOnReady) + this.loadOnReady = null + } else { + this.player.play() + } + } + this.onDurationCheck() + } + onPlay = () => { + this.isPlaying = true + const { volume, muted, onStart, onPlay, playbackRate } = this.props + if (this.startOnPlay) { + if (this.player.setPlaybackRate) { + this.player.setPlaybackRate(playbackRate) + } + this.player.setVolume(muted ? 0 : volume) + onStart() + this.startOnPlay = false + } + onPlay() + if (this.seekOnPlay) { + this.seekTo(this.seekOnPlay) + this.seekOnPlay = null + } + this.onDurationCheck() + } + onPause = () => { + this.isPlaying = false + this.props.onPause() + } + onEnded = () => { + const { activePlayer, loop, onEnded } = this.props + if (activePlayer.loopOnEnded && loop) { + this.seekTo(0) + } + onEnded() + } + onDurationCheck = () => { + clearTimeout(this.durationCheckTimeout) + const duration = this.getDuration() + if (duration) { + this.props.onDuration(duration) + } else { + this.durationCheckTimeout = setTimeout(this.onDurationCheck, 100) + } + } + ref = player => { + if (player) { + this.player = player + } + } + render () { + const Player = this.props.activePlayer + return ( + + ) + } +} diff --git a/src/ReactPlayer.js b/src/ReactPlayer.js index 43b47abc..2a9e1db1 100644 --- a/src/ReactPlayer.js +++ b/src/ReactPlayer.js @@ -1,37 +1,19 @@ import React, { Component } from 'react' import { propTypes, defaultProps, DEPRECATED_CONFIG_PROPS } from './props' -import { getConfig, omit } from './utils' -import YouTube from './players/YouTube' -import SoundCloud from './players/SoundCloud' -import Vimeo from './players/Vimeo' -import Facebook from './players/Facebook' +import { getConfig, omit, isObject } from './utils' +import players from './players' +import Player from './Player' import FilePlayer from './players/FilePlayer' -import Streamable from './players/Streamable' -import Vidme from './players/Vidme' -import Wistia from './players/Wistia' -import DailyMotion from './players/DailyMotion' -import Twitch from './players/Twitch' +import renderPreloadPlayers from './preload' const SUPPORTED_PROPS = Object.keys(propTypes) -const SUPPORTED_PLAYERS = [ - YouTube, - SoundCloud, - Vimeo, - Facebook, - Streamable, - Vidme, - Wistia, - Twitch, - DailyMotion -] export default class ReactPlayer extends Component { static displayName = 'ReactPlayer' static propTypes = propTypes static defaultProps = defaultProps static canPlay = url => { - const players = [...SUPPORTED_PLAYERS, FilePlayer] for (let Player of players) { if (Player.canPlay(url)) { return true @@ -47,17 +29,13 @@ export default class ReactPlayer extends Component { clearTimeout(this.progressTimeout) } shouldComponentUpdate (nextProps) { - return ( - this.props.url !== nextProps.url || - this.props.playing !== nextProps.playing || - this.props.loop !== nextProps.loop || - this.props.volume !== nextProps.volume || - this.props.muted !== nextProps.muted || - this.props.playbackRate !== nextProps.playbackRate || - this.props.height !== nextProps.height || - this.props.width !== nextProps.width || - this.props.hidden !== nextProps.hidden - ) + for (let key of Object.keys(this.props)) { + const prop = this.props[key] + if (!isObject(prop) && prop !== nextProps[key]) { + return true + } + } + return false } seekTo = fraction => { if (!this.player) return null @@ -99,51 +77,31 @@ export default class ReactPlayer extends Component { } this.progressTimeout = setTimeout(this.progress, this.props.progressFrequency) } - renderActivePlayer (url) { - if (!url) return null - for (let Player of SUPPORTED_PLAYERS) { + getActivePlayer (url) { + for (let Player of players) { if (Player.canPlay(url)) { - return this.renderPlayer(Player) + return Player } } // Fall back to FilePlayer if nothing else can play the URL - return this.renderPlayer(FilePlayer) - } - renderPlayer = Player => { - return ( - - ) - } - activePlayerRef = player => { - this.player = player + return FilePlayer } wrapperRef = wrapper => { this.wrapper = wrapper } - renderPreloadPlayers (url) { - // Render additional players if preload config is set - const preloadPlayers = [] - if (!YouTube.canPlay(url) && this.config.youtube.preload) { - preloadPlayers.push(YouTube) - } - if (!Vimeo.canPlay(url) && this.config.vimeo.preload) { - preloadPlayers.push(Vimeo) - } - if (!DailyMotion.canPlay(url) && this.config.dailymotion.preload) { - preloadPlayers.push(DailyMotion) - } - return preloadPlayers.map(this.renderPreloadPlayer) + activePlayerRef = player => { + this.player = player } - renderPreloadPlayer = Player => { + renderActivePlayer (url) { + if (!url) return null + const activePlayer = this.getActivePlayer(url) return ( ) } @@ -151,7 +109,7 @@ export default class ReactPlayer extends Component { const { url, style, width, height } = this.props const otherProps = omit(this.props, SUPPORTED_PROPS, DEPRECATED_CONFIG_PROPS) const activePlayer = this.renderActivePlayer(url) - const preloadPlayers = this.renderPreloadPlayers(url) + const preloadPlayers = renderPreloadPlayers(url, this.config) return (
{[ activePlayer, ...preloadPlayers ]} diff --git a/src/players/Base.js b/src/players/Base.js deleted file mode 100644 index 3f90ac49..00000000 --- a/src/players/Base.js +++ /dev/null @@ -1,130 +0,0 @@ -import { Component } from 'react' - -import { propTypes, defaultProps } from '../props' - -const SEEK_ON_PLAY_EXPIRY = 5000 - -export default class Base extends Component { - static propTypes = propTypes - static defaultProps = defaultProps - isReady = false - startOnPlay = true - seekOnPlay = null - componentDidMount () { - const { url } = this.props - this.mounted = true - if (url) { - this.load(url) - } - } - componentWillUnmount () { - this.stop() - this.mounted = false - } - componentWillReceiveProps (nextProps) { - const { url, playing, volume, muted, playbackRate } = this.props - // Invoke player methods based on incoming props - if (url !== nextProps.url && nextProps.url) { - this.seekOnPlay = null - this.startOnPlay = true - this.load(nextProps.url) - } - if (url && !nextProps.url) { - this.stop() - clearTimeout(this.updateTimeout) - } - if (!playing && nextProps.playing) { - this.play() - } - if (playing && !nextProps.playing) { - this.pause() - } - if (volume !== nextProps.volume && !nextProps.muted) { - this.setVolume(nextProps.volume) - } - if (muted !== nextProps.muted) { - this.setVolume(nextProps.muted ? 0 : nextProps.volume) - } - if (playbackRate !== nextProps.playbackRate && this.setPlaybackRate) { - this.setPlaybackRate(nextProps.playbackRate) - } - } - shouldComponentUpdate (nextProps) { - return this.props.url !== nextProps.url || - this.props.loop !== nextProps.loop - } - callPlayer (method, ...args) { - // Util method for calling a method on this.player - // but guard against errors and console.warn instead - if (!this.isReady || !this.player || !this.player[method]) { - let message = `ReactPlayer: ${this.constructor.displayName} player could not call %c${method}%c – ` - if (!this.isReady) { - message += 'The player was not ready' - } else if (!this.player) { - message += 'The player was not available' - } else if (!this.player[method]) { - message += 'The method was not available' - } - console.warn(message, 'font-weight: bold', '') - return null - } - return this.player[method](...args) - } - seekTo (amount) { - // When seeking before player is ready, store value and seek later - if (!this.isReady && amount !== 0) { - this.seekOnPlay = amount - setTimeout(() => { - this.seekOnPlay = null - }, SEEK_ON_PLAY_EXPIRY) - } - // Return the seconds to seek to - if (amount > 0 && amount < 1) { - // Convert fraction to seconds based on duration - return this.getDuration() * amount - } - return amount - } - onPlay = () => { - const { volume, muted, onStart, onPlay, playbackRate } = this.props - if (this.startOnPlay) { - if (this.setPlaybackRate) { - this.setPlaybackRate(playbackRate) - } - this.setVolume(muted ? 0 : volume) - onStart() - this.startOnPlay = false - } - onPlay() - if (this.seekOnPlay) { - this.seekTo(this.seekOnPlay) - this.seekOnPlay = null - } - this.onDurationCheck() - } - onReady = () => { - const { onReady, playing } = this.props - this.isReady = true - this.loadingSDK = false - onReady() - if (playing || this.preloading) { - this.preloading = false - if (this.loadOnReady) { - this.load(this.loadOnReady) - this.loadOnReady = null - } else { - this.play() - } - } - this.onDurationCheck() - } - onDurationCheck = () => { - clearTimeout(this.durationCheckTimeout) - const duration = this.getDuration() - if (duration) { - this.props.onDuration(duration) - } else { - this.durationCheckTimeout = setTimeout(this.onDurationCheck, 100) - } - } -} diff --git a/src/players/DailyMotion.js b/src/players/DailyMotion.js index df758b49..23874141 100644 --- a/src/players/DailyMotion.js +++ b/src/players/DailyMotion.js @@ -1,27 +1,18 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK, parseStartTime } from '../utils' +import { callPlayer, getSDK, parseStartTime } from '../utils' const SDK_URL = 'https://api.dmcdn.net/all.js' const SDK_GLOBAL = 'DM' const SDK_GLOBAL_READY = 'dmAsyncInit' const MATCH_URL = /^.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/ -const BLANK_VIDEO_URL = 'http://www.dailymotion.com/video/x522udb' -export default class DailyMotion extends Base { +export default class DailyMotion extends Component { static displayName = 'DailyMotion' - static canPlay (url) { - return MATCH_URL.test(url) - } - componentDidMount () { - const { url, config } = this.props - if (!url && config.dailymotion.preload) { - this.preloading = true - this.load(BLANK_VIDEO_URL) - } - super.componentDidMount() - } + static canPlay = url => MATCH_URL.test(url) + static loopOnEnded = true + + callPlayer = callPlayer parseId (url) { const m = url.match(MATCH_URL) return m[4] || m[2] @@ -36,11 +27,6 @@ export default class DailyMotion extends Base { }) return } - if (this.loadingSDK) { - this.loadOnReady = url - return - } - this.loadingSDK = true getSDK(SDK_URL, SDK_GLOBAL, SDK_GLOBAL_READY, DM => DM.player).then(DM => { const Player = DM.player this.player = new Player(this.container, { @@ -55,32 +41,21 @@ export default class DailyMotion extends Base { ...config.dailymotion.params }, events: { - apiready: () => { - this.loadingSDK = false - this.onReady() - }, + apiready: this.props.onReady, seeked: () => this.props.onSeek(this.player.currentTime), - video_end: this.onEnded, + video_end: this.props.onEnded, durationchange: this.onDurationChange, pause: this.props.onPause, - playing: this.onPlay, + playing: this.props.onPlay, waiting: this.props.onBuffer, error: event => onError(event) } }) }, onError) } - onDurationChange = (event) => { - const { onDuration } = this.props + onDurationChange = () => { const duration = this.getDuration() - onDuration(duration) - } - onEnded = () => { - const { loop, onEnded } = this.props - if (loop) { - this.seekTo(0) - } - onEnded() + this.props.onDuration(duration) } play () { this.callPlayer('play') @@ -91,15 +66,13 @@ export default class DailyMotion extends Base { stop () { // Nothing to do } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.callPlayer('seek', seconds) } setVolume (fraction) { this.callPlayer('setVolume', fraction) } getDuration () { - if (!this.isReady) return null return this.player.duration || null } getCurrentTime () { @@ -116,7 +89,7 @@ export default class DailyMotion extends Base { width: '100%', height: '100%', backgroundColor: 'black', - display: this.props.url ? 'block' : 'none' + ...this.props.style } return (
diff --git a/src/players/Facebook.js b/src/players/Facebook.js index 72b4bd1f..3d094d04 100644 --- a/src/players/Facebook.js +++ b/src/players/Facebook.js @@ -1,7 +1,6 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK, randomString } from '../utils' +import { callPlayer, getSDK, randomString } from '../utils' const SDK_URL = '//connect.facebook.net/en_US/sdk.js' const SDK_GLOBAL = 'FB' @@ -9,14 +8,15 @@ const SDK_GLOBAL_READY = 'fbAsyncInit' const MATCH_URL = /^https:\/\/www\.facebook\.com\/([^/?].+\/)?video(s|\.php)[/?].*$/ const PLAYER_ID_PREFIX = 'facebook-player-' -export default class Facebook extends Base { +export default class Facebook extends Component { static displayName = 'Facebook' - static canPlay (url) { - return MATCH_URL.test(url) - } + static canPlay = url => MATCH_URL.test(url) + static loopOnEnded = true + + callPlayer = callPlayer playerID = PLAYER_ID_PREFIX + randomString() - load (url) { - if (this.isReady) { + load (url, isReady) { + if (isReady) { getSDK(SDK_URL, SDK_GLOBAL, SDK_GLOBAL_READY).then(FB => FB.XFBML.parse()) return } @@ -29,23 +29,16 @@ export default class Facebook extends Base { FB.Event.subscribe('xfbml.ready', msg => { if (msg.type === 'video' && msg.id === this.playerID) { this.player = msg.instance - this.player.subscribe('startedPlaying', this.onPlay) + this.player.subscribe('startedPlaying', this.props.onPlay) this.player.subscribe('paused', this.props.onPause) - this.player.subscribe('finishedPlaying', this.onEnded) + this.player.subscribe('finishedPlaying', this.props.onEnded) this.player.subscribe('startedBuffering', this.props.onBuffer) this.player.subscribe('error', this.props.onError) - this.onReady() + this.props.onReady() } }) }) } - onEnded = () => { - const { loop, onEnded } = this.props - if (loop) { - this.seekTo(0) - } - onEnded() - } play () { this.callPlayer('play') } @@ -55,15 +48,14 @@ export default class Facebook extends Base { stop () { // Nothing to do } - seekTo (amount) { - const seconds = super.seekTo(amount) - this.player.seek(seconds) + seekTo (seconds) { + this.callPlayer('seek', seconds) } setVolume (fraction) { if (fraction !== 0) { this.callPlayer('unmute') } - this.player.setVolume(fraction) + this.callPlayer('setVolume', fraction) } getDuration () { return this.callPlayer('getDuration') diff --git a/src/players/FilePlayer.js b/src/players/FilePlayer.js index ff178a27..ae1b3bde 100644 --- a/src/players/FilePlayer.js +++ b/src/players/FilePlayer.js @@ -1,6 +1,5 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' import { getSDK } from '../utils' const AUDIO_EXTENSIONS = /\.(m4a|mp4a|mpga|mp2|mp2a|mp3|m2a|m3a|wav|weba|aac|oga|spx)($|\?)/i @@ -12,36 +11,37 @@ const DASH_EXTENSIONS = /\.(mpd)($|\?)/i const DASH_SDK_URL = 'https://cdnjs.cloudflare.com/ajax/libs/dashjs/2.5.0/dash.all.min.js' const DASH_GLOBAL = 'dashjs' -export default class FilePlayer extends Base { - static displayName = 'FilePlayer' - static canPlay (url) { - if (url instanceof Array) { - for (let item of url) { - if (typeof item === 'string' && this.canPlay(item)) { - return true - } - if (this.canPlay(item.src)) { - return true - } +function canPlay (url) { + if (url instanceof Array) { + for (let item of url) { + if (typeof item === 'string' && canPlay(item)) { + return true + } + if (canPlay(item.src)) { + return true } - return false } - return ( - AUDIO_EXTENSIONS.test(url) || - VIDEO_EXTENSIONS.test(url) || - HLS_EXTENSIONS.test(url) || - DASH_EXTENSIONS.test(url) - ) - } + return false + } + return ( + AUDIO_EXTENSIONS.test(url) || + VIDEO_EXTENSIONS.test(url) || + HLS_EXTENSIONS.test(url) || + DASH_EXTENSIONS.test(url) + ) +} + +export default class FilePlayer extends Component { + static displayName = 'FilePlayer' + static canPlay = canPlay + componentDidMount () { this.addListeners() - super.componentDidMount() } componentWillReceiveProps (nextProps) { if (this.shouldUseAudio(this.props) !== this.shouldUseAudio(nextProps)) { this.removeListeners() } - super.componentWillReceiveProps(nextProps) } componentDidUpdate (prevProps) { if (this.shouldUseAudio(this.props) !== this.shouldUseAudio(prevProps)) { @@ -50,12 +50,11 @@ export default class FilePlayer extends Base { } componentWillUnmount () { this.removeListeners() - super.componentWillUnmount() } addListeners () { - const { playsinline, onPause, onEnded, onError } = this.props - this.player.addEventListener('canplay', this.onReady) - this.player.addEventListener('play', this.onPlay) + const { onReady, onPlay, onPause, onEnded, onError, playsinline } = this.props + this.player.addEventListener('canplay', onReady) + this.player.addEventListener('play', onPlay) this.player.addEventListener('pause', onPause) this.player.addEventListener('seeked', this.onSeek) this.player.addEventListener('ended', onEnded) @@ -66,9 +65,9 @@ export default class FilePlayer extends Base { } } removeListeners () { - const { onPause, onEnded, onError } = this.props - this.player.removeEventListener('canplay', this.onReady) - this.player.removeEventListener('play', this.onPlay) + const { onReady, onPlay, onPause, onEnded, onError } = this.props + this.player.removeEventListener('canplay', onReady) + this.player.removeEventListener('play', onPlay) this.player.removeEventListener('pause', onPause) this.player.removeEventListener('seeked', this.onSeek) this.player.removeEventListener('ended', onEnded) @@ -120,8 +119,7 @@ export default class FilePlayer extends Base { this.dash.reset() } } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.player.currentTime = seconds } setVolume (fraction) { @@ -162,8 +160,7 @@ export default class FilePlayer extends Base { const src = url instanceof Array || useHLS || useDASH ? undefined : url const style = { width: !width || width === 'auto' ? width : '100%', - height: !height || height === 'auto' ? height : '100%', - display: url ? 'block' : 'none' + height: !height || height === 'auto' ? height : '100%' } return ( MATCH_URL.test(url) + static loopOnEnded = true + + callPlayer = callPlayer duration = null currentTime = null fractionLoaded = null - load (url) { + load (url, isReady) { getSDK(SDK_URL, SDK_GLOBAL).then(SC => { const { PLAY, PLAY_PROGRESS, PAUSE, FINISH, ERROR } = SC.Widget.Events - if (!this.isReady) { + if (!isReady) { this.player = SC.Widget(this.iframe) - this.player.bind(PLAY, () => { - // Use widgetIsPlaying to prevent calling play() when widget - // is playing, which causes bugs with the SC widget - this.widgetIsPlaying = true - this.onPlay() - }) - this.player.bind(PAUSE, () => { - this.widgetIsPlaying = false - this.props.onPause() - }) + this.player.bind(PLAY, this.props.onPlay) + this.player.bind(PAUSE, this.props.onPause) this.player.bind(PLAY_PROGRESS, e => { this.currentTime = e.currentPosition / 1000 this.fractionLoaded = e.loadedProgress @@ -47,33 +30,26 @@ export default class SoundCloud extends Base { this.player.bind(ERROR, e => this.props.onError(e)) } this.player.load(url, { - ...DEFAULT_OPTIONS, ...this.props.config.soundcloud.options, callback: () => { - this.widgetIsPlaying = false this.player.getDuration(duration => { this.duration = duration / 1000 - this.onReady() + this.props.onReady() }) } }) }) } play () { - if (!this.widgetIsPlaying) { - this.callPlayer('play') - } + this.callPlayer('play') } pause () { - if (this.widgetIsPlaying) { - this.callPlayer('pause') - } + this.callPlayer('pause') } stop () { // Nothing to do } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.callPlayer('seekTo', seconds * 1000) } setVolume (fraction) { diff --git a/src/players/Streamable.js b/src/players/Streamable.js index 1199671a..6b8ed64d 100644 --- a/src/players/Streamable.js +++ b/src/players/Streamable.js @@ -1,30 +1,24 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK } from '../utils' +import { callPlayer, getSDK } from '../utils' const SDK_URL = '//cdn.embed.ly/player-0.0.12.min.js' const SDK_GLOBAL = 'playerjs' const MATCH_URL = /^https?:\/\/streamable.com\/([a-z0-9]+)$/ -export default class Streamable extends Base { +export default class Streamable extends Component { static displayName = 'Streamable' - static canPlay (url) { - return MATCH_URL.test(url) - } + static canPlay = url => MATCH_URL.test(url) + + callPlayer = callPlayer duration = null currentTime = null secondsLoaded = null load (url) { - if (this.loadingSDK) { - this.loadOnReady = url - return - } - this.loadingSDK = true getSDK(SDK_URL, SDK_GLOBAL).then(playerjs => { this.player = new playerjs.Player(this.iframe) - this.player.on('ready', this.onReady) - this.player.on('play', this.onPlay) + this.player.on('ready', this.props.onReady) + this.player.on('play', this.props.onPlay) this.player.on('pause', this.props.onPause) this.player.on('seeked', this.props.onSeek) this.player.on('ended', this.props.onEnded) @@ -49,8 +43,7 @@ export default class Streamable extends Base { stop () { // Nothing to do } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.callPlayer('setCurrentTime', seconds) } setVolume (fraction) { diff --git a/src/players/Twitch.js b/src/players/Twitch.js index 757a3620..6db3f851 100644 --- a/src/players/Twitch.js +++ b/src/players/Twitch.js @@ -1,7 +1,6 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK, randomString } from '../utils' +import { callPlayer, getSDK, randomString } from '../utils' const SDK_URL = '//player.twitch.tv/js/embed/v1.js' const SDK_GLOBAL = 'Twitch' @@ -9,17 +8,18 @@ const MATCH_VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)twitch\.tv\/videos\/(\d+)($|\ const MATCH_CHANNEL_URL = /^(?:https?:\/\/)?(?:www\.)twitch\.tv\/([a-z0-9_]+)($|\?)/ const PLAYER_ID_PREFIX = 'twitch-player-' -export default class Twitch extends Base { +export default class Twitch extends Component { static displayName = 'Twitch' - static canPlay (url) { - return MATCH_VIDEO_URL.test(url) || MATCH_CHANNEL_URL.test(url) - } + static canPlay = url => MATCH_VIDEO_URL.test(url) || MATCH_CHANNEL_URL.test(url) + static loopOnEnded = true + + callPlayer = callPlayer playerID = PLAYER_ID_PREFIX + randomString() - load (url) { + load (url, isReady) { const { playsinline, onError } = this.props const isChannel = MATCH_CHANNEL_URL.test(url) const id = isChannel ? url.match(MATCH_CHANNEL_URL)[1] : url.match(MATCH_VIDEO_URL)[1] - if (this.isReady) { + if (isReady) { if (isChannel) { this.player.setChannel(id) } else { @@ -27,11 +27,6 @@ export default class Twitch extends Base { } return } - if (this.loadingSDK) { - this.loadOnReady = url - return - } - this.loadingSDK = true getSDK(SDK_URL, SDK_GLOBAL).then(Twitch => { this.player = new Twitch.Player(this.playerID, { video: isChannel ? '' : id, @@ -41,19 +36,12 @@ export default class Twitch extends Base { playsinline: playsinline }) const { READY, PLAY, PAUSE, ENDED } = Twitch.Player - this.player.addEventListener(READY, this.onReady) - this.player.addEventListener(PLAY, this.onPlay) + this.player.addEventListener(READY, this.props.onReady) + this.player.addEventListener(PLAY, this.props.onPlay) this.player.addEventListener(PAUSE, this.props.onPause) - this.player.addEventListener(ENDED, this.onEnded) + this.player.addEventListener(ENDED, this.props.onEnded) }, onError) } - onEnded = () => { - const { loop, onEnded } = this.props - if (loop) { - this.seekTo(0) - } - onEnded() - } play () { this.callPlayer('play') } @@ -63,8 +51,7 @@ export default class Twitch extends Base { stop () { this.callPlayer('pause') } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.callPlayer('seek', seconds) } setVolume (fraction) { diff --git a/src/players/Vidme.js b/src/players/Vidme.js index 21d9b97f..6e23062d 100644 --- a/src/players/Vidme.js +++ b/src/players/Vidme.js @@ -7,9 +7,8 @@ const cache = {} // Cache song data requests export default class Vidme extends FilePlayer { static displayName = 'Vidme' - static canPlay (url) { - return MATCH_URL.test(url) - } + static canPlay = url => MATCH_URL.test(url) + getData (url) { const { onError } = this.props const id = url.match(MATCH_URL)[1] @@ -42,7 +41,6 @@ export default class Vidme extends FilePlayer { const { onError } = this.props this.stop() this.getData(url).then(data => { - if (!this.mounted) return this.player.src = this.getURL(data) }, onError) } diff --git a/src/players/Vimeo.js b/src/players/Vimeo.js index 55afd4e9..2fbbcf57 100644 --- a/src/players/Vimeo.js +++ b/src/players/Vimeo.js @@ -1,41 +1,26 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK } from '../utils' +import { callPlayer, getSDK } from '../utils' const SDK_URL = 'https://player.vimeo.com/api/player.js' const SDK_GLOBAL = 'Vimeo' const MATCH_URL = /https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/ -const BLANK_VIDEO_URL = 'https://vimeo.com/127250231' -export default class Vimeo extends Base { +export default class Vimeo extends Component { static displayName = 'Vimeo' - static canPlay (url) { - return MATCH_URL.test(url) - } + static canPlay = url => MATCH_URL.test(url) + + callPlayer = callPlayer duration = null currentTime = null secondsLoaded = null - componentDidMount () { - const { url, config } = this.props - if (!url && config.vimeo.preload) { - this.preloading = true - this.load(BLANK_VIDEO_URL) - } - super.componentDidMount() - } - load (url) { + load (url, isReady) { const id = url.match(MATCH_URL)[3] this.duration = null - if (this.isReady) { + if (isReady) { this.player.loadVideo(id).catch(this.props.onError) return } - if (this.loadingSDK) { - this.loadOnReady = url - return - } - this.loadingSDK = true getSDK(SDK_URL, SDK_GLOBAL).then(Vimeo => { this.player = new Vimeo.Player(this.container, { ...this.props.config.vimeo.playerOptions, @@ -48,12 +33,12 @@ export default class Vimeo extends Base { iframe.style.height = '100%' }).catch(this.props.onError) this.player.on('loaded', () => { - this.onReady() + this.props.onReady() this.player.getDuration().then(duration => { this.duration = duration }) }) - this.player.on('play', this.onPlay) + this.player.on('play', this.props.onPlay) this.player.on('pause', this.props.onPause) this.player.on('seeked', e => this.props.onSeek(e.seconds)) this.player.on('ended', this.props.onEnded) @@ -73,11 +58,9 @@ export default class Vimeo extends Base { this.callPlayer('pause') } stop () { - if (this.preloading) return this.callPlayer('unload') } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.callPlayer('setCurrentTime', seconds) } setVolume (fraction) { @@ -101,7 +84,7 @@ export default class Vimeo extends Base { height: '100%', overflow: 'hidden', backgroundColor: 'black', - display: this.props.url ? 'block' : 'none' + ...this.props.style } return
} diff --git a/src/players/Wistia.js b/src/players/Wistia.js index 0d68fc12..8ce1c3a4 100644 --- a/src/players/Wistia.js +++ b/src/players/Wistia.js @@ -1,23 +1,22 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK } from '../utils' +import { callPlayer, getSDK } from '../utils' const SDK_URL = '//fast.wistia.com/assets/external/E-v1.js' const SDK_GLOBAL = 'Wistia' const MATCH_URL = /^https?:\/\/(.+)?(wistia.com|wi.st)\/(medias|embed)\/(.*)$/ -export default class Wistia extends Base { +export default class Wistia extends Component { static displayName = 'Wistia' - static canPlay (url) { - return MATCH_URL.test(url) - } + static canPlay = url => MATCH_URL.test(url) + static loopOnEnded = true + + callPlayer = callPlayer getID (url) { return url && url.match(MATCH_URL)[4] } load (url) { - const { controls, onStart, onPause, onSeek, onEnded, config } = this.props - this.loadingSDK = true + const { controls, onReady, onPlay, onPause, onSeek, onEnded, config } = this.props getSDK(SDK_URL, SDK_GLOBAL).then(() => { window._wq = window._wq || [] window._wq.push({ @@ -28,12 +27,11 @@ export default class Wistia extends Base { }, onReady: player => { this.player = player - this.player.bind('start', onStart) - this.player.bind('play', this.onPlay) + this.player.bind('play', onPlay) this.player.bind('pause', onPause) this.player.bind('seek', onSeek) this.player.bind('end', onEnded) - this.onReady() + onReady() } }) }) @@ -47,8 +45,7 @@ export default class Wistia extends Base { stop () { this.callPlayer('remove') } - seekTo (amount) { - const seconds = super.seekTo(amount) + seekTo (seconds) { this.callPlayer('time', seconds) } setVolume (fraction) { @@ -71,8 +68,7 @@ export default class Wistia extends Base { const className = `wistia_embed wistia_async_${id}` const style = { width: '100%', - height: '100%', - display: this.props.url ? 'block' : 'none' + height: '100%' } return (
diff --git a/src/players/YouTube.js b/src/players/YouTube.js index 46632de0..0e53438c 100644 --- a/src/players/YouTube.js +++ b/src/players/YouTube.js @@ -1,42 +1,28 @@ -import React from 'react' +import React, { Component } from 'react' -import Base from './Base' -import { getSDK, parseStartTime } from '../utils' +import { callPlayer, getSDK, parseStartTime } from '../utils' const SDK_URL = 'https://www.youtube.com/iframe_api' const SDK_GLOBAL = 'YT' const SDK_GLOBAL_READY = 'onYouTubeIframeAPIReady' const MATCH_URL = /^(?:https?:\/\/)?(?:www\.|m\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/ -const BLANK_VIDEO_URL = 'https://www.youtube.com/watch?v=GlCmAC4MHek' -export default class YouTube extends Base { +export default class YouTube extends Component { static displayName = 'YouTube' - static canPlay (url) { - return MATCH_URL.test(url) - } - componentDidMount () { - const { url, config } = this.props - if (!url && config.youtube.preload) { - this.preloading = true - this.load(BLANK_VIDEO_URL) - } - super.componentDidMount() - } - load (url) { + static canPlay = url => MATCH_URL.test(url) + static loopOnEnded = true + + callPlayer = callPlayer + load (url, isReady) { const { playsinline, controls, config, onError } = this.props const id = url && url.match(MATCH_URL)[1] - if (this.isReady) { + if (isReady) { this.player.cueVideoById({ videoId: id, startSeconds: parseStartTime(url) }) return } - if (this.loadingSDK) { - this.loadOnReady = url - return - } - this.loadingSDK = true getSDK(SDK_URL, SDK_GLOBAL, SDK_GLOBAL_READY, YT => YT.loaded).then(YT => { this.player = new YT.Player(this.container, { width: '100%', @@ -50,7 +36,7 @@ export default class YouTube extends Base { ...config.youtube.playerVars }, events: { - onReady: this.onReady, + onReady: this.props.onReady, onStateChange: this.onStateChange, onError: event => onError(event.data) } @@ -58,20 +44,13 @@ export default class YouTube extends Base { }, onError) } onStateChange = ({ data }) => { - const { onPause, onBuffer } = this.props + const { onPlay, onPause, onBuffer, onEnded, onReady } = this.props const { PLAYING, PAUSED, BUFFERING, ENDED, CUED } = window[SDK_GLOBAL].PlayerState - if (data === PLAYING) this.onPlay() + if (data === PLAYING) onPlay() if (data === PAUSED) onPause() if (data === BUFFERING) onBuffer() - if (data === ENDED) this.onEnded() - if (data === CUED) this.onReady() - } - onEnded = () => { - const { loop, onEnded } = this.props - if (loop) { - this.seekTo(0) - } - onEnded() + if (data === ENDED) onEnded() + if (data === CUED) onReady() } play () { this.callPlayer('playVideo') @@ -80,13 +59,11 @@ export default class YouTube extends Base { this.callPlayer('pauseVideo') } stop () { - if (this.preloading) return if (!document.body.contains(this.callPlayer('getIframe'))) return this.callPlayer('stopVideo') } seekTo (amount) { - const seconds = super.seekTo(amount) - this.callPlayer('seekTo', seconds) + this.callPlayer('seekTo', amount) } setVolume (fraction) { this.callPlayer('setVolume', fraction * 100) @@ -110,7 +87,7 @@ export default class YouTube extends Base { const style = { width: '100%', height: '100%', - display: this.props.url ? 'block' : 'none' + ...this.props.style } return (
diff --git a/src/players/index.js b/src/players/index.js new file mode 100644 index 00000000..5808be55 --- /dev/null +++ b/src/players/index.js @@ -0,0 +1,23 @@ +import YouTube from './YouTube' +import SoundCloud from './SoundCloud' +import Vimeo from './Vimeo' +import Facebook from './Facebook' +import Streamable from './Streamable' +import Vidme from './Vidme' +import Wistia from './Wistia' +import Twitch from './Twitch' +import DailyMotion from './DailyMotion' +import FilePlayer from './FilePlayer' + +export default [ + YouTube, + SoundCloud, + Vimeo, + Facebook, + Streamable, + Vidme, + Wistia, + Twitch, + DailyMotion, + FilePlayer +] diff --git a/src/preload.js b/src/preload.js new file mode 100644 index 00000000..b9e5b1e9 --- /dev/null +++ b/src/preload.js @@ -0,0 +1,44 @@ +import React from 'react' + +import Player from './Player' +import YouTube from './players/YouTube' +import Vimeo from './players/Vimeo' +import DailyMotion from './players/DailyMotion' + +const PRELOAD_PLAYERS = [ + { + Player: YouTube, + configKey: 'youtube', + url: 'https://www.youtube.com/watch?v=GlCmAC4MHek' + }, + { + Player: Vimeo, + configKey: 'vimeo', + url: 'https://vimeo.com/127250231' + }, + { + Player: DailyMotion, + configKey: 'dailymotion', + url: 'http://www.dailymotion.com/video/xqdpyk' + } +] + +export default function renderPreloadPlayers (url, config) { + const players = [] + + for (let player of PRELOAD_PLAYERS) { + if (!player.Player.canPlay(url) && config[player.configKey].preload) { + players.push( + + ) + } + } + + return players +} diff --git a/src/utils.js b/src/utils.js index e5daf3b0..0352d395 100644 --- a/src/utils.js +++ b/src/utils.js @@ -91,3 +91,24 @@ export function omit (object, ...arrays) { } return output } + +export function callPlayer (method, ...args) { + // Util method for calling a method on this.player + // but guard against errors and console.warn instead + if (!this.player || !this.player[method]) { + let message = `ReactPlayer: ${this.constructor.displayName} player could not call %c${method}%c – ` + if (!this.player) { + message += 'The player was not available' + } else if (!this.player[method]) { + message += 'The method was not available' + } + console.warn(message, 'font-weight: bold', '') + return null + } + return this.player[method](...args) +} + +export function isObject (val) { + if (val === null) return false + return typeof val === 'function' || typeof val === 'object' +}