entry_types/scrolled/package/src/frontend/PlayerControls/WaveformPlayerControls/Wavesurfer.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import assign from 'deep-assign';
import Measure from 'react-measure';
import WaveSurfer from 'wavesurfer.js';
const EVENTS = [
'audioprocess',
'error',
'finish',
'loading',
'mouseup',
'pause',
'play',
'ready',
'scroll',
'seek',
'zoom'
];
function capitaliseFirstLetter(string) {
return string.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('');
}
function positiveIntegerProptype(props, propName, componentName) {
const n = props[propName];
if (n !== undefined && (typeof n !== 'number' || n !== parseInt(n, 10) || n < 0)) {
return new Error(`Invalid ${propName} supplied to ${componentName},
expected a positive integer`);
}
return null;
}
class Wavesurfer extends Component {
constructor(props) {
super(props);
this.state = {
isReady: false
};
if (typeof WaveSurfer === undefined) {
throw new Error('WaveSurfer is undefined!');
}
this._loadMediaElt = this._loadMediaElt.bind(this);
this._loadAudio = this._loadAudio.bind(this);
this._seekTo = this._seekTo.bind(this);
this._handleResize = () => {
if (this.state.isReady) {
// Force redraw
this._wavesurfer.zoom(false);
}
};
}
componentDidMount() {
const options = assign({}, this.props.options, {
container: this.wavesurferEl
});
// media element loading is only supported by MediaElement backend
if (this.props.mediaElt) {
options.backend = 'MediaElement';
}
this._wavesurfer = WaveSurfer.create(options);
// file was loaded, wave was drawn
this._wavesurfer.on('ready', () => {
this.setState({
isReady: true,
pos: this.props.pos
});
// set initial position
if (this.props.pos) {
this._seekTo(this.props.pos);
}
// set initial volume
if (this.props.volume != null) {
this._wavesurfer.setVolume(this.props.volume);
}
// set initial playing state
if (this.props.playing) {
this._wavesurfer.play();
}
// set initial zoom
if (this.props.zoom) {
this._wavesurfer.zoom(this.props.zoom);
}
});
this._wavesurfer.on('audioprocess', (pos) => {
this.setState({
pos
});
this.props.onPosChange({
wavesurfer: this._wavesurfer,
originalArgs: [pos]
});
});
// `audioprocess` is not fired when seeking, so we have to plug into the
// `seek` event and calculate the equivalent in seconds (seek event
// receives a position float 0-1) – See the README.md for explanation why we
// need this
this._wavesurfer.on('seek', (pos) => {
const formattedPos = this._posToSec(pos);
this.setState({
formattedPos
});
this.props.onPosChange({
wavesurfer: this._wavesurfer,
originalArgs: [formattedPos]
});
});
// hook up events to callback handlers passed in as props
EVENTS.forEach((e) => {
const propCallback = this.props[`on${capitaliseFirstLetter(e)}`];
const wavesurfer = this._wavesurfer;
if (propCallback) {
this._wavesurfer.on(e, (...originalArgs) => {
propCallback({
wavesurfer,
originalArgs
});
});
}
});
// if audioFile prop, load file
if (this.props.audioFile) {
this._loadAudio(this.props.audioFile, this.props.audioPeaks);
}
// if mediaElt prop, load media Element
if (this.props.mediaElt) {
this._loadMediaElt(this.props.mediaElt, this.props.audioPeaks);
}
}
// update wavesurfer rendering manually
componentDidUpdate(prevProps) {
// update audioFile
if (this.props.audioFile !== this.props.audioFile) {
this._loadAudio(this.props.audioFile, this.props.audioPeaks);
}
// update mediaElt
if (prevProps.mediaElt !== this.props.mediaElt) {
this._loadMediaElt(this.props.mediaElt, this.props.audioPeaks);
}
// update peaks
if (prevProps.audioPeaks !== this.props.audioPeaks) {
if (this.props.mediaElt) {
this._loadMediaElt(prevProps.mediaElt, this.props.audioPeaks);
} else {
this._loadAudio(this.props.audioFile, this.props.audioPeaks);
}
}
// update position
if (this.props.pos &&
this.state.isReady &&
prevProps.pos !== this.props.pos &&
this.props.pos !== this.state.pos) {
this._seekTo(this.props.pos);
}
// update playing state
if (this.props.playing !== prevProps.playing) {
if (this.props.playing) {
this._wavesurfer.play();
} else {
this._wavesurfer.pause();
}
}
// update volume
if (this.props.volume !== prevProps.volume) {
this._wavesurfer.setVolume(this.props.volume);
}
// update volume
if (this.props.zoom !== prevProps.zoom) {
this._wavesurfer.zoom(this.props.zoom);
}
// update audioRate
if (this.props.options.audioRate !== prevProps.options.audioRate) {
this._wavesurfer.setPlaybackRate(this.props.options.audioRate);
}
if (prevProps.options.waveColor !== this.props.options.waveColor) {
this._wavesurfer.setWaveColor(this.props.options.waveColor);
}
if (prevProps.options.progressColor !== this.props.options.progressColor) {
this._wavesurfer.setProgressColor(this.props.options.progressColor);
}
if (prevProps.options.cursorColor !== this.props.options.cursorColor) {
this._wavesurfer.setCursorColor(this.props.options.cursorColor);
}
if (prevProps.options.height !== this.props.options.height) {
this._wavesurfer.setHeight(this.props.options.height);
}
}
componentWillUnmount() {
// remove listeners
EVENTS.forEach((e) => {
this._wavesurfer.un(e);
});
// destroy wavesurfer instance
this._wavesurfer.destroy();
}
// receives seconds and transforms this to the position as a float 0-1
_secToPos(sec) {
return (1 / this._wavesurfer.getDuration()) * sec;
}
// receives position as a float 0-1 and transforms this to seconds
_posToSec(pos) {
return pos * this._wavesurfer.getDuration();
}
// pos is in seconds, the 0-1 proportional position we calculate here …
_seekTo(sec) {
const pos = this._secToPos(sec);
if (this.props.options.autoCenter) {
this._wavesurfer.seekAndCenter(pos);
} else {
this._wavesurfer.seekTo(pos);
}
}
// load a media element selector or HTML element
// if selector, get the HTML element for it
// and pass to _loadAudio
_loadMediaElt(selectorOrElt, audioPeaks) {
if (selectorOrElt instanceof global.HTMLElement) {
this._loadAudio(selectorOrElt, audioPeaks);
} else {
// Ignore if media element cannot be found. There are edge cases
// where React already unmounted the corresponding media element,
// but the parent Waveform component still holds the old media
// element id.
if (global.document.querySelector(selectorOrElt)) {
this._loadAudio(global.document.querySelector(selectorOrElt), audioPeaks);
}
}
}
// pass audio data to wavesurfer
_loadAudio(audioFileOrElt, audioPeaks) {
if (audioFileOrElt instanceof global.HTMLElement) {
// media element
this._wavesurfer.loadMediaElement(audioFileOrElt, audioPeaks);
} else if (typeof audioFileOrElt === 'string') {
// bog-standard string is handled by load method and ajax call
this._wavesurfer.load(audioFileOrElt, audioPeaks);
} else if (audioFileOrElt instanceof global.Blob || audioFileOrElt instanceof global.File) {
// blob or file is loaded with loadBlob method
this._wavesurfer.loadBlob(audioFileOrElt, audioPeaks);
} else {
throw new Error(`Wavesurfer._loadAudio expects prop audioFile
to be either HTMLElement, string or file/blob`);
}
}
_measureIfResponsive(children) {
if (this.props.responsive) {
return (
<Measure client onResize={this._handleResize}>
{({ measureRef }) => (
<div ref={measureRef}>
{children}
</div>
)
}
</Measure>
);
}
return children;
}
render() {
const childrenWithProps = (this.props.children)
? React.Children.map(
this.props.children,
child => React.cloneElement(child, {
wavesurfer: this._wavesurfer,
isReady: this.state.isReady
}))
: false;
return this._measureIfResponsive(
<div>
<div ref={(c) => { this.wavesurferEl = c; }} />
{childrenWithProps}
</div>
);
}
}
Wavesurfer.propTypes = {
playing: PropTypes.bool,
pos: PropTypes.number,
audioFile: (props, propName, componentName) => {
const prop = props[propName];
if (prop &&
typeof prop !== 'string' &&
!(prop instanceof global.Blob) &&
!(prop instanceof global.File)) {
return new Error(`Invalid ${propName} supplied to ${componentName}
expected either string or file/blob`);
}
return null;
},
mediaElt: PropTypes.oneOfType([
PropTypes.string,
PropTypes.instanceOf(global.HTMLElement)
]),
audioPeaks: PropTypes.array,
volume: PropTypes.number,
zoom: PropTypes.number,
responsive: PropTypes.bool,
onPosChange: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.array
]),
options: PropTypes.shape({
audioRate: PropTypes.number,
backend: PropTypes.oneOf(['WebAudio', 'MediaElement']),
barWidth: (props, propName, componentName) => {
const prop = props[propName];
if (prop !== undefined && typeof prop !== 'number') {
return new Error(`Invalid ${propName} supplied to ${componentName}
expected either undefined or number`);
}
return null;
},
cursorColor: PropTypes.string,
cursorWidth: positiveIntegerProptype,
dragSelection: PropTypes.bool,
fillParent: PropTypes.bool,
height: positiveIntegerProptype,
hideScrollbar: PropTypes.bool,
interact: PropTypes.bool,
loopSelection: PropTypes.bool,
mediaControls: PropTypes.bool,
minPxPerSec: positiveIntegerProptype,
normalize: PropTypes.bool,
pixelRatio: PropTypes.number,
progressColor: PropTypes.string,
scrollParent: PropTypes.bool,
skipLength: PropTypes.number,
waveColor: PropTypes.string,
autoCenter: PropTypes.bool
})
};
Wavesurfer.defaultProps = {
playing: false,
pos: 0,
options: WaveSurfer.defaultParams,
responsive: true,
onPosChange: () => {}
};
export default Wavesurfer;