mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +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 { useQueueRestoreTimestamp } from '/@/renderer/features/player/hooks/use-queue-restore';
|
||||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||||
|
import { RadioWebPlayer } from '/@/renderer/features/radio/components/radio-web-player';
|
||||||
import {
|
import {
|
||||||
useIsRadioActive,
|
useIsRadioActive,
|
||||||
useRadioAudioInstance,
|
useRadioAudioInstance,
|
||||||
@@ -147,7 +148,7 @@ export const AudioPlayers = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isRadioActive && playbackType === PlayerType.WEB) {
|
if (isRadioActive && playbackType === PlayerType.WEB) {
|
||||||
return null;
|
return <RadioWebPlayer />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 IcecastMetadataStats from 'icecast-metadata-stats';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
|
|
||||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
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, usePlayerStoreBase, useSettingsStore } from '/@/renderer/store';
|
||||||
import {
|
|
||||||
usePlaybackType,
|
|
||||||
usePlayerMuted,
|
|
||||||
usePlayerStoreBase,
|
|
||||||
usePlayerVolume,
|
|
||||||
useSettingsStore,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
|
||||||
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
export interface RadioMetadata {
|
export interface RadioMetadata {
|
||||||
@@ -132,10 +123,6 @@ export const useRadioAudioInstance = () => {
|
|||||||
const currentStreamUrl = useRadioStore((state) => state.currentStreamUrl);
|
const currentStreamUrl = useRadioStore((state) => state.currentStreamUrl);
|
||||||
const isPlaying = useRadioStore((state) => state.isPlaying);
|
const isPlaying = useRadioStore((state) => state.isPlaying);
|
||||||
const playbackType = usePlaybackType();
|
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;
|
const isUsingMpv = playbackType === PlayerType.LOCAL && mpvPlayer;
|
||||||
|
|
||||||
// Handle mpv playback
|
// Handle mpv playback
|
||||||
@@ -188,102 +175,6 @@ export const useRadioAudioInstance = () => {
|
|||||||
};
|
};
|
||||||
}, [isUsingMpv, setIsPlaying, setCurrentStreamUrl, setStationName]);
|
}, [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(
|
usePlayerEvents(
|
||||||
{
|
{
|
||||||
onPlayerStatus: (properties, prev) => {
|
onPlayerStatus: (properties, prev) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user