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    /// The presentation timestamp rescaled from the frame's millisecond base into
172    /// `timescale` ticks per second.
173    ///
174    /// Frame timestamps are stored in milliseconds (a 1 kHz time base), but media
175    /// containers and wire formats run other clocks: RTP and MPEG-TS use 90 kHz
176    /// for video, RTP audio uses the sample rate, and CMAF/fMP4 pick a per-track
177    /// timescale. This converts without changing how frames are stored, so a
178    /// muxer/packetizer can emit timestamps in its own base:
179    ///
180    /// ```
181    /// # use arcly_stream::{CodecId, MediaFrame};
182    /// # use bytes::Bytes;
183    /// let f = MediaFrame::new_video(1000, 1000, Bytes::new(), CodecId::H264, true);
184    /// assert_eq!(f.pts_in(90_000), 90_000); // 1000 ms → 90 kHz ticks
185    /// ```
186    ///
187    /// The math is done in `i128` so a large `pts × timescale` cannot overflow.
188    pub fn pts_in(&self, timescale: u32) -> i64 {
189        rescale_ms(self.pts, timescale)
190    }
191
192    /// The decode timestamp rescaled into `timescale` ticks per second. See
193    /// [`pts_in`](Self::pts_in).
194    pub fn dts_in(&self, timescale: u32) -> i64 {
195        rescale_ms(self.dts, timescale)
196    }
197
198    /// The frame duration rescaled into `timescale` ticks per second, or `None`
199    /// when the duration is unknown. See [`pts_in`](Self::pts_in).
200    pub fn duration_in(&self, timescale: u32) -> Option<i64> {
201        self.duration.map(|d| rescale_ms(d as i64, timescale))
202    }
203}
204
205/// Rescale a millisecond-based timestamp into `timescale` ticks per second.
206/// Computed in `i128` so the intermediate `value × timescale` never overflows
207/// for any realistic stream duration.
208fn rescale_ms(value_ms: i64, timescale: u32) -> i64 {
209    (value_ms as i128 * timescale as i128 / 1000) as i64
210}
211
212/// Video-specific frame carrying extra metadata.
213#[derive(Debug, Clone)]
214pub struct VideoFrame {
215    /// The underlying media frame.
216    pub inner: MediaFrame,
217    /// Coded width in pixels.
218    pub width: u32,
219    /// Coded height in pixels.
220    pub height: u32,
221    /// Frame-rate numerator.
222    pub fps_num: u32,
223    /// Frame-rate denominator.
224    pub fps_den: u32,
225}
226
227/// Audio-specific frame carrying extra metadata.
228#[derive(Debug, Clone)]
229pub struct AudioFrame {
230    /// The underlying media frame.
231    pub inner: MediaFrame,
232    /// Sample rate in Hz.
233    pub sample_rate: u32,
234    /// Channel count.
235    pub channels: u8,
236    /// Bits per sample.
237    pub bits_per_sample: u8,
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn video_keyframe_classification() {
246        let key = MediaFrame::new_video(0, 0, Bytes::from_static(b"x"), CodecId::H264, true);
247        assert!(key.is_keyframe());
248        assert!(key.is_video());
249        assert!(!key.is_audio());
250        assert_eq!(key.frame_type, FrameType::Key);
251
252        let delta = MediaFrame::new_video(1, 1, Bytes::new(), CodecId::H264, false);
253        assert!(!delta.is_keyframe());
254        assert!(delta.is_video());
255    }
256
257    #[test]
258    fn audio_frame_defaults_track_and_type() {
259        let a = MediaFrame::new_audio(10, Bytes::new(), CodecId::AAC);
260        assert!(a.is_audio());
261        assert!(!a.is_video());
262        assert_eq!(a.track_id, 1);
263        assert_eq!(a.dts, a.pts); // audio has no separate decode order
264    }
265
266    #[test]
267    fn codec_id_roundtrips_through_u8() {
268        for id in [CodecId::H264, CodecId::Opus, CodecId::Raw, CodecId::Unknown] {
269            assert_eq!(CodecId::try_from(id as u8), Ok(id));
270        }
271        assert_eq!(CodecId::try_from(200u8), Err(200));
272    }
273
274    #[test]
275    fn rescales_timestamps_into_other_timebases() {
276        let mut f = MediaFrame::new_video(1000, 990, Bytes::new(), CodecId::H264, true);
277        f.duration = Some(40); // 40 ms
278                               // 1000 ms → 90 kHz video ticks.
279        assert_eq!(f.pts_in(90_000), 90_000);
280        assert_eq!(f.dts_in(90_000), 89_100);
281        assert_eq!(f.duration_in(90_000), Some(3_600));
282        // 48 kHz audio base.
283        assert_eq!(f.pts_in(48_000), 48_000);
284        // Unknown duration stays None.
285        let g = MediaFrame::new_audio(10, Bytes::new(), CodecId::AAC);
286        assert_eq!(g.duration_in(90_000), None);
287    }
288
289    #[test]
290    fn rescale_does_not_overflow_for_long_streams() {
291        // ~24h of milliseconds into 90 kHz must not overflow (i128 intermediate).
292        let f = MediaFrame::new_video(86_400_000, 86_400_000, Bytes::new(), CodecId::H264, true);
293        assert_eq!(f.pts_in(90_000), 86_400_000i64 * 90);
294    }
295
296    #[test]
297    fn frame_flags_compose() {
298        let flags = FrameFlags::CONFIG | FrameFlags::FIRST;
299        assert!(flags.contains(FrameFlags::CONFIG));
300        assert!(!flags.contains(FrameFlags::GOP_END));
301    }
302}