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}