rtspsrc: Factor out SDP → Caps, parse more attributes

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: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1425>
This commit is contained in:
Nirbheek Chauhan 2024-01-12 01:24:58 +05:30
parent 437326ebfd
commit 42425abb69
4 changed files with 460 additions and 170 deletions

View file

@ -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

View file

@ -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<Url> = 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::<String>("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::<i32>() 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<Url> {
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<RtspSetupParams> = 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::<i32>() else {
let Ok(pt) = m.fmt.parse::<u8>() 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<UdpSocket> = None;
let mut rtcp_socket: Option<UdpSocket> = 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::<IpAddr>() 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::<BTreeSet<_>>();
@ -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)) => {

View file

@ -13,6 +13,7 @@ use gst::prelude::*;
mod body;
mod imp;
mod sdp;
mod tcp_message;
mod transport;

431
net/rtsp/src/rtspsrc/sdp.rs Normal file
View file

@ -0,0 +1,431 @@
// Rust RTSP Server
//
// Copyright (C) 2024 Nirbheek Chauhan <nirbheek@centricular.com>
//
// 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
// <https://mozilla.org/MPL/2.0/>.
//
// 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<Url> {
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::<i32>() 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::<String>("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<Attribute>,
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<Connection>) -> (BTreeSet<RtspProtocol>, 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::<IpAddr>() 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)
}