diff --git a/src/config.rs b/src/config.rs index 3f03f33..602d6dc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,7 +33,7 @@ pub struct Config { #[arg( short = 'h', long = "height", - default_value = "720", + default_value = "1053", help = "Window height" )] pub height: u32, @@ -47,6 +47,30 @@ pub struct Config { help = "UI language (en, zh-TW, etc.)" )] pub locale: String, + + #[arg( + short = 's', + long = "start", + default_value = "0", + help = "Start time in seconds" + )] + pub start_seconds: f64, + + #[arg( + short = 'e', + long = "end", + default_value = "0", + help = "End time in seconds (0 = no limit)" + )] + pub end_seconds: f64, + + #[arg( + short = 'l', + long = "loop", + default_value = "false", + help = "Loop playback between start and end" + )] + pub loop_playback: bool, } impl Config { diff --git a/src/main.rs b/src/main.rs index 69a2305..0e25451 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,8 +45,13 @@ fn run(config: &Config) -> Result<()> { let ttf_context = ttf::init().map_err(|e| anyhow::anyhow!("TTF init failed: {}", e))?; let font: Option = ttf_context - .load_font("/System/Library/Fonts/Supplemental/Arial.ttf", 16) - .ok(); + .load_font("/System/Library/Fonts/Supplemental/Songti.ttc", 24) + .ok() + .or_else(|| { + ttf_context + .load_font("/System/Library/Fonts/Supplemental/Arial.ttf", 24) + .ok() + }); let window = video_subsystem .window("MoMentry Playground", config.width, config.height) @@ -98,6 +103,27 @@ fn run(config: &Config) -> Result<()> { dec.start_decoding(0)?; decoder = Some(dec); + let is_reverse_init = config.loop_playback + && config.start_seconds > config.end_seconds + && config.end_seconds > 0.0; + + if let Some(ref mut dec) = decoder { + if is_reverse_init { + let start_ms = (config.start_seconds * 1000.0) as u64; + let end_ms = (config.end_seconds * 1000.0) as u64; + dec.enable_reverse(start_ms, end_ms); + dec.fill_reverse_buffer(600).ok(); + info!( + "Reverse playback enabled ({}s -> {}s)", + config.start_seconds, config.end_seconds + ); + } else if config.start_seconds > 0.0 { + let start_ms = (config.start_seconds * 1000.0) as u64; + dec.seek(start_ms).ok(); + info!("Seeked to {:.1}s", config.start_seconds); + } + } + let player = AudioPlayer::new(); info!("Audio player initialized"); audio_player = Some(player); @@ -149,22 +175,51 @@ fn run(config: &Config) -> Result<()> { } let mut player_state = PlayerState::default(); + let mut reached_end = false; + + let is_reverse = config.loop_playback + && config.start_seconds > config.end_seconds + && config.end_seconds > 0.0; + if is_reverse { + player_state.muted = true; + } + 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; } + if config.start_seconds > 0.0 { + let time_ms = (config.start_seconds * 1000.0) as u64; + player_state.current_time_ms = time_ms; + if let Some(ref info) = video_info { + player_state.current_frame = (config.start_seconds * info.fps) as u64; + } + } + 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; + let text_input = video_subsystem.text_input(); + + let header_height: i32 = 60; + let status_bar_height: i32 = 60; + let search_bar_height: i32 = 80; + let video_content_top: i32 = 230; + let video_content_height: i32 = 700; + let asr_top: i32 = 930; + let asr_height: i32 = 80; + let progress_y: i32 = 1010; + let progress_height: i32 = 43; info!("Main loop started - waiting for events..."); let mut running = true; + let mut last_frame_time = std::time::Instant::now(); + let frame_duration = std::time::Duration::from_millis(16); // ~60fps + while running { for event in event_pump.poll_iter() { match event { @@ -183,6 +238,7 @@ fn run(config: &Config) -> Result<()> { match key { sdl2::keyboard::Keycode::Escape => { if show_search { + let _ = text_input.stop(); show_search = false; search_query.clear(); search_results.clear(); @@ -193,21 +249,27 @@ fn run(config: &Config) -> Result<()> { } } sdl2::keyboard::Keycode::H => { - show_help = !show_help; + if !show_search { + show_help = !show_help; + } } sdl2::keyboard::Keycode::Slash => { if searcher.is_some() { show_search = true; search_query.clear(); search_results.clear(); + let _ = text_input.start(); info!("Search mode enabled"); } } sdl2::keyboard::Keycode::Return | sdl2::keyboard::Keycode::KpEnter => { if show_search && !search_query.is_empty() { - if let Some(ref search) = searcher { - search_results = search.search(&search_query); - info!("Found {} results", search_results.len()); + let query = search_query.trim_start_matches('/').trim(); + if !query.is_empty() { + if let Some(ref search) = searcher { + search_results = search.search(query); + info!("Found {} results", search_results.len()); + } } } } @@ -250,6 +312,7 @@ fn run(config: &Config) -> Result<()> { &config.video, result.time_ms, false, + is_reverse, ); info!("Jumped to: {}", result.text); } @@ -258,53 +321,75 @@ fn run(config: &Config) -> Result<()> { } } sdl2::keyboard::Keycode::Space => { - 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 { - if !player_state.muted { - audio.resume(); - } + if !show_search { + let was_playing = + player_state.playback == PlaybackState::Playing; + player_state.playback = if was_playing { + PlaybackState::Paused } else { - audio.pause(); + PlaybackState::Playing + }; + if let Some(ref mut audio) = audio_player { + if player_state.playback == PlaybackState::Playing { + if !player_state.muted && !is_reverse { + audio.resume( + config.video.as_ref().unwrap(), + player_state.current_time_ms, + ); + } + } else { + audio.pause(); + } } } } sdl2::keyboard::Keycode::S => { - player_state.show_subtitle = !player_state.show_subtitle; - info!( - "Subtitle: {}", - if player_state.show_subtitle { - "ON" - } else { - "OFF" - } - ); + if !show_search { + 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" } - ); + if !show_search { + 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; + if !show_search { + player_state.show_chunks = !player_state.show_chunks; + } } 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(); + if !show_search { + 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 + && !is_reverse + { + audio.resume( + config.video.as_ref().unwrap(), + player_state.current_time_ms, + ); + } } + info!( + "Audio: {}", + if player_state.muted { "MUTED" } else { "ON" } + ); } - info!("Audio: {}", if player_state.muted { "MUTED" } else { "ON" }); } sdl2::keyboard::Keycode::F => { is_fullscreen = !is_fullscreen; @@ -319,6 +404,7 @@ fn run(config: &Config) -> Result<()> { &config.video, 0, player_state.playback == PlaybackState::Playing, + is_reverse, ); } } @@ -335,6 +421,7 @@ fn run(config: &Config) -> Result<()> { &config.video, time_ms, player_state.playback == PlaybackState::Playing, + is_reverse, ); } } @@ -352,6 +439,7 @@ fn run(config: &Config) -> Result<()> { &config.video, time_ms, player_state.playback == PlaybackState::Playing, + is_reverse, ); } } @@ -369,6 +457,7 @@ fn run(config: &Config) -> Result<()> { &config.video, time_ms, player_state.playback == PlaybackState::Playing, + is_reverse, ); } } @@ -464,7 +553,13 @@ fn run(config: &Config) -> Result<()> { 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); + sync_audio( + &mut audio_player, + &config.video, + time_ms, + false, + is_reverse, + ); info!( "Seeked to frame {} ({:.1}%)", target_frame, @@ -497,8 +592,11 @@ fn run(config: &Config) -> Result<()> { }; if let Some(ref mut audio) = audio_player { if player_state.playback == PlaybackState::Playing { - if !player_state.muted { - audio.resume(); + if !player_state.muted && !is_reverse { + audio.resume( + config.video.as_ref().unwrap(), + player_state.current_time_ms, + ); } } else { audio.pause(); @@ -563,29 +661,117 @@ fn run(config: &Config) -> Result<()> { }; 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; + let is_reverse = config.loop_playback + && config.start_seconds > config.end_seconds + && config.end_seconds > 0.0; - tex.update(None, &data, (info.width * 3) as usize) - .map_err(|e| anyhow::anyhow!("Texture update failed: {}", e)) - .ok(); + if player_state.playback == PlaybackState::Playing { + if config.end_seconds > 0.0 { + let end_time_ms = (config.end_seconds * 1000.0) as u64; + let start_time_ms = (config.start_seconds * 1000.0) as u64; + let should_loop = config.loop_playback && config.start_seconds > 0.0; + + let reached_boundary = if is_reverse { + player_state.current_time_ms <= end_time_ms + } else { + player_state.current_time_ms >= end_time_ms + }; + + if reached_boundary { + if should_loop { + if let Some(ref mut dec) = decoder { + dec.seek(start_time_ms).ok(); + if is_reverse { + dec.enable_reverse(start_time_ms, end_time_ms); } + player_state.current_time_ms = start_time_ms; + if let Some(ref info) = video_info { + player_state.current_frame = + (config.start_seconds * info.fps) as u64; + } + if let Some(ref mut audio) = audio_player { + audio.pause(); + } + info!("Looping back to {:.1}s", config.start_seconds); } - Ok(None) => { - info!("Playback ended"); - break; + } else if !reached_end { + reached_end = true; + info!("Reached end time: {:.1}s", config.end_seconds); + player_state.playback = PlaybackState::Paused; + if let Some(ref mut audio) = audio_player { + audio.pause(); } - Err(e) => { - warn!("Frame read error: {}", e); - break; + } + } + } + } + + let should_render_frame = player_state.playback == PlaybackState::Playing; + + if should_render_frame { + let elapsed = last_frame_time.elapsed(); + let frame_duration_ms = 16; + let target_frame_duration = std::time::Duration::from_millis(frame_duration_ms); + + if elapsed >= target_frame_duration { + last_frame_time = std::time::Instant::now(); + + if let Some(ref mut dec) = decoder { + let fps = video_info.as_ref().map(|i| i.fps).unwrap_or(30.0); + + if is_reverse { + if dec.buffer_size() == 0 { + let start_ms = (config.start_seconds * 1000.0) as u64; + let end_ms = (config.end_seconds * 1000.0) as u64; + dec.enable_reverse(start_ms, end_ms); + dec.fill_reverse_buffer(600).ok(); + player_state.current_time_ms = start_ms; + player_state.current_frame = (config.start_seconds * fps) as u64; + } else if dec.buffer_size() < 100 { + dec.refill_reverse_buffer(600).ok(); + } + + if let Some(data) = dec.get_buffer_front() { + if let Some(ref mut tex) = texture { + tex.update( + None, + &data, + (video_info.as_ref().unwrap().width * 3) as usize, + ) + .map_err(|e| anyhow::anyhow!("Texture update failed: {}", e)) + .ok(); + } + player_state.current_time_ms = + dec.get_frame_time().unwrap_or(player_state.current_time_ms); + player_state.current_frame = + player_state.current_frame.saturating_sub(1); + } + } else { + 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; + + if let Some(ref mut tex) = texture { + tex.update(None, &data, (info.width * 3) as usize) + .map_err(|e| { + anyhow::anyhow!("Texture update failed: {}", e) + }) + .ok(); + } + } + } + Ok(None) => { + if player_state.playback == PlaybackState::Playing { + player_state.playback = PlaybackState::Paused; + } + } + Err(e) => { + warn!("Frame read error: {}", e); + } } } } @@ -597,12 +783,13 @@ fn run(config: &Config) -> Result<()> { .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 search_bar_offset = if show_search { search_bar_height } else { 0 }; + let video_top = video_content_top + search_bar_offset; 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; + let scale_y = video_content_height as f64 / vid_height as f64; scale_x.min(scale_y).min(1.0) }; @@ -610,7 +797,8 @@ fn run(config: &Config) -> Result<()> { 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 + let offset_y = video_top + + ((video_content_height as i32 - scaled_h as i32) / 2) as i32 + player_state.pan_y as i32; if let Some(ref mut tex) = texture { @@ -664,18 +852,15 @@ fn run(config: &Config) -> Result<()> { { 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 y = asr_top + (asr_height - query.height as i32) / 2; 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)); + canvas.set_draw_color(sdl2::pixels::Color::RGBA(0, 0, 0, 180)); let _ = canvas.fill_rect(Rect::new( - rect.x() - 6, - rect.y() - 3, - rect.width() + 12, - rect.height() + 6, + rect.x() - 8, + rect.y() - 4, + rect.width() + 16, + rect.height() + 8, )); canvas.copy(&tex_label, None, Some(rect)).ok(); } @@ -685,6 +870,9 @@ fn run(config: &Config) -> Result<()> { } } + canvas.set_draw_color(sdl2::pixels::Color::RGBA(30, 30, 40, 200)); + let _ = canvas.fill_rect(Rect::new(0, 60, config.width, 60)); + 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); @@ -722,14 +910,7 @@ fn run(config: &Config) -> Result<()> { .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, - )); + let rect = Rect::new(10, 5, surface.width(), surface.height()); canvas.copy(&tex_label, None, Some(rect)).ok(); } } @@ -738,21 +919,13 @@ fn run(config: &Config) -> Result<()> { .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, - )); + let rect = Rect::new(10, 70, surface.width(), surface.height()); canvas.copy(&tex_label, None, Some(rect)).ok(); } } } - let progress_y = config.height as i32 - progress_height + 5; - let progress_bar_height = 8i32; + let progress_bar_height = 20i32; canvas.set_draw_color(sdl2::pixels::Color::RGB(50, 50, 50)); let _ = canvas.fill_rect(Rect::new( 10, @@ -805,7 +978,7 @@ fn run(config: &Config) -> Result<()> { { let rect = Rect::new( bar_start + bar_width - surface.width() as i32 - 10, - progress_y - 25, + progress_y - 22, surface.width(), surface.height(), ); @@ -827,21 +1000,13 @@ fn run(config: &Config) -> Result<()> { if show_search { if let Some(ref font) = font { - let search_bg_h = 250u32; - let search_bg_y = (config.height as i32 - search_bg_h as i32) / 2; - canvas.set_draw_color(sdl2::pixels::Color::RGBA(20, 20, 30, 230)); + let search_bg_y = header_height + status_bar_height; + canvas.set_draw_color(sdl2::pixels::Color::RGBA(20, 20, 30, 240)); let _ = canvas.fill_rect(Rect::new( - config.width as i32 / 2 - 300, + 0, search_bg_y, - 600, - search_bg_h, - )); - canvas.set_draw_color(sdl2::pixels::Color::RGBA(60, 60, 80, 255)); - let _ = canvas.draw_rect(Rect::new( - config.width as i32 / 2 - 300, - search_bg_y, - 600, - search_bg_h, + config.width as u32, + search_bar_height as u32, )); let input_text = if search_query.is_empty() { @@ -851,56 +1016,36 @@ fn run(config: &Config) -> Result<()> { }; if let Ok(surface) = font .render(&input_text) - .solid(sdl2::pixels::Color::RGB(200, 200, 200)) + .solid(sdl2::pixels::Color::RGB(220, 220, 220)) { if let Ok(tex) = texture_creator.create_texture_from_surface(&surface) { - let rect = Rect::new( - config.width as i32 / 2 - 290, - search_bg_y + 10, - surface.width(), - surface.height(), - ); + let rect = + Rect::new(20, search_bg_y + 10, surface.width(), surface.height()); canvas.copy(&tex, None, Some(rect)).ok(); } } if !search_results.is_empty() { - let mut y_offset = search_bg_y + 40; + let mut x_offset = 400; for (i, result) in search_results.iter().take(6).enumerate() { let result_text = format!("[{}] {}", i + 1, result.text); let color = if i == 0 { - sdl2::pixels::Color::RGB(100, 200, 255) + sdl2::pixels::Color::RGB(100, 220, 255) } else { sdl2::pixels::Color::RGB(180, 180, 180) }; if let Ok(surface) = font.render(&result_text).solid(color) { if let Ok(tex) = texture_creator.create_texture_from_surface(&surface) { let rect = Rect::new( - config.width as i32 / 2 - 290, - y_offset, - surface.width().min(580), + x_offset, + search_bg_y + 10, + surface.width().min(500), surface.height(), ); canvas.copy(&tex, None, Some(rect)).ok(); } } - y_offset += 25; - } - } else if !search_query.is_empty() { - let hint = "Press Enter to search..."; - if let Ok(surface) = font - .render(hint) - .solid(sdl2::pixels::Color::RGB(150, 150, 150)) - { - if let Ok(tex) = texture_creator.create_texture_from_surface(&surface) { - let rect = Rect::new( - config.width as i32 / 2 - 290, - search_bg_y + 40, - surface.width(), - surface.height(), - ); - canvas.copy(&tex, None, Some(rect)).ok(); - } + x_offset += 520; } } } @@ -993,11 +1138,20 @@ fn sync_audio( video_path: &Option, time_ms: u64, is_playing: bool, + is_reverse: 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 { + info!( + "sync_audio called: is_reverse={}, time_ms={}", + is_reverse, time_ms + ); + if !is_reverse { + audio.play(path.as_path(), time_ms); + if !is_playing { + audio.pause(); + } + } else { audio.pause(); } } diff --git a/src/player/audio.rs b/src/player/audio.rs index 61b80ca..207b34e 100644 --- a/src/player/audio.rs +++ b/src/player/audio.rs @@ -60,8 +60,8 @@ impl AudioPlayer { info!("Audio paused"); } - pub fn resume(&mut self) { - info!("Audio resumed"); + pub fn resume(&mut self, path: &std::path::Path, current_time_ms: u64) -> Option<()> { + self.play(path, current_time_ms) } pub fn stop(&mut self) { diff --git a/src/player/ffmpeg.rs b/src/player/ffmpeg.rs index 6521ee1..0e5136e 100644 --- a/src/player/ffmpeg.rs +++ b/src/player/ffmpeg.rs @@ -1,6 +1,7 @@ //! FFmpeg wrapper use anyhow::{Context, Result}; +use std::collections::VecDeque; use std::io::{BufReader, Read}; use std::path::Path; use std::process::{Child, ChildStdout, Command, Stdio}; @@ -20,6 +21,14 @@ pub struct FFmpegDecoder { process: Option, stdout: Option>, info: VideoInfo, + frame_buffer: VecDeque>, + frame_times: VecDeque, + buffer_loaded: bool, + current_read_ms: u64, + display_time_ms: u64, + reverse_start_ms: u64, + reverse_end_ms: u64, + in_reverse_mode: bool, } impl FFmpegDecoder { @@ -32,6 +41,14 @@ impl FFmpegDecoder { process: None, stdout: None, info, + frame_buffer: VecDeque::new(), + frame_times: VecDeque::new(), + buffer_loaded: false, + current_read_ms: 0, + display_time_ms: 0, + reverse_start_ms: 0, + reverse_end_ms: 0, + in_reverse_mode: false, }) } @@ -124,12 +141,39 @@ impl FFmpegDecoder { self.process = Some(child); self.stdout = Some(BufReader::new(stdout)); + self.current_read_ms = start_ms; Ok(()) } pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> { - self.start_decoding(timestamp_ms) + self.in_reverse_mode = false; + self.frame_buffer.clear(); + self.current_read_ms = timestamp_ms; + self.start_decoding(timestamp_ms)?; + let frame_size = (self.info.width * self.info.height * 3) as usize; + for _ in 0..5 { + let mut dummy = vec![0u8; frame_size]; + if let Some(ref mut reader) = self.stdout { + if reader.read_exact(&mut dummy).is_err() { + break; + } + } + } + Ok(()) + } + + pub fn enable_reverse(&mut self, start_ms: u64, end_ms: u64) { + self.stop(); + self.frame_buffer.clear(); + self.frame_times.clear(); + self.buffer_loaded = false; + self.reverse_start_ms = start_ms; + self.reverse_end_ms = end_ms; + self.current_read_ms = end_ms; + self.display_time_ms = start_ms; + self.in_reverse_mode = true; + self.start_decoding(end_ms).ok(); } pub fn stop(&mut self) { @@ -142,11 +186,33 @@ impl FFmpegDecoder { pub fn read_frame(&mut self) -> Result>> { let frame_size = (self.info.width * self.info.height * 3) as usize; + + if let Some(frame) = self.frame_buffer.pop_front() { + if self.in_reverse_mode && self.frame_buffer.len() < 300 { + let _ = self.refill_reverse_buffer(600); + } + return Ok(Some(frame)); + } + + if self.in_reverse_mode { + if self.current_read_ms <= self.reverse_end_ms { + return Ok(None); + } + let _ = self.refill_reverse_buffer(30); + if let Some(frame) = self.frame_buffer.pop_front() { + return Ok(Some(frame)); + } + return Ok(None); + } + let mut buffer = vec![0u8; frame_size]; if let Some(ref mut reader) = self.stdout { match reader.read_exact(&mut buffer) { - Ok(_) => Ok(Some(buffer)), + Ok(_) => { + self.current_read_ms += 16; + Ok(Some(buffer)) + } Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None), Err(e) => Err(e.into()), } @@ -154,6 +220,133 @@ impl FFmpegDecoder { Ok(None) } } + + pub fn fill_reverse_buffer(&mut self, num_frames: usize) -> Result<()> { + let frame_size = (self.info.width * self.info.height * 3) as usize; + self.frame_buffer.clear(); + self.frame_times.clear(); + + let frame_duration_ms = (1000.0 / self.info.fps) as u64; + let total_range = self.reverse_start_ms.saturating_sub(self.reverse_end_ms); + let max_frames = (total_range / frame_duration_ms).max(1) as usize; + let frames_to_read = max_frames.min(num_frames); + + let mut temp_frames: Vec> = Vec::new(); + let mut temp_times: Vec = Vec::new(); + + for _ in 0..frames_to_read { + let mut buffer = vec![0u8; frame_size]; + if let Some(ref mut reader) = self.stdout { + match reader.read_exact(&mut buffer) { + Ok(_) => { + temp_frames.push(buffer); + temp_times.push(self.current_read_ms); + self.current_read_ms += frame_duration_ms; + } + Err(_) => break, + } + } else { + break; + } + } + + for i in (0..temp_frames.len()).rev() { + self.frame_buffer.push_back(temp_frames.remove(i)); + self.frame_times.push_back(temp_times.remove(i)); + } + + Ok(()) + } + + pub fn refill_reverse_buffer(&mut self, _num_frames: usize) -> Result<()> { + if !self.in_reverse_mode { + return Ok(()); + } + + self.frame_buffer.clear(); + self.frame_times.clear(); + self.current_read_ms = self.reverse_end_ms; + self.display_time_ms = self.reverse_start_ms; + + let frame_duration_ms = (1000.0 / self.info.fps) as u64; + let frames_to_read = + ((self.reverse_start_ms - self.reverse_end_ms) / frame_duration_ms).max(1) as usize; + + self.stop(); + self.start_decoding(self.reverse_end_ms)?; + + let frame_size = (self.info.width * self.info.height * 3) as usize; + + let mut temp_frames: Vec> = Vec::new(); + let mut temp_times: Vec = Vec::new(); + + for _ in 0..frames_to_read { + let mut buffer = vec![0u8; frame_size]; + if let Some(ref mut reader) = self.stdout { + match reader.read_exact(&mut buffer) { + Ok(_) => { + temp_frames.push(buffer); + temp_times.push(self.current_read_ms); + self.current_read_ms += frame_duration_ms; + } + Err(_) => break, + } + } else { + break; + } + } + + for i in (0..temp_frames.len()).rev() { + self.frame_buffer.push_back(temp_frames.remove(i)); + self.frame_times.push_back(temp_times.remove(i)); + } + + Ok(()) + } + + pub fn has_frames_in_buffer(&self) -> bool { + !self.frame_buffer.is_empty() + } + + pub fn buffer_size(&self) -> usize { + self.frame_buffer.len() + } + + pub fn get_buffer_front(&mut self) -> Option> { + self.frame_buffer.pop_front() + } + + pub fn get_frame_time(&mut self) -> Option { + if self.in_reverse_mode { + let time = self.display_time_ms; + self.display_time_ms = self + .display_time_ms + .saturating_sub((1000.0 / self.info.fps) as u64); + Some(time) + } else { + self.frame_times.pop_front() + } + } + + pub fn needs_loading(&self) -> bool { + self.in_reverse_mode && self.frame_buffer.len() < 300 + } + + pub fn get_current_time_ms(&self) -> u64 { + self.current_read_ms + } + + pub fn set_current_time_ms(&mut self, ms: u64) { + self.current_read_ms = ms; + } + + pub fn get_reverse_end_ms(&self) -> u64 { + self.reverse_end_ms + } + + pub fn is_at_boundary(&self) -> bool { + self.in_reverse_mode && self.current_read_ms <= self.reverse_end_ms + } } impl Drop for FFmpegDecoder {