Skip to main content

arcly_stream/codec/
mod.rs

1//! Codec bitstream helpers — turning wire bytes into engine metadata and
2//! [`MediaFrame`](crate::MediaFrame) classification.
3//!
4//! Gated behind the per-codec features (`codec-h264`, `codec-h265`, `codec-av1`,
5//! `codec-vp9`, `codec-vvc`, `codec-aac`; `codec` = H.264 + AAC; `codecs-all` =
6//! everything). Every parser is pure Rust with **no native dependencies**.
7//!
8//! All codecs share the [`CodecParser`] contract: extract [`VideoParams`] from a
9//! config header, and classify random-access points so the kernel's GOP cache,
10//! segmenter, eviction, and recorder — which key only on
11//! [`MediaFrame::is_keyframe`](crate::MediaFrame::is_keyframe) — work unchanged.
12//!
13//! ```
14//! # #[cfg(feature = "codec-h265")] {
15//! use arcly_stream::codec::dispatch;
16//! use arcly_stream::CodecId;
17//!
18//! let access_unit: &[u8] = &[/* … Annex-B HEVC bytes … */];
19//! // Codec-erased dispatch by CodecId (used at the ingest boundary):
20//! let is_rap = dispatch::is_random_access_point(CodecId::H265, access_unit);
21//! # let _ = is_rap;
22//! # }
23//! ```
24
25mod bitreader;
26pub mod parser;
27
28#[cfg(test)]
29pub(crate) mod testutil;
30
31pub use parser::{CodecConfig, CodecParser, VideoParams};
32
33#[cfg(feature = "_nal")]
34pub mod nal;
35
36#[cfg(feature = "codec-h264")]
37#[cfg_attr(docsrs, doc(cfg(feature = "codec-h264")))]
38pub mod h264;
39
40#[cfg(feature = "codec-h265")]
41#[cfg_attr(docsrs, doc(cfg(feature = "codec-h265")))]
42pub mod h265;
43
44#[cfg(feature = "codec-vvc")]
45#[cfg_attr(docsrs, doc(cfg(feature = "codec-vvc")))]
46pub mod vvc;
47
48#[cfg(feature = "codec-av1")]
49#[cfg_attr(docsrs, doc(cfg(feature = "codec-av1")))]
50pub mod av1;
51
52#[cfg(feature = "codec-av1")]
53pub(crate) mod obu;
54
55#[cfg(feature = "codec-vp9")]
56#[cfg_attr(docsrs, doc(cfg(feature = "codec-vp9")))]
57pub mod vp9;
58
59#[cfg(feature = "codec-aac")]
60#[cfg_attr(docsrs, doc(cfg(feature = "codec-aac")))]
61pub mod aac;
62
63// ── Back-compat flat re-exports (pre-split public surface) ──────────────────
64#[cfg(feature = "codec-aac")]
65pub use aac::{parse_adts, AdtsHeader};
66#[cfg(feature = "codec-h264")]
67pub use h264::{annexb_to_avcc, avcc_to_annexb, iter_nals_annexb, parse_sps, SpsInfo};
68
69/// Codec-erased dispatch over [`CodecId`](crate::CodecId).
70///
71/// Protocol handlers call these to classify an access unit without naming a
72/// concrete parser. Each returns a conservative default (`false` / `None`) for a
73/// codec whose feature is not enabled.
74pub mod dispatch {
75    use super::*;
76    use crate::CodecId;
77
78    /// Extract [`VideoParams`] from a config access unit for `codec`.
79    #[allow(unused_variables)]
80    pub fn parse_config(codec: CodecId, data: &[u8]) -> Option<VideoParams> {
81        match codec {
82            #[cfg(feature = "codec-h264")]
83            CodecId::H264 => <h264::H264 as CodecParser>::parse_config(data),
84            #[cfg(feature = "codec-h265")]
85            CodecId::H265 => <h265::H265 as CodecParser>::parse_config(data),
86            #[cfg(feature = "codec-vvc")]
87            CodecId::VVC => <vvc::Vvc as CodecParser>::parse_config(data),
88            #[cfg(feature = "codec-av1")]
89            CodecId::AV1 => <av1::Av1 as CodecParser>::parse_config(data),
90            #[cfg(feature = "codec-vp9")]
91            CodecId::VP9 => <vp9::Vp9 as CodecParser>::parse_config(data),
92            _ => None,
93        }
94    }
95
96    /// Whether `data` is a random-access point under `codec`.
97    #[allow(unused_variables)]
98    pub fn is_random_access_point(codec: CodecId, data: &[u8]) -> bool {
99        match codec {
100            #[cfg(feature = "codec-h264")]
101            CodecId::H264 => <h264::H264 as CodecParser>::is_random_access_point(data),
102            #[cfg(feature = "codec-h265")]
103            CodecId::H265 => <h265::H265 as CodecParser>::is_random_access_point(data),
104            #[cfg(feature = "codec-vvc")]
105            CodecId::VVC => <vvc::Vvc as CodecParser>::is_random_access_point(data),
106            #[cfg(feature = "codec-av1")]
107            CodecId::AV1 => <av1::Av1 as CodecParser>::is_random_access_point(data),
108            #[cfg(feature = "codec-vp9")]
109            CodecId::VP9 => <vp9::Vp9 as CodecParser>::is_random_access_point(data),
110            _ => false,
111        }
112    }
113
114    /// Extract the codec's parameter-set NAL units (H.264 SPS/PPS, H.265
115    /// VPS/SPS/PPS) from an Annex-B access unit, re-framed Annex-B with 4-byte
116    /// start codes — i.e. a config record suitable for `parse_config` /
117    /// `Fmp4Muxer::init_segment`. Returns `None` for codecs whose config is not a
118    /// NAL parameter set, or when the access unit carries none.
119    ///
120    /// This lets in-band ingests (MPEG-TS over UDP/SRT, RTSP) synthesize the
121    /// CONFIG access unit that container formats like FLV deliver out-of-band, so
122    /// every downstream packager gets a decoder config regardless of ingest.
123    #[allow(unused_variables)]
124    pub fn parameter_sets(codec: CodecId, annexb: &[u8]) -> Option<bytes::Bytes> {
125        use crate::codec::nal::iter_nals;
126        let is_param_set = |nal: &[u8]| -> bool {
127            let Some(&b0) = nal.first() else { return false };
128            match codec {
129                #[cfg(feature = "codec-h264")]
130                CodecId::H264 => matches!(b0 & 0x1f, h264::NAL_SPS | h264::NAL_PPS),
131                #[cfg(feature = "codec-h265")]
132                CodecId::H265 => {
133                    matches!(
134                        (b0 >> 1) & 0x3f,
135                        h265::NAL_VPS | h265::NAL_SPS | h265::NAL_PPS
136                    )
137                }
138                _ => false,
139            }
140        };
141        let mut out = Vec::new();
142        for nal in iter_nals(annexb) {
143            if is_param_set(nal) {
144                out.extend_from_slice(&[0, 0, 0, 1]);
145                out.extend_from_slice(nal);
146            }
147        }
148        (!out.is_empty()).then(|| bytes::Bytes::from(out))
149    }
150
151    /// The HLS `CODECS` attribute for `codec` given parsed `params`.
152    #[allow(unused_variables)]
153    pub fn hls_codec_string(codec: CodecId, params: &VideoParams) -> Option<String> {
154        match codec {
155            #[cfg(feature = "codec-h264")]
156            CodecId::H264 => Some(<h264::H264 as CodecParser>::hls_codec_string(params)),
157            #[cfg(feature = "codec-h265")]
158            CodecId::H265 => Some(<h265::H265 as CodecParser>::hls_codec_string(params)),
159            #[cfg(feature = "codec-vvc")]
160            CodecId::VVC => Some(<vvc::Vvc as CodecParser>::hls_codec_string(params)),
161            #[cfg(feature = "codec-av1")]
162            CodecId::AV1 => Some(<av1::Av1 as CodecParser>::hls_codec_string(params)),
163            #[cfg(feature = "codec-vp9")]
164            CodecId::VP9 => Some(<vp9::Vp9 as CodecParser>::hls_codec_string(params)),
165            _ => None,
166        }
167    }
168}