236 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			236 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import React from 'react';
 | |
| import PropTypes from 'prop-types';
 | |
| import WaveSurfer from 'wavesurfer.js';
 | |
| import { defineMessages, injectIntl } from 'react-intl';
 | |
| import { formatTime } from 'flavours/glitch/features/video';
 | |
| import Icon from 'flavours/glitch/components/icon';
 | |
| import classNames from 'classnames';
 | |
| import { throttle } from 'lodash';
 | |
| 
 | |
| const messages = defineMessages({
 | |
|   play: { id: 'video.play', defaultMessage: 'Play' },
 | |
|   pause: { id: 'video.pause', defaultMessage: 'Pause' },
 | |
|   mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
 | |
|   unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
 | |
|   download: { id: 'video.download', defaultMessage: 'Download file' },
 | |
| });
 | |
| 
 | |
| export default @injectIntl
 | |
| class Audio extends React.PureComponent {
 | |
| 
 | |
|   static propTypes = {
 | |
|     src: PropTypes.string.isRequired,
 | |
|     alt: PropTypes.string,
 | |
|     duration: PropTypes.number,
 | |
|     peaks: PropTypes.arrayOf(PropTypes.number),
 | |
|     height: PropTypes.number,
 | |
|     preload: PropTypes.bool,
 | |
|     editable: PropTypes.bool,
 | |
|     intl: PropTypes.object.isRequired,
 | |
|   };
 | |
| 
 | |
|   state = {
 | |
|     currentTime: 0,
 | |
|     duration: null,
 | |
|     paused: true,
 | |
|     muted: false,
 | |
|     volume: 0.5,
 | |
|   };
 | |
| 
 | |
|   // hard coded in components.scss
 | |
|   // any way to get ::before values programatically?
 | |
| 
 | |
|   volWidth = 50;
 | |
| 
 | |
|   volOffset = 70;
 | |
| 
 | |
|   volHandleOffset = v => {
 | |
|     const offset = v * this.volWidth + this.volOffset;
 | |
|     return (offset > 110) ? 110 : offset;
 | |
|   }
 | |
| 
 | |
|   setVolumeRef = c => {
 | |
|     this.volume = c;
 | |
|   }
 | |
| 
 | |
|   setWaveformRef = c => {
 | |
|     this.waveform = c;
 | |
|   }
 | |
| 
 | |
|   componentDidMount () {
 | |
|     if (this.waveform) {
 | |
|       this._updateWaveform();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   componentDidUpdate (prevProps) {
 | |
|     if (this.waveform && prevProps.src !== this.props.src) {
 | |
|       this._updateWaveform();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   componentWillUnmount () {
 | |
|     if (this.wavesurfer) {
 | |
|       this.wavesurfer.destroy();
 | |
|       this.wavesurfer = null;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _updateWaveform () {
 | |
|     const { src, height, duration, peaks, preload } = this.props;
 | |
| 
 | |
|     const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color');
 | |
|     const waveColor     = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color');
 | |
| 
 | |
|     if (this.wavesurfer) {
 | |
|       this.wavesurfer.destroy();
 | |
|       this.loaded = false;
 | |
|     }
 | |
| 
 | |
|     const wavesurfer = WaveSurfer.create({
 | |
|       container: this.waveform,
 | |
|       height,
 | |
|       barWidth: 3,
 | |
|       cursorWidth: 0,
 | |
|       progressColor,
 | |
|       waveColor,
 | |
|       backend: 'MediaElement',
 | |
|       interact: preload,
 | |
|     });
 | |
| 
 | |
|     wavesurfer.setVolume(this.state.volume);
 | |
| 
 | |
|     if (preload) {
 | |
|       wavesurfer.load(src);
 | |
|       this.loaded = true;
 | |
|     } else {
 | |
|       wavesurfer.load(src, peaks, 'none', duration);
 | |
|       this.loaded = false;
 | |
|     }
 | |
| 
 | |
|     wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) }));
 | |
|     wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) }));
 | |
|     wavesurfer.on('pause', () => this.setState({ paused: true }));
 | |
|     wavesurfer.on('play', () => this.setState({ paused: false }));
 | |
|     wavesurfer.on('volume', volume => this.setState({ volume }));
 | |
|     wavesurfer.on('mute', muted => this.setState({ muted }));
 | |
| 
 | |
|     this.wavesurfer = wavesurfer;
 | |
