Files
feishin/src/renderer/features/player/hooks/use-media-session.ts
T

247 lines
7.8 KiB
TypeScript

import isElectron from 'is-electron';
import React, { useCallback, useEffect, useMemo } from 'react';
import { getItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import {
useIsRadioActive,
useRadioPlayer,
} from '/@/renderer/features/radio/hooks/use-radio-player';
import {
usePlaybackSettings,
usePlaybackType,
usePlayerStore,
useSettingsStore,
useSkipButtons,
useTimestampStoreBase,
} from '/@/renderer/store';
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
const mediaSession = navigator.mediaSession;
export const useMediaSession = () => {
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
const player = usePlayer();
const skip = useSkipButtons();
const playbackType = useSettingsStore((state) => state.playback.type);
const isRadioActive = useIsRadioActive();
const { isPlaying: isRadioPlaying, metadata: radioMetadata, stationName } = useRadioPlayer();
const isMediaSessionEnabled = useMemo(() => {
// Always enable media session on web
if (!isElectron()) {
return true;
}
return Boolean(mediaSessionEnabled && playbackType === PlayerType.WEB);
}, [mediaSessionEnabled, playbackType]);
useEffect(() => {
if (!isMediaSessionEnabled) {
return;
}
mediaSession.setActionHandler('nexttrack', () => {
if (isRadioActive && isRadioPlaying) {
return;
}
player.mediaNext();
});
mediaSession.setActionHandler('pause', () => {
player.mediaPause();
});
mediaSession.setActionHandler('play', () => {
player.mediaPlay();
});
mediaSession.setActionHandler('previoustrack', () => {
if (isRadioActive && isRadioPlaying) {
return;
}
player.mediaPrevious();
});
mediaSession.setActionHandler('seekto', (e) => {
if (isRadioActive && isRadioPlaying) {
return;
}
if (e.seekTime) {
player.mediaSeekToTimestamp(e.seekTime);
} else if (e.seekOffset) {
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
player.mediaSeekToTimestamp(currentTimestamp + e.seekOffset);
}
});
mediaSession.setActionHandler('stop', () => {
player.mediaStop();
});
mediaSession.setActionHandler('seekbackward', (e) => {
if (isRadioActive && isRadioPlaying) {
return;
}
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
player.mediaSeekToTimestamp(
currentTimestamp - (e.seekOffset || skip?.skipBackwardSeconds || 5),
);
});
mediaSession.setActionHandler('seekforward', (e) => {
if (isRadioActive && isRadioPlaying) {
return;
}
const currentTimestamp = useTimestampStoreBase.getState().timestamp;
player.mediaSeekToTimestamp(
currentTimestamp + (e.seekOffset || skip?.skipForwardSeconds || 5),
);
});
return () => {
mediaSession.setActionHandler('nexttrack', null);
mediaSession.setActionHandler('pause', null);
mediaSession.setActionHandler('play', null);
mediaSession.setActionHandler('previoustrack', null);
mediaSession.setActionHandler('seekto', null);
mediaSession.setActionHandler('stop', null);
mediaSession.setActionHandler('seekbackward', null);
mediaSession.setActionHandler('seekforward', null);
};
}, [
player,
skip?.skipBackwardSeconds,
skip?.skipForwardSeconds,
isMediaSessionEnabled,
isRadioActive,
isRadioPlaying,
]);
const updateMediaSessionMetadata = useCallback(
(song: QueueSong | undefined) => {
if (!isMediaSessionEnabled) {
return;
}
// Handle radio metadata when radio is active and playing
if (isRadioActive && isRadioPlaying) {
const title = radioMetadata?.title || stationName || 'Radio';
const artist = radioMetadata?.artist || stationName || '';
mediaSession.metadata = new MediaMetadata({
album: stationName || '',
artist: artist,
artwork: [],
title: title,
});
return;
}
// Handle regular song metadata
if (!song) {
return;
}
const imageUrl = getItemImageUrl({
id: song?.imageId || undefined,
imageUrl: song?.imageUrl,
itemType: LibraryItem.SONG,
type: 'itemCard',
});
mediaSession.metadata = new MediaMetadata({
album: song?.album ?? '',
artist: song?.artistName ?? '',
artwork: imageUrl ? [{ src: imageUrl, type: 'image/png' }] : [],
title: song?.name ?? '',
});
},
[isMediaSessionEnabled, isRadioActive, isRadioPlaying, radioMetadata, stationName],
);
// Update metadata when radio metadata changes
useEffect(() => {
if (!isMediaSessionEnabled) {
return;
}
if (isRadioActive && isRadioPlaying) {
updateMediaSessionMetadata(undefined);
}
}, [
isMediaSessionEnabled,
isRadioActive,
isRadioPlaying,
radioMetadata,
stationName,
updateMediaSessionMetadata,
]);
usePlayerEvents(
{
onCurrentSongChange: (properties) => {
if (!isMediaSessionEnabled) {
return;
}
if (isRadioActive && isRadioPlaying) {
return;
}
updateMediaSessionMetadata(properties.song);
},
onPlayerRepeated: () => {
if (!isMediaSessionEnabled) {
return;
}
if (isRadioActive && isRadioPlaying) {
return;
}
const currentSong = usePlayerStore.getState().getCurrentSong();
updateMediaSessionMetadata(currentSong);
},
onPlayerStatus: (properties) => {
if (!isMediaSessionEnabled) {
return;
}
const status = properties.status;
mediaSession.playbackState = status === PlayerStatus.PLAYING ? 'playing' : 'paused';
},
},
[isMediaSessionEnabled, isRadioActive, isRadioPlaying, updateMediaSessionMetadata],
);
};
const MediaSessionHookInner = () => {
useMediaSession();
return null;
};
export const MediaSessionHook = () => {
const isElectronEnv = isElectron();
const playbackType = usePlaybackType();
const isMediaSessionEnabled = useSettingsStore((state) => state.playback.mediaSession);
// We always want to enable media session when on web
// Otherwise, only enable if it is explicitly enabled in the settings AND using the web player
const shouldUseMediaSession =
!isElectronEnv || (isMediaSessionEnabled && playbackType === PlayerType.WEB);
if (!shouldUseMediaSession) {
return null;
}
return React.createElement(MediaSessionHookInner);
};