// 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); } }