1use std::fmt;
2use std::str::FromStr;
3
4use crate::attributes::{AttributeError, ErrorContext, TypedAttribute};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum ReferenceClock {
11 Ntp(Ntp),
12 Ptp(Ptp),
13 Gps,
14 Gal,
15 Glonass,
16 Local,
17 Private(PrivateSource),
18 Ext(ClockSourceExt),
19}
20
21impl ReferenceClock {
22 pub fn from_ntp_hostname(hostname: impl ToString) -> Self {
24 NtpServerAddr::from_hostname(hostname).into()
25 }
26
27 pub fn from_ntp_hostname_with_port(hostname: impl ToString, port: u16) -> Self {
29 NtpServerAddr::from_hostname_with_port(hostname, port).into()
30 }
31
32 pub fn new_ntp_traceable() -> Self {
34 NtpServerAddr::new_traceable().into()
35 }
36
37 pub fn from_ptp_gmid(version: PtpVersion, gmid: impl Into<Eui64>) -> Self {
39 Ptp::from_gmid(version, gmid).into()
40 }
41
42 pub fn from_ptp_gmid_with_domain(gmid: impl Into<Eui64>, domain: PtpDomain) -> Self {
44 Ptp::from_gmid_with_domain(gmid, domain).into()
45 }
46
47 pub fn from_ptp_gmid_with_domain_name(gmid: impl Into<Eui64>, name: impl ToString) -> Self {
51 Ptp::from_gmid_with_domain_name(gmid, name).into()
52 }
53
54 pub fn try_from_ptp_gmid_with_domain_number(
60 gmid: impl Into<Eui64>,
61 number: u8,
62 ) -> Result<Self, AttributeError> {
63 Ok(Ptp::try_from_gmid_with_domain_number(gmid, number)?.into())
64 }
65
66 pub fn new_ptp_traceable(version: PtpVersion) -> Self {
68 Ptp::new_traceable(version).into()
69 }
70
71 pub fn new_private_source_standard() -> Self {
73 PrivateSource::Standard.into()
74 }
75
76 pub fn new_private_source_traceable() -> Self {
78 PrivateSource::Traceable.into()
79 }
80
81 pub fn from_clock_source_ext_name(name: impl ToString) -> Self {
83 ClockSourceExt::new(name).into()
84 }
85
86 pub fn from_clock_source_ext_name_with_value(
88 name: impl ToString,
89 value: impl ToString,
90 ) -> Self {
91 ClockSourceExt::with_value(name, value).into()
92 }
93}
94
95impl FromStr for ReferenceClock {
96 type Err = AttributeError;
97
98 fn from_str(s: &str) -> Result<Self, AttributeError> {
99 if let Some(ntp) = s.strip_prefix("ntp=") {
100 Ok(Self::Ntp(
101 Ntp::from_str(ntp).with_attr(<Self as TypedAttribute>::NAME)?,
102 ))
103 } else if let Some(ptp) = s.strip_prefix("ptp=") {
104 Ok(Self::Ptp(
105 Ptp::from_str(ptp).with_attr(<Self as TypedAttribute>::NAME)?,
106 ))
107 } else if s == "gps" {
108 Ok(Self::Gps)
109 } else if s == "gal" {
110 Ok(Self::Gal)
111 } else if s == "glonass" {
112 Ok(Self::Glonass)
113 } else if s == "local" {
114 Ok(Self::Local)
115 } else if s == "private" {
116 Ok(Self::Private(PrivateSource::Standard))
117 } else if s == "private:traceable" {
118 Ok(Self::Private(PrivateSource::Traceable))
119 } else {
120 Ok(Self::Ext(
121 ClockSourceExt::from_str(s).with_attr(<Self as TypedAttribute>::NAME)?,
122 ))
123 }
124 }
125}
126
127impl fmt::Display for ReferenceClock {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 Self::Ntp(ntp) => write!(f, "ntp={ntp}"),
131 Self::Ptp(ptp) => write!(f, "ptp={ptp}"),
132 Self::Gps => write!(f, "gps"),
133 Self::Gal => write!(f, "gal"),
134 Self::Glonass => write!(f, "glonass"),
135 Self::Local => write!(f, "local"),
136 Self::Private(private) => {
137 if *private == PrivateSource::Traceable {
138 write!(f, "private:traceable")
139 } else {
140 write!(f, "private")
141 }
142 }
143 Self::Ext(ext) => write!(f, "{ext}"),
144 }
145 }
146}
147
148impl TypedAttribute for ReferenceClock {
149 const NAME: &'static str = "ts-refclk";
150}
151
152impl From<Ntp> for ReferenceClock {
153 fn from(ntp: Ntp) -> Self {
154 ReferenceClock::Ntp(ntp)
155 }
156}
157
158impl From<NtpServerAddr> for ReferenceClock {
159 fn from(server: NtpServerAddr) -> Self {
160 ReferenceClock::Ntp(Ntp { server })
161 }
162}
163
164impl From<Ptp> for ReferenceClock {
165 fn from(ptp: Ptp) -> Self {
166 ReferenceClock::Ptp(ptp)
167 }
168}
169
170impl From<(PtpVersion, PtpServer)> for ReferenceClock {
171 fn from(ptp: (PtpVersion, PtpServer)) -> Self {
172 ReferenceClock::Ptp(Ptp::new(ptp.0, ptp.1))
173 }
174}
175
176impl From<PrivateSource> for ReferenceClock {
177 fn from(private_src: PrivateSource) -> Self {
178 ReferenceClock::Private(private_src)
179 }
180}
181
182impl From<ClockSourceExt> for ReferenceClock {
183 fn from(clock_src_ext: ClockSourceExt) -> Self {
184 ReferenceClock::Ext(clock_src_ext)
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct Ntp {
191 pub server: NtpServerAddr,
192}
193
194impl Ntp {
195 pub fn from_hostname(hostname: impl ToString) -> Ntp {
197 NtpServerAddr::from_hostname(hostname).into()
198 }
199
200 pub fn from_hostname_with_port(hostname: impl ToString, port: u16) -> Ntp {
202 NtpServerAddr::from_hostname_with_port(hostname, port).into()
203 }
204
205 pub fn new_traceable() -> Ntp {
207 NtpServerAddr::new_traceable().into()
208 }
209}
210
211impl FromStr for Ntp {
212 type Err = AttributeError;
213
214 fn from_str(s: &str) -> Result<Self, AttributeError> {
215 let server = NtpServerAddr::from_str(s)?;
216 Ok(Self { server })
217 }
218}
219
220impl fmt::Display for Ntp {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 write!(f, "{}", self.server)
223 }
224}
225
226impl From<NtpServerAddr> for Ntp {
227 fn from(server: NtpServerAddr) -> Self {
228 Ntp { server }
229 }
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum NtpServerAddr {
235 HostPort { hostname: String, port: Option<u16> },
236 Traceable,
237}
238
239impl NtpServerAddr {
240 pub fn from_hostname(hostname: impl ToString) -> NtpServerAddr {
242 NtpServerAddr::HostPort {
243 hostname: hostname.to_string(),
244 port: None,
245 }
246 }
247
248 pub fn from_hostname_with_port(hostname: impl ToString, port: u16) -> NtpServerAddr {
250 NtpServerAddr::HostPort {
251 hostname: hostname.to_string(),
252 port: Some(port),
253 }
254 }
255
256 pub fn new_traceable() -> NtpServerAddr {
258 NtpServerAddr::Traceable
259 }
260
261 pub fn builder(hostname: impl ToString) -> crate::builders::NtpServerAddr {
263 crate::builders::NtpServerAddr::new(hostname)
264 }
265}
266
267impl FromStr for NtpServerAddr {
268 type Err = AttributeError;
269
270 fn from_str(s: &str) -> Result<Self, AttributeError> {
271 if s.is_empty() {
272 return Err(AttributeError::ParamNotFound {
273 param: "NTP server address".to_string(),
274 attr: String::new(), });
276 }
277
278 if s == "/traceable/" {
279 Ok(Self::Traceable)
280 } else if let Some((hostname, port_str)) = s.rsplit_once(':') {
281 if hostname.is_empty() {
282 return Err(AttributeError::ParamNotFound {
283 param: "hostname in NTP server address".to_string(),
284 attr: String::new(), });
286 }
287
288 let port = port_str
289 .parse::<u16>()
290 .map_err(|_| AttributeError::InvalidParamValue {
291 param: "port in NTP server address".to_string(),
292 val: port_str.to_string(),
293 attr: String::new(), })?;
295
296 Ok(Self::HostPort {
297 hostname: hostname.to_string(),
298 port: Some(port),
299 })
300 } else {
301 Ok(Self::HostPort {
302 hostname: s.to_string(),
303 port: None,
304 })
305 }
306 }
307}
308
309impl fmt::Display for NtpServerAddr {
310 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 match self {
312 Self::HostPort {
313 hostname,
314 port: Some(port),
315 } => write!(f, "{hostname}:{port}"),
316 Self::HostPort {
317 hostname,
318 port: None,
319 } => write!(f, "{hostname}"),
320 Self::Traceable => write!(f, "/traceable/"),
321 }
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct Ptp {
328 pub version: PtpVersion,
329 pub server: PtpServer,
330}
331
332impl Ptp {
333 pub fn new(version: PtpVersion, server: PtpServer) -> Self {
335 Ptp { version, server }
336 }
337
338 pub fn from_gmid(version: PtpVersion, gmid: impl Into<Eui64>) -> Self {
340 Ptp {
341 version,
342 server: PtpServer::from_gmid(gmid),
343 }
344 }
345
346 pub fn from_gmid_with_domain(gmid: impl Into<Eui64>, domain: PtpDomain) -> Self {
348 let version = match domain {
349 PtpDomain::DomainName { .. } => PtpVersion::Ieee1588_2002,
350 PtpDomain::DomainNumber(_) => PtpVersion::Ieee1588_2008,
351 };
352
353 Ptp {
354 version,
355 server: PtpServer::from_gmid_with_domain(gmid, domain),
356 }
357 }
358
359 pub fn from_gmid_with_domain_name(gmid: impl Into<Eui64>, name: impl ToString) -> Self {
363 Self::from_gmid_with_domain(gmid, PtpDomain::from_name(name))
364 }
365
366 pub fn try_from_gmid_with_domain_number(
372 gmid: impl Into<Eui64>,
373 number: u8,
374 ) -> Result<Self, AttributeError> {
375 Ok(Self::from_gmid_with_domain(
376 gmid,
377 PtpDomain::try_from_number(number)?,
378 ))
379 }
380
381 pub fn new_traceable(version: PtpVersion) -> Self {
383 Ptp {
384 version,
385 server: PtpServer::new_traceable(),
386 }
387 }
388}
389
390impl From<(PtpVersion, PtpServer)> for Ptp {
391 fn from(ptp: (PtpVersion, PtpServer)) -> Self {
392 Ptp::new(ptp.0, ptp.1)
393 }
394}
395
396impl FromStr for Ptp {
397 type Err = AttributeError;
398
399 fn from_str(s: &str) -> Result<Self, AttributeError> {
400 let (version_str, s) = s
401 .split_once(':')
402 .ok_or_else(|| AttributeError::ParamNotFound {
403 param: "PTP version".to_string(),
404 attr: String::new(), })?;
406 let version = PtpVersion::from_str(version_str)?;
407
408 if s.is_empty() {
409 return Err(AttributeError::ParamNotFound {
410 param: "PTP server".to_string(),
411 attr: String::new(), });
413 }
414
415 let server = PtpServer::from_str(s)?;
416
417 Ok(Self { version, server })
418 }
419}
420
421impl fmt::Display for Ptp {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 write!(f, "{}:{}", self.version, self.server)
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
429pub enum PtpVersion {
430 Ieee1588_2002,
431 Ieee1588_2008,
432 #[allow(non_camel_case_types)]
433 Ieee8021As_2011,
434 Ext(String),
435}
436
437impl FromStr for PtpVersion {
438 type Err = AttributeError;
439
440 fn from_str(s: &str) -> Result<Self, AttributeError> {
441 match s {
442 "IEEE1588-2002" => Ok(Self::Ieee1588_2002),
443 "IEEE1588-2008" => Ok(Self::Ieee1588_2008),
444 "IEEE802.1AS-2011" => Ok(Self::Ieee8021As_2011),
445 _ => {
446 if s.is_empty() {
447 return Err(AttributeError::ParamNotFound {
448 param: "PTP version".to_string(),
449 attr: String::new(), });
451 }
452 Ok(Self::Ext(s.to_string()))
453 }
454 }
455 }
456}
457
458impl fmt::Display for PtpVersion {
459 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460 match self {
461 Self::Ieee1588_2002 => write!(f, "IEEE1588-2002"),
462 Self::Ieee1588_2008 => write!(f, "IEEE1588-2008"),
463 Self::Ieee8021As_2011 => write!(f, "IEEE802.1AS-2011"),
464 Self::Ext(v) => write!(f, "{v}"),
465 }
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq)]
471pub enum PtpServer {
472 GmidDomain {
473 gmid: Eui64,
474 domain: Option<PtpDomain>,
475 },
476 Traceable,
477}
478
479impl PtpServer {
480 pub fn from_gmid(gmid: impl Into<Eui64>) -> Self {
482 PtpServer::GmidDomain {
483 gmid: gmid.into(),
484 domain: None,
485 }
486 }
487
488 pub fn from_gmid_with_domain(gmid: impl Into<Eui64>, domain: PtpDomain) -> Self {
490 PtpServer::GmidDomain {
491 gmid: gmid.into(),
492 domain: Some(domain),
493 }
494 }
495
496 pub fn from_gmid_with_domain_name(gmid: impl Into<Eui64>, name: impl ToString) -> Self {
500 PtpServer::GmidDomain {
501 gmid: gmid.into(),
502 domain: Some(PtpDomain::from_name(name)),
503 }
504 }
505
506 pub fn try_from_ptp_gmid_with_domain_number(
510 gmid: impl Into<Eui64>,
511 number: u8,
512 ) -> Result<Self, AttributeError> {
513 Ok(PtpServer::GmidDomain {
514 gmid: gmid.into(),
515 domain: Some(PtpDomain::try_from_number(number)?),
516 })
517 }
518
519 pub fn new_traceable() -> Self {
520 PtpServer::Traceable
521 }
522}
523
524impl FromStr for PtpServer {
525 type Err = AttributeError;
526
527 fn from_str(s: &str) -> Result<Self, AttributeError> {
528 if s == "traceable" {
529 return Ok(Self::Traceable);
530 }
531
532 let (gmid_str, domain_str) = s
533 .split_once(':')
534 .map(|(gmid, domain)| (gmid, Some(domain)))
535 .unwrap_or((s, None));
536
537 let gmid = Eui64::from_str(gmid_str).map_err(|_| AttributeError::InvalidParamValue {
538 param: "PTP GMID".to_string(),
539 val: gmid_str.to_string(),
540 attr: String::new(), })?;
542
543 let domain = domain_str.map(PtpDomain::from_str).transpose()?;
544
545 Ok(Self::GmidDomain { gmid, domain })
546 }
547}
548
549impl fmt::Display for PtpServer {
550 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
551 match self {
552 Self::Traceable => write!(f, "traceable"),
553 Self::GmidDomain { gmid, domain } => {
554 if let Some(dom) = domain {
555 write!(f, "{gmid}:{dom}")
556 } else {
557 write!(f, "{gmid}")
558 }
559 }
560 }
561 }
562}
563
564#[derive(Debug, Copy, Clone, PartialEq, Eq)]
566pub struct Eui64 {
567 pub bytes: [u8; 8],
568}
569
570impl Eui64 {
571 pub fn new(id: u64) -> Self {
572 Self {
573 bytes: id.to_be_bytes(),
574 }
575 }
576
577 pub fn as_u64(&self) -> u64 {
578 u64::from_be_bytes(self.bytes)
579 }
580}
581
582impl FromStr for Eui64 {
583 type Err = AttributeError;
584
585 fn from_str(s: &str) -> Result<Self, AttributeError> {
586 if s.len() != 7 * 3 + 2 {
587 return Err(AttributeError::InvalidParamValue {
588 param: "EUI64".to_string(),
589 val: s.to_string(),
590 attr: String::new(), });
592 }
593 let mut bytes = [0u8; 8];
594 for (i, digit) in s.split('-').enumerate() {
595 if digit.len() != 2 {
596 return Err(AttributeError::InvalidParamValue {
597 param: "EUI64 segment".to_string(),
598 val: digit.to_string(),
599 attr: String::new(), });
601 }
602 bytes[i] =
603 u8::from_str_radix(digit, 16).map_err(|_| AttributeError::InvalidParamValue {
604 param: "EUI64 hex digit".to_string(),
605 val: digit.to_string(),
606 attr: String::new(), })?;
608 }
609
610 Ok(Self { bytes })
611 }
612}
613
614impl From<u64> for Eui64 {
615 fn from(id: u64) -> Self {
616 Eui64::new(id)
617 }
618}
619
620impl fmt::Display for Eui64 {
621 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
622 for byte in &self.bytes[..7] {
623 write!(f, "{:02X}-", byte)?;
624 }
625 write!(f, "{:02X}", self.bytes[7])
626 }
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
631pub enum PtpDomain {
632 DomainName {
634 name: String,
635 },
636 DomainNumber(u8),
638}
639
640impl PtpDomain {
641 pub fn from_name(name: impl ToString) -> Self {
643 PtpDomain::DomainName {
644 name: name.to_string(),
645 }
646 }
647
648 pub fn try_from_number(number: u8) -> Result<Self, AttributeError> {
652 if number > 127 {
653 return Err(AttributeError::InvalidParamValue {
654 param: "PTP domain".to_string(),
655 val: number.to_string(),
656 attr: String::new(), });
658 }
659
660 Ok(PtpDomain::DomainNumber(number))
661 }
662}
663
664impl FromStr for PtpDomain {
665 type Err = AttributeError;
666
667 fn from_str(s: &str) -> Result<Self, AttributeError> {
668 if s.len() == 16 {
673 Ok(Self::DomainName {
674 name: s.to_string(),
675 })
676 } else if let Ok(domain_num) = s.parse::<u8>() {
677 if domain_num > 127 {
678 return Err(AttributeError::InvalidParamValue {
679 param: "PTP domain".to_string(),
680 val: domain_num.to_string(),
681 attr: String::new(), });
683 }
684
685 Ok(Self::DomainNumber(domain_num))
686 } else {
687 Err(AttributeError::InvalidParamValue {
688 param: "PTP domain".to_string(),
689 val: s.to_string(),
690 attr: String::new(), })
692 }
693 }
694}
695
696impl fmt::Display for PtpDomain {
697 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
698 match self {
699 Self::DomainName { name } => write!(f, "{name}"),
700 Self::DomainNumber(n) => write!(f, "{n}"),
701 }
702 }
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707pub enum PrivateSource {
708 Standard,
709 Traceable,
710}
711
712#[derive(Debug, Clone, PartialEq, Eq)]
714pub struct ClockSourceExt {
715 pub name: String,
716 pub value: Option<String>,
717}
718
719impl ClockSourceExt {
720 pub fn new(name: impl ToString) -> Self {
722 ClockSourceExt {
723 name: name.to_string(),
724 value: None,
725 }
726 }
727
728 pub fn with_value(name: impl ToString, value: impl ToString) -> Self {
730 ClockSourceExt {
731 name: name.to_string(),
732 value: Some(value.to_string()),
733 }
734 }
735
736 pub fn set_value(&mut self, value: impl ToString) {
737 self.value = Some(value.to_string());
738 }
739
740 pub fn builder(name: impl ToString) -> crate::builders::ClockSourceExt {
742 crate::builders::ClockSourceExt::new(name)
743 }
744}
745
746impl FromStr for ClockSourceExt {
747 type Err = AttributeError;
748
749 fn from_str(s: &str) -> Result<Self, AttributeError> {
750 let (name, value) = s
751 .split_once('=')
752 .map(|(name, value)| (name, Some(value)))
753 .unwrap_or((s, None));
754 if name.is_empty() {
755 return Err(AttributeError::ParamNotFound {
756 param: "Clock source name".to_string(),
757 attr: String::new(), });
759 }
760
761 Ok(Self {
762 name: name.to_string(),
763 value: value.map(|s| s.to_string()),
764 })
765 }
766}
767
768impl fmt::Display for ClockSourceExt {
769 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
770 if let Some(ref value) = self.value {
771 write!(f, "{}={value}", self.name)
772 } else {
773 write!(f, "{}", self.name)
774 }
775 }
776}
777
778#[derive(Debug, Clone, PartialEq, Eq, Default)]
782pub struct MediaClockSource {
783 pub id: Option<MediaClockId>,
784 pub clock: MediaClock,
785}
786
787impl MediaClockSource {
788 pub fn new(clock: impl Into<MediaClock>) -> Self {
790 MediaClockSource {
791 id: None,
792 clock: clock.into(),
793 }
794 }
795
796 pub fn with_clock_id(clock: impl Into<MediaClock>, id: MediaClockId) -> Self {
798 MediaClockSource {
799 id: Some(id),
800 clock: clock.into(),
801 }
802 }
803
804 pub fn new_direct() -> Self {
806 MediaClock::new_direct().into()
807 }
808
809 pub fn from_direct_offset(offset: u32) -> Self {
811 MediaClock::from_direct_offset(offset).into()
812 }
813
814 pub fn from_direct_offset_with_id(offset: u32, id: MediaClockId) -> Self {
816 MediaClockSource {
817 id: Some(id),
818 clock: MediaClock::from_direct_offset(offset),
819 }
820 }
821
822 pub fn from_direct_rate(rate: impl Into<Rate>) -> Self {
824 MediaClock::from_direct_rate(rate).into()
825 }
826
827 pub fn from_direct_rate_with_id(rate: impl Into<Rate>, id: MediaClockId) -> Self {
829 MediaClockSource {
830 id: Some(id),
831 clock: MediaClock::from_direct_rate(rate),
832 }
833 }
834
835 pub fn from_direct_offset_and_rate(offset: u32, rate: impl Into<Rate>) -> Self {
837 MediaClock::from_direct_offset_and_rate(offset, rate).into()
838 }
839
840 pub fn from_direct_offset_and_rate_with_id(
842 offset: u32,
843 rate: impl Into<Rate>,
844 id: MediaClockId,
845 ) -> Self {
846 MediaClockSource {
847 id: Some(id),
848 clock: MediaClock::from_direct_offset_and_rate(offset, rate),
849 }
850 }
851
852 pub fn from_ieee1722_stream_id(ieee1722_stream_id: Eui64) -> Self {
854 MediaClock::from_ieee1722_stream_id(ieee1722_stream_id).into()
855 }
856
857 pub fn from_ieee1722_stream_id_with_id(ieee1722_stream_id: Eui64, id: MediaClockId) -> Self {
859 MediaClockSource {
860 id: Some(id),
861 clock: MediaClock::from_ieee1722_stream_id(ieee1722_stream_id),
862 }
863 }
864
865 pub fn from_extended_name(name: impl ToString) -> Self {
867 MediaClock::from_extended_name(name).into()
868 }
869
870 pub fn from_extended_name_with_id(name: impl ToString, id: MediaClockId) -> Self {
872 MediaClockSource {
873 id: Some(id),
874 clock: MediaClock::from_extended_name(name),
875 }
876 }
877
878 pub fn from_extended_name_with_value(name: impl ToString, value: impl ToString) -> Self {
880 MediaClock::from_extended_name_with_value(name, value).into()
881 }
882
883 pub fn from_extended_name_with_value_with_id(
885 name: impl ToString,
886 value: impl ToString,
887 id: MediaClockId,
888 ) -> Self {
889 MediaClockSource {
890 id: Some(id),
891 clock: MediaClock::from_extended_name_with_value(name, value),
892 }
893 }
894
895 pub fn set_id(&mut self, id: MediaClockId) {
896 self.id = Some(id);
897 }
898
899 pub fn builder(clock: impl Into<MediaClock>) -> crate::builders::MediaClockSource {
901 crate::builders::MediaClockSource::new(clock)
902 }
903}
904
905impl<T: Into<MediaClock>> From<T> for MediaClockSource {
906 fn from(media_clock: T) -> Self {
907 MediaClockSource {
908 id: None,
909 clock: media_clock.into(),
910 }
911 }
912}
913
914impl FromStr for MediaClockSource {
915 type Err = AttributeError;
916
917 fn from_str(mut s: &str) -> Result<Self, AttributeError> {
918 let mut id = None;
919 if let Some(id_str) = s.strip_prefix("id=") {
920 let (id_str, rest) = id_str.split_once(' ').unwrap();
921 id = Some(MediaClockId::from_str(id_str).with_attr(<Self as TypedAttribute>::NAME)?);
922
923 s = rest;
924 }
925
926 let clock = MediaClock::from_str(s).with_attr(<Self as TypedAttribute>::NAME)?;
927
928 Ok(Self { id, clock })
929 }
930}
931
932impl fmt::Display for MediaClockSource {
933 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
934 if let Some(ref id) = self.id {
935 write!(f, "id={id} {}", self.clock)?;
936 } else {
937 write!(f, "{}", self.clock)?;
938 }
939 Ok(())
940 }
941}
942
943impl TypedAttribute for MediaClockSource {
944 const NAME: &'static str = "mediaclk";
945}
946
947#[derive(Debug, Clone, Default, PartialEq, Eq)]
949pub enum MediaClock {
950 #[default]
951 Sender,
952 Direct(Direct),
953 Ieee1722StreamId(Eui64),
954 Ext(MediaClockExt),
955}
956
957impl MediaClock {
958 pub fn new_direct() -> Self {
960 MediaClock::Direct(Direct::new())
961 }
962
963 pub fn from_direct_offset(offset: u32) -> Self {
965 MediaClock::Direct(Direct::with_offset(offset))
966 }
967
968 pub fn from_direct_rate(rate: impl Into<Rate>) -> Self {
970 MediaClock::Direct(Direct::with_rate(rate))
971 }
972
973 pub fn from_direct_offset_and_rate(offset: u32, rate: impl Into<Rate>) -> Self {
975 MediaClock::Direct(Direct::with_offset_and_rate(offset, rate))
976 }
977
978 pub fn from_ieee1722_stream_id(ieee1722_stream_id: Eui64) -> Self {
980 MediaClock::Ieee1722StreamId(ieee1722_stream_id)
981 }
982
983 pub fn from_extended_name(name: impl ToString) -> Self {
985 MediaClock::Ext(MediaClockExt::new(name))
986 }
987
988 pub fn from_extended_name_with_value(name: impl ToString, value: impl ToString) -> Self {
990 MediaClock::Ext(MediaClockExt::with_value(name, value))
991 }
992}
993
994impl From<Direct> for MediaClock {
995 fn from(direct: Direct) -> Self {
996 MediaClock::Direct(direct)
997 }
998}
999
1000impl From<Eui64> for MediaClock {
1001 fn from(ieee1722_stream_id: Eui64) -> Self {
1002 MediaClock::Ieee1722StreamId(ieee1722_stream_id)
1003 }
1004}
1005
1006impl From<MediaClockExt> for MediaClock {
1007 fn from(media_clock_ext: MediaClockExt) -> Self {
1008 MediaClock::Ext(media_clock_ext)
1009 }
1010}
1011
1012impl FromStr for MediaClock {
1013 type Err = AttributeError;
1014
1015 fn from_str(s: &str) -> Result<Self, AttributeError> {
1016 if s == "sender" {
1017 Ok(Self::Sender)
1018 } else if let Some(s) = s.strip_prefix("direct") {
1019 Ok(Self::Direct(
1020 Direct::from_str(s).with_param_context("direct")?,
1021 ))
1022 } else if let Some(s) = s.strip_prefix("IEEE1722=") {
1023 let gmid = Eui64::from_str(s).with_param_context("IEEE1722 stream ID")?;
1024 Ok(Self::Ieee1722StreamId(gmid))
1025 } else {
1026 Ok(Self::Ext(MediaClockExt::from_str(s)?))
1027 }
1028 }
1029}
1030
1031impl fmt::Display for MediaClock {
1032 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1033 match self {
1034 Self::Sender => write!(f, "sender"),
1035 Self::Direct(Direct { offset, rate }) => {
1036 if let Some(offset) = offset {
1037 write!(f, "direct={offset}")?;
1038 } else {
1039 write!(f, "direct")?;
1040 }
1041 if let Some(Rate {
1042 numerator,
1043 denominator,
1044 }) = rate
1045 {
1046 write!(f, " rate={numerator}/{denominator}")?;
1047 }
1048 Ok(())
1049 }
1050 Self::Ieee1722StreamId(gmid) => write!(f, "IEEE1722={gmid}"),
1051 Self::Ext(ext) => {
1052 if let Some(ref value) = ext.value {
1053 write!(f, "{}={value}", ext.name)
1054 } else {
1055 write!(f, "{}", ext.name)
1056 }
1057 }
1058 }
1059 }
1060}
1061
1062#[derive(Debug, Clone, PartialEq, Eq)]
1064pub struct MediaClockId {
1065 pub src: bool,
1066 pub tag: String,
1067}
1068
1069impl MediaClockId {
1070 pub fn new(tag: impl ToString) -> Self {
1072 MediaClockId {
1073 src: false,
1074 tag: tag.to_string(),
1075 }
1076 }
1077
1078 pub fn new_with_src_prefix(tag: impl ToString) -> Self {
1080 MediaClockId {
1081 src: true,
1082 tag: tag.to_string(),
1083 }
1084 }
1085
1086 pub fn set_src_prefix(&mut self, src: bool) {
1087 self.src = src;
1088 }
1089}
1090
1091impl FromStr for MediaClockId {
1092 type Err = AttributeError;
1093
1094 fn from_str(s: &str) -> Result<Self, AttributeError> {
1095 let (src, tag_str) = if let Some(s) = s.strip_prefix("src:") {
1096 (true, s)
1097 } else {
1098 (false, s)
1099 };
1100
1101 if tag_str.is_empty() {
1102 return Err(AttributeError::ParamNotFound {
1103 param: "media clock tag".to_string(),
1104 attr: String::new(), });
1106 }
1107
1108 Ok(Self {
1109 src,
1110 tag: tag_str.to_string(),
1111 })
1112 }
1113}
1114
1115impl fmt::Display for MediaClockId {
1116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1117 if self.src {
1118 write!(f, "src:{}", self.tag)
1119 } else {
1120 write!(f, "{}", self.tag)
1121 }
1122 }
1123}
1124
1125#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
1127pub struct Rate {
1128 pub numerator: u32,
1129 pub denominator: u32,
1130}
1131
1132impl Rate {
1133 pub fn new(numerator: u32, denominator: u32) -> Self {
1134 Rate {
1135 numerator,
1136 denominator,
1137 }
1138 }
1139
1140 pub fn is_one(&self) -> bool {
1142 self.numerator == 1 && self.denominator == 1
1143 }
1144}
1145
1146impl From<(u32, u32)> for Rate {
1147 fn from(rate: (u32, u32)) -> Self {
1148 Rate {
1149 numerator: rate.0,
1150 denominator: rate.1,
1151 }
1152 }
1153}
1154
1155#[derive(Debug, Clone, PartialEq, Eq, Default)]
1157pub struct Direct {
1158 pub offset: Option<u32>,
1159 pub rate: Option<Rate>,
1160}
1161
1162impl Direct {
1163 pub fn new() -> Self {
1165 Direct {
1166 offset: None,
1167 rate: None,
1168 }
1169 }
1170
1171 pub fn with_offset(offset: u32) -> Self {
1173 Direct {
1174 offset: Some(offset),
1175 rate: None,
1176 }
1177 }
1178
1179 pub fn set_offset(&mut self, offset: u32) {
1180 self.offset = Some(offset);
1181 }
1182
1183 pub fn with_rate(rate: impl Into<Rate>) -> Self {
1185 Direct {
1186 offset: None,
1187 rate: Some(rate.into()),
1188 }
1189 }
1190
1191 pub fn set_rate(&mut self, rate: impl Into<Rate>) {
1192 self.rate = Some(rate.into());
1193 }
1194
1195 pub fn with_offset_and_rate(offset: u32, rate: impl Into<Rate>) -> Self {
1197 Direct {
1198 offset: Some(offset),
1199 rate: Some(rate.into()),
1200 }
1201 }
1202
1203 pub fn builder() -> crate::builders::Direct {
1205 crate::builders::Direct::new()
1206 }
1207}
1208
1209impl FromStr for Direct {
1210 type Err = AttributeError;
1211
1212 fn from_str(s: &str) -> Result<Self, AttributeError> {
1213 if s.is_empty() {
1214 return Ok(Self {
1215 offset: None,
1216 rate: None,
1217 });
1218 }
1219
1220 let (offset, s) = if let Some(s) = s.strip_prefix('=') {
1221 let (offset_str, s) = s.split_once(' ').unwrap_or((s, ""));
1222
1223 let offset =
1224 offset_str
1225 .parse::<u32>()
1226 .map_err(|_| AttributeError::InvalidParamValue {
1227 param: "offset".to_string(),
1228 val: offset_str.to_string(),
1229 attr: String::new(), })?;
1231
1232 (Some(offset), s)
1233 } else {
1234 (None, s.strip_prefix(' ').unwrap_or(""))
1235 };
1236
1237 let rate = if s.is_empty() {
1238 None
1239 } else if let Some(s) = s.strip_prefix("rate=") {
1240 let (num_str, den_str) =
1241 s.split_once('/')
1242 .ok_or_else(|| AttributeError::InvalidParamValue {
1243 param: "rate format".to_string(),
1244 val: s.to_string(),
1245 attr: String::new(), })?;
1247
1248 let numerator =
1249 num_str
1250 .parse::<u32>()
1251 .map_err(|_| AttributeError::InvalidParamValue {
1252 param: "rate numerator".to_string(),
1253 val: num_str.to_string(),
1254 attr: String::new(), })?;
1256 let denominator =
1257 den_str
1258 .parse::<u32>()
1259 .map_err(|_| AttributeError::InvalidParamValue {
1260 param: "rate denominator".to_string(),
1261 val: den_str.to_string(),
1262 attr: String::new(), })?;
1264
1265 Some(Rate {
1266 numerator,
1267 denominator,
1268 })
1269 } else {
1270 return Err(AttributeError::InvalidParamValue {
1271 param: "direct clock parameters".to_string(),
1272 val: s.to_string(),
1273 attr: String::new(), });
1275 };
1276
1277 Ok(Self { offset, rate })
1278 }
1279}
1280
1281#[derive(Debug, Clone, PartialEq, Eq)]
1283pub struct MediaClockExt {
1284 pub name: String,
1285 pub value: Option<String>,
1286}
1287
1288impl MediaClockExt {
1289 pub fn new(name: impl ToString) -> Self {
1291 MediaClockExt {
1292 name: name.to_string(),
1293 value: None,
1294 }
1295 }
1296
1297 pub fn with_value(name: impl ToString, value: impl ToString) -> Self {
1299 MediaClockExt {
1300 name: name.to_string(),
1301 value: Some(value.to_string()),
1302 }
1303 }
1304
1305 pub fn set_value(&mut self, value: impl ToString) {
1306 self.value = Some(value.to_string());
1307 }
1308
1309 pub fn builder(name: impl ToString) -> crate::builders::MediaClockExt {
1311 crate::builders::MediaClockExt::new(name)
1312 }
1313}
1314
1315impl FromStr for MediaClockExt {
1316 type Err = AttributeError;
1317
1318 fn from_str(s: &str) -> Result<Self, AttributeError> {
1319 let (name, value) = s
1320 .split_once('=')
1321 .map(|(name, value)| (name, Some(value)))
1322 .unwrap_or((s, None));
1323
1324 if name.is_empty() {
1325 return Err(AttributeError::ParamNotFound {
1326 param: "clock source name".to_string(),
1327 attr: String::new(), });
1329 }
1330
1331 Ok(Self {
1332 name: name.to_string(),
1333 value: value.map(|s| s.to_string()),
1334 })
1335 }
1336}
1337
1338impl fmt::Display for MediaClockExt {
1339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1340 if let Some(ref value) = self.value {
1341 write!(f, "{}={value}", self.name)
1342 } else {
1343 write!(f, "{}", self.name)
1344 }
1345 }
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350 use super::*;
1351
1352 #[test]
1353 fn test_reference_clock() {
1354 let test_cases = [
1355 (
1356 "ntp=/traceable/",
1357 ReferenceClock::Ntp(Ntp {
1358 server: NtpServerAddr::Traceable,
1359 }),
1360 ),
1361 ("local", ReferenceClock::Local),
1362 (
1363 "ntp=203.0.113.10",
1364 ReferenceClock::Ntp(Ntp {
1365 server: NtpServerAddr::HostPort {
1366 hostname: "203.0.113.10".to_string(),
1367 port: None,
1368 },
1369 }),
1370 ),
1371 (
1372 "ntp=ntp.example.com:123",
1373 ReferenceClock::Ntp(Ntp {
1374 server: NtpServerAddr::HostPort {
1375 hostname: "ntp.example.com".to_string(),
1376 port: Some(123),
1377 },
1378 }),
1379 ),
1380 (
1381 "ntp=[::1]:123",
1382 ReferenceClock::Ntp(Ntp {
1383 server: NtpServerAddr::HostPort {
1384 hostname: "[::1]".to_string(),
1385 port: Some(123),
1386 },
1387 }),
1388 ),
1389 (
1390 "ptp=IEEE802.1AS-2011:39-A7-94-FF-FE-07-CB-D0",
1391 ReferenceClock::Ptp(Ptp {
1392 version: PtpVersion::Ieee8021As_2011,
1393 server: PtpServer::GmidDomain {
1394 gmid: Eui64 {
1395 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1396 },
1397 domain: None,
1398 },
1399 }),
1400 ),
1401 (
1402 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:0",
1403 ReferenceClock::Ptp(Ptp {
1404 version: PtpVersion::Ieee1588_2008,
1405 server: PtpServer::GmidDomain {
1406 gmid: Eui64 {
1407 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1408 },
1409 domain: Some(PtpDomain::DomainNumber(0)),
1410 },
1411 }),
1412 ),
1413 (
1414 "ptp=IEEE1588-2002:39-A7-94-FF-FE-07-CB-D0:testdomain123456",
1415 ReferenceClock::Ptp(Ptp {
1416 version: PtpVersion::Ieee1588_2002,
1417 server: PtpServer::GmidDomain {
1418 gmid: Eui64 {
1419 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1420 },
1421 domain: Some(PtpDomain::DomainName {
1422 name: "testdomain123456".to_string(),
1423 }),
1424 },
1425 }),
1426 ),
1427 (
1428 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:127",
1429 ReferenceClock::Ptp(Ptp {
1430 version: PtpVersion::Ieee1588_2008,
1431 server: PtpServer::GmidDomain {
1432 gmid: Eui64 {
1433 bytes: [0x39, 0xA7, 0x94, 0xFF, 0xFE, 0x07, 0xCB, 0xD0],
1434 },
1435 domain: Some(PtpDomain::DomainNumber(127)),
1436 },
1437 }),
1438 ),
1439 ("private", ReferenceClock::Private(PrivateSource::Standard)),
1440 (
1441 "private:traceable",
1442 ReferenceClock::Private(PrivateSource::Traceable),
1443 ),
1444 ("gps", ReferenceClock::Gps),
1445 ("gal", ReferenceClock::Gal),
1446 ("glonass", ReferenceClock::Glonass),
1447 ];
1448
1449 for (input, expected) in test_cases {
1450 println!("Testing: {input}");
1451 let parsed = match input.parse::<ReferenceClock>() {
1452 Ok(parsed) => parsed,
1453 Err(err) => {
1454 unreachable!("Failed to parse '{}': {}", input, err);
1455 }
1456 };
1457
1458 assert_eq!(parsed, expected, "Parse mismatch for: {input}");
1459
1460 let roundtrip = expected.to_string();
1461 assert_eq!(roundtrip, input, "Round-trip failed for: {input}");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_reference_clock_invalid() {
1467 let invalid_cases = vec![
1468 (
1469 "",
1470 AttributeError::ParamNotFound {
1471 param: "Clock source name".to_string(),
1472 attr: "ts-refclk".to_string(),
1473 },
1474 ),
1475 (
1476 "ntp=",
1477 AttributeError::ParamNotFound {
1478 param: "NTP server address".to_string(),
1479 attr: "ts-refclk".to_string(),
1480 },
1481 ),
1482 (
1483 "ntp=:123",
1484 AttributeError::ParamNotFound {
1485 param: "hostname in NTP server address".to_string(),
1486 attr: "ts-refclk".to_string(),
1487 },
1488 ),
1489 (
1490 "ptp=:39-A7-94-FF-FE-07-CB-D0",
1491 AttributeError::ParamNotFound {
1492 param: "PTP version".to_string(),
1493 attr: "ts-refclk".to_string(),
1494 },
1495 ),
1496 (
1497 "ptp=IEEE1588-2008:",
1498 AttributeError::ParamNotFound {
1499 param: "PTP server".to_string(),
1500 attr: "ts-refclk".to_string(),
1501 },
1502 ),
1503 (
1504 "ptp=IEEE1588-2008:invalid-eui64",
1505 AttributeError::InvalidParamValue {
1506 param: "PTP GMID".to_string(),
1507 val: "invalid-eui64".to_string(),
1508 attr: "ts-refclk".to_string(),
1509 },
1510 ),
1511 (
1512 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:tooshortname",
1513 AttributeError::InvalidParamValue {
1514 param: "PTP domain".to_string(),
1515 val: "tooshortname".to_string(),
1516 attr: "ts-refclk".to_string(),
1517 },
1518 ),
1519 (
1520 "ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:128",
1521 AttributeError::InvalidParamValue {
1522 param: "PTP domain".to_string(),
1523 val: "128".to_string(),
1524 attr: "ts-refclk".to_string(),
1525 },
1526 ),
1527 ];
1528
1529 for (input, expected_err) in invalid_cases {
1530 println!("Testing invalid: {input}");
1531 assert_eq!(input.parse::<ReferenceClock>(), Err(expected_err));
1532 }
1533 }
1534
1535 #[test]
1536 fn test_media_clock_source() {
1537 let test_cases = vec![
1538 (
1539 "direct",
1540 MediaClockSource {
1541 id: None,
1542 clock: MediaClock::Direct(Direct {
1543 offset: None,
1544 rate: None,
1545 }),
1546 },
1547 ),
1548 (
1549 "direct=963214424",
1550 MediaClockSource {
1551 id: None,
1552 clock: MediaClock::Direct(Direct {
1553 offset: Some(963214424),
1554 rate: None,
1555 }),
1556 },
1557 ),
1558 (
1559 "direct=963214424 rate=1000/1001",
1560 MediaClockSource {
1561 id: None,
1562 clock: MediaClock::Direct(Direct {
1563 offset: Some(963214424),
1564 rate: Some(Rate::new(1000, 1001)),
1565 }),
1566 },
1567 ),
1568 (
1569 "direct rate=1000/1001",
1570 MediaClockSource {
1571 id: None,
1572 clock: MediaClock::Direct(Direct {
1573 offset: None,
1574 rate: Some(Rate::new(1000, 1001)),
1575 }),
1576 },
1577 ),
1578 (
1579 "sender",
1580 MediaClockSource {
1581 id: None,
1582 clock: MediaClock::Sender,
1583 },
1584 ),
1585 (
1586 "IEEE1722=38-D6-6D-8E-D2-78-13-2F",
1587 MediaClockSource {
1588 id: None,
1589 clock: MediaClock::Ieee1722StreamId(Eui64 {
1590 bytes: [0x38, 0xD6, 0x6D, 0x8E, 0xD2, 0x78, 0x13, 0x2F],
1591 }),
1592 },
1593 ),
1594 (
1595 "id=MDA6NjA6MmI6MjA6MTI6MWY= sender",
1596 MediaClockSource {
1597 id: Some(MediaClockId {
1598 src: false,
1599 tag: "MDA6NjA6MmI6MjA6MTI6MWY=".to_string(),
1600 }),
1601 clock: MediaClock::Sender,
1602 },
1603 ),
1604 (
1605 "id=src:MDA6NjA6MmI6MjA6MTI6MWY= direct",
1606 MediaClockSource {
1607 id: Some(MediaClockId {
1608 src: true,
1609 tag: "MDA6NjA6MmI6MjA6MTI6MWY=".to_string(),
1610 }),
1611 clock: MediaClock::Direct(Direct {
1612 offset: None,
1613 rate: None,
1614 }),
1615 },
1616 ),
1617 ];
1618
1619 for (input, expected) in test_cases {
1620 println!("Testing: {input}");
1621 let parsed = match input.parse::<MediaClockSource>() {
1622 Ok(parsed) => parsed,
1623 Err(err) => {
1624 unreachable!("Failed to parse '{}': {}", input, err);
1625 }
1626 };
1627
1628 assert_eq!(parsed, expected, "Parse mismatch for: {input}");
1629
1630 let roundtrip = expected.to_string();
1631 assert_eq!(roundtrip, input, "Round-trip failed for: {input}");
1632 }
1633 }
1634
1635 #[test]
1636 fn test_media_clock_source_invalid() {
1637 let invalid_cases = vec![
1638 (
1639 "",
1640 AttributeError::ParamNotFound {
1641 param: "clock source name".to_string(),
1642 attr: "mediaclk".to_string(),
1643 },
1644 ),
1645 (
1646 "direct=notanumber",
1647 AttributeError::InvalidParamValue {
1648 param: "direct (offset)".to_string(),
1649 val: "notanumber".to_string(),
1650 attr: "mediaclk".to_string(),
1651 },
1652 ),
1653 (
1654 "direct=count rate=100/abc",
1655 AttributeError::InvalidParamValue {
1656 param: "direct (offset)".to_string(),
1657 val: "count".to_string(),
1658 attr: "mediaclk".to_string(),
1659 },
1660 ),
1661 (
1662 "IEEE1722=invalid-eui64",
1663 AttributeError::InvalidParamValue {
1664 param: "IEEE1722 stream ID (EUI64)".to_string(),
1665 val: "invalid-eui64".to_string(),
1666 attr: "mediaclk".to_string(),
1667 },
1668 ),
1669 (
1670 "id= sender",
1671 AttributeError::ParamNotFound {
1672 param: "media clock tag".to_string(),
1673 attr: "mediaclk".to_string(),
1674 },
1675 ),
1676 ];
1677
1678 for (input, expected_err) in invalid_cases {
1679 println!("Testing invalid: {input}");
1680 assert_eq!(input.parse::<MediaClockSource>(), Err(expected_err));
1681 }
1682 }
1683
1684 #[test]
1685 fn parse_clock_signalling_attr() {
1686 use crate::{Ssrc, SsrcAttribute};
1687
1688 let sdp = "v=0\n\
1689 m=image 54111 TCP t38\n\
1690 c=IN IP4 192.0.2.2\n\
1691 a=ts-refclk:local\n\
1692 a=mediaclk:direct=654321\n\
1693 a=ssrc:1234 ts-refclk:ntp=pool.ntp.org\n\
1694 a=ssrc:1234 mediaclk:direct=987654\n\
1695 ";
1696
1697 let medias = crate::Session::parse(sdp.as_bytes()).unwrap().medias;
1698 assert_eq!(
1699 medias[0].get_first_attribute_typed::<ReferenceClock>(),
1700 Some(Ok(ReferenceClock::Local))
1701 );
1702 assert_eq!(
1703 medias[0].get_first_attribute_typed::<MediaClockSource>(),
1704 Some(Ok(MediaClockSource {
1705 id: None,
1706 clock: MediaClock::Direct(Direct {
1707 offset: Some(654321),
1708 rate: None
1709 })
1710 })),
1711 );
1712
1713 let ssrc_attrs = medias[0].attributes_typed::<Ssrc>();
1714 for ssrc_attr in ssrc_attrs {
1715 let ssrc_attr = ssrc_attr.unwrap();
1716 match ssrc_attr.attribute {
1717 SsrcAttribute::ReferenceClock => {
1718 let ssrc_refclk = ssrc_attr.get_typed::<ReferenceClock>().unwrap();
1719 assert_eq!(
1720 ssrc_refclk,
1721 ReferenceClock::Ntp(Ntp {
1722 server: NtpServerAddr::HostPort {
1723 hostname: "pool.ntp.org".to_string(),
1724 port: None,
1725 }
1726 })
1727 );
1728 }
1729 SsrcAttribute::MediaClockSource => {
1730 let ssrc_mediaclk = ssrc_attr.get_typed::<MediaClockSource>().unwrap();
1731 assert_eq!(
1732 ssrc_mediaclk,
1733 MediaClockSource {
1734 id: None,
1735 clock: MediaClock::Direct(Direct {
1736 offset: Some(987654),
1737 rate: None
1738 })
1739 }
1740 );
1741 }
1742 other => unreachable!("{:?}", other),
1743 }
1744 }
1745 }
1746
1747 #[test]
1748 fn build_clock_signalling_attr() {
1749 use crate::{Media, MediaType, TransportProto};
1750
1751 const NTP_SERVER_ADDR: &str = "pool.ntp.org";
1752
1753 let media = Media::builder(MediaType::Audio, 5000, TransportProto::RtpAvp, 96)
1754 .attribute(ReferenceClock::Local)
1755 .attribute(ReferenceClock::from_ntp_hostname(NTP_SERVER_ADDR))
1756 .build();
1757
1758 let mut refclk_iter = media.attributes_typed::<ReferenceClock>();
1759 assert_eq!(refclk_iter.next(), Some(Ok(ReferenceClock::Local)));
1760 assert_eq!(
1761 refclk_iter.next(),
1762 Some(Ok(ReferenceClock::Ntp(Ntp {
1763 server: NtpServerAddr::HostPort {
1764 hostname: NTP_SERVER_ADDR.to_string(),
1765 port: None
1766 }
1767 })))
1768 );
1769 }
1770}