From c1a1f4a6009ac22ac7c8fe2296aae66960805345 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 21 Mar 2026 15:19:28 +0300 Subject: [PATCH] pinakes-ui: use system player for HLS streams Signed-off-by: NotAShelf Change-Id: I0255e79e25cde100a063476de2c8fe0d6a6a6964 --- .../pinakes-ui/src/components/media_player.rs | 125 +++++++----------- 1 file changed, 48 insertions(+), 77 deletions(-) diff --git a/crates/pinakes-ui/src/components/media_player.rs b/crates/pinakes-ui/src/components/media_player.rs index 5c84fca..7bc76d5 100644 --- a/crates/pinakes-ui/src/components/media_player.rs +++ b/crates/pinakes-ui/src/components/media_player.rs @@ -1,7 +1,4 @@ -use dioxus::{ - document::{Script, eval}, - prelude::*, -}; +use dioxus::{document::eval, prelude::*}; use super::utils::format_duration; @@ -126,41 +123,30 @@ impl PlayQueue { } } -/// Generate JavaScript to initialize hls.js for an HLS stream URL. +/// Open a URL in the system's default media player. /// -/// Destroys any previously created instance stored at `window.__hlsInstances` -/// keyed by element ID before creating a new one, preventing memory leaks and -/// multiple instances competing for the same video element across re-renders. -fn hls_init_script(video_id: &str, hls_url: &str) -> String { - // JSON-encode both values so embedded quotes/backslashes cannot break out of - // the JS string. - let encoded_url = - serde_json::to_string(hls_url).unwrap_or_else(|_| "\"\"".to_string()); - let encoded_id = - serde_json::to_string(video_id).unwrap_or_else(|_| "\"\"".to_string()); - format!( - r#" - (function() {{ - window.__hlsInstances = window.__hlsInstances || {{}}; - var existing = window.__hlsInstances[{encoded_id}]; - if (existing) {{ - existing.destroy(); - window.__hlsInstances[{encoded_id}] = null; - }} - if (typeof Hls !== 'undefined' && Hls.isSupported()) {{ - var hls = new Hls(); - hls.loadSource({encoded_url}); - hls.attachMedia(document.getElementById({encoded_id})); - window.__hlsInstances[{encoded_id}] = hls; - }} else {{ - var video = document.getElementById({encoded_id}); - if (video && video.canPlayType('application/vnd.apple.mpegurl')) {{ - video.src = {encoded_url}; - }} - }} - }})(); - "# - ) +/// Uses `xdg-open` on Linux, `open` on macOS, and `cmd /c start` on Windows. +/// The call is fire-and-forget; failures are logged as warnings. +fn open_in_system_player(url: &str) { + #[cfg(target_os = "linux")] + let result = std::process::Command::new("xdg-open").arg(url).spawn(); + #[cfg(target_os = "macos")] + let result = std::process::Command::new("open").arg(url).spawn(); + #[cfg(target_os = "windows")] + let result = std::process::Command::new("cmd") + .args(["/c", "start", "", url]) + .spawn(); + #[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "windows" + )))] + let result: std::io::Result = + Err(std::io::Error::other("unsupported platform")); + + if let Err(e) = result { + tracing::warn!("failed to open system player: {e}"); + } } #[component] @@ -179,6 +165,10 @@ pub fn MediaPlayer( let mut muted = use_signal(|| false); let is_video = media_type == "video"; + // HLS adaptive streams (.m3u8) cannot be decoded natively by webkit2gtk on + // Linux. Detect them and show an external-player button instead of attempting + // in-process playback via a JavaScript library. + let is_hls = src.ends_with(".m3u8") || src.contains("/stream/hls/"); let is_playing = *playing.read(); let cur_time = *current_time.read(); let dur = *duration.read(); @@ -240,35 +230,6 @@ pub fn MediaPlayer( }); }); - // HLS initialization for .m3u8 streams. - // use_effect must be called unconditionally to maintain stable hook ordering. - let is_hls = src.ends_with(".m3u8"); - let hls_src = src.clone(); - use_effect(move || { - if !hls_src.ends_with(".m3u8") { - return; - } - let js = hls_init_script("pinakes-player", &hls_src); - spawn(async move { - // Poll until hls.js is loaded rather than using a fixed delay, so we - // initialize as soon as the script is ready without timing out on slow - // connections. Max wait: 25 * 100ms = 2.5s. - const MAX_POLLS: u32 = 25; - for _ in 0..MAX_POLLS { - if let Ok(val) = eval("typeof Hls !== 'undefined'").await { - if val == serde_json::Value::Bool(true) { - let _ = eval(&js).await; - return; - } - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - tracing::warn!( - "hls.js did not load within 2.5s; HLS stream will not play" - ); - }); - }); - // Autoplay on mount if autoplay { let src_auto = src.clone(); @@ -435,32 +396,41 @@ pub fn MediaPlayer( let play_icon = if is_playing { "\u{23f8}" } else { "\u{25b6}" }; let mute_icon = if is_muted { "\u{1f507}" } else { "\u{1f50a}" }; - rsx! { - // Load hls.js for HLS stream support when needed - if is_hls { - // Pin to a specific release so unexpected upstream changes cannot - // break playback. Update this when intentionally upgrading hls.js. - Script { src: "https://cdn.jsdelivr.net/npm/hls.js@1.5.15/dist/hls.min.js" } - } + let open_src = src.clone(); + rsx! { div { class: if is_video { "media-player media-player-video" } else { "media-player media-player-audio" }, tabindex: "0", onkeydown: on_keydown, - // Hidden native element; for HLS streams skip the src attr (hls.js attaches it) + // HLS adaptive streams cannot be decoded natively in the desktop WebView. + // Show a prompt to open in the system's default media player instead. + if is_hls { + div { class: "player-hls-notice", + p { class: "text-muted", + "Adaptive (HLS) streams require an external media player." + } + button { + class: "btn btn-primary", + onclick: move |_| open_in_system_player(&open_src), + "Open in system player" + } + } + } else { + if is_video { video { id: "pinakes-player", class: "player-native-video", - src: if is_hls { String::new() } else { src.clone() }, + src: src.clone(), preload: "metadata", } } else { audio { id: "pinakes-player", class: "player-native-audio", - src: if is_hls { String::new() } else { src.clone() }, + src: src.clone(), preload: "metadata", } } @@ -521,6 +491,7 @@ pub fn MediaPlayer( } } } + } // end else (non-HLS) } } }