diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9f8c076..ae44b68 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 39707c8..9e496e1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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}"); } diff --git a/src-tauri/src/macos.rs b/src-tauri/src/macos.rs index a657b85..b018b5f 100644 --- a/src-tauri/src/macos.rs +++ b/src-tauri/src/macos.rs @@ -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::() + ]; + } + } + + // 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::(), + inFrame: std::ptr::null_mut::(), + inContentWorld: page_world, + withUserGesture: true, + completionHandler: std::ptr::null_mut::() + ]; + 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 diff --git a/src/lib/themeMedia.ts b/src/lib/themeMedia.ts index f10ddec..4185357 100644 --- a/src/lib/themeMedia.ts +++ b/src/lib/themeMedia.ts @@ -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(() => {}); }); } }; diff --git a/src/lib/themes.ts b/src/lib/themes.ts index 15186da..ad1ddef 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -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) => {