Базовая версия голосовых сообщений и аудиоплеер. Кодирование OPUS
This commit is contained in:
@@ -1,25 +1,261 @@
|
||||
import { createContext, useRef } from "react";
|
||||
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);
|
||||
|
||||
const PlayerContext = createContext(null);
|
||||
/**
|
||||
* Провайдер для Audio/Video плеера
|
||||
*/
|
||||
interface PlayerProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export function PlayerProvider(props : PlayerProviderProps) {
|
||||
|
||||
export function PlayerProvider(props: PlayerProviderProps) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
const playVoice = () => {
|
||||
|
||||
}
|
||||
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={null}>
|
||||
<PlayerContext.Provider
|
||||
value={{
|
||||
playAudio,
|
||||
playing: isPlaying,
|
||||
pause,
|
||||
resume,
|
||||
stop,
|
||||
setDuration,
|
||||
duration,
|
||||
totalDuration,
|
||||
currentMessageId,
|
||||
lastMessageId,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<audio ref={audioRef} />
|
||||
</PlayerContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user