From 42425abb69df018ee9fcc9433451c6ef3d24af76 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Fri, 12 Jan 2024 01:24:58 +0530 Subject: [PATCH] =?UTF-8?q?rtspsrc:=20Factor=20out=20SDP=20=E2=86=92=20Cap?= =?UTF-8?q?s,=20parse=20more=20attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This could be a struct of some kind derived from sdp_types::Media etc, but this is fine for now. Adds parsing of framesize, and fallbacks for missing or incomplete rtpmap. Part-of: --- net/rtsp/README.md | 10 +- net/rtsp/src/rtspsrc/imp.rs | 188 ++-------------- net/rtsp/src/rtspsrc/mod.rs | 1 + net/rtsp/src/rtspsrc/sdp.rs | 431 ++++++++++++++++++++++++++++++++++++ 4 files changed, 460 insertions(+), 170 deletions(-) create mode 100644 net/rtsp/src/rtspsrc/sdp.rs diff --git a/net/rtsp/README.md b/net/rtsp/README.md index a66ed12f..5086617a 100644 --- a/net/rtsp/README.md +++ b/net/rtsp/README.md @@ -38,8 +38,13 @@ Roughly in order of priority: * `GET_PARAMETER` / `SET_PARAMETER` * Make TCP connection optional when using UDP transport - Or TCP reconnection if UDP has not timed out -* Parse SDP rtcp-fb attributes -* Parse SDP ssrc attributes +* Parse more SDP attributes + - extmap + - key-mgmt + - rid + - rtcp-fb + - source-filter + - ssrc * Clock sync support, such as RFC7273 * PAUSE support with VOD * Seeking support with VOD @@ -60,7 +65,6 @@ yet: ## Maintenance and future cleanup -* Refactor SDP → Caps parsing into a module * Test with market RTSP cameras - Currently, only live555 and gst-rtsp-server have been tested * Add tokio-console and tokio tracing support diff --git a/net/rtsp/src/rtspsrc/imp.rs b/net/rtsp/src/rtspsrc/imp.rs index c5e79d67..2fc3ba9c 100644 --- a/net/rtsp/src/rtspsrc/imp.rs +++ b/net/rtsp/src/rtspsrc/imp.rs @@ -45,6 +45,7 @@ use gst::prelude::*; use gst::subclass::prelude::*; use super::body::Body; +use super::sdp; use super::transport::RtspTransportInfo; const DEFAULT_LOCATION: Option = None; @@ -1360,99 +1361,6 @@ impl RtspTaskState { Ok(()) } - fn parse_fmtp(fmtp: &str, s: &mut gst::structure::Structure) { - // Non-compliant RTSP servers will incorrectly set these here, ignore them - let ignore_fields = [ - "media", - "payload", - "clock-rate", - "encoding-name", - "encoding-params", - ]; - let encoding_name = s.get::("encoding-name").unwrap(); - let Some((_, fmtp)) = fmtp.split_once(' ') else { - gst::warning!(CAT, "Could not parse fmtp: {fmtp}"); - return; - }; - let iter = fmtp.split(';').map_while(|x| x.split_once('=')); - for (k, v) in iter { - let k = k.trim().to_ascii_lowercase(); - if ignore_fields.contains(&k.as_str()) { - continue; - } - if encoding_name == "H264" && k == "profile-level-id" { - let profile_idc = u8::from_str_radix(&v[0..2], 16); - let csf_idc = u8::from_str_radix(&v[2..4], 16); - let level_idc = u8::from_str_radix(&v[4..6], 16); - if let (Ok(p), Ok(c), Ok(l)) = (profile_idc, csf_idc, level_idc) { - let sps = &[p, c, l]; - let profile = gst_pbutils::codec_utils_h264_get_profile(sps); - let level = gst_pbutils::codec_utils_h264_get_level(sps); - if let (Ok(profile), Ok(level)) = (profile, level) { - s.set("profile", profile); - s.set("level", level); - continue; - } - } - gst::warning!(CAT, "Failed to parse profile-level-id {v}, ignoring..."); - continue; - } - s.set(k, v); - } - } - - fn parse_rtpmap(rtpmap: &str, s: &mut gst::structure::Structure) -> Result<(), RtspError> { - let Some((_, rtpmap)) = rtpmap.split_once(' ') else { - return Err(RtspError::InvalidMessage( - "Could not parse rtpmap: {rtpmap}", - )); - }; - - let mut iter = rtpmap.split('/'); - let Some(encoding_name) = iter.next() else { - return Err(RtspError::InvalidMessage( - "Could not parse encoding-name from rtpmap: {rtpmap}", - )); - }; - s.set("encoding-name", encoding_name); - - let Some(v) = iter.next() else { - return Err(RtspError::InvalidMessage( - "Could not parse clock-rate from rtpmap: {rtpmap}", - )); - }; - - let Ok(clock_rate) = v.parse::() else { - return Err(RtspError::InvalidMessage( - "Could not parse clock-rate from rtpmap: {rtpmap}", - )); - }; - s.set("clock-rate", clock_rate); - - if let Some(v) = iter.next() { - s.set("encoding-params", v); - } - - debug_assert!(iter.next().is_none()); - - Ok(()) - } - - // https://datatracker.ietf.org/doc/html/rfc2326#appendix-C.1.1 - fn parse_control_path(path: &str, base: &Url) -> Option { - match Url::parse(path) { - Ok(v) => Some(v), - Err(url::ParseError::RelativeUrlWithoutBase) => { - if path == "*" { - Some(base.clone()) - } else { - base.join(path).ok() - } - } - Err(_) => None, - } - } - fn parse_setup_transports( transports: &Transports, s: &mut gst::Structure, @@ -1518,9 +1426,10 @@ impl RtspTaskState { // No attribute and no value have the same meaning for us .ok() .flatten() - .and_then(|v| Self::parse_control_path(v, &base)); + .and_then(|v| sdp::parse_control_path(v, &base)); let mut b = gst::Structure::builder("application/x-rtp"); + // TODO: parse range for VOD let skip_attrs = ["control", "range"]; for sdp_types::Attribute { attribute, value } in &sdp.attributes { if skip_attrs.contains(&attribute.as_str()) { @@ -1528,6 +1437,8 @@ impl RtspTaskState { } b = b.field(format!("a-{attribute}"), value); } + // TODO: parse global extmap + let message_structure = b.build(); let conn_source = sdp @@ -1539,7 +1450,6 @@ impl RtspTaskState { let mut port_next = port_start; let mut stream_num = 0; let mut setup_params: Vec = Vec::new(); - let skip_attrs = ["control", "rtpmap", "fmtp"]; for m in &sdp.medias { if !["audio", "video"].contains(&m.media.as_str()) { gst::info!(CAT, "Ignoring unsupported media {}", m.media); @@ -1550,7 +1460,7 @@ impl RtspTaskState { // No attribute and no value have the same meaning for us .ok() .flatten() - .and_then(|v| Self::parse_control_path(v, &base)); + .and_then(|v| sdp::parse_control_path(v, &base)); let Some(control_url) = media_control.as_ref().or(self.aggregate_control.as_ref()) else { gst::warning!( @@ -1563,87 +1473,31 @@ impl RtspTaskState { }; // RTP caps - // FIXME: move SDP -> Caps parsing to a separate file - debug_assert_eq!(m.port, 0); // TCP - let Ok(pt) = m.fmt.parse::() else { + let Ok(pt) = m.fmt.parse::() else { gst::error!(CAT, "Could not parse pt: {}, ignoring media", m.fmt); continue; }; let mut s = message_structure.clone(); - s.set("media", &m.media); - s.set("payload", pt); + let media = m.media.to_ascii_lowercase(); + s.set("media", &media); + s.set("payload", pt as i32); - if let Ok(Some(rtpmap)) = m.get_first_attribute_value("rtpmap") { - Self::parse_rtpmap(rtpmap, &mut s)?; - } else { - gst::warning!(CAT, "No rtpmap for {} {}, skipping", m.media, m.fmt); + if let Err(err) = sdp::parse_media_attributes(&m.attributes, pt, &media, &mut s) { + gst::warning!( + CAT, + "Skipping media {} {}, no rtpmap: {err:?}", + m.media, + m.fmt + ); continue; } - if let Ok(Some(fmtp)) = m.get_first_attribute_value("fmtp") { - Self::parse_fmtp(fmtp, &mut s); - } - - for sdp_types::Attribute { attribute, value } in &m.attributes { - if skip_attrs.contains(&attribute.as_str()) { - continue; - } - // https://github.com/sdroege/sdp-types/issues/17 - if attribute == "ssrc" { - continue; - } - s.set(format!("a-{attribute}"), value); - } - - // TODO: rtcp-fb: fields - - if s.get_optional("encoding-name") == Ok(Some("H264")) { - if s.get_optional("level-asymmetry-allowed") != Ok(Some("0")) - && s.has_field("level") - { - s.remove_field("level"); - } - if s.has_field("level-asymmetry-allowed") { - s.remove_field("level-asymmetry-allowed"); - }; - } - // SETUP let mut rtp_socket: Option = None; let mut rtcp_socket: Option = None; let mut transports = Vec::new(); - let mut is_ipv4 = true; - let mut conn_protocols = BTreeSet::new(); - for conn in &m.connections { - if conn.nettype != "IN" { - continue; - } - // XXX: For now, assume that all connections use the same addrtype - match conn.addrtype.as_str() { - "IP4" => is_ipv4 = true, - "IP6" => is_ipv4 = false, - _ => continue, - }; - // Strip subnet mask, if any - let addr = if let Some((first, _)) = conn.connection_address.split_once('/') { - first - } else { - conn.connection_address.as_str() - }; - let Ok(addr) = addr.parse::() else { - continue; - }; - // If this is an instance of gst-rtsp-server that only supports - // udp-multicast, it will put the multicast address in the media - // connections field. - if addr.is_multicast() { - conn_protocols.insert(RtspProtocol::UdpMulticast); - } else { - conn_protocols.insert(RtspProtocol::Tcp); - conn_protocols.insert(RtspProtocol::Udp); - } - } + let (conn_protocols, is_ipv4) = sdp::parse_connections(&m.connections); let protocols = if !conn_protocols.is_empty() { let p = protocols.iter().cloned().collect::>(); @@ -2007,7 +1861,7 @@ async fn udp_rtp_task( // We would not be connected if the server didn't give us a Transport header or its Transport // header didn't specify the server port, so we don't know the sender port from which we will // get data till we get the first packet here. - if !socket.peer_addr().is_ok() { + if socket.peer_addr().is_err() { let ret = match time::timeout(t, socket.peek_sender()).await { Ok(Ok(addr)) => { let _ = socket.connect(addr).await; @@ -2035,10 +1889,10 @@ async fn udp_rtp_task( pool.set_active(true).unwrap(); let error = loop { let Ok(buffer) = pool.acquire_buffer(None) else { - break format!("Failed to acquire buffer"); + break "Failed to acquire buffer".to_string(); }; let Ok(mut map) = buffer.into_mapped_buffer_writable() else { - break format!("Failed to map buffer writable"); + break "Failed to map buffer writable".to_string(); }; match time::timeout(t, socket.recv(map.as_mut_slice())).await { Ok(Ok(len)) => { diff --git a/net/rtsp/src/rtspsrc/mod.rs b/net/rtsp/src/rtspsrc/mod.rs index dd2ed94a..429d9403 100644 --- a/net/rtsp/src/rtspsrc/mod.rs +++ b/net/rtsp/src/rtspsrc/mod.rs @@ -13,6 +13,7 @@ use gst::prelude::*; mod body; mod imp; +mod sdp; mod tcp_message; mod transport; diff --git a/net/rtsp/src/rtspsrc/sdp.rs b/net/rtsp/src/rtspsrc/sdp.rs new file mode 100644 index 00000000..e2e7e324 --- /dev/null +++ b/net/rtsp/src/rtspsrc/sdp.rs @@ -0,0 +1,431 @@ +// Rust RTSP Server +// +// Copyright (C) 2024 Nirbheek Chauhan +// +// This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at +// . +// +// SPDX-License-Identifier: MPL-2.0 + +use super::imp::RtspError; +use super::imp::RtspProtocol; +use super::imp::CAT; +use sdp_types::Attribute; +use sdp_types::Connection; +use std::collections::BTreeSet; +use std::net::IpAddr; +use url::Url; + +macro_rules! init_payload_info { + ($($t:expr),*,) => { + [ + $( + { + let (pt, media, encoding_name, clock_rate, encoding_params) = $t; + PayloadInfo { + pt, + media, + encoding_name, + clock_rate, + encoding_params, + } + } + ),* + ] + }; +} + +struct PayloadInfo<'a> { + pt: u8, + media: &'a str, + encoding_name: &'a str, + clock_rate: u32, + encoding_params: Option<&'a str>, +} + +// Copied from gst-plugins-base/gst-libs/gst/rtp/gstrtppayloads.h +const STATIC_PAYLOAD_INFO: &[PayloadInfo] = &init_payload_info!( + // static audio + (0, "audio", "PCMU", 8000, Some("1")), + // (1, "audio", "reserved", 0, None), + // (2, "audio", "reserved", 0, None), + (3, "audio", "GSM", 8000, Some("1")), + (4, "audio", "G723", 8000, Some("1")), + (5, "audio", "DVI4", 8000, Some("1")), + (6, "audio", "DVI4", 16000, Some("1")), + (7, "audio", "LPC", 8000, Some("1")), + (8, "audio", "PCMA", 8000, Some("1")), + (9, "audio", "G722", 8000, Some("1")), + (10, "audio", "L16", 44100, Some("2")), + (11, "audio", "L16", 44100, Some("1")), + (12, "audio", "QCELP", 8000, Some("1")), + (13, "audio", "CN", 8000, Some("1")), + (14, "audio", "MPA", 90000, None), + (15, "audio", "G728", 8000, Some("1")), + (16, "audio", "DVI4", 11025, Some("1")), + (17, "audio", "DVI4", 22050, Some("1")), + (18, "audio", "G729", 8000, Some("1")), + // (19, "audio", "reserved", 0, None), + // (20, "audio", "unassigned", 0, None), + // (21, "audio", "unassigned", 0, None), + // (22, "audio", "unassigned", 0, None), + // (23, "audio", "unassigned", 0, None), + + // video and video/audio + // (24, "video", "unassigned", 0, None), + (25, "video", "CelB", 90000, None), + (26, "video", "JPEG", 90000, None), + // (27, "video", "unassigned", 0, None), + (28, "video", "nv", 90000, None), + // (29, "video", "unassigned", 0, None), + // (30, "video", "unassigned", 0, None), + (31, "video", "H261", 90000, None), + (32, "video", "MPV", 90000, None), + (33, "video", "MP2T", 90000, None), + (34, "video", "H263", 90000, None), + // (35-71, "unassigned", 0, 0, None), + // (72-76, "reserved", 0, 0, None), + // (77-95, "unassigned", 0, 0, None), + // (96-127, "dynamic", 0, 0, None), +); + +// Known media types with dynamic payloads, can only be matched via name +const DYNAMIC_PAYLOAD_INFO: &[PayloadInfo] = &init_payload_info!( + (0, "application", "parityfec", 0, None), // [RFC3009] + (0, "application", "rtx", 0, None), // [RFC4588] + (0, "audio", "AMR", 8000, None), // [RFC4867][RFC3267] + (0, "audio", "AMR-WB", 16000, None), // [RFC4867][RFC3267] + (0, "audio", "DAT12", 0, None), // [RFC3190] + (0, "audio", "dsr-es201108", 0, None), // [RFC3557] + (0, "audio", "EVRC", 8000, Some("1")), // [RFC4788] + (0, "audio", "EVRC0", 8000, Some("1")), // [RFC4788] + (0, "audio", "EVRC1", 8000, Some("1")), // [RFC4788] + (0, "audio", "EVRCB", 8000, Some("1")), // [RFC4788] + (0, "audio", "EVRCB0", 8000, Some("1")), // [RFC4788] + (0, "audio", "EVRCB1", 8000, Some("1")), // [RFC4788] + (0, "audio", "G7221", 16000, Some("1")), // [RFC3047] + (0, "audio", "G726-16", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "G726-24", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "G726-32", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "G726-40", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "G729D", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "G729E", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "GSM-EFR", 8000, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "L8", 0, None), // [RFC3551][RFC4856] + (0, "audio", "RED", 0, None), // [RFC2198][RFC3555] + (0, "audio", "rtx", 0, None), // [RFC4588] + (0, "audio", "VDVI", 0, Some("1")), // [RFC3551][RFC4856] + (0, "audio", "L20", 0, None), // [RFC3190] + (0, "audio", "L24", 0, None), // [RFC3190] + (0, "audio", "MP4A-LATM", 0, None), // [RFC3016] + (0, "audio", "mpa-robust", 90000, None), // [RFC3119] + (0, "audio", "parityfec", 0, None), // [RFC3009] + (0, "audio", "SMV", 8000, Some("1")), // [RFC3558] + (0, "audio", "SMV0", 8000, Some("1")), // [RFC3558] + (0, "audio", "t140c", 0, None), // [RFC4351] + (0, "audio", "t38", 0, None), // [RFC4612] + (0, "audio", "telephone-event", 0, None), // [RFC4733] + (0, "audio", "tone", 0, None), // [RFC4733] + (0, "audio", "DVI4", 0, None), // [RFC4856] + (0, "audio", "G722", 0, None), // [RFC4856] + (0, "audio", "G723", 0, None), // [RFC4856] + (0, "audio", "G728", 0, None), // [RFC4856] + (0, "audio", "G729", 0, None), // [RFC4856] + (0, "audio", "GSM", 0, None), // [RFC4856] + (0, "audio", "L16", 0, None), // [RFC4856] + (0, "audio", "LPC", 0, None), // [RFC4856] + (0, "audio", "PCMA", 0, None), // [RFC4856] + (0, "audio", "PCMU", 0, None), // [RFC4856] + (0, "text", "parityfec", 0, None), // [RFC3009] + (0, "text", "red", 1000, None), // [RFC4102] + (0, "text", "rtx", 0, None), // [RFC4588] + (0, "text", "t140", 1000, None), // [RFC4103] + (0, "video", "BMPEG", 90000, None), // [RFC2343][RFC3555] + (0, "video", "BT656", 90000, None), // [RFC2431][RFC3555] + (0, "video", "DV", 90000, None), // [RFC3189] + (0, "video", "H263-1998", 90000, None), // [RFC2429][RFC3555] + (0, "video", "H263-2000", 90000, None), // [RFC2429][RFC3555] + (0, "video", "MP1S", 90000, None), // [RFC2250][RFC3555] + (0, "video", "MP2P", 90000, None), // [RFC2250][RFC3555] + (0, "video", "MP4V-ES", 90000, None), // [RFC3016] + (0, "video", "parityfec", 0, None), // [RFC3009] + (0, "video", "pointer", 90000, None), // [RFC2862] + (0, "video", "raw", 90000, None), // [RFC4175] + (0, "video", "rtx", 0, None), // [RFC4588] + (0, "video", "SMPTE292M", 0, None), // [RFC3497] + (0, "video", "vc1", 90000, None), // [RFC4425] + // not in http://www.iana.org/assignments/rtp-parameters + (0, "audio", "AC3", 0, None), + (0, "audio", "ILBC", 8000, None), + (0, "audio", "MPEG4-GENERIC", 0, None), + (0, "audio", "SPEEX", 0, None), + (0, "audio", "OPUS", 48000, None), + (0, "application", "MPEG4-GENERIC", 0, None), + (0, "video", "H264", 90000, None), + (0, "video", "H265", 90000, None), + (0, "video", "MPEG4-GENERIC", 90000, None), + (0, "video", "THEORA", 0, None), + (0, "video", "VORBIS", 0, None), + (0, "video", "X-SV3V-ES", 90000, None), + (0, "video", "X-SORENSON-VIDEO", 90000, None), + (0, "video", "VP8", 90000, None), + (0, "video", "VP9", 90000, None), +); + +// https://datatracker.ietf.org/doc/html/rfc2326#appendix-C.1.1 +pub fn parse_control_path(path: &str, base: &Url) -> Option { + match Url::parse(path) { + Ok(v) => Some(v), + Err(url::ParseError::RelativeUrlWithoutBase) => { + if path == "*" { + Some(base.clone()) + } else { + base.join(path).ok() + } + } + Err(_) => None, + } +} + +fn parse_rtpmap( + rtpmap: &str, + pt: u8, + media: &str, + s: &mut gst::structure::Structure, +) -> Result<(), RtspError> { + let Some((_pt, rtpmap)) = rtpmap.split_once(' ') else { + return Err(RtspError::Fatal(format!( + "Could not parse rtpmap: {rtpmap}" + ))); + }; + + let mut iter = rtpmap.split('/'); + let Some(encoding_name) = iter.next() else { + return Err(RtspError::Fatal(format!( + "Could not parse encoding-name from rtpmap: {rtpmap}" + ))); + }; + let encoding_name = encoding_name.to_ascii_uppercase(); + s.set("encoding-name", &encoding_name); + + let Some(v) = iter.next() else { + if pt >= 96 { + return guess_rtpmap_from_pt(pt, media, s).map_err(|err| { + RtspError::Fatal(format!( + "Could not get clock-rate from rtpmap {rtpmap}: {}", + err + )) + }); + } else { + return guess_rtpmap_from_encoding_name(&encoding_name, media, s).map_err(|err| { + RtspError::Fatal(format!( + "Could not get clock-rate from rtpmap {rtpmap}: {}", + err + )) + }); + } + }; + + let Ok(clock_rate) = v.parse::() else { + return Err(RtspError::Fatal(format!( + "Could not parse clock-rate from rtpmap: {rtpmap}" + ))); + }; + s.set("clock-rate", clock_rate); + + if let Some(v) = iter.next() { + s.set("encoding-params", v); + } + + debug_assert!(iter.next().is_none()); + + Ok(()) +} + +fn guess_rtpmap_from_encoding_name( + encoding_name: &str, + media: &str, + s: &mut gst::structure::Structure, +) -> Result<(), RtspError> { + for info in STATIC_PAYLOAD_INFO + .iter() + .chain(DYNAMIC_PAYLOAD_INFO.iter()) + { + if media == info.media && encoding_name == info.encoding_name { + s.set("encoding-name", info.encoding_name); + if info.clock_rate > 0 { + s.set("clock-rate", info.clock_rate); + } + if let Some(v) = info.encoding_params { + s.set("encoding-params", v); + }; + return Ok(()); + } + } + Err(RtspError::Fatal(format!( + "Cannot guess rtpmap: unknown encoding name {encoding_name}" + ))) +} + +fn guess_rtpmap_from_pt( + pt: u8, + media: &str, + s: &mut gst::structure::Structure, +) -> Result<(), RtspError> { + if pt >= 96 { + return Err(RtspError::Fatal(format!( + "Unknown dynamic payload type {pt}", + ))); + } + for info in STATIC_PAYLOAD_INFO { + if pt == info.pt && media == info.media { + s.set("encoding-name", info.encoding_name); + if info.clock_rate > 0 { + s.set("clock-rate", info.clock_rate); + } + if let Some(v) = info.encoding_params { + s.set("encoding-params", v); + }; + return Ok(()); + } + } + Err(RtspError::Fatal(format!( + "Cannot guess rtpmap: unknown static payload type {pt}" + ))) +} + +fn parse_fmtp(fmtp: &str, s: &mut gst::structure::Structure) { + // Non-compliant RTSP servers will incorrectly set these here, ignore them + let ignore_fields = [ + "media", + "payload", + "clock-rate", + "encoding-name", + "encoding-params", + ]; + let encoding_name = s.get::("encoding-name").unwrap(); + let Some((_pt, fmtp)) = fmtp.split_once(' ') else { + gst::warning!(CAT, "Could not parse fmtp: {fmtp}"); + return; + }; + let iter = fmtp.split(';').map_while(|x| x.split_once('=')); + for (k, v) in iter { + let k = k.trim().to_ascii_lowercase(); + if ignore_fields.contains(&k.as_str()) { + continue; + } + if encoding_name == "H264" && k == "profile-level-id" { + let profile_idc = u8::from_str_radix(&v[0..2], 16); + let csf_idc = u8::from_str_radix(&v[2..4], 16); + let level_idc = u8::from_str_radix(&v[4..6], 16); + if let (Ok(p), Ok(c), Ok(l)) = (profile_idc, csf_idc, level_idc) { + let sps = &[p, c, l]; + let profile = gst_pbutils::codec_utils_h264_get_profile(sps); + let level = gst_pbutils::codec_utils_h264_get_level(sps); + if let (Ok(profile), Ok(level)) = (profile, level) { + s.set("profile", profile); + s.set("level", level); + continue; + } + } + gst::warning!(CAT, "Failed to parse profile-level-id {v}, ignoring..."); + continue; + } + s.set(k, v); + } + + // Adjust H264 caps for level asymmetry + if encoding_name == "H264" { + if s.get_optional("level-asymmetry-allowed") != Ok(Some("0")) && s.has_field("level") { + s.remove_field("level"); + } + if s.has_field("level-asymmetry-allowed") { + s.remove_field("level-asymmetry-allowed"); + }; + } +} + +fn parse_framesize(framesize: &str, s: &mut gst::structure::Structure) { + let Some((_pt, dim)) = framesize.split_once(' ') else { + gst::warning!(CAT, "Could not parse framesize {framesize}, ignoring"); + return; + }; + + s.set("a-framesize", dim); +} + +pub fn parse_media_attributes( + attrs: &Vec, + pt: u8, + media: &str, + s: &mut gst::structure::Structure, +) -> Result<(), RtspError> { + let mut skip_attrs = vec!["control", "range", "ssrc"]; + + for Attribute { attribute, value } in attrs { + let attr = attribute.as_str(); + if skip_attrs.contains(&attr) { + continue; + } + + let Some(value) = value else { + continue; + }; + + if attr.starts_with("x-") { + s.set(attr, value); + continue; + } + + match attr { + "rtpmap" => parse_rtpmap(value, pt, media, s)?, + "fmtp" => parse_fmtp(value, s), + "framesize" => parse_framesize(value, s), + // TODO: extmap, key-mgmt, rid, rtcp-fb, source-filter, ssrc + _ => s.set(format!("a-{attribute}"), value), + }; + skip_attrs.push(attr); + } + + if !skip_attrs.contains(&"rtpmap") { + guess_rtpmap_from_pt(pt, media, s)?; + } + + Ok(()) +} + +pub fn parse_connections(conns: &Vec) -> (BTreeSet, bool) { + let mut is_ipv4 = true; + let mut conn_protocols = BTreeSet::new(); + for conn in conns { + if conn.nettype != "IN" { + continue; + } + // XXX: For now, assume that all connections use the same addrtype + match conn.addrtype.as_str() { + "IP4" => is_ipv4 = true, + "IP6" => is_ipv4 = false, + _ => continue, + }; + // Strip subnet mask, if any + let addr = if let Some((first, _)) = conn.connection_address.split_once('/') { + first + } else { + conn.connection_address.as_str() + }; + let Ok(addr) = addr.parse::() else { + continue; + }; + // If this is an instance of gst-rtsp-server that only supports + // udp-multicast, it will put the multicast address in the media + // connections field. + if addr.is_multicast() { + conn_protocols.insert(RtspProtocol::UdpMulticast); + } else { + conn_protocols.insert(RtspProtocol::Tcp); + conn_protocols.insert(RtspProtocol::Udp); + } + } + (conn_protocols, is_ipv4) +}