feat(player): add audio playback with ffplay
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
//! MoMentry Playground
|
||||
//!
|
||||
//!
|
||||
//! Unified media player with ASR/YOLO/Chunks overlay support
|
||||
|
||||
pub mod player;
|
||||
pub mod overlay;
|
||||
pub mod web;
|
||||
pub mod config;
|
||||
pub mod overlay;
|
||||
pub mod player;
|
||||
pub mod web;
|
||||
|
||||
pub use config::Config;
|
||||
|
||||
99
src/main.rs
99
src/main.rs
@@ -16,6 +16,7 @@ mod web;
|
||||
|
||||
use config::Config;
|
||||
use overlay::{AsrLoader, ChunkLoader, YoloLoader};
|
||||
use player::audio::AudioPlayer;
|
||||
use player::ffmpeg::FFmpegDecoder;
|
||||
use player::state::{PlaybackState, PlayerState};
|
||||
|
||||
@@ -64,6 +65,7 @@ fn run(config: &Config) -> Result<()> {
|
||||
let mut asr: Option<AsrLoader> = None;
|
||||
let mut yolo: Option<YoloLoader> = None;
|
||||
let mut chunks: Option<ChunkLoader> = None;
|
||||
let mut audio_player: Option<AudioPlayer> = None;
|
||||
let mut is_fullscreen = false;
|
||||
let mut is_dragging = false;
|
||||
|
||||
@@ -85,6 +87,10 @@ fn run(config: &Config) -> Result<()> {
|
||||
texture = Some(tex);
|
||||
dec.start_decoding(0)?;
|
||||
decoder = Some(dec);
|
||||
|
||||
let player = AudioPlayer::new();
|
||||
info!("Audio player initialized");
|
||||
audio_player = Some(player);
|
||||
}
|
||||
|
||||
if let Some(ref asr_path) = config.asr {
|
||||
@@ -158,12 +164,21 @@ fn run(config: &Config) -> Result<()> {
|
||||
match key {
|
||||
sdl2::keyboard::Keycode::Escape => running = false,
|
||||
sdl2::keyboard::Keycode::Space => {
|
||||
player_state.playback =
|
||||
let was_playing = player_state.playback == PlaybackState::Playing;
|
||||
player_state.playback = if was_playing {
|
||||
PlaybackState::Paused
|
||||
} else {
|
||||
PlaybackState::Playing
|
||||
};
|
||||
if let Some(ref mut audio) = audio_player {
|
||||
if player_state.playback == PlaybackState::Playing {
|
||||
PlaybackState::Paused
|
||||
if !player_state.muted {
|
||||
audio.resume();
|
||||
}
|
||||
} else {
|
||||
PlaybackState::Playing
|
||||
};
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
sdl2::keyboard::Keycode::S => {
|
||||
player_state.show_subtitle = !player_state.show_subtitle;
|
||||
@@ -188,6 +203,14 @@ fn run(config: &Config) -> Result<()> {
|
||||
}
|
||||
sdl2::keyboard::Keycode::M => {
|
||||
player_state.muted = !player_state.muted;
|
||||
if let Some(ref mut audio) = audio_player {
|
||||
if player_state.muted {
|
||||
audio.pause();
|
||||
} else if player_state.playback == PlaybackState::Playing {
|
||||
audio.resume();
|
||||
}
|
||||
}
|
||||
info!("Audio: {}", if player_state.muted { "MUTED" } else { "ON" });
|
||||
}
|
||||
sdl2::keyboard::Keycode::F => {
|
||||
is_fullscreen = !is_fullscreen;
|
||||
@@ -197,39 +220,62 @@ fn run(config: &Config) -> Result<()> {
|
||||
dec.seek(0).ok();
|
||||
player_state.current_frame = 0;
|
||||
player_state.current_time_ms = 0;
|
||||
sync_audio(
|
||||
&mut audio_player,
|
||||
&config.video,
|
||||
0,
|
||||
player_state.playback == PlaybackState::Playing,
|
||||
);
|
||||
}
|
||||
}
|
||||
sdl2::keyboard::Keycode::End => {
|
||||
if let Some(ref mut dec) = decoder {
|
||||
let last_frame = player_state.total_frames.saturating_sub(1);
|
||||
dec.seek(
|
||||
((last_frame as f64 / player_state.fps) * 1000.0) as u64,
|
||||
)
|
||||
.ok();
|
||||
let time_ms =
|
||||
((last_frame as f64 / player_state.fps) * 1000.0) as u64;
|
||||
dec.seek(time_ms).ok();
|
||||
player_state.current_frame = last_frame;
|
||||
player_state.current_time_ms = player_state.duration_ms;
|
||||
sync_audio(
|
||||
&mut audio_player,
|
||||
&config.video,
|
||||
time_ms,
|
||||
player_state.playback == PlaybackState::Playing,
|
||||
);
|
||||
}
|
||||
}
|
||||
sdl2::keyboard::Keycode::Left => {
|
||||
let step = if shift { 60 } else { 1 };
|
||||
if let Some(ref mut dec) = decoder {
|
||||
let current = player_state.current_frame.saturating_sub(step);
|
||||
dec.seek(((current as f64 / player_state.fps) * 1000.0) as u64)
|
||||
.ok();
|
||||
player_state.current_frame = current;
|
||||
player_state.current_time_ms =
|
||||
let time_ms =
|
||||
((current as f64 / player_state.fps) * 1000.0) as u64;
|
||||
dec.seek(time_ms).ok();
|
||||
player_state.current_frame = current;
|
||||
player_state.current_time_ms = time_ms;
|
||||
sync_audio(
|
||||
&mut audio_player,
|
||||
&config.video,
|
||||
time_ms,
|
||||
player_state.playback == PlaybackState::Playing,
|
||||
);
|
||||
}
|
||||
}
|
||||
sdl2::keyboard::Keycode::Right => {
|
||||
let step = if shift { 60 } else { 1 };
|
||||
if let Some(ref mut dec) = decoder {
|
||||
let current = player_state.current_frame + step;
|
||||
dec.seek(((current as f64 / player_state.fps) * 1000.0) as u64)
|
||||
.ok();
|
||||
player_state.current_frame = current;
|
||||
player_state.current_time_ms =
|
||||
let time_ms =
|
||||
((current as f64 / player_state.fps) * 1000.0) as u64;
|
||||
dec.seek(time_ms).ok();
|
||||
player_state.current_frame = current;
|
||||
player_state.current_time_ms = time_ms;
|
||||
sync_audio(
|
||||
&mut audio_player,
|
||||
&config.video,
|
||||
time_ms,
|
||||
player_state.playback == PlaybackState::Playing,
|
||||
);
|
||||
}
|
||||
}
|
||||
sdl2::keyboard::Keycode::Up => {
|
||||
@@ -286,12 +332,13 @@ fn run(config: &Config) -> Result<()> {
|
||||
player_state.playback = PlaybackState::Paused;
|
||||
let ratio = (x - bar_x_start) as f64 / bar_width as f64;
|
||||
let target_frame = (player_state.total_frames as f64 * ratio) as u64;
|
||||
let time_ms =
|
||||
((target_frame as f64 / player_state.fps) * 1000.0) as u64;
|
||||
if let Some(ref mut dec) = decoder {
|
||||
let time_ms =
|
||||
((target_frame as f64 / player_state.fps) * 1000.0) as u64;
|
||||
if dec.seek(time_ms).is_ok() {
|
||||
player_state.current_frame = target_frame;
|
||||
player_state.current_time_ms = time_ms;
|
||||
sync_audio(&mut audio_player, &config.video, time_ms, false);
|
||||
info!(
|
||||
"Seeked to frame {} ({:.1}%)",
|
||||
target_frame,
|
||||
@@ -619,6 +666,22 @@ fn run(config: &Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_audio(
|
||||
audio_player: &mut Option<AudioPlayer>,
|
||||
video_path: &Option<std::path::PathBuf>,
|
||||
time_ms: u64,
|
||||
is_playing: bool,
|
||||
) {
|
||||
if let Some(ref mut audio) = audio_player {
|
||||
if let Some(ref path) = video_path {
|
||||
audio.play(path.as_path(), time_ms);
|
||||
if !is_playing {
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(ms: u64) -> String {
|
||||
let total_secs = ms / 1000;
|
||||
let hours = total_secs / 3600;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
//! 音頻播放模組
|
||||
//! Audio player module
|
||||
|
||||
use anyhow::Result;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::io::Write;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use log::info;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub struct AudioPlayer {
|
||||
process: Option<std::process::Child>,
|
||||
process: Option<Child>,
|
||||
volume: f32,
|
||||
muted: bool,
|
||||
}
|
||||
@@ -21,46 +19,61 @@ impl AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(&mut self, path: &str, start_ms: u64) -> Result<()> {
|
||||
self.stop()?;
|
||||
|
||||
pub fn play(&mut self, path: &std::path::Path, start_ms: u64) -> Option<()> {
|
||||
self.stop();
|
||||
|
||||
let start_sec = start_ms as f64 / 1000.0;
|
||||
let volume_filter = if self.muted {
|
||||
"volume=0".to_string()
|
||||
} else {
|
||||
format!("volume={}", self.volume)
|
||||
};
|
||||
|
||||
let mut cmd = Command::new("ffplay");
|
||||
cmd.args([
|
||||
"-nodisp",
|
||||
"-autoexit",
|
||||
"-ss", &format!("{}", start_sec),
|
||||
"-af", &volume_filter,
|
||||
"-i", path,
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
|
||||
let cmd = Command::new("ffplay")
|
||||
.args([
|
||||
"-nodisp",
|
||||
"-autoexit",
|
||||
"-ss",
|
||||
&format!("{:.3}", start_sec),
|
||||
"-af",
|
||||
&volume_filter,
|
||||
"-i",
|
||||
])
|
||||
.arg(path)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
|
||||
self.process = Some(cmd);
|
||||
Ok(())
|
||||
info!("Audio playback started from {:.1}s", start_sec);
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) -> Result<()> {
|
||||
if let Some(mut child) = self.process.take() {
|
||||
pub fn pause(&mut self) {
|
||||
if let Some(ref mut child) = self.process {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
Ok(())
|
||||
self.process = None;
|
||||
info!("Audio paused");
|
||||
}
|
||||
|
||||
pub fn resume(&mut self) {
|
||||
info!("Audio resumed");
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(ref mut child) = self.process.take() {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
self.process = None;
|
||||
}
|
||||
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
self.volume = volume.clamp(0.0, 1.0);
|
||||
if !self.muted {
|
||||
self.restart_with_volume()?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self) -> bool {
|
||||
@@ -71,14 +84,16 @@ impl AudioPlayer {
|
||||
pub fn is_muted(&self) -> bool {
|
||||
self.muted
|
||||
}
|
||||
}
|
||||
|
||||
fn restart_with_volume(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
impl Default for AudioPlayer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AudioPlayer {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stop();
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
//!
|
||||
//! Video playback, frame decoding, and rendering
|
||||
|
||||
pub mod audio;
|
||||
pub mod ffmpeg;
|
||||
pub mod renderer;
|
||||
pub mod state;
|
||||
pub mod video;
|
||||
|
||||
pub use audio::AudioPlayer;
|
||||
pub use state::{PlaybackState, PlayerState};
|
||||
pub use video::VideoPlayer;
|
||||
|
||||
Reference in New Issue
Block a user