///|
pub struct SdpField {
kind : Byte
value : String
} derive(Debug, Eq)
///|
pub fn SdpField::new(kind~ : Byte, value~ : String) -> SdpField raise SdpError {
if kind < b'a' || kind > b'z' {
raise InvalidSyntax("SDP field kind must be a lowercase ASCII letter")
}
if value.contains("\r") || value.contains("\n") {
raise InvalidSyntax("SDP field value contains a line break")
}
{ kind, value, }
}
///|
pub fn SdpField::kind(self : SdpField) -> Byte {
self.kind
}
///|
pub fn SdpField::value(self : SdpField) -> String {
self.value
}
///|
fn SdpField::marshal(self : SdpField) -> String {
String::from_array([self.kind.to_char()]) + "=" + self.value
}
///|
pub struct SdpAttribute {
key : String
value : String?
} derive(Debug, Eq)
///|
pub fn SdpAttribute::new(
key~ : String,
value? : String,
) -> SdpAttribute raise SdpError {
if key.is_empty() ||
key.contains(":") ||
key.contains(" ") ||
key.contains("\r") ||
key.contains("\n") {
raise InvalidSyntax("invalid SDP attribute key")
}
match value {
Some(value) if value.contains("\r") || value.contains("\n") =>
raise InvalidSyntax("SDP attribute value contains a line break")
_ => ()
}
{ key, value, }
}
///|
pub fn SdpAttribute::key(self : SdpAttribute) -> String {
self.key
}
///|
pub fn SdpAttribute::value(self : SdpAttribute) -> String? {
self.value
}
///|
fn SdpAttribute::parse(value : String) -> SdpAttribute raise SdpError {
match value.split_once(":") {
Some((key, value)) =>
SdpAttribute::new(key=key.to_owned(), value=value.to_owned())
None => SdpAttribute::new(key=value)
}
}
///|
fn SdpAttribute::marshal(self : SdpAttribute) -> String {
match self.value {
Some(value) => self.key + ":" + value
None => self.key
}
}
///|
fn decimal_uint(value : StringView, context : String) -> UInt raise SdpError {
@string.from_str(value) catch {
_ => raise InvalidSyntax("invalid decimal \{context}")
}
}
///|
fn decimal_uint64(
value : StringView,
context : String,
) -> UInt64 raise SdpError {
@string.from_str(value) catch {
_ => raise InvalidSyntax("invalid decimal \{context}")
}
}
///|
fn space_tokens(value : String) -> Array[String] {
value
.split(" ")
.filter(part => !part.is_empty())
.map(part => part.to_owned())
.to_array()
}
///|
pub struct MediaDescription {
media_kind : String
port : UInt16
port_count : UInt16?
protocol : String
formats : Array[String]
fields : Array[SdpField]
} derive(Debug, Eq)
///|
pub fn MediaDescription::new(
media_kind~ : String,
port~ : UInt16,
port_count? : UInt16,
protocol~ : String,
formats~ : Array[String],
fields? : Array[SdpField] = [],
) -> MediaDescription raise SdpError {
if media_kind.is_empty() || protocol.is_empty() || formats.is_empty() {
raise InvalidSyntax("SDP media line has an empty required token")
}
match port_count {
Some(0) => raise InvalidSyntax("SDP media port count must be positive")
_ => ()
}
{
media_kind,
port,
port_count,
protocol,
formats: formats.copy(),
fields: fields.copy(),
}
}
///|
fn MediaDescription::parse(value : String) -> MediaDescription raise SdpError {
let tokens = space_tokens(value)
if tokens.length() < 4 {
raise InvalidSyntax("SDP media line requires at least four tokens")
}
let port_tokens = tokens[1].split("/").to_array()
if port_tokens.length() < 1 || port_tokens.length() > 2 {
raise InvalidSyntax("invalid SDP media port")
}
let port = decimal_uint(port_tokens[0], "media port")
if port > 0xffffU {
raise InvalidSyntax("SDP media port exceeds 65535")
}
let port_count = if port_tokens.length() == 2 {
let count = decimal_uint(port_tokens[1], "media port count")
if count == 0U || count > 0xffffU {
raise InvalidSyntax("invalid SDP media port count")
}
Some(count.to_uint16())
} else {
None
}
match port_count {
Some(count) =>
MediaDescription::new(
media_kind=tokens[0],
port=port.to_uint16(),
port_count=count,
protocol=tokens[2],
formats=tokens[3:].to_owned(),
)
None =>
MediaDescription::new(
media_kind=tokens[0],
port=port.to_uint16(),
protocol=tokens[2],
formats=tokens[3:].to_owned(),
)
}
}
///|
fn MediaDescription::marshal_media_line(self : MediaDescription) -> String {
let port = match self.port_count {
Some(count) => "\{self.port}/\{count}"
None => self.port.to_string()
}
self.media_kind +
" " +
port +
" " +
self.protocol +
" " +
self.formats.join(" ")
}
///|
pub fn MediaDescription::media_kind(self : MediaDescription) -> String {
self.media_kind
}
///|
pub fn MediaDescription::port(self : MediaDescription) -> UInt16 {
self.port
}
///|
pub fn MediaDescription::port_count(self : MediaDescription) -> UInt16? {
self.port_count
}
///|
pub fn MediaDescription::protocol(self : MediaDescription) -> String {
self.protocol
}
///|
pub fn MediaDescription::formats(self : MediaDescription) -> Array[String] {
self.formats.copy()
}
///|
pub fn MediaDescription::fields(self : MediaDescription) -> Array[SdpField] {
self.fields.copy()
}
///|
pub fn MediaDescription::add_field(
self : MediaDescription,
field : SdpField,
) -> Unit {
self.fields.push(field)
}
///|
pub fn MediaDescription::add_attribute(
self : MediaDescription,
attribute : SdpAttribute,
) -> Unit raise SdpError {
self.fields.push(SdpField::new(kind=b'a', value=attribute.marshal()))
}
///|
pub fn MediaDescription::attributes(
self : MediaDescription,
key : String,
) -> Array[SdpAttribute] raise SdpError {
let result : Array[SdpAttribute] = []
for field in self.fields {
if field.kind == b'a' {
let attribute = SdpAttribute::parse(field.value)
if attribute.key == key {
result.push(attribute)
}
}
}
result
}
///|
pub fn MediaDescription::attribute(
self : MediaDescription,
key : String,
) -> SdpAttribute? raise SdpError {
let attributes = self.attributes(key)
if attributes.is_empty() {
None
} else {
Some(attributes[0])
}
}
///|
pub struct SdpDocument {
session_fields : Array[SdpField]
media_descriptions : Array[MediaDescription]
} derive(Debug, Eq)
///|
pub fn SdpDocument::new(
session_fields~ : Array[SdpField],
media_descriptions? : Array[MediaDescription] = [],
) -> SdpDocument raise SdpError {
let document = {
session_fields: session_fields.copy(),
media_descriptions: media_descriptions.copy(),
}
document.validate()
document
}
///|
fn SdpDocument::validate(self : SdpDocument) -> Unit raise SdpError {
if self.session_fields.length() < 4 {
raise InvalidSyntax("SDP is missing mandatory session fields")
}
if self.session_fields[0].kind != b'v' ||
self.session_fields[1].kind != b'o' ||
self.session_fields[2].kind != b's' {
raise InvalidSyntax("SDP must begin with v=, o=, and s= fields")
}
if self.session_fields[0].value != "0" {
raise InvalidSyntax("unsupported SDP version")
}
let mut versions = 0
let mut origins = 0
let mut names = 0
let mut timings = 0
for field in self.session_fields {
match field.kind {
b'v' => versions += 1
b'o' => origins += 1
b's' => names += 1
b't' => timings += 1
b'm' => raise InvalidSyntax("media field appears in session fields")
_ => ()
}
}
if versions != 1 || origins != 1 || names != 1 || timings < 1 {
raise InvalidSyntax("invalid count of mandatory SDP session fields")
}
let origin = space_tokens(self.session_fields[1].value)
if origin.length() != 6 {
raise InvalidSyntax("SDP origin requires six tokens")
}
ignore(decimal_uint64(origin[1], "origin session id"))
ignore(decimal_uint64(origin[2], "origin session version"))
}
///|
pub fn SdpDocument::parse(sdp : String) -> SdpDocument raise SdpError {
if sdp.is_empty() {
raise InvalidSyntax("SDP is empty")
}
let raw_lines = sdp.split("\n").to_array()
let session_fields : Array[SdpField] = []
let media_descriptions : Array[MediaDescription] = []
let mut current_media : MediaDescription? = None
for index = 0; index < raw_lines.length(); index = index + 1 {
let raw = raw_lines[index]
let line = if raw.has_suffix("\r") { raw[0:raw.length() - 1] } else { raw }
if line.is_empty() {
if index == raw_lines.length() - 1 {
continue
}
raise InvalidSyntax("SDP contains an empty line")
}
if line.contains("\r") {
raise InvalidSyntax("SDP contains a bare carriage return")
}
if line.length() < 2 || line[1] != '=' {
raise InvalidSyntax("SDP line does not use the = form")
}
let kind = line[0].to_byte()
let value = line[2:].to_owned()
if kind == b'm' {
let media = MediaDescription::parse(value)
media_descriptions.push(media)
current_media = Some(media)
} else {
let field = SdpField::new(kind~, value~)
match current_media {
Some(media) => media.fields.push(field)
None => session_fields.push(field)
}
}
}
SdpDocument::new(session_fields~, media_descriptions~)
}
///|
pub fn SdpDocument::marshal(self : SdpDocument) -> String {
let lines : Array[String] = []
for field in self.session_fields {
lines.push(field.marshal())
}
for media in self.media_descriptions {
lines.push("m=" + media.marshal_media_line())
for field in media.fields {
lines.push(field.marshal())
}
}
lines.join("\r\n") + "\r\n"
}
///|
pub fn SdpDocument::session_fields(self : SdpDocument) -> Array[SdpField] {
self.session_fields.copy()
}
///|
pub fn SdpDocument::media_descriptions(
self : SdpDocument,
) -> Array[MediaDescription] {
self.media_descriptions.copy()
}
///|
pub fn SdpDocument::session_attributes(
self : SdpDocument,
key : String,
) -> Array[SdpAttribute] raise SdpError {
let result : Array[SdpAttribute] = []
for field in self.session_fields {
if field.kind == b'a' {
let attribute = SdpAttribute::parse(field.value)
if attribute.key == key {
result.push(attribute)
}
}
}
result
}
///|
pub fn SdpDocument::session_attribute(
self : SdpDocument,
key : String,
) -> SdpAttribute? raise SdpError {
let attributes = self.session_attributes(key)
if attributes.is_empty() {
None
} else {
Some(attributes[0])
}
}