pinakes-ui: use system player for HLS streams
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I0255e79e25cde100a063476de2c8fe0d6a6a6964
This commit is contained in:
parent
bac79a2c08
commit
c1a1f4a600
1 changed files with 48 additions and 77 deletions
|
|
@ -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<std::process::Child> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue