import { CaretDownOutlined, ControlOutlined, MutedOutlined, SoundOutlined, } from '@ant-design/icons'; import { Button, InputNumber, Popover } from 'antd'; import { Slider } from 'antd'; import cn from 'classnames'; import { observer } from 'mobx-react-lite'; import React, { useContext, useEffect, useRef, useState } from 'react'; 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; const nativeTs = data.timestamp; const startTs = nativeTs ? nativeTs > sessionStart ? nativeTs - sessionStart : nativeTs : pa.timestamp - sessionStart; return { url: data.url, timestamp: data.timestamp, start: Math.max(0, startTs), }; }), [audioEvents.length, sessionStart] ); React.useEffect(() => { Object.entries(audioRefs.current).forEach(([url, audio]) => { if (audio) { audio.loop = false; 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) { const targetTime = (timeMs + delta * 1000 - file.start) / 1000; const fileLength = fileLengths.current[key]; if (targetTime < 0 || (fileLength && targetTime > fileLength)) { audio.pause(); audio.currentTime = 0; return; } else { audio.currentTime = targetTime; } } } }); }; const changePlaybackSpeed = (speed: number) => { Object.values(audioRefs.current).forEach((audio) => { if (audio) { audio.playbackRate = speed; } }); }; useEffect(() => { const deltaMs = delta * 1000; const deltaTime = Math.abs(lastPlayerTime.current - time - deltaMs); if (deltaTime >= 250) { handleSeek(time); } Object.entries(audioRefs.current).forEach(([url, audio]) => { if (audio) { const file = files.find((f) => f.url === url); const fileLength = fileLengths.current[url]; if (file) { if (fileLength && fileLength * 1000 + file.start < time) { return; } if (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); const fileLength = fileLengths.current[url]; if (file) { if (fileLength && fileLength * 1000 + file.start < time) { audio.pause(); return; } if (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);