Skip to main content

ustreamer_encode/
lib.rs

1//! Hardware video encoding backends.
2//!
3//! Provides a trait [`FrameEncoder`] with platform-specific implementations:
4//! - **VideoToolbox** (macOS): H.265 Main10 via `VTCompressionSession`
5//! - **NVENC** (NVIDIA): H.265/AV1 via NVIDIA Video Codec SDK
6//! - **AMF** (AMD): feature-gated staged-BGRA HEVC encode via the AMD driver runtime
7#[cfg(all(
8    feature = "amf-direct",
9    any(target_os = "linux", target_os = "windows")
10))]
11pub mod amf;
12#[cfg(all(
13    feature = "amf-direct",
14    any(target_os = "linux", target_os = "windows")
15))]
16mod hevc;
17
18#[cfg(all(
19    feature = "nvenc-direct",
20    any(target_os = "linux", target_os = "windows")
21))]
22pub mod nvenc;
23#[cfg(target_os = "macos")]
24pub mod videotoolbox;
25
26use ustreamer_capture::CapturedFrame;
27use ustreamer_proto::{
28    control::{ControlMessage, DecoderConfigMessage},
29    quality::EncodeParams,
30};
31
32/// Encoded output from a single frame.
33#[derive(Debug)]
34pub struct EncodedFrame {
35    /// Raw NALUs (H.265) or OBUs (AV1).
36    pub data: Vec<u8>,
37    /// Whether this is a keyframe.
38    pub is_keyframe: bool,
39    /// Whether this frame is a settle/refine frame.
40    pub is_refine: bool,
41    /// Whether this frame was encoded losslessly.
42    pub is_lossless: bool,
43    /// Encode duration in microseconds.
44    pub encode_time_us: u64,
45}
46
47/// Browser decoder configuration for the current encoded stream.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct DecoderConfig {
50    /// RFC 6381 codec string, for example `hvc1.1.6.L153.B0`.
51    pub codec: String,
52    /// Optional codec-specific description payload (for example `hvcC`).
53    pub description: Option<Vec<u8>>,
54    /// Encoded frame width.
55    pub coded_width: u32,
56    /// Encoded frame height.
57    pub coded_height: u32,
58}
59
60impl DecoderConfig {
61    /// Build the typed control message expected by the browser client.
62    pub fn to_control_message(&self) -> ControlMessage {
63        let mut message = DecoderConfigMessage::low_latency(self.codec.clone())
64            .with_dimensions(self.coded_width, self.coded_height);
65        if let Some(description) = &self.description {
66            message = message.with_description(description.clone());
67        }
68
69        ControlMessage::DecoderConfig(message)
70    }
71
72    /// Build the JSON control message expected by the browser client.
73    pub fn to_control_message_bytes(&self) -> Vec<u8> {
74        self.to_control_message()
75            .to_bytes()
76            .expect("decoder control message should serialize")
77    }
78}
79
80/// Trait for hardware video encoder implementations.
81pub trait FrameEncoder: Send {
82    /// Encode a captured frame with the given parameters.
83    fn encode(
84        &mut self,
85        frame: &CapturedFrame,
86        params: &EncodeParams,
87    ) -> Result<EncodedFrame, EncodeError>;
88
89    /// Flush any buffered frames.
90    fn flush(&mut self) -> Result<Vec<EncodedFrame>, EncodeError>;
91
92    /// Return the browser decoder configuration for the current stream, if known.
93    fn decoder_config(&self) -> Option<DecoderConfig> {
94        None
95    }
96}
97
98#[derive(Debug, thiserror::Error)]
99pub enum EncodeError {
100    #[error("encoder initialization failed: {0}")]
101    InitFailed(String),
102    #[error("encoding failed: {0}")]
103    EncodeFailed(String),
104    #[error("unsupported configuration: {0}")]
105    UnsupportedConfig(String),
106    #[error("unsupported frame input: {0}")]
107    UnsupportedFrame(String),
108}
109
110#[cfg(test)]
111mod tests {
112    use super::DecoderConfig;
113    use ustreamer_proto::control::ControlMessage;
114
115    #[test]
116    fn decoder_config_control_message_includes_base64_description() {
117        let config = DecoderConfig {
118            codec: "hvc1.1.6.L153.B0".into(),
119            description: Some(vec![0x01, 0x02, 0x03]),
120            coded_width: 1920,
121            coded_height: 1080,
122        };
123
124        let message = config.to_control_message();
125        let bytes = config.to_control_message_bytes();
126        assert_eq!(ControlMessage::from_slice(&bytes).unwrap(), message);
127
128        let json = String::from_utf8(bytes).unwrap();
129        assert!(json.contains("\"type\":\"decoder-config\""));
130        assert!(json.contains("\"codec\":\"hvc1.1.6.L153.B0\""));
131        assert!(json.contains("\"codedWidth\":1920"));
132        assert!(json.contains("\"codedHeight\":1080"));
133        assert!(json.contains("\"descriptionBase64\":\"AQID\""));
134    }
135
136    #[test]
137    fn decoder_config_control_message_omits_description_when_absent() {
138        let config = DecoderConfig {
139            codec: "av01.0.08M.08".into(),
140            description: None,
141            coded_width: 1280,
142            coded_height: 720,
143        };
144
145        let bytes = config.to_control_message_bytes();
146        let message = String::from_utf8(bytes.clone()).unwrap();
147        assert!(message.contains("\"codec\":\"av01.0.08M.08\""));
148        assert!(!message.contains("descriptionBase64"));
149        assert_eq!(
150            ControlMessage::from_slice(&bytes).unwrap(),
151            config.to_control_message()
152        );
153    }
154}