From a64b30d0471aebe55efbf9dee30badbb736ac6df Mon Sep 17 00:00:00 2001 From: MAZE Date: Tue, 3 Sep 2024 16:50:26 +0330 Subject: [PATCH] fix: play sounds on iOS --- src/components/app/app.tsx | 25 +++++++------ src/contexts/sound.tsx | 73 ++++++++++++++++++++++++++++++++++++++ src/hooks/use-sound.ts | 20 ++++++++++- 3 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 src/contexts/sound.tsx diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index e0eee08..1ef6cd2 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 { SoundProvider } from '@/contexts/sound'; import { sounds } from '@/data/sounds'; import { FADE_OUT } from '@/constants/events'; @@ -86,17 +87,19 @@ export function App() { }, [favoriteSounds, categories]); return ( - - - -
- - - + + + + +
+ + + - - - - + + + + + ); } diff --git a/src/contexts/sound.tsx b/src/contexts/sound.tsx new file mode 100644 index 0000000..402e602 --- /dev/null +++ b/src/contexts/sound.tsx @@ -0,0 +1,73 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { Howler } from 'howler'; + +// Define the context's interface +interface SoundContextType { + connectBufferSource: (bufferSource: AudioBufferSourceNode) => void; +} + +// Create the SoundContext with an empty initial value +const SoundContext = createContext(undefined); + +// Custom hook to use the SoundContext +export const useSoundContext = (): SoundContextType => { + const context = useContext(SoundContext); + if (!context) { + throw new Error('useSoundContext must be used within a SoundProvider'); + } + return context; +}; + +// Props for the SoundProvider component +interface SoundProviderProps { + children: React.ReactNode; +} + +export const SoundProvider: React.FC = ({ children }) => { + const [dest, setDest] = useState( + null, + ); + const [audioTag, setAudioTag] = useState(null); + + useEffect(() => { + if (typeof window !== 'undefined') { + // Get the Howler.js AudioContext after the component is mounted + const audioCtx = Howler.ctx; + + if (audioCtx) { + const mediaDest = audioCtx.createMediaStreamDestination(); + setDest(mediaDest); + + // Create an audio element to trick iOS + const audioElement = document.createElement('audio'); + audioElement.srcObject = mediaDest.stream; + audioElement.style.display = 'none'; // Hide the audio element + document.body.appendChild(audioElement); + setAudioTag(audioElement); + + return () => { + // Clean up the audio element on unmount + document.body.removeChild(audioElement); + }; + } + } + }, []); + + // Function to connect a buffer source to the MediaStreamDestination + const connectBufferSource = (bufferSource: AudioBufferSourceNode) => { + if (dest) { + bufferSource.connect(dest); + + // Start playing the audio once the first buffer connects + if (audioTag && audioTag.paused) { + audioTag.play().catch(() => console.error('Failed to play audio')); + } + } + }; + + return ( + + {children} + + ); +}; diff --git a/src/hooks/use-sound.ts b/src/hooks/use-sound.ts index 934092d..8baab42 100644 --- a/src/hooks/use-sound.ts +++ b/src/hooks/use-sound.ts @@ -5,6 +5,7 @@ import { useLoadingStore } from '@/stores/loading'; import { subscribe } from '@/lib/event'; import { useSSR } from './use-ssr'; import { FADE_OUT } from '@/constants/events'; +import { useSoundContext } from '@/contexts/sound'; /** * A custom React hook to manage sound playback using Howler.js with additional features. @@ -34,6 +35,8 @@ export function useSound( const setIsLoading = useLoadingStore(state => state.set); const { isBrowser } = useSSR(); + const { connectBufferSource } = useSoundContext(); // Access SoundContext + const sound = useMemo(() => { let sound: Howl | null = null; @@ -43,6 +46,14 @@ export function useSound( onload: () => { setIsLoading(src, false); setHasLoaded(true); + + // Connect the buffer source to the MediaStreamDestination + // @ts-ignore + const source = sound!._sounds[0]._node.bufferSource; + if (source) { + console.log('DOOOOPE'); + connectBufferSource(source); + } }, preload: options.preload ?? false, src: src, @@ -50,7 +61,14 @@ export function useSound( } return sound; - }, [src, isBrowser, setIsLoading, html5, options.preload]); + }, [ + src, + isBrowser, + setIsLoading, + html5, + options.preload, + connectBufferSource, + ]); useEffect(() => { if (sound) {