Remove mute

This commit is contained in:
2025-12-10 17:14:38 +01:00
parent bde6b8a3de
commit 581e28863b
4 changed files with 109 additions and 64 deletions

View File

@@ -187,7 +187,7 @@ public class ChannelDirectoryService {
asset.setAudioPitch(request.getAudioPitch()); asset.setAudioPitch(request.getAudioPitch());
} }
if (request.getAudioVolume() != null && request.getAudioVolume() >= 0) { 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); asset.setAudioVolume(clamped);
} }
assetRepository.save(asset); assetRepository.save(asset);

View File

@@ -24,16 +24,22 @@ let interactionState = null;
let lastSizeInputChanged = null; let lastSizeInputChanged = null;
const HANDLE_SIZE = 10; const HANDLE_SIZE = 10;
const ROTATE_HANDLE_OFFSET = 32; 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 controlsPanel = document.getElementById('asset-controls');
const widthInput = document.getElementById('asset-width'); const widthInput = document.getElementById('asset-width');
const heightInput = document.getElementById('asset-height'); const heightInput = document.getElementById('asset-height');
const aspectLockInput = document.getElementById('maintain-aspect'); const aspectLockInput = document.getElementById('maintain-aspect');
const speedInput = document.getElementById('asset-speed'); const speedInput = document.getElementById('asset-speed');
const muteInput = document.getElementById('asset-muted');
const speedLabel = document.getElementById('asset-speed-label'); 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 selectedZLabel = document.getElementById('asset-z-level');
const playbackSection = document.getElementById('playback-section'); const playbackSection = document.getElementById('playback-section');
const volumeSection = document.getElementById('volume-section');
const audioSection = document.getElementById('audio-section'); const audioSection = document.getElementById('audio-section');
const layoutSection = document.getElementById('layout-section'); const layoutSection = document.getElementById('layout-section');
const audioLoopInput = document.getElementById('asset-audio-loop'); 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 audioSpeedInput = document.getElementById('asset-audio-speed');
const audioSpeedLabel = document.getElementById('asset-audio-speed-label'); const audioSpeedLabel = document.getElementById('asset-audio-speed-label');
const audioPitchInput = document.getElementById('asset-audio-pitch'); const audioPitchInput = document.getElementById('asset-audio-pitch');
const audioVolumeInput = document.getElementById('asset-audio-volume');
const controlsPlaceholder = document.getElementById('asset-controls-placeholder'); const controlsPlaceholder = document.getElementById('asset-controls-placeholder');
const fileNameLabel = document.getElementById('asset-file-name'); const fileNameLabel = document.getElementById('asset-file-name');
const assetInspector = document.getElementById('asset-inspector'); const assetInspector = document.getElementById('asset-inspector');
@@ -167,6 +172,38 @@ function setAudioSpeedLabel(percentValue) {
audioSpeedLabel.textContent = `${formatted}x`; 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) { function queueAudioForUnlock(controller) {
if (!controller) return; if (!controller) return;
pendingAudioUnlock.add(controller); pendingAudioUnlock.add(controller);
@@ -185,12 +222,11 @@ if (widthInput) widthInput.addEventListener('change', () => commitSizeChange());
if (heightInput) heightInput.addEventListener('input', () => handleSizeInputChange('height')); if (heightInput) heightInput.addEventListener('input', () => handleSizeInputChange('height'));
if (heightInput) heightInput.addEventListener('change', () => commitSizeChange()); if (heightInput) heightInput.addEventListener('change', () => commitSizeChange());
if (speedInput) speedInput.addEventListener('input', updatePlaybackFromInputs); 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 (audioLoopInput) audioLoopInput.addEventListener('change', updateAudioSettingsFromInputs);
if (audioDelayInput) audioDelayInput.addEventListener('change', updateAudioSettingsFromInputs); if (audioDelayInput) audioDelayInput.addEventListener('change', updateAudioSettingsFromInputs);
if (audioSpeedInput) audioSpeedInput.addEventListener('change', updateAudioSettingsFromInputs); if (audioSpeedInput) audioSpeedInput.addEventListener('change', updateAudioSettingsFromInputs);
if (audioPitchInput) audioPitchInput.addEventListener('change', updateAudioSettingsFromInputs); if (audioPitchInput) audioPitchInput.addEventListener('change', updateAudioSettingsFromInputs);
if (audioVolumeInput) audioVolumeInput.addEventListener('change', updateAudioSettingsFromInputs);
if (selectedVisibilityBtn) { if (selectedVisibilityBtn) {
selectedVisibilityBtn.addEventListener('click', () => { selectedVisibilityBtn.addEventListener('click', () => {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
@@ -796,7 +832,7 @@ function applyAudioSettings(controller, asset, resetPosition = false) {
const speed = Math.max(0.25, asset.audioSpeed || 1); const speed = Math.max(0.25, asset.audioSpeed || 1);
const pitch = Math.max(0.5, asset.audioPitch || 1); const pitch = Math.max(0.5, asset.audioPitch || 1);
controller.element.playbackRate = speed * pitch; 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; controller.element.volume = volume;
if (resetPosition) { if (resetPosition) {
controller.element.currentTime = 0; controller.element.currentTime = 0;
@@ -871,7 +907,9 @@ function ensureMedia(asset) {
element.crossOrigin = 'anonymous'; element.crossOrigin = 'anonymous';
if (isVideoElement(element)) { if (isVideoElement(element)) {
element.loop = true; 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.playsInline = true;
element.autoplay = false; element.autoplay = false;
element.preload = 'metadata'; element.preload = 'metadata';
@@ -972,26 +1010,19 @@ function applyMediaSettings(element, asset) {
} }
const nextSpeed = asset.speed ?? 1; const nextSpeed = asset.speed ?? 1;
const effectiveSpeed = Math.max(nextSpeed, 0.01); const effectiveSpeed = Math.max(nextSpeed, 0.01);
const wasMuted = element.muted;
if (element.playbackRate !== effectiveSpeed) { if (element.playbackRate !== effectiveSpeed) {
element.playbackRate = effectiveSpeed; element.playbackRate = effectiveSpeed;
} }
const shouldMute = asset.muted ?? true; const volume = clamp(asset.audioVolume ?? 1, 0, MAX_VOLUME);
if (element.muted !== shouldMute) { element.muted = volume === 0;
element.muted = shouldMute; element.volume = Math.min(volume, 1);
}
if (nextSpeed === 0) { if (nextSpeed === 0) {
element.pause(); element.pause();
return; return;
} }
const playPromise = element.play(); const playPromise = element.play();
if (playPromise?.catch) { if (playPromise?.catch) {
playPromise.catch(() => { playPromise.catch(() => { });
if (!shouldMute && wasMuted) {
element.muted = true;
element.play().catch(() => { });
}
});
} }
} }
@@ -1426,15 +1457,24 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
playbackSection.classList.toggle('hidden', !shouldShowPlayback); playbackSection.classList.toggle('hidden', !shouldShowPlayback);
speedInput?.classList?.toggle('disabled', !shouldShowPlayback); speedInput?.classList?.toggle('disabled', !shouldShowPlayback);
} }
if (muteInput) { if (volumeSection) {
muteInput.checked = !!asset.muted; const showVolume = isAudioAsset(asset) || isVideoAsset(asset);
muteInput.disabled = !isVideoAsset(asset); volumeSection.classList.toggle('hidden', !showVolume);
muteInput.parentElement?.classList.toggle('disabled', !isVideoAsset(asset)); 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) { 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]; const audioInputs = [audioLoopInput, audioDelayInput, audioSpeedInput, audioPitchInput];
audioInputs.forEach((input) => { audioInputs.forEach((input) => {
if (!input) return; if (!input) return;
input.disabled = !showAudio; input.disabled = !showAudio;
@@ -1446,7 +1486,6 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
audioSpeedInput.value = Math.round(Math.max(0.25, asset.audioSpeed ?? 1) * 100); audioSpeedInput.value = Math.round(Math.max(0.25, asset.audioSpeed ?? 1) * 100);
setAudioSpeedLabel(audioSpeedInput.value); setAudioSpeedLabel(audioSpeedInput.value);
audioPitchInput.value = Math.round(Math.max(0.5, asset.audioPitch ?? 1) * 100); 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(); drawAndList();
} }
function updateMuteFromInput() { function updateVolumeFromInput() {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
if (!asset || !isVideoAsset(asset)) return; if (!asset || !(isVideoAsset(asset) || isAudioAsset(asset))) return;
asset.muted = !!muteInput?.checked; const sliderValue = Math.max(0, Math.min(VOLUME_SLIDER_MAX, parseFloat(volumeInput?.value) || 100));
updateRenderState(asset); const volumeValue = sliderToVolume(sliderValue);
persistTransform(asset); setVolumeLabel(sliderValue);
asset.audioVolume = volumeValue;
const media = mediaCache.get(asset.id); const media = mediaCache.get(asset.id);
if (media) { if (media) {
applyMediaSettings(media, asset); applyMediaSettings(media, asset);
} }
if (isAudioAsset(asset)) {
const controller = ensureAudioController(asset);
applyAudioSettings(controller, asset);
}
persistTransform(asset);
drawAndList(); drawAndList();
} }
@@ -1566,7 +1611,6 @@ function updateAudioSettingsFromInputs() {
setAudioSpeedLabel(nextAudioSpeedPercent); setAudioSpeedLabel(nextAudioSpeedPercent);
asset.audioSpeed = Math.max(0.25, (nextAudioSpeedPercent / 100)); asset.audioSpeed = Math.max(0.25, (nextAudioSpeedPercent / 100));
asset.audioPitch = Math.max(0.5, (parseInt(audioPitchInput?.value || '100', 10) / 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); const controller = ensureAudioController(asset);
applyAudioSettings(controller, asset); applyAudioSettings(controller, asset);
persistTransform(asset); persistTransform(asset);
@@ -1876,7 +1920,6 @@ function persistTransform(asset, silent = false) {
height: asset.height, height: asset.height,
rotation: asset.rotation, rotation: asset.rotation,
speed: asset.speed, speed: asset.speed,
muted: asset.muted,
zIndex: asset.zIndex, zIndex: asset.zIndex,
audioLoop: asset.audioLoop, audioLoop: asset.audioLoop,
audioDelayMillis: asset.audioDelayMillis, audioDelayMillis: asset.audioDelayMillis,

View File

@@ -387,8 +387,20 @@ function applyAudioElementSettings(element, asset) {
const speed = Math.max(0.25, asset.audioSpeed || 1); const speed = Math.max(0.25, asset.audioSpeed || 1);
const pitch = Math.max(0.5, asset.audioPitch || 1); const pitch = Math.max(0.5, asset.audioPitch || 1);
element.playbackRate = speed * pitch; element.playbackRate = speed * pitch;
const volume = Math.max(0, Math.min(1, asset.audioVolume ?? 1)); const volume = Math.max(0, Math.min(2, asset.audioVolume ?? 1));
element.volume = volume; 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) { function handleAudioEnded(assetId) {
@@ -510,7 +522,7 @@ function ensureMedia(asset) {
element.crossOrigin = 'anonymous'; element.crossOrigin = 'anonymous';
if (isVideoElement(element)) { if (isVideoElement(element)) {
element.loop = true; element.loop = true;
element.muted = asset.muted ?? true; applyMediaVolume(element, asset);
element.playsInline = true; element.playsInline = true;
element.autoplay = true; element.autoplay = true;
element.onloadeddata = draw; element.onloadeddata = draw;
@@ -618,7 +630,7 @@ function applyVideoSource(element, objectUrl, asset) {
if (playback === 0) { if (playback === 0) {
element.pause(); element.pause();
} else { } else {
element.play().catch(() => {}); element.play().catch(() => queueAudioForUnlock({ element }));
} }
} }
@@ -670,25 +682,16 @@ function applyMediaSettings(element, asset) {
} }
const nextSpeed = asset.speed ?? 1; const nextSpeed = asset.speed ?? 1;
const effectiveSpeed = Math.max(nextSpeed, 0.01); const effectiveSpeed = Math.max(nextSpeed, 0.01);
const wasMuted = element.muted;
if (element.playbackRate !== effectiveSpeed) { if (element.playbackRate !== effectiveSpeed) {
element.playbackRate = effectiveSpeed; element.playbackRate = effectiveSpeed;
} }
const shouldMute = asset.muted ?? true; applyMediaVolume(element, asset);
if (element.muted !== shouldMute) {
element.muted = shouldMute;
}
if (nextSpeed === 0) { if (nextSpeed === 0) {
element.pause(); element.pause();
} else { } else {
const playPromise = element.play(); const playPromise = element.play();
if (playPromise?.catch) { if (playPromise?.catch) {
playPromise.catch(() => { playPromise.catch(() => queueAudioForUnlock({ element }));
if (!shouldMute && wasMuted) {
element.muted = true;
element.play().catch(() => {});
}
});
} }
} }
} }

View File

@@ -28,8 +28,7 @@
<iframe th:src="${'https://player.twitch.tv/?channel=' + broadcaster + '&parent=localhost'}" allowfullscreen></iframe> <iframe th:src="${'https://player.twitch.tv/?channel=' + broadcaster + '&parent=localhost'}" allowfullscreen></iframe>
<canvas id="admin-canvas"></canvas> <canvas id="admin-canvas"></canvas>
</section> </section>
<section class="controls assets-panel"> <section class="controls-full panel">
<div class="controls-full panel">
<h3>Overlay assets</h3> <h3>Overlay assets</h3>
<p>Upload overlay visuals and adjust them inline.</p> <p>Upload overlay visuals and adjust them inline.</p>
<div class="asset-management"> <div class="asset-management">
@@ -105,7 +104,6 @@
<div class="panel-section" id="playback-section"> <div class="panel-section" id="playback-section">
<div class="section-header"> <div class="section-header">
<h5>Playback</h5> <h5>Playback</h5>
<p class="field-note">Video-only controls.</p>
</div> </div>
<div class="stacked-field"> <div class="stacked-field">
<div class="label-row"> <div class="label-row">
@@ -115,14 +113,19 @@
<input id="asset-speed" class="range-input" type="range" min="0" max="1000" step="10" value="100" /> <input id="asset-speed" class="range-input" type="range" min="0" max="1000" step="10" value="100" />
<div class="range-meta"><span>0%</span><span>1000%</span></div> <div class="range-meta"><span>0%</span><span>1000%</span></div>
</div> </div>
<div class="control-grid condensed split-row"> </div>
<label class="checkbox-inline toggle inline-toggle">
<input id="asset-muted" type="checkbox" /> <div class="panel-section" id="volume-section">
<span class="toggle-track" aria-hidden="true"> <div class="section-header">
<span class="toggle-thumb"></span> <h5>Volume</h5>
</span> </div>
<span class="toggle-label">Mute video</span> <div class="stacked-field">
</label> <div class="label-row">
<span>Playback volume</span>
<span class="value-hint" id="asset-volume-label">100%</span>
</div>
<input id="asset-volume" class="range-input" type="range" min="0" max="200" step="1" value="100" />
<div class="range-meta"><span>0%</span><span>200%</span></div>
</div> </div>
</div> </div>
@@ -157,11 +160,7 @@
Pitch (%) Pitch (%)
<input id="asset-audio-pitch" class="range-input" type="range" min="50" max="200" step="5" value="100" /> <input id="asset-audio-pitch" class="range-input" type="range" min="50" max="200" step="5" value="100" />
</label> </label>
<label> <div></div>
Volume (%)
<input id="asset-audio-volume" class="range-input" type="range" min="0" max="100" step="1" value="100" />
</label>
</div>
</div> </div>
</div> </div>
</div> </div>