net: Add QUIC source and sink

To test, run receiver as

```bash
gst-launch-1.0 -v -e quicsrc caps=audio/x-opus use-datagram=true ! opusparse ! opusdec ! audio/x-raw,format=S16LE,rate=48000,channels=2,layout=interleaved ! audioconvert ! autoaudiosink
```

run sender as

```bash
gst-launch-1.0 -v -e audiotestsrc num-buffers=512 ! audio/x-raw,format=S16LE,rate=48000,channels=2,layout=interleaved ! opusenc ! quicsink use-datagram=true
```

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1036>
This commit is contained in:
Sanchayan Maity 2022-12-10 13:07:33 +05:30
parent 8e675de690
commit 953f6a3fd7
11 changed files with 1939 additions and 148 deletions

510
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,7 @@ members = [
"net/webrtc", "net/webrtc",
"net/webrtc/protocol", "net/webrtc/protocol",
"net/webrtc/signalling", "net/webrtc/signalling",
"net/quic",
"text/ahead", "text/ahead",
"text/json", "text/json",

55
net/quic/Cargo.toml Normal file
View file

@ -0,0 +1,55 @@
[package]
name = "gst-plugin-quic"
version.workspace = true
authors = ["Sanchayan Maity <sanchayan@asymptotic.io"]
repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs"
license = "MPL-2.0"
edition = "2021"
description = "GStreamer Plugin for QUIC"
rust-version = "1.63"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
gst-base = { package = "gstreamer-base", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" }
once_cell = "1.19"
tokio = { version = "1.36.0", default-features = false, features = ["time", "rt-multi-thread"] }
futures = "0.3.30"
quinn = "0.10.2"
rustls = { version = "0.21.8", default-features = false, features = ["dangerous_configuration", "quic"] }
rustls-pemfile = "1.0.3"
rcgen = "0.12.1"
bytes = "1.5.0"
thiserror = "1"
[dev-dependencies]
gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_20"] }
serial_test = "3"
[lib]
name = "gstquic"
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
required-features = ["tls-rustls"]
[build-dependencies]
gst-plugin-version-helper = { path="../../version-helper" }
[features]
static = []
capi = []
doc = []
[package.metadata.capi]
min_version = "0.8.0"
[package.metadata.capi.header]
enabled = false
[package.metadata.capi.library]
install_subdir = "gstreamer-1.0"
versioning = false
[package.metadata.capi.pkg_config]
requires_private = "gstreamer-1.0, gstreamer-base-1.0, gobject-2.0, glib-2.0"

3
net/quic/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
gst_plugin_version_helper::info()
}

38
net/quic/src/lib.rs Normal file
View file

@ -0,0 +1,38 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//
// 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
#![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)]
/**
* plugin-quic:
*
* Since: plugins-rs-0.11.0
*/
use gst::glib;
mod quicsink;
mod quicsrc;
mod utils;
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
quicsink::register(plugin)?;
quicsrc::register(plugin)?;
Ok(())
}
gst::plugin_define!(
quic,
env!("CARGO_PKG_DESCRIPTION"),
plugin_init,
concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")),
"MPL",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_REPOSITORY"),
env!("BUILD_REL_DATE")
);

View file

