use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tracing::{debug, info, trace};
use crate::config::paths::mpv_socket_path;
use crate::error::AudioError;
#[derive(Debug, Serialize)]
struct MpvCommand {
command: Vec<Value>,
request_id: u64,
}
#[derive(Debug, Deserialize)]
struct MpvResponse {
#[serde(default)]
request_id: Option<u64>,
#[serde(default)]
data: Option<Value>,
#[serde(default)]
error: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct MpvEvent {
event: String,
#[serde(default)]
name: Option<String>,
#[serde(default)]
data: Option<Value>,
}
pub struct MpvController {
socket_path: PathBuf,
process: Option<Child>,
request_id: AtomicU64,
socket: Option<UnixStream>,
}
impl MpvController {
pub fn new() -> Self {
Self {
socket_path: mpv_socket_path(),
process: None,
request_id: AtomicU64::new(1),
socket: None,
}
}
pub fn start(&mut self) -> Result<(), AudioError> {
if self.process.is_some() {
return Ok(());
}
let _ = std::fs::remove_file(&self.socket_path);
info!("Starting MPV with socket: {}", self.socket_path.display());
let child = Command::new("mpv")
.arg("--idle") .arg("--no-video") .arg("--no-terminal") .arg("--gapless-audio=yes") .arg("--prefetch-playlist=yes") .arg("--cache=yes") .arg("--cache-secs=120") .arg("--demuxer-max-bytes=100MiB") .arg(format!("--input-ipc-server={}", self.socket_path.display()))
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(AudioError::MpvSpawn)?;
self.process = Some(child);
for _ in 0..50 {
if self.socket_path.exists() {
std::thread::sleep(Duration::from_millis(50));
break;
}
std::thread::sleep(Duration::from_millis(100));
}
if !self.socket_path.exists() {
return Err(AudioError::MpvIpc("Socket not created".to_string()));
}
self.connect()?;
info!("MPV started successfully");
Ok(())
}
fn connect(&mut self) -> Result<(), AudioError> {
let stream = UnixStream::connect(&self.socket_path).map_err(AudioError::MpvSocket)?;
stream
.set_read_timeout(Some(Duration::from_millis(100)))
.map_err(AudioError::MpvSocket)?;
self.socket = Some(stream);
debug!("Connected to MPV socket");
Ok(())
}
pub fn is_running(&self) -> bool {
self.socket.is_some()
}
fn send_command(&mut self, args: Vec<Value>) -> Result<Option<Value>, AudioError> {
let result = self.try_send_command(args);
if matches!(&result, Err(AudioError::MpvIpc(_) | AudioError::MpvSocket(_))) {
self.socket = None;
}
result
}
fn try_send_command(&mut self, args: Vec<Value>) -> Result<Option<Value>, AudioError> {
let socket = self.socket.as_mut().ok_or(AudioError::MpvNotRunning)?;
let request_id = self.request_id.fetch_add(1, Ordering::SeqCst);
let cmd = MpvCommand {
command: args,
request_id,
};
let json = serde_json::to_string(&cmd)?;
debug!("Sending MPV command: {}", json);
writeln!(socket, "{}", json).map_err(|e| AudioError::MpvIpc(e.to_string()))?;
socket
.flush()
.map_err(|e| AudioError::MpvIpc(e.to_string()))?;
let mut reader = BufReader::new(socket.try_clone().map_err(AudioError::MpvSocket)?);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => return Err(AudioError::MpvIpc("Socket closed".to_string())),
Ok(_) => {
if let Ok(resp) = serde_json::from_str::<MpvResponse>(&line) {
if resp.request_id == Some(request_id) {
if resp.error != "success" {
return Err(AudioError::MpvIpc(resp.error));
}
return Ok(resp.data);
}
}
if let Ok(event) = serde_json::from_str::<MpvEvent>(&line) {
trace!("MPV event: {:?}", event);
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(Duration::from_millis(1));
}
Err(e) => return Err(AudioError::MpvIpc(e.to_string())),
}
}
}
pub fn loadfile(&mut self, path: &str) -> Result<(), AudioError> {
info!("Loading: {}", path.split('?').next().unwrap_or(path));
self.send_command(vec![json!("loadfile"), json!(path), json!("replace")])?;
Ok(())
}
pub fn loadfile_append(&mut self, path: &str) -> Result<(), AudioError> {
debug!(
"Appending to playlist: {}",
path.split('?').next().unwrap_or(path)
);
self.send_command(vec![json!("loadfile"), json!(path), json!("append")])?;
Ok(())
}
pub fn playlist_remove(&mut self, index: usize) -> Result<(), AudioError> {
debug!("Removing playlist entry {}", index);
self.send_command(vec![json!("playlist-remove"), json!(index)])?;
Ok(())
}
pub fn get_playlist_pos(&mut self) -> Result<Option<i64>, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("playlist-pos")])?;
Ok(data.and_then(|v| v.as_i64()))
}
pub fn get_playlist_count(&mut self) -> Result<usize, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("playlist-count")])?;
Ok(data.and_then(|v| v.as_u64()).unwrap_or(0) as usize)
}
pub fn pause(&mut self) -> Result<(), AudioError> {
debug!("Pausing playback");
self.send_command(vec![json!("set_property"), json!("pause"), json!(true)])?;
Ok(())
}
pub fn resume(&mut self) -> Result<(), AudioError> {
debug!("Resuming playback");
self.send_command(vec![json!("set_property"), json!("pause"), json!(false)])?;
Ok(())
}
pub fn toggle_pause(&mut self) -> Result<bool, AudioError> {
let paused = self.is_paused()?;
if paused {
self.resume()?;
} else {
self.pause()?;
}
Ok(!paused)
}
pub fn is_paused(&mut self) -> Result<bool, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("pause")])?;
Ok(data.and_then(|v| v.as_bool()).unwrap_or(false))
}
pub fn stop(&mut self) -> Result<(), AudioError> {
debug!("Stopping playback");
self.send_command(vec![json!("stop")])?;
Ok(())
}
pub fn seek(&mut self, position: f64) -> Result<(), AudioError> {
debug!("Seeking to {:.1}s", position);
self.send_command(vec![json!("seek"), json!(position), json!("absolute")])?;
Ok(())
}
pub fn seek_relative(&mut self, offset: f64) -> Result<(), AudioError> {
debug!("Seeking {:+.1}s", offset);
self.send_command(vec![json!("seek"), json!(offset), json!("relative")])?;
Ok(())
}
pub fn get_time_pos(&mut self) -> Result<f64, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("time-pos")])?;
Ok(data.and_then(|v| v.as_f64()).unwrap_or(0.0))
}
pub fn get_duration(&mut self) -> Result<f64, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("duration")])?;
Ok(data.and_then(|v| v.as_f64()).unwrap_or(0.0))
}
pub fn set_volume(&mut self, volume: i32) -> Result<(), AudioError> {
debug!("Setting volume to {}", volume);
self.send_command(vec![
json!("set_property"),
json!("volume"),
json!(volume.clamp(0, 100)),
])?;
Ok(())
}
pub fn get_sample_rate(&mut self) -> Result<Option<u32>, AudioError> {
let data = self.send_command(vec![
json!("get_property"),
json!("audio-params/samplerate"),
])?;
Ok(data.and_then(|v| v.as_u64()).map(|v| v as u32))
}
pub fn get_bit_depth(&mut self) -> Result<Option<u32>, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("audio-params/format")])?;
let format = data.and_then(|v| v.as_str().map(String::from));
Ok(format.and_then(|f| {
if f.contains("32") || f.contains("float") {
Some(32)
} else if f.contains("24") {
Some(24)
} else if f.contains("16") {
Some(16)
} else if f.contains("8") {
Some(8)
} else {
None
}
}))
}
pub fn get_audio_format(&mut self) -> Result<Option<String>, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("audio-params/format")])?;
Ok(data.and_then(|v| v.as_str().map(String::from)))
}
pub fn get_channels(&mut self) -> Result<Option<String>, AudioError> {
let data = self.send_command(vec![
json!("get_property"),
json!("audio-params/channel-count"),
])?;
let count = data.and_then(|v| v.as_u64()).map(|v| v as u32);
Ok(count.map(|c| match c {
1 => "Mono".to_string(),
2 => "Stereo".to_string(),
n => format!("{}ch", n),
}))
}
pub fn is_idle(&mut self) -> Result<bool, AudioError> {
let data = self.send_command(vec![json!("get_property"), json!("idle-active")])?;
Ok(data.and_then(|v| v.as_bool()).unwrap_or(true))
}
pub fn quit(&mut self) -> Result<(), AudioError> {
if self.socket.is_some() {
let _ = self.send_command(vec![json!("quit")]);
}
if let Some(mut child) = self.process.take() {
let _ = child.kill();
let _ = child.wait();
}
self.socket = None;
let _ = std::fs::remove_file(&self.socket_path);
info!("MPV shut down");
Ok(())
}
}
impl Drop for MpvController {
fn drop(&mut self) {
let _ = self.quit();
}
}
impl Default for MpvController {
fn default() -> Self {
Self::new()
}
}