mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Refactor broadcast page
This commit is contained in:
216
src/main/resources/static/js/broadcast/audioManager.js
Normal file
216
src/main/resources/static/js/broadcast/audioManager.js
Normal file
@@ -0,0 +1,216 @@
|
||||
const audioUnlockEvents = ["pointerdown", "keydown", "touchstart"];
|
||||
|
||||
export function createAudioManager({ assets, globalScope = globalThis }) {
|
||||
const audioControllers = new Map();
|
||||
const pendingAudioUnlock = new Set();
|
||||
|
||||
audioUnlockEvents.forEach((eventName) => {
|
||||
globalScope.addEventListener(eventName, () => {
|
||||
if (!pendingAudioUnlock.size) return;
|
||||
pendingAudioUnlock.forEach((controller) => safePlay(controller, pendingAudioUnlock));
|
||||
pendingAudioUnlock.clear();
|
||||
});
|
||||
});
|
||||
|
||||
function ensureAudioController(asset) {
|
||||
const cached = audioControllers.get(asset.id);
|
||||
if (cached && cached.src === asset.url) {
|
||||
applyAudioSettings(cached, asset);
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (cached) {
|
||||
clearAudio(asset.id);
|
||||
}
|
||||
|
||||
const element = new Audio(asset.url);
|
||||
element.autoplay = true;
|
||||
element.preload = "auto";
|
||||
element.controls = false;
|
||||
element.addEventListener("loadedmetadata", () => recordDuration(asset.id, element.duration));
|
||||
const controller = {
|
||||
id: asset.id,
|
||||
src: asset.url,
|
||||
element,
|
||||
delayTimeout: null,
|
||||
loopEnabled: false,
|
||||
loopActive: true,
|
||||
delayMs: 0,
|
||||
baseDelayMs: 0,
|
||||
};
|
||||
element.onended = () => handleAudioEnded(asset.id);
|
||||
audioControllers.set(asset.id, controller);
|
||||
applyAudioSettings(controller, asset, true);
|
||||
return controller;
|
||||
}
|
||||
|
||||
function applyAudioSettings(controller, asset, resetPosition = false) {
|
||||
controller.loopEnabled = !!asset.audioLoop;
|
||||
controller.loopActive = controller.loopEnabled && controller.loopActive !== false;
|
||||
controller.baseDelayMs = Math.max(0, asset.audioDelayMillis || 0);
|
||||
controller.delayMs = controller.baseDelayMs;
|
||||
applyAudioElementSettings(controller.element, asset);
|
||||
if (resetPosition) {
|
||||
controller.element.currentTime = 0;
|
||||
controller.element.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function applyAudioElementSettings(element, asset) {
|
||||
const speed = Math.max(0.25, asset.audioSpeed || 1);
|
||||
const pitch = Math.max(0.5, asset.audioPitch || 1);
|
||||
element.playbackRate = speed * pitch;
|
||||
const volume = Math.max(0, Math.min(2, asset.audioVolume ?? 1));
|
||||
element.volume = Math.min(volume, 1);
|
||||
}
|
||||
|
||||
function getAssetVolume(asset) {
|
||||
return Math.max(0, Math.min(2, asset?.audioVolume ?? 1));
|
||||
}
|
||||
|
||||
function applyMediaVolume(element, asset) {
|
||||
if (!element) return 1;
|
||||
const volume = getAssetVolume(asset);
|
||||
element.volume = Math.min(volume, 1);
|
||||
return volume;
|
||||
}
|
||||
|
||||
function handleAudioEnded(assetId) {
|
||||
const controller = audioControllers.get(assetId);
|
||||
if (!controller) return;
|
||||
controller.element.currentTime = 0;
|
||||
if (controller.delayTimeout) {
|
||||
clearTimeout(controller.delayTimeout);
|
||||
}
|
||||
if (controller.loopEnabled && controller.loopActive) {
|
||||
controller.delayTimeout = setTimeout(() => {
|
||||
safePlay(controller, pendingAudioUnlock);
|
||||
}, controller.delayMs);
|
||||
} else {
|
||||
controller.element.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function stopAudio(assetId) {
|
||||
const controller = audioControllers.get(assetId);
|
||||
if (!controller) return;
|
||||
if (controller.delayTimeout) {
|
||||
clearTimeout(controller.delayTimeout);
|
||||
}
|
||||
controller.element.pause();
|
||||
controller.element.currentTime = 0;
|
||||
controller.delayTimeout = null;
|
||||
controller.delayMs = controller.baseDelayMs;
|
||||
controller.loopActive = false;
|
||||
}
|
||||
|
||||
function playAudioImmediately(asset) {
|
||||
const controller = ensureAudioController(asset);
|
||||
if (controller.delayTimeout) {
|
||||
clearTimeout(controller.delayTimeout);
|
||||
controller.delayTimeout = null;
|
||||
}
|
||||
controller.element.currentTime = 0;
|
||||
const originalDelay = controller.delayMs;
|
||||
controller.delayMs = 0;
|
||||
safePlay(controller, pendingAudioUnlock);
|
||||
controller.delayMs = controller.baseDelayMs ?? originalDelay ?? 0;
|
||||
}
|
||||
|
||||
function playOverlappingAudio(asset) {
|
||||
const temp = new Audio(asset.url);
|
||||
temp.autoplay = true;
|
||||
temp.preload = "auto";
|
||||
temp.controls = false;
|
||||
applyAudioElementSettings(temp, asset);
|
||||
const controller = { element: temp };
|
||||
temp.onended = () => {
|
||||
temp.remove();
|
||||
};
|
||||
safePlay(controller, pendingAudioUnlock);
|
||||
}
|
||||
|
||||
function handleAudioPlay(asset, shouldPlay) {
|
||||
const controller = ensureAudioController(asset);
|
||||
controller.loopActive = !!shouldPlay;
|
||||
if (!shouldPlay) {
|
||||
stopAudio(asset.id);
|
||||
return;
|
||||
}
|
||||
if (asset.audioLoop) {
|
||||
controller.delayMs = controller.baseDelayMs;
|
||||
safePlay(controller, pendingAudioUnlock);
|
||||
} else {
|
||||
playOverlappingAudio(asset);
|
||||
}
|
||||
}
|
||||
|
||||
function autoStartAudio(asset) {
|
||||
if (asset.hidden) {
|
||||
return;
|
||||
}
|
||||
const controller = ensureAudioController(asset);
|
||||
if (!controller.loopEnabled || !controller.loopActive) {
|
||||
return;
|
||||
}
|
||||
if (!controller.element.paused && !controller.element.ended) {
|
||||
return;
|
||||
}
|
||||
if (controller.delayTimeout) {
|
||||
return;
|
||||
}
|
||||
controller.delayTimeout = setTimeout(() => {
|
||||
safePlay(controller, pendingAudioUnlock);
|
||||
}, controller.delayMs);
|
||||
}
|
||||
|
||||
function recordDuration(assetId, seconds) {
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) {
|
||||
return;
|
||||
}
|
||||
const asset = assets.get(assetId);
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
const nextMs = Math.round(seconds * 1000);
|
||||
if (asset.durationMs === nextMs) {
|
||||
return;
|
||||
}
|
||||
asset.durationMs = nextMs;
|
||||
}
|
||||
|
||||
function clearAudio(assetId) {
|
||||
const audio = audioControllers.get(assetId);
|
||||
if (!audio) {
|
||||
return;
|
||||
}
|
||||
if (audio.delayTimeout) {
|
||||
clearTimeout(audio.delayTimeout);
|
||||
}
|
||||
audio.element.pause();
|
||||
audio.element.currentTime = 0;
|
||||
audio.element.src = "";
|
||||
audio.element.remove();
|
||||
audioControllers.delete(assetId);
|
||||
}
|
||||
|
||||
return {
|
||||
ensureAudioController,
|
||||
applyMediaVolume,
|
||||
handleAudioPlay,
|
||||
stopAudio,
|
||||
playAudioImmediately,
|
||||
autoStartAudio,
|
||||
clearAudio,
|
||||
};
|
||||
}
|
||||
|
||||
function safePlay(controller, pendingUnlock) {
|
||||
if (!controller?.element) return;
|
||||
const playPromise = controller.element.play();
|
||||
if (playPromise?.catch) {
|
||||
playPromise.catch(() => {
|
||||
pendingUnlock.add(controller);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user