@ -0,0 +1,472 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//
// 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 crate::utils::{
client_endpoint, make_socket_addr, wait, WaitError, CONNECTION_CLOSE_CODE, CONNECTION_CLOSE_MSG,
};
use bytes::Bytes;
use futures::future;
use gst::{glib, prelude::*, subclass::prelude::*};
use gst_base::subclass::prelude::*;
use once_cell::sync::Lazy;
use quinn::{Connection, SendStream};
use std::net::SocketAddr;
use std::sync::Mutex;
static DEFAULT_SERVER_NAME: &str = "localhost";
static DEFAULT_SERVER_ADDR: &str = "127.0.0.1:5000";
static DEFAULT_CLIENT_ADDR: &str = "127.0.0.1:5001";
const DEFAULT_TIMEOUT: u32 = 15;
const DEFAULT_SECURE_CONNECTION: bool = true;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new("quicsink", gst::DebugColorFlags::empty(), Some("QUIC Sink"))
});
struct Started {
connection: Connection,
stream: Option<SendStream>,
}
#[derive(Default)]
enum State {
#[default]
Stopped,
Started(Started),
}
#[derive(Clone, Debug)]
struct Settings {
client_address: SocketAddr,
server_address: SocketAddr,
server_name: String,
timeout: u32,
secure_conn: bool,
use_datagram: bool,
}
impl Default for Settings {
fn default() -> Self {
Settings {
client_address: DEFAULT_CLIENT_ADDR.parse::<SocketAddr>().unwrap(),
server_address: DEFAULT_SERVER_ADDR.parse::<SocketAddr>().unwrap(),
server_name: DEFAULT_SERVER_NAME.to_string(),
timeout: DEFAULT_TIMEOUT,
secure_conn: DEFAULT_SECURE_CONNECTION,
use_datagram: false,
}
}
}
pub struct QuicSink {
settings: Mutex<Settings>,
state: Mutex<State>,
canceller: Mutex<Option<future::AbortHandle>>,
}
impl Default for QuicSink {
fn default() -> Self {
Self {
settings: Mutex::new(Settings::default()),
state: Mutex::new(State::default()),
canceller: Mutex::new(None),
}
}
}
impl GstObjectImpl for QuicSink {}
impl ElementImpl for QuicSink {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
gst::subclass::ElementMetadata::new(
"QUIC Sink",
"Source/Network/QUIC",
"Send data over the network via QUIC",
"Sanchayan Maity <sanchayan@asymptotic.io>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&gst::Caps::new_any(),
)
.unwrap();
vec![sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
}
impl ObjectImpl for QuicSink {
fn constructed(&self) {
self.parent_constructed();
}
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("server-name")
.nick("QUIC server name")
.blurb("Name of the QUIC server which is in server certificate")
.build(),
glib::ParamSpecString::builder("server-address")
.nick("QUIC server address")
.blurb("Address of the QUIC server to connect to e.g. 127.0.0.1:5000")
.build(),
glib::ParamSpecString::builder("client-address")
.nick("QUIC client address")
.blurb("Address to be used by this QUIC client e.g. 127.0.0.1:5001")
.build(),
glib::ParamSpecUInt::builder("timeout")
.nick("Timeout")
.blurb("Value in seconds to timeout QUIC endpoint requests (0 = No timeout).")
.maximum(3600)
.default_value(DEFAULT_TIMEOUT)
.readwrite()
.build(),
glib::ParamSpecBoolean::builder("secure-connection")
.nick("Use secure connection")
.blurb("Use certificates for QUIC connection. False: Insecure connection, True: Secure connection.")
.default_value(DEFAULT_SECURE_CONNECTION)
.build(),
glib::ParamSpecBoolean::builder("use-datagram")
.nick("Use datagram")
.blurb("Use datagram for lower latency, unreliable messaging")
.default_value(false)
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"server-name" => {
let mut settings = self.settings.lock().unwrap();
settings.server_name = value.get::<String>().expect("type checked upstream");
}
"server-address" => {
let addr = value.get::<String>().expect("type checked upstream");
let addr = make_socket_addr(&addr);
match addr {
Ok(server_address) => {
let mut settings = self.settings.lock().unwrap();
settings.server_address = server_address;
}
Err(e) => gst::element_imp_error!(
self,
gst::ResourceError::Failed,
["Invalid server address: {}", e]
),
}
}
"client-address" => {
let addr = value.get::<String>().expect("type checked upstream");
let addr = make_socket_addr(&addr);
match addr {
Ok(client_address) => {
let mut settings = self.settings.lock().unwrap();
settings.client_address = client_address;
}
Err(e) => gst::element_imp_error!(
self,
gst::ResourceError::Failed,
["Invalid client address: {}", e]
),
}
}
"timeout" => {
let mut settings = self.settings.lock().unwrap();
settings.timeout = value.get().expect("type checked upstream");
}
"secure-connection" => {
let mut settings = self.settings.lock().unwrap();
settings.secure_conn = value.get().expect("type checked upstream");
}
"use-datagram" => {
let mut settings = self.settings.lock().unwrap();
settings.use_datagram = value.get().expect("type checked upstream");
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"server-name" => {
let settings = self.settings.lock().unwrap();
settings.server_name.to_value()
}
"server-address" => {
let settings = self.settings.lock().unwrap();
settings.server_address.to_string().to_value()
}
"client-address" => {
let settings = self.settings.lock().unwrap();
settings.client_address.to_string().to_value()
}
"timeout" => {
let settings = self.settings.lock().unwrap();
settings.timeout.to_value()
}
"secure-connection" => {
let settings = self.settings.lock().unwrap();
settings.secure_conn.to_value()
}
"use-datagram" => {
let settings = self.settings.lock().unwrap();
settings.use_datagram.to_value()
}
_ => unimplemented!(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for QuicSink {
const NAME: &'static str = "GstQUICSink";
type Type = super::QuicSink;
type ParentType = gst_base::BaseSink;
}
impl BaseSinkImpl for QuicSink {
fn start(&self) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap();
let timeout = settings.timeout;
drop(settings);
let mut state = self.state.lock().unwrap();
if let State::Started { .. } = *state {
unreachable!("QuicSink is already started");
}
match wait(&self.canceller, self.establish_connection(), timeout) {
Ok(Ok((c, s))) => {
*state = State::Started(Started {
connection: c,
stream: s,
});
gst::info!(CAT, imp: self, "Started");
Ok(())
}
Ok(Err(e)) => match e {
WaitError::FutureAborted => {
gst::warning!(CAT, imp: self, "Connection aborted");
Ok(())
}
WaitError::FutureError(err) => {
gst::error!(CAT, imp: self, "Connection request failed: {}", err);
Err(gst::error_msg!(
gst::ResourceError::Failed,
["Connection request failed: {}", err]
))
}
},
Err(e) => {
gst::error!(CAT, imp: self, "Failed to establish a connection: {:?}", e);
Err(gst::error_msg!(
gst::ResourceError::Failed,
["Failed to establish a connection: {:?}", e]
))
}
}
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap();
let timeout = settings.timeout;
let use_datagram = settings.use_datagram;
drop(settings);
let mut state = self.state.lock().unwrap();
if let State::Started(ref mut state) = *state {
let connection = &state.connection;
let mut close_msg = CONNECTION_CLOSE_MSG.to_string();
if !use_datagram {
let send = &mut state.stream.as_mut().unwrap();
// Shutdown stream gracefully
match wait(&self.canceller, send.finish(), timeout) {
Ok(r) => {
if let Err(e) = r {
close_msg = format!("Stream finish request error: {}", e);
gst::error!(CAT, imp: self, "{}", close_msg);
}
}
Err(e) => match e {
WaitError::FutureAborted => {
close_msg = "Stream finish request aborted".to_string();
gst::warning!(CAT, imp: self, "{}", close_msg);
}
WaitError::FutureError(e) => {
close_msg = format!("Stream finish request future error: {}", e);
gst::error!(CAT, imp: self, "{}", close_msg);
}
},
};
}
connection.close(CONNECTION_CLOSE_CODE.into(), close_msg.as_bytes());
}
*state = State::Stopped;
gst::info!(CAT, imp: self, "Stopped");
Ok(())
}
fn render(&self, buffer: &gst::Buffer) -> Result<gst::FlowSuccess, gst::FlowError> {
if let State::Stopped = *self.state.lock().unwrap() {
gst::element_imp_error!(self, gst::CoreError::Failed, ["Not started yet"]);
return Err(gst::FlowError::Error);
}
gst::trace!(CAT, imp: self, "Rendering {:?}", buffer);
let map = buffer.map_readable().map_err(|_| {
gst::element_imp_error!(self, gst::CoreError::Failed, ["Failed to map buffer"]);
gst::FlowError::Error
})?;
match self.send_buffer(&map) {
Ok(_) => Ok(gst::FlowSuccess::Ok),
Err(err) => match err {
Some(error_message) => {
gst::error!(CAT, imp: self, "Data sending failed: {}", error_message);
self.post_error_message(error_message);
Err(gst::FlowError::Error)
}
_ => {
gst::info!(CAT, imp: self, "Send interrupted. Flushing...");
Err(gst::FlowError::Flushing)
}
},
}
}
}
impl QuicSink {
fn send_buffer(&self, src: &[u8]) -> Result<(), Option<gst::ErrorMessage>> {
let settings = self.settings.lock().unwrap();
let timeout = settings.timeout;
let use_datagram = settings.use_datagram;
drop(settings);
let mut state = self.state.lock().unwrap();
let (conn, stream) = match *state {
State::Started(Started {
ref connection,
ref mut stream,
}) => (connection, stream),
State::Stopped => {
return Err(Some(gst::error_msg!(
gst::LibraryError::Failed,
["Cannot send before start()"]
)));
}
};
if use_datagram {
match conn.send_datagram(Bytes::copy_from_slice(src)) {
Ok(_) => Ok(()),
Err(e) => Err(Some(gst::error_msg!(
gst::ResourceError::Failed,
["Sending data failed: {}", e]
))),
}
} else {
let send = &mut stream.as_mut().unwrap();
match wait(&self.canceller, send.write_all(src), timeout) {
Ok(Ok(_)) => Ok(()),
Ok(Err(e)) => Err(Some(gst::error_msg!(
gst::ResourceError::Failed,
["Sending data failed: {}", e]
))),
Err(e) => match e {
WaitError::FutureAborted => {
gst::warning!(CAT, imp: self, "Sending aborted");
Ok(())
}
WaitError::FutureError(e) => Err(Some(gst::error_msg!(
gst::ResourceError::Failed,
["Sending data failed: {}", e]
))),
},
}
}
}
async fn establish_connection(&self) -> Result<(Connection, Option<SendStream>), WaitError> {
let client_addr;
let server_addr;
let server_name;
let use_datagram;
let secure_conn;
{
let settings = self.settings.lock().unwrap();
client_addr = settings.client_address;
server_addr = settings.server_address;
server_name = settings.server_name.clone();
use_datagram = settings.use_datagram;
secure_conn = settings.secure_conn;
}
let endpoint = client_endpoint(client_addr, secure_conn).map_err(|err| {
WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Failed to configure endpoint: {}", err]
))
})?;
let connection = endpoint
.connect(server_addr, &server_name)
.unwrap()
.await
.map_err(|err| {
WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Connection error: {}", err]
))
})?;
let stream = if !use_datagram {
let res = connection.open_uni().await.map_err(|err| {
WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Failed to open stream: {}", err]
))
})?;
Some(res)
} else {
None
};
Ok((connection, stream))
}
}