|   }
 | |
| 
 | |
|   togglePlay = () => {
 | |
|     if (this.state.paused) {
 | |
|       if (!this.props.preload && !this.loaded) {
 | |
|         this.wavesurfer.createBackend();
 | |
|         this.wavesurfer.createPeakCache();
 | |
|         this.wavesurfer.load(this.props.src);
 | |
|         this.wavesurfer.toggleInteraction();
 | |
|         this.loaded = true;
 | |
|       }
 | |
| 
 | |
|       this.wavesurfer.play();
 | |
|       this.setState({ paused: false });
 | |
|     } else {
 | |
|       this.wavesurfer.pause();
 | |
|       this.setState({ paused: true });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   toggleMute = () => {
 | |
|     this.wavesurfer.setMute(!this.state.muted);
 | |
|   }
 | |
| 
 | |
|   handleVolumeMouseDown = e => {
 | |
|     document.addEventListener('mousemove', this.handleMouseVolSlide, true);
 | |
|     document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
 | |
|     document.addEventListener('touchmove', this.handleMouseVolSlide, true);
 | |
|     document.addEventListener('touchend', this.handleVolumeMouseUp, true);
 | |
| 
 | |
|     this.handleMouseVolSlide(e);
 | |
| 
 | |
|     e.preventDefault();
 | |
|     e.stopPropagation();
 | |
|   }
 | |
| 
 | |
|   handleVolumeMouseUp = () => {
 | |
|     document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
 | |
|     document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
 | |
|     document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
 | |
|     document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
 | |
|   }
 | |
| 
 | |
|   handleMouseVolSlide = throttle(e => {
 | |
|     const rect = this.volume.getBoundingClientRect();
 | |
|     const x    = (e.clientX - rect.left) / this.volWidth; // x position within the element.
 | |
| 
 | |
|     if(!isNaN(x)) {
 | |
|       let slideamt = x;
 | |
| 
 | |
|       if (x > 1) {
 | |
|         slideamt = 1;
 | |
|       } else if(x < 0) {
 | |
|         slideamt = 0;
 | |
|       }
 | |
| 
 | |
|       this.wavesurfer.setVolume(slideamt);
 | |
|     }
 | |
|   }, 60);
 | |
| 
 | |
|   render () {
 | |
|     const { height, intl, alt, editable } = this.props;
 | |
|     const { paused, muted, volume, currentTime } = this.state;
 | |
| 
 | |
|     const volumeWidth     = muted ? 0 : volume * this.volWidth;
 | |
|     const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
 | |
| 
 | |
|     return (
 | |
|       <div className={classNames('audio-player', { editable })}>
 | |
|         <div className='audio-player__progress-placeholder' style={{ display: 'none' }} />
 | |
|         <div className='audio-player__wave-placeholder' style={{ display: 'none' }} />
 | |
| 
 | |
|         <div
 | |
|           className='audio-player__waveform'
 | |
|           aria-label={alt}
 | |
|           title={alt}
 | |
|           style={{ height }}
 | |
|           ref={this.setWaveformRef}
 | |
|         />
 | |
| 
 | |
|         <div className='video-player__controls active'>
 | |
|           <div className='video-player__buttons-bar'>
 | |
|             <div className='video-player__buttons left'>
 | |
|               <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
 | |
|               <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
 | |
| 
 | |
|               <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
 | |
|                  
 | |
|                 <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} />
 | |
| 
 | |
|                 <span
 | |
|                   className={classNames('video-player__volume__handle')}
 | |
|                   tabIndex='0'
 | |
|                   style={{ left: `${volumeHandleLoc}px` }}
 | |
|                 />
 | |
|               </div>
 | |
| 
 | |
|               <span>
 | |
|                 <span className='video-player__time-current'>{formatTime(currentTime)}</span>
 | |
|                 <span className='video-player__time-sep'>/</span>
 | |
|                 <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span>
 | |
|               </span>
 | |
|             </div>
 | |
| 
 | |
|             <div className='video-player__buttons right'>
 | |
|               <button type='button' aria-label={intl.formatMessage(messages.download)}>
 | |
|                 <a className='video-player__download__icon' href={this.props.src} download>
 | |
|                   <Icon id={'download'} fixedWidth />
 | |
|                 </a>
 | |
|               </button>
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       </div>
 | |
|     );
 | |
|   }
 | |
| 
 | |
| }
 |