From ba12db3c72601b81e5f00ea1fac2152e848b1ce5 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Fri, 10 Apr 2026 17:54:48 +0200 Subject: [PATCH] =?UTF-8?q?OPUS=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MessageAttachments/MessageVoice.tsx | 6 +- .../PlayerProvider/PlayerProvider.tsx | 57 ++++++++++++++++--- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/app/components/MessageAttachments/MessageVoice.tsx b/app/components/MessageAttachments/MessageVoice.tsx index ce04dda..a9c1e59 100644 --- a/app/components/MessageAttachments/MessageVoice.tsx +++ b/app/components/MessageAttachments/MessageVoice.tsx @@ -114,8 +114,6 @@ export function MessageVoice(props: AttachmentProps) { totalDuration, currentMessageId, } = usePlayerContext(); - - // Важно: состояние "активности" теперь берется из глобального плеера const messageId = String((props.parent as any)?.id ?? (props.attachment as any)?.messageId ?? props.attachment.id); const isCurrentTrack = currentMessageId === messageId; @@ -123,7 +121,7 @@ export function MessageVoice(props: AttachmentProps) { const safeCurrent = isCurrentTrack ? currentDuration : 0; const playbackProgress = Math.max(0, Math.min(1, safeCurrent / fullDuration)); - const createAudioBlob = () => new Blob([Buffer.from(props.attachment.blob, "binary")], { type: "audio/webm" }); + const createAudioBlob = () => new Blob([Buffer.from(props.attachment.blob, "binary")], { type: "audio/webm;codecs=opus" }); const ensureStarted = (seekToSec?: number) => { const blob = createAudioBlob(); @@ -204,7 +202,7 @@ export function MessageVoice(props: AttachmentProps) { {downloadStatus !== DownloadStatus.DOWNLOADED && } - {downloadStatus === DownloadStatus.DOWNLOADED && + {downloadStatus === DownloadStatus.DOWNLOADED && !isUploading && (isCurrentTrack && playing ? ( ) : ( diff --git a/app/providers/PlayerProvider/PlayerProvider.tsx b/app/providers/PlayerProvider/PlayerProvider.tsx index 6a62109..06b0a66 100644 --- a/app/providers/PlayerProvider/PlayerProvider.tsx +++ b/app/providers/PlayerProvider/PlayerProvider.tsx @@ -16,6 +16,7 @@ export interface PlayerContextValue { totalDuration: number; currentMessageId: string | null; lastMessageId: string | null; + lastError: string | null; } export const PlayerContext = createContext(null); @@ -40,6 +41,7 @@ export function PlayerProvider(props: PlayerProviderProps) { const [isPlaying, setIsPlaying] = useState(false); const [duration, setDurationState] = useState(0); const [totalDuration, setTotalDuration] = useState(0); + const [lastError, setLastError] = useState(null); const [currentMessageId, setCurrentMessageId] = useState(null); const [lastMessageId, setLastMessageId] = useState(null); @@ -64,6 +66,22 @@ export function PlayerProvider(props: PlayerProviderProps) { setTotalDuration(safe); }; + const decodeMediaError = (err: MediaError | null) => { + if (!err) return "Unknown media error"; + switch (err.code) { + case MediaError.MEDIA_ERR_ABORTED: + return "Playback aborted"; + case MediaError.MEDIA_ERR_NETWORK: + return "Network error while loading audio"; + case MediaError.MEDIA_ERR_DECODE: + return "Audio decode error"; + case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: + return "Audio source is not supported"; + default: + return `Unknown media error (${err.code})`; + } + }; + useEffect(() => { const audio = audioRef.current; if (!audio) return; @@ -89,6 +107,7 @@ export function PlayerProvider(props: PlayerProviderProps) { if (isLoadingRef.current) return; if (isSeekingRef.current) return; if (rafTimeUpdateRef.current != null) return; + rafTimeUpdateRef.current = requestAnimationFrame(() => { rafTimeUpdateRef.current = null; if (!isLoadingRef.current && !isSeekingRef.current) { @@ -103,9 +122,7 @@ export function PlayerProvider(props: PlayerProviderProps) { const onSeeked = () => { if (isSeekingRef.current) { isSeekingRef.current = false; - if (!isLoadingRef.current) { - commitDuration(audio.currentTime || 0); - } + if (!isLoadingRef.current) commitDuration(audio.currentTime || 0); return; } if (isLoadingRef.current) return; @@ -113,9 +130,20 @@ export function PlayerProvider(props: PlayerProviderProps) { }; const onCanPlay = () => { - if (isLoadingRef.current) { - isLoadingRef.current = false; - } + if (isLoadingRef.current) isLoadingRef.current = false; + }; + + const onError = (_e: Event) => { + const message = decodeMediaError(audio.error); + setLastError(message); + + console.error("Audio playback error", { + message, + mediaError: audio.error, + currentSrc: audio.currentSrc, + readyState: audio.readyState, + networkState: audio.networkState, + }); }; audio.addEventListener("play", onPlay); @@ -126,6 +154,7 @@ export function PlayerProvider(props: PlayerProviderProps) { audio.addEventListener("durationchange", onDurationChange); audio.addEventListener("seeked", onSeeked); audio.addEventListener("canplay", onCanPlay); + audio.addEventListener("error", onError); return () => { audio.removeEventListener("play", onPlay); @@ -136,6 +165,7 @@ export function PlayerProvider(props: PlayerProviderProps) { audio.removeEventListener("durationchange", onDurationChange); audio.removeEventListener("seeked", onSeeked); audio.removeEventListener("canplay", onCanPlay); + audio.removeEventListener("error", onError); if (rafTimeUpdateRef.current != null) { cancelAnimationFrame(rafTimeUpdateRef.current); @@ -162,6 +192,12 @@ export function PlayerProvider(props: PlayerProviderProps) { const el = audioRef.current; if (!el) return; + // чтобы не было warning о неиспользуемых args при строгих правилах + void artist; + void title; + + setLastError(null); + if (objectUrlRef.current) { URL.revokeObjectURL(objectUrlRef.current); objectUrlRef.current = null; @@ -176,7 +212,6 @@ export function PlayerProvider(props: PlayerProviderProps) { isSeekingRef.current = false; el.src = audioSrc; - durationRef.current = 0; const msgId = messageId ?? null; @@ -185,6 +220,7 @@ export function PlayerProvider(props: PlayerProviderProps) { isPlayingRef.current = true; setIsPlaying(true); + const prevDuration = durationRef.current; requestAnimationFrame(() => { if (durationRef.current === prevDuration) { @@ -192,9 +228,10 @@ export function PlayerProvider(props: PlayerProviderProps) { } }); - void el.play().catch(() => { + void el.play().catch((err) => { isLoadingRef.current = false; commitPlaying(false); + setLastError(err instanceof Error ? err.message : "play() failed"); }); }; @@ -210,8 +247,9 @@ export function PlayerProvider(props: PlayerProviderProps) { commitPlaying(true); - void el.play().catch(() => { + void el.play().catch((err) => { commitPlaying(false); + setLastError(err instanceof Error ? err.message : "resume() failed"); }); }; @@ -252,6 +290,7 @@ export function PlayerProvider(props: PlayerProviderProps) { totalDuration, currentMessageId, lastMessageId, + lastError, }} > {props.children}