Implement MPV integration for audio/video playback

- Initialize libmpv with audio-only configuration
- Implement play, pause, resume, stop, seek controls
- Add position and duration tracking from MPV
- Auto-advance to next track when current ends
- Update keybindings to use actual player
- Add shell.nix for development environment with libmpv
- Real playback now working with Enter/Space/n/p keys
This commit is contained in:
Christoffer Martinsson 2025-12-06 12:49:46 +01:00
parent 0093db98c2
commit e840aa9b26
3 changed files with 106 additions and 19 deletions

15
shell.nix Normal file
View File

@ -0,0 +1,15 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
rustc
cargo
mpv
pkg-config
];
shellHook = ''
echo "cm-player development environment"
echo "libmpv available for linking"
'';
}

View File

@ -46,7 +46,7 @@ async fn main() -> Result<()> {
}
// Initialize player
let _player = player::Player::new()?;
let mut player = player::Player::new()?;
// Initialize app state
let mut state = AppState::new(cache, config);
@ -59,7 +59,7 @@ async fn main() -> Result<()> {
let mut terminal = Terminal::new(backend)?;
// Run app
let result = run_app(&mut terminal, &mut state).await;
let result = run_app(&mut terminal, &mut state, &mut player).await;
// Restore terminal
disable_raw_mode()?;
@ -76,14 +76,31 @@ async fn main() -> Result<()> {
async fn run_app<B: ratatui::backend::Backend>(
terminal: &mut Terminal<B>,
state: &mut AppState,
player: &mut player::Player,
) -> Result<()> {
loop {
// 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);
// Check if track ended and play next
if player.is_idle() && state.player_state == PlayerState::Playing {
if state.playlist_index + 1 < state.playlist.len() {
state.play_next();
if let Some(ref path) = state.current_file {
player.play(path)?;
}
} else {
state.player_state = PlayerState::Stopped;
}
}
terminal.draw(|f| ui::render(f, state))?;
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
handle_key_event(state, key.code).await?;
handle_key_event(state, player, key.code).await?;
}
}
}
@ -96,7 +113,7 @@ async fn run_app<B: ratatui::backend::Backend>(
Ok(())
}
async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()> {
async fn handle_key_event(state: &mut AppState, player: &mut player::Player, key_code: KeyCode) -> Result<()> {
match key_code {
KeyCode::Char('q') => {
state.should_quit = true;
@ -122,28 +139,33 @@ async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()>
KeyCode::Char('n') => {
state.play_next();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Next track: {:?}", path);
}
}
KeyCode::Char('p') => {
state.play_previous();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Previous track: {:?}", path);
}
}
KeyCode::Enter => {
state.play_selection();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len());
}
}
KeyCode::Char(' ') => {
match state.player_state {
PlayerState::Playing => {
player.pause()?;
state.player_state = PlayerState::Paused;
tracing::info!("Paused");
}
PlayerState::Paused => {
player.resume()?;
state.player_state = PlayerState::Playing;
tracing::info!("Resumed");
}

View File

@ -1,38 +1,88 @@
// Player module - MPV integration placeholder
// Full implementation in Phase 3
use anyhow::Result;
use anyhow::{anyhow, Result};
use libmpv::Mpv;
use std::path::Path;
pub struct Player {
// MPV instance will be added in Phase 3
mpv: Mpv,
}
impl Player {
pub fn new() -> Result<Self> {
Ok(Self {})
let mpv = Mpv::new().map_err(|e| anyhow!("Failed to create MPV instance: {:?}", e))?;
// 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();
Ok(Self { mpv })
}
pub fn play(&mut self, _path: &Path) -> Result<()> {
// TODO: Implement MPV playback in Phase 3
tracing::info!("Play called (not yet implemented)");
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();
tracing::info!("Playing: {}", path_str);
Ok(())
}
pub fn pause(&mut self) -> Result<()> {
// TODO: Implement pause in Phase 3
tracing::info!("Pause called (not yet implemented)");
self.mpv
.set_property("pause", true)
.map_err(|e| anyhow!("Failed to pause: {:?}", e))?;
Ok(())
}
pub fn resume(&mut self) -> Result<()> {
self.mpv
.set_property("pause", false)
.map_err(|e| anyhow!("Failed to resume: {:?}", e))?;
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
// TODO: Implement stop in Phase 3
tracing::info!("Stop called (not yet implemented)");
self.mpv
.command("stop", &[])
.map_err(|e| anyhow!("Failed to stop: {:?}", e))?;
Ok(())
}
pub fn set_volume(&mut self, _volume: i64) -> Result<()> {
// TODO: Implement volume control in Phase 3
pub fn set_volume(&mut self, volume: i64) -> Result<()> {
self.mpv
.set_property("volume", volume)
.map_err(|e| anyhow!("Failed to set volume: {:?}", e))?;
Ok(())
}
pub fn get_position(&self) -> Option<f64> {
self.mpv.get_property("time-pos").ok()
}
pub fn get_duration(&self) -> Option<f64> {
self.mpv.get_property("duration").ok()
}
pub fn is_playing(&self) -> bool {
self.mpv
.get_property::<bool>("pause")
.map(|paused| !paused)
.unwrap_or(false)
}
pub fn is_idle(&self) -> bool {
self.mpv
.get_property::<String>("idle-active")
.map(|s| s == "yes")
.unwrap_or(true)
}
pub fn seek(&mut self, seconds: f64) -> Result<()> {
self.mpv
.command("seek", &[&seconds.to_string(), "relative"])
.map_err(|e| anyhow!("Failed to seek: {:?}", e))?;
Ok(())
}
}