OPUS сборка
This commit is contained in:
@@ -114,8 +114,6 @@ export function MessageVoice(props: AttachmentProps) {
|
|||||||
totalDuration,
|
totalDuration,
|
||||||
currentMessageId,
|
currentMessageId,
|
||||||
} = usePlayerContext();
|
} = usePlayerContext();
|
||||||
|
|
||||||
// Важно: состояние "активности" теперь берется из глобального плеера
|
|
||||||
const messageId = String((props.parent as any)?.id ?? (props.attachment as any)?.messageId ?? props.attachment.id);
|
const messageId = String((props.parent as any)?.id ?? (props.attachment as any)?.messageId ?? props.attachment.id);
|
||||||
const isCurrentTrack = currentMessageId === messageId;
|
const isCurrentTrack = currentMessageId === messageId;
|
||||||
|
|
||||||
@@ -123,7 +121,7 @@ export function MessageVoice(props: AttachmentProps) {
|
|||||||
const safeCurrent = isCurrentTrack ? currentDuration : 0;
|
const safeCurrent = isCurrentTrack ? currentDuration : 0;
|
||||||
const playbackProgress = Math.max(0, Math.min(1, safeCurrent / fullDuration));
|
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 ensureStarted = (seekToSec?: number) => {
|
||||||
const blob = createAudioBlob();
|
const blob = createAudioBlob();
|
||||||
@@ -204,7 +202,7 @@ export function MessageVoice(props: AttachmentProps) {
|
|||||||
|
|
||||||
{downloadStatus !== DownloadStatus.DOWNLOADED && <IconArrowDown color="white" size={22} />}
|
{downloadStatus !== DownloadStatus.DOWNLOADED && <IconArrowDown color="white" size={22} />}
|
||||||
|
|
||||||
{downloadStatus === DownloadStatus.DOWNLOADED &&
|
{downloadStatus === DownloadStatus.DOWNLOADED && !isUploading &&
|
||||||
(isCurrentTrack && playing ? (
|
(isCurrentTrack && playing ? (
|
||||||
<IconPlayerPauseFilled color="white" size={22} />
|
<IconPlayerPauseFilled color="white" size={22} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface PlayerContextValue {
|
|||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
currentMessageId: string | null;
|
currentMessageId: string | null;
|
||||||
lastMessageId: string | null;
|
lastMessageId: string | null;
|
||||||
|
lastError: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerContext = createContext<PlayerContextValue | null>(null);
|
export const PlayerContext = createContext<PlayerContextValue | null>(null);
|
||||||
@@ -40,6 +41,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [duration, setDurationState] = useState(0);
|
const [duration, setDurationState] = useState(0);
|
||||||
const [totalDuration, setTotalDuration] = useState(0);
|
const [totalDuration, setTotalDuration] = useState(0);
|
||||||
|
const [lastError, setLastError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
|
const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
|
||||||
const [lastMessageId, setLastMessageId] = useState<string | null>(null);
|
const [lastMessageId, setLastMessageId] = useState<string | null>(null);
|
||||||
@@ -64,6 +66,22 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
setTotalDuration(safe);
|
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(() => {
|
useEffect(() => {
|
||||||
const audio = audioRef.current;
|
const audio = audioRef.current;
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
@@ -89,6 +107,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
if (isLoadingRef.current) return;
|
if (isLoadingRef.current) return;
|
||||||
if (isSeekingRef.current) return;
|
if (isSeekingRef.current) return;
|
||||||
if (rafTimeUpdateRef.current != null) return;
|
if (rafTimeUpdateRef.current != null) return;
|
||||||
|
|
||||||
rafTimeUpdateRef.current = requestAnimationFrame(() => {
|
rafTimeUpdateRef.current = requestAnimationFrame(() => {
|
||||||
rafTimeUpdateRef.current = null;
|
rafTimeUpdateRef.current = null;
|
||||||
if (!isLoadingRef.current && !isSeekingRef.current) {
|
if (!isLoadingRef.current && !isSeekingRef.current) {
|
||||||
@@ -103,9 +122,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
const onSeeked = () => {
|
const onSeeked = () => {
|
||||||
if (isSeekingRef.current) {
|
if (isSeekingRef.current) {
|
||||||
isSeekingRef.current = false;
|
isSeekingRef.current = false;
|
||||||
if (!isLoadingRef.current) {
|
if (!isLoadingRef.current) commitDuration(audio.currentTime || 0);
|
||||||
commitDuration(audio.currentTime || 0);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isLoadingRef.current) return;
|
if (isLoadingRef.current) return;
|
||||||
@@ -113,9 +130,20 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCanPlay = () => {
|
const onCanPlay = () => {
|
||||||
if (isLoadingRef.current) {
|
if (isLoadingRef.current) isLoadingRef.current = false;
|
||||||
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);
|
audio.addEventListener("play", onPlay);
|
||||||
@@ -126,6 +154,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
audio.addEventListener("durationchange", onDurationChange);
|
audio.addEventListener("durationchange", onDurationChange);
|
||||||
audio.addEventListener("seeked", onSeeked);
|
audio.addEventListener("seeked", onSeeked);
|
||||||
audio.addEventListener("canplay", onCanPlay);
|
audio.addEventListener("canplay", onCanPlay);
|
||||||
|
audio.addEventListener("error", onError);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
audio.removeEventListener("play", onPlay);
|
audio.removeEventListener("play", onPlay);
|
||||||
@@ -136,6 +165,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
audio.removeEventListener("durationchange", onDurationChange);
|
audio.removeEventListener("durationchange", onDurationChange);
|
||||||
audio.removeEventListener("seeked", onSeeked);
|
audio.removeEventListener("seeked", onSeeked);
|
||||||
audio.removeEventListener("canplay", onCanPlay);
|
audio.removeEventListener("canplay", onCanPlay);
|
||||||
|
audio.removeEventListener("error", onError);
|
||||||
|
|
||||||
if (rafTimeUpdateRef.current != null) {
|
if (rafTimeUpdateRef.current != null) {
|
||||||
cancelAnimationFrame(rafTimeUpdateRef.current);
|
cancelAnimationFrame(rafTimeUpdateRef.current);
|
||||||
@@ -162,6 +192,12 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
const el = audioRef.current;
|
const el = audioRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
|
// чтобы не было warning о неиспользуемых args при строгих правилах
|
||||||
|
void artist;
|
||||||
|
void title;
|
||||||
|
|
||||||
|
setLastError(null);
|
||||||
|
|
||||||
if (objectUrlRef.current) {
|
if (objectUrlRef.current) {
|
||||||
URL.revokeObjectURL(objectUrlRef.current);
|
URL.revokeObjectURL(objectUrlRef.current);
|
||||||
objectUrlRef.current = null;
|
objectUrlRef.current = null;
|
||||||
@@ -176,7 +212,6 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
isSeekingRef.current = false;
|
isSeekingRef.current = false;
|
||||||
|
|
||||||
el.src = audioSrc;
|
el.src = audioSrc;
|
||||||
|
|
||||||
durationRef.current = 0;
|
durationRef.current = 0;
|
||||||
|
|
||||||
const msgId = messageId ?? null;
|
const msgId = messageId ?? null;
|
||||||
@@ -185,6 +220,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
|
|
||||||
isPlayingRef.current = true;
|
isPlayingRef.current = true;
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
|
||||||
const prevDuration = durationRef.current;
|
const prevDuration = durationRef.current;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (durationRef.current === prevDuration) {
|
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;
|
isLoadingRef.current = false;
|
||||||
commitPlaying(false);
|
commitPlaying(false);
|
||||||
|
setLastError(err instanceof Error ? err.message : "play() failed");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,8 +247,9 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
|
|
||||||
commitPlaying(true);
|
commitPlaying(true);
|
||||||
|
|
||||||
void el.play().catch(() => {
|
void el.play().catch((err) => {
|
||||||
commitPlaying(false);
|
commitPlaying(false);
|
||||||
|
setLastError(err instanceof Error ? err.message : "resume() failed");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -252,6 +290,7 @@ export function PlayerProvider(props: PlayerProviderProps) {
|
|||||||
totalDuration,
|
totalDuration,
|
||||||
currentMessageId,
|
currentMessageId,
|
||||||
lastMessageId,
|
lastMessageId,
|
||||||
|
lastError,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
Reference in New Issue
Block a user