diff --git a/app/soapbox/features/audio/index.js b/app/soapbox/features/audio/index.js
deleted file mode 100644
index 98bfc66b5..000000000
--- a/app/soapbox/features/audio/index.js
+++ /dev/null
@@ -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 (
-
-
-
-
-
- {this.props.poster &&

}
-
-
-
-
-
-
-
-
-
-
-
-
- {formatTime(Math.floor(currentTime))}
- {duration && (<>
- /
- {formatTime(Math.floor(duration))}
- >)}
-
-
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/audio/index.tsx b/app/soapbox/features/audio/index.tsx
new file mode 100644
index 000000000..0bef3c3d9
--- /dev/null
+++ b/app/soapbox/features/audio/index.tsx
@@ -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) => void,
+}
+
+const Audio: React.FC = (props) => {
+ const {
+ src,
+ alt = '',
+ poster,
+ accentColor,
+ backgroundColor,
+ foregroundColor,
+ cacheWidth,
+ fullscreen,
+ autoPlay,
+ editable,
+ deployPictureInPicture = false,
+ } = props;
+
+ const intl = useIntl();
+
+ const [width, setWidth] = useState(props.width);
+ const [height, setHeight] = useState(props.height);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [buffer, setBuffer] = useState(0);
+ const [duration, setDuration] = useState(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(new Visualizer(TICK_SIZE));
+ const audioContext = useRef(null);
+
+ const player = useRef(null);
+ const audio = useRef(null);
+ const seek = useRef(null);
+ const slider = useRef(null);
+ const canvas = useRef(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 (
+
+
+
+
+
+ {poster && (
+

+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatTime(Math.floor(currentTime))}
+ {getDuration() && (<>
+ /
+ {formatTime(Math.floor(getDuration()))}
+ >)}
+
+
+
+
+
+
+
+ );
+};
+
+export default Audio;
\ No newline at end of file