feat(player): implement SDL2 video playback with FFmpeg decoder
This commit is contained in:
192
src/main.rs
192
src/main.rs
@@ -3,8 +3,8 @@
|
||||
//! 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,15 +14,13 @@ 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();
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let config = Config::load();
|
||||
let config = Config::load()?;
|
||||
info!("MoMentry Playground starting...");
|
||||
info!("Window: {}x{}", config.width, config.height);
|
||||
|
||||
@@ -35,19 +33,46 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
);
|
||||
|
||||
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 {
|
||||
@@ -76,65 +101,113 @@ 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;
|
||||
}
|
||||
|
||||
let mut event_pump = sdl_context
|
||||
.event_pump()
|
||||
.map_err(|e| anyhow::anyhow!("Event pump failed: {}", e))?;
|
||||
|
||||
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)?;
|
||||
let mut running = true;
|
||||
while running {
|
||||
for event in event_pump.poll_iter() {
|
||||
match event {
|
||||
sdl2::event::Event::Quit { .. } => {
|
||||
running = false;
|
||||
}
|
||||
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
|
||||
canvas.clear();
|
||||
|
||||
loop {
|
||||
let start = std::time::Instant::now();
|
||||
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;
|
||||
|
||||
match video.read_frame() {
|
||||
Ok(Some(frame)) => {
|
||||
state.current_frame = frame.frame_number;
|
||||
state.current_time_ms = frame.timestamp_ms;
|
||||
tex.update(None, &data, (info.width * 3) as usize)
|
||||
.map_err(|e| anyhow::anyhow!("Texture update failed: {}", e))?;
|
||||
|
||||
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 {
|
||||
let detections = yolo_loader.get_detections(frame.frame_number);
|
||||
let detections =
|
||||
yolo_loader.get_detections(player_state.current_frame);
|
||||
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 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;
|
||||
|
||||
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) => {
|
||||
info!("Playback ended");
|
||||
@@ -145,12 +218,15 @@ fn run_playback_loop(
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user