src/audio/web-audio-player.js
/**
* @title ITSLanguage Javascript Audio
* @overview This is part of the ITSLanguage Javascript SDK to perform audio related functions.
* @copyright (c) 2014 ITSLanguage
* @license MIT
* @author d-centralize
*
* This class fires the same events as the HTML5 Audio does. {@link http://www.w3schools.com/tags/ref_av_dom.asp}
* @private
*/
export default class WebAudioPlayer {
/**
* ITSLanguage WebAudioPlayer non-graphical component.
*
* This player uses the HTML5 Audio component for playback.
*
* @param {?Object} options - Override any of the default settings.
*/
constructor(options) {
this._settings = Object.assign({}, options);
this._initPlayer();
}
_initPlayer() {
this.sound = new window.Audio();
this._pauseIsStop = false;
// The its.AudioPlayer API is based upon the same API calls as the
// HTML5 Audio element itself, therefore, just bubble up all events.
const self = this;
this.sound.addEventListener('playing', () => {
if (self._settings.playingCb) {
self._settings.playingCb();
}
});
this.sound.addEventListener('timeupdate', () => {
if (self._settings.timeupdateCb) {
self._settings.timeupdateCb();
}
});
this.sound.addEventListener('durationchange', () => {
if (self._settings.durationchangeCb) {
self._settings.durationchangeCb();
}
});
this.sound.addEventListener('canplay', () => {
if (self._settings.canplayCb) {
self._settings.canplayCb();
}
});
this.sound.addEventListener('ended', () => {
if (self._settings.endedCb) {
self._settings.endedCb();
}
});
this.sound.addEventListener('pause', () => {
// The HTML5 audio player only has a pause(), no stop().
// To differentiate between the two, a flag is set in case the user
// explicitly stopped (not paused) the audio.
if (self._pauseIsStop === true) {
self._pauseIsStop = false;
if (self._settings.pauseCb) {
self._settings.pauseCb();
}
} else if (self._settings.stoppedCb) {
self._settings.stoppedCb();
}
if (self._settings.playbackStoppedCb) {
self._settings.playbackStoppedCb();
}
});
this.sound.addEventListener('progress', () => {
if (self._settings.progressCb) {
self._settings.progressCb();
}
});
this.sound.addEventListener('error', e => {
switch (e.target.error.code) {
case e.target.error.MEDIA_ERR_ABORTED:
console.error('You aborted the playback.');
break;
case e.target.error.MEDIA_ERR_NETWORK:
console.error(
'A network error caused the audio download to fail.');
break;
case e.target.error.MEDIA_ERR_DECODE:
console.error(
'The audio playback was aborted due to a corruption ' +
'problem or because the media used features your ' +
'browser did not support.');
break;
case e.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
console.error(
'The audio could not be loaded, either because the ' +
'server or network failed or because the format is ' +
'not supported.');
break;
default:
console.error('An unknown error occurred.');
break;
}
if (self._settings.errorCb) {
self._settings.errorCb();
}
});
}
/**
* Preload audio from an URL.
*
* @param {string} url - The URL that contains the audio.
* @param {boolean} [preload=true] - Try preloading metadata and possible some audio (default).
* Set to false to not download anything until playing.
* @param {?Function} loadedCb - The callback that is invoked when the duration of the audio file
* is first known.
*/
load(url, preload, loadedCb) {
preload = preload === undefined ? true : preload;
// Automatically begin buffering the file, even if autoplay is off.
this.sound.autobuffer = Boolean(preload);
// Preloading options:
// none - Do not preload any media.
// Wait for a play event before downloading anything.
// metadata - Preload just the metadata. Grab the start and the end of
// the file via range-request and determine the duration.
// auto - Preload the whole file. Grab the start and the end of the
// file to determine duration, then seek back to the start
// again for the preload proper.
this.sound.preload = preload ? 'auto' : 'none';
const self = this;
if (loadedCb) {
this.sound.addEventListener('durationchange', () => {
console.log('Duration change for ' + url + ' to : ' +
self.sound.duration);
loadedCb(self.sound);
});
}
this.sound.src = url;
}
/**
* Start or continue playback of audio.
*
* @param {?number} position - When position is given, start playing from this position (seconds).
*/
play(position) {
if (position !== undefined) {
if (this.sound.readyState < this.sound.HAVE_METADATA) {
// In case the audio wasn't already preloaded, do it now.
this.sound.preload = 'auto';
console.warn('Playing from a given position is not possible. ' +
'Audio was not yet loaded. Try again.');
} else {
console.debug('Scrub position to: ' + position);
this.sound.currentTime = position;
}
}
this.sound.play();
console.debug('Start playing from position: ' + this.sound.currentTime);
}
/**
* Unload previously loaded audio.
*/
reset() {
this._initPlayer();
}
/**
* Stop playback of audio.
*/
stop() {
// The HTML5 audio player only has a pause(), no stop().
// To differentiate between the two, set a flag.
this.sound.pause();
this.sound.currentTime = 0;
}
/**
* Pause playback of audio.
*/
pause() {
this._pauseIsStop = true;
this.sound.pause();
}
/**
* Start preloading audio.
*/
preload() {
// In case the audio wasn't already preloaded, do it now.
if (this.sound.preload !== 'auto') {
console.info('Start preloading audio.');
this.sound.preload = 'auto';
}
}
/**
* Start playing audio at the given offset.
*
* @param {number} percentage - Start at this percentage (0..100) of the audio stream.
*/
scrub(percentage) {
// In case the audio wasn't already preloaded, do it now.
if (this.sound.readyState < this.sound.HAVE_METADATA) {
this.preload();
console.warn('Scrubbing not possible. Audio was not yet loaded. ' +
'Try again.');
return;
}
const newTime = this.sound.duration / 100 * percentage;
console.log('Moving audio position to: ' + percentage + '%: ' +
newTime + 's of total playing time: ' + this.sound.duration);
this.sound.currentTime = newTime;
}
/**
* Returns the percentage of which the buffer is filled.
*
* @returns {number} Percentage of buffer fill.
*/
getBufferFill() {
if (this.sound.buffered === undefined ||
this.sound.buffered.length === 0) {
// Nothing buffered yet.
return 0;
}
// The fact that there's not one buffer segment is ignored here.
// Truely representing the buffered state requires multiple
// loading bars.
// Usually, when user didn't seek yet, there are two segments:
// Got segment from: 0 to: 187.63999938964844
// Got segment from: 222.44700622558594 to: 228.1140899658203
// The latter is gained when the HTML5 audio component tries to find
// the total audio duration.
// More info:
// http://html5doctor.com/html5-audio-the-state-of-play/#time-ranges
let probableEnd = 0;
for (let i = 0; i < this.sound.buffered.length; i++) {
const start = this.sound.buffered.start(i);
const end = this.sound.buffered.end(i);
// console.log('Got segment from: ' + start + ' to: ' + end);
// Often, the segment that starts from 0 keeps growing and
// indicates -most likely- the biggest buffer.
if (start === 0) {
probableEnd = end;
}
}
// Round up,so the buffer won't get stuck on 99% when
// duration and buffer are equal, except for some far decimal.
const loaded = Math.round(probableEnd * 100 / this.sound.duration);
console.log('Buffer filled to ' + loaded + '%');
return loaded;
}
/**
* Returns the current playing time as offset in seconds from the start.
*
* @returns {number} Time in seconds as offset from the start.
*/
getCurrentTime() {
return this.sound.currentTime;
}
/**
* Returns the total duration in seconds.
*
* @returns {number} Time in seconds of fragment duration. 0 if no audio is loaded.
*/
getDuration() {
let duration = this.sound.duration;
// When no audio is loaded, the duration may be NaN
if (!duration) {
duration = 0;
}
return duration;
}
/**
* Returns state of the player.
*
* @returns {boolean} True when player is currently playing. False when paused or stopped.
*/
isPlaying() {
return !this.sound.paused;
}
setPlaybackRate(rate) {
this.sound.playbackRate = rate;
}
getPlaybackRate() {
return this.sound.playbackRate;
}
/**
* Returns ready state of the player.
*
* @returns {boolean} True when player is ready to start loading data or play. False when no audio is loaded
* or preparing.
*/
canPlay() {
// Either the player is in a valid readyState (preloaded), or
// the player has a source attached and doesn't show any loading error (non-preloaded).
return this.sound.readyState >= this.sound.HAVE_METADATA ||
this.sound.src && !this.sound.error;
}
setAudioVolume(volume) {
this.sound.volume = volume;
}
getAudioVolume() {
return this.sound.volume;
}
}