mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Broadcaster audio
This commit is contained in:
@@ -11,6 +11,7 @@ const mediaCache = new Map();
|
|||||||
const renderStates = new Map();
|
const renderStates = new Map();
|
||||||
const animatedCache = new Map();
|
const animatedCache = new Map();
|
||||||
const audioControllers = new Map();
|
const audioControllers = new Map();
|
||||||
|
const pendingAudioUnlock = new Set();
|
||||||
let drawPending = false;
|
let drawPending = false;
|
||||||
let zOrderDirty = true;
|
let zOrderDirty = true;
|
||||||
let zOrderCache = [];
|
let zOrderCache = [];
|
||||||
@@ -45,6 +46,17 @@ const selectedDeleteBtn = document.getElementById('selected-asset-delete');
|
|||||||
const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80"><rect width="100%" height="100%" fill="#1f2937" rx="8"/><g fill="#fbbf24" transform="translate(20 20)"><circle cx="15" cy="20" r="6"/><rect x="28" y="5" width="12" height="30" rx="2"/><rect x="45" y="10" width="140" height="5" fill="#fef3c7"/><rect x="45" y="23" width="110" height="5" fill="#fef3c7"/></g><text x="20" y="70" fill="#e5e7eb" font-family="sans-serif" font-size="14">Audio</text></svg>');
|
const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80"><rect width="100%" height="100%" fill="#1f2937" rx="8"/><g fill="#fbbf24" transform="translate(20 20)"><circle cx="15" cy="20" r="6"/><rect x="28" y="5" width="12" height="30" rx="2"/><rect x="45" y="10" width="140" height="5" fill="#fef3c7"/><rect x="45" y="23" width="110" height="5" fill="#fef3c7"/></g><text x="20" y="70" fill="#e5e7eb" font-family="sans-serif" font-size="14">Audio</text></svg>');
|
||||||
const aspectLockState = new Map();
|
const aspectLockState = new Map();
|
||||||
const commitSizeChange = debounce(() => applyTransformFromInputs(), 180);
|
const commitSizeChange = debounce(() => applyTransformFromInputs(), 180);
|
||||||
|
const audioUnlockEvents = ['pointerdown', 'keydown', 'touchstart'];
|
||||||
|
|
||||||
|
audioUnlockEvents.forEach((eventName) => {
|
||||||
|
window.addEventListener(eventName, () => {
|
||||||
|
if (!pendingAudioUnlock.size) return;
|
||||||
|
pendingAudioUnlock.forEach((controller) => {
|
||||||
|
safePlay(controller);
|
||||||
|
});
|
||||||
|
pendingAudioUnlock.clear();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function debounce(fn, wait = 150) {
|
function debounce(fn, wait = 150) {
|
||||||
let timeout;
|
let timeout;
|
||||||
@@ -54,6 +66,60 @@ function debounce(fn, wait = 150) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDurationLabel(durationMs) {
|
||||||
|
const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60) % 60;
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (asset.id === selectedAssetId) {
|
||||||
|
updateSelectedAssetSummary(asset);
|
||||||
|
}
|
||||||
|
drawAndList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDuration(asset) {
|
||||||
|
return asset && Number.isFinite(asset.durationMs) && asset.durationMs > 0 && (isAudioAsset(asset) || isVideoAsset(asset));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDurationBadge(asset) {
|
||||||
|
if (!hasDuration(asset)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return formatDurationLabel(asset.durationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueAudioForUnlock(controller) {
|
||||||
|
if (!controller) return;
|
||||||
|
pendingAudioUnlock.add(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safePlay(controller) {
|
||||||
|
if (!controller?.element) return;
|
||||||
|
const playPromise = controller.element.play();
|
||||||
|
if (playPromise?.catch) {
|
||||||
|
playPromise.catch(() => queueAudioForUnlock(controller));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (widthInput) widthInput.addEventListener('input', () => handleSizeInputChange('width'));
|
if (widthInput) widthInput.addEventListener('input', () => handleSizeInputChange('width'));
|
||||||
if (widthInput) widthInput.addEventListener('change', () => commitSizeChange());
|
if (widthInput) widthInput.addEventListener('change', () => commitSizeChange());
|
||||||
if (heightInput) heightInput.addEventListener('input', () => handleSizeInputChange('height'));
|
if (heightInput) heightInput.addEventListener('input', () => handleSizeInputChange('height'));
|
||||||
@@ -604,8 +670,10 @@ function ensureAudioController(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const element = new Audio(asset.url);
|
const element = new Audio(asset.url);
|
||||||
|
element.autoplay = true;
|
||||||
element.controls = true;
|
element.controls = true;
|
||||||
element.preload = 'auto';
|
element.preload = 'auto';
|
||||||
|
element.addEventListener('loadedmetadata', () => recordDuration(asset.id, element.duration));
|
||||||
const controller = {
|
const controller = {
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
src: asset.url,
|
src: asset.url,
|
||||||
@@ -645,7 +713,7 @@ function handleAudioEnded(assetId) {
|
|||||||
}
|
}
|
||||||
if (controller.loopEnabled) {
|
if (controller.loopEnabled) {
|
||||||
controller.delayTimeout = setTimeout(() => {
|
controller.delayTimeout = setTimeout(() => {
|
||||||
controller.element.play().catch(() => {});
|
safePlay(controller);
|
||||||
}, controller.delayMs);
|
}, controller.delayMs);
|
||||||
} else {
|
} else {
|
||||||
controller.element.pause();
|
controller.element.pause();
|
||||||
@@ -672,7 +740,7 @@ function playAudioFromCanvas(asset, resetDelay = false) {
|
|||||||
}
|
}
|
||||||
controller.element.currentTime = 0;
|
controller.element.currentTime = 0;
|
||||||
controller.delayMs = resetDelay ? 0 : controller.baseDelayMs;
|
controller.delayMs = resetDelay ? 0 : controller.baseDelayMs;
|
||||||
controller.element.play().catch(() => {});
|
safePlay(controller);
|
||||||
controller.delayMs = controller.baseDelayMs;
|
controller.delayMs = controller.baseDelayMs;
|
||||||
requestDraw();
|
requestDraw();
|
||||||
}
|
}
|
||||||
@@ -684,7 +752,7 @@ function autoStartAudio(asset) {
|
|||||||
const controller = ensureAudioController(asset);
|
const controller = ensureAudioController(asset);
|
||||||
if (controller.loopEnabled && controller.element.paused && !controller.delayTimeout) {
|
if (controller.loopEnabled && controller.element.paused && !controller.delayTimeout) {
|
||||||
controller.delayTimeout = setTimeout(() => {
|
controller.delayTimeout = setTimeout(() => {
|
||||||
controller.element.play().catch(() => {});
|
safePlay(controller);
|
||||||
}, controller.delayMs);
|
}, controller.delayMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -719,6 +787,7 @@ function ensureMedia(asset) {
|
|||||||
element.playsInline = true;
|
element.playsInline = true;
|
||||||
element.autoplay = true;
|
element.autoplay = true;
|
||||||
element.onloadeddata = requestDraw;
|
element.onloadeddata = requestDraw;
|
||||||
|
element.onloadedmetadata = () => recordDuration(asset.id, element.duration);
|
||||||
element.src = asset.url;
|
element.src = asset.url;
|
||||||
const playback = asset.speed ?? 1;
|
const playback = asset.speed ?? 1;
|
||||||
element.playbackRate = Math.max(playback, 0.01);
|
element.playbackRate = Math.max(playback, 0.01);
|
||||||
@@ -890,6 +959,10 @@ function renderAssetList() {
|
|||||||
if (aspectLabel) {
|
if (aspectLabel) {
|
||||||
badges.appendChild(createBadge(aspectLabel, 'subtle'));
|
badges.appendChild(createBadge(aspectLabel, 'subtle'));
|
||||||
}
|
}
|
||||||
|
const durationLabel = getDurationBadge(asset);
|
||||||
|
if (durationLabel) {
|
||||||
|
badges.appendChild(createBadge(durationLabel, 'subtle'));
|
||||||
|
}
|
||||||
meta.appendChild(badges);
|
meta.appendChild(badges);
|
||||||
|
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
@@ -1016,6 +1089,12 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
|
|||||||
if (audioSection) {
|
if (audioSection) {
|
||||||
const showAudio = isAudioAsset(asset);
|
const showAudio = isAudioAsset(asset);
|
||||||
audioSection.classList.toggle('hidden', !showAudio);
|
audioSection.classList.toggle('hidden', !showAudio);
|
||||||
|
const audioInputs = [audioLoopInput, audioDelayInput, audioSpeedInput, audioPitchInput, audioVolumeInput];
|
||||||
|
audioInputs.forEach((input) => {
|
||||||
|
if (!input) return;
|
||||||
|
input.disabled = !showAudio;
|
||||||
|
input.parentElement?.classList?.toggle('disabled', !showAudio);
|
||||||
|
});
|
||||||
if (showAudio) {
|
if (showAudio) {
|
||||||
audioLoopInput.checked = !!asset.audioLoop;
|
audioLoopInput.checked = !!asset.audioLoop;
|
||||||
audioDelayInput.value = Math.max(0, asset.audioDelayMillis ?? 0);
|
audioDelayInput.value = Math.max(0, asset.audioDelayMillis ?? 0);
|
||||||
@@ -1048,6 +1127,10 @@ function updateSelectedAssetSummary(asset) {
|
|||||||
if (aspectLabel) {
|
if (aspectLabel) {
|
||||||
selectedAssetBadges.appendChild(createBadge(aspectLabel, 'subtle'));
|
selectedAssetBadges.appendChild(createBadge(aspectLabel, 'subtle'));
|
||||||
}
|
}
|
||||||
|
const durationLabel = getDurationBadge(asset);
|
||||||
|
if (durationLabel) {
|
||||||
|
selectedAssetBadges.appendChild(createBadge(durationLabel, 'subtle'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selectedVisibilityBtn) {
|
if (selectedVisibilityBtn) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const mediaCache = new Map();
|
|||||||
const renderStates = new Map();
|
const renderStates = new Map();
|
||||||
const animatedCache = new Map();
|
const animatedCache = new Map();
|
||||||
const audioControllers = new Map();
|
const audioControllers = new Map();
|
||||||
|
const pendingAudioUnlock = new Set();
|
||||||
const TARGET_FPS = 60;
|
const TARGET_FPS = 60;
|
||||||
const MIN_FRAME_TIME = 1000 / TARGET_FPS;
|
const MIN_FRAME_TIME = 1000 / TARGET_FPS;
|
||||||
let lastRenderTime = 0;
|
let lastRenderTime = 0;
|
||||||
@@ -17,6 +18,15 @@ let sortedAssetsCache = [];
|
|||||||
let assetsDirty = true;
|
let assetsDirty = true;
|
||||||
let renderIntervalId = null;
|
let renderIntervalId = null;
|
||||||
const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80"><rect width="100%" height="100%" fill="#0f172a" rx="8"/><g fill="#22d3ee" transform="translate(20 20)"><circle cx="15" cy="20" r="6"/><rect x="28" y="5" width="12" height="30" rx="2"/><rect x="45" y="10" width="140" height="5" fill="#a5f3fc"/><rect x="45" y="23" width="110" height="5" fill="#a5f3fc"/></g><text x="20" y="70" fill="#e5e7eb" font-family="sans-serif" font-size="14">Audio</text></svg>');
|
const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80"><rect width="100%" height="100%" fill="#0f172a" rx="8"/><g fill="#22d3ee" transform="translate(20 20)"><circle cx="15" cy="20" r="6"/><rect x="28" y="5" width="12" height="30" rx="2"/><rect x="45" y="10" width="140" height="5" fill="#a5f3fc"/><rect x="45" y="23" width="110" height="5" fill="#a5f3fc"/></g><text x="20" y="70" fill="#e5e7eb" font-family="sans-serif" font-size="14">Audio</text></svg>');
|
||||||
|
const audioUnlockEvents = ['pointerdown', 'keydown', 'touchstart'];
|
||||||
|
|
||||||
|
audioUnlockEvents.forEach((eventName) => {
|
||||||
|
window.addEventListener(eventName, () => {
|
||||||
|
if (!pendingAudioUnlock.size) return;
|
||||||
|
pendingAudioUnlock.forEach((controller) => safePlay(controller));
|
||||||
|
pendingAudioUnlock.clear();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const socket = new SockJS('/ws');
|
const socket = new SockJS('/ws');
|
||||||
@@ -177,6 +187,34 @@ function lerp(a, b, t) {
|
|||||||
return a + (b - a) * t;
|
return a + (b - a) * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queueAudioForUnlock(controller) {
|
||||||
|
if (!controller) return;
|
||||||
|
pendingAudioUnlock.add(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safePlay(controller) {
|
||||||
|
if (!controller?.element) return;
|
||||||
|
const playPromise = controller.element.play();
|
||||||
|
if (playPromise?.catch) {
|
||||||
|
playPromise.catch(() => queueAudioForUnlock(controller));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isVideoAsset(asset) {
|
function isVideoAsset(asset) {
|
||||||
return asset?.mediaType?.startsWith('video/');
|
return asset?.mediaType?.startsWith('video/');
|
||||||
}
|
}
|
||||||
@@ -295,8 +333,10 @@ function ensureAudioController(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const element = new Audio(asset.url);
|
const element = new Audio(asset.url);
|
||||||
|
element.autoplay = true;
|
||||||
element.preload = 'auto';
|
element.preload = 'auto';
|
||||||
element.controls = false;
|
element.controls = false;
|
||||||
|
element.addEventListener('loadedmetadata', () => recordDuration(asset.id, element.duration));
|
||||||
const controller = {
|
const controller = {
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
src: asset.url,
|
src: asset.url,
|
||||||
@@ -336,7 +376,7 @@ function handleAudioEnded(assetId) {
|
|||||||
}
|
}
|
||||||
if (controller.loopEnabled) {
|
if (controller.loopEnabled) {
|
||||||
controller.delayTimeout = setTimeout(() => {
|
controller.delayTimeout = setTimeout(() => {
|
||||||
controller.element.play().catch(() => {});
|
safePlay(controller);
|
||||||
}, controller.delayMs);
|
}, controller.delayMs);
|
||||||
} else {
|
} else {
|
||||||
controller.element.pause();
|
controller.element.pause();
|
||||||
@@ -352,7 +392,7 @@ function playAudioImmediately(asset) {
|
|||||||
controller.element.currentTime = 0;
|
controller.element.currentTime = 0;
|
||||||
const originalDelay = controller.delayMs;
|
const originalDelay = controller.delayMs;
|
||||||
controller.delayMs = 0;
|
controller.delayMs = 0;
|
||||||
controller.element.play().catch(() => {});
|
safePlay(controller);
|
||||||
controller.delayMs = controller.baseDelayMs ?? originalDelay ?? 0;
|
controller.delayMs = controller.baseDelayMs ?? originalDelay ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +408,7 @@ function autoStartAudio(asset) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controller.delayTimeout = setTimeout(() => {
|
controller.delayTimeout = setTimeout(() => {
|
||||||
controller.element.play().catch(() => {});
|
safePlay(controller);
|
||||||
}, controller.delayMs);
|
}, controller.delayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +442,7 @@ function ensureMedia(asset) {
|
|||||||
element.playsInline = true;
|
element.playsInline = true;
|
||||||
element.autoplay = true;
|
element.autoplay = true;
|
||||||
element.onloadeddata = draw;
|
element.onloadeddata = draw;
|
||||||
|
element.onloadedmetadata = () => recordDuration(asset.id, element.duration);
|
||||||
element.src = asset.url;
|
element.src = asset.url;
|
||||||
const playback = asset.speed ?? 1;
|
const playback = asset.speed ?? 1;
|
||||||
element.playbackRate = Math.max(playback, 0.01);
|
element.playbackRate = Math.max(playback, 0.01);
|
||||||
|
|||||||
Reference in New Issue
Block a user