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:
parent
0093db98c2
commit
e840aa9b26
15
shell.nix
Normal file
15
shell.nix
Normal 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"
|
||||
'';
|
||||
}
|
||||
30
src/main.rs
30
src/main.rs
@ -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");
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user