import isElectron from 'is-electron'; import { useEffect } from 'react'; import { eventEmitter } from '/@/renderer/events/event-emitter'; import { UserFavoriteEventPayload, UserRatingEventPayload } from '/@/renderer/events/events'; import { DiscordRpcHook } from '/@/renderer/features/discord-rpc/use-discord-rpc'; import { MainPlayerListenerHook } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener'; import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player'; import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player'; import { SleepTimerHook } from '/@/renderer/features/player/components/sleep-timer-button'; import { AutoDJHook } from '/@/renderer/features/player/hooks/use-auto-dj'; import { AutosaveHook } from '/@/renderer/features/player/hooks/use-autosave'; import { MediaSessionHook } from '/@/renderer/features/player/hooks/use-media-session'; import { MPRISHook } from '/@/renderer/features/player/hooks/use-mpris'; import { PlaybackHotkeysHook } from '/@/renderer/features/player/hooks/use-playback-hotkeys'; import { PowerSaveBlockerHook } from '/@/renderer/features/player/hooks/use-power-save-blocker'; import { QueueRestoreTimestampHook } from '/@/renderer/features/player/hooks/use-queue-restore'; import { ScrobbleHook } from '/@/renderer/features/player/hooks/use-scrobble'; import { UpdateCurrentSongHook } from '/@/renderer/features/player/hooks/use-update-current-song'; import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio'; import { RadioWebPlayer } from '/@/renderer/features/radio/components/radio-web-player'; import { RadioAudioInstanceHook, RadioMetadataHook, useIsRadioActive, } from '/@/renderer/features/radio/hooks/use-radio-player'; import { RemoteHook } from '/@/renderer/features/remote/hooks/use-remote'; import { VisualizerSystemAudioBridgeHook } from '/@/renderer/features/visualizer/components/visualizer-system-audio-bridge'; import { updateQueueFavorites, updateQueueRatings, useCurrentServerId, usePlaybackSettings, usePlaybackType, useSettingsStoreActions, } from '/@/renderer/store'; import { logFn } from '/@/renderer/utils/logger'; import { toast } from '/@/shared/components/toast/toast'; import { LibraryItem } from '/@/shared/types/domain-types'; import { PlayerType } from '/@/shared/types/types'; const CODEC_PROBES = [ { codec: 'mp3', container: 'mp3', mime: 'audio/mpeg' }, { codec: 'aac', container: 'mp4', mime: 'audio/mp4; codecs="mp4a.40.2"' }, { codec: 'aac', container: 'aac', mime: 'audio/aac' }, { codec: 'aac', container: 'mp4', mime: 'audio/x-m4a' }, { codec: 'opus', container: 'ogg', mime: 'audio/ogg; codecs="opus"' }, { codec: 'opus', container: 'webm', mime: 'audio/webm; codecs="opus"' }, { codec: 'vorbis', container: 'ogg', mime: 'audio/ogg; codecs="vorbis"' }, { codec: 'vorbis', container: 'webm', mime: 'audio/webm; codecs="vorbis"' }, { codec: 'flac', container: 'flac', mime: 'audio/flac' }, { codec: ['pcm', 'wav'], container: 'wav', mime: 'audio/wav' }, { codec: 'alac', container: 'mp4', mime: 'audio/mp4; codecs="alac"' }, ]; const DEFAULT_TRANSCODING_PROFILES = [ { audioCodec: 'flac', container: 'flac', protocol: 'http' }, { audioCodec: 'opus', container: 'ogg', protocol: 'http' }, { audioCodec: 'mp3', container: 'mp3', protocol: 'http' }, ]; const SAFARI_TRANSCODING_PROFILES = [{ audioCodec: 'mp3', container: 'mp3', protocol: 'http' }]; const DIRECT_PLAY_PROFILES: { audioCodecs: string[]; containers: string[]; protocols: string[]; }[] = []; export function getDefaultTranscodingProfiles() { return isSafari() ? SAFARI_TRANSCODING_PROFILES : DEFAULT_TRANSCODING_PROFILES; } export function getDirectPlayProfiles() { return DIRECT_PLAY_PROFILES; } // Shamelessly taken from NavidromeUI function detectBrowserProfile() { const audio = new Audio(); for (const { codec, container, mime } of CODEC_PROBES) { if (audio.canPlayType(mime) === 'maybe' || audio.canPlayType(mime) === 'probably') { DIRECT_PLAY_PROFILES.push({ audioCodecs: Array.isArray(codec) ? codec : [codec], containers: [container], protocols: ['http'], }); } } logFn.info('DIRECT_PLAY_PROFILES', { meta: DIRECT_PLAY_PROFILES }); return DIRECT_PLAY_PROFILES; } function isSafari() { const ua = navigator.userAgent; return ua.includes('Safari') && !ua.includes('Chrome') && !ua.includes('Chromium'); } export const AudioPlayers = () => { const playbackType = usePlaybackType(); const serverId = useCurrentServerId(); const { resetSampleRate } = useSettingsStoreActions(); const { audioDeviceId, mpvProperties: { audioSampleRateHz }, webAudio, } = usePlaybackSettings(); const { setWebAudio, webAudio: audioContext } = useWebAudio(); useEffect(() => { detectBrowserProfile(); }, []); return ( <> ); }; const AudioPlayersContent = ({ audioContext, audioDeviceId, audioSampleRateHz, playbackType, resetSampleRate, serverId, setWebAudio, webAudio, }: { audioContext: ReturnType['webAudio']; audioDeviceId: null | string | undefined; audioSampleRateHz: number | undefined; playbackType: PlayerType; resetSampleRate: ReturnType['resetSampleRate']; serverId: null | string; setWebAudio: ReturnType['setWebAudio']; webAudio: boolean; }) => { const isRadioActive = useIsRadioActive(); useEffect(() => { if (webAudio && 'AudioContext' in window) { let context: AudioContext; try { context = new AudioContext({ latencyHint: 'playback', sampleRate: audioSampleRateHz || undefined, }); } catch (error) { // In practice, this should never be hit because the UI should validate // the range. However, the actual supported range is not guaranteed toast.error({ message: (error as Error).message }); context = new AudioContext({ latencyHint: 'playback' }); resetSampleRate(); } const gains = [context.createGain(), context.createGain()]; for (const gain of gains) { gain.connect(context.destination); } setWebAudio!({ context, gains }); } // Intentionally ignore the sample rate dependency, as it makes things really messy // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { // Not standard, just used in chromium-based browsers. See // https://developer.chrome.com/blog/audiocontext-setsinkid/. if (!isElectron()) { return; } if (playbackType !== PlayerType.WEB) { return; } if (audioContext && 'setSinkId' in audioContext.context && audioDeviceId) { const setSink = async () => { try { if (audioContext.context.state !== 'closed') { await (audioContext.context as any).setSinkId(audioDeviceId); } } catch (error) { toast.error({ message: `Error setting sink: ${(error as Error).message}` }); } }; setSink(); } }, [audioContext, audioDeviceId, playbackType]); // Listen to favorite and rating events to update queue songs useEffect(() => { const handleFavorite = (payload: UserFavoriteEventPayload) => { if (payload.itemType !== LibraryItem.SONG || payload.serverId !== serverId) { return; } updateQueueFavorites(payload.id, payload.favorite); }; const handleRating = (payload: UserRatingEventPayload) => { if (payload.itemType !== LibraryItem.SONG || payload.serverId !== serverId) { return; } updateQueueRatings(payload.id, payload.rating); }; eventEmitter.on('USER_FAVORITE', handleFavorite); eventEmitter.on('USER_RATING', handleRating); return () => { eventEmitter.off('USER_FAVORITE', handleFavorite); eventEmitter.off('USER_RATING', handleRating); }; }, [serverId]); if (isRadioActive && playbackType === PlayerType.LOCAL) { return ; } if (isRadioActive && playbackType === PlayerType.WEB) { return ; } return ( <> {playbackType === PlayerType.WEB && } {playbackType === PlayerType.LOCAL && } ); };