mirror of
https://github.com/funamitech/mastodon
synced 2024-12-14 06:38:39 +09:00
baa3db3001
Change video player to not loop, since the audio player doesn't Change playback and mute buttons to feel snappier
252 lines
8.0 KiB
JavaScript
252 lines
8.0 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 'mastodon/features/video';
|
|
import Icon from 'mastodon/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();
|
|
}
|
|
|
|
window.addEventListener('scroll', this.handleScroll);
|
|
}
|
|
|
|
componentDidUpdate (prevProps) {
|
|
if (this.waveform && prevProps.src !== this.props.src) {
|
|
this._updateWaveform();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
window.removeEventListener('scroll', this.handleScroll);
|
|
|
|
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.setState({ paused: false }, () => this.wavesurfer.play());
|
|
} else {
|
|
this.setState({ paused: true }, () => this.wavesurfer.pause());
|
|
}
|
|
}
|
|
|
|
toggleMute = () => {
|
|
const muted = !this.state.muted;
|
|
this.setState({ muted }, () => this.wavesurfer.setMute(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);
|
|
|
|
handleScroll = throttle(() => {
|
|
if (!this.waveform || !this.wavesurfer) {
|
|
return;
|
|
}
|
|
|
|
const { top, height } = this.waveform.getBoundingClientRect();
|
|
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
|
|
|
if (!this.state.paused && !inView) {
|
|
this.setState({ paused: true }, () => this.wavesurfer.pause());
|
|
}
|
|
}, 150, { trailing: true })
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
}
|