fix(audio,sessions): unify audio playback state and improve session discovery
Some checks failed
CI / test (push) Failing after 18s
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:
@@ -105,6 +105,26 @@ async def list_refresh_sessions_for_user(*, user_id: int) -> list[RefreshSession
|
||||
try:
|
||||
redis = get_redis_client()
|
||||
session_ids = await redis.smembers(f"auth:user_refresh:{user_id}")
|
||||
if not session_ids:
|
||||
cursor = 0
|
||||
discovered: list[str] = []
|
||||
while True:
|
||||
cursor, keys = await redis.scan(cursor=cursor, match="auth:refresh:*", count=200)
|
||||
for raw_key in keys:
|
||||
key = raw_key.decode("utf-8") if isinstance(raw_key, bytes) else str(raw_key)
|
||||
if not key.startswith("auth:refresh:"):
|
||||
continue
|
||||
jti = key.removeprefix("auth:refresh:")
|
||||
if not jti:
|
||||
continue
|
||||
owner = await redis.get(key)
|
||||
if owner and str(owner).isdigit() and int(owner) == user_id:
|
||||
discovered.append(jti)
|
||||
if cursor == 0:
|
||||
break
|
||||
if discovered:
|
||||
await redis.sadd(f"auth:user_refresh:{user_id}", *discovered)
|
||||
session_ids = {item.encode("utf-8") for item in discovered}
|
||||
sessions: list[RefreshSession] = []
|
||||
stale: list[str] = []
|
||||
for raw_jti in session_ids:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user