janus_plugin/
sdp.rs

1/// Utilities to write SDP offers and answers using Janus's SDP parsing machinery.
2
3use glib_sys;
4use libc;
5use janus_plugin_sys as ffi;
6use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor};
7use serde::ser::{Serialize, Serializer};
8pub use ffi::sdp::janus_sdp_generate_answer as generate_answer;
9pub use ffi::sdp::janus_sdp_generate_offer as generate_offer;
10use std::collections::HashMap;
11use std::error::Error;
12use std::fmt;
13use std::ffi::{CStr, CString};
14use std::ops::Deref;
15use std::str;
16use crate::utils::GLibString;
17
18pub type RawSdp = ffi::sdp::janus_sdp;
19pub type RawMLine = ffi::sdp::janus_sdp_mline;
20pub type RawAttribute = ffi::sdp::janus_sdp_attribute;
21pub use ffi::sdp::janus_sdp_mtype as MediaType;
22pub use ffi::sdp::janus_sdp_mdirection as MediaDirection;
23
24// courtesy of c_string crate, which also has some other stuff we aren't interested in
25// taking in as a dependency here.
26macro_rules! c_str {
27    ($lit:expr) => {
28        unsafe {
29            ::std::ffi::CStr::from_ptr(concat!($lit, "\0").as_ptr() as *const ::std::os::raw::c_char)
30        }
31    }
32}
33
34/// SDP attributes which may refer to a specific RTP payload type.
35static MEDIA_PAYLOAD_ATTRIBUTES: [&str; 3] = ["rtpmap", "fmtp", "rtcp-fb"];
36
37#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
38/// Available Janus audio codecs. See utils.c.
39pub enum AudioCodec {
40    Opus,
41    Pcmu,
42    Pcma,
43    G722,
44    Isac16,
45    Isac32,
46}
47
48impl AudioCodec {
49    pub fn to_str(self) -> &'static str {
50        self.to_cstr().to_str().unwrap()
51    }
52    pub fn to_cstr(self) -> &'static CStr {
53        match self {
54            AudioCodec::Opus => c_str!("opus"),
55            AudioCodec::Pcmu => c_str!("pcmu"),
56            AudioCodec::Pcma => c_str!("pcma"),
57            AudioCodec::G722 => c_str!("g722"),
58            AudioCodec::Isac16 => c_str!("isac16"),
59            AudioCodec::Isac32 => c_str!("isac32"),
60        }
61    }
62}
63
64#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
65/// Available Janus video codecs. See utils.c.
66pub enum VideoCodec {
67    Vp8,
68    Vp9,
69    H264,
70    Av1,
71    H265,
72}
73
74impl VideoCodec {
75    pub fn to_str(self) -> &'static str {
76        self.to_cstr().to_str().unwrap()
77    }
78    pub fn to_cstr(self) -> &'static CStr {
79        match self {
80            VideoCodec::Vp8 => c_str!("vp8"),
81            VideoCodec::Vp9 => c_str!("vp9"),
82            VideoCodec::H264 => c_str!("h264"),
83            VideoCodec::Av1 => c_str!("av1"),
84            VideoCodec::H265 => c_str!("h265"),
85        }
86    }
87}
88
89#[repr(i32)]
90#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
91/// Parameters controlling SDP offer answering behavior. Used as keys in the parameter list
92/// for `janus_sdp_generate_answer`. See sdp-utils.h in the Janus source for more details.
93pub enum OfferAnswerParameters {
94    /// Used to signal the end of the offer-answer parameter list.
95    Done = 0,
96    /// Whether to accept or reject audio.
97    Audio = 1,
98    /// Whether to accept or reject video.
99    Video = 2,
100    /// Whether to accept or reject data.
101    Data = 3,
102    /// The MediaDirection for the audio stream.
103    AudioDirection = 4,
104    /// The MediaDirection for the video stream.
105    VideoDirection = 5,
106    /// The AudioCodec for the audio stream.
107    AudioCodec = 6,
108    /// The VideoCodec for the video stream.
109    VideoCodec = 7,
110    /// Use this profile for VP9
111    Vp9Profile = 8,
112    /// Use this profile for H.264
113    H264Profile = 9,
114    /// The payload type for the audio stream.
115    AudioPayloadType = 10,
116    /// The payload type for the video stream.
117    VideoPayloadType = 11,
118    /// Whether to negotiate telephone events.
119    AudioDtmf = 12,
120    /// Add a custom fmtp string for audio
121    AudioFmtp = 13,
122    /// Add a custom fmtp string for video
123    /// @note This property is ignored if Vp9Profile or H264Profile is used on a compliant codec.
124    VideoFmtp = 14,
125    /// Whether to add RTCP-FB attributes.
126    VideoRtcpfbDefaults = 15,
127    DataLegacy = 16,
128    AudioExtension = 17,
129    VideoExtension = 18,
130    AcceptExtmap = 19,
131}
132
133/// An SDP session description.
134pub struct Sdp {
135    pub ptr: *mut RawSdp, // annoyingly pub because of answer_sdp macro
136}
137
138/// An error indicating that we failed to parse an SDP for some reason.
139#[derive(Debug, Clone)]
140pub struct SdpParseError {
141    buffer: Vec<u8>
142}
143
144impl Error for SdpParseError {}
145
146impl fmt::Display for SdpParseError {
147    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148        f.write_str(str::from_utf8(&self.buffer).unwrap_or("SDP parsing failed, but the error was not valid UTF-8 :("))
149    }
150}
151
152impl Sdp {
153    pub unsafe fn new(ptr: *mut RawSdp) -> Option<Self> {
154        ptr.as_mut().map(|p| Self { ptr: p })
155    }
156
157    /// Parses an SDP offer string from a client into a structured SDP object.
158    pub fn parse(offer: &CStr) -> Result<Self, SdpParseError> {
159        let mut error_buffer = Vec::with_capacity(512);
160        let error_ptr = error_buffer.as_mut_ptr() as *mut _;
161        unsafe {
162            let result = ffi::sdp::janus_sdp_parse(offer.as_ptr(), error_ptr, error_buffer.capacity());
163            Sdp::new(result).ok_or_else(|| {
164                error_buffer.set_len(libc::strlen(error_ptr));
165                SdpParseError { buffer: error_buffer }
166            })
167        }
168    }
169
170    /// Gets the payload type number for a codec in this SDP, or None if the codec isn't present.
171    pub fn get_payload_type(&self, codec_name: &CStr) -> Option<i32> {
172        unsafe {
173            match ffi::sdp::janus_sdp_get_codec_pt(self.ptr, codec_name.as_ptr()) {
174                err if err < 0 => None,
175                n => Some(n),
176            }
177        }
178    }
179
180    /// Gets the payload type number for a codec and provided video profile in this SDP, or None if the codec isn't present with the provided video profile.
181    pub fn get_payload_type_full(&self, codec_name: &CStr, profile: &CStr) -> Option<i32> {
182        unsafe {
183            match ffi::sdp::janus_sdp_get_codec_pt_full(self.ptr, codec_name.as_ptr(), profile.as_ptr()) {
184                err if err < 0 => None,
185                n => Some(n),
186            }
187        }
188    }
189
190    /// Adds an attribute for the m-line with the given payload type.
191    pub fn add_attribute(&mut self, pt: i32, name: &CStr, contents: &CStr) {
192        for (_media, m_lines) in self.get_mlines() {
193            unsafe {
194                for m_line in m_lines {
195                    if !glib_sys::g_list_find(m_line.ptypes, pt as *const _).is_null() {
196                        let attr = ffi::sdp::janus_sdp_attribute_create(name.as_ptr(), contents.as_ptr());
197                        ffi::sdp::janus_sdp_attribute_add_to_mline(m_line as *mut _, attr as *mut _);
198                    }
199                }
200            }
201        }
202    }
203
204    /// Rewrites any references from one dynamically assigned payload type in this SDP to another dynamically assigned
205    /// payload type.
206    pub fn rewrite_payload_type(&mut self, from: i32, to: i32) {
207        let from_pt_string = from.to_string();
208        let to_pt_string = to.to_string();
209        for (_media, m_lines) in self.get_mlines() {
210            unsafe {
211                for m_line in m_lines {
212                    // 1. replace the payload type ID in this media line's payload type list
213                    if !glib_sys::g_list_find(m_line.ptypes, from as *const _).is_null() {
214                        // payload type data in the list is cast to pointers
215                        m_line.ptypes = glib_sys::g_list_remove(m_line.ptypes, from as *const _);
216                        m_line.ptypes = glib_sys::g_list_prepend(m_line.ptypes, to as *mut _);
217                    }
218                    // 2. rewrite the values of attribute lines with the old payload type to have the new payload type
219                    let mut attr_node = m_line.attributes;
220                    while let Some(node) = attr_node.as_ref() {
221                        let next = node.next; // we might delete this link, so grab next now!
222                        let data = node.data as *mut RawAttribute;
223                        let attr = data.as_ref().expect("Null data in SDP attribute node :(");
224                        let name = CStr::from_ptr(attr.name).to_str().expect("Invalid attribute name in SDP :(");
225                        if MEDIA_PAYLOAD_ATTRIBUTES.contains(&name) {
226                            // each of the attributes with payload types in the values look like "$pt $stuff"
227                            // where $stuff is specifying payload-type-specfic options; just rewrite $pt
228                            let value = CStr::from_ptr(attr.value).to_str().expect("Invalid attribute value in SDP :(");
229                            if value.starts_with(&from_pt_string) {
230                                // value string is copied into the attribute
231                                let new_val = CString::new(value.replacen(&from_pt_string, &to_pt_string, 1)).unwrap();
232                                let new_attr = ffi::sdp::janus_sdp_attribute_create(attr.name, new_val.as_ptr());
233                                m_line.attributes = glib_sys::g_list_prepend(m_line.attributes, new_attr as *mut _);
234                                m_line.attributes = glib_sys::g_list_delete_link(m_line.attributes, attr_node);
235                                ffi::sdp::janus_sdp_attribute_destroy(data);
236                            }
237                        }
238                        attr_node = next;
239                    }
240                }
241            }
242        }
243    }
244
245    /// Returns a map of all the SDP media lines per SDP media type.
246    pub fn get_mlines(&self) -> HashMap<MediaType, Vec<&mut RawMLine>> {
247        let mut result = HashMap::new();
248        unsafe {
249            let mut ml_node = (*self.ptr).m_lines;
250            while let Some(node) = ml_node.as_ref() {
251                let ml = (node.data as *mut RawMLine).as_mut().expect("Null data in SDP media node :(");
252                result.entry(ml.type_).or_insert_with(Vec::new).push(ml);
253                ml_node = node.next;
254            }
255            result
256        }
257    }
258
259    /// Writes this SDP into an owned C-style string.
260    pub fn to_glibstring(&self) -> GLibString {
261        unsafe {
262            let sdp = ffi::sdp::janus_sdp_write(self.ptr);
263            GLibString::from_chars(sdp).expect("Mysterious error writing SDP to string :(")
264        }
265    }
266}
267
268impl Deref for Sdp {
269    type Target = RawSdp;
270
271    fn deref(&self) -> &RawSdp {
272        unsafe { &*self.ptr }
273    }
274}
275
276impl Drop for Sdp {
277    fn drop(&mut self) {
278        unsafe {
279            ffi::sdp::janus_sdp_destroy(self.ptr);
280        }
281    }
282}
283
284impl fmt::Debug for Sdp {
285    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
286        write!(f, "Sdp {{ {} }}", self.to_glibstring().to_string_lossy())
287    }
288}
289
290impl Serialize for Sdp {
291    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
292        self.to_glibstring().serialize(serializer)
293    }
294}
295
296impl<'de> Deserialize<'de> for Sdp {
297    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
298        struct SdpVisitor;
299        impl<'de> Visitor<'de> for SdpVisitor {
300            type Value = Sdp;
301
302            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
303                formatter.write_str("an SDP string")
304            }
305
306            fn visit_str<E>(self, value: &str) -> Result<Sdp, E> where E: de::Error {
307                if let Ok(cs_value) = CString::new(value) {
308                    if let Ok(sdp) = Sdp::parse(&cs_value) {
309                        return Ok(sdp)
310                    }
311                }
312                Err(E::invalid_value(Unexpected::Str(value), &self))
313            }
314        }
315        deserializer.deserialize_str(SdpVisitor)
316    }
317}
318
319unsafe impl Send for Sdp {}
320
321#[macro_export]
322/// Given an SDP offer from a client, generates an SDP answer.
323/// (This has to be a macro because `generate_answer` is variadic.)
324macro_rules! answer_sdp {
325    ($sdp:expr $(, $param:expr, $value:expr)* $(,)*) => {
326        unsafe {
327            let result = $crate::sdp::generate_answer(
328                $sdp.ptr,
329                $($param, $value,)*
330                $crate::sdp::OfferAnswerParameters::Done
331            );
332            $crate::sdp::Sdp::new(result).expect("Mysterious error generating SDP answer :(")
333        }
334    }
335}
336
337#[macro_export]
338/// Generates an SDP offer given some parameters.
339/// (This has to be a macro because `generate_offer` is variadic.)
340macro_rules! offer_sdp {
341    ($name:expr, $address:expr $(, $param:expr, $value:expr)* $(,)*) => {
342        unsafe {
343            let result = $crate::sdp::generate_offer(
344                $name,
345                $address,
346                $($param, $value,)*
347                $crate::sdp::OfferAnswerParameters::Done
348            );
349            $crate::sdp::Sdp::new(result).expect("Mysterious error generating SDP offer :(")
350        }
351    }
352}