mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
use existing web player engine for radio playback (#1543)
This commit is contained in:
@@ -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 <RadioWebPlayer />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 | WebPlayerEngineHandle>(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<PlayerStatus>(
|
||||
isPlaying ? PlayerStatus.PLAYING : PlayerStatus.PAUSED,
|
||||
);
|
||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||
const processedMediaElementRef = useRef<HTMLMediaElement | null>(null);
|
||||
const player1SourceRef = useRef<MediaElementAudioSourceNode | null>(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 (
|
||||
<WebPlayerEngine
|
||||
isMuted={isMuted}
|
||||
isTransitioning={false}
|
||||
onEndedPlayer1={onEndedPlayer1}
|
||||
onEndedPlayer2={() => {}}
|
||||
onProgressPlayer1={onProgressPlayer1}
|
||||
onProgressPlayer2={() => {}}
|
||||
onStartedPlayer1={handlePlayer1Start}
|
||||
onStartedPlayer2={() => {}}
|
||||
playerNum={1}
|
||||
playerRef={playerRef}
|
||||
playerStatus={playerStatus}
|
||||
preservesPitch={preservePitch}
|
||||
speed={1}
|
||||
src1={currentStreamUrl || undefined}
|
||||
src2={undefined}
|
||||
volume={volume}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLAudioElement | null>(null);
|
||||
const activeAudioRef = useRef<HTMLAudioElement | null>(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) => {
|
||||
|
||||
Reference in New Issue
Block a user