rtp: Initial rtpbin2 element

Can receive and recevie one or more RTP sessions containing multiple
pt/ssrc combinations.

Demultiplexing happens internally instead of relying on separate
elements.

Co-Authored-By: François Laignel <francois@centricular.com>
Co-Authored-By: Mathieu Duponchelle <mathieu@centricular.com>
Co-Authored-By: Sebastian Dröge <sebastian@centricular.com>
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1426>
This commit is contained in:
Matthew Waters 2023-10-12 17:06:48 +11:00
parent 984a9fe5ff
commit 27ad26c258
10 changed files with 4945 additions and 3 deletions

15
Cargo.lock generated
View file

@ -2754,21 +2754,28 @@ dependencies = [
"bitstream-io", "bitstream-io",
"byte-slice-cast", "byte-slice-cast",
"chrono", "chrono",
"futures",
"gio",
"gst-plugin-version-helper", "gst-plugin-version-helper",
"gstreamer", "gstreamer",
"gstreamer-app", "gstreamer-app",
"gstreamer-audio", "gstreamer-audio",
"gstreamer-base",
"gstreamer-check", "gstreamer-check",
"gstreamer-net",
"gstreamer-rtp", "gstreamer-rtp",
"gstreamer-video", "gstreamer-video",
"hex", "hex",
"log",
"once_cell", "once_cell",
"rand", "rand",
"rtcp-types",
"rtp-types", "rtp-types",
"slab", "slab",
"smallvec", "smallvec",
"thiserror", "thiserror",
"time", "time",
"tokio",
] ]
[[package]] [[package]]
@ -5821,6 +5828,14 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "rtcp-types"
version = "0.0.1"
source = "git+https://github.com/ystreet/rtcp-types#f7fddfb87e9d7f4fed0b967fedc34995dd81ca86"
dependencies = [
"thiserror",
]
[[package]] [[package]]
name = "rtp-types" name = "rtp-types"
version = "0.1.1" version = "0.1.1"

View file

@ -7191,6 +7191,92 @@
}, },
"rank": "marginal" "rank": "marginal"
}, },
"rtpbin2": {
"author": "Matthew Waters <matthew@centricular.com>",
"description": "RTP sessions management",
"hierarchy": [
"GstRtpBin2",
"GstElement",
"GstObject",
"GInitiallyUnowned",
"GObject"
],
"klass": "Network/RTP/Filter",
"pad-templates": {
"rtcp_recv_sink_%%u": {
"caps": "application/x-rtcp:\n",
"direction": "sink",
"presence": "request"
},
"rtcp_send_src_%%u": {
"caps": "application/x-rtcp:\n",
"direction": "src",
"presence": "request"
},
"rtp_recv_sink_%%u": {
"caps": "application/x-rtp:\n",
"direction": "sink",
"presence": "request"
},
"rtp_recv_src_%%u_%%u_%%u": {
"caps": "application/x-rtp:\n",
"direction": "src",
"presence": "sometimes"
},
"rtp_send_sink_%%u": {
"caps": "application/x-rtp:\n",
"direction": "sink",
"presence": "request"
},
"rtp_send_src_%%u": {
"caps": "application/x-rtp:\n",
"direction": "src",
"presence": "sometimes"
}
},
"properties": {
"latency": {
"blurb": "Amount of ms to buffer",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "0",
"max": "-1",
"min": "0",
"mutable": "ready",
"readable": true,
"type": "guint",
"writable": true
},
"min-rtcp-interval": {
"blurb": "Minimum time (in ms) between RTCP reports",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"default": "5000",
"max": "-1",
"min": "0",
"mutable": "ready",
"readable": true,
"type": "guint",
"writable": true
},
"stats": {
"blurb": "Statistics about the session",
"conditionally-available": false,
"construct": false,
"construct-only": false,
"controllable": false,
"mutable": "null",
"readable": true,
"type": "guint",
"writable": false
}
},
"rank": "none"
},
"rtpgccbwe": { "rtpgccbwe": {
"author": "Thibault Saunier <tsaunier@igalia.com>", "author": "Thibault Saunier <tsaunier@igalia.com>",
"description": "Estimates current network bandwidth using the Google Congestion Control algorithm notifying about it through the 'bitrate' property", "description": "Estimates current network bandwidth using the Google Congestion Control algorithm notifying about it through the 'bitrate' property",

View file

@ -16,16 +16,24 @@ byte-slice-cast = "1.2"
chrono = { version = "0.4", default-features = false } chrono = { version = "0.4", default-features = false }
gst = { workspace = true, features = ["v1_20"] } gst = { workspace = true, features = ["v1_20"] }
gst-audio = { workspace = true, features = ["v1_20"] } gst-audio = { workspace = true, features = ["v1_20"] }
gst-base = { workspace = true, features = ["v1_20"] }
gst-net = { workspace = true, features = ["v1_20"] }
gst-rtp = { workspace = true, features = ["v1_20"] } gst-rtp = { workspace = true, features = ["v1_20"] }
gst-video = { workspace = true, features = ["v1_20"] } gst-video = { workspace = true, features = ["v1_20"] }
futures = "0.3"
gio.workspace = true
hex = "0.4.3" hex = "0.4.3"
log = "0.4"
once_cell.workspace = true once_cell.workspace = true
rand = { version = "0.8", default-features = false, features = ["std", "std_rng" ] } rand = { version = "0.8", default-features = false, features = ["std", "std_rng" ] }
rtp-types = { version = "0.1" } rtp-types = { version = "0.1" }
rtcp-types = { git = "https://github.com/ystreet/rtcp-types", version = "0.0" }
slab = "0.4.9" slab = "0.4.9"
smallvec = { version = "1.11", features = ["union", "write", "const_generics", "const_new"] } smallvec = { version = "1.11", features = ["union", "write", "const_generics", "const_new"] }
thiserror = "1" thiserror = "1"
time = { version = "0.3", default-features = false, features = ["std"] } time = { version = "0.3", default-features = false, features = ["std"] }
# TODO: experiment with other async executors (mio, async-std, etc)
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "time"] }
[dev-dependencies] [dev-dependencies]
gst-check = { workspace = true, features = ["v1_20"] } gst-check = { workspace = true, features = ["v1_20"] }
@ -56,4 +64,4 @@ versioning = false
import_library = false import_library = false
[package.metadata.capi.pkg_config] [package.metadata.capi.pkg_config]
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-rtp-1.0, gobject-2.0, glib-2.0, gmodule-2.0" requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-rtp-1.0, gstreamer-net-1.0, gobject-2.0, glib-2.0, gmodule-2.0, gio-2.0"

