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

This commit is contained in:
accusys
2026-03-19 01:23:27 +08:00
parent 2d871a62c2
commit 0b75987fd0
10 changed files with 293 additions and 233 deletions

View File

@@ -1,10 +1,10 @@
//! MoMentry Playground - Main entry point
//!
//!
//! Unified media player with ASR/YOLO/Chunks overlay support
use anyhow::Result;
use clap::Parser;
use log::{error, info};
use sdl2::pixels::PixelFormatEnum;
use std::path::Path;
mod config;
@@ -14,42 +14,67 @@ mod web;
use config::Config;
use overlay::{AsrLoader, YoloLoader};
use player::{Video, Renderer, PlaybackState};
use player::state::PlayerState;
use player::ffmpeg::FFmpegDecoder;
use player::state::{PlaybackState, PlayerState};
fn main() -> Result<()> {
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("info")
).init();
let config = Config::load();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let config = Config::load()?;
info!("MoMentry Playground starting...");
info!("Window: {}x{}", config.width, config.height);
if let Err(e) = run(&config) {
error!("Application error: {}", e);
std::process::exit(1);
}
Ok(())
}
fn run(config: &Config) -> Result<()> {
let mut video = Video::new();
let mut renderer = Renderer::new("MoMentry Playground", config.width, config.height)?;
let sdl_context = sdl2::init().map_err(|e| anyhow::anyhow!("SDL init failed: {}", e))?;
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 yolo: Option<YoloLoader> = None;
if let Some(ref video_path) = config.video {
info!("Loading video: {:?}", video_path);
let info_data = video.open(video_path)?;
info!("Video info: {}x{} @ {:.2}fps, {} frames",
info_data.width, info_data.height, info_data.fps, info_data.total_frames);
renderer.create_texture(info_data.width, info_data.height)?;
let path = Path::new(video_path);
let mut dec = FFmpegDecoder::new(path)?;
let info = dec.get_info();
info!(
"Video info: {}x{} @ {:.2}fps, {} frames",
info.width, info.height, info.fps, info.frame_count
);
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 {
info!("Loading ASR: {:?}", asr_path);
match AsrLoader::load(asr_path) {
@@ -62,7 +87,7 @@ fn run(config: &Config) -> Result<()> {
}
}
}
if let Some(ref yolo_path) = config.yolo {
info!("Loading YOLO: {:?}", yolo_path);
match YoloLoader::load(yolo_path) {
@@ -75,82 +100,133 @@ fn run(config: &Config) -> Result<()> {
}
}
}
if config.fullscreen {
renderer.set_fullscreen(true)?;
}
let mut player_state = PlayerState::default();
if let Some(info) = video.get_info() {
player_state.total_frames = info.total_frames;
if let Some(ref dec) = decoder {
let info = dec.get_info();
player_state.total_frames = info.frame_count;
player_state.duration_ms = info.duration_ms;
player_state.fps = info.fps;
}
info!("Main loop started - waiting for events...");
if let Some(ref video_path) = config.video {
video.play()?;
player_state.playback = PlaybackState::Playing;
run_playback_loop(&mut video, &mut renderer, &mut player_state, &mut asr, &mut yolo)?;
}
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
fn run_playback_loop(
video: &mut Video,
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 {
let start = std::time::Instant::now();
match video.read_frame() {
Ok(Some(frame)) => {
state.current_frame = frame.frame_number;
state.current_time_ms = frame.timestamp_ms;
renderer.update_texture(&frame.data)?;
if state.show_yolo {
if let Some(ref mut yolo_loader) = yolo {
let detections = yolo_loader.get_detections(frame.frame_number);
for det in detections {
renderer.draw_bbox(
det.x1 as i32,
det.y1 as i32,
(det.x2 - det.x1) as u32,
(det.y2 - det.y1) as u32,
&det.class_name,
);
let mut event_pump = sdl_context
.event_pump()
.map_err(|e| anyhow::anyhow!("Event pump failed: {}", e))?;
info!("Main loop started - waiting for events...");
let mut running = true;
while running {
for event in event_pump.poll_iter() {
match event {
sdl2::event::Event::Quit { .. } => {
running = false;
}
sdl2::event::Event::KeyDown { keycode, .. } => {
if let Some(key) = keycode {
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;
}
}
_ => {}
}
}
}
renderer.present();
}
Ok(None) => {
info!("Playback ended");
break;
}
Err(e) => {
error!("Frame read error: {}", e);
break;
_ => {}
}
}
let elapsed = start.elapsed();
if elapsed < frame_duration {
std::thread::sleep(frame_duration - elapsed);
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
canvas.clear();
if player_state.playback == PlaybackState::Playing {
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;
tex.update(None, &data, (info.width * 3) as usize)
.map_err(|e| anyhow::anyhow!("Texture update failed: {}", e))?;
canvas
.copy(tex, None, None)
.map_err(|e| anyhow::anyhow!("Copy failed: {}", e))?;
if player_state.show_yolo {
if let Some(ref mut yolo_loader) = yolo {
let detections =
yolo_loader.get_detections(player_state.current_frame);
for det in detections {
let x1 = det.x1 as i32;
let y1 = det.y1 as i32;
let w = (det.x2 - det.x1) as u32;
let h = (det.y2 - det.y1) as u32;
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) => {
info!("Playback ended");
break;
}
Err(e) => {
error!("Frame read error: {}", e);
break;
}
}
}
}
}
canvas.present();
std::thread::sleep(std::time::Duration::from_millis(16));
}
info!("Application closed");
Ok(())
}