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
This commit is contained in:
Christoffer Martinsson 2025-12-06 12:54:56 +01:00
parent e840aa9b26
commit c0fd204b97
3 changed files with 159 additions and 42 deletions

View File

@ -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"

View File

@ -79,6 +79,9 @@ async fn run_app<B: ratatui::backend::Backend>(
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);

View File

@ -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<UnixStream>,
position: f64,
duration: f64,
is_paused: bool,
is_idle: bool,
}
impl Player {
pub fn new() -> Result<Self> {
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<Value> {
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<f64> {
self.mpv.get_property("time-pos").ok()
Some(self.position)
}
pub fn get_duration(&self) -> Option<f64> {
self.mpv.get_property("duration").ok()
Some(self.duration)
}
pub fn is_playing(&self) -> bool {
self.mpv
.get_property::<bool>("pause")
.map(|paused| !paused)
.unwrap_or(false)
!self.is_paused && !self.is_idle
}
pub fn is_idle(&self) -> bool {
self.mpv
.get_property::<String>("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();
}
}