feat(player): implement SDL2 video playback with FFmpeg decoder

This commit is contained in:
2026-03-19 01:25:05 +08:00
parent 2d871a62c2
commit 5868f0da05

View File

@@ -3,8 +3,8 @@
//! Unified media player with ASR/YOLO/Chunks overlay support //! Unified media player with ASR/YOLO/Chunks overlay support
use anyhow::Result; use anyhow::Result;
use clap::Parser;
use log::{error, info}; use log::{error, info};
use sdl2::pixels::PixelFormatEnum;
use std::path::Path; use std::path::Path;
mod config; mod config;
@@ -14,15 +14,13 @@ mod web;
use config::Config; use config::Config;
use overlay::{AsrLoader, YoloLoader}; use overlay::{AsrLoader, YoloLoader};
use player::{Video, Renderer, PlaybackState}; use player::ffmpeg::FFmpegDecoder;
use player::state::PlayerState; use player::state::{PlaybackState, PlayerState};
fn main() -> Result<()> { fn main() -> Result<()> {
env_logger::Builder::from_env( env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
env_logger::Env::default().default_filter_or("info")
).init();
let config = Config::load(); let config = Config::load()?;
info!("MoMentry Playground starting..."); info!("MoMentry Playground starting...");
info!("Window: {}x{}", config.width, config.height); info!("Window: {}x{}", config.width, config.height);
@@ -35,19 +33,46 @@ fn main() -> Result<()> {
} }
fn run(config: &Config) -> Result<()> { fn run(config: &Config) -> Result<()> {
let mut video = Video::new(); let sdl_context = sdl2::init().map_err(|e| anyhow::anyhow!("SDL init failed: {}", e))?;
let mut renderer = Renderer::new("MoMentry Playground", config.width, config.height)?; let video_subsystem = sdl_context
.video()
.map_err(|e| anyhow::anyhow!("Video subsystem failed: {}", e))?;
let window = video_subsystem
.window("MoMentry Playground", config.width, config.height)
.position_centered()
.build()
.map_err(|e| anyhow::anyhow!("Window creation failed: {}", e))?;
let mut canvas = window
.into_canvas()
.build()
.map_err(|e| anyhow::anyhow!("Canvas creation failed: {}", e))?;
let texture_creator = canvas.texture_creator();
let mut decoder: Option<FFmpegDecoder> = None;
let mut texture: Option<sdl2::render::Texture> = None;
let mut asr: Option<AsrLoader> = None; let mut asr: Option<AsrLoader> = None;
let mut yolo: Option<YoloLoader> = None; let mut yolo: Option<YoloLoader> = None;
if let Some(ref video_path) = config.video { if let Some(ref video_path) = config.video {
info!("Loading video: {:?}", video_path); info!("Loading video: {:?}", video_path);
let info_data = video.open(video_path)?; let path = Path::new(video_path);
info!("Video info: {}x{} @ {:.2}fps, {} frames", let mut dec = FFmpegDecoder::new(path)?;
info_data.width, info_data.height, info_data.fps, info_data.total_frames); let info = dec.get_info();
info!(
"Video info: {}x{} @ {:.2}fps, {} frames",
info.width, info.height, info.fps, info.frame_count
);
renderer.create_texture(info_data.width, info_data.height)?; let tex = texture_creator
.create_texture_streaming(PixelFormatEnum::RGB24, info.width, info.height)
.map_err(|e| anyhow::anyhow!("Texture creation failed: {}", e))?;
texture = Some(tex);
dec.start_decoding(0)?;
decoder = Some(dec);
} }
if let Some(ref asr_path) = config.asr { if let Some(ref asr_path) = config.asr {
@@ -76,65 +101,113 @@ fn run(config: &Config) -> Result<()> {
} }
} }
if config.fullscreen {
renderer.set_fullscreen(true)?;
}
let mut player_state = PlayerState::default(); let mut player_state = PlayerState::default();
if let Some(info) = video.get_info() { if let Some(ref dec) = decoder {
player_state.total_frames = info.total_frames; let info = dec.get_info();
player_state.total_frames = info.frame_count;
player_state.duration_ms = info.duration_ms; player_state.duration_ms = info.duration_ms;
player_state.fps = info.fps; player_state.fps = info.fps;
} }
let mut event_pump = sdl_context
.event_pump()
.map_err(|e| anyhow::anyhow!("Event pump failed: {}", e))?;
info!("Main loop started - waiting for events..."); info!("Main loop started - waiting for events...");
if let Some(ref video_path) = config.video { let mut running = true;
video.play()?; while running {
player_state.playback = PlaybackState::Playing; for event in event_pump.poll_iter() {
run_playback_loop(&mut video, &mut renderer, &mut player_state, &mut asr, &mut yolo)?; match event {
sdl2::event::Event::Quit { .. } => {
running = false;
} }
sdl2::event::Event::KeyDown { keycode, .. } => {
loop { if let Some(key) = keycode {
std::thread::sleep(std::time::Duration::from_millis(100)); match key {
sdl2::keyboard::Keycode::Escape => running = false,
sdl2::keyboard::Keycode::Space => {
player_state.playback =
if player_state.playback == PlaybackState::Playing {
PlaybackState::Paused
} else {
PlaybackState::Playing
};
}
sdl2::keyboard::Keycode::S => {
player_state.show_subtitle = !player_state.show_subtitle;
}
sdl2::keyboard::Keycode::Y => {
player_state.show_yolo = !player_state.show_yolo;
}
sdl2::keyboard::Keycode::C => {
player_state.show_chunks = !player_state.show_chunks;
}
sdl2::keyboard::Keycode::M => {
player_state.muted = !player_state.muted;
}
sdl2::keyboard::Keycode::Left => {
if let Some(ref mut dec) = decoder {
let current = player_state.current_frame.saturating_sub(1);
dec.seek(
((current as f64 / player_state.fps) * 1000.0) as u64,
)?;
player_state.current_frame = current;
}
}
sdl2::keyboard::Keycode::Right => {
if let Some(ref mut dec) = decoder {
let current = player_state.current_frame + 1;
dec.seek(
((current as f64 / player_state.fps) * 1000.0) as u64,
)?;
player_state.current_frame = current;
}
}
_ => {}
}
}
}
_ => {}
} }
} }
fn run_playback_loop( canvas.set_draw_color(sdl2::pixels::Color::BLACK);
video: &mut Video, canvas.clear();
renderer: &mut Renderer,
state: &mut PlayerState,
asr: &mut Option<AsrLoader>,
yolo: &mut Option<YoloLoader>,
) -> Result<()> {
let frame_duration = std::time::Duration::from_millis(16);
loop { if player_state.playback == PlaybackState::Playing {
let start = std::time::Instant::now(); if let Some(ref mut dec) = decoder {
if let Some(ref mut tex) = texture {
match dec.read_frame() {
Ok(Some(data)) => {
let info = dec.get_info();
player_state.current_frame += 1;
player_state.current_time_ms =
((player_state.current_frame as f64 / info.fps) * 1000.0) as u64;
match video.read_frame() { tex.update(None, &data, (info.width * 3) as usize)
Ok(Some(frame)) => { .map_err(|e| anyhow::anyhow!("Texture update failed: {}", e))?;
state.current_frame = frame.frame_number;
state.current_time_ms = frame.timestamp_ms;
renderer.update_texture(&frame.data)?; canvas
.copy(tex, None, None)
.map_err(|e| anyhow::anyhow!("Copy failed: {}", e))?;
if state.show_yolo { if player_state.show_yolo {
if let Some(ref mut yolo_loader) = yolo { if let Some(ref mut yolo_loader) = yolo {
let detections = yolo_loader.get_detections(frame.frame_number); let detections =
yolo_loader.get_detections(player_state.current_frame);
for det in detections { for det in detections {
renderer.draw_bbox( let x1 = det.x1 as i32;
det.x1 as i32, let y1 = det.y1 as i32;
det.y1 as i32, let w = (det.x2 - det.x1) as u32;
(det.x2 - det.x1) as u32, let h = (det.y2 - det.y1) as u32;
(det.y2 - det.y1) as u32,
&det.class_name,
);
}
}
}
renderer.present(); canvas.set_draw_color(sdl2::pixels::Color::RGB(0, 255, 0));
let _ =
canvas.draw_rect(sdl2::rect::Rect::new(x1, y1, w, h));
}
}
}
} }
Ok(None) => { Ok(None) => {
info!("Playback ended"); info!("Playback ended");
@@ -145,12 +218,15 @@ fn run_playback_loop(
break; break;
} }
} }
}
let elapsed = start.elapsed();
if elapsed < frame_duration {
std::thread::sleep(frame_duration - elapsed);
} }
} }
canvas.present();
std::thread::sleep(std::time::Duration::from_millis(16));
}
info!("Application closed");
Ok(()) Ok(())
} }