feat: add play-external IPC for launching media in an external player

Mirrors stremio-glutin-shell's play-external action (PR Stremio/stremio-glutin-shell#2)
so the Windows shell can hand a stream off to mpv, VLC or PotPlayer
when the user picks an external player in the WebUI.

- Allowlist of Windows player URL schemes: mpv://, vlc://, potplayer://.
  iOS/macOS-only schemes (iina, infuse, outplayer, open-vidhub) have
  no Windows handlers and are intentionally not listed.
- mpv has no Windows URL handler: spawn the binary directly with `--`
  separating options from the stream URL. Tries common install paths
  (Program Files, scoop's %LOCALAPPDATA%\Programs\mpv, %LOCALAPPDATA%\mpv)
  before falling back to PATH lookup.
- Spawned with CREATE_BREAKAWAY_FROM_JOB so the JobObject's
  KILL_ON_JOB_CLOSE doesn't kill mpv when the shell exits.
- vlc:// and potplayer:// are routed through `open::that` to use the
  OS protocol handler the user already has registered.

Test: cargo fmt --all -- --check, cargo clippy --all -- -D warnings.
This commit is contained in:
Claude 2026-05-10 18:28:03 +00:00
parent bbbe882faf
commit b4955b58e6
No known key found for this signature in database

View file

@ -298,6 +298,52 @@ impl MainWindow {
}
}
}
Some("play-external") => {
if let Some(arg) = msg.get_params() {
let arg = arg.as_str().unwrap_or("");
let arg_lc = arg.to_lowercase();
const ALLOWED_SCHEMES: &[&str] = &["mpv://", "vlc://", "potplayer://"];
let allowed = ALLOWED_SCHEMES.iter().any(|s| arg_lc.starts_with(s));
if !arg.is_empty() && allowed {
if let Some(stream_url) =
arg_lc.starts_with("mpv://").then(|| &arg[6..])
{
// `--` ends mpv's option parsing; the stream URL can't smuggle flags.
let mpv_paths: Vec<String> = vec![
std::env::var("ProgramFiles")
.ok()
.map(|v| format!("{v}\\mpv\\mpv.exe")),
std::env::var("ProgramFiles(x86)")
.ok()
.map(|v| format!("{v}\\mpv\\mpv.exe")),
std::env::var("LOCALAPPDATA")
.ok()
.map(|v| format!("{v}\\Programs\\mpv\\mpv.exe")),
std::env::var("LOCALAPPDATA")
.ok()
.map(|v| format!("{v}\\mpv\\mpv.exe")),
Some("mpv.exe".to_string()),
]
.into_iter()
.flatten()
.collect();
for path in &mpv_paths {
if Command::new(path)
.arg("--")
.arg(stream_url)
.creation_flags(CREATE_BREAKAWAY_FROM_JOB)
.spawn()
.is_ok()
{
break;
}
}
} else {
open::that(arg).ok();
}
}
}
}
Some("win-focus") => {
focus_sender.notice();
}