Skip to main content

media_pp/elements/driver/webrtc/
stream_info.rs

1use std::{fmt, ptr};
2
3use ffmpeg_next as ffmpeg;
4use str0m::format::{Codec, CodecSpec};
5
6use crate::error::Result;
7
8use super::command::WebRtcError;
9
10/// Parameters confirmed from actual payloads on an inbound WebRTC track.
11///
12/// SDP exposes several possible codecs, while this value identifies the one
13/// the remote sender actually used. Audio payload metadata is sufficient as
14/// soon as the first payload arrives. H.264 is different: construction waits
15/// until both SPS and PPS have arrived, so [`Self::codec_parameters`] can
16/// describe the stream without borrowing the remote encoder's configuration.
17#[derive(Clone, PartialEq, Eq)]
18pub struct WebRtcStreamInfo {
19    codec_spec: CodecSpec,
20    h264: Option<H264Config>,
21}
22
23impl fmt::Debug for WebRtcStreamInfo {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        f.debug_struct("WebRtcStreamInfo")
26            .field("codec_spec", &self.codec_spec)
27            .field("video_dimensions", &self.video_dimensions())
28            .field("codec_parameters_ready", &self.codec_parameters_ready())
29            .finish()
30    }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34struct H264Config {
35    sps: Vec<u8>,
36    pps: Vec<u8>,
37    width: u32,
38    height: u32,
39}
40
41impl WebRtcStreamInfo {
42    /// Returns the full str0m payload specification observed on the stream.
43    pub fn codec_spec(&self) -> CodecSpec {
44        self.codec_spec
45    }
46
47    /// Returns the selected codec family.
48    pub fn codec(&self) -> Codec {
49        self.codec_spec.codec
50    }
51
52    /// Returns dimensions parsed from received codec configuration, currently
53    /// `Some` for H.264 information returned by `wait_stream_info`.
54    pub fn video_dimensions(&self) -> Option<(u32, u32)> {
55        self.h264.as_ref().map(|h264| (h264.width, h264.height))
56    }
57
58    /// Returns the RTP timestamp time base derived from the codec clock rate.
59    pub fn time_base(&self) -> Result<ffmpeg::Rational> {
60        let clock_rate = clock_rate_i32(self.codec_spec)?;
61        Ok(ffmpeg::Rational::new(1, clock_rate))
62    }
63
64    /// Builds FFmpeg codec parameters for this depayloaded stream.
65    ///
66    /// The result describes the compressed stream independently of its next
67    /// consumer and can be passed to a decoder or container muxer. H.264
68    /// includes received SPS/PPS and dimensions, while Opus includes the
69    /// negotiated channel layout and `OpusHead`. Whether a particular
70    /// container accepts the codec remains the muxer's responsibility.
71    pub fn codec_parameters(&self) -> Result<ffmpeg::codec::Parameters> {
72        let (medium, id) = ffmpeg_codec(self.codec_spec.codec).ok_or(
73            WebRtcError::UnsupportedCodecParameters(self.codec_spec.codec),
74        )?;
75        let mut parameters = base_parameters(self.codec_spec, medium, id)?;
76        match self.codec_spec.codec {
77            Codec::H264 => {
78                let h264 = self
79                    .h264
80                    .as_ref()
81                    .ok_or(WebRtcError::H264ParameterSetsNotReceived)?;
82                set_h264_parameters(&mut parameters, h264)?;
83            }
84            Codec::Opus => {
85                let channels = audio_channels(self.codec_spec)?;
86                set_extradata(&mut parameters, &opus_head(channels))?;
87            }
88            _ => {}
89        }
90        Ok(parameters)
91    }
92
93    fn codec_parameters_ready(&self) -> bool {
94        match self.codec_spec.codec {
95            Codec::H264 => self.h264.is_some(),
96            Codec::Opus => self
97                .codec_spec
98                .channels
99                .is_some_and(|channels| (1..=2).contains(&channels)),
100            codec => ffmpeg_codec(codec).is_some(),
101        }
102    }
103}
104
105impl From<CodecSpec> for WebRtcStreamInfo {
106    fn from(codec_spec: CodecSpec) -> Self {
107        Self {
108            codec_spec,
109            h264: None,
110        }
111    }
112}
113
114/// Per-track payload observer owned by the `WebRtcPeer` thread.
115pub(super) struct StreamInfoProbe {
116    codec_spec: Option<CodecSpec>,
117    h264_sps: Option<Vec<u8>>,
118    h264_pps: Option<Vec<u8>>,
119}
120
121impl StreamInfoProbe {
122    pub(super) fn new() -> Self {
123        Self {
124            codec_spec: None,
125            h264_sps: None,
126            h264_pps: None,
127        }
128    }
129
130    /// Returns once the observed codec has enough information for the public
131    /// stream-info contract. H.264 may span several frames.
132    pub(super) fn observe(
133        &mut self,
134        codec_spec: CodecSpec,
135        payload: &[u8],
136    ) -> Option<WebRtcStreamInfo> {
137        if self.codec_spec != Some(codec_spec) {
138            self.codec_spec = Some(codec_spec);
139            self.h264_sps = None;
140            self.h264_pps = None;
141        }
142        if codec_spec.codec != Codec::H264 {
143            return Some(codec_spec.into());
144        }
145
146        for nalu in annex_b_nalus(payload) {
147            match nalu.first().map(|byte| byte & 0x1f) {
148                Some(7) => self.h264_sps = Some(nalu.to_vec()),
149                Some(8) => self.h264_pps = Some(nalu.to_vec()),
150                _ => {}
151            }
152        }
153        let (Some(sps), Some(pps)) = (&self.h264_sps, &self.h264_pps) else {
154            return None;
155        };
156        let (width, height) = parse_h264_dimensions(sps)?;
157        Some(WebRtcStreamInfo {
158            codec_spec,
159            h264: Some(H264Config {
160                sps: sps.clone(),
161                pps: pps.clone(),
162                width,
163                height,
164            }),
165        })
166    }
167}
168
169fn base_parameters(
170    codec_spec: CodecSpec,
171    medium: ffmpeg::media::Type,
172    id: ffmpeg::codec::Id,
173) -> Result<ffmpeg::codec::Parameters> {
174    let audio = medium == ffmpeg::media::Type::Audio;
175    let sample_rate = audio.then(|| clock_rate_i32(codec_spec)).transpose()?;
176    let channels = if audio {
177        match codec_spec.channels {
178            Some(0) => return Err(invalid_channel_count(codec_spec).into()),
179            channels => channels,
180        }
181    } else {
182        None
183    };
184    let mut parameters = ffmpeg::codec::Parameters::new();
185
186    // SAFETY: `parameters` is exclusively owned. These are plain fields, and
187    // `av_channel_layout_default` initializes its owned channel layout.
188    unsafe {
189        let raw = parameters.as_mut_ptr();
190        (*raw).codec_type = medium.into();
191        (*raw).codec_id = id.into();
192        if let Some(sample_rate) = sample_rate {
193            (*raw).sample_rate = sample_rate;
194        }
195        if let Some(channels) = channels {
196            ffmpeg::ffi::av_channel_layout_default(&mut (*raw).ch_layout, i32::from(channels));
197        }
198    }
199    Ok(parameters)
200}
201
202fn set_h264_parameters(
203    parameters: &mut ffmpeg::codec::Parameters,
204    h264: &H264Config,
205) -> Result<()> {
206    // Keep FFmpeg-facing configuration in Annex-B form. Besides being valid
207    // decoder extradata, this tells the MP4 muxer that incoming access units
208    // use Annex-B too, so it converts both the header to avcC and packet NALUs
209    // to length-prefixed samples while writing the container.
210    let extradata = annex_b_h264_extradata(&h264.sps, &h264.pps)?;
211    // SAFETY: `parameters` is exclusively borrowed and SPS validation
212    // guarantees the indexed profile bytes exist.
213    unsafe {
214        let raw = parameters.as_mut_ptr();
215        (*raw).width = h264.width as i32;
216        (*raw).height = h264.height as i32;
217        (*raw).profile = i32::from(h264.sps[1]);
218        (*raw).level = i32::from(h264.sps[3]);
219    }
220    set_extradata(parameters, &extradata)
221}
222
223fn set_extradata(parameters: &mut ffmpeg::codec::Parameters, bytes: &[u8]) -> Result<()> {
224    let size = i32::try_from(bytes.len())
225        .map_err(|_| WebRtcError::CodecConfigurationTooLarge { size: bytes.len() })?;
226    let padded = bytes
227        .len()
228        .checked_add(ffmpeg::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize)
229        .ok_or(WebRtcError::CodecConfigurationTooLarge { size: bytes.len() })?;
230
231    // SAFETY: FFmpeg owns and frees extradata allocated with `av_mallocz`.
232    // The required padding stays zero and the copy fits the allocation.
233    unsafe {
234        let allocation = ffmpeg::ffi::av_mallocz(padded) as *mut u8;
235        if allocation.is_null() {
236            return Err(WebRtcError::CodecParametersAllocationFailed { size: padded }.into());
237        }
238        ptr::copy_nonoverlapping(bytes.as_ptr(), allocation, bytes.len());
239        let raw = parameters.as_mut_ptr();
240        (*raw).extradata = allocation;
241        (*raw).extradata_size = size;
242    }
243    Ok(())
244}
245
246fn annex_b_h264_extradata(sps: &[u8], pps: &[u8]) -> Result<Vec<u8>> {
247    if sps.len() < 4 || sps.first().map(|byte| byte & 0x1f) != Some(7) {
248        return Err(WebRtcError::InvalidH264ParameterSet("SPS").into());
249    }
250    if pps.is_empty() || pps.first().map(|byte| byte & 0x1f) != Some(8) {
251        return Err(WebRtcError::InvalidH264ParameterSet("PPS").into());
252    }
253    let mut result = Vec::with_capacity(8 + sps.len() + pps.len());
254    result.extend_from_slice(&[0, 0, 0, 1]);
255    result.extend_from_slice(sps);
256    result.extend_from_slice(&[0, 0, 0, 1]);
257    result.extend_from_slice(pps);
258    Ok(result)
259}
260
261fn opus_head(channels: u8) -> Vec<u8> {
262    let mut result = Vec::with_capacity(19);
263    result.extend_from_slice(b"OpusHead");
264    result.push(1);
265    result.push(channels);
266    result.extend_from_slice(&0u16.to_le_bytes());
267    result.extend_from_slice(&48_000u32.to_le_bytes());
268    result.extend_from_slice(&0i16.to_le_bytes());
269    result.push(0);
270    result
271}
272
273fn clock_rate_i32(codec_spec: CodecSpec) -> std::result::Result<i32, WebRtcError> {
274    let clock_rate = codec_spec.clock_rate.get();
275    i32::try_from(clock_rate).map_err(|_| WebRtcError::InvalidStreamClockRate {
276        codec: codec_spec.codec,
277        clock_rate,
278    })
279}
280
281fn audio_channels(codec_spec: CodecSpec) -> std::result::Result<u8, WebRtcError> {
282    match codec_spec.channels {
283        // Opus mapping family 0 (the only mapping WebRTC negotiates here) is
284        // defined for mono and stereo only.
285        Some(channels @ 1..=2) => Ok(channels),
286        _ => Err(invalid_channel_count(codec_spec)),
287    }
288}
289
290fn invalid_channel_count(codec_spec: CodecSpec) -> WebRtcError {
291    WebRtcError::InvalidStreamChannelCount {
292        codec: codec_spec.codec,
293        channels: codec_spec.channels.unwrap_or(0),
294    }
295}
296
297fn ffmpeg_codec(codec: Codec) -> Option<(ffmpeg::media::Type, ffmpeg::codec::Id)> {
298    use ffmpeg::{codec::Id, media::Type};
299    match codec {
300        Codec::Opus => Some((Type::Audio, Id::OPUS)),
301        Codec::PCMU => Some((Type::Audio, Id::PCM_MULAW)),
302        Codec::PCMA => Some((Type::Audio, Id::PCM_ALAW)),
303        Codec::H264 => Some((Type::Video, Id::H264)),
304        Codec::H265 => Some((Type::Video, Id::HEVC)),
305        Codec::H266 => Some((Type::Video, Id::VVC)),
306        Codec::Vp8 => Some((Type::Video, Id::VP8)),
307        Codec::Vp9 => Some((Type::Video, Id::VP9)),
308        Codec::Av1 => Some((Type::Video, Id::AV1)),
309        _ => None,
310    }
311}
312
313/// The str0m codec an encoder or demuxer describing itself with `id` feeds.
314/// The inverse of [`ffmpeg_codec`], and deliberately its mirror image: a
315/// codec added to one has to be added to the other.
316pub(super) fn str0m_codec(id: ffmpeg::codec::Id) -> Option<Codec> {
317    use ffmpeg::codec::Id;
318    match id {
319        Id::OPUS => Some(Codec::Opus),
320        Id::PCM_MULAW => Some(Codec::PCMU),
321        Id::PCM_ALAW => Some(Codec::PCMA),
322        Id::H264 => Some(Codec::H264),
323        Id::HEVC => Some(Codec::H265),
324        Id::VVC => Some(Codec::H266),
325        Id::VP8 => Some(Codec::Vp8),
326        Id::VP9 => Some(Codec::Vp9),
327        Id::AV1 => Some(Codec::Av1),
328        _ => None,
329    }
330}
331
332pub(super) fn annex_b_nalus(data: &[u8]) -> Vec<&[u8]> {
333    let mut starts = Vec::new();
334    let mut offset = 0;
335    while offset + 3 <= data.len() {
336        let length = if data[offset..].starts_with(&[0, 0, 0, 1]) {
337            4
338        } else if data[offset..].starts_with(&[0, 0, 1]) {
339            3
340        } else {
341            offset += 1;
342            continue;
343        };
344        starts.push((offset, length));
345        offset += length;
346    }
347    starts
348        .iter()
349        .enumerate()
350        .filter_map(|(index, (start, length))| {
351            let nalu_start = start + length;
352            let nalu_end = starts
353                .get(index + 1)
354                .map(|(next, _)| *next)
355                .unwrap_or(data.len());
356            (nalu_start < nalu_end).then_some(&data[nalu_start..nalu_end])
357        })
358        .collect()
359}
360
361fn parse_h264_dimensions(sps: &[u8]) -> Option<(u32, u32)> {
362    if sps.len() < 4 || sps[0] & 0x1f != 7 {
363        return None;
364    }
365    let mut rbsp = Vec::with_capacity(sps.len() - 1);
366    let mut zeros = 0;
367    for &byte in &sps[1..] {
368        if zeros >= 2 && byte == 3 {
369            zeros = 0;
370            continue;
371        }
372        rbsp.push(byte);
373        zeros = if byte == 0 { zeros + 1 } else { 0 };
374    }
375
376    let mut bits = BitReader::new(&rbsp);
377    let profile_idc = bits.read_bits(8)? as u8;
378    bits.read_bits(8)?;
379    bits.read_bits(8)?;
380    bits.read_ue()?;
381
382    let mut chroma_format_idc = 1;
383    let mut separate_colour_plane = false;
384    if matches!(
385        profile_idc,
386        44 | 83 | 86 | 100 | 110 | 118 | 122 | 128 | 134 | 135 | 138 | 139 | 244
387    ) {
388        chroma_format_idc = bits.read_ue()?;
389        if chroma_format_idc > 3 {
390            return None;
391        }
392        if chroma_format_idc == 3 {
393            separate_colour_plane = bits.read_bit()?;
394        }
395        bits.read_ue()?;
396        bits.read_ue()?;
397        bits.read_bit()?;
398        if bits.read_bit()? {
399            let count = if chroma_format_idc == 3 { 12 } else { 8 };
400            for index in 0..count {
401                if bits.read_bit()? {
402                    skip_scaling_list(&mut bits, if index < 6 { 16 } else { 64 })?;
403                }
404            }
405        }
406    }
407
408    bits.read_ue()?;
409    match bits.read_ue()? {
410        0 => {
411            bits.read_ue()?;
412        }
413        1 => {
414            bits.read_bit()?;
415            bits.read_se()?;
416            bits.read_se()?;
417            for _ in 0..bits.read_ue()? {
418                bits.read_se()?;
419            }
420        }
421        2 => {}
422        _ => return None,
423    }
424    bits.read_ue()?;
425    bits.read_bit()?;
426    let width_in_mbs = bits.read_ue()?.checked_add(1)?;
427    let height_in_map_units = bits.read_ue()?.checked_add(1)?;
428    let frame_mbs_only = bits.read_bit()?;
429    if !frame_mbs_only {
430        bits.read_bit()?;
431    }
432    bits.read_bit()?;
433    let (crop_left, crop_right, crop_top, crop_bottom) = if bits.read_bit()? {
434        (
435            bits.read_ue()?,
436            bits.read_ue()?,
437            bits.read_ue()?,
438            bits.read_ue()?,
439        )
440    } else {
441        (0, 0, 0, 0)
442    };
443
444    let frame_factor = if frame_mbs_only { 1 } else { 2 };
445    let chroma_array_type = if separate_colour_plane {
446        0
447    } else {
448        chroma_format_idc
449    };
450    let (sub_width_c, sub_height_c) = match chroma_array_type {
451        0 => (1, 1),
452        1 => (2, 2),
453        2 => (2, 1),
454        3 => (1, 1),
455        _ => return None,
456    };
457    let crop_unit_x = if chroma_array_type == 0 {
458        1
459    } else {
460        sub_width_c
461    };
462    let crop_unit_y = if chroma_array_type == 0 {
463        frame_factor
464    } else {
465        sub_height_c * frame_factor
466    };
467    let width = width_in_mbs
468        .checked_mul(16)?
469        .checked_sub((crop_left + crop_right).checked_mul(crop_unit_x)?)?;
470    let height = height_in_map_units
471        .checked_mul(16)?
472        .checked_mul(frame_factor)?
473        .checked_sub((crop_top + crop_bottom).checked_mul(crop_unit_y)?)?;
474    (width > 0 && height > 0 && width <= i32::MAX as u32 && height <= i32::MAX as u32)
475        .then_some((width, height))
476}
477
478fn skip_scaling_list(bits: &mut BitReader<'_>, size: usize) -> Option<()> {
479    let mut last_scale = 8i32;
480    let mut next_scale = 8i32;
481    for _ in 0..size {
482        if next_scale != 0 {
483            next_scale = (last_scale + bits.read_se()? + 256) % 256;
484        }
485        if next_scale != 0 {
486            last_scale = next_scale;
487        }
488    }
489    Some(())
490}
491
492struct BitReader<'a> {
493    data: &'a [u8],
494    bit: usize,
495}
496
497impl<'a> BitReader<'a> {
498    fn new(data: &'a [u8]) -> Self {
499        Self { data, bit: 0 }
500    }
501
502    fn read_bit(&mut self) -> Option<bool> {
503        Some(self.read_bits(1)? != 0)
504    }
505
506    fn read_bits(&mut self, count: usize) -> Option<u32> {
507        if count > 32 || self.bit.checked_add(count)? > self.data.len().checked_mul(8)? {
508            return None;
509        }
510        let mut value = 0u32;
511        for _ in 0..count {
512            let byte = self.data[self.bit / 8];
513            value = (value << 1) | u32::from((byte >> (7 - self.bit % 8)) & 1);
514            self.bit += 1;
515        }
516        Some(value)
517    }
518
519    fn read_ue(&mut self) -> Option<u32> {
520        let mut leading_zeroes = 0usize;
521        while !self.read_bit()? {
522            leading_zeroes += 1;
523            if leading_zeroes > 31 {
524                return None;
525            }
526        }
527        let suffix = self.read_bits(leading_zeroes)?;
528        ((1u32 << leading_zeroes) - 1).checked_add(suffix)
529    }
530
531    fn read_se(&mut self) -> Option<i32> {
532        let code_num = self.read_ue()?;
533        let magnitude = i32::try_from(code_num.checked_add(1)? / 2).ok()?;
534        Some(if code_num % 2 == 0 {
535            -magnitude
536        } else {
537            magnitude
538        })
539    }
540}
541
542#[cfg(test)]
543mod tests {
544    use ffmpeg_next as ffmpeg;
545    use str0m::{
546        format::{Codec, CodecSpec, FormatParams},
547        media::Frequency,
548    };
549
550    use super::{StreamInfoProbe, WebRtcStreamInfo};
551    use crate::elements::WebRtcError;
552
553    const SPS: &[u8] = &[
554        0x67, 0x42, 0xc0, 0x1f, 0x1a, 0x32, 0x35, 0x01, 0x40, 0x7a, 0x40, 0x3c, 0x22, 0x11, 0xa8,
555    ];
556    const PPS: &[u8] = &[0x68, 0x1a, 0x34, 0xe3, 0xc8];
557
558    fn spec(codec: Codec, clock_rate: Frequency, channels: Option<u8>) -> CodecSpec {
559        CodecSpec {
560            codec,
561            clock_rate,
562            channels,
563            format: FormatParams::default(),
564        }
565    }
566
567    #[test]
568    fn h264_waits_for_both_parameter_sets_and_derives_codec_parameters() {
569        let codec = spec(Codec::H264, Frequency::NINETY_KHZ, None);
570        let mut probe = StreamInfoProbe::new();
571        let mut sps_payload = vec![0, 0, 0, 1];
572        sps_payload.extend_from_slice(SPS);
573        assert!(probe.observe(codec, &sps_payload).is_none());
574
575        let mut pps_payload = vec![0, 0, 1];
576        pps_payload.extend_from_slice(PPS);
577        let info = probe
578            .observe(codec, &pps_payload)
579            .expect("PPS completes H.264 stream info");
580        let parameters = info.codec_parameters().expect("H.264 codec parameters");
581
582        assert_eq!(info.video_dimensions(), Some((640, 480)));
583        assert_eq!(info.time_base().unwrap(), ffmpeg::Rational::new(1, 90_000));
584        assert_eq!(parameters.id(), ffmpeg::codec::Id::H264);
585        // SAFETY: read-only access to live parameters.
586        unsafe {
587            assert_eq!((*parameters.as_ptr()).width, 640);
588            assert_eq!((*parameters.as_ptr()).height, 480);
589            assert!((*parameters.as_ptr()).extradata_size > 0);
590        }
591    }
592
593    #[test]
594    fn opus_derives_complete_codec_parameters() {
595        let info = WebRtcStreamInfo::from(spec(Codec::Opus, Frequency::FORTY_EIGHT_KHZ, Some(2)));
596        let parameters = info.codec_parameters().expect("Opus codec parameters");
597        assert_eq!(parameters.id(), ffmpeg::codec::Id::OPUS);
598        // SAFETY: read-only access to live parameters.
599        unsafe {
600            assert_eq!((*parameters.as_ptr()).sample_rate, 48_000);
601            assert_eq!((*parameters.as_ptr()).ch_layout.nb_channels, 2);
602            let extra = std::slice::from_raw_parts(
603                (*parameters.as_ptr()).extradata,
604                (*parameters.as_ptr()).extradata_size as usize,
605            );
606            assert_eq!(&extra[..8], b"OpusHead");
607        }
608    }
609
610    #[test]
611    fn vp8_derives_codec_parameters_without_container_policy() {
612        let info = WebRtcStreamInfo::from(spec(Codec::Vp8, Frequency::NINETY_KHZ, None));
613        let parameters = info.codec_parameters().expect("VP8 codec parameters");
614
615        assert_eq!(parameters.id(), ffmpeg::codec::Id::VP8);
616        assert_eq!(info.time_base().unwrap(), ffmpeg::Rational::new(1, 90_000));
617    }
618
619    #[test]
620    fn rtx_cannot_form_codec_parameters() {
621        let info = WebRtcStreamInfo::from(spec(Codec::Rtx, Frequency::NINETY_KHZ, None));
622        assert!(matches!(
623            info.codec_parameters()
624                .err()
625                .expect("RTX codec-parameter rejection"),
626            crate::Error::WebRtcError(WebRtcError::UnsupportedCodecParameters(Codec::Rtx))
627        ));
628    }
629
630    #[test]
631    fn invalid_clock_rate_and_channels_are_typed_errors() {
632        let clock_rate = Frequency::new(u32::MAX).unwrap();
633        let info = WebRtcStreamInfo::from(spec(Codec::Opus, clock_rate, Some(2)));
634        assert!(matches!(
635            info.time_base().unwrap_err(),
636            crate::Error::WebRtcError(WebRtcError::InvalidStreamClockRate {
637                codec: Codec::Opus,
638                clock_rate: u32::MAX,
639            })
640        ));
641
642        let no_channels =
643            WebRtcStreamInfo::from(spec(Codec::Opus, Frequency::FORTY_EIGHT_KHZ, None));
644        assert!(matches!(
645            no_channels
646                .codec_parameters()
647                .err()
648                .expect("complete Opus parameters require a channel count"),
649            crate::Error::WebRtcError(WebRtcError::InvalidStreamChannelCount {
650                codec: Codec::Opus,
651                channels: 0,
652            })
653        ));
654
655        let zero_channels =
656            WebRtcStreamInfo::from(spec(Codec::Opus, Frequency::FORTY_EIGHT_KHZ, Some(0)));
657        assert!(matches!(
658            zero_channels
659                .codec_parameters()
660                .err()
661                .expect("zero channels are invalid"),
662            crate::Error::WebRtcError(WebRtcError::InvalidStreamChannelCount {
663                codec: Codec::Opus,
664                channels: 0,
665            })
666        ));
667    }
668
669    /// A payload that never completes the parameter sets leaves the probe
670    /// unconfirmed rather than panicking: this runs on `WebRtcPeer`'s own
671    /// ICE/DTLS thread, directly on bytes a remote peer chose, so the only
672    /// two acceptable outcomes are "confirmed" and "not yet". The caller
673    /// sees the second as a `wait_stream_info` timeout it can retry.
674    ///
675    /// `parse_h264_dimensions` and `BitReader` return `Option` throughout
676    /// today, and `annex_b_h264_extradata`'s own validation is unreachable
677    /// behind that — these cases exist so a later rewrite reaching for
678    /// indexing or `unwrap` fails here instead of taking the connection
679    /// down.
680    #[test]
681    fn malformed_h264_payloads_never_confirm_and_never_panic() {
682        let codec = spec(Codec::H264, Frequency::NINETY_KHZ, None);
683        let annex_b = |nalu: &[u8]| {
684            let mut payload = vec![0, 0, 0, 1];
685            payload.extend_from_slice(nalu);
686            payload
687        };
688
689        // Nothing at all, and a start code with no NAL behind it.
690        for payload in [vec![], vec![0, 0, 0, 1], vec![0, 0, 1]] {
691            let mut probe = StreamInfoProbe::new();
692            assert!(
693                probe.observe(codec, &payload).is_none(),
694                "an empty payload cannot confirm a stream"
695            );
696        }
697
698        // NAL types that are neither SPS (7) nor PPS (8) — an IDR slice and
699        // an SEI, both of which a real sender emits constantly.
700        for nal_type in [1u8, 5, 6, 9, 31] {
701            let mut probe = StreamInfoProbe::new();
702            let payload = annex_b(&[0x60 | nal_type, 0x42, 0xc0, 0x1f]);
703            assert!(
704                probe.observe(codec, &payload).is_none(),
705                "NAL type {nal_type} must not be mistaken for a parameter set"
706            );
707        }
708
709        // Every truncation of a real SPS, each followed by a valid PPS, so a
710        // parameter set that cannot be parsed is never rescued by the other
711        // one arriving intact.
712        //
713        // Not every truncation fails to parse: an SPS carries its dimension
714        // fields well before its end, so cutting off only the trailing VUI
715        // still yields the real width and height rather than garbage. What
716        // has to hold for *all* of them is that the outcome is one of the
717        // two the caller can act on — unconfirmed, or confirmed with
718        // parameters that actually build — and never a panic on this
719        // thread. Anything shorter than the NAL header plus profile bytes
720        // is rejected outright by `parse_h264_dimensions`' own guard.
721        for length in 0..SPS.len() {
722            let mut probe = StreamInfoProbe::new();
723            let _ = probe.observe(codec, &annex_b(&SPS[..length]));
724            let confirmed = probe.observe(codec, &annex_b(PPS));
725            if length < 4 {
726                assert!(
727                    confirmed.is_none(),
728                    "a {length}-byte SPS is too short to describe anything"
729                );
730            }
731            if let Some(info) = confirmed {
732                info.codec_parameters().unwrap_or_else(|error| {
733                    panic!("a {length}-byte SPS confirmed but then failed to build: {error}")
734                });
735            }
736        }
737
738        // Exp-Golomb with more leading zeroes than `read_ue` accepts: a
739        // valid SPS NAL header and profile bytes, then a run of zero bits
740        // long enough to overflow the shift the suffix is built with.
741        let mut sps = vec![0x67, 0x42, 0xc0, 0x1f];
742        sps.extend(std::iter::repeat_n(0x00, 16));
743        sps.push(0x01);
744        let mut probe = StreamInfoProbe::new();
745        let _ = probe.observe(codec, &annex_b(&sps));
746        assert!(
747            probe.observe(codec, &annex_b(PPS)).is_none(),
748            "an over-long Exp-Golomb code must fail the parse, not the process"
749        );
750    }
751}