Skip to main content

g2g_core/
frame.rs

1use crate::caps::Caps;
2use crate::memory::MemoryDomain;
3use crate::meta::FrameMetaSet;
4use crate::segment::Segment;
5
6#[derive(Debug)]
7#[non_exhaustive]
8pub enum PipelinePacket {
9    CapsChanged(Caps),
10    DataFrame(Frame),
11    Eos,
12    /// Seek flush: discard in-flight and buffered data and reset position
13    /// state. Unlike `Eos`, the stream resumes after a flush, so elements
14    /// reset rather than terminate.
15    Flush,
16    /// The playback [`Segment`] in force for subsequent `DataFrame`s (M80, the
17    /// GStreamer SEGMENT-event analog). Like `CapsChanged` it is **ordered** in
18    /// the stream: it sits before the first `DataFrame` it governs, so a sink
19    /// maps each frame's timestamp to running time via the most recent
20    /// `Segment`. Every stream opens with one; a flushing seek emits a fresh
21    /// one after the `Flush`. Elements forward it downstream unchanged unless
22    /// they remap time.
23    Segment(Segment),
24    /// Deadline tick: the runner's fan-in arm reached the tick deadline the
25    /// element declared via
26    /// [`MultiInputElement::tick_interval_ns`](crate::fanout::MultiInputElement::tick_interval_ns),
27    /// delivered as `process(0, Tick)`. It lets a fan-in element emit output when
28    /// its inputs stall (a compositor holding the last frame of a stalled pad,
29    /// zero-order-hold) instead of waiting for a packet that may never come.
30    ///
31    /// It may fire spuriously: the deadline says "time passed", not "output is
32    /// due", so the element decides whether to emit. It is only ever delivered to
33    /// an element that declared an interval, and it never crosses a link: the arm
34    /// originates and consumes it, so no encoder, queue, or transport sees one.
35    Tick,
36}
37
38#[derive(Debug)]
39pub struct Frame {
40    pub domain: MemoryDomain,
41    pub timing: FrameTiming,
42    pub sequence: u64,
43    /// Per-frame metadata side-channel: typed blobs that travel with the buffer
44    /// (the GstMeta / GstAnalyticsRelationMeta analog). Empty on construction. A
45    /// zero-sized unit when the `metadata` feature is off, so the no_std / RTOS
46    /// baseline pays nothing; with the feature on it is a typed
47    /// [`FrameMetaSet`](crate::meta::FrameMetaSet) carrying e.g.
48    /// [`AnalyticsMeta`](crate::meta) detections. See `crate::meta`.
49    pub meta: FrameMetaSet,
50}
51
52impl Frame {
53    /// Construct a frame with an empty metadata set. Prefer this over the bare
54    /// struct literal at new construction sites so a future `Frame` field
55    /// addition does not break them.
56    #[inline]
57    pub fn new(domain: MemoryDomain, timing: FrameTiming, sequence: u64) -> Self {
58        Frame {
59            domain,
60            timing,
61            sequence,
62            meta: FrameMetaSet::new(),
63        }
64    }
65
66    /// Duplicate this frame for fan-out (a tee branch, or a multicast route).
67    /// The buffer is shared where the memory domain allows it: GPU handles and
68    /// pre-shared `System` bytes are refcounted (cheap), owned `System` bytes are
69    /// deep-copied (the honest cost of handing CPU bytes to a second consumer).
70    /// Per-frame metadata is shared by `Arc` refcount, with copy-on-write on the
71    /// branch that mutates it, so the copies never alias. `Frame` is deliberately
72    /// not `Clone` (owned CPU bytes make a silent clone a surprise cost); this is
73    /// the explicit, named fan-out primitive instead. See
74    /// [`MemoryDomain::share`](crate::MemoryDomain::share).
75    ///
76    /// Fan-out is a heap operation (the domain `share` refcounts or deep-copies),
77    /// so it is gated behind `alloc`; the heap-free MCU path is a single linear
78    /// chain with no tee.
79    #[cfg(feature = "alloc")]
80    pub fn share(&self) -> Frame {
81        let meta = self.meta.clone();
82        Frame {
83            domain: self.domain.share(),
84            timing: self.timing,
85            sequence: self.sequence,
86            meta,
87        }
88    }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub struct FrameTiming {
93    pub pts_ns: u64,
94    pub dts_ns: u64,
95    pub duration_ns: u64,
96    /// Media-clock capture time (e.g. RTP-derived). Stream-relative.
97    pub capture_ns: u64,
98    /// Wall-clock monotonic nanoseconds stamped at source ingestion,
99    /// using the process-wide epoch from `metrics::monotonic_ns`. The
100    /// glass-to-glass latency is `sink_now - arrival_ns`. Zero on
101    /// frames synthesized by transforms or unit tests.
102    pub arrival_ns: u64,
103    /// Whether this frame begins an independently-decodable unit (a keyframe /
104    /// IDR). Set by the parsers that detect it; consumed by trick-mode KEY_UNIT
105    /// playback (the sink drops non-keyframes under a `TRICKMODE` segment) and by
106    /// keyframe-aware seeking. `false` when unknown (the safe default: a frame is
107    /// treated as a dependent frame unless a producer marks it a keyframe).
108    pub keyframe: bool,
109}
110
111impl FrameTiming {
112    /// `pts_ns` marking a frame with no presentation time: it is presented as
113    /// soon as it arrives rather than held for a deadline. The same value
114    /// GStreamer spells `GST_CLOCK_TIME_NONE`.
115    pub const PTS_NONE: u64 = u64::MAX;
116
117    /// The presentation time, `None` when it is unset ([`Self::PTS_NONE`]).
118    pub fn pts(&self) -> Option<u64> {
119        (self.pts_ns != Self::PTS_NONE).then_some(self.pts_ns)
120    }
121}
122
123#[cfg(all(test, feature = "alloc"))]
124mod tests {
125    use super::*;
126    use crate::memory::{MemoryDomain, SystemSlice};
127    use alloc::boxed::Box;
128
129    fn frame() -> Frame {
130        Frame::new(
131            MemoryDomain::System(SystemSlice::from_boxed(Box::new([0u8; 4]))),
132            FrameTiming::default(),
133            0,
134        )
135    }
136
137    #[test]
138    fn new_constructs_a_frame_with_an_empty_meta_set() {
139        // The constructor is the future-proof path: it fills `meta` so new
140        // call sites do not break when more fields land.
141        let f = frame();
142        assert_eq!(f.sequence, 0);
143        // `meta` is empty either way; this also pins that `Frame::new` and the
144        // struct literal stay in sync (a compile check more than a value check).
145        let _ = f.meta;
146    }
147
148    #[test]
149    fn share_duplicates_bytes_and_metadata_for_fanout() {
150        // The fan-out primitive: a shared frame carries the same bytes, timing and
151        // sequence, and (for owned System memory) an independent copy, so a branch
152        // that mutates one does not disturb the other.
153        let mut orig = Frame::new(
154            MemoryDomain::System(SystemSlice::from_boxed(Box::new([1u8, 2, 3, 4]))),
155            FrameTiming {
156                pts_ns: 42,
157                ..FrameTiming::default()
158            },
159            7,
160        );
161        let dup = orig.share();
162        assert_eq!(dup.sequence, 7);
163        assert_eq!(dup.timing.pts_ns, 42);
164        match (&mut orig.domain, &dup.domain) {
165            (MemoryDomain::System(a), MemoryDomain::System(b)) => {
166                assert_eq!(a.as_slice(), b.as_slice(), "the copy sees the same bytes");
167                a.as_mut_slice()[0] = 99;
168                assert_eq!(
169                    b.as_slice()[0],
170                    1,
171                    "owned CPU bytes are deep-copied, not aliased"
172                );
173            }
174            _ => panic!("expected System memory on both"),
175        }
176    }
177
178    #[cfg(feature = "metadata")]
179    #[test]
180    fn metadata_on_set_is_an_empty_container() {
181        // With the feature on, `FrameMetaSet` is the Vec-backed container, not
182        // the ZST. A fresh frame still carries nothing.
183        let f = frame();
184        assert!(f.meta.is_empty(), "a fresh frame's metadata set is empty");
185    }
186}