use anyhow::{Result, anyhow};
use rustrtc::{MediaKind, SdpType, SessionDescription};
#[derive(Debug, Clone)]
pub struct IceCredentials {
pub ufrag: String,
pub pwd: String,
}
impl IceCredentials {
pub fn generate() -> Self {
use rand::RngExt;
let mut rng = rand::rng();
const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let ufrag_len = rng.random_range(4..=8);
let ufrag: String = (0..ufrag_len)
.map(|_| CHARS[rng.random_range(0..CHARS.len())] as char)
.collect();
let pwd_len = rng.random_range(22..=32);
let pwd: String = (0..pwd_len)
.map(|_| CHARS[rng.random_range(0..CHARS.len())] as char)
.collect();
Self { ufrag, pwd }
}
}
#[derive(Debug, Clone)]
pub struct DtlsInfo {
pub fingerprint: String,
pub setup: String,
}
impl DtlsInfo {
pub fn generate_placeholder() -> Self {
use rand::RngExt;
let mut rng = rand::rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
let fingerprint = bytes
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(":");
Self {
fingerprint,
setup: "passive".to_string(),
}
}
pub fn from_certificate(fingerprint: &str) -> Self {
Self {
fingerprint: fingerprint.to_string(),
setup: "passive".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SdpFormat {
WebRtc,
Rtp,
}
pub fn detect_sdp_format(sdp: &str) -> SdpFormat {
if sdp.contains("UDP/TLS/RTP/SAVPF")
|| sdp.contains("a=fingerprint:")
|| sdp.contains("a=ice-ufrag:")
{
SdpFormat::WebRtc
} else {
SdpFormat::Rtp
}
}
pub struct SdpBridge;
impl SdpBridge {
pub fn webrtc_to_rtp(webrtc_sdp: &str) -> Result<String> {
let parsed = SessionDescription::parse(SdpType::Offer, webrtc_sdp)
.or_else(|_| SessionDescription::parse(SdpType::Answer, webrtc_sdp))
.map_err(|e| anyhow!("Failed to parse WebRTC SDP: {}", e))?;
let mut rtp_sdp = String::new();
rtp_sdp.push_str("v=0\r\n");
rtp_sdp.push_str(&format!(
"o=- {} {} IN IP4 {}\r\n",
parsed.session.origin.session_id,
parsed.session.origin.session_version,
parsed.session.origin.unicast_address
));
rtp_sdp.push_str("s=RustPBX Bridge\r\n");
rtp_sdp.push_str(&format!(
"c=IN IP4 {}\r\n",
parsed.session.origin.unicast_address
));
rtp_sdp.push_str("t=0 0\r\n");
for section in &parsed.media_sections {
let is_audio = section.kind == MediaKind::Audio;
let is_video = section.kind == MediaKind::Video;
if !is_audio && !is_video {
continue;
}
let mut rtp_formats = Vec::new();
let mut rtp_attrs = Vec::new();
for format in §ion.formats {
if let Ok(pt) = format.parse::<u8>() {
rtp_formats.push(pt);
}
}
let mut codec_found = false;
for attr in §ion.attributes {
if attr.key == "rtpmap" {
if let Some(ref value) = attr.value {
let parts: Vec<&str> = value.splitn(2, ' ').collect();
if parts.len() == 2 {
let pt_str = parts[0];
let codec_info = parts[1];
if is_audio {
if codec_info.contains("PCMU")
|| codec_info.contains("PCMA")
|| codec_info.contains("telephone-event")
|| codec_info.contains("G722")
|| codec_info.contains("G729")
{
rtp_attrs
.push(format!("a=rtpmap:{} {}\r\n", pt_str, codec_info));
codec_found = true;
}
} else if is_video {
rtp_attrs.push(format!("a=rtpmap:{} {}\r\n", pt_str, codec_info));
codec_found = true;
}
}
}
} else if attr.key == "fmtp" {
if is_audio {
if let Some(ref value) = attr.value
&& (value.contains("101") || value.contains("telephone-event"))
{
rtp_attrs.push(format!("a=fmtp:{}\r\n", value));
}
} else if is_video {
if let Some(ref value) = attr.value {
rtp_attrs.push(format!("a=fmtp:{}\r\n", value));
}
}
} else if attr.key == "sendrecv"
|| attr.key == "sendonly"
|| attr.key == "recvonly"
|| attr.key == "inactive"
{
rtp_attrs.push(format!("a={}\r\n", attr.key));
} else if is_video && (attr.key == "rtcp-fb" || attr.key == "rtx") {
if let Some(ref value) = attr.value {
rtp_attrs.push(format!("a={}:{}\r\n", attr.key, value));
}
}
}
if is_audio && !codec_found {
rtp_formats = vec![0, 101];
rtp_attrs.push("a=rtpmap:0 PCMU/8000\r\n".to_string());
rtp_attrs.push("a=rtpmap:101 telephone-event/8000\r\n".to_string());
rtp_attrs.push("a=fmtp:101 0-15\r\n".to_string());
}
let format_str = rtp_formats
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(" ");
let media_type = if is_audio { "audio" } else { "video" };
rtp_sdp.push_str(&format!(
"m={} {} RTP/AVP {}\r\n",
media_type, section.port, format_str
));
for attr in rtp_attrs {
rtp_sdp.push_str(&attr);
}
}
Ok(rtp_sdp)
}
pub fn rtp_to_webrtc(
rtp_sdp: &str,
fingerprint: &str,
ice_ufrag: &str,
ice_pwd: &str,
) -> Result<String> {
let parsed = SessionDescription::parse(SdpType::Offer, rtp_sdp)
.or_else(|_| SessionDescription::parse(SdpType::Answer, rtp_sdp))
.map_err(|e| anyhow!("Failed to parse RTP SDP: {}", e))?;
let mut webrtc_sdp = String::new();
webrtc_sdp.push_str("v=0\r\n");
webrtc_sdp.push_str(&format!(
"o=- {} {} IN IP4 {}\r\n",
parsed.session.origin.session_id,
parsed.session.origin.session_version,
parsed.session.origin.unicast_address
));
webrtc_sdp.push_str("s=RustPBX Bridge\r\n");
webrtc_sdp.push_str(&format!(
"c=IN IP4 {}\r\n",
parsed.session.origin.unicast_address
));
webrtc_sdp.push_str("t=0 0\r\n");
for section in &parsed.media_sections {
let is_audio = section.kind == MediaKind::Audio;
let is_video = section.kind == MediaKind::Video;
if !is_audio && !is_video {
continue;
}
let mut webrtc_formats = Vec::new();
let mut webrtc_attrs = Vec::new();
for format in §ion.formats {
if let Ok(pt) = format.parse::<u8>() {
webrtc_formats.push(pt);
}
}
for attr in §ion.attributes {
if attr.key == "rtpmap" {
if let Some(ref value) = attr.value {
webrtc_attrs.push(format!("a=rtpmap:{}\r\n", value));
}
} else if attr.key == "fmtp" {
if let Some(ref value) = attr.value {
webrtc_attrs.push(format!("a=fmtp:{}\r\n", value));
}
} else if attr.key == "sendrecv"
|| attr.key == "sendonly"
|| attr.key == "recvonly"
|| attr.key == "inactive"
{
webrtc_attrs.push(format!("a={}\r\n", attr.key));
} else if is_video && (attr.key == "rtcp-fb" || attr.key == "rtx") {
if let Some(ref value) = attr.value {
webrtc_attrs.push(format!("a={}:{}\r\n", attr.key, value));
}
}
}
webrtc_attrs.push(format!("a=fingerprint:sha-256 {}\r\n", fingerprint));
webrtc_attrs.push("a=setup:passive\r\n".to_string());
webrtc_attrs.push(format!("a=ice-ufrag:{}\r\n", ice_ufrag));
webrtc_attrs.push(format!("a=ice-pwd:{}\r\n", ice_pwd));
webrtc_attrs.push("a=rtcp-mux\r\n".to_string());
webrtc_attrs.push("a=mid:0\r\n".to_string());
let format_str = webrtc_formats
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(" ");
let media_type = if is_audio { "audio" } else { "video" };
webrtc_sdp.push_str(&format!(
"m={} {} UDP/TLS/RTP/SAVPF {}\r\n",
media_type, section.port, format_str
));
for attr in webrtc_attrs {
webrtc_sdp.push_str(&attr);
}
}
Ok(webrtc_sdp)
}
pub fn needs_bridging(caller_sdp: &str, callee_sdp: &str) -> bool {
let caller_format = detect_sdp_format(caller_sdp);
let callee_format = detect_sdp_format(callee_sdp);
caller_format != callee_format
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_webrtc_sdp() {
let webrtc_sdp = "v=0\r\n\
o=- 123 123 IN IP4 127.0.0.1\r\n\
s=-\r\n\
m=audio 1234 UDP/TLS/RTP/SAVPF 111\r\n\
a=fingerprint:sha-256 AA:BB\r\n";
assert_eq!(detect_sdp_format(webrtc_sdp), SdpFormat::WebRtc);
}
#[test]
fn test_detect_rtp_sdp() {
let rtp_sdp = "v=0\r\n\
o=- 123 123 IN IP4 127.0.0.1\r\n\
s=-\r\n\
m=audio 1234 RTP/AVP 0\r\n";
assert_eq!(detect_sdp_format(rtp_sdp), SdpFormat::Rtp);
}
#[test]
fn test_webrtc_to_rtp_conversion() {
let webrtc_sdp = "v=0\r\n\
o=- 123456 123456 IN IP4 127.0.0.1\r\n\
s=-\r\n\
c=IN IP4 127.0.0.1\r\n\
t=0 0\r\n\
m=audio 12345 UDP/TLS/RTP/SAVPF 111 101\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:101 telephone-event/8000\r\n\
a=fingerprint:sha-256 AA:BB:CC:DD\r\n\
a=setup:actpass\r\n\
a=ice-ufrag:abcd\r\n\
a=ice-pwd:xyz\r\n\
a=rtcp-mux\r\n\
a=sendrecv\r\n";
let rtp_sdp = SdpBridge::webrtc_to_rtp(webrtc_sdp).unwrap();
assert!(rtp_sdp.contains("RTP/AVP"));
assert!(!rtp_sdp.contains("SAVPF"));
assert!(!rtp_sdp.contains("fingerprint"));
assert!(!rtp_sdp.contains("ice-ufrag"));
assert!(!rtp_sdp.contains("rtcp-mux"));
assert!(rtp_sdp.contains("telephone-event"));
}
#[test]
fn test_rtp_to_webrtc_conversion() {
let rtp_sdp = "v=0\r\n\
o=- 123456 123456 IN IP4 127.0.0.1\r\n\
s=-\r\n\
c=IN IP4 127.0.0.1\r\n\
t=0 0\r\n\
m=audio 54321 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:101 telephone-event/8000\r\n\
a=sendrecv\r\n";
let webrtc_sdp =
SdpBridge::rtp_to_webrtc(rtp_sdp, "AA:BB:CC:DD:EE:FF", "ufrag123", "pwd456").unwrap();
assert!(webrtc_sdp.contains("UDP/TLS/RTP/SAVPF"));
assert!(webrtc_sdp.contains("fingerprint:sha-256 AA:BB:CC:DD:EE:FF"));
assert!(webrtc_sdp.contains("ice-ufrag:ufrag123"));
assert!(webrtc_sdp.contains("ice-pwd:pwd456"));
assert!(webrtc_sdp.contains("rtcp-mux"));
assert!(webrtc_sdp.contains("setup:passive"));
assert!(webrtc_sdp.contains("PCMU/8000"));
}
}