From c0fd204b97d364792279a7a878155e5862a59094 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Sat, 6 Dec 2025 12:54:56 +0100 Subject: [PATCH] Replace libmpv with MPV IPC subprocess approach - Remove libmpv dependency to avoid version mismatch issues - Spawn mpv as subprocess with --input-ipc-server - Communicate via Unix socket with JSON IPC protocol - Add update_properties() to poll MPV state - No linking required, only mpv binary needed at runtime - More stable and portable across MPV versions --- Cargo.toml | 4 +- src/main.rs | 3 + src/player/mod.rs | 194 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 159 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 844dc3a..96b5e5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ toml = "0.8" walkdir = "2.5" dirs = "5.0" -# MPV player -libmpv = "2.0" +# MPV player (via IPC) +tempfile = "3.14" # Error handling anyhow = "1.0" diff --git a/src/main.rs b/src/main.rs index e3b9724..c70a05b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,9 @@ async fn run_app( player: &mut player::Player, ) -> Result<()> { loop { + // Update player properties from MPV + player.update_properties(); + // Update position and duration from player state.current_position = player.get_position().unwrap_or(0.0); state.current_duration = player.get_duration().unwrap_or(0.0); diff --git a/src/player/mod.rs b/src/player/mod.rs index 24d047e..3c73256 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,88 +1,202 @@ -use anyhow::{anyhow, Result}; -use libmpv::Mpv; -use std::path::Path; +use anyhow::{Context, Result}; +use serde_json::{json, Value}; +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::net::UnixStream; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; +use tempfile::NamedTempFile; pub struct Player { - mpv: Mpv, + process: Child, + socket_path: PathBuf, + socket: Option, + position: f64, + duration: f64, + is_paused: bool, + is_idle: bool, } impl Player { pub fn new() -> Result { - let mpv = Mpv::new().map_err(|e| anyhow!("Failed to create MPV instance: {:?}", e))?; + // Create temporary socket file + let socket_file = NamedTempFile::new().context("Failed to create temp socket")?; + let socket_path = socket_file.path().to_path_buf(); + drop(socket_file); // Remove the file so MPV can create the socket - // Configure MPV for audio/video playback - mpv.set_property("vo", "null").ok(); // No video output for TUI mode - mpv.set_property("video", "no").ok(); // Disable video decoding - mpv.set_property("volume", 100).ok(); - mpv.set_property("pause", false).ok(); + // Spawn MPV with IPC server + let process = Command::new("mpv") + .arg("--idle") + .arg("--no-video") + .arg("--no-terminal") + .arg(format!("--input-ipc-server={}", socket_path.display())) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .context("Failed to spawn MPV process")?; - Ok(Self { mpv }) + tracing::info!("MPV process started with IPC at {:?}", socket_path); + + // Wait for socket to be created + std::thread::sleep(Duration::from_millis(500)); + + Ok(Self { + process, + socket_path, + socket: None, + position: 0.0, + duration: 0.0, + is_paused: false, + is_idle: true, + }) + } + + fn connect(&mut self) -> Result<()> { + if self.socket.is_none() { + let stream = UnixStream::connect(&self.socket_path) + .context("Failed to connect to MPV IPC socket")?; + stream.set_nonblocking(true).ok(); + self.socket = Some(stream); + } + Ok(()) + } + + fn send_command(&mut self, command: &str, args: &[Value]) -> Result<()> { + self.connect()?; + + let mut command_array = vec![json!(command)]; + command_array.extend_from_slice(args); + + let cmd = json!({ + "command": command_array + }); + + if let Some(ref mut socket) = self.socket { + let msg = format!("{}\n", cmd); + socket.write_all(msg.as_bytes()).context("Failed to write to socket")?; + } + + Ok(()) + } + + fn get_property(&mut self, property: &str) -> Option { + self.connect().ok()?; + + let cmd = json!({ + "command": ["get_property", property], + "request_id": 1 + }); + + if let Some(ref mut socket) = self.socket { + let msg = format!("{}\n", cmd); + socket.write_all(msg.as_bytes()).ok()?; + + // Try to read response (non-blocking) + socket.set_nonblocking(false).ok(); + socket.set_read_timeout(Some(Duration::from_millis(100))).ok(); + + let mut reader = BufReader::new(socket.try_clone().ok()?); + let mut response = String::new(); + reader.read_line(&mut response).ok()?; + + socket.set_nonblocking(true).ok(); + + let parsed: Value = serde_json::from_str(&response).ok()?; + return parsed.get("data").cloned(); + } + + None } pub fn play(&mut self, path: &Path) -> Result<()> { let path_str = path.to_string_lossy(); - self.mpv - .command("loadfile", &[&path_str, "replace"]) - .map_err(|e| anyhow!("Failed to load file: {:?}", e))?; - self.mpv.set_property("pause", false).ok(); + self.send_command("loadfile", &[json!(path_str), json!("replace")])?; + self.is_paused = false; + self.is_idle = false; tracing::info!("Playing: {}", path_str); Ok(()) } pub fn pause(&mut self) -> Result<()> { - self.mpv - .set_property("pause", true) - .map_err(|e| anyhow!("Failed to pause: {:?}", e))?; + self.send_command("set_property", &[json!("pause"), json!(true)])?; + self.is_paused = true; Ok(()) } pub fn resume(&mut self) -> Result<()> { - self.mpv - .set_property("pause", false) - .map_err(|e| anyhow!("Failed to resume: {:?}", e))?; + self.send_command("set_property", &[json!("pause"), json!(false)])?; + self.is_paused = false; Ok(()) } pub fn stop(&mut self) -> Result<()> { - self.mpv - .command("stop", &[]) - .map_err(|e| anyhow!("Failed to stop: {:?}", e))?; + self.send_command("stop", &[])?; + self.is_idle = true; Ok(()) } pub fn set_volume(&mut self, volume: i64) -> Result<()> { - self.mpv - .set_property("volume", volume) - .map_err(|e| anyhow!("Failed to set volume: {:?}", e))?; + self.send_command("set_property", &[json!("volume"), json!(volume)])?; Ok(()) } + pub fn update_properties(&mut self) { + // Update position + if let Some(val) = self.get_property("time-pos") { + if let Some(pos) = val.as_f64() { + self.position = pos; + } + } + + // Update duration + if let Some(val) = self.get_property("duration") { + if let Some(dur) = val.as_f64() { + self.duration = dur; + } + } + + // Update pause state + if let Some(val) = self.get_property("pause") { + if let Some(paused) = val.as_bool() { + self.is_paused = paused; + } + } + + // Update idle state + if let Some(val) = self.get_property("idle-active") { + if let Some(idle) = val.as_bool() { + self.is_idle = idle; + } + } + } + pub fn get_position(&self) -> Option { - self.mpv.get_property("time-pos").ok() + Some(self.position) } pub fn get_duration(&self) -> Option { - self.mpv.get_property("duration").ok() + Some(self.duration) } pub fn is_playing(&self) -> bool { - self.mpv - .get_property::("pause") - .map(|paused| !paused) - .unwrap_or(false) + !self.is_paused && !self.is_idle } pub fn is_idle(&self) -> bool { - self.mpv - .get_property::("idle-active") - .map(|s| s == "yes") - .unwrap_or(true) + self.is_idle } pub fn seek(&mut self, seconds: f64) -> Result<()> { - self.mpv - .command("seek", &[&seconds.to_string(), "relative"]) - .map_err(|e| anyhow!("Failed to seek: {:?}", e))?; + self.send_command("seek", &[json!(seconds), json!("relative")])?; Ok(()) } } + +impl Drop for Player { + fn drop(&mut self) { + self.send_command("quit", &[]).ok(); + self.process.kill().ok(); + std::fs::remove_file(&self.socket_path).ok(); + } +}