261 lines
7.6 KiB
TypeScript
261 lines
7.6 KiB
TypeScript
import { createContext, useEffect, useRef, useState } from "react";
|
|
|
|
export interface PlayerContextValue {
|
|
playAudio: (
|
|
artist: string,
|
|
title: string,
|
|
audio: string | Blob | File,
|
|
messageId?: string | null
|
|
) => void;
|
|
playing: boolean;
|
|
pause: () => void;
|
|
resume: () => void;
|
|
stop: () => void;
|
|
setDuration: (duration: number) => void;
|
|
duration: number;
|
|
totalDuration: number;
|
|
currentMessageId: string | null;
|
|
lastMessageId: string | null;
|
|
}
|
|
|
|
export const PlayerContext = createContext<PlayerContextValue | null>(null);
|
|
|
|
interface PlayerProviderProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function PlayerProvider(props: PlayerProviderProps) {
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
|
const objectUrlRef = useRef<string | null>(null);
|
|
const rafTimeUpdateRef = useRef<number | null>(null);
|
|
|
|
const isLoadingRef = useRef(false);
|
|
const isSeekingRef = useRef(false);
|
|
|
|
const durationRef = useRef(0);
|
|
const totalDurationRef = useRef(0);
|
|
|
|
const isPlayingRef = useRef(false);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [duration, setDurationState] = useState(0);
|
|
const [totalDuration, setTotalDuration] = useState(0);
|
|
|
|
const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
|
|
const [lastMessageId, setLastMessageId] = useState<string | null>(null);
|
|
|
|
const commitPlaying = (next: boolean) => {
|
|
if (isPlayingRef.current === next) return;
|
|
isPlayingRef.current = next;
|
|
setIsPlaying(next);
|
|
};
|
|
|
|
const commitDuration = (next: number) => {
|
|
const safe = Number.isFinite(next) && next >= 0 ? next : 0;
|
|
if (Math.abs(safe - durationRef.current) < 0.033) return;
|
|
durationRef.current = safe;
|
|
setDurationState(safe);
|
|
};
|
|
|
|
const commitTotalDuration = (next: number) => {
|
|
const safe = Number.isFinite(next) && next > 0 ? next : 0;
|
|
if (Math.abs(safe - totalDurationRef.current) < 0.05) return;
|
|
totalDurationRef.current = safe;
|
|
setTotalDuration(safe);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const audio = audioRef.current;
|
|
if (!audio) return;
|
|
|
|
const onPlay = () => {
|
|
if (isLoadingRef.current) return;
|
|
commitPlaying(true);
|
|
};
|
|
|
|
const onPause = () => {
|
|
if (isLoadingRef.current) return;
|
|
commitPlaying(false);
|
|
};
|
|
|
|
const onEnded = () => {
|
|
commitPlaying(false);
|
|
durationRef.current = 0;
|
|
setDurationState(0);
|
|
setCurrentMessageId(null);
|
|
};
|
|
|
|
const onTimeUpdate = () => {
|
|
if (isLoadingRef.current) return;
|
|
if (isSeekingRef.current) return;
|
|
if (rafTimeUpdateRef.current != null) return;
|
|
rafTimeUpdateRef.current = requestAnimationFrame(() => {
|
|
rafTimeUpdateRef.current = null;
|
|
if (!isLoadingRef.current && !isSeekingRef.current) {
|
|
commitDuration(audio.currentTime || 0);
|
|
}
|
|
});
|
|
};
|
|
|
|
const onLoadedMetadata = () => commitTotalDuration(audio.duration);
|
|
const onDurationChange = () => commitTotalDuration(audio.duration);
|
|
|
|
const onSeeked = () => {
|
|
if (isSeekingRef.current) {
|
|
isSeekingRef.current = false;
|
|
if (!isLoadingRef.current) {
|
|
commitDuration(audio.currentTime || 0);
|
|
}
|
|
return;
|
|
}
|
|
if (isLoadingRef.current) return;
|
|
commitDuration(audio.currentTime || 0);
|
|
};
|
|
|
|
const onCanPlay = () => {
|
|
if (isLoadingRef.current) {
|
|
isLoadingRef.current = false;
|
|
}
|
|
};
|
|
|
|
audio.addEventListener("play", onPlay);
|
|
audio.addEventListener("pause", onPause);
|
|
audio.addEventListener("ended", onEnded);
|
|
audio.addEventListener("timeupdate", onTimeUpdate);
|
|
audio.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
audio.addEventListener("durationchange", onDurationChange);
|
|
audio.addEventListener("seeked", onSeeked);
|
|
audio.addEventListener("canplay", onCanPlay);
|
|
|
|
return () => {
|
|
audio.removeEventListener("play", onPlay);
|
|
audio.removeEventListener("pause", onPause);
|
|
audio.removeEventListener("ended", onEnded);
|
|
audio.removeEventListener("timeupdate", onTimeUpdate);
|
|
audio.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
audio.removeEventListener("durationchange", onDurationChange);
|
|
audio.removeEventListener("seeked", onSeeked);
|
|
audio.removeEventListener("canplay", onCanPlay);
|
|
|
|
if (rafTimeUpdateRef.current != null) {
|
|
cancelAnimationFrame(rafTimeUpdateRef.current);
|
|
rafTimeUpdateRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (objectUrlRef.current) {
|
|
URL.revokeObjectURL(objectUrlRef.current);
|
|
objectUrlRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const playAudio = (
|
|
artist: string,
|
|
title: string,
|
|
audio: string | Blob | File,
|
|
messageId?: string | null
|
|
) => {
|
|
const el = audioRef.current;
|
|
if (!el) return;
|
|
|
|
if (objectUrlRef.current) {
|
|
URL.revokeObjectURL(objectUrlRef.current);
|
|
objectUrlRef.current = null;
|
|
}
|
|
|
|
const audioSrc = typeof audio === "string" ? audio : URL.createObjectURL(audio);
|
|
if (typeof audio !== "string") {
|
|
objectUrlRef.current = audioSrc;
|
|
}
|
|
|
|
isLoadingRef.current = true;
|
|
isSeekingRef.current = false;
|
|
|
|
el.src = audioSrc;
|
|
|
|
durationRef.current = 0;
|
|
|
|
const msgId = messageId ?? null;
|
|
setCurrentMessageId(msgId);
|
|
if (msgId) setLastMessageId(msgId);
|
|
|
|
isPlayingRef.current = true;
|
|
setIsPlaying(true);
|
|
const prevDuration = durationRef.current;
|
|
requestAnimationFrame(() => {
|
|
if (durationRef.current === prevDuration) {
|
|
setDurationState(0);
|
|
}
|
|
});
|
|
|
|
void el.play().catch(() => {
|
|
isLoadingRef.current = false;
|
|
commitPlaying(false);
|
|
});
|
|
};
|
|
|
|
const pause = () => {
|
|
const el = audioRef.current;
|
|
if (!el) return;
|
|
el.pause();
|
|
};
|
|
|
|
const resume = () => {
|
|
const el = audioRef.current;
|
|
if (!el) return;
|
|
|
|
commitPlaying(true);
|
|
|
|
void el.play().catch(() => {
|
|
commitPlaying(false);
|
|
});
|
|
};
|
|
|
|
const stop = () => {
|
|
const el = audioRef.current;
|
|
if (!el) return;
|
|
|
|
isLoadingRef.current = true;
|
|
el.pause();
|
|
el.currentTime = 0;
|
|
isLoadingRef.current = false;
|
|
|
|
durationRef.current = 0;
|
|
setDurationState(0);
|
|
commitPlaying(false);
|
|
setCurrentMessageId(null);
|
|
};
|
|
|
|
const setDuration = (sec: number) => {
|
|
const el = audioRef.current;
|
|
if (!el) return;
|
|
|
|
isSeekingRef.current = true;
|
|
el.currentTime = Math.max(0, sec);
|
|
commitDuration(el.currentTime || 0);
|
|
};
|
|
|
|
return (
|
|
<PlayerContext.Provider
|
|
value={{
|
|
playAudio,
|
|
playing: isPlaying,
|
|
pause,
|
|
resume,
|
|
stop,
|
|
setDuration,
|
|
duration,
|
|
totalDuration,
|
|
currentMessageId,
|
|
lastMessageId,
|
|
}}
|
|
>
|
|
{props.children}
|
|
<audio ref={audioRef} />
|
|
</PlayerContext.Provider>
|
|
);
|
|
} |