fix(player): mpv draws all Linux video, and no longer runs text from a URL

Security (DR-298, DR-299):
- The pinned libmpv crate's Mpv::command joins its arguments and calls
  mpv_command_string, which parses `;` as a command separator. Stream
  URLs carry server-controlled ids and TranscodingUrl, and a download's
  file:// path carries its track title, so a crafted title could run any
  mpv command, `run` included. Every call now goes through
  mpv_command::command, an argv built for mpv_command. The same parse
  broke loadfile for every downloaded title containing a space.
- mpv's tls-verify defaults to no, and its URLs carry the ApiKey. Every
  handle is now hardened with tls-verify=yes and ytdl=no before its
  first loadfile, and fails construction if it cannot be.

Linux video (DR-235 phase 1):
- native_video::enabled() is unconditional on Linux; the
  JELLYTAU_NATIVE_VIDEO opt-in is retired. No platform reports a
  webview video fallback, so the Settings switch no longer appears.
  Windows keeps the webview element until mpv reaches it (DR-237).
- The Linux device profile is unchanged (still h264, DR-234), so this
  ships the configuration that was tested under the env var.
This commit is contained in:
2026-09-24 20:38:32 -04:00
parent fd1277746d
commit 9d9d81bef3
13 changed files with 298 additions and 161 deletions
+37 -53
View File
@@ -2187,32 +2187,16 @@ pub struct PlaybackCapabilities {
/// native backend. Native audio exists on Linux (mpv) and Android
/// (ExoPlayer); everything else (Windows, future desktops) uses the webview.
pub uses_webview_audio: bool,
/// True when video can be rendered by a native surface composited *behind*
/// a transparent webview. Android only: ExoPlayer draws into a SurfaceView
/// beneath the WebView. Linux cannot do this (WebKitGTK/Wayland
/// compositing), so it stays on the HTML5 element.
/// True when video is rendered by a native surface composited *behind* a
/// transparent webview: ExoPlayer's SurfaceView on Android, mpv's GL area on
/// Linux.
pub supports_native_video: bool,
/// True when the user may send video to the webview element instead of the
/// native renderer — the frontend offers the switch only then, and honours
/// the stored preference only then. See [`webview_video_fallback`].
/// the stored preference only then. False on every platform since DR-235.
pub webview_video_fallback: bool,
}
/// Whether the user may send video to the webview `<video>` element instead of
/// the native renderer.
///
/// Never on Android: ExoPlayer is its only video renderer. Downloads there are
/// the untouched source file (DR-293), and the webview decodes none of the
/// AC-3/E-AC-3/DTS/TrueHD that ExoPlayer plays through the FFmpeg extension, so
/// the fallback would be a silent film. Beside mpv's native video on Linux the
/// webview is still the tested fallback; everywhere else it is the only
/// renderer and there is nothing to switch.
///
/// TRACES: UR-003, UR-071 | DR-293 | UT-259
pub fn webview_video_fallback(is_android: bool, native_video_enabled: bool) -> bool {
!is_android && native_video_enabled
}
/// Report this platform's playback capabilities to the frontend.
///
/// TRACES: UR-003, UR-005 | DR-004, DR-023, DR-024
@@ -2227,11 +2211,12 @@ pub async fn player_get_capabilities() -> Result<PlaybackCapabilities, String> {
// TRACES: UR-080 | DR-235
supports_native_video: cfg!(target_os = "android")
|| crate::player::native_video::enabled(),
// TRACES: UR-003, UR-071 | DR-293
webview_video_fallback: webview_video_fallback(
cfg!(target_os = "android"),
crate::player::native_video::enabled(),
),
// No platform offers one: Android since DR-293, Linux since DR-235,
// and on Windows the webview is the only video renderer, so there is
// nothing to fall back *from*. Kept on the wire until phase 3 deletes
// the frontend switch with the rest of the webview video path.
// TRACES: UR-080, UR-003 | DR-235, DR-293
webview_video_fallback: false,
})
}
@@ -2246,7 +2231,8 @@ pub(super) fn get_player_status(controller: &PlayerController) -> PlayerStatus {
// two fight over the audio. TRACES: UR-080 | DR-235
(VideoBackend::Native, false)
} else {
// Linux and other platforms use HTML5 video element in frontend
// Windows: the webview <video> element is its only video renderer
// until mpv reaches it (DR-237).
(VideoBackend::Html5, true)
};
@@ -3087,35 +3073,33 @@ pub async fn player_disable_jellyfin(player: State<'_, PlayerStateWrapper>) -> R
mod tests {
use crate::utils::lock::MutexSafe;
/// Android has one video renderer, ExoPlayer. The webview element could only
/// be reached by the user switching native video off, and a file downloaded
/// as the untouched original — AC-3 audio included — plays silent there,
/// so the switch is gone on Android (DR-293). Where mpv draws video on Linux
/// the webview is still the tested fallback, so the switch stays there;
/// everywhere else the webview is the only renderer and there is nothing to
/// switch.
/// The webview is not a video renderer anywhere the app ships a native one:
/// Android since DR-293, Linux since DR-235 made mpv its only video path.
/// So the frontend is never offered the switch, and a stored "native video
/// off" from before cannot send Linux video back to the `<video>` element.
///
/// TRACES: UR-003, UR-071 | DR-293 | UT-259
#[test]
fn test_webview_video_fallback_is_offered_only_beside_mpv_native_video() {
use super::webview_video_fallback;
/// TRACES: UR-080, UR-003 | DR-235, DR-293 | UT-272
#[tokio::test]
async fn test_no_platform_offers_a_webview_video_fallback() {
let caps = super::player_get_capabilities().await.unwrap();
assert!(!caps.webview_video_fallback);
if cfg!(target_os = "linux") {
assert!(caps.supports_native_video, "mpv draws video on Linux");
assert!(!caps.uses_webview_audio);
}
}
assert!(
!webview_video_fallback(true, false),
"Android: ExoPlayer is the only video renderer"
);
assert!(
!webview_video_fallback(true, true),
"Android never falls back, whatever else is switched on"
);
assert!(
webview_video_fallback(false, true),
"Linux with mpv native video: the webview is the fallback"
);
assert!(
!webview_video_fallback(false, false),
"the webview is the only renderer; nothing to fall back from"
);
/// And the status the video page reads agrees: on Linux the frontend is told
/// the native backend renders, never to load a `<video>` element.
///
/// TRACES: UR-080 | DR-235 | UT-272
#[test]
fn test_linux_video_is_not_sent_to_the_webview() {
let controller = crate::player::PlayerController::default();
let status = super::get_player_status(&controller);
if cfg!(target_os = "linux") {
assert!(!status.use_html5_element);
}
}
/// UT-206 — the volume the command hands on is always a real number in