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

@@ -10,48 +10,71 @@ interface AudioPlayerState {
audioEl: HTMLAudioElement | null;
isPlaying: boolean;
volume: number;
activate: (audioEl: HTMLAudioElement, track: ActiveTrack) => void;
detach: (audioEl: HTMLAudioElement) => void;
setPlaying: (audioEl: HTMLAudioElement, isPlaying: boolean) => void;
setVolume: (audioEl: HTMLAudioElement, volume: number) => void;
duration: number;
position: number;
playTrack: (track: ActiveTrack) => Promise<void>;
togglePlay: () => Promise<void>;
seekTo: (seconds: number) => void;
seekBy: (secondsDelta: number) => void;
setVolume: (volume: number) => void;
stop: () => void;
}
let globalAudioEl: HTMLAudioElement | null = null;
let isBound = false;
function ensureAudio(): HTMLAudioElement {
if (globalAudioEl) {
return globalAudioEl;
}
globalAudioEl = new Audio();
globalAudioEl.preload = "metadata";
return globalAudioEl;
}
export const useAudioPlayerStore = create<AudioPlayerState>((set, get) => ({
track: null,
audioEl: null,
audioEl: ensureAudio(),
isPlaying: false,
volume: 1,
activate: (audioEl, track) => {
set({
audioEl,
track,
isPlaying: !audioEl.paused,
volume: audioEl.volume ?? 1,
});
},
detach: (audioEl) => {
const current = get().audioEl;
if (current !== audioEl) {
return;
duration: 0,
position: 0,
playTrack: async (track) => {
const audio = ensureAudio();
if (!isBound) {
audio.addEventListener("timeupdate", () => {
set({ position: audio.currentTime || 0 });
});
audio.addEventListener("loadedmetadata", () => {
set({ duration: Number.isFinite(audio.duration) ? audio.duration : 0 });
});
audio.addEventListener("play", () => set({ isPlaying: true }));
audio.addEventListener("pause", () => set({ isPlaying: false }));
audio.addEventListener("ended", () => set({ isPlaying: false, position: 0 }));
isBound = true;
}
set({ audioEl: null, track: null, isPlaying: false });
},
setPlaying: (audioEl, isPlaying) => {
if (get().audioEl !== audioEl) {
return;
const current = get().track;
if (current?.src !== track.src) {
audio.pause();
audio.currentTime = 0;
audio.src = track.src;
set({ track, position: 0, duration: 0 });
} else {
set({ track });
}
set({ isPlaying });
},
setVolume: (audioEl, volume) => {
if (get().audioEl !== audioEl) {
return;
audio.volume = get().volume;
try {
await audio.play();
set({ isPlaying: true, audioEl: audio });
} catch {
set({ isPlaying: false, audioEl: audio });
}
set({ volume });
},
togglePlay: async () => {
const audio = get().audioEl;
const audio = ensureAudio();
if (!audio.src) {
return;
}
if (!audio) {
return;
}
@@ -63,12 +86,37 @@ export const useAudioPlayerStore = create<AudioPlayerState>((set, get) => ({
audio.pause();
set({ isPlaying: false });
},
seekTo: (seconds) => {
const audio = ensureAudio();
if (!audio.src) {
return;
}
const target = Math.max(0, Math.min(Number.isFinite(audio.duration) ? audio.duration : seconds, seconds));
audio.currentTime = target;
set({ position: target });
},
seekBy: (secondsDelta) => {
const audio = ensureAudio();
if (!audio.src) {
return;
}
const duration = Number.isFinite(audio.duration) ? audio.duration : Number.MAX_SAFE_INTEGER;
const target = Math.max(0, Math.min(duration, (audio.currentTime || 0) + secondsDelta));
audio.currentTime = target;
set({ position: target });
},
setVolume: (volume) => {
const audio = ensureAudio();
const normalized = Math.max(0, Math.min(1, volume));
audio.volume = normalized;
set({ volume: normalized });
},
stop: () => {
const audio = get().audioEl;
const audio = ensureAudio();
if (audio) {
audio.pause();
audio.currentTime = 0;
}
set({ audioEl: null, track: null, isPlaying: false });
set({ audioEl: audio, track: null, isPlaying: false, position: 0, duration: 0 });
},
}));