@checksub_team/peaks_timeline
Version:
JavaScript UI component for displaying audio waveforms
456 lines (381 loc) • 12.5 kB
JavaScript
/**
* @file
*
* Defines the {@link WaveformBuilder} class.
*
* @module waveform-builder
*/
define([
'waveform-data',
'./utils'
], function(
WaveformData,
Utils) {
'use strict';
var isXhr2 = ('withCredentials' in new XMLHttpRequest());
/**
* Creates and returns a WaveformData object, either by requesting the
* waveform data from the server, or by creating the waveform data using the
* Web Audio API.
*
* @class
* @alias WaveformBuilder
*
* @param {Peaks} peaks
*/
function WaveformBuilder(peaks) {
this._peaks = peaks;
}
/**
* Options for requesting remote waveform data.
*
* @typedef {Object} RemoteWaveformDataOptions
* @global
* @property {String=} arraybuffer
* @property {String=} json
*/
/**
* Options for supplying local waveform data.
*
* @typedef {Object} LocalWaveformDataOptions
* @global
* @property {ArrayBuffer=} arraybuffer
* @property {Object=} json
*/
/**
* Options for the Web Audio waveform builder.
*
* @typedef {Object} WaveformBuilderWebAudioOptions
* @global
* @property {AudioContext} audioContext
* @property {AudioBuffer=} audioBuffer
* @property {Number=} scale
* @property {Boolean=} multiChannel
*/
/**
* Options for [WaveformBuilder.init]{@link WaveformBuilder#init}.
*
* @typedef {Object} WaveformBuilderInitOptions
* @global
* @property {RemoteWaveformDataOptions=} dataUri
* @property {LocalWaveformDataOptions=} waveformData
* @property {WaveformBuilderWebAudioOptions=} webAudio
* @property {Boolean=} withCredentials
* @property {Array<Number>=} zoomLevels
*/
/**
* Callback for receiving the waveform data.
*
* @callback WaveformBuilderInitCallback
* @global
* @param {Error} error
* @param {WaveformData} waveformData
*/
/**
* Loads or creates the waveform data.
*
* @private
* @param {WaveformBuilderInitOptions} options
* @param {WaveformBuilderInitCallback} callback
*/
WaveformBuilder.prototype.init = function(options, callback) {
if ((options.dataUri && (options.webAudio || options.audioContext)) ||
(options.waveformData && (options.webAudio || options.audioContext)) ||
(options.dataUri && options.waveformData)) {
// eslint-disable-next-line max-len
callback(new TypeError('Peaks.init(): You may only pass one source (webAudio, dataUri, or waveformData) to render waveform data.'));
return;
}
if (options.audioContext) {
// eslint-disable-next-line max-len
this._peaks.options.deprecationLogger('Peaks.init(): The audioContext option is deprecated, please pass a webAudio object instead');
options.webAudio = {
audioContext: options.audioContext
};
}
if (options.objectUrl) {
return this._buildWaveformDataFromObjectUrl(options, callback);
}
else if (options.dataUri) {
return this._getRemoteWaveformData(options, callback);
}
else if (options.waveformData) {
return this._buildWaveformFromLocalData(options, callback);
}
else if (options.webAudio) {
if (options.webAudio.audioBuffer) {
return this._buildWaveformDataFromAudioBuffer(options, callback);
}
else {
return this._buildWaveformDataUsingWebAudio(options, callback);
}
}
else {
// eslint-disable-next-line max-len
callback(new Error('Peaks.init(): You must pass an audioContext, or dataUri, or waveformData to render waveform data'));
}
};
/* eslint-disable max-len */
/**
* Fetches waveform data, based on the given options.
*
* @private
* @param {Object} options
* @param {String|Object} options.dataUri
* @param {String} options.dataUri.arraybuffer Waveform data URL
* (binary format)
* @param {String} options.dataUri.json Waveform data URL (JSON format)
* @param {String} options.defaultUriFormat Either 'arraybuffer' (for binary
* data) or 'json'
* @param {WaveformBuilderInitCallback} callback
*
* @see Refer to the <a href="https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md">data format documentation</a>
* for details of the binary and JSON waveform data formats.
*/
/* eslint-enable max-len */
WaveformBuilder.prototype._getRemoteWaveformData = function(options, callback) {
var self = this;
var dataUri = null;
var requestType = null;
var url;
if (Utils.isObject(options.dataUri)) {
dataUri = options.dataUri;
}
else if (Utils.isString(options.dataUri)) {
// Backward compatibility
dataUri = {};
dataUri[options.dataUriDefaultFormat || 'json'] = options.dataUri;
}
else {
callback(new TypeError('Peaks.init(): The dataUri option must be an object'));
return;
}
['ArrayBuffer', 'JSON'].some(function(connector) {
if (window[connector]) {
requestType = connector.toLowerCase();
url = dataUri[requestType];
return Boolean(url);
}
});
if (!url) {
callback(new Error('Peaks.init(): Unable to determine a compatible dataUri format for this browser'));
return;
}
var xhr = self._createXHR(url, requestType, options.withCredentials, function(event) {
if (this.readyState !== 4) {
return;
}
if (this.status !== 200) {
callback(
new Error('Unable to fetch remote data. HTTP status ' + this.status)
);
return;
}
var waveformData = WaveformData.create(event.target.response);
if (waveformData.channels !== 1 && waveformData.channels !== 2) {
callback(new Error('Peaks.init(): Only mono or stereo waveforms are currently supported'));
return;
}
callback(null, waveformData);
},
function() {
callback(new Error('XHR Failed'));
});
xhr.send();
};
/* eslint-disable max-len */
/**
* Creates a waveform from given data, based on the given options.
*
* @private
* @param {Object} options
* @param {Object} options.waveformData
* @param {ArrayBuffer} options.waveformData.arraybuffer Waveform data (binary format)
* @param {Object} options.waveformData.json Waveform data (JSON format)
* @param {WaveformBuilderInitCallback} callback
*
* @see Refer to the <a href="https://github.com/bbc/audiowaveform/blob/master/doc/DataFormat.md">data format documentation</a>
* for details of the binary and JSON waveform data formats.
*/
/* eslint-enable max-len */
WaveformBuilder.prototype._buildWaveformFromLocalData = function(options, callback) {
var waveformData = null;
var data = null;
if (Utils.isObject(options.waveformData)) {
waveformData = options.waveformData;
}
else {
callback(new Error('Peaks.init(): The waveformData option must be an object'));
return;
}
if (Utils.isObject(waveformData.json)) {
data = waveformData.json;
}
else if (Utils.isArrayBuffer(waveformData.arraybuffer)) {
data = waveformData.arraybuffer;
}
if (!data) {
callback(new Error('Peaks.init(): Unable to determine a compatible waveformData format'));
return;
}
try {
var createdWaveformData = WaveformData.create(data);
if (createdWaveformData.channels !== 1 && createdWaveformData.channels !== 2) {
callback(new Error('Peaks.init(): Only mono or stereo waveforms are currently supported'));
return;
}
callback(null, createdWaveformData);
}
catch (err) {
callback(err);
}
};
/**
* Creates waveform data using the Web Audio API.
*
* @private
* @param {Object} options
* @param {AudioContext} options.webAudio.audioContext
* @param {WaveformBuilderInitCallback} callback
*/
WaveformBuilder.prototype._buildWaveformDataUsingWebAudio = function(options, callback) {
var self = this;
var audioContext = window.AudioContext || window.webkitAudioContext;
if (!(options.webAudio.audioContext instanceof audioContext)) {
callback(new TypeError('Peaks.init(): The webAudio.audioContext option must be a valid AudioContext'));
return;
}
var webAudioOptions = options.webAudio;
if (webAudioOptions.scale !== options.zoomLevels[0]) {
webAudioOptions.scale = options.zoomLevels[0];
}
// If the media element has already selected which source to play, its
// currentSrc attribute will contain the source media URL. Otherwise,
// we wait for a canplay event to tell us when the media is ready.
var mediaSourceUrl = self._peaks.player.getCurrentSource();
if (mediaSourceUrl) {
self._requestAudioAndBuildWaveformData(
mediaSourceUrl,
webAudioOptions,
options.withCredentials,
callback
);
}
else {
self._peaks.once('player_canplay', function(player) {
self._requestAudioAndBuildWaveformData(
player.getCurrentSource(),
webAudioOptions,
options.withCredentials,
callback
);
});
}
};
/**
* Creates waveform data using the Web Audio API.
*
* @private
* @param {Object} options
* @param {WaveformBuilderInitCallback} callback
*/
WaveformBuilder.prototype._buildWaveformDataFromObjectUrl = function(options, callback) {
var self = this;
var webAudioOptions = {
scale: options.minScale
};
webAudioOptions.audioContext = new window.AudioContext();
if (options.objectUrl) {
self._requestAudioAndBuildWaveformData(
options.objectUrl,
webAudioOptions,
options.withCredentials,
callback
);
}
};
WaveformBuilder.prototype._buildWaveformDataFromAudioBuffer = function(options, callback) {
var webAudioOptions = options.webAudio;
if (webAudioOptions.scale !== options.zoomLevels[0]) {
webAudioOptions.scale = options.zoomLevels[0];
}
var webAudioBuilderOptions = {
audio_buffer: webAudioOptions.audioBuffer,
split_channels: webAudioOptions.multiChannel,
scale: webAudioOptions.scale
};
WaveformData.createFromAudio(webAudioBuilderOptions, callback);
};
/**
* Fetches the audio content, based on the given options, and creates waveform
* data using the Web Audio API.
*
* @private
* @param {url} The media source URL
* @param {WaveformBuilderWebAudioOptions} webAudio
* @param {Boolean} withCredentials
* @param {WaveformBuilderInitCallback} callback
*/
WaveformBuilder.prototype._requestAudioAndBuildWaveformData = function(url,
webAudio, withCredentials, callback) {
var self = this;
if (!url) {
self._peaks.logger('Peaks.init(): The mediaElement src is invalid');
return;
}
var xhr = self._createXHR(url, 'arraybuffer', withCredentials, function(event) {
if (this.readyState !== 4) {
return;
}
if (this.status !== 200) {
callback(
new Error('Unable to fetch remote data. HTTP status ' + this.status)
);
return;
}
var webAudioBuilderOptions = {
audio_context: webAudio.audioContext,
array_buffer: event.target.response,
split_channels: webAudio.multiChannel,
scale: webAudio.scale
};
WaveformData.createFromAudio(webAudioBuilderOptions, callback);
},
function() {
callback(new Error('XHR Failed'));
});
xhr.send();
};
/**
* @private
* @param {String} url
* @param {String} requestType
* @param {Boolean} withCredentials
* @param {Function} onLoad
* @param {Function} onError
*
* @returns {XMLHttpRequest}
*/
WaveformBuilder.prototype._createXHR = function(url, requestType,
withCredentials, onLoad, onError) {
var xhr = new XMLHttpRequest();
// open an XHR request to the data source file
xhr.open('GET', url, true);
if (isXhr2) {
try {
xhr.responseType = requestType;
}
catch (e) { // eslint-disable-line no-unused-vars
// Some browsers like Safari 6 do handle XHR2 but not the json
// response type, doing only a try/catch fails in IE9
}
}
xhr.onload = onLoad;
xhr.onerror = onError;
if (isXhr2 && withCredentials) {
xhr.withCredentials = true;
}
return xhr;
};
return WaveformBuilder;
});