闭社主体 forked from https://github.com/tootsuite/mastodon
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.

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