fix(audio,sessions): unify audio playback state and improve session discovery
Some checks failed
CI / test (push) Failing after 18s

- move voice/audio players to single global audio engine with shared volume

- stop/reset previous track when switching to another media

- keep playback alive across chat switches via global audio element

- list refresh sessions by redis scan fallback when user session set is missing
This commit is contained in:
2026-03-08 11:48:13 +03:00
parent 27d3340a37
commit 897defc39d
3 changed files with 122 additions and 99 deletions

View File

@@ -956,88 +956,43 @@ async function downloadFileFromUrl(url: string): Promise<void> {
}
function AudioInlinePlayer({ src, title }: { src: string; title: string }) {
const audioRef = useRef<HTMLAudioElement | null>(null);
const activate = useAudioPlayerStore((s) => s.activate);
const detach = useAudioPlayerStore((s) => s.detach);
const setPlayingState = useAudioPlayerStore((s) => s.setPlaying);
const setVolumeState = useAudioPlayerStore((s) => s.setVolume);
const [isPlaying, setIsPlaying] = useState(false);
const [duration, setDuration] = useState(0);
const [position, setPosition] = useState(0);
const [volume, setVolume] = useState(1);
const track = useAudioPlayerStore((s) => s.track);
const isPlayingGlobal = useAudioPlayerStore((s) => s.isPlaying);
const durationGlobal = useAudioPlayerStore((s) => s.duration);
const positionGlobal = useAudioPlayerStore((s) => s.position);
const volumeGlobal = useAudioPlayerStore((s) => s.volume);
const playTrack = useAudioPlayerStore((s) => s.playTrack);
const togglePlayGlobal = useAudioPlayerStore((s) => s.togglePlay);
const seekToGlobal = useAudioPlayerStore((s) => s.seekTo);
const setVolumeGlobal = useAudioPlayerStore((s) => s.setVolume);
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const onLoaded = () => {
setDuration(Number.isFinite(audio.duration) ? audio.duration : 0);
};
const onTime = () => {
setPosition(audio.currentTime || 0);
};
const onEnded = () => {
setIsPlaying(false);
setPlayingState(audio, false);
};
const onPlay = () => {
activate(audio, { src, title });
setPlayingState(audio, true);
};
const onPause = () => {
setPlayingState(audio, false);
};
audio.addEventListener("loadedmetadata", onLoaded);
audio.addEventListener("timeupdate", onTime);
audio.addEventListener("ended", onEnded);
audio.addEventListener("play", onPlay);
audio.addEventListener("pause", onPause);
return () => {
audio.removeEventListener("loadedmetadata", onLoaded);
audio.removeEventListener("timeupdate", onTime);
audio.removeEventListener("ended", onEnded);
audio.removeEventListener("play", onPlay);
audio.removeEventListener("pause", onPause);
detach(audio);
};
}, [activate, detach, setPlayingState, src, title]);
const isActiveTrack = track?.src === src;
const isPlaying = isActiveTrack && isPlayingGlobal;
const duration = isActiveTrack ? durationGlobal : 0;
const position = isActiveTrack ? positionGlobal : 0;
const volume = volumeGlobal;
async function togglePlay() {
const audio = audioRef.current;
if (!audio) return;
if (isPlaying) {
audio.pause();
setIsPlaying(false);
if (isActiveTrack) {
await togglePlayGlobal();
return;
}
try {
activate(audio, { src, title });
await audio.play();
setIsPlaying(true);
} catch {
setIsPlaying(false);
}
await playTrack({ src, title });
}
function onSeek(nextValue: number) {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = nextValue;
setPosition(nextValue);
if (!isActiveTrack) {
return;
}
seekToGlobal(nextValue);
}
function onVolume(nextValue: number) {
const audio = audioRef.current;
if (!audio) return;
audio.volume = nextValue;
setVolume(nextValue);
setVolumeState(audio, nextValue);
setVolumeGlobal(nextValue);
}
return (
<div className="rounded-lg border border-sky-500/40 bg-sky-600/20 px-2 py-1.5">
<audio ref={audioRef} preload="metadata" src={src} />
<div className="flex items-center gap-2">
<button
className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-slate-900/70 text-xs text-white hover:bg-slate-900"