/** * Theme audio for the room code lobby: reload, volume, loop, and fade-out. * Implements the overlay component contract: init, activate, deactivate, update, getStatus. */ /** * @typedef {object} AudioControllerInputs * @property {HTMLInputElement} enabled * @property {HTMLInputElement} volume * @property {HTMLInputElement} soundUrl */ class AudioController { /** @type {HTMLAudioElement | null} */ #audio = null; /** @type {AudioControllerInputs | null} */ #inputs = null; /** @type {boolean} */ #active = false; /** @type {ReturnType | null} */ #fadeInterval = null; /** * @param {HTMLAudioElement} audioElement * @param {AudioControllerInputs} inputs */ init(audioElement, inputs) { this.#audio = audioElement; this.#inputs = inputs; } /** * @param {object} [_ctx] */ activate(_ctx) { this.deactivate(); this.#active = true; const inputs = this.#inputs; const audio = this.#audio; if (!inputs || !audio) { return; } if (!inputs.enabled.checked) { return; } audio.src = inputs.soundUrl.value; audio.load(); audio.volume = parseFloat(inputs.volume.value); audio.loop = true; audio.currentTime = 0; audio.play().catch((error) => { console.log('Audio playback failed:', error); }); } deactivate() { this.#active = false; if (this.#fadeInterval != null) { clearInterval(this.#fadeInterval); this.#fadeInterval = null; } const audio = this.#audio; const inputs = this.#inputs; if (!audio) { return; } audio.pause(); audio.currentTime = 0; if (inputs) { audio.volume = parseFloat(inputs.volume.value); } } /** * @param {object} [_ctx] */ update(_ctx) { // No in-state audio updates. } getStatus() { const audio = this.#audio; const inputs = this.#inputs; return { active: this.#active, enabled: inputs?.enabled.checked ?? false, playing: Boolean(audio && !audio.paused), volume: audio?.volume ?? 0, src: audio?.currentSrc ?? audio?.src ?? '', }; } /** * Gradually reduce volume to zero, then pause. No-op if audio is already paused. * @param {number} durationMs */ startFadeOut(durationMs) { const audio = this.#audio; if (!audio || audio.paused) { return; } if (this.#fadeInterval != null) { clearInterval(this.#fadeInterval); this.#fadeInterval = null; } const startVolume = audio.volume; const stepCount = 10; const intervalMs = Math.max(1, durationMs / stepCount); let step = 0; this.#fadeInterval = setInterval(() => { step += 1; audio.volume = Math.max(0, startVolume * (1 - step / stepCount)); if (step >= stepCount) { audio.volume = 0; audio.pause(); if (this.#fadeInterval != null) { clearInterval(this.#fadeInterval); this.#fadeInterval = null; } } }, intervalMs); } } window.OBS = window.OBS || {}; window.OBS.AudioController = AudioController;