#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SdpMediaType {
Video,
Audio,
Application,
}
impl SdpMediaType {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Video => "video",
Self::Audio => "audio",
Self::Application => "application",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SdpAttribute {
pub key: String,
pub value: String,
}
impl SdpAttribute {
#[must_use]
pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
#[must_use]
pub fn flag(key: impl Into<String>) -> Self {
Self {
key: key.into(),
value: String::new(),
}
}
#[must_use]
pub fn to_sdp_line(&self) -> String {
if self.value.is_empty() {
format!("a={}", self.key)
} else {
format!("a={}:{}", self.key, self.value)
}
}
}
#[derive(Debug, Clone)]
pub struct SdpMediaSection {
pub media_type: SdpMediaType,
pub port: u16,
pub payload_type: u8,
pub connection_addr: String,
pub attributes: Vec<SdpAttribute>,
}
impl SdpMediaSection {
#[must_use]
pub fn new(
media_type: SdpMediaType,
port: u16,
payload_type: u8,
connection_addr: impl Into<String>,
) -> Self {
Self {
media_type,
port,
payload_type,
connection_addr: connection_addr.into(),
attributes: Vec::new(),
}
}
pub fn add_attribute(&mut self, attr: SdpAttribute) {
self.attributes.push(attr);
}
pub fn add_kv(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.attributes.push(SdpAttribute::new(key, value));
}
#[must_use]
pub fn is_multicast(&self) -> bool {
if let Ok(ip) = self.connection_addr.parse::<std::net::Ipv4Addr>() {
return ip.is_multicast();
}
false
}
#[must_use]
pub fn to_sdp_lines(&self) -> Vec<String> {
let mut lines = Vec::new();
lines.push(format!(
"m={} {} RTP/AVP {}",
self.media_type.as_str(),
self.port,
self.payload_type
));
lines.push(format!("c=IN IP4 {}", self.connection_addr));
for attr in &self.attributes {
lines.push(attr.to_sdp_line());
}
lines
}
}
#[derive(Debug, Clone)]
pub struct SdpSession {
pub session_name: String,
pub originator: String,
pub attributes: Vec<SdpAttribute>,
pub media: Vec<SdpMediaSection>,
}
impl SdpSession {
#[must_use]
pub fn new(session_name: impl Into<String>, originator: impl Into<String>) -> Self {
Self {
session_name: session_name.into(),
originator: originator.into(),
attributes: Vec::new(),
media: Vec::new(),
}
}
pub fn add_attribute(&mut self, attr: SdpAttribute) {
self.attributes.push(attr);
}
pub fn add_media(&mut self, section: SdpMediaSection) {
self.media.push(section);
}
#[must_use]
pub fn media_count(&self) -> usize {
self.media.len()
}
#[must_use]
pub fn media_of_type(&self, t: SdpMediaType) -> Vec<&SdpMediaSection> {
self.media.iter().filter(|m| m.media_type == t).collect()
}
#[must_use]
pub fn to_sdp_string(&self) -> String {
let mut lines = Vec::new();
lines.push("v=0".to_string());
lines.push(format!("o={}", self.originator));
lines.push(format!("s={}", self.session_name));
lines.push("t=0 0".to_string());
for attr in &self.attributes {
lines.push(attr.to_sdp_line());
}
for section in &self.media {
lines.extend(section.to_sdp_lines());
}
lines.join("\r\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_media_type_as_str() {
assert_eq!(SdpMediaType::Video.as_str(), "video");
assert_eq!(SdpMediaType::Audio.as_str(), "audio");
assert_eq!(SdpMediaType::Application.as_str(), "application");
}
#[test]
fn test_attribute_kv_line() {
let attr = SdpAttribute::new("rtpmap", "96 raw/90000");
assert_eq!(attr.to_sdp_line(), "a=rtpmap:96 raw/90000");
}
#[test]
fn test_attribute_flag_line() {
let attr = SdpAttribute::flag("recvonly");
assert_eq!(attr.to_sdp_line(), "a=recvonly");
}
#[test]
fn test_is_multicast_true() {
let sec = SdpMediaSection::new(SdpMediaType::Video, 5004, 96, "239.100.0.1");
assert!(sec.is_multicast());
}
#[test]
fn test_is_multicast_false() {
let sec = SdpMediaSection::new(SdpMediaType::Video, 5004, 96, "192.168.1.10");
assert!(!sec.is_multicast());
}
#[test]
fn test_media_section_m_line() {
let sec = SdpMediaSection::new(SdpMediaType::Audio, 5006, 97, "239.100.0.2");
let lines = sec.to_sdp_lines();
assert!(lines[0].starts_with("m=audio 5006 RTP/AVP 97"));
}
#[test]
fn test_media_section_c_line() {
let sec = SdpMediaSection::new(SdpMediaType::Video, 5004, 96, "239.100.0.1");
let lines = sec.to_sdp_lines();
assert!(lines[1].contains("239.100.0.1"));
}
#[test]
fn test_add_attribute_to_section() {
let mut sec = SdpMediaSection::new(SdpMediaType::Video, 5004, 96, "239.100.0.1");
sec.add_kv("rtpmap", "96 raw/90000");
let lines = sec.to_sdp_lines();
assert!(lines.iter().any(|l| l.contains("rtpmap")));
}
#[test]
fn test_session_media_count() {
let mut sess = SdpSession::new("Test", "- 0 0 IN IP4 127.0.0.1");
assert_eq!(sess.media_count(), 0);
sess.add_media(SdpMediaSection::new(
SdpMediaType::Video,
5004,
96,
"239.100.0.1",
));
assert_eq!(sess.media_count(), 1);
}
#[test]
fn test_session_media_of_type() {
let mut sess = SdpSession::new("S", "- 0 0 IN IP4 127.0.0.1");
sess.add_media(SdpMediaSection::new(
SdpMediaType::Video,
5004,
96,
"239.0.0.1",
));
sess.add_media(SdpMediaSection::new(
SdpMediaType::Audio,
5006,
97,
"239.0.0.2",
));
sess.add_media(SdpMediaSection::new(
SdpMediaType::Video,
5008,
98,
"239.0.0.3",
));
assert_eq!(sess.media_of_type(SdpMediaType::Video).len(), 2);
assert_eq!(sess.media_of_type(SdpMediaType::Audio).len(), 1);
assert_eq!(sess.media_of_type(SdpMediaType::Application).len(), 0);
}
#[test]
fn test_sdp_string_starts_with_version() {
let sess = SdpSession::new("Test Session", "- 0 0 IN IP4 127.0.0.1");
let sdp = sess.to_sdp_string();
assert!(sdp.starts_with("v=0"));
}
#[test]
fn test_sdp_string_has_session_name() {
let sess = SdpSession::new("MyStream", "- 0 0 IN IP4 10.0.0.1");
let sdp = sess.to_sdp_string();
assert!(sdp.contains("s=MyStream"));
}
#[test]
fn test_sdp_string_session_attribute() {
let mut sess = SdpSession::new("S", "- 0 0 IN IP4 127.0.0.1");
sess.add_attribute(SdpAttribute::flag("sendonly"));
let sdp = sess.to_sdp_string();
assert!(sdp.contains("a=sendonly"));
}
}