use std::net::{IpAddr, SocketAddr};
use {
combine::error::*,
combine::parser::char::*,
combine::parser::combinator::*,
combine::stream::StreamErrorFor,
combine::*,
combine::{ParseError, Parser, Stream},
};
use crate::dtls::Fingerprint;
use crate::ice::{Candidate, CandidateKind};
use crate::rtp_::{Direction, Extension, Mid, Pt, SessionId, Ssrc};
use crate::sdp::SdpError;
use super::data::*;
pub fn sdp_parser<Input>() -> impl Parser<Input, Output = Sdp>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
(session_parser(), many::<Vec<_>, _, _>(media_parser())).map(|(session, media)| Sdp {
session,
media_lines: media,
})
}
pub fn session_parser<Input>() -> impl Parser<Input, Output = Session>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
(
typed_line('v', token('0')), originator_line(), typed_line('s', token('-')), many::<Vec<_>, _, _>(ignored_session_line()),
optional(bandwidth_line()), typed_line('t', string("0 0")), many::<Vec<_>, _, _>(typed_line('r', any_value())), many::<Vec<_>, _, _>(session_attribute_line()),
)
.map(|(_, id, _, _, bw, _, _, attrs)| Session { id, bw, attrs })
}
fn originator_line<Input>() -> impl Parser<Input, Output = SessionId>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let session_string = typed_line(
'o',
(
not_sp(),
token(' '),
many1::<String, _, _>(digit()),
token(' '),
any_value(),
)
.map(|(_, _, sess, _, _)| sess),
);
from_str(session_string).map(|x: u64| -> SessionId { SessionId::from(x) })
}
fn bandwidth_line<Input>() -> impl Parser<Input, Output = Bandwidth>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
typed_line('b', (many1(satisfy(|c| c != ':')), token(':'), any_value()))
.map(|(typ, _, val)| Bandwidth { typ, val })
}
fn attribute_line<Input, Pval, Out>(
attribute: &'static str,
val: Pval,
) -> impl Parser<Input, Output = Out>
where
Input: Stream<Token = char>,
Pval: Parser<Input, Output = Out>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
typed_line('a', (string(attribute), token(':'), val)).map(|(_, _, val)| val)
}
fn attribute_line_flag<Input>(attribute: &'static str) -> impl Parser<Input, Output = ()>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
typed_line('a', (string(attribute)).map(|_| ()))
}
fn session_attribute_line<Input>() -> impl Parser<Input, Output = SessionAttribute>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let group = attribute_line(
"group",
(
not_sp(),
token(' '),
sep_by1(not_sp().map(|m| Mid::from(m.as_str())), token(' ')),
),
)
.map(|(typ, _, mids)| SessionAttribute::Group { typ, mids });
let msid_semantic = attribute_line(
"msid-semantic",
(
optional(token(' ')),
not_sp(),
token(' '),
sep_by1(not_sp(), token(' ')),
),
)
.map(
|(_, semantic, _, stream_ids)| SessionAttribute::MsidSemantic {
semantic,
stream_ids,
},
);
let ice_lite = attribute_line_flag("ice-lite").map(|_| SessionAttribute::IceLite);
let ice_ufrag = attribute_line("ice-ufrag", any_value()).map(SessionAttribute::IceUfrag);
let ice_pwd = attribute_line("ice-pwd", any_value()).map(SessionAttribute::IcePwd);
let ice_opt = attribute_line("ice-options", any_value()).map(SessionAttribute::IceOptions);
let hex_byte = count_min_max(2, 2, hex_digit()).and_then(|x: String| {
u8::from_str_radix(&x, 16).map_err(StreamErrorFor::<Input>::message_format)
});
let finger = attribute_line(
"fingerprint",
(not_sp(), token(' '), sep_by1(hex_byte, token(':'))),
)
.map(|(hash_func, _, bytes)| SessionAttribute::Fingerprint(Fingerprint { hash_func, bytes }));
let setup_val = choice((
attempt(string("actpass").map(|_| Setup::ActPass)),
attempt(string("active").map(|_| Setup::Active)),
attempt(string("passive").map(|_| Setup::Passive)),
));
let setup = attribute_line("setup", setup_val).map(SessionAttribute::Setup);
let cand = candidate_attribute().map(SessionAttribute::Candidate);
let endof = attribute_line_flag("end-of-candidates").map(|_| SessionAttribute::EndOfCandidates);
let unused = typed_line('a', any_value()).map(SessionAttribute::Unused);
choice((
attempt(group),
attempt(msid_semantic),
attempt(ice_lite),
attempt(ice_ufrag),
attempt(ice_pwd),
attempt(ice_opt),
attempt(finger),
attempt(setup),
attempt(cand),
attempt(endof),
unused,
))
}
pub fn parse_candidate(s: &str) -> Result<Candidate, SdpError> {
candidate()
.parse(s)
.map(|(c, _)| c)
.map_err(|e| SdpError::ParseError(e.to_string()))
}
fn candidate<Input>() -> impl Parser<Input, Output = Candidate>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let port = || {
not_sp::<Input>().and_then(|s| {
s.parse::<u16>()
.map_err(StreamErrorFor::<Input>::message_format)
})
};
let ip_addr = || {
not_sp().and_then(|s| {
s.parse::<IpAddr>()
.map_err(StreamErrorFor::<Input>::message_format)
})
};
let kind = choice((
string("host").map(|_| CandidateKind::Host),
string("prflx").map(|_| CandidateKind::PeerReflexive),
string("srflx").map(|_| CandidateKind::ServerReflexive),
string("relay").map(|_| CandidateKind::Relayed),
));
(
string("candidate:").and_then(|s| {
s.parse::<String>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
not_sp(),
token(' '),
not_sp().and_then(|s| {
s.parse::<u16>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
token(' '),
not_sp().and_then(|s| {
s.as_str().try_into().map_err(|_| {
StreamErrorFor::<Input>::message_format(format!("invalid protocol: {}", s))
})
}),
token(' '),
not_sp().and_then(|s| {
s.parse::<u32>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
token(' '),
ip_addr(),
token(' '),
port(),
string(" typ "),
kind,
optional((
attempt(string(" raddr ")),
ip_addr(),
string(" rport "),
port(),
)),
optional((attempt(string(" ufrag ")), not_sp())),
)
.map(
|(_, found, _, comp_id, _, proto, _, prio, _, addr, _, port, _, kind, raddr, ufrag)| {
Candidate::parsed(
found,
comp_id,
proto,
prio, SocketAddr::from((addr, port)),
kind,
raddr.map(|(_, addr, _, port)| SocketAddr::from((addr, port))),
ufrag.map(|(_, u)| u),
)
},
)
}
#[doc(hidden)]
pub fn candidate_attribute<Input>() -> impl Parser<Input, Output = Candidate>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
(string("a="), candidate(), line_end()).map(|(_, c, _)| c)
}
fn ignored_session_line<Input>() -> impl Parser<Input, Output = ()>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let ignored = choice((
token('i'),
token('u'),
token('e'),
token('p'),
token('c'),
token('z'),
token('k'),
));
line(ignored, any_value()).map(|_| ())
}
fn media_parser<Input>() -> impl Parser<Input, Output = MediaLine>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
(
media_line(),
optional(typed_line('c', any_value())), optional(bandwidth_line()), many::<Vec<_>, _, _>(media_attribute_line()),
)
.and_then(|((typ, port, proto, pts), _, bw, attrs)| {
let m = MediaLine {
typ,
disabled: port == "0",
proto,
pts,
bw,
attrs,
};
if let Some(err) = m.check_consistent() {
warn!("{:?}", err);
return Err(StreamErrorFor::<Input>::message_format(err));
}
Ok(m)
})
}
fn media_line<Input>() -> impl Parser<Input, Output = (MediaType, String, Proto, Vec<Pt>)>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let media_type = choice((
attempt(string("audio").map(|_| MediaType::Audio)),
attempt(string("video").map(|_| MediaType::Video)),
attempt(string("application").map(|_| MediaType::Application)),
not_sp().map(MediaType::Unknown),
));
let proto_line = choice((
attempt(string("UDP/TLS/RTP/SAVPF").map(|_| Proto::Srtp)),
attempt(string("DTLS/SCTP").map(|_| Proto::Sctp)),
attempt(string("UDP/DTLS/SCTP").map(|_| Proto::Sctp)),
));
let parse_pt = not_sp().and_then(|s| {
s.parse::<u8>()
.map(Pt::from)
.map_err(StreamErrorFor::<Input>::message_format)
});
typed_line(
'm',
(
media_type, token(' '),
not_sp(), token(' '),
proto_line, token(' '),
choice((
attempt(sep_by(parse_pt, token(' '))), any_value().map(|_| vec![]),
)),
),
)
.map(|(typ, _, port, _, proto, _, pts)| (typ, port, proto, pts))
}
fn media_attribute_line<Input>() -> impl Parser<Input, Output = MediaAttribute>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
let rtcp = attribute_line("rtcp", any_value()).map(MediaAttribute::Rtcp);
let ice_ufrag = attribute_line("ice-ufrag", any_value()).map(MediaAttribute::IceUfrag);
let ice_pwd = attribute_line("ice-pwd", any_value()).map(MediaAttribute::IcePwd);
let ice_opt = attribute_line("ice-options", any_value()).map(MediaAttribute::IceOptions);
let hex_byte = count_min_max(2, 2, hex_digit()).and_then(|x: String| {
u8::from_str_radix(&x, 16).map_err(StreamErrorFor::<Input>::message_format)
});
let finger = attribute_line(
"fingerprint",
(not_sp(), token(' '), sep_by1(hex_byte, token(':'))),
)
.map(|(hash_func, _, bytes)| MediaAttribute::Fingerprint(Fingerprint { hash_func, bytes }));
let setup_val = choice((
attempt(string("actpass").map(|_| Setup::ActPass)),
attempt(string("active").map(|_| Setup::Active)),
attempt(string("passive").map(|_| Setup::Passive)),
));
let setup = attribute_line("setup", setup_val).map(MediaAttribute::Setup);
let mid = attribute_line("mid", any_value())
.map(|m| Mid::from(m.as_str()))
.map(MediaAttribute::Mid);
let sctp_port = attribute_line(
"sctp-port",
not_sp::<Input>().and_then(|s| {
s.parse::<u16>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
)
.map(MediaAttribute::SctpPort);
let max_message_size = attribute_line(
"max-message-size",
not_sp::<Input>().and_then(|s| {
s.parse::<usize>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
)
.map(MediaAttribute::MaxMessageSize);
let extmap = attribute_line(
"extmap",
(
many1::<String, _, _>(satisfy(|c| c != '/' && c != ' ')).and_then(|s| {
s.parse::<u8>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
optional((token('/'), not_sp().map(|d| Direction::from(&d[..])))),
token(' '),
not_sp().map(|uri| Extension::from_sdp_uri(&uri)),
optional((token(' '), any_value())),
),
)
.map(|(id, _dir_opt, _, ext, _ext_opt)| MediaAttribute::ExtMap { id, ext });
let direction = choice((
attempt(attribute_line_flag("recvonly").map(|_| MediaAttribute::RecvOnly)),
attempt(attribute_line_flag("sendrecv").map(|_| MediaAttribute::SendRecv)),
attempt(attribute_line_flag("sendonly").map(|_| MediaAttribute::SendOnly)),
attempt(attribute_line_flag("inactive").map(|_| MediaAttribute::Inactive)),
));
let msid = attribute_line("msid", (not_sp(), token(' '), any_value())).map(
|(stream_id, _, track_id)| {
MediaAttribute::Msid(Msid {
stream_id,
track_id,
})
},
);
let rtcpmux = attribute_line_flag("rtcp-mux").map(|_| MediaAttribute::RtcpMux);
let rtcpmuxonly = attribute_line_flag("rtcp-mux-only").map(|_| MediaAttribute::RtcpMuxOnly);
let rtcprsize = attribute_line_flag("rtcp-rsize").map(|_| MediaAttribute::RtcpRsize);
let cand = candidate_attribute().map(MediaAttribute::Candidate);
let endof = attribute_line_flag("end-of-candidates").map(|_| MediaAttribute::EndOfCandidates);
let pt = || {
not_sp().and_then(|s| {
s.parse::<u8>()
.map(Pt::from)
.map_err(StreamErrorFor::<Input>::message_format)
})
};
let rtpmap = attribute_line(
"rtpmap",
(
pt(),
token(' '),
many1::<String, _, _>(satisfy(|c| c != '/' && c != '\r' && c != '\n')),
token('/'),
many1::<String, _, _>(satisfy(|c| c != '/' && c != '\r' && c != '\n')).and_then(|s| {
s.parse::<u32>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
optional((
token('/'),
any_value().and_then(|s| {
s.parse::<u8>()
.map_err(StreamErrorFor::<Input>::message_format)
}),
)), ),
)
.map(|(pt, _, codec, _, clock_rate, opt_channels)| {
let channels = opt_channels.map(|(_, e)| e);
MediaAttribute::RtpMap {
pt,
value: RtpMap {
codec: codec.as_str().into(),
clock_rate,
channels,
},
}
});
let rtcp_fb = attribute_line("rtcp-fb", (pt(), token(' '), any_value()))
.map(|(pt, _, value)| MediaAttribute::RtcpFb { pt, value });
let fmtp_param = sep_by1(
key_val().map(|(k, v)| FormatParam::parse(&k, &v)),
token(';'),
);
let fmtp1 = attribute_line("fmtp", (pt(), token(' '), fmtp_param))
.map(|(pt, _, values)| MediaAttribute::Fmtp { pt, values });
let fmtp2 = attribute_line("fmtp", (pt(), token(' '), not_sp())).map(|(pt, _, _value)| {
MediaAttribute::Fmtp {
pt,
values: vec![FormatParam::Unknown],
}
});
let fmtp = choice((attempt(fmtp1), attempt(fmtp2)));
let rid = attribute_line(
"rid",
(
name().map(RestrictionId::new_active),
token(' '),
choice((string("send"), string("recv"))),
optional((
token(' '),
optional((
string("pt="),
sep_by1::<Vec<Pt>, _, _, _>(pt(), token(',')),
optional(token(';')),
)),
sep_by::<Vec<(String, String)>, _, _, _>(key_val(), token(';')),
)),
),
)
.map(|(id, _, direction, x)| {
let mut pt = vec![];
let mut restriction = vec![];
if let Some((_, ps, rs)) = x {
if let Some((_, ps, _)) = ps {
pt = ps;
}
restriction = rs;
}
MediaAttribute::Rid {
id,
direction,
pt,
restriction,
}
});
let simul1 = |direction: &'static str| {
(
string(direction),
token(' '),
sep_by1::<Vec<Vec<(String, bool)>>, _, _, _>(
sep_by1(
optional(token('~'))
.and(name())
.map(|x| (x.1, x.0.is_none())),
token(','),
),
token(';'),
),
)
};
let simul2 = choice((
(simul1("send"), optional((token(' '), simul1("recv")))),
(simul1("recv"), optional((token(' '), simul1("send")))),
));
let simulcast = attribute_line("simulcast", simul2).map(|(s1, maybe_s2)| {
let mut send = SimulcastGroups(vec![]);
let mut recv = SimulcastGroups(vec![]);
fn to_simul(to: &mut SimulcastGroups, groups: Vec<Vec<(String, bool)>>) {
for group in groups {
let first = group.into_iter().next();
if let Some(rid) = first.map(|(rid, active)| RestrictionId::new(rid, active)) {
to.0.push(rid);
}
}
}
{
let to = if s1.0 == "send" { &mut send } else { &mut recv };
to_simul(to, s1.2);
}
if let Some(s2) = maybe_s2 {
let s2 = s2.1;
let to = if s2.0 == "send" { &mut send } else { &mut recv };
to_simul(to, s2.2);
}
MediaAttribute::Simulcast(Simulcast {
send,
recv,
is_munged: false,
})
});
let ssrc_group = attribute_line(
"ssrc-group",
(
not_sp(),
token(' '),
sep_by1(
not_sp().and_then(|s| {
s.parse::<u32>()
.map(Ssrc::from)
.map_err(StreamErrorFor::<Input>::message_format)
}),
token(' '),
),
),
)
.map(|(semantics, _, ssrcs)| MediaAttribute::SsrcGroup { semantics, ssrcs });
let ssrc = attribute_line(
"ssrc",
(
not_sp().and_then(|s| {
s.parse::<u32>()
.map(Ssrc::from)
.map_err(StreamErrorFor::<Input>::message_format)
}),
token(' '),
many1::<String, _, _>(satisfy(|c| c != ':')),
token(':'),
any_value(),
),
)
.map(|(ssrc, _, attr, _, value)| MediaAttribute::Ssrc { ssrc, attr, value });
let unused = typed_line('a', any_value()).map(MediaAttribute::Unused);
choice((
attempt(ice_ufrag),
attempt(ice_pwd),
attempt(ice_opt),
attempt(finger),
attempt(setup),
attempt(mid),
attempt(sctp_port),
attempt(max_message_size),
attempt(extmap),
attempt(direction),
attempt(msid),
attempt(rtcp),
attempt(rtcpmux),
attempt(rtcpmuxonly),
attempt(rtcprsize),
attempt(cand),
attempt(endof),
attempt(rtpmap),
attempt(rtcp_fb),
attempt(fmtp),
attempt(rid),
attempt(simulcast),
attempt(ssrc_group),
attempt(ssrc),
unused,
))
}
fn typed_line<Input, Pval, Out>(expected: char, val: Pval) -> impl Parser<Input, Output = Out>
where
Input: Stream<Token = char>,
Pval: Parser<Input, Output = Out>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
line(token(expected), val)
}
fn line<Input, Ptyp, Pval, Out>(typ: Ptyp, val: Pval) -> impl Parser<Input, Output = Out>
where
Ptyp: Parser<Input, Output = char>,
Pval: Parser<Input, Output = Out>,
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
attempt((typ, token('='), val, line_end()))
.map(|(_, _, value, _)| value)
.message("sdp line")
}
fn name<Input>() -> impl Parser<Input, Output = String>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
many1(satisfy(|c: char| c.is_alphanumeric()))
}
fn not_sp<Input>() -> impl Parser<Input, Output = String>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
many1(satisfy(|c| c != ' ' && c != '\r' && c != '\n'))
}
fn any_value<Input>() -> impl Parser<Input, Output = String>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
many1(satisfy(|c| c != '\r' && c != '\n'))
}
fn line_end<Input>() -> impl Parser<Input, Output = ()>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
choice((crlf().map(|_| ()), newline().map(|_| ()), eof()))
}
fn key_val<Input>() -> impl Parser<Input, Output = (String, String)>
where
Input: Stream<Token = char>,
Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
(
optional(spaces()),
many1(satisfy(|c| c != '=' && c != ' ' && c != '\r' && c != '\n')),
token('='),
many1(satisfy(|c| c != ';' && c != ' ' && c != '\r' && c != '\n')),
)
.map(|(_, key, _, val)| (key, val))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn line_a() {
assert_eq!(
line(letter(), any_value()).parse("a=mid:0"),
Ok(("mid:0".to_string(), ""))
)
}
#[test]
fn line_end_crlf() {
assert_eq!(line_end().parse("\r\n"), Ok(((), "")));
}
#[test]
fn line_end_lf() {
assert_eq!(line_end().parse("\n"), Ok(((), "")));
}
#[test]
fn line_end_eof() {
assert_eq!(line_end().parse(""), Ok(((), "")));
}
#[test]
fn typed_line_v() {
assert_eq!(typed_line('v', token('0')).parse("v=0"), Ok(('0', "")))
}
#[test]
fn attribute_line_flag_foo() {
assert_eq!(attribute_line_flag("foo").parse("a=foo"), Ok(((), "")))
}
#[test]
fn attribute_line_foo_bar() {
assert_eq!(
attribute_line("foo", any_value()).parse("a=foo:bar"),
Ok(("bar".to_string(), ""))
)
}
#[test]
fn session_attribute_line_simple() {
let x = session_attribute_line().parse("a=ice-lite");
assert_eq!(x, Ok((SessionAttribute::IceLite, "")));
}
#[test]
fn session_attribute_line_finger() {
let x = session_attribute_line().parse("a=fingerprint:sha-256 45:AD:5C:82:F8:BE");
assert_eq!(
x,
Ok((
SessionAttribute::Fingerprint(Fingerprint {
hash_func: "sha-256".to_string(),
bytes: vec![69, 173, 92, 130, 248, 190],
}),
""
))
);
}
#[test]
fn media_attribute_line_rid_simple() {
let x = media_attribute_line().parse("a=rid:lo send").unwrap();
assert_eq!("a=rid:lo send\r\n", x.0.to_string());
}
#[test]
fn media_attribute_line_rid_pt() {
let x = media_attribute_line()
.parse("a=rid:lo send pt=99,100")
.unwrap();
assert_eq!("a=rid:lo send pt=99,100\r\n", x.0.to_string());
}
#[test]
fn media_attribute_line_rid_restr() {
let x = media_attribute_line()
.parse("a=rid:lo send max-br=64000;max-height=360")
.unwrap();
assert_eq!(
"a=rid:lo send max-br=64000;max-height=360\r\n",
x.0.to_string()
);
}
#[test]
fn media_attribute_line_rid_pt_restr() {
let x = media_attribute_line()
.parse("a=rid:lo send pt=99,100;max-br=64000;max-height=360")
.unwrap();
assert_eq!(
"a=rid:lo send pt=99,100;max-br=64000;max-height=360\r\n",
x.0.to_string()
);
}
#[test]
fn media_attribute_line_simulcast() {
let x = media_attribute_line()
.parse("a=simulcast:send 3;4")
.unwrap();
assert_eq!("a=simulcast:send 3;4\r\n", x.0.to_string());
}
#[test]
fn media_attribute_line_simulcast_alt() {
let x = media_attribute_line()
.parse("a=simulcast:send 2,3;4")
.unwrap();
assert_eq!("a=simulcast:send 2;4\r\n", x.0.to_string());
}
#[test]
fn media_attribute_line_simulcast_send_recv() {
let x = media_attribute_line()
.parse("a=simulcast:send 2;3 recv 4")
.unwrap();
assert_eq!("a=simulcast:send 2;3 recv 4\r\n", x.0.to_string());
}
#[test]
fn media_attribute_line_simulcast_recv_send() {
let x = media_attribute_line()
.parse("a=simulcast:recv 2;3 send 4")
.unwrap();
assert_eq!("a=simulcast:send 4 recv 2;3\r\n", x.0.to_string());
}
#[test]
fn media_line_simple() {
let m = media_line().parse("m=audio 9 UDP/TLS/RTP/SAVPF 10\r\n");
assert_eq!(
m,
Ok((
(MediaType::Audio, "9".into(), Proto::Srtp, vec![10.into()],),
""
))
);
}
#[test]
fn session_parser_simple() {
let sdp = "v=0\n\
o=- 6564425948916445306 2 IN IP4 127.0.0.1\n\
s=-\n\
t=0 0\n\
a=group:BUNDLE 0\n\
a=msid-semantic: WMS\n\
m=application 9 DTLS/SCTP 5000\n\
";
assert_eq!(
session_parser().parse(sdp),
Ok((
Session {
id: 6_564_425_948_916_445_306.into(),
bw: None,
attrs: vec![
SessionAttribute::Group {
typ: "BUNDLE".into(),
mids: vec!["0".into()],
},
SessionAttribute::Unused("msid-semantic: WMS".into())
],
},
"m=application 9 DTLS/SCTP 5000\n"
))
);
}
#[test]
fn parse_sdp_firefox() {
let sdp = "v=0\r\n\
o=mozilla...THIS_IS_SDPARTA-83.0 7052848360639826063 0 IN IP4 0.0.0.0\r\n\
s=-\r\n\
t=0 0\r\n\
a=fingerprint:sha-256 37:FC:96:B5:73:98:E6:F9:C5:0B:D9:EE:B1:F8:D0:01:07:2E:75:E8:6C:A4:32:A7:DC:63:99:5E:68:5C:BF:FB\r\n\
a=ice-options:trickle\r\n\
a=msid-semantic:WMS *\r\n";
assert_eq!(
sdp_parser().parse(sdp),
Ok((
Sdp {
session: Session {
id: 7052848360639826063.into(),
bw: None,
attrs: vec![
SessionAttribute::Fingerprint(Fingerprint {
hash_func: "sha-256".into(),
bytes: vec![
0x37, 0xFC, 0x96, 0xB5, 0x73, 0x98, 0xE6, 0xF9, 0xC5, 0x0B,
0xD9, 0xEE, 0xB1, 0xF8, 0xD0, 0x01, 0x07, 0x2E, 0x75, 0xE8,
0x6C, 0xA4, 0x32, 0xA7, 0xDC, 0x63, 0x99, 0x5E, 0x68, 0x5C,
0xBF, 0xFB
]
}),
SessionAttribute::IceOptions("trickle".to_string()),
SessionAttribute::MsidSemantic {
semantic: "WMS".into(),
stream_ids: vec!["*".into()]
},
]
},
media_lines: vec![]
},
""
))
);
}
#[test]
fn parse_offer_sdp_firefox() {
let sdp = "v=0\r\n\
o=mozilla...THIS_IS_SDPARTA-84.0 9033133899747520364 1 IN IP4 0.0.0.0\r\n\
s=-\r\n\
t=0 0\r\n\
a=fingerprint:sha-256 AE:DC:49:AE:CA:55:35:CB:4E:FA:FE:70:99:30:C0:14:C3:B8:06:80:1F:A9:DA:9A:7C:FB:B7:20:AB:83:60:45\r\n\
a=group:BUNDLE 0\r\n\
a=ice-options:trickle\r\n\
a=msid-semantic:WMS *\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101\r\n\
c=IN IP4 0.0.0.0\r\n\
a=sendonly\r\n\
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
a=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\n\
a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n\
a=fmtp:101 0-15\r\n\
a=ice-pwd:cd25258044061ec2ecc73378eb3dc6a3\r\n\
a=ice-ufrag:c1e284ad\r\n\
a=mid:0\r\n\
a=msid:- {5c7f12e5-b4bd-7142-9a06-2885a2d1cb66}\r\n\
a=rtcp-mux\r\n\
a=rtpmap:109 opus/48000/2\r\n\
a=rtpmap:9 G722/8000/1\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:101 telephone-event/8000/1\r\n\
a=setup:actpass\r\n\
a=ssrc:1481683531 cname:{326ec0d2-d1ae-974c-b1ad-aea85cdfa0ad}\r\n";
let parsed = sdp_parser().parse(sdp);
assert!(parsed.is_ok());
}
#[test]
fn parse_offer_sdp_chrome() {
let sdp = "v=0\r\n\
o=- 5058682828002148772 3 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
a=group:BUNDLE 0\r\n\
a=msid-semantic: WMS 5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n\
c=IN IP4 0.0.0.0\r\n\
a=rtcp:9 IN IP4 0.0.0.0\r\n\
a=ice-ufrag:S5hk\r\n\
a=ice-pwd:0zV/Yu3y8aDzbHgqWhnVQhqP\r\n\
a=ice-options:trickle\r\n\
a=fingerprint:sha-256 8C:64:ED:03:76:D0:3D:B4:88:08:91:64:08:80:A8:C6:5A:BF:8B:4E:38:27:96:CA:08:49:25:73:46:60:20:DC\r\n\
a=setup:actpass\r\n\
a=mid:0\r\n\
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n\
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\
a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n\
a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n\
a=sendrecv\r\n\
a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
a=rtcp-mux\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtcp-fb:111 transport-cc\r\n\
a=fmtp:111 minptime=10;useinbandfec=1\r\n\
a=rtpmap:103 ISAC/16000\r\n\
a=rtpmap:104 ISAC/32000\r\n\
a=rtpmap:9 G722/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:106 CN/32000\r\n\
a=rtpmap:105 CN/16000\r\n\
a=rtpmap:13 CN/8000\r\n\
a=rtpmap:110 telephone-event/48000\r\n\
a=rtpmap:112 telephone-event/32000\r\n\
a=rtpmap:113 telephone-event/16000\r\n\
a=rtpmap:126 telephone-event/8000\r\n\
a=ssrc:3948621874 cname:xeXs3aE9AOBn00yJ\r\n\
a=ssrc:3948621874 msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
a=ssrc:3948621874 mslabel:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK\r\n\
a=ssrc:3948621874 label:f78dde68-7055-4e20-bb37-433803dd1ed1\r\n\
";
let parsed = sdp_parser().parse(sdp);
assert!(parsed.is_ok());
}
#[test]
fn parse_safari_data_channel() {
let sdp = "v=0\r\n\
o=- 4611516372927609806 2 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
a=group:BUNDLE 0\r\n\
a=extmap-allow-mixed\r\n\
a=msid-semantic: WMS\r\n\
m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n\
c=IN IP4 0.0.0.0\r\n\
a=ice-ufrag:HhS+\r\n\
a=ice-pwd:FhYTGhlAtKCe6KFIX8b+AThW\r\n\
a=ice-options:trickle\r\n\
a=fingerprint:sha-256 B4:12:1C:7C:7D:ED:F1:FA:61:07:57:9C:29:BE:58:E3:BC:41:E7:13:8E:7D:D3:9D:1F:94:6E:A5:23:46:94:23\r\n\
a=setup:actpass\r\n\
a=mid:0\r\n\
a=sctp-port:5000\r\n\
a=max-message-size:262144\r\n\
";
let (sdp, _) = sdp_parser().parse(sdp).unwrap();
assert!(!sdp.media_lines.is_empty());
assert_eq!(sdp.media_lines[0].mid().to_string(), "0");
}
#[test]
fn parse_minimal() {
let sdp = "v=0\r\n\
o=- 5058682828002148772 3 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF\r\n\
c=IN IP4 0.0.0.0\r\n\
a=rtcp:9 IN IP4 0.0.0.0\r\n\
a=setup:actpass\r\n\
a=inactive\r\n\
a=mid:0\r\n\
";
let parsed = sdp_parser().parse(sdp);
assert!(parsed.is_ok());
}
#[test]
fn parse_candidate_ufrag() {
let a = "a=candidate:1 1 udp 1845494015 198.51.100.100 11100 typ srflx raddr 203.0.113.100 rport 10100 ufrag abc\r\n";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.ufrag(), Some("abc"));
let a = "a=candidate:1 1 udp 1845494015 198.51.100.100 11100 typ host ufrag abc\r\n";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.ufrag(), Some("abc"));
}
#[test]
fn parse_firefox_missing_setup_on_mid1() {
let sdp = "v=0\r\n\
o=mozilla...THIS_IS_SDPARTA-99.0 7710052215259647220 2 IN IP4 0.0.0.0\r\n\
s=-\r\n\
t=0 0\r\n\
a=fingerprint:sha-256 A6:64:23:37:94:7E:4B:40:F6:62:86:8C:DD:09:D5:08:7E:D4:0E:68:58:93:45:EC:99:F2:91:F7:19:72:E7:BB\r\n\
a=group:BUNDLE 0 hxI i1X mxk B3D kNI nbB xIZ bKm Hkn\r\n\
a=ice-options:trickle\r\n\
a=msid-semantic:WMS *\r\n\
m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n\
c=IN IP4 0.0.0.0\r\n\
a=inactive\r\n\
a=mid:1\r\n\
a=rtpmap:0 PCMU/8000\r\n\
";
let (sdp, _) = sdp_parser().parse(sdp).unwrap();
assert!(sdp.media_lines[0].setup().is_none()); }
#[test]
fn parse_no_media_c_line() {
let sdp = "v=0\r\n\
o=- 0 0 IN IP4 172.17.0.1\r\n\
s=-\r\n\
c=IN IP4 172.17.0.1\r\n\
t=0 0\r\n\
m=application 9999 UDP/DTLS/SCTP webrtc-datachannel\r\n\
a=mid:0\r\n\
a=ice-options:ice2\r\n\
a=ice-ufrag:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1\r\n\
a=ice-pwd:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1:libp2p+webrtc+v1/a75469cf670c4079f8c06af4a963c8a1\r\n\
a=fingerprint:sha-256 FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF\r\n\
a=setup:actpass\r\n\
a=sctp-port:5000\r\n\
a=max-message-size:16384\r\n\
";
let parsed = sdp_parser().parse(sdp);
println!("{:?}", parsed);
parsed.expect("to parse ok");
}
}