Skip to main content

rskit_media/stream/
time.rs

1//! Time-related types for media processing.
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7
8/// A time point in microseconds from the start of the media.
9///
10/// Microsecond precision matches FFmpeg's internal timestamp resolution,
11/// avoiding precision loss during conversion.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub struct Timestamp(pub u64);
14
15impl Timestamp {
16    /// Create from milliseconds.
17    pub fn from_millis(ms: u64) -> Self {
18        Self(ms.saturating_mul(1000))
19    }
20
21    /// Create from microseconds.
22    pub fn from_micros(us: u64) -> Self {
23        Self(us)
24    }
25
26    /// Create from seconds (floating point).
27    pub fn from_seconds(s: f64) -> Self {
28        Self((s * 1_000_000.0) as u64)
29    }
30
31    /// Create from hours, minutes, and seconds.
32    pub fn from_hms(h: u32, m: u32, s: f64) -> Self {
33        let total_secs = (h as f64) * 3600.0 + (m as f64) * 60.0 + s;
34        Self::from_seconds(total_secs)
35    }
36
37    /// Get the value in milliseconds (truncates sub-millisecond part).
38    pub fn as_millis(&self) -> u64 {
39        self.0 / 1000
40    }
41
42    /// Get the value in microseconds.
43    pub fn as_micros(&self) -> u64 {
44        self.0
45    }
46
47    /// Get the value in seconds (floating point).
48    pub fn as_seconds(&self) -> f64 {
49        self.0 as f64 / 1_000_000.0
50    }
51
52    /// Convert to a [`Duration`].
53    pub fn as_duration(&self) -> Duration {
54        Duration::from_micros(self.0)
55    }
56
57    /// Format as "HH:MM:SS.mmm" (FFmpeg-compatible).
58    pub fn to_ffmpeg_time(&self) -> String {
59        let total_us = self.0;
60        let ms = (total_us / 1000) % 1000;
61        let total_secs = total_us / 1_000_000;
62        let secs = total_secs % 60;
63        let total_mins = total_secs / 60;
64        let mins = total_mins % 60;
65        let hours = total_mins / 60;
66        format!("{hours:02}:{mins:02}:{secs:02}.{ms:03}")
67    }
68}
69
70impl std::fmt::Display for Timestamp {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.write_str(&self.to_ffmpeg_time())
73    }
74}
75
76/// A time range within a media file.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub struct TimeRange {
79    /// Start of the range.
80    pub start: Timestamp,
81    /// End of the range.
82    pub end: Timestamp,
83}
84
85impl TimeRange {
86    /// Create a new time range.
87    pub fn new(start: Timestamp, end: Timestamp) -> Self {
88        Self { start, end }
89    }
90
91    /// Create from millisecond values.
92    pub fn from_millis(start_ms: u64, end_ms: u64) -> Self {
93        Self {
94            start: Timestamp::from_millis(start_ms),
95            end: Timestamp::from_millis(end_ms),
96        }
97    }
98
99    /// Create from second values.
100    pub fn from_seconds(start: f64, end: f64) -> Self {
101        Self {
102            start: Timestamp::from_seconds(start),
103            end: Timestamp::from_seconds(end),
104        }
105    }
106
107    /// Duration of this range.
108    pub fn duration(&self) -> Duration {
109        Duration::from_micros(self.end.0.saturating_sub(self.start.0))
110    }
111
112    /// Duration of this range in milliseconds.
113    pub fn duration_ms(&self) -> u64 {
114        self.end.as_millis().saturating_sub(self.start.as_millis())
115    }
116
117    /// Check if a timestamp falls within this range.
118    pub fn contains(&self, ts: Timestamp) -> bool {
119        ts >= self.start && ts <= self.end
120    }
121
122    /// Check if this range overlaps with another.
123    pub fn overlaps(&self, other: &TimeRange) -> bool {
124        self.start < other.end && other.start < self.end
125    }
126
127    /// Merge two overlapping ranges into one, if they overlap.
128    pub fn merge(&self, other: &TimeRange) -> Option<TimeRange> {
129        if self.overlaps(other) {
130            Some(TimeRange {
131                start: std::cmp::min(self.start, other.start),
132                end: std::cmp::max(self.end, other.end),
133            })
134        } else {
135            None
136        }
137    }
138
139    /// Split this range at a timestamp.
140    pub fn split_at(&self, ts: Timestamp) -> (Option<TimeRange>, Option<TimeRange>) {
141        if ts <= self.start {
142            (None, Some(*self))
143        } else if ts >= self.end {
144            (Some(*self), None)
145        } else {
146            (
147                Some(TimeRange::new(self.start, ts)),
148                Some(TimeRange::new(ts, self.end)),
149            )
150        }
151    }
152
153    /// Shift this range by a signed millisecond offset.
154    pub fn shift(&self, offset_ms: i64) -> Self {
155        let offset_us = offset_ms * 1000;
156        let shift = |ts: Timestamp| {
157            if offset_us >= 0 {
158                Timestamp(ts.0.saturating_add(offset_us as u64))
159            } else {
160                Timestamp(ts.0.saturating_sub(offset_us.unsigned_abs()))
161            }
162        };
163        Self {
164            start: shift(self.start),
165            end: shift(self.end),
166        }
167    }
168}
169
170/// A labeled segment within a media file.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct Segment {
173    /// Time range of this segment.
174    pub range: TimeRange,
175    /// Optional label (e.g., "intro", "chorus").
176    pub label: Option<String>,
177    /// Optional confidence score (0.0 – 1.0).
178    pub confidence: Option<f32>,
179    /// Arbitrary metadata.
180    pub metadata: HashMap<String, serde_json::Value>,
181}
182
183impl Segment {
184    /// Create a new segment with the given time range.
185    pub fn new(range: TimeRange) -> Self {
186        Self {
187            range,
188            label: None,
189            confidence: None,
190            metadata: HashMap::new(),
191        }
192    }
193
194    /// Set the label.
195    #[must_use]
196    pub fn with_label(mut self, label: impl Into<String>) -> Self {
197        self.label = Some(label.into());
198        self
199    }
200
201    /// Set the confidence score (clamped to 0.0–1.0).
202    #[must_use]
203    pub fn with_confidence(mut self, c: f32) -> Self {
204        self.confidence = Some(c.clamp(0.0, 1.0));
205        self
206    }
207
208    /// Add a metadata key-value pair.
209    #[must_use]
210    pub fn with_meta(mut self, key: impl Into<String>, val: impl Into<serde_json::Value>) -> Self {
211        self.metadata.insert(key.into(), val.into());
212        self
213    }
214}