UNPKG

react-wavesurfer

Version:

React component wrapper for wavesurfer.js

342 lines (295 loc) 9.23 kB
import React, { Component, PropTypes } from 'react'; import assign from 'deep-assign'; const WaveSurfer = require('wavesurfer.js'); const EVENTS = [ 'audioprocess', 'error', 'finish', 'loading', 'mouseup', 'pause', 'play', 'ready', 'scroll', 'seek', 'zoom' ]; /** * @description Capitalise the first letter of a string */ function capitaliseFirstLetter(string) { return string.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(''); } /** * @description Throws an error if the prop is defined and not an integer or not positive */ 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 = { pos: 0 }; if (typeof WaveSurfer === undefined) { throw new Error('WaveSurfer is undefined!'); } this._wavesurfer = Object.create(WaveSurfer); this._isReady = false; this._loadMediaElt = this._loadMediaElt.bind(this); this._loadAudio = this._loadAudio.bind(this); this._seekTo = this._seekTo.bind(this); } componentDidMount() { const options = assign({}, this.props.options, { container: this.refs.wavesurfer }); // media element loading is only supported by MediaElement backend if (this.props.mediaElt) { options.backend = 'MediaElement'; } this._wavesurfer.init(options); // file was loaded, wave was drawn this._wavesurfer.on('ready', () => { this._isReady = true; // set initial position if (this.props.pos) { this._seekTo(this.props.pos); } // set initial volume if (this.props.volume) { this._wavesurfer.setVolume(this.props.volume); } // 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 componentWillReceiveProps(nextProps) { // update audioFile if (this.props.audioFile !== nextProps.audioFile) { this._loadAudio(nextProps.audioFile, nextProps.audioPeaks); } // update mediaElt if (this.props.mediaElt !== nextProps.mediaElt) { this._loadMediaElt(nextProps.mediaElt, nextProps.audioPeaks); } // update peaks if (this.props.audioPeaks !== nextProps.audioPeaks) { if (nextProps.mediaElt) { this._loadMediaElt(nextProps.mediaElt, nextProps.audioPeaks); } else { this._loadAudio(nextProps.audioFile, nextProps.audioPeaks); } } // update position if (nextProps.pos && this._isReady && nextProps.pos !== this.props.pos && nextProps.pos !== this.state.pos) { this._seekTo(nextProps.pos); } // update playing state if (this.props.playing !== nextProps.playing || this._wavesurfer.isPlaying() !== nextProps.playing) { if (nextProps.playing) { this._wavesurfer.play(); } else { this._wavesurfer.pause(); } } // update volume if (this.props.volume !== nextProps.volume) { this._wavesurfer.setVolume(nextProps.volume); } // update volume if (this.props.zoom !== nextProps.zoom) { this._wavesurfer.zoom(nextProps.zoom); } } shouldComponentUpdate() { return false; } 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 HTMLElement) { this._loadAudio(selectorOrElt, audioPeaks); } else { if (!document.querySelector(selectorOrElt)) { throw new Error('Media Element not found!'); } this._loadAudio(document.querySelector(selectorOrElt), audioPeaks); } } // pass audio data to wavesurfer _loadAudio(audioFileOrElt, audioPeaks) { if (audioFileOrElt instanceof 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 Blob || audioFileOrElt instanceof 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`); } } render() { let childrenWithProps = (this.props.children) ? React.Children.map( this.props.children, child => React.cloneElement(child, { wavesurfer: this._wavesurfer, isReady: this._isReady })) : false; return ( <div> <div ref="wavesurfer" /> {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 Blob && !prop instanceof File) { return new Error(`Invalid ${propName} supplied to ${componentName} expected either string or file/blob`); } return null; }, mediaElt: PropTypes.oneOfType([ PropTypes.string, PropTypes.instanceOf(HTMLElement) ]), audioPeaks: PropTypes.array, volume: PropTypes.number, zoom: PropTypes.number, 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, onPosChange: () => {} }; export default Wavesurfer;