use std::{net::IpAddr, str::FromStr};
use bstr::*;
use fallible_iterator::FallibleIterator;
pub mod attributes;
pub mod builders;
pub mod clock_signalling;
pub mod enums;
mod parser;
mod writer;
pub use attributes::*;
pub use clock_signalling::*;
pub use enums::*;
pub use parser::ParserError;
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Origin {
pub username: Option<String>,
pub sess_id: String,
pub sess_version: u64,
pub nettype: NetType,
pub addrtype: AddrType,
pub unicast_address: String,
}
impl Origin {
pub fn with_ip_addr(
sess_id: impl ToString,
sess_version: u64,
unicast_address: impl Into<IpAddr>,
) -> Self {
let unicast_address = unicast_address.into();
Origin {
username: Default::default(),
sess_id: sess_id.to_string(),
sess_version,
nettype: NetType::In,
addrtype: unicast_address.into(),
unicast_address: unicast_address.to_string(),
}
}
pub fn new(
sess_id: impl ToString,
sess_version: u64,
nettype: NetType,
addrtype: AddrType,
unicast_address: impl ToString,
) -> Self {
Origin {
username: Default::default(),
sess_id: sess_id.to_string(),
sess_version,
nettype,
addrtype,
unicast_address: unicast_address.to_string(),
}
}
pub fn set_unicast_ip_address(&mut self, unicast_address: impl Into<IpAddr>) {
let unicast_address = unicast_address.into();
self.addrtype = unicast_address.into();
self.unicast_address = unicast_address.to_string();
}
pub fn try_parse_unicast_ip_address(&self) -> Result<IpAddr, &str> {
self.unicast_address
.parse::<IpAddr>()
.map_err(|_| self.unicast_address.as_str())
}
pub fn set_username(&mut self, username: impl ToString) {
self.username = Some(username.to_string());
}
pub fn builder_with_ip_addr(
sess_id: impl ToString,
sess_version: u64,
unicast_address: impl Into<IpAddr>,
) -> builders::Origin {
builders::Origin::with_ip_addr(sess_id, sess_version, unicast_address)
}
pub fn builder(
sess_id: impl ToString,
sess_version: u64,
nettype: NetType,
addrtype: AddrType,
unicast_address: impl ToString,
) -> builders::Origin {
builders::Origin::new(sess_id, sess_version, nettype, addrtype, unicast_address)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Connection {
pub nettype: NetType,
pub addrtype: AddrType,
pub connection_address: String,
}
impl Connection {
pub fn from_ip_addr(connection_address: impl Into<IpAddr>) -> Self {
Self::from_ip_addr_ttl(connection_address, None)
}
pub fn from_ip_addr_ttl(connection_address: impl Into<IpAddr>, ttl: Option<u8>) -> Self {
let ip = connection_address.into();
let connection_address = match ttl {
Some(ttl) if ip.is_ipv4() => format!("{ip}/{ttl}"),
_ => ip.to_string(),
};
Connection {
nettype: NetType::In,
addrtype: ip.into(),
connection_address,
}
}
pub fn new(nettype: NetType, addrtype: AddrType, connection_address: impl ToString) -> Self {
Connection {
nettype,
addrtype,
connection_address: connection_address.to_string(),
}
}
pub fn set_connection_ip_address(&mut self, connection_address: impl Into<IpAddr>) {
let connection_address = connection_address.into();
self.addrtype = connection_address.into();
self.connection_address = connection_address.to_string();
}
#[deprecated(since = "0.3.1", note = "use Connection::try_address instead")]
pub fn try_parse_connection_ip_address(&self) -> Result<IpAddr, &str> {
parse_connection_ip_address(&self.connection_address)
}
pub fn try_address(&self) -> Result<ConnectionAddress, ConnectionAddressError> {
parse_connection_address(&self.connection_address, &self.addrtype)
}
pub fn is_multicast(&self) -> bool {
parse_connection_ip_address(&self.connection_address)
.map(|address| address.is_multicast())
.unwrap_or(false)
}
pub fn try_is_multicast(&self) -> Result<bool, ConnectionAddressError> {
parse_connection_ip_address(&self.connection_address)
.map(|address| address.is_multicast())
.map_err(|_| ConnectionAddressError::InvalidAddress(self.connection_address.clone()))
}
pub fn is_ipv4(&self) -> bool {
parse_connection_ip_address(&self.connection_address)
.map(|address| address.is_ipv4())
.unwrap_or(false)
}
pub fn is_ipv6(&self) -> bool {
parse_connection_ip_address(&self.connection_address)
.map(|address| address.is_ipv6())
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConnectionAddress {
pub ip: IpAddr,
pub ttl: Option<u8>,
pub count: Option<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ConnectionAddressError {
#[error("invalid connection address '{0}'")]
InvalidAddress(String),
#[error("invalid time-to-live in connection address '{0}'")]
InvalidTtl(String),
#[error("invalid address count in connection address '{0}'")]
InvalidCount(String),
}
pub(crate) fn parse_connection_address(
address: &str,
addrtype: &AddrType,
) -> Result<ConnectionAddress, ConnectionAddressError> {
let ip = parse_connection_ip_address(address)
.map_err(|_| ConnectionAddressError::InvalidAddress(address.to_string()))?;
let ttl = match parse_ttl(address, addrtype) {
Some(Ok(ttl)) => Some(ttl),
Some(Err(_)) => return Err(ConnectionAddressError::InvalidTtl(address.to_string())),
None => None,
};
let count = match parse_number_of_addresses(address, addrtype) {
Some(Ok(count)) => Some(count),
Some(Err(_)) => return Err(ConnectionAddressError::InvalidCount(address.to_string())),
None => None,
};
Ok(ConnectionAddress { ip, ttl, count })
}
pub(crate) fn parse_connection_ip_address(address: &str) -> Result<IpAddr, &str> {
let ip_part = match address.split_once('/') {
Some((ip_part, _suffix)) => ip_part,
None => address,
};
ip_part.parse::<IpAddr>().map_err(|_| address)
}
pub(crate) fn parse_ttl<'a>(address: &'a str, addrtype: &AddrType) -> Option<Result<u8, &'a str>> {
if addrtype != &AddrType::Ip4 {
return None;
}
let rest = address.split_once('/')?.1;
let ttl = match rest.split_once('/') {
Some((ttl, _count)) => ttl,
None => rest,
};
Some(ttl.parse::<u8>().map_err(|_| ttl))
}
pub(crate) fn parse_number_of_addresses<'a>(
address: &'a str,
addrtype: &AddrType,
) -> Option<Result<u8, &'a str>> {
let rest = address.split_once('/')?.1;
let count = match addrtype {
AddrType::Ip4 => rest.split_once('/')?.1,
AddrType::Ip6 => rest,
AddrType::Other(_) => return None,
};
Some(count.parse::<u8>().map_err(|_| count))
}
impl<T: Into<IpAddr>> From<T> for Connection {
fn from(ip_addr: T) -> Self {
Connection::from_ip_addr(ip_addr)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bandwidth {
pub bwtype: String,
pub bandwidth: u64,
}
impl Bandwidth {
pub fn new(bwtype: BandwidthType, bandwidth: u64) -> Self {
Bandwidth {
bwtype: bwtype.to_string(),
bandwidth,
}
}
pub fn try_parse_bwtype(&self) -> Result<BandwidthType, ParseEnumError> {
BandwidthType::from_str(self.bwtype.as_str())
}
pub fn set_bwtype(&mut self, bwtype: BandwidthType) {
self.bwtype = bwtype.to_string();
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Time {
pub start_time: u64,
pub stop_time: u64,
pub repeats: Vec<Repeat>,
}
impl Time {
pub fn new(start_time: u64, stop_time: u64) -> Self {
Time {
start_time,
stop_time,
repeats: Default::default(),
}
}
pub fn add_repeat(&mut self, repeat: Repeat) {
self.repeats.push(repeat);
}
pub fn add_repeats(&mut self, repeats: impl IntoIterator<Item = Repeat>) {
self.repeats.extend(repeats)
}
pub fn builder(start_time: u64, stop_time: u64) -> builders::Time {
builders::Time::new(start_time, stop_time)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Repeat {
pub repeat_interval: u64,
pub active_duration: u64,
pub offsets: Vec<u64>,
}
impl Repeat {
pub fn new(repeat_interval: u64, active_duration: u64) -> Self {
Repeat {
repeat_interval,
active_duration,
offsets: Default::default(),
}
}
pub fn add_offset(&mut self, offset: u64) {
self.offsets.push(offset);
}
pub fn add_offsets(&mut self, offsets: impl IntoIterator<Item = u64>) {
self.offsets.extend(offsets)
}
pub fn builder(repeat_interval: u64, active_duration: u64) -> builders::Repeat {
builders::Repeat::new(repeat_interval, active_duration)
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TimeZone {
pub adjustment_time: u64,
pub offset: i64,
}
impl TimeZone {
pub fn new(adjustment_time: u64, offset: i64) -> Self {
TimeZone {
adjustment_time,
offset,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Key {
pub method: String,
pub encryption_key: Option<String>,
}
impl Key {
pub fn new(method: KeyMethod) -> Self {
Key {
method: method.to_string(),
encryption_key: Default::default(),
}
}
pub fn with_encryption_key(method: KeyMethod, encryption_key: impl ToString) -> Self {
Key {
method: method.to_string(),
encryption_key: Some(encryption_key.to_string()),
}
}
pub fn try_parse_keymethod(&self) -> Result<KeyMethod, ParseEnumError> {
KeyMethod::from_str(self.method.as_str())
}
pub fn set_keymethod(&mut self, method: KeyMethod) {
self.method = method.to_string();
}
}
impl From<KeyMethod> for Key {
fn from(method: KeyMethod) -> Self {
Key {
method: method.to_string(),
encryption_key: None,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Attribute {
pub attribute: String,
pub value: Option<String>,
}
impl Attribute {
pub fn new(attribute: impl ToString) -> Self {
Attribute {
attribute: attribute.to_string(),
value: Default::default(),
}
}
pub fn with_value(attribute: impl ToString, value: impl ToString) -> Self {
Attribute {
attribute: attribute.to_string(),
value: Some(value.to_string()),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Media {
pub media: String,
pub port: u16,
pub num_ports: Option<u16>,
pub proto: String,
pub fmt: String,
pub media_title: Option<String>,
pub connections: Vec<Connection>,
pub bandwidths: Vec<Bandwidth>,
pub key: Option<Key>,
pub attributes: Vec<Attribute>,
}
impl Media {
pub fn new(media: MediaType, port: u16, proto: TransportProto, fmt: impl ToString) -> Self {
Media {
media: media.to_string(),
port,
num_ports: Default::default(),
proto: proto.to_string(),
fmt: fmt.to_string(),
media_title: Default::default(),
connections: Default::default(),
bandwidths: Default::default(),
key: Default::default(),
attributes: Default::default(),
}
}
pub fn builder(
media: MediaType,
port: u16,
proto: TransportProto,
fmt: impl ToString,
) -> builders::Media {
builders::Media::new(media, port, proto, fmt)
}
pub fn set_num_ports(&mut self, num_ports: u16) {
self.num_ports = Some(num_ports);
}
pub fn set_media_title(&mut self, media_title: impl ToString) {
self.media_title = Some(media_title.to_string());
}
pub fn add_connection(&mut self, connection: impl Into<Connection>) {
self.connections.push(connection.into());
}
pub fn add_connections(
&mut self,
connections: impl IntoIterator<Item = impl Into<Connection>>,
) {
self.connections
.extend(connections.into_iter().map(|c| c.into()))
}
pub fn add_bandwidth(&mut self, bandwidth: Bandwidth) {
self.bandwidths.push(bandwidth);
}
pub fn add_bandwidths(&mut self, bandwidths: impl IntoIterator<Item = Bandwidth>) {
self.bandwidths.extend(bandwidths)
}
pub fn set_encryption_key(&mut self, key: impl Into<Key>) {
self.key = Some(key.into());
}
pub fn add_attribute(&mut self, attribute: impl Into<Attribute>) {
self.attributes.push(attribute.into());
}
pub fn add_attribute_from_str(&mut self, attribute: impl ToString) {
self.attributes.push(Attribute::new(attribute));
}
pub fn add_attribute_with_value(&mut self, attribute: impl ToString, value: impl ToString) {
self.attributes
.push(Attribute::with_value(attribute, value));
}
pub fn add_attributes(&mut self, attributes: impl IntoIterator<Item = impl Into<Attribute>>) {
self.attributes
.extend(attributes.into_iter().map(|a| a.into()))
}
pub fn add_attributes_from_strs(
&mut self,
attributes: impl IntoIterator<Item = impl ToString>,
) {
self.attributes
.extend(attributes.into_iter().map(|a| Attribute::new(a)))
}
pub fn has_attribute(&self, name: &str) -> bool {
self.attributes
.iter()
.any(|a| a.attribute.eq_ignore_ascii_case(name))
}
pub fn get_first_attribute_value<'a>(&'a self, name: &'a str) -> Option<Option<&'a str>> {
self.get_attribute_values(name).next()
}
pub fn get_attribute_values<'a>(
&'a self,
name: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
self.attributes
.iter()
.filter(move |a| a.attribute.eq_ignore_ascii_case(name))
.map(|a| a.value.as_deref())
}
pub fn try_parse_mediatype(&self) -> Result<MediaType, ParseEnumError> {
MediaType::from_str(self.media.as_str())
}
pub fn set_mediatype(&mut self, media: MediaType) {
self.media = media.to_string();
}
pub fn parse_transport_proto(&self) -> TransportProto {
TransportProto::from(self.proto.as_str())
}
pub fn set_transport_proto(&mut self, proto: TransportProto) {
self.proto = proto.to_string();
}
pub fn get_first_attribute_typed<T: TypedAttribute>(
&self,
) -> Option<Result<T, AttributeError>> {
self.attributes_typed().next()
}
pub fn attributes_typed<'a, T: TypedAttribute>(
&'a self,
) -> impl Iterator<Item = Result<T, AttributeError>> + 'a {
self.attributes
.iter()
.filter(move |a| a.attribute.eq_ignore_ascii_case(T::NAME))
.map(|a| {
T::from_str(a.value.as_deref().unwrap_or_default())
})
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Session {
pub origin: Origin,
pub session_name: String,
pub session_description: Option<String>,
pub uri: Option<String>,
pub emails: Vec<String>,
pub phones: Vec<String>,
pub connection: Option<Connection>,
pub bandwidths: Vec<Bandwidth>,
pub times: Vec<Time>,
pub time_zones: Vec<TimeZone>,
pub key: Option<Key>,
pub attributes: Vec<Attribute>,
pub medias: Vec<Media>,
}
impl Session {
pub fn new(origin: Origin, session_name: impl ToString) -> Self {
Session {
origin,
session_name: session_name.to_string(),
session_description: Default::default(),
uri: Default::default(),
emails: Default::default(),
phones: Default::default(),
connection: Default::default(),
bandwidths: Default::default(),
times: Default::default(),
time_zones: Default::default(),
key: Default::default(),
attributes: Default::default(),
medias: Default::default(),
}
}
pub fn builder(origin: Origin, session_name: impl ToString) -> builders::Session {
builders::Session::new(origin, session_name)
}
pub fn set_session_description(&mut self, session_description: impl ToString) {
self.session_description = Some(session_description.to_string());
}
pub fn set_uri(&mut self, uri: impl ToString) {
self.uri = Some(uri.to_string());
}
pub fn add_email(&mut self, email: impl ToString) {
self.emails.push(email.to_string());
}
pub fn add_emails(&mut self, emails: impl IntoIterator<Item = impl ToString>) {
self.emails
.extend(emails.into_iter().map(|e| e.to_string()));
}
pub fn add_phone(&mut self, phone: impl ToString) {
self.phones.push(phone.to_string());
}
pub fn add_phones(&mut self, phones: impl IntoIterator<Item = impl ToString>) {
self.phones
.extend(phones.into_iter().map(|p| p.to_string()));
}
pub fn set_connection(&mut self, connection: impl Into<Connection>) {
self.connection = Some(connection.into());
}
pub fn add_bandwidth(&mut self, bandwidth: Bandwidth) {
self.bandwidths.push(bandwidth);
}
pub fn add_bandwidths(&mut self, bandwidths: impl IntoIterator<Item = Bandwidth>) {
self.bandwidths.extend(bandwidths);
}
pub fn add_time(&mut self, time: Time) {
self.times.push(time);
}
pub fn add_times(&mut self, times: impl IntoIterator<Item = Time>) {
self.times.extend(times);
}
pub fn add_time_zone(&mut self, time_zone: TimeZone) {
self.time_zones.push(time_zone);
}
pub fn add_time_zones(&mut self, time_zones: impl IntoIterator<Item = TimeZone>) {
self.time_zones.extend(time_zones);
}
pub fn set_encryption_key(&mut self, key: impl Into<Key>) {
self.key = Some(key.into());
}
pub fn add_attribute(&mut self, attribute: impl Into<Attribute>) {
self.attributes.push(attribute.into());
}
pub fn add_attribute_from_str(&mut self, attribute: impl ToString) {
self.attributes.push(Attribute::new(attribute));
}
pub fn add_attribute_with_value(&mut self, attribute: impl ToString, value: impl ToString) {
self.attributes
.push(Attribute::with_value(attribute, value));
}
pub fn add_attributes(&mut self, attributes: impl IntoIterator<Item = impl Into<Attribute>>) {
self.attributes
.extend(attributes.into_iter().map(|a| a.into()))
}
pub fn add_attributes_from_strs(
&mut self,
attributes: impl IntoIterator<Item = impl ToString>,
) {
self.attributes
.extend(attributes.into_iter().map(|a| Attribute::new(a)))
}
pub fn add_media(&mut self, media: Media) {
self.medias.push(media);
}
pub fn add_medias(&mut self, medias: impl IntoIterator<Item = Media>) {
self.medias.extend(medias)
}
pub fn has_attribute(&self, name: &str) -> bool {
self.attributes
.iter()
.any(|a| a.attribute.eq_ignore_ascii_case(name))
}
pub fn get_first_attribute_value<'a>(&'a self, name: &'a str) -> Option<Option<&'a str>> {
self.get_attribute_values(name).next()
}
pub fn get_attribute_values<'a>(
&'a self,
name: &'a str,
) -> impl Iterator<Item = Option<&'a str>> {
self.attributes
.iter()
.filter(move |a| a.attribute.eq_ignore_ascii_case(name))
.map(|a| a.value.as_deref())
}
pub fn get_first_attribute_typed<T: TypedAttribute>(
&self,
) -> Option<Result<T, AttributeError>> {
self.attributes_typed().next()
}
pub fn attributes_typed<'a, T: TypedAttribute>(
&'a self,
) -> impl Iterator<Item = Result<T, AttributeError>> + 'a {
self.attributes
.iter()
.filter(move |a| a.attribute.eq_ignore_ascii_case(T::NAME))
.map(|a| {
T::from_str(a.value.as_deref().unwrap_or_default())
})
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use super::*;
#[test]
fn parse_write() {
let sdp = "v=0\r
o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r
s=SDP Seminar\r
i=A Seminar on the session description protocol\r
u=http://www.example.com/seminars/sdp.pdf\r
e=j.doe@example.com (Jane Doe)\r
p=+1 617 555-6011\r
c=IN IP4 224.2.17.12/127\r
b=AS:128\r
t=2873397496 2873404696\r
r=7d 1h 0 25h\r
z=2882844526 -1h 2898848070 0\r
k=clear:1234\r
a=recvonly\r
m=audio 49170 RTP/AVP 0\r
a=fmtp:0 0-15\r
m=video 51372/2 RTP/AVP 99 97 98\r
a=rtpmap:99 h263-1998/90000\r
a=fingerprint:sha-256 3A:96:6D:57:B2:C2:C7:61:A0:46:3E:1C:97:39:D3:F7:0A:88:A0:B1:EC:03:FB:10:A5:5D:3A:37:AB:DD:02:AA\r
a=extmap:2/sendrecv http://example.com/082005/ext.htm#xmeta short\r
";
let parsed = Session::parse(sdp.as_bytes()).unwrap();
let mut written = vec![];
parsed.write(&mut written).unwrap();
assert_eq!(String::from_utf8_lossy(&written), sdp);
assert_eq!(parsed.origin.addrtype, AddrType::Ip4);
assert_ne!(parsed.origin.nettype, NetType::Pstn);
assert_eq!(
parsed.origin.try_parse_unicast_ip_address(),
Ok(IpAddr::V4(std::net::Ipv4Addr::new(10, 47, 16, 5)))
);
assert_eq!(parsed.medias[0].try_parse_mediatype(), Ok(MediaType::Audio));
assert_ne!(
parsed.medias[1].parse_transport_proto(),
TransportProto::RtpSavpf,
);
let f = fallible_iterator::convert(parsed.medias[0].attributes_typed::<Fmtp>())
.collect::<Vec<_>>()
.expect("Valid vector of attributes");
assert_eq!(f.len(), 1);
assert_eq!(f[0].format_specific_params[0].param, "0-15");
let e = fallible_iterator::convert(parsed.medias[1].attributes_typed::<ExtMap>())
.collect::<Vec<_>>()
.expect("Vector of extmap attributes");
assert_eq!(e[0].id, 2);
assert_eq!(e[0].direction, Some(Direction::SendRecv));
assert_eq!(
e[0].uri,
"http://example.com/082005/ext.htm#xmeta".to_string()
);
assert_eq!(e[0].attributes, Some("short".to_string()));
let f = fallible_iterator::convert(parsed.medias[1].attributes_typed::<Fingerprint>())
.collect::<Vec<_>>()
.expect("Vector of fingerprint attributes");
assert_eq!(f[0].hash_func, HashFunc::Sha256);
assert_eq!(f[0].fingerprint[4], 0xB2);
assert_eq!(f[0].fingerprint.last(), Some(&0xAA));
}
#[test]
fn parse_media_attributes() {
let media = Media {
media: "video".into(),
port: 51372,
num_ports: Some(2),
proto: "RTP/AVP".into(),
fmt: "99 100".into(),
media_title: None,
connections: vec![],
bandwidths: vec![],
key: None,
attributes: vec![
Attribute {
attribute: "rtpmap".into(),
value: Some("99 h263-1998/90000".into()),
},
Attribute {
attribute: "rtpmap".into(),
value: Some("100 h264/90000".into()),
},
Attribute {
attribute: "rtpmap".into(),
value: None,
},
Attribute {
attribute: "rtpmap".into(),
value: Some(
RtpMap {
payload_type: 101,
encoding_name: "L16".into(),
clock_rate: 16000,
encoding_params: Some("2".into()),
}
.to_string(),
),
},
Attribute {
attribute: "fmtp".into(),
value: Some(
Fmtp {
fmt: 100,
format_specific_params: Vec::from([
FmtpParam {
param: "profile-level-id".to_string(),
val: Some("42e016".to_string()),
},
FmtpParam {
param: "max-mbps".to_string(),
val: Some("108000".to_string()),
},
FmtpParam {
param: "max-fs".to_string(),
val: Some("3600".to_string()),
},
]),
}
.to_string(),
),
},
Attribute {
attribute: "rtcp".into(),
value: Some(
Rtcp {
port: 53020,
nettype: Some(NetType::In),
addrtype: Some(AddrType::Ip4),
connection_address: Some(
std::net::Ipv4Addr::new(126, 16, 64, 4).to_string(),
),
}
.to_string(),
),
},
Attribute {
attribute: "fingerprint".into(),
value: Some(
Fingerprint {
hash_func: HashFunc::Other(("custom").to_string()),
fingerprint: [0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6].to_vec(),
}
.to_string(),
),
},
],
};
assert!(media.has_attribute("rtpmap"));
assert!(media.has_attribute("rtcp"));
assert!(!media.has_attribute("foo"));
assert_eq!(
media.get_first_attribute_value("rtpmap"),
Some(Some("99 h263-1998/90000"))
);
assert_eq!(
media.get_first_attribute_value("rtcp"),
Some(Some("53020 IN IP4 126.16.64.4"))
);
assert!(media.get_first_attribute_value("foo").is_none());
assert_eq!(
media.get_attribute_values("rtpmap").collect::<Vec<_>>(),
&[
Some("99 h263-1998/90000"),
Some("100 h264/90000"),
None,
Some("101 L16/16000/2"),
]
);
let v = media
.attributes_typed::<RtpMap>()
.collect::<Vec<Result<RtpMap, AttributeError>>>();
assert_eq!(v[0].as_ref().unwrap().clock_rate, 90000);
assert_eq!(v[1].as_ref().unwrap().encoding_name, "h264");
let v = media
.attributes_typed::<RtpMap>()
.filter(|attr| {
let Ok(at) = attr else { return false };
at.payload_type == 99
})
.collect::<Vec<_>>();
assert_eq!(v.len(), 1);
assert_eq!(v[0].as_ref().unwrap().encoding_name, "h263-1998");
assert_eq!(
media.get_first_attribute_value("fmtp"),
Some(Some(
"100 profile-level-id=42e016;max-mbps=108000;max-fs=3600"
))
);
let r = media.attributes_typed::<Rtcp>().collect::<Vec<_>>();
assert_eq!(r[0].as_ref().unwrap().addrtype, Some(AddrType::Ip4));
assert_eq!(r[0].as_ref().unwrap().port, 53020);
let rtcp_attr = media.get_first_attribute_typed::<Rtcp>().unwrap().unwrap();
assert_eq!(rtcp_attr.addrtype, Some(AddrType::Ip4));
assert_eq!(rtcp_attr.port, 53020);
assert_eq!(
media
.get_attribute_values("fingerprint")
.collect::<Vec<_>>(),
&[Some("custom A1:B2:C3:D4:E5:F6")]
);
assert!(media.get_attribute_values("foo").next().is_none());
}
#[test]
fn parse_session_attributes() {
let session = Session {
origin: Origin {
username: Some("jdoe".into()),
sess_id: "2890844526".into(),
sess_version: 2890842807,
nettype: "IN".into(),
addrtype: "IP4".into(),
unicast_address: "10.47.16.5".into(),
},
session_name: "SDP Seminar".into(),
session_description: None,
uri: None,
emails: vec![],
phones: vec![],
connection: None,
bandwidths: vec![],
times: vec![Time {
start_time: 0,
stop_time: 0,
repeats: vec![],
}],
time_zones: vec![],
key: None,
attributes: vec![
Attribute {
attribute: "rtpmap".into(),
value: Some("99 h263-1998/90000".into()),
},
Attribute {
attribute: "rtpmap".into(),
value: Some("100 h264/90000".into()),
},
Attribute {
attribute: "rtpmap".into(),
value: Some(
RtpMap {
payload_type: 101,
encoding_name: "L16".into(),
clock_rate: 16000,
encoding_params: Some("2".into()),
}
.to_string(),
),
},
Attribute {
attribute: "rtcp".into(),
value: None,
},
Attribute {
attribute: "extmap".into(),
value: Some(
ExtMap {
id: 1,
direction: None,
uri: "URI-toffset".to_string(),
attributes: None,
}
.to_string(),
),
},
],
medias: vec![],
};
assert!(session.has_attribute("rtpmap"));
assert!(session.has_attribute("rtcp"));
assert!(!session.has_attribute("foo"));
assert_eq!(
session.get_first_attribute_value("rtpmap"),
Some(Some("99 h263-1998/90000"))
);
let v = session
.get_first_attribute_value("rtpmap")
.unwrap()
.unwrap();
let rtpmap = RtpMap::from_str(v).unwrap();
assert_eq!(rtpmap.clock_rate, 90000);
assert_eq!(rtpmap.encoding_name, "h263-1998");
assert_ne!(rtpmap.encoding_name, "h263");
assert_ne!(rtpmap.encoding_params, Some("2".to_string()));
assert_eq!(rtpmap.payload_type, 99);
let rtpmap = session
.get_first_attribute_typed::<RtpMap>()
.unwrap()
.unwrap();
assert_eq!(rtpmap.clock_rate, 90000);
assert_eq!(rtpmap.encoding_name, "h263-1998");
assert_ne!(rtpmap.encoding_name, "h263");
assert_ne!(rtpmap.encoding_params, Some("2".to_string()));
assert_eq!(rtpmap.payload_type, 99);
assert_eq!(session.get_first_attribute_value("rtcp"), Some(None));
assert!(session.get_first_attribute_value("foo").is_none());
assert_eq!(
session.get_attribute_values("rtpmap").collect::<Vec<_>>(),
&[
Some("99 h263-1998/90000"),
Some("100 h264/90000"),
Some("101 L16/16000/2")
]
);
assert_eq!(
session.get_attribute_values("rtcp").collect::<Vec<_>>(),
&[None]
);
assert_eq!(
session.get_attribute_values("extmap").collect::<Vec<_>>(),
&[Some("1 URI-toffset")]
);
assert!(session.get_attribute_values("foo").next().is_none());
let a = fallible_iterator::convert(session.attributes_typed::<RtpMap>())
.collect::<Vec<_>>()
.expect("Valid vector of attributes");
assert_eq!(a[2].encoding_name, "L16");
assert_eq!(a[0].payload_type, 99);
}
#[test]
fn origin_parse_address_error() {
use std::net::Ipv6Addr;
let mut origin = Origin {
username: None,
sess_id: "1234".to_string(),
sess_version: 0,
nettype: NetType::In,
addrtype: AddrType::Ip4,
unicast_address: "127.0.0.1".to_string(),
};
assert_eq!(origin.addrtype, AddrType::Ip4);
assert_eq!(
origin.try_parse_unicast_ip_address().unwrap(),
IpAddr::V4(Ipv4Addr::LOCALHOST),
);
origin.unicast_address = "127.0.0.1/24".to_string();
assert_eq!(
origin.try_parse_unicast_ip_address().unwrap_err(),
origin.unicast_address
);
origin.unicast_address = "non-ip".to_string();
assert_eq!(
origin.try_parse_unicast_ip_address().unwrap_err(),
origin.unicast_address
);
origin.set_unicast_ip_address(Ipv6Addr::LOCALHOST);
assert_eq!(origin.addrtype, AddrType::Ip6);
assert_eq!(
origin.try_parse_unicast_ip_address().unwrap(),
IpAddr::V6(Ipv6Addr::LOCALHOST),
);
}
#[test]
#[allow(deprecated)]
fn connection_parse_address_error() {
use std::net::Ipv6Addr;
let mut connection = Connection {
nettype: NetType::In,
addrtype: AddrType::Ip4,
connection_address: "127.0.0.1".to_string(),
};
assert_eq!(connection.addrtype, AddrType::Ip4);
assert_eq!(
connection.try_parse_connection_ip_address().unwrap(),
IpAddr::V4(Ipv4Addr::LOCALHOST),
);
connection.connection_address = "127.0.0.1/24".to_string();
assert_eq!(
connection.try_parse_connection_ip_address().unwrap(),
IpAddr::V4(Ipv4Addr::LOCALHOST),
);
connection.connection_address = "non-ip".to_string();
assert_eq!(
connection.try_parse_connection_ip_address().unwrap_err(),
connection.connection_address
);
connection.set_connection_ip_address(Ipv6Addr::LOCALHOST);
assert_eq!(connection.addrtype, AddrType::Ip6);
assert_eq!(
connection.try_parse_connection_ip_address().unwrap(),
IpAddr::V6(Ipv6Addr::LOCALHOST),
);
}
#[test]
fn connection_try_is_multicast() {
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1");
assert_eq!(connection.try_is_multicast(), Ok(true));
let connection = Connection::new(NetType::In, AddrType::Ip4, "127.0.0.1");
assert_eq!(connection.try_is_multicast(), Ok(false));
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/x");
assert_eq!(connection.try_is_multicast(), Ok(true));
let connection = Connection::new(NetType::In, AddrType::Ip4, "x/16");
assert_eq!(
connection.try_is_multicast().unwrap_err(),
ConnectionAddressError::InvalidAddress("x/16".into())
);
}
#[test]
fn connection_is_ipv4_ipv6() {
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/16");
assert!(connection.is_ipv4());
assert!(!connection.is_ipv6());
let connection = Connection::new(NetType::In, AddrType::Ip6, "ff12::5/3");
assert!(connection.is_ipv6());
assert!(!connection.is_ipv4());
let connection = Connection::new(NetType::In, AddrType::Ip4, "x/16");
assert!(!connection.is_ipv4());
assert!(!connection.is_ipv6());
}
#[test]
fn connection_try_address() {
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/16/2");
assert_eq!(
connection.try_address().unwrap(),
ConnectionAddress {
ip: "224.2.1.1".parse().unwrap(),
ttl: Some(16),
count: Some(2),
}
);
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/16");
assert_eq!(
connection.try_address().unwrap(),
ConnectionAddress {
ip: "224.2.1.1".parse().unwrap(),
ttl: Some(16),
count: None,
}
);
let connection = Connection::new(NetType::In, AddrType::Ip4, "127.0.0.1");
assert_eq!(
connection.try_address().unwrap(),
ConnectionAddress {
ip: "127.0.0.1".parse().unwrap(),
ttl: None,
count: None,
}
);
let connection = Connection::new(NetType::In, AddrType::Ip6, "ff12::5/3");
assert_eq!(
connection.try_address().unwrap(),
ConnectionAddress {
ip: "ff12::5".parse().unwrap(),
ttl: None,
count: Some(3),
}
);
let connection = Connection::new(NetType::In, AddrType::Ip4, "x/16");
assert_eq!(
connection.try_address().unwrap_err(),
ConnectionAddressError::InvalidAddress("x/16".into())
);
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/x");
assert_eq!(
connection.try_address().unwrap_err(),
ConnectionAddressError::InvalidTtl("224.2.1.1/x".into())
);
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/300");
assert_eq!(
connection.try_address().unwrap_err(),
ConnectionAddressError::InvalidTtl("224.2.1.1/300".into())
);
let connection = Connection::new(NetType::In, AddrType::Ip4, "224.2.1.1/16/x");
assert_eq!(
connection.try_address().unwrap_err(),
ConnectionAddressError::InvalidCount("224.2.1.1/16/x".into())
);
}
#[test]
fn connection_from_ip_addr_ttl() {
use std::net::Ipv6Addr;
let connection = Connection::from_ip_addr_ttl(Ipv4Addr::new(224, 2, 1, 1), Some(16));
assert_eq!(connection.nettype, NetType::In);
assert_eq!(connection.addrtype, AddrType::Ip4);
assert_eq!(connection.connection_address, "224.2.1.1/16");
let connection =
Connection::from_ip_addr_ttl(Ipv6Addr::new(0xff12, 0, 0, 0, 0, 0, 0, 5), Some(16));
assert_eq!(connection.addrtype, AddrType::Ip6);
assert_eq!(connection.connection_address, "ff12::5");
let connection = Connection::from_ip_addr_ttl(Ipv4Addr::new(224, 2, 1, 1), None);
assert_eq!(connection.connection_address, "224.2.1.1");
}
#[test]
fn attribute_lookup_case_insensitive() {
let sdp = b"v=0\r
o=- 0 0 IN IP4 0.0.0.0\r
s=x\r
t=0 0\r
a=Foo:Bar\r
m=audio 49170 RTP/AVP 0\r
a=RTPMAP:0 pcma/8000\r
";
let session = Session::parse(&sdp[..]).unwrap();
assert_eq!(session.attributes[0].attribute, "Foo");
assert!(session.has_attribute("foo"));
assert!(session.has_attribute("FOO"));
assert_eq!(session.get_first_attribute_value("FOO"), Some(Some("Bar")));
let media = &session.medias[0];
assert!(media.has_attribute("rtpmap"));
assert!(media.has_attribute("RtPmAp"));
assert_eq!(
media.get_attribute_values("rtpmap").collect::<Vec<_>>(),
vec![Some("0 pcma/8000")]
);
}
}