Files
OBS-overlay/js/audio-controller.js
cottongin fa7363bc78 fix: convert ES modules to classic scripts for file:// compatibility
Browsers block ES module imports over the file:// protocol due to CORS.
Users opening the overlay by double-clicking the HTML file saw all JS
fail to load. Replace import/export with a window.OBS global namespace
and classic <script> tags so the overlay works without a local server.

Made-with: Cursor
2026-03-20 22:20:12 -04:00

143 lines
3.1 KiB
JavaScript

/**
* 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<typeof setInterval> | 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;