fix(theme): keep video unmuted with persistent volume knob on launch
This commit is contained in:
1 parent
e861cda23d
commit
ce572de0d2
9 files changed
+105
-83
No files matched your search
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
+12
-8
@@ -172,17 +172,21 @@
|
|||||||
[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,
|
||||||
video::-webkit-media-controls-enclosure,
|
video::-webkit-media-controls-enclosure,
|
||||||
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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in new issue
Block a user