import { CaretDownOutlined, ControlOutlined, MutedOutlined, SoundOutlined, } from '@ant-design/icons'; import { Button, InputNumber, Popover } from 'antd'; import { Slider } from 'antd'; import { observer } from 'mobx-react-lite'; import React, { useContext, useEffect, useRef, useState } from 'react'; import cn from 'classnames'; import { PlayerContext } from 'App/components/Session/playerContext'; function DropdownAudioPlayer({ audioEvents, }: { audioEvents: { payload: Record; timestamp: number }[]; }) { const { store } = useContext(PlayerContext); const [isVisible, setIsVisible] = useState(false); const [volume, setVolume] = useState(35); const [delta, setDelta] = useState(0); const [deltaInputValue, setDeltaInputValue] = useState(0); const [isMuted, setIsMuted] = useState(false); const lastPlayerTime = useRef(0); const audioRefs = useRef>({}); const fileLengths = useRef>({}); const { time = 0, speed = 1, playing, sessionStart } = store?.get() ?? {}; const files = React.useMemo(() => audioEvents.map((pa) => { const data = pa.payload; return { url: data.url, timestamp: data.timestamp, start: pa.timestamp - sessionStart, }; }), [audioEvents, sessionStart]) React.useEffect(() => { Object.entries(audioRefs.current).forEach(([url, audio]) => { if (audio) { audio.addEventListener('loadedmetadata', () => { fileLengths.current[url] = audio.duration; }) } }) }, [audioRefs.current]) const toggleMute = () => { Object.values(audioRefs.current).forEach((audio) => { if (audio) { audio.muted = !audio.muted; } }); setIsMuted(!isMuted); if (!isMuted) { onVolumeChange(0); } else { onVolumeChange(35); } }; const toggleVisible = () => { setIsVisible(!isVisible); }; const handleDelta = (value: any) => { setDeltaInputValue(parseFloat(value)); }; const onSync = () => { setDelta(deltaInputValue); handleSeek(time + deltaInputValue * 1000); }; const onCancel = () => { setDeltaInputValue(0); setIsVisible(false); }; const onReset = () => { setDelta(0); setDeltaInputValue(0); handleSeek(time); }; const onVolumeChange = (value: number) => { Object.values(audioRefs.current).forEach((audio) => { if (audio) { audio.volume = value / 100; } }); setVolume(value); setIsMuted(value === 0); }; const handleSeek = (timeMs: number) => { Object.entries(audioRefs.current).forEach(([key, audio]) => { if (audio) { const file = files.find((f) => f.url === key); if (file) { audio.currentTime = Math.max( (timeMs + delta * 1000 - file.start) / 1000, 0 ); } } }); }; const changePlaybackSpeed = (speed: number) => { Object.values(audioRefs.current).forEach((audio) => { if (audio) { audio.playbackRate = speed; } }); }; useEffect(() => { const deltaMs = delta * 1000; if (Math.abs(lastPlayerTime.current - time - deltaMs) >= 250) { handleSeek(time); } Object.entries(audioRefs.current).forEach(([url, audio]) => { if (audio) { const file = files.find((f) => f.url === url); if (audio.ended && fileLengths.current[url] < time) { return; } if (file && time >= file.start) { if (audio.paused && playing) { audio.play(); } } else { audio.pause(); } } }); lastPlayerTime.current = time + deltaMs; }, [time, delta]); useEffect(() => { Object.values(audioRefs.current).forEach((audio) => { if (audio) { audio.muted = isMuted; } }) }, [isMuted]) useEffect(() => { changePlaybackSpeed(speed); }, [speed]); useEffect(() => { Object.entries(audioRefs.current).forEach(([url, audio]) => { if (audio) { const file = files.find((f) => f.url === url); if (file && playing && time >= file.start) { audio.play(); } else { audio.pause(); } } }); setVolume(isMuted ? 0 : volume); }, [playing]); const buttonIcon = 'px-2 cursor-pointer border border-gray-light hover:border-main hover:text-main hover:z-10 h-fit' return (
} >
{isMuted ? : }
{isVisible ? (
Audio Track Synchronization
`${value}s`} parser={(value) => value?.replace('s', '') as unknown as number} stringMode onChange={handleDelta} />
) : null}
{files.map((file) => ( ))}
); } export default observer(DropdownAudioPlayer);