2 Commits

Author SHA1 Message Date
MAZE
ad57f082ca fix: wip for ios 2025-02-18 19:53:20 +03:30
MAZE
54b46123b4 fix: wip for ios 2025-02-18 19:42:05 +03:30

View File

@@ -20,6 +20,77 @@ import { FADE_OUT } from '@/constants/events';
import type { Sound } from '@/data/types';
import { subscribe } from '@/lib/event';
/**
* =========================================
*/
declare global {
interface Window {
__howlerStreamPatched?: boolean;
}
}
/**
* Patches Howler's master gain node to route its output into a hidden HTML audio element.
* An intermediate splitter node is used in an attempt to reduce the banging noise observed on iOS.
* Also adds a listener to resume the AudioContext when the document becomes visible.
*/
export function setupAudioStream(): void {
if (
typeof window !== 'undefined' &&
Howler.ctx &&
!window.__howlerStreamPatched
) {
const audioCtx = Howler.ctx;
// Create a MediaStream destination node to capture the output.
const streamDestination = audioCtx.createMediaStreamDestination();
// Create a splitter gain node to help split the signal cleanly.
const splitter = audioCtx.createGain();
// Disconnect the master gain.
Howler.masterGain.disconnect();
// Reconnect masterGain: one branch to the AudioContext's default destination,
// and one branch through the splitter to the MediaStream destination.
Howler.masterGain.connect(audioCtx.destination);
Howler.masterGain.connect(splitter);
splitter.connect(streamDestination);
// Create a hidden HTML audio element to play the captured stream.
const audioElement = document.createElement('audio');
audioElement.setAttribute('playsinline', 'true'); // crucial for iOS playback
audioElement.srcObject = streamDestination.stream;
audioElement.style.display = 'none';
document.body.appendChild(audioElement);
// Attempt to start playback (must be triggered by a user gesture).
audioElement.play().catch((err: unknown) => {
console.error('Failed to play background stream:', err);
});
// Listen for visibility changes: if the document becomes visible and the AudioContext is suspended, resume it.
document.addEventListener('visibilitychange', () => {
if (
document.visibilityState === 'visible' &&
audioCtx.state === 'suspended'
) {
audioCtx
.resume()
.catch((err: unknown) =>
console.error('Error resuming AudioContext:', err),
);
}
});
window.__howlerStreamPatched = true;
}
}
/**
* =========================================
*/
export function App() {
const categories = useMemo(() => sounds.categories, []);
@@ -86,6 +157,19 @@ export function App() {
return [...favorites, ...categories];
}, [favoriteSounds, categories]);
useEffect(() => {
const handleUserInteraction = () => {
setupAudioStream();
document.removeEventListener('click', handleUserInteraction);
};
document.addEventListener('click', handleUserInteraction);
return () => {
document.removeEventListener('click', handleUserInteraction);
};
}, []);
return (
<SnackbarProvider>
<StoreConsumer>