601 lines
26 KiB
Rust
601 lines
26 KiB
Rust
//! MoMentry Playground - Main entry point
|
|
//!
|
|
//! Unified media player with ASR/YOLO/Chunks overlay support
|
|
|
|
use anyhow::Result;
|
|
use log::{error, info, warn};
|
|
use sdl2::pixels::PixelFormatEnum;
|
|
use sdl2::rect::Rect;
|
|
use sdl2::ttf::{self, Font};
|
|
use std::path::Path;
|
|
|
|
mod config;
|
|
mod overlay;
|
|
mod player;
|
|
mod web;
|
|
|
|
use config::Config;
|
|
use overlay::{AsrLoader, ChunkLoader, YoloLoader};
|
|
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()?;
|
|
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 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 ttf_context = ttf::init().map_err(|e| anyhow::anyhow!("TTF init failed: {}", e))?;
|
|
let font: Option<Font> = ttf_context
|
|
.load_font("/System/Library/Fonts/Supplemental/Arial.ttf", 16)
|
|
.ok();
|
|
|
|
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 video_info = None;
|
|
let mut asr: Option<AsrLoader> = None;
|
|
let mut yolo: Option<YoloLoader> = None;
|
|
let mut chunks: Option<ChunkLoader> = None;
|
|
let mut is_fullscreen = false;
|
|
|
|
if let Some(ref video_path) = config.video {
|
|
info!("Loading video: {:?}", video_path);
|
|
let path = Path::new(video_path);
|
|
let mut dec = FFmpegDecoder::new(path)?;
|
|
let info = dec.get_info();
|
|
video_info = Some(info.clone());
|
|
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) {
|
|
Ok(loader) => {
|
|
info!("ASR loaded: {} segments", loader.segment_count());
|
|
asr = Some(loader);
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to load ASR: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(ref yolo_path) = config.yolo {
|
|
info!("Loading YOLO: {:?}", yolo_path);
|
|
match YoloLoader::load(yolo_path) {
|
|
Ok(loader) => {
|
|
info!("YOLO loaded: {} frames", loader.total_frames());
|
|
yolo = Some(loader);
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to load YOLO: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(ref chunks_path) = config.chunks {
|
|
info!("Loading Chunks: {:?}", chunks_path);
|
|
match ChunkLoader::load(Path::new(chunks_path)) {
|
|
Ok(loader) => {
|
|
info!("Chunks loaded: {} scenes", loader.scene_count());
|
|
chunks = Some(loader);
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to load Chunks: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut player_state = PlayerState::default();
|
|
if let Some(ref info) = video_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))?;
|
|
|
|
let header_height: i32 = 50;
|
|
let progress_height: i32 = 30;
|
|
|
|
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, keymod, ..
|
|
} => {
|
|
if let Some(key) = keycode {
|
|
let shift = keymod.intersects(sdl2::keyboard::Mod::LSHIFTMOD)
|
|
|| keymod.intersects(sdl2::keyboard::Mod::RSHIFTMOD);
|
|
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;
|
|
info!(
|
|
"Subtitle: {}",
|
|
if player_state.show_subtitle {
|
|
"ON"
|
|
} else {
|
|
"OFF"
|
|
}
|
|
);
|
|
}
|
|
sdl2::keyboard::Keycode::Y => {
|
|
player_state.show_yolo = !player_state.show_yolo;
|
|
info!(
|
|
"YOLO: {}",
|
|
if player_state.show_yolo { "ON" } else { "OFF" }
|
|
);
|
|
}
|
|
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::F => {
|
|
is_fullscreen = !is_fullscreen;
|
|
}
|
|
sdl2::keyboard::Keycode::Home => {
|
|
if let Some(ref mut dec) = decoder {
|
|
dec.seek(0).ok();
|
|
player_state.current_frame = 0;
|
|
player_state.current_time_ms = 0;
|
|
}
|
|
}
|
|
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();
|
|
player_state.current_frame = last_frame;
|
|
player_state.current_time_ms = player_state.duration_ms;
|
|
}
|
|
}
|
|
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 =
|
|
((current as f64 / player_state.fps) * 1000.0) as u64;
|
|
}
|
|
}
|
|
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 =
|
|
((current as f64 / player_state.fps) * 1000.0) as u64;
|
|
}
|
|
}
|
|
sdl2::keyboard::Keycode::Up => {
|
|
if player_state.zoom > 1.0 {
|
|
player_state.pan_y = (player_state.pan_y - 50.0).max(-500.0);
|
|
}
|
|
}
|
|
sdl2::keyboard::Keycode::Down => {
|
|
if player_state.zoom > 1.0 {
|
|
player_state.pan_y = (player_state.pan_y + 50.0).min(500.0);
|
|
}
|
|
}
|
|
sdl2::keyboard::Keycode::Equals | sdl2::keyboard::Keycode::KpPlus => {
|
|
player_state.zoom = (player_state.zoom * 1.2).min(10.0);
|
|
}
|
|
sdl2::keyboard::Keycode::Minus | sdl2::keyboard::Keycode::KpMinus => {
|
|
player_state.zoom = (player_state.zoom / 1.2).max(0.5);
|
|
if player_state.zoom == 1.0 {
|
|
player_state.pan_x = 0.0;
|
|
player_state.pan_y = 0.0;
|
|
}
|
|
}
|
|
sdl2::keyboard::Keycode::Backquote | sdl2::keyboard::Keycode::R => {
|
|
player_state.zoom = 1.0;
|
|
player_state.pan_x = 0.0;
|
|
player_state.pan_y = 0.0;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
sdl2::event::Event::MouseWheel { y, .. } => {
|
|
if y > 0 {
|
|
player_state.zoom = (player_state.zoom * 1.1).min(10.0);
|
|
} else if y < 0 {
|
|
player_state.zoom = (player_state.zoom / 1.1).max(0.5);
|
|
if player_state.zoom == 1.0 {
|
|
player_state.pan_x = 0.0;
|
|
player_state.pan_y = 0.0;
|
|
}
|
|
}
|
|
}
|
|
sdl2::event::Event::MouseButtonDown { x, y, .. } => {
|
|
let progress_y = config.height as i32 - progress_height + 5;
|
|
let bar_height = 20i32;
|
|
if y >= progress_y
|
|
&& y <= progress_y + bar_height
|
|
&& player_state.total_frames > 0
|
|
{
|
|
let bar_x_start = 10i32;
|
|
let bar_width = config.width as i32 - 20;
|
|
if x >= bar_x_start && x <= bar_x_start + bar_width {
|
|
let ratio = (x - bar_x_start) as f64 / bar_width as f64;
|
|
let target_frame = (player_state.total_frames as f64 * ratio) 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;
|
|
info!(
|
|
"Seeked to frame {} ({:.1}%)",
|
|
target_frame,
|
|
ratio * 100.0
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
|
|
canvas.clear();
|
|
|
|
let fs_type = if is_fullscreen {
|
|
sdl2::video::FullscreenType::Desktop
|
|
} else {
|
|
sdl2::video::FullscreenType::Off
|
|
};
|
|
canvas.window_mut().set_fullscreen(fs_type).ok();
|
|
|
|
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)) => {
|
|
if let Some(ref info) = video_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))
|
|
.ok();
|
|
}
|
|
}
|
|
Ok(None) => {
|
|
info!("Playback ended");
|
|
break;
|
|
}
|
|
Err(e) => {
|
|
warn!("Frame read error: {}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let (vid_width, vid_height) = video_info
|
|
.as_ref()
|
|
.map(|i| (i.width, i.height))
|
|
.unwrap_or((config.width, config.height));
|
|
|
|
let video_area_height = config.height as i32 - header_height - progress_height;
|
|
let scale: f64 = if player_state.zoom != 1.0 {
|
|
player_state.zoom as f64
|
|
} else {
|
|
let scale_x = config.width as f64 / vid_width as f64;
|
|
let scale_y = video_area_height as f64 / vid_height as f64;
|
|
scale_x.min(scale_y).min(1.0)
|
|
};
|
|
|
|
let scaled_w = (vid_width as f64 * scale) as u32;
|
|
let scaled_h = (vid_height as f64 * scale) as u32;
|
|
let offset_x =
|
|
((config.width as i32 - scaled_w as i32) / 2) as i32 + player_state.pan_x as i32;
|
|
let offset_y = ((config.height as i32 - progress_height - scaled_h as i32) / 2) as i32
|
|
+ player_state.pan_y as i32;
|
|
|
|
if let Some(ref mut tex) = texture {
|
|
let dst = Rect::new(offset_x, offset_y, scaled_w, scaled_h);
|
|
canvas.copy(tex, None, Some(dst)).ok();
|
|
}
|
|
|
|
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 * scale) as i32 + offset_x;
|
|
let y1 = (det.y1 * scale) as i32 + offset_y;
|
|
let w = ((det.x2 - det.x1) * scale) as u32;
|
|
let h = ((det.y2 - det.y1) * scale) as u32;
|
|
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGB(0, 255, 0));
|
|
let _ = canvas.draw_rect(Rect::new(x1, y1, w, h));
|
|
|
|
if w > 30 && h > 10 {
|
|
if let Some(ref f) = font {
|
|
let label =
|
|
format!("{} {:.0}%", det.class_name, det.confidence * 100.0);
|
|
if let Ok(surface) =
|
|
f.render(&label).solid(sdl2::pixels::Color::RGB(0, 255, 0))
|
|
{
|
|
if let Ok(tex_label) =
|
|
texture_creator.create_texture_from_surface(&surface)
|
|
{
|
|
let lw = surface.width().min(w as u32);
|
|
let label_rect = Rect::new(x1, y1.saturating_sub(18), lw, 16);
|
|
canvas.copy(&tex_label, None, Some(label_rect)).ok();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if player_state.show_subtitle {
|
|
if let Some(ref asr_loader) = asr {
|
|
if let Some(text) = asr_loader.get_text_at(player_state.current_time_ms as f64) {
|
|
if let Some(ref f) = font {
|
|
if let Ok(surface) = f
|
|
.render(&text)
|
|
.blended(sdl2::pixels::Color::RGBA(255, 255, 255, 255))
|
|
{
|
|
if let Ok(tex_label) =
|
|
texture_creator.create_texture_from_surface(&surface)
|
|
{
|
|
let query = tex_label.query();
|
|
let x = (config.width - query.width) / 2;
|
|
let y = config.height as i32
|
|
- progress_height
|
|
- query.height as i32
|
|
- 10;
|
|
let rect =
|
|
Rect::new(x as i32, y as i32, query.width as u32, query.height);
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 200));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
rect.x() - 6,
|
|
rect.y() - 3,
|
|
rect.width() + 12,
|
|
rect.height() + 6,
|
|
));
|
|
canvas.copy(&tex_label, None, Some(rect)).ok();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(ref f) = font {
|
|
let time_str = format_time(player_state.current_time_ms);
|
|
let duration_str = format_time(player_state.duration_ms);
|
|
let play_icon = if player_state.playback == PlaybackState::Playing {
|
|
"▶"
|
|
} else {
|
|
"⏸"
|
|
};
|
|
let progress_pct = if player_state.total_frames > 0 {
|
|
(player_state.current_frame as f64 / player_state.total_frames as f64 * 100.0)
|
|
as u32
|
|
} else {
|
|
0
|
|
};
|
|
let line1 = format!(
|
|
"{} {:>5} / {} ({:>3}%) Frame {:>6} / {:>6} {:.2}fps",
|
|
play_icon,
|
|
time_str,
|
|
duration_str,
|
|
progress_pct,
|
|
player_state.current_frame,
|
|
player_state.total_frames,
|
|
player_state.fps
|
|
);
|
|
let line2 = format!(
|
|
"[S]ub {} [Y]olo {} [C]hunks [M]ute Zoom{:.1}x [F]ullscreen [`]Reset",
|
|
if player_state.show_subtitle {
|
|
"ON"
|
|
} else {
|
|
"OFF"
|
|
},
|
|
if player_state.show_yolo { "ON" } else { "OFF" },
|
|
player_state.zoom
|
|
);
|
|
|
|
if let Ok(surface) = f
|
|
.render(&line1)
|
|
.solid(sdl2::pixels::Color::RGB(200, 200, 200))
|
|
{
|
|
if let Ok(tex_label) = texture_creator.create_texture_from_surface(&surface) {
|
|
let rect = Rect::new(10, 10, surface.width(), surface.height());
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 150));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
5,
|
|
5,
|
|
surface.width() + 10,
|
|
surface.height() + 10,
|
|
));
|
|
canvas.copy(&tex_label, None, Some(rect)).ok();
|
|
}
|
|
}
|
|
if let Ok(surface) = f
|
|
.render(&line2)
|
|
.solid(sdl2::pixels::Color::RGB(150, 150, 150))
|
|
{
|
|
if let Ok(tex_label) = texture_creator.create_texture_from_surface(&surface) {
|
|
let rect = Rect::new(10, 30, surface.width(), surface.height());
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 150));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
5,
|
|
25,
|
|
surface.width() + 10,
|
|
surface.height() + 10,
|
|
));
|
|
canvas.copy(&tex_label, None, Some(rect)).ok();
|
|
}
|
|
}
|
|
}
|
|
|
|
let progress_y = config.height as i32 - progress_height + 5;
|
|
let progress_bar_height = 8i32;
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGB(50, 50, 50));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
10,
|
|
progress_y,
|
|
(config.width - 20) as u32,
|
|
progress_bar_height as u32,
|
|
));
|
|
|
|
if player_state.total_frames > 0 {
|
|
let progress = player_state.current_frame as f64 / player_state.total_frames as f64;
|
|
let progress_width = ((config.width - 20) as f64 * progress) as u32;
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGB(100, 150, 255));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
10,
|
|
progress_y,
|
|
progress_width,
|
|
progress_bar_height as u32,
|
|
));
|
|
|
|
if player_state.show_chunks {
|
|
if let Some(ref chunk_loader) = chunks {
|
|
let bar_start = 10i32;
|
|
let bar_width = config.width as i32 - 20;
|
|
let boundaries = chunk_loader.get_scene_boundaries();
|
|
for &boundary_frame in &boundaries {
|
|
let ratio = boundary_frame as f64 / player_state.total_frames as f64;
|
|
let x_pos = (bar_start + (bar_width as f64 * ratio) as i32)
|
|
.min(bar_start + bar_width - 1);
|
|
if x_pos > bar_start && x_pos < bar_start + bar_width {
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGB(255, 200, 50));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
x_pos,
|
|
progress_y - 2,
|
|
2,
|
|
(progress_bar_height + 4) as u32,
|
|
));
|
|
}
|
|
}
|
|
if let Some(current_scene) =
|
|
chunk_loader.get_current_scene(player_state.current_frame)
|
|
{
|
|
if let Some(ref font) = font {
|
|
let scene_text = format!("Scene {}", current_scene.scene_number);
|
|
if let Ok(surface) = font
|
|
.render(&scene_text)
|
|
.solid(sdl2::pixels::Color::RGB(255, 200, 50))
|
|
{
|
|
if let Ok(tex) =
|
|
texture_creator.create_texture_from_surface(&surface)
|
|
{
|
|
let rect = Rect::new(
|
|
bar_start + bar_width - surface.width() as i32 - 10,
|
|
progress_y - 25,
|
|
surface.width(),
|
|
surface.height(),
|
|
);
|
|
canvas.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 180));
|
|
let _ = canvas.fill_rect(Rect::new(
|
|
rect.x() - 4,
|
|
rect.y() - 2,
|
|
rect.width() + 8,
|
|
rect.height() + 4,
|
|
));
|
|
canvas.copy(&tex, None, Some(rect)).ok();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
canvas.present();
|
|
std::thread::sleep(std::time::Duration::from_millis(16));
|
|
}
|
|
|
|
info!("Application closed");
|
|
Ok(())
|
|
}
|
|
|
|
fn format_time(ms: u64) -> String {
|
|
let total_secs = ms / 1000;
|
|
let hours = total_secs / 3600;
|
|
let minutes = (total_secs % 3600) / 60;
|
|
let seconds = total_secs % 60;
|
|
let millis = ms % 1000;
|
|
if hours > 0 {
|
|
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
|
|
} else {
|
|
format!("{:02}:{:02}.{:03}", minutes, seconds, millis)
|
|
}
|
|
}
|