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::{
|
use dioxus::{document::eval, prelude::*};
|
||||||
document::{Script, eval},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::utils::format_duration;
|
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`
|
/// Uses `xdg-open` on Linux, `open` on macOS, and `cmd /c start` on Windows.
|
||||||
/// keyed by element ID before creating a new one, preventing memory leaks and
|
/// The call is fire-and-forget; failures are logged as warnings.
|
||||||
/// multiple instances competing for the same video element across re-renders.
|
fn open_in_system_player(url: &str) {
|
||||||
fn hls_init_script(video_id: &str, hls_url: &str) -> String {
|
#[cfg(target_os = "linux")]
|
||||||
// JSON-encode both values so embedded quotes/backslashes cannot break out of
|
let result = std::process::Command::new("xdg-open").arg(url).spawn();
|
||||||
// the JS string.
|
#[cfg(target_os = "macos")]
|
||||||
let encoded_url =
|
let result = std::process::Command::new("open").arg(url).spawn();
|
||||||
serde_json::to_string(hls_url).unwrap_or_else(|_| "\"\"".to_string());
|
#[cfg(target_os = "windows")]
|
||||||
let encoded_id =
|
let result = std::process::Command::new("cmd")
|
||||||
serde_json::to_string(video_id).unwrap_or_else(|_| "\"\"".to_string());
|
.args(["/c", "start", "", url])
|
||||||
format!(
|
.spawn();
|
||||||
r#"
|
#[cfg(not(any(
|
||||||
(function() {{
|
target_os = "linux",
|
||||||
window.__hlsInstances = window.__hlsInstances || {{}};
|
target_os = "macos",
|
||||||
var existing = window.__hlsInstances[{encoded_id}];
|
target_os = "windows"
|
||||||
if (existing) {{
|
)))]
|
||||||
existing.destroy();
|
let result: std::io::Result<std::process::Child> =
|
||||||
window.__hlsInstances[{encoded_id}] = null;
|
Err(std::io::Error::other("unsupported platform"));
|
||||||
}}
|
|
||||||
if (typeof Hls !== 'undefined' && Hls.isSupported()) {{
|
if let Err(e) = result {
|
||||||
var hls = new Hls();
|
tracing::warn!("failed to open system player: {e}");
|
||||||
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};
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
}})();
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -179,6 +165,10 @@ pub fn MediaPlayer(
|
||||||
let mut muted = use_signal(|| false);
|
let mut muted = use_signal(|| false);
|
||||||
|
|
||||||
let is_video = media_type == "video";
|
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 is_playing = *playing.read();
|
||||||
let cur_time = *current_time.read();
|
let cur_time = *current_time.read();
|
||||||
let dur = *duration.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
|
// Autoplay on mount
|
||||||
if autoplay {
|
if autoplay {
|
||||||
let src_auto = src.clone();
|
let src_auto = src.clone();
|
||||||
|
|
@ -435,32 +396,41 @@ pub fn MediaPlayer(
|
||||||
let play_icon = if is_playing { "\u{23f8}" } else { "\u{25b6}" };
|
let play_icon = if is_playing { "\u{23f8}" } else { "\u{25b6}" };
|
||||||
let mute_icon = if is_muted { "\u{1f507}" } else { "\u{1f50a}" };
|
let mute_icon = if is_muted { "\u{1f507}" } else { "\u{1f50a}" };
|
||||||
|
|
||||||
rsx! {
|
let open_src = src.clone();
|
||||||
// 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" }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: if is_video { "media-player media-player-video" } else { "media-player media-player-audio" },
|
class: if is_video { "media-player media-player-video" } else { "media-player media-player-audio" },
|
||||||
tabindex: "0",
|
tabindex: "0",
|
||||||
onkeydown: on_keydown,
|
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 {
|
if is_video {
|
||||||
video {
|
video {
|
||||||
id: "pinakes-player",
|
id: "pinakes-player",
|
||||||
class: "player-native-video",
|
class: "player-native-video",
|
||||||
src: if is_hls { String::new() } else { src.clone() },
|
src: src.clone(),
|
||||||
preload: "metadata",
|
preload: "metadata",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
audio {
|
audio {
|
||||||
id: "pinakes-player",
|
id: "pinakes-player",
|
||||||
class: "player-native-audio",
|
class: "player-native-audio",
|
||||||
src: if is_hls { String::new() } else { src.clone() },
|
src: src.clone(),
|
||||||
preload: "metadata",
|
preload: "metadata",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -521,6 +491,7 @@ pub fn MediaPlayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // end else (non-HLS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue