mirror of
https://github.com/remvze/moodist.git
synced 2026-03-02 10:03:14 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad57f082ca | ||
|
|
54b46123b4 |
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user