#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MediaDescription {
pub media: String,
pub payload_type: u8,
pub encoding: Option<String>,
pub control: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Sdp {
pub media: Vec<MediaDescription>,
}
impl Sdp {
pub fn parse(text: &str) -> Sdp {
let mut media: Vec<MediaDescription> = Vec::new();
for line in text.lines() {
let line = line.trim_end();
if let Some(rest) = line.strip_prefix("m=") {
let mut parts = rest.split(' ');
let kind = parts.next().unwrap_or("").to_string();
let pt = parts.nth(2).and_then(|p| p.parse().ok()).unwrap_or(0);
media.push(MediaDescription {
media: kind,
payload_type: pt,
encoding: None,
control: None,
});
} else if let Some(rest) = line.strip_prefix("a=") {
let Some(current) = media.last_mut() else {
continue;
};
if let Some(rtpmap) = rest.strip_prefix("rtpmap:") {
if let Some((_, enc)) = rtpmap.split_once(' ') {
current.encoding = Some(enc.split('/').next().unwrap_or(enc).to_string());
}
} else if let Some(control) = rest.strip_prefix("control:") {
current.control = Some(control.to_string());
}
}
}
Sdp { media }
}
pub fn first_video_control(&self, base_url: &str) -> Option<String> {
let video = self.media.iter().find(|m| m.media == "video")?;
let control = video.control.as_deref().unwrap_or("");
if control.is_empty() || control == "*" {
Some(base_url.to_string())
} else if control.starts_with("rtsp://") {
Some(control.to_string())
} else {
Some(format!("{}/{}", base_url.trim_end_matches('/'), control))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "v=0\r\n\
o=- 0 0 IN IP4 127.0.0.1\r\n\
s=Session\r\n\
m=video 0 RTP/AVP 96\r\n\
a=rtpmap:96 H264/90000\r\n\
a=control:trackID=1\r\n\
m=audio 0 RTP/AVP 97\r\n\
a=rtpmap:97 MPEG4-GENERIC/48000/2\r\n\
a=control:trackID=2\r\n";
#[test]
fn parses_both_media_tracks() {
let sdp = Sdp::parse(SAMPLE);
assert_eq!(sdp.media.len(), 2);
let v = &sdp.media[0];
assert_eq!(v.media, "video");
assert_eq!(v.payload_type, 96);
assert_eq!(v.encoding.as_deref(), Some("H264"));
assert_eq!(v.control.as_deref(), Some("trackID=1"));
assert_eq!(sdp.media[1].encoding.as_deref(), Some("MPEG4-GENERIC"));
}
#[test]
fn builds_relative_control_url() {
let sdp = Sdp::parse(SAMPLE);
assert_eq!(
sdp.first_video_control("rtsp://cam/stream"),
Some("rtsp://cam/stream/trackID=1".to_string())
);
}
#[test]
fn absolute_control_url_overrides_base() {
let sdp = Sdp::parse("m=video 0 RTP/AVP 96\r\na=control:rtsp://cam/abs\r\n");
assert_eq!(
sdp.first_video_control("rtsp://cam/stream"),
Some("rtsp://cam/abs".to_string())
);
}
#[test]
fn empty_sdp_yields_no_media() {
assert!(Sdp::parse("v=0\r\n").media.is_empty());
}
}