View file

@ -0,0 +1,26 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//G
// 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 gst::glib;
use gst::prelude::*;
pub mod imp;
glib::wrapper! {
pub struct QuicSink(ObjectSubclass<imp::QuicSink>) @extends gst_base::BaseSink, gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"quicsink",
gst::Rank::MARGINAL,
QuicSink::static_type(),
)
}

583
net/quic/src/quicsrc/imp.rs Normal file
View file

@ -0,0 +1,583 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//
// 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::QuicPrivateKeyType;
use crate::utils::{
make_socket_addr, server_endpoint, wait, WaitError, CONNECTION_CLOSE_CODE, CONNECTION_CLOSE_MSG,
};
use bytes::Bytes;
use futures::future;
use gst::{glib, prelude::*, subclass::prelude::*};
use gst_base::prelude::*;
use gst_base::subclass::base_src::CreateSuccess;
use gst_base::subclass::prelude::*;
use once_cell::sync::Lazy;
use quinn::{Connection, ConnectionError, RecvStream};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Mutex;
static DEFAULT_SERVER_NAME: &str = "localhost";
static DEFAULT_SERVER_ADDR: &str = "127.0.0.1:5000";
const DEFAULT_TIMEOUT: u32 = 15;
const DEFAULT_PRIVATE_KEY_TYPE: QuicPrivateKeyType = QuicPrivateKeyType::Pkcs8;
const DEFAULT_SECURE_CONNECTION: bool = true;
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
gst::DebugCategory::new(
"quicsrc",
gst::DebugColorFlags::empty(),
Some("QUIC Source"),
)
});
struct Started {
connection: Connection,
stream: Option<RecvStream>,
}
#[derive(Default)]
enum State {
#[default]
Stopped,
Started(Started),
}
#[derive(Clone, Debug)]
struct Settings {
server_address: SocketAddr,
server_name: String,
timeout: u32,
secure_conn: bool,
caps: gst::Caps,
use_datagram: bool,
certificate_path: Option<PathBuf>,
private_key_type: QuicPrivateKeyType,
}
impl Default for Settings {
fn default() -> Self {
Settings {
server_address: DEFAULT_SERVER_ADDR.parse::<SocketAddr>().unwrap(),
server_name: DEFAULT_SERVER_NAME.to_string(),
timeout: DEFAULT_TIMEOUT,
secure_conn: DEFAULT_SECURE_CONNECTION,
caps: gst::Caps::new_any(),
use_datagram: false,
certificate_path: None,
private_key_type: DEFAULT_PRIVATE_KEY_TYPE,
}
}
}
pub struct QuicSrc {
settings: Mutex<Settings>,
state: Mutex<State>,
canceller: Mutex<Option<future::AbortHandle>>,
}
impl Default for QuicSrc {
fn default() -> Self {
Self {
settings: Mutex::new(Settings::default()),
state: Mutex::new(State::default()),
canceller: Mutex::new(None),
}
}
}
impl GstObjectImpl for QuicSrc {}
impl ElementImpl for QuicSrc {
fn metadata() -> Option<&'static gst::subclass::ElementMetadata> {
static ELEMENT_METADATA: Lazy<gst::subclass::ElementMetadata> = Lazy::new(|| {
#[cfg(feature = "doc")]
QuicPrivateKeyType::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty());
gst::subclass::ElementMetadata::new(
"QUIC Source",
"Source/Network/QUIC",
"Receive data over the network via QUIC",
"Sanchayan Maity <sanchayan@asymptotic.io>",
)
});
Some(&*ELEMENT_METADATA)
}
fn pad_templates() -> &'static [gst::PadTemplate] {
static PAD_TEMPLATES: Lazy<Vec<gst::PadTemplate>> = Lazy::new(|| {
let sink_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&gst::Caps::new_any(),
)
.unwrap();
vec![sink_pad_template]
});
PAD_TEMPLATES.as_ref()
}
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
if transition == gst::StateChange::NullToReady {
let settings = self.settings.lock().unwrap();
/*
* Fail the state change if a secure connection was requested but
* no certificate path was provided.
*/
if settings.secure_conn && settings.certificate_path.is_none() {
gst::error!(
CAT,
imp: self,
"Certificate path not provided for secure connection"
);
return Err(gst::StateChangeError);
}
}
self.parent_change_state(transition)
}
}
impl ObjectImpl for QuicSrc {
fn constructed(&self) {
self.parent_constructed();
self.obj().set_format(gst::Format::Bytes);
}
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("server-name")
.nick("QUIC server name")
.blurb("Name of the QUIC server which is in server certificate")
.build(),
glib::ParamSpecString::builder("server-address")
.nick("QUIC server address")
.blurb("Address of the QUIC server to connect to e.g. 127.0.0.1:5000")
.build(),
glib::ParamSpecUInt::builder("timeout")
.nick("Timeout")
.blurb("Value in seconds to timeout QUIC endpoint requests (0 = No timeout).")
.maximum(3600)
.default_value(DEFAULT_TIMEOUT)
.readwrite()
.build(),
glib::ParamSpecBoolean::builder("secure-connection")
.nick("Use secure connection")
.blurb("Use certificates for QUIC connection. False: Insecure connection, True: Secure connection.")
.default_value(DEFAULT_SECURE_CONNECTION)
.build(),
glib::ParamSpecString::builder("certificate-path")
.nick("Certificate Path")
.blurb("Path where the certificate files cert.pem and privkey.pem are stored")
.build(),
glib::ParamSpecBoxed::builder::<gst::Caps>("caps")
.nick("caps")
.blurb("The caps of the source pad")
.build(),
glib::ParamSpecBoolean::builder("use-datagram")
.nick("Use datagram")
.blurb("Use datagram for lower latency, unreliable messaging")
.default_value(false)
.build(),
glib::ParamSpecEnum::builder_with_default::<QuicPrivateKeyType>("private-key-type", DEFAULT_PRIVATE_KEY_TYPE)
.nick("Whether PKCS8 or RSA key type is considered for private key")
.blurb("Read given private key as PKCS8 or RSA")
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"server-name" => {
let mut settings = self.settings.lock().unwrap();
settings.server_name = value.get::<String>().expect("type checked upstream");
}
"server-address" => {
let addr = value.get::<String>().expect("type checked upstream");
let addr = make_socket_addr(&addr);
match addr {
Ok(server_address) => {
let mut settings = self.settings.lock().unwrap();
settings.server_address = server_address;
}
Err(e) => gst::element_imp_error!(
self,
gst::ResourceError::Failed,
["Invalid server address: {}", e]
),
}
}
"caps" => {
let mut settings = self.settings.lock().unwrap();
settings.caps = value
.get::<Option<gst::Caps>>()
.expect("type checked upstream")
.unwrap_or_else(gst::Caps::new_any);
let srcpad = self.obj().static_pad("src").expect("source pad expected");
srcpad.mark_reconfigure();
}
"timeout" => {
let mut settings = self.settings.lock().unwrap();
settings.timeout = value.get().expect("type checked upstream");
}
"secure-connection" => {
let mut settings = self.settings.lock().unwrap();
settings.secure_conn = value.get().expect("type checked upstream");
}
"certificate-path" => {
let value: String = value.get().unwrap();
let mut settings = self.settings.lock().unwrap();
settings.certificate_path = Some(value.into());
}
"use-datagram" => {
let mut settings = self.settings.lock().unwrap();
settings.use_datagram = value.get().expect("type checked upstream");
}
"private-key-type" => {
let mut settings = self.settings.lock().unwrap();
settings.private_key_type = value
.get::<QuicPrivateKeyType>()
.expect("type checked upstream");
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"server-name" => {
let settings = self.settings.lock().unwrap();
settings.server_name.to_value()
}
"server-address" => {
let settings = self.settings.lock().unwrap();
settings.server_address.to_string().to_value()
}
"caps" => {
let settings = self.settings.lock().unwrap();
settings.caps.to_value()
}
"timeout" => {
let settings = self.settings.lock().unwrap();
settings.timeout.to_value()
}
"secure-connection" => {
let settings = self.settings.lock().unwrap();
settings.secure_conn.to_value()
}
"certificate-path" => {
let settings = self.settings.lock().unwrap();
let certpath = settings.certificate_path.as_ref();
certpath.and_then(|file| file.to_str()).to_value()
}
"use-datagram" => {
let settings = self.settings.lock().unwrap();
settings.use_datagram.to_value()
}
"private-key-type" => {
let settings = self.settings.lock().unwrap();
settings.private_key_type.to_value()
}
_ => unimplemented!(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for QuicSrc {
const NAME: &'static str = "GstQUICSrc";
type Type = super::QuicSrc;
type ParentType = gst_base::BaseSrc;
}
impl BaseSrcImpl for QuicSrc {
fn is_seekable(&self) -> bool {
false
}
fn start(&self) -> Result<(), gst::ErrorMessage> {
let settings = self.settings.lock().unwrap();
let timeout = settings.timeout;
drop(settings);
let mut state = self.state.lock().unwrap();
if let State::Started { .. } = *state {
unreachable!("QuicSrc already started");
}
match wait(&self.canceller, self.wait_for_connection(), timeout) {
Ok(Ok((c, s))) => {
*state = State::Started(Started {
connection: c,
stream: s,
});
gst::info!(CAT, imp: self, "Started");
Ok(())
}
Ok(Err(e)) | Err(e) => match e {
WaitError::FutureAborted => {
gst::warning!(CAT, imp: self, "Connection aborted");
Ok(())
}
WaitError::FutureError(err) => {
gst::error!(CAT, imp: self, "Connection request failed: {}", err);
Err(gst::error_msg!(
gst::ResourceError::Failed,
["Connection request failed: {}", err]
))
}
},
}
}
fn stop(&self) -> Result<(), gst::ErrorMessage> {
self.cancel();
let mut state = self.state.lock().unwrap();
if let State::Started(ref mut state) = *state {
let connection = &state.connection;
connection.close(
CONNECTION_CLOSE_CODE.into(),
CONNECTION_CLOSE_MSG.as_bytes(),
);
}
*state = State::Stopped;
Ok(())
}
fn query(&self, query: &mut gst::QueryRef) -> bool {
if let gst::QueryViewMut::Scheduling(q) = query.view_mut() {
q.set(
gst::SchedulingFlags::SEQUENTIAL | gst::SchedulingFlags::BANDWIDTH_LIMITED,
1,
-1,
0,
);
q.add_scheduling_modes(&[gst::PadMode::Pull, gst::PadMode::Push]);
return true;
}
BaseSrcImplExt::parent_query(self, query)
}
fn create(
&self,
offset: u64,
buffer: Option<&mut gst::BufferRef>,
length: u32,
) -> Result<CreateSuccess, gst::FlowError> {
let data = self.get(offset, u64::from(length));
match data {
Ok(bytes) => {
if bytes.is_empty() {
gst::debug!(CAT, imp: self, "End of stream");
return Err(gst::FlowError::Eos);
}
if let Some(buffer) = buffer {
if let Err(copied_bytes) = buffer.copy_from_slice(0, bytes.as_ref()) {
buffer.set_size(copied_bytes);
}
Ok(CreateSuccess::FilledBuffer)
} else {
Ok(CreateSuccess::NewBuffer(gst::Buffer::from_slice(bytes)))
}
}
Err(None) => Err(gst::FlowError::Flushing),
Err(Some(err)) => {
gst::error!(CAT, imp: self, "Could not GET: {}", err);
Err(gst::FlowError::Error)
}
}
}
fn unlock(&self) -> Result<(), gst::ErrorMessage> {
self.cancel();
Ok(())
}
fn caps(&self, filter: Option<&gst::Caps>) -> Option<gst::Caps> {
let settings = self.settings.lock().unwrap();
let mut tmp_caps = settings.caps.clone();
gst::debug!(CAT, imp: self, "Advertising our own caps: {:?}", &tmp_caps);
if let Some(filter_caps) = filter {
gst::debug!(
CAT,
imp: self,
"Intersecting with filter caps: {:?}",
&filter_caps
);
tmp_caps = filter_caps.intersect_with_mode(&tmp_caps, gst::CapsIntersectMode::First);
};
gst::debug!(CAT, imp: self, "Returning caps: {:?}", &tmp_caps);
Some(tmp_caps)
}
}
impl QuicSrc {
fn get(&self, _offset: u64, length: u64) -> Result<Bytes, Option<gst::ErrorMessage>> {
let settings = self.settings.lock().unwrap();
let timeout = settings.timeout;
let use_datagram = settings.use_datagram;
drop(settings);
let mut state = self.state.lock().unwrap();
let (conn, stream) = match *state {
State::Started(Started {
ref connection,
ref mut stream,
}) => (connection, stream),
State::Stopped => {
return Err(Some(gst::error_msg!(
gst::LibraryError::Failed,
["Cannot get data before start"]
)));
}
};
let future = async {
if use_datagram {
match conn.read_datagram().await {
Ok(bytes) => Ok(bytes),
Err(err) => match err {
ConnectionError::ApplicationClosed(_)
| ConnectionError::ConnectionClosed(_) => Ok(Bytes::new()),
_ => Err(WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Datagram read error: {}", err]
))),
},
}
} else {
let recv = stream.as_mut().unwrap();
match recv.read_chunk(length as usize, true).await {
Ok(Some(chunk)) => Ok(chunk.bytes),
Ok(None) => Ok(Bytes::new()),
Err(err) => Err(WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Stream read error: {}", err]
))),
}
}
};
match wait(&self.canceller, future, timeout) {
Ok(Ok(bytes)) => Ok(bytes),
Ok(Err(e)) | Err(e) => match e {
WaitError::FutureAborted => {
gst::warning!(CAT, imp: self, "Read from stream request aborted");
Err(None)
}
WaitError::FutureError(e) => {
gst::error!(CAT, imp: self, "Failed to read from stream: {}", e);
Err(Some(e))
}
},
}
}
fn cancel(&self) {
let mut canceller = self.canceller.lock().unwrap();
if let Some(c) = canceller.take() {
c.abort()
};
}
async fn wait_for_connection(&self) -> Result<(Connection, Option<RecvStream>), WaitError> {
let server_addr;
let server_name;
let use_datagram;
let secure_conn;
let cert_path;
let private_key_type;
{
let settings = self.settings.lock().unwrap();
server_addr = settings.server_address;
server_name = settings.server_name.clone();
use_datagram = settings.use_datagram;
secure_conn = settings.secure_conn;
cert_path = settings.certificate_path.clone();
private_key_type = settings.private_key_type;
}
let endpoint = server_endpoint(
server_addr,
&server_name,
secure_conn,
cert_path,
private_key_type,
)
.map_err(|err| {
WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Failed to configure endpoint: {}", err]
))
})?;
let incoming_conn = endpoint.accept().await.unwrap();
let connection = incoming_conn.await.map_err(|err| {
WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Connection error: {}", err]
))
})?;
let stream = if !use_datagram {
let res = connection.accept_uni().await.map_err(|err| {
WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Failed to open stream: {}", err]
))
})?;
Some(res)
} else {
None
};
gst::info!(
CAT,
imp: self,
"Remote connection accepted: {}",
connection.remote_address()
);
Ok((connection, stream))
}
}

