Skip to content
File

Blob: firmware/vendor/str0m/src/format/codec_config.rs

rust1634 lines
1use std::collections::HashSet;
2use std::ops::{Deref, DerefMut};
3 
4use crate::packet::H266ProfileTierLevel;
5use crate::packet::MediaKind;
6use crate::rtp_::Pt;
7use crate::rtp_::{Direction, Frequency};
8 
9use super::codec::{Codec, CodecSpec};
10use super::format_params::FormatParams;
11use super::payload_params::{Claimed, PREFERED_RANGES, PayloadParams};
12 
13/// Default payload type for PCMU (G.711 μ-law).
14pub(crate) const PT_PCMU: Pt = Pt::new_with_value(0);
15 
16/// Default payload type for PCMA (G.711 A-law).
17pub(crate) const PT_PCMA: Pt = Pt::new_with_value(8);
18 
19/// Default payload type for G722.
20pub(crate) const PT_G722: Pt = Pt::new_with_value(9);
21 
22/// Static payload type for Comfort Noise at 8000 Hz (RFC 3389).
23pub(crate) const PT_COMFORT_NOISE: Pt = Pt::new_with_value(13);
24 
25/// Default payload type for VP8.
26pub(crate) const PT_VP8: Pt = Pt::new_with_value(96);
27 
28/// Default payload type for VP8 RTX.
29pub(crate) const PT_VP8_RTX: Pt = Pt::new_with_value(97);
30 
31/// Default payload type for VP9 profile 0.
32pub(crate) const PT_VP9: Pt = Pt::new_with_value(98);
33 
34/// Default payload type for VP9 profile 0 RTX.
35pub(crate) const PT_VP9_RTX: Pt = Pt::new_with_value(99);
36 
37/// Default payload type for AV1.
38pub(crate) const PT_AV1: Pt = Pt::new_with_value(45);
39 
40/// Default payload type for AV1 RTX.
41pub(crate) const PT_AV1_RTX: Pt = Pt::new_with_value(46);
42 
43/// Default payload type for H265.
44pub(crate) const PT_H265: Pt = Pt::new_with_value(102);
45 
46/// Default payload type for H265 RTX.
47pub(crate) const PT_H265_RTX: Pt = Pt::new_with_value(103);
48 
49/// Default payload type for H266 (VVC).
50pub(crate) const PT_H266: Pt = Pt::new_with_value(104);
51 
52/// Default payload type for H266 RTX.
53pub(crate) const PT_H266_RTX: Pt = Pt::new_with_value(105);
54 
55/// Default payload type for Opus.
56pub(crate) const PT_OPUS: Pt = Pt::new_with_value(111);
57 
58/// Session config for all codecs.
59#[derive(Debug, Clone, Default)]
60pub struct CodecConfig {
61 params: Vec<PayloadParams>,
62}
63 
64impl CodecConfig {
65 /// Creates a new empty config.
66 pub fn empty() -> Self {
67 CodecConfig::default()
68 }
69 
70 /// Creates a new config from payload params
71 pub fn new_from_payload_params(payload_params: Vec<PayloadParams>) -> Self {
72 CodecConfig {
73 params: payload_params,
74 }
75 }
76 
77 /// Creates a new config with all default configurations enabled.
78 pub fn new_with_defaults() -> Self {
79 let mut c = Self::empty();
80 c.enable_opus(true);
81 
82 c.enable_vp8(true);
83 c.enable_h264(true);
84 c.enable_h265(true);
85 // H266/VVC off by default: no browser supports it.
86 c.enable_h266(false);
87 c.enable_vp9(true);
88 c.enable_av1(true);
89 
90 c
91 }
92 
93 /// Returns a reference to the payload parameters.
94 pub fn params(&self) -> &[PayloadParams] {
95 &self.params
96 }
97 
98 /// Clear all configured configs.
99 pub fn clear(&mut self) {
100 self.params.clear();
101 }
102 
103 /// Manually configure a payload type.
104 pub fn add_config(
105 &mut self,
106 pt: Pt,
107 resend: Option<Pt>,
108 codec: Codec,
109 clock_rate: Frequency,
110 channels: Option<u8>,
111 format: FormatParams,
112 ) {
113 let (fb_transport_cc, fb_fir, fb_nack, fb_pli, fb_remb) = if codec.is_video() {
114 (true, true, true, true, true)
115 } else {
116 (true, false, false, false, false)
117 };
118 
119 let p = PayloadParams {
120 pt,
121 spec: CodecSpec {
122 codec,
123 clock_rate,
124 channels,
125 format,
126 },
127 resend,
128 fb_transport_cc,
129 fb_fir,
130 fb_nack,
131 fb_pli,
132 fb_remb,
133 locked: false,
134 };
135 
136 self.params.push(p);
137 }
138 
139 fn add_static_config(&mut self, pt: Pt) {
140 let spec = CodecSpec::from_static_pt(pt).expect("known static payload type");
141 self.add_config(
142 pt,
143 None,
144 spec.codec,
145 spec.clock_rate,
146 spec.channels,
147 spec.format,
148 );
149 }
150 
151 /// Convenience for adding a h264 payload type.
152 pub fn add_h264(
153 &mut self,
154 pt: Pt,
155 resend: Option<Pt>,
156 packetization_mode: bool,
157 profile_level_id: u32,
158 ) {
159 self.add_config(
160 pt,
161 resend,
162 Codec::H264,
163 Frequency::NINETY_KHZ,
164 None,
165 FormatParams {
166 level_asymmetry_allowed: Some(true),
167 packetization_mode: if packetization_mode { Some(1) } else { Some(0) },
168 profile_level_id: Some(profile_level_id),
169 ..Default::default()
170 },
171 )
172 }
173 
174 /// Convenience for adding a h265 payload type.
175 pub fn add_h265(
176 &mut self,
177 pt: Pt,
178 resend: Option<Pt>,
179 profile_id: u8,
180 tier_flag: u8,
181 level_id: u8,
182 ) {
183 use crate::packet::H265ProfileTierLevel;
184 
185 let ptl = H265ProfileTierLevel::new(profile_id, tier_flag, level_id)
186 .unwrap_or(H265ProfileTierLevel::FALLBACK);
187 
188 self.add_config(
189 pt,
190 resend,
191 Codec::H265,
192 Frequency::NINETY_KHZ,
193 None,
194 FormatParams {
195 h265_profile_tier_level: Some(ptl),
196 ..Default::default()
197 },
198 )
199 }
200 
201 /// Convenience for adding a PCM u-law payload type.
202 pub fn enable_pcmu(&mut self, enabled: bool) {
203 self.params.retain(|c| c.spec.codec != Codec::PCMU);
204 if !enabled {
205 return;
206 }
207 self.add_static_config(PT_PCMU);
208 }
209 
210 /// Convenience for adding a PCM a-law payload type.
211 pub fn enable_pcma(&mut self, enabled: bool) {
212 self.params.retain(|c| c.spec.codec != Codec::PCMA);
213 if !enabled {
214 return;
215 }
216 self.add_static_config(PT_PCMA);
217 }
218 
219 /// Convenience for adding a G722 payload type.
220 ///
221 /// G722 samples audio at 16 kHz, but per RFC 3551 §4.5.2 the RTP clock rate is
222 /// 8000 Hz. The codec is configured at 16 kHz here; str0m maps to 8 kHz for the
223 /// SDP `a=rtpmap` line and when converting to/from RTP timestamps. See
224 /// <https://en.wikipedia.org/wiki/RTP_payload_formats#cite_note-55>
225 pub fn enable_g722(&mut self, enabled: bool) {
226 self.params.retain(|c| c.spec.codec != Codec::G722);
227 if !enabled {
228 return;
229 }
230 self.add_static_config(PT_G722);
231 }
232 
233 /// Add the static Comfort Noise payload type at 8000 Hz.
234 ///
235 /// Higher clock rates require dynamic payload types and can be configured
236 /// with [`CodecConfig::add_config`].
237 pub fn enable_comfort_noise(&mut self, enabled: bool) {
238 self.params.retain(|c| c.spec.codec != Codec::CN);
239 if !enabled {
240 return;
241 }
242 self.add_static_config(PT_COMFORT_NOISE);
243 }
244 
245 /// Add a default OPUS payload type.
246 pub fn enable_opus(&mut self, enabled: bool) {
247 self.params.retain(|c| c.spec.codec != Codec::Opus);
248 if !enabled {
249 return;
250 }
251 self.add_config(
252 PT_OPUS,
253 None,
254 Codec::Opus,
255 Frequency::FORTY_EIGHT_KHZ,
256 Some(2),
257 FormatParams {
258 min_p_time: Some(10),
259 use_inband_fec: Some(true),
260 ..Default::default()
261 },
262 )
263 }
264 
265 /// Add a default VP8 payload type.
266 pub fn enable_vp8(&mut self, enabled: bool) {
267 self.params.retain(|c| c.spec.codec != Codec::Vp8);
268 if !enabled {
269 return;
270 }
271 self.add_config(
272 PT_VP8,
273 Some(PT_VP8_RTX),
274 Codec::Vp8,
275 Frequency::NINETY_KHZ,
276 None,
277 FormatParams::default(),
278 )
279 }
280 
281 /// Add a default H264 payload type.
282 pub fn enable_h264(&mut self, enabled: bool) {
283 self.params.retain(|c| c.spec.codec != Codec::H264);
284 if !enabled {
285 return;
286 }
287 const PARAMS: &[(u8, u8, bool, u32)] = &[
288 (127, 121, true, 0x42001f),
289 (125, 107, false, 0x42001f),
290 (108, 109, true, 0x42e01f),
291 (124, 120, false, 0x42e01f),
292 (123, 119, true, 0x4d001f),
293 (35, 36, false, 0x4d001f),
294 (114, 115, true, 0x64001f),
295 ];
296 
297 for p in PARAMS {
298 self.add_h264(p.0.into(), Some(p.1.into()), p.2, p.3)
299 }
300 }
301 
302 /// Add a default H265 payload type.
303 /// This enables H265 as a video codec (clock rate 90kHz) with default RTX payload type.
304 /// Note: H265 is still considered an experimental/hidden codec in parts of the public API,
305 /// but it is supported internally for packetization/depacketization.
306 pub fn enable_h265(&mut self, enabled: bool) {
307 self.params.retain(|c| c.spec.codec != Codec::H265);
308 if !enabled {
309 return;
310 }
311 
312 // Start with single default configuration
313 // Profile/tier/level negotiation will happen via SDP fmtp parameters
314 //
315 // Level 6.0 (level_id=180) — a high capability level well supported by modern browsers.
316 // Level-id formula per ITU-T H.265 Annex A: level_id = (a*10 + b) * 3
317 // e.g. Level 6.0 → (6*10 + 0) * 3 = 180
318 //
319 // Chromium H265Level enum definition (kLevel6 = 180):
320 // https://source.chromium.org/chromium/chromium/src/+/main:
321 // third_party/webrtc/api/video_codecs/h265_profile_tier_level.h
322 self.add_h265(PT_H265, Some(PT_H265_RTX), 1, 0, 180); // Main, Main tier, Level 6.0
323 }
324 
325 /// Convenience for adding a h266 payload type.
326 pub fn add_h266(
327 &mut self,
328 pt: Pt,
329 resend: Option<Pt>,
330 profile_id: u8,
331 tier_flag: u8,
332 level_id: u8,
333 ) {
334 let ptl = H266ProfileTierLevel::new(profile_id, tier_flag, level_id)
335 .unwrap_or(H266ProfileTierLevel::FALLBACK);
336 
337 self.add_config(
338 pt,
339 resend,
340 Codec::H266,
341 Frequency::NINETY_KHZ,
342 None,
343 FormatParams {
344 h266_profile_tier_level: Some(ptl),
345 ..Default::default()
346 },
347 )
348 }
349 
350 /// Add a default H266 (VVC) payload type.
351 ///
352 /// Enables H266 as a video codec (clock rate 90kHz) with a default RTX
353 /// payload type. Profile/tier/level negotiation happens via the SDP fmtp
354 /// parameters profile-id / tier-flag / level-id (RFC 9328 §7.2).
355 pub fn enable_h266(&mut self, enabled: bool) {
356 self.params.retain(|c| c.spec.codec != Codec::H266);
357 if !enabled {
358 return;
359 }
360 
361 // Single default configuration with a high capability level.
362 //
363 // Level 6.0 (level_id=96) — per ITU-T H.266 Annex A the level_id is
364 // 16*major + 3*minor, e.g. Level 6.0 → 16*6 = 96.
365 self.add_h266(PT_H266, Some(PT_H266_RTX), 1, 0, 96); // Main 10, Main tier, Level 6.0
366 }
367 
368 /// Add a default VP9 payload type.
369 pub fn enable_vp9(&mut self, enabled: bool) {
370 self.params.retain(|c| c.spec.codec != Codec::Vp9);
371 if !enabled {
372 return;
373 }
374 self.add_config(
375 PT_VP9,
376 Some(PT_VP9_RTX),
377 Codec::Vp9,
378 Frequency::NINETY_KHZ,
379 None,
380 FormatParams {
381 profile_id: Some(0),
382 ..Default::default()
383 },
384 );
385 self.add_config(
386 100.into(),
387 Some(101.into()),
388 Codec::Vp9,
389 Frequency::NINETY_KHZ,
390 None,
391 FormatParams {
392 profile_id: Some(2),
393 ..Default::default()
394 },
395 );
396 }
397 
398 /// Add a default AV1 payload type.
399 pub fn enable_av1(&mut self, enabled: bool) {
400 self.params.retain(|c| c.spec.codec != Codec::Av1);
401 if !enabled {
402 return;
403 }
404 self.add_config(
405 PT_AV1,
406 Some(PT_AV1_RTX),
407 Codec::Av1,
408 Frequency::NINETY_KHZ,
409 None,
410 FormatParams::default(),
411 );
412 }
413 
414 /// Match the given parameters to the configured parameters.
415 ///
416 /// In a server scenario, a certain codec configuration might not have the same
417 /// payload type (PT) for two different peers. We will have incoming data with one
418 /// PT and need to match that against the PT of the outgoing.
419 ///
420 /// This call performs matching and if a match is found, returns the _local_ PT
421 /// that can be used for sending media.
422 pub fn match_params(&self, params: PayloadParams) -> Option<&PayloadParams> {
423 let c = self.params.iter().max_by_key(|p| p.match_score(&params))?;
424 c.match_score(&params)?; // avoid None, which isn't a match.
425 Some(c)
426 }
427 
428 /// When we get remote payload parameters, we need to match differently depending on direction.
429 pub(crate) fn sdp_match_remote(
430 &self,
431 remote_params: PayloadParams,
432 remote_dir: Direction,
433 ) -> Option<Pt> {
434 // If we have no matching parameters locally, we can't accept the remote in any way.
435 let our_params = self.match_params(remote_params)?;
436 
437 if remote_dir.sdp_is_receiving() {
438 // The remote is talking about its own receive requirements. The PTs are not suggestions.
439 Some(remote_params.pt())
440 } else {
441 // We can override the remote with our local config. We would have adjusted to
442 // the remote earlier if we can.
443 Some(our_params.pt())
444 }
445 }
446 
447 /// Find a payload parameter using a finder function.
448 pub fn find(&self, mut f: impl FnMut(&PayloadParams) -> bool) -> Option<&PayloadParams> {
449 self.params.iter().find(move |p| f(p))
450 }
451 
452 pub(crate) fn all_for_kind(&self, kind: MediaKind) -> impl Iterator<Item = &PayloadParams> {
453 self.params.iter().filter(move |params| {
454 if kind == MediaKind::Video {
455 params.spec.codec.is_video()
456 } else {
457 params.spec.codec.is_audio()
458 }
459 })
460 }
461 
462 pub(crate) fn update_params(&mut self, remote_params: &[PayloadParams], remote_dir: Direction) {
463 // 0-128 of "claimed" PTs. I.e. PTs that we already allocated to something.
464 let mut claimed: [bool; 128] = [false; 128];
465 
466 // Make a pass with all that are definitely confirmed by the remote, since these can't change.
467 for p in self.params.iter() {
468 if !p.locked {
469 continue;
470 }
471 
472 claimed.assert_claim_once(p.pt);
473 
474 if let Some(rtx) = p.resend {
475 claimed.assert_claim_once(rtx);
476 }
477 }
478 
479 // Collect all currently unlocked PTs since we will need to avoid them when reassigning.
480 let mut unlocked = self
481 .params
482 .iter()
483 .filter(|p| !p.locked)
484 .flat_map(|p| [Some(p.pt()), p.resend()])
485 .flatten()
486 .collect::<HashSet<_>>();
487 
488 // Now lock potential new parameters to remote.
489 //
490 // If the remote is doing `SendOnly`, we are receiving, so the PTs are suggestions,
491 // and we are allowed to ANSWER with our own allocations as overrides.
492 // If SendRecv or RecvOnly, the remote is talking about its own receiving capabilities
493 // and we are not allowed to change it in the ANSWER.
494 let local_is_controlling = !remote_dir.sdp_is_receiving();
495 
496 for p in self.params.iter_mut() {
497 p.update_param(
498 remote_params,
499 &mut claimed,
500 local_is_controlling,
501 &mut unlocked,
502 );
503 }
504 
505 // Make a pass to reassign unconfirmed payloads that have PT which are now claimed.
506 for p in self.params.iter_mut() {
507 if p.locked {
508 continue;
509 }
510 
511 if claimed.is_claimed(p.pt) {
512 let Some(pt) = claimed.find_unclaimed(PREFERED_RANGES, &unlocked) else {
513 // TODO: handle this gracefully.
514 panic!("Exhausted all PT ranges, inconsistent PayloadParam state");
515 };
516 
517 debug!("Reassigned PT {} => {}", p.pt, pt);
518 p.pt = pt;
519 
520 claimed.assert_claim_once(pt);
521 unlocked.remove(&pt);
522 }
523 
524 let Some(rtx) = p.resend else {
525 continue;
526 };
527 
528 if claimed.is_claimed(rtx) {
529 let Some(rtx) = claimed.find_unclaimed(PREFERED_RANGES, &unlocked) else {
530 // TODO: handle this gracefully.
531 panic!("Exhausted all PT ranges, inconsistent PayloadParam state");
532 };
533 
534 debug!("Reassigned RTX PT {:?} => {:?}", p.resend, rtx);
535 p.resend = Some(rtx);
536 
537 claimed.assert_claim_once(rtx);
538 unlocked.remove(&rtx);
539 }
540 }
541 }
542 
543 pub(crate) fn has_pt(&self, pt: Pt) -> bool {
544 self.params.iter().any(|p| p.pt() == pt)
545 }
546}
547 
548impl Deref for CodecConfig {
549 type Target = [PayloadParams];
550 
551 fn deref(&self) -> &Self::Target {
552 &self.params
553 }
554}
555 
556impl DerefMut for CodecConfig {
557 fn deref_mut(&mut self) -> &mut Self::Target {
558 &mut self.params
559 }
560}
561 
562impl CodecSpec {
563 /// Get the standardized codec definition for a static RTP payload type.
564 pub(crate) fn from_static_pt(pt: Pt) -> Option<Self> {
565 let (codec, clock_rate) = if pt == PT_PCMU {
566 (Codec::PCMU, Frequency::EIGHT_KHZ)
567 } else if pt == PT_PCMA {
568 (Codec::PCMA, Frequency::EIGHT_KHZ)
569 } else if pt == PT_G722 {
570 // G722 is represented as 16 kHz internally. CodecSpec::rtp_clock_rate()
571 // maps it to the standardized 8 kHz RTP clock rate on the wire.
572 (Codec::G722, Frequency::SIXTEEN_KHZ)
573 } else if pt == PT_COMFORT_NOISE {
574 (Codec::CN, Frequency::EIGHT_KHZ)
575 } else {
576 return None;
577 };
578 
579 Some(CodecSpec {
580 codec,
581 clock_rate,
582 channels: None,
583 format: FormatParams::default(),
584 })
585 }
586}
587 
588#[cfg(test)]
589mod test {
590 use crate::packet::MediaKind;
591 use crate::rtp_::Direction;
592 use crate::rtp_::Frequency;
593 
594 use super::*;
595 use crate::format::{CodecSpec, FormatParams};
596 
597 #[test]
598 fn static_payload_types_have_canonical_codec_definitions() {
599 let definitions = [
600 (PT_PCMU, Codec::PCMU, Frequency::EIGHT_KHZ),
601 (PT_PCMA, Codec::PCMA, Frequency::EIGHT_KHZ),
602 (PT_G722, Codec::G722, Frequency::SIXTEEN_KHZ),
603 (PT_COMFORT_NOISE, Codec::CN, Frequency::EIGHT_KHZ),
604 ];
605 
606 for (pt, codec, clock_rate) in definitions {
607 let spec = CodecSpec::from_static_pt(pt).expect("static payload definition");
608 assert_eq!(spec.codec, codec);
609 assert_eq!(spec.clock_rate, clock_rate);
610 assert_eq!(spec.channels, None);
611 assert_eq!(spec.format, FormatParams::default());
612 }
613 
614 assert!(CodecSpec::from_static_pt(PT_OPUS).is_none());
615 }
616 
617 #[test]
618 fn dynamic_comfort_noise_collision_preserves_configured_pt() {
619 let mut claimed = PayloadParams::new(
620 96.into(),
621 None,
622 CodecSpec {
623 codec: Codec::Opus,
624 clock_rate: Frequency::FORTY_EIGHT_KHZ,
625 channels: Some(2),
626 format: FormatParams::default(),
627 },
628 );
629 claimed.locked = true;
630 
631 let local_cn = PayloadParams::new(
632 97.into(),
633 None,
634 CodecSpec {
635 codec: Codec::CN,
636 clock_rate: Frequency::SIXTEEN_KHZ,
637 channels: None,
638 format: FormatParams::default(),
639 },
640 );
641 let remote_cn = PayloadParams::new(
642 96.into(),
643 None,
644 CodecSpec {
645 codec: Codec::CN,
646 clock_rate: Frequency::SIXTEEN_KHZ,
647 channels: None,
648 format: FormatParams::default(),
649 },
650 );
651 
652 let mut config = CodecConfig::new_from_payload_params(vec![claimed, local_cn]);
653 config.update_params(&[remote_cn], Direction::SendOnly);
654 
655 let cn = config
656 .iter()
657 .find(|p| p.spec().codec == Codec::CN)
658 .expect("configured CN payload");
659 assert_eq!(
660 *cn.pt(),
661 97,
662 "dynamic CN must retain its configured payload type"
663 );
664 }
665 
666 #[test]
667 fn collision_preserves_configured_primary_and_rtx_pts() {
668 let mut claimed = PayloadParams::new(
669 96.into(),
670 Some(97.into()),
671 CodecSpec {
672 codec: Codec::H264,
673 clock_rate: Frequency::NINETY_KHZ,
674 channels: None,
675 format: FormatParams::default(),
676 },
677 );
678 claimed.locked = true;
679 
680 let vp8 = CodecSpec {
681 codec: Codec::Vp8,
682 clock_rate: Frequency::NINETY_KHZ,
683 channels: None,
684 format: FormatParams::default(),
685 };
686 let local_vp8 = PayloadParams::new(110.into(), Some(111.into()), vp8);
687 let remote_vp8 = PayloadParams::new(96.into(), Some(97.into()), vp8);
688 
689 let mut config = CodecConfig::new_from_payload_params(vec![claimed, local_vp8]);
690 config.update_params(&[remote_vp8], Direction::SendOnly);
691 
692 let vp8 = config
693 .iter()
694 .find(|p| p.spec().codec == Codec::Vp8)
695 .expect("configured VP8 payload");
696 assert_eq!((*vp8.pt(), vp8.resend().map(|pt| *pt)), (110, Some(111)));
697 }
698 
699 #[test]
700 fn enable_comfort_noise_uses_static_8khz_payload_type() {
701 let mut config = CodecConfig::empty();
702 
703 config.enable_comfort_noise(true);
704 
705 let params = config
706 .params()
707 .iter()
708 .find(|p| p.spec().codec == Codec::CN)
709 .expect("Comfort Noise should be enabled");
710 assert_eq!(params.pt(), Pt::new_with_value(13));
711 assert_eq!(params.spec().clock_rate, Frequency::EIGHT_KHZ);
712 assert_eq!(params.spec().channels, None);
713 
714 config.enable_comfort_noise(false);
715 assert!(config.params().iter().all(|p| p.spec().codec != Codec::CN));
716 }
717 
718 #[test]
719 fn comfort_noise_supports_dynamic_payload_types_at_higher_clock_rates() {
720 let mut config = CodecConfig::empty();
721 
722 for (pt, clock_rate) in [(96, 16_000), (97, 24_000), (98, 32_000), (99, 48_000)] {
723 config.add_config(
724 pt.into(),
725 None,
726 Codec::CN,
727 Frequency::new(clock_rate).unwrap(),
728 None,
729 FormatParams::default(),
730 );
731 }
732 
733 let configured: Vec<_> = config
734 .params()
735 .iter()
736 .map(|p| (*p.pt(), p.spec().clock_rate.get()))
737 .collect();
738 assert_eq!(
739 configured,
740 vec![(96, 16_000), (97, 24_000), (98, 32_000), (99, 48_000)]
741 );
742 }
743 
744 #[test]
745 fn test_pt_conflict_different_directions() {
746 // Simulates:
747 // 1. str0m OFFER sendonly H264 PT 108, RTX PT 109
748 // 2. FF ANSWER recvonly (locks PT 109 as H264 RTX)
749 // 3. FF OFFER sendonly Opus PT 109
750 // 4. Crash on PT 109 conflict
751 
752 let mut config = CodecConfig::empty();
753 
754 // Initial local config: H264 with PT 108, RTX 109 and Opus with PT 111
755 config.add_config(
756 108.into(),
757 Some(109.into()),
758 Codec::H264,
759 Frequency::NINETY_KHZ,
760 None,
761 FormatParams {
762 packetization_mode: Some(1),
763 profile_level_id: Some(0x42e01f),
764 ..Default::default()
765 },
766 );
767 config.add_config(
768 111.into(),
769 None,
770 Codec::Opus,
771 Frequency::FORTY_EIGHT_KHZ,
772 Some(2),
773 FormatParams::default(),
774 );
775 
776 // Step 2: Remote answers recvonly with our H264 PT 108/109
777 // This locks PT 109 as RTX
778 let remote_h264_params = vec![PayloadParams::new(
779 108.into(),
780 Some(109.into()),
781 CodecSpec {
782 codec: Codec::H264,
783 clock_rate: Frequency::NINETY_KHZ,
784 channels: None,
785 format: FormatParams {
786 packetization_mode: Some(1),
787 profile_level_id: Some(0x42e01f),
788 ..Default::default()
789 },
790 },
791 )];
792 
793 config.update_params(&remote_h264_params, Direction::RecvOnly);
794 
795 // Verify PT 109 is now locked as RTX for H264
796 assert!(
797 config
798 .params()
799 .iter()
800 .any(|p| p.resend() == Some(109.into()) && p.locked)
801 );
802 
803 // Step 3: Remote offers sendonly Opus PT 109
804 // This should NOT crash - PT 109 should be remapped to 111 (Opus default)
805 let remote_opus_params = vec![PayloadParams::new(
806 109.into(),
807 None,
808 CodecSpec {
809 codec: Codec::Opus,
810 clock_rate: Frequency::FORTY_EIGHT_KHZ,
811 channels: Some(2),
812 format: FormatParams::default(),
813 },
814 )];
815 
816 config.update_params(&remote_opus_params, Direction::SendOnly);
817 
818 // Verify the fix: Opus should be remapped to PT_OPUS (its default)
819 let opus_param = config
820 .params()
821 .iter()
822 .find(|p| p.spec().codec == Codec::Opus);
823 assert!(opus_param.is_some(), "Opus param should exist");
824 assert_eq!(
825 opus_param.unwrap().pt(),
826 PT_OPUS,
827 "Opus should be remapped to PT_OPUS (default)"
828 );
829 assert!(opus_param.unwrap().locked, "Opus param should be locked");
830 
831 // Verify H264 RTX PT 109 is still locked
832 assert!(
833 config
834 .params()
835 .iter()
836 .any(|p| p.resend() == Some(109.into()) && p.locked)
837 );
838 }
839 
840 #[test]
841 fn test_pt_conflict_same_mline_multiple_codecs() {
842 // Reproduces a bug we have had where reassigned PTs were conflicting with unlocked PTs.
843 //
844 // SCENARIO & ROOT CAUSE:
845 // This test reproduces the exact bug from sdp.json using str0m's default config.
846 //
847 // 1. str0m starts with DEFAULT codec configuration:
848 // - VP8 at PT 96/97 (unlocked)
849 // - H264 High (0x64001f) at PT 114/115 (unlocked)
850 // - Many other H264 profiles at various PTs (127, 125, 108, 124, 123, 35)
851 //
852 // 2. Chrome (peer A) offers video with H264 only - PTs: 103/104, 109/114, 117/118
853 // Chrome's OFFER direction: sendonly (Chrome sends video to us)
854 // **CRITICAL**: Chrome uses PT 114 as RTX for H264 42e01f (PT 109)
855 //
856 // 3. str0m ANSWERs accepting Chrome's H264 offer:
857 // - Chrome's H264 PTs (103, 109, 117) are LOCKED
858 // - Chrome's RTX PTs (104, 114, 118) are LOCKED
859 // - VP8 at PT 96 remains UNLOCKED (Chrome didn't negotiate it)
860 // - H264 High at PT 114 CONFLICTS with Chrome's RTX usage of PT 114!
861 //
862 // 4. THE BUG (before fix): update_params reassigns H264 High from PT 114:
863 // - Searches for available PT starting from 96
864 // - PT 96 appears "unclaimed" because it only checked LOCKED PTs
865 // - VP8 is at PT 96 but UNLOCKED, so wasn't checked!
866 // - H264 High gets reassigned to PT 96
867 // - Now BOTH VP8 and H264 High are at PT 96! ❌
868 //
869 // 5. Later, Firefox (peer B) joins - str0m creates NEW video m-line (mid KvE)
870 // When generating SDP, str0m collects ALL video codecs from codec_config
871 // Result (before fix): PT 96 appears TWICE in the same m-line!
872 // - Line 729 in sdp.json: a=rtpmap:96 VP8/90000
873 // - Line 791 in sdp.json: a=rtpmap:96 H264/90000 profile-level-id=64001f
874 //
875 // EXPECTED: Each PT in an m-line must be unique
876 // THE FIX: Now checks BOTH locked and unlocked PTs when reassigning
877 
878 // Start with str0m's default codec configuration
879 // This includes VP8 at 96/97 and H264 High at 114/115 (among others)
880 let mut config = CodecConfig::new_with_defaults();
881 
882 // Verify H264 High starts at its default PT 114 BEFORE Chrome's offer
883 let h264_high_before = config
884 .params()
885 .iter()
886 .find(|p| {
887 p.spec().codec == Codec::H264 && p.spec().format.profile_level_id == Some(0x64001f)
888 })
889 .expect("H264 High should exist");
890 assert_eq!(
891 h264_high_before.pt(),
892 114.into(),
893 "H264 High should start at PT 114"
894 );
895 
896 // Simulate Chrome's OFFER with H264 only (sendonly from Chrome's perspective)
897 // Chrome is sending video to us, so we're receiving
898 // Direction::SendOnly means the remote is sending (we receive)
899 let chrome_h264_params = vec![
900 PayloadParams::new(
901 103.into(),
902 Some(104.into()),
903 CodecSpec {
904 codec: Codec::H264,
905 clock_rate: Frequency::NINETY_KHZ,
906 channels: None,
907 format: FormatParams {
908 packetization_mode: Some(1),
909 profile_level_id: Some(0x42001f),
910 ..Default::default()
911 },
912 },
913 ),
914 PayloadParams::new(
915 109.into(),
916 Some(114.into()),
917 CodecSpec {
918 codec: Codec::H264,
919 clock_rate: Frequency::NINETY_KHZ,
920 channels: None,
921 format: FormatParams {
922 packetization_mode: Some(1),
923 profile_level_id: Some(0x42e01f),
924 ..Default::default()
925 },
926 },
927 ),
928 PayloadParams::new(
929 117.into(),
930 Some(118.into()),
931 CodecSpec {
932 codec: Codec::H264,
933 clock_rate: Frequency::NINETY_KHZ,
934 channels: None,
935 format: FormatParams {
936 packetization_mode: Some(1),
937 profile_level_id: Some(0x4d001f),
938 ..Default::default()
939 },
940 },
941 ),
942 ];
943 
944 // Process Chrome's offer - this locks the H264 PTs that Chrome offered
945 // Chrome is SendOnly, so from our perspective we're receiving (recvonly)
946 config.update_params(&chrome_h264_params, Direction::SendOnly);
947 
948 // Verify: Chrome's H264 profiles are locked at their specific PTs
949 assert!(
950 config.params().iter().any(|p| p.pt() == 103.into()
951 && p.spec().codec == Codec::H264
952 && p.spec().format.profile_level_id == Some(0x42001f)
953 && p.locked),
954 "H264 42001f should be locked at PT 103"
955 );
956 assert!(
957 config.params().iter().any(|p| p.pt() == 109.into()
958 && p.spec().codec == Codec::H264
959 && p.spec().format.profile_level_id == Some(0x42e01f)
960 && p.locked),
961 "H264 42e01f should be locked at PT 109"
962 );
963 assert!(
964 config.params().iter().any(|p| p.pt() == 117.into()
965 && p.spec().codec == Codec::H264
966 && p.spec().format.profile_level_id == Some(0x4d001f)
967 && p.locked),
968 "H264 4d001f should be locked at PT 117"
969 );
970 
971 // Verify: VP8 at PT 96 is NOT locked (Chrome didn't negotiate it)
972 let vp8_param = config
973 .params()
974 .iter()
975 .find(|p| p.spec().codec == Codec::Vp8);
976 assert!(vp8_param.is_some(), "VP8 should still exist");
977 assert_eq!(vp8_param.unwrap().pt(), 96.into(), "VP8 should be at PT 96");
978 assert!(!vp8_param.unwrap().locked, "VP8 should NOT be locked");
979 
980 // THE FIX VERIFICATION:
981 // H264 64001f was originally at PT 114, but Chrome used PT 114 for RTX
982 // update_params should reassign H264 64001f to a PT that avoids BOTH:
983 // - Locked PTs (103, 104, 109, 114, 117, 118)
984 // - Unlocked PTs already in use (96 for VP8, 97 for VP8 RTX)
985 let h264_high_param = config.params().iter().find(|p| {
986 p.spec().codec == Codec::H264 && p.spec().format.profile_level_id == Some(0x64001f)
987 });
988 assert!(h264_high_param.is_some(), "H264 64001f should still exist");
989 let h264_pt = h264_high_param.unwrap().pt();
990 
991 // Should NOT be at PT 96 (VP8 is there)
992 assert_ne!(
993 h264_pt,
994 96.into(),
995 "FIX WORKING: H264 64001f should NOT be at PT 96 (VP8 is there)"
996 );
997 
998 // Should NOT be at any of the locked PTs
999 assert!(
1000 ![103, 104, 109, 114, 117, 118].contains(&{ *h264_pt }),
1001 "H264 64001f should not conflict with locked PTs"
1002 );
1003 
1004 assert!(
1005 !h264_high_param.unwrap().locked,
1006 "H264 64001f should NOT be locked (Chrome didn't offer it)"
1007 );
1008 
1009 // Now simulate creating a new video m-line (for Firefox's camera)
1010 // This is where the bug manifests: we collect ALL video codecs from config
1011 let all_video_params: Vec<_> = config.all_for_kind(MediaKind::Video).collect();
1012 
1013 // Extract just the PTs (including RTX PTs)
1014 let mut all_pts = Vec::new();
1015 for p in all_video_params {
1016 all_pts.push(p.pt());
1017 if let Some(rtx) = p.resend() {
1018 all_pts.push(rtx);
1019 }
1020 }
1021 
1022 // THE FIX: With the bug fixed, no PT should appear multiple times
1023 // Check for duplicate PTs - this should now PASS
1024 let mut seen = std::collections::HashSet::new();
1025 let mut duplicates = Vec::new();
1026 for pt in &all_pts {
1027 if !seen.insert(pt) {
1028 duplicates.push(*pt);
1029 }
1030 }
1031 
1032 assert!(
1033 duplicates.is_empty(),
1034 "FAILED: Found duplicate PTs in same m-line: {:?}\nAll PTs: {:?}\n
1035 This creates invalid SDP with multiple a=rtpmap lines for the same PT!",
1036 duplicates,
1037 all_pts
1038 );
1039 }
1040 
1041 // ── H.265 direction-based negotiation tests ──────────────────────────
1042 
1043 /// Helper: build a CodecConfig containing only H.265 at the given PTL.
1044 fn h265_config(profile_id: u8, tier_flag: u8, level_id: u8) -> CodecConfig {
1045 let mut config = CodecConfig::empty();
1046 config.add_h265(
1047 102.into(),
1048 Some(103.into()),
1049 profile_id,
1050 tier_flag,
1051 level_id,
1052 );
1053 config
1054 }
1055 
1056 /// Helper: build a single-element remote PayloadParams for H.265 with full PTL.
1057 fn h265_remote_ptl(profile_id: u8, tier_flag: u8, level_id: u8) -> Vec<PayloadParams> {
1058 use crate::packet::H265ProfileTierLevel;
1059 vec![PayloadParams::new(
1060 102.into(),
1061 Some(103.into()),
1062 CodecSpec {
1063 codec: Codec::H265,
1064 clock_rate: Frequency::NINETY_KHZ,
1065 channels: None,
1066 format: FormatParams {
1067 h265_profile_tier_level: Some(
1068 H265ProfileTierLevel::new(profile_id, tier_flag, level_id).unwrap(),
1069 ),
1070 ..Default::default()
1071 },
1072 },
1073 )]
1074 }
1075 
1076 /// Helper: build a single-element remote PayloadParams for H.265 with profile-id only.
1077 fn h265_remote_profile_only(profile_id: u32) -> Vec<PayloadParams> {
1078 vec![PayloadParams::new(
1079 102.into(),
1080 Some(103.into()),
1081 CodecSpec {
1082 codec: Codec::H265,
1083 clock_rate: Frequency::NINETY_KHZ,
1084 channels: None,
1085 format: FormatParams {
1086 profile_id: Some(profile_id),
1087 ..Default::default()
1088 },
1089 },
1090 )]
1091 }
1092 
1093 /// Helper: build a single-element remote PayloadParams for H.265 with no fmtp.
1094 fn h265_remote_no_fmtp() -> Vec<PayloadParams> {
1095 vec![PayloadParams::new(
1096 102.into(),
1097 Some(103.into()),
1098 CodecSpec {
1099 codec: Codec::H265,
1100 clock_rate: Frequency::NINETY_KHZ,
1101 channels: None,
1102 format: FormatParams::default(),
1103 },
1104 )]
1105 }
1106 
1107 /// Helper: extract the H.265 param from a config (panics if missing).
1108 fn get_h265(config: &CodecConfig) -> &PayloadParams {
1109 config
1110 .params()
1111 .iter()
1112 .find(|p| p.spec().codec == Codec::H265)
1113 .expect("H.265 param missing")
1114 }
1115 
1116 // ── recvonly: remote sends to us ─────────────────────────────────────
1117 
1118 /// Remote sends at Level 5.2, we can receive at Level 6.0.
1119 /// Negotiated level should narrow to min(6.0, 5.2) = 5.2.
1120 #[test]
1121 fn test_h265_recvonly_remote_level_lower() {
1122 let mut config = h265_config(1, 0, 180); // Main, Main tier, Level 6.0
1123 let remote = h265_remote_ptl(1, 0, 156); // Level 5.2
1124 
1125 // Direction::SendOnly = remote is sending (we receive)
1126 config.update_params(&remote, Direction::SendOnly);
1127 
1128 let ptl = get_h265(&config)
1129 .spec()
1130 .format
1131 .h265_profile_tier_level
1132 .unwrap();
1133 assert_eq!(
1134 ptl.level_id(),
1135 156,
1136 "Should narrow to min(180,156) = Level 5.2"
1137 );
1138 assert_eq!(ptl.profile_id(), 1, "Profile should be preserved");
1139 assert_eq!(ptl.tier_flag(), 0, "Tier should be preserved");
1140 }
1141 
1142 /// Remote sends at Level 6.0, we can receive at Level 5.2.
1143 /// Negotiated level should narrow to min(5.2, 6.0) = 5.2.
1144 #[test]
1145 fn test_h265_recvonly_local_level_lower() {
1146 let mut config = h265_config(1, 0, 156); // Level 5.2
1147 let remote = h265_remote_ptl(1, 0, 180); // Level 6.0
1148 
1149 config.update_params(&remote, Direction::SendOnly);
1150 
1151 let ptl = get_h265(&config)
1152 .spec()
1153 .format
1154 .h265_profile_tier_level
1155 .unwrap();
1156 assert_eq!(
1157 ptl.level_id(),
1158 156,
1159 "Should narrow to min(156,180) = Level 5.2"
1160 );
1161 }
1162 
1163 // ── sendrecv: both sides send and receive ────────────────────────────
1164 
1165 /// Both sides sendrecv. Local Level 6.0, remote Level 5.2.
1166 /// Effective level should narrow to min(6.0, 5.2) = 5.2.
1167 #[test]
1168 fn test_h265_sendrecv_remote_level_lower() {
1169 let mut config = h265_config(1, 0, 180); // Level 6.0
1170 let remote = h265_remote_ptl(1, 0, 156); // Level 5.2
1171 
1172 config.update_params(&remote, Direction::SendRecv);
1173 
1174 let ptl = get_h265(&config)
1175 .spec()
1176 .format
1177 .h265_profile_tier_level
1178 .unwrap();
1179 assert_eq!(ptl.level_id(), 156, "sendrecv: should narrow to Level 5.2");
1180 }
1181 
1182 /// Both sides sendrecv. Local Level 5.2, remote Level 6.0.
1183 /// Effective level should narrow to min(5.2, 6.0) = 5.2.
1184 #[test]
1185 fn test_h265_sendrecv_local_level_lower() {
1186 let mut config = h265_config(1, 0, 156); // Level 5.2
1187 let remote = h265_remote_ptl(1, 0, 180); // Level 6.0
1188 
1189 config.update_params(&remote, Direction::SendRecv);
1190 
1191 let ptl = get_h265(&config)
1192 .spec()
1193 .format
1194 .h265_profile_tier_level
1195 .unwrap();
1196 assert_eq!(ptl.level_id(), 156, "sendrecv: should narrow to Level 5.2");
1197 }
1198 
1199 /// Same level on both sides. Should remain unchanged.
1200 #[test]
1201 fn test_h265_sendrecv_same_level() {
1202 let mut config = h265_config(1, 0, 156); // Level 5.2
1203 let remote = h265_remote_ptl(1, 0, 156); // Level 5.2
1204 
1205 config.update_params(&remote, Direction::SendRecv);
1206 
1207 let ptl = get_h265(&config)
1208 .spec()
1209 .format
1210 .h265_profile_tier_level
1211 .unwrap();
1212 assert_eq!(ptl.level_id(), 156, "Same levels should stay at Level 5.2");
1213 }
1214 
1215 // ── sendonly: we send, remote receives ───────────────────────────────
1216 
1217 /// Remote declares recvonly at Level 5.2, we offer sendonly at Level 6.0.
1218 /// Negotiated level should narrow to 5.2 (receiver wins).
1219 #[test]
1220 fn test_h265_sendonly_receiver_level_lower() {
1221 let mut config = h265_config(1, 0, 180); // Level 6.0
1222 let remote = h265_remote_ptl(1, 0, 156); // Level 5.2
1223 
1224 // Direction::RecvOnly = remote is receiving (we send)
1225 config.update_params(&remote, Direction::RecvOnly);
1226 
1227 let ptl = get_h265(&config)
1228 .spec()
1229 .format
1230 .h265_profile_tier_level
1231 .unwrap();
1232 assert_eq!(
1233 ptl.level_id(),
1234 156,
1235 "sendonly: should narrow to receiver's Level 5.2"
1236 );
1237 }
1238 
1239 /// Remote offers only profile-id=1, no tier/level.
1240 /// Local should drop its full PTL and echo back profile-id only.
1241 #[test]
1242 fn test_h265_profile_only_remote_mirrors() {
1243 let mut config = h265_config(1, 0, 180); // Full PTL
1244 let remote = h265_remote_profile_only(1); // profile-id only
1245 
1246 config.update_params(&remote, Direction::SendRecv);
1247 
1248 let h265 = get_h265(&config);
1249 assert!(
1250 h265.spec().format.h265_profile_tier_level.is_none(),
1251 "Full PTL should be cleared when remote only offers profile-id"
1252 );
1253 assert_eq!(
1254 h265.spec().format.profile_id,
1255 Some(1),
1256 "profile_id should mirror the remote's value"
1257 );
1258 }
1259 
1260 // ── no-fmtp remote ──────────────────────────────────────────────────
1261 
1262 /// Remote has no H.265 fmtp at all. Local should clear both fields.
1263 #[test]
1264 fn test_h265_no_fmtp_remote_clears() {
1265 let mut config = h265_config(1, 0, 180); // Full PTL
1266 let remote = h265_remote_no_fmtp();
1267 
1268 config.update_params(&remote, Direction::SendRecv);
1269 
1270 let h265 = get_h265(&config);
1271 assert!(
1272 h265.spec().format.h265_profile_tier_level.is_none(),
1273 "PTL should be cleared when remote has no fmtp"
1274 );
1275 assert!(
1276 h265.spec().format.profile_id.is_none(),
1277 "profile_id should be cleared when remote has no fmtp"
1278 );
1279 }
1280 
1281 // ── profile_id field cleanup ─────────────────────────────────────────
1282 
1283 /// When remote has full PTL, the `profile_id` field must be None
1284 /// to avoid duplicate serialization (profile-id appears in both
1285 /// h265_profile_tier_level AND the VP9-style profile_id field).
1286 #[test]
1287 fn test_h265_profile_id_cleared_on_ptl_match() {
1288 let mut config = CodecConfig::empty();
1289 // Artificially set both fields to simulate a misconfigured state
1290 config.add_config(
1291 102.into(),
1292 Some(103.into()),
1293 Codec::H265,
1294 Frequency::NINETY_KHZ,
1295 None,
1296 FormatParams {
1297 h265_profile_tier_level: Some(
1298 crate::packet::H265ProfileTierLevel::new(1, 0, 180).unwrap(),
1299 ),
1300 profile_id: Some(1), // should be cleared after negotiation
1301 ..Default::default()
1302 },
1303 );
1304 let remote = h265_remote_ptl(1, 0, 156);
1305 
1306 config.update_params(&remote, Direction::SendRecv);
1307 
1308 let h265 = get_h265(&config);
1309 assert!(
1310 h265.spec().format.profile_id.is_none(),
1311 "profile_id field must be None when full PTL is present"
1312 );
1313 assert!(
1314 h265.spec().format.h265_profile_tier_level.is_some(),
1315 "PTL should still be present"
1316 );
1317 }
1318 
1319 // ── default level ────────────────────────────────────────────────────
1320 
1321 /// `enable_h265` should produce Level 6.0 (level_id = 180).
1322 #[test]
1323 fn test_h265_default_level_is_6_0() {
1324 let mut config = CodecConfig::empty();
1325 config.enable_h265(true);
1326 
1327 let h265 = get_h265(&config);
1328 let ptl = h265.spec().format.h265_profile_tier_level.unwrap();
1329 assert_eq!(
1330 ptl.level_id(),
1331 180,
1332 "Default H.265 level should be 6.0 (180)"
1333 );
1334 assert_eq!(
1335 ptl.profile_id(),
1336 1,
1337 "Default H.265 profile should be Main (1)"
1338 );
1339 assert_eq!(ptl.tier_flag(), 0, "Default H.265 tier should be Main (0)");
1340 }
1341 
1342 // H.266 direction-based negotiation tests.
1343 
1344 /// Helper: build a CodecConfig containing only H.266 at the given PTL.
1345 fn h266_config(profile_id: u8, tier_flag: u8, level_id: u8) -> CodecConfig {
1346 let mut config = CodecConfig::empty();
1347 config.add_h266(PT_H266, Some(PT_H266_RTX), profile_id, tier_flag, level_id);
1348 config
1349 }
1350 
1351 /// Helper: build a single-element remote PayloadParams for H.266 with full PTL.
1352 fn h266_remote_ptl(profile_id: u8, tier_flag: u8, level_id: u8) -> Vec<PayloadParams> {
1353 vec![PayloadParams::new(
1354 PT_H266,
1355 Some(PT_H266_RTX),
1356 CodecSpec {
1357 codec: Codec::H266,
1358 clock_rate: Frequency::NINETY_KHZ,
1359 channels: None,
1360 format: FormatParams {
1361 h266_profile_tier_level: Some(
1362 H266ProfileTierLevel::new(profile_id, tier_flag, level_id).unwrap(),
1363 ),
1364 ..Default::default()
1365 },
1366 },
1367 )]
1368 }
1369 
1370 /// Helper: build a single-element remote PayloadParams for H.266 with profile-id only.
1371 fn h266_remote_profile_only(profile_id: u32) -> Vec<PayloadParams> {
1372 vec![PayloadParams::new(
1373 PT_H266,
1374 Some(PT_H266_RTX),
1375 CodecSpec {
1376 codec: Codec::H266,
1377 clock_rate: Frequency::NINETY_KHZ,
1378 channels: None,
1379 format: FormatParams {
1380 profile_id: Some(profile_id),
1381 ..Default::default()
1382 },
1383 },
1384 )]
1385 }
1386 
1387 /// Helper: build a single-element remote PayloadParams for H.266 with no fmtp.
1388 fn h266_remote_no_fmtp() -> Vec<PayloadParams> {
1389 vec![PayloadParams::new(
1390 PT_H266,
1391 Some(PT_H266_RTX),
1392 CodecSpec {
1393 codec: Codec::H266,
1394 clock_rate: Frequency::NINETY_KHZ,
1395 channels: None,
1396 format: FormatParams::default(),
1397 },
1398 )]
1399 }
1400 
1401 /// Helper: extract the H.266 param from a config (panics if missing).
1402 fn get_h266(config: &CodecConfig) -> &PayloadParams {
1403 config
1404 .params()
1405 .iter()
1406 .find(|p| p.spec().codec == Codec::H266)
1407 .expect("H.266 param missing")
1408 }
1409 
1410 // recvonly: remote sends to us.
1411 
1412 /// Remote sends at Level 4.0, we can receive at Level 6.0.
1413 /// Negotiated level should narrow to min(6.0, 4.0) = 4.0.
1414 #[test]
1415 fn test_h266_recvonly_remote_level_lower() {
1416 let mut config = h266_config(1, 0, 96); // Main 10, Main tier, Level 6.0
1417 let remote = h266_remote_ptl(1, 0, 64); // Level 4.0
1418 
1419 // Direction::SendOnly = remote is sending (we receive)
1420 config.update_params(&remote, Direction::SendOnly);
1421 
1422 let ptl = get_h266(&config)
1423 .spec()
1424 .format
1425 .h266_profile_tier_level
1426 .unwrap();
1427 assert_eq!(
1428 ptl.level_id(),
1429 64,
1430 "Should narrow to min(96,64) = Level 4.0"
1431 );
1432 assert_eq!(ptl.profile_id(), 1, "Profile should be preserved");
1433 assert_eq!(ptl.tier_flag(), 0, "Tier should be preserved");
1434 }
1435 
1436 /// Remote sends at Level 6.0, we can receive at Level 4.0.
1437 /// Negotiated level should narrow to min(4.0, 6.0) = 4.0.
1438 #[test]
1439 fn test_h266_recvonly_local_level_lower() {
1440 let mut config = h266_config(1, 0, 64); // Level 4.0
1441 let remote = h266_remote_ptl(1, 0, 96); // Level 6.0
1442 
1443 config.update_params(&remote, Direction::SendOnly);
1444 
1445 let ptl = get_h266(&config)
1446 .spec()
1447 .format
1448 .h266_profile_tier_level
1449 .unwrap();
1450 assert_eq!(
1451 ptl.level_id(),
1452 64,
1453 "Should narrow to min(64,96) = Level 4.0"
1454 );
1455 }
1456 
1457 // sendrecv: both sides send and receive.
1458 
1459 /// Both sides sendrecv. Local Level 6.0, remote Level 4.0.
1460 /// Effective level should narrow to min(6.0, 4.0) = 4.0.
1461 #[test]
1462 fn test_h266_sendrecv_remote_level_lower() {
1463 let mut config = h266_config(1, 0, 96); // Level 6.0
1464 let remote = h266_remote_ptl(1, 0, 64); // Level 4.0
1465 
1466 config.update_params(&remote, Direction::SendRecv);
1467 
1468 let ptl = get_h266(&config)
1469 .spec()
1470 .format
1471 .h266_profile_tier_level
1472 .unwrap();
1473 assert_eq!(ptl.level_id(), 64, "sendrecv: should narrow to Level 4.0");
1474 }
1475 
1476 /// Both sides sendrecv. Local Level 4.0, remote Level 6.0.
1477 /// Effective level should narrow to min(4.0, 6.0) = 4.0.
1478 #[test]
1479 fn test_h266_sendrecv_local_level_lower() {
1480 let mut config = h266_config(1, 0, 64); // Level 4.0
1481 let remote = h266_remote_ptl(1, 0, 96); // Level 6.0
1482 
1483 config.update_params(&remote, Direction::SendRecv);
1484 
1485 let ptl = get_h266(&config)
1486 .spec()
1487 .format
1488 .h266_profile_tier_level
1489 .unwrap();
1490 assert_eq!(ptl.level_id(), 64, "sendrecv: should narrow to Level 4.0");
1491 }
1492 
1493 /// Same level on both sides. Should remain unchanged.
1494 #[test]
1495 fn test_h266_sendrecv_same_level() {
1496 let mut config = h266_config(1, 0, 64); // Level 4.0
1497 let remote = h266_remote_ptl(1, 0, 64); // Level 4.0
1498 
1499 config.update_params(&remote, Direction::SendRecv);
1500 
1501 let ptl = get_h266(&config)
1502 .spec()
1503 .format
1504 .h266_profile_tier_level
1505 .unwrap();
1506 assert_eq!(ptl.level_id(), 64, "Same levels should stay at Level 4.0");
1507 }
1508 
1509 // sendonly: we send, remote receives.
1510 
1511 /// Remote declares recvonly at Level 4.0, we offer sendonly at Level 6.0.
1512 /// Negotiated level should narrow to 4.0 (receiver wins).
1513 #[test]
1514 fn test_h266_sendonly_receiver_level_lower() {
1515 let mut config = h266_config(1, 0, 96); // Level 6.0
1516 let remote = h266_remote_ptl(1, 0, 64); // Level 4.0
1517 
1518 // Direction::RecvOnly = remote is receiving (we send)
1519 config.update_params(&remote, Direction::RecvOnly);
1520 
1521 let ptl = get_h266(&config)
1522 .spec()
1523 .format
1524 .h266_profile_tier_level
1525 .unwrap();
1526 assert_eq!(
1527 ptl.level_id(),
1528 64,
1529 "sendonly: should narrow to receiver's Level 4.0"
1530 );
1531 }
1532 
1533 /// Remote offers only profile-id=1, no tier/level.
1534 /// Local should drop its full PTL and echo back profile-id only.
1535 #[test]
1536 fn test_h266_profile_only_remote_mirrors() {
1537 let mut config = h266_config(1, 0, 96); // Full PTL
1538 let remote = h266_remote_profile_only(1); // profile-id only
1539 
1540 config.update_params(&remote, Direction::SendRecv);
1541 
1542 let h266 = get_h266(&config);
1543 assert!(
1544 h266.spec().format.h266_profile_tier_level.is_none(),
1545 "Full PTL should be cleared when remote only offers profile-id"
1546 );
1547 assert_eq!(
1548 h266.spec().format.profile_id,
1549 Some(1),
1550 "profile_id should mirror the remote's value"
1551 );
1552 }
1553 
1554 // no-fmtp remote.
1555 
1556 /// Remote has no H.266 fmtp at all. Local should clear both fields.
1557 #[test]
1558 fn test_h266_no_fmtp_remote_clears() {
1559 let mut config = h266_config(1, 0, 96); // Full PTL
1560 let remote = h266_remote_no_fmtp();
1561 
1562 config.update_params(&remote, Direction::SendRecv);
1563 
1564 let h266 = get_h266(&config);
1565 assert!(
1566 h266.spec().format.h266_profile_tier_level.is_none(),
1567 "PTL should be cleared when remote has no fmtp"
1568 );
1569 assert!(
1570 h266.spec().format.profile_id.is_none(),
1571 "profile_id should be cleared when remote has no fmtp"
1572 );
1573 }
1574 
1575 // profile_id field cleanup.
1576 
1577 /// When remote has full PTL, the `profile_id` field must be None
1578 /// to avoid duplicate serialization (profile-id appears in both
1579 /// h266_profile_tier_level AND the VP9-style profile_id field).
1580 #[test]
1581 fn test_h266_profile_id_cleared_on_ptl_match() {
1582 let mut config = CodecConfig::empty();
1583 // Artificially set both fields to simulate a misconfigured state
1584 config.add_config(
1585 PT_H266,
1586 Some(PT_H266_RTX),
1587 Codec::H266,
1588 Frequency::NINETY_KHZ,
1589 None,
1590 FormatParams {
1591 h266_profile_tier_level: Some(H266ProfileTierLevel::new(1, 0, 96).unwrap()),
1592 profile_id: Some(1), // should be cleared after negotiation
1593 ..Default::default()
1594 },
1595 );
1596 let remote = h266_remote_ptl(1, 0, 64);
1597 
1598 config.update_params(&remote, Direction::SendRecv);
1599 
1600 let h266 = get_h266(&config);
1601 assert!(
1602 h266.spec().format.profile_id.is_none(),
1603 "profile_id field must be None when full PTL is present"
1604 );
1605 assert!(
1606 h266.spec().format.h266_profile_tier_level.is_some(),
1607 "PTL should still be present"
1608 );
1609 }
1610 
1611 // H.266 default level.
1612 
1613 /// `enable_h266(true)` should add H.266 at Main 10 / Main tier / Level 6.0.
1614 #[test]
1615 fn test_h266_enable_default_level() {
1616 let mut config = CodecConfig::empty();
1617 config.enable_h266(true);
1618 
1619 let h266 = config
1620 .params()
1621 .iter()
1622 .find(|p| p.spec().codec == Codec::H266)
1623 .expect("H.266 param missing");
1624 let ptl = h266.spec().format.h266_profile_tier_level.unwrap();
1625 assert_eq!(
1626 ptl.profile_id(),
1627 1,
1628 "Default H.266 profile should be Main 10 (1)"
1629 );
1630 assert_eq!(ptl.tier_flag(), 0, "Default H.266 tier should be Main (0)");
1631 assert_eq!(ptl.level_id(), 96, "Default H.266 level should be 6.0 (96)");
1632 }
1633}