fix(theme): stream video conversion progress and auto-play v1 theme media
This commit is contained in:
1 parent
62b31169cc
commit
7332618e85
6 files changed
+274
-35
No files matched your search
+85
-6
@@ -66,7 +66,11 @@ const ensureStatusListener = async () => {
|
||||
} else if (stage === 'extract') {
|
||||
themeMediaStatus.set('Installing video engine…');
|
||||
} else if (stage === 'optimize') {
|
||||
themeMediaStatus.set('Converting theme video for hardware playback…');
|
||||
if (progress > 0) {
|
||||
themeMediaStatus.set(`Converting theme video… ${Math.round(progress * 100)}%`);
|
||||
} else {
|
||||
themeMediaStatus.set('Converting theme video…');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -74,6 +78,14 @@ const ensureStatusListener = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const initThemeMediaStatusListener = () => {
|
||||
void ensureStatusListener();
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
void ensureStatusListener();
|
||||
}
|
||||
|
||||
const blobUrlPromises = new Map<string, Promise<string>>();
|
||||
const optimizedUrlPromises = new Map<string, Promise<string>>();
|
||||
const objectUrls = new Set<string>();
|
||||
@@ -108,7 +120,7 @@ const optimizedThemeMediaUrl = async (source: string, video: boolean): Promise<s
|
||||
}
|
||||
|
||||
setPreparing(1);
|
||||
themeMediaStatus.set('Preparing theme video for smooth playback…');
|
||||
themeMediaStatus.set('Converting theme video…');
|
||||
await ensureStatusListener();
|
||||
try {
|
||||
const optimized = await invoke<string>('optimize_theme_media', {
|
||||
@@ -181,6 +193,32 @@ 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;
|
||||
@@ -189,15 +227,25 @@ const armAutoplayRetry = (node: HTMLMediaElement) => {
|
||||
const retry = () => {
|
||||
document.removeEventListener('pointerdown', retry, true);
|
||||
document.removeEventListener('keydown', retry, true);
|
||||
document.removeEventListener('click', retry, true);
|
||||
autoplayArmed.delete(node);
|
||||
if (node.isConnected) void node.play().catch(() => {});
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', retry, true);
|
||||
document.addEventListener('keydown', retry, true);
|
||||
document.addEventListener('click', retry, true);
|
||||
};
|
||||
|
||||
export const playThemeMedia = (node: HTMLMediaElement) => {
|
||||
if (node instanceof HTMLVideoElement) {
|
||||
node.playsInline = true;
|
||||
node.setAttribute('playsinline', '');
|
||||
node.setAttribute('webkit-playsinline', '');
|
||||
node.controls = false;
|
||||
node.removeAttribute('controls');
|
||||
}
|
||||
|
||||
void node
|
||||
.play()
|
||||
.then(() => {
|
||||
@@ -210,7 +258,25 @@ export const playThemeMedia = (node: HTMLMediaElement) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Theme media autoplay was blocked', error);
|
||||
armAutoplayRetry(node);
|
||||
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 {
|
||||
armAutoplayRetry(node);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -570,11 +636,24 @@ export function smoothThemeMedia(node: HTMLElement) {
|
||||
const videoLifecycles = new WeakMap<HTMLVideoElement, () => void>();
|
||||
|
||||
const prepare = (media: HTMLMediaElement) => {
|
||||
if (media instanceof HTMLVideoElement && !videoLifecycles.has(media)) {
|
||||
const lifecycle = themeVideoLifecycle(media);
|
||||
videoLifecycles.set(media, () => lifecycle.destroy());
|
||||
if (media instanceof HTMLVideoElement) {
|
||||
media.playsInline = true;
|
||||
media.setAttribute('playsinline', '');
|
||||
media.setAttribute('webkit-playsinline', '');
|
||||
media.controls = false;
|
||||
media.removeAttribute('controls');
|
||||
|
||||
if (!videoLifecycles.has(media)) {
|
||||
const lifecycle = themeVideoLifecycle(media);
|
||||
videoLifecycles.set(media, () => lifecycle.destroy());
|
||||
}
|
||||
} else {
|
||||
media.controls = false;
|
||||
media.removeAttribute('controls');
|
||||
}
|
||||
|
||||
playThemeMedia(media);
|
||||
|
||||
const source =
|
||||
media.getAttribute('src') ??
|
||||
media.querySelector<HTMLSourceElement>('source[src]')?.getAttribute('src') ??
|
||||
|
||||
+6
-1
@@ -1,5 +1,5 @@
|
||||
import { active_custom_theme, custom_theme_audio_playing, custom_themes } from './global';
|
||||
import { clearThemeMediaCache } from './themeMedia';
|
||||
import { clearThemeMediaCache, playThemeMedia } from './themeMedia';
|
||||
import { isThemeV2, normalizeThemeV2, type ThemeV2Asset, type ThemeV2Definition } from './themeV2';
|
||||
import { disableMediaControls, setGlobalVolume } from './utils';
|
||||
import { betterFetch } from '@better-fetch/fetch';
|
||||
@@ -442,6 +442,11 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum
|
||||
}
|
||||
setGlobalVolume(volume);
|
||||
disableMediaControls();
|
||||
|
||||
// Ensure all media elements mounted by v1 themes start playback automatically
|
||||
themeContainer.querySelectorAll<HTMLMediaElement>('video, audio').forEach((media) => {
|
||||
playThemeMedia(media);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteTheme = async (themeToUninstall: Theme) => {
|
||||
|
||||
Reference in new issue
Block a user