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