View file

@ -14,12 +14,17 @@
* *
* Since: plugins-rs-0.9.0 * Since: plugins-rs-0.9.0
*/ */
#[macro_use]
extern crate log;
use gst::glib; use gst::glib;
#[macro_use] #[macro_use]
mod utils; mod utils;
mod gcc; mod gcc;
mod rtpbin2;
mod audio_discont; mod audio_discont;
mod baseaudiopay; mod baseaudiopay;
@ -42,6 +47,7 @@ mod tests;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gcc::register(plugin)?; gcc::register(plugin)?;
rtpbin2::register(plugin)?;
#[cfg(feature = "doc")] #[cfg(feature = "doc")]
{ {

1460
net/rtp/src/rtpbin2/imp.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
// SPDX-License-Identifier: MPL-2.0
use gst::glib;
use gst::prelude::*;
use once_cell::sync::Lazy;
mod imp;
mod session;
mod source;
mod time;
glib::wrapper! {
pub struct RtpBin2(ObjectSubclass<imp::RtpBin2>) @extends gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"rtpbin2",
gst::Rank::NONE,
RtpBin2::static_type(),
)
}
pub static RUNTIME: Lazy<tokio::runtime::Runtime> = Lazy::new(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_time()
.worker_threads(1)
.build()
.unwrap()
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
// SPDX-License-Identifier: MPL-2.0
use std::{
ops::{Add, Sub},
time::{Duration, SystemTime},
};
// time between the NTP time at 1900-01-01 and the unix EPOCH (1970-01-01)
const NTP_OFFSET: Duration = Duration::from_secs((365 * 70 + 17) * 24 * 60 * 60);
// 2^32
const F32: f64 = 4_294_967_296.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct NtpTime(u64);
impl NtpTime {
pub fn from_duration(dur: Duration) -> Self {
Self((dur.as_secs_f64() * F32) as u64)
}
pub fn as_u32(self) -> u32 {
((self.0 >> 16) & 0xffffffff) as u32
}
pub fn as_u64(self) -> u64 {
self.0
}
}
impl Sub for NtpTime {
type Output = NtpTime;
fn sub(self, rhs: Self) -> Self::Output {
NtpTime(self.0 - rhs.0)
}
}
impl Add for NtpTime {
type Output = NtpTime;
fn add(self, rhs: Self) -> Self::Output {
NtpTime(self.0 + rhs.0)
}
}
pub fn system_time_to_ntp_time_u64(time: SystemTime) -> NtpTime {
let dur = time
.duration_since(SystemTime::UNIX_EPOCH)
.expect("time is before unix epoch?!")
+ NTP_OFFSET;
NtpTime::from_duration(dur)
}
impl From<u64> for NtpTime {
fn from(value: u64) -> Self {
NtpTime(value)
}
}

159
net/rtp/tests/rtpbin2.rs Normal file
View file

@ -0,0 +1,159 @@
//
// Copyright (C) 2023 Matthew Waters <matthew@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 std::sync::{Arc, Mutex};
use gst::{prelude::*, Caps};
use gst_check::Harness;
use rtp_types::*;
fn init() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
gst::init().unwrap();
gstrsrtp::plugin_register_static().expect("rtpbin2 test");
});
}
const TEST_SSRC: u32 = 0x12345678;
const TEST_PT: u8 = 96;
const TEST_CLOCK_RATE: u32 = 48000;
fn generate_rtp_buffer(seqno: u16, rtpts: u32, payload_len: usize) -> gst::Buffer {
let payload = vec![4; payload_len];
let packet = RtpPacketBuilder::new()
.ssrc(TEST_SSRC)
.payload_type(TEST_PT)
.sequence_number(seqno)
.timestamp(rtpts)
.payload(&payload);
let size = packet.calculate_size().unwrap();
let mut data = vec![0; size];
packet.write_into(&mut data).unwrap();
gst::Buffer::from_mut_slice(data)
}
#[test]
fn test_send() {
init();
let mut h = Harness::with_padnames("rtpbin2", Some("rtp_send_sink_0"), Some("rtp_send_src_0"));
h.play();
let caps = Caps::builder("application/x-rtp")
.field("media", "audio")
.field("payload", TEST_PT as i32)
.field("clock-rate", TEST_CLOCK_RATE as i32)
.field("encoding-name", "custom-test")
.build();
h.set_src_caps(caps);
h.push(generate_rtp_buffer(500, 20, 9)).unwrap();
h.push(generate_rtp_buffer(501, 30, 11)).unwrap();
let buffer = h.pull().unwrap();
let mapped = buffer.map_readable().unwrap();
let rtp = rtp_types::RtpPacket::parse(&mapped).unwrap();
assert_eq!(rtp.sequence_number(), 500);
let buffer = h.pull().unwrap();
let mapped = buffer.map_readable().unwrap();
let rtp = rtp_types::RtpPacket::parse(&mapped).unwrap();
assert_eq!(rtp.sequence_number(), 501);
let stats = h.element().unwrap().property::<gst::Structure>("stats");
let session_stats = stats.get::<gst::Structure>("0").unwrap();
let source_stats = session_stats
.get::<gst::Structure>(TEST_SSRC.to_string())
.unwrap();
assert_eq!(source_stats.get::<u32>("ssrc").unwrap(), TEST_SSRC);
assert_eq!(
source_stats.get::<u32>("clock-rate").unwrap(),
TEST_CLOCK_RATE
);
assert!(source_stats.get::<bool>("sender").unwrap());
assert!(source_stats.get::<bool>("local").unwrap());
assert_eq!(source_stats.get::<u64>("packets-sent").unwrap(), 2);
assert_eq!(source_stats.get::<u64>("octets-sent").unwrap(), 20);
}
#[test]
fn test_receive() {
init();
let h = Arc::new(Mutex::new(Harness::with_padnames(
"rtpbin2",
Some("rtp_recv_sink_0"),
None,
)));
let weak_h = Arc::downgrade(&h);
let mut inner = h.lock().unwrap();
inner
.element()
.unwrap()
.connect_pad_added(move |_elem, pad| {
weak_h
.upgrade()
.unwrap()
.lock()
.unwrap()
.add_element_src_pad(pad)
});
inner.play();
let caps = Caps::builder("application/x-rtp")
.field("media", "audio")
.field("payload", TEST_PT as i32)
.field("clock-rate", TEST_CLOCK_RATE as i32)
.field("encoding-name", "custom-test")
.build();
inner.set_src_caps(caps);
// Cannot push with harness lock as the 'pad-added' handler needs to add the newly created pad to
// the harness and needs to also take the harness lock. Workaround by pushing from the
// internal harness pad directly.
let push_pad = inner
.element()
.unwrap()
.static_pad("rtp_recv_sink_0")
.unwrap()
.peer()
.unwrap();
drop(inner);
push_pad.push(generate_rtp_buffer(500, 20, 9)).unwrap();
push_pad.push(generate_rtp_buffer(501, 30, 11)).unwrap();
let mut inner = h.lock().unwrap();
let buffer = inner.pull().unwrap();
let mapped = buffer.map_readable().unwrap();
let rtp = rtp_types::RtpPacket::parse(&mapped).unwrap();
assert_eq!(rtp.sequence_number(), 500);
let buffer = inner.pull().unwrap();
let mapped = buffer.map_readable().unwrap();
let rtp = rtp_types::RtpPacket::parse(&mapped).unwrap();
assert_eq!(rtp.sequence_number(), 501);
let stats = inner.element().unwrap().property::<gst::Structure>("stats");
let session_stats = stats.get::<gst::Structure>("0").unwrap();
let source_stats = session_stats
.get::<gst::Structure>(TEST_SSRC.to_string())
.unwrap();
assert_eq!(source_stats.get::<u32>("ssrc").unwrap(), TEST_SSRC);
assert_eq!(
source_stats.get::<u32>("clock-rate").unwrap(),
TEST_CLOCK_RATE
);
assert!(source_stats.get::<bool>("sender").unwrap());
assert!(!source_stats.get::<bool>("local").unwrap());
assert_eq!(source_stats.get::<u64>("packets-received").unwrap(), 2);
assert_eq!(source_stats.get::<u64>("octets-received").unwrap(), 20);
}