fix(theme): keep video unmuted with persistent volume knob on launch
This commit is contained in:
1 parent
e861cda23d
commit
ce572de0d2
9 files changed
+164
-142
No files matched your search
@@ -27,57 +27,55 @@
|
||||
</script>
|
||||
|
||||
<div data-tauri-drag-region class="titlebar z-[100]" class:macos-window={$platform === 'macos'}>
|
||||
{#if $custom_theme_audio_playing}
|
||||
{#if $custom_theme_audio_playing}
|
||||
<div class="relative flex items-center justify-center group">
|
||||
<div class="relative flex items-center justify-center p-1 cursor-pointer">
|
||||
<div
|
||||
class="absolute inset-0 bg-background rounded
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}"
|
||||
></div>
|
||||
{#if $custom_theme_audio_playing || ($active_custom_theme && $active_custom_theme.name !== 'Default')}
|
||||
<div class="relative flex items-center justify-center group">
|
||||
<div class="relative flex items-center justify-center p-1 cursor-pointer">
|
||||
<div
|
||||
class="absolute inset-0 bg-background rounded
|
||||
scale-80 opacity-0
|
||||
group-hover:opacity-100
|
||||
group-hover:scale-100
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}"
|
||||
></div>
|
||||
|
||||
{#if $custom_theme_volume > 0.7}
|
||||
<ThemeIcon name="volume" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{:else if $custom_theme_volume >= 0.15}
|
||||
<ThemeIcon name="volume-low" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{:else if $custom_theme_volume > 0}
|
||||
<ThemeIcon name="volume-low" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{:else}
|
||||
<ThemeIcon name="volume-off" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if $custom_theme_volume > 0.7}
|
||||
<ThemeIcon name="volume" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{:else if $custom_theme_volume >= 0.15}
|
||||
<ThemeIcon name="volume-low" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{:else if $custom_theme_volume > 0}
|
||||
<ThemeIcon name="volume-low" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{:else}
|
||||
<ThemeIcon name="volume-off" set={iconSet} class="z-10 pointer-events-none" size={14} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute top-full right-0 translate-x-[7px]
|
||||
opacity-0 invisible pointer-events-none -translate-y-2
|
||||
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto group-hover:translate-y-0
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
z-50"
|
||||
>
|
||||
<div class="h-2 w-full"></div>
|
||||
|
||||
<div
|
||||
class="absolute top-full right-0 translate-x-[7px]
|
||||
opacity-0 invisible pointer-events-none -translate-y-2
|
||||
group-hover:opacity-100 group-hover:visible group-hover:pointer-events-auto group-hover:translate-y-0
|
||||
{$reduceAnimations ? '' : 'transition-all duration-200 ease-out'}
|
||||
z-50"
|
||||
class="flex items-center justify-center h-[120px] w-[35px]
|
||||
bg-black/20 backdrop-blur-sm border border-white/10 rounded-xl"
|
||||
>
|
||||
<div class="h-2 w-full"></div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-center h-[120px] w-[35px]
|
||||
bg-black/20 backdrop-blur-sm border border-white/10 rounded-xl"
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
bind:value={$custom_theme_volume}
|
||||
oninput={() => {
|
||||
config.value<number>('volume').set($custom_theme_volume);
|
||||
}}
|
||||
class="volume-slider"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
bind:value={$custom_theme_volume}
|
||||
oninput={() => {
|
||||
config.value<number>('volume').set($custom_theme_volume);
|
||||
}}
|
||||
class="volume-slider"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="relative flex items-center justify-center p-1 group" id="titlebar-minimize">
|
||||
<div
|
||||
|
||||
+48
-78
@@ -15,7 +15,8 @@
|
||||
*/
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { writable } from 'svelte/store';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { custom_theme_volume } from './global';
|
||||
|
||||
type MediaCleanup = () => void;
|
||||
|
||||
@@ -193,48 +194,29 @@ export const clearThemeMediaCache = () => {
|
||||
objectUrls.clear();
|
||||
};
|
||||
|
||||
const unmutingArmed = new WeakSet<HTMLMediaElement>();
|
||||
|
||||
/** Restore full sound as soon as the user interacts with the launcher. */
|
||||
const armUnmuteOnInteraction = (node: HTMLMediaElement, targetVolume: number) => {
|
||||
if (unmutingArmed.has(node)) return;
|
||||
unmutingArmed.add(node);
|
||||
|
||||
const unmute = () => {
|
||||
window.removeEventListener('pointerdown', unmute, true);
|
||||
window.removeEventListener('keydown', unmute, true);
|
||||
window.removeEventListener('click', unmute, true);
|
||||
unmutingArmed.delete(node);
|
||||
if (node.isConnected) {
|
||||
node.muted = false;
|
||||
node.volume = targetVolume;
|
||||
if (node.paused) {
|
||||
void node.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('pointerdown', unmute, true);
|
||||
window.addEventListener('keydown', unmute, true);
|
||||
window.addEventListener('click', unmute, true);
|
||||
};
|
||||
|
||||
/** Retry playback on the first click or key press after an autoplay rejection. */
|
||||
const armAutoplayRetry = (node: HTMLMediaElement) => {
|
||||
if (autoplayArmed.has(node)) return;
|
||||
autoplayArmed.add(node);
|
||||
|
||||
const retry = () => {
|
||||
document.removeEventListener('pointerdown', retry, true);
|
||||
document.removeEventListener('keydown', retry, true);
|
||||
document.removeEventListener('click', retry, true);
|
||||
window.removeEventListener('pointerdown', retry, true);
|
||||
window.removeEventListener('keydown', retry, true);
|
||||
window.removeEventListener('click', retry, true);
|
||||
window.removeEventListener('focus', retry, true);
|
||||
autoplayArmed.delete(node);
|
||||
if (node.isConnected) void node.play().catch(() => {});
|
||||
if (node.isConnected) {
|
||||
node.muted = false;
|
||||
const targetVolume = get(custom_theme_volume);
|
||||
if (targetVolume > 0) node.volume = targetVolume;
|
||||
void node.play().catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', retry, true);
|
||||
document.addEventListener('keydown', retry, true);
|
||||
document.addEventListener('click', retry, true);
|
||||
window.addEventListener('pointerdown', retry, true);
|
||||
window.addEventListener('keydown', retry, true);
|
||||
window.addEventListener('click', retry, true);
|
||||
window.addEventListener('focus', retry, true);
|
||||
};
|
||||
|
||||
export const playThemeMedia = (node: HTMLMediaElement) => {
|
||||
@@ -246,38 +228,28 @@ export const playThemeMedia = (node: HTMLMediaElement) => {
|
||||
node.removeAttribute('controls');
|
||||
}
|
||||
|
||||
void node
|
||||
.play()
|
||||
.then(() => {
|
||||
console.debug('Theme media playing', {
|
||||
element: node.tagName,
|
||||
muted: node.muted,
|
||||
volume: node.volume,
|
||||
source: node.currentSrc,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Theme media autoplay was blocked', error);
|
||||
if (node instanceof HTMLVideoElement && !node.muted) {
|
||||
// Fallback: Start muted immediately so the background video plays
|
||||
// automatically without freezing or showing native play buttons,
|
||||
// and unmute on the first user interaction.
|
||||
const targetVolume = node.volume > 0 ? node.volume : 0.15;
|
||||
node.muted = true;
|
||||
void node
|
||||
.play()
|
||||
.then(() => {
|
||||
console.debug('Theme video started playing muted after autoplay block');
|
||||
armUnmuteOnInteraction(node, targetVolume);
|
||||
})
|
||||
.catch((mutedError) => {
|
||||
console.warn('Theme video muted autoplay was also blocked', mutedError);
|
||||
armAutoplayRetry(node);
|
||||
});
|
||||
} else {
|
||||
node.muted = false;
|
||||
const currentVolume = get(custom_theme_volume);
|
||||
if (node.volume === 0 && currentVolume > 0) {
|
||||
node.volume = currentVolume;
|
||||
}
|
||||
|
||||
const playPromise = node.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(() => {
|
||||
console.debug('Theme media playing', {
|
||||
element: node.tagName,
|
||||
muted: node.muted,
|
||||
volume: node.volume,
|
||||
source: node.currentSrc,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Theme media autoplay was blocked', error);
|
||||
armAutoplayRetry(node);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -291,13 +263,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => {
|
||||
mediaCleanups.delete(node);
|
||||
|
||||
const previousTime = Number.isFinite(node.currentTime) ? node.currentTime : 0;
|
||||
const wasPlaying = !node.paused;
|
||||
const needsPreparation =
|
||||
node instanceof HTMLVideoElement && isMac && loopbackThemePattern.test(source);
|
||||
|
||||
// Keep the last frame visible while the first H.264 encode runs instead of
|
||||
// letting WebKit struggle through the software VP9 track.
|
||||
if (needsPreparation) node.pause();
|
||||
const wasPlaying = !node.paused || node.autoplay;
|
||||
|
||||
void themeMediaUrl(source, node instanceof HTMLVideoElement).then((url) => {
|
||||
if (mediaTokens.get(node) !== token || !node.isConnected) return;
|
||||
@@ -324,7 +290,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (wasPlaying || node.autoplay) playThemeMedia(node);
|
||||
playThemeMedia(node);
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
@@ -336,7 +302,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => {
|
||||
if (node.src !== source) {
|
||||
node.src = source;
|
||||
node.load();
|
||||
if (wasPlaying || node.autoplay) playThemeMedia(node);
|
||||
playThemeMedia(node);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -652,14 +618,18 @@ export function smoothThemeMedia(node: HTMLElement) {
|
||||
media.removeAttribute('controls');
|
||||
}
|
||||
|
||||
playThemeMedia(media);
|
||||
|
||||
const source =
|
||||
media.getAttribute('src') ??
|
||||
media.querySelector<HTMLSourceElement>('source[src]')?.getAttribute('src') ??
|
||||
undefined;
|
||||
if (!source) return;
|
||||
if (!isThemeAssetUrl(source) && !loopbackThemePattern.test(source)) return;
|
||||
if (!source) {
|
||||
playThemeMedia(media);
|
||||
return;
|
||||
}
|
||||
if (!isThemeAssetUrl(source) && !loopbackThemePattern.test(source)) {
|
||||
playThemeMedia(media);
|
||||
return;
|
||||
}
|
||||
if (lastSource.get(media) === source) return;
|
||||
|
||||
lastSource.set(media, source);
|
||||
|
||||
@@ -445,6 +445,8 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum
|
||||
|
||||
// Ensure all media elements mounted by v1 themes start playback automatically
|
||||
themeContainer.querySelectorAll<HTMLMediaElement>('video, audio').forEach((media) => {
|
||||
media.muted = false;
|
||||
media.volume = volume;
|
||||
playThemeMedia(media);
|
||||
});
|
||||
};
|
||||
|
||||
+3
-1
@@ -267,12 +267,13 @@ export function trackMediaContainer(node: HTMLElement) {
|
||||
|
||||
function update() {
|
||||
custom_theme_audio_playing.set(
|
||||
Array.from(media).some((el) => !el.paused && !(el instanceof HTMLVideoElement && el.muted))
|
||||
Array.from(media).some((el) => !el.paused)
|
||||
);
|
||||
}
|
||||
|
||||
function register(el: HTMLMediaElement) {
|
||||
media.add(el);
|
||||
update();
|
||||
|
||||
const handler = () => update();
|
||||
el.addEventListener('play', handler);
|
||||
@@ -280,6 +281,7 @@ export function trackMediaContainer(node: HTMLElement) {
|
||||
|
||||
return () => {
|
||||
media.delete(el);
|
||||
update();
|
||||
el.removeEventListener('play', handler);
|
||||
el.removeEventListener('pause', handler);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user