fix(theme): unlock unmuted media autoplay without user interaction on macos
This commit is contained in:
1 parent
ce572de0d2
commit
a025f9b285
5 files changed
+89
-1
No files matched your search
@@ -3055,6 +3055,7 @@ pub async fn reveal_window(window: tauri::WebviewWindow) -> Result<(), String> {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
let res = crate::macos::activate_and_focus_window(&win);
|
let res = crate::macos::activate_and_focus_window(&win);
|
||||||
|
let _ = crate::macos::unlock_media_autoplay(&win);
|
||||||
let _ = tx.send(res);
|
let _ = tx.send(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3066,3 +3067,16 @@ pub async fn reveal_window(window: tauri::WebviewWindow) -> Result<(), String> {
|
|||||||
rx.await
|
rx.await
|
||||||
.map_err(|_| "Main thread channel closed".to_string())?
|
.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(())
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ use crate::{
|
|||||||
resolve_osu_folder, restore_theme_backup, reveal_window, run_open_tablet_driver, run_osu, run_osu_updater,
|
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,
|
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,
|
stop_open_tablet_driver, theme_asset_base, theme_editor_context, theme_preview_url,
|
||||||
valid_osu_folder,
|
unlock_media_autoplay, valid_osu_folder,
|
||||||
},
|
},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
@@ -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::unlock_media_autoplay(&window) {
|
||||||
|
log::warn!(target: "launcher", "{error}");
|
||||||
|
}
|
||||||
if let Err(error) = macos::activate_and_focus_window(&window) {
|
if let Err(error) = macos::activate_and_focus_window(&window) {
|
||||||
log::warn!(target: "launcher", "Could not focus launcher window on startup: {error}");
|
log::warn!(target: "launcher", "Could not focus launcher window on startup: {error}");
|
||||||
}
|
}
|
||||||
@@ -216,6 +219,7 @@ pub fn run() {
|
|||||||
set_logging_enabled,
|
set_logging_enabled,
|
||||||
open_logs_folder,
|
open_logs_folder,
|
||||||
reveal_window,
|
reveal_window,
|
||||||
|
unlock_media_autoplay,
|
||||||
logging::list_log_bundles,
|
logging::list_log_bundles,
|
||||||
logging::read_log_bundle
|
logging::read_log_bundle
|
||||||
])
|
])
|
||||||
@@ -238,6 +242,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::unlock_media_autoplay(&window) {
|
||||||
|
log::warn!(target: "launcher", "{error}");
|
||||||
|
}
|
||||||
if let Err(error) = macos::activate_and_focus_window(&window) {
|
if let Err(error) = macos::activate_and_focus_window(&window) {
|
||||||
log::warn!(target: "launcher", "Could not focus launcher window on ready: {error}");
|
log::warn!(target: "launcher", "Could not focus launcher window on ready: {error}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"))
|
.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.
|
/// Show a native open panel that accepts `.app` bundles as well as folders.
|
||||||
///
|
///
|
||||||
/// AppKit treats application bundles as packages (directories) by default, so a
|
/// AppKit treats application bundles as packages (directories) by default, so a
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ 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);
|
||||||
armAutoplayRetry(node);
|
armAutoplayRetry(node);
|
||||||
|
void invoke('unlock_media_autoplay').catch(() => {});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -449,6 +449,8 @@ export const loadTheme = async (theme: Theme, themeContainer: HTMLElement, volum
|
|||||||
media.volume = volume;
|
media.volume = volume;
|
||||||
playThemeMedia(media);
|
playThemeMedia(media);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void invoke('unlock_media_autoplay').catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTheme = async (themeToUninstall: Theme) => {
|
export const deleteTheme = async (themeToUninstall: Theme) => {
|
||||||
|
|||||||
Reference in new issue
Block a user