feat(windows): mpv draws Windows video into the app window
DR-237's video half. mpv now renders Windows video the way tauri-plugin-libmpv does on Windows: MpvBackend is handed the main window's HWND and sets it as `wid` before mpv initialises, so mpv draws as a child of the app window beneath the transparent WebView2, with vo=gpu-next,gpu. osc, default bindings, VO keyboard and cursor handling are off, so the Svelte controls drawn over the picture are the only ones. - video_output() decides per platform (UT-274): Window(hwnd) on Windows, RenderApi on Linux, Off without native video. Windows with no handle draws nothing rather than letting mpv open a top-level window. - native_video::enabled() is true on Windows as well, so the frontend takes the native path there: NativePlayerAdapter, and the page clears its background while video is on screen. - enableNativeVideoCompositing() no longer logs a missing Android bridge as an error on the desktop, where there is no bridge to have. Verified: every option, wid included, is accepted by the real libmpv on Linux and by the shipped Windows DLL under wine; the Windows unit suite passes under wine (949). Not yet seen on real Windows hardware.
This commit is contained in:
@@ -149,12 +149,73 @@ pub fn registered_handle() -> *mut libmpv_sys::mpv_handle {
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
/// How mpv shows video on this platform.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-231, DR-237
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum VideoOutput {
|
||||
/// No picture: audio-only playback, or nowhere to draw.
|
||||
Off,
|
||||
/// Linux: frames through the render API into the GTK surface beneath the
|
||||
/// webview (`video_surface`).
|
||||
RenderApi,
|
||||
/// Windows: mpv renders as a child of the app's own window (`wid`, set
|
||||
/// before initialisation), beneath the transparent WebView2 — the
|
||||
/// arrangement tauri-plugin-libmpv ships on Windows.
|
||||
Window(i64),
|
||||
}
|
||||
|
||||
impl VideoOutput {
|
||||
/// Runtime options for this output. `wid` is not among them: it only takes
|
||||
/// effect before initialisation, so the constructor sets it separately.
|
||||
pub(crate) fn options(&self) -> Vec<(&'static str, String)> {
|
||||
match self {
|
||||
VideoOutput::Off => vec![("video", "no".to_string())],
|
||||
VideoOutput::RenderApi => vec![("vo", "libmpv".to_string())],
|
||||
VideoOutput::Window(_) => [
|
||||
// libplacebo's renderer, with the classic one as fallback for a
|
||||
// build or GPU that lacks it.
|
||||
("vo", "gpu-next,gpu"),
|
||||
// mpv is a surface here, not a player: the app's controls are
|
||||
// drawn over it, so its own controller and bindings must not
|
||||
// answer clicks, keys or the cursor.
|
||||
("osc", "no"),
|
||||
("input-default-bindings", "no"),
|
||||
("input-vo-keyboard", "no"),
|
||||
("input-cursor", "no"),
|
||||
("cursor-autohide", "no"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.to_string()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide the video output from whether native video is on, the platform, and
|
||||
/// the app window's handle (Windows only).
|
||||
///
|
||||
/// TRACES: UR-080 | DR-231, DR-237 | UT-274
|
||||
pub(crate) fn video_output(native: bool, is_windows: bool, window: Option<i64>) -> VideoOutput {
|
||||
match (native, is_windows, window) {
|
||||
(false, _, _) => VideoOutput::Off,
|
||||
(true, true, Some(wid)) => VideoOutput::Window(wid),
|
||||
// No handle: mpv would open a top-level window of its own.
|
||||
(true, true, None) => VideoOutput::Off,
|
||||
(true, false, _) => VideoOutput::RenderApi,
|
||||
}
|
||||
}
|
||||
|
||||
impl MpvBackend {
|
||||
/// Create a new MPV backend
|
||||
///
|
||||
/// `video_window` is the app window's native handle (an HWND), which mpv
|
||||
/// draws video into on Windows; `None` elsewhere.
|
||||
pub fn new(
|
||||
event_emitter: Option<Arc<dyn PlayerEventEmitter>>,
|
||||
playback_reporter: Arc<TokioMutex<Option<PlaybackReporter>>>,
|
||||
position_throttler: Arc<EventThrottler>,
|
||||
video_window: Option<i64>,
|
||||
) -> Result<Self, PlayerError> {
|
||||
info!("[MpvBackend] Initializing MPV backend...");
|
||||
|
||||
@@ -166,7 +227,22 @@ impl MpvBackend {
|
||||
libc::setlocale(libc::LC_NUMERIC, c_locale.as_ptr());
|
||||
}
|
||||
|
||||
let mpv = Mpv::new().map_err(|e| PlayerError {
|
||||
let output = video_output(
|
||||
super::native_video::enabled(),
|
||||
cfg!(target_os = "windows"),
|
||||
video_window,
|
||||
);
|
||||
if super::native_video::enabled() && output == VideoOutput::Off {
|
||||
error!("[MpvBackend] no window handle to draw video into; video will have no picture");
|
||||
}
|
||||
// `wid` only takes effect before initialisation. TRACES: UR-080 | DR-237
|
||||
let mpv = Mpv::with_initializer(|init| {
|
||||
if let VideoOutput::Window(wid) = output {
|
||||
init.set_property("wid", wid)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| PlayerError {
|
||||
message: format!("Failed to initialize MPV: {:?}", e),
|
||||
})?;
|
||||
// TRACES: UR-012 | DR-299
|
||||
@@ -203,24 +279,21 @@ impl MpvBackend {
|
||||
//
|
||||
// Linux video went through the webview until DR-235, and decoding it
|
||||
// here too would have burned a core for a picture nobody saw — hence
|
||||
// `video: no`. With native video, mpv needs both the decoder *and*
|
||||
// `vo=libmpv` — the render API only works through that output, and the
|
||||
// default would try to open a window of its own.
|
||||
// `video: no`. With native video, mpv needs the decoder *and* an output
|
||||
// that draws where the app wants it: the render API on Linux (the default
|
||||
// would open a window of its own), the app's window on Windows.
|
||||
//
|
||||
// Set at construction because mpv resolves the video output when it
|
||||
// initialises; flipping it later does not re-open one.
|
||||
//
|
||||
// TRACES: UR-080 | DR-231, DR-235
|
||||
if super::native_video::enabled() {
|
||||
mpv.set_property("vo", "libmpv").map_err(|e| PlayerError {
|
||||
message: format!("Failed to select the libmpv video output: {:?}", e),
|
||||
})?;
|
||||
info!("[MpvBackend] native video enabled (vo=libmpv)");
|
||||
} else {
|
||||
mpv.set_property("video", "no").map_err(|e| PlayerError {
|
||||
message: format!("Failed to configure MPV video: {:?}", e),
|
||||
})?;
|
||||
// TRACES: UR-080 | DR-231, DR-235, DR-237
|
||||
for (name, value) in output.options() {
|
||||
mpv.set_property(name, value.as_str())
|
||||
.map_err(|e| PlayerError {
|
||||
message: format!("Failed to set {name}={value}: {:?}", e),
|
||||
})?;
|
||||
}
|
||||
info!("[MpvBackend] video output: {:?}", output);
|
||||
|
||||
// Set volume to 100% (we'll control via MPV's volume property)
|
||||
mpv.set_property("volume", 100i64)
|
||||
@@ -1023,3 +1096,97 @@ mod af_filter_tests {
|
||||
assert!(eq_pos < norm_pos, "normalizer runs after EQ: {af}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod video_output_tests {
|
||||
use super::{video_output, VideoOutput};
|
||||
|
||||
/// Windows draws into the app's own window: mpv is handed its HWND before
|
||||
/// initialising and renders as a child of it, beneath the transparent
|
||||
/// WebView2.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-237 | UT-274
|
||||
#[test]
|
||||
fn windows_native_video_renders_into_the_app_window() {
|
||||
assert_eq!(
|
||||
video_output(true, true, Some(0x1234)),
|
||||
VideoOutput::Window(0x1234)
|
||||
);
|
||||
}
|
||||
|
||||
/// Without a handle mpv would open a top-level window of its own, a second
|
||||
/// window floating beside the app. No picture is the honest failure.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-237 | UT-274
|
||||
#[test]
|
||||
fn windows_without_a_window_handle_draws_nothing() {
|
||||
assert_eq!(video_output(true, true, None), VideoOutput::Off);
|
||||
}
|
||||
|
||||
/// Linux keeps the render API the GTK surface draws from.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-231 | UT-274
|
||||
#[test]
|
||||
fn linux_native_video_uses_the_render_api() {
|
||||
assert_eq!(video_output(true, false, None), VideoOutput::RenderApi);
|
||||
}
|
||||
|
||||
/// TRACES: UR-080 | DR-231 | UT-274
|
||||
#[test]
|
||||
fn no_native_video_decodes_no_picture() {
|
||||
assert_eq!(video_output(false, true, Some(1)), VideoOutput::Off);
|
||||
assert_eq!(video_output(false, false, None), VideoOutput::Off);
|
||||
}
|
||||
|
||||
/// In the app's window mpv must not act as a player of its own: its
|
||||
/// on-screen controller and key/mouse bindings would compete with the
|
||||
/// Svelte controls drawn over it.
|
||||
///
|
||||
/// TRACES: UR-080 | DR-237 | UT-274
|
||||
#[test]
|
||||
fn a_window_output_hands_all_input_to_the_app() {
|
||||
let opts = VideoOutput::Window(7).options();
|
||||
for (k, v) in [
|
||||
("vo", "gpu-next,gpu"),
|
||||
("osc", "no"),
|
||||
("input-default-bindings", "no"),
|
||||
("input-vo-keyboard", "no"),
|
||||
("input-cursor", "no"),
|
||||
("cursor-autohide", "no"),
|
||||
] {
|
||||
assert!(
|
||||
opts.iter().any(|(ok, ov)| *ok == k && ov == v),
|
||||
"missing {k}={v} in {opts:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
VideoOutput::Off.options(),
|
||||
vec![("video", "no".to_string())]
|
||||
);
|
||||
}
|
||||
|
||||
/// Every option the outputs set is one this libmpv accepts, `wid` included —
|
||||
/// against the real library, so a misspelt or removed option fails here
|
||||
/// rather than as a player that will not start on a user's machine. Runs on
|
||||
/// the Windows DLL too (under wine in the cross-build).
|
||||
///
|
||||
/// TRACES: UR-080 | DR-237 | UT-274
|
||||
#[test]
|
||||
fn libmpv_accepts_every_video_output_option() {
|
||||
let mpv = libmpv::Mpv::with_initializer(|init| {
|
||||
init.set_property("wid", 0i64)?;
|
||||
Ok(())
|
||||
})
|
||||
.expect("libmpv must accept wid before initialisation");
|
||||
for output in [
|
||||
VideoOutput::Off,
|
||||
VideoOutput::RenderApi,
|
||||
VideoOutput::Window(0),
|
||||
] {
|
||||
for (name, value) in output.options() {
|
||||
mpv.set_property(name, value.as_str())
|
||||
.unwrap_or_else(|e| panic!("libmpv rejected {name}={value}: {e:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user