diff --git a/src/Player.js b/src/Player.js
index c8242df7..89883ec1 100644
--- a/src/Player.js
+++ b/src/Player.js
@@ -213,7 +213,7 @@ export default class Player extends Component {
handleEnded = () => {
const { activePlayer, loop, onEnded } = this.props
- if (activePlayer.loopOnEnded && loop) {
+ if ((activePlayer?._result?.loopOnEnded || activePlayer?.loopOnEnded) && loop) {
this.seekTo(0)
}
if (!loop) {
diff --git a/src/demo/App.js b/src/demo/App.js
index de96a7d2..8f6a01f4 100644
--- a/src/demo/App.js
+++ b/src/demo/App.js
@@ -282,6 +282,13 @@ class App extends Component {
{this.renderLoadButton('https://www.youtube.com/playlist?list=PLogRWNZ498ETeQNYrOlqikEML3bKJcdcx', 'Playlist')}
+
SoundCloud |
diff --git a/src/patterns.js b/src/patterns.js
index 36f6ab7c..cc73bb52 100644
--- a/src/patterns.js
+++ b/src/patterns.js
@@ -1,6 +1,7 @@
import { isMediaStream, isBlobUrl } from './utils'
export const MATCH_URL_YOUTUBE = /(?:youtu\.be\/|youtube(?:-nocookie)?\.com\/(?:embed\/|v\/|watch\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))((\w|-){11})|youtube\.com\/playlist\?list=|youtube\.com\/user\//
+export const MATCH_URL_SPOTIFY = /spotify.+$/
export const MATCH_URL_SOUNDCLOUD = /(?:soundcloud\.com|snd\.sc)\/[^.]+$/
export const MATCH_URL_VIMEO = /vimeo\.com\/(?!progressive_redirect).+/
export const MATCH_URL_FACEBOOK = /^https?:\/\/(www\.)?facebook\.com.*\/(video(s)?|watch|story)(\.php?|\/).+$/
@@ -50,6 +51,7 @@ export const canPlay = {
}
return MATCH_URL_YOUTUBE.test(url)
},
+ spotify: url => MATCH_URL_SPOTIFY.test(url),
soundcloud: url => MATCH_URL_SOUNDCLOUD.test(url) && !AUDIO_EXTENSIONS.test(url),
vimeo: url => MATCH_URL_VIMEO.test(url) && !VIDEO_EXTENSIONS.test(url) && !HLS_EXTENSIONS.test(url),
facebook: url => MATCH_URL_FACEBOOK.test(url) || MATCH_URL_FACEBOOK_WATCH.test(url),
diff --git a/src/players/Spotify.js b/src/players/Spotify.js
new file mode 100644
index 00000000..dbef7571
--- /dev/null
+++ b/src/players/Spotify.js
@@ -0,0 +1,137 @@
+import React, { Component } from 'react'
+import { getSDK, callPlayer } from '../utils'
+import { canPlay } from '../patterns'
+
+const SDK_URL = 'https://open.spotify.com/embed-podcast/iframe-api/v1'
+const SDK_GLOBAL = 'SpotifyIframeApi'
+const SDK_GLOBAL_READY = 'SpotifyIframeApi'
+
+export default class Spotify extends Component {
+ static displayName = 'Spotify'
+ static loopOnEnded = true
+ static canPlay = canPlay.spotify
+ callPlayer = callPlayer
+ duration = null
+ currentTime = null
+ totalTime = null
+ player = null
+
+ componentDidMount () {
+ this.props.onMount && this.props.onMount(this)
+ }
+
+ load (url) {
+ if (window[SDK_GLOBAL] && !this.player) {
+ this.initializePlayer(window[SDK_GLOBAL], url)
+ return
+ } else if (this.player) {
+ this.callPlayer('loadUri', this.props.url)
+ return
+ }
+
+ window.onSpotifyIframeApiReady = (IFrameAPI) => this.initializePlayer(IFrameAPI, url)
+ getSDK(SDK_URL, SDK_GLOBAL, SDK_GLOBAL_READY)
+ }
+
+ initializePlayer = (IFrameAPI, url) => {
+ if (!this.container) return
+
+ const options = {
+ width: '100%',
+ height: '100%',
+ uri: url
+ }
+ const callback = (EmbedController) => {
+ this.player = EmbedController
+ this.player.addListener('playback_update', this.onStateChange)
+ this.player.addListener('ready', this.props.onReady)
+ }
+ IFrameAPI.createController(this.container, options, callback)
+ }
+
+ onStateChange = (event) => {
+ const { data } = event
+ const { onPlay, onPause, onBuffer, onBufferEnd, onEnded } = this.props
+
+ if (data.position >= data.duration && data.position && data.duration) {
+ onEnded()
+ }
+ if (data.isPaused === true) onPause()
+ if (data.isPaused === false && data.isBuffering === false) {
+ this.currentTime = data.position
+ this.totalTime = data.duration
+ onPlay()
+ onBufferEnd()
+ }
+ if (data.isBuffering === true) onBuffer()
+ }
+
+ play () {
+ this.callPlayer('resume')
+ }
+
+ pause () {
+ this.callPlayer('pause')
+ }
+
+ stop () {
+ this.callPlayer('destroy')
+ }
+
+ seekTo (amount) {
+ this.callPlayer('seek', amount)
+ if (!this.props.playing) {
+ this.pause()
+ } else {
+ this.play()
+ }
+ }
+
+ setVolume (fraction) {
+ // No volume support
+ }
+
+ mute () {
+ // No volume support
+ }
+
+ unmute () {
+ // No volume support
+ }
+
+ setPlaybackRate (rate) {
+ // No playback rate support
+ }
+
+ setLoop (loop) {
+ // No loop support
+ }
+
+ getDuration () {
+ return this.totalTime / 1000
+ }
+
+ getCurrentTime () {
+ return this.currentTime / 1000
+ }
+
+ getSecondsLoaded () {
+ // No seconds loaded support
+ }
+
+ ref = container => {
+ this.container = container
+ }
+
+ render () {
+ const style = {
+ width: '100%',
+ height: '100%'
+ }
+ return (
+
+ )
+ }
+}
diff --git a/src/players/index.js b/src/players/index.js
index 0513b47c..8058f053 100644
--- a/src/players/index.js
+++ b/src/players/index.js
@@ -9,6 +9,12 @@ export default [
canPlay: canPlay.youtube,
lazyPlayer: lazy(() => import(/* webpackChunkName: 'reactPlayerYouTube' */'./YouTube'))
},
+ {
+ key: 'spotify',
+ name: 'Spotify',
+ canPlay: canPlay.spotify,
+ lazyPlayer: lazy(() => import(/* webpackChunkName: 'reactPlayerSpotify' */'./Spotify'))
+ },
{
key: 'soundcloud',
name: 'SoundCloud',
diff --git a/test/players/Spotify.js b/test/players/Spotify.js
new file mode 100644
index 00000000..1113f961
--- /dev/null
+++ b/test/players/Spotify.js
@@ -0,0 +1,110 @@
+import React from 'react'
+import test from 'ava'
+import sinon from 'sinon'
+import { shallow } from 'enzyme'
+import testPlayerMethods from '../helpers/testPlayerMethods'
+import * as utils from '../../src/utils'
+import Spotify from '../../src/players/Spotify'
+
+global.window = {}
+const TEST_URL = 'spotify:track:0KhB428j00T8lxKCpHweKw'
+
+testPlayerMethods(Spotify, {
+ play: 'resume',
+ pause: 'pause',
+ stop: 'destroy',
+ seekTo: 'seek'
+})
+
+test('load() - Player not initialized and sdk not loaded', t => {
+ class MockPlayer {
+ constructor (container, options) {
+ t.true(container === 'mock-container')
+ setTimeout(options.events.onReady, 100)
+ }
+ }
+ const getSDK = sinon.stub(utils, 'getSDK').resolves({ MockPlayer })
+
+ const instance = shallow(
+
+ ).instance()
+ instance.container = 'mock-container'
+ instance.load(TEST_URL)
+ t.truthy(global.window.onSpotifyIframeApiReady)
+ t.true(getSDK.calledOnce)
+ getSDK.restore()
+})
+
+test('load() - sdk already loaded', t => {
+ const getSDK = sinon.stub(utils, 'getSDK')
+ window.SpotifyIframeApi = true
+
+ const instance = shallow(
+
+ ).instance()
+ const initializePlayer = sinon.stub(instance, 'initializePlayer')
+ instance.container = 'mock-container'
+ instance.load(TEST_URL)
+ t.false(getSDK.calledOnce)
+ t.true(initializePlayer.calledOnce)
+ getSDK.restore()
+ initializePlayer.restore()
+})
+
+test('load() - player already initialized', t => {
+ const getSDK = sinon.stub(utils, 'getSDK')
+ window.SpotifyIframeApi = true
+
+ const instance = shallow(
+
+ ).instance()
+ instance.player = true
+ const initializePlayer = sinon.stub(instance, 'initializePlayer')
+ const callPlayer = sinon.stub(instance, 'callPlayer')
+ instance.container = 'mock-container'
+
+ instance.load(TEST_URL)
+ t.false(getSDK.calledOnce)
+ t.false(initializePlayer.calledOnce)
+ t.true(callPlayer.calledOnce)
+ getSDK.restore()
+ initializePlayer.restore()
+ callPlayer.restore()
+})
+
+test('onStateChange() - play', t => {
+ const called = {}
+ const onPlay = () => { called.onPlay = true }
+ const onBufferEnd = () => { called.onBufferEnd = true }
+ const instance = shallow().instance()
+ instance.onStateChange({ data: { isPaused: false, isBuffering: false } })
+ t.true(called.onPlay && called.onBufferEnd)
+})
+
+test('onStateChange() - pause', async t => {
+ const onPause = () => t.pass()
+ const instance = shallow().instance()
+ instance.onStateChange({ data: { isPaused: true } })
+})
+
+test('onStateChange() - buffer', async t => {
+ const onBuffer = () => t.pass()
+ const instance = shallow().instance()
+ instance.onStateChange({ data: { isBuffering: true } })
+})
+
+test('onStateChange() - ended', async t => {
+ const onEnded = () => t.pass()
+ const instance = shallow( {}} onBufferEnd={() => {}} />).instance()
+ instance.onStateChange({ data: { duration: 100, position: 105, isPaused: false, isBuffering: false } })
+})
+
+test('render()', t => {
+ const wrapper = shallow()
+ const style = { width: '100%', height: '100%' }
+ t.true(wrapper.contains(
+
+ ))
+})
diff --git a/types/spotify.d.ts b/types/spotify.d.ts
new file mode 100644
index 00000000..4e2683cd
--- /dev/null
+++ b/types/spotify.d.ts
@@ -0,0 +1,5 @@
+import BaseReactPlayer, { BaseReactPlayerProps } from './base'
+
+export interface SpotifyPlayerProps extends BaseReactPlayerProps {}
+
+export default class SpotifyPlayer extends BaseReactPlayer {}
|