|
| 1 | +import React from 'react'; |
| 2 | +import PropTypes from 'prop-types'; |
| 3 | +import WaveSurfer from 'wavesurfer.js'; |
| 4 | +import { defineMessages, injectIntl } from 'react-intl'; |
| 5 | +import { formatTime } from 'mastodon/features/video'; |
| 6 | +import Icon from 'mastodon/components/icon'; |
| 7 | +import classNames from 'classnames'; |
| 8 | +import { throttle } from 'lodash'; |
| 9 | + |
| 10 | +const messages = defineMessages({ |
| 11 | + play: { id: 'video.play', defaultMessage: 'Play' }, |
| 12 | + pause: { id: 'video.pause', defaultMessage: 'Pause' }, |
| 13 | + mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, |
| 14 | + unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, |
| 15 | +}); |
| 16 | + |
| 17 | +const arrayOf = (length, fill) => (new Array(length)).fill(fill); |
| 18 | + |
| 19 | +export default @injectIntl |
| 20 | +class Audio extends React.PureComponent { |
| 21 | + |
| 22 | + static propTypes = { |
| 23 | + src: PropTypes.string.isRequired, |
| 24 | + alt: PropTypes.string, |
| 25 | + duration: PropTypes.number, |
| 26 | + height: PropTypes.number, |
| 27 | + preload: PropTypes.bool, |
| 28 | + editable: PropTypes.bool, |
| 29 | + intl: PropTypes.object.isRequired, |
| 30 | + }; |
| 31 | + |
| 32 | + state = { |
| 33 | + currentTime: 0, |
| 34 | + duration: null, |
| 35 | + paused: true, |
| 36 | + muted: false, |
| 37 | + volume: 0.5, |
| 38 | + }; |
| 39 | + |
| 40 | + // hard coded in components.scss |
| 41 | + // any way to get ::before values programatically? |
| 42 | + |
| 43 | + volWidth = 50; |
| 44 | + |
| 45 | + volOffset = 70; |
| 46 | + |
| 47 | + volHandleOffset = v => { |
| 48 | + const offset = v * this.volWidth + this.volOffset; |
| 49 | + return (offset > 110) ? 110 : offset; |
| 50 | + } |
| 51 | + |
| 52 | + setVolumeRef = c => { |
| 53 | + this.volume = c; |
| 54 | + } |
| 55 | + |
| 56 | + setWaveformRef = c => { |
| 57 | + this.waveform = c; |
| 58 | + } |
| 59 | + |
| 60 | + componentDidMount () { |
| 61 | + if (this.waveform) { |
| 62 | + this._updateWaveform(); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + componentDidUpdate (prevProps) { |
| 67 | + if (this.waveform && prevProps.src !== this.props.src) { |
| 68 | + this._updateWaveform(); |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + componentWillUnmount () { |
| 73 | + if (this.wavesurfer) { |
| 74 | + this.wavesurfer.destroy(); |
| 75 | + this.wavesurfer = null; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + _updateWaveform () { |
| 80 | + const { src, height, duration, preload } = this.props; |
| 81 | + |
| 82 | + const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color'); |
| 83 | + const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color'); |
| 84 | + |
| 85 | + if (this.wavesurfer) { |
| 86 | + this.wavesurfer.destroy(); |
| 87 | + } |
| 88 | + |
| 89 | + const wavesurfer = WaveSurfer.create({ |
| 90 | + container: this.waveform, |
| 91 | + height, |
| 92 | + barWidth: 3, |
| 93 | + cursorWidth: 0, |
| 94 | + progressColor, |
| 95 | + waveColor, |
| 96 | + forceDecode: true, |
| 97 | + }); |
| 98 | + |
| 99 | + wavesurfer.setVolume(this.state.volume); |
| 100 | + |
| 101 | + if (preload) { |
| 102 | + wavesurfer.load(src); |
| 103 | + } else { |
| 104 | + wavesurfer.load(src, arrayOf(1, 0.5), null, duration); |
| 105 | + } |
| 106 | + |
| 107 | + wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) })); |
| 108 | + wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) })); |
| 109 | + wavesurfer.on('pause', () => this.setState({ paused: true })); |
| 110 | + wavesurfer.on('play', () => this.setState({ paused: false })); |
| 111 | + wavesurfer.on('volume', volume => this.setState({ volume })); |
| 112 | + wavesurfer.on('mute', muted => this.setState({ muted })); |
| 113 | + |
| 114 | + this.wavesurfer = wavesurfer; |
| 115 | + } |
| 116 | + |
| 117 | + togglePlay = () => { |
| 118 | + if (this.state.paused) { |
| 119 | + if (!this.props.preload) { |
| 120 | + this.wavesurfer.createBackend(); |
| 121 | + this.wavesurfer.createPeakCache(); |
| 122 | + this.wavesurfer.load(this.props.src); |
| 123 | + } |
| 124 | + |
| 125 | + this.wavesurfer.play(); |
| 126 | + } else { |
| 127 | + this.wavesurfer.pause(); |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + toggleMute = () => { |
| 132 | + this.wavesurfer.setMute(!this.state.muted); |
| 133 | + } |
| 134 | + |
| 135 | + handleVolumeMouseDown = e => { |
| 136 | + document.addEventListener('mousemove', this.handleMouseVolSlide, true); |
| 137 | + document.addEventListener('mouseup', this.handleVolumeMouseUp, true); |
| 138 | + document.addEventListener('touchmove', this.handleMouseVolSlide, true); |
| 139 | + document.addEventListener('touchend', this.handleVolumeMouseUp, true); |
| 140 | + |
| 141 | + this.handleMouseVolSlide(e); |
| 142 | + |
| 143 | + e.preventDefault(); |
| 144 | + e.stopPropagation(); |
| 145 | + } |
| 146 | + |
| 147 | + handleVolumeMouseUp = () => { |
| 148 | + document.removeEventListener('mousemove', this.handleMouseVolSlide, true); |
| 149 | + document.removeEventListener('mouseup', this.handleVolumeMouseUp, true); |
| 150 | + document.removeEventListener('touchmove', this.handleMouseVolSlide, true); |
| 151 | + document.removeEventListener('touchend', this.handleVolumeMouseUp, true); |
| 152 | + } |
| 153 | + |
| 154 | + handleMouseVolSlide = throttle(e => { |
| 155 | + const rect = this.volume.getBoundingClientRect(); |
| 156 | + const x = (e.clientX - rect.left) / this.volWidth; // x position within the element. |
| 157 | + |
| 158 | + if(!isNaN(x)) { |
| 159 | + let slideamt = x; |
| 160 | + |
| 161 | + if (x > 1) { |
| 162 | + slideamt = 1; |
| 163 | + } else if(x < 0) { |
| 164 | + slideamt = 0; |
| 165 | + } |
| 166 | + |
| 167 | + this.wavesurfer.setVolume(slideamt); |
| 168 | + } |
| 169 | + }, 60); |
| 170 | + |
| 171 | + render () { |
| 172 | + const { height, intl, alt, editable } = this.props; |
| 173 | + const { paused, muted, volume, currentTime } = this.state; |
| 174 | + |
| 175 | + const volumeWidth = muted ? 0 : volume * this.volWidth; |
| 176 | + const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume); |
| 177 | + |
| 178 | + return ( |
| 179 | + <div className={classNames('audio-player', { editable })}> |
| 180 | + <div className='audio-player__progress-placeholder' style={{ display: 'none' }} /> |
| 181 | + <div className='audio-player__wave-placeholder' style={{ display: 'none' }} /> |
| 182 | + |
| 183 | + <div |
| 184 | + className='audio-player__waveform' |
| 185 | + aria-label={alt} |
| 186 | + title={alt} |
| 187 | + style={{ height }} |
| 188 | + ref={this.setWaveformRef} |
| 189 | + /> |
| 190 | + |
| 191 | + <div className='video-player__controls active'> |
| 192 | + <div className='video-player__buttons-bar'> |
| 193 | + <div className='video-player__buttons left'> |
| 194 | + <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> |
| 195 | + <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> |
| 196 | + |
| 197 | + <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> |
| 198 | + <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> |
| 199 | + |
| 200 | + <span |
| 201 | + className={classNames('video-player__volume__handle')} |
| 202 | + tabIndex='0' |
| 203 | + style={{ left: `${volumeHandleLoc}px` }} |
| 204 | + /> |
| 205 | + </div> |
| 206 | + |
| 207 | + <span> |
| 208 | + <span className='video-player__time-current'>{formatTime(currentTime)}</span> |
| 209 | + <span className='video-player__time-sep'>/</span> |
| 210 | + <span className='video-player__time-total'>{formatTime(this.state.duration || Math.floor(this.props.duration))}</span> |
| 211 | + </span> |
| 212 | + </div> |
| 213 | + </div> |
| 214 | + </div> |
| 215 | + </div> |
| 216 | + ); |
| 217 | + } |
| 218 | + |
| 219 | +} |
0 commit comments