Skip to main content

arcly_stream/
frame.rs

1//! The media frame model โ€” the unit of data that flows through the engine.
2
3use bytes::Bytes;
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7/// Identifies the codec used to encode a media sample.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[repr(u8)]
10#[non_exhaustive]
11pub enum CodecId {
12    /// H.264 / AVC video.
13    H264 = 0,
14    /// H.265 / HEVC video.
15    H265 = 1,
16    /// VP8 video.
17    VP8 = 2,
18    /// VP9 video.
19    VP9 = 3,
20    /// AV1 video.
21    AV1 = 4,
22    /// VVC / H.266 video.
23    VVC = 12,
24    /// AAC audio.
25    AAC = 5,
26    /// Opus audio.
27    Opus = 6,
28    /// G.711 A-law audio.
29    G711Alaw = 7,
30    /// G.711 ยต-law audio.
31    G711Ulaw = 8,
32    /// G.722 audio.
33    G722 = 9,
34    /// MP3 audio.
35    MP3 = 10,
36    /// Raw / passthrough payload (no codec interpretation).
37    Raw = 11,
38    /// Unknown or unspecified codec.
39    Unknown = 255,
40}
41
42impl TryFrom<u8> for CodecId {
43    type Error = u8;
44
45    fn try_from(v: u8) -> std::result::Result<Self, u8> {
46        match v {
47            0 => Ok(CodecId::H264),
48            1 => Ok(CodecId::H265),
49            2 => Ok(CodecId::VP8),
50            3 => Ok(CodecId::VP9),
51            4 => Ok(CodecId::AV1),
52            5 => Ok(CodecId::AAC),
53            6 => Ok(CodecId::Opus),
54            7 => Ok(CodecId::G711Alaw),
55            8 => Ok(CodecId::G711Ulaw),
56            9 => Ok(CodecId::G722),
57            10 => Ok(CodecId::MP3),
58            11 => Ok(CodecId::Raw),
59            12 => Ok(CodecId::VVC),
60            255 => Ok(CodecId::Unknown),
61            n => Err(n),
62        }
63    }
64}
65
66/// Frame classification for video.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum FrameType {
69    /// Intra / keyframe (IDR for H.264)
70    Key,
71    /// Inter / delta frame
72    Delta,
73    /// Audio frame
74    Audio,
75}
76
77bitflags::bitflags! {
78    /// Bit flags attached to every MediaFrame.
79    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
80    pub struct FrameFlags: u16 {
81        /// Frame is the first in the stream.
82        const FIRST        = 0b0000_0001;
83        /// Frame completes a GOP (Group of Pictures).
84        const GOP_END      = 0b0000_0010;
85        /// Frame carries sequence/decoder config (SPS/PPS for H.264).
86        const CONFIG       = 0b0000_0100;
87        /// Frame is a discontinuity marker.
88        const DISCONTINUITY = 0b0000_1000;
89    }
90}
91
92/// A single decoded or encoded media sample.
93///
94/// `data` is always a [`bytes::Bytes`] slice โ€” zero-copy cloning is safe and
95/// cheap.  The same frame can be fanned-out to many subscribers without
96/// copying the underlying buffer.
97#[derive(Debug, Clone)]
98pub struct MediaFrame {
99    /// Presentation timestamp in the stream's time base (milliseconds).
100    pub pts: i64,
101    /// Decode timestamp in the stream's time base (milliseconds).
102    pub dts: i64,
103    /// Duration of this frame in milliseconds.  `None` = unknown.
104    pub duration: Option<u64>,
105    /// Encoded / raw payload.
106    pub data: Bytes,
107    /// Codec that produced this frame.
108    pub codec: CodecId,
109    /// Frame type classification.
110    pub frame_type: FrameType,
111    /// Bit flags.
112    pub flags: FrameFlags,
113    /// Track index (0 = first video, 1 = first audio, etc.).
114    pub track_id: u32,
115}
116
117impl MediaFrame {
118    /// Build a video frame; `is_key` selects [`FrameType::Key`] vs
119    /// [`FrameType::Delta`]. Track id defaults to 0.
120    pub fn new_video(pts: i64, dts: i64, data: Bytes, codec: CodecId, is_key: bool) -> Self {
121        Self {
122            pts,
123            dts,
124            duration: None,
125            data,
126            codec,
127            frame_type: if is_key {
128                FrameType::Key
129            } else {
130                FrameType::Delta
131            },
132            flags: FrameFlags::empty(),
133            track_id: 0,
134        }
135    }
136
137    /// Build an audio frame (`dts == pts`, [`FrameType::Audio`], track id 1).
138    pub fn new_audio(pts: i64, data: Bytes, codec: CodecId) -> Self {
139        Self {
140            pts,
141            dts: pts,
142            duration: None,
143            data,
144            codec,
145            frame_type: FrameType::Audio,
146            flags: FrameFlags::empty(),
147            track_id: 1,
148        }
149    }
150
151    /// Whether this frame is a video keyframe ([`FrameType::Key`]).
152    pub fn is_keyframe(&self) -> bool {
153        self.frame_type == FrameType::Key
154    }
155
156    /// Whether this frame is audio.
157    pub fn is_audio(&self) -> bool {
158        self.frame_type == FrameType::Audio
159    }
160
161    /// Whether this frame is video (key or delta).
162    pub fn is_video(&self) -> bool {
163        matches!(self.frame_type, FrameType::Key | FrameType::Delta)
164    }
165
166    /// The presentation timestamp as a [`Duration`] (from the absolute `pts`).
167    pub fn pts_duration(&self) -> Duration {
168        Duration::from_millis(self.pts.unsigned_abs())
169    }
170}
171
172/// Video-specific frame carrying extra metadata.
173#[derive(Debug, Clone)]
174pub struct VideoFrame {
175    /// The underlying media frame.
176    pub inner: MediaFrame,
177    /// Coded width in pixels.
178    pub width: u32,
179    /// Coded height in pixels.
180    pub height: u32,
181    /// Frame-rate numerator.
182    pub fps_num: u32,
183    /// Frame-rate denominator.
184    pub fps_den: u32,
185}
186
187/// Audio-specific frame carrying extra metadata.
188#[derive(Debug, Clone)]
189pub struct AudioFrame {
190    /// The underlying media frame.
191    pub inner: MediaFrame,
192    /// Sample rate in Hz.
193    pub sample_rate: u32,
194    /// Channel count.
195    pub channels: u8,
196    /// Bits per sample.
197    pub bits_per_sample: u8,
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn video_keyframe_classification() {
206        let key = MediaFrame::new_video(0, 0, Bytes::from_static(b"x"), CodecId::H264, true);
207        assert!(key.is_keyframe());
208        assert!(key.is_video());
209        assert!(!key.is_audio());
210        assert_eq!(key.frame_type, FrameType::Key);
211
212        let delta = MediaFrame::new_video(1, 1, Bytes::new(), CodecId::H264, false);
213        assert!(!delta.is_keyframe());
214        assert!(delta.is_video());
215    }
216
217    #[test]
218    fn audio_frame_defaults_track_and_type() {
219        let a = MediaFrame::new_audio(10, Bytes::new(), CodecId::AAC);
220        assert!(a.is_audio());
221        assert!(!a.is_video());
222        assert_eq!(a.track_id, 1);
223        assert_eq!(a.dts, a.pts); // audio has no separate decode order
224    }
225
226    #[test]
227    fn codec_id_roundtrips_through_u8() {
228        for id in [CodecId::H264, CodecId::Opus, CodecId::Raw, CodecId::Unknown] {
229            assert_eq!(CodecId::try_from(id as u8), Ok(id));
230        }
231        assert_eq!(CodecId::try_from(200u8), Err(200));
232    }
233
234    #[test]
235    fn frame_flags_compose() {
236        let flags = FrameFlags::CONFIG | FrameFlags::FIRST;
237        assert!(flags.contains(FrameFlags::CONFIG));
238        assert!(!flags.contains(FrameFlags::GOP_END));
239    }
240}