From 17926ea9c1a9624c0e9ae95cf9ec6dae17f8717d Mon Sep 17 00:00:00 2001 From: riyad Date: Wed, 30 Oct 2024 18:56:46 +0600 Subject: [PATCH 1/2] feat: updated ref --- src/components/AudioPlayer.tsx | 837 +++++++++++++++++---------------- src/components/index.ts | 1 + 2 files changed, 434 insertions(+), 404 deletions(-) diff --git a/src/components/AudioPlayer.tsx b/src/components/AudioPlayer.tsx index b7327c6..56f8b19 100644 --- a/src/components/AudioPlayer.tsx +++ b/src/components/AudioPlayer.tsx @@ -1,457 +1,486 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { AudioInterface, AudioPlayerRef } from './core.interface'; import { iconPaths } from '../helpers/icons/icons'; import { formatTime } from '../helpers/utils/formatTime'; import { getRangeBox } from '../helpers/utils/getRangeBox'; import getDeviceEventNames from '../helpers/utils/getDeviceEventNames'; import './audioPlay.css'; -export interface AudioInterface { - autoPlay?: boolean; - className?: string; - src: string; - loop?: boolean; - preload?: 'auto' | 'metadata' | 'none'; - backgroundColor?: string; - color?: string; - width?: number | string; - style?: React.CSSProperties; - sliderColor?: string; - volume?: number; - volumePlacement?: 'top' | 'bottom'; - hasKeyBindings?: boolean; - onPlay?: () => void; - onPause?: () => void; - onEnd?: () => void; - onError?: (event: React.SyntheticEvent, errorMessage: string) => void; -} - -export const AudioPlayer: React.FC = ({ - autoPlay = false, - className = '', - src, - loop = false, - preload = 'auto', - backgroundColor, - color, - width, - style, - sliderColor, - volume = 100, - volumePlacement = 'top', - hasKeyBindings = true, - onPlay, - onPause, - onEnd, - onError -}) => { - const audioRef = useRef(null); - const currentlyDragged = useRef(null); - const rewindPin = useRef(null); - const volumePin = useRef(null); - const [canPlay, setCanPlay] = useState(preload === 'none'); - const [isPlaying, setIsPlaying] = useState(false); - const [progressBarPercent, setProgressBarPercent] = useState(0); - const [currentTime, setCurrentTime] = useState('0:00'); - const [totalTime, setTotalTime] = useState('--:--'); - const [volumeOpen, setVolumeOpen] = useState(false); - const [volumeProgress, setVolumeProgress] = useState(100); - const [speakerIcon, setSpeakerIcon] = useState(getVolumePath(volume)); - const [coefficient, setCoefficient] = useState(0); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - handleReload(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [src]); - - useEffect(() => { - if (audioRef.current?.duration && audioRef.current.duration !== Infinity) { - setTotalTime(formatTime(audioRef.current.duration)); - } - }, [audioRef.current?.duration]); +export const AudioPlayer = forwardRef( + ( + { + autoPlay = false, + className = '', + src, + loop = false, + preload = 'auto', + backgroundColor, + color, + width, + style, + sliderColor, + volume = 100, + volumePlacement = 'top', + hasKeyBindings = true, + onPlay, + onPause, + onEnd, + onError + }, + ref + ) => { + const wrapperRef = useRef(null); + const audioRef = useRef(null); + const currentlyDragged = useRef(null); + const rewindPin = useRef(null); + const volumePin = useRef(null); + const [canPlay, setCanPlay] = useState(preload === 'none'); + const [isPlaying, setIsPlaying] = useState(false); + const [progressBarPercent, setProgressBarPercent] = useState(0); + const [currentTime, setCurrentTime] = useState('0:00'); + const [totalTime, setTotalTime] = useState('--:--'); + const [volumeOpen, setVolumeOpen] = useState(false); + const [volumeProgress, setVolumeProgress] = useState(100); + const [speakerIcon, setSpeakerIcon] = useState(getVolumePath(volume)); + const [coefficient, setCoefficient] = useState(0); + const [hasError, setHasError] = useState(false); + + useEffect(() => { + handleReload(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [src]); + + useEffect(() => { + if (audioRef.current?.duration && audioRef.current.duration !== Infinity) { + setTotalTime(formatTime(audioRef.current.duration)); + } + }, [audioRef.current?.duration]); + + useEffect(() => { + if (!isNaN(volume)) { + const tempVol = volume > 100 ? 100 : volume < 0 ? 0 : volume; + setVolumeProgress(tempVol); + if (audioRef.current) { + audioRef.current.volume = tempVol / 100; + } + } + }, [volume]); - useEffect(() => { - if (!isNaN(volume)) { - const tempVol = volume > 100 ? 100 : volume < 0 ? 0 : volume; - setVolumeProgress(tempVol); - if (audioRef.current) { - audioRef.current.volume = tempVol / 100; + useImperativeHandle(ref, () => ({ + play: () => { + play(); + }, + pause: () => { + pause(); + }, + stop: () => { + stop(); + }, + focus: () => { + focus(); } - } - }, [volume]); + })); - const getTotalDuration = () => { - if (!audioRef.current) { - return 0; - } - return audioRef.current.duration !== Infinity ? audioRef.current.duration : audioRef.current.buffered.end(0); - }; + const getTotalDuration = () => { + if (!audioRef.current) { + return 0; + } + return audioRef.current.duration !== Infinity ? audioRef.current.duration : audioRef.current.buffered.end(0); + }; - const handleCanPlay = () => { - setCanPlay(true); - }; + const handleCanPlay = () => { + setCanPlay(true); + }; - const handleReload = () => { - if (audioRef.current) { - setIsPlaying(false); - setTotalTime('--:--'); - setCanPlay(false); - setHasError(false); - audioRef.current.src = src; - audioRef.current.load(); - } - }; - - const handleOnError = (event: React.SyntheticEvent) => { - setCanPlay(true); - setHasError(true); - if (onError) { - const mediaError = (event.target as HTMLAudioElement).error; - let errorMessage = 'An unknown error occurred.'; - - if (mediaError?.code) { - switch (mediaError?.code) { - case mediaError.MEDIA_ERR_ABORTED: - errorMessage = 'The media playback was aborted.'; - break; - case mediaError.MEDIA_ERR_NETWORK: - errorMessage = 'A network error caused the media to fail.'; - break; - case mediaError.MEDIA_ERR_DECODE: - errorMessage = 'The media playback was aborted due to a decoding error.'; - break; - case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: - errorMessage = 'The media source format is not supported.'; - break; - default: - errorMessage = 'An unknown error occurred.'; - break; + const handleReload = () => { + if (audioRef.current) { + setIsPlaying(false); + setTotalTime('--:--'); + setCanPlay(false); + setHasError(false); + audioRef.current.src = src; + audioRef.current.load(); + } + }; + + const handleOnError = (event: React.SyntheticEvent) => { + setCanPlay(true); + setHasError(true); + if (onError) { + const mediaError = (event.target as HTMLAudioElement).error; + let errorMessage = 'An unknown error occurred.'; + + if (mediaError?.code) { + switch (mediaError?.code) { + case mediaError.MEDIA_ERR_ABORTED: + errorMessage = 'The media playback was aborted.'; + break; + case mediaError.MEDIA_ERR_NETWORK: + errorMessage = 'A network error caused the media to fail.'; + break; + case mediaError.MEDIA_ERR_DECODE: + errorMessage = 'The media playback was aborted due to a decoding error.'; + break; + case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: + errorMessage = 'The media source format is not supported.'; + break; + default: + errorMessage = 'An unknown error occurred.'; + break; + } } + + onError(event, errorMessage); } + }; - onError(event, errorMessage); - } - }; + const handleEnded = () => { + setIsPlaying(false); + if (audioRef.current) { + audioRef.current.currentTime = 0; + if (totalTime === '--:--') { + handleReload(); + } + if (onEnd) { + onEnd(); + } + } + }; - const handleEnded = () => { - setIsPlaying(false); - if (audioRef.current) { - audioRef.current.currentTime = 0; - if (totalTime === '--:--') { - handleReload(); + const handleUpdateVolume = () => { + if (audioRef.current) { + setVolumeProgress(audioRef.current.volume * 100); + if (audioRef.current.volume >= 0.5) { + setSpeakerIcon(iconPaths.fullVolume); + } else if (audioRef.current.volume < 0.5 && audioRef.current.volume > 0.05) { + setSpeakerIcon(iconPaths.midVolume); + } else if (audioRef.current.volume <= 0.05) { + setSpeakerIcon(iconPaths.lowVolume); + } } - if (onEnd) { - onEnd(); + }; + + const handleUpdateProgress = () => { + if (audioRef.current) { + const current = audioRef.current.currentTime; + const percent = (current / getTotalDuration()) * 100; + setProgressBarPercent(percent); + setCurrentTime(formatTime(current)); } - } - }; - - const handleUpdateVolume = () => { - if (audioRef.current) { - setVolumeProgress(audioRef.current.volume * 100); - if (audioRef.current.volume >= 0.5) { - setSpeakerIcon(iconPaths.fullVolume); - } else if (audioRef.current.volume < 0.5 && audioRef.current.volume > 0.05) { - setSpeakerIcon(iconPaths.midVolume); - } else if (audioRef.current.volume <= 0.05) { - setSpeakerIcon(iconPaths.lowVolume); + }; + + const handleLoadedMetaData = () => { + if (audioRef.current?.duration && audioRef.current?.duration !== Infinity) { + setTotalTime(formatTime(audioRef.current.duration ?? 0)); + const currentTime = audioRef.current.duration * coefficient; + audioRef.current.currentTime = currentTime; } - } - }; - - const handleUpdateProgress = () => { - if (audioRef.current) { - const current = audioRef.current.currentTime; - const percent = (current / getTotalDuration()) * 100; - setProgressBarPercent(percent); - setCurrentTime(formatTime(current)); - } - }; + }; - const handleLoadedMetaData = () => { - if (audioRef.current?.duration && audioRef.current?.duration !== Infinity) { - setTotalTime(formatTime(audioRef.current.duration ?? 0)); - const currentTime = audioRef.current.duration * coefficient; - audioRef.current.currentTime = currentTime; - } - }; + function getVolumePath(volumeLevel: number) { + const MIN_VOLUME = 0; + const MAX_VOLUME = 100; - function getVolumePath(volumeLevel: number) { - const MIN_VOLUME = 0; - const MAX_VOLUME = 100; + volumeLevel = isNaN(volumeLevel) ? 100 : Math.max(MIN_VOLUME, Math.min(volumeLevel, MAX_VOLUME)); - volumeLevel = isNaN(volumeLevel) ? 100 : Math.max(MIN_VOLUME, Math.min(volumeLevel, MAX_VOLUME)); + if (volumeLevel >= 50) { + return iconPaths.fullVolume; + } else if (volumeLevel > 5) { + return iconPaths.midVolume; + } - if (volumeLevel >= 50) { - return iconPaths.fullVolume; - } else if (volumeLevel > 5) { - return iconPaths.midVolume; + return iconPaths.lowVolume; } - return iconPaths.lowVolume; - } + const togglePlay = () => { + if (audioRef.current) { + if (preload === 'none' && !audioRef.current.duration) { + setCanPlay(false); + } - const togglePlay = () => { - if (audioRef.current) { - if (preload === 'none' && !audioRef.current.duration) { - setCanPlay(false); + if (audioRef.current.paused) { + play(); + } else { + pause(); + } } + }; - if (audioRef.current.paused) { - audioRef.current.play(); + const play = () => { + if (hasError) { + handleReload(); + } else { + audioRef.current?.play(); setIsPlaying(true); if (onPlay) { onPlay(); } - } else { - audioRef.current.pause(); - setIsPlaying(false); - if (onPause) { - onPause(); - } } - } - }; - - const inRange = (event: MouseEvent | TouchEvent | React.MouseEvent) => { - const rangeBox = getRangeBox(event, currentlyDragged.current); - const rect = rangeBox.getBoundingClientRect(); - const direction = rangeBox.dataset.direction; - if (direction === 'horizontal') { - const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX; - if (clientX - rect.left < 0 || clientX - rect.right > 0) return false; - } else { - const min = rect.top; - const max = min + rangeBox.offsetHeight; - const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY; - if (clientY < min || clientY > max) return false; - } - return true; - }; - - function getCoefficient(event: MouseEvent | TouchEvent | React.MouseEvent) { - const slider = getRangeBox(event, currentlyDragged.current); - const rect = slider.getBoundingClientRect(); - let K = 0; - - if (slider.dataset.direction === 'horizontal') { - const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX; - const offsetX = clientX - rect.left; - const width = slider.clientWidth; - K = offsetX / width; - } else if (slider.dataset.direction === 'vertical') { - const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY; - const height = slider.clientHeight; - const offsetY = clientY - rect.top; - K = 1 - offsetY / height; - } + }; - return K; - } + const pause = () => { + audioRef.current?.pause(); + setIsPlaying(false); + if (onPause) { + onPause(); + } + }; - const rewind = (event: MouseEvent | TouchEvent | React.MouseEvent) => { - if (inRange(event) && audioRef.current) { - if (preload === 'none' && !audioRef.current.duration) { - setCanPlay(false); - audioRef.current.load(); - setCoefficient(getCoefficient(event)); - } else if (audioRef.current.duration) { - audioRef.current.currentTime = getTotalDuration() * getCoefficient(event); + const stop = () => { + if (audioRef.current) { + audioRef.current.pause(); + setIsPlaying(false); + audioRef.current.currentTime = 0; + } + }; + + const focus = () => { + wrapperRef.current?.focus(); + }; + + const inRange = (event: MouseEvent | TouchEvent | React.MouseEvent) => { + const rangeBox = getRangeBox(event, currentlyDragged.current); + const rect = rangeBox.getBoundingClientRect(); + const direction = rangeBox.dataset.direction; + if (direction === 'horizontal') { + const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX; + if (clientX - rect.left < 0 || clientX - rect.right > 0) return false; + } else { + const min = rect.top; + const max = min + rangeBox.offsetHeight; + const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY; + if (clientY < min || clientY > max) return false; + } + return true; + }; + + function getCoefficient(event: MouseEvent | TouchEvent | React.MouseEvent) { + const slider = getRangeBox(event, currentlyDragged.current); + const rect = slider.getBoundingClientRect(); + let K = 0; + + if (slider.dataset.direction === 'horizontal') { + const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX; + const offsetX = clientX - rect.left; + const width = slider.clientWidth; + K = offsetX / width; + } else if (slider.dataset.direction === 'vertical') { + const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY; + const height = slider.clientHeight; + const offsetY = clientY - rect.top; + K = 1 - offsetY / height; } - } - }; - const changeVolume = (event: MouseEvent | TouchEvent | React.MouseEvent) => { - if (inRange(event) && audioRef.current) { - audioRef.current.volume = getCoefficient(event); + return K; } - }; - - const handleRewindDragging = () => { - currentlyDragged.current = rewindPin.current; - const events = getDeviceEventNames(); - window.addEventListener(events.move, rewind, false); - - window.addEventListener( - events.up, - () => { - currentlyDragged.current = null; - window.removeEventListener(events.move, rewind, false); - }, - { once: true } - ); - }; - - const handleVolumeDragging = () => { - currentlyDragged.current = volumePin.current; - const events = getDeviceEventNames(); - window.addEventListener(events.move, changeVolume, false); + const rewind = (event: MouseEvent | TouchEvent | React.MouseEvent) => { + if (inRange(event) && audioRef.current) { + if (preload === 'none' && !audioRef.current.duration) { + setCanPlay(false); + audioRef.current.load(); + setCoefficient(getCoefficient(event)); + } else if (audioRef.current.duration) { + audioRef.current.currentTime = getTotalDuration() * getCoefficient(event); + } + } + }; - window.addEventListener( - events.up, - () => { - currentlyDragged.current = null; - window.removeEventListener(events.move, changeVolume, false); - }, - false - ); - }; + const changeVolume = (event: MouseEvent | TouchEvent | React.MouseEvent) => { + if (inRange(event) && audioRef.current) { + audioRef.current.volume = getCoefficient(event); + } + }; + + const handleRewindDragging = () => { + currentlyDragged.current = rewindPin.current; + const events = getDeviceEventNames(); + window.addEventListener(events.move, rewind, false); + + window.addEventListener( + events.up, + () => { + currentlyDragged.current = null; + window.removeEventListener(events.move, rewind, false); + }, + { once: true } + ); + }; + + const handleVolumeDragging = () => { + currentlyDragged.current = volumePin.current; + const events = getDeviceEventNames(); + + window.addEventListener(events.move, changeVolume, false); + + window.addEventListener( + events.up, + () => { + currentlyDragged.current = null; + window.removeEventListener(events.move, changeVolume, false); + }, + false + ); + }; + + const adjustAudioTime = (percentage: number) => { + if (audioRef.current) { + const currentTime = audioRef.current.currentTime + getTotalDuration() * (percentage / 100); + audioRef.current.currentTime = Math.min(currentTime, getTotalDuration()); + } + }; - const adjustAudioTime = (percentage: number) => { - if (audioRef.current) { - const currentTime = audioRef.current.currentTime + getTotalDuration() * (percentage / 100); - audioRef.current.currentTime = Math.min(currentTime, getTotalDuration()); - } - }; + const adjustVolume = (delta: number) => { + if (audioRef.current) { + audioRef.current.volume = Math.max(0, Math.min(1, audioRef.current.volume + delta)); + } + }; - const adjustVolume = (delta: number) => { - if (audioRef.current) { - audioRef.current.volume = Math.max(0, Math.min(1, audioRef.current.volume + delta)); - } - }; + const handleKeyPress = (event: React.KeyboardEvent) => { + if (!hasKeyBindings) { + return; + } + event.preventDefault(); + switch (event.key) { + case 'ArrowLeft': + adjustAudioTime(-5); + break; + case 'ArrowRight': + adjustAudioTime(5); + break; + case 'ArrowUp': + adjustVolume(0.05); + break; + case 'ArrowDown': + adjustVolume(-0.05); + break; + case ' ': + togglePlay(); + break; + default: + //Nothing to do + break; + } + }; + + const handleOnPlay = () => { + setIsPlaying(true); + }; + + return ( +
+ {hasError && ( + + + + + + )} + {!canPlay && !hasError && ( +
+
+
+ )} + {canPlay && !hasError && ( +
togglePlay()}> + + + +
+ )} - const handleKeyPress = (event: React.KeyboardEvent) => { - if (!hasKeyBindings) { - return; - } - event.preventDefault(); - switch (event.key) { - case 'ArrowLeft': - adjustAudioTime(-5); - break; - case 'ArrowRight': - adjustAudioTime(5); - break; - case 'ArrowUp': - adjustVolume(0.05); - break; - case 'ArrowDown': - adjustVolume(-0.05); - break; - case ' ': - togglePlay(); - break; - default: - //Nothing to do - break; - } - }; - - const handleOnPlay = () => { - setIsPlaying(true); - }; - - return ( -
- {hasError && ( - - - - - - )} - {!canPlay && !hasError && ( -
-
-
- )} - {canPlay && !hasError && ( -
togglePlay()}> - - - -
- )} - -
- {currentTime} -
-
+
+ {currentTime} +
+ className="rap-progress" + style={{ + ...{ width: progressBarPercent + '%' }, + ...(sliderColor ? { backgroundColor: sliderColor } : {}) + }} + > +
+
+ {totalTime !== '--:--' && {totalTime}}
- {totalTime !== '--:--' && {totalTime}} -
-
-
setVolumeOpen((vol) => !vol)}> - - - -
-
-
{ - e.preventDefault(); - e.stopPropagation(); - }} - > -
-
+
+
setVolumeOpen((vol) => !vol)}> + + + +
+
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + > +
+ className="rap-progress" + style={{ + ...{ height: `${volumeProgress}%` }, + ...(sliderColor ? { backgroundColor: sliderColor } : {}) + }} + > +
+
+
setVolumeOpen(false)}>
-
setVolumeOpen(false)}>
+ +
+ ); + } +); - -
- ); -}; +AudioPlayer.displayName = 'AudioPlayer'; diff --git a/src/components/index.ts b/src/components/index.ts index 58f3d53..338d001 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1 +1,2 @@ export * from './AudioPlayer'; +export * from './core.interface'; From e910359e47476135b3581178432585cbbde5e98c Mon Sep 17 00:00:00 2001 From: riyad Date: Wed, 30 Oct 2024 18:57:49 +0600 Subject: [PATCH 2/2] feat: added missing file --- src/components/core.interface.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/components/core.interface.ts diff --git a/src/components/core.interface.ts b/src/components/core.interface.ts new file mode 100644 index 0000000..8d18896 --- /dev/null +++ b/src/components/core.interface.ts @@ -0,0 +1,26 @@ +export interface AudioInterface { + autoPlay?: boolean; + className?: string; + src: string; + loop?: boolean; + preload?: 'auto' | 'metadata' | 'none'; + backgroundColor?: string; + color?: string; + width?: number | string; + style?: React.CSSProperties; + sliderColor?: string; + volume?: number; + volumePlacement?: 'top' | 'bottom'; + hasKeyBindings?: boolean; + onPlay?: () => void; + onPause?: () => void; + onEnd?: () => void; + onError?: (event: React.SyntheticEvent, errorMessage: string) => void; +} + +export interface AudioPlayerRef { + play: () => void; + pause: () => void; + stop: () => void; + focus: () => void; +}