use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SdpMediaType {
Audio,
Video,
Application,
Text,
Message,
Other(String),
}
impl fmt::Display for SdpMediaType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Audio => f.write_str("audio"),
Self::Video => f.write_str("video"),
Self::Application => f.write_str("application"),
Self::Text => f.write_str("text"),
Self::Message => f.write_str("message"),
Self::Other(s) => f.write_str(s),
}
}
}
impl FromStr for SdpMediaType {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"audio" => Self::Audio,
"video" => Self::Video,
"application" => Self::Application,
"text" => Self::Text,
"message" => Self::Message,
other => Self::Other(other.to_string()),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SdpDirection {
SendRecv,
SendOnly,
RecvOnly,
Inactive,
}
impl fmt::Display for SdpDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SendRecv => f.write_str("sendrecv"),
Self::SendOnly => f.write_str("sendonly"),
Self::RecvOnly => f.write_str("recvonly"),
Self::Inactive => f.write_str("inactive"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseSdpDirectionError(String);
impl fmt::Display for ParseSdpDirectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unrecognized SDP direction attribute: {:?}", self.0)
}
}
impl std::error::Error for ParseSdpDirectionError {}
impl FromStr for SdpDirection {
type Err = ParseSdpDirectionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sendrecv" => Ok(Self::SendRecv),
"sendonly" => Ok(Self::SendOnly),
"recvonly" => Ok(Self::RecvOnly),
"inactive" => Ok(Self::Inactive),
other => Err(ParseSdpDirectionError(other.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct SdpCodec {
media: SdpMediaType,
payload_type: u8,
name: String,
clock_rate: u32,
channels: Option<u8>,
fmtp: Option<String>,
ptime: Option<u32>,
maxptime: Option<u32>,
bitrate: Option<u32>,
direction: SdpDirection,
has_rtpmap: bool,
}
impl SdpCodec {
#[allow(clippy::too_many_arguments)]
pub fn new(
media: SdpMediaType,
payload_type: u8,
name: impl Into<String>,
clock_rate: u32,
channels: Option<u8>,
fmtp: Option<String>,
ptime: Option<u32>,
maxptime: Option<u32>,
bitrate: Option<u32>,
direction: SdpDirection,
has_rtpmap: bool,
) -> Self {
Self {
media,
payload_type,
name: name.into(),
clock_rate,
channels,
fmtp,
ptime,
maxptime,
bitrate,
direction,
has_rtpmap,
}
}
pub fn media(&self) -> &SdpMediaType {
&self.media
}
pub fn payload_type(&self) -> u8 {
self.payload_type
}
pub fn name(&self) -> &str {
&self.name
}
pub fn clock_rate(&self) -> u32 {
self.clock_rate
}
pub fn channels(&self) -> Option<u8> {
self.channels
}
pub fn fmtp(&self) -> Option<&str> {
self.fmtp
.as_deref()
}
pub fn ptime(&self) -> Option<u32> {
self.ptime
}
pub fn maxptime(&self) -> Option<u32> {
self.maxptime
}
pub fn bitrate(&self) -> Option<u32> {
self.bitrate
}
pub fn direction(&self) -> SdpDirection {
self.direction
}
pub fn has_rtpmap(&self) -> bool {
self.has_rtpmap
}
pub fn fmtp_mut(&mut self) -> &mut Option<String> {
&mut self.fmtp
}
pub fn ptime_mut(&mut self) -> &mut Option<u32> {
&mut self.ptime
}
pub fn maxptime_mut(&mut self) -> &mut Option<u32> {
&mut self.maxptime
}
pub fn bitrate_mut(&mut self) -> &mut Option<u32> {
&mut self.bitrate
}
pub fn channels_mut(&mut self) -> &mut Option<u8> {
&mut self.channels
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sdp_media_type_display() {
assert_eq!(SdpMediaType::Audio.to_string(), "audio");
assert_eq!(SdpMediaType::Video.to_string(), "video");
assert_eq!(SdpMediaType::Application.to_string(), "application");
assert_eq!(SdpMediaType::Text.to_string(), "text");
assert_eq!(SdpMediaType::Message.to_string(), "message");
assert_eq!(
SdpMediaType::Other("image".to_string()).to_string(),
"image"
);
}
#[test]
fn sdp_media_type_from_str_known() {
assert_eq!(
"audio"
.parse::<SdpMediaType>()
.unwrap(),
SdpMediaType::Audio
);
assert_eq!(
"video"
.parse::<SdpMediaType>()
.unwrap(),
SdpMediaType::Video
);
assert_eq!(
"application"
.parse::<SdpMediaType>()
.unwrap(),
SdpMediaType::Application
);
assert_eq!(
"text"
.parse::<SdpMediaType>()
.unwrap(),
SdpMediaType::Text
);
assert_eq!(
"message"
.parse::<SdpMediaType>()
.unwrap(),
SdpMediaType::Message
);
}
#[test]
fn sdp_media_type_from_str_unknown_becomes_other() {
let t = "image"
.parse::<SdpMediaType>()
.unwrap();
assert_eq!(t, SdpMediaType::Other("image".to_string()));
}
#[test]
fn sdp_media_type_round_trip() {
for s in &[
"audio",
"video",
"application",
"text",
"message",
"datachannel",
] {
let parsed: SdpMediaType = s
.parse()
.unwrap();
assert_eq!(&parsed.to_string(), s);
}
}
#[test]
fn sdp_direction_display() {
assert_eq!(SdpDirection::SendRecv.to_string(), "sendrecv");
assert_eq!(SdpDirection::SendOnly.to_string(), "sendonly");
assert_eq!(SdpDirection::RecvOnly.to_string(), "recvonly");
assert_eq!(SdpDirection::Inactive.to_string(), "inactive");
}
#[test]
fn sdp_direction_from_str() {
assert_eq!(
"sendrecv"
.parse::<SdpDirection>()
.unwrap(),
SdpDirection::SendRecv
);
assert_eq!(
"sendonly"
.parse::<SdpDirection>()
.unwrap(),
SdpDirection::SendOnly
);
assert_eq!(
"recvonly"
.parse::<SdpDirection>()
.unwrap(),
SdpDirection::RecvOnly
);
assert_eq!(
"inactive"
.parse::<SdpDirection>()
.unwrap(),
SdpDirection::Inactive
);
}
#[test]
fn sdp_direction_from_str_error() {
let err = "halfduplex"
.parse::<SdpDirection>()
.unwrap_err();
assert!(err
.to_string()
.contains("halfduplex"));
}
#[test]
fn sdp_direction_round_trip() {
for dir in &[
SdpDirection::SendRecv,
SdpDirection::SendOnly,
SdpDirection::RecvOnly,
SdpDirection::Inactive,
] {
let parsed: SdpDirection = dir
.to_string()
.parse()
.unwrap();
assert_eq!(&parsed, dir);
}
}
fn make_audio_codec() -> SdpCodec {
SdpCodec::new(
SdpMediaType::Audio,
0,
"PCMU",
8000,
Some(1),
None,
Some(20),
None,
Some(64000),
SdpDirection::SendRecv,
false,
)
}
#[test]
fn sdp_codec_accessors() {
let c = make_audio_codec();
assert_eq!(c.media(), &SdpMediaType::Audio);
assert_eq!(c.payload_type(), 0);
assert_eq!(c.name(), "PCMU");
assert_eq!(c.clock_rate(), 8000);
assert_eq!(c.channels(), Some(1));
assert_eq!(c.fmtp(), None);
assert_eq!(c.ptime(), Some(20));
assert_eq!(c.maxptime(), None);
assert_eq!(c.bitrate(), Some(64000));
assert_eq!(c.direction(), SdpDirection::SendRecv);
assert!(!c.has_rtpmap());
}
#[test]
fn sdp_codec_with_rtpmap() {
let c = SdpCodec::new(
SdpMediaType::Audio,
111,
"opus",
48000,
Some(2),
Some("minptime=10;useinbandfec=1".into()),
Some(20),
None,
None,
SdpDirection::SendRecv,
true,
);
assert_eq!(c.name(), "opus");
assert_eq!(c.clock_rate(), 48000);
assert_eq!(c.channels(), Some(2));
assert_eq!(c.fmtp(), Some("minptime=10;useinbandfec=1"));
assert!(c.has_rtpmap());
}
#[test]
fn sdp_codec_video_no_channels() {
let c = SdpCodec::new(
SdpMediaType::Video,
99,
"H264",
90000,
None,
None,
None,
None,
None,
SdpDirection::SendRecv,
true,
);
assert_eq!(c.media(), &SdpMediaType::Video);
assert!(c
.channels()
.is_none());
}
#[test]
fn sdp_codec_mutable_accessors() {
let mut c = make_audio_codec();
*c.ptime_mut() = Some(30);
assert_eq!(c.ptime(), Some(30));
*c.fmtp_mut() = Some("mode=20".into());
assert_eq!(c.fmtp(), Some("mode=20"));
*c.bitrate_mut() = None;
assert!(c
.bitrate()
.is_none());
*c.maxptime_mut() = Some(40);
assert_eq!(c.maxptime(), Some(40));
*c.channels_mut() = Some(2);
assert_eq!(c.channels(), Some(2));
}
}