Files
bodyshop/client/src/utils/soundManager.js

98 lines
2.8 KiB
JavaScript

// src/utils/soundManager.js
// Handles audio init, autoplay unlock, and queued plays.
// When a tab successfully unlocks audio, it CLAIMS LEADERSHIP immediately for that bodyshop.
import { claimLeadershipNow } from "./singleTabAudioLeader";
let baseAudio = null;
let unlocked = false;
let queuedPlays = 0;
let installingUnlockHandlers = false;
/**
* Initialize the new-message sound.
* @param {string} url
* @param {number} volume
*/
export function initNewMessageSound(url, volume = 0.7) {
baseAudio = new Audio(url);
baseAudio.preload = "auto";
baseAudio.volume = volume;
}
/** Has this tab unlocked audio? (optional helper) */
export function isAudioUnlocked() {
return unlocked;
}
/**
* Unlocks audio if not already unlocked.
* On success, this tab immediately becomes the sound LEADER for the given bodyshop.
*/
export async function unlockAudio(bodyshopId) {
if (unlocked) return;
try {
// Chrome/Safari: playing any media (even muted) after a gesture unlocks audio.
const a = new Audio();
a.muted = true;
await a.play().catch(() => {
// ignore
});
unlocked = true;
// Immediately become the leader because THIS tab can actually play sound.
claimLeadershipNow(bodyshopId);
// Flush exactly one queued ding (avoid spamming if many queued while locked)
if (queuedPlays > 0 && baseAudio) {
queuedPlays = 0;
const b = baseAudio.cloneNode(true);
b.play().catch(() => {
// ignore
});
}
} finally {
removeUnlockListeners();
}
}
/** Installs listeners to unlock audio on first gesture. */
function addUnlockListeners(bodyshopId) {
if (installingUnlockHandlers) return;
installingUnlockHandlers = true;
const handler = () => unlockAudio(bodyshopId);
window.addEventListener("click", handler, { once: true, passive: true });
window.addEventListener("touchstart", handler, { once: true, passive: true });
window.addEventListener("keydown", handler, { once: true });
}
/** Removes listeners to unlock audio on first gesture. */
function removeUnlockListeners() {
// With {once:true} they self-remove; we only reset the flag.
installingUnlockHandlers = false;
}
/**
* Plays the new-message ding. If blocked, queue one and wait for first gesture.
*/
export async function playNewMessageSound(bodyshopId) {
if (!baseAudio) return;
try {
const a = baseAudio.cloneNode(true);
await a.play();
} catch (err) {
// Most common: NotAllowedError due to missing prior gesture
if (err?.name === "NotAllowedError") {
queuedPlays = Math.min(queuedPlays + 1, 1); // cap at 1
addUnlockListeners(bodyshopId);
// Let the app know we need user interaction (optional UI prompt)
window.dispatchEvent(new CustomEvent("sound-needs-unlock"));
return;
}
// Other errors can be logged
console.error("Audio play error:", err);
}
}