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:
parent
e840aa9b26
commit
c0fd204b97
@ -20,8 +20,8 @@ toml = "0.8"
|
|||||||
walkdir = "2.5"
|
walkdir = "2.5"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
|
|
||||||
# MPV player
|
# MPV player (via IPC)
|
||||||
libmpv = "2.0"
|
tempfile = "3.14"
|
||||||
|
|
||||||
# Error handling
|
# Error handling
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|||||||
@ -79,6 +79,9 @@ async fn run_app<B: ratatui::backend::Backend>(
|
|||||||
player: &mut player::Player,
|
player: &mut player::Player,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
|
// Update player properties from MPV
|
||||||
|
player.update_properties();
|
||||||
|
|
||||||
// Update position and duration from player
|
// Update position and duration from player
|
||||||
state.current_position = player.get_position().unwrap_or(0.0);
|
state.current_position = player.get_position().unwrap_or(0.0);
|
||||||
state.current_duration = player.get_duration().unwrap_or(0.0);
|
state.current_duration = player.get_duration().unwrap_or(0.0);
|
||||||
|
|||||||
@ -1,88 +1,202 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Context, Result};
|
||||||
use libmpv::Mpv;
|
use serde_json::{json, Value};
|
||||||
use std::path::Path;
|
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 {
|
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 {
|
impl Player {
|
||||||
pub fn new() -> Result<Self> {
|
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
|
// Spawn MPV with IPC server
|
||||||
mpv.set_property("vo", "null").ok(); // No video output for TUI mode
|
let process = Command::new("mpv")
|
||||||
mpv.set_property("video", "no").ok(); // Disable video decoding
|
.arg("--idle")
|
||||||
mpv.set_property("volume", 100).ok();
|
.arg("--no-video")
|
||||||
mpv.set_property("pause", false).ok();
|
.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<()> {
|
pub fn play(&mut self, path: &Path) -> Result<()> {
|
||||||
let path_str = path.to_string_lossy();
|
let path_str = path.to_string_lossy();
|
||||||
self.mpv
|
self.send_command("loadfile", &[json!(path_str), json!("replace")])?;
|
||||||
.command("loadfile", &[&path_str, "replace"])
|
self.is_paused = false;
|
||||||
.map_err(|e| anyhow!("Failed to load file: {:?}", e))?;
|
self.is_idle = false;
|
||||||
self.mpv.set_property("pause", false).ok();
|
|
||||||
tracing::info!("Playing: {}", path_str);
|
tracing::info!("Playing: {}", path_str);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause(&mut self) -> Result<()> {
|
pub fn pause(&mut self) -> Result<()> {
|
||||||
self.mpv
|
self.send_command("set_property", &[json!("pause"), json!(true)])?;
|
||||||
.set_property("pause", true)
|
self.is_paused = true;
|
||||||
.map_err(|e| anyhow!("Failed to pause: {:?}", e))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resume(&mut self) -> Result<()> {
|
pub fn resume(&mut self) -> Result<()> {
|
||||||
self.mpv
|
self.send_command("set_property", &[json!("pause"), json!(false)])?;
|
||||||
.set_property("pause", false)
|
self.is_paused = false;
|
||||||
.map_err(|e| anyhow!("Failed to resume: {:?}", e))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self) -> Result<()> {
|
pub fn stop(&mut self) -> Result<()> {
|
||||||
self.mpv
|
self.send_command("stop", &[])?;
|
||||||
.command("stop", &[])
|
self.is_idle = true;
|
||||||
.map_err(|e| anyhow!("Failed to stop: {:?}", e))?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_volume(&mut self, volume: i64) -> Result<()> {
|
pub fn set_volume(&mut self, volume: i64) -> Result<()> {
|
||||||
self.mpv
|
self.send_command("set_property", &[json!("volume"), json!(volume)])?;
|
||||||
.set_property("volume", volume)
|
|
||||||
.map_err(|e| anyhow!("Failed to set volume: {:?}", e))?;
|
|
||||||
Ok(())
|
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> {
|
pub fn get_position(&self) -> Option<f64> {
|
||||||
self.mpv.get_property("time-pos").ok()
|
Some(self.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_duration(&self) -> Option<f64> {
|
pub fn get_duration(&self) -> Option<f64> {
|
||||||
self.mpv.get_property("duration").ok()
|
Some(self.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_playing(&self) -> bool {
|
pub fn is_playing(&self) -> bool {
|
||||||
self.mpv
|
!self.is_paused && !self.is_idle
|
||||||
.get_property::<bool>("pause")
|
|
||||||
.map(|paused| !paused)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_idle(&self) -> bool {
|
pub fn is_idle(&self) -> bool {
|
||||||
self.mpv
|
self.is_idle
|
||||||
.get_property::<String>("idle-active")
|
|
||||||
.map(|s| s == "yes")
|
|
||||||
.unwrap_or(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek(&mut self, seconds: f64) -> Result<()> {
|
pub fn seek(&mut self, seconds: f64) -> Result<()> {
|
||||||
self.mpv
|
self.send_command("seek", &[json!(seconds), json!("relative")])?;
|
||||||
.command("seek", &[&seconds.to_string(), "relative"])
|
|
||||||
.map_err(|e| anyhow!("Failed to seek: {:?}", e))?;
|
|
||||||
Ok(())
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user