diff --git a/src/renderer/features/player/components/audio-players.tsx b/src/renderer/features/player/components/audio-players.tsx
index 54991d81f..3b0f4eb41 100644
--- a/src/renderer/features/player/components/audio-players.tsx
+++ b/src/renderer/features/player/components/audio-players.tsx
@@ -15,6 +15,7 @@ import { usePowerSaveBlocker } from '/@/renderer/features/player/hooks/use-power
import { useQueueRestoreTimestamp } from '/@/renderer/features/player/hooks/use-queue-restore';
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
+import { RadioWebPlayer } from '/@/renderer/features/radio/components/radio-web-player';
import {
useIsRadioActive,
useRadioAudioInstance,
@@ -147,7 +148,7 @@ export const AudioPlayers = () => {
}
if (isRadioActive && playbackType === PlayerType.WEB) {
- return null;
+ return ;
}
return (
diff --git a/src/renderer/features/radio/components/radio-web-player.tsx b/src/renderer/features/radio/components/radio-web-player.tsx
new file mode 100644
index 000000000..1332532ce
--- /dev/null
+++ b/src/renderer/features/radio/components/radio-web-player.tsx
@@ -0,0 +1,155 @@
+import type ReactPlayer from 'react-player';
+
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import {
+ WebPlayerEngine,
+ WebPlayerEngineHandle,
+} from '/@/renderer/features/player/audio-player/engine/web-player-engine';
+import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
+import {
+ useIsRadioActive,
+ useRadioPlayer,
+ useRadioStore,
+} from '/@/renderer/features/radio/hooks/use-radio-player';
+import { usePlaybackSettings, usePlayerMuted, usePlayerVolume } from '/@/renderer/store';
+import { toast } from '/@/shared/components/toast/toast';
+import { PlayerStatus } from '/@/shared/types/types';
+
+export function RadioWebPlayer() {
+ const playerRef = useRef(null);
+ const { currentStreamUrl, isPlaying } = useRadioPlayer();
+ const { actions } = useRadioStore();
+ const { setCurrentStreamUrl, setIsPlaying, setStationName } = actions;
+ const isRadioActive = useIsRadioActive();
+ const isMuted = usePlayerMuted();
+ const volume = usePlayerVolume();
+ const { preservePitch } = usePlaybackSettings();
+ const { webAudio } = useWebAudio();
+
+ const [playerStatus, setPlayerStatus] = useState(
+ isPlaying ? PlayerStatus.PLAYING : PlayerStatus.PAUSED,
+ );
+ const [player1Source, setPlayer1Source] = useState(null);
+ const processedMediaElementRef = useRef(null);
+ const player1SourceRef = useRef(null);
+
+ useEffect(() => {
+ player1SourceRef.current = player1Source;
+ }, [player1Source]);
+
+ useEffect(() => {
+ setPlayerStatus(isPlaying ? PlayerStatus.PLAYING : PlayerStatus.PAUSED);
+ }, [isPlaying]);
+
+ // Cleanup source only on unmount
+ useEffect(() => {
+ return () => {
+ if (player1SourceRef.current) {
+ try {
+ player1SourceRef.current.disconnect();
+ } catch {
+ // Ignore disconnect errors
+ }
+ setPlayer1Source(null);
+ processedMediaElementRef.current = null;
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!webAudio || !player1Source) return;
+
+ const linearVolume = volume / 100;
+ const gainValue = isMuted ? 0 : linearVolume;
+
+ try {
+ webAudio.gains[0].gain.setValueAtTime(gainValue, 0);
+ } catch (error) {
+ console.error('Error setting radio volume gain', error);
+ }
+ }, [volume, isMuted, webAudio, player1Source]);
+
+ const handlePlayer1Start = useCallback(
+ async (player: ReactPlayer) => {
+ if (!webAudio) return;
+
+ const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
+ if (!internal) return;
+
+ // If we've already processed this exact media element, reuse the existing source
+ if (processedMediaElementRef.current === internal && player1Source) {
+ // Ensure it's still connected to the gain node
+ try {
+ if (!player1Source.context) {
+ const { gains } = webAudio;
+ player1Source.connect(gains[0]);
+ }
+ } catch {
+ // Already connected, which is what we want
+ }
+ return;
+ }
+
+ if (currentStreamUrl) {
+ if (webAudio.context.state !== 'running') {
+ await webAudio.context.resume();
+ }
+ }
+
+ try {
+ const { context, gains } = webAudio;
+ const source = context.createMediaElementSource(internal);
+ source.connect(gains[0]);
+ setPlayer1Source(source);
+ processedMediaElementRef.current = internal;
+ } catch {
+ processedMediaElementRef.current = internal;
+
+ if (webAudio && webAudio.gains[0]) {
+ const linearVolume = volume / 100;
+ const gainValue = isMuted ? 0 : linearVolume;
+ webAudio.gains[0].gain.setValueAtTime(gainValue, 0);
+ }
+ }
+ },
+ [player1Source, currentStreamUrl, webAudio, volume, isMuted],
+ );
+
+ const onProgressPlayer1 = useCallback(() => {
+ // We don't need to handle progress for radio streams
+ }, []);
+
+ const onEndedPlayer1 = useCallback(() => {
+ console.error('Radio stream ended unexpectedly');
+ setIsPlaying(false);
+ setCurrentStreamUrl(null);
+ setStationName(null);
+ toast.error({ message: 'Radio stream ended unexpectedly' });
+ }, [setIsPlaying, setCurrentStreamUrl, setStationName]);
+
+ if (!isRadioActive) {
+ return null;
+ }
+
+ return (
+ {}}
+ onProgressPlayer1={onProgressPlayer1}
+ onProgressPlayer2={() => {}}
+ onStartedPlayer1={handlePlayer1Start}
+ onStartedPlayer2={() => {}}
+ playerNum={1}
+ playerRef={playerRef}
+ playerStatus={playerStatus}
+ preservesPitch={preservePitch}
+ speed={1}
+ src1={currentStreamUrl || undefined}
+ src2={undefined}
+ volume={volume}
+ />
+ );
+}
diff --git a/src/renderer/features/radio/hooks/use-radio-player.ts b/src/renderer/features/radio/hooks/use-radio-player.ts
index 8a7156de8..3c2779b34 100644
--- a/src/renderer/features/radio/hooks/use-radio-player.ts
+++ b/src/renderer/features/radio/hooks/use-radio-player.ts
@@ -1,19 +1,10 @@
-import console from 'console';
import IcecastMetadataStats from 'icecast-metadata-stats';
import isElectron from 'is-electron';
-import { useEffect, useRef } from 'react';
+import { useEffect } from 'react';
import { createWithEqualityFn } from 'zustand/traditional';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
-import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
-import {
- usePlaybackType,
- usePlayerMuted,
- usePlayerStoreBase,
- usePlayerVolume,
- useSettingsStore,
-} from '/@/renderer/store';
-import { toast } from '/@/shared/components/toast/toast';
+import { usePlaybackType, usePlayerStoreBase, useSettingsStore } from '/@/renderer/store';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
export interface RadioMetadata {
@@ -132,10 +123,6 @@ export const useRadioAudioInstance = () => {
const currentStreamUrl = useRadioStore((state) => state.currentStreamUrl);
const isPlaying = useRadioStore((state) => state.isPlaying);
const playbackType = usePlaybackType();
- const volume = usePlayerVolume();
- const isMuted = usePlayerMuted();
- const audioRef = useRef(null);
- const activeAudioRef = useRef(null);
const isUsingMpv = playbackType === PlayerType.LOCAL && mpvPlayer;
// Handle mpv playback
@@ -188,102 +175,6 @@ export const useRadioAudioInstance = () => {
};
}, [isUsingMpv, setIsPlaying, setCurrentStreamUrl, setStationName]);
- // Handle web playback
- useEffect(() => {
- if (isUsingMpv) {
- if (audioRef.current) {
- audioRef.current.pause();
- audioRef.current.src = '';
- audioRef.current = null;
- }
- return;
- }
-
- if (currentStreamUrl && isPlaying) {
- if (audioRef.current) {
- audioRef.current.pause();
- audioRef.current.src = '';
- }
-
- const audio = new Audio(currentStreamUrl);
- audioRef.current = audio;
- activeAudioRef.current = audio;
-
- const linearVolume = volume / 100;
- const logVolume = convertToLogVolume(linearVolume);
- audio.volume = logVolume;
- audio.muted = isMuted;
-
- audio.addEventListener('play', () => {
- setIsPlaying(true);
- });
-
- audio.addEventListener('pause', () => {
- setIsPlaying(false);
- });
-
- audio.addEventListener('ended', () => {
- setIsPlaying(false);
- setCurrentStreamUrl(null);
- setStationName(null);
- });
-
- audio.addEventListener('error', (error) => {
- console.error('Radio stream error:', error);
- });
-
- // Attempt to play
- audio.play().catch((error) => {
- if (activeAudioRef.current !== audio) {
- return;
- }
-
- console.error('Failed to play audio:', error);
- setIsPlaying(false);
- setCurrentStreamUrl(null);
- setStationName(null);
- toast.error({ message: 'Failed to play radio stream' });
- });
- } else if (!currentStreamUrl || !isPlaying) {
- if (audioRef.current) {
- audioRef.current.pause();
- audioRef.current.src = '';
- audioRef.current = null;
- }
- activeAudioRef.current = null;
- }
-
- return () => {
- if (audioRef.current) {
- if (activeAudioRef.current === audioRef.current) {
- activeAudioRef.current = null;
- }
- audioRef.current.pause();
- audioRef.current.src = '';
- audioRef.current = null;
- }
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- currentStreamUrl,
- isPlaying,
- isUsingMpv,
- setIsPlaying,
- setCurrentStreamUrl,
- setStationName,
- ]);
-
- useEffect(() => {
- if (isUsingMpv || !audioRef.current) {
- return;
- }
-
- const linearVolume = volume / 100;
- const logVolume = convertToLogVolume(linearVolume);
- audioRef.current.volume = logVolume;
- audioRef.current.muted = isMuted;
- }, [volume, isMuted, isUsingMpv]);
-
usePlayerEvents(
{
onPlayerStatus: (properties, prev) => {