fix(theme): unlock unmuted media autoplay without user interaction on macos

This commit is contained in:
HorizonCode committed 2026-11-01 13:57:47 +01:00
1 parent ce572de0d2
commit a025f9b285
5 files changed
+89 -1

No files matched your search

+14
View File
@@ -3055,6 +3055,7 @@ pub async fn reveal_window(window: tauri::WebviewWindow) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
let res = crate::macos::activate_and_focus_window(&win);
let _ = crate::macos::unlock_media_autoplay(&win);
let _ = tx.send(res);
}
@@ -3066,3 +3067,16 @@ pub async fn reveal_window(window: tauri::WebviewWindow) -> Result<(), String> {
rx.await
.map_err(|_| "Main thread channel closed".to_string())?
}
#[tauri::command]
pub async fn unlock_media_autoplay(window: tauri::WebviewWindow) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
crate::macos::unlock_media_autoplay(&window)?;
}
#[cfg(not(target_os = "macos"))]
{
let _ = window;
}
Ok(())
}
+8 -1
View File
@@ -33,7 +33,7 @@ use crate::{
resolve_osu_folder, restore_theme_backup, reveal_window, run_open_tablet_driver, run_osu, run_osu_updater,
save_theme_project, set_logging_enabled, set_osu_config_values, set_osu_user_config_values,
stop_open_tablet_driver, theme_asset_base, theme_editor_context, theme_preview_url,
valid_osu_folder,
unlock_media_autoplay, valid_osu_folder,
},
state::AppState,
};
@@ -136,6 +136,9 @@ pub fn run() {
if let Err(error) = macos::allow_media_autoplay(&window) {
log::warn!(target: "launcher", "{error}");
}
if let Err(error) = macos::unlock_media_autoplay(&window) {
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}");
}
@@ -216,6 +219,7 @@ pub fn run() {
set_logging_enabled,
open_logs_folder,
reveal_window,
unlock_media_autoplay,
logging::list_log_bundles,
logging::read_log_bundle
])
@@ -238,6 +242,9 @@ pub fn run() {
if let Err(error) = macos::allow_media_autoplay(&window) {
log::warn!(target: "launcher", "{error}");
}
if let Err(error) = macos::unlock_media_autoplay(&window) {
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}");
}
+64
View File
@@ -197,6 +197,70 @@ pub fn allow_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String>
.map_err(|error| format!("Could not enable theme media autoplay: {error}"))
}
/// Programmatically unlock media autoplay with unmuted sound using WebKit's
/// user gesture SPI and media resumption APIs.
pub fn unlock_media_autoplay(window: &tauri::WebviewWindow) -> Result<(), String> {
use objc2::msg_send;
use objc2::runtime::AnyObject;
use std::ffi::CString;
window
.with_webview(|webview| unsafe {
let webview = webview.inner() as *mut AnyObject;
if webview.is_null() {
return;
}
// 1. Resume all media playback
let sel_resume = objc2::sel!(_resumeAllMediaPlayback);
if msg_send![webview, respondsToSelector: sel_resume] {
let _: () = msg_send![webview, _resumeAllMediaPlayback];
} else {
let sel_resume_pub = objc2::sel!(resumeAllMediaPlayback:);
if msg_send![webview, respondsToSelector: sel_resume_pub] {
let _: () = msg_send![
webview,
resumeAllMediaPlayback: std::ptr::null_mut::<AnyObject>()
];
}
}
// 2. Evaluate unmuted media play script with explicit user gesture SPI
let sel_eval = objc2::sel!(
_evaluateJavaScript:withSourceURL:inFrame:inContentWorld:withUserGesture:completionHandler:
);
if msg_send![webview, respondsToSelector: sel_eval] {
let script_str = match CString::new(
"document.querySelectorAll('video, audio').forEach(function(el) { \
el.muted = false; \
var p = el.play(); \
if (p !== undefined) p.catch(function() {}); \
});",
) {
Ok(s) => s,
Err(_) => return,
};
let ns_script: *mut AnyObject =
msg_send![objc2::class!(NSString), stringWithUTF8String: script_str.as_ptr()];
let page_world: *mut AnyObject =
msg_send![objc2::class!(WKContentWorld), pageWorld];
let _: () = msg_send![
webview,
_evaluateJavaScript: ns_script,
withSourceURL: std::ptr::null_mut::<AnyObject>(),
inFrame: std::ptr::null_mut::<AnyObject>(),
inContentWorld: page_world,
withUserGesture: true,
completionHandler: std::ptr::null_mut::<AnyObject>()
];
log::debug!(target: "launcher", "Unlocked theme media autoplay with user gesture SPI");
}
})
.map_err(|error| format!("Could not unlock theme media autoplay: {error}"))
}
/// Show a native open panel that accepts `.app` bundles as well as folders.
///
/// AppKit treats application bundles as packages (directories) by default, so a
+1
View File
@@ -248,6 +248,7 @@ export const playThemeMedia = (node: HTMLMediaElement) => {
.catch((error) => {
console.warn('Theme media autoplay was blocked', error);
armAutoplayRetry(node);
void invoke('unlock_media_autoplay').catch(() => {});
});
}
};
+2
View File
@@ -449,6 +449,8 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum
media.volume = volume;
playThemeMedia(media);
});
void invoke('unlock_media_autoplay').catch(() => {});
};
export const deleteTheme = async (themeToUninstall: Theme) => {