const EventEmitter2 = require('eventemitter2').EventEmitter2
const arrayBufferToAudioBuffer = require('arraybuffer-to-audiobuffer');
const {scaleLinear} = require('d3-scale')
const randomInt = require('random-int')
const PlayerEventTypes = require('./constants/PlayerEventTypes')
/**
* Class reprensenting a Player
* @namespace Player
* @example
* const player = new Player()
*/
class Player {
/**
* @desc Create a Player
*/
constructor() {
const ee = new EventEmitter2({
wildcard: true
})
this.emit = ee.emit.bind(ee)
/**
* @method on
* @memberof Player
* @instance
* @desc Listen for player events
* @param {String} eventName - name of even
* @param {Function} callback - called when the event occurs
* @example
* player.on(Player.EventTypes.PLAY, () => {
*
* })
* @example
* player.on(Player.EventTypes.PLAYBACK_RATE, () => {
*
* })
*/
this.on = ee.on.bind(ee)
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this._queue = []
this._currentQueueIndex = 0
this._currentSource = null
this._currentBuffer = null
this._context = new AudioContext()
this._playbackRate = 1
this._playbackRateScale = scaleLinear()
.domain([0.75, 2])
.range([0.75, 2])
.clamp(true)
this._volume = 1
this._maxVolume = 1
this._volumeScale = scaleLinear()
.domain([0, 1])
.range([0, this._maxVolume])
.clamp(true)
this._maxVolumeScale = scaleLinear()
.domain([0, 1])
.range([0, 1])
.clamp(true)
this.random = false
this.repeat = false
this.isPlaying = false
this.isReady = true
this._log('Player ready');
this.emit(PlayerEventTypes.READY);
}
_log(type, message) {
if (type && !message) {
message = type;
type = 'log';
}
setTimeout(() => {
this.emit(PlayerEventTypes.LOG, message);
}, 0);
if (this._debug) {
console[type](message);
}
}
/**
* getCurrentUrl
* @desc Get currently playing audio source url
* @return {String}
* @example
* const url = player.getCurrentUrl()
*/
getCurrentUrl() {
const source = this._queue[this._currentQueueIndex]
if (typeof source === 'string') {
return source
}
return null
}
/**
* emptyQueue
* @desc Empties queue
* @return {Promise}
* @example
* player.emptyQueue()
*/
emptyQueue() {
return new Promise((resolve, reject) => {
this._queue = [];
this._currentQueueIndex = 0
this._audio = null;
this._currentBuffer = null;
this._currentSource = null;
this.stop()
this._log('Empty queue');
this.emit(PlayerEventTypes.EMPTY_QUEUE);
resolve();
});
}
/**
* enqueue
* @desc Enqueues an audio source to play
* @param {(DataView|Uint8Array|AudioBuffer|ArrayBuffer|String)} source - an audio source to play
* @return {Promise}
* @example
* const url = 'https://example.com/audio.mp3'
*
* player.enqueue(url)
* @example
* player.enqueue(audioBuffer)
* @example
* player.enqueue(arrayBuffer)
* @example
* player.enqueue(blob)
*/
enqueue(source) {
return new Promise((resolve, reject) => {
if (Array.isArray(source)) {
return source.forEach(x => this.enqueue(x))
}
if (!source) {
const error = new Error('argument cannot be empty.');
this._log(error);
return reject(error);
}
const stringType = ({}).toString.call(source).replace(/\[.*\s(\w+)\]/, '$1');
const proceed = (audioBuffer) => {
this._queue.push(audioBuffer);
this._log('Enqueue audio');
this.emit(PlayerEventTypes.ENQUEUE);
return resolve(audioBuffer);
};
if (stringType === 'DataView' || stringType === 'Uint8Array') {
return arrayBufferToAudioBuffer(source.buffer, this._context)
.then(proceed);
} else if (stringType === 'AudioBuffer') {
return proceed(source);
} else if (stringType === 'ArrayBuffer') {
return arrayBufferToAudioBuffer(source, this._context)
.then(proceed);
} else if (stringType === 'String') {
return proceed(source);
} else {
const error = new Error('Invalid type.');
this.emit('error', error);
return reject(error);
}
});
}
/**
* deqeue
* @desc Deques an audio source from queue
* @return {Promise}
* @example
* player.deque()
*/
deque() {
return new Promise((resolve, reject) => {
if (this.random) {
this._currentQueueIndex = randomInt(0, this._queue.length-1)
}
const source = this._queue[this._currentQueueIndex]
if (source) {
this._log('Deque audio');
this.emit(PlayerEventTypes.DEQUE);
return resolve(source);
}
return reject(new Error('no source to play'));
});
}
/**
* play
* @desc Plays current audio source in queue
* @return {Promise}
* @example
* player.play()
*/
play() {
return new Promise((resolve, reject) => {
this.isPlaying = true
// if paused then resume
if (this._context.state === 'suspended') {
this._context.resume();
this._log('Play audio');
this.emit(PlayerEventTypes.PLAY);
resolve();
// if paused then resume
} else if (this._audio && this._audio.paused) {
this._log('Play audio');
this.emit(PlayerEventTypes.PLAY);
this._audio.play();
resolve();
// if paused then resume
} else {
return this.deque()
.then(audioBuffer => {
this._log('Play audio');
this.emit(PlayerEventTypes.PLAY);
if (typeof audioBuffer === 'string') {
return this.playUrl(audioBuffer);
}
return this.playAudioBuffer(audioBuffer);
})
.then(resolve);
}
});
}
/**
* playQueue
* @desc Start playing audio sources in queue
* @return {Promise}
* @example
* player.playQueue()
*/
playQueue() {
return this.play().then(() => {
if (this._queue.length) {
return this.playQueue();
}
});
}
/**
* stop
* @desc Stop playing current audio source
* @return {Promise}
* @example
* player.stop()
*/
stop() {
return new Promise((resolve, reject) => {
this.isPlaying = false
if (this._currentSource) {
this._currentSource.onended = function() {};
this._currentSource.stop();
}
if (this._audio) {
this._audio.onended = function() {};
this._audio.currentTime = 0;
this._audio.pause();
}
this._log('Stop audio');
this.emit(PlayerEventTypes.STOP);
});
}
/**
* pause
* @desc Pause playing current audio source
* @return {Promise}
* @example
* player.pause()
*/
pause() {
return new Promise((resolve, reject) => {
this.isPlaying = false
if (this._currentSource && this._context.state === 'running') {
this._context.suspend();
}
if (this._audio) {
this._audio.pause();
}
this._log('Pause audio');
this.emit(PlayerEventTypes.PAUSE);
});
}
/**
* replay
* @desc Replay current audio source
* @return {Promise}
* @example
* player.replay()
*/
replay() {
return new Promise((resolve, reject) => {
if (this._currentBuffer) {
this._log('Replay audio');
this.emit(PlayerEventTypes.REPLAY);
if (this._context.state === 'suspended') {
this._context.resume();
}
if (this._currentSource) {
this._currentSource.stop();
this._currentSource.onended = function() {};
}
return this.playAudioBuffer(this._currentBuffer);
} else if (this._audio) {
this._log('Replay audio');
this.emit(PlayerEventTypes.REPLAY);
return this.playUrl(this._audio.src);
} else {
const error = new Error('No audio source loaded.');
this.emit('error', error)
reject();
}
});
}
/**
* playBlob
* @desc Play an audio Blob
* @param {Blob} blob - audio blob
* @return {Promise}
* @example
* const blob = new Blob([dataView], {
* type: 'audio/wav'
* })
*
* player.playBlob(blob)
*/
playBlob(blob) {
return new Promise((resolve, reject) => {
if (!blob) {
reject();
}
const objectUrl = URL.createObjectURL(blob);
const audio = new Audio();
audio.src = objectUrl;
this._currentBuffer = null;
this._currentSource = null;
this._audio = audio;
this._audio.playbackRate = this._playbackRate
audio.onended = () => {
this._log('Audio ended');
this.emit(PlayerEventTypes.ENDED);
resolve();
};
audio.onerror = (error) => {
this.emit('error', error);
reject(error);
};
audio.onload = (event) => {
URL.revokeObjectUrl(objectUrl);
};
audio.play();
});
}
/**
* playAudioBuffer
* @desc Play an AudioBuffer
* @param {AudioBuffer} audioBuffer - an AudioBuffer
* @return {Promise}
* @example
* player.playAudioBuffer(audioBuffer)
*/
playAudioBuffer(buffer) {
return new Promise((resolve, reject) => {
if (!buffer) {
reject();
}
const source = this._context.createBufferSource();
source.buffer = buffer;
source.connect(this._context.destination);
source.start(0);
this._currentBuffer = buffer;
this._currentSource = source;
this._audio = null;
source.onended = (event) => {
this._log('Audio ended');
this.emit(PlayerEventTypes.ENDED);
resolve();
};
source.onerror = (error) => {
this.emit('error', error);
reject(error);
};
});
}
/**
* getCurrentAudioBuffer
* @desc Return current audio buffer playing
* @return {AudioBuffer}
* @example
* player.getCurrentAudioBuffer()
*/
getCurrentAudioBuffer() {
return Promise.resolve(this._currentBuffer)
}
/**
* playUrl
* @desc Play an MP3 url
* @param {String} url - MP3 url
* @return {Promise}
* @example
* const url = 'https://example.com/audio.mp3'
*
* player.playUrl(url)
*/
playUrl(url) {
return new Promise((resolve, reject) => {
const usingAudioTag = false
if (usingAudioTag) {
const audio = new Audio();
audio.src = url;
this._currentBuffer = null;
this._currentSource = null;
this._audio = audio;
this._audio.playbackRate = this._playbackRate
audio.onended = (event) => {
this._log('Audio ended');
this.emit(PlayerEventTypes.ENDED);
resolve();
};
audio.onerror = (error) => {
this.emit('error', error);
reject(error);
};
audio.play();
} else {
return this.getAudioDataFromUrl(url)
.then(buffer => {
return this.playAudioBuffer(buffer)
})
}
});
}
/**
* getAudioDataFromUrl
* @desc Get the binary audio data from an audio source url in ArrayBuffer form
* @param {String} url - audio source url
* @return {Promise} arrayBuffer
* @example
* const url = 'https://example.com/audio.mp3'
*
* player.getAudioDataFromUrl()
* .then(arrayBuffer => {
*
* })
*/
getAudioDataFromUrl(url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest()
request.open('GET', url, true)
request.responseType = 'arraybuffer'
request.onload = () => {
const audioData = request.response
this._context.decodeAudioData(audioData, (buffer) => {
resolve(buffer)
},
(error) => {
reject(error)
})
}
request.send()
})
}
/**
* next
* @desc Play next audio source in queue
* @return {Promise}
* @example
* player.next()
*/
next() {
return new Promise((resolve, reject) => {
const isLast = (this._currentQueueIndex === this._queue.length-1)
if (isLast) {
this._currentQueueIndex = -1
}
const source = this._queue[this._currentQueueIndex + 1]
if (source) {
this._currentQueueIndex++
this._log('Next audio');
this.emit(PlayerEventTypes.NEXT);
const continuePlaying = this.isPlaying
this.stop()
this._audio = null
resolve(source)
if (continuePlaying) {
this.play()
}
}
return reject(new Error('no source to play'));
});
}
/**
* previous
* @desc Play previous audio source in queue
* @return {Promise}
* @example
* player.previous()
*/
previous() {
return new Promise((resolve, reject) => {
const source = this._queue[this._currentQueueIndex - 1]
if (source) {
this._currentQueueIndex--
this._log('Previous audio');
this.emit(PlayerEventTypes.PREVIOUS);
const continuePlaying = this.isPlaying
this.stop()
this._audio = null
resolve(source)
if (continuePlaying) {
this.play()
}
}
return reject(new Error('no source to play'))
});
}
/**
* setRandom
* @desc Enable to disable random playback
* @param {Boolean} enabled - boolean to enable random playback
* @return {Promise}
* @example
* player.setRandom(true)
*/
setRandom(enabled) {
this._log(`Set random: ${enabled}`)
this.emit(PlayerEventTypes.RANDOM)
this.random = enabled
}
/**
* setRepeat
* @desc Enable to disable repeat mode. Repeat mode replays the audio sources once the entire queue has finished playing.
* @param {Boolean} enabled - boolean to enable repeat mode
* @return {Promise}
* @example
* player.setRepeat(true)
*/
setRepeat(enabled) {
this._log(`Set repeat: ${enabled}`)
this.emit(PlayerEventTypes.REPEAT)
this.repeat = enabled
}
/**
* hasNext
* @desc Return true if there's an audio source to play next in queue
* @return {Boolean} hasNext
* @example
* const hasNext = player.hasNext()
*/
hasNext() {
//const hasNext = this._queue.length > 1 && this._currentQueueIndex < this._queue.length-1
const hasNext = this._queue.length > 1
return hasNext
}
/**
* hasPrevious
* @desc Return true if there's a previous audio source in queue
* @return {Boolean} hasPrevious
* @example
* const hasPrevious = player.hasPrevious()
*/
hasPrevious() {
const hasPrevious = this._queue.length > 1 && this._currentQueueIndex > 0
return hasPrevious
}
/**
* setPlaybackRate
* @desc Set the plaback rate speed, range 0.75-2
* @param {Number} playbackRate - new playback rate
* @return {Promise}
* @example
* player.setPlaybackRate(2)
*/
setPlaybackRate(rate) {
return new Promise((resolve, reject) => {
this._playbackRate = this._playbackRateScale(rate)
if (this._audio) {
this._audio.playbackRate = this._playbackRate
}
if (this._currentSource) {
this._currentSource.playbackRate.value = this._playbackRate
}
this._log(`Set playback rate: ${this._playbackRate}`);
this.emit(PlayerEventTypes.PLAYBACK_RATE, this._playbackRate);
resolve(this._playbackRate)
})
}
/**
* getPlaybackRate
* @desc Get the current plaback rate speed
* @return {Number} playback rate speed
* @example
* const playbackRate = player.getPlaybackRate()
*/
getPlaybackRate() {
return this._playbackRate
}
/**
* setVolume
* @desc Set volume, range 0-1
* @param {Number} volume - volume value
* @return {Promise}
* @example
* player.setVolume(0.9)
*/
setVolume(volume) {
return new Promise((resolve, reject) => {
this._volume = this._volumeScale(volume)
if (this._audio) {
this._audio.volume = this._volume;
}
this._log(`Set volume: ${this._volume}`);
this.emit(PlayerEventTypes.VOLUME, this._volume);
resolve(this._volume)
})
}
/**
* getVolume
* @desc Get current volume value
* @return {Number} volume - current volume value
* @example
* player.getVolume()
*/
getVolume() {
return this._volume
}
/**
* setMaxVolume
* @desc Set the maximum volume limit. For example if max volume is set to 0.6, then when volume is scaled from 0-0.6, meaning that volume level at 1 will play at 0.6
* @param {Number} maxVolume - max volume, range 0-1
* @return {Promise}
* @example
* player.setMaxVolume(0.8)
*/
setMaxVolume(maxVolume) {
return new Promise((resolve, reject) => {
this._maxVolume = this._maxVolumeScale(maxVolume)
this._volumeScale = scaleLinear()
.domain([0, 1])
.range([0, this._maxVolume])
.clamp(true)
this.setVolume(this._volume)
this._log(`Set max volume: ${this._volume}`);
this.emit(PlayerEventTypes.MAX_VOLUME, this._maxVolume);
resolve()
})
}
/**
* getMaxVolume
* @desc Get max volume value
* @return {Number} maxVolume - max volume value
* @example
* player.getMaxVolume()
*/
getMaxVolume() {
return this._maxVolume
}
/**
* setMuted
* @desc Set volume level to muted
* @param {Boolean} enabled - boolean to enable mute
* @return {Promise}
* @example
* player.setMuted(true)
*/
// TODO
setMuted(enabled) {
this.muted = !!enabled
return Promise.resolve(this.muted)
}
/**
* getCurrentTime
* @desc Return elapsed time in seconds since the audio source started playing
* @return {Number} time - current time
* @example
* player.getCurrentTime()
*/
getCurrentTime() {
if (this._context) {
return this._context.currentTime
}
if (this._audio) {
return this._audio.currentTime
}
return 0
}
/**
* seekTo
* @desc Seek to a specified time in audio source
* @param {Number} seconds - number of seconds to seek to
* @return {Promise}
* @example
* const seconds = 45
*
* player.seekTo(seconds)
*/
seekTo(seconds) {
if (this._context) {
this._context.currentTime = seconds
}
if (this._audio) {
this._audio.currentTime = seconds
}
return Promise.resolve()
}
/**
* getDuration
* @desc Get duration of audio source
* @return {Number} duration - duration of audio source
* @example
* player.getDuration()
*/
getDuration() {
return 0
}
/**
* @static EventTypes
* @type {Object}
* @desc Return event types
* @return {Object} eventTypes - all player event types
* @example
* const EventTypes = Player.EventTypes
*
* {
LOG: 'log',
ERROR: 'error',
READY: 'ready',
PLAY: 'play',
REPLAY: 'replay',
PAUSE: 'pause',
STOP: 'pause',
NEXT: 'next',
PREVIOUS: 'previous',
RANDOM: 'random',
REPEAT: 'repeat',
PLAYBACK_RATE: 'playbackRate',
VOLUME: 'volume',
MAX_VOLUME: 'maxVolume',
MUTED: 'muted',
ENDED: 'ended',
ENQUEUE: 'enqueue',
DEQUE: 'deque',
EMPTY_QUEUE: 'emptyQueue',
STATE_CHANGE: 'stateChange'
* }
*/
static get EventTypes() {
return PlayerEventTypes
}
}
module.exports = Player