feat(player): add audio playback with ffplay

This commit is contained in:
accusys
2026-03-19 02:07:42 +08:00
parent a7d41b3c17
commit 342abd5aea
16 changed files with 156 additions and 72 deletions

View File

@@ -2,9 +2,9 @@
//!
//! 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;

View File

@@ -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;

View File

@@ -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,8 +19,8 @@ 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 {
@@ -31,36 +29,51 @@ impl AudioPlayer {
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();
}
}

View File

@@ -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;

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
/Users/accusys/momentry_playground/target/release/deps/momentry-d0e10ec51fcac34b.d: src/main.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
/Users/accusys/momentry_playground/target/release/deps/momentry-d0e10ec51fcac34b.d: src/main.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/audio.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
/Users/accusys/momentry_playground/target/release/deps/momentry-d0e10ec51fcac34b: src/main.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
/Users/accusys/momentry_playground/target/release/deps/momentry-d0e10ec51fcac34b: src/main.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/audio.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
src/main.rs:
src/config.rs:
@@ -9,6 +9,7 @@ src/overlay/asr.rs:
src/overlay/chunk.rs:
src/overlay/yolo.rs:
src/player/mod.rs:
src/player/audio.rs:
src/player/ffmpeg.rs:
src/player/renderer.rs:
src/player/state.rs:

View File

@@ -1,19 +1,20 @@
/Users/accusys/momentry_playground/target/release/deps/momentry_playground-f7686bde04eb7bb7.d: src/lib.rs src/player/mod.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/web/mod.rs src/web/bridge.rs src/config.rs
/Users/accusys/momentry_playground/target/release/deps/momentry_playground-f7686bde04eb7bb7.d: src/lib.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/audio.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
/Users/accusys/momentry_playground/target/release/deps/libmomentry_playground-f7686bde04eb7bb7.rlib: src/lib.rs src/player/mod.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/web/mod.rs src/web/bridge.rs src/config.rs
/Users/accusys/momentry_playground/target/release/deps/libmomentry_playground-f7686bde04eb7bb7.rlib: src/lib.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/audio.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
/Users/accusys/momentry_playground/target/release/deps/libmomentry_playground-f7686bde04eb7bb7.rmeta: src/lib.rs src/player/mod.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/web/mod.rs src/web/bridge.rs src/config.rs
/Users/accusys/momentry_playground/target/release/deps/libmomentry_playground-f7686bde04eb7bb7.rmeta: src/lib.rs src/config.rs src/overlay/mod.rs src/overlay/asr.rs src/overlay/chunk.rs src/overlay/yolo.rs src/player/mod.rs src/player/audio.rs src/player/ffmpeg.rs src/player/renderer.rs src/player/state.rs src/player/video.rs src/web/mod.rs src/web/bridge.rs
src/lib.rs:
src/player/mod.rs:
src/player/ffmpeg.rs:
src/player/renderer.rs:
src/player/state.rs:
src/player/video.rs:
src/config.rs:
src/overlay/mod.rs:
src/overlay/asr.rs:
src/overlay/chunk.rs:
src/overlay/yolo.rs:
src/player/mod.rs:
src/player/audio.rs:
src/player/ffmpeg.rs:
src/player/renderer.rs:
src/player/state.rs:
src/player/video.rs:
src/web/mod.rs:
src/web/bridge.rs:
src/config.rs:

View File

@@ -1 +1 @@
/Users/accusys/momentry_playground/target/release/libmomentry_playground.rlib: /Users/accusys/momentry_playground/src/config.rs /Users/accusys/momentry_playground/src/lib.rs /Users/accusys/momentry_playground/src/overlay/asr.rs /Users/accusys/momentry_playground/src/overlay/chunk.rs /Users/accusys/momentry_playground/src/overlay/mod.rs /Users/accusys/momentry_playground/src/overlay/yolo.rs /Users/accusys/momentry_playground/src/player/ffmpeg.rs /Users/accusys/momentry_playground/src/player/mod.rs /Users/accusys/momentry_playground/src/player/renderer.rs /Users/accusys/momentry_playground/src/player/state.rs /Users/accusys/momentry_playground/src/player/video.rs /Users/accusys/momentry_playground/src/web/bridge.rs /Users/accusys/momentry_playground/src/web/mod.rs
/Users/accusys/momentry_playground/target/release/libmomentry_playground.rlib: /Users/accusys/momentry_playground/src/config.rs /Users/accusys/momentry_playground/src/lib.rs /Users/accusys/momentry_playground/src/overlay/asr.rs /Users/accusys/momentry_playground/src/overlay/chunk.rs /Users/accusys/momentry_playground/src/overlay/mod.rs /Users/accusys/momentry_playground/src/overlay/yolo.rs /Users/accusys/momentry_playground/src/player/audio.rs /Users/accusys/momentry_playground/src/player/ffmpeg.rs /Users/accusys/momentry_playground/src/player/mod.rs /Users/accusys/momentry_playground/src/player/renderer.rs /Users/accusys/momentry_playground/src/player/state.rs /Users/accusys/momentry_playground/src/player/video.rs /Users/accusys/momentry_playground/src/web/bridge.rs /Users/accusys/momentry_playground/src/web/mod.rs

Binary file not shown.

View File

@@ -1 +1 @@
/Users/accusys/momentry_playground/target/release/momentry: /Users/accusys/momentry_playground/src/config.rs /Users/accusys/momentry_playground/src/lib.rs /Users/accusys/momentry_playground/src/main.rs /Users/accusys/momentry_playground/src/overlay/asr.rs /Users/accusys/momentry_playground/src/overlay/chunk.rs /Users/accusys/momentry_playground/src/overlay/mod.rs /Users/accusys/momentry_playground/src/overlay/yolo.rs /Users/accusys/momentry_playground/src/player/ffmpeg.rs /Users/accusys/momentry_playground/src/player/mod.rs /Users/accusys/momentry_playground/src/player/renderer.rs /Users/accusys/momentry_playground/src/player/state.rs /Users/accusys/momentry_playground/src/player/video.rs /Users/accusys/momentry_playground/src/web/bridge.rs /Users/accusys/momentry_playground/src/web/mod.rs
/Users/accusys/momentry_playground/target/release/momentry: /Users/accusys/momentry_playground/src/config.rs /Users/accusys/momentry_playground/src/lib.rs /Users/accusys/momentry_playground/src/main.rs /Users/accusys/momentry_playground/src/overlay/asr.rs /Users/accusys/momentry_playground/src/overlay/chunk.rs /Users/accusys/momentry_playground/src/overlay/mod.rs /Users/accusys/momentry_playground/src/overlay/yolo.rs /Users/accusys/momentry_playground/src/player/audio.rs /Users/accusys/momentry_playground/src/player/ffmpeg.rs /Users/accusys/momentry_playground/src/player/mod.rs /Users/accusys/momentry_playground/src/player/renderer.rs /Users/accusys/momentry_playground/src/player/state.rs /Users/accusys/momentry_playground/src/player/video.rs /Users/accusys/momentry_playground/src/web/bridge.rs /Users/accusys/momentry_playground/src/web/mod.rs