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",
"openError": "could not open file",
"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",
"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",
@@ -25,6 +25,7 @@ interface WebPlayerEngineProps {
isTransitioning: boolean;
onEndedPlayer1: () => void;
onEndedPlayer2: () => void;
onErrorPause: () => void;
onProgressPlayer1: (e: PlayerOnProgressProps) => void;
onProgressPlayer2: (e: PlayerOnProgressProps) => void;
onStartedPlayer1: (player: ReactPlayer) => void;
@@ -39,6 +40,9 @@ interface WebPlayerEngineProps {
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
// 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
@@ -53,6 +57,7 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
isTransitioning,
onEndedPlayer1,
onEndedPlayer2,
onErrorPause,
onProgressPlayer1,
onProgressPlayer2,
onStartedPlayer1,
@@ -69,6 +74,8 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const player1Ref = 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 [isLoading, setIsLoading] = useState(true);
@@ -150,7 +157,12 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const volume1 = convertToLogVolume(internalVolume1);
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) => {
const { current: player } = playerRef;
@@ -165,17 +177,46 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
meta: { error },
});
if (
error?.code !== MediaError.MEDIA_ERR_DECODE &&
error?.code !== MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
) {
const isNetworkError =
error?.code === MediaError.MEDIA_ERR_NETWORK ||
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;
}
onEnded();
if (error?.code === MediaError.MEDIA_ERR_DECODE) {
onEnded();
} else {
if (onErrorPause) {
onErrorPause();
}
}
};
};
useEffect(() => {
networkRetryCount1.current = 0;
networkRetryCount2.current = 0;
}, [src1, src2]);
useEffect(() => {
const player1 = player1Ref.current?.getInternalPlayer();
if (player1 && player1 instanceof HTMLAudioElement) {
@@ -224,7 +265,12 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
id="web-player-1"
muted={isMuted}
onEnded={src1 ? () => onEndedPlayer1() : undefined}
onError={handleOnError(player1Ref, () => onEndedPlayer1())}
onError={handleOnError(
player1Ref,
() => onEndedPlayer1(),
onErrorPause,
networkRetryCount1,
)}
onProgress={onProgressPlayer1}
onReady={handleOnReadyPlayer1}
playbackRate={speed || 1}
@@ -244,7 +290,12 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
id="web-player-2"
muted={isMuted}
onEnded={src2 ? () => onEndedPlayer2() : undefined}
onError={handleOnError(player2Ref, () => onEndedPlayer2())}
onError={handleOnError(
player2Ref,
() => onEndedPlayer2(),
onErrorPause,
networkRetryCount2,
)}
onProgress={onProgressPlayer2}
onReady={handleOnReadyPlayer2}
playbackRate={speed || 1}
@@ -2,6 +2,7 @@ import type { Dispatch } from 'react';
import type ReactPlayer from 'react-player';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
WebPlayerEngine,
@@ -21,6 +22,7 @@ import {
usePlayerProperties,
usePlayerVolume,
} from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast';
import { QueueSong } from '/@/shared/types/domain-types';
import { CrossfadeStyle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
@@ -29,8 +31,9 @@ const PLAY_PAUSE_FADE_INTERVAL = 10;
export function WebPlayer() {
const playerRef = useRef<null | WebPlayerEngineHandle>(null);
const { t } = useTranslation();
const { num, player1, player2, status } = usePlayerData();
const { mediaAutoNext, setTimestamp } = usePlayerActions();
const { mediaAutoNext, mediaPause, setTimestamp } = usePlayerActions();
const playback = useMpvSettings();
const { webAudio } = useWebAudio();
@@ -443,12 +446,20 @@ export function WebPlayer() {
[player2Source, player2Url, webAudio],
);
const handleOnErrorPause = useCallback(() => {
mediaPause();
toast.error({
message: t('error.playbackPausedDueToError', { postProcess: 'sentenceCase' }),
});
}, [mediaPause, t]);
return (
<WebPlayerEngine
isMuted={isMuted}
isTransitioning={Boolean(isTransitioning)}
onEndedPlayer1={handleOnEndedPlayer1}
onEndedPlayer2={handleOnEndedPlayer2}
onErrorPause={handleOnErrorPause}
onProgressPlayer1={onProgressPlayer1}
onProgressPlayer2={onProgressPlayer2}
onStartedPlayer1={handlePlayer1Start}