Merge branch 'audio-tsx' into 'develop'

Audio: convert to TSX

See merge request soapbox-pub/soapbox!1830
environments/review-develop-3zknud/deployments/1137
Alex Gleason 2022-10-13 16:43:49 +00:00
commit 48bf42b513
2 zmienionych plików z 583 dodań i 535 usunięć

Wyświetl plik

@ -1,535 +0,0 @@
import classNames from 'clsx';
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
import { formatTime, getPointerPosition, fileNameFromURL } from 'soapbox/features/video';
import Visualizer from './visualizer';
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' },
});
const TICK_SIZE = 10;
const PADDING = 180;
export default @injectIntl
class Audio extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
poster: PropTypes.string,
duration: PropTypes.number,
width: PropTypes.number,
height: PropTypes.number,
editable: PropTypes.bool,
fullscreen: PropTypes.bool,
intl: PropTypes.object.isRequired,
cacheWidth: PropTypes.func,
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
currentTime: PropTypes.number,
autoPlay: PropTypes.bool,
volume: PropTypes.number,
muted: PropTypes.bool,
deployPictureInPicture: PropTypes.func,
};
state = {
width: this.props.width,
currentTime: 0,
buffer: 0,
duration: null,
paused: true,
muted: false,
volume: 0.5,
dragging: false,
};
constructor(props) {
super(props);
this.visualizer = new Visualizer(TICK_SIZE);
}
setPlayerRef = c => {
this.player = c;
if (this.player) {
this._setDimensions();
}
}
_pack() {
return {
src: this.props.src,
volume: this.audio.volume,
muted: this.audio.muted,
currentTime: this.audio.currentTime,
poster: this.props.poster,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
accentColor: this.props.accentColor,
};
}
_setDimensions() {
const width = this.player.offsetWidth;
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16 / 9));
if (this.props.cacheWidth) {
this.props.cacheWidth(width);
}
this.setState({ width, height });
}
setSeekRef = c => {
this.seek = c;
}
setVolumeRef = c => {
this.volume = c;
}
setAudioRef = c => {
this.audio = c;
if (this.audio) {
this.setState({ volume: this.audio.volume, muted: this.audio.muted });
}
}
setCanvasRef = c => {
this.canvas = c;
this.visualizer.setCanvas(c);
}
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height || prevProps.accentColor !== this.props.accentColor) {
this._clear();
this._draw();
}
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize);
if (!this.state.paused && this.audio && this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('audio', this._pack());
}
}
togglePlay = () => {
if (!this.audioContext) {
this._initAudioContext();
}
if (this.state.paused) {
this.setState({ paused: false }, () => this.audio.play());
} else {
this.setState({ paused: true }, () => this.audio.pause());
}
}
handleResize = debounce(() => {
if (this.player) {
this._setDimensions();
}
}, 250, {
trailing: true,
});
handlePlay = () => {
this.setState({ paused: false });
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
this._renderCanvas();
}
handlePause = () => {
this.setState({ paused: true });
if (this.audioContext) {
this.audioContext.suspend();
}
}
handleProgress = () => {
const lastTimeRange = this.audio.buffered.length - 1;
if (lastTimeRange > -1) {
this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) });
}
}
toggleMute = () => {
const muted = !this.state.muted;
this.setState({ muted }, () => {
this.audio.muted = 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);
}
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
document.addEventListener('touchmove', this.handleMouseMove, true);
document.addEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: true });
this.audio.pause();
this.handleMouseMove(e);
e.preventDefault();
e.stopPropagation();
}
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
this.setState({ dragging: false });
this.audio.play();
}
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
const currentTime = this.audio.duration * x;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.audio.currentTime = currentTime;
});
}
}, 15);
handleTimeUpdate = () => {
this.setState({
currentTime: this.audio.currentTime,
duration: this.audio.duration,
});
}
handleMouseVolSlide = throttle(e => {
const { x } = getPointerPosition(this.volume, e);
if (!isNaN(x)) {
this.setState({ volume: x }, () => {
this.audio.volume = x;
});
}
}, 15);
handleScroll = throttle(() => {
if (!this.canvas || !this.audio) {
return;
}
const { top, height } = this.canvas.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
this.audio.pause();
if (this.props.deployPictureInPicture) {
this.props.deployPictureInPicture('audio', this._pack());
}
this.setState({ paused: true });
}
}, 150, { trailing: true });
handleMouseEnter = () => {
this.setState({ hovered: true });
}
handleMouseLeave = () => {
this.setState({ hovered: false });
}
handleLoadedData = () => {
const { autoPlay, currentTime, volume, muted } = this.props;
this.setState({ duration: this.audio.duration });
if (currentTime) {
this.audio.currentTime = currentTime;
}
if (volume !== undefined) {
this.audio.volume = volume;
}
if (muted !== undefined) {
this.audio.muted = muted;
}
if (autoPlay) {
this.togglePlay();
}
}
_initAudioContext() {
// eslint-disable-next-line compat/compat
const AudioContext = window.AudioContext || window.webkitAudioContext;
const context = new AudioContext();
const source = context.createMediaElementSource(this.audio);
this.visualizer.setAudioContext(context, source);
source.connect(context.destination);
this.audioContext = context;
}
handleDownload = () => {
fetch(this.props.src).then(res => res.blob()).then(blob => {
const element = document.createElement('a');
const objectURL = URL.createObjectURL(blob);
element.setAttribute('href', objectURL);
element.setAttribute('download', fileNameFromURL(this.props.src));
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
URL.revokeObjectURL(objectURL);
}).catch(err => {
console.error(err);
});
}
_renderCanvas() {
requestAnimationFrame(() => {
if (!this.audio) return;
this.handleTimeUpdate();
this._clear();
this._draw();
if (!this.state.paused) {
this._renderCanvas();
}
});
}
_clear() {
this.visualizer.clear(this.state.width, this.state.height);
}
_draw() {
this.visualizer.draw(this._getCX(), this._getCY(), this._getAccentColor(), this._getRadius(), this._getScaleCoefficient());
}
_getRadius() {
return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
}
_getScaleCoefficient() {
return (this.state.height || this.props.height) / 982;
}
_getCX() {
return Math.floor(this.state.width / 2) || null;
}
_getCY() {
return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient())) || null;
}
_getAccentColor() {
return this.props.accentColor || '#ffffff';
}
_getBackgroundColor() {
return this.props.backgroundColor || '#000000';
}
_getForegroundColor() {
return this.props.foregroundColor || '#ffffff';
}
seekBy(time) {
const currentTime = this.audio.currentTime + time;
if (!isNaN(currentTime)) {
this.setState({ currentTime }, () => {
this.audio.currentTime = currentTime;
});
}
}
handleAudioKeyDown = e => {
// On the audio element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
this.togglePlay();
}
}
handleKeyDown = e => {
switch (e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();
this.togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
this.toggleMute();
break;
case 'j':
e.preventDefault();
e.stopPropagation();
this.seekBy(-10);
break;
case 'l':
e.preventDefault();
e.stopPropagation();
this.seekBy(10);
break;
}
}
render() {
const { src, intl, alt, editable } = this.props;
const { paused, muted, volume, currentTime, buffer, dragging } = this.state;
const duration = this.state.duration || this.props.duration;
const progress = Math.min((currentTime / duration) * 100, 100);
return (
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex='0' onKeyDown={this.handleKeyDown}>
<audio
src={src}
ref={this.setAudioRef}
preload='auto'
onPlay={this.handlePlay}
onPause={this.handlePause}
onProgress={this.handleProgress}
onLoadedData={this.handleLoadedData}
crossOrigin='anonymous'
/>
<canvas
role='button'
tabIndex='0'
className='audio-player__canvas'
width={this.state.width}
height={this.state.height}
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
ref={this.setCanvasRef}
onClick={this.togglePlay}
onKeyDown={this.handleAudioKeyDown}
title={alt}
aria-label={alt}
/>
{this.props.poster && <img
src={this.props.poster}
alt=''
width={(this._getRadius() - TICK_SIZE) * 2 || null}
height={(this._getRadius() - TICK_SIZE) * 2 || null}
style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
/>}
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
<div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getAccentColor() }} />
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
onKeyDown={this.handleAudioKeyDown}
/>
</div>
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon src={paused ? require('@tabler/icons/player-play.svg') : require('@tabler/icons/player-pause.svg')} /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon src={muted ? require('@tabler/icons/volume-3.svg') : require('@tabler/icons/volume.svg')} /></button>
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
<span
className='video-player__volume__handle'
tabIndex='0'
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
/>
</div>
<span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
{duration && (<>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(Math.floor(duration))}</span>
</>)}
</span>
</div>
<div className='video-player__buttons right'>
<a
title={intl.formatMessage(messages.download)}
aria-label={intl.formatMessage(messages.download)}
className='video-player__download__icon player-button'
href={this.props.src}
download
target='_blank'
>
<Icon src={require('@tabler/icons/download.svg')} />
</a>
</div>
</div>
</div>
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,583 @@
import classNames from 'clsx';
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
import React, { useEffect, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
import { formatTime, getPointerPosition } from 'soapbox/features/video';
import Visualizer from './visualizer';
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' },
});
const TICK_SIZE = 10;
const PADDING = 180;
interface IAudio {
src: string,
alt?: string,
poster?: string,
duration?: number,
width?: number,
height?: number,
editable?: boolean,
fullscreen?: boolean,
cacheWidth?: (width: number) => void,
backgroundColor?: string,
foregroundColor?: string,
accentColor?: string,
currentTime?: number,
autoPlay?: boolean,
volume?: number,
muted?: boolean,
deployPictureInPicture?: (type: string, opts: Record<string, any>) => void,
}
const Audio: React.FC<IAudio> = (props) => {
const {
src,
alt = '',
poster,
accentColor,
backgroundColor,
foregroundColor,
cacheWidth,
fullscreen,
autoPlay,
editable,
deployPictureInPicture = false,
} = props;
const intl = useIntl();
const [width, setWidth] = useState<number | undefined>(props.width);
const [height, setHeight] = useState<number | undefined>(props.height);
const [currentTime, setCurrentTime] = useState(0);
const [buffer, setBuffer] = useState(0);
const [duration, setDuration] = useState<number | undefined>(undefined);
const [paused, setPaused] = useState(true);
const [muted, setMuted] = useState(false);
const [volume, setVolume] = useState(0.5);
const [dragging, setDragging] = useState(false);
const [hovered, setHovered] = useState(false);
const visualizer = useRef<Visualizer>(new Visualizer(TICK_SIZE));
const audioContext = useRef<AudioContext | null>(null);
const player = useRef<HTMLDivElement>(null);
const audio = useRef<HTMLAudioElement>(null);
const seek = useRef<HTMLDivElement>(null);
const slider = useRef<HTMLDivElement>(null);
const canvas = useRef<HTMLCanvasElement>(null);
const _pack = () => ({
src: props.src,
volume: audio.current?.volume,
muted: audio.current?.muted,
currentTime: audio.current?.currentTime,
poster: props.poster,
backgroundColor: props.backgroundColor,
foregroundColor: props.foregroundColor,
accentColor: props.accentColor,
});
const _setDimensions = () => {
if (player.current) {
const width = player.current.offsetWidth;
const height = fullscreen ? player.current.offsetHeight : (width / (16 / 9));
if (cacheWidth) {
cacheWidth(width);
}
setWidth(width);
setHeight(height);
}
};
const togglePlay = () => {
if (!audioContext.current) {
_initAudioContext();
}
if (paused) {
audio.current?.play();
} else {
audio.current?.pause();
}
setPaused(!paused);
};
const handleResize = debounce(() => {
if (player.current) {
_setDimensions();
}
}, 250, {
trailing: true,
});
const handlePlay = () => {
setPaused(false);
if (audioContext.current?.state === 'suspended') {
audioContext.current?.resume();
}
_renderCanvas();
};
const handlePause = () => {
setPaused(true);
audioContext.current?.suspend();
};
const handleProgress = () => {
if (audio.current) {
const lastTimeRange = audio.current.buffered.length - 1;
if (lastTimeRange > -1) {
setBuffer(Math.ceil(audio.current.buffered.end(lastTimeRange) / audio.current.duration * 100));
}
}
};
const toggleMute = () => {
const nextMuted = !muted;
setMuted(nextMuted);
if (audio.current) {
audio.current.muted = nextMuted;
}
};
const handleVolumeMouseDown: React.MouseEventHandler = e => {
document.addEventListener('mousemove', handleMouseVolSlide, true);
document.addEventListener('mouseup', handleVolumeMouseUp, true);
document.addEventListener('touchmove', handleMouseVolSlide, true);
document.addEventListener('touchend', handleVolumeMouseUp, true);
handleMouseVolSlide(e);
e.preventDefault();
e.stopPropagation();
};
const handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', handleMouseVolSlide, true);
document.removeEventListener('mouseup', handleVolumeMouseUp, true);
document.removeEventListener('touchmove', handleMouseVolSlide, true);
document.removeEventListener('touchend', handleVolumeMouseUp, true);
};
const handleMouseDown: React.MouseEventHandler = e => {
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('mouseup', handleMouseUp, true);
document.addEventListener('touchmove', handleMouseMove, true);
document.addEventListener('touchend', handleMouseUp, true);
setDragging(true);
audio.current?.pause();
handleMouseMove(e);
e.preventDefault();
e.stopPropagation();
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('mouseup', handleMouseUp, true);
document.removeEventListener('touchmove', handleMouseMove, true);
document.removeEventListener('touchend', handleMouseUp, true);
setDragging(false);
audio.current?.play();
};
const handleMouseMove = throttle((e) => {
if (audio.current && seek.current) {
const { x } = getPointerPosition(seek.current, e);
const currentTime = audio.current.duration * x;
if (!isNaN(currentTime)) {
setCurrentTime(currentTime);
audio.current.currentTime = currentTime;
}
}
}, 15);
const handleTimeUpdate = () => {
if (audio.current) {
setCurrentTime(audio.current.currentTime);
setDuration(audio.current.duration);
}
};
const handleMouseVolSlide = throttle(e => {
if (audio.current && slider.current) {
const { x } = getPointerPosition(slider.current, e);
if (!isNaN(x)) {
setVolume(x);
audio.current.volume = x;
}
}
}, 15);
const handleScroll = throttle(() => {
if (!canvas.current || !audio.current) {
return;
}
const { top, height } = canvas.current.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!paused && !inView) {
audio.current.pause();
if (deployPictureInPicture) {
deployPictureInPicture('audio', _pack());
}
setPaused(true);
}
}, 150, { trailing: true });
const handleMouseEnter = () => {
setHovered(true);
};
const handleMouseLeave = () => {
setHovered(false);
};
const handleLoadedData = () => {
if (audio.current) {
setDuration(audio.current.duration);
if (currentTime) {
audio.current.currentTime = currentTime;
}
if (volume !== undefined) {
audio.current.volume = volume;
}
if (muted !== undefined) {
audio.current.muted = muted;
}
if (autoPlay) {
togglePlay();
}
}
};
const _initAudioContext = () => {
if (audio.current) {
// @ts-ignore
// eslint-disable-next-line compat/compat
const AudioContext = window.AudioContext || window.webkitAudioContext;
const context = new AudioContext();
const source = context.createMediaElementSource(audio.current);
visualizer.current.setAudioContext(context, source);
source.connect(context.destination);
audioContext.current = context;
}
};
const _renderCanvas = () => {
requestAnimationFrame(() => {
if (!audio.current) return;
handleTimeUpdate();
_clear();
_draw();
if (!paused) {
_renderCanvas();
}
});
};
const _clear = () => {
visualizer.current?.clear(width || 0, height || 0);
};
const _draw = () => {
visualizer.current?.draw(_getCX(), _getCY(), _getAccentColor(), _getRadius(), _getScaleCoefficient());
};
const _getRadius = (): number => {
return ((height || props.height || 0) - (PADDING * _getScaleCoefficient()) * 2) / 2;
};
const _getScaleCoefficient = (): number => {
return (height || props.height || 0) / 982;
};
const _getCX = (): number => {
return Math.floor((width || 0) / 2);
};
const _getCY = (): number => {
return Math.floor(_getRadius() + (PADDING * _getScaleCoefficient()));
};
const _getAccentColor = (): string => {
return accentColor || '#ffffff';
};
const _getBackgroundColor = (): string => {
return backgroundColor || '#000000';
};
const _getForegroundColor = (): string => {
return foregroundColor || '#ffffff';
};
const seekBy = (time: number) => {
if (audio.current) {
const currentTime = audio.current.currentTime + time;
if (!isNaN(currentTime)) {
setCurrentTime(currentTime);
audio.current.currentTime = currentTime;
}
}
};
const handleAudioKeyDown: React.KeyboardEventHandler = e => {
// On the audio element or the seek bar, we can safely use the space bar
// for playback control because there are no buttons to press
if (e.key === ' ') {
e.preventDefault();
e.stopPropagation();
togglePlay();
}
};
const handleKeyDown: React.KeyboardEventHandler = e => {
switch (e.key) {
case 'k':
e.preventDefault();
e.stopPropagation();
togglePlay();
break;
case 'm':
e.preventDefault();
e.stopPropagation();
toggleMute();
break;
case 'j':
e.preventDefault();
e.stopPropagation();
seekBy(-10);
break;
case 'l':
e.preventDefault();
e.stopPropagation();
seekBy(10);
break;
}
};
const getDuration = () => duration || props.duration || 0;
const progress = Math.min((currentTime / getDuration()) * 100, 100);
useEffect(() => {
if (player.current) {
_setDimensions();
}
}, [player.current]);
useEffect(() => {
if (audio.current) {
setVolume(audio.current.volume);
setMuted(audio.current.muted);
}
}, [audio.current]);
useEffect(() => {
if (canvas.current && visualizer.current) {
visualizer.current.setCanvas(canvas.current);
}
}, [canvas.current, visualizer.current]);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
if (!paused && audio.current && deployPictureInPicture) {
deployPictureInPicture('audio', _pack());
}
};
}, []);
useEffect(() => {
_clear();
_draw();
}, [src, width, height, accentColor]);
return (
<div
className={classNames('audio-player', { editable })}
ref={player}
style={{
backgroundColor: _getBackgroundColor(),
color: _getForegroundColor(),
width: '100%',
height: fullscreen ? '100%' : (height || props.height),
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
tabIndex={0}
onKeyDown={handleKeyDown}
>
<audio
src={src}
ref={audio}
preload='auto'
onPlay={handlePlay}
onPause={handlePause}
onProgress={handleProgress}
onLoadedData={handleLoadedData}
crossOrigin='anonymous'
/>
<canvas
role='button'
tabIndex={0}
className='audio-player__canvas'
width={width}
height={height}
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }}
ref={canvas}
onClick={togglePlay}
onKeyDown={handleAudioKeyDown}
title={alt}
aria-label={alt}
/>
{poster && (
<img
src={poster}
alt=''
width={(_getRadius() - TICK_SIZE) * 2}
height={(_getRadius() - TICK_SIZE) * 2}
style={{
position: 'absolute',
left: _getCX(),
top: _getCY(),
transform: 'translate(-50%, -50%)',
borderRadius: '50%',
pointerEvents: 'none',
}}
/>
)}
<div className='video-player__seek' onMouseDown={handleMouseDown} ref={seek}>
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} />
<div
className='video-player__seek__progress'
style={{ width: `${progress}%`, backgroundColor: _getAccentColor() }}
/>
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex={0}
style={{ left: `${progress}%`, backgroundColor: _getAccentColor() }}
onKeyDown={handleAudioKeyDown}
/>
</div>
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button
type='button'
title={intl.formatMessage(paused ? messages.play : messages.pause)}
aria-label={intl.formatMessage(paused ? messages.play : messages.pause)}
className='player-button'
onClick={togglePlay}
>
<Icon src={paused ? require('@tabler/icons/player-play.svg') : require('@tabler/icons/player-pause.svg')} />
</button>
<button
type='button'
title={intl.formatMessage(muted ? messages.unmute : messages.mute)}
aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)}
className='player-button'
onClick={toggleMute}
>
<Icon src={muted ? require('@tabler/icons/volume-3.svg') : require('@tabler/icons/volume.svg')} />
</button>
<div
className={classNames('video-player__volume', { active: hovered })}
ref={slider}
onMouseDown={handleVolumeMouseDown}
>
<div
className='video-player__volume__current'
style={{
width: `${volume * 100}%`,
backgroundColor: _getAccentColor(),
}}
/>
<span
className='video-player__volume__handle'
tabIndex={0}
style={{ left: `${volume * 100}%`, backgroundColor: _getAccentColor() }}
/>
</div>
<span className='video-player__time'>
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span>
{getDuration() && (<>
<span className='video-player__time-sep'>/</span>
<span className='video-player__time-total'>{formatTime(Math.floor(getDuration()))}</span>
</>)}
</span>
</div>
<div className='video-player__buttons right'>
<a
title={intl.formatMessage(messages.download)}
aria-label={intl.formatMessage(messages.download)}
className='video-player__download__icon player-button'
href={src}
download
target='_blank'
>
<Icon src={require('@tabler/icons/download.svg')} />
</a>
</div>
</div>
</div>
</div>
);
};
export default Audio;