You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

236 lines
7.4 KiB

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