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
+266
-27
No files matched your search
+100
-21
@@ -1788,17 +1788,20 @@ pub async fn optimize_theme_media(app: AppHandle, source_url: String) -> Result<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = app.emit(
|
let original = source_url.clone();
|
||||||
|
let app_handle = app.clone();
|
||||||
|
let on_progress = move |progress: f64| {
|
||||||
|
let _ = app_handle.emit(
|
||||||
"theme-media-progress",
|
"theme-media-progress",
|
||||||
ThemeMediaProgress {
|
ThemeMediaProgress {
|
||||||
stage: "optimize".to_string(),
|
stage: "optimize".to_string(),
|
||||||
progress: 0.0,
|
progress,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let original = source_url.clone();
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
optimize_theme_media_blocking(&ffmpeg, &source, &original, &cache_name, &destination)
|
optimize_theme_media_blocking(&ffmpeg, &source, &original, &cache_name, &destination, on_progress)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|error| format!("Theme media optimizer thread failed: {error}"))?
|
.map_err(|error| format!("Theme media optimizer thread failed: {error}"))?
|
||||||
@@ -2059,13 +2062,82 @@ fn find_media_binary(name: &str) -> Option<PathBuf> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn optimize_theme_media_blocking(
|
fn run_ffmpeg_with_progress<F>(
|
||||||
|
mut command: std::process::Command,
|
||||||
|
duration_us: Option<u64>,
|
||||||
|
on_progress: &F,
|
||||||
|
) -> Result<(bool, String), String>
|
||||||
|
where
|
||||||
|
F: Fn(f64) + Send + Sync,
|
||||||
|
{
|
||||||
|
use std::io::{BufRead, BufReader, Read};
|
||||||
|
|
||||||
|
command.args(["-progress", "pipe:1"]);
|
||||||
|
command.stdout(std::process::Stdio::piped());
|
||||||
|
command.stderr(std::process::Stdio::piped());
|
||||||
|
|
||||||
|
let mut child = command
|
||||||
|
.spawn()
|
||||||
|
.map_err(|error| format!("Could not spawn ffmpeg: {error}"))?;
|
||||||
|
|
||||||
|
let stdout = child.stdout.take();
|
||||||
|
let stderr = child.stderr.take();
|
||||||
|
|
||||||
|
let stderr_thread = std::thread::spawn(move || {
|
||||||
|
let mut err_str = String::new();
|
||||||
|
if let Some(mut err) = stderr {
|
||||||
|
let _ = err.read_to_string(&mut err_str);
|
||||||
|
}
|
||||||
|
err_str
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(out) = stdout {
|
||||||
|
let reader = BufReader::new(out);
|
||||||
|
let mut last_progress = 0.0;
|
||||||
|
let mut last_emit = std::time::Instant::now();
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
let Ok(line) = line else { break };
|
||||||
|
if let Some(val_str) = line.strip_prefix("out_time_us=") {
|
||||||
|
if let Ok(us) = val_str.trim().parse::<u64>()
|
||||||
|
&& let Some(total_us) = duration_us
|
||||||
|
&& total_us > 0
|
||||||
|
{
|
||||||
|
let progress = (us as f64 / total_us as f64).clamp(0.0, 0.99);
|
||||||
|
if (progress - last_progress) >= 0.01
|
||||||
|
|| last_emit.elapsed() >= std::time::Duration::from_millis(250)
|
||||||
|
{
|
||||||
|
last_progress = progress;
|
||||||
|
last_emit = std::time::Instant::now();
|
||||||
|
on_progress(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if line.starts_with("progress=end") {
|
||||||
|
on_progress(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.map_err(|error| format!("Failed waiting for ffmpeg: {error}"))?;
|
||||||
|
let stderr_str = stderr_thread.join().unwrap_or_default();
|
||||||
|
|
||||||
|
Ok((status.success(), stderr_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn optimize_theme_media_blocking<F>(
|
||||||
ffmpeg: &Path,
|
ffmpeg: &Path,
|
||||||
source: &Path,
|
source: &Path,
|
||||||
source_url: &str,
|
source_url: &str,
|
||||||
cache_name: &str,
|
cache_name: &str,
|
||||||
destination: &Path,
|
destination: &Path,
|
||||||
) -> Result<String, String> {
|
on_progress: F,
|
||||||
|
) -> Result<String, String>
|
||||||
|
where
|
||||||
|
F: Fn(f64) + Send + Sync,
|
||||||
|
{
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
let probe = Command::new(ffmpeg)
|
let probe = Command::new(ffmpeg)
|
||||||
@@ -2089,6 +2161,17 @@ fn optimize_theme_media_blocking(
|
|||||||
return Ok(source_url.to_string());
|
return Ok(source_url.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let duration_us = probe_text.lines().find_map(|line| {
|
||||||
|
let index = line.find("Duration: ")?;
|
||||||
|
let time_str = line[index + 10..].split(',').next()?.trim();
|
||||||
|
let mut parts = time_str.split(':');
|
||||||
|
let hours: f64 = parts.next()?.parse().ok()?;
|
||||||
|
let minutes: f64 = parts.next()?.parse().ok()?;
|
||||||
|
let seconds: f64 = parts.next()?.parse().ok()?;
|
||||||
|
let total_secs = hours * 3600.0 + minutes * 60.0 + seconds;
|
||||||
|
Some((total_secs * 1_000_000.0) as u64)
|
||||||
|
});
|
||||||
|
|
||||||
let temporary = destination.with_extension("mp4.tmp");
|
let temporary = destination.with_extension("mp4.tmp");
|
||||||
let _ = std::fs::remove_file(&temporary);
|
let _ = std::fs::remove_file(&temporary);
|
||||||
|
|
||||||
@@ -2098,6 +2181,7 @@ fn optimize_theme_media_blocking(
|
|||||||
source.display()
|
source.display()
|
||||||
);
|
);
|
||||||
let started = std::time::Instant::now();
|
let started = std::time::Instant::now();
|
||||||
|
on_progress(0.0);
|
||||||
|
|
||||||
let mut command = Command::new(ffmpeg);
|
let mut command = Command::new(ffmpeg);
|
||||||
command.args(["-hide_banner", "-loglevel", "error", "-nostdin", "-y", "-i"]);
|
command.args(["-hide_banner", "-loglevel", "error", "-nostdin", "-y", "-i"]);
|
||||||
@@ -2131,18 +2215,14 @@ fn optimize_theme_media_blocking(
|
|||||||
"mp4",
|
"mp4",
|
||||||
]);
|
]);
|
||||||
command.arg(&temporary);
|
command.arg(&temporary);
|
||||||
let first = command.output();
|
|
||||||
|
|
||||||
let success = match first {
|
let success = match run_ffmpeg_with_progress(command, duration_us, &on_progress) {
|
||||||
Ok(output) if output.status.success() => true,
|
Ok((true, _)) => true,
|
||||||
Ok(output) => {
|
Ok((false, stderr)) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
target: "launcher",
|
target: "launcher",
|
||||||
"Hardware theme video encode failed: {}",
|
"Hardware theme video encode failed: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
stderr.chars().take(500).collect::<String>()
|
||||||
.chars()
|
|
||||||
.take(500)
|
|
||||||
.collect::<String>()
|
|
||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -2186,16 +2266,14 @@ fn optimize_theme_media_blocking(
|
|||||||
"mp4",
|
"mp4",
|
||||||
]);
|
]);
|
||||||
fallback.arg(&temporary);
|
fallback.arg(&temporary);
|
||||||
match fallback.output() {
|
|
||||||
Ok(output) if output.status.success() => true,
|
match run_ffmpeg_with_progress(fallback, duration_us, &on_progress) {
|
||||||
Ok(output) => {
|
Ok((true, _)) => true,
|
||||||
|
Ok((false, stderr)) => {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
target: "launcher",
|
target: "launcher",
|
||||||
"Software theme video encode failed: {}",
|
"Software theme video encode failed: {}",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
stderr.chars().take(500).collect::<String>()
|
||||||
.chars()
|
|
||||||
.take(500)
|
|
||||||
.collect::<String>()
|
|
||||||
);
|
);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -2213,6 +2291,7 @@ fn optimize_theme_media_blocking(
|
|||||||
|
|
||||||
std::fs::rename(&temporary, destination)
|
std::fs::rename(&temporary, destination)
|
||||||
.map_err(|error| format!("Could not store the optimized theme video: {error}"))?;
|
.map_err(|error| format!("Could not store the optimized theme video: {error}"))?;
|
||||||
|
on_progress(1.0);
|
||||||
log::info!(
|
log::info!(
|
||||||
target: "launcher",
|
target: "launcher",
|
||||||
"Optimized theme video in {:.1}s: {}",
|
"Optimized theme video in {:.1}s: {}",
|
||||||
|
|||||||
+61
-1
@@ -106,7 +106,9 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String>
|
|||||||
let configuration: *mut AnyObject = msg_send![webview, configuration];
|
let configuration: *mut AnyObject = msg_send![webview, configuration];
|
||||||
if configuration.is_null() {
|
if configuration.is_null() {
|
||||||
return;
|
return;
|
||||||
} // WKAudiovisualMediaTypesNone. This is a public WKWebView setting.
|
}
|
||||||
|
|
||||||
|
// WKAudiovisualMediaTypesNone. This is a public WKWebView setting.
|
||||||
let _: () =
|
let _: () =
|
||||||
msg_send![configuration, setMediaTypesRequiringUserActionForPlayback: 0usize];
|
msg_send![configuration, setMediaTypesRequiringUserActionForPlayback: 0usize];
|
||||||
let current: usize = msg_send![configuration, mediaTypesRequiringUserActionForPlayback];
|
let current: usize = msg_send![configuration, mediaTypesRequiringUserActionForPlayback];
|
||||||
@@ -114,6 +116,64 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String>
|
|||||||
target: "launcher",
|
target: "launcher",
|
||||||
"Theme media autoplay policy: {current}"
|
"Theme media autoplay policy: {current}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Configure webpage preferences for unconditional media autoplay
|
||||||
|
let default_preferences: *mut AnyObject = msg_send![configuration, defaultWebpagePreferences];
|
||||||
|
if !default_preferences.is_null() {
|
||||||
|
let sel = objc2::sel!(_setAutoplayPolicy:);
|
||||||
|
let responds: bool = msg_send![default_preferences, respondsToSelector: sel];
|
||||||
|
if responds {
|
||||||
|
// 1 = _WKWebsiteAutoplayPolicyAllow (allow autoplay unconditionally)
|
||||||
|
let _: () = msg_send![default_preferences, _setAutoplayPolicy: 1isize];
|
||||||
|
log::debug!(target: "launcher", "Set WKWebpagePreferences._setAutoplayPolicy to Allow (1)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure WKPreferences
|
||||||
|
let preferences: *mut AnyObject = msg_send![configuration, preferences];
|
||||||
|
if !preferences.is_null() {
|
||||||
|
let sel_audio = objc2::sel!(_setRequiresUserGestureForAudioPlayback:);
|
||||||
|
let responds: bool = msg_send![preferences, respondsToSelector: sel_audio];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![preferences, _setRequiresUserGestureForAudioPlayback: false];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel_video = objc2::sel!(_setRequiresUserGestureForVideoPlayback:);
|
||||||
|
let responds: bool = msg_send![preferences, respondsToSelector: sel_video];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![preferences, _setRequiresUserGestureForVideoPlayback: false];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel_inline = objc2::sel!(_setAllowsInlineMediaPlayback:);
|
||||||
|
let responds: bool = msg_send![preferences, respondsToSelector: sel_inline];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![preferences, _setAllowsInlineMediaPlayback: true];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel_inline_req = objc2::sel!(_setInlineMediaPlaybackRequiresPlaysInlineAttribute:);
|
||||||
|
let responds: bool = msg_send![preferences, respondsToSelector: sel_inline_req];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![preferences, _setInlineMediaPlaybackRequiresPlaysInlineAttribute: false];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel_invis = objc2::sel!(_setInvisibleMediaAutoplayNotPermitted:);
|
||||||
|
let responds: bool = msg_send![preferences, respondsToSelector: sel_invis];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![preferences, _setInvisibleMediaAutoplayNotPermitted: false];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel_media_load = objc2::sel!(_setMediaDataLoadsAutomatically:);
|
||||||
|
let responds: bool = msg_send![configuration, respondsToSelector: sel_media_load];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![configuration, _setMediaDataLoadsAutomatically: true];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel_audio_action = objc2::sel!(_setRequiresUserActionForAudioPlayback:);
|
||||||
|
let responds: bool = msg_send![configuration, respondsToSelector: sel_audio_action];
|
||||||
|
if responds {
|
||||||
|
let _: () = msg_send![configuration, _setRequiresUserActionForAudioPlayback: false];
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.map_err(|error| format!("Could not enable theme media autoplay: {error}"))
|
.map_err(|error| format!("Could not enable theme media autoplay: {error}"))
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -172,4 +172,17 @@
|
|||||||
[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 */
|
||||||
|
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 {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+82
-3
@@ -66,7 +66,11 @@ const ensureStatusListener = async () => {
|
|||||||
} else if (stage === 'extract') {
|
} else if (stage === 'extract') {
|
||||||
themeMediaStatus.set('Installing video engine…');
|
themeMediaStatus.set('Installing video engine…');
|
||||||
} else if (stage === 'optimize') {
|
} 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) {
|
} 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 blobUrlPromises = new Map<string, Promise<string>>();
|
||||||
const optimizedUrlPromises = new Map<string, Promise<string>>();
|
const optimizedUrlPromises = new Map<string, Promise<string>>();
|
||||||
const objectUrls = new Set<string>();
|
const objectUrls = new Set<string>();
|
||||||
@@ -108,7 +120,7 @@ const optimizedThemeMediaUrl = async (source: string, video: boolean): Promise<s
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPreparing(1);
|
setPreparing(1);
|
||||||
themeMediaStatus.set('Preparing theme video for smooth playback…');
|
themeMediaStatus.set('Converting theme video…');
|
||||||
await ensureStatusListener();
|
await ensureStatusListener();
|
||||||
try {
|
try {
|
||||||
const optimized = await invoke<string>('optimize_theme_media', {
|
const optimized = await invoke<string>('optimize_theme_media', {
|
||||||
@@ -181,6 +193,32 @@ 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;
|
||||||
@@ -189,15 +227,25 @@ const armAutoplayRetry = (node: HTMLMediaElement) => {
|
|||||||
const retry = () => {
|
const retry = () => {
|
||||||
document.removeEventListener('pointerdown', retry, true);
|
document.removeEventListener('pointerdown', retry, true);
|
||||||
document.removeEventListener('keydown', retry, true);
|
document.removeEventListener('keydown', retry, true);
|
||||||
|
document.removeEventListener('click', retry, true);
|
||||||
autoplayArmed.delete(node);
|
autoplayArmed.delete(node);
|
||||||
if (node.isConnected) void node.play().catch(() => {});
|
if (node.isConnected) void node.play().catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('pointerdown', retry, true);
|
document.addEventListener('pointerdown', retry, true);
|
||||||
document.addEventListener('keydown', retry, true);
|
document.addEventListener('keydown', retry, true);
|
||||||
|
document.addEventListener('click', retry, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const playThemeMedia = (node: HTMLMediaElement) => {
|
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
|
void node
|
||||||
.play()
|
.play()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -210,8 +258,26 @@ 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -570,10 +636,23 @@ export function smoothThemeMedia(node: HTMLElement) {
|
|||||||
const videoLifecycles = new WeakMap<HTMLVideoElement, () => void>();
|
const videoLifecycles = new WeakMap<HTMLVideoElement, () => void>();
|
||||||
|
|
||||||
const prepare = (media: HTMLMediaElement) => {
|
const prepare = (media: HTMLMediaElement) => {
|
||||||
if (media instanceof HTMLVideoElement && !videoLifecycles.has(media)) {
|
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);
|
const lifecycle = themeVideoLifecycle(media);
|
||||||
videoLifecycles.set(media, () => lifecycle.destroy());
|
videoLifecycles.set(media, () => lifecycle.destroy());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
media.controls = false;
|
||||||
|
media.removeAttribute('controls');
|
||||||
|
}
|
||||||
|
|
||||||
|
playThemeMedia(media);
|
||||||
|
|
||||||
const source =
|
const source =
|
||||||
media.getAttribute('src') ??
|
media.getAttribute('src') ??
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,5 @@
|
|||||||
import { active_custom_theme, custom_theme_audio_playing, custom_themes } from './global';
|
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 { isThemeV2, normalizeThemeV2, type ThemeV2Asset, type ThemeV2Definition } from './themeV2';
|
||||||
import { disableMediaControls, setGlobalVolume } from './utils';
|
import { disableMediaControls, setGlobalVolume } from './utils';
|
||||||
import { betterFetch } from '@better-fetch/fetch';
|
import { betterFetch } from '@better-fetch/fetch';
|
||||||
@@ -442,6 +442,11 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum
|
|||||||
}
|
}
|
||||||
setGlobalVolume(volume);
|
setGlobalVolume(volume);
|
||||||
disableMediaControls();
|
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) => {
|
export const deleteTheme = async (themeToUninstall: Theme) => {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
import { exit, getLauncherVersion, getPlatform, setLoggingEnabled } from '$lib/osuUtil';
|
import { exit, getLauncherVersion, getPlatform, setLoggingEnabled } from '$lib/osuUtil';
|
||||||
import * as presence from '$lib/presence';
|
import * as presence from '$lib/presence';
|
||||||
import {
|
import {
|
||||||
|
initThemeMediaStatusListener,
|
||||||
smoothThemeMedia,
|
smoothThemeMedia,
|
||||||
themeMediaPreparing,
|
themeMediaPreparing,
|
||||||
themeMediaStatus,
|
themeMediaStatus,
|
||||||
@@ -148,6 +149,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
initThemeMediaStatusListener();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ver = await getLauncherVersion();
|
const ver = await getLauncherVersion();
|
||||||
if (ver) launcherVersion.set(ver);
|
if (ver) launcherVersion.set(ver);
|
||||||
@@ -528,7 +531,7 @@
|
|||||||
<div
|
<div
|
||||||
class="pointer-events-none fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full border border-white/10 bg-black/75 px-4 py-2 text-xs font-medium text-white/80 shadow-xl backdrop-blur animate-pulse"
|
class="pointer-events-none fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full border border-white/10 bg-black/75 px-4 py-2 text-xs font-medium text-white/80 shadow-xl backdrop-blur animate-pulse"
|
||||||
>
|
>
|
||||||
{$themeMediaStatus || 'Preparing theme video for smooth playback…'}
|
{$themeMediaStatus || 'Converting theme video…'}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
Reference in new issue
Block a user