mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-26 05:21:00 +00:00
1436 lines
45 KiB
Rust
1436 lines
45 KiB
Rust
use std::{
|
|
collections::{BTreeMap, HashMap, HashSet},
|
|
ops::Deref,
|
|
sync::atomic::{AtomicBool, Ordering},
|
|
};
|
|
|
|
use anyhow::{anyhow, Context, Error};
|
|
use gst::{glib, prelude::*};
|
|
use std::sync::LazyLock;
|
|
|
|
static CAT: LazyLock<gst::DebugCategory> = LazyLock::new(|| {
|
|
gst::DebugCategory::new(
|
|
"webrtcutils",
|
|
gst::DebugColorFlags::empty(),
|
|
Some("WebRTC Utils"),
|
|
)
|
|
});
|
|
|
|
pub fn gvalue_to_json(val: &gst::glib::Value) -> Option<serde_json::Value> {
|
|
match val.type_() {
|
|
glib::Type::STRING => Some(val.get::<String>().unwrap().into()),
|
|
glib::Type::BOOL => Some(val.get::<bool>().unwrap().into()),
|
|
glib::Type::I32 => Some(val.get::<i32>().unwrap().into()),
|
|
glib::Type::U32 => Some(val.get::<u32>().unwrap().into()),
|
|
glib::Type::I_LONG | glib::Type::I64 => Some(val.get::<i64>().unwrap().into()),
|
|
glib::Type::U_LONG | glib::Type::U64 => Some(val.get::<u64>().unwrap().into()),
|
|
glib::Type::F32 => Some(val.get::<f32>().unwrap().into()),
|
|
glib::Type::F64 => Some(val.get::<f64>().unwrap().into()),
|
|
_ => {
|
|
if let Ok(s) = val.get::<gst::Structure>() {
|
|
serde_json::to_value(
|
|
s.iter()
|
|
.filter_map(|(name, value)| {
|
|
gvalue_to_json(value).map(|value| (name.to_string(), value))
|
|
})
|
|
.collect::<HashMap<String, serde_json::Value>>(),
|
|
)
|
|
.ok()
|
|
} else if let Ok(a) = val.get::<gst::Array>() {
|
|
serde_json::to_value(
|
|
a.iter()
|
|
.filter_map(|value| gvalue_to_json(value))
|
|
.collect::<Vec<serde_json::Value>>(),
|
|
)
|
|
.ok()
|
|
} else if let Some((_klass, values)) = gst::glib::FlagsValue::from_value(val) {
|
|
Some(
|
|
values
|
|
.iter()
|
|
.map(|value| value.nick())
|
|
.collect::<Vec<&str>>()
|
|
.join("+")
|
|
.into(),
|
|
)
|
|
} else if let Ok(value) = val.serialize() {
|
|
Some(value.as_str().into())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn json_to_gststructure(val: &serde_json::Value) -> Option<glib::SendValue> {
|
|
match val {
|
|
serde_json::Value::Bool(v) => Some(v.to_send_value()),
|
|
serde_json::Value::Number(n) => {
|
|
if n.is_u64() {
|
|
Some(n.as_u64().unwrap().to_send_value())
|
|
} else if n.is_i64() {
|
|
Some(n.as_i64().unwrap().to_send_value())
|
|
} else if n.is_f64() {
|
|
Some(n.as_f64().unwrap().to_send_value())
|
|
} else {
|
|
todo!("Unhandled case {n:?}");
|
|
}
|
|
}
|
|
serde_json::Value::String(v) => Some(v.to_send_value()),
|
|
serde_json::Value::Array(v) => {
|
|
let array = v
|
|
.iter()
|
|
.filter_map(json_to_gststructure)
|
|
.collect::<Vec<glib::SendValue>>();
|
|
Some(gst::Array::from_values(array).to_send_value())
|
|
}
|
|
serde_json::Value::Object(v) => Some(serialize_json_object(v).to_send_value()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn serialize_json_object(val: &serde_json::Map<String, serde_json::Value>) -> gst::Structure {
|
|
let mut res = gst::Structure::new_empty("v");
|
|
|
|
val.iter().for_each(|(k, v)| {
|
|
if let Some(gvalue) = json_to_gststructure(v) {
|
|
res.set_value(k, gvalue);
|
|
}
|
|
});
|
|
|
|
res
|
|
}
|
|
|
|
use crate::RUNTIME;
|
|
use futures::future;
|
|
use futures::prelude::*;
|
|
use gst::ErrorMessage;
|
|
#[cfg(feature = "whip")]
|
|
use reqwest::header::HeaderMap;
|
|
#[cfg(feature = "whip")]
|
|
use reqwest::redirect::Policy;
|
|
use std::sync::Mutex;
|
|
use std::time::Duration;
|
|
|
|
#[derive(Debug)]
|
|
pub enum WaitError {
|
|
FutureAborted,
|
|
FutureError(ErrorMessage),
|
|
}
|
|
|
|
pub async fn wait_async<F, T>(
|
|
canceller: &Mutex<Option<future::AbortHandle>>,
|
|
future: F,
|
|
timeout: u32,
|
|
) -> Result<T, WaitError>
|
|
where
|
|
F: Send + Future<Output = T>,
|
|
T: Send + 'static,
|
|
{
|
|
let (abort_handle, abort_registration) = future::AbortHandle::new_pair();
|
|
{
|
|
let mut canceller_guard = canceller.lock().unwrap();
|
|
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(WaitError::FutureError(gst::error_msg!(
|
|
gst::ResourceError::Read,
|
|
["Request timeout, elapsed: {}", e]
|
|
))),
|
|
}
|
|
}
|
|
};
|
|
|
|
let future = async {
|
|
match future::Abortable::new(future, abort_registration).await {
|
|
Ok(Ok(r)) => Ok(r),
|
|
|
|
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 = future.await;
|
|
|
|
let mut canceller_guard = canceller.lock().unwrap();
|
|
*canceller_guard = None;
|
|
|
|
res
|
|
}
|
|
|
|
pub fn wait<F, T>(
|
|
canceller: &Mutex<Option<future::AbortHandle>>,
|
|
future: F,
|
|
timeout: u32,
|
|
) -> Result<T, WaitError>
|
|
where
|
|
F: Send + Future<Output = Result<T, ErrorMessage>>,
|
|
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 {
|
|
future.await
|
|
} else {
|
|
let res = tokio::time::timeout(Duration::from_secs(timeout.into()), future).await;
|
|
|
|
match res {
|
|
Ok(r) => 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 = {
|
|
let _enter = RUNTIME.enter();
|
|
futures::executor::block_on(future)
|
|
};
|
|
|
|
canceller_guard = canceller.lock().unwrap();
|
|
*canceller_guard = None;
|
|
|
|
res
|
|
}
|
|
|
|
#[cfg(feature = "whip")]
|
|
pub fn parse_redirect_location(
|
|
headermap: &HeaderMap,
|
|
old_url: &reqwest::Url,
|
|
) -> Result<reqwest::Url, ErrorMessage> {
|
|
let location = match headermap.get(reqwest::header::LOCATION) {
|
|
Some(location) => location,
|
|
None => {
|
|
return Err(gst::error_msg!(
|
|
gst::ResourceError::Failed,
|
|
["Location header field should be present for WHIP/WHEP resource URL"]
|
|
));
|
|
}
|
|
};
|
|
|
|
let location = match location.to_str() {
|
|
Ok(loc) => loc,
|
|
Err(e) => {
|
|
return Err(gst::error_msg!(
|
|
gst::ResourceError::Failed,
|
|
["Failed to convert location to string {}", e]
|
|
));
|
|
}
|
|
};
|
|
|
|
match reqwest::Url::parse(location) {
|
|
Ok(url) => Ok(url), // Location URL is an absolute path
|
|
Err(_) => {
|
|
// Location URL is a relative path
|
|
let new_url = old_url.clone().join(location).map_err(|err| {
|
|
gst::error_msg!(
|
|
gst::ResourceError::Failed,
|
|
["URL join operation failed: {:?}", err]
|
|
)
|
|
})?;
|
|
|
|
Ok(new_url)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "whip")]
|
|
pub fn build_reqwest_client(pol: Policy) -> reqwest::Client {
|
|
let client_builder = reqwest::Client::builder();
|
|
client_builder.redirect(pol).build().unwrap()
|
|
}
|
|
|
|
#[cfg(feature = "whip")]
|
|
pub fn set_ice_servers(
|
|
webrtcbin: &gst::Element,
|
|
headermap: &HeaderMap,
|
|
) -> Result<(), ErrorMessage> {
|
|
for link in headermap.get_all("link").iter() {
|
|
let link = link.to_str().map_err(|err| {
|
|
gst::error_msg!(
|
|
gst::ResourceError::Failed,
|
|
[
|
|
"Header value should contain only visible ASCII strings: {}",
|
|
err
|
|
]
|
|
)
|
|
})?;
|
|
|
|
let item_map = match parse_link_header::parse_with_rel(link) {
|
|
Ok(map) => map,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
let link = match item_map.contains_key("ice-server") {
|
|
true => item_map.get("ice-server").unwrap(),
|
|
false => continue, // Not a link header we care about
|
|
};
|
|
|
|
// Note: webrtcbin needs ice servers to be in the below format
|
|
// <scheme>://<user:pass>@<url>
|
|
// and the ice-servers (link headers) received from the whip server might be
|
|
// in the format <scheme>:<host> with username and password as separate params.
|
|
// Constructing these with 'url' crate also require a format/parse
|
|
// for changing <scheme>:<host> to <scheme>://<user>:<password>@<host>.
|
|
// So preferred to use the String rather
|
|
|
|
// check if uri has ://
|
|
let ice_server_url = if link.uri.has_authority() {
|
|
// use raw_uri as is
|
|
// username and password in the link.uri.params ignored
|
|
link.uri.clone()
|
|
} else {
|
|
// No builder pattern is provided by reqwest::Url. Use string operation.
|
|
// construct url as '<scheme>://<user:pass>@<url>'
|
|
let url = format!("{}://{}", link.uri.scheme(), link.uri.path());
|
|
|
|
let mut new_url = match reqwest::Url::parse(url.as_str()) {
|
|
Ok(url) => url,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
if let Some(user) = link.params.get("username") {
|
|
new_url.set_username(user.as_str()).unwrap();
|
|
if let Some(pass) = link.params.get("credential") {
|
|
new_url.set_password(Some(pass.as_str())).unwrap();
|
|
}
|
|
}
|
|
|
|
new_url
|
|
};
|
|
|
|
// It's nicer to not collapse the `else if` and its inner `if`
|
|
#[allow(clippy::collapsible_if)]
|
|
if link.uri.scheme() == "stun" {
|
|
webrtcbin.set_property_from_str("stun-server", ice_server_url.as_str());
|
|
} else if link.uri.scheme().starts_with("turn") {
|
|
if !webrtcbin.emit_by_name::<bool>("add-turn-server", &[&ice_server_url.as_str()]) {
|
|
return Err(gst::error_msg!(
|
|
gst::ResourceError::Failed,
|
|
["Failed to set turn server {}", ice_server_url]
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn build_link_header(url_str: &str) -> Result<String, url::ParseError> {
|
|
let url = url::Url::parse(url_str)?;
|
|
|
|
let mut link_str: String = "<".to_owned() + url.scheme();
|
|
if let Some(host) = url.host_str() {
|
|
link_str = link_str + ":" + host;
|
|
}
|
|
|
|
if let Some(port) = url.port() {
|
|
link_str = link_str + ":" + port.to_string().as_str();
|
|
}
|
|
|
|
link_str += url.path();
|
|
|
|
if let Some(query) = url.query() {
|
|
link_str = link_str + "?" + query;
|
|
}
|
|
|
|
link_str += ">";
|
|
|
|
if let Some(password) = url.password() {
|
|
link_str = link_str
|
|
+ "; "
|
|
+ "rel=\"ice-server\""
|
|
+ "; "
|
|
+ "username=\""
|
|
+ url.username()
|
|
+ "\"; "
|
|
+ "credential:\""
|
|
+ password
|
|
+ "\"; "
|
|
+ "credential-type:\"password\";";
|
|
}
|
|
|
|
Ok(link_str)
|
|
}
|
|
|
|
/// Wrapper around `gst::ElementFactory::make` with a better error
|
|
/// message
|
|
pub fn make_element(element: &str, name: Option<&str>) -> Result<gst::Element, Error> {
|
|
let mut builder = gst::ElementFactory::make(element);
|
|
if let Some(name) = name {
|
|
builder = builder.name(name);
|
|
}
|
|
|
|
builder
|
|
.build()
|
|
.with_context(|| format!("Failed to make element {element}"))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DecodingInfo {
|
|
has_decoder: AtomicBool,
|
|
}
|
|
|
|
impl Clone for DecodingInfo {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
has_decoder: AtomicBool::new(self.has_decoder.load(Ordering::SeqCst)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct EncodingInfo {
|
|
encoder: Option<gst::ElementFactory>,
|
|
payloader: gst::ElementFactory,
|
|
output_filter: Option<gst::Caps>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Codec {
|
|
pub name: String,
|
|
pub caps: gst::Caps,
|
|
pub stream_type: gst::StreamType,
|
|
pub is_raw: bool,
|
|
|
|
payload_type: Option<i32>,
|
|
decoding_info: Option<DecodingInfo>,
|
|
encoding_info: Option<EncodingInfo>,
|
|
}
|
|
|
|
impl Codec {
|
|
pub fn new(
|
|
name: &str,
|
|
stream_type: gst::StreamType,
|
|
caps: &gst::Caps,
|
|
decoders: &glib::List<gst::ElementFactory>,
|
|
depayloaders: &glib::List<gst::ElementFactory>,
|
|
encoders: &glib::List<gst::ElementFactory>,
|
|
payloaders: &glib::List<gst::ElementFactory>,
|
|
) -> Self {
|
|
let has_decoder = Self::has_decoder_for_caps(caps, decoders);
|
|
let has_depayloader = Self::has_depayloader_for_codec(name, depayloaders);
|
|
|
|
let decoding_info = if has_depayloader && has_decoder {
|
|
Some(DecodingInfo {
|
|
has_decoder: AtomicBool::new(has_decoder),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let encoder = Self::get_encoder_for_caps(caps, encoders);
|
|
let payloader = Self::get_payloader_for_codec(name, payloaders);
|
|
|
|
let encoding_info = if let (Some(encoder), Some(payloader)) = (encoder, payloader) {
|
|
Some(EncodingInfo {
|
|
encoder: Some(encoder),
|
|
payloader,
|
|
output_filter: None,
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Self {
|
|
caps: caps.clone(),
|
|
stream_type,
|
|
name: name.into(),
|
|
is_raw: false,
|
|
payload_type: None,
|
|
decoding_info,
|
|
encoding_info,
|
|
}
|
|
}
|
|
|
|
pub fn new_raw(
|
|
name: &str,
|
|
stream_type: gst::StreamType,
|
|
depayloaders: &glib::List<gst::ElementFactory>,
|
|
payloaders: &glib::List<gst::ElementFactory>,
|
|
) -> Self {
|
|
let decoding_info = if Self::has_depayloader_for_codec(name, depayloaders) {
|
|
Some(DecodingInfo {
|
|
has_decoder: AtomicBool::new(false),
|
|
})
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let payloader = Self::get_payloader_for_codec(name, payloaders);
|
|
let encoding_info = payloader.map(|payloader| EncodingInfo {
|
|
encoder: None,
|
|
payloader,
|
|
output_filter: None,
|
|
});
|
|
|
|
let mut caps = None;
|
|
if let Some(elem) = Codec::get_payloader_for_codec(name, payloaders) {
|
|
if let Some(tmpl) = elem
|
|
.static_pad_templates()
|
|
.iter()
|
|
.find(|template| template.direction() == gst::PadDirection::Sink)
|
|
{
|
|
caps = Some(tmpl.caps());
|
|
}
|
|
}
|
|
|
|
Self {
|
|
caps: caps.unwrap_or_else(gst::Caps::new_empty),
|
|
stream_type,
|
|
name: name.into(),
|
|
is_raw: true,
|
|
payload_type: None,
|
|
decoding_info,
|
|
encoding_info,
|
|
}
|
|
}
|
|
|
|
pub fn can_encode(&self) -> bool {
|
|
self.encoding_info.is_some()
|
|
}
|
|
|
|
pub fn set_pt(&mut self, pt: i32) {
|
|
self.payload_type = Some(pt);
|
|
}
|
|
|
|
pub fn can_be_received(&self) -> bool {
|
|
if self.decoding_info.is_none() {
|
|
return false;
|
|
}
|
|
|
|
if self.is_raw {
|
|
return true;
|
|
}
|
|
|
|
let decoder_info = self.decoding_info.as_ref().unwrap();
|
|
if decoder_info.has_decoder.load(Ordering::SeqCst) {
|
|
true
|
|
} else if Self::has_decoder_for_caps(
|
|
&self.caps,
|
|
// Replicating decodebin logic
|
|
&gst::ElementFactory::factories_with_type(
|
|
gst::ElementFactoryType::DECODER,
|
|
gst::Rank::MARGINAL,
|
|
),
|
|
) {
|
|
// Check if new decoders have been installed meanwhile
|
|
decoder_info.has_decoder.store(true, Ordering::SeqCst);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn get_encoder_for_caps(
|
|
caps: &gst::Caps,
|
|
encoders: &glib::List<gst::ElementFactory>,
|
|
) -> Option<gst::ElementFactory> {
|
|
encoders
|
|
.iter()
|
|
.find(|factory| {
|
|
factory.static_pad_templates().iter().any(|template| {
|
|
let template_caps = template.caps();
|
|
template.direction() == gst::PadDirection::Src
|
|
&& !template_caps.is_any()
|
|
&& caps.can_intersect(&template_caps)
|
|
})
|
|
})
|
|
.cloned()
|
|
}
|
|
|
|
fn get_payloader_for_codec(
|
|
codec: &str,
|
|
payloaders: &glib::List<gst::ElementFactory>,
|
|
) -> Option<gst::ElementFactory> {
|
|
payloaders
|
|
.iter()
|
|
.find(|factory| {
|
|
factory.static_pad_templates().iter().any(|template| {
|
|
let template_caps = template.caps();
|
|
|
|
if template.direction() != gst::PadDirection::Src || template_caps.is_any() {
|
|
return false;
|
|
}
|
|
|
|
template_caps.iter().any(|s| {
|
|
s.has_field("encoding-name")
|
|
&& s.get::<gst::List>("encoding-name").map_or_else(
|
|
|_| {
|
|
if let Ok(encoding_name) = s.get::<&str>("encoding-name") {
|
|
encoding_name == codec
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
|encoding_names| {
|
|
encoding_names.iter().any(|v| {
|
|
v.get::<&str>()
|
|
.is_ok_and(|encoding_name| encoding_name == codec)
|
|
})
|
|
},
|
|
)
|
|
})
|
|
})
|
|
})
|
|
.cloned()
|
|
}
|
|
|
|
fn has_decoder_for_caps(caps: &gst::Caps, decoders: &glib::List<gst::ElementFactory>) -> bool {
|
|
decoders.iter().any(|factory| {
|
|
factory.static_pad_templates().iter().any(|template| {
|
|
let template_caps = template.caps();
|
|
template.direction() == gst::PadDirection::Sink
|
|
&& !template_caps.is_any()
|
|
&& caps.can_intersect(&template_caps)
|
|
})
|
|
})
|
|
}
|
|
|
|
fn has_depayloader_for_codec(
|
|
codec: &str,
|
|
depayloaders: &glib::List<gst::ElementFactory>,
|
|
) -> bool {
|
|
depayloaders.iter().any(|factory| {
|
|
factory.static_pad_templates().iter().any(|template| {
|
|
let template_caps = template.caps();
|
|
|
|
if template.direction() != gst::PadDirection::Sink {
|
|
return false;
|
|
}
|
|
|
|
template_caps.iter().any(|s| {
|
|
s.has_field("encoding-name")
|
|
&& s.get::<gst::List>("encoding-name").map_or_else(
|
|
|_| {
|
|
if let Ok(encoding_name) = s.get::<&str>("encoding-name") {
|
|
encoding_name == codec
|
|
} else {
|
|
false
|
|
}
|
|
},
|
|
|encoding_names| {
|
|
encoding_names.iter().any(|v| {
|
|
v.get::<&str>()
|
|
.map_or(false, |encoding_name| encoding_name == codec)
|
|
})
|
|
},
|
|
)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn is_video(&self) -> bool {
|
|
matches!(self.stream_type, gst::StreamType::VIDEO)
|
|
}
|
|
|
|
pub fn payload(&self) -> Option<i32> {
|
|
self.payload_type
|
|
}
|
|
|
|
pub fn build_encoder(&self) -> Option<Result<gst::Element, Error>> {
|
|
self.encoding_info.as_ref().and_then(|info| {
|
|
info.encoder.as_ref().map(|encoder| {
|
|
encoder
|
|
.create()
|
|
.build()
|
|
.with_context(|| format!("Creating encoder {}", encoder.name()))
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn create_payloader(&self) -> Option<gst::Element> {
|
|
self.encoding_info
|
|
.as_ref()
|
|
.map(|info| info.payloader.create().build().unwrap())
|
|
}
|
|
|
|
pub fn raw_converter_filter(&self) -> Result<gst::Element, Error> {
|
|
let caps = if self.is_video() {
|
|
let mut structure_builder = gst::Structure::builder("video/x-raw")
|
|
.field("pixel-aspect-ratio", gst::Fraction::new(1, 1));
|
|
|
|
if self
|
|
.encoder_name()
|
|
.map(|e| e.as_str() == "nvh264enc")
|
|
.unwrap_or(false)
|
|
{
|
|
// Quirk: nvh264enc can perform conversion from RGB formats, but
|
|
// doesn't advertise / negotiate colorimetry correctly, leading
|
|
// to incorrect color display in Chrome (but interestingly not in
|
|
// Firefox). In any case, restrict to exclude RGB formats altogether,
|
|
// and let videoconvert do the conversion properly if needed.
|
|
structure_builder =
|
|
structure_builder.field("format", gst::List::new(["NV12", "YV12", "I420"]));
|
|
}
|
|
|
|
gst::Caps::builder_full_with_any_features()
|
|
.structure(structure_builder.build())
|
|
.build()
|
|
} else {
|
|
gst::Caps::builder("audio/x-raw").build()
|
|
};
|
|
|
|
gst::ElementFactory::make("capsfilter")
|
|
.property("caps", &caps)
|
|
.build()
|
|
.with_context(|| "Creating capsfilter caps")
|
|
}
|
|
|
|
pub fn encoder_factory(&self) -> Option<gst::ElementFactory> {
|
|
self.encoding_info
|
|
.as_ref()
|
|
.and_then(|info| info.encoder.clone())
|
|
}
|
|
|
|
pub fn encoder_name(&self) -> Option<String> {
|
|
self.encoding_info.as_ref().and_then(|info| {
|
|
info.encoder
|
|
.as_ref()
|
|
.map(|encoder| encoder.name().to_string())
|
|
})
|
|
}
|
|
|
|
pub fn set_output_filter(&mut self, caps: gst::Caps) {
|
|
if let Some(info) = self.encoding_info.as_mut() {
|
|
info.output_filter = Some(caps);
|
|
}
|
|
}
|
|
|
|
pub fn output_filter(&self) -> Option<gst::Caps> {
|
|
self.encoding_info
|
|
.as_ref()
|
|
.and_then(|info| info.output_filter.clone())
|
|
}
|
|
|
|
pub fn build_parser(&self) -> Result<Option<gst::Element>, Error> {
|
|
match self.name.as_str() {
|
|
"VP9" => make_element("vp9parse", None),
|
|
"H264" => make_element("h264parse", None),
|
|
"H265" => make_element("h265parse", None),
|
|
"AV1" => make_element("av1parse", None),
|
|
_ => return Ok(None),
|
|
}
|
|
.map(Some)
|
|
}
|
|
|
|
pub fn parser_caps(&self, force_profile: bool) -> gst::Caps {
|
|
let codec_caps_name = self.caps.structure(0).unwrap().name();
|
|
match self.name.as_str() {
|
|
"H264" => {
|
|
if force_profile {
|
|
gst::debug!(
|
|
CAT,
|
|
"No H264 profile requested, selecting constrained-baseline"
|
|
);
|
|
|
|
gst::Caps::builder(codec_caps_name)
|
|
.field("stream-format", "avc")
|
|
.field("profile", "constrained-baseline")
|
|
.build()
|
|
} else {
|
|
gst::Caps::builder(codec_caps_name)
|
|
.field("stream-format", "avc")
|
|
.build()
|
|
}
|
|
}
|
|
"H265" => gst::Caps::new_empty_simple("video/x-h265"),
|
|
_ => gst::Caps::new_any(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub static AUDIO_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("audio/x-raw"));
|
|
pub static OPUS_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("audio/x-opus"));
|
|
|
|
pub static VIDEO_CAPS: LazyLock<gst::Caps> = LazyLock::new(|| {
|
|
gst::Caps::builder_full_with_any_features()
|
|
.structure(gst::Structure::new_empty("video/x-raw"))
|
|
.build()
|
|
});
|
|
pub static VP8_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("video/x-vp8"));
|
|
pub static VP9_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("video/x-vp9"));
|
|
pub static H264_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("video/x-h264"));
|
|
pub static H265_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("video/x-h265"));
|
|
pub static AV1_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("video/x-av1"));
|
|
|
|
pub static RTP_CAPS: LazyLock<gst::Caps> =
|
|
LazyLock::new(|| gst::Caps::new_empty_simple("application/x-rtp"));
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Codecs(Vec<Codec>);
|
|
|
|
impl Deref for Codecs {
|
|
type Target = Vec<Codec>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Codecs {
|
|
pub fn to_map(&self) -> BTreeMap<i32, Codec> {
|
|
self.0
|
|
.iter()
|
|
.map(|codec| (codec.payload().unwrap(), codec.clone()))
|
|
.collect()
|
|
}
|
|
|
|
pub fn from_map(codecs: &BTreeMap<i32, Codec>) -> Self {
|
|
Self(codecs.values().cloned().collect())
|
|
}
|
|
|
|
pub fn find_for_payloadable_caps(&self, caps: &gst::Caps) -> Option<Codec> {
|
|
self.iter()
|
|
.find(|codec| codec.caps.can_intersect(caps) && codec.encoding_info.is_some())
|
|
.cloned()
|
|
}
|
|
}
|
|
|
|
static CODECS: LazyLock<Codecs> = LazyLock::new(|| {
|
|
let decoders = gst::ElementFactory::factories_with_type(
|
|
gst::ElementFactoryType::DECODER,
|
|
gst::Rank::MARGINAL,
|
|
);
|
|
|
|
let depayloaders = gst::ElementFactory::factories_with_type(
|
|
gst::ElementFactoryType::DEPAYLOADER,
|
|
gst::Rank::MARGINAL,
|
|
);
|
|
|
|
let encoders = gst::ElementFactory::factories_with_type(
|
|
gst::ElementFactoryType::ENCODER,
|
|
gst::Rank::MARGINAL,
|
|
);
|
|
|
|
let payloaders = gst::ElementFactory::factories_with_type(
|
|
gst::ElementFactoryType::PAYLOADER,
|
|
gst::Rank::MARGINAL,
|
|
);
|
|
|
|
Codecs(vec![
|
|
Codec::new(
|
|
"OPUS",
|
|
gst::StreamType::AUDIO,
|
|
&OPUS_CAPS,
|
|
&decoders,
|
|
&depayloaders,
|
|
&encoders,
|
|
&payloaders,
|
|
),
|
|
Codec::new_raw("L24", gst::StreamType::AUDIO, &depayloaders, &payloaders),
|
|
Codec::new_raw("L16", gst::StreamType::AUDIO, &depayloaders, &payloaders),
|
|
Codec::new_raw("L8", gst::StreamType::AUDIO, &depayloaders, &payloaders),
|
|
Codec::new(
|
|
"VP8",
|
|
gst::StreamType::VIDEO,
|
|
&VP8_CAPS,
|
|
&decoders,
|
|
&depayloaders,
|
|
&encoders,
|
|
&payloaders,
|
|
),
|
|
Codec::new(
|
|
"H264",
|
|
gst::StreamType::VIDEO,
|
|
&H264_CAPS,
|
|
&decoders,
|
|
&depayloaders,
|
|
&encoders,
|
|
&payloaders,
|
|
),
|
|
Codec::new(
|
|
"VP9",
|
|
gst::StreamType::VIDEO,
|
|
&VP9_CAPS,
|
|
&decoders,
|
|
&depayloaders,
|
|
&encoders,
|
|
&payloaders,
|
|
),
|
|
Codec::new(
|
|
"H265",
|
|
gst::StreamType::VIDEO,
|
|
&H265_CAPS,
|
|
&decoders,
|
|
&depayloaders,
|
|
&encoders,
|
|
&payloaders,
|
|
),
|
|
Codec::new(
|
|
"AV1",
|
|
gst::StreamType::VIDEO,
|
|
&AV1_CAPS,
|
|
&decoders,
|
|
&depayloaders,
|
|
&encoders,
|
|
&payloaders,
|
|
),
|
|
Codec::new_raw("RAW", gst::StreamType::VIDEO, &depayloaders, &payloaders),
|
|
])
|
|
});
|
|
|
|
impl Codecs {
|
|
pub fn find(encoding_name: &str) -> Option<Codec> {
|
|
CODECS
|
|
.iter()
|
|
.find(|codec| codec.name == encoding_name)
|
|
.cloned()
|
|
}
|
|
|
|
pub fn video_codecs<'a>() -> impl Iterator<Item = &'a Codec> {
|
|
CODECS
|
|
.iter()
|
|
.filter(|codec| codec.stream_type == gst::StreamType::VIDEO)
|
|
}
|
|
|
|
pub fn audio_codecs<'a>() -> impl Iterator<Item = &'a Codec> {
|
|
CODECS
|
|
.iter()
|
|
.filter(|codec| codec.stream_type == gst::StreamType::AUDIO)
|
|
}
|
|
|
|
/// List all codecs that can be used for encoding the given caps and assign
|
|
/// a payload type to each of them. This is useful to initiate SDP negotiation.
|
|
pub fn list_encoders<'a>(caps: impl IntoIterator<Item = &'a gst::StructureRef>) -> Codecs {
|
|
let mut payload = 96..128;
|
|
|
|
Codecs(
|
|
caps.into_iter()
|
|
.filter_map(move |s| {
|
|
let caps = gst::Caps::builder_full().structure(s.to_owned()).build();
|
|
|
|
CODECS
|
|
.iter()
|
|
.find(|codec| {
|
|
codec
|
|
.encoding_info
|
|
.as_ref()
|
|
.is_some_and(|_| codec.caps.can_intersect(&caps))
|
|
})
|
|
.and_then(|codec| {
|
|
/* Assign a payload type to the codec */
|
|
if let Some(pt) = payload.next() {
|
|
let mut codec = codec.clone();
|
|
|
|
codec.payload_type = Some(pt);
|
|
|
|
Some(codec)
|
|
} else {
|
|
gst::warning!(
|
|
CAT,
|
|
"Too many formats for available payload type range, ignoring {}",
|
|
s
|
|
);
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.collect()
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn has_raw_caps(caps: &gst::Caps) -> bool {
|
|
caps.iter()
|
|
.any(|s| ["video/x-raw", "audio/x-raw"].contains(&s.name().as_str()))
|
|
}
|
|
|
|
pub fn cleanup_codec_caps(mut caps: gst::Caps) -> gst::Caps {
|
|
assert!(caps.is_fixed());
|
|
|
|
if let Some(s) = caps.make_mut().structure_mut(0) {
|
|
if ["video/x-h264", "video/x-h265"].contains(&s.name().as_str()) {
|
|
s.remove_fields(["codec_data"]);
|
|
} else if ["video/x-vp8", "video/x-vp9"].contains(&s.name().as_str()) {
|
|
s.remove_fields(["profile"]);
|
|
} else if s.name() == "audio/x-opus" {
|
|
s.remove_fields(["streamheader"]);
|
|
}
|
|
}
|
|
|
|
caps
|
|
}
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Deserialize, Serialize)]
|
|
pub struct NavigationEvent {
|
|
pub mid: Option<String>,
|
|
#[serde(flatten)]
|
|
pub event: gst_video::NavigationEvent,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
#[serde(tag = "type")]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum ControlRequest {
|
|
NavigationEvent {
|
|
event: gst_video::NavigationEvent,
|
|
},
|
|
#[serde(rename_all = "camelCase")]
|
|
CustomUpstreamEvent {
|
|
structure_name: String,
|
|
structure: serde_json::Value,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
#[serde(untagged)]
|
|
pub enum StringOrRequest {
|
|
String(String),
|
|
Request(ControlRequest),
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ControlRequestMessage {
|
|
pub id: u64,
|
|
pub mid: Option<String>,
|
|
pub request: StringOrRequest,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(tag = "type")]
|
|
pub struct ControlResponseMessage {
|
|
pub id: u64,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
pub fn find_smallest_available_ext_id(ids: impl IntoIterator<Item = u32>) -> u32 {
|
|
let used_numbers: HashSet<_> = ids.into_iter().collect();
|
|
(1..).find(|&num| !used_numbers.contains(&num)).unwrap()
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum CoerceTarget {
|
|
Undefined,
|
|
U64,
|
|
I64,
|
|
F64,
|
|
Other(serde_json::Value),
|
|
}
|
|
|
|
fn pick_coerce_target(
|
|
arr: &Vec<serde_json::Value>,
|
|
mut target: CoerceTarget,
|
|
) -> Result<CoerceTarget, Error> {
|
|
for val in arr {
|
|
match val {
|
|
serde_json::Value::Null => {
|
|
return Err(anyhow!("Untyped null values are not handled"));
|
|
}
|
|
serde_json::Value::Bool(_) => match &target {
|
|
CoerceTarget::Undefined => {
|
|
target = CoerceTarget::Other(val.clone());
|
|
}
|
|
CoerceTarget::Other(other) => {
|
|
if !other.is_boolean() {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
},
|
|
serde_json::Value::Number(v) => {
|
|
let v_target = if v.as_u64().is_some() {
|
|
CoerceTarget::U64
|
|
} else if v.as_i64().is_some() {
|
|
CoerceTarget::I64
|
|
} else {
|
|
CoerceTarget::F64
|
|
};
|
|
match &target {
|
|
CoerceTarget::Undefined => {
|
|
target = v_target;
|
|
}
|
|
CoerceTarget::Other(_) => {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
CoerceTarget::U64 => {
|
|
target = v_target;
|
|
}
|
|
CoerceTarget::I64 => {
|
|
if matches!(v_target, CoerceTarget::F64) {
|
|
target = CoerceTarget::F64;
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
serde_json::Value::Array(a) => {
|
|
target = pick_coerce_target(a, target)?;
|
|
}
|
|
serde_json::Value::Object(_) => match &target {
|
|
CoerceTarget::Undefined => {
|
|
target = CoerceTarget::Other(val.clone());
|
|
}
|
|
CoerceTarget::Other(other) => {
|
|
if !other.is_object() {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
},
|
|
serde_json::Value::String(_) => match &target {
|
|
CoerceTarget::Undefined => {
|
|
target = CoerceTarget::Other(val.clone());
|
|
}
|
|
CoerceTarget::Other(other) => {
|
|
if !other.is_object() {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
}
|
|
_ => {
|
|
return Err(anyhow!("Mixed types in arrays are not supported"));
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
Ok(target)
|
|
}
|
|
|
|
fn deserialize_serde_value(
|
|
val: &serde_json::Value,
|
|
mut target: CoerceTarget,
|
|
) -> Result<gst::glib::SendValue, Error> {
|
|
match val {
|
|
serde_json::Value::Null => Err(anyhow!("Untyped null values are not handled")),
|
|
serde_json::Value::Bool(v) => Ok(v.to_send_value()),
|
|
serde_json::Value::Number(v) => match target {
|
|
CoerceTarget::U64 => Ok(v
|
|
.as_u64()
|
|
.ok_or(anyhow!("Mixed types in arrays are not supported"))?
|
|
.to_send_value()),
|
|
CoerceTarget::I64 => Ok(v
|
|
.as_i64()
|
|
.ok_or(anyhow!("Mixed types in arrays are not supported"))?
|
|
.to_send_value()),
|
|
CoerceTarget::F64 => Ok(v
|
|
.as_f64()
|
|
.expect("all numbers coerce to f64")
|
|
.to_send_value()),
|
|
CoerceTarget::Undefined => {
|
|
if let Some(u) = v.as_u64() {
|
|
Ok(u.to_send_value())
|
|
} else if let Some(i) = v.as_i64() {
|
|
Ok(i.to_send_value())
|
|
} else if let Some(f) = v.as_f64() {
|
|
Ok(f.to_send_value())
|
|
} else {
|
|
unreachable!()
|
|
}
|
|
}
|
|
_ => unreachable!(),
|
|
},
|
|
serde_json::Value::String(v) => Ok(v.to_send_value()),
|
|
serde_json::Value::Array(a) => {
|
|
let mut gst_array = gst::Array::default();
|
|
|
|
target = pick_coerce_target(a, target)?;
|
|
|
|
for val in a {
|
|
gst_array.append_value(deserialize_serde_value(val, target.to_owned())?);
|
|
}
|
|
|
|
Ok(gst_array.to_send_value())
|
|
}
|
|
serde_json::Value::Object(_) => {
|
|
Ok(deserialize_serde_object(val, "webrtcsink-deserialized")?.to_send_value())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn deserialize_serde_object(
|
|
obj: &serde_json::Value,
|
|
name: &str,
|
|
) -> Result<gst::Structure, Error> {
|
|
let serde_json::Value::Object(map) = obj else {
|
|
return Err(anyhow!("not a serde object"));
|
|
};
|
|
|
|
let mut ret = gst::Structure::builder(name);
|
|
|
|
for (key, value) in map {
|
|
ret = ret.field(
|
|
key,
|
|
deserialize_serde_value(value, CoerceTarget::Undefined)?,
|
|
);
|
|
}
|
|
|
|
Ok(ret.build())
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct VideoTimeCodeFlags(gst_video::VideoTimeCodeFlags);
|
|
|
|
impl serde::Serialize for VideoTimeCodeFlags {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
if self.0.bits() == 0 {
|
|
serializer.serialize_str("none")
|
|
} else {
|
|
let mut ret = String::new();
|
|
if self.0.contains(gst_video::VideoTimeCodeFlags::DROP_FRAME) {
|
|
ret.push_str("drop-frame");
|
|
if self.0.contains(gst_video::VideoTimeCodeFlags::INTERLACED) {
|
|
ret.push_str("+interlaced");
|
|
}
|
|
} else if self.0.contains(gst_video::VideoTimeCodeFlags::INTERLACED) {
|
|
ret.push_str("interlaced");
|
|
}
|
|
serializer.serialize_str(&ret)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Debug, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(tag = "type")]
|
|
pub enum Meta {
|
|
TimeCode {
|
|
hours: u32,
|
|
minutes: u32,
|
|
seconds: u32,
|
|
frames: u32,
|
|
field_count: u32,
|
|
fps: gst::Fraction,
|
|
flags: VideoTimeCodeFlags,
|
|
latest_daily_jam: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Serialize, Debug, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum Info {
|
|
Meta(Meta),
|
|
}
|
|
|
|
#[derive(Serialize, Debug, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[serde(tag = "type")]
|
|
pub struct InfoMessage {
|
|
pub mid: String,
|
|
pub info: Info,
|
|
}
|
|
|
|
pub fn serialize_meta(buffer: &gst::BufferRef, forward_metas: &HashSet<String>) -> Vec<Meta> {
|
|
let mut ret = vec![];
|
|
|
|
buffer.foreach_meta(|meta| {
|
|
if forward_metas.contains("timecode") {
|
|
if let Some(tc_meta) = meta.downcast_ref::<gst_video::VideoTimeCodeMeta>() {
|
|
let tc = tc_meta.tc();
|
|
ret.push(Meta::TimeCode {
|
|
hours: tc.hours(),
|
|
minutes: tc.minutes(),
|
|
seconds: tc.seconds(),
|
|
frames: tc.frames(),
|
|
field_count: tc.field_count(),
|
|
fps: tc.fps(),
|
|
flags: VideoTimeCodeFlags(tc.flags()),
|
|
latest_daily_jam: tc
|
|
.latest_daily_jam()
|
|
.and_then(|dt| {
|
|
let gst_dt: gst::DateTime = dt.into();
|
|
gst_dt.to_iso8601_string().ok()
|
|
})
|
|
.map(|s| s.to_string()),
|
|
});
|
|
}
|
|
}
|
|
std::ops::ControlFlow::Continue(())
|
|
});
|
|
|
|
ret
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_deserialize_array() -> Result<(), String> {
|
|
let arr = serde_json::from_str::<serde_json::Value>("[1, -1, 1.0]").unwrap();
|
|
let gst_arr = deserialize_serde_value(&arr, CoerceTarget::Undefined)
|
|
.unwrap()
|
|
.get::<gst::Array>()
|
|
.unwrap();
|
|
let gst_type = gst_arr.first().unwrap().value_type();
|
|
assert_eq!(gst_type, f64::static_type());
|
|
|
|
let arr = serde_json::from_str::<serde_json::Value>("[1, -1]").unwrap();
|
|
let gst_arr = deserialize_serde_value(&arr, CoerceTarget::Undefined)
|
|
.unwrap()
|
|
.get::<gst::Array>()
|
|
.unwrap();
|
|
let gst_type = gst_arr.first().unwrap().value_type();
|
|
assert_eq!(gst_type, i64::static_type());
|
|
|
|
let arr = serde_json::from_str::<serde_json::Value>("[1]").unwrap();
|
|
let gst_arr = deserialize_serde_value(&arr, CoerceTarget::Undefined)
|
|
.unwrap()
|
|
.get::<gst::Array>()
|
|
.unwrap();
|
|
let gst_type = gst_arr.first().unwrap().value_type();
|
|
assert_eq!(gst_type, u64::static_type());
|
|
|
|
// u64::MAX can't be represented as i64, mixed types
|
|
let arr = serde_json::from_str::<serde_json::Value>("[18446744073709551615, -1]").unwrap();
|
|
assert!(deserialize_serde_value(&arr, CoerceTarget::Undefined).is_err());
|
|
|
|
// we won't coerce bool to i64, mixed types
|
|
let arr = serde_json::from_str::<serde_json::Value>("[true, -1]").unwrap();
|
|
assert!(deserialize_serde_value(&arr, CoerceTarget::Undefined).is_err());
|
|
|
|
let arr = serde_json::from_str::<serde_json::Value>("[[0.2, 0], [0, 0]]").unwrap();
|
|
let gst_arr = deserialize_serde_value(&arr, CoerceTarget::Undefined)
|
|
.unwrap()
|
|
.get::<gst::Array>()
|
|
.unwrap();
|
|
let gst_type = gst_arr
|
|
.first()
|
|
.unwrap()
|
|
.get::<gst::Array>()
|
|
.unwrap()
|
|
.first()
|
|
.unwrap()
|
|
.value_type();
|
|
assert_eq!(gst_type, f64::static_type());
|
|
let gst_type = gst_arr
|
|
.last()
|
|
.unwrap()
|
|
.get::<gst::Array>()
|
|
.unwrap()
|
|
.first()
|
|
.unwrap()
|
|
.value_type();
|
|
assert_eq!(gst_type, f64::static_type());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialize_meta() -> Result<(), String> {
|
|
gst::init().unwrap();
|
|
|
|
let mut buffer = gst::Buffer::new();
|
|
let time_code = gst_video::VideoTimeCode::new(
|
|
gst::Fraction::new(30, 1),
|
|
None,
|
|
gst_video::VideoTimeCodeFlags::empty(),
|
|
10,
|
|
53,
|
|
17,
|
|
0,
|
|
0,
|
|
);
|
|
gst_video::VideoTimeCodeMeta::add(
|
|
buffer.get_mut().unwrap(),
|
|
&time_code.try_into().unwrap(),
|
|
);
|
|
|
|
assert_eq!(
|
|
serialize_meta(&buffer, &[String::from("timecode")].into()),
|
|
vec![Meta::TimeCode {
|
|
hours: 10,
|
|
minutes: 53,
|
|
seconds: 17,
|
|
frames: 0,
|
|
field_count: 0,
|
|
fps: gst::Fraction::new(30, 1),
|
|
flags: VideoTimeCodeFlags(gst_video::VideoTimeCodeFlags::empty()),
|
|
latest_daily_jam: None,
|
|
}]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn test_find_smallest_available_ext_id_case(
|
|
ids: impl IntoIterator<Item = u32>,
|
|
expected: u32,
|
|
) -> Result<(), String> {
|
|
let actual = find_smallest_available_ext_id(ids);
|
|
|
|
if actual != expected {
|
|
return Err(format!("Expected {}, got {}", expected, actual));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_smallest_available_ext_id() -> Result<(), String> {
|
|
[
|
|
(vec![], 1u32),
|
|
(vec![2u32, 3u32, 4u32], 1u32),
|
|
(vec![1u32, 3u32, 4u32], 2u32),
|
|
(vec![4u32, 1u32, 3u32], 2u32),
|
|
(vec![1u32, 2u32, 3u32], 4u32),
|
|
]
|
|
.into_iter()
|
|
.try_for_each(|(input, expected)| test_find_smallest_available_ext_id_case(input, expected))
|
|
}
|
|
}
|