mirror of
https://github.com/remvze/moodist.git
synced 2026-03-12 07:04:42 +08:00
feat: media session support
This commit is contained in:
@@ -12,6 +12,7 @@ import { Categories } from '@/components/categories';
|
||||
import { SharedModal } from '@/components/modals/shared';
|
||||
import { Toolbar } from '@/components/toolbar';
|
||||
import { SnackbarProvider } from '@/contexts/snackbar';
|
||||
import { MediaControls } from '@/components/media-controls';
|
||||
|
||||
import { sounds } from '@/data/sounds';
|
||||
import { FADE_OUT } from '@/constants/events';
|
||||
@@ -88,6 +89,7 @@ export function App() {
|
||||
return (
|
||||
<SnackbarProvider>
|
||||
<StoreConsumer>
|
||||
<MediaControls />
|
||||
<Container>
|
||||
<div id="app" />
|
||||
<Buttons />
|
||||
|
||||
1
src/components/media-controls/index.ts
Normal file
1
src/components/media-controls/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MediaControls } from './media-controls';
|
||||
13
src/components/media-controls/media-controls.tsx
Normal file
13
src/components/media-controls/media-controls.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMediaSessionStore } from '@/stores/media-session';
|
||||
|
||||
import { MediaSessionTrack } from './media-session-track';
|
||||
|
||||
export function MediaControls() {
|
||||
const mediaControlsEnabled = useMediaSessionStore(state => state.enabled);
|
||||
|
||||
if (!mediaControlsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MediaSessionTrack />;
|
||||
}
|
||||
104
src/components/media-controls/media-session-track.tsx
Normal file
104
src/components/media-controls/media-session-track.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { getSilenceDataURL } from '@/helpers/sound';
|
||||
import { BrowserDetect } from '@/helpers/browser-detect';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import { useSSR } from '@/hooks/use-ssr';
|
||||
import { useDarkTheme } from '@/hooks/use-dark-theme';
|
||||
|
||||
const metadata: MediaMetadataInit = {
|
||||
artist: 'Moodist',
|
||||
title: 'Ambient Sounds for Focus and Calm',
|
||||
};
|
||||
|
||||
export function MediaSessionTrack() {
|
||||
const { isBrowser } = useSSR();
|
||||
const isDarkTheme = useDarkTheme();
|
||||
const [isGenerated, setIsGenerated] = useState(false);
|
||||
const isPlaying = useSoundStore(state => state.isPlaying);
|
||||
const play = useSoundStore(state => state.play);
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
const masterAudioSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const artworkURL = isDarkTheme ? '/logo-dark.png' : '/logo-light.png';
|
||||
|
||||
const generateSilence = useCallback(async () => {
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
masterAudioSoundRef.current.src = await getSilenceDataURL();
|
||||
setIsGenerated(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser || !isPlaying || !isGenerated) return;
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
...metadata,
|
||||
artwork: [
|
||||
{
|
||||
sizes: '200x200',
|
||||
src: artworkURL,
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
});
|
||||
}, [artworkURL, isBrowser, isDarkTheme, isGenerated, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
generateSilence();
|
||||
}, [generateSilence]);
|
||||
|
||||
const startMasterAudio = useCallback(async () => {
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
if (!masterAudioSoundRef.current.paused) return;
|
||||
|
||||
try {
|
||||
await masterAudioSoundRef.current.play();
|
||||
|
||||
navigator.mediaSession.playbackState = 'playing';
|
||||
navigator.mediaSession.setActionHandler('play', play);
|
||||
navigator.mediaSession.setActionHandler('pause', pause);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}, [pause, play]);
|
||||
|
||||
const stopMasterAudio = useCallback(() => {
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
/**
|
||||
* Otherwise in Safari we cannot play the audio again
|
||||
* through the media session controls
|
||||
*/
|
||||
if (BrowserDetect.isSafari()) {
|
||||
masterAudioSoundRef.current.load();
|
||||
} else {
|
||||
masterAudioSoundRef.current.pause();
|
||||
}
|
||||
navigator.mediaSession.playbackState = 'paused';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGenerated) return;
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
|
||||
if (isPlaying) {
|
||||
startMasterAudio();
|
||||
} else {
|
||||
stopMasterAudio();
|
||||
}
|
||||
}, [isGenerated, isPlaying, startMasterAudio, stopMasterAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
const masterAudioSound = masterAudioSoundRef.current;
|
||||
|
||||
return () => {
|
||||
masterAudioSound?.pause();
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', null);
|
||||
navigator.mediaSession.setActionHandler('pause', null);
|
||||
navigator.mediaSession.playbackState = 'none';
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <audio id="media-session-track" loop ref={masterAudioSoundRef} />;
|
||||
}
|
||||
20
src/components/menu/items/media-controls.tsx
Normal file
20
src/components/menu/items/media-controls.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IoMdPlayCircle } from 'react-icons/io/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function MediaControls({
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Item
|
||||
active={active}
|
||||
icon={<IoMdPlayCircle />}
|
||||
label="Media Controls"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -29,10 +29,17 @@ import { useSoundStore } from '@/stores/sound';
|
||||
import styles from './menu.module.css';
|
||||
import { useCloseListener } from '@/hooks/use-close-listener';
|
||||
import { closeModals } from '@/lib/modal';
|
||||
import { MediaControls } from './items/media-controls';
|
||||
import { useMediaSessionStore } from '@/stores/media-session';
|
||||
|
||||
export function Menu() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const mediaControlsEnabled = useMediaSessionStore(state => state.enabled);
|
||||
const toggleMediaControls = useMediaSessionStore(state => state.toggle);
|
||||
const isMediaSessionSupported = useMediaSessionStore(
|
||||
state => state.isSupported,
|
||||
);
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
||||
const initial = useMemo(
|
||||
@@ -108,6 +115,12 @@ export function Menu() {
|
||||
>
|
||||
<PresetsItem open={() => open('presets')} />
|
||||
<ShareItem open={() => open('shareLink')} />
|
||||
{isMediaSessionSupported ? (
|
||||
<MediaControls
|
||||
active={mediaControlsEnabled}
|
||||
onClick={toggleMediaControls}
|
||||
/>
|
||||
) : null}
|
||||
<ShuffleItem />
|
||||
<SleepTimerItem open={() => open('sleepTimer')} />
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { useNoteStore } from '@/stores/note';
|
||||
import { usePresetStore } from '@/stores/preset';
|
||||
import { useMediaSessionStore } from '@/stores/media-session';
|
||||
|
||||
interface StoreConsumerProps {
|
||||
children: React.ReactNode;
|
||||
@@ -13,6 +14,7 @@ export function StoreConsumer({ children }: StoreConsumerProps) {
|
||||
useSoundStore.persist.rehydrate();
|
||||
useNoteStore.persist.rehydrate();
|
||||
usePresetStore.persist.rehydrate();
|
||||
useMediaSessionStore.persist.rehydrate();
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
Reference in New Issue
Block a user