arcly_stream/segment.rs
1//! Keyframe-boundary segmentation timing, shared by the HLS packager and the
2//! recorder.
3//!
4//! Both [`packager`](crate::packager) and [`record`](crate::record) need the
5//! same decision: hold frames until the first keyframe, then cut a new segment
6//! at the first keyframe at or after a target duration. [`SegmentClock`]
7//! captures exactly that state machine so the two consumers share one tested
8//! implementation rather than re-deriving it.
9
10use crate::MediaFrame;
11
12/// What a consumer should do with a frame, per [`SegmentClock::observe`].
13#[derive(Debug, Clone, Copy, PartialEq, Default)]
14pub struct SegmentDecision {
15 /// Drop this frame: no keyframe has anchored a segment yet.
16 pub skip: bool,
17 /// Finalize the currently-open segment with this duration (seconds) *before*
18 /// writing the current frame.
19 pub cut_previous: Option<f64>,
20 /// A new segment begins at this frame (the consumer should reset its muxer).
21 pub open_new: bool,
22}
23
24/// Tracks segment boundaries from a frame stream's keyframes and timestamps.
25///
26/// ```
27/// use arcly_stream::segment::SegmentClock;
28/// use arcly_stream::{CodecId, MediaFrame};
29/// use bytes::Bytes;
30///
31/// let key = |pts| MediaFrame::new_video(pts, pts, Bytes::new(), CodecId::H264, true);
32/// let mut clock = SegmentClock::new(2); // 2-second target
33///
34/// // First keyframe opens segment 0.
35/// let d = clock.observe(&key(0));
36/// assert!(d.open_new && !d.skip && d.cut_previous.is_none());
37///
38/// // A keyframe before the target does not cut.
39/// assert_eq!(clock.observe(&key(1000)).cut_previous, None);
40///
41/// // A keyframe at/after the target cuts the previous segment.
42/// let d = clock.observe(&key(2000));
43/// assert_eq!(d.cut_previous, Some(2.0));
44/// assert!(d.open_new);
45/// ```
46#[derive(Debug, Clone)]
47pub struct SegmentClock {
48 target_ms: i64,
49 started: bool,
50 seg_start_pts: i64,
51 last_pts: i64,
52}
53
54impl SegmentClock {
55 /// A clock targeting `target_secs`-long segments (minimum 1 second).
56 pub fn new(target_secs: u64) -> Self {
57 Self {
58 target_ms: (target_secs.max(1) * 1000) as i64,
59 started: false,
60 seg_start_pts: 0,
61 last_pts: 0,
62 }
63 }
64
65 /// Whether a segment is currently open.
66 pub fn is_open(&self) -> bool {
67 self.started
68 }
69
70 /// Feed one frame and learn what to do with it (call before writing it).
71 pub fn observe(&mut self, frame: &MediaFrame) -> SegmentDecision {
72 // Use the DECODE timestamp for segment timing. Frames arrive in decode
73 // order, and with B-frames the presentation timestamp is non-monotonic in
74 // that order — so elapsed/part math on `pts` jitters (and can go negative),
75 // producing irregular segment/part boundaries and stuttery LL playback.
76 // `dts` is monotonic in arrival order. Keyframes carry `dts == pts`, so the
77 // cut points are unchanged for non-B-frame sources.
78 if !self.started {
79 // Anchor the first segment on the first keyframe; drop anything
80 // before it so every segment is independently decodable.
81 if !frame.is_keyframe() {
82 return SegmentDecision {
83 skip: true,
84 ..SegmentDecision::default()
85 };
86 }
87 self.started = true;
88 self.seg_start_pts = frame.dts;
89 self.last_pts = frame.dts;
90 return SegmentDecision {
91 skip: false,
92 cut_previous: None,
93 open_new: true,
94 };
95 }
96
97 let mut decision = SegmentDecision::default();
98 if frame.is_keyframe() {
99 let elapsed_ms = (frame.dts - self.seg_start_pts).max(0);
100 if elapsed_ms >= self.target_ms {
101 decision.cut_previous = Some(elapsed_ms as f64 / 1000.0);
102 decision.open_new = true;
103 self.seg_start_pts = frame.dts;
104 }
105 }
106 self.last_pts = frame.dts;
107 decision
108 }
109
110 /// Close any open segment at end-of-stream, returning its duration in
111 /// seconds (or `None` if no segment is open).
112 pub fn flush(&mut self) -> Option<f64> {
113 if !self.started {
114 return None;
115 }
116 self.started = false;
117 Some((self.last_pts - self.seg_start_pts).max(0) as f64 / 1000.0)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::CodecId;
125 use bytes::Bytes;
126
127 fn frame(pts: i64, key: bool) -> MediaFrame {
128 MediaFrame::new_video(pts, pts, Bytes::from_static(b"x"), CodecId::H264, key)
129 }
130
131 #[test]
132 fn skips_until_first_keyframe() {
133 let mut c = SegmentClock::new(2);
134 assert!(c.observe(&frame(0, false)).skip);
135 let d = c.observe(&frame(10, true));
136 assert!(!d.skip && d.open_new && d.cut_previous.is_none());
137 assert!(c.is_open());
138 }
139
140 #[test]
141 fn cuts_on_keyframe_after_target_and_flushes_tail() {
142 let mut c = SegmentClock::new(2);
143 c.observe(&frame(0, true)); // open seg 0
144 assert_eq!(c.observe(&frame(1000, true)).cut_previous, None); // under target
145 let d = c.observe(&frame(2000, true)); // >= target
146 assert_eq!(d.cut_previous, Some(2.0));
147 assert!(d.open_new);
148 c.observe(&frame(2500, false));
149 assert_eq!(c.flush(), Some(0.5)); // tail of seg 1
150 assert_eq!(c.flush(), None); // idempotent once closed
151 }
152}