View file

@ -0,0 +1,36 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//
// 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 gst::glib;
use gst::prelude::*;
mod imp;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "GstQuicPrivateKeyType")]
pub enum QuicPrivateKeyType {
#[enum_value(name = "PKCS8: PKCS #8 Private Key.", nick = "pkcs8")]
Pkcs8,
#[enum_value(name = "RSA: RSA Private Key.", nick = "rsa")]
Rsa,
}
glib::wrapper! {
pub struct QuicSrc(ObjectSubclass<imp::QuicSrc>) @extends gst_base::BaseSrc, gst::Element, gst::Object;
}
pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
gst::Element::register(
Some(plugin),
"quicsrc",
gst::Rank::MARGINAL,
QuicSrc::static_type(),
)
}

252
net/quic/src/utils.rs Normal file
View file

@ -0,0 +1,252 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//G
// 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 crate::quicsrc::QuicPrivateKeyType;
use futures::future;
use futures::prelude::*;
use gst::ErrorMessage;
use once_cell::sync::Lazy;
use quinn::{ClientConfig, Endpoint, ServerConfig};
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::net::{AddrParseError, SocketAddr};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use thiserror::Error;
use tokio::runtime;
pub const CONNECTION_CLOSE_CODE: u32 = 0;
pub const CONNECTION_CLOSE_MSG: &str = "Stopped";
#[derive(Error, Debug)]
pub enum WaitError {
#[error("Future aborted")]
FutureAborted,
#[error("Future returned an error: {0}")]
FutureError(ErrorMessage),
}
pub static RUNTIME: Lazy<runtime::Runtime> = Lazy::new(|| {
runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(1)
.thread_name("gst-quic-runtime")
.build()
.unwrap()
});
pub fn wait<F, T>(
canceller: &Mutex<Option<future::AbortHandle>>,
future: F,
timeout: u32,
) -> Result<T, WaitError>
where
F: Send + Future<Output = T>,
T: Send + 'static,
{
let mut canceller_guard = canceller.lock().unwrap();
let (abort_handle, abort_registration) = future::AbortHandle::new_pair();
if canceller_guard.is_some() {
return Err(WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Old Canceller should not exist"]
)));
}
canceller_guard.replace(abort_handle);
drop(canceller_guard);
let future = async {
if timeout == 0 {
Ok(future.await)
} else {
let res = tokio::time::timeout(Duration::from_secs(timeout.into()), future).await;
match res {
Ok(r) => Ok(r),
Err(e) => Err(gst::error_msg!(
gst::ResourceError::Read,
["Request timeout, elapsed: {}", e.to_string()]
)),
}
}
};
let future = async {
match future::Abortable::new(future, abort_registration).await {
Ok(Ok(res)) => Ok(res),
Ok(Err(err)) => Err(WaitError::FutureError(gst::error_msg!(
gst::ResourceError::Failed,
["Future resolved with an error {:?}", err]
))),
Err(future::Aborted) => Err(WaitError::FutureAborted),
}
};
let res = RUNTIME.block_on(future);
canceller_guard = canceller.lock().unwrap();
*canceller_guard = None;
res
}
/*
* Following functions are taken from Quinn documentation/repository
*/
pub fn make_socket_addr(addr: &str) -> Result<SocketAddr, AddrParseError> {
addr.parse::<SocketAddr>()
}
struct SkipServerVerification;
impl SkipServerVerification {
pub fn new() -> Arc<Self> {
Arc::new(Self)
}
}
impl rustls::client::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
Ok(rustls::client::ServerCertVerified::assertion())
}
}
fn configure_client(secure_conn: bool) -> Result<ClientConfig, Box<dyn Error>> {
if secure_conn {
Ok(ClientConfig::with_native_roots())
} else {
let crypto = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_no_client_auth();
Ok(ClientConfig::new(Arc::new(crypto)))
}
}
fn read_certs_from_file(
certificate_path: Option<PathBuf>,
private_key_type: QuicPrivateKeyType,
) -> Result<(Vec<rustls::Certificate>, rustls::PrivateKey), Box<dyn Error>> {
/*
* NOTE:
*
* Certificate file here should correspond to fullchain.pem where
* fullchain.pem = cert.pem + chain.pem.
* fullchain.pem DOES NOT include a CA's Root Certificates.
*
* One typically uses chain.pem (or the first certificate in it) when asked
* for a CA bundle or CA certificate.
*
* One typically uses fullchain.pem when asked for the entire certificate
* chain in a single file. For example, this is the case of modern day
* Apache and nginx.
*/
let cert_file = certificate_path
.clone()
.expect("Expected path to certificates be valid")
.join("fullchain.pem");
let key_file = certificate_path
.expect("Expected path to certificates be valid")
.join("privkey.pem");
let certs: Vec<rustls::Certificate> = {
let cert_file = File::open(cert_file.as_path())?;
let mut cert_file_rdr = BufReader::new(cert_file);
let cert_vec = rustls_pemfile::certs(&mut cert_file_rdr)?;
cert_vec.into_iter().map(rustls::Certificate).collect()
};
let key: rustls::PrivateKey = {
let key_file = File::open(key_file.as_path())?;
let mut key_file_rdr = BufReader::new(key_file);
let mut key_vec;
// If the file starts with "BEGIN RSA PRIVATE KEY"
if let QuicPrivateKeyType::Rsa = private_key_type {
key_vec = rustls_pemfile::rsa_private_keys(&mut key_file_rdr)?;
} else {
// If the file starts with "BEGIN PRIVATE KEY"
key_vec = rustls_pemfile::pkcs8_private_keys(&mut key_file_rdr)?;
}
assert_eq!(key_vec.len(), 1);
rustls::PrivateKey(key_vec.remove(0))
};
Ok((certs, key))
}
fn configure_server(
server_name: &str,
secure_conn: bool,
certificate_path: Option<PathBuf>,
private_key_type: QuicPrivateKeyType,
) -> Result<(ServerConfig, Vec<rustls::Certificate>), Box<dyn Error>> {
let (cert, key) = if secure_conn {
read_certs_from_file(certificate_path, private_key_type).unwrap()
} else {
let cert = rcgen::generate_simple_self_signed(vec![server_name.into()]).unwrap();
let cert_der = cert.serialize_der().unwrap();
let priv_key = cert.serialize_private_key_der();
let priv_key = rustls::PrivateKey(priv_key);
let cert_chain = vec![rustls::Certificate(cert_der)];
(cert_chain, priv_key)
};
let mut server_config = ServerConfig::with_single_cert(cert.clone(), key)?;
Arc::get_mut(&mut server_config.transport)
.unwrap()
.max_concurrent_bidi_streams(0_u8.into())
.max_concurrent_uni_streams(1_u8.into());
Ok((server_config, cert))
}
pub fn server_endpoint(
server_addr: SocketAddr,
server_name: &str,
secure_conn: bool,
certificate_path: Option<PathBuf>,
private_key_type: QuicPrivateKeyType,
) -> Result<Endpoint, Box<dyn Error>> {
let (server_config, _) =
configure_server(server_name, secure_conn, certificate_path, private_key_type)?;
let endpoint = Endpoint::server(server_config, server_addr)?;
Ok(endpoint)
}
pub fn client_endpoint(
client_addr: SocketAddr,
secure_conn: bool,
) -> Result<Endpoint, Box<dyn Error>> {
let client_cfg = configure_client(secure_conn)?;
let mut endpoint = Endpoint::client(client_addr)?;
endpoint.set_default_client_config(client_cfg);
Ok(endpoint)
}

111
net/quic/tests/quic.rs Normal file
View file

@ -0,0 +1,111 @@
// Copyright (C) 2024, Asymptotic Inc.
// Author: Sanchayan Maity <sanchayan@asymptotic.io>
//
// 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 gst::prelude::*;
use serial_test::serial;
use std::thread;
fn init() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
gst::init().unwrap();
gstquic::plugin_register_static().expect("QUIC source sink send receive tests");
});
}
fn make_buffer(content: &[u8]) -> gst::Buffer {
let mut buf = gst::Buffer::from_slice(content.to_owned());
buf.make_mut().set_pts(gst::ClockTime::from_mseconds(200));
buf
}
#[test]
#[serial]
fn test_send_receive_without_datagram() {
init();
let content = "Hello, world!\n".as_bytes();
thread::spawn(move || {
let mut h1 = gst_check::Harness::new("quicsink");
h1.set_src_caps(gst::Caps::builder("text/plain").build());
h1.play();
assert!(h1.push(make_buffer(content)) == Ok(gst::FlowSuccess::Ok));
h1.push_event(gst::event::Eos::new());
h1.element().unwrap().set_state(gst::State::Null).unwrap();
drop(h1);
});
let mut h2 = gst_check::Harness::new("quicsrc");
h2.play();
let buf = h2.pull_until_eos().unwrap().unwrap();
assert_eq!(
content,
buf.into_mapped_buffer_readable().unwrap().as_slice()
);
h2.element().unwrap().set_state(gst::State::Null).unwrap();
drop(h2);
}
#[test]
#[serial]
fn test_send_receive_with_datagram() {
init();
let content = "Hello, world!\n".as_bytes();
// Use a different port address compared to the default that will be used
// in the other test. We get a address already in use error otherwise.
thread::spawn(move || {
let mut h1 = gst_check::Harness::new_empty();
h1.add_parse(format!("quicsrc use-datagram=true server-address=127.0.0.1:6000").as_str());
h1.play();
let buf = h1.pull_until_eos().unwrap().unwrap();
assert_eq!(
content,
buf.into_mapped_buffer_readable().unwrap().as_slice()
);
h1.element().unwrap().set_state(gst::State::Null).unwrap();
drop(h1);
});
let mut h2 = gst_check::Harness::new_empty();
h2.add_parse(format!("quicsink use-datagram=true client-address=127.0.0.1:6001 server-address=127.0.0.1:6000").as_str());
h2.set_src_caps(gst::Caps::builder("text/plain").build());
h2.play();
assert!(h2.push(make_buffer(content)) == Ok(gst::FlowSuccess::Ok));
h2.push_event(gst::event::Eos::new());
h2.element().unwrap().set_state(gst::State::Null).unwrap();
drop(h2);
}