use std::path::PathBuf;
use std::time::Duration;
use crate::core::capture::{FrameRecord, Recording};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment {
pub path: PathBuf,
pub duration_ms: u128,
}
pub fn timeline_to_segments(
recording: &Recording,
idle_cap: Option<Duration>,
) -> Vec<Segment> {
let end_ms = recording.end_ms.unwrap_or_else(|| {
recording.frames.last().map(tc_of).unwrap_or(0)
});
let cap_ms = idle_cap.map(|d| d.as_millis());
let mut segments = Vec::new();
let mut current: Option<(PathBuf, u128)> = None;
for entry in &recording.frames {
match entry {
FrameRecord::Frame { tc_ms, path } => {
if let Some((prev_path, prev_start)) = current.take() {
segments.push(make_segment(prev_path, prev_start, *tc_ms, cap_ms));
}
current = Some((path.clone(), *tc_ms));
}
FrameRecord::Skipped { .. } => { }
}
}
if let Some((path, start)) = current {
segments.push(make_segment(path, start, end_ms, cap_ms));
}
segments
}
fn make_segment(path: PathBuf, start_tc: u128, end_tc: u128, cap_ms: Option<u128>) -> Segment {
let real_span = end_tc.saturating_sub(start_tc);
let duration_ms = match cap_ms {
Some(cap) => real_span.min(cap),
None => real_span,
};
Segment { path, duration_ms }
}
fn tc_of(entry: &FrameRecord) -> u128 {
match entry {
FrameRecord::Frame { tc_ms, .. } | FrameRecord::Skipped { tc_ms } => *tc_ms,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_recording_yields_no_segments() {
let r = Recording::default();
assert_eq!(timeline_to_segments(&r, None), Vec::<Segment>::new());
}
#[test]
fn single_frame_spans_to_end_ms() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 100, path: PathBuf::from("a.bmp") });
r.finish(500);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments, vec![Segment {
path: PathBuf::from("a.bmp"),
duration_ms: 400, }]);
}
#[test]
fn skipped_runs_extend_the_preceding_frame_segment() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Skipped { tc_ms: 250 });
r.push(FrameRecord::Skipped { tc_ms: 500 });
r.push(FrameRecord::Frame { tc_ms: 750, path: PathBuf::from("b.bmp") });
r.finish(1000);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments, vec![
Segment { path: PathBuf::from("a.bmp"), duration_ms: 750 }, Segment { path: PathBuf::from("b.bmp"), duration_ms: 250 }, ]);
}
#[test]
fn last_segment_uses_end_ms_not_carry_forward() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Frame { tc_ms: 250, path: PathBuf::from("b.bmp") });
r.finish(10_000);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments[1].duration_ms, 9_750); }
#[test]
fn no_first_frame_flash_when_two_frames_have_equal_intervals() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Frame { tc_ms: 250, path: PathBuf::from("b.bmp") });
r.finish(500);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments[0].duration_ms, 250); }
#[test]
fn idle_cap_none_preserves_real_spans() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Skipped { tc_ms: 1_000 });
r.push(FrameRecord::Skipped { tc_ms: 2_000 });
r.push(FrameRecord::Frame { tc_ms: 5_000, path: PathBuf::from("b.bmp") });
r.finish(6_000);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments[0].duration_ms, 5_000); assert_eq!(segments[1].duration_ms, 1_000);
}
#[test]
fn idle_cap_some_caps_each_segment_independently() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Skipped { tc_ms: 250 });
r.push(FrameRecord::Frame { tc_ms: 5_000, path: PathBuf::from("b.bmp") });
r.push(FrameRecord::Skipped { tc_ms: 5_250 });
r.finish(10_000);
let segments = timeline_to_segments(&r, Some(Duration::from_millis(3_000)));
assert_eq!(segments[0].duration_ms, 3_000); assert_eq!(segments[1].duration_ms, 3_000); }
#[test]
fn missing_end_ms_falls_back_to_last_tc() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Frame { tc_ms: 250, path: PathBuf::from("b.bmp") });
let segments = timeline_to_segments(&r, None);
assert_eq!(segments[0].duration_ms, 250);
assert_eq!(segments[1].duration_ms, 0); }
#[test]
fn idle_cap_zero_does_not_panic() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Frame { tc_ms: 1_000, path: PathBuf::from("b.bmp") });
r.finish(2_000);
let segments = timeline_to_segments(&r, Some(Duration::ZERO));
assert!(segments.iter().all(|s| s.duration_ms == 0));
}
#[test]
fn only_skipped_entries_yields_no_segments() {
let mut r = Recording::default();
r.push(FrameRecord::Skipped { tc_ms: 0 });
r.push(FrameRecord::Skipped { tc_ms: 250 });
r.finish(500);
let segments = timeline_to_segments(&r, None);
assert!(segments.is_empty());
}
#[test]
fn regression_first_frame_does_not_flash() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Frame { tc_ms: 1000, path: PathBuf::from("b.bmp") });
r.finish(2000);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments[0].duration_ms, 1000, "first frame must not flash");
}
#[test]
fn regression_trailing_idle_after_last_frame_preserved() {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
r.push(FrameRecord::Frame { tc_ms: 250, path: PathBuf::from("b.bmp") });
r.push(FrameRecord::Skipped { tc_ms: 500 });
r.push(FrameRecord::Skipped { tc_ms: 750 });
r.push(FrameRecord::Skipped { tc_ms: 1000 });
r.finish(1200);
let segments = timeline_to_segments(&r, None);
assert_eq!(segments[1].duration_ms, 950);
}
#[test]
fn regression_natural_mode_visibly_differs_from_dedup_on_idle() {
let build = |dedup: bool| -> Recording {
let mut r = Recording::default();
r.push(FrameRecord::Frame { tc_ms: 0, path: PathBuf::from("a.bmp") });
if dedup {
r.push(FrameRecord::Skipped { tc_ms: 5_000 });
} else {
r.push(FrameRecord::Frame { tc_ms: 5_000, path: PathBuf::from("a.bmp") });
}
r.finish(10_000);
r
};
let natural = timeline_to_segments(&build(false), None);
let default = timeline_to_segments(&build(true), Some(Duration::from_secs(3)));
assert_eq!(natural[0].duration_ms, 5_000);
assert_eq!(natural[1].duration_ms, 5_000);
assert_eq!(default[0].duration_ms, 3_000);
assert_ne!(natural[0].duration_ms, default[0].duration_ms);
}
}