add 10s retry for playback on network err (#1779)

This commit is contained in:
jeffvli
2026-03-04 22:32:33 -08:00
parent 513e9e822d
commit 6ef9efc8bf
3 changed files with 72 additions and 9 deletions
+1
View File
@@ -224,6 +224,7 @@
"notificationDenied": "permissions for notifications were denied. this setting has no effect", "notificationDenied": "permissions for notifications were denied. this setting has no effect",
"openError": "could not open file", "openError": "could not open file",
"playbackError": "an error occurred when trying to play the media", "playbackError": "an error occurred when trying to play the media",
"playbackPausedDueToError": "playback was paused due to an error",
"remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server", "remoteDisableError": "an error occurred when trying to $t(common.disable) the remote server",
"remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server", "remoteEnableError": "an error occurred when trying to $t(common.enable) the remote server",
"remotePortError": "an error occurred when trying to set the remote server port", "remotePortError": "an error occurred when trying to set the remote server port",
@@ -25,6 +25,7 @@ interface WebPlayerEngineProps {
isTransitioning: boolean; isTransitioning: boolean;
onEndedPlayer1: () => void; onEndedPlayer1: () => void;
onEndedPlayer2: () => void; onEndedPlayer2: () => void;
onErrorPause: () => void;
onProgressPlayer1: (e: PlayerOnProgressProps) => void; onProgressPlayer1: (e: PlayerOnProgressProps) => void;
onProgressPlayer2: (e: PlayerOnProgressProps) => void; onProgressPlayer2: (e: PlayerOnProgressProps) => void;
onStartedPlayer1: (player: ReactPlayer) => void; onStartedPlayer1: (player: ReactPlayer) => void;
@@ -39,6 +40,9 @@ interface WebPlayerEngineProps {
volume: number; volume: number;
} }
const MAX_NETWORK_RETRIES = 5;
const NETWORK_RETRY_DELAY_MS = 2000;
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393 // Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
// This is used so that the player will always have an <audio> element. This means that // This is used so that the player will always have an <audio> element. This means that
// player1Source and player2Source are connected BEFORE the user presses play for // player1Source and player2Source are connected BEFORE the user presses play for
@@ -53,6 +57,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
isTransitioning, isTransitioning,
onEndedPlayer1, onEndedPlayer1,
onEndedPlayer2, onEndedPlayer2,
onErrorPause,
onProgressPlayer1, onProgressPlayer1,
onProgressPlayer2, onProgressPlayer2,
onStartedPlayer1, onStartedPlayer1,
@@ -69,6 +74,8 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const player1Ref = useRef<null | ReactPlayer>(null); const player1Ref = useRef<null | ReactPlayer>(null);
const player2Ref = useRef<null | ReactPlayer>(null); const player2Ref = useRef<null | ReactPlayer>(null);
const networkRetryCount1 = useRef(0);
const networkRetryCount2 = useRef(0);
const [ReactPlayerComponent, setReactPlayerComponent] = useState<any>(null); const [ReactPlayerComponent, setReactPlayerComponent] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@@ -150,7 +157,12 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const volume1 = convertToLogVolume(internalVolume1); const volume1 = convertToLogVolume(internalVolume1);
const volume2 = convertToLogVolume(internalVolume2); const volume2 = convertToLogVolume(internalVolume2);
const handleOnError = (playerRef: React.RefObject<null | ReactPlayer>, onEnded: () => void) => { const handleOnError = (
playerRef: React.RefObject<null | ReactPlayer>,
onEnded: () => void,
onErrorPause: () => void,
networkRetryCountRef: React.RefObject<number>,
) => {
return ({ target }: ErrorEvent) => { return ({ target }: ErrorEvent) => {
const { current: player } = playerRef; const { current: player } = playerRef;
@@ -165,17 +177,46 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
meta: { error }, meta: { error },
}); });
if ( const isNetworkError =
error?.code !== MediaError.MEDIA_ERR_DECODE && error?.code === MediaError.MEDIA_ERR_NETWORK ||
error?.code !== MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED error?.code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED;
) {
if (isNetworkError) {
if (networkRetryCountRef.current < MAX_NETWORK_RETRIES) {
networkRetryCountRef.current += 1;
const audio = target;
setTimeout(() => {
audio.load();
audio.play().catch(() => {
logFn.error(logMsg[LogCategory.PLAYER].playbackError, {
category: LogCategory.PLAYER,
meta: { error: 'Failed to play audio after network error' },
});
});
}, NETWORK_RETRY_DELAY_MS);
return;
}
}
if (error?.code !== MediaError.MEDIA_ERR_DECODE && !isNetworkError) {
return; return;
} }
onEnded(); if (error?.code === MediaError.MEDIA_ERR_DECODE) {
onEnded();
} else {
if (onErrorPause) {
onErrorPause();
}
}
}; };
}; };
useEffect(() => {
networkRetryCount1.current = 0;
networkRetryCount2.current = 0;
}, [src1, src2]);
useEffect(() => { useEffect(() => {
const player1 = player1Ref.current?.getInternalPlayer(); const player1 = player1Ref.current?.getInternalPlayer();
if (player1 && player1 instanceof HTMLAudioElement) { if (player1 && player1 instanceof HTMLAudioElement) {
@@ -224,7 +265,12 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
id="web-player-1" id="web-player-1"
muted={isMuted} muted={isMuted}
onEnded={src1 ? () => onEndedPlayer1() : undefined} onEnded={src1 ? () => onEndedPlayer1() : undefined}
onError={handleOnError(player1Ref, () => onEndedPlayer1())} onError={handleOnError(
player1Ref,
() => onEndedPlayer1(),
onErrorPause,
networkRetryCount1,
)}
onProgress={onProgressPlayer1} onProgress={onProgressPlayer1}
onReady={handleOnReadyPlayer1} onReady={handleOnReadyPlayer1}
playbackRate={speed || 1} playbackRate={speed || 1}
@@ -244,7 +290,12 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
id="web-player-2" id="web-player-2"
muted={isMuted} muted={isMuted}
onEnded={src2 ? () => onEndedPlayer2() : undefined} onEnded={src2 ? () => onEndedPlayer2() : undefined}
onError={handleOnError(player2Ref, () => onEndedPlayer2())} onError={handleOnError(
player2Ref,
() => onEndedPlayer2(),
onErrorPause,
networkRetryCount2,
)}
onProgress={onProgressPlayer2} onProgress={onProgressPlayer2}
onReady={handleOnReadyPlayer2} onReady={handleOnReadyPlayer2}
playbackRate={speed || 1} playbackRate={speed || 1}
@@ -2,6 +2,7 @@ import type { Dispatch } from 'react';
import type ReactPlayer from 'react-player'; import type ReactPlayer from 'react-player';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
WebPlayerEngine, WebPlayerEngine,
@@ -21,6 +22,7 @@ import {
usePlayerProperties, usePlayerProperties,
usePlayerVolume, usePlayerVolume,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast';
import { QueueSong } from '/@/shared/types/domain-types'; import { QueueSong } from '/@/shared/types/domain-types';
import { CrossfadeStyle, PlayerStatus, PlayerStyle } from '/@/shared/types/types'; import { CrossfadeStyle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
@@ -29,8 +31,9 @@ const PLAY_PAUSE_FADE_INTERVAL = 10;
export function WebPlayer() { export function WebPlayer() {
const playerRef = useRef<null | WebPlayerEngineHandle>(null); const playerRef = useRef<null | WebPlayerEngineHandle>(null);
const { t } = useTranslation();
const { num, player1, player2, status } = usePlayerData(); const { num, player1, player2, status } = usePlayerData();
const { mediaAutoNext, setTimestamp } = usePlayerActions(); const { mediaAutoNext, mediaPause, setTimestamp } = usePlayerActions();
const playback = useMpvSettings(); const playback = useMpvSettings();
const { webAudio } = useWebAudio(); const { webAudio } = useWebAudio();
@@ -443,12 +446,20 @@ export function WebPlayer() {
[player2Source, player2Url, webAudio], [player2Source, player2Url, webAudio],
); );
const handleOnErrorPause = useCallback(() => {
mediaPause();
toast.error({
message: t('error.playbackPausedDueToError', { postProcess: 'sentenceCase' }),
});
}, [mediaPause, t]);
return ( return (
<WebPlayerEngine <WebPlayerEngine
isMuted={isMuted} isMuted={isMuted}
isTransitioning={Boolean(isTransitioning)} isTransitioning={Boolean(isTransitioning)}
onEndedPlayer1={handleOnEndedPlayer1} onEndedPlayer1={handleOnEndedPlayer1}
onEndedPlayer2={handleOnEndedPlayer2} onEndedPlayer2={handleOnEndedPlayer2}
onErrorPause={handleOnErrorPause}
onProgressPlayer1={onProgressPlayer1} onProgressPlayer1={onProgressPlayer1}
onProgressPlayer2={onProgressPlayer2} onProgressPlayer2={onProgressPlayer2}
onStartedPlayer1={handlePlayer1Start} onStartedPlayer1={handlePlayer1Start}