use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use crate::crypto::Fingerprint;
use crate::io::TcpType;
use crate::rtp_::{Direction, Extension, Frequency, Mid, Pt, SessionId, Ssrc};
use crate::{Candidate, CandidateKind};
use combine::error::*;
use combine::parser::char::*;
use combine::parser::combinator::*;
use combine::stream::StreamErrorFor;
use combine::*;
use combine::{ParseError, Parser, Stream};
use super::data::*;
/// Creates a parser of SDP
pub fn sdp_parser() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
(session_parser(), many::, _, _>(media_parser())).map(|(session, media)| Sdp {
session,
media_lines: media,
})
}
// /////////////////////////////////////////////////// Session description
/// 1. First line must be v=0
/// 2. The second SDP line MUST be an "o=" line The sess-id MUST be representable by a 64-bit signed
/// integer, and the initial value MUST be less than (2**62)-1
/// 3. Third line is the session name. The "s=" field MUST NOT be empty and SHOULD contain
/// ISO 10646 characters (RFC 8866 section 5.3).
///
/// Session is over when we find a "t=" line MUST be added, both and
/// SHOULD be set to zero, e.g. "t=0 0".
pub fn session_parser() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
(
typed_line('v', token('0')), // v=0
originator_line(), // o=- 6564425948916445306 2 IN IP4 127.0.0.1
// The "s=" field MUST NOT be empty (RFC 8866 section 5.3)
typed_line('s', any_value()),
many::, _, _>(ignored_session_line()),
optional(bandwidth_line()), // b=CT:1234
typed_line('t', string("0 0")), // t=0 0
many::, _, _>(typed_line('r', any_value())), // r should never appear
//
many::, _, _>(session_attribute_line()),
)
.map(|(_, id, _, _, bw, _, _, attrs)| Session { id, bw, attrs })
}
/// `o= `
fn originator_line() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
let session_string = typed_line(
'o',
// o=- 6564425948916445306 2 IN IP4 127.0.0.1
(
not_sp(),
token(' '),
many1::(digit()),
token(' '),
any_value(),
)
.map(|(_, _, sess, _, _)| sess),
);
from_str(session_string).map(|x: u64| -> SessionId { SessionId::from(x) })
}
/// `b=:`
fn bandwidth_line() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
typed_line('b', (many1(satisfy(|c| c != ':')), token(':'), any_value()))
.map(|(typ, _, val)| Bandwidth { typ, val })
}
/// An a= line that with value like: `a=:`.
fn attribute_line(
attribute: &'static str,
val: Pval,
) -> impl Parser
where
Input: Stream,
Pval: Parser,
Input::Error: ParseError,
{
typed_line('a', (string(attribute), token(':'), val)).map(|(_, _, val)| val)
}
/// An a= line that has no value like: `a=ice-lite`.
fn attribute_line_flag(attribute: &'static str) -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
typed_line('a', (string(attribute)).map(|_| ()))
}
/// a=foo:bar lines belonging before the first m= line
fn session_attribute_line() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
// a=group:BUNDLE 1 2
// a=group:LS 1 2
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 });
// a=msid-semantic: WMS 9ce81ef6-c7cb-4da5-8f5f-3e7cc9b5f9b0
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,
},
);
// a=ice-lite
let ice_lite = attribute_line_flag("ice-lite").map(|_| SessionAttribute::IceLite);
// a=ice-ufrag:IdNYTNL1fjvjyEzL
let ice_ufrag = attribute_line("ice-ufrag", any_value()).map(SessionAttribute::IceUfrag);
// a=ice-pwd:4d64pT3T1xfwbZvi9fQKjoPb
let ice_pwd = attribute_line("ice-pwd", any_value()).map(SessionAttribute::IcePwd);
// a=ice-options:trickle
let ice_opt = attribute_line("ice-options", any_value()).map(SessionAttribute::IceOptions);
// a=fingerprint:sha-256 45:AD:5C:82:F8:BE:B5:2A:D1:74:A6:16:D0:50:CD:86:9C:97:9D:BD:06:8C:C9:85:C9:CD:AB:2B:A8:56:03:CD
// "sha-1" / "sha-224" / "sha-256" /
// "sha-384" / "sha-512" /
// "md5" / "md2"
let hex_byte = count_min_max(2, 2, hex_digit()).and_then(|x: String| {
u8::from_str_radix(&x, 16).map_err(StreamErrorFor::::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)),
));
// a=setup:actpass
let setup = attribute_line("setup", setup_val).map(SessionAttribute::Setup);
// a=candidate
let cand = candidate_attribute().map(SessionAttribute::Candidate);
// a=end-of-candidates
let endof = attribute_line_flag("end-of-candidates").map(|_| SessionAttribute::EndOfCandidates);
// tls-id
// identity
// extmap
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,
))
}
/// Parser for candidate, without attribute prefix (a=).
fn candidate() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
// Reference: https://datatracker.ietf.org/doc/html/rfc5245#section-15.1
let port = || {
not_sp::().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
})
};
let ip_addr = || {
not_sp().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::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::()
.map_err(StreamErrorFor::::message_format)
}),
not_sp(),
token(' '),
not_sp().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
}),
token(' '),
not_sp().and_then(|s| {
s.as_str().try_into().map_err(|_| {
StreamErrorFor::::message_format(format!("invalid protocol: {}", s))
})
}),
token(' '),
not_sp().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
}),
token(' '),
ip_addr(),
token(' '),
port(),
string(" typ "),
kind,
optional((
attempt(string(" raddr ")),
ip_addr(),
string(" rport "),
port(),
)),
optional((
attempt(string(" tcptype ")),
not_sp().and_then(|s| {
TcpType::from_str(s.as_str()).map_err(StreamErrorFor::::message_format)
}),
)),
optional((attempt(string(" generation ")), not_sp())),
optional((attempt(string(" network-id ")), not_sp())),
optional((attempt(string(" ufrag ")), not_sp())),
optional((attempt(string(" network-cost ")), not_sp())),
)
.map(
|(
_,
found,
_,
comp_id,
_,
proto,
_,
prio,
_,
addr,
_,
port,
_,
kind,
raddr, // (" raddr ", addr, " rport ", port)
tcptype, // (" tcptype ", tcptype)
_, // (" generation ", generation)
_, // (" network-id ", network_id)
ufrag, // (" ufrag ", ufrag)
_, // ("network-cost", network_cost)
)| {
Candidate::from_parts(
found,
comp_id,
proto,
prio, // remote candidates calculate prio on their side
SocketAddr::from((addr, port)),
kind,
raddr.map(|(_, addr, _, port)| SocketAddr::from((addr, port))),
tcptype.map(|(_, tcptype)| tcptype),
ufrag.map(|(_, u)| u),
)
},
)
}
/// Parser for a=candidate lines.
pub(crate) fn candidate_attribute() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
(string("a="), candidate(), line_end()).map(|(_, c, _)| c)
}
/// Session line with a key we ignore (spec says we should validate them, but meh).
fn ignored_session_line() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
// TODO: there must be a better way
let ignored = choice((
token('i'),
token('u'),
token('e'),
token('p'),
token('c'),
token('z'),
token('k'),
));
line(ignored, any_value()).map(|_| ())
}
// /////////////////////////////////////////////////// Media description
/// A m= section with attributes, until next m= or EOF
fn media_parser() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
(
media_line(),
optional(typed_line('c', any_value())), // c=IN IP4 0.0.0.0
optional(bandwidth_line()), // b=AS:2500
many::, _, _>(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::::message_format(err));
}
Ok(m)
})
}
/// The m= line
// dormammu:
// m=audio 9 UDP/TLS/RTP/SAVPF 111
// m=video 9 UDP/TLS/RTP/SAVPF 96 97 125 107 100 101
// m=application 9 DTLS/SCTP 5000
// chrome:
// m=audio 64205 UDP/TLS/RTP/SAVPF 111
// m=video 53151 UDP/TLS/RTP/SAVPF 96 97 125 107 100 101
// m=application 54055 DTLS/SCTP 5000
//
// newer chrome:
// m=application 9 UDP/DTLS/SCTP webrtc-datachannel
fn media_line() -> impl Parser)>
where
Input: Stream,
Input::Error: ParseError,
{
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::()
.map(Pt::from)
.map_err(StreamErrorFor::::message_format)
});
// m= ...
// is:
// where either matches a=rtpmap: or a=sctpmap:
typed_line(
'm',
(
media_type, // type: audio, video etc.
token(' '),
not_sp(), // port: just set to 9 or something
token(' '),
proto_line, // proto: or
token(' '),
choice((
attempt(sep_by(parse_pt, token(' '))), //
any_value().map(|_| vec![]),
)),
),
)
.map(|(typ, _, port, _, proto, _, pts)| (typ, port, proto, pts))
}
/// a=foo:bar lines belonging before the first m= line
fn media_attribute_line() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
// a=rtcp:9 IN IP4 0.0.0.0
let rtcp = attribute_line("rtcp", any_value()).map(MediaAttribute::Rtcp);
// a=ice-ufrag:IdNYTNL1fjvjyEzL
let ice_ufrag = attribute_line("ice-ufrag", any_value()).map(MediaAttribute::IceUfrag);
// a=ice-pwd:4d64pT3T1xfwbZvi9fQKjoPb
let ice_pwd = attribute_line("ice-pwd", any_value()).map(MediaAttribute::IcePwd);
// a=ice-options:trickle
let ice_opt = attribute_line("ice-options", any_value()).map(MediaAttribute::IceOptions);
// a=fingerprint:sha-256 45:AD:5C:82:F8:BE:B5:2A:D1:74:A6:16:D0:50:CD:86:9C:97:9D:BD:06:8C:C9:85:C9:CD:AB:2B:A8:56:03:CD
// "sha-1" / "sha-224" / "sha-256" /
// "sha-384" / "sha-512" /
// "md5" / "md2"
let hex_byte = count_min_max(2, 2, hex_digit()).and_then(|x: String| {
u8::from_str_radix(&x, 16).map_err(StreamErrorFor::::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)),
));
// a=setup:actpass
let setup = attribute_line("setup", setup_val).map(MediaAttribute::Setup);
// a=mid:0
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::().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
}),
)
.map(MediaAttribute::SctpPort);
let max_message_size = attribute_line(
"max-message-size",
not_sp::().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
}),
)
.map(MediaAttribute::MaxMessageSize);
// a=sctp-init:
// See draft-hancke-tsvwg-snap (ยง4.2: base64 defined in RFC 4566, i.e. standard with padding)
//
// The base64 value is stored as-is; decoding happens when consumed.
let sctp_init = attribute_line("sctp-init", any_value()).map(MediaAttribute::SctpInit);
// a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
// a=extmap:["/"]
let extmap = attribute_line(
"extmap",
(
many1::(satisfy(|c| c != '/' && c != ' ')).and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::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)),
));
// // a=msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1
// // a=msid:- f78dde68-7055-4e20-bb37-433803dd1ed1
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);
// a=candidate
let cand = candidate_attribute().map(MediaAttribute::Candidate);
// a=end-of-candidates
let endof = attribute_line_flag("end-of-candidates").map(|_| MediaAttribute::EndOfCandidates);
let pt = || {
not_sp().and_then(|s| {
let pt = s
.parse::()
.map_err(StreamErrorFor::::message_format)?;
// RTP payload type is 7-bit; reject out-of-range values rather than panicking later
// when PTs are claimed against a 128-entry table.
if pt > 127 {
return Err(StreamErrorFor::::message_format(format!(
"payload type out of range (0-127): {pt}"
)));
}
Ok(Pt::from(pt))
})
};
// a=rtpmap:111 opus/48000/2
let rtpmap = attribute_line(
"rtpmap",
(
pt(),
token(' '),
many1::(satisfy(|c| c != '/' && c != '\r' && c != '\n')),
token('/'),
many1::(satisfy(|c| c != '/' && c != '\r' && c != '\n')).and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
}),
optional((
token('/'),
any_value().and_then(|s| {
s.parse::()
.map_err(StreamErrorFor::::message_format)
}),
)), // only audio has the last /2 (channels)
),
)
.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,
},
}
});
// a=rtcp-fb:111 transport-cc
// a=rtcp-fb:111 ccm fir
// a=rtcp-fb:111 nack
// a=rtcp-fb:111 nack pli
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(), token(';')).map(FormatParam::parse_pairs);
// a=fmtp:111 minptime=10; useinbandfec=1
// a=fmtp:111 minptime=10;useinbandfec=1
let fmtp1 = attribute_line("fmtp", (pt(), token(' '), fmtp_param))
.map(|(pt, _, values)| MediaAttribute::Fmtp { pt, values });
// a=fmtp:101 0-15
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)));
// a=rid: [pt=;]=
let rid = attribute_line(
"rid",
(
name().map(RestrictionId::new_active),
token(' '),
choice((string("send"), string("recv"))),
optional((
token(' '),
optional((
string("pt="),
sep_by1::, _, _, _>(pt(), token(',')),
// TODO this is not really optional when there is
// a restriction part. It means we are incorrectly
// allowing this: a=rid:foo send pt=111max-br=64000
optional(token(';')),
)),
sep_by::, _, _, _>(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::>, _, _, _>(
sep_by1(
optional(token('~'))
.and(name())
.map(|x| (x.1, x.0.is_none())),
token(','),
),
token(';'),
),
)
};
// Parser guarantee that it's send => recv or recv => send.
let simul2 = choice((
(simul1("send"), optional((token(' '), simul1("recv")))),
(simul1("recv"), optional((token(' '), simul1("send")))),
));
// a=simulcast: ;, [same]
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>) {
for group in groups {
// TODO: Properly support rid alternatives, for now we ignore any alternatives
// provided and use the first rid.
let first = group.into_iter().next();
if let Some(rid) = first.map(|(rid, active)| RestrictionId::new(rid, active)) {
to.0.push(SimulcastLayer {
restriction_id: rid,
attributes: None,
});
}
}
}
{
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,
})
});
// a=ssrc-group:FID 1111 2222
let ssrc_group = attribute_line(
"ssrc-group",
(
not_sp(),
token(' '),
sep_by1(
not_sp().and_then(|s| {
s.parse::()
.map(Ssrc::from)
.map_err(StreamErrorFor::::message_format)
}),
token(' '),
),
),
)
.map(|(semantics, _, ssrcs)| MediaAttribute::SsrcGroup { semantics, ssrcs });
// a=ssrc:3948621874 cname:xeXs3aE9AOBn00yJ
// a=ssrc:3948621874 msid:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK f78dde68-7055-4e20-bb37-433803dd1ed1
// a=ssrc:3948621874 mslabel:5UUdwiuY7OML2EkQtF38pJtNP5v7In1LhjEK
// a=ssrc:3948621874 label:f78dde68-7055-4e20-bb37-433803dd1ed1
let ssrc = attribute_line(
"ssrc",
(
not_sp().and_then(|s| {
s.parse::()
.map(Ssrc::from)
.map_err(StreamErrorFor::::message_format)
}),
token(' '),
many1::(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);
// Split into two choice() calls because combine's choice macro supports at most 25 parsers.
choice((
attempt(choice((
attempt(ice_ufrag),
attempt(ice_pwd),
attempt(ice_opt),
attempt(finger),
attempt(setup),
attempt(mid),
attempt(sctp_port),
attempt(max_message_size),
attempt(sctp_init),
attempt(extmap),
attempt(direction),
attempt(msid),
attempt(rtcp),
))),
attempt(choice((
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,
))
}
// /////////////////////////////////////////////////// Generic things below
/// A specific line
fn typed_line(expected: char, val: Pval) -> impl Parser
where
Input: Stream,
Pval: Parser,
Input::Error: ParseError,
{
line(token(expected), val)
}
/// A line with some parser for value and parser for type.
fn line(typ: Ptyp, val: Pval) -> impl Parser
where
Ptyp: Parser,
Pval: Parser,
Input: Stream,
Input::Error: ParseError,
{
attempt((typ, token('='), val, line_end()))
.map(|(_, _, value, _)| value)
.message("sdp line")
}
/// alphanumeric name.
fn name() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
many1(satisfy(|c: char| c.is_alphanumeric()))
}
/// Not SP, \r or \n
fn not_sp() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
many1(satisfy(|c| c != ' ' && c != '\r' && c != '\n'))
}
/// Any value that isn't \r or \n.
fn any_value() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
many1(satisfy(|c| c != '\r' && c != '\n'))
}
/// We discovered a stray \n in safari SDP. This line end handles \r\n, \n or EOF.
fn line_end() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
choice((crlf().map(|_| ()), newline().map(|_| ()), eof()))
}
// minptime=10
fn key_val() -> impl Parser
where
Input: Stream,
Input::Error: ParseError,
{
(
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::*;
use crate::format::Codec;
use crate::io::Protocol;
#[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 rtpmap_payload_type_over_127_is_rejected() {
// RTP payload type is 7-bit. An out-of-range PT in a=rtpmap must not be accepted as an
// RtpMap; otherwise it becomes a PayloadParams that panics when claimed against a
// 128-entry table. It instead falls through to Unused and is ignored (the m-line then
// fails its own "missing a=rtpmap" check). 127 is the largest valid PT.
let bad = media_attribute_line().parse("a=rtpmap:200 opus/48000/2");
assert!(
!matches!(bad, Ok((MediaAttribute::RtpMap { .. }, _))),
"out-of-range rtpmap PT must not parse as RtpMap: {bad:?}"
);
assert!(matches!(
media_attribute_line().parse("a=rtpmap:127 opus/48000/2"),
Ok((MediaAttribute::RtpMap { .. }, _))
));
}
#[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 session_parser_named_session() {
// A session name other than "-".
let sdp = "v=0\r\n\
o=publisher 4489045141692799359 2 IN IP4 127.0.0.1\r\n\
s=NamedPublishSession\r\n\
t=0 0\r\n\
a=group:BUNDLE 0 1\r\n\
a=extmap-allow-mixed\r\n\
a=msid-semantic: WMS\r\n";
assert_eq!(
session_parser().parse(sdp),
Ok((
Session {
id: 4_489_045_141_692_799_359.into(),
bw: None,
attrs: vec![
SessionAttribute::Group {
typ: "BUNDLE".into(),
mids: vec!["0".into(), "1".into()],
},
SessionAttribute::Unused("extmap-allow-mixed".into()),
SessionAttribute::Unused("msid-semantic: WMS".into()),
],
},
""
))
);
}
#[test]
fn session_parser_space_session_name() {
// "If a session has no meaningful name, the value "s= " SHOULD be used
// (i.e., a single space as the session name)." (RFC 8866 section 5.3)
let sdp = "v=0\n\
o=- 6564425948916445306 2 IN IP4 127.0.0.1\n\
s= \n\
t=0 0\n\
";
assert_eq!(
session_parser().parse(sdp),
Ok((
Session {
id: 6_564_425_948_916_445_306.into(),
bw: None,
attrs: vec![],
},
""
))
);
}
#[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_offer_sdp_ffmpeg_whip() {
// ffmpeg -re \
// -f lavfi -i "testsrc=size=640x480:rate=30" \
// -f lavfi -i "sine=frequency=440:sample_rate=48000" \
// -c:v libx264 -profile:v baseline -pix_fmt yuv420p -g 60 -tune zerolatency \
// -c:a libopus -ar 48000 -ac 2 \
// -f whip "http://127.0.0.1:8080/whip"
let sdp = "v=0\r\n\
o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1\r\n\
s=FFmpegPublishSession\r\n\
t=0 0\r\n\
a=group:BUNDLE 0 1\r\n\
a=extmap-allow-mixed\r\n\
a=msid-semantic: WMS\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\
c=IN IP4 0.0.0.0\r\n\
a=ice-ufrag:08a9c3f4\r\n\
a=ice-pwd:a202d23be47d1decaa6f67d04d4e6c16\r\n\
a=fingerprint:sha-256 21:69:7C:46:D3:DC:4D:D3:66:61:5C:C5:6A:0B:F7:36\
:BD:91:A9:2A:FF:BC:1A:59:D9:38:38:C8:E2:A4:8D:AF\r\n\
a=setup:passive\r\n\
a=mid:0\r\n\
a=sendonly\r\n\
a=msid:FFmpeg audio\r\n\
a=rtcp-mux\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=ssrc:3166090378 cname:FFmpeg\r\n\
a=ssrc:3166090378 msid:FFmpeg audio\r\n\
m=video 9 UDP/TLS/RTP/SAVPF 106 105\r\n\
c=IN IP4 0.0.0.0\r\n\
a=ice-ufrag:08a9c3f4\r\n\
a=ice-pwd:a202d23be47d1decaa6f67d04d4e6c16\r\n\
a=fingerprint:sha-256 21:69:7C:46:D3:DC:4D:D3:66:61:5C:C5:6A:0B:F7:36\
:BD:91:A9:2A:FF:BC:1A:59:D9:38:38:C8:E2:A4:8D:AF\r\n\
a=setup:passive\r\n\
a=mid:1\r\n\
a=sendonly\r\n\
a=msid:FFmpeg video\r\n\
a=rtcp-mux\r\n\
a=rtcp-rsize\r\n\
a=rtpmap:106 H264/90000\r\n\
a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001e\r\n\
a=rtcp-fb:106 nack\r\n\
a=rtpmap:105 rtx/90000\r\n\
a=fmtp:105 apt=106\r\n\
a=ssrc-group:FID 3166090379 3166090380\r\n\
a=ssrc:3166090379 cname:FFmpeg\r\n\
a=ssrc:3166090379 msid:FFmpeg video\r\n\
";
let (sdp, _) = sdp_parser().parse(sdp).unwrap();
assert_eq!(sdp.media_lines.len(), 2);
assert_eq!(sdp.media_lines[0].mid().to_string(), "0");
assert_eq!(sdp.media_lines[1].mid().to_string(), "1");
}
#[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 bare_static_audio_pts_are_resolved() {
let sdp = "v=0\r\n\
o=- 1 1 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
a=group:BUNDLE 0\r\n\
m=audio 9 UDP/TLS/RTP/SAVPF 0 8 9 13\r\n\
c=IN IP4 0.0.0.0\r\n\
a=setup:actpass\r\n\
a=sendrecv\r\n\
a=mid:0\r\n\
";
let (sdp, _) = sdp_parser()
.parse(sdp)
.expect("static PTs must not require an a=rtpmap line");
sdp.assert_consistency()
.expect("bare static PTs are consistent");
let params = sdp.media_lines[0].rtp_params();
let definitions: Vec<_> = params
.iter()
.map(|p| (*p.pt(), p.spec().codec, p.spec().clock_rate))
.collect();
assert_eq!(
definitions,
vec![
(0, Codec::PCMU, Frequency::EIGHT_KHZ),
(8, Codec::PCMA, Frequency::EIGHT_KHZ),
(9, Codec::G722, Frequency::SIXTEEN_KHZ),
(13, Codec::CN, Frequency::EIGHT_KHZ),
]
);
}
#[test]
fn parse_candidates() {
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"));
let a = "a=candidate:3684617590 1 udp 2122260223 10.217.229.219 50028 typ host generation 0 network-id 1 network-cost 900";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.addr(), "10.217.229.219:50028".parse().unwrap());
let a = "a=candidate:387183333 1 udp 1686052607 113.185.55.72 31267 typ srflx raddr 10.217.229.219 rport 50028 generation 0 network-id 1 network-cost 900";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.addr(), "113.185.55.72:31267".parse().unwrap());
let a = "a=candidate:2501718406 1 tcp 1518280447 10.217.229.219 9 typ host tcptype active generation 0 network-id 1 network-cost 900";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.addr(), "10.217.229.219:9".parse().unwrap());
let a = "a=candidate:387183333 1 udp 1686052607 113.185.55.72 41775 typ srflx raddr 10.217.229.219 rport 50028 generation 0 network-id 1 network-cost 900";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.addr(), "113.185.55.72:41775".parse().unwrap());
let a = "a=candidate:3936339338 1 udp 2122265343 fd00:f8aa:3ff5:5914:90bf:3c5:3378:9d4b 59125 \
typ host generation 0 network-id 4 network-cost 50";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(
c.addr(),
"[fd00:f8aa:3ff5:5914:90bf:3c5:3378:9d4b]:59125"
.parse()
.unwrap()
);
assert_eq!(c.proto(), Protocol::Udp);
let a = "a=candidate:2113932030 1 tcp 2113932030 142.250.82.253 19305 \
typ host tcptype passive generation 0";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.addr(), "142.250.82.253:19305".parse().unwrap());
assert_eq!(c.proto(), Protocol::Tcp);
assert_eq!(c.tcptype(), Some(TcpType::Passive));
let a =
"a=candidate:2113932029 1 ssltcp 2113932029 142.250.82.252 443 typ host generation 0";
let (c, _) = candidate_attribute().parse(a).unwrap();
assert_eq!(c.addr(), "142.250.82.252:443".parse().unwrap());
assert_eq!(c.proto(), Protocol::SslTcp);
assert_eq!(c.tcptype(), None);
}
#[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();
// Firefox 'a=setup' attribute can be missing
// the "a=setup:" sdp attribute (which should be mandatory)
// - https://www.rfc-editor.org/rfc/rfc5763#section-5
// - https://www.rfc-editor.org/rfc/rfc4145
assert!(sdp.media_lines[0].setup().is_none()); // must not crash
}
#[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");
}
/// Test parsing an SDP offer containing H.265 with sprop-max-don-diff.
/// Verifies the fmtp line is correctly parsed into FormatParam::SpropMaxDonDiff
/// alongside H265ProfileTierLevel.
#[test]
fn parse_sdp_h265_with_sprop_max_don_diff() {
// Fingerprint split across lines to stay within 110 cols.
let sdp = concat!(
"v=0\r\n",
"o=- 1234567890 2 IN IP4 127.0.0.1\r\n",
"s=-\r\n",
"t=0 0\r\n",
"a=group:BUNDLE 0\r\n",
"a=fingerprint:sha-256 ",
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:",
"66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:",
"22:33:44:55:66:77:88:99\r\n",
"a=msid-semantic: WMS\r\n",
"m=video 9 UDP/TLS/RTP/SAVPF 102 103\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:abcd\r\n",
"a=ice-pwd:abcdefghijklmnopqrstuv\r\n",
"a=ice-options:trickle\r\n",
"a=fingerprint:sha-256 ",
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:",
"66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:",
"22:33:44:55:66:77:88:99\r\n",
"a=setup:actpass\r\n",
"a=mid:0\r\n",
"a=sendrecv\r\n",
"a=rtpmap:102 H265/90000\r\n",
"a=fmtp:102 profile-id=1;tier-flag=0;",
"level-id=93;sprop-max-don-diff=32\r\n",
"a=rtpmap:103 rtx/90000\r\n",
"a=fmtp:103 apt=102\r\n",
);
let (sdp, _remaining) = sdp_parser().parse(sdp).expect("SDP should parse");
// Find the video media section
assert_eq!(sdp.media_lines.len(), 1);
let media = &sdp.media_lines[0];
// Find fmtp for PT 102 (H.265)
let fmtp_values: Vec<_> = media
.attrs
.iter()
.filter_map(|a| {
if let MediaAttribute::Fmtp { pt, values } = a {
if *pt == 102.into() {
return Some(values.clone());
}
}
None
})
.flatten()
.collect();
// Should contain H265ProfileTierLevel
assert!(
fmtp_values
.iter()
.any(|p| matches!(p, FormatParam::H265ProfileTierLevel(_))),
"Should have H265ProfileTierLevel param"
);
// Should contain SpropMaxDonDiff(32)
assert!(
fmtp_values
.iter()
.any(|p| matches!(p, FormatParam::SpropMaxDonDiff(32))),
"Should have SpropMaxDonDiff(32) param, got: {:?}",
fmtp_values
);
// Verify PTL values
if let Some(FormatParam::H265ProfileTierLevel(ptl)) = fmtp_values
.iter()
.find(|p| matches!(p, FormatParam::H265ProfileTierLevel(_)))
{
assert_eq!(ptl.profile_id(), 1);
assert_eq!(ptl.tier_flag(), 0);
assert_eq!(ptl.level_id(), 93);
} else {
panic!("H265ProfileTierLevel not found");
}
}
}
// Safari addTransceiver('audio', {direction: 'sendonly'}))
// v=0
// o=- 1625652694357831865 2 IN IP4 127.0.0.1
// s=-
// t=0 0
// a=group:BUNDLE 0
// a=msid-semantic: WMS
// m=audio 9 UDP/TLS/RTP/SAVPF 111 103 9 102 0 8 105 13 110 113 126
// c=IN IP4 0.0.0.0\na=rtcp:9 IN IP4 0.0.0.0
// a=ice-ufrag:CTNF
// a=ice-pwd:XbIkl+k8mwLz60TFwb9crzbz
// a=ice-options:trickle
// a=fingerprint:sha-256 77:8C:32:B8:DA:4E:78:64:C8:4A:2A:E2:1D:60:2A:83:6B:51:B8:D5:EE:5A:ED:75:4E:C6:98:2D:78:4D:94:D8
// a=setup:actpass
// a=mid:0
// a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
// a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
// a=sendonly
// a=msid:- 39a7c3c3-ab8c-4b25-a47b-db52d89c2db1
// a=rtcp-mux
// a=rtpmap:111 opus/48000/2
// a=rtcp-fb:111 transport-cc
// a=fmtp:111 minptime=10;useinbandfec=1
// a=rtpmap:103 ISAC/16000
// a=rtpmap:9 G722/8000
// a=rtpmap:102 ILBC/8000
// a=rtpmap:0 PCMU/8000
// a=rtpmap:8 PCMA/8000
// a=rtpmap:105 CN/16000
// a=rtpmap:13 CN/8000
// a=rtpmap:110 telephone-event/48000
// a=rtpmap:113 telephone-event/16000
// a=rtpmap:126 telephone-event/8000
// a=ssrc:457025658 cname:6OQ+jzZE+UnQgSUr
// a=ssrc:457025658 msid:- 39a7c3c3-ab8c-4b25-a47b-db52d89c2db1
// a=ssrc:457025658 mslabel:-
// a=ssrc:457025658 label:39a7c3c3-ab8c-4b25-a47b-db52d89c2db1
// Chrome addTransceiver('audio', {direction: 'sendonly'}))
// v=0
// o=- 6740661649996974832 2 IN IP4 127.0.0.1
// s=-
// t=0 0
// a=group:BUNDLE 0
// a=msid-semantic: WMS
// m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
// c=IN IP4 0.0.0.0
// a=rtcp:9 IN IP4 0.0.0.0
// a=ice-ufrag:Y28C
// a=ice-pwd:W6fHYINgGi9QF0BHdM/kchTW
// a=ice-options:trickle
// a=fingerprint:sha-256 E0:C0:2D:52:8D:FA:14:69:A0:A5:5D:63:E0:82:92:DB:37:38:D2:F3:12:D0:1F:4E:E5:6F:1A:F5:C3:97:6B:32
// a=setup:actpass
// a=mid:0
// a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
// a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
// a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
// a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
// a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
// a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
// a=sendonly
// a=msid:- 7a08dda6-518f-4027-b707-410a6d414176
// a=rtcp-mux
// a=rtpmap:111 opus/48000/2
// a=rtcp-fb:111 transport-cc
// a=fmtp:111 minptime=10;useinbandfec=1
// a=rtpmap:103 ISAC/16000
// a=rtpmap:104 ISAC/32000
// a=rtpmap:9 G722/8000
// a=rtpmap:0 PCMU/8000
// a=rtpmap:8 PCMA/8000
// a=rtpmap:106 CN/32000
// a=rtpmap:105 CN/16000
// a=rtpmap:13 CN/8000
// a=rtpmap:110 telephone-event/48000
// a=rtpmap:112 telephone-event/32000
// a=rtpmap:113 telephone-event/16000
// a=rtpmap:126 telephone-event/8000
// a=ssrc:2147603131 cname:TbS1Ajv9obq6/63I
// a=ssrc:2147603131 msid:- 7a08dda6-518f-4027-b707-410a6d414176
// a=ssrc:2147603131 mslabel:-
// a=ssrc:2147603131 label:7a08dda6-518f-4027-b707-410a6d414176
// Chrome add video
// pc.addTransceiver("video", {
// direction: 'sendonly',
// sendEncodings: [
// { rid: 'hi' },
// { rid: 'lo', scaleResolutionDownBy: 2 },
// ]
// })
// v=0
// o=- 2847298852000198709 3 IN IP4 127.0.0.1
// s=-
// t=0 0
// a=group:BUNDLE 0
// a=extmap-allow-mixed
// a=msid-semantic: WMS
// m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 104 105 106 107 108 109 127 125 39 40 45 46 98 99 100 101 112 113 114
// c=IN IP4 0.0.0.0
// a=rtcp:9 IN IP4 0.0.0.0
// a=ice-ufrag:numD
// a=ice-pwd:L/hHurqERpVUACiI8To+vXLv
// a=ice-options:trickle
// a=fingerprint:sha-256 A5:79:72:59:3D:32:74:9C:44:70:DE:39:15:C3:99:51:32:6E:0D:F0:60:DD:2F:31:90:E5:96:B4:1D:CA:48:E1
// a=setup:actpass
// a=mid:0
// a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
// a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
// a=extmap:3 urn:3gpp:video-orientation
// a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
// a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
// a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
// a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
// a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
// a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
// a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
// a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
// a=extmap:13 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension
// a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00
// a=sendonly
// a=msid:- 7d27ad91-4770-4a4d-a053-2b06233753d2
// a=rtcp-mux
// a=rtcp-rsize
// a=rtpmap:96 VP8/90000
// a=rtcp-fb:96 goog-remb
// a=rtcp-fb:96 transport-cc
// a=rtcp-fb:96 ccm fir
// a=rtcp-fb:96 nack
// a=rtcp-fb:96 nack pli
// a=rtpmap:97 rtx/90000
// a=fmtp:97 apt=96
// a=rtpmap:102 H264/90000
// a=rtcp-fb:102 goog-remb
// a=rtcp-fb:102 transport-cc
// a=rtcp-fb:102 ccm fir
// a=rtcp-fb:102 nack
// a=rtcp-fb:102 nack pli
// a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
// a=rtpmap:103 rtx/90000
// a=fmtp:103 apt=102
// a=rtpmap:104 H264/90000
// a=rtcp-fb:104 goog-remb
// a=rtcp-fb:104 transport-cc
// a=rtcp-fb:104 ccm fir
// a=rtcp-fb:104 nack
// a=rtcp-fb:104 nack pli
// a=fmtp:104 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
// a=rtpmap:105 rtx/90000
// a=fmtp:105 apt=104
// a=rtpmap:106 H264/90000
// a=rtcp-fb:106 goog-remb
// a=rtcp-fb:106 transport-cc
// a=rtcp-fb:106 ccm fir
// a=rtcp-fb:106 nack
// a=rtcp-fb:106 nack pli
// a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
// a=rtpmap:107 rtx/90000
// a=fmtp:107 apt=106
// a=rtpmap:108 H264/90000
// a=rtcp-fb:108 goog-remb
// a=rtcp-fb:108 transport-cc
// a=rtcp-fb:108 ccm fir
// a=rtcp-fb:108 nack
// a=rtcp-fb:108 nack pli
// a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
// a=rtpmap:109 rtx/90000
// a=fmtp:109 apt=108
// a=rtpmap:127 H264/90000
// a=rtcp-fb:127 goog-remb
// a=rtcp-fb:127 transport-cc
// a=rtcp-fb:127 ccm fir
// a=rtcp-fb:127 nack
// a=rtcp-fb:127 nack pli
// a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
// a=rtpmap:125 rtx/90000
// a=fmtp:125 apt=127
// a=rtpmap:39 H264/90000
// a=rtcp-fb:39 goog-remb
// a=rtcp-fb:39 transport-cc
// a=rtcp-fb:39 ccm fir
// a=rtcp-fb:39 nack
// a=rtcp-fb:39 nack pli
// a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
// a=rtpmap:40 rtx/90000
// a=fmtp:40 apt=39
// a=rtpmap:45 AV1/90000
// a=rtcp-fb:45 goog-remb
// a=rtcp-fb:45 transport-cc
// a=rtcp-fb:45 ccm fir
// a=rtcp-fb:45 nack
// a=rtcp-fb:45 nack pli
// a=fmtp:45 level-idx=5;profile=0;tier=0
// a=rtpmap:46 rtx/90000
// a=fmtp:46 apt=45
// a=rtpmap:98 VP9/90000
// a=rtcp-fb:98 goog-remb
// a=rtcp-fb:98 transport-cc
// a=rtcp-fb:98 ccm fir
// a=rtcp-fb:98 nack
// a=rtcp-fb:98 nack pli
// a=fmtp:98 profile-id=0
// a=rtpmap:99 rtx/90000
// a=fmtp:99 apt=98
// a=rtpmap:100 VP9/90000
// a=rtcp-fb:100 goog-remb
// a=rtcp-fb:100 transport-cc
// a=rtcp-fb:100 ccm fir
// a=rtcp-fb:100 nack
// a=rtcp-fb:100 nack pli
// a=fmtp:100 profile-id=2
// a=rtpmap:101 rtx/90000
// a=fmtp:101 apt=100
// a=rtpmap:112 red/90000
// a=rtpmap:113 rtx/90000
// a=fmtp:113 apt=112
// a=rtpmap:114 ulpfec/90000
// a=rid:hi send
// a=rid:lo send
// a=simulcast:send hi;lo
// DataChannel only in Safari
// v=0
// o=- 4611516372927609806 2 IN IP4 127.0.0.1
// s=-
// t=0 0
// a=group:BUNDLE 0
// a=extmap-allow-mixed
// a=msid-semantic: WMS
// m=application 9 UDP/DTLS/SCTP webrtc-datachannel
// c=IN IP4 0.0.0.0
// a=ice-ufrag:HhS+
// a=ice-pwd:FhYTGhlAtKCe6KFIX8b+AThW
// a=ice-options:trickle
// 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
// a=setup:actpass
// a=mid:0
// a=sctp-port:5000
// a=max-message-size:262144