From 18ed2e6f055d7e32b4a9df33cdb724eaf1f930aa Mon Sep 17 00:00:00 2001 From: Aleksandr Shoronov Date: Sat, 18 Jan 2025 13:53:46 +0200 Subject: [PATCH] feat: media session support --- README.md | 1 + public/logo-dark.png | Bin 0 -> 10797 bytes public/logo-light.png | Bin 0 -> 10845 bytes src/components/app/app.tsx | 2 + src/components/media-controls/index.ts | 1 + .../media-controls/media-controls.tsx | 13 +++ .../media-controls/media-session-track.tsx | 104 ++++++++++++++++++ src/components/menu/items/media-controls.tsx | 20 ++++ src/components/menu/menu.tsx | 13 +++ .../store-consumer/store-consumer.tsx | 2 + src/helpers/browser-detect.ts | 16 +++ src/helpers/sound.ts | 82 ++++++++++++++ src/hooks/use-dark-theme.ts | 27 +++++ src/stores/media-session/index.ts | 33 ++++++ .../media-session/media-session.actions.ts | 29 +++++ .../media-session/media-session.state.ts | 18 +++ 16 files changed, 361 insertions(+) create mode 100644 public/logo-dark.png create mode 100644 public/logo-light.png create mode 100644 src/components/media-controls/index.ts create mode 100644 src/components/media-controls/media-controls.tsx create mode 100644 src/components/media-controls/media-session-track.tsx create mode 100644 src/components/menu/items/media-controls.tsx create mode 100644 src/helpers/browser-detect.ts create mode 100644 src/helpers/sound.ts create mode 100644 src/hooks/use-dark-theme.ts create mode 100644 src/stores/media-session/index.ts create mode 100644 src/stores/media-session/media-session.actions.ts create mode 100644 src/stores/media-session/media-session.state.ts diff --git a/README.md b/README.md index c837afbf..bb1aa5d1 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ 1. 📓 Notepad for quick notes. 1. 🍅 Pomodoro timer. 1. ✅ Simple to-do list (soon). +1. ⏯️ Media controls. 1. ⌨️ Keyboard shortcuts for everything. 1. 🥷 Privacy focused: no data collection. 1. 💰 Completely free, open-source, and self-hostable. diff --git a/public/logo-dark.png b/public/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5e10b4d5dc936626e2c1df8fc6913ae1cc1a6269 GIT binary patch literal 10797 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 F{{hL@Vut_# literal 0 HcmV?d00001 diff --git a/public/logo-light.png b/public/logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5c902f7cc160140e17ad9f5649e41cbc05146db5 GIT binary patch literal 10845 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 b^N+betxWlEbZ-;bcnGK{XrgN5EW-W|n7?AB literal 0 HcmV?d00001 diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index e0eee08d..c5f890d4 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -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 ( +
diff --git a/src/components/media-controls/index.ts b/src/components/media-controls/index.ts new file mode 100644 index 00000000..fc42fd2f --- /dev/null +++ b/src/components/media-controls/index.ts @@ -0,0 +1 @@ +export { MediaControls } from './media-controls'; diff --git a/src/components/media-controls/media-controls.tsx b/src/components/media-controls/media-controls.tsx new file mode 100644 index 00000000..60cf4779 --- /dev/null +++ b/src/components/media-controls/media-controls.tsx @@ -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 ; +} diff --git a/src/components/media-controls/media-session-track.tsx b/src/components/media-controls/media-session-track.tsx new file mode 100644 index 00000000..6a29ab1b --- /dev/null +++ b/src/components/media-controls/media-session-track.tsx @@ -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(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