diff --git a/net/quinn/src/common.rs b/net/quinn/src/common.rs
index 89bf8e9b..877e557f 100644
--- a/net/quinn/src/common.rs
+++ b/net/quinn/src/common.rs
@@ -6,6 +6,7 @@
// .
//
// SPDX-License-Identifier: MPL-2.0
+use bytes::BufMut;
use gst::glib;
use quinn::VarInt;
@@ -84,3 +85,93 @@ impl Default for QuinnQuicTransportConfig {
}
}
}
+
+// Taken from quinn-rs.
+pub fn get_varint_size(val: u64) -> usize {
+ if val < 2u64.pow(6) {
+ 1
+ } else if val < 2u64.pow(14) {
+ 2
+ } else if val < 2u64.pow(30) {
+ 4
+ } else if val < 2u64.pow(62) {
+ 8
+ } else {
+ unreachable!("malformed VarInt");
+ }
+}
+
+// Adapted from quinn-rs.
+pub fn get_varint(data: &[u8]) -> Option<(u64 /* VarInt value */, usize /* VarInt length */)> {
+ if data.is_empty() {
+ return None;
+ }
+
+ let data_length = data.len();
+ let tag = data[0] >> 6;
+
+ match tag {
+ 0b00 => {
+ let mut slice = [0; 1];
+ slice.clone_from_slice(&data[..1]);
+ slice[0] &= 0b0011_1111;
+
+ Some((u64::from(slice[0]), 1))
+ }
+ 0b01 => {
+ if data_length < 2 {
+ return None;
+ }
+
+ let mut buf = [0; 2];
+ buf.clone_from_slice(&data[..2]);
+ buf[0] &= 0b0011_1111;
+
+ Some((
+ u64::from(u16::from_be_bytes(buf[..2].try_into().unwrap())),
+ 2,
+ ))
+ }
+ 0b10 => {
+ if data_length < 4 {
+ return None;
+ }
+
+ let mut buf = [0; 4];
+ buf.clone_from_slice(&data[..4]);
+ buf[0] &= 0b0011_1111;
+
+ Some((
+ u64::from(u32::from_be_bytes(buf[..4].try_into().unwrap())),
+ 4,
+ ))
+ }
+ 0b11 => {
+ if data_length < 8 {
+ return None;
+ }
+
+ let mut buf = [0; 8];
+ buf.clone_from_slice(&data[..8]);
+ buf[0] &= 0b0011_1111;
+
+ Some((u64::from_be_bytes(buf[..8].try_into().unwrap()), 8))
+ }
+ _ => unreachable!(),
+ }
+}
+
+// Taken from quinn-rs.
+pub fn set_varint(data: &mut B, val: u64) {
+ if val < 2u64.pow(6) {
+ data.put_u8(val as u8);
+ } else if val < 2u64.pow(14) {
+ data.put_u16(0b01 << 14 | val as u16);
+ } else if val < 2u64.pow(30) {
+ data.put_u32(0b10 << 30 | val as u32);
+ } else if val < 2u64.pow(62) {
+ data.put_u64(0b11 << 62 | val);
+ } else {
+ unreachable!("malformed varint");
+ }
+}
diff --git a/net/quinn/src/lib.rs b/net/quinn/src/lib.rs
index 432ca5f0..5ee69b31 100644
--- a/net/quinn/src/lib.rs
+++ b/net/quinn/src/lib.rs
@@ -26,6 +26,8 @@ mod quinnquicmux;
pub mod quinnquicquery;
mod quinnquicsink;
mod quinnquicsrc;
+mod quinnroqdemux;
+mod quinnroqmux;
mod utils;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
@@ -36,6 +38,8 @@ fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
}
quinnquicdemux::register(plugin)?;
quinnquicmux::register(plugin)?;
+ quinnroqmux::register(plugin)?;
+ quinnroqdemux::register(plugin)?;
quinnquicsink::register(plugin)?;
quinnquicsrc::register(plugin)?;
diff --git a/net/quinn/src/quinnroqdemux/imp.rs b/net/quinn/src/quinnroqdemux/imp.rs
new file mode 100644
index 00000000..9248ebd8
--- /dev/null
+++ b/net/quinn/src/quinnroqdemux/imp.rs
@@ -0,0 +1,664 @@
+// Copyright (C) 2024, Asymptotic Inc.
+// Author: Sanchayan Maity
+//
+// 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
+
+// Implements RTP over QUIC as per the following specification,
+// https://datatracker.ietf.org/doc/draft-ietf-avtcore-rtp-over-quic/
+
+use crate::common::*;
+use crate::quinnquicmeta::QuinnQuicMeta;
+use crate::quinnquicquery::*;
+use bytes::{Buf, BytesMut};
+use gst::{glib, prelude::*, subclass::prelude::*};
+use std::collections::{HashMap, VecDeque};
+use std::io::{self, Cursor, Read};
+use std::ops::{Range, RangeBounds, RangeFrom};
+use std::sync::{LazyLock, Mutex};
+
+const SIGNAL_FLOW_ID_MAP: &str = "request-flow-id-map";
+
+static CAT: LazyLock = LazyLock::new(|| {
+ gst::DebugCategory::new(
+ "quinnroqdemux",
+ gst::DebugColorFlags::empty(),
+ Some("Quinn RTP over QUIC Demuxer"),
+ )
+});
+
+struct Reassembler {
+ buffer: BytesMut,
+ last_packet_len: u64,
+ // Track the timestamp of the last buffer pushed. Chunk boundaries
+ // in QUIC do not correspond to peer writes, and hence cannot be
+ // used for framing. BaseSrc will timestamp the buffers captured
+ // with running time. We make a best case effort to timestamp the
+ // reassembled packets by using the timestamp of the last buffer
+ // pushed after which a reassembled packet becomes available.
+ last_ts: Option,
+ // Source pad which this reassembler is for, primarily used for
+ // trace logging.
+ pad: gst::Pad,
+}
+
+impl Reassembler {
+ fn new(pad: gst::Pad) -> Self {
+ Self {
+ // `quinnquicsrc` reads 4096 bytes at a time by default
+ // which is the default BaseSrc blocksize.
+ buffer: BytesMut::with_capacity(4096),
+ // Track the length of the packet we are reassembling
+ last_packet_len: 0,
+ last_ts: None,
+ pad,
+ }
+ }
+
+ fn build_buffer(&mut self, packet_sz: u64) -> gst::Buffer {
+ let packet = self.buffer.split_to(packet_sz as usize);
+
+ let mut buffer = gst::Buffer::with_size(packet_sz as usize).unwrap();
+ {
+ let buffer_mut = buffer.get_mut().unwrap();
+ {
+ let mut buf_mut = buffer_mut.map_writable().unwrap();
+ buf_mut.clone_from_slice(&packet);
+ }
+
+ // Set DTS and let downstream manage PTS using jitterbuffer
+ // as jitterbuffer will do the DTS -> PTS work.
+ buffer_mut.set_dts(self.last_ts);
+ }
+
+ buffer.to_owned()
+ }
+
+ fn push_buffer(
+ &mut self,
+ buffer: &mut gst::Buffer,
+ ) -> Result {
+ if buffer.size() == 0 {
+ return Ok(gst::FlowSuccess::Ok);
+ }
+
+ self.last_ts = buffer.dts();
+
+ let buffer_mut = buffer.get_mut().ok_or(gst::FlowError::Error)?;
+ let map = buffer_mut
+ .map_readable()
+ .map_err(|_| gst::FlowError::Error)?;
+ let buffer_slice = map.as_slice();
+
+ self.buffer.extend_from_slice(buffer_slice);
+
+ gst::trace!(
+ CAT,
+ obj = self.pad,
+ "Added buffer of {} bytes, current buffer size: {}",
+ buffer_slice.len(),
+ self.buffer.len()
+ );
+
+ Ok(gst::FlowSuccess::Ok)
+ }
+
+ fn push_bytes(
+ &mut self,
+ buffer: &[u8],
+ dts: Option,
+ ) -> Result {
+ if buffer.is_empty() {
+ return Ok(gst::FlowSuccess::Ok);
+ }
+
+ self.last_ts = dts;
+
+ self.buffer.extend_from_slice(buffer);
+
+ gst::trace!(
+ CAT,
+ obj = self.pad,
+ "Added {} bytes, current buffer size: {}",
+ buffer.len(),
+ self.buffer.len()
+ );
+
+ Ok(gst::FlowSuccess::Ok)
+ }
+
+ fn pop(&mut self) -> Option {
+ if self.buffer.is_empty() {
+ return None;
+ }
+
+ self.last_ts?;
+
+ if self.last_packet_len != 0 {
+ // In the middle of reassembling a packet
+ if self.buffer.len() > self.last_packet_len as usize {
+ // Enough data to reassemble the packet
+ let buffer = self.build_buffer(self.last_packet_len);
+
+ self.last_packet_len = 0;
+
+ gst::trace!(
+ CAT,
+ obj = self.pad,
+ "Reassembled packet of size: {}, current buffer size: {}",
+ buffer.size(),
+ self.buffer.len()
+ );
+
+ return Some(buffer.to_owned());
+ } else {
+ // Do not have enough data yet to reassemble the packet
+ return None;
+ }
+ }
+
+ let (packet_sz, packet_sz_len) = get_varint(&self.buffer)?;
+
+ gst::trace!(
+ CAT,
+ obj = self.pad,
+ "Reassembler, packet size length: {}, packet: {}, buffer: {}",
+ packet_sz_len,
+ packet_sz,
+ self.buffer.len(),
+ );
+
+ self.buffer.advance(packet_sz_len);
+
+ if packet_sz > self.buffer.len() as u64 {
+ // Packet will span multiple buffers
+ self.last_packet_len = packet_sz;
+
+ gst::trace!(
+ CAT,
+ obj = self.pad,
+ "Accumulating for packet of size: {}, current buffer size: {}",
+ packet_sz,
+ self.buffer.len()
+ );
+
+ return None;
+ }
+
+ let buffer = self.build_buffer(packet_sz);
+
+ gst::trace!(
+ CAT,
+ obj = self.pad,
+ "Reassembled packet of size: {}, current buffer size: {}",
+ buffer.size(),
+ self.buffer.len()
+ );
+
+ Some(buffer.to_owned())
+ }
+}
+
+#[derive(Default)]
+struct Started {
+ stream_pads_map: HashMap,
+ datagram_pads_map: HashMap,
+}
+
+#[derive(Default)]
+enum State {
+ #[default]
+ Stopped,
+ Started(Started),
+}
+
+pub struct QuinnRoqDemux {
+ state: Mutex,
+ sinkpad: gst::Pad,
+}
+
+impl GstObjectImpl for QuinnRoqDemux {}
+
+impl ElementImpl for QuinnRoqDemux {
+ fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
+ static ELEMENT_METADATA: LazyLock = LazyLock::new(|| {
+ gst::subclass::ElementMetadata::new(
+ "Quinn RTP over QUIC Demultiplexer",
+ "Source/Network/QUIC",
+ "Demultiplexes multiple RTP streams over QUIC",
+ "Sanchayan Maity ",
+ )
+ });
+
+ Some(&*ELEMENT_METADATA)
+ }
+
+ fn pad_templates() -> &'static [gst::PadTemplate] {
+ static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| {
+ let sink_pad_template = gst::PadTemplate::new(
+ "sink",
+ gst::PadDirection::Sink,
+ gst::PadPresence::Always,
+ &gst::Caps::new_any(),
+ )
+ .unwrap();
+
+ let src_caps = gst::Caps::builder("application/x-rtp").build();
+
+ let src_pad_template = gst::PadTemplate::new(
+ "src_%u", // src_
+ gst::PadDirection::Src,
+ gst::PadPresence::Sometimes,
+ &src_caps,
+ )
+ .unwrap();
+
+ vec![sink_pad_template, src_pad_template]
+ });
+
+ PAD_TEMPLATES.as_ref()
+ }
+
+ fn change_state(
+ &self,
+ transition: gst::StateChange,
+ ) -> Result {
+ let ret = self.parent_change_state(transition)?;
+
+ if let gst::StateChange::NullToReady = transition {
+ let mut state = self.state.lock().unwrap();
+ *state = State::Started(Started::default());
+ }
+
+ Ok(ret)
+ }
+}
+
+impl ObjectImpl for QuinnRoqDemux {
+ fn signals() -> &'static [glib::subclass::Signal] {
+ static SIGNALS: LazyLock> = LazyLock::new(|| {
+ /*
+ * See section 5.1 of RTP over QUIC specification.
+ *
+ * Endpoints need to associate flow identifiers with RTP
+ * streams. Depending on the context of the application,
+ * the association can be statically configured, signaled
+ * using an out-of-band signaling mechanism (e.g., SDP),
+ * or applications might be able to identify the stream
+ * based on the RTP packets sent on the stream (e.g., by
+ * inspecting the payload type).
+ *
+ * In this implementation, we use this signal to associate
+ * the flow-id with an incoming stream by requesting caps.
+ * If no caps are provided, the pipeline will fail with a
+ * flow error.
+ *
+ * TODO: Close the connection with ROQ_UNKNOWN_FLOW_ID error
+ * code if the signal fails. This will have to be communicated
+ * upstream to quinnquicsrc.
+ */
+ vec![glib::subclass::Signal::builder(SIGNAL_FLOW_ID_MAP)
+ .param_types([u64::static_type()])
+ .return_type::()
+ .build()]
+ });
+
+ SIGNALS.as_ref()
+ }
+
+ fn constructed(&self) {
+ self.parent_constructed();
+
+ self.obj()
+ .add_pad(&self.sinkpad)
+ .expect("Failed to add sink pad");
+ }
+}
+
+#[glib::object_subclass]
+impl ObjectSubclass for QuinnRoqDemux {
+ const NAME: &'static str = "GstQuinnQuicRtpDemux";
+ type Type = super::QuinnRoqDemux;
+ type ParentType = gst::Element;
+
+ fn with_class(klass: &Self::Class) -> Self {
+ let sinkpad = gst::Pad::builder_from_template(&klass.pad_template("sink").unwrap())
+ .chain_function(|_pad, parent, buffer| {
+ QuinnRoqDemux::catch_panic_pad_function(
+ parent,
+ || Err(gst::FlowError::Error),
+ |demux| demux.sink_chain(buffer),
+ )
+ })
+ .event_function(|pad, parent, event| {
+ QuinnRoqDemux::catch_panic_pad_function(
+ parent,
+ || false,
+ |demux| demux.sink_event(pad, event),
+ )
+ })
+ .build();
+
+ Self {
+ state: Mutex::new(State::default()),
+ sinkpad,
+ }
+ }
+}
+
+impl ChildProxyImpl for QuinnRoqDemux {
+ fn children_count(&self) -> u32 {
+ let object = self.obj();
+ object.num_pads() as u32
+ }
+
+ fn child_by_name(&self, name: &str) -> Option {
+ let object = self.obj();
+ object
+ .pads()
+ .into_iter()
+ .find(|p| p.name() == name)
+ .map(|p| p.upcast())
+ }
+
+ fn child_by_index(&self, index: u32) -> Option {
+ let object = self.obj();
+ object
+ .pads()
+ .into_iter()
+ .nth(index as usize)
+ .map(|p| p.upcast())
+ }
+}
+
+impl QuinnRoqDemux {
+ fn add_srcpad_for_flowid(&self, flow_id: u64) -> Result {
+ let caps = self
+ .obj()
+ .emit_by_name::