From 581e28863b58e0bdd72eac5aeb6a481beafe5ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Wed, 10 Dec 2025 17:14:38 +0100 Subject: [PATCH] Remove mute --- .../app/service/ChannelDirectoryService.java | 2 +- src/main/resources/static/js/admin.js | 103 +++++++++++++----- src/main/resources/static/js/broadcast.js | 33 +++--- src/main/resources/templates/admin.html | 35 +++--- 4 files changed, 109 insertions(+), 64 deletions(-) diff --git a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java index 270bf1e..225ab83 100644 --- a/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java +++ b/src/main/java/com/imgfloat/app/service/ChannelDirectoryService.java @@ -187,7 +187,7 @@ public class ChannelDirectoryService { asset.setAudioPitch(request.getAudioPitch()); } if (request.getAudioVolume() != null && request.getAudioVolume() >= 0) { - double clamped = Math.max(0.0, Math.min(1.0, request.getAudioVolume())); + double clamped = Math.max(0.0, Math.min(2.0, request.getAudioVolume())); asset.setAudioVolume(clamped); } assetRepository.save(asset); diff --git a/src/main/resources/static/js/admin.js b/src/main/resources/static/js/admin.js index 2f9fe90..d159bb7 100644 --- a/src/main/resources/static/js/admin.js +++ b/src/main/resources/static/js/admin.js @@ -24,16 +24,22 @@ let interactionState = null; let lastSizeInputChanged = null; const HANDLE_SIZE = 10; const ROTATE_HANDLE_OFFSET = 32; +const MAX_VOLUME = 2; +const VOLUME_SLIDER_MAX = 200; +const VOLUME_CURVE_STRENGTH = -0.6; + const controlsPanel = document.getElementById('asset-controls'); const widthInput = document.getElementById('asset-width'); const heightInput = document.getElementById('asset-height'); const aspectLockInput = document.getElementById('maintain-aspect'); const speedInput = document.getElementById('asset-speed'); -const muteInput = document.getElementById('asset-muted'); const speedLabel = document.getElementById('asset-speed-label'); +const volumeInput = document.getElementById('asset-volume'); +const volumeLabel = document.getElementById('asset-volume-label'); const selectedZLabel = document.getElementById('asset-z-level'); const playbackSection = document.getElementById('playback-section'); +const volumeSection = document.getElementById('volume-section'); const audioSection = document.getElementById('audio-section'); const layoutSection = document.getElementById('layout-section'); const audioLoopInput = document.getElementById('asset-audio-loop'); @@ -41,7 +47,6 @@ const audioDelayInput = document.getElementById('asset-audio-delay'); const audioSpeedInput = document.getElementById('asset-audio-speed'); const audioSpeedLabel = document.getElementById('asset-audio-speed-label'); const audioPitchInput = document.getElementById('asset-audio-pitch'); -const audioVolumeInput = document.getElementById('asset-audio-volume'); const controlsPlaceholder = document.getElementById('asset-controls-placeholder'); const fileNameLabel = document.getElementById('asset-file-name'); const assetInspector = document.getElementById('asset-inspector'); @@ -167,6 +172,38 @@ function setAudioSpeedLabel(percentValue) { audioSpeedLabel.textContent = `${formatted}x`; } +function clamp(value, min, max) { + return Math.min(max, Math.max(min, value)); +} + +function sliderToVolume(sliderValue) { + const normalized = clamp(sliderValue, 0, VOLUME_SLIDER_MAX) / VOLUME_SLIDER_MAX; + const curved = normalized + VOLUME_CURVE_STRENGTH * normalized * (1 - normalized) * (1 - 2 * normalized); + return clamp(curved * MAX_VOLUME, 0, MAX_VOLUME); +} + +function volumeToSlider(volumeValue) { + const target = clamp(volumeValue ?? 1, 0, MAX_VOLUME) / MAX_VOLUME; + let low = 0; + let high = VOLUME_SLIDER_MAX; + for (let i = 0; i < 24; i += 1) { + const mid = (low + high) / 2; + const midNormalized = sliderToVolume(mid) / MAX_VOLUME; + if (midNormalized < target) { + low = mid; + } else { + high = mid; + } + } + return Math.round(high); +} + +function setVolumeLabel(sliderValue) { + if (!volumeLabel) return; + const volumePercent = Math.round(sliderToVolume(sliderValue) * 100); + volumeLabel.textContent = `${volumePercent}%`; +} + function queueAudioForUnlock(controller) { if (!controller) return; pendingAudioUnlock.add(controller); @@ -185,12 +222,11 @@ if (widthInput) widthInput.addEventListener('change', () => commitSizeChange()); if (heightInput) heightInput.addEventListener('input', () => handleSizeInputChange('height')); if (heightInput) heightInput.addEventListener('change', () => commitSizeChange()); if (speedInput) speedInput.addEventListener('input', updatePlaybackFromInputs); -if (muteInput) muteInput.addEventListener('change', updateMuteFromInput); +if (volumeInput) volumeInput.addEventListener('input', updateVolumeFromInput); if (audioLoopInput) audioLoopInput.addEventListener('change', updateAudioSettingsFromInputs); if (audioDelayInput) audioDelayInput.addEventListener('change', updateAudioSettingsFromInputs); if (audioSpeedInput) audioSpeedInput.addEventListener('change', updateAudioSettingsFromInputs); if (audioPitchInput) audioPitchInput.addEventListener('change', updateAudioSettingsFromInputs); -if (audioVolumeInput) audioVolumeInput.addEventListener('change', updateAudioSettingsFromInputs); if (selectedVisibilityBtn) { selectedVisibilityBtn.addEventListener('click', () => { const asset = getSelectedAsset(); @@ -796,7 +832,7 @@ function applyAudioSettings(controller, asset, resetPosition = false) { const speed = Math.max(0.25, asset.audioSpeed || 1); const pitch = Math.max(0.5, asset.audioPitch || 1); controller.element.playbackRate = speed * pitch; - const volume = Math.max(0, Math.min(1, asset.audioVolume ?? 1)); + const volume = clamp(asset.audioVolume ?? 1, 0, MAX_VOLUME); controller.element.volume = volume; if (resetPosition) { controller.element.currentTime = 0; @@ -871,7 +907,9 @@ function ensureMedia(asset) { element.crossOrigin = 'anonymous'; if (isVideoElement(element)) { element.loop = true; - element.muted = asset.muted ?? true; + const volume = clamp(asset.audioVolume ?? 1, 0, MAX_VOLUME); + element.muted = volume === 0; + element.volume = Math.min(volume, 1); element.playsInline = true; element.autoplay = false; element.preload = 'metadata'; @@ -972,26 +1010,19 @@ function applyMediaSettings(element, asset) { } const nextSpeed = asset.speed ?? 1; const effectiveSpeed = Math.max(nextSpeed, 0.01); - const wasMuted = element.muted; if (element.playbackRate !== effectiveSpeed) { element.playbackRate = effectiveSpeed; } - const shouldMute = asset.muted ?? true; - if (element.muted !== shouldMute) { - element.muted = shouldMute; - } + const volume = clamp(asset.audioVolume ?? 1, 0, MAX_VOLUME); + element.muted = volume === 0; + element.volume = Math.min(volume, 1); if (nextSpeed === 0) { element.pause(); return; } const playPromise = element.play(); if (playPromise?.catch) { - playPromise.catch(() => { - if (!shouldMute && wasMuted) { - element.muted = true; - element.play().catch(() => { }); - } - }); + playPromise.catch(() => { }); } } @@ -1426,15 +1457,24 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) { playbackSection.classList.toggle('hidden', !shouldShowPlayback); speedInput?.classList?.toggle('disabled', !shouldShowPlayback); } - if (muteInput) { - muteInput.checked = !!asset.muted; - muteInput.disabled = !isVideoAsset(asset); - muteInput.parentElement?.classList.toggle('disabled', !isVideoAsset(asset)); + if (volumeSection) { + const showVolume = isAudioAsset(asset) || isVideoAsset(asset); + volumeSection.classList.toggle('hidden', !showVolume); + const volumeControls = volumeSection.querySelectorAll('input'); + volumeControls.forEach((control) => { + control.disabled = !showVolume; + control.classList.toggle('disabled', !showVolume); + }); + if (showVolume && volumeInput) { + const sliderValue = volumeToSlider(asset.audioVolume ?? 1); + volumeInput.value = sliderValue; + setVolumeLabel(sliderValue); + } } if (audioSection) { const showAudio = isAudioAsset(asset); audioSection.classList.toggle('hidden', !showAudio); - const audioInputs = [audioLoopInput, audioDelayInput, audioSpeedInput, audioPitchInput, audioVolumeInput]; + const audioInputs = [audioLoopInput, audioDelayInput, audioSpeedInput, audioPitchInput]; audioInputs.forEach((input) => { if (!input) return; input.disabled = !showAudio; @@ -1446,7 +1486,6 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) { audioSpeedInput.value = Math.round(Math.max(0.25, asset.audioSpeed ?? 1) * 100); setAudioSpeedLabel(audioSpeedInput.value); audioPitchInput.value = Math.round(Math.max(0.5, asset.audioPitch ?? 1) * 100); - audioVolumeInput.value = Math.round(Math.max(0, Math.min(1, asset.audioVolume ?? 1)) * 100); } } } @@ -1544,16 +1583,22 @@ function updatePlaybackFromInputs() { drawAndList(); } -function updateMuteFromInput() { +function updateVolumeFromInput() { const asset = getSelectedAsset(); - if (!asset || !isVideoAsset(asset)) return; - asset.muted = !!muteInput?.checked; - updateRenderState(asset); - persistTransform(asset); + if (!asset || !(isVideoAsset(asset) || isAudioAsset(asset))) return; + const sliderValue = Math.max(0, Math.min(VOLUME_SLIDER_MAX, parseFloat(volumeInput?.value) || 100)); + const volumeValue = sliderToVolume(sliderValue); + setVolumeLabel(sliderValue); + asset.audioVolume = volumeValue; const media = mediaCache.get(asset.id); if (media) { applyMediaSettings(media, asset); } + if (isAudioAsset(asset)) { + const controller = ensureAudioController(asset); + applyAudioSettings(controller, asset); + } + persistTransform(asset); drawAndList(); } @@ -1566,7 +1611,6 @@ function updateAudioSettingsFromInputs() { setAudioSpeedLabel(nextAudioSpeedPercent); asset.audioSpeed = Math.max(0.25, (nextAudioSpeedPercent / 100)); asset.audioPitch = Math.max(0.5, (parseInt(audioPitchInput?.value || '100', 10) / 100)); - asset.audioVolume = Math.max(0, Math.min(1, (parseInt(audioVolumeInput?.value || '100', 10) / 100))); const controller = ensureAudioController(asset); applyAudioSettings(controller, asset); persistTransform(asset); @@ -1876,7 +1920,6 @@ function persistTransform(asset, silent = false) { height: asset.height, rotation: asset.rotation, speed: asset.speed, - muted: asset.muted, zIndex: asset.zIndex, audioLoop: asset.audioLoop, audioDelayMillis: asset.audioDelayMillis, diff --git a/src/main/resources/static/js/broadcast.js b/src/main/resources/static/js/broadcast.js index 10f0db4..fe8c5d2 100644 --- a/src/main/resources/static/js/broadcast.js +++ b/src/main/resources/static/js/broadcast.js @@ -387,8 +387,20 @@ 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(1, asset.audioVolume ?? 1)); - element.volume = volume; + 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.muted = volume === 0; + element.volume = Math.min(volume, 1); + return volume; } function handleAudioEnded(assetId) { @@ -510,7 +522,7 @@ function ensureMedia(asset) { element.crossOrigin = 'anonymous'; if (isVideoElement(element)) { element.loop = true; - element.muted = asset.muted ?? true; + applyMediaVolume(element, asset); element.playsInline = true; element.autoplay = true; element.onloadeddata = draw; @@ -618,7 +630,7 @@ function applyVideoSource(element, objectUrl, asset) { if (playback === 0) { element.pause(); } else { - element.play().catch(() => {}); + element.play().catch(() => queueAudioForUnlock({ element })); } } @@ -670,25 +682,16 @@ function applyMediaSettings(element, asset) { } const nextSpeed = asset.speed ?? 1; const effectiveSpeed = Math.max(nextSpeed, 0.01); - const wasMuted = element.muted; if (element.playbackRate !== effectiveSpeed) { element.playbackRate = effectiveSpeed; } - const shouldMute = asset.muted ?? true; - if (element.muted !== shouldMute) { - element.muted = shouldMute; - } + applyMediaVolume(element, asset); if (nextSpeed === 0) { element.pause(); } else { const playPromise = element.play(); if (playPromise?.catch) { - playPromise.catch(() => { - if (!shouldMute && wasMuted) { - element.muted = true; - element.play().catch(() => {}); - } - }); + playPromise.catch(() => queueAudioForUnlock({ element })); } } } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 8221ac1..4f33b01 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -28,10 +28,9 @@ -
-
-

Overlay assets

-

Upload overlay visuals and adjust them inline.

+
+

Overlay assets

+

Upload overlay visuals and adjust them inline.

@@ -105,7 +104,6 @@
Playback
-

Video-only controls.

@@ -115,14 +113,19 @@
0%1000%
-
- +
+ +
+
+
Volume
+
+
+
+ Playback volume + 100% +
+ +
0%200%
@@ -157,10 +160,7 @@ Pitch (%) - +
@@ -168,7 +168,6 @@
-