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;
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
Binary file not shown.
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user