wavesurfer.js
Version:
Interactive navigable audio visualization using Web Audio and Canvas
444 lines (400 loc) • 12.5 kB
JavaScript
import WebAudio from './webaudio';
import * as util from './util';
/**
* MediaElement backend
*/
export default class MediaElement extends WebAudio {
/**
* Construct the backend
*
* @param {WavesurferParams} params Wavesurfer parameters
*/
constructor(params) {
super(params);
/** @private */
this.params = params;
/**
* Initially a dummy media element to catch errors. Once `_load` is
* called, this will contain the actual `HTMLMediaElement`.
* @private
*/
this.media = {
currentTime: 0,
duration: 0,
paused: true,
playbackRate: 1,
play() {},
pause() {},
volume: 0
};
/** @private */
this.mediaType = params.mediaType.toLowerCase();
/** @private */
this.elementPosition = params.elementPosition;
/** @private */
this.peaks = null;
/** @private */
this.playbackRate = 1;
/** @private */
this.volume = 1;
/** @private */
this.isMuted = false;
/** @private */
this.buffer = null;
/** @private */
this.onPlayEnd = null;
/** @private */
this.mediaListeners = {};
}
/**
* Initialise the backend, called in `wavesurfer.createBackend()`
*/
init() {
this.setPlaybackRate(this.params.audioRate);
this.createTimer();
}
/**
* Attach event listeners to media element.
*/
_setupMediaListeners() {
this.mediaListeners.error = () => {
this.fireEvent('error', 'Error loading media element');
};
this.mediaListeners.waiting = () => {
this.fireEvent('waiting');
};
this.mediaListeners.canplay = () => {
this.fireEvent('canplay');
};
this.mediaListeners.ended = () => {
this.fireEvent('finish');
};
// listen to and relay play, pause and seeked events to enable
// playback control from the external media element
this.mediaListeners.play = () => {
this.fireEvent('play');
};
this.mediaListeners.pause = () => {
this.fireEvent('pause');
};
this.mediaListeners.seeked = event => {
this.fireEvent('seek');
};
this.mediaListeners.volumechange = event => {
this.isMuted = this.media.muted;
if (this.isMuted) {
this.volume = 0;
} else {
this.volume = this.media.volume;
}
this.fireEvent('volume');
};
// reset event listeners
Object.keys(this.mediaListeners).forEach(id => {
this.media.removeEventListener(id, this.mediaListeners[id]);
this.media.addEventListener(id, this.mediaListeners[id]);
});
}
/**
* Create a timer to provide a more precise `audioprocess` event.
*/
createTimer() {
const onAudioProcess = () => {
if (this.isPaused()) {
return;
}
this.fireEvent('audioprocess', this.getCurrentTime());
// Call again in the next frame
util.frame(onAudioProcess)();
};
this.on('play', onAudioProcess);
// Update the progress one more time to prevent it from being stuck in
// case of lower framerates
this.on('pause', () => {
this.fireEvent('audioprocess', this.getCurrentTime());
});
}
/**
* Create media element with url as its source,
* and append to container element.
*
* @param {string} url Path to media file
* @param {HTMLElement} container HTML element
* @param {number[]|Number.<Array[]>} peaks Array of peak data
* @param {string} preload HTML 5 preload attribute value
* @throws Will throw an error if the `url` argument is not a valid media
* element.
*/
load(url, container, peaks, preload) {
const media = document.createElement(this.mediaType);
media.controls = this.params.mediaControls;
media.autoplay = this.params.autoplay || false;
media.preload = preload == null ? 'auto' : preload;
media.src = url;
media.style.width = '100%';
const prevMedia = container.querySelector(this.mediaType);
if (prevMedia) {
container.removeChild(prevMedia);
}
container.appendChild(media);
this._load(media, peaks, preload);
}
/**
* Load existing media element.
*
* @param {HTMLMediaElement} elt HTML5 Audio or Video element
* @param {number[]|Number.<Array[]>} peaks Array of peak data
*/
loadElt(elt, peaks) {
elt.controls = this.params.mediaControls;
elt.autoplay = this.params.autoplay || false;
this._load(elt, peaks, elt.preload);
}
/**
* Method called by both `load` (from url)
* and `loadElt` (existing media element) methods.
*
* @param {HTMLMediaElement} media HTML5 Audio or Video element
* @param {number[]|Number.<Array[]>} peaks Array of peak data
* @param {string} preload HTML 5 preload attribute value
* @throws Will throw an error if the `media` argument is not a valid media
* element.
* @private
*/
_load(media, peaks, preload) {
// verify media element is valid
if (
!(media instanceof HTMLMediaElement) ||
typeof media.addEventListener === 'undefined'
) {
throw new Error('media parameter is not a valid media element');
}
// load must be called manually on iOS, otherwise peaks won't draw
// until a user interaction triggers load --> 'ready' event
//
// note that we avoid calling media.load here when given peaks and preload == 'none'
// as this almost always triggers some browser fetch of the media.
if (typeof media.load == 'function' && !(peaks && preload == 'none')) {
// Resets the media element and restarts the media resource. Any
// pending events are discarded. How much media data is fetched is
// still affected by the preload attribute.
media.load();
}
this.media = media;
this._setupMediaListeners();
this.peaks = peaks;
this.onPlayEnd = null;
this.buffer = null;
this.isMuted = media.muted;
this.setPlaybackRate(this.playbackRate);
this.setVolume(this.volume);
}
/**
* Used by `wavesurfer.isPlaying()` and `wavesurfer.playPause()`
*
* @return {boolean} Media paused or not
*/
isPaused() {
return !this.media || this.media.paused;
}
/**
* Used by `wavesurfer.getDuration()`
*
* @return {number} Duration
*/
getDuration() {
if (this.explicitDuration) {
return this.explicitDuration;
}
let duration = (this.buffer || this.media).duration;
if (duration >= Infinity) {
// streaming audio
duration = this.media.seekable.end(0);
}
return duration;
}
/**
* Returns the current time in seconds relative to the audio-clip's
* duration.
*
* @return {number} Current time
*/
getCurrentTime() {
return this.media && this.media.currentTime;
}
/**
* Get the position from 0 to 1
*
* @return {number} Current position
*/
getPlayedPercents() {
return this.getCurrentTime() / this.getDuration() || 0;
}
/**
* Get the audio source playback rate.
*
* @return {number} Playback rate
*/
getPlaybackRate() {
return this.playbackRate || this.media.playbackRate;
}
/**
* Set the audio source playback rate.
*
* @param {number} value Playback rate
*/
setPlaybackRate(value) {
this.playbackRate = value || 1;
this.media.playbackRate = this.playbackRate;
}
/**
* Used by `wavesurfer.seekTo()`
*
* @param {number} start Position to start at in seconds
*/
seekTo(start) {
if (start != null && !isNaN(start)) {
this.media.currentTime = start;
}
this.clearPlayEnd();
}
/**
* Plays the loaded audio region.
*
* @param {number} start Start offset in seconds, relative to the beginning
* of a clip.
* @param {number} end When to stop, relative to the beginning of a clip.
* @emits MediaElement#play
* @return {Promise} Result
*/
play(start, end) {
this.seekTo(start);
const promise = this.media.play();
end && this.setPlayEnd(end);
return promise;
}
/**
* Pauses the loaded audio.
*
* @emits MediaElement#pause
* @return {Promise} Result
*/
pause() {
let promise;
if (this.media) {
promise = this.media.pause();
}
this.clearPlayEnd();
return promise;
}
/**
* Set the play end
*
* @param {number} end Where to end
*/
setPlayEnd(end) {
this.clearPlayEnd();
this._onPlayEnd = time => {
if (time >= end) {
this.pause();
this.seekTo(end);
}
};
this.on('audioprocess', this._onPlayEnd);
}
/** @private */
clearPlayEnd() {
if (this._onPlayEnd) {
this.un('audioprocess', this._onPlayEnd);
this._onPlayEnd = null;
}
}
/**
* Compute the max and min value of the waveform when broken into
* <length> subranges.
*
* @param {number} length How many subranges to break the waveform into.
* @param {number} first First sample in the required range.
* @param {number} last Last sample in the required range.
* @return {number[]|Number.<Array[]>} Array of 2*<length> peaks or array of
* arrays of peaks consisting of (max, min) values for each subrange.
*/
getPeaks(length, first, last) {
if (this.buffer) {
return super.getPeaks(length, first, last);
}
return this.peaks || [];
}
/**
* Set the sink id for the media player
*
* @param {string} deviceId String value representing audio device id.
* @returns {Promise} A Promise that resolves to `undefined` when there
* are no errors.
*/
setSinkId(deviceId) {
if (deviceId) {
if (!this.media.setSinkId) {
return Promise.reject(
new Error('setSinkId is not supported in your browser')
);
}
return this.media.setSinkId(deviceId);
}
return Promise.reject(new Error('Invalid deviceId: ' + deviceId));
}
/**
* Get the current volume
*
* @return {number} value A floating point value between 0 and 1.
*/
getVolume() {
return this.volume;
}
/**
* Set the audio volume
*
* @param {number} value A floating point value between 0 and 1.
*/
setVolume(value) {
this.volume = value;
// no need to change when it's already at that volume
if (this.media.volume !== this.volume) {
this.media.volume = this.volume;
}
}
/**
* Enable or disable muted audio
*
* @since 4.0.0
* @param {boolean} muted Specify `true` to mute audio.
*/
setMute(muted) {
// This causes a volume change to be emitted too through the
// volumechange event listener.
this.isMuted = this.media.muted = muted;
}
/**
* This is called when wavesurfer is destroyed
*
*/
destroy() {
this.pause();
this.unAll();
this.destroyed = true;
// cleanup media event listeners
Object.keys(this.mediaListeners).forEach(id => {
if (this.media) {
this.media.removeEventListener(id, this.mediaListeners[id]);
}
});
if (
this.params.removeMediaElementOnDestroy &&
this.media &&
this.media.parentNode
) {
this.media.parentNode.removeChild(this.media);
}
this.media = null;
}
}