mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-05-20 09:18:15 +00:00
42425abb69
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>
432 lines
15 KiB
Rust
432 lines
15 KiB
Rust
// 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)
|
|
}
|