Add audio buffer for WSLg stability
Increase mpv audio buffer to 2 seconds to fix stuttering on WSLg. Ref: https://github.com/microsoft/wslg/issues/1257
This commit is contained in:
parent
4529fad61d
commit
7b4c664011
66
CLAUDE.md
66
CLAUDE.md
@ -90,6 +90,72 @@ paths = [
|
||||
- `r` - Rescan library (manual refresh)
|
||||
- `q` - Quit
|
||||
|
||||
## API for OS-Wide Shortcuts
|
||||
|
||||
cm-player provides a Unix socket API for external control, allowing integration with OS-wide media keys and custom shortcuts.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Single Binary**: `cm-player` acts as both TUI server and CLI client
|
||||
- **IPC**: Unix socket at `$XDG_RUNTIME_DIR/cm-player.sock` (or `/tmp/cm-player.sock`)
|
||||
- **Protocol**: JSON commands over Unix socket
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Start TUI (server mode)
|
||||
cm-player
|
||||
|
||||
# Send commands to running instance (client mode)
|
||||
cm-player play-pause
|
||||
cm-player next
|
||||
cm-player prev
|
||||
cm-player stop
|
||||
cm-player volume-up
|
||||
cm-player volume-down
|
||||
cm-player volume 50
|
||||
cm-player seek-forward 30
|
||||
cm-player seek-backward 10
|
||||
cm-player quit
|
||||
```
|
||||
|
||||
### OS-Wide Keyboard Shortcuts
|
||||
|
||||
**i3/sway config:**
|
||||
```
|
||||
bindsym XF86AudioPlay exec cm-player play-pause
|
||||
bindsym XF86AudioNext exec cm-player next
|
||||
bindsym XF86AudioPrev exec cm-player prev
|
||||
bindsym XF86AudioStop exec cm-player stop
|
||||
```
|
||||
|
||||
**KDE/GNOME:**
|
||||
Add custom shortcuts pointing to `cm-player <command>`
|
||||
|
||||
### JSON Protocol
|
||||
|
||||
Commands are JSON objects with a `command` field:
|
||||
|
||||
```json
|
||||
{"command": "play-pause"}
|
||||
{"command": "next"}
|
||||
{"command": "prev"}
|
||||
{"command": "stop"}
|
||||
{"command": "volume-up"}
|
||||
{"command": "volume-down"}
|
||||
{"command": "volume-set", "volume": 50}
|
||||
{"command": "seek-forward", "seconds": 30}
|
||||
{"command": "seek-backward", "seconds": 10}
|
||||
{"command": "get-status"}
|
||||
{"command": "quit"}
|
||||
```
|
||||
|
||||
Responses:
|
||||
```json
|
||||
{"success": true, "message": null, "data": null}
|
||||
{"success": false, "message": "error details", "data": null}
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
- **MPV IPC** - Communicates with mpv via Unix socket and JSON protocol
|
||||
- **No Version Lock** - Uses mpv binary, not libmpv library (avoids version mismatch)
|
||||
|
||||
182
src/main.rs
182
src/main.rs
@ -1,3 +1,4 @@
|
||||
mod api;
|
||||
mod cache;
|
||||
mod config;
|
||||
mod player;
|
||||
@ -13,7 +14,8 @@ use crossterm::{
|
||||
};
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
use state::{AppState, PlayerState};
|
||||
use std::io;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use tracing_subscriber;
|
||||
|
||||
// UI update intervals and thresholds
|
||||
@ -23,6 +25,12 @@ const POLL_DURATION_ACTIVE_MS: u64 = 100; // 10 FPS when playing/paused
|
||||
const DOUBLE_CLICK_MS: u128 = 500; // Double-click detection threshold
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Check if we're in client mode (sending command to running instance)
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 {
|
||||
return send_command(&args[1..]);
|
||||
}
|
||||
|
||||
// Initialize logging to file to avoid interfering with TUI
|
||||
let log_file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
@ -60,6 +68,11 @@ fn main() -> Result<()> {
|
||||
let mut state = AppState::new(cache, config);
|
||||
tracing::info!("State initialized");
|
||||
|
||||
// Start API server
|
||||
let socket_path = api::get_default_socket_path()?;
|
||||
let api_rx = api::start_api_server(socket_path)?;
|
||||
tracing::info!("API server started");
|
||||
|
||||
// Setup terminal
|
||||
enable_raw_mode()?;
|
||||
tracing::info!("Raw mode enabled");
|
||||
@ -71,7 +84,7 @@ fn main() -> Result<()> {
|
||||
tracing::info!("Terminal created, entering main loop");
|
||||
|
||||
// Run app (ensure terminal cleanup even on error)
|
||||
let result = run_app(&mut terminal, &mut state, &mut player);
|
||||
let result = run_app(&mut terminal, &mut state, &mut player, api_rx);
|
||||
|
||||
// Restore terminal (always run cleanup, even if result is Err)
|
||||
let cleanup_result = (|| -> Result<()> {
|
||||
@ -93,6 +106,102 @@ fn main() -> Result<()> {
|
||||
result
|
||||
}
|
||||
|
||||
/// Send a command to a running cm-player instance (client mode)
|
||||
fn send_command(args: &[String]) -> Result<()> {
|
||||
let socket_path = api::get_default_socket_path()?;
|
||||
|
||||
if !socket_path.exists() {
|
||||
eprintln!("Error: cm-player is not running (socket not found at {:?})", socket_path);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Parse command
|
||||
let command = match args[0].as_str() {
|
||||
"play-pause" | "pp" => api::ApiCommand::PlayPause,
|
||||
"stop" => api::ApiCommand::Stop,
|
||||
"next" | "n" => api::ApiCommand::Next,
|
||||
"prev" | "p" => api::ApiCommand::Prev,
|
||||
"volume-up" | "vu" => api::ApiCommand::VolumeUp,
|
||||
"volume-down" | "vd" => api::ApiCommand::VolumeDown,
|
||||
"volume" | "v" => {
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: cm-player volume <0-100>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let volume: i64 = args[1].parse()
|
||||
.context("Volume must be a number between 0-100")?;
|
||||
api::ApiCommand::VolumeSet { volume }
|
||||
}
|
||||
"seek-forward" | "sf" => {
|
||||
let seconds = if args.len() > 1 {
|
||||
args[1].parse().unwrap_or(10.0)
|
||||
} else {
|
||||
10.0
|
||||
};
|
||||
api::ApiCommand::SeekForward { seconds }
|
||||
}
|
||||
"seek-backward" | "sb" => {
|
||||
let seconds = if args.len() > 1 {
|
||||
args[1].parse().unwrap_or(10.0)
|
||||
} else {
|
||||
10.0
|
||||
};
|
||||
api::ApiCommand::SeekBackward { seconds }
|
||||
}
|
||||
"status" | "s" => api::ApiCommand::GetStatus,
|
||||
"quit" | "q" => api::ApiCommand::Quit,
|
||||
_ => {
|
||||
eprintln!("Unknown command: {}", args[0]);
|
||||
print_usage();
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Connect to socket and send command
|
||||
let mut stream = UnixStream::connect(&socket_path)
|
||||
.context("Failed to connect to cm-player")?;
|
||||
|
||||
let command_json = serde_json::to_string(&command)?;
|
||||
writeln!(stream, "{}", command_json)?;
|
||||
|
||||
// Read response
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut response_line = String::new();
|
||||
reader.read_line(&mut response_line)?;
|
||||
|
||||
let response: api::ApiResponse = serde_json::from_str(&response_line)?;
|
||||
|
||||
if response.success {
|
||||
if let Some(data) = response.data {
|
||||
println!("{}", serde_json::to_string_pretty(&data)?);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Error: {}", response.message.unwrap_or_else(|| "Unknown error".to_string()));
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!("Usage: cm-player [command] [args]");
|
||||
eprintln!();
|
||||
eprintln!("Commands:");
|
||||
eprintln!(" play-pause, pp Toggle play/pause");
|
||||
eprintln!(" stop Stop playback");
|
||||
eprintln!(" next, n Next track");
|
||||
eprintln!(" prev, p Previous track");
|
||||
eprintln!(" volume-up, vu Volume up by 5%");
|
||||
eprintln!(" volume-down, vd Volume down by 5%");
|
||||
eprintln!(" volume, v <0-100> Set volume");
|
||||
eprintln!(" seek-forward, sf [sec] Seek forward (default: 10s)");
|
||||
eprintln!(" seek-backward, sb [sec] Seek backward (default: 10s)");
|
||||
eprintln!(" status, s Get player status");
|
||||
eprintln!(" quit, q Quit cm-player");
|
||||
eprintln!();
|
||||
eprintln!("If no command is provided, cm-player starts in TUI mode.");
|
||||
}
|
||||
|
||||
// Common action functions that both keyboard and mouse handlers can call
|
||||
|
||||
fn action_toggle_folder(state: &mut AppState) {
|
||||
@ -320,6 +429,7 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
state: &mut AppState,
|
||||
player: &mut player::Player,
|
||||
api_rx: std::sync::mpsc::Receiver<api::ApiCommand>,
|
||||
) -> Result<()> {
|
||||
let mut metadata_update_counter = 0u32;
|
||||
let mut last_position = 0.0f64;
|
||||
@ -332,6 +442,74 @@ fn run_app<B: ratatui::backend::Backend>(
|
||||
loop {
|
||||
let mut state_changed = false;
|
||||
|
||||
// Check for API commands (non-blocking)
|
||||
while let Ok(cmd) = api_rx.try_recv() {
|
||||
tracing::debug!("Processing API command: {:?}", cmd);
|
||||
match cmd {
|
||||
api::ApiCommand::PlayPause => {
|
||||
if let Some(player_state) = player.get_player_state() {
|
||||
match player_state {
|
||||
PlayerState::Stopped => {
|
||||
// Play current file or first in playlist
|
||||
if state.current_file.is_none() && !state.playlist.is_empty() {
|
||||
state.current_file = Some(state.playlist[0].clone());
|
||||
}
|
||||
if let Some(ref file) = state.current_file {
|
||||
player.play(file)?;
|
||||
}
|
||||
}
|
||||
PlayerState::Playing => player.pause()?,
|
||||
PlayerState::Paused => player.resume()?,
|
||||
}
|
||||
state_changed = true;
|
||||
}
|
||||
}
|
||||
api::ApiCommand::Stop => {
|
||||
player.stop()?;
|
||||
state.current_file = None;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::Next => {
|
||||
action_navigate_track(state, player, 1)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::Prev => {
|
||||
action_navigate_track(state, player, -1)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::VolumeUp => {
|
||||
state.volume = (state.volume + 5).min(100);
|
||||
player.set_volume(state.volume)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::VolumeDown => {
|
||||
state.volume = (state.volume - 5).max(0);
|
||||
player.set_volume(state.volume)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::VolumeSet { volume } => {
|
||||
state.volume = volume.clamp(0, 100);
|
||||
player.set_volume(state.volume)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::SeekForward { seconds } => {
|
||||
player.seek(seconds)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::SeekBackward { seconds } => {
|
||||
player.seek(-seconds)?;
|
||||
state_changed = true;
|
||||
}
|
||||
api::ApiCommand::GetStatus => {
|
||||
// Status query - no state change needed
|
||||
tracing::debug!("Status query received");
|
||||
}
|
||||
api::ApiCommand::Quit => {
|
||||
state.should_quit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if mpv process died (e.g., user closed video window)
|
||||
if !player.is_process_alive() {
|
||||
if let Some(player_state) = player.get_player_state() {
|
||||
|
||||
@ -35,6 +35,7 @@ impl Player {
|
||||
.arg("--no-terminal")
|
||||
.arg("--profile=fast")
|
||||
.arg("--audio-display=no") // Don't show cover art for audio files
|
||||
.arg("--audio-buffer=2") // Larger buffer for WSLg audio stability
|
||||
.arg(format!("--input-ipc-server={}", socket_path.display()))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user