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
|
// Initialize player
|
||||||
let _player = player::Player::new()?;
|
let mut player = player::Player::new()?;
|
||||||
|
|
||||||
// Initialize app state
|
// Initialize app state
|
||||||
let mut state = AppState::new(cache, config);
|
let mut state = AppState::new(cache, config);
|
||||||
@ -59,7 +59,7 @@ async fn main() -> Result<()> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
// Run app
|
// Run app
|
||||||
let result = run_app(&mut terminal, &mut state).await;
|
let result = run_app(&mut terminal, &mut state, &mut player).await;
|
||||||
|
|
||||||
// Restore terminal
|
// Restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
@ -76,14 +76,31 @@ async fn main() -> Result<()> {
|
|||||||
async fn run_app<B: ratatui::backend::Backend>(
|
async fn run_app<B: ratatui::backend::Backend>(
|
||||||
terminal: &mut Terminal<B>,
|
terminal: &mut Terminal<B>,
|
||||||
state: &mut AppState,
|
state: &mut AppState,
|
||||||
|
player: &mut player::Player,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
loop {
|
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))?;
|
terminal.draw(|f| ui::render(f, state))?;
|
||||||
|
|
||||||
if event::poll(std::time::Duration::from_millis(100))? {
|
if event::poll(std::time::Duration::from_millis(100))? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if key.kind == KeyEventKind::Press {
|
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(())
|
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 {
|
match key_code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
state.should_quit = true;
|
state.should_quit = true;
|
||||||
@ -122,28 +139,33 @@ async fn handle_key_event(state: &mut AppState, key_code: KeyCode) -> Result<()>
|
|||||||
KeyCode::Char('n') => {
|
KeyCode::Char('n') => {
|
||||||
state.play_next();
|
state.play_next();
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
tracing::info!("Next track: {:?}", path);
|
tracing::info!("Next track: {:?}", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => {
|
||||||
state.play_previous();
|
state.play_previous();
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
tracing::info!("Previous track: {:?}", path);
|
tracing::info!("Previous track: {:?}", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
state.play_selection();
|
state.play_selection();
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len());
|
tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
match state.player_state {
|
match state.player_state {
|
||||||
PlayerState::Playing => {
|
PlayerState::Playing => {
|
||||||
|
player.pause()?;
|
||||||
state.player_state = PlayerState::Paused;
|
state.player_state = PlayerState::Paused;
|
||||||
tracing::info!("Paused");
|
tracing::info!("Paused");
|
||||||
}
|
}
|
||||||
PlayerState::Paused => {
|
PlayerState::Paused => {
|
||||||
|
player.resume()?;
|
||||||
state.player_state = PlayerState::Playing;
|
state.player_state = PlayerState::Playing;
|
||||||
tracing::info!("Resumed");
|
tracing::info!("Resumed");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,88 @@
|
|||||||
// Player module - MPV integration placeholder
|
use anyhow::{anyhow, Result};
|
||||||
// Full implementation in Phase 3
|
use libmpv::Mpv;
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
// MPV instance will be added in Phase 3
|
mpv: Mpv,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new() -> Result<Self> {
|
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<()> {
|
pub fn play(&mut self, path: &Path) -> Result<()> {
|
||||||
// TODO: Implement MPV playback in Phase 3
|
let path_str = path.to_string_lossy();
|
||||||
tracing::info!("Play called (not yet implemented)");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pause(&mut self) -> Result<()> {
|
pub fn pause(&mut self) -> Result<()> {
|
||||||
// TODO: Implement pause in Phase 3
|
self.mpv
|
||||||
tracing::info!("Pause called (not yet implemented)");
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self) -> Result<()> {
|
pub fn stop(&mut self) -> Result<()> {
|
||||||
// TODO: Implement stop in Phase 3
|
self.mpv
|
||||||
tracing::info!("Stop called (not yet implemented)");
|
.command("stop", &[])
|
||||||
|
.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<()> {
|
||||||
// TODO: Implement volume control in Phase 3
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user