import React from 'react'; import PropTypes from 'prop-types'; import Sound from 'react-sound'; import SoundCloud from '../utils/soundcloud'; import multiSoundDisabled from '../utils/multi-sound-disabled'; import { getInitialTrackQueueAndIndex } from '../utils/getInitialTrackIndex'; const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2, 3]; const soundProvider = (Player, events) => { class EnhancedPlayer extends React.Component { constructor(props) { super(props); const { volume, cycleTracks, defaultShuffle, shuffleEnabled, } = this.props; this.state = { tracks: [], activeIndex: 0, // Determine active track by index // trackQueue: List of track indexes that represents the order of the playlist // i.e. [0, 1, 2, 3, 4] will play the 1st, 2nd, 3rd, etc track. // [5, 4, 3, 2, 1] will play the tracks reversed. // [4, 2, 0, ...] will play the 5th track first, 3rd second, then the 1st, etc. trackQueue: [], playStatus: Sound.status.STOPPED, position: 0, duration: 0, playbackRate: 1, volume: volume == null ? 100 : volume, cycleTracks, repeatingTrackIndex: null, isMultiSoundDisabled: multiSoundDisabled(), buffering: false, shuffle: shuffleEnabled && defaultShuffle, }; this.playTrack = this.playTrack.bind(this); this.pauseTrack = this.pauseTrack.bind(this); this.togglePlay = this.togglePlay.bind(this); this.nextTrack = this.nextTrack.bind(this); this.prevTrack = this.prevTrack.bind(this); this.setPosition = this.setPosition.bind(this); this.setVolume = this.setVolume.bind(this); this.skipPosition = this.skipPosition.bind(this); this.setPlaybackRate = this.setPlaybackRate.bind(this); this.toggleTracklistCycling = this.toggleTracklistCycling.bind(this); this.toggleShuffle = this.toggleShuffle.bind(this); this.setTrackCycling = this.setTrackCycling.bind(this); this.reverseTracks = this.reverseTracks.bind(this); this.getFinalProps = this.getFinalProps.bind(this); this.onPlaying = this.onPlaying.bind(this); this.onFinishedPlaying = this.onFinishedPlaying.bind(this); } componentDidMount() { const { tracksUrl, soundcloudClientId, reverseTrackOrder, initialTrack, } = this.props; const { shuffle } = this.state; const tracksPromised = fetch(tracksUrl).then(res => res.json()); if (!soundcloudClientId) { tracksPromised.then(tracks => { const { trackQueue, activeIndex } = getInitialTrackQueueAndIndex({ tracks, initialTrack, reverseTrackOrder, shuffle, }); this.setState( { tracks, activeIndex, trackQueue, }, () => { if (reverseTrackOrder) { this.reverseTracks(); } }, ); }); return; } const sc = new SoundCloud(soundcloudClientId); const scTracks = tracksPromised .then(tracks => sc.fetchSoundCloudStreams(tracks)) .catch(err => console.error(err)); // eslint-disable-line no-console // Make sure if SoundCloud fetching fails // we delegate and load our tracks anyway const promiseArray = [tracksPromised, scTracks].map(p => p.catch(error => ({ status: 'error', error, })), ); Promise.all(promiseArray).then(res => { if (res[1].status === 'error') { return this.setState({ tracks: res[0] }); } const tracks = sc.mapStreamsToTracks(...res); const { trackQueue, activeIndex } = getInitialTrackQueueAndIndex({ tracks, initialTrack, reverseTrackOrder, shuffle, }); return this.setState( () => ({ tracks, activeIndex, trackQueue, }), () => { if (reverseTrackOrder) { this.reverseTracks(); } }, ); }); } // Events onPlaying({ duration, position }) { this.setState( () => ({ duration, position }), () => { if (events && events.onPlaying) { events.onPlaying(this.getFinalProps()); } }, ); } onFinishedPlaying() { const { stopOnTrackFinish, delayBetweenTracks = 0 } = this.props; const delayBetweenTracksMs = delayBetweenTracks * 1000; this.setState(() => ({ playStatus: Sound.status.STOPPED })); if (stopOnTrackFinish) { return; } if (events && events.onFinishedPlaying) { setTimeout(() => { events.onFinishedPlaying(this.getFinalProps()); }, delayBetweenTracksMs); } } getFinalProps() { const { tracks, activeIndex } = this.state; const currentTrack = tracks[activeIndex] || {}; return { playTrack: this.playTrack, pauseTrack: this.pauseTrack, togglePlay: this.togglePlay, nextTrack: this.nextTrack, prevTrack: this.prevTrack, setPosition: this.setPosition, skipPosition: this.skipPosition, setPlaybackRate: this.setPlaybackRate, setVolume: this.setVolume, toggleTracklistCycling: this.toggleTracklistCycling, setTrackCycling: this.setTrackCycling, toggleShuffle: this.toggleShuffle, currentTrack, ...this.props, ...this.state, }; } setVolume(volume) { this.setState(() => ({ volume })); } setPosition(position) { this.setState(() => ({ position })); } setTrackCycling(index, event) { if (event) { event.preventDefault(); } const { activeIndex, cycleTracks } = this.state; if (cycleTracks && index != null) { this.toggleTracklistCycling(); } this.setState( ({ repeatingTrackIndex }) => ({ repeatingTrackIndex: repeatingTrackIndex === index ? null : index, }), () => { if (index != null && activeIndex !== index) { this.playTrack(index); } }, ); } setPlaybackRate() { this.setState(({ playbackRate }) => { const currentIndex = PLAYBACK_RATES.findIndex( rate => rate === playbackRate, ); const nextIndex = (PLAYBACK_RATES.length + (currentIndex + 1)) % PLAYBACK_RATES.length; return { playbackRate: PLAYBACK_RATES[nextIndex], }; }); } toggleShuffle() { const { initialTrack, reverseTrackOrder } = this.props; const { tracks } = this.state; this.setState( prev => ({ shuffle: !prev.shuffle, }), () => { this.setState(() => { const { trackQueue } = getInitialTrackQueueAndIndex({ tracks, initialTrack, reverseTrackOrder, shuffle: this.state.shuffle, }); return { trackQueue, }; }); if (this.state.shuffle) { // Shuffle track queue } else { // Unshuffle track queue } }, ); } skipPosition(direction = 1) { const { position } = this.state; const { skipAmount } = this.props; const amount = parseInt(skipAmount, 10) * 1000; this.setPosition(position + amount * direction); } playTrack(index, event) { if (event) { event.preventDefault(); } const { repeatingTrackIndex, isMultiSoundDisabled } = this.state; if (isMultiSoundDisabled) { window.soundManager.pauseAll(); } this.setState(() => ({ activeIndex: index, position: 0, playStatus: Sound.status.PLAYING, })); // Reset repating track index if the track is not the active one. if (index !== repeatingTrackIndex && repeatingTrackIndex != null) { this.setTrackCycling(null); } } pauseTrack(event) { if (event) { event.preventDefault(); } const { playStatus } = this.state; if (playStatus === Sound.status.PLAYING) { this.setState(() => ({ playStatus: Sound.status.PAUSED })); } } togglePlay(index, event) { if (event) { event.preventDefault(); } const { activeIndex } = this.state; if (typeof index === 'number' && index !== activeIndex) { this.playTrack(index); return; } this.setState(({ playStatus, isMultiSoundDisabled }) => { if (playStatus !== Sound.status.PLAYING && isMultiSoundDisabled) { window.soundManager.pauseAll(); } return { playStatus: playStatus === Sound.status.PLAYING ? Sound.status.PAUSED : Sound.status.PLAYING, }; }); } nextTrack() { const { trackQueue, activeIndex } = this.state; const currentQueueIndex = trackQueue.indexOf(activeIndex); const nextQueueIndex = (currentQueueIndex + 1) % trackQueue.length; const nextTrackIndex = trackQueue[nextQueueIndex]; this.playTrack(nextTrackIndex); } prevTrack() { const { trackQueue, activeIndex } = this.state; const currentQueueIndex = trackQueue.indexOf(activeIndex); const prevQueueIndex = (currentQueueIndex + trackQueue.length - 1) % trackQueue.length; const prevTrackIndex = trackQueue[prevQueueIndex]; this.playTrack(prevTrackIndex); } toggleTracklistCycling() { const { repeatingTrackIndex } = this.state; if (repeatingTrackIndex !== null) { this.setTrackCycling(null); } this.setState(state => ({ cycleTracks: !state.cycleTracks, })); } reverseTracks() { this.setState(state => ({ tracks: state.tracks.slice().reverse(), })); } render() { const { tracks, playStatus, position, volume, playbackRate } = this.state; const finalProps = this.getFinalProps(); return (
{tracks.length > 0 && ( this.pauseTrack()} playbackRate={playbackRate} onBufferChange={buffering => { this.setState({ buffering }); }} /> )}
); } } EnhancedPlayer.propTypes = { volume: PropTypes.number, cycleTracks: PropTypes.bool, tracksUrl: PropTypes.string, soundcloudClientId: PropTypes.string, reverseTrackOrder: PropTypes.bool, skipAmount: PropTypes.number, stopOnTrackFinish: PropTypes.bool, delayBetweenTracks: PropTypes.number, initialTrack: PropTypes.number, shuffleEnabled: PropTypes.bool, defaultShuffle: PropTypes.bool, }; return EnhancedPlayer; }; export default soundProvider;