///|
pub(all) enum DtlsSetup {
ActPass
Active
Passive
HoldConn
} derive(Debug, Eq)
///|
pub fn DtlsSetup::parse(value : String) -> DtlsSetup raise SdpError {
match value {
"actpass" => ActPass
"active" => Active
"passive" => Passive
"holdconn" => HoldConn
_ => raise InvalidSyntax("unknown DTLS setup role")
}
}
///|
pub fn DtlsSetup::to_string(self : DtlsSetup) -> String {
match self {
ActPass => "actpass"
Active => "active"
Passive => "passive"
HoldConn => "holdconn"
}
}
///|
fn is_hex_digit(value : UInt16) -> Bool {
(value >= '0' && value <= '9') ||
(value >= 'a' && value <= 'f') ||
(value >= 'A' && value <= 'F')
}
///|
pub struct DtlsFingerprint {
algorithm : String
value : String
} derive(Debug, Eq)
///|
pub fn DtlsFingerprint::new(
algorithm~ : String,
value~ : String,
) -> DtlsFingerprint raise SdpError {
let octets = match algorithm.to_lower() {
"sha-256" => 32
"sha-384" => 48
"sha-512" => 64
_ => raise UnsupportedAttribute("unsupported DTLS fingerprint algorithm")
}
if value.length() != octets * 3 - 1 {
raise InvalidSyntax("DTLS fingerprint has the wrong number of octets")
}
for index = 0; index < value.length(); index = index + 1 {
if index % 3 == 2 {
if value[index] != ':' {
raise InvalidSyntax("DTLS fingerprint octets must use colons")
}
} else if !is_hex_digit(value[index]) {
raise InvalidSyntax("DTLS fingerprint contains a non-hex digit")
}
}
{ algorithm: algorithm.to_lower(), value, }
}
///|
pub fn DtlsFingerprint::algorithm(self : DtlsFingerprint) -> String {
self.algorithm
}
///|
pub fn DtlsFingerprint::value(self : DtlsFingerprint) -> String {
self.value
}
///|
fn DtlsFingerprint::marshal(self : DtlsFingerprint) -> String {
self.algorithm + " " + self.value
}
///|
fn DtlsFingerprint::parse(value : String) -> DtlsFingerprint raise SdpError {
let tokens = space_tokens(value)
if tokens.length() != 2 {
raise InvalidSyntax("DTLS fingerprint requires algorithm and value")
}
DtlsFingerprint::new(algorithm=tokens[0], value=tokens[1])
}
///|
pub struct DataChannelParameters {
mid : String
bundle_mids : Array[String]
ice_credentials : @ice.IceCredentials
fingerprint : DtlsFingerprint
setup : DtlsSetup
candidates : Array[String]
end_of_candidates : Bool
sctp_port : UInt16
max_message_size : UInt64
} derive(Debug, Eq)
///|
pub fn DataChannelParameters::mid(self : DataChannelParameters) -> String {
self.mid
}
///|
pub fn DataChannelParameters::bundle_mids(
self : DataChannelParameters,
) -> Array[String] {
self.bundle_mids.copy()
}
///|
pub fn DataChannelParameters::ice_credentials(
self : DataChannelParameters,
) -> @ice.IceCredentials {
self.ice_credentials
}
///|
pub fn DataChannelParameters::fingerprint(
self : DataChannelParameters,
) -> DtlsFingerprint {
self.fingerprint
}
///|
pub fn DataChannelParameters::setup(self : DataChannelParameters) -> DtlsSetup {
self.setup
}
///|
pub fn DataChannelParameters::candidates(
self : DataChannelParameters,
) -> Array[String] {
self.candidates.copy()
}
///|
pub fn DataChannelParameters::end_of_candidates(
self : DataChannelParameters,
) -> Bool {
self.end_of_candidates
}
///|
pub fn DataChannelParameters::sctp_port(self : DataChannelParameters) -> UInt16 {
self.sctp_port
}
///|
pub fn DataChannelParameters::max_message_size(
self : DataChannelParameters,
) -> UInt64 {
self.max_message_size
}
///|
pub fn DataChannelParameters::validate_for_type(
self : DataChannelParameters,
sdp_type : SdpType,
) -> Unit raise SdpError {
match sdp_type {
Offer if self.setup != ActPass =>
raise InvalidSyntax("initial data-channel offer must use setup:actpass")
Answer | Pranswer if self.setup != Active && self.setup != Passive =>
raise InvalidSyntax("data-channel answer must choose active or passive")
Rollback =>
raise InvalidSyntax("rollback descriptions must not contain SDP")
_ => ()
}
}
///|
fn attribute_value(
document : SdpDocument,
media : MediaDescription,
key : String,
) -> String raise SdpError {
let media_attributes = media.attributes(key)
if media_attributes.length() > 1 {
raise InvalidSyntax("duplicate media-level a=\{key} attribute")
}
if media_attributes.length() == 1 {
return match media_attributes[0].value {
Some(value) => value
None => raise InvalidSyntax("a=\{key} requires a value")
}
}
let session_attributes = document.session_attributes(key)
if session_attributes.length() > 1 {
raise InvalidSyntax("duplicate session-level a=\{key} attribute")
}
if session_attributes.length() == 1 {
return match session_attributes[0].value {
Some(value) => value
None => raise InvalidSyntax("a=\{key} requires a value")
}
}
raise InvalidSyntax("missing a=\{key} attribute")
}
///|
pub fn SdpDocument::datachannel_parameters(
self : SdpDocument,
) -> DataChannelParameters raise SdpError {
let applications = self.media_descriptions.filter(media => {
media.media_kind == "application"
})
if applications.length() != 1 {
raise InvalidSyntax(
"JSEP data channel requires one application media section",
)
}
let media = applications[0]
if media.protocol != "UDP/DTLS/SCTP" && media.protocol != "TCP/DTLS/SCTP" {
raise UnsupportedAttribute("unsupported data-channel media protocol")
}
if !media.formats.contains("webrtc-datachannel") {
raise UnsupportedAttribute("missing webrtc-datachannel media format")
}
let mid = attribute_value(self, media, "mid")
if mid.is_empty() || mid.contains(" ") {
raise InvalidSyntax("invalid data-channel MID")
}
let groups = self.session_attributes("group")
let mut bundle_mids : Array[String] = []
for group in groups {
match group.value {
Some(value) => {
let tokens = space_tokens(value)
if !tokens.is_empty() && tokens[0] == "BUNDLE" {
if bundle_mids.length() > 0 {
raise InvalidSyntax("duplicate BUNDLE group")
}
bundle_mids = tokens[1:].to_owned()
}
}
None => raise InvalidSyntax("a=group requires a value")
}
}
if bundle_mids.is_empty() || !bundle_mids.contains(mid) {
raise InvalidSyntax("data-channel MID is not present in a BUNDLE group")
}
let ice_credentials = @ice.IceCredentials::new(
username_fragment=attribute_value(self, media, "ice-ufrag"),
password=attribute_value(self, media, "ice-pwd"),
) catch {
_ => raise InvalidSyntax("invalid ICE credentials")
}
let fingerprint = DtlsFingerprint::parse(
attribute_value(self, media, "fingerprint"),
)
let setup = DtlsSetup::parse(attribute_value(self, media, "setup"))
let sctp_port = decimal_uint64(
attribute_value(self, media, "sctp-port"),
"SCTP port",
)
if sctp_port == 0UL || sctp_port > 0xffffUL {
raise InvalidSyntax("SCTP port must be between 1 and 65535")
}
let max_message_size = match media.attribute("max-message-size") {
Some(attribute) =>
match attribute.value {
Some(value) => decimal_uint64(value, "maximum message size")
None => raise InvalidSyntax("a=max-message-size requires a value")
}
None => 65536UL
}
let candidates : Array[String] = []
for candidate in media.attributes("candidate") {
match candidate.value {
Some(value) if !value.is_empty() => candidates.push(value)
_ => raise InvalidSyntax("a=candidate requires a value")
}
}
let end_of_candidates = media.attribute("end-of-candidates") is Some(_)
{
mid,
bundle_mids,
ice_credentials,
fingerprint,
setup,
candidates,
end_of_candidates,
sctp_port: sctp_port.to_uint16(),
max_message_size,
}
}
///|
pub fn SdpDocument::new_datachannel(
session_id~ : UInt64,
session_version~ : UInt64,
mid~ : String,
ice_credentials~ : @ice.IceCredentials,
fingerprint~ : DtlsFingerprint,
setup~ : DtlsSetup,
candidates? : Array[String] = [],
end_of_candidates? : Bool = false,
sctp_port? : UInt16 = 5000,
max_message_size? : UInt64 = 65536UL,
media_sections? : Array[RtpMediaParameters] = [],
application_index? : Int = 0,
) -> SdpDocument raise SdpError {
if mid.is_empty() || mid.contains(" ") {
raise InvalidSyntax("invalid data-channel MID")
}
if sctp_port == 0 {
raise InvalidSyntax("SCTP port must be nonzero")
}
if application_index < 0 || application_index > media_sections.length() {
raise InvalidSyntax("data-channel media index is outside the SDP")
}
let bundle_mids : Array[String] = []
for index = 0; index <= media_sections.length(); index = index + 1 {
let next_mid = if index == application_index {
mid
} else {
let media_index = if index < application_index {
index
} else {
index - 1
}
media_sections[media_index].mid
}
if bundle_mids.contains(next_mid) {
raise InvalidSyntax("duplicate BUNDLE MID")
}
bundle_mids.push(next_mid)
}
let session_fields = [
SdpField::new(kind=b'v', value="0"),
SdpField::new(
kind=b'o',
value="- \{session_id} \{session_version} IN IP4 0.0.0.0",
),
SdpField::new(kind=b's', value="-"),
SdpField::new(kind=b't', value="0 0"),
SdpField::new(kind=b'a', value="group:BUNDLE " + bundle_mids.join(" ")),
SdpField::new(kind=b'a', value="msid-semantic: WMS"),
]
let media = MediaDescription::new(
media_kind="application",
port=9,
protocol="UDP/DTLS/SCTP",
formats=["webrtc-datachannel"],
fields=[SdpField::new(kind=b'c', value="IN IP4 0.0.0.0")],
)
media.add_attribute(SdpAttribute::new(key="mid", value=mid))
media.add_attribute(
SdpAttribute::new(
key="ice-ufrag",
value=ice_credentials.username_fragment(),
),
)
media.add_attribute(
SdpAttribute::new(key="ice-pwd", value=ice_credentials.password()),
)
media.add_attribute(SdpAttribute::new(key="ice-options", value="trickle"))
media.add_attribute(
SdpAttribute::new(key="fingerprint", value=fingerprint.marshal()),
)
media.add_attribute(SdpAttribute::new(key="setup", value=setup.to_string()))
media.add_attribute(
SdpAttribute::new(key="sctp-port", value=sctp_port.to_string()),
)
media.add_attribute(
SdpAttribute::new(
key="max-message-size",
value=max_message_size.to_string(),
),
)
for candidate in candidates {
if candidate.is_empty() ||
candidate.contains("\r") ||
candidate.contains("\n") {
raise InvalidSyntax("invalid ICE candidate attribute")
}
media.add_attribute(SdpAttribute::new(key="candidate", value=candidate))
}
if end_of_candidates {
media.add_attribute(SdpAttribute::new(key="end-of-candidates"))
}
let media_descriptions : Array[MediaDescription] = []
let mut media_index = 0
for section in media_sections {
if media_index == application_index {
media_descriptions.push(media)
}
let formats = section.codecs.map(codec => {
codec.payload_type.to_uint().to_string()
})
let rtp_media = MediaDescription::new(
media_kind=section.kind,
port=9,
protocol="UDP/TLS/RTP/SAVPF",
formats~,
fields=[SdpField::new(kind=b'c', value="IN IP4 0.0.0.0")],
)
rtp_media.add_attribute(SdpAttribute::new(key="mid", value=section.mid))
rtp_media.add_attribute(
SdpAttribute::new(
key="ice-ufrag",
value=ice_credentials.username_fragment(),
),
)
rtp_media.add_attribute(
SdpAttribute::new(key="ice-pwd", value=ice_credentials.password()),
)
rtp_media.add_attribute(
SdpAttribute::new(key="ice-options", value="trickle"),
)
rtp_media.add_attribute(
SdpAttribute::new(key="fingerprint", value=fingerprint.marshal()),
)
rtp_media.add_attribute(
SdpAttribute::new(key="setup", value=setup.to_string()),
)
rtp_media.add_attribute(SdpAttribute::new(key="rtcp-mux"))
rtp_media.add_attribute(SdpAttribute::new(key="rtcp-rsize"))
rtp_media.add_attribute(
SdpAttribute::new(key=section.direction.to_string()),
)
match (section.stream_id, section.track_id) {
(Some(stream_id), Some(track_id)) =>
rtp_media.add_attribute(
SdpAttribute::new(key="msid", value=stream_id + " " + track_id),
)
_ => ()
}
for extension in section.header_extensions {
rtp_media.add_attribute(
SdpAttribute::new(
key="extmap",
value=extension.id.to_uint().to_string() + " " + extension.uri,
),
)
}
for codec in section.codecs {
let encoded_payload_type = codec.payload_type.to_uint().to_string()
let mut mapping = encoded_payload_type +
" " +
codec.encoding_name +
"/" +
codec.clock_rate.to_string()
if codec.channels != 1 {
mapping = mapping + "/" + codec.channels.to_string()
}
rtp_media.add_attribute(SdpAttribute::new(key="rtpmap", value=mapping))
if !codec.fmtp.is_empty() {
rtp_media.add_attribute(
SdpAttribute::new(
key="fmtp",
value=encoded_payload_type + " " + codec.fmtp,
),
)
}
for feedback in codec.rtcp_feedback {
rtp_media.add_attribute(
SdpAttribute::new(
key="rtcp-fb",
value=encoded_payload_type + " " + feedback,
),
)
}
}
let send_simulcast_rids : Array[String] = []
let receive_simulcast_rids : Array[String] = []
let simulcast_ssrcs : Array[String] = []
for stream in section.streams {
match stream.rid {
Some(rid) => {
let direction = match stream.rid_direction {
RidSend => "send"
RidRecv => "recv"
}
rtp_media.add_attribute(
SdpAttribute::new(key="rid", value=rid + " " + direction),
)
let encoded_rid = if stream.paused { "~" + rid } else { rid }
match stream.rid_direction {
RidSend => send_simulcast_rids.push(encoded_rid)
RidRecv => receive_simulcast_rids.push(encoded_rid)
}
}
None => ()
}
match (stream.rid_direction, stream.ssrc) {
(RidSend, Some(ssrc)) => simulcast_ssrcs.push(ssrc.to_string())
_ => ()
}
}
if send_simulcast_rids.length() > 1 {
rtp_media.add_attribute(
SdpAttribute::new(
key="simulcast",
value="send " + send_simulcast_rids.join(";"),
),
)
}
if receive_simulcast_rids.length() > 1 {
rtp_media.add_attribute(
SdpAttribute::new(
key="simulcast",
value="recv " + receive_simulcast_rids.join(";"),
),
)
}
if simulcast_ssrcs.length() > 1 {
rtp_media.add_attribute(
SdpAttribute::new(
key="ssrc-group",
value="SIM " + simulcast_ssrcs.join(" "),
),
)
}
match section.cname {
Some(cname) =>
for stream in section.streams {
match stream.ssrc {
Some(ssrc) => {
match stream.rtx_ssrc {
Some(rtx_ssrc) =>
rtp_media.add_attribute(
SdpAttribute::new(
key="ssrc-group",
value="FID " +
ssrc.to_string() +
" " +
rtx_ssrc.to_string(),
),
)
None => ()
}
rtp_media.add_attribute(
SdpAttribute::new(
key="ssrc",
value=ssrc.to_string() + " cname:" + cname,
),
)
match (section.stream_id, section.track_id) {
(Some(stream_id), Some(track_id)) =>
rtp_media.add_attribute(
SdpAttribute::new(
key="ssrc",
value=ssrc.to_string() +
" msid:" +
stream_id +
" " +
track_id,
),
)
_ => ()
}
match stream.rtx_ssrc {
Some(rtx_ssrc) => {
rtp_media.add_attribute(
SdpAttribute::new(
key="ssrc",
value=rtx_ssrc.to_string() + " cname:" + cname,
),
)
match (section.stream_id, section.track_id) {
(Some(stream_id), Some(track_id)) =>
rtp_media.add_attribute(
SdpAttribute::new(
key="ssrc",
value=rtx_ssrc.to_string() +
" msid:" +
stream_id +
" " +
track_id,
),
)
_ => ()
}
}
None => ()
}
}
None => ()
}
}
None => ()
}
for candidate in candidates {
rtp_media.add_attribute(
SdpAttribute::new(key="candidate", value=candidate),
)
}
if end_of_candidates {
rtp_media.add_attribute(SdpAttribute::new(key="end-of-candidates"))
}
media_descriptions.push(rtp_media)
media_index += 1
}
if application_index == media_sections.length() {
media_descriptions.push(media)
}
SdpDocument::new(session_fields~, media_descriptions~)
}
///|
pub fn SessionDescription::document(
self : SessionDescription,
) -> SdpDocument raise SdpError {
if self.sdp_type == Rollback {
if !self.sdp.is_empty() {
raise InvalidSyntax("rollback description must have empty SDP")
}
raise InvalidSyntax("rollback description has no SDP document")
}
SdpDocument::parse(self.sdp)
}
///|
pub fn SessionDescription::datachannel_parameters(
self : SessionDescription,
) -> DataChannelParameters raise SdpError {
let parameters = self.document().datachannel_parameters()
parameters.validate_for_type(self.sdp_type)
parameters
}
///|
pub fn SessionDescription::from_document(
sdp_type~ : SdpType,
document : SdpDocument,
) -> SessionDescription raise SdpError {
let parameters = document.datachannel_parameters()
parameters.validate_for_type(sdp_type)
SessionDescription::new(sdp_type~, sdp=document.marshal())
}