fix(player): correct reverse playback frame positioning
- Read frames from reverse_end_ms instead of reverse_start_ms - Reverse buffer now displays correct video content (130s instead of 139s) - Maintain forward playback compatibility
This commit is contained in:
@@ -33,7 +33,7 @@ pub struct Config {
|
|||||||
#[arg(
|
#[arg(
|
||||||
short = 'h',
|
short = 'h',
|
||||||
long = "height",
|
long = "height",
|
||||||
default_value = "720",
|
default_value = "1053",
|
||||||
help = "Window height"
|
help = "Window height"
|
||||||
)]
|
)]
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
@@ -47,6 +47,30 @@ pub struct Config {
|
|||||||
help = "UI language (en, zh-TW, etc.)"
|
help = "UI language (en, zh-TW, etc.)"
|
||||||
)]
|
)]
|
||||||
pub locale: String,
|
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 {
|
impl Config {
|
||||||
|
|||||||
432
src/main.rs
432
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 ttf_context = ttf::init().map_err(|e| anyhow::anyhow!("TTF init failed: {}", e))?;
|
||||||
let font: Option<Font> = ttf_context
|
let font: Option<Font> = ttf_context
|
||||||
.load_font("/System/Library/Fonts/Supplemental/Arial.ttf", 16)
|
.load_font("/System/Library/Fonts/Supplemental/Songti.ttc", 24)
|
||||||
.ok();
|
.ok()
|
||||||
|
.or_else(|| {
|
||||||
|
ttf_context
|
||||||
|
.load_font("/System/Library/Fonts/Supplemental/Arial.ttf", 24)
|
||||||
|
.ok()
|
||||||
|
});
|
||||||
|
|
||||||
let window = video_subsystem
|
let window = video_subsystem
|
||||||
.window("MoMentry Playground", config.width, config.height)
|
.window("MoMentry Playground", config.width, config.height)
|
||||||
@@ -98,6 +103,27 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
dec.start_decoding(0)?;
|
dec.start_decoding(0)?;
|
||||||
decoder = Some(dec);
|
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();
|
let player = AudioPlayer::new();
|
||||||
info!("Audio player initialized");
|
info!("Audio player initialized");
|
||||||
audio_player = Some(player);
|
audio_player = Some(player);
|
||||||
@@ -149,22 +175,51 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut player_state = PlayerState::default();
|
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 {
|
if let Some(ref info) = video_info {
|
||||||
player_state.total_frames = info.frame_count;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
let mut event_pump = sdl_context
|
||||||
.event_pump()
|
.event_pump()
|
||||||
.map_err(|e| anyhow::anyhow!("Event pump failed: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Event pump failed: {}", e))?;
|
||||||
|
|
||||||
let header_height: i32 = 50;
|
let text_input = video_subsystem.text_input();
|
||||||
let progress_height: i32 = 30;
|
|
||||||
|
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...");
|
info!("Main loop started - waiting for events...");
|
||||||
|
|
||||||
let mut running = true;
|
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 {
|
while running {
|
||||||
for event in event_pump.poll_iter() {
|
for event in event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
@@ -183,6 +238,7 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
match key {
|
match key {
|
||||||
sdl2::keyboard::Keycode::Escape => {
|
sdl2::keyboard::Keycode::Escape => {
|
||||||
if show_search {
|
if show_search {
|
||||||
|
let _ = text_input.stop();
|
||||||
show_search = false;
|
show_search = false;
|
||||||
search_query.clear();
|
search_query.clear();
|
||||||
search_results.clear();
|
search_results.clear();
|
||||||
@@ -193,21 +249,27 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sdl2::keyboard::Keycode::H => {
|
sdl2::keyboard::Keycode::H => {
|
||||||
show_help = !show_help;
|
if !show_search {
|
||||||
|
show_help = !show_help;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sdl2::keyboard::Keycode::Slash => {
|
sdl2::keyboard::Keycode::Slash => {
|
||||||
if searcher.is_some() {
|
if searcher.is_some() {
|
||||||
show_search = true;
|
show_search = true;
|
||||||
search_query.clear();
|
search_query.clear();
|
||||||
search_results.clear();
|
search_results.clear();
|
||||||
|
let _ = text_input.start();
|
||||||
info!("Search mode enabled");
|
info!("Search mode enabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sdl2::keyboard::Keycode::Return | sdl2::keyboard::Keycode::KpEnter => {
|
sdl2::keyboard::Keycode::Return | sdl2::keyboard::Keycode::KpEnter => {
|
||||||
if show_search && !search_query.is_empty() {
|
if show_search && !search_query.is_empty() {
|
||||||
if let Some(ref search) = searcher {
|
let query = search_query.trim_start_matches('/').trim();
|
||||||
search_results = search.search(&search_query);
|
if !query.is_empty() {
|
||||||
info!("Found {} results", search_results.len());
|
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,
|
&config.video,
|
||||||
result.time_ms,
|
result.time_ms,
|
||||||
false,
|
false,
|
||||||
|
is_reverse,
|
||||||
);
|
);
|
||||||
info!("Jumped to: {}", result.text);
|
info!("Jumped to: {}", result.text);
|
||||||
}
|
}
|
||||||
@@ -258,53 +321,75 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sdl2::keyboard::Keycode::Space => {
|
sdl2::keyboard::Keycode::Space => {
|
||||||
let was_playing = player_state.playback == PlaybackState::Playing;
|
if !show_search {
|
||||||
player_state.playback = if was_playing {
|
let was_playing =
|
||||||
PlaybackState::Paused
|
player_state.playback == PlaybackState::Playing;
|
||||||
} else {
|
player_state.playback = if was_playing {
|
||||||
PlaybackState::Playing
|
PlaybackState::Paused
|
||||||
};
|
|
||||||
if let Some(ref mut audio) = audio_player {
|
|
||||||
if player_state.playback == PlaybackState::Playing {
|
|
||||||
if !player_state.muted {
|
|
||||||
audio.resume();
|
|
||||||
}
|
|
||||||
} else {
|
} 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 => {
|
sdl2::keyboard::Keycode::S => {
|
||||||
player_state.show_subtitle = !player_state.show_subtitle;
|
if !show_search {
|
||||||
info!(
|
player_state.show_subtitle = !player_state.show_subtitle;
|
||||||
"Subtitle: {}",
|
info!(
|
||||||
if player_state.show_subtitle {
|
"Subtitle: {}",
|
||||||
"ON"
|
if player_state.show_subtitle {
|
||||||
} else {
|
"ON"
|
||||||
"OFF"
|
} else {
|
||||||
}
|
"OFF"
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sdl2::keyboard::Keycode::Y => {
|
sdl2::keyboard::Keycode::Y => {
|
||||||
player_state.show_yolo = !player_state.show_yolo;
|
if !show_search {
|
||||||
info!(
|
player_state.show_yolo = !player_state.show_yolo;
|
||||||
"YOLO: {}",
|
info!(
|
||||||
if player_state.show_yolo { "ON" } else { "OFF" }
|
"YOLO: {}",
|
||||||
);
|
if player_state.show_yolo { "ON" } else { "OFF" }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sdl2::keyboard::Keycode::C => {
|
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 => {
|
sdl2::keyboard::Keycode::M => {
|
||||||
player_state.muted = !player_state.muted;
|
if !show_search {
|
||||||
if let Some(ref mut audio) = audio_player {
|
player_state.muted = !player_state.muted;
|
||||||
if player_state.muted {
|
if let Some(ref mut audio) = audio_player {
|
||||||
audio.pause();
|
if player_state.muted {
|
||||||
} else if player_state.playback == PlaybackState::Playing {
|
audio.pause();
|
||||||
audio.resume();
|
} 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 => {
|
sdl2::keyboard::Keycode::F => {
|
||||||
is_fullscreen = !is_fullscreen;
|
is_fullscreen = !is_fullscreen;
|
||||||
@@ -319,6 +404,7 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
&config.video,
|
&config.video,
|
||||||
0,
|
0,
|
||||||
player_state.playback == PlaybackState::Playing,
|
player_state.playback == PlaybackState::Playing,
|
||||||
|
is_reverse,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,6 +421,7 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
&config.video,
|
&config.video,
|
||||||
time_ms,
|
time_ms,
|
||||||
player_state.playback == PlaybackState::Playing,
|
player_state.playback == PlaybackState::Playing,
|
||||||
|
is_reverse,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,6 +439,7 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
&config.video,
|
&config.video,
|
||||||
time_ms,
|
time_ms,
|
||||||
player_state.playback == PlaybackState::Playing,
|
player_state.playback == PlaybackState::Playing,
|
||||||
|
is_reverse,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,6 +457,7 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
&config.video,
|
&config.video,
|
||||||
time_ms,
|
time_ms,
|
||||||
player_state.playback == PlaybackState::Playing,
|
player_state.playback == PlaybackState::Playing,
|
||||||
|
is_reverse,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -464,7 +553,13 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
if dec.seek(time_ms).is_ok() {
|
if dec.seek(time_ms).is_ok() {
|
||||||
player_state.current_frame = target_frame;
|
player_state.current_frame = target_frame;
|
||||||
player_state.current_time_ms = time_ms;
|
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!(
|
info!(
|
||||||
"Seeked to frame {} ({:.1}%)",
|
"Seeked to frame {} ({:.1}%)",
|
||||||
target_frame,
|
target_frame,
|
||||||
@@ -497,8 +592,11 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
};
|
};
|
||||||
if let Some(ref mut audio) = audio_player {
|
if let Some(ref mut audio) = audio_player {
|
||||||
if player_state.playback == PlaybackState::Playing {
|
if player_state.playback == PlaybackState::Playing {
|
||||||
if !player_state.muted {
|
if !player_state.muted && !is_reverse {
|
||||||
audio.resume();
|
audio.resume(
|
||||||
|
config.video.as_ref().unwrap(),
|
||||||
|
player_state.current_time_ms,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
audio.pause();
|
audio.pause();
|
||||||
@@ -563,29 +661,117 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
};
|
};
|
||||||
canvas.window_mut().set_fullscreen(fs_type).ok();
|
canvas.window_mut().set_fullscreen(fs_type).ok();
|
||||||
|
|
||||||
if player_state.playback == PlaybackState::Playing {
|
let is_reverse = config.loop_playback
|
||||||
if let Some(ref mut dec) = decoder {
|
&& config.start_seconds > config.end_seconds
|
||||||
if let Some(ref mut tex) = texture {
|
&& config.end_seconds > 0.0;
|
||||||
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)
|
if player_state.playback == PlaybackState::Playing {
|
||||||
.map_err(|e| anyhow::anyhow!("Texture update failed: {}", e))
|
if config.end_seconds > 0.0 {
|
||||||
.ok();
|
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) => {
|
} else if !reached_end {
|
||||||
info!("Playback ended");
|
reached_end = true;
|
||||||
break;
|
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))
|
.map(|i| (i.width, i.height))
|
||||||
.unwrap_or((config.width, config.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 {
|
let scale: f64 = if player_state.zoom != 1.0 {
|
||||||
player_state.zoom as f64
|
player_state.zoom as f64
|
||||||
} else {
|
} else {
|
||||||
let scale_x = config.width as f64 / vid_width as f64;
|
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)
|
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 scaled_h = (vid_height as f64 * scale) as u32;
|
||||||
let offset_x =
|
let offset_x =
|
||||||
((config.width as i32 - scaled_w as i32) / 2) as i32 + player_state.pan_x as i32;
|
((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;
|
+ player_state.pan_y as i32;
|
||||||
|
|
||||||
if let Some(ref mut tex) = texture {
|
if let Some(ref mut tex) = texture {
|
||||||
@@ -664,18 +852,15 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
{
|
{
|
||||||
let query = tex_label.query();
|
let query = tex_label.query();
|
||||||
let x = (config.width - query.width) / 2;
|
let x = (config.width - query.width) / 2;
|
||||||
let y = config.height as i32
|
let y = asr_top + (asr_height - query.height as i32) / 2;
|
||||||
- progress_height
|
|
||||||
- query.height as i32
|
|
||||||
- 10;
|
|
||||||
let rect =
|
let rect =
|
||||||
Rect::new(x as i32, y as i32, query.width as u32, query.height);
|
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(
|
let _ = canvas.fill_rect(Rect::new(
|
||||||
rect.x() - 6,
|
rect.x() - 8,
|
||||||
rect.y() - 3,
|
rect.y() - 4,
|
||||||
rect.width() + 12,
|
rect.width() + 16,
|
||||||
rect.height() + 6,
|
rect.height() + 8,
|
||||||
));
|
));
|
||||||
canvas.copy(&tex_label, None, Some(rect)).ok();
|
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 {
|
if let Some(ref f) = font {
|
||||||
let time_str = format_time(player_state.current_time_ms);
|
let time_str = format_time(player_state.current_time_ms);
|
||||||
let duration_str = format_time(player_state.duration_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))
|
.solid(sdl2::pixels::Color::RGB(200, 200, 200))
|
||||||
{
|
{
|
||||||
if let Ok(tex_label) = texture_creator.create_texture_from_surface(&surface) {
|
if let Ok(tex_label) = texture_creator.create_texture_from_surface(&surface) {
|
||||||
let rect = Rect::new(10, 10, surface.width(), surface.height());
|
let rect = Rect::new(10, 5, 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();
|
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))
|
.solid(sdl2::pixels::Color::RGB(150, 150, 150))
|
||||||
{
|
{
|
||||||
if let Ok(tex_label) = texture_creator.create_texture_from_surface(&surface) {
|
if let Ok(tex_label) = texture_creator.create_texture_from_surface(&surface) {
|
||||||
let rect = Rect::new(10, 30, surface.width(), surface.height());
|
let rect = Rect::new(10, 70, 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();
|
canvas.copy(&tex_label, None, Some(rect)).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let progress_y = config.height as i32 - progress_height + 5;
|
let progress_bar_height = 20i32;
|
||||||
let progress_bar_height = 8i32;
|
|
||||||
canvas.set_draw_color(sdl2::pixels::Color::RGB(50, 50, 50));
|
canvas.set_draw_color(sdl2::pixels::Color::RGB(50, 50, 50));
|
||||||
let _ = canvas.fill_rect(Rect::new(
|
let _ = canvas.fill_rect(Rect::new(
|
||||||
10,
|
10,
|
||||||
@@ -805,7 +978,7 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
{
|
{
|
||||||
let rect = Rect::new(
|
let rect = Rect::new(
|
||||||
bar_start + bar_width - surface.width() as i32 - 10,
|
bar_start + bar_width - surface.width() as i32 - 10,
|
||||||
progress_y - 25,
|
progress_y - 22,
|
||||||
surface.width(),
|
surface.width(),
|
||||||
surface.height(),
|
surface.height(),
|
||||||
);
|
);
|
||||||
@@ -827,21 +1000,13 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
|
|
||||||
if show_search {
|
if show_search {
|
||||||
if let Some(ref font) = font {
|
if let Some(ref font) = font {
|
||||||
let search_bg_h = 250u32;
|
let search_bg_y = header_height + status_bar_height;
|
||||||
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, 240));
|
||||||
canvas.set_draw_color(sdl2::pixels::Color::RGBA(20, 20, 30, 230));
|
|
||||||
let _ = canvas.fill_rect(Rect::new(
|
let _ = canvas.fill_rect(Rect::new(
|
||||||
config.width as i32 / 2 - 300,
|
0,
|
||||||
search_bg_y,
|
search_bg_y,
|
||||||
600,
|
config.width as u32,
|
||||||
search_bg_h,
|
search_bar_height as u32,
|
||||||
));
|
|
||||||
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,
|
|
||||||
));
|
));
|
||||||
|
|
||||||
let input_text = if search_query.is_empty() {
|
let input_text = if search_query.is_empty() {
|
||||||
@@ -851,56 +1016,36 @@ fn run(config: &Config) -> Result<()> {
|
|||||||
};
|
};
|
||||||
if let Ok(surface) = font
|
if let Ok(surface) = font
|
||||||
.render(&input_text)
|
.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) {
|
if let Ok(tex) = texture_creator.create_texture_from_surface(&surface) {
|
||||||
let rect = Rect::new(
|
let rect =
|
||||||
config.width as i32 / 2 - 290,
|
Rect::new(20, search_bg_y + 10, surface.width(), surface.height());
|
||||||
search_bg_y + 10,
|
|
||||||
surface.width(),
|
|
||||||
surface.height(),
|
|
||||||
);
|
|
||||||
canvas.copy(&tex, None, Some(rect)).ok();
|
canvas.copy(&tex, None, Some(rect)).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !search_results.is_empty() {
|
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() {
|
for (i, result) in search_results.iter().take(6).enumerate() {
|
||||||
let result_text = format!("[{}] {}", i + 1, result.text);
|
let result_text = format!("[{}] {}", i + 1, result.text);
|
||||||
let color = if i == 0 {
|
let color = if i == 0 {
|
||||||
sdl2::pixels::Color::RGB(100, 200, 255)
|
sdl2::pixels::Color::RGB(100, 220, 255)
|
||||||
} else {
|
} else {
|
||||||
sdl2::pixels::Color::RGB(180, 180, 180)
|
sdl2::pixels::Color::RGB(180, 180, 180)
|
||||||
};
|
};
|
||||||
if let Ok(surface) = font.render(&result_text).solid(color) {
|
if let Ok(surface) = font.render(&result_text).solid(color) {
|
||||||
if let Ok(tex) = texture_creator.create_texture_from_surface(&surface) {
|
if let Ok(tex) = texture_creator.create_texture_from_surface(&surface) {
|
||||||
let rect = Rect::new(
|
let rect = Rect::new(
|
||||||
config.width as i32 / 2 - 290,
|
x_offset,
|
||||||
y_offset,
|
search_bg_y + 10,
|
||||||
surface.width().min(580),
|
surface.width().min(500),
|
||||||
surface.height(),
|
surface.height(),
|
||||||
);
|
);
|
||||||
canvas.copy(&tex, None, Some(rect)).ok();
|
canvas.copy(&tex, None, Some(rect)).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
y_offset += 25;
|
x_offset += 520;
|
||||||
}
|
|
||||||
} 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -993,11 +1138,20 @@ fn sync_audio(
|
|||||||
video_path: &Option<std::path::PathBuf>,
|
video_path: &Option<std::path::PathBuf>,
|
||||||
time_ms: u64,
|
time_ms: u64,
|
||||||
is_playing: bool,
|
is_playing: bool,
|
||||||
|
is_reverse: bool,
|
||||||
) {
|
) {
|
||||||
if let Some(ref mut audio) = audio_player {
|
if let Some(ref mut audio) = audio_player {
|
||||||
if let Some(ref path) = video_path {
|
if let Some(ref path) = video_path {
|
||||||
audio.play(path.as_path(), time_ms);
|
info!(
|
||||||
if !is_playing {
|
"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();
|
audio.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ impl AudioPlayer {
|
|||||||
info!("Audio paused");
|
info!("Audio paused");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resume(&mut self) {
|
pub fn resume(&mut self, path: &std::path::Path, current_time_ms: u64) -> Option<()> {
|
||||||
info!("Audio resumed");
|
self.play(path, current_time_ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&mut self) {
|
pub fn stop(&mut self) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! FFmpeg wrapper
|
//! FFmpeg wrapper
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::io::{BufReader, Read};
|
use std::io::{BufReader, Read};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::{Child, ChildStdout, Command, Stdio};
|
use std::process::{Child, ChildStdout, Command, Stdio};
|
||||||
@@ -20,6 +21,14 @@ pub struct FFmpegDecoder {
|
|||||||
process: Option<Child>,
|
process: Option<Child>,
|
||||||
stdout: Option<BufReader<ChildStdout>>,
|
stdout: Option<BufReader<ChildStdout>>,
|
||||||
info: VideoInfo,
|
info: VideoInfo,
|
||||||
|
frame_buffer: VecDeque<Vec<u8>>,
|
||||||
|
frame_times: VecDeque<u64>,
|
||||||
|
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 {
|
impl FFmpegDecoder {
|
||||||
@@ -32,6 +41,14 @@ impl FFmpegDecoder {
|
|||||||
process: None,
|
process: None,
|
||||||
stdout: None,
|
stdout: None,
|
||||||
info,
|
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.process = Some(child);
|
||||||
self.stdout = Some(BufReader::new(stdout));
|
self.stdout = Some(BufReader::new(stdout));
|
||||||
|
self.current_read_ms = start_ms;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek(&mut self, timestamp_ms: u64) -> Result<()> {
|
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) {
|
pub fn stop(&mut self) {
|
||||||
@@ -142,11 +186,33 @@ impl FFmpegDecoder {
|
|||||||
|
|
||||||
pub fn read_frame(&mut self) -> Result<Option<Vec<u8>>> {
|
pub fn read_frame(&mut self) -> Result<Option<Vec<u8>>> {
|
||||||
let frame_size = (self.info.width * self.info.height * 3) as usize;
|
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];
|
let mut buffer = vec![0u8; frame_size];
|
||||||
|
|
||||||
if let Some(ref mut reader) = self.stdout {
|
if let Some(ref mut reader) = self.stdout {
|
||||||
match reader.read_exact(&mut buffer) {
|
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) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
}
|
}
|
||||||
@@ -154,6 +220,133 @@ impl FFmpegDecoder {
|
|||||||
Ok(None)
|
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<u8>> = Vec::new();
|
||||||
|
let mut temp_times: Vec<u64> = 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<u8>> = Vec::new();
|
||||||
|
let mut temp_times: Vec<u64> = 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<Vec<u8>> {
|
||||||
|
self.frame_buffer.pop_front()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_frame_time(&mut self) -> Option<u64> {
|
||||||
|
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 {
|
impl Drop for FFmpegDecoder {
|
||||||
|
|||||||
Reference in New Issue
Block a user