Skip to content
File

Blob: firmware/vendor/str0m/src/sdp/data.rs

rust3721 lines
1#![allow(clippy::single_match)]
2 
3use combine::EasyParser;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::fmt::{self};
7use std::ops::Deref;
8 
9use crate::crypto::Fingerprint;
10use crate::format::Codec;
11use crate::format::CodecSpec;
12use crate::format::FormatParams;
13use crate::format::PayloadParams;
14use crate::packet::H265ProfileTierLevel;
15use crate::packet::H266ProfileTierLevel;
16use crate::rtp_::{Direction, Extension, Frequency, Mid, Pt, Rid, SessionId, Ssrc};
17use crate::{Candidate, IceCreds, VERSION};
18use str0m_proto::Id;
19 
20use super::SdpError;
21use super::parser::sdp_parser;
22 
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Sdp {
25 pub session: Session,
26 pub media_lines: Vec<MediaLine>,
27}
28 
29impl Sdp {
30 pub(crate) fn parse(input: &str) -> Result<Sdp, SdpError> {
31 sdp_parser()
32 .easy_parse(input)
33 .map(|(sdp, _)| sdp)
34 .map_err(|e| SdpError::ParseError(e.to_string()))
35 }
36 
37 /// Get the MIDs listed in the BUNDLE group, if any.
38 pub(crate) fn bundle_mids(&self) -> Option<&[Mid]> {
39 self.session.attrs.iter().find_map(|a| {
40 if let SessionAttribute::Group { typ, mids } = a {
41 if typ == "BUNDLE" {
42 return Some(mids.as_slice());
43 }
44 }
45 None
46 })
47 }
48 
49 pub(crate) fn assert_consistency(&self) -> Result<(), SdpError> {
50 match self.do_assert_consistency() {
51 None => Ok(()),
52 Some(error) => Err(SdpError::Inconsistent(error)),
53 }
54 }
55 
56 pub(crate) fn fingerprint(&self) -> Option<Fingerprint> {
57 self.session
58 .fingerprint()
59 .or_else(|| self.media_lines.iter().find_map(|m| m.fingerprint()))
60 }
61 
62 pub(crate) fn ice_creds(&self) -> Option<IceCreds> {
63 self.session
64 .ice_creds()
65 .or_else(|| self.media_lines.iter().find_map(|m| m.ice_creds()))
66 }
67 
68 pub(crate) fn ice_candidates(&self) -> impl Iterator<Item = &Candidate> {
69 let mut candidates: HashSet<&Candidate> = HashSet::new();
70 
71 // Session level ice candidates.
72 candidates.extend(self.session.ice_candidates());
73 
74 // Ice candidates.
75 for m in &self.media_lines {
76 candidates.extend(m.ice_candidates());
77 }
78 
79 candidates.into_iter()
80 }
81 
82 /// Get the `a=sctp-init` value from the application m-line, if present.
83 ///
84 /// Returns the base64-encoded SCTP INIT value.
85 pub(crate) fn sctp_init(&self) -> Option<&str> {
86 self.media_lines
87 .iter()
88 .find(|m| m.typ.is_channel())
89 .and_then(|m| m.sctp_init())
90 }
91 
92 pub(crate) fn setup(&self) -> Option<Setup> {
93 self.session
94 .setup()
95 .or_else(|| self.media_lines.iter().find_map(|m| m.setup()))
96 }
97 
98 fn do_assert_consistency(&self) -> Option<String> {
99 // TODO: SDP assertions we need to make:
100 // 1. Ensure that every m-line has the same PT configuration for a codec. I.e. if FIR is enabled
101 // for PT 96 in one m-line it must be in all m-lines.
102 // 2. Compare with previous m-lines that remote isn't narrowing/expanding PT and/or extmaps.
103 
104 let group = self
105 .session
106 .attrs
107 .iter()
108 .find(|a| matches!(a, SessionAttribute::Group { .. }));
109 
110 if let Some(SessionAttribute::Group { mids, .. }) = group {
111 if !mids.len() == self.media_lines.len() {
112 return Some(format!(
113 "a=group mid count doesn't match m-line count {} != {}",
114 mids.len(),
115 self.media_lines.len()
116 ));
117 }
118 for (media, mid) in self.media_lines.iter().zip(mids.iter()) {
119 media.check_consistent()?;
120 let m = media.mid();
121 if m != *mid {
122 return Some(format!("Mid order not matching a=group {m} != {mid}"));
123 }
124 }
125 } else {
126 return Some("Session attribute a=group missing".into());
127 }
128 
129 None
130 }
131}
132 
133/// Session info, before the first m= line
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct Session {
136 pub id: SessionId,
137 pub bw: Option<Bandwidth>,
138 pub attrs: Vec<SessionAttribute>,
139}
140 
141/// Bandwidth from b= line
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct Bandwidth {
144 pub typ: String,
145 pub val: String,
146}
147 
148impl Session {
149 pub fn setup(&self) -> Option<Setup> {
150 let setup = self.attrs.iter().find_map(|m| {
151 if let SessionAttribute::Setup(v) = m {
152 Some(v)
153 } else {
154 None
155 }
156 })?;
157 
158 Some(*setup)
159 }
160 
161 pub fn ice_creds(&self) -> Option<IceCreds> {
162 let ufrag = self.attrs.iter().find_map(|m| {
163 if let SessionAttribute::IceUfrag(v) = m {
164 Some(v)
165 } else {
166 None
167 }
168 })?;
169 
170 let pass = self.attrs.iter().find_map(|m| {
171 if let SessionAttribute::IcePwd(v) = m {
172 Some(v)
173 } else {
174 None
175 }
176 })?;
177 
178 Some(IceCreds {
179 ufrag: ufrag.to_string(),
180 pass: pass.to_string(),
181 })
182 }
183 
184 pub fn fingerprint(&self) -> Option<Fingerprint> {
185 for a in &self.attrs {
186 if let SessionAttribute::Fingerprint(v) = a {
187 return Some(v.clone());
188 }
189 }
190 None
191 }
192 
193 pub fn ice_lite(&self) -> bool {
194 self.attrs
195 .iter()
196 .any(|a| matches!(a, SessionAttribute::IceLite))
197 }
198 
199 pub fn ice_candidates(&self) -> impl Iterator<Item = &Candidate> {
200 self.attrs.iter().filter_map(|a| {
201 if let SessionAttribute::Candidate(v) = a {
202 Some(v)
203 } else {
204 None
205 }
206 })
207 }
208 
209 pub fn end_of_candidates(&self) -> bool {
210 self.attrs
211 .iter()
212 .any(|a| matches!(a, SessionAttribute::EndOfCandidates))
213 }
214}
215 
216/// Attributes before the first m= line.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub enum SessionAttribute {
219 Group {
220 typ: String, // BUNDLE, LS etc
221 mids: Vec<Mid>, // 0 1 2 3
222 },
223 MsidSemantic {
224 semantic: String, // WMS
225 stream_ids: Vec<String>,
226 },
227 AllowMixedExts,
228 IceLite,
229 IceUfrag(String),
230 IcePwd(String),
231 IceOptions(String),
232 Fingerprint(Fingerprint),
233 Setup(Setup), // active, passive, actpass, holdconn
234 Candidate(Candidate),
235 EndOfCandidates,
236 Unused(String),
237}
238 
239fn is_dir(a: &MediaAttribute) -> bool {
240 use MediaAttribute::*;
241 matches!(a, SendRecv | SendOnly | RecvOnly | Inactive)
242}
243 
244/// An m-line
245#[derive(Debug, Default, Clone, PartialEq, Eq)]
246pub struct MediaLine {
247 pub typ: MediaType,
248 pub disabled: bool,
249 pub proto: Proto,
250 pub pts: Vec<Pt>, // payload types 96 97 125 107 from the m= line
251 pub bw: Option<Bandwidth>,
252 pub attrs: Vec<MediaAttribute>,
253}
254 
255impl MediaLine {
256 pub fn mid(&self) -> Mid {
257 self.attrs
258 .iter()
259 .find_map(|a| {
260 if let MediaAttribute::Mid(m) = a {
261 Some(*m)
262 } else {
263 None
264 }
265 })
266 // We should only use `mid()` once we're certain there is
267 // a mid line. This is checked by `check_consistent`.
268 .expect("missing a=mid")
269 }
270 
271 /// Get the sctp-init attribute value (base64-encoded string).
272 ///
273 /// Returns `None` if no `a=sctp-init` attribute is present.
274 pub fn sctp_init(&self) -> Option<&str> {
275 self.attrs.iter().find_map(|a| {
276 if let MediaAttribute::SctpInit(v) = a {
277 Some(v.as_str())
278 } else {
279 None
280 }
281 })
282 }
283 
284 pub fn msid(&self) -> Option<Msid> {
285 self.attrs.iter().find_map(|a| {
286 if let MediaAttribute::Msid(m) = a {
287 Some(m.clone())
288 } else {
289 None
290 }
291 })
292 }
293 
294 pub fn direction(&self) -> Direction {
295 for a in &self.attrs {
296 match a {
297 MediaAttribute::SendRecv => return Direction::SendRecv,
298 MediaAttribute::SendOnly => return Direction::SendOnly,
299 MediaAttribute::RecvOnly => return Direction::RecvOnly,
300 MediaAttribute::Inactive => return Direction::Inactive,
301 _ => {}
302 }
303 }
304 // Should we error here?
305 Direction::Inactive
306 }
307 
308 pub fn set_direction(&mut self, dir: Direction) {
309 let idx = self
310 .attrs
311 .iter()
312 .position(is_dir)
313 .expect("m-line must have direction");
314 self.attrs[idx] = dir.into();
315 }
316 
317 pub fn rtp_params(&self) -> Vec<PayloadParams> {
318 let mut rtp_maps: Vec<_> = self
319 .attrs
320 .iter()
321 .filter_map(|a| {
322 if let MediaAttribute::RtpMap { pt, value: c } = a {
323 Some((*pt, *c))
324 } else {
325 None
326 }
327 })
328 .collect();
329 
330 for pt in &self.pts {
331 if !rtp_maps.iter().any(|(mapped, _)| mapped == pt) {
332 if let Some(spec) = CodecSpec::from_static_pt(*pt) {
333 rtp_maps.push((*pt, spec.into()));
334 }
335 }
336 }
337 
338 let fmtps: Vec<_> = self
339 .attrs
340 .iter()
341 .filter_map(|a| {
342 if let MediaAttribute::Fmtp { pt, values } = a {
343 Some((pt, values))
344 } else {
345 None
346 }
347 })
348 .collect();
349 
350 let fbs: Vec<_> = self
351 .attrs
352 .iter()
353 .filter_map(|a| {
354 if let MediaAttribute::RtcpFb { pt, value } = a {
355 Some((pt, value))
356 } else {
357 None
358 }
359 })
360 .collect();
361 
362 let mut params: Vec<_> = rtp_maps
363 .iter()
364 .filter(|(_, c)| c.codec.is_audio() | c.codec.is_video())
365 .map(|(pt, c)| PayloadParams::new(*pt, None, (*c).into()))
366 .collect();
367 
368 for p in &mut params {
369 for (pt, values) in fmtps.iter() {
370 // find matching a=fmtp line, if it exists.
371 if **pt == p.pt {
372 for param in values.iter() {
373 p.spec.format.set_param(param);
374 }
375 }
376 
377 // find resend pt, if there is one.
378 for fp in values.iter() {
379 if let FormatParam::Apt(v) = fp {
380 if *v == p.pt {
381 // ensure this is a rtx
382 let is_rtx = rtp_maps
383 .iter()
384 .any(|(cpt, c)| cpt == *pt && c.codec == Codec::Rtx);
385 if is_rtx {
386 p.resend = Some(**pt);
387 }
388 }
389 }
390 }
391 }
392 
393 // rtcp feedback mechanisms
394 for (pt, value) in fbs.iter() {
395 if **pt == p.pt {
396 match &value[..] {
397 "goog-remb" => {
398 p.fb_remb = true;
399 }
400 "transport-cc" => {
401 p.fb_transport_cc = true;
402 }
403 "ccm fir" => {
404 p.fb_fir = true;
405 }
406 "nack" => {
407 p.fb_nack = true;
408 }
409 "nack pli" => {
410 p.fb_pli = true;
411 }
412 _ => {
413 //
414 }
415 }
416 }
417 }
418 }
419 
420 params
421 }
422 
423 pub fn check_consistent(&self) -> Option<String> {
424 use MediaAttribute::*;
425 
426 let mid_count = self.attrs.iter().filter(|a| matches!(a, Mid(_))).count();
427 
428 if mid_count == 0 {
429 return Some(format!(
430 "Media is missing a=mid: {} {}",
431 self.typ, self.proto
432 ));
433 }
434 
435 if mid_count > 1 {
436 return Some(format!(
437 "Media has more than one a=mid: {} {}",
438 self.typ, self.proto
439 ));
440 }
441 
442 let setup = self.attrs.iter().filter(|a| matches!(a, Setup(_))).count();
443 
444 if setup > 1 {
445 return Some(format!(
446 "Expected 0 or 1 a=setup: line for mid: {}",
447 self.mid()
448 ));
449 }
450 
451 let dir_count = self.attrs.iter().filter(|a| is_dir(a)).count();
452 
453 if self.proto == Proto::Srtp && dir_count != 1 {
454 return Some(format!(
455 "Expected exactly one of a=sendrecv, a=sendonly, a=recvonly, a=inactive for mid: {}",
456 self.mid()
457 ));
458 }
459 
460 if self.proto == Proto::Srtp && self.pts.is_empty() {
461 return Some(format!("Expected at least one PT for mid: {}", self.mid()));
462 }
463 
464 for m in &self.pts {
465 let rtp_count = self
466 .attrs
467 .iter()
468 .filter(|a| {
469 if let MediaAttribute::RtpMap { pt, value: _ } = a {
470 pt == m
471 } else {
472 false
473 }
474 })
475 .count();
476 if rtp_count == 0 && CodecSpec::from_static_pt(*m).is_none() {
477 return Some(format!("Missing a=rtp_map:{} for mid: {}", m, self.mid()));
478 }
479 if rtp_count > 1 {
480 return Some(format!(
481 "More than one a=rtp_map:{} for mid: {}",
482 m,
483 self.mid()
484 ));
485 }
486 }
487 None
488 }
489 
490 pub fn setup(&self) -> Option<Setup> {
491 let setup = self.attrs.iter().find_map(|m| {
492 if let MediaAttribute::Setup(v) = m {
493 Some(v)
494 } else {
495 None
496 }
497 })?;
498 
499 Some(*setup)
500 }
501 
502 pub fn ice_creds(&self) -> Option<IceCreds> {
503 let ufrag = self.attrs.iter().find_map(|m| {
504 if let MediaAttribute::IceUfrag(v) = m {
505 Some(v)
506 } else {
507 None
508 }
509 })?;
510 
511 let pass = self.attrs.iter().find_map(|m| {
512 if let MediaAttribute::IcePwd(v) = m {
513 Some(v)
514 } else {
515 None
516 }
517 })?;
518 
519 Some(IceCreds {
520 ufrag: ufrag.to_string(),
521 pass: pass.to_string(),
522 })
523 }
524 pub fn fingerprint(&self) -> Option<Fingerprint> {
525 for a in &self.attrs {
526 if let MediaAttribute::Fingerprint(v) = a {
527 return Some(v.clone());
528 }
529 }
530 None
531 }
532 
533 /// This hoovers the ice candidates from all m-lines, lots of dupes.
534 /// For WebRTC we don't expect different ice states per media line.
535 pub fn ice_candidates(&self) -> impl Iterator<Item = &Candidate> {
536 self.attrs.iter().filter_map(|a| {
537 if let MediaAttribute::Candidate(v) = a {
538 Some(v)
539 } else {
540 None
541 }
542 })
543 }
544 
545 /// Any end-of-candidate in any m-line.
546 /// For WebRTC we don't expect different ice states per media line.
547 pub fn end_of_candidates(&self) -> bool {
548 self.attrs
549 .iter()
550 .any(|a| matches!(a, MediaAttribute::EndOfCandidates))
551 }
552 
553 pub fn extmaps(&self) -> Vec<(u8, &Extension)> {
554 let mut ret = vec![];
555 
556 for a in &self.attrs {
557 if let MediaAttribute::ExtMap { id, ext } = a {
558 ret.push((*id, ext));
559 }
560 }
561 
562 ret
563 }
564 
565 pub fn rids(&self) -> Vec<Rid> {
566 let mut ret = vec![];
567 for a in &self.attrs {
568 if let MediaAttribute::Rid { id, .. } = a {
569 ret.push(id.0.as_str().into())
570 }
571 }
572 ret
573 }
574 
575 pub fn simulcast(&self) -> Option<Simulcast> {
576 let mut found = None;
577 
578 for a in &self.attrs {
579 if let MediaAttribute::Simulcast(s) = a {
580 found = Some(s.clone());
581 }
582 if let MediaAttribute::Rid {
583 pt, restriction, ..
584 } = a
585 {
586 if !pt.is_empty() {
587 warn!("Not currently supporting PT via a=rid");
588 }
589 if !restriction.is_empty() {
590 warn!("Not currently supporting restrictions via a=rid");
591 }
592 }
593 }
594 
595 if found.is_some() {
596 return found;
597 }
598 
599 // Here we could handle munged SDPs and we used to, but we have dropped support for this.
600 
601 None
602 }
603 
604 pub fn ssrc_info(&self) -> Vec<SsrcInfo> {
605 let mut v = vec![];
606 
607 fn by_ssrc(v: &mut Vec<SsrcInfo>, ssrc: Ssrc) -> &mut SsrcInfo {
608 if let Some(pos) = v.iter().position(|i| i.ssrc == ssrc) {
609 &mut v[pos]
610 } else {
611 v.push(SsrcInfo {
612 ssrc,
613 ..Default::default()
614 });
615 v.last_mut().unwrap()
616 }
617 }
618 
619 for a in &self.attrs {
620 match a {
621 MediaAttribute::Ssrc { ssrc, attr, value } => {
622 let info = by_ssrc(&mut v, *ssrc);
623 
624 // a=ssrc:2147603131 cname:TbS1Ajv9obq6/63I
625 // a=ssrc:2147603131 msid:- 7a08dda6-518f-4027-b707-410a6d414176
626 match attr.to_lowercase().as_str() {
627 "cname" => info.cname = Some(value.clone()),
628 "msid" => {
629 let mut iter = value.split(' ');
630 
631 fn trim_and_no_minus(s: &str) -> Option<String> {
632 let s = s.trim();
633 
634 if s == "-" { None } else { Some(s.into()) }
635 }
636 
637 if let Some(stream_id) = iter.next() {
638 info.stream_id = trim_and_no_minus(stream_id);
639 }
640 if let Some(track_id) = iter.next() {
641 info.track_id = trim_and_no_minus(track_id);
642 }
643 }
644 _ => {}
645 }
646 }
647 _ => {}
648 }
649 }
650 // Match this second to ensure we preserve order of a=ssrc.
651 for a in &self.attrs {
652 match a {
653 MediaAttribute::SsrcGroup { semantics, ssrcs } => {
654 if semantics.to_lowercase() != "fid" {
655 continue;
656 }
657 
658 // a=ssrc-group:FID 659652645 98148385
659 // Should be two SSRC after FID.
660 if ssrcs.len() != 2 {
661 continue;
662 }
663 
664 let info = by_ssrc(&mut v, ssrcs[1]);
665 info.repairs = Some(ssrcs[0]);
666 }
667 _ => {}
668 }
669 }
670 
671 v
672 }
673 
674 /// Get the max-message-size attribute value, if present
675 pub fn max_message_size(&self) -> Option<usize> {
676 self.attrs.iter().find_map(|a| {
677 if let MediaAttribute::MaxMessageSize(size) = a {
678 Some(*size)
679 } else {
680 None
681 }
682 })
683 }
684}
685 
686#[derive(Debug, Clone, PartialEq, Eq)]
687pub struct SsrcInfo {
688 pub ssrc: Ssrc,
689 /// the other ssrc this ssrc is repairing
690 pub repairs: Option<Ssrc>,
691 pub cname: Option<String>,
692 pub stream_id: Option<String>,
693 pub track_id: Option<String>,
694}
695 
696impl Default for SsrcInfo {
697 fn default() -> Self {
698 Self {
699 ssrc: 0.into(),
700 repairs: None,
701 cname: None,
702 stream_id: None,
703 track_id: None,
704 }
705 }
706}
707 
708impl From<Direction> for MediaAttribute {
709 fn from(v: Direction) -> Self {
710 match v {
711 Direction::SendOnly => MediaAttribute::SendOnly,
712 Direction::RecvOnly => MediaAttribute::RecvOnly,
713 Direction::SendRecv => MediaAttribute::SendRecv,
714 Direction::Inactive => MediaAttribute::Inactive,
715 }
716 }
717}
718 
719/// Identifier of an `a=rid` restriction.
720///
721/// Defined in https://tools.ietf.org/html/draft-ietf-avtext-rid-09
722#[derive(Debug, Clone, PartialEq, Eq)]
723pub struct RestrictionId(pub String, pub bool);
724 
725impl RestrictionId {
726 pub fn new(rid: String, active: bool) -> Self {
727 RestrictionId(rid, active)
728 }
729 
730 pub fn new_active(rid: String) -> Self {
731 RestrictionId(rid, true)
732 }
733 
734 pub fn to_sdp(&self) -> String {
735 format!("{}{}", if self.1 { "" } else { "~" }, self.0)
736 }
737}
738 
739impl Default for RestrictionId {
740 fn default() -> Self {
741 RestrictionId("".to_string(), true)
742 }
743}
744 
745/// "audio", "video", "application"
746#[derive(Debug, Default, Clone, PartialEq, Eq)]
747pub enum MediaType {
748 #[default]
749 Audio,
750 Video,
751 Application,
752 // If a parsed SDP has a value we don't recognize, we stick it
753 // in here. We could consider making that a parse exception instead.
754 #[doc(hidden)]
755 Unknown(String),
756}
757 
758impl MediaType {
759 pub fn is_media(&self) -> bool {
760 matches!(self, MediaType::Audio | MediaType::Video)
761 }
762 
763 pub fn is_channel(&self) -> bool {
764 matches!(self, MediaType::Application)
765 }
766}
767 
768#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
769pub enum Proto {
770 #[default]
771 Srtp,
772 Sctp,
773}
774 
775impl Proto {
776 pub fn proto_line(&self) -> &str {
777 match self {
778 Proto::Srtp => "UDP/TLS/RTP/SAVPF",
779 Proto::Sctp => "UDP/DTLS/SCTP",
780 }
781 }
782}
783 
784#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
785pub enum Setup {
786 #[default]
787 ActPass,
788 Active,
789 Passive,
790}
791 
792impl fmt::Display for Setup {
793 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
794 write!(
795 f,
796 "{}",
797 match self {
798 Setup::ActPass => "actpass",
799 Setup::Active => "active",
800 Setup::Passive => "passive",
801 }
802 )
803 }
804}
805 
806impl Setup {
807 pub fn setup_line(&self) -> &str {
808 match self {
809 Setup::ActPass => "actpass",
810 Setup::Active => "active",
811 Setup::Passive => "passive",
812 }
813 }
814 
815 pub fn compare_to_remote(&self, remote: Setup) -> Option<Setup> {
816 use Setup::*;
817 match (self, remote) {
818 (ActPass, ActPass) => None,
819 (ActPass, Active) => Some(Passive),
820 (ActPass, Passive) => Some(Active),
821 (Active, ActPass) => Some(Active),
822 (Active, Active) => None,
823 (Active, Passive) => Some(Active),
824 (Passive, ActPass) => Some(Passive),
825 (Passive, Active) => Some(Passive),
826 (Passive, Passive) => None,
827 }
828 }
829 
830 pub fn invert(&self) -> Setup {
831 match self {
832 Setup::ActPass => Setup::ActPass,
833 Setup::Active => Setup::Passive,
834 Setup::Passive => Setup::Active,
835 }
836 }
837}
838 
839/// Attributes before the first m= line.
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub enum MediaAttribute {
842 // The "a=rtcp" line MUST NOT be added if the most recent answer included an "a=rtcp-mux" line.
843 Rtcp(String),
844 IceUfrag(String),
845 IcePwd(String),
846 IceOptions(String),
847 Fingerprint(Fingerprint),
848 Setup(Setup), // active, passive, actpass, holdconn
849 Mid(Mid), // 0, 1, 2
850 SctpPort(u16),
851 MaxMessageSize(usize),
852 /// a=sctp-init:<base64-encoded SCTP INIT chunk>
853 /// See draft-hancke-tsvwg-snap
854 SctpInit(String),
855 // a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
856 // a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
857 ExtMap {
858 id: u8, // 1-14 inclusive,
859 ext: Extension,
860 },
861 RecvOnly, // a=recvonly
862 SendRecv, // a=sendrecv
863 SendOnly, // a=sendonly
864 Inactive, // a=inactive
865 // a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1
866 // a=msid:- 78dde68-7055-4e20-bb37-433803dd1ed1
867 Msid(Msid),
868 RtcpMux, //
869 RtcpMuxOnly, // only in offer, answer with a=rtcp-mux
870 // reduced size rtcp. remove this if not supported.
871 RtcpRsize,
872 Candidate(Candidate),
873 EndOfCandidates,
874 RtpMap {
875 pt: Pt,
876 value: RtpMap,
877 },
878 // rtcp-fb RTCP feedback parameters, repeated
879 RtcpFb {
880 pt: Pt, // 111
881 value: String, // nack, nack pli, ccm fir...
882 },
883 // format parameters, seems to be one of these
884 Fmtp {
885 pt: Pt, // 111
886 values: Vec<FormatParam>, // minptime=10;useinbandfec=1
887 },
888 // a=rid:<rid-id> <direction> [pt=<fmt-list>;]<restriction>=<value>
889 // a=rid:hi send pt=111,112;max-br=64000;max-height=360
890 // https://tools.ietf.org/html/draft-ietf-mmusic-rid-15
891 Rid {
892 id: RestrictionId, //
893 direction: &'static str, // send or recv
894 // No pt means the rid applies to all
895 pt: Vec<Pt>, // 111, 112 (rtpmap no)
896 restriction: Vec<(String, String)>,
897 },
898 // a=rid:hi send
899 // a=rid:lo send
900 // a=simulcast:send hi;lo
901 // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-14
902 // a=simulcast:<send/recv> <alt A>;<alt B>,<or C> <send/recv> [same]
903 Simulcast(Simulcast),
904 SsrcGroup {
905 semantics: String, // i.e. "FID"
906 ssrcs: Vec<Ssrc>, // <normal stream> <repair stream>
907 },
908 Ssrc {
909 ssrc: Ssrc, // synchronization source id
910 attr: String,
911 value: String,
912 },
913 Unused(String),
914}
915 
916impl MediaAttribute {
917 pub fn is_direction(&self) -> bool {
918 use MediaAttribute::*;
919 matches!(self, RecvOnly | SendRecv | SendOnly | Inactive)
920 }
921}
922 
923#[derive(Debug, Clone, Copy, PartialEq, Eq)]
924pub enum FormatParam {
925 /// The minimum duration of media represented by a packet.
926 ///
927 /// Default 3. Max 120.
928 MinPTime(u8),
929 
930 /// Decoder-side preference for stereo audio. Default is mono per
931 /// RFC 7587 Section 7.1: <https://datatracker.ietf.org/doc/html/rfc7587#section-7.1>
932 Stereo(bool),
933 
934 /// Sender-side stereo hint for Opus. This is the symmetric m-line companion
935 /// to `stereo` and signals the sender's intention to transmit stereo.
936 SpropStereo(bool),
937 
938 /// Specifies that the decoder can do Opus in-band FEC
939 UseInbandFec(bool),
940 
941 /// Specifies that the decoder can do Opus DTX
942 UseDtx(bool),
943 
944 /// Whether h264 sending media encoded at a different level in the offerer-to-answerer
945 /// direction than the level in the answerer-to-offerer direction, is allowed.
946 LevelAsymmetryAllowed(bool),
947 
948 /// What h264 packetization mode is used.
949 ///
950 /// * 0 - single nal.
951 /// * 1 - STAP-A, FU-A is allowed. Non-interleaved.
952 PacketizationMode(u8),
953 
954 /// H264 profile level.
955 ///
956 /// * 42 00 1f - 4200=baseline (B) 1f=level 3.1
957 /// * 42 e0 1f - 42e0=constrained baseline (CB) 1f=level 3.1
958 /// * 4d 00 1f - 4d00=main (M) 1f=level 3.1
959 /// * 64 00 1f - 6400=high (H) 1f=level 3.1
960 ProfileLevelId(u32),
961 
962 /// VP9 profile id
963 ProfileId(u32),
964 
965 /// AV1 profile
966 Profile(u8),
967 
968 /// AV1 level-idx
969 LevelIdx(u8),
970 
971 /// AV1 tier
972 Tier(u8),
973 
974 /// H.265/HEVC profile, tier, and level.
975 H265ProfileTierLevel(crate::packet::H265ProfileTierLevel),
976 
977 /// H.266/VVC profile, tier, and level.
978 H266ProfileTierLevel(crate::packet::H266ProfileTierLevel),
979 
980 /// H.265 sprop-max-don-diff parameter (RFC 7798 ยง7.1).
981 /// When > 0, DONL fields are included in RTP packets to support
982 /// out-of-order NAL unit decoding. Valid range: 0โ€“32767.
983 SpropMaxDonDiff(u16),
984 
985 /// RTX (resend) codecs, which PT it concerns.
986 Apt(Pt),
987 
988 /// Unrecognized fmtp.
989 Unknown,
990}
991 
992impl FormatParam {
993 pub fn parse(k: &str, v: &str) -> Self {
994 use FormatParam::*;
995 match k {
996 "minptime" => v.parse().map(MinPTime).ok(),
997 "useinbandfec" => Some(UseInbandFec(v == "1")),
998 "usedtx" => Some(UseDtx(v == "1")),
999 "stereo" => Some(Stereo(v == "1")),
1000 "sprop-stereo" => Some(SpropStereo(v == "1")),
1001 "level-asymmetry-allowed" => Some(LevelAsymmetryAllowed(v == "1")),
1002 "packetization-mode" => v.parse().map(PacketizationMode).ok(),
1003 "profile-level-id" => u32::from_str_radix(v, 16)
1004 .or_else(|_| v.parse())
1005 .map(ProfileLevelId)
1006 .ok(),
1007 "profile-id" => v.parse().map(ProfileId).ok(),
1008 "profile" => v.parse().map(Profile).ok(),
1009 "level-idx" => v.parse().map(LevelIdx).ok(),
1010 "tier" => v.parse().map(Tier).ok(),
1011 "sprop-max-don-diff" => v
1012 .parse::<u16>()
1013 .ok()
1014 .filter(|&v| v <= 32767)
1015 .map(SpropMaxDonDiff),
1016 "apt" => v.parse::<u8>().map(|v| Apt(Pt::from(v))).ok(),
1017 _ => None,
1018 }
1019 .unwrap_or_else(|| {
1020 trace!("Failed to parse FormatParam: {k}={v}");
1021 Unknown
1022 })
1023 }
1024 
1025 /// Parse multiple format parameters from key-value pairs.
1026 /// Handles the H.265/H.266 special case where three params combine into
1027 /// one composite ProfileTierLevel.
1028 pub fn parse_pairs(pairs: Vec<(String, String)>) -> Vec<FormatParam> {
1029 // Check if this looks like H.265/H.266 by presence of tier-flag or level-id.
1030 // Both codecs use the same three fmtp keys (RFC 7798 / RFC 9328);
1031 // their level-id value spaces are disjoint (H.265: 30ร—level, e.g. 93;
1032 // H.266: 16ร—major + 3ร—minor, e.g. 51), which disambiguates them.
1033 let is_ptl = pairs
1034 .iter()
1035 .any(|(k, _)| k == "tier-flag" || k == "level-id");
1036 
1037 if is_ptl {
1038 // Build composite ProfileTierLevel: H.265 first, else H.266.
1039 let map: HashMap<String, String> = pairs.into_iter().collect();
1040 
1041 let mut result = vec![];
1042 if let Some(ptl) = H265ProfileTierLevel::from_fmtp(&map) {
1043 result.push(FormatParam::H265ProfileTierLevel(ptl));
1044 } else if let Some(ptl) = H266ProfileTierLevel::from_fmtp(&map) {
1045 result.push(FormatParam::H266ProfileTierLevel(ptl));
1046 }
1047 
1048 // Include non-PTL parameters.
1049 for (k, v) in map.iter() {
1050 if k != "profile-id" && k != "tier-flag" && k != "level-id" {
1051 result.push(FormatParam::parse(k, v));
1052 }
1053 }
1054 
1055 result
1056 } else {
1057 // Standard parsing for non-PTL codecs.
1058 pairs
1059 .into_iter()
1060 .map(|(k, v)| FormatParam::parse(&k, &v))
1061 .collect()
1062 }
1063 }
1064}
1065 
1066impl fmt::Display for FormatParam {
1067 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1068 use FormatParam::*;
1069 match self {
1070 MinPTime(v) => write!(f, "minptime={v}"),
1071 UseInbandFec(v) => write!(f, "useinbandfec={}", i32::from(*v)),
1072 UseDtx(v) => write!(f, "usedtx={}", i32::from(*v)),
1073 Stereo(v) => write!(f, "stereo={}", i32::from(*v)),
1074 SpropStereo(v) => write!(f, "sprop-stereo={}", i32::from(*v)),
1075 LevelAsymmetryAllowed(v) => {
1076 write!(f, "level-asymmetry-allowed={}", i32::from(*v))
1077 }
1078 PacketizationMode(v) => write!(f, "packetization-mode={}", *v),
1079 ProfileLevelId(v) => write!(f, "profile-level-id={:06x}", *v),
1080 ProfileId(v) => write!(f, "profile-id={}", *v),
1081 Profile(v) => write!(f, "profile={}", v),
1082 LevelIdx(v) => write!(f, "level-idx={}", *v),
1083 Tier(v) => write!(f, "tier={}", v),
1084 H265ProfileTierLevel(ptl) => {
1085 write!(
1086 f,
1087 "profile-id={};tier-flag={};level-id={}",
1088 ptl.profile_id(),
1089 ptl.tier_flag(),
1090 ptl.level_id()
1091 )
1092 }
1093 H266ProfileTierLevel(ptl) => {
1094 write!(
1095 f,
1096 "profile-id={};tier-flag={};level-id={}",
1097 ptl.profile_id(),
1098 ptl.tier_flag(),
1099 ptl.level_id()
1100 )
1101 }
1102 SpropMaxDonDiff(v) => write!(f, "sprop-max-don-diff={}", *v),
1103 Apt(v) => write!(f, "apt={v}"),
1104 Unknown => Ok(()),
1105 }
1106 }
1107}
1108 
1109impl PayloadParams {
1110 pub(crate) fn as_media_attrs(&self, attrs: &mut Vec<MediaAttribute>) {
1111 attrs.push(MediaAttribute::RtpMap {
1112 pt: self.pt,
1113 value: self.spec.into(),
1114 });
1115 
1116 if self.fb_transport_cc {
1117 attrs.push(MediaAttribute::RtcpFb {
1118 pt: self.pt,
1119 value: "transport-cc".into(),
1120 });
1121 }
1122 if self.fb_remb {
1123 attrs.push(MediaAttribute::RtcpFb {
1124 pt: self.pt,
1125 value: "goog-remb".into(),
1126 });
1127 }
1128 if self.fb_fir {
1129 attrs.push(MediaAttribute::RtcpFb {
1130 pt: self.pt,
1131 value: "ccm fir".into(),
1132 });
1133 }
1134 if self.fb_nack {
1135 attrs.push(MediaAttribute::RtcpFb {
1136 pt: self.pt,
1137 value: "nack".into(),
1138 });
1139 }
1140 if self.fb_pli {
1141 attrs.push(MediaAttribute::RtcpFb {
1142 pt: self.pt,
1143 value: "nack pli".into(),
1144 });
1145 }
1146 
1147 let fmtps = self.spec.format.to_format_param();
1148 if !fmtps.is_empty() {
1149 attrs.push(MediaAttribute::Fmtp {
1150 pt: self.pt,
1151 values: fmtps,
1152 });
1153 }
1154 
1155 if let Some(pt) = self.resend {
1156 attrs.push(MediaAttribute::RtpMap {
1157 pt,
1158 value: RtpMap {
1159 codec: Codec::Rtx,
1160 clock_rate: self.spec.rtp_clock_rate(),
1161 channels: None,
1162 },
1163 });
1164 attrs.push(MediaAttribute::Fmtp {
1165 pt,
1166 values: vec![FormatParam::Apt(self.pt)],
1167 });
1168 }
1169 }
1170}
1171 
1172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1173pub struct RtpMap {
1174 pub codec: Codec,
1175 pub clock_rate: Frequency,
1176 pub channels: Option<u8>,
1177}
1178 
1179impl From<RtpMap> for CodecSpec {
1180 fn from(v: RtpMap) -> Self {
1181 // G722 uses an 8000 Hz RTP clock rate in SDP (RFC 3551 ยง4.5.2), but str0m
1182 // treats it as a 16 kHz mono codec everywhere user facing. Normalize on the way in.
1183 // See https://en.wikipedia.org/wiki/RTP_payload_formats#cite_note-55
1184 let (clock_rate, channels) = if v.codec == Codec::G722 {
1185 let channels = if v.channels == Some(1) {
1186 None
1187 } else {
1188 v.channels
1189 };
1190 (Frequency::SIXTEEN_KHZ, channels)
1191 } else {
1192 (v.clock_rate, v.channels)
1193 };
1194 CodecSpec {
1195 codec: v.codec,
1196 clock_rate,
1197 channels,
1198 format: FormatParams::default(),
1199 }
1200 }
1201}
1202 
1203impl From<CodecSpec> for RtpMap {
1204 fn from(v: CodecSpec) -> Self {
1205 RtpMap {
1206 codec: v.codec,
1207 // G722 is signalled with an 8000 Hz clock rate in SDP (RFC 3551 ยง4.5.2),
1208 // even though it samples at 16 kHz. See
1209 // https://en.wikipedia.org/wiki/RTP_payload_formats#cite_note-55
1210 clock_rate: v.rtp_clock_rate(),
1211 channels: v.channels,
1212 }
1213 }
1214}
1215 
1216#[derive(Clone, Debug, PartialEq, Eq)]
1217pub struct Simulcast {
1218 pub send: SimulcastGroups,
1219 pub recv: SimulcastGroups,
1220 /// If this is created synthetically for a munged SDP.
1221 pub is_munged: bool,
1222}
1223 
1224impl Simulcast {
1225 pub fn invert(self) -> Self {
1226 Simulcast {
1227 send: self.recv,
1228 recv: self.send,
1229 is_munged: self.is_munged,
1230 }
1231 }
1232}
1233 
1234/// RID organization inside a=simulcast line.
1235///
1236/// `a=simulcast send 2;3,4` would result in
1237/// `SimulcastGroups(vec![SimulcastLayer("2"), SimulcastLayer("3")])`
1238///
1239/// The choice between 3 and 4 for the rid is currently ignored and the first option is always
1240/// selected.
1241#[derive(Clone, Debug, PartialEq, Eq)]
1242pub struct SimulcastGroups(pub Vec<SimulcastLayer>);
1243 
1244/// Represents a simulcast layer in a group
1245#[derive(Clone, Debug, PartialEq, Eq)]
1246pub struct SimulcastLayer {
1247 /// The layer's rid
1248 pub restriction_id: RestrictionId,
1249 /// Optional attributes per RFC 8851
1250 pub attributes: Option<Vec<(String, String)>>,
1251}
1252 
1253impl Deref for SimulcastGroups {
1254 type Target = [SimulcastLayer];
1255 
1256 fn deref(&self) -> &Self::Target {
1257 &self.0
1258 }
1259}
1260 
1261#[derive(Debug, Clone, PartialEq, Eq)]
1262pub struct Msid {
1263 pub stream_id: String,
1264 pub track_id: String,
1265}
1266 
1267impl Msid {
1268 pub fn random() -> Self {
1269 Msid {
1270 stream_id: Id::<30>::random().to_string(),
1271 track_id: Id::<30>::random().to_string(),
1272 }
1273 }
1274}
1275 
1276impl fmt::Display for SimulcastGroups {
1277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1278 for (idx, layer) in self.0.iter().enumerate() {
1279 if idx + 1 == self.0.len() {
1280 write!(f, "{}", layer.restriction_id.to_sdp())?;
1281 } else {
1282 write!(f, "{};", layer.restriction_id.to_sdp())?;
1283 }
1284 }
1285 Ok(())
1286 }
1287}
1288 
1289impl fmt::Display for Sdp {
1290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291 write!(f, "{}", self.session)?;
1292 for m in &self.media_lines {
1293 write!(f, "{m}")?;
1294 }
1295 Ok(())
1296 }
1297}
1298 
1299impl fmt::Display for Session {
1300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1301 write!(f, "v=0\r\n")?;
1302 write!(f, "o=str0m-{} {} 2 IN IP4 0.0.0.0\r\n", VERSION, self.id)?;
1303 write!(f, "s=-\r\n")?;
1304 if let Some(bw) = &self.bw {
1305 write!(f, "b={}:{}\r\n", bw.typ, bw.val)?;
1306 }
1307 write!(f, "t=0 0\r\n")?;
1308 for a in &self.attrs {
1309 write!(f, "{a}")?;
1310 }
1311 Ok(())
1312 }
1313}
1314 
1315impl fmt::Display for SessionAttribute {
1316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1317 use SessionAttribute::*;
1318 match self {
1319 Group { typ, mids } => {
1320 let mids: Vec<_> = mids.iter().map(|m| m.to_string()).collect();
1321 write!(f, "a=group:{} {}\r\n", typ, mids.join(" "))?;
1322 }
1323 AllowMixedExts => write!(f, "a=extmap-allow-mixed\r\n")?,
1324 MsidSemantic {
1325 semantic,
1326 stream_ids,
1327 } => {
1328 write!(
1329 f,
1330 "a=msid-semantic: {} {}\r\n",
1331 semantic,
1332 stream_ids.join(" ")
1333 )?;
1334 }
1335 IceLite => write!(f, "a=ice-lite\r\n")?,
1336 IceUfrag(v) => write!(f, "a=ice-ufrag:{v}\r\n")?,
1337 IcePwd(v) => write!(f, "a=ice-pwd:{v}\r\n")?,
1338 IceOptions(v) => write!(f, "a=ice-options:{v}\r\n")?,
1339 Fingerprint(v) => {
1340 write!(
1341 f,
1342 "a=fingerprint:{} {}\r\n",
1343 v.hash_func,
1344 FingerprintFmt(&v.bytes)
1345 )?;
1346 }
1347 Setup(v) => write!(f, "a=setup:{}\r\n", v.setup_line())?,
1348 Candidate(c) => write!(f, "a={}\r\n", c.to_sdp_string())?,
1349 EndOfCandidates => write!(f, "a=end-of-candidates\r\n")?,
1350 Unused(v) => write!(f, "a={v}\r\n")?,
1351 }
1352 Ok(())
1353 }
1354}
1355 
1356impl fmt::Display for MediaLine {
1357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1358 let port = if self.disabled { 0 } else { 9 };
1359 write!(f, "m={} {} {} ", self.typ, port, self.proto)?;
1360 let len = self.pts.len();
1361 if self.typ.is_channel() {
1362 write!(f, "webrtc-datachannel\r\n")?;
1363 } else {
1364 for (idx, m) in self.pts.iter().enumerate() {
1365 if idx + 1 < len {
1366 write!(f, "{m} ")?;
1367 } else {
1368 write!(f, "{m}")?;
1369 }
1370 }
1371 write!(f, "\r\n")?;
1372 }
1373 write!(f, "c=IN IP4 0.0.0.0\r\n")?;
1374 if let Some(bw) = &self.bw {
1375 write!(f, "b={}:{}\r\n", bw.typ, bw.val)?;
1376 }
1377 for a in &self.attrs {
1378 write!(f, "{a}")?;
1379 }
1380 Ok(())
1381 }
1382}
1383 
1384impl fmt::Display for MediaType {
1385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1386 match self {
1387 MediaType::Audio => write!(f, "audio"),
1388 MediaType::Video => write!(f, "video"),
1389 MediaType::Application => write!(f, "application"),
1390 MediaType::Unknown(v) => write!(f, "{v}"),
1391 }
1392 }
1393}
1394 
1395impl fmt::Display for Proto {
1396 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1397 write!(f, "{}", self.proto_line())
1398 }
1399}
1400 
1401impl fmt::Display for MediaAttribute {
1402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1403 use MediaAttribute::*;
1404 match self {
1405 Rtcp(v) => write!(f, "a=rtcp:{v}\r\n")?,
1406 IceUfrag(v) => write!(f, "a=ice-ufrag:{v}\r\n")?,
1407 IcePwd(v) => write!(f, "a=ice-pwd:{v}\r\n")?,
1408 IceOptions(v) => write!(f, "a=ice-options:{v}\r\n")?,
1409 Fingerprint(v) => {
1410 write!(
1411 f,
1412 "a=fingerprint:{} {}\r\n",
1413 v.hash_func,
1414 FingerprintFmt(&v.bytes)
1415 )?;
1416 }
1417 Setup(v) => write!(f, "a=setup:{}\r\n", v.setup_line())?,
1418 Mid(v) => write!(f, "a=mid:{v}\r\n")?,
1419 SctpPort(v) => write!(f, "a=sctp-port:{v}\r\n")?,
1420 MaxMessageSize(v) => write!(f, "a=max-message-size:{v}\r\n")?,
1421 SctpInit(v) => write!(f, "a=sctp-init:{v}\r\n")?,
1422 ExtMap { id, ext } => {
1423 if !ext.is_serialized() {
1424 return Ok(());
1425 }
1426 write!(f, "a=extmap:{}", id)?;
1427 // if let Some(d) = &e.direction {
1428 // write!(f, "/{d}")?;
1429 // }
1430 write!(f, " {}", ext.as_uri())?;
1431 // if let Some(e) = &e.ext {
1432 // write!(f, " {}", e)?;
1433 // }
1434 write!(f, "\r\n")?;
1435 }
1436 RecvOnly => write!(f, "a=recvonly\r\n")?,
1437 SendRecv => write!(f, "a=sendrecv\r\n")?,
1438 SendOnly => write!(f, "a=sendonly\r\n")?,
1439 Inactive => write!(f, "a=inactive\r\n")?,
1440 // a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1
1441 // a=msid:- 78dde68-7055-4e20-bb37-433803dd1ed1
1442 Msid(v) => write!(f, "a=msid:{} {}\r\n", v.stream_id, v.track_id)?,
1443 RtcpMux => write!(f, "a=rtcp-mux\r\n")?,
1444 RtcpMuxOnly => write!(f, "a=rtcp-mux-only\r\n")?,
1445 RtcpRsize => write!(f, "a=rtcp-rsize\r\n")?,
1446 Candidate(c) => write!(f, "a={}\r\n", c.to_sdp_string())?,
1447 EndOfCandidates => write!(f, "a=end-of-candidates\r\n")?,
1448 RtpMap { pt, value: c } => {
1449 write!(f, "a=rtpmap:{} {}/{}", pt, c.codec, c.clock_rate)?;
1450 if let Some(e) = c.channels {
1451 write!(f, "/{e}")?;
1452 }
1453 write!(f, "\r\n")?;
1454 }
1455 RtcpFb { pt, value } => write!(f, "a=rtcp-fb:{pt} {value}\r\n")?,
1456 Fmtp { pt, values } => {
1457 if values.is_empty() {
1458 // Skip empty fmtp lines - they're invalid SDP
1459 return Ok(());
1460 }
1461 write!(f, "a=fmtp:{pt} ")?;
1462 for (idx, v) in values.iter().enumerate() {
1463 if idx + 1 < values.len() {
1464 write!(f, "{v};")?;
1465 } else {
1466 write!(f, "{v}\r\n")?;
1467 }
1468 }
1469 }
1470 // a=rid:hi send pt=111,112;max-br=64000;max-height=360
1471 Rid {
1472 id,
1473 direction,
1474 pt,
1475 restriction,
1476 } => {
1477 write!(f, "a=rid:{} {}", id.0, direction)?;
1478 for (idx, p) in pt.iter().enumerate() {
1479 if idx == 0 {
1480 write!(f, " pt=")?;
1481 }
1482 if idx + 1 == pt.len() {
1483 write!(f, "{p}")?;
1484 } else {
1485 write!(f, "{p},")?;
1486 }
1487 }
1488 for (idx, (k, v)) in restriction.iter().enumerate() {
1489 if idx == 0 {
1490 if pt.is_empty() {
1491 write!(f, " ")?;
1492 } else {
1493 write!(f, ";")?;
1494 }
1495 }
1496 if idx + 1 == restriction.len() {
1497 write!(f, "{k}={v}")?;
1498 } else {
1499 write!(f, "{k}={v};")?;
1500 }
1501 }
1502 write!(f, "\r\n")?;
1503 }
1504 // a=simulcast:<send/recv> <alt A>;<alt B>,<or C> <send/recv> [same]
1505 Simulcast(x) => {
1506 let self::Simulcast {
1507 send,
1508 recv,
1509 is_munged,
1510 } = x;
1511 assert!(
1512 !(send.0.is_empty() && recv.0.is_empty()),
1513 "Empty a=simulcast"
1514 );
1515 if *is_munged {
1516 // don't write
1517 return Ok(());
1518 }
1519 write!(f, "a=simulcast:")?;
1520 if !send.0.is_empty() {
1521 write!(f, "send {send}")?;
1522 }
1523 if !recv.0.is_empty() {
1524 if !send.0.is_empty() {
1525 write!(f, " ")?;
1526 }
1527 write!(f, "recv {recv}")?;
1528 }
1529 write!(f, "\r\n")?;
1530 }
1531 SsrcGroup { semantics, ssrcs } => {
1532 write!(f, "a=ssrc-group:{semantics} ")?;
1533 for (idx, ssrc) in ssrcs.iter().enumerate() {
1534 if idx + 1 < ssrcs.len() {
1535 write!(f, "{} ", **ssrc)?;
1536 } else {
1537 write!(f, "{}\r\n", **ssrc)?;
1538 }
1539 }
1540 }
1541 Ssrc { ssrc, attr, value } => {
1542 write!(f, "a=ssrc:{} {}:{}\r\n", **ssrc, attr, value)?;
1543 }
1544 Unused(v) => write!(f, "a={v}\r\n")?,
1545 }
1546 Ok(())
1547 }
1548}
1549 
1550pub struct FingerprintFmt<'a>(pub &'a [u8]);
1551 
1552impl<'a> std::fmt::Display for FingerprintFmt<'a> {
1553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1554 let last = self.0.len() - 1;
1555 for (idx, b) in self.0.iter().enumerate() {
1556 if idx < last {
1557 write!(f, "{b:02X}:")?;
1558 } else {
1559 write!(f, "{b:02X}")?;
1560 }
1561 }
1562 Ok(())
1563 }
1564}
1565 
1566#[cfg(test)]
1567mod test {
1568 use crate::VERSION;
1569 use crate::packet::H265ProfileTierLevel;
1570 use crate::rtp_::{Extension, Frequency};
1571 
1572 use super::*;
1573 
1574 /// Tests for general format parameter serialization and parsing.
1575 /// These tests verify that format parameters can be correctly converted to/from strings.
1576 mod format_params {
1577 use super::*;
1578 
1579 #[test]
1580 fn fmtp_param_to_string() {
1581 let f = FormatParams {
1582 min_p_time: Some(10),
1583 stereo: Some(true),
1584 use_inband_fec: Some(true),
1585 ..Default::default()
1586 };
1587 assert_eq!(f.to_string(), "minptime=10;stereo=1;useinbandfec=1");
1588 }
1589 }
1590 
1591 /// Tests for SDP parsing, structure validation, and serialization.
1592 /// Covers error handling, SDP generation, BUNDLE groups, and media line configuration.
1593 mod sdp_parsing {
1594 use super::*;
1595 
1596 #[test]
1597 fn parse_error() {
1598 let input = "v=0\r\n\
1599 o=mozilla...THIS_IS_SDPARTA-99.0 7710052215259647220 2 IN IP4 0.0.0.0\r\n\
1600 s=-\r\n\
1601 t=0 0\r\n\
1602 a=fingerprint:sha-256 A6:64:23:37:94:7E:4B:40:F6:62:86:8C:DD:09:D5:08:\
16037E:D4:0E:68:58:93:45:EC:99:F2:91:F7:19:72:E7:BB\r\n\
1604 a=group:BUNDLE 0 hxI i1X mxk B3D kNI nbB xIZ bKm Hkn\r\n\
1605 a=ice-options:trickle\r\n\
1606 a=msid-semantic:WMS *\r\n\
1607 m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n\
1608 c=IN IP4 0.0.0.0\r\n\
1609 a=setup:actpass\r\n\
1610 a=mid:1\r\n\
1611 a=rtpmap:0 PCMU/8000\r\n\
1612 ";
1613 
1614 let sdp = Sdp::parse(input);
1615 
1616 match sdp {
1617 Err(SdpError::ParseError(out)) => {
1618 assert!(out.starts_with(&"Parse error at ".to_string()));
1619 assert!(out.contains(
1620 &"Expected exactly one of a=sendrecv, a=sendonly, a=recvonly, a=inactive for mid: 1"
1621 .to_string()
1622 ));
1623 }
1624 _ => panic!(),
1625 }
1626 }
1627 
1628 #[test]
1629 fn write_sdp() {
1630 let sdp = Sdp {
1631 session: Session {
1632 id: 5_058_682_828_002_148_772.into(),
1633 bw: None,
1634 attrs: vec![
1635 SessionAttribute::Group {
1636 typ: "BUNDLE".into(),
1637 mids: vec!["0".into()],
1638 },
1639 SessionAttribute::Unused(
1640 "msid-semantic: WMS 5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK".into(),
1641 ),
1642 ],
1643 },
1644 media_lines: vec![
1645 MediaLine {
1646 typ: MediaType::Audio,
1647 disabled: false,
1648 proto: Proto::Srtp,
1649 pts: vec![111, 103, 104, 9, 0, 8, 106, 105, 13, 110, 112, 113, 126]
1650 .into_iter()
1651 .map(Pt::from)
1652 .collect(),
1653 bw: None,
1654 attrs: vec![
1655 MediaAttribute::Rtcp("9 IN IP4 0.0.0.0".into()),
1656 MediaAttribute::IceUfrag("S5hk".into()),
1657 MediaAttribute::IcePwd("0zV/Yu3y8aDzbHgqWhnVQhqP".into()),
1658 MediaAttribute::IceOptions("trickle".into()),
1659 MediaAttribute::Fingerprint(Fingerprint {
1660 hash_func: "sha-256".into(),
1661 bytes: vec![
1662 140, 100, 237, 3, 118, 208, 61, 180, 136, 8, 145, 100, 8, 128,
1663 168, 198, 90, 191, 139, 78, 56, 39, 150, 202, 8, 73, 37, 115,
1664 70, 96, 32, 220,
1665 ],
1666 }),
1667 MediaAttribute::Setup(Setup::ActPass),
1668 MediaAttribute::Mid("0".into()),
1669 MediaAttribute::ExtMap{ id: 1, ext: Extension::AudioLevel },
1670 MediaAttribute::ExtMap{ id: 2, ext: Extension::AbsoluteSendTime },
1671 MediaAttribute::ExtMap{ id: 3, ext: Extension::TransportSequenceNumber },
1672 MediaAttribute::ExtMap{ id: 4, ext: Extension::RtpMid },
1673 MediaAttribute::ExtMap{ id: 5, ext: Extension::RtpStreamId },
1674 MediaAttribute::ExtMap{ id: 6, ext: Extension::RepairedRtpStreamId },
1675 MediaAttribute::SendRecv,
1676 MediaAttribute::Msid(Msid {
1677 stream_id: "5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK".into(),
1678 track_id: "f78dde68-7055-4e20-bb37-433803dd1ed1".into(),
1679 }),
1680 MediaAttribute::RtcpMux,
1681 MediaAttribute::RtpMap {
1682 pt: 111.into(),
1683 value: RtpMap {
1684 codec: "opus".into(),
1685 clock_rate: Frequency::FORTY_EIGHT_KHZ,
1686 channels: Some(2),
1687 },
1688 },
1689 MediaAttribute::RtcpFb { pt: 111.into(), value: "transport-cc".into() },
1690 MediaAttribute::Fmtp {
1691 pt: 111.into(),
1692 values: vec![FormatParam::MinPTime(10), FormatParam::UseInbandFec(true)],
1693 },
1694 MediaAttribute::Ssrc {
1695 ssrc: 3_948_621_874.into(),
1696 attr: "cname".into(),
1697 value: "xeXs3aE9AOBn00yJ".into(),
1698 },
1699 MediaAttribute::Ssrc {
1700 ssrc: 3_948_621_874.into(),
1701 attr: "msid".into(),
1702 value: "5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1"
1703 .into(),
1704 },
1705 MediaAttribute::Ssrc {
1706 ssrc: 3_948_621_874.into(),
1707 attr: "mslabel".into(),
1708 value: "5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK".into(),
1709 },
1710 MediaAttribute::Ssrc {
1711 ssrc: 3_948_621_874.into(),
1712 attr: "label".into(),
1713 value: "f78dde68-7055-4e20-bb37-433803dd1ed1".into(),
1714 },
1715 ],
1716 },
1717 MediaLine {
1718 typ: MediaType::Video,
1719 disabled: false,
1720 proto: Proto::Srtp,
1721 pts: vec![45.into(), 46.into()],
1722 bw: None,
1723 attrs: vec![
1724 MediaAttribute::Rtcp("9 IN IP4 0.0.0.0".into()),
1725 MediaAttribute::IceUfrag("S5hk".into()),
1726 MediaAttribute::IcePwd("0zV/Yu3y8aDzbHgqWhnVQhqP".into()),
1727 MediaAttribute::IceOptions("trickle".into()),
1728 MediaAttribute::Fingerprint(Fingerprint {
1729 hash_func: "sha-256".into(),
1730 bytes: vec![
1731 140, 100, 237, 3, 118, 208, 61, 180, 136, 8, 145, 100, 8, 128,
1732 168, 198, 90, 191, 139, 78, 56, 39, 150, 202, 8, 73, 37, 115,
1733 70, 96, 32, 220,
1734 ],
1735 }),
1736 MediaAttribute::Setup(Setup::ActPass),
1737 MediaAttribute::Mid("1".into()),
1738 MediaAttribute::ExtMap{ id: 14, ext: Extension::TransmissionTimeOffset },
1739 MediaAttribute::ExtMap{ id: 2, ext: Extension::AbsoluteSendTime },
1740 MediaAttribute::ExtMap{ id: 13, ext: Extension::VideoOrientation },
1741 MediaAttribute::ExtMap{ id: 3, ext: Extension::TransportSequenceNumber },
1742 MediaAttribute::ExtMap{ id: 5, ext: Extension::PlayoutDelay },
1743 MediaAttribute::ExtMap{ id: 6, ext: Extension::VideoContentType },
1744 MediaAttribute::ExtMap{ id: 7, ext: Extension::VideoTiming },
1745 MediaAttribute::ExtMap{ id: 8, ext: Extension::ColorSpace },
1746 MediaAttribute::ExtMap{ id: 4, ext: Extension::RtpMid },
1747 MediaAttribute::ExtMap{ id: 10, ext: Extension::RtpStreamId },
1748 MediaAttribute::ExtMap{ id: 11, ext: Extension::RepairedRtpStreamId },
1749 MediaAttribute::SendRecv,
1750 MediaAttribute::Msid(Msid {
1751 stream_id: "-".into(),
1752 track_id: "4018fd65-ac50-4861-89a4-1f2cc35bbb5e".into(),
1753 }),
1754 MediaAttribute::RtcpMux,
1755 MediaAttribute::RtcpRsize,
1756 MediaAttribute::RtpMap {
1757 pt: 45.into(),
1758 value: RtpMap {
1759 codec: Codec::Av1,
1760 clock_rate: Frequency::NINETY_KHZ,
1761 channels: None,
1762 },
1763 },
1764 MediaAttribute::RtcpFb { pt: 45.into(), value: "goog-remb".into() },
1765 MediaAttribute::RtcpFb { pt: 45.into(), value: "transport-cc".into() },
1766 MediaAttribute::RtcpFb { pt: 45.into(), value: "ccm fir".into() },
1767 MediaAttribute::RtcpFb { pt: 45.into(), value: "nack".into() },
1768 MediaAttribute::RtcpFb { pt: 45.into(), value: "nack pli".into() },
1769 MediaAttribute::Fmtp {
1770 pt: 45.into(),
1771 values: vec![
1772 FormatParam::LevelIdx(5),
1773 FormatParam::Profile(0),
1774 FormatParam::Tier(0),
1775 ],
1776 },
1777 MediaAttribute::RtpMap {
1778 pt: 46.into(),
1779 value: RtpMap {
1780 codec: Codec::Rtx,
1781 clock_rate: Frequency::NINETY_KHZ,
1782 channels: None,
1783 },
1784 },
1785 MediaAttribute::Fmtp { pt: 46.into(), values: vec![FormatParam::Apt(45.into())] }
1786 ],
1787 }
1788 ],
1789 };
1790 assert_eq!(
1791 &format!("{sdp}"),
1792 &format!(
1793 "v=0\r\n\
1794 o=str0m-{VERSION} 5058682828002148772 2 IN IP4 0.0.0.0\r\n\
1795 s=-\r\n\
1796 t=0 0\r\n\
1797 a=group:BUNDLE 0\r\n\
1798 a=msid-semantic: WMS 5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK\r\n\
1799 m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n\
1800 c=IN IP4 0.0.0.0\r\n\
1801 a=rtcp:9 IN IP4 0.0.0.0\r\n\
1802 a=ice-ufrag:S5hk\r\n\
1803 a=ice-pwd:0zV/Yu3y8aDzbHgqWhnVQhqP\r\n\
1804 a=ice-options:trickle\r\n\
1805 a=fingerprint:sha-256 8C:64:ED:03:76:D0:3D:B4:88:08:91:64:08:80:A8:C6:\
18065A:BF:8B:4E:38:27:96:CA:08:49:25:73:46:60:20:DC\r\n\
1807 a=setup:actpass\r\n\
1808 a=mid:0\r\n\
1809 a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
1810 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
1811 a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n\
1812 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
1813 a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
1814 a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
1815 a=sendrecv\r\n\
1816 a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
1817 a=rtcp-mux\r\n\
1818 a=rtpmap:111 opus/48000/2\r\n\
1819 a=rtcp-fb:111 transport-cc\r\n\
1820 a=fmtp:111 minptime=10;useinbandfec=1\r\n\
1821 a=ssrc:3948621874 cname:xeXs3aE9AOBn00yJ\r\n\
1822 a=ssrc:3948621874 msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK \
1823f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
1824 a=ssrc:3948621874 mslabel:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK\r\n\
1825 a=ssrc:3948621874 label:f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
1826 m=video 9 UDP/TLS/RTP/SAVPF 45 46\r\n\
1827 c=IN IP4 0.0.0.0\r\n\
1828 a=rtcp:9 IN IP4 0.0.0.0\r\n\
1829 a=ice-ufrag:S5hk\r\n\
1830 a=ice-pwd:0zV/Yu3y8aDzbHgqWhnVQhqP\r\n\
1831 a=ice-options:trickle\r\n\
1832 a=fingerprint:sha-256 8C:64:ED:03:76:D0:3D:B4:88:08:91:64:08:80:A8:C6:\
18335A:BF:8B:4E:38:27:96:CA:08:49:25:73:46:60:20:DC\r\n\
1834 a=setup:actpass\r\n\
1835 a=mid:1\r\n\
1836 a=extmap:14 urn:ietf:params:rtp-hdrext:toffset\r\n\
1837 a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
1838 a=extmap:13 urn:3gpp:video-orientation\r\n\
1839 a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n\
1840 a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n\
1841 a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\n\
1842 a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\n\
1843 a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\n\
1844 a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
1845 a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
1846 a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
1847 a=sendrecv\r\n\
1848 a=msid:- 4018fd65-ac50-4861-89a4-1f2cc35bbb5e\r\n\
1849 a=rtcp-mux\r\n\
1850 a=rtcp-rsize\r\n\
1851 a=rtpmap:45 AV1/90000\r\n\
1852 a=rtcp-fb:45 goog-remb\r\n\
1853 a=rtcp-fb:45 transport-cc\r\n\
1854 a=rtcp-fb:45 ccm fir\r\n\
1855 a=rtcp-fb:45 nack\r\n\
1856 a=rtcp-fb:45 nack pli\r\n\
1857 a=fmtp:45 level-idx=5;profile=0;tier=0\r\n\
1858 a=rtpmap:46 rtx/90000\r\n\
1859 a=fmtp:46 apt=45\r\n\
1860 "
1861 )
1862 );
1863 }
1864 
1865 #[test]
1866 fn bundle_mids_returns_correct_mids() {
1867 let input = "v=0\r\n\
1868 o=- 123456 2 IN IP4 127.0.0.1\r\n\
1869 s=-\r\n\
1870 t=0 0\r\n\
1871 a=group:BUNDLE 0 1 2\r\n\
1872 a=msid-semantic:WMS *\r\n\
1873 m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\
1874 c=IN IP4 0.0.0.0\r\n\
1875 a=mid:0\r\n\
1876 a=sendrecv\r\n\
1877 a=rtpmap:111 opus/48000/2\r\n\
1878 a=setup:actpass\r\n\
1879 a=ice-ufrag:test\r\n\
1880 a=ice-pwd:testpassword1234\r\n\
1881 m=video 0 UDP/TLS/RTP/SAVPF 96\r\n\
1882 c=IN IP4 0.0.0.0\r\n\
1883 a=mid:1\r\n\
1884 a=sendrecv\r\n\
1885 a=rtpmap:96 VP8/90000\r\n\
1886 a=setup:actpass\r\n\
1887 a=ice-ufrag:test\r\n\
1888 a=ice-pwd:testpassword1234\r\n\
1889 m=application 0 UDP/DTLS/SCTP webrtc-datachannel\r\n\
1890 c=IN IP4 0.0.0.0\r\n\
1891 a=mid:2\r\n\
1892 a=setup:actpass\r\n\
1893 a=ice-ufrag:test\r\n\
1894 a=ice-pwd:testpassword1234\r\n\
1895 a=sctp-port:5000\r\n\
1896 ";
1897 
1898 let sdp = Sdp::parse(input).expect("should parse");
1899 
1900 // bundle_mids should return all MIDs from the BUNDLE group
1901 let mids = sdp.bundle_mids().expect("should have BUNDLE group");
1902 assert_eq!(mids.len(), 3);
1903 assert_eq!(mids[0], "0".into());
1904 assert_eq!(mids[1], "1".into());
1905 assert_eq!(mids[2], "2".into());
1906 
1907 // The m-lines with port=0 should have disabled=true at parsing level
1908 // (interpretation of whether this means "rejected" happens later)
1909 assert!(!sdp.media_lines[0].disabled, "audio has port=9");
1910 assert!(sdp.media_lines[1].disabled, "video has port=0");
1911 assert!(sdp.media_lines[2].disabled, "application has port=0");
1912 }
1913 }
1914 
1915 /// Opus codec-specific tests.
1916 /// These tests are mainly verify SDP format params.
1917 mod opus_codec {
1918 use super::*;
1919 
1920 #[test]
1921 fn opus_fmtp_round_trip() {
1922 let f = FormatParams {
1923 use_dtx: Some(true),
1924 stereo: Some(true),
1925 sprop_stereo: Some(true),
1926 ..Default::default()
1927 };
1928 
1929 let fmtp_str = f.to_string();
1930 assert_eq!(fmtp_str, "stereo=1;sprop-stereo=1;usedtx=1");
1931 
1932 let parsed = FormatParams::parse_line(&fmtp_str);
1933 assert_eq!(parsed, f);
1934 }
1935 
1936 #[test]
1937 fn opus_default_to_mono() {
1938 let f = FormatParams {
1939 ..Default::default()
1940 };
1941 let fmtp_str = f.to_string();
1942 assert_eq!(fmtp_str, "");
1943 
1944 let parsed = FormatParams::parse_line(&fmtp_str);
1945 assert!(parsed.stereo.is_none());
1946 assert_eq!(parsed.stereo.unwrap_or(false), false);
1947 assert_eq!(parsed.sprop_stereo.unwrap_or(false), false);
1948 }
1949 }
1950 
1951 /// Core H.265 (HEVC) codec-specific tests.
1952 /// Tests basic H.265 parameter handling including profile-tier-level parsing,
1953 /// serialization, and format parameter combinations.
1954 mod h265_codec {
1955 use super::*;
1956 
1957 /// Test that H.265 format parameters can be serialized to a string and parsed back
1958 /// without loss of information. Verifies the round-trip conversion works correctly.
1959 #[test]
1960 fn h265_fmtp_round_trip() {
1961 // Test that H.265 fmtp params round-trip correctly
1962 let ptl = H265ProfileTierLevel::new(1, 0, 93).unwrap(); // Main, Main tier, Level 3.1
1963 
1964 let f = FormatParams {
1965 h265_profile_tier_level: Some(ptl),
1966 ..Default::default()
1967 };
1968 
1969 // Serialize to string
1970 let fmtp_str = f.to_string();
1971 assert_eq!(fmtp_str, "profile-id=1;tier-flag=0;level-id=93");
1972 
1973 // Parse back
1974 let parsed = FormatParams::parse_line(&fmtp_str);
1975 assert_eq!(parsed.h265_profile_tier_level, Some(ptl));
1976 }
1977 
1978 /// Test different combinations of H.265 profiles, tiers, and levels to ensure
1979 /// they are all serialized correctly. Covers Main, Main 10, different tiers, and levels.
1980 #[test]
1981 fn h265_different_profiles() {
1982 // Test Main profile (1)
1983 let main = H265ProfileTierLevel::new(1, 0, 93).unwrap();
1984 let f = FormatParams {
1985 h265_profile_tier_level: Some(main),
1986 ..Default::default()
1987 };
1988 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=93");
1989 
1990 // Test Main 10 profile (2)
1991 let main10 = H265ProfileTierLevel::new(2, 0, 93).unwrap();
1992 let f = FormatParams {
1993 h265_profile_tier_level: Some(main10),
1994 ..Default::default()
1995 };
1996 assert_eq!(f.to_string(), "profile-id=2;tier-flag=0;level-id=93");
1997 
1998 // Test different tier (High tier)
1999 let high_tier = H265ProfileTierLevel::new(1, 1, 93).unwrap();
2000 let f = FormatParams {
2001 h265_profile_tier_level: Some(high_tier),
2002 ..Default::default()
2003 };
2004 assert_eq!(f.to_string(), "profile-id=1;tier-flag=1;level-id=93");
2005 
2006 // Test different level (Level 5.1 = 153)
2007 let level_5_1 = H265ProfileTierLevel::new(1, 0, 153).unwrap();
2008 let f = FormatParams {
2009 h265_profile_tier_level: Some(level_5_1),
2010 ..Default::default()
2011 };
2012 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=153");
2013 }
2014 
2015 /// Test parsing H.265 parameters from key-value pairs as they would appear in SDP.
2016 /// Verifies that the three separate params (profile-id, tier-flag, level-id) are
2017 /// correctly combined into a single H265ProfileTierLevel composite.
2018 #[test]
2019 fn h265_parse_from_pairs() {
2020 // Test parsing from key-value pairs (as would come from SDP)
2021 let pairs = vec![
2022 ("profile-id".to_string(), "1".to_string()),
2023 ("tier-flag".to_string(), "0".to_string()),
2024 ("level-id".to_string(), "93".to_string()),
2025 ];
2026 
2027 let params = FormatParam::parse_pairs(pairs);
2028 
2029 // Should have exactly one H265ProfileTierLevel param
2030 let h265_params: Vec<_> = params
2031 .iter()
2032 .filter(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2033 .collect();
2034 assert_eq!(h265_params.len(), 1);
2035 
2036 if let FormatParam::H265ProfileTierLevel(ptl) = h265_params[0] {
2037 assert_eq!(ptl.profile_id(), 1);
2038 assert_eq!(ptl.tier_flag(), 0);
2039 assert_eq!(ptl.level_id(), 93);
2040 } else {
2041 panic!("Expected H265ProfileTierLevel");
2042 }
2043 }
2044 
2045 /// Test that H.265 profile-tier-level params can coexist with other H.265-specific
2046 /// parameters like sprop-max-don-diff without interfering with each other.
2047 #[test]
2048 fn h265_mixed_with_other_params() {
2049 // Test H.265 params mixed with other format params
2050 let pairs = vec![
2051 ("profile-id".to_string(), "1".to_string()),
2052 ("tier-flag".to_string(), "0".to_string()),
2053 ("level-id".to_string(), "93".to_string()),
2054 ("sprop-max-don-diff".to_string(), "0".to_string()),
2055 ];
2056 
2057 let params = FormatParam::parse_pairs(pairs);
2058 
2059 // Should have H265ProfileTierLevel and SpropMaxDonDiff
2060 assert!(
2061 params
2062 .iter()
2063 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2064 );
2065 assert!(
2066 params
2067 .iter()
2068 .any(|p| matches!(p, FormatParam::SpropMaxDonDiff(0)))
2069 );
2070 }
2071 
2072 /// Test sprop-max-don-diff parsing, display, and round-trip for various values.
2073 #[test]
2074 fn sprop_max_don_diff_parse_and_display() {
2075 // Parse standalone sprop-max-don-diff
2076 let param = FormatParam::parse("sprop-max-don-diff", "32");
2077 assert!(matches!(param, FormatParam::SpropMaxDonDiff(32)));
2078 
2079 // Display format
2080 assert_eq!(param.to_string(), "sprop-max-don-diff=32");
2081 
2082 // Parse value 0 (DONL disabled)
2083 let param0 = FormatParam::parse("sprop-max-don-diff", "0");
2084 assert!(matches!(param0, FormatParam::SpropMaxDonDiff(0)));
2085 
2086 // Parse max valid value (RFC 7798 ยง7.1: 0..32767)
2087 let param_max = FormatParam::parse("sprop-max-don-diff", "32767");
2088 assert!(matches!(param_max, FormatParam::SpropMaxDonDiff(32767)));
2089 
2090 // Out-of-range value (32768) rejected per RFC 7798 ยง7.1
2091 let param_over = FormatParam::parse("sprop-max-don-diff", "32768");
2092 assert!(matches!(param_over, FormatParam::Unknown));
2093 
2094 // Invalid value falls back to Unknown
2095 let param_bad = FormatParam::parse("sprop-max-don-diff", "notanumber");
2096 assert!(matches!(param_bad, FormatParam::Unknown));
2097 }
2098 
2099 /// Test that sprop-max-don-diff > 0 round-trips through H.265 fmtp with PTL params.
2100 #[test]
2101 fn h265_sprop_max_don_diff_nonzero_with_ptl() {
2102 let pairs = vec![
2103 ("profile-id".to_string(), "1".to_string()),
2104 ("tier-flag".to_string(), "0".to_string()),
2105 ("level-id".to_string(), "93".to_string()),
2106 ("sprop-max-don-diff".to_string(), "32".to_string()),
2107 ];
2108 
2109 let params = FormatParam::parse_pairs(pairs);
2110 
2111 // Should have both H265ProfileTierLevel and SpropMaxDonDiff(32)
2112 assert!(
2113 params
2114 .iter()
2115 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2116 );
2117 assert!(
2118 params
2119 .iter()
2120 .any(|p| matches!(p, FormatParam::SpropMaxDonDiff(32)))
2121 );
2122 
2123 // Verify PTL values are correct
2124 if let Some(FormatParam::H265ProfileTierLevel(ptl)) = params
2125 .iter()
2126 .find(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2127 {
2128 assert_eq!(ptl.profile_id(), 1);
2129 assert_eq!(ptl.tier_flag(), 0);
2130 assert_eq!(ptl.level_id(), 93);
2131 }
2132 }
2133 }
2134 
2135 /// Tests ensuring H.265 parameters are not confused with other codecs.
2136 /// Verifies that H.265's three-parameter format (profile-id, tier-flag, level-id)
2137 /// is correctly distinguished from H.264's profile-level-id, VP9's profile-id,
2138 /// and AV1's profile/tier/level-idx parameters.
2139 mod no_confusion {
2140 use super::*;
2141 
2142 /// Test that H.264's profile-level-id parameter (single hex value) is not confused
2143 /// with H.265's three separate parameters. H.264 uses a different format entirely.
2144 #[test]
2145 fn h265_no_confusion_with_h264() {
2146 // H.264 uses profile-level-id (single param)
2147 let h264_pairs = vec![
2148 ("profile-level-id".to_string(), "42e01f".to_string()),
2149 ("packetization-mode".to_string(), "1".to_string()),
2150 ];
2151 
2152 let h264_params = FormatParam::parse_pairs(h264_pairs);
2153 
2154 // Should NOT have H265ProfileTierLevel
2155 assert!(
2156 !h264_params
2157 .iter()
2158 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2159 );
2160 
2161 // Should have ProfileLevelId (H.264)
2162 assert!(
2163 h264_params
2164 .iter()
2165 .any(|p| matches!(p, FormatParam::ProfileLevelId(_)))
2166 );
2167 assert!(
2168 h264_params
2169 .iter()
2170 .any(|p| matches!(p, FormatParam::PacketizationMode(1)))
2171 );
2172 }
2173 
2174 /// Test that VP9's profile-id parameter (standalone) is not confused with H.265's
2175 /// profile-id which must be combined with tier-flag and level-id.
2176 #[test]
2177 fn h265_no_confusion_with_vp9() {
2178 // VP9 uses profile-id (single param, not combined)
2179 let vp9_pairs = vec![("profile-id".to_string(), "0".to_string())];
2180 
2181 let vp9_params = FormatParam::parse_pairs(vp9_pairs);
2182 
2183 // Should NOT have H265ProfileTierLevel (missing tier-flag and level-id)
2184 assert!(
2185 !vp9_params
2186 .iter()
2187 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2188 );
2189 
2190 // Should have ProfileId (VP9)
2191 assert!(
2192 vp9_params
2193 .iter()
2194 .any(|p| matches!(p, FormatParam::ProfileId(0)))
2195 );
2196 }
2197 
2198 /// Test that AV1's parameters (profile, tier, level-idx) use different naming than
2199 /// H.265 (profile-id, tier-flag, level-id) and are not confused during parsing.
2200 #[test]
2201 fn h265_no_confusion_with_av1() {
2202 // AV1 uses profile, level-idx, tier (but should not be confused with H.265)
2203 let av1_pairs = vec![
2204 ("profile".to_string(), "0".to_string()),
2205 ("level-idx".to_string(), "5".to_string()),
2206 ("tier".to_string(), "0".to_string()),
2207 ];
2208 
2209 let av1_params = FormatParam::parse_pairs(av1_pairs);
2210 
2211 // Should NOT have H265ProfileTierLevel
2212 // (AV1 uses different param names: profile vs profile-id,
2213 // tier vs tier-flag, level-idx vs level-id)
2214 assert!(
2215 !av1_params
2216 .iter()
2217 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2218 );
2219 
2220 // Should have AV1-specific params
2221 assert!(
2222 av1_params
2223 .iter()
2224 .any(|p| matches!(p, FormatParam::Profile(0)))
2225 );
2226 assert!(
2227 av1_params
2228 .iter()
2229 .any(|p| matches!(p, FormatParam::LevelIdx(5)))
2230 );
2231 assert!(av1_params.iter().any(|p| matches!(p, FormatParam::Tier(0))));
2232 }
2233 }
2234 
2235 /// Additional H.265 parsing and validation tests.
2236 /// Covers incomplete parameter handling and complete SDP integration scenarios.
2237 mod h265_additional {
2238 use super::*;
2239 
2240 /// Test that incomplete H.265 parameter sets do not create a composite ProfileTierLevel.
2241 /// All three parameters (profile-id, tier-flag, level-id) must be present.
2242 #[test]
2243 fn h265_partial_params_no_composite() {
2244 // If only some H.265 params are present, should not create composite
2245 let partial_pairs = vec![
2246 ("profile-id".to_string(), "1".to_string()),
2247 ("tier-flag".to_string(), "0".to_string()),
2248 // Missing level-id
2249 ];
2250 
2251 let params = FormatParam::parse_pairs(partial_pairs);
2252 
2253 // Should NOT have H265ProfileTierLevel (incomplete)
2254 assert!(
2255 !params
2256 .iter()
2257 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2258 );
2259 }
2260 
2261 /// Test H.265 parameters in a complete SDP offer/answer scenario.
2262 /// Verifies that H.265 rtpmap and fmtp lines are correctly parsed along with RTX.
2263 #[test]
2264 fn h265_in_complete_sdp() {
2265 let input = "v=0\r\n\
2266 o=- 123456 2 IN IP4 127.0.0.1\r\n\
2267 s=-\r\n\
2268 t=0 0\r\n\
2269 a=group:BUNDLE 0\r\n\
2270 m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n\
2271 c=IN IP4 0.0.0.0\r\n\
2272 a=mid:0\r\n\
2273 a=sendrecv\r\n\
2274 a=rtpmap:96 H265/90000\r\n\
2275 a=fmtp:96 profile-id=1;tier-flag=0;level-id=93\r\n\
2276 a=rtpmap:97 rtx/90000\r\n\
2277 a=fmtp:97 apt=96\r\n\
2278 a=setup:actpass\r\n\
2279 a=ice-ufrag:test\r\n\
2280 a=ice-pwd:testpassword\r\n\
2281 ";
2282 
2283 let sdp = Sdp::parse(input).expect("should parse");
2284 
2285 // Get RTP params
2286 let params = sdp.media_lines[0].rtp_params();
2287 
2288 // Find H.265 payload
2289 let h265_payload = params
2290 .iter()
2291 .find(|p| p.spec.codec == Codec::H265)
2292 .expect("should have H.265");
2293 
2294 assert_eq!(h265_payload.pt, 96.into());
2295 assert_eq!(h265_payload.spec.clock_rate, Frequency::NINETY_KHZ);
2296 
2297 // Check H.265 params
2298 let ptl = h265_payload
2299 .spec
2300 .format
2301 .h265_profile_tier_level
2302 .expect("should have H.265 PTL");
2303 assert_eq!(ptl.profile_id(), 1);
2304 assert_eq!(ptl.tier_flag(), 0);
2305 assert_eq!(ptl.level_id(), 93);
2306 
2307 // Check RTX
2308 assert_eq!(h265_payload.resend, Some(97.into()));
2309 }
2310 }
2311 
2312 /// Multi-codec integration tests.
2313 /// Verifies that multiple video codecs (H.264, H.265, VP8, VP9) can coexist
2314 /// in the same SDP without parameter conflicts or interference.
2315 mod multi_codec {
2316 use super::*;
2317 
2318 /// Test that H.264, H.265, and other video codecs can coexist in the same SDP
2319 /// without their format parameters interfering with each other.
2320 #[test]
2321 fn h265_and_h264_coexist() {
2322 let input = "v=0\r\n\
2323 o=- 123456 2 IN IP4 127.0.0.1\r\n\
2324 s=-\r\n\
2325 t=0 0\r\n\
2326 a=group:BUNDLE 0\r\n\
2327 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98\r\n\
2328 c=IN IP4 0.0.0.0\r\n\
2329 a=mid:0\r\n\
2330 a=sendrecv\r\n\
2331 a=rtpmap:96 H264/90000\r\n\
2332 a=fmtp:96 profile-level-id=42e01f;packetization-mode=1\r\n\
2333 a=rtpmap:97 H265/90000\r\n\
2334 a=fmtp:97 profile-id=1;tier-flag=0;level-id=93\r\n\
2335 a=rtpmap:98 VP8/90000\r\n\
2336 a=setup:actpass\r\n\
2337 a=ice-ufrag:test\r\n\
2338 a=ice-pwd:testpassword\r\n\
2339 ";
2340 
2341 let sdp = Sdp::parse(input).expect("should parse");
2342 let params = sdp.media_lines[0].rtp_params();
2343 
2344 // Check H.264
2345 let h264 = params
2346 .iter()
2347 .find(|p| p.spec.codec == Codec::H264)
2348 .expect("should have H.264");
2349 assert_eq!(h264.pt, 96.into());
2350 assert_eq!(h264.spec.format.profile_level_id, Some(0x42e01f));
2351 assert_eq!(h264.spec.format.packetization_mode, Some(1));
2352 assert!(h264.spec.format.h265_profile_tier_level.is_none());
2353 
2354 // Check H.265
2355 let h265 = params
2356 .iter()
2357 .find(|p| p.spec.codec == Codec::H265)
2358 .expect("should have H.265");
2359 assert_eq!(h265.pt, 97.into());
2360 let ptl = h265
2361 .spec
2362 .format
2363 .h265_profile_tier_level
2364 .expect("should have H.265 PTL");
2365 assert_eq!(ptl.profile_id(), 1);
2366 assert!(h265.spec.format.profile_level_id.is_none());
2367 
2368 // Check VP8
2369 let vp8 = params
2370 .iter()
2371 .find(|p| p.spec.codec == Codec::Vp8)
2372 .expect("should have VP8");
2373 assert_eq!(vp8.pt, 98.into());
2374 assert!(vp8.spec.format.h265_profile_tier_level.is_none());
2375 assert!(vp8.spec.format.profile_level_id.is_none());
2376 }
2377 }
2378 
2379 /// H.265 serialization and edge case tests.
2380 /// Tests media attribute generation, empty parameter handling, extreme values,
2381 /// parameter ordering independence, DONL parameters, and invalid value handling.
2382 mod h265_serialization {
2383 use super::*;
2384 
2385 /// Test that H.265 parameters are correctly serialized as media attributes,
2386 /// including rtpmap, fmtp with ProfileTierLevel, rtcp-fb, and RTX payload.
2387 #[test]
2388 fn h265_serialization_in_media_attrs() {
2389 let ptl = H265ProfileTierLevel::new(2, 1, 153).unwrap(); // Main 10, High tier, Level 5.1
2390 
2391 let mut payload = PayloadParams::new(
2392 100.into(),
2393 Some(101.into()),
2394 CodecSpec {
2395 codec: Codec::H265,
2396 clock_rate: Frequency::NINETY_KHZ,
2397 channels: None,
2398 format: FormatParams {
2399 h265_profile_tier_level: Some(ptl),
2400 ..Default::default()
2401 },
2402 },
2403 );
2404 payload.fb_transport_cc = true;
2405 payload.fb_nack = true;
2406 payload.fb_pli = true;
2407 
2408 let mut attrs = vec![];
2409 payload.as_media_attrs(&mut attrs);
2410 
2411 // Should have rtpmap for H.265
2412 assert!(attrs.iter().any(|a| matches!(a,
2413 MediaAttribute::RtpMap { pt, value }
2414 if *pt == 100.into() && value.codec == Codec::H265
2415 )));
2416 
2417 // Should have fmtp with H.265 params
2418 let fmtp = attrs.iter().find(|a| {
2419 matches!(a,
2420 MediaAttribute::Fmtp { pt, .. } if *pt == 100.into()
2421 )
2422 });
2423 assert!(fmtp.is_some());
2424 
2425 if let Some(MediaAttribute::Fmtp { values, .. }) = fmtp {
2426 assert!(
2427 values
2428 .iter()
2429 .any(|v| matches!(v, FormatParam::H265ProfileTierLevel(p)
2430 if p.profile_id() == 2 && p.tier_flag() == 1 && p.level_id() == 153
2431 ))
2432 );
2433 }
2434 
2435 // Should have rtx mapping
2436 assert!(attrs.iter().any(|a| matches!(a,
2437 MediaAttribute::RtpMap { pt, value }
2438 if *pt == 101.into() && value.codec == Codec::Rtx
2439 )));
2440 }
2441 
2442 /// Test that empty format parameters do not generate an a=fmtp line in SDP,
2443 /// avoiding invalid SDP syntax with empty fmtp attributes.
2444 #[test]
2445 fn h265_empty_fmtp_not_serialized() {
2446 // Empty fmtp should not be written
2447 let payload = PayloadParams::new(
2448 100.into(),
2449 None,
2450 CodecSpec {
2451 codec: Codec::H265,
2452 clock_rate: Frequency::NINETY_KHZ,
2453 channels: None,
2454 format: FormatParams::default(),
2455 },
2456 );
2457 
2458 let mut attrs = vec![];
2459 payload.as_media_attrs(&mut attrs);
2460 
2461 // Should NOT have fmtp line (it would be empty)
2462 assert!(
2463 !attrs
2464 .iter()
2465 .any(|a| matches!(a, MediaAttribute::Fmtp { .. }))
2466 );
2467 }
2468 
2469 /// Test H.265 with extreme profile/tier/level values at boundaries.
2470 /// Verifies parsing and serialization of edge case values.
2471 #[test]
2472 fn h265_extreme_values() {
2473 // Test minimum level (Level 1.0 = 30)
2474 let min_level = H265ProfileTierLevel::new(1, 0, 30).unwrap();
2475 let f = FormatParams {
2476 h265_profile_tier_level: Some(min_level),
2477 ..Default::default()
2478 };
2479 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=30");
2480 
2481 // Test maximum level (Level 6.2 = 186)
2482 let max_level = H265ProfileTierLevel::new(1, 0, 186).unwrap();
2483 let f = FormatParams {
2484 h265_profile_tier_level: Some(max_level),
2485 ..Default::default()
2486 };
2487 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=186");
2488 
2489 // Test Main10 with High tier
2490 let main10_high = H265ProfileTierLevel::new(2, 1, 93).unwrap();
2491 let f = FormatParams {
2492 h265_profile_tier_level: Some(main10_high),
2493 ..Default::default()
2494 };
2495 assert_eq!(f.to_string(), "profile-id=2;tier-flag=1;level-id=93");
2496 }
2497 
2498 /// Test H.265 parameter ordering doesn't affect parsing.
2499 /// SDP parameters can appear in any order.
2500 #[test]
2501 fn h265_parameter_order_independence() {
2502 // Normal order
2503 let normal_order = vec![
2504 ("profile-id".to_string(), "1".to_string()),
2505 ("tier-flag".to_string(), "0".to_string()),
2506 ("level-id".to_string(), "93".to_string()),
2507 ];
2508 
2509 // Reversed order
2510 let reversed_order = vec![
2511 ("level-id".to_string(), "93".to_string()),
2512 ("tier-flag".to_string(), "0".to_string()),
2513 ("profile-id".to_string(), "1".to_string()),
2514 ];
2515 
2516 // Mixed order
2517 let mixed_order = vec![
2518 ("tier-flag".to_string(), "0".to_string()),
2519 ("profile-id".to_string(), "1".to_string()),
2520 ("level-id".to_string(), "93".to_string()),
2521 ];
2522 
2523 let normal_params = FormatParam::parse_pairs(normal_order);
2524 let reversed_params = FormatParam::parse_pairs(reversed_order);
2525 let mixed_params = FormatParam::parse_pairs(mixed_order);
2526 
2527 // All should create H265ProfileTierLevel
2528 assert!(
2529 normal_params
2530 .iter()
2531 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2532 );
2533 assert!(
2534 reversed_params
2535 .iter()
2536 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2537 );
2538 assert!(
2539 mixed_params
2540 .iter()
2541 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2542 );
2543 
2544 // All should have the same values
2545 for params in [&normal_params, &reversed_params, &mixed_params] {
2546 if let Some(FormatParam::H265ProfileTierLevel(ptl)) = params
2547 .iter()
2548 .find(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2549 {
2550 assert_eq!(ptl.profile_id(), 1);
2551 assert_eq!(ptl.tier_flag(), 0);
2552 assert_eq!(ptl.level_id(), 93);
2553 }
2554 }
2555 }
2556 
2557 #[test]
2558 fn h265_invalid_parameter_values() {
2559 // Invalid profile-id
2560 let invalid_profile = vec![
2561 ("profile-id".to_string(), "999".to_string()),
2562 ("tier-flag".to_string(), "0".to_string()),
2563 ("level-id".to_string(), "93".to_string()),
2564 ];
2565 let params = FormatParam::parse_pairs(invalid_profile);
2566 assert!(
2567 !params
2568 .iter()
2569 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2570 );
2571 
2572 // Invalid tier-flag
2573 let invalid_tier = vec![
2574 ("profile-id".to_string(), "1".to_string()),
2575 ("tier-flag".to_string(), "5".to_string()),
2576 ("level-id".to_string(), "93".to_string()),
2577 ];
2578 let params = FormatParam::parse_pairs(invalid_tier);
2579 assert!(
2580 !params
2581 .iter()
2582 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2583 );
2584 
2585 // Invalid level-id
2586 let invalid_level = vec![
2587 ("profile-id".to_string(), "1".to_string()),
2588 ("tier-flag".to_string(), "0".to_string()),
2589 ("level-id".to_string(), "999".to_string()),
2590 ];
2591 let params = FormatParam::parse_pairs(invalid_level);
2592 assert!(
2593 !params
2594 .iter()
2595 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2596 );
2597 }
2598 }
2599 
2600 /// H.265 integration with RTCP feedback mechanisms.
2601 /// Tests H.265 behavior with multiple codecs and various RTCP feedback types
2602 /// including NACK, PLI, FIR, and transport-cc.
2603 mod h265_integration {
2604 use super::*;
2605 
2606 /// Test H.265 in SDP with multiple video codecs and feedback mechanisms.
2607 /// Verifies proper parsing of complex multi-codec scenarios.
2608 #[test]
2609 fn h265_multi_codec_with_feedback() {
2610 let input = "v=0\r\n\
2611 o=- 123456 2 IN IP4 127.0.0.1\r\n\
2612 s=-\r\n\
2613 t=0 0\r\n\
2614 a=group:BUNDLE 0\r\n\
2615 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99\r\n\
2616 c=IN IP4 0.0.0.0\r\n\
2617 a=mid:0\r\n\
2618 a=sendrecv\r\n\
2619 a=rtpmap:96 H265/90000\r\n\
2620 a=fmtp:96 profile-id=1;tier-flag=0;level-id=93\r\n\
2621 a=rtcp-fb:96 nack\r\n\
2622 a=rtcp-fb:96 nack pli\r\n\
2623 a=rtcp-fb:96 ccm fir\r\n\
2624 a=rtcp-fb:96 transport-cc\r\n\
2625 a=rtpmap:97 rtx/90000\r\n\
2626 a=fmtp:97 apt=96\r\n\
2627 a=rtpmap:98 VP9/90000\r\n\
2628 a=fmtp:98 profile-id=0\r\n\
2629 a=rtpmap:99 rtx/90000\r\n\
2630 a=fmtp:99 apt=98\r\n\
2631 a=setup:actpass\r\n\
2632 a=ice-ufrag:test\r\n\
2633 a=ice-pwd:testpassword\r\n\
2634 ";
2635 
2636 let sdp = Sdp::parse(input).expect("should parse");
2637 let params = sdp.media_lines[0].rtp_params();
2638 
2639 // Check H.265 with all feedback mechanisms
2640 let h265 = params
2641 .iter()
2642 .find(|p| p.spec.codec == Codec::H265)
2643 .expect("should have H.265");
2644 assert_eq!(h265.pt, 96.into());
2645 assert!(h265.fb_nack);
2646 assert!(h265.fb_pli);
2647 assert!(h265.fb_fir);
2648 assert!(h265.fb_transport_cc);
2649 assert_eq!(h265.resend, Some(97.into()));
2650 
2651 // Verify H.265 PTL is correct
2652 let ptl = h265
2653 .spec
2654 .format
2655 .h265_profile_tier_level
2656 .expect("should have PTL");
2657 assert_eq!(ptl.profile_id(), 1);
2658 
2659 // Check VP9 is separate and correct
2660 let vp9 = params
2661 .iter()
2662 .find(|p| p.spec.codec == Codec::Vp9)
2663 .expect("should have VP9");
2664 assert_eq!(vp9.pt, 98.into());
2665 assert_eq!(vp9.spec.format.profile_id, Some(0));
2666 assert!(vp9.spec.format.h265_profile_tier_level.is_none());
2667 }
2668 }
2669 
2670 /// Advanced H.265 functionality tests.
2671 /// Covers SDP round-trip serialization, incomplete parameter permutations,
2672 /// browser compatibility (Chrome profile-only mode), and validation of all
2673 /// standard H.265 level values (1.0 through 6.2).
2674 mod h265_advanced {
2675 use super::*;
2676 
2677 /// Test H.265 SDP serialization maintains parameter integrity.
2678 /// Ensures that serialized SDP can be parsed back with identical values.
2679 #[test]
2680 fn h265_sdp_serialization_round_trip() {
2681 let ptl = H265ProfileTierLevel::new(2, 0, 153).unwrap();
2682 
2683 let payload = PayloadParams::new(
2684 100.into(),
2685 Some(101.into()),
2686 CodecSpec {
2687 codec: Codec::H265,
2688 clock_rate: Frequency::NINETY_KHZ,
2689 channels: None,
2690 format: FormatParams {
2691 h265_profile_tier_level: Some(ptl),
2692 ..Default::default()
2693 },
2694 },
2695 );
2696 
2697 let mut attrs = vec![];
2698 payload.as_media_attrs(&mut attrs);
2699 
2700 // Find the fmtp line
2701 let fmtp = attrs
2702 .iter()
2703 .find_map(|a| match a {
2704 MediaAttribute::Fmtp { pt, values } if *pt == 100.into() => Some(values),
2705 _ => None,
2706 })
2707 .expect("should have fmtp");
2708 
2709 // Should contain H265ProfileTierLevel
2710 assert!(
2711 fmtp.iter().any(
2712 |v| matches!(v, FormatParam::H265ProfileTierLevel(p) if p.profile_id() == 2)
2713 )
2714 );
2715 }
2716 
2717 /// Test that only two of three H.265 params doesn't create PTL.
2718 /// Each permutation of missing parameter should fail.
2719 #[test]
2720 fn h265_incomplete_params_all_permutations() {
2721 // Missing level-id
2722 let missing_level = vec![
2723 ("profile-id".to_string(), "1".to_string()),
2724 ("tier-flag".to_string(), "0".to_string()),
2725 ];
2726 let params = FormatParam::parse_pairs(missing_level);
2727 assert!(
2728 !params
2729 .iter()
2730 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2731 );
2732 
2733 // Missing tier-flag
2734 let missing_tier = vec![
2735 ("profile-id".to_string(), "1".to_string()),
2736 ("level-id".to_string(), "93".to_string()),
2737 ];
2738 let params = FormatParam::parse_pairs(missing_tier);
2739 assert!(
2740 !params
2741 .iter()
2742 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2743 );
2744 
2745 // Missing profile-id
2746 let missing_profile = vec![
2747 ("tier-flag".to_string(), "0".to_string()),
2748 ("level-id".to_string(), "93".to_string()),
2749 ];
2750 let params = FormatParam::parse_pairs(missing_profile);
2751 assert!(
2752 !params
2753 .iter()
2754 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2755 );
2756 
2757 // Only one parameter
2758 let only_profile = vec![("profile-id".to_string(), "1".to_string())];
2759 let params = FormatParam::parse_pairs(only_profile);
2760 assert!(
2761 !params
2762 .iter()
2763 .any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
2764 );
2765 }
2766 
2767 /// Test H.265 with Chrome-style profile-only in complete SDP.
2768 /// Verifies that browser compatibility mode works in real SDP.
2769 #[test]
2770 fn h265_chrome_profile_only_sdp() {
2771 let input = "v=0\r\n\
2772 o=- 123456 2 IN IP4 127.0.0.1\r\n\
2773 s=-\r\n\
2774 t=0 0\r\n\
2775 a=group:BUNDLE 0\r\n\
2776 m=video 9 UDP/TLS/RTP/SAVPF 96\r\n\
2777 c=IN IP4 0.0.0.0\r\n\
2778 a=mid:0\r\n\
2779 a=sendrecv\r\n\
2780 a=rtpmap:96 H265/90000\r\n\
2781 a=fmtp:96 profile-id=1\r\n\
2782 a=setup:actpass\r\n\
2783 a=ice-ufrag:test\r\n\
2784 a=ice-pwd:testpassword\r\n\
2785 ";
2786 
2787 let sdp = Sdp::parse(input).expect("should parse");
2788 let params = sdp.media_lines[0].rtp_params();
2789 
2790 let h265 = params
2791 .iter()
2792 .find(|p| p.spec.codec == Codec::H265)
2793 .expect("should have H.265");
2794 
2795 // Should NOT have full PTL
2796 assert!(h265.spec.format.h265_profile_tier_level.is_none());
2797 
2798 // Should have profile_id field instead
2799 assert_eq!(h265.spec.format.profile_id, Some(1));
2800 }
2801 
2802 /// Test H.265 different level values in valid range.
2803 /// Ensures all valid level IDs are properly handled.
2804 #[test]
2805 fn h265_all_valid_levels() {
2806 let valid_levels = vec![
2807 30, // Level 1.0
2808 60, // Level 2.0
2809 63, // Level 2.1
2810 90, // Level 3.0
2811 93, // Level 3.1
2812 120, // Level 4.0
2813 123, // Level 4.1
2814 150, // Level 5.0
2815 153, // Level 5.1
2816 156, // Level 5.2
2817 180, // Level 6.0
2818 183, // Level 6.1
2819 186, // Level 6.2
2820 ];
2821 
2822 for level_id in valid_levels {
2823 let ptl = H265ProfileTierLevel::new(1, 0, level_id);
2824 assert!(ptl.is_some(), "Level ID {} should be valid", level_id);
2825 
2826 let f = FormatParams {
2827 h265_profile_tier_level: ptl,
2828 ..Default::default()
2829 };
2830 
2831 let serialized = f.to_string();
2832 assert!(serialized.contains(&format!("level-id={}", level_id)));
2833 }
2834 }
2835 }
2836 
2837 /// Core H.266 (VVC) codec-specific tests, mirrored from h265_codec.
2838 /// Tests basic H.266 parameter handling including profile-tier-level parsing,
2839 /// serialization, and format parameter combinations.
2840 mod h266_codec {
2841 use super::*;
2842 
2843 /// Test that H.266 format parameters can be serialized to a string and parsed back
2844 /// without loss of information. Verifies the round-trip conversion works correctly.
2845 #[test]
2846 fn h266_fmtp_round_trip() {
2847 // Test that H.266 fmtp params round-trip correctly
2848 let ptl = H266ProfileTierLevel::new(1, 0, 51).unwrap(); // Main, Main tier, Level 3.1
2849 
2850 let f = FormatParams {
2851 h266_profile_tier_level: Some(ptl),
2852 ..Default::default()
2853 };
2854 
2855 // Serialize to string
2856 let fmtp_str = f.to_string();
2857 assert_eq!(fmtp_str, "profile-id=1;tier-flag=0;level-id=51");
2858 
2859 // Parse back
2860 let parsed = FormatParams::parse_line(&fmtp_str);
2861 assert_eq!(parsed.h266_profile_tier_level, Some(ptl));
2862 }
2863 
2864 /// Test different combinations of H.266 profiles, tiers, and levels to ensure
2865 /// they are all serialized correctly. Covers Main, Main 10, different tiers, and levels.
2866 #[test]
2867 fn h266_different_profiles() {
2868 // Test Main profile (1)
2869 let main = H266ProfileTierLevel::new(1, 0, 51).unwrap();
2870 let f = FormatParams {
2871 h266_profile_tier_level: Some(main),
2872 ..Default::default()
2873 };
2874 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=51");
2875 
2876 // Test Main 10 profile (2)
2877 let main10 = H266ProfileTierLevel::new(33, 0, 51).unwrap();
2878 let f = FormatParams {
2879 h266_profile_tier_level: Some(main10),
2880 ..Default::default()
2881 };
2882 assert_eq!(f.to_string(), "profile-id=33;tier-flag=0;level-id=51");
2883 
2884 // Test different tier (High tier)
2885 let high_tier = H266ProfileTierLevel::new(1, 1, 51).unwrap();
2886 let f = FormatParams {
2887 h266_profile_tier_level: Some(high_tier),
2888 ..Default::default()
2889 };
2890 assert_eq!(f.to_string(), "profile-id=1;tier-flag=1;level-id=51");
2891 
2892 // Test different level (Level 5.1 = 153)
2893 let level_5_1 = H266ProfileTierLevel::new(1, 0, 83).unwrap();
2894 let f = FormatParams {
2895 h266_profile_tier_level: Some(level_5_1),
2896 ..Default::default()
2897 };
2898 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=83");
2899 }
2900 
2901 /// Test parsing H.266 parameters from key-value pairs as they would appear in SDP.
2902 /// Verifies that the three separate params (profile-id, tier-flag, level-id) are
2903 /// correctly combined into a single H266ProfileTierLevel composite.
2904 #[test]
2905 fn h266_parse_from_pairs() {
2906 // Test parsing from key-value pairs (as would come from SDP)
2907 let pairs = vec![
2908 ("profile-id".to_string(), "1".to_string()),
2909 ("tier-flag".to_string(), "0".to_string()),
2910 ("level-id".to_string(), "51".to_string()),
2911 ];
2912 
2913 let params = FormatParam::parse_pairs(pairs);
2914 
2915 // Should have exactly one H266ProfileTierLevel param
2916 let h266_params: Vec<_> = params
2917 .iter()
2918 .filter(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
2919 .collect();
2920 assert_eq!(h266_params.len(), 1);
2921 
2922 if let FormatParam::H266ProfileTierLevel(ptl) = h266_params[0] {
2923 assert_eq!(ptl.profile_id(), 1);
2924 assert_eq!(ptl.tier_flag(), 0);
2925 assert_eq!(ptl.level_id(), 51);
2926 } else {
2927 panic!("Expected H266ProfileTierLevel");
2928 }
2929 }
2930 
2931 /// Test that H.266 profile-tier-level params can coexist with other H.266-specific
2932 /// parameters like sprop-max-don-diff without interfering with each other.
2933 #[test]
2934 fn h266_mixed_with_other_params() {
2935 // Test H.266 params mixed with other format params
2936 let pairs = vec![
2937 ("profile-id".to_string(), "1".to_string()),
2938 ("tier-flag".to_string(), "0".to_string()),
2939 ("level-id".to_string(), "51".to_string()),
2940 ("sprop-max-don-diff".to_string(), "0".to_string()),
2941 ];
2942 
2943 let params = FormatParam::parse_pairs(pairs);
2944 
2945 // Should have H266ProfileTierLevel and SpropMaxDonDiff
2946 assert!(
2947 params
2948 .iter()
2949 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
2950 );
2951 assert!(
2952 params
2953 .iter()
2954 .any(|p| matches!(p, FormatParam::SpropMaxDonDiff(0)))
2955 );
2956 }
2957 
2958 /// Test sprop-max-don-diff parsing, display, and round-trip for various values.
2959 #[test]
2960 fn sprop_max_don_diff_parse_and_display() {
2961 // Parse standalone sprop-max-don-diff
2962 let param = FormatParam::parse("sprop-max-don-diff", "32");
2963 assert!(matches!(param, FormatParam::SpropMaxDonDiff(32)));
2964 
2965 // Display format
2966 assert_eq!(param.to_string(), "sprop-max-don-diff=32");
2967 
2968 // Parse value 0 (DONL disabled)
2969 let param0 = FormatParam::parse("sprop-max-don-diff", "0");
2970 assert!(matches!(param0, FormatParam::SpropMaxDonDiff(0)));
2971 
2972 // Parse max valid value (RFC 9328 ยง7.2: 0..32767)
2973 let param_max = FormatParam::parse("sprop-max-don-diff", "32767");
2974 assert!(matches!(param_max, FormatParam::SpropMaxDonDiff(32767)));
2975 
2976 // Out-of-range value (32768) rejected per RFC 9328 ยง7.2
2977 let param_over = FormatParam::parse("sprop-max-don-diff", "32768");
2978 assert!(matches!(param_over, FormatParam::Unknown));
2979 
2980 // Invalid value falls back to Unknown
2981 let param_bad = FormatParam::parse("sprop-max-don-diff", "notanumber");
2982 assert!(matches!(param_bad, FormatParam::Unknown));
2983 }
2984 
2985 /// Test that sprop-max-don-diff > 0 round-trips through H.266 fmtp with PTL params.
2986 #[test]
2987 fn h266_sprop_max_don_diff_nonzero_with_ptl() {
2988 let pairs = vec![
2989 ("profile-id".to_string(), "1".to_string()),
2990 ("tier-flag".to_string(), "0".to_string()),
2991 ("level-id".to_string(), "51".to_string()),
2992 ("sprop-max-don-diff".to_string(), "32".to_string()),
2993 ];
2994 
2995 let params = FormatParam::parse_pairs(pairs);
2996 
2997 // Should have both H266ProfileTierLevel and SpropMaxDonDiff(32)
2998 assert!(
2999 params
3000 .iter()
3001 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3002 );
3003 assert!(
3004 params
3005 .iter()
3006 .any(|p| matches!(p, FormatParam::SpropMaxDonDiff(32)))
3007 );
3008 
3009 // Verify PTL values are correct
3010 if let Some(FormatParam::H266ProfileTierLevel(ptl)) = params
3011 .iter()
3012 .find(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3013 {
3014 assert_eq!(ptl.profile_id(), 1);
3015 assert_eq!(ptl.tier_flag(), 0);
3016 assert_eq!(ptl.level_id(), 51);
3017 }
3018 }
3019 }
3020 
3021 /// Tests ensuring H.266 parameters are not confused with other codecs.
3022 /// Verifies that H.266's three-parameter format (profile-id, tier-flag, level-id)
3023 /// is correctly distinguished from H.264's profile-level-id, VP9's profile-id,
3024 /// and AV1's profile/tier/level-idx parameters.
3025 mod no_confusion_h266 {
3026 use super::*;
3027 
3028 /// Test that H.264's profile-level-id parameter (single hex value) is not confused
3029 /// with H.266's three separate parameters. H.264 uses a different format entirely.
3030 #[test]
3031 fn h266_no_confusion_with_h264() {
3032 // H.264 uses profile-level-id (single param)
3033 let h264_pairs = vec![
3034 ("profile-level-id".to_string(), "42e01f".to_string()),
3035 ("packetization-mode".to_string(), "1".to_string()),
3036 ];
3037 
3038 let h264_params = FormatParam::parse_pairs(h264_pairs);
3039 
3040 // Should NOT have H266ProfileTierLevel
3041 assert!(
3042 !h264_params
3043 .iter()
3044 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3045 );
3046 
3047 // Should have ProfileLevelId (H.264)
3048 assert!(
3049 h264_params
3050 .iter()
3051 .any(|p| matches!(p, FormatParam::ProfileLevelId(_)))
3052 );
3053 assert!(
3054 h264_params
3055 .iter()
3056 .any(|p| matches!(p, FormatParam::PacketizationMode(1)))
3057 );
3058 }
3059 
3060 /// Test that VP9's profile-id parameter (standalone) is not confused with H.266's
3061 /// profile-id which must be combined with tier-flag and level-id.
3062 #[test]
3063 fn h266_no_confusion_with_vp9() {
3064 // VP9 uses profile-id (single param, not combined)
3065 let vp9_pairs = vec![("profile-id".to_string(), "0".to_string())];
3066 
3067 let vp9_params = FormatParam::parse_pairs(vp9_pairs);
3068 
3069 // Should NOT have H266ProfileTierLevel (missing tier-flag and level-id)
3070 assert!(
3071 !vp9_params
3072 .iter()
3073 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3074 );
3075 
3076 // Should have ProfileId (VP9)
3077 assert!(
3078 vp9_params
3079 .iter()
3080 .any(|p| matches!(p, FormatParam::ProfileId(0)))
3081 );
3082 }
3083 
3084 /// Test that AV1's parameters (profile, tier, level-idx) use different naming than
3085 /// H.266 (profile-id, tier-flag, level-id) and are not confused during parsing.
3086 #[test]
3087 fn h266_no_confusion_with_av1() {
3088 // AV1 uses profile, level-idx, tier (but should not be confused with H.266)
3089 let av1_pairs = vec![
3090 ("profile".to_string(), "0".to_string()),
3091 ("level-idx".to_string(), "5".to_string()),
3092 ("tier".to_string(), "0".to_string()),
3093 ];
3094 
3095 let av1_params = FormatParam::parse_pairs(av1_pairs);
3096 
3097 // Should NOT have H266ProfileTierLevel
3098 // (AV1 uses different param names: profile vs profile-id,
3099 // tier vs tier-flag, level-idx vs level-id)
3100 assert!(
3101 !av1_params
3102 .iter()
3103 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3104 );
3105 
3106 // Should have AV1-specific params
3107 assert!(
3108 av1_params
3109 .iter()
3110 .any(|p| matches!(p, FormatParam::Profile(0)))
3111 );
3112 assert!(
3113 av1_params
3114 .iter()
3115 .any(|p| matches!(p, FormatParam::LevelIdx(5)))
3116 );
3117 assert!(av1_params.iter().any(|p| matches!(p, FormatParam::Tier(0))));
3118 }
3119 }
3120 
3121 /// Additional H.266 parsing and validation tests.
3122 /// Covers incomplete parameter handling and complete SDP integration scenarios.
3123 mod h266_additional {
3124 use super::*;
3125 
3126 /// Test that incomplete H.266 parameter sets do not create a composite ProfileTierLevel.
3127 /// All three parameters (profile-id, tier-flag, level-id) must be present.
3128 #[test]
3129 fn h266_partial_params_no_composite() {
3130 // If only some H.266 params are present, should not create composite
3131 let partial_pairs = vec![
3132 ("profile-id".to_string(), "1".to_string()),
3133 ("tier-flag".to_string(), "0".to_string()),
3134 // Missing level-id
3135 ];
3136 
3137 let params = FormatParam::parse_pairs(partial_pairs);
3138 
3139 // Should NOT have H266ProfileTierLevel (incomplete)
3140 assert!(
3141 !params
3142 .iter()
3143 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3144 );
3145 }
3146 
3147 /// Test H.266 parameters in a complete SDP offer/answer scenario.
3148 /// Verifies that H.266 rtpmap and fmtp lines are correctly parsed along with RTX.
3149 #[test]
3150 fn h266_in_complete_sdp() {
3151 let input = "v=0\r\n\
3152 o=- 123456 2 IN IP4 127.0.0.1\r\n\
3153 s=-\r\n\
3154 t=0 0\r\n\
3155 a=group:BUNDLE 0\r\n\
3156 m=video 9 UDP/TLS/RTP/SAVPF 96 97\r\n\
3157 c=IN IP4 0.0.0.0\r\n\
3158 a=mid:0\r\n\
3159 a=sendrecv\r\n\
3160 a=rtpmap:96 H266/90000\r\n\
3161 a=fmtp:96 profile-id=1;tier-flag=0;level-id=51\r\n\
3162 a=rtpmap:97 rtx/90000\r\n\
3163 a=fmtp:97 apt=96\r\n\
3164 a=setup:actpass\r\n\
3165 a=ice-ufrag:test\r\n\
3166 a=ice-pwd:testpassword\r\n\
3167 ";
3168 
3169 let sdp = Sdp::parse(input).expect("should parse");
3170 
3171 // Get RTP params
3172 let params = sdp.media_lines[0].rtp_params();
3173 
3174 // Find H.266 payload
3175 let h266_payload = params
3176 .iter()
3177 .find(|p| p.spec.codec == Codec::H266)
3178 .expect("should have H.266");
3179 
3180 assert_eq!(h266_payload.pt, 96.into());
3181 assert_eq!(h266_payload.spec.clock_rate, Frequency::NINETY_KHZ);
3182 
3183 // Check H.266 params
3184 let ptl = h266_payload
3185 .spec
3186 .format
3187 .h266_profile_tier_level
3188 .expect("should have H.266 PTL");
3189 assert_eq!(ptl.profile_id(), 1);
3190 assert_eq!(ptl.tier_flag(), 0);
3191 assert_eq!(ptl.level_id(), 51);
3192 
3193 // Check RTX
3194 assert_eq!(h266_payload.resend, Some(97.into()));
3195 }
3196 }
3197 
3198 /// Multi-codec integration tests.
3199 /// Verifies that multiple video codecs (H.264, H.266, VP8, VP9) can coexist
3200 /// in the same SDP without parameter conflicts or interference.
3201 mod multi_codec_h266 {
3202 use super::*;
3203 
3204 /// Test that H.264, H.266, and other video codecs can coexist in the same SDP
3205 /// without their format parameters interfering with each other.
3206 #[test]
3207 fn h266_and_h264_coexist() {
3208 let input = "v=0\r\n\
3209 o=- 123456 2 IN IP4 127.0.0.1\r\n\
3210 s=-\r\n\
3211 t=0 0\r\n\
3212 a=group:BUNDLE 0\r\n\
3213 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98\r\n\
3214 c=IN IP4 0.0.0.0\r\n\
3215 a=mid:0\r\n\
3216 a=sendrecv\r\n\
3217 a=rtpmap:96 H264/90000\r\n\
3218 a=fmtp:96 profile-level-id=42e01f;packetization-mode=1\r\n\
3219 a=rtpmap:97 H266/90000\r\n\
3220 a=fmtp:97 profile-id=1;tier-flag=0;level-id=51\r\n\
3221 a=rtpmap:98 VP8/90000\r\n\
3222 a=setup:actpass\r\n\
3223 a=ice-ufrag:test\r\n\
3224 a=ice-pwd:testpassword\r\n\
3225 ";
3226 
3227 let sdp = Sdp::parse(input).expect("should parse");
3228 let params = sdp.media_lines[0].rtp_params();
3229 
3230 // Check H.264
3231 let h264 = params
3232 .iter()
3233 .find(|p| p.spec.codec == Codec::H264)
3234 .expect("should have H.264");
3235 assert_eq!(h264.pt, 96.into());
3236 assert_eq!(h264.spec.format.profile_level_id, Some(0x42e01f));
3237 assert_eq!(h264.spec.format.packetization_mode, Some(1));
3238 assert!(h264.spec.format.h266_profile_tier_level.is_none());
3239 
3240 // Check H.266
3241 let h266 = params
3242 .iter()
3243 .find(|p| p.spec.codec == Codec::H266)
3244 .expect("should have H.266");
3245 assert_eq!(h266.pt, 97.into());
3246 let ptl = h266
3247 .spec
3248 .format
3249 .h266_profile_tier_level
3250 .expect("should have H.266 PTL");
3251 assert_eq!(ptl.profile_id(), 1);
3252 assert!(h266.spec.format.profile_level_id.is_none());
3253 
3254 // Check VP8
3255 let vp8 = params
3256 .iter()
3257 .find(|p| p.spec.codec == Codec::Vp8)
3258 .expect("should have VP8");
3259 assert_eq!(vp8.pt, 98.into());
3260 assert!(vp8.spec.format.h266_profile_tier_level.is_none());
3261 assert!(vp8.spec.format.profile_level_id.is_none());
3262 }
3263 }
3264 
3265 /// H.266 serialization and edge case tests.
3266 /// Tests media attribute generation, empty parameter handling, extreme values,
3267 /// parameter ordering independence, DONL parameters, and invalid value handling.
3268 mod h266_serialization {
3269 use super::*;
3270 
3271 /// Test that H.266 parameters are correctly serialized as media attributes,
3272 /// including rtpmap, fmtp with ProfileTierLevel, rtcp-fb, and RTX payload.
3273 #[test]
3274 fn h266_serialization_in_media_attrs() {
3275 let ptl = H266ProfileTierLevel::new(33, 1, 83).unwrap(); // Main 10, High tier, Level 5.1
3276 
3277 let mut payload = PayloadParams::new(
3278 100.into(),
3279 Some(101.into()),
3280 CodecSpec {
3281 codec: Codec::H266,
3282 clock_rate: Frequency::NINETY_KHZ,
3283 channels: None,
3284 format: FormatParams {
3285 h266_profile_tier_level: Some(ptl),
3286 ..Default::default()
3287 },
3288 },
3289 );
3290 payload.fb_transport_cc = true;
3291 payload.fb_nack = true;
3292 payload.fb_pli = true;
3293 
3294 let mut attrs = vec![];
3295 payload.as_media_attrs(&mut attrs);
3296 
3297 // Should have rtpmap for H.266
3298 assert!(attrs.iter().any(|a| matches!(a,
3299 MediaAttribute::RtpMap { pt, value }
3300 if *pt == 100.into() && value.codec == Codec::H266
3301 )));
3302 
3303 // Should have fmtp with H.266 params
3304 let fmtp = attrs.iter().find(|a| {
3305 matches!(a,
3306 MediaAttribute::Fmtp { pt, .. } if *pt == 100.into()
3307 )
3308 });
3309 assert!(fmtp.is_some());
3310 
3311 if let Some(MediaAttribute::Fmtp { values, .. }) = fmtp {
3312 assert!(
3313 values
3314 .iter()
3315 .any(|v| matches!(v, FormatParam::H266ProfileTierLevel(p)
3316 if p.profile_id() == 33 && p.tier_flag() == 1 && p.level_id() == 83
3317 ))
3318 );
3319 }
3320 
3321 // Should have rtx mapping
3322 assert!(attrs.iter().any(|a| matches!(a,
3323 MediaAttribute::RtpMap { pt, value }
3324 if *pt == 101.into() && value.codec == Codec::Rtx
3325 )));
3326 }
3327 
3328 /// Test that empty format parameters do not generate an a=fmtp line in SDP,
3329 /// avoiding invalid SDP syntax with empty fmtp attributes.
3330 #[test]
3331 fn h266_empty_fmtp_not_serialized() {
3332 // Empty fmtp should not be written
3333 let payload = PayloadParams::new(
3334 100.into(),
3335 None,
3336 CodecSpec {
3337 codec: Codec::H266,
3338 clock_rate: Frequency::NINETY_KHZ,
3339 channels: None,
3340 format: FormatParams::default(),
3341 },
3342 );
3343 
3344 let mut attrs = vec![];
3345 payload.as_media_attrs(&mut attrs);
3346 
3347 // Should NOT have fmtp line (it would be empty)
3348 assert!(
3349 !attrs
3350 .iter()
3351 .any(|a| matches!(a, MediaAttribute::Fmtp { .. }))
3352 );
3353 }
3354 
3355 /// Test H.266 with extreme profile/tier/level values at boundaries.
3356 /// Verifies parsing and serialization of edge case values.
3357 #[test]
3358 fn h266_extreme_values() {
3359 // Test minimum level (Level 1.0 = 30)
3360 let min_level = H266ProfileTierLevel::new(1, 0, 16).unwrap();
3361 let f = FormatParams {
3362 h266_profile_tier_level: Some(min_level),
3363 ..Default::default()
3364 };
3365 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=16");
3366 
3367 // Test maximum level (Level 6.2 = 186)
3368 let max_level = H266ProfileTierLevel::new(1, 0, 102).unwrap();
3369 let f = FormatParams {
3370 h266_profile_tier_level: Some(max_level),
3371 ..Default::default()
3372 };
3373 assert_eq!(f.to_string(), "profile-id=1;tier-flag=0;level-id=102");
3374 
3375 // Test Main10 with High tier
3376 let main10_high = H266ProfileTierLevel::new(33, 1, 51).unwrap();
3377 let f = FormatParams {
3378 h266_profile_tier_level: Some(main10_high),
3379 ..Default::default()
3380 };
3381 assert_eq!(f.to_string(), "profile-id=33;tier-flag=1;level-id=51");
3382 }
3383 
3384 /// Test H.266 parameter ordering doesn't affect parsing.
3385 /// SDP parameters can appear in any order.
3386 #[test]
3387 fn h266_parameter_order_independence() {
3388 // Normal order
3389 let normal_order = vec![
3390 ("profile-id".to_string(), "1".to_string()),
3391 ("tier-flag".to_string(), "0".to_string()),
3392 ("level-id".to_string(), "51".to_string()),
3393 ];
3394 
3395 // Reversed order
3396 let reversed_order = vec![
3397 ("level-id".to_string(), "51".to_string()),
3398 ("tier-flag".to_string(), "0".to_string()),
3399 ("profile-id".to_string(), "1".to_string()),
3400 ];
3401 
3402 // Mixed order
3403 let mixed_order = vec![
3404 ("tier-flag".to_string(), "0".to_string()),
3405 ("profile-id".to_string(), "1".to_string()),
3406 ("level-id".to_string(), "51".to_string()),
3407 ];
3408 
3409 let normal_params = FormatParam::parse_pairs(normal_order);
3410 let reversed_params = FormatParam::parse_pairs(reversed_order);
3411 let mixed_params = FormatParam::parse_pairs(mixed_order);
3412 
3413 // All should create H266ProfileTierLevel
3414 assert!(
3415 normal_params
3416 .iter()
3417 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3418 );
3419 assert!(
3420 reversed_params
3421 .iter()
3422 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3423 );
3424 assert!(
3425 mixed_params
3426 .iter()
3427 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3428 );
3429 
3430 // All should have the same values
3431 for params in [&normal_params, &reversed_params, &mixed_params] {
3432 if let Some(FormatParam::H266ProfileTierLevel(ptl)) = params
3433 .iter()
3434 .find(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3435 {
3436 assert_eq!(ptl.profile_id(), 1);
3437 assert_eq!(ptl.tier_flag(), 0);
3438 assert_eq!(ptl.level_id(), 51);
3439 }
3440 }
3441 }
3442 
3443 #[test]
3444 fn h266_invalid_parameter_values() {
3445 // Invalid profile-id
3446 let invalid_profile = vec![
3447 ("profile-id".to_string(), "999".to_string()),
3448 ("tier-flag".to_string(), "0".to_string()),
3449 ("level-id".to_string(), "51".to_string()),
3450 ];
3451 let params = FormatParam::parse_pairs(invalid_profile);
3452 assert!(
3453 !params
3454 .iter()
3455 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3456 );
3457 
3458 // Invalid tier-flag
3459 let invalid_tier = vec![
3460 ("profile-id".to_string(), "1".to_string()),
3461 ("tier-flag".to_string(), "5".to_string()),
3462 ("level-id".to_string(), "51".to_string()),
3463 ];
3464 let params = FormatParam::parse_pairs(invalid_tier);
3465 assert!(
3466 !params
3467 .iter()
3468 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3469 );
3470 
3471 // Invalid level-id
3472 let invalid_level = vec![
3473 ("profile-id".to_string(), "1".to_string()),
3474 ("tier-flag".to_string(), "0".to_string()),
3475 ("level-id".to_string(), "999".to_string()),
3476 ];
3477 let params = FormatParam::parse_pairs(invalid_level);
3478 assert!(
3479 !params
3480 .iter()
3481 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3482 );
3483 }
3484 }
3485 
3486 /// H.266 integration with RTCP feedback mechanisms.
3487 /// Tests H.266 behavior with multiple codecs and various RTCP feedback types
3488 /// including NACK, PLI, FIR, and transport-cc.
3489 mod h266_integration {
3490 use super::*;
3491 
3492 /// Test H.266 in SDP with multiple video codecs and feedback mechanisms.
3493 /// Verifies proper parsing of complex multi-codec scenarios.
3494 #[test]
3495 fn h266_multi_codec_with_feedback() {
3496 let input = "v=0\r\n\
3497 o=- 123456 2 IN IP4 127.0.0.1\r\n\
3498 s=-\r\n\
3499 t=0 0\r\n\
3500 a=group:BUNDLE 0\r\n\
3501 m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99\r\n\
3502 c=IN IP4 0.0.0.0\r\n\
3503 a=mid:0\r\n\
3504 a=sendrecv\r\n\
3505 a=rtpmap:96 H266/90000\r\n\
3506 a=fmtp:96 profile-id=1;tier-flag=0;level-id=51\r\n\
3507 a=rtcp-fb:96 nack\r\n\
3508 a=rtcp-fb:96 nack pli\r\n\
3509 a=rtcp-fb:96 ccm fir\r\n\
3510 a=rtcp-fb:96 transport-cc\r\n\
3511 a=rtpmap:97 rtx/90000\r\n\
3512 a=fmtp:97 apt=96\r\n\
3513 a=rtpmap:98 VP9/90000\r\n\
3514 a=fmtp:98 profile-id=0\r\n\
3515 a=rtpmap:99 rtx/90000\r\n\
3516 a=fmtp:99 apt=98\r\n\
3517 a=setup:actpass\r\n\
3518 a=ice-ufrag:test\r\n\
3519 a=ice-pwd:testpassword\r\n\
3520 ";
3521 
3522 let sdp = Sdp::parse(input).expect("should parse");
3523 let params = sdp.media_lines[0].rtp_params();
3524 
3525 // Check H.266 with all feedback mechanisms
3526 let h266 = params
3527 .iter()
3528 .find(|p| p.spec.codec == Codec::H266)
3529 .expect("should have H.266");
3530 assert_eq!(h266.pt, 96.into());
3531 assert!(h266.fb_nack);
3532 assert!(h266.fb_pli);
3533 assert!(h266.fb_fir);
3534 assert!(h266.fb_transport_cc);
3535 assert_eq!(h266.resend, Some(97.into()));
3536 
3537 // Verify H.266 PTL is correct
3538 let ptl = h266
3539 .spec
3540 .format
3541 .h266_profile_tier_level
3542 .expect("should have PTL");
3543 assert_eq!(ptl.profile_id(), 1);
3544 
3545 // Check VP9 is separate and correct
3546 let vp9 = params
3547 .iter()
3548 .find(|p| p.spec.codec == Codec::Vp9)
3549 .expect("should have VP9");
3550 assert_eq!(vp9.pt, 98.into());
3551 assert_eq!(vp9.spec.format.profile_id, Some(0));
3552 assert!(vp9.spec.format.h266_profile_tier_level.is_none());
3553 }
3554 }
3555 
3556 /// Advanced H.266 functionality tests.
3557 /// Covers SDP round-trip serialization, incomplete parameter permutations,
3558 /// browser compatibility (Chrome profile-only mode), and validation of all
3559 /// standard H.266 level values (1.0 through 6.2).
3560 mod h266_advanced {
3561 use super::*;
3562 
3563 /// Test H.266 SDP serialization maintains parameter integrity.
3564 /// Ensures that serialized SDP can be parsed back with identical values.
3565 #[test]
3566 fn h266_sdp_serialization_round_trip() {
3567 let ptl = H266ProfileTierLevel::new(33, 0, 83).unwrap();
3568 
3569 let payload = PayloadParams::new(
3570 100.into(),
3571 Some(101.into()),
3572 CodecSpec {
3573 codec: Codec::H266,
3574 clock_rate: Frequency::NINETY_KHZ,
3575 channels: None,
3576 format: FormatParams {
3577 h266_profile_tier_level: Some(ptl),
3578 ..Default::default()
3579 },
3580 },
3581 );
3582 
3583 let mut attrs = vec![];
3584 payload.as_media_attrs(&mut attrs);
3585 
3586 // Find the fmtp line
3587 let fmtp = attrs
3588 .iter()
3589 .find_map(|a| match a {
3590 MediaAttribute::Fmtp { pt, values } if *pt == 100.into() => Some(values),
3591 _ => None,
3592 })
3593 .expect("should have fmtp");
3594 
3595 // Should contain H266ProfileTierLevel
3596 assert!(fmtp.iter().any(
3597 |v| matches!(v, FormatParam::H266ProfileTierLevel(p) if p.profile_id() == 33)
3598 ));
3599 }
3600 
3601 /// Test that only two of three H.266 params doesn't create PTL.
3602 /// Each permutation of missing parameter should fail.
3603 #[test]
3604 fn h266_incomplete_params_all_permutations() {
3605 // Missing level-id
3606 let missing_level = vec![
3607 ("profile-id".to_string(), "1".to_string()),
3608 ("tier-flag".to_string(), "0".to_string()),
3609 ];
3610 let params = FormatParam::parse_pairs(missing_level);
3611 assert!(
3612 !params
3613 .iter()
3614 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3615 );
3616 
3617 // Missing tier-flag
3618 let missing_tier = vec![
3619 ("profile-id".to_string(), "1".to_string()),
3620 ("level-id".to_string(), "51".to_string()),
3621 ];
3622 let params = FormatParam::parse_pairs(missing_tier);
3623 assert!(
3624 !params
3625 .iter()
3626 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3627 );
3628 
3629 // Missing profile-id
3630 let missing_profile = vec![
3631 ("tier-flag".to_string(), "0".to_string()),
3632 ("level-id".to_string(), "51".to_string()),
3633 ];
3634 let params = FormatParam::parse_pairs(missing_profile);
3635 assert!(
3636 !params
3637 .iter()
3638 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3639 );
3640 
3641 // Only one parameter
3642 let only_profile = vec![("profile-id".to_string(), "1".to_string())];
3643 let params = FormatParam::parse_pairs(only_profile);
3644 assert!(
3645 !params
3646 .iter()
3647 .any(|p| matches!(p, FormatParam::H266ProfileTierLevel(_)))
3648 );
3649 }
3650 
3651 /// Test H.266 with Chrome-style profile-only in complete SDP.
3652 /// Verifies that browser compatibility mode works in real SDP.
3653 #[test]
3654 fn h266_chrome_profile_only_sdp() {
3655 let input = "v=0\r\n\
3656 o=- 123456 2 IN IP4 127.0.0.1\r\n\
3657 s=-\r\n\
3658 t=0 0\r\n\
3659 a=group:BUNDLE 0\r\n\
3660 m=video 9 UDP/TLS/RTP/SAVPF 96\r\n\
3661 c=IN IP4 0.0.0.0\r\n\
3662 a=mid:0\r\n\
3663 a=sendrecv\r\n\
3664 a=rtpmap:96 H266/90000\r\n\
3665 a=fmtp:96 profile-id=1\r\n\
3666 a=setup:actpass\r\n\
3667 a=ice-ufrag:test\r\n\
3668 a=ice-pwd:testpassword\r\n\
3669 ";
3670 
3671 let sdp = Sdp::parse(input).expect("should parse");
3672 let params = sdp.media_lines[0].rtp_params();
3673 
3674 let h266 = params
3675 .iter()
3676 .find(|p| p.spec.codec == Codec::H266)
3677 .expect("should have H.266");
3678 
3679 // Should NOT have full PTL
3680 assert!(h266.spec.format.h266_profile_tier_level.is_none());
3681 
3682 // Should have profile_id field instead
3683 assert_eq!(h266.spec.format.profile_id, Some(1));
3684 }
3685 
3686 /// Test H.266 different level values in valid range.
3687 /// Ensures all valid level IDs are properly handled.
3688 #[test]
3689 fn h266_all_valid_levels() {
3690 let valid_levels = vec![
3691 16, // Level 1.0
3692 32, // Level 2.0
3693 35, // Level 2.1
3694 48, // Level 3.0
3695 51, // Level 3.1
3696 64, // Level 4.0
3697 67, // Level 4.1
3698 80, // Level 5.0
3699 83, // Level 5.1
3700 86, // Level 5.2
3701 96, // Level 6.0
3702 99, // Level 6.1
3703 102, // Level 6.2
3704 ];
3705 
3706 for level_id in valid_levels {
3707 let ptl = H266ProfileTierLevel::new(1, 0, level_id);
3708 assert!(ptl.is_some(), "Level ID {} should be valid", level_id);
3709 
3710 let f = FormatParams {
3711 h266_profile_tier_level: ptl,
3712 ..Default::default()
3713 };
3714 
3715 let serialized = f.to_string();
3716 assert!(serialized.contains(&format!("level-id={}", level_id)));
3717 }
3718 }
3719 }
3720}