fix(theme): keep video unmuted with persistent volume knob on launch

This commit is contained in:
HorizonCode committed 2026-11-01 13:35:54 +01:00
1 parent e861cda23d
commit ce572de0d2
9 files changed
+99 -77

No files matched your search

+6
View File
@@ -136,6 +136,9 @@ pub fn run() {
if let Err(error) = macos::allow_media_autoplay(&window) { if let Err(error) = macos::allow_media_autoplay(&window) {
log::warn!(target: "launcher", "{error}"); log::warn!(target: "launcher", "{error}");
} }
if let Err(error) = macos::activate_and_focus_window(&window) {
log::warn!(target: "launcher", "Could not focus launcher window on startup: {error}");
}
} }
Ok(()) Ok(())
@@ -235,6 +238,9 @@ pub fn run() {
if let Err(error) = macos::allow_media_autoplay(&window) { if let Err(error) = macos::allow_media_autoplay(&window) {
log::warn!(target: "launcher", "{error}"); log::warn!(target: "launcher", "{error}");
} }
if let Err(error) = macos::activate_and_focus_window(&window) {
log::warn!(target: "launcher", "Could not focus launcher window on ready: {error}");
}
} }
presence::handle_run_event(app, &event); presence::handle_run_event(app, &event);
+20 -1
View File
@@ -96,6 +96,14 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String>
use objc2::msg_send; use objc2::msg_send;
use objc2::runtime::AnyObject; use objc2::runtime::AnyObject;
unsafe extern "C" {
fn object_getInstanceVariable(
obj: *mut AnyObject,
name: *const std::ffi::c_char,
out_value: *mut *mut std::ffi::c_void,
) -> *const std::ffi::c_void;
}
window window
.with_webview(|webview| unsafe { .with_webview(|webview| unsafe {
let webview = webview.inner() as *mut AnyObject; let webview = webview.inner() as *mut AnyObject;
@@ -103,7 +111,18 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String>
return; return;
} }
let configuration: *mut AnyObject = msg_send![webview, configuration]; // -[WKWebView configuration] returns a copy. To affect live playback,
// access the internal `_configuration` ivar directly when available.
let mut raw_config: *mut std::ffi::c_void = std::ptr::null_mut();
let ivar_name = c"_configuration";
let ivar = object_getInstanceVariable(webview, ivar_name.as_ptr(), &mut raw_config);
let configuration: *mut AnyObject = if !ivar.is_null() && !raw_config.is_null() {
log::debug!(target: "launcher", "Accessing live WKWebView._configuration ivar");
raw_config as *mut AnyObject
} else {
msg_send![webview, configuration]
};
if configuration.is_null() { if configuration.is_null() {
return; return;
} }
+2 -1
View File
@@ -21,7 +21,8 @@
"decorations": false, "decorations": false,
"resizable": false, "resizable": false,
"maximizable": false, "maximizable": false,
"shadow": true "shadow": true,
"focus": true
} }
], ],
"withGlobalTauri": true, "withGlobalTauri": true,
+6 -2
View File
@@ -172,6 +172,7 @@
[role="button"]:not([disabled]) { [role="button"]:not([disabled]) {
cursor: pointer; cursor: pointer;
} }
}
/* Suppress browser and WebKit native media controls and overlay play buttons on theme videos */ /* Suppress browser and WebKit native media controls and overlay play buttons on theme videos */
video::-webkit-media-controls, video::-webkit-media-controls,
@@ -179,10 +180,13 @@
video::-webkit-media-controls-panel, video::-webkit-media-controls-panel,
video::-webkit-media-controls-play-button, video::-webkit-media-controls-play-button,
video::-webkit-media-controls-start-playback-button, video::-webkit-media-controls-start-playback-button,
video::-webkit-media-controls-overlay-play-button { video::-webkit-media-controls-overlay-play-button,
video::-webkit-media-controls-overlay-enclosure {
display: none !important; display: none !important;
-webkit-appearance: none !important; -webkit-appearance: none !important;
opacity: 0 !important; opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important; pointer-events: none !important;
} width: 0 !important;
height: 0 !important;
} }
@@ -27,8 +27,7 @@
</script> </script>
<div data-tauri-drag-region class="titlebar z-[100]" class:macos-window={$platform === 'macos'}> <div data-tauri-drag-region class="titlebar z-[100]" class:macos-window={$platform === 'macos'}>
{#if $custom_theme_audio_playing} {#if $custom_theme_audio_playing || ($active_custom_theme && $active_custom_theme.name !== 'Default')}
{#if $custom_theme_audio_playing}
<div class="relative flex items-center justify-center group"> <div class="relative flex items-center justify-center group">
<div class="relative flex items-center justify-center p-1 cursor-pointer"> <div class="relative flex items-center justify-center p-1 cursor-pointer">
<div <div
@@ -78,7 +77,6 @@
</div> </div>
</div> </div>
{/if} {/if}
{/if}
<div class="relative flex items-center justify-center p-1 group" id="titlebar-minimize"> <div class="relative flex items-center justify-center p-1 group" id="titlebar-minimize">
<div <div
class="absolute inset-0 bg-background rounded class="absolute inset-0 bg-background rounded
+36 -66
View File
@@ -15,7 +15,8 @@
*/ */
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event'; 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; type MediaCleanup = () => void;
@@ -193,48 +194,29 @@ export const clearThemeMediaCache = () => {
objectUrls.clear(); 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. */ /** Retry playback on the first click or key press after an autoplay rejection. */
const armAutoplayRetry = (node: HTMLMediaElement) => { const armAutoplayRetry = (node: HTMLMediaElement) => {
if (autoplayArmed.has(node)) return; if (autoplayArmed.has(node)) return;
autoplayArmed.add(node); autoplayArmed.add(node);
const retry = () => { const retry = () => {
document.removeEventListener('pointerdown', retry, true); window.removeEventListener('pointerdown', retry, true);
document.removeEventListener('keydown', retry, true); window.removeEventListener('keydown', retry, true);
document.removeEventListener('click', retry, true); window.removeEventListener('click', retry, true);
window.removeEventListener('focus', retry, true);
autoplayArmed.delete(node); 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); window.addEventListener('pointerdown', retry, true);
document.addEventListener('keydown', retry, true); window.addEventListener('keydown', retry, true);
document.addEventListener('click', retry, true); window.addEventListener('click', retry, true);
window.addEventListener('focus', retry, true);
}; };
export const playThemeMedia = (node: HTMLMediaElement) => { export const playThemeMedia = (node: HTMLMediaElement) => {
@@ -246,8 +228,15 @@ export const playThemeMedia = (node: HTMLMediaElement) => {
node.removeAttribute('controls'); node.removeAttribute('controls');
} }
void node node.muted = false;
.play() const currentVolume = get(custom_theme_volume);
if (node.volume === 0 && currentVolume > 0) {
node.volume = currentVolume;
}
const playPromise = node.play();
if (playPromise !== undefined) {
playPromise
.then(() => { .then(() => {
console.debug('Theme media playing', { console.debug('Theme media playing', {
element: node.tagName, element: node.tagName,
@@ -258,26 +247,9 @@ export const playThemeMedia = (node: HTMLMediaElement) => {
}) })
.catch((error) => { .catch((error) => {
console.warn('Theme media autoplay was blocked', 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); armAutoplayRetry(node);
}); });
} else {
armAutoplayRetry(node);
} }
});
}; };
/** /**
@@ -291,13 +263,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => {
mediaCleanups.delete(node); mediaCleanups.delete(node);
const previousTime = Number.isFinite(node.currentTime) ? node.currentTime : 0; const previousTime = Number.isFinite(node.currentTime) ? node.currentTime : 0;
const wasPlaying = !node.paused; const wasPlaying = !node.paused || node.autoplay;
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();
void themeMediaUrl(source, node instanceof HTMLVideoElement).then((url) => { void themeMediaUrl(source, node instanceof HTMLVideoElement).then((url) => {
if (mediaTokens.get(node) !== token || !node.isConnected) return; 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 = () => { const onError = () => {
@@ -336,7 +302,7 @@ const loadMediaSource = (node: HTMLMediaElement, source: string) => {
if (node.src !== source) { if (node.src !== source) {
node.src = source; node.src = source;
node.load(); node.load();
if (wasPlaying || node.autoplay) playThemeMedia(node); playThemeMedia(node);
} }
}; };
@@ -652,14 +618,18 @@ export function smoothThemeMedia(node: HTMLElement) {
media.removeAttribute('controls'); media.removeAttribute('controls');
} }
playThemeMedia(media);
const source = const source =
media.getAttribute('src') ?? media.getAttribute('src') ??
media.querySelector<HTMLSourceElement>('source[src]')?.getAttribute('src') ?? media.querySelector<HTMLSourceElement>('source[src]')?.getAttribute('src') ??
undefined; undefined;
if (!source) return; if (!source) {
if (!isThemeAssetUrl(source) && !loopbackThemePattern.test(source)) return; playThemeMedia(media);
return;
}
if (!isThemeAssetUrl(source) && !loopbackThemePattern.test(source)) {
playThemeMedia(media);
return;
}
if (lastSource.get(media) === source) return; if (lastSource.get(media) === source) return;
lastSource.set(media, source); lastSource.set(media, source);
+2
View File
@@ -445,6 +445,8 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum
// Ensure all media elements mounted by v1 themes start playback automatically // Ensure all media elements mounted by v1 themes start playback automatically
themeContainer.querySelectorAll<HTMLMediaElement>('video, audio').forEach((media) => { themeContainer.querySelectorAll<HTMLMediaElement>('video, audio').forEach((media) => {
media.muted = false;
media.volume = volume;
playThemeMedia(media); playThemeMedia(media);
}); });
}; };
+3 -1
View File
@@ -267,12 +267,13 @@ export function trackMediaContainer(node: HTMLElement) {
function update() { function update() {
custom_theme_audio_playing.set( 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) { function register(el: HTMLMediaElement) {
media.add(el); media.add(el);
update();
const handler = () => update(); const handler = () => update();
el.addEventListener('play', handler); el.addEventListener('play', handler);
@@ -280,6 +281,7 @@ export function trackMediaContainer(node: HTMLElement) {
return () => { return () => {
media.delete(el); media.delete(el);
update();
el.removeEventListener('play', handler); el.removeEventListener('play', handler);
el.removeEventListener('pause', handler); el.removeEventListener('pause', handler);
}; };
+23 -3
View File
@@ -277,9 +277,12 @@
const last_theme = localThemes.find((t) => t.name === lastThemeName); const last_theme = localThemes.find((t) => t.name === lastThemeName);
const canLoadLastTheme = Boolean(last_theme && isThemeTrusted(last_theme)); const canLoadLastTheme = Boolean(last_theme && isThemeTrusted(last_theme));
const initialVolume = await config_theme_volume.get(0.15);
custom_theme_volume.set(initialVolume);
if (!last_theme || !canLoadLastTheme) { if (!last_theme || !canLoadLastTheme) {
await config.value<string>('theme').set('Default'); await config.value<string>('theme').set('Default');
loadTheme(defaultTheme, $custom_theme_container!, await config_theme_volume.get(0.15)); loadTheme(defaultTheme, $custom_theme_container!, initialVolume);
if (last_theme && !canLoadLastTheme) { if (last_theme && !canLoadLastTheme) {
sileo.error({ sileo.error({
title: 'Theme needs approval', title: 'Theme needs approval',
@@ -288,10 +291,10 @@
}); });
} }
} else { } else {
loadTheme(last_theme, $custom_theme_container!, await config_theme_volume.get(0.15)); loadTheme(last_theme, $custom_theme_container!, initialVolume);
} }
custom_theme_volume.set(await config_theme_volume.get(0.15)); void invoke('reveal_window').catch(() => {});
patch.set(await config_patching.get(true)); patch.set(await config_patching.get(true));
customCursor.set(await config_custom_cursor.get(true)); customCursor.set(await config_custom_cursor.get(true));
cursorSmoothening.set(await config_cursor_smoothening.get(true)); cursorSmoothening.set(await config_cursor_smoothening.get(true));
@@ -381,6 +384,23 @@
</script> </script>
<svelte:head> <svelte:head>
<style>
video::-webkit-media-controls,
video::-webkit-media-controls-enclosure,
video::-webkit-media-controls-panel,
video::-webkit-media-controls-play-button,
video::-webkit-media-controls-start-playback-button,
video::-webkit-media-controls-overlay-play-button,
video::-webkit-media-controls-overlay-enclosure {
display: none !important;
-webkit-appearance: none !important;
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
width: 0 !important;
height: 0 !important;
}
</style>
<!-- eslint-disable-next-line svelte/no-at-html-tags --> <!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html `<style>${$active_custom_theme?.cssStyle || ''}</style>`} {@html `<style>${$active_custom_theme?.cssStyle || ''}</style>`}
{#if $active_custom_theme?.definition} {#if $active_custom_theme?.definition}