Базовая версия голосовых сообщений и аудиоплеер. Кодирование OPUS

This commit is contained in:
RoyceDa
2026-04-10 17:20:44 +02:00
parent 93ef692eb5
commit b596d36543
11 changed files with 1204 additions and 96 deletions

View File

@@ -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>
)
);
}