diff options
author | Arun Raghavan <arun@asymptotic.io> | 2020-04-04 02:26:41 +0300 |
---|---|---|
committer | Arun Raghavan <arun@arunraghavan.net> | 2020-04-05 22:10:46 +0300 |
commit | 205b6040fbb918c0fa736874b09f8e3f3f261e44 (patch) | |
tree | 37ed0d22ece764ec91dccefd3c2148dc26d5b96b /generic | |
parent | 5d992692f0c494c64a379de6201239efe614a15e (diff) |
Reorganise plugins into directories by function
This should start making navigating the tree a little easier to start
with, and we can then move to allowing building specific groups of
plugins as well.
The plugins are moved into the following hierarchy:
audio
/ gst-plugin-audiofx
/ gst-plugin-claxon
/ gst-plugin-csound
/ gst-plugin-lewton
generic
/ gst-plugin-file
/ gst-plugin-sodium
/ gst-plugin-threadshare
net
/ gst-plugin-reqwest
/ gst-plugin-rusoto
utils
/ gst-plugin-fallbackswitch
/ gst-plugin-togglerecord
video
/ gst-plugin-cdg
/ gst-plugin-closedcaption
/ gst-plugin-dav1d
/ gst-plugin-flv
/ gst-plugin-gif
/ gst-plugin-rav1e
gst-plugin-tutorial
gst-plugin-version-helper
Diffstat (limited to 'generic')
56 files changed, 21902 insertions, 0 deletions
diff --git a/generic/gst-plugin-file/Cargo.toml b/generic/gst-plugin-file/Cargo.toml new file mode 100644 index 000000000..bc1949c47 --- /dev/null +++ b/generic/gst-plugin-file/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "gst-plugin-file" +version = "0.6.0" +authors = ["Sebastian Dröge <sebastian@centricular.com>"] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +license = "MIT/Apache-2.0" +description = "Rust File Plugin" + +[dependencies] +url = "2" +glib = { git = "https://github.com/gtk-rs/glib" } +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gstreamer-base = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +lazy_static = "1.0" + +[lib] +name = "gstrsfile" +crate-type = ["cdylib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../gst-plugin-version-helper" } diff --git a/generic/gst-plugin-file/build.rs b/generic/gst-plugin-file/build.rs new file mode 100644 index 000000000..0d1ddb61d --- /dev/null +++ b/generic/gst-plugin-file/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/generic/gst-plugin-file/src/file_location.rs b/generic/gst-plugin-file/src/file_location.rs new file mode 100644 index 000000000..8eba2c01a --- /dev/null +++ b/generic/gst-plugin-file/src/file_location.rs @@ -0,0 +1,141 @@ +use glib; +use gst; +use url::Url; + +use std::convert::AsRef; +use std::fmt; +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +#[cfg(target_os = "windows")] +const WIN_EXT_PATH_PREFIX: &str = "\\\\?\\"; +#[cfg(target_os = "windows")] +const WIN_EXT_PATH_PREFIX_LEN: usize = 4; + +#[derive(Debug)] +pub(super) struct FileLocation(PathBuf); + +impl FileLocation { + pub(super) fn try_from_path_str(path_str: String) -> Result<Self, glib::Error> { + FileLocation::try_from(PathBuf::from(path_str)) + } + + pub(super) fn try_from_uri_str(uri_str: &str) -> Result<Self, glib::Error> { + match Url::parse(uri_str) { + Ok(url) => { + if url.scheme() != "file" { + return Err(glib::Error::new( + gst::URIError::UnsupportedProtocol, + format!("Unsupported URI {}", uri_str).as_str(), + )); + } + + let path = url.to_file_path().or_else(|_| { + Err(glib::Error::new( + gst::URIError::BadUri, + format!("Unsupported URI {}", uri_str).as_str(), + )) + })?; + + FileLocation::try_from(path) + } + Err(err) => Err(glib::Error::new( + gst::URIError::BadUri, + format!("Couldn't parse URI {}: {}", uri_str, err.to_string()).as_str(), + )), + } + } + + fn try_from(location: PathBuf) -> Result<Self, glib::Error> { + let location_str = location.to_str().ok_or_else(|| { + glib::Error::new( + gst::URIError::BadReference, + format!("Invalid path {:?}", location).as_str(), + ) + })?; + + let file_name = location.file_name().ok_or_else(|| { + glib::Error::new( + gst::URIError::BadReference, + format!("Expected a path with a filename, got {}", location_str,).as_str(), + ) + })?; + + // The filename might not exist yet, so check the parent only. + // Note: `location` contains a filename, so its parent can't be `None` + let mut parent_dir = location + .parent() + .expect("FileSink::set_location `location` with filename but without a parent") + .to_owned(); + if parent_dir.is_relative() && parent_dir.components().next() == None { + // `location` only contains the filename + // need to specify "." for `canonicalize` to resolve the actual path + parent_dir = PathBuf::from("."); + } + + let parent_canonical = parent_dir.canonicalize().map_err(|err| { + glib::Error::new( + gst::URIError::BadReference, + format!( + "Could not resolve path {}: {}", + location_str, + err.to_string(), + ) + .as_str(), + ) + })?; + + #[cfg(target_os = "windows")] + let parent_canonical = { + let has_prefix = parent_canonical + .to_str() + .unwrap() // already checked above + .starts_with(WIN_EXT_PATH_PREFIX); + if has_prefix { + // Remove the "extended length path" prefix + // for compatibility with applications which can't deal with it. + // See https://doc.rust-lang.org/std/fs/fn.canonicalize.html + let parent_canonical_str = parent_canonical.to_str().unwrap(); + PathBuf::from(&parent_canonical_str[WIN_EXT_PATH_PREFIX_LEN..]) + } else { + parent_canonical + } + }; + + let location_canonical = parent_canonical.join(file_name); + Url::from_file_path(&location_canonical) + .map_err(|_| { + glib::Error::new( + gst::URIError::BadReference, + format!("Could not resolve path to URL {}", location_str).as_str(), + ) + }) + .map(|_| FileLocation(location_canonical)) + } + + fn to_str(&self) -> &str { + self.0 + .to_str() + .expect("FileLocation: couldn't get `&str` from internal `PathBuf`") + } +} + +impl AsRef<Path> for FileLocation { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +impl Deref for FileLocation { + type Target = Path; + + fn deref(&self) -> &Path { + self.0.as_ref() + } +} + +impl fmt::Display for FileLocation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_str()) + } +} diff --git a/generic/gst-plugin-file/src/filesink.rs b/generic/gst-plugin-file/src/filesink.rs new file mode 100644 index 000000000..ee912ec15 --- /dev/null +++ b/generic/gst-plugin-file/src/filesink.rs @@ -0,0 +1,339 @@ +// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com> +// 2016 Luis de Bethencourt <luisbg@osg.samsung.com> +// 2018 François Laignel <fengalin@free.fr> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst_base; +use gst_base::subclass::prelude::*; + +use std::fs::File; +use std::io::Write; +use std::sync::Mutex; + +use url::Url; + +use file_location::FileLocation; + +const DEFAULT_LOCATION: Option<FileLocation> = None; + +#[derive(Debug)] +struct Settings { + location: Option<FileLocation>, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + location: DEFAULT_LOCATION, + } + } +} + +static PROPERTIES: [subclass::Property; 1] = [subclass::Property("location", |name| { + glib::ParamSpec::string( + name, + "File Location", + "Location of the file to write", + None, + glib::ParamFlags::READWRITE, + ) +})]; + +enum State { + Stopped, + Started { file: File, position: u64 }, +} + +impl Default for State { + fn default() -> State { + State::Stopped + } +} + +pub struct FileSink { + settings: Mutex<Settings>, + state: Mutex<State>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "rsfilesink", + gst::DebugColorFlags::empty(), + Some("File Sink"), + ); +} + +impl FileSink { + fn set_location( + &self, + element: &gst_base::BaseSink, + location: Option<FileLocation>, + ) -> Result<(), glib::Error> { + let state = self.state.lock().unwrap(); + if let State::Started { .. } = *state { + return Err(glib::Error::new( + gst::URIError::BadState, + "Changing the `location` property on a started `filesink` is not supported", + )); + } + + let mut settings = self.settings.lock().unwrap(); + settings.location = match location { + Some(location) => { + match settings.location { + Some(ref location_cur) => { + gst_info!( + CAT, + obj: element, + "Changing `location` from {:?} to {}", + location_cur, + location, + ); + } + None => { + gst_info!(CAT, obj: element, "Setting `location` to {}", location,); + } + } + Some(location) + } + None => { + gst_info!(CAT, obj: element, "Resetting `location` to None",); + None + } + }; + + Ok(()) + } +} + +impl ObjectSubclass for FileSink { + const NAME: &'static str = "RsFileSink"; + type ParentType = gst_base::BaseSink; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new() -> Self { + Self { + settings: Mutex::new(Default::default()), + state: Mutex::new(Default::default()), + } + } + + fn type_init(type_: &mut subclass::InitializingType<Self>) { + type_.add_interface::<gst::URIHandler>(); + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "File Sink", + "Sink/File", + "Write stream to a file", + "François Laignel <fengalin@free.fr>, Luis de Bethencourt <luisbg@osg.samsung.com>", + ); + + let caps = gst::Caps::new_any(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for FileSink { + glib_object_impl!(); + + fn set_property(&self, obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + match *prop { + subclass::Property("location", ..) => { + let element = obj.downcast_ref::<gst_base::BaseSink>().unwrap(); + + let res = match value.get::<String>() { + Ok(Some(location)) => FileLocation::try_from_path_str(location) + .and_then(|file_location| self.set_location(&element, Some(file_location))), + Ok(None) => self.set_location(&element, None), + Err(_) => unreachable!("type checked upstream"), + }; + + if let Err(err) = res { + gst_error!( + CAT, + obj: element, + "Failed to set property `location`: {}", + err + ); + } + } + _ => unimplemented!(), + }; + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + match *prop { + subclass::Property("location", ..) => { + let settings = self.settings.lock().unwrap(); + let location = settings + .location + .as_ref() + .map(|location| location.to_string()); + + Ok(location.to_value()) + } + _ => unimplemented!(), + } + } +} + +impl ElementImpl for FileSink {} + +impl BaseSinkImpl for FileSink { + fn start(&self, element: &gst_base::BaseSink) -> Result<(), gst::ErrorMessage> { + let mut state = self.state.lock().unwrap(); + if let State::Started { .. } = *state { + unreachable!("FileSink already started"); + } + + let settings = self.settings.lock().unwrap(); + let location = settings.location.as_ref().ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::Settings, + ["File location is not defined"] + ) + })?; + + let file = File::create(location).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + [ + "Could not open file {} for writing: {}", + location, + err.to_string(), + ] + ) + })?; + gst_debug!(CAT, obj: element, "Opened file {:?}", file); + + *state = State::Started { file, position: 0 }; + gst_info!(CAT, obj: element, "Started"); + + Ok(()) + } + + fn stop(&self, element: &gst_base::BaseSink) -> Result<(), gst::ErrorMessage> { + let mut state = self.state.lock().unwrap(); + if let State::Stopped = *state { + return Err(gst_error_msg!( + gst::ResourceError::Settings, + ["FileSink not started"] + )); + } + + *state = State::Stopped; + gst_info!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + // TODO: implement seek in BYTES format + + fn render( + &self, + element: &gst_base::BaseSink, + buffer: &gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let mut state = self.state.lock().unwrap(); + let (file, position) = match *state { + State::Started { + ref mut file, + ref mut position, + } => (file, position), + State::Stopped => { + gst_element_error!(element, gst::CoreError::Failed, ["Not started yet"]); + return Err(gst::FlowError::Error); + } + }; + + gst_trace!(CAT, obj: element, "Rendering {:?}", buffer); + let map = buffer.map_readable().map_err(|_| { + gst_element_error!(element, gst::CoreError::Failed, ["Failed to map buffer"]); + gst::FlowError::Error + })?; + + file.write_all(map.as_ref()).map_err(|err| { + gst_element_error!( + element, + gst::ResourceError::Write, + ["Failed to write buffer: {}", err] + ); + gst::FlowError::Error + })?; + + *position += map.len() as u64; + + Ok(gst::FlowSuccess::Ok) + } +} + +impl URIHandlerImpl for FileSink { + fn get_uri(&self, _element: &gst::URIHandler) -> Option<String> { + let settings = self.settings.lock().unwrap(); + + // Conversion to Url already checked while building the `FileLocation` + settings.location.as_ref().map(|location| { + Url::from_file_path(location) + .expect("FileSink::get_uri couldn't build `Url` from `location`") + .into_string() + }) + } + + fn set_uri(&self, element: &gst::URIHandler, uri: &str) -> Result<(), glib::Error> { + let element = element.dynamic_cast_ref::<gst_base::BaseSink>().unwrap(); + + // Special case for "file://" as this is used by some applications to test + // with `gst_element_make_from_uri` if there's an element that supports the URI protocol + + if uri != "file://" { + let file_location = FileLocation::try_from_uri_str(uri)?; + self.set_location(&element, Some(file_location)) + } else { + Ok(()) + } + } + + fn get_uri_type() -> gst::URIType { + gst::URIType::Sink + } + + fn get_protocols() -> Vec<String> { + vec!["file".to_string()] + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "rsfilesink", + gst::Rank::None, + FileSink::get_type(), + ) +} diff --git a/generic/gst-plugin-file/src/filesrc.rs b/generic/gst-plugin-file/src/filesrc.rs new file mode 100644 index 000000000..7d7b9f553 --- /dev/null +++ b/generic/gst-plugin-file/src/filesrc.rs @@ -0,0 +1,393 @@ +// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com> +// 2018 François Laignel <fengalin@free.fr> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst_base; +use gst_base::prelude::*; +use gst_base::subclass::prelude::*; + +use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::sync::Mutex; + +use url::Url; + +use file_location::FileLocation; + +const DEFAULT_LOCATION: Option<FileLocation> = None; + +#[derive(Debug)] +struct Settings { + location: Option<FileLocation>, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + location: DEFAULT_LOCATION, + } + } +} + +static PROPERTIES: [subclass::Property; 1] = [subclass::Property("location", |name| { + glib::ParamSpec::string( + name, + "File Location", + "Location of the file to read from", + None, + glib::ParamFlags::READWRITE, + ) +})]; + +enum State { + Stopped, + Started { file: File, position: u64 }, +} + +impl Default for State { + fn default() -> State { + State::Stopped + } +} + +pub struct FileSrc { + settings: Mutex<Settings>, + state: Mutex<State>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "rsfilesrc", + gst::DebugColorFlags::empty(), + Some("File Source"), + ); +} + +impl FileSrc { + fn set_location( + &self, + element: &gst_base::BaseSrc, + location: Option<FileLocation>, + ) -> Result<(), glib::Error> { + let state = self.state.lock().unwrap(); + if let State::Started { .. } = *state { + return Err(glib::Error::new( + gst::URIError::BadState, + "Changing the `location` property on a started `filesrc` is not supported", + )); + } + + let mut settings = self.settings.lock().unwrap(); + settings.location = match location { + Some(location) => { + if !location.exists() { + return Err(glib::Error::new( + gst::URIError::BadReference, + format!("{} doesn't exist", location).as_str(), + )); + } + + if !location.is_file() { + return Err(glib::Error::new( + gst::URIError::BadReference, + format!("{} is not a file", location).as_str(), + )); + } + + match settings.location { + Some(ref location_cur) => { + gst_info!( + CAT, + obj: element, + "Changing `location` from {:?} to {}", + location_cur, + location, + ); + } + None => { + gst_info!(CAT, obj: element, "Setting `location to {}", location,); + } + } + Some(location) + } + None => { + gst_info!(CAT, obj: element, "Resetting `location` to None",); + None + } + }; + + Ok(()) + } +} + +impl ObjectSubclass for FileSrc { + const NAME: &'static str = "RsFileSrc"; + type ParentType = gst_base::BaseSrc; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new() -> Self { + Self { + settings: Mutex::new(Default::default()), + state: Mutex::new(Default::default()), + } + } + + fn type_init(type_: &mut subclass::InitializingType<Self>) { + type_.add_interface::<gst::URIHandler>(); + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "File Source", + "Source/File", + "Read stream from a file", + "François Laignel <fengalin@free.fr>, Sebastian Dröge <sebastian@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for FileSrc { + glib_object_impl!(); + + fn set_property(&self, obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + match *prop { + subclass::Property("location", ..) => { + let element = obj.downcast_ref::<gst_base::BaseSrc>().unwrap(); + + let res = match value.get::<String>() { + Ok(Some(location)) => FileLocation::try_from_path_str(location) + .and_then(|file_location| self.set_location(&element, Some(file_location))), + Ok(None) => self.set_location(&element, None), + Err(_) => unreachable!("type checked upstream"), + }; + + if let Err(err) = res { + gst_error!( + CAT, + obj: element, + "Failed to set property `location`: {}", + err + ); + } + } + _ => unimplemented!(), + }; + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + match *prop { + subclass::Property("location", ..) => { + let settings = self.settings.lock().unwrap(); + let location = settings + .location + .as_ref() + .map(|location| location.to_string()); + + Ok(location.to_value()) + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst_base::BaseSrc>().unwrap(); + element.set_format(gst::Format::Bytes); + } +} + +impl ElementImpl for FileSrc {} + +impl BaseSrcImpl for FileSrc { + fn is_seekable(&self, _src: &gst_base::BaseSrc) -> bool { + true + } + + fn get_size(&self, _src: &gst_base::BaseSrc) -> Option<u64> { + let state = self.state.lock().unwrap(); + if let State::Started { ref file, .. } = *state { + file.metadata().ok().map(|m| m.len()) + } else { + None + } + } + + fn start(&self, element: &gst_base::BaseSrc) -> Result<(), gst::ErrorMessage> { + let mut state = self.state.lock().unwrap(); + if let State::Started { .. } = *state { + unreachable!("FileSrc already started"); + } + + let settings = self.settings.lock().unwrap(); + let location = settings.location.as_ref().ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::Settings, + ["File location is not defined"] + ) + })?; + + let file = File::open(location).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + [ + "Could not open file {} for reading: {}", + location, + err.to_string(), + ] + ) + })?; + + gst_debug!(CAT, obj: element, "Opened file {:?}", file); + + *state = State::Started { file, position: 0 }; + + gst_info!(CAT, obj: element, "Started"); + + Ok(()) + } + + fn stop(&self, element: &gst_base::BaseSrc) -> Result<(), gst::ErrorMessage> { + let mut state = self.state.lock().unwrap(); + if let State::Stopped = *state { + return Err(gst_error_msg!( + gst::ResourceError::Settings, + ["FileSrc not started"] + )); + } + + *state = State::Stopped; + + gst_info!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn fill( + &self, + element: &gst_base::BaseSrc, + offset: u64, + _length: u32, + buffer: &mut gst::BufferRef, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let mut state = self.state.lock().unwrap(); + + let (file, position) = match *state { + State::Started { + ref mut file, + ref mut position, + } => (file, position), + State::Stopped => { + gst_element_error!(element, gst::CoreError::Failed, ["Not started yet"]); + return Err(gst::FlowError::Error); + } + }; + + if *position != offset { + file.seek(SeekFrom::Start(offset)).map_err(|err| { + gst_element_error!( + element, + gst::LibraryError::Failed, + ["Failed to seek to {}: {}", offset, err.to_string()] + ); + gst::FlowError::Error + })?; + + *position = offset; + } + + let size = { + let mut map = buffer.map_writable().map_err(|_| { + gst_element_error!(element, gst::LibraryError::Failed, ["Failed to map buffer"]); + gst::FlowError::Error + })?; + + file.read(map.as_mut()).map_err(|err| { + gst_element_error!( + element, + gst::LibraryError::Failed, + ["Failed to read at {}: {}", offset, err.to_string()] + ); + gst::FlowError::Error + })? + }; + + *position += size as u64; + + buffer.set_size(size); + + Ok(gst::FlowSuccess::Ok) + } +} + +impl URIHandlerImpl for FileSrc { + fn get_uri(&self, _element: &gst::URIHandler) -> Option<String> { + let settings = self.settings.lock().unwrap(); + + // Conversion to Url already checked while building the `FileLocation` + settings.location.as_ref().map(|location| { + Url::from_file_path(location) + .expect("FileSrc::get_uri couldn't build `Url` from `location`") + .into_string() + }) + } + + fn set_uri(&self, element: &gst::URIHandler, uri: &str) -> Result<(), glib::Error> { + let element = element.dynamic_cast_ref::<gst_base::BaseSrc>().unwrap(); + + // Special case for "file://" as this is used by some applications to test + // with `gst_element_make_from_uri` if there's an element that supports the URI protocol + + if uri != "file://" { + let file_location = FileLocation::try_from_uri_str(uri)?; + self.set_location(&element, Some(file_location)) + } else { + Ok(()) + } + } + + fn get_uri_type() -> gst::URIType { + gst::URIType::Src + } + + fn get_protocols() -> Vec<String> { + vec!["file".to_string()] + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "rsfilesrc", + gst::Rank::None, + FileSrc::get_type(), + ) +} diff --git a/generic/gst-plugin-file/src/lib.rs b/generic/gst-plugin-file/src/lib.rs new file mode 100644 index 000000000..90616e87b --- /dev/null +++ b/generic/gst-plugin-file/src/lib.rs @@ -0,0 +1,40 @@ +// Copyright (C) 2016-2017 Sebastian Dröge <sebastian@centricular.com> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![crate_type = "cdylib"] + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gstreamer as gst; +extern crate gstreamer_base as gst_base; +extern crate url; +#[macro_use] +extern crate lazy_static; + +mod file_location; +mod filesink; +mod filesrc; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + filesink::register(plugin)?; + filesrc::register(plugin)?; + Ok(()) +} + +gst_plugin_define!( + rsfile, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MIT/X11", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/generic/gst-plugin-sodium/Cargo.toml b/generic/gst-plugin-sodium/Cargo.toml new file mode 100644 index 000000000..85f44983a --- /dev/null +++ b/generic/gst-plugin-sodium/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "gst-plugin-sodium" +version = "0.1.0" +authors = ["Jordan Petridis <jordan@centricular.com>"] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugin-rs" +description = "libsodium-based file encryption and decryption" +license = "MIT" +edition = "2018" + +[dependencies] +glib = { git = "https://github.com/gtk-rs/glib" } +gst = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"], package="gstreamer" } +gst-base = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"], package = "gstreamer-base" } +sodiumoxide = "0.2.1" +lazy_static = "1.3.0" +hex = "0.4" +smallvec = "1.0" + +# example +clap = { version = "2.33", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } + +[dev-dependencies] +pretty_assertions = "0.6" +rand = "0.7" + +[dev-dependencies.gst-check] +git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" +package="gstreamer-check" + +[dev-dependencies.gst-app] +git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" +package="gstreamer-app" + +[lib] +name = "gstsodium" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[[example]] +name = "generate-keys" +path = "examples/generate_keys.rs" +required-features = ["serde", "serde_json", "clap"] + +[[example]] +name = "encrypt-example" +path = "examples/encrypt_example.rs" +required-features = ["serde", "serde_json", "clap"] + +[[example]] +name = "decrypt-example" +path = "examples/decrypt_example.rs" +required-features = ["serde", "serde_json", "clap"] + +[build-dependencies] +gst-plugin-version-helper = { path="../../gst-plugin-version-helper" } diff --git a/generic/gst-plugin-sodium/build.rs b/generic/gst-plugin-sodium/build.rs new file mode 100644 index 000000000..0d1ddb61d --- /dev/null +++ b/generic/gst-plugin-sodium/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/generic/gst-plugin-sodium/examples/decrypt_example.rs b/generic/gst-plugin-sodium/examples/decrypt_example.rs new file mode 100644 index 000000000..9908a0fd3 --- /dev/null +++ b/generic/gst-plugin-sodium/examples/decrypt_example.rs @@ -0,0 +1,150 @@ +// decrypt_example.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use gst::prelude::*; +use sodiumoxide::crypto::box_; + +use std::error::Error; +use std::fs::File; +use std::path::PathBuf; + +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use serde_json; + +#[derive(Debug, Serialize, Deserialize)] +struct Keys { + public: box_::PublicKey, + private: box_::SecretKey, +} + +impl Keys { + fn from_file(file: &PathBuf) -> Result<Self, Box<dyn Error>> { + let f = File::open(&file)?; + serde_json::from_reader(f).map_err(From::from) + } +} + +fn main() -> Result<(), Box<dyn Error>> { + let matches = App::new("Decrypt a gstsodium10 file.") + .version("1.0") + .author("Jordan Petridis <jordan@centricular.com>") + .arg( + Arg::with_name("input") + .short("i") + .long("input") + .value_name("FILE") + .help("File to encrypt") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("output") + .short("o") + .long("output") + .value_name("FILE") + .help("File to decrypt") + .required(true) + .takes_value(true), + ) + .get_matches(); + + gst::init()?; + gstsodium::plugin_register_static().expect("Failed to register sodium plugin"); + + let input_loc = matches.value_of("input").unwrap(); + let out_loc = matches.value_of("output").unwrap(); + + let receiver_keys = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("examples"); + r.push("receiver_sample"); + r.set_extension("json"); + r + }; + + let sender_keys = { + let mut s = PathBuf::new(); + s.push(env!("CARGO_MANIFEST_DIR")); + s.push("examples"); + s.push("sender_sample"); + s.set_extension("json"); + s + }; + + let receiver = &Keys::from_file(&receiver_keys)?; + let sender = &Keys::from_file(&sender_keys)?; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + let decrypter = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + let typefind = gst::ElementFactory::make("typefind", None).unwrap(); + let filesink = gst::ElementFactory::make("filesink", None).unwrap(); + + filesrc + .set_property("location", &input_loc) + .expect("Failed to set location property"); + filesink + .set_property("location", &out_loc) + .expect("Failed to set location property"); + + decrypter.set_property("receiver-key", &glib::Bytes::from_owned(receiver.private.0))?; + decrypter.set_property("sender-key", &glib::Bytes::from_owned(sender.public))?; + + let pipeline = gst::Pipeline::new(Some("test-pipeline")); + pipeline + .add_many(&[&filesrc, &decrypter, &typefind, &filesink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &decrypter, &typefind, &filesink]) + .expect("failed to link the elements"); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + break; + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Playing` state"); + + Ok(()) +} diff --git a/generic/gst-plugin-sodium/examples/encrypt_example.rs b/generic/gst-plugin-sodium/examples/encrypt_example.rs new file mode 100644 index 000000000..6189e3242 --- /dev/null +++ b/generic/gst-plugin-sodium/examples/encrypt_example.rs @@ -0,0 +1,145 @@ +// encrypt_example.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use gst::prelude::*; +use sodiumoxide::crypto::box_; + +use std::error::Error; +use std::fs::File; +use std::path::PathBuf; + +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use serde_json; + +#[derive(Debug, Serialize, Deserialize)] +struct Keys { + public: box_::PublicKey, + private: box_::SecretKey, +} + +impl Keys { + fn from_file(file: &PathBuf) -> Result<Self, Box<dyn Error>> { + let f = File::open(&file)?; + serde_json::from_reader(f).map_err(From::from) + } +} + +fn main() -> Result<(), Box<dyn Error>> { + let matches = App::new("Encrypt a file with in the gstsodium10 format") + .version("1.0") + .author("Jordan Petridis <jordan@centricular.com>") + .arg( + Arg::with_name("input") + .short("i") + .long("input") + .value_name("FILE") + .help("File to encrypt") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("output") + .short("o") + .long("output") + .value_name("FILE") + .help("File to decrypt") + .required(true) + .takes_value(true), + ) + .get_matches(); + + gst::init()?; + gstsodium::plugin_register_static().expect("Failed to register sodium plugin"); + + let input_loc = matches.value_of("input").unwrap(); + let out_loc = matches.value_of("output").unwrap(); + + let receiver_keys = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("examples"); + r.push("receiver_sample"); + r.set_extension("json"); + r + }; + + let sender_keys = { + let mut s = PathBuf::new(); + s.push(env!("CARGO_MANIFEST_DIR")); + s.push("examples"); + s.push("sender_sample"); + s.set_extension("json"); + s + }; + + let receiver = &Keys::from_file(&receiver_keys)?; + let sender = &Keys::from_file(&sender_keys)?; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + let encrypter = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + let filesink = gst::ElementFactory::make("filesink", None).unwrap(); + + filesrc + .set_property("location", &input_loc) + .expect("Failed to set location property"); + filesink + .set_property("location", &out_loc) + .expect("Failed to set location property"); + + encrypter.set_property("receiver-key", &glib::Bytes::from_owned(receiver.public))?; + encrypter.set_property("sender-key", &glib::Bytes::from_owned(sender.private.0))?; + + let pipeline = gst::Pipeline::new(Some("test-pipeline")); + pipeline + .add_many(&[&filesrc, &encrypter, &filesink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &encrypter, &filesink]) + .expect("failed to link the elements"); + + pipeline.set_state(gst::State::Playing)?; + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + break; + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline.set_state(gst::State::Null)?; + + Ok(()) +} diff --git a/generic/gst-plugin-sodium/examples/generate_keys.rs b/generic/gst-plugin-sodium/examples/generate_keys.rs new file mode 100644 index 000000000..b7de47772 --- /dev/null +++ b/generic/gst-plugin-sodium/examples/generate_keys.rs @@ -0,0 +1,102 @@ +// generate_keys.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use clap::{App, Arg}; +use serde::{Deserialize, Serialize}; +use serde_json; +use sodiumoxide::crypto::box_; +use std::fs::File; + +#[derive(Debug, Serialize, Deserialize)] +struct Keys { + public: box_::PublicKey, + private: box_::SecretKey, +} + +impl Keys { + fn new() -> Self { + let (public, private) = box_::gen_keypair(); + Keys { public, private } + } + + fn write_to_file(&self, path: &str, json: bool) { + if json { + let path = if !path.ends_with(".json") { + format!("{}.json", path) + } else { + path.into() + }; + + let file = + File::create(&path).unwrap_or_else(|_| panic!("Failed to create file at {}", path)); + serde_json::to_writer(file, &self) + .unwrap_or_else(|_| panic!("Failed to write to file at {}", path)); + } else { + use std::io::Write; + use std::path::PathBuf; + + let mut private = PathBuf::from(path); + private.set_extension("prv"); + let mut file = File::create(&private) + .unwrap_or_else(|_| panic!("Failed to create file at {}", private.display())); + file.write_all(&self.private.0) + .unwrap_or_else(|_| panic!("Failed to write to file at {}", private.display())); + + let mut public = PathBuf::from(path); + public.set_extension("pub"); + let mut file = File::create(&public) + .unwrap_or_else(|_| panic!("Failed to create file at {}", public.display())); + file.write_all(self.public.as_ref()) + .unwrap_or_else(|_| panic!("Failed to write to file at {}", public.display())); + } + } +} + +fn main() { + let matches = App::new("Generate the keys to be used with the sodium element") + .version("1.0") + .author("Jordan Petridis <jordan@centricular.com>") + .about("Generate a pair of Sodium's crypto_box_curve25519xsalsa20poly1305 keys.") + .arg( + Arg::with_name("path") + .long("path") + .short("p") + .value_name("FILE") + .help("Path to write the Keys") + .required(true) + .takes_value(true), + ) + .arg( + Arg::with_name("json") + .long("json") + .short("j") + .help("Write a JSON file instead of a key.prv/key.pub pair"), + ) + .get_matches(); + + let keys = Keys::new(); + + let path = matches.value_of("path").unwrap(); + keys.write_to_file(path, matches.is_present("json")); +} diff --git a/generic/gst-plugin-sodium/examples/receiver_sample.json b/generic/gst-plugin-sodium/examples/receiver_sample.json new file mode 100644 index 000000000..619b365f8 --- /dev/null +++ b/generic/gst-plugin-sodium/examples/receiver_sample.json @@ -0,0 +1,2 @@ + +{"public":[28,95,33,124,28,103,80,78,7,28,234,40,226,179,253,166,169,64,78,5,57,92,151,179,221,89,68,70,44,225,219,19],"private":[54,221,217,54,94,235,167,2,187,249,71,31,59,27,19,166,78,236,102,48,29,142,41,189,22,146,218,69,147,165,240,235]} diff --git a/generic/gst-plugin-sodium/examples/sender_sample.json b/generic/gst-plugin-sodium/examples/sender_sample.json new file mode 100644 index 000000000..d7ab1fa6c --- /dev/null +++ b/generic/gst-plugin-sodium/examples/sender_sample.json @@ -0,0 +1 @@ +{"public":[66,248,199,74,216,55,228,116,52,17,147,56,65,130,134,148,157,153,235,171,179,147,120,71,100,243,133,120,160,14,111,65],"private":[154,227,90,239,206,184,202,234,176,161,14,91,218,98,142,13,145,223,210,222,224,240,98,51,142,165,255,1,159,100,242,162]} diff --git a/generic/gst-plugin-sodium/src/decrypter.rs b/generic/gst-plugin-sodium/src/decrypter.rs new file mode 100644 index 000000000..39891ee17 --- /dev/null +++ b/generic/gst-plugin-sodium/src/decrypter.rs @@ -0,0 +1,733 @@ +// decrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use gst::prelude::*; +use gst::subclass::prelude::*; +use sodiumoxide::crypto::box_; + +use std::sync::Mutex; + +lazy_static! { + static ref CAT: gst::DebugCategory = { + gst::DebugCategory::new( + "sodiumdecrypter", + gst::DebugColorFlags::empty(), + Some("Decrypter Element"), + ) + }; +} + +static PROPERTIES: [subclass::Property; 2] = [ + subclass::Property("receiver-key", |name| { + glib::ParamSpec::boxed( + name, + "Receiver Key", + "The private key of the Reeiver", + glib::Bytes::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("sender-key", |name| { + glib::ParamSpec::boxed( + name, + "Sender Key", + "The public key of the Sender", + glib::Bytes::static_type(), + glib::ParamFlags::WRITABLE, + ) + }), +]; + +#[derive(Debug, Clone, Default)] +struct Props { + receiver_key: Option<glib::Bytes>, + sender_key: Option<glib::Bytes>, +} + +#[derive(Debug)] +struct State { + adapter: gst_base::UniqueAdapter, + initial_nonce: Option<box_::Nonce>, + precomputed_key: box_::PrecomputedKey, + block_size: Option<u32>, +} + +impl State { + fn from_props(props: &Props) -> Result<Self, gst::ErrorMessage> { + let sender_key = props + .sender_key + .as_ref() + .and_then(|k| box_::PublicKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Sender's Key from property: {:?}", + props.sender_key + ) + .as_ref()] + ) + })?; + + let receiver_key = props + .receiver_key + .as_ref() + .and_then(|k| box_::SecretKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Receiver's Key from property: {:?}", + props.receiver_key + ) + .as_ref()] + ) + })?; + + let precomputed_key = box_::precompute(&sender_key, &receiver_key); + + Ok(Self { + adapter: gst_base::UniqueAdapter::new(), + precomputed_key, + initial_nonce: None, + block_size: None, + }) + } + + // Split the buffer into N(`chunk_index`) chunks of `block_size`, + // decrypt them, and push them to the internal adapter for further + // retrieval + fn decrypt_into_adapter( + &mut self, + element: &gst::Element, + pad: &gst::Pad, + buffer: &gst::Buffer, + chunk_index: u64, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let map = buffer.map_readable().map_err(|_| { + gst_element_error!( + element, + gst::StreamError::Format, + ["Failed to map buffer readable"] + ); + + gst::FlowError::Error + })?; + + gst_debug!(CAT, obj: pad, "Returned pull size: {}", map.len()); + + let mut nonce = add_nonce(self.initial_nonce.unwrap(), chunk_index); + let block_size = self.block_size.expect("Block size wasn't set") as usize + box_::MACBYTES; + + for subbuffer in map.chunks(block_size) { + let plain = box_::open_precomputed(&subbuffer, &nonce, &self.precomputed_key).map_err( + |_| { + gst_element_error!( + element, + gst::StreamError::Format, + ["Failed to decrypt buffer"] + ); + gst::FlowError::Error + }, + )?; + // assumes little endian + nonce.increment_le_inplace(); + self.adapter.push(gst::Buffer::from_mut_slice(plain)); + } + + Ok(gst::FlowSuccess::Ok) + } + + // Retrieve the requested buffer out of the adapter. + fn get_requested_buffer( + &mut self, + pad: &gst::Pad, + buffer: Option<&mut gst::BufferRef>, + requested_size: u32, + adapter_offset: usize, + ) -> Result<gst::PadGetRangeSuccess, gst::FlowError> { + let avail = self.adapter.available(); + gst_debug!(CAT, obj: pad, "Avail: {}", avail); + gst_debug!(CAT, obj: pad, "Adapter offset: {}", adapter_offset); + + // if this underflows, the available buffer in the adapter is smaller than the + // requested offset, which means we have reached EOS + let available_buffer = avail + .checked_sub(adapter_offset) + .ok_or(gst::FlowError::Eos)?; + + // if the available buffer size is smaller than the requested, it's a short + // read and return that. Else return the requested size + let available_size = if available_buffer <= requested_size as usize { + available_buffer + } else { + requested_size as usize + }; + + if available_size == 0 { + self.adapter.clear(); + + // if the requested buffer was 0 sized, return an + // empty buffer + if requested_size == 0 { + if let Some(buffer) = buffer { + buffer.set_size(0); + return Ok(gst::PadGetRangeSuccess::FilledBuffer); + } else { + return Ok(gst::PadGetRangeSuccess::NewBuffer(gst::Buffer::new())); + } + } + + return Err(gst::FlowError::Eos); + } + + // discard what we don't need + assert!(self.adapter.available() >= adapter_offset); + self.adapter.flush(adapter_offset); + + assert!(self.adapter.available() >= available_size); + let res = if let Some(buffer) = buffer { + let mut map = match buffer.map_writable() { + Ok(map) => map, + Err(_) => { + gst_error!(CAT, obj: pad, "Failed to map provided buffer writable"); + return Err(gst::FlowError::Error); + } + }; + self.adapter.copy(0, &mut map[..available_size]); + if map.len() != available_size { + drop(map); + buffer.set_size(available_size); + } + gst::PadGetRangeSuccess::FilledBuffer + } else { + let buffer = self + .adapter + .take_buffer(available_size) + .expect("Failed to get buffer from adapter"); + gst::PadGetRangeSuccess::NewBuffer(buffer) + }; + + // Cleanup the adapter + self.adapter.clear(); + + Ok(res) + } +} + +/// Calculate the nonce of a block based on the initial nonce +/// and the block index in the stream. +/// +/// This is a faster way of doing `(0..chunk_index).for_each(|_| nonce.increment_le_inplace());` +fn add_nonce(initial_nonce: box_::Nonce, chunk_index: u64) -> box_::Nonce { + let mut nonce = initial_nonce.0; + // convert our index to a bytes array + // add padding so our 8byte array of the chunk_index will have an + // equal length with the nonce, padding at the end cause little endian + let idx = chunk_index.to_le_bytes(); + let idx = &[ + idx[0], idx[1], idx[2], idx[3], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + assert_eq!(idx.len(), box_::NONCEBYTES); + + // add the chunk index to the nonce + sodiumoxide::utils::add_le(&mut nonce, idx).expect("Failed to calculate the nonce"); + + // construct back a nonce from our custom array + box_::Nonce::from_slice(&nonce).expect("Failed to convert slice back to Nonce") +} + +struct Decrypter { + srcpad: gst::Pad, + sinkpad: gst::Pad, + props: Mutex<Props>, + state: Mutex<Option<State>>, +} + +impl Decrypter { + fn set_pad_functions(_sinkpad: &gst::Pad, srcpad: &gst::Pad) { + srcpad.set_getrange_function(|pad, parent, offset, buffer, size| { + Decrypter::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |decrypter, element| decrypter.get_range(pad, element, offset, buffer, size), + ) + }); + + srcpad.set_activatemode_function(|pad, parent, mode, active| { + Decrypter::catch_panic_pad_function( + parent, + || { + Err(gst_loggable_error!( + CAT, + "Panic activating srcpad with mode" + )) + }, + |decrypter, element| { + decrypter.src_activatemode_function(pad, element, mode, active) + }, + ) + }); + + srcpad.set_query_function(|pad, parent, query| { + Decrypter::catch_panic_pad_function( + parent, + || false, + |decrypter, element| decrypter.src_query(pad, element, query), + ) + }); + } + + fn src_activatemode_function( + &self, + _pad: &gst::Pad, + element: &gst::Element, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError> { + match mode { + gst::PadMode::Pull => { + self.sinkpad + .activate_mode(mode, active) + .map_err(gst::LoggableError::from)?; + + // Set the nonce and block size from the headers + // right after we activate the pad + self.check_headers(element) + } + gst::PadMode::Push => Err(gst_loggable_error!(CAT, "Push mode not supported")), + _ => Err(gst_loggable_error!( + CAT, + "Failed to activate the pad in Unknown mode, {:?}", + mode + )), + } + } + + fn src_query(&self, pad: &gst::Pad, element: &gst::Element, query: &mut gst::QueryRef) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + + match query.view_mut() { + QueryView::Scheduling(mut q) => { + let mut peer_query = gst::Query::new_scheduling(); + let res = self.sinkpad.peer_query(&mut peer_query); + if !res { + return res; + } + + gst_log!(CAT, obj: pad, "Upstream returned {:?}", peer_query); + + let (flags, min, max, align) = peer_query.get_result(); + q.set(flags, min, max, align); + q.add_scheduling_modes(&[gst::PadMode::Pull]); + gst_log!(CAT, obj: pad, "Returning {:?}", q.get_mut_query()); + true + } + QueryView::Duration(ref mut q) => { + use std::convert::TryInto; + + if q.get_format() != gst::Format::Bytes { + return pad.query_default(Some(element), query); + } + + /* First let's query the bytes duration upstream */ + let mut peer_query = gst::query::Query::new_duration(gst::Format::Bytes); + + if !self.sinkpad.peer_query(&mut peer_query) { + gst_error!(CAT, "Failed to query upstream duration"); + return false; + } + + let size = match peer_query.get_result().try_into().unwrap() { + gst::format::Bytes(Some(size)) => size, + gst::format::Bytes(None) => { + gst_error!(CAT, "Failed to query upstream duration"); + + return false; + } + }; + + let state = self.state.lock().unwrap(); + let state = match state.as_ref() { + // If state isn't set, it means that the + // element hasn't been activated yet. + None => return false, + Some(s) => s, + }; + + // subtract static offsets + let size = size - super::HEADERS_SIZE as u64; + + // calculate the number of chunks that exist in the stream + let total_chunks = + (size - 1) / state.block_size.expect("Block size wasn't set") as u64; + // subtrack the MAC of each block + let size = size - total_chunks * box_::MACBYTES as u64; + + gst_debug!(CAT, obj: pad, "Setting duration bytes: {}", size); + q.set(gst::format::Bytes::from(size)); + + true + } + _ => pad.query_default(Some(element), query), + } + } + + fn check_headers(&self, element: &gst::Element) -> Result<(), gst::LoggableError> { + let is_none = { + let mutex_state = self.state.lock().unwrap(); + let state = mutex_state.as_ref().unwrap(); + state.initial_nonce.is_none() + }; + + if !is_none { + return Ok(()); + } + + let buffer = self + .sinkpad + .pull_range(0, crate::HEADERS_SIZE as u32) + .map_err(|err| { + let err = gst_loggable_error!( + CAT, + "Failed to pull nonce from the stream, reason: {:?}", + err + ); + err.log_with_object(element); + err + })?; + + if buffer.get_size() != crate::HEADERS_SIZE { + let err = gst_loggable_error!(CAT, "Headers buffer has wrong size"); + err.log_with_object(element); + return Err(err); + } + + let map = buffer.map_readable().map_err(|_| { + let err = gst_loggable_error!(CAT, "Failed to map buffer readable"); + err.log_with_object(element); + err + })?; + + let sodium_header_slice = &map[..crate::TYPEFIND_HEADER_SIZE]; + if sodium_header_slice != crate::TYPEFIND_HEADER { + let err = gst_loggable_error!(CAT, "Buffer has wrong typefind header"); + err.log_with_object(element); + return Err(err); + } + + let nonce_slice = + &map[crate::TYPEFIND_HEADER_SIZE..crate::TYPEFIND_HEADER_SIZE + box_::NONCEBYTES]; + assert_eq!(nonce_slice.len(), box_::NONCEBYTES); + let nonce = box_::Nonce::from_slice(nonce_slice).ok_or_else(|| { + let err = gst_loggable_error!(CAT, "Failed to create nonce from buffer"); + err.log_with_object(&self.srcpad); + err + })?; + + let slice = &map[crate::TYPEFIND_HEADER_SIZE + box_::NONCEBYTES..crate::HEADERS_SIZE]; + assert_eq!( + crate::HEADERS_SIZE - crate::TYPEFIND_HEADER_SIZE - box_::NONCEBYTES, + 4 + ); + let block_size = u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]); + + // reacquire the lock again to change the state + let mut state = self.state.lock().unwrap(); + let state = state.as_mut().unwrap(); + + state.initial_nonce = Some(nonce); + gst_debug!(CAT, obj: element, "Setting nonce to: {:?}", nonce.0); + state.block_size = Some(block_size); + gst_debug!(CAT, obj: element, "Setting block size to: {}", block_size); + + Ok(()) + } + + fn pull_requested_buffer( + &self, + pad: &gst::Pad, + element: &gst::Element, + requested_size: u32, + block_size: u32, + chunk_index: u64, + ) -> Result<gst::Buffer, gst::FlowError> { + let pull_offset = super::HEADERS_SIZE as u64 + + (chunk_index * block_size as u64) + + (chunk_index * box_::MACBYTES as u64); + + gst_debug!(CAT, obj: pad, "Pull offset: {}", pull_offset); + gst_debug!(CAT, obj: pad, "block size: {}", block_size); + + // calculate how many chunks are needed, if we need something like 3.2 + // round the number to 4 and cut the buffer afterwards. + let checked = requested_size.checked_add(block_size).ok_or_else(|| { + gst_element_error!( + element, + gst::LibraryError::Failed, + [ + "Addition overflow when adding requested pull size and block size: {} + {}", + requested_size, + block_size, + ] + ); + gst::FlowError::Error + })?; + + // Read at least one chunk in case 0 bytes were requested + let total_chunks = u32::max((checked - 1) / block_size, 1); + gst_debug!(CAT, obj: pad, "Blocks to be pulled: {}", total_chunks); + + // Pull a buffer of all the chunks we will need + let checked_size = total_chunks.checked_mul(block_size).ok_or_else(|| { + gst_element_error!( + element, + gst::LibraryError::Failed, + [ + "Overflowed trying to calculate the buffer size to pull: {} * {}", + total_chunks, + block_size, + ] + ); + gst::FlowError::Error + })?; + + let total_size = checked_size + (total_chunks * box_::MACBYTES as u32); + gst_debug!(CAT, obj: pad, "Requested pull size: {}", total_size); + + self.sinkpad.pull_range(pull_offset, total_size).map_err(|err| { + match err { + gst::FlowError::Flushing => { + gst_debug!(CAT, obj: &self.sinkpad, "Pausing after pulling buffer, reason: flushing"); + } + gst::FlowError::Eos => { + gst_debug!(CAT, obj: &self.sinkpad, "Eos"); + } + flow => { + gst_error!(CAT, obj: &self.sinkpad, "Failed to pull, reason: {:?}", flow); + } + }; + + err + }) + } + + fn get_range( + &self, + pad: &gst::Pad, + element: &gst::Element, + offset: u64, + buffer: Option<&mut gst::BufferRef>, + requested_size: u32, + ) -> Result<gst::PadGetRangeSuccess, gst::FlowError> { + let block_size = { + let mut mutex_state = self.state.lock().unwrap(); + // This will only be run after READY state, + // and will be guaranted to be initialized + let state = mutex_state.as_mut().unwrap(); + // Cleanup the adapter + state.adapter.clear(); + state.block_size.expect("Block size wasn't set") + }; + + gst_debug!(CAT, obj: pad, "Requested offset: {}", offset); + gst_debug!(CAT, obj: pad, "Requested size: {}", requested_size); + + let chunk_index = offset as u64 / block_size as u64; + gst_debug!(CAT, obj: pad, "Stream Block index: {}", chunk_index); + + let pull_offset = offset - (chunk_index * block_size as u64); + assert!(pull_offset <= std::u32::MAX as u64); + let pull_offset = pull_offset as u32; + + let pulled_buffer = self.pull_requested_buffer( + pad, + element, + requested_size + pull_offset, + block_size, + chunk_index, + )?; + + let mut state = self.state.lock().unwrap(); + // This will only be run after READY state, + // and will be guaranted to be initialized + let state = state.as_mut().unwrap(); + + state.decrypt_into_adapter(element, &self.srcpad, &pulled_buffer, chunk_index)?; + + let adapter_offset = pull_offset as usize; + state.get_requested_buffer(&self.srcpad, buffer, requested_size, adapter_offset) + } +} + +impl ObjectSubclass for Decrypter { + const NAME: &'static str = "RsSodiumDecryptor"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::new_from_template(&templ, Some("sink")); + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::new_from_template(&templ, Some("src")); + + Decrypter::set_pad_functions(&sinkpad, &srcpad); + let props = Mutex::new(Props::default()); + let state = Mutex::new(None); + + Self { + srcpad, + sinkpad, + props, + state, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Decrypter", + "Generic", + "libsodium-based file decrypter", + "Jordan Petridis <jordan@centricular.com>", + ); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &gst::Caps::new_any(), + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_caps = gst::Caps::builder("application/x-sodium-encrypted").build(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for Decrypter { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(&self.sinkpad).unwrap(); + element.add_pad(&self.srcpad).unwrap(); + } + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("sender-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.sender_key = value.get().expect("type checked upstream"); + } + + subclass::Property("receiver-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.receiver_key = value.get().expect("type checked upstream"); + } + + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("receiver-key", ..) => { + let props = self.props.lock().unwrap(); + Ok(props.receiver_key.to_value()) + } + + _ => unimplemented!(), + } + } +} + +impl ElementImpl for Decrypter { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_debug!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + let props = self.props.lock().unwrap().clone(); + + // Create an internal state struct from the provided properties or + // refuse to change state + let state_ = State::from_props(&props).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + + let mut state = self.state.lock().unwrap(); + *state = Some(state_); + } + gst::StateChange::ReadyToNull => { + let _ = self.state.lock().unwrap().take(); + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToNull { + let _ = self.state.lock().unwrap().take(); + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "sodiumdecrypter", + gst::Rank::None, + Decrypter::get_type(), + ) +} diff --git a/generic/gst-plugin-sodium/src/encrypter.rs b/generic/gst-plugin-sodium/src/encrypter.rs new file mode 100644 index 000000000..60a6e01e8 --- /dev/null +++ b/generic/gst-plugin-sodium/src/encrypter.rs @@ -0,0 +1,572 @@ +// encrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use gst::prelude::*; +use gst::subclass::prelude::*; +use smallvec::SmallVec; +use sodiumoxide::crypto::box_; + +type BufferVec = SmallVec<[gst::Buffer; 16]>; + +use std::sync::Mutex; + +lazy_static! { + static ref CAT: gst::DebugCategory = { + gst::DebugCategory::new( + "sodiumencrypter", + gst::DebugColorFlags::empty(), + Some("Encrypter Element"), + ) + }; +} + +static PROPERTIES: [subclass::Property; 3] = [ + subclass::Property("receiver-key", |name| { + glib::ParamSpec::boxed( + name, + "Receiver Key", + "The public key of the Receiver", + glib::Bytes::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("sender-key", |name| { + glib::ParamSpec::boxed( + name, + "Sender Key", + "The private key of the Sender", + glib::Bytes::static_type(), + glib::ParamFlags::WRITABLE, + ) + }), + subclass::Property("block-size", |name| { + glib::ParamSpec::uint( + name, + "Block Size", + "The block-size of the chunks", + 1024, + std::u32::MAX, + 32768, + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Debug, Clone)] +struct Props { + receiver_key: Option<glib::Bytes>, + sender_key: Option<glib::Bytes>, + block_size: u32, +} + +impl Default for Props { + fn default() -> Self { + Props { + receiver_key: None, + sender_key: None, + block_size: 32768, + } + } +} + +#[derive(Debug)] +struct State { + adapter: gst_base::UniqueAdapter, + nonce: box_::Nonce, + precomputed_key: box_::PrecomputedKey, + block_size: u32, + write_headers: bool, +} + +impl State { + fn from_props(props: &Props) -> Result<Self, gst::ErrorMessage> { + let sender_key = props + .sender_key + .as_ref() + .and_then(|k| box_::SecretKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Sender's Key from property: {:?}", + props.sender_key + ) + .as_ref()] + ) + })?; + + let receiver_key = props + .receiver_key + .as_ref() + .and_then(|k| box_::PublicKey::from_slice(&k)) + .ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::NotFound, + [format!( + "Failed to set Receiver's Key from property: {:?}", + props.receiver_key + ) + .as_ref()] + ) + })?; + + // This env variable is only meant to bypass nonce regeneration during + // tests to get determinisic results. It should never be used outside + // of testing environments. + let nonce = if let Ok(val) = std::env::var("GST_SODIUM_ENCRYPT_NONCE") { + let bytes = hex::decode(val).expect("Failed to decode hex variable"); + assert_eq!(bytes.len(), box_::NONCEBYTES); + box_::Nonce::from_slice(&bytes).unwrap() + } else { + box_::gen_nonce() + }; + + let precomputed_key = box_::precompute(&receiver_key, &sender_key); + + Ok(Self { + adapter: gst_base::UniqueAdapter::new(), + precomputed_key, + nonce, + block_size: props.block_size, + write_headers: true, + }) + } + + fn seal(&mut self, message: &[u8]) -> Vec<u8> { + let ciphertext = box_::seal_precomputed(message, &self.nonce, &self.precomputed_key); + self.nonce.increment_le_inplace(); + ciphertext + } + + fn encrypt_message(&mut self, buffer: &gst::BufferRef) -> gst::Buffer { + let map = buffer + .map_readable() + .expect("Failed to map buffer readable"); + + let sealed = self.seal(&map); + gst::Buffer::from_mut_slice(sealed) + } + + fn encrypt_blocks(&mut self, block_size: usize) -> Result<BufferVec, gst::FlowError> { + assert_ne!(block_size, 0); + + let mut buffers = BufferVec::new(); + + // As long we have enough bytes to encrypt a block, or more, we do so + // else the leftover bytes on the adapter will be pushed when EOS + // is sent. + while self.adapter.available() >= block_size { + let buffer = self.adapter.take_buffer(block_size).unwrap(); + let out_buf = self.encrypt_message(&buffer); + + buffers.push(out_buf); + } + + Ok(buffers) + } +} + +struct Encrypter { + srcpad: gst::Pad, + sinkpad: gst::Pad, + props: Mutex<Props>, + state: Mutex<Option<State>>, +} + +impl Encrypter { + fn set_pad_functions(sinkpad: &gst::Pad, srcpad: &gst::Pad) { + sinkpad.set_chain_function(|pad, parent, buffer| { + Encrypter::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |encrypter, element| encrypter.sink_chain(pad, element, buffer), + ) + }); + sinkpad.set_event_function(|pad, parent, event| { + Encrypter::catch_panic_pad_function( + parent, + || false, + |encrypter, element| encrypter.sink_event(pad, element, event), + ) + }); + + srcpad.set_query_function(|pad, parent, query| { + Encrypter::catch_panic_pad_function( + parent, + || false, + |encrypter, element| encrypter.src_query(pad, element, query), + ) + }); + srcpad.set_event_function(|pad, parent, event| { + Encrypter::catch_panic_pad_function( + parent, + || false, + |encrypter, element| encrypter.src_event(pad, element, event), + ) + }); + } + + fn sink_chain( + &self, + pad: &gst::Pad, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + gst_log!(CAT, obj: pad, "Handling buffer {:?}", buffer); + + let mut buffers = BufferVec::new(); + let mut state_guard = self.state.lock().unwrap(); + let state = state_guard.as_mut().unwrap(); + + if state.write_headers { + let mut headers = Vec::with_capacity(40); + headers.extend_from_slice(crate::TYPEFIND_HEADER); + // Write the Nonce used into the stream. + headers.extend_from_slice(state.nonce.as_ref()); + // Write the block_size into the stream + headers.extend_from_slice(&state.block_size.to_le_bytes()); + + buffers.push(gst::Buffer::from_mut_slice(headers)); + state.write_headers = false; + } + + state.adapter.push(buffer); + + // Encrypt the whole blocks, if any, and push them. + buffers.extend( + state + .encrypt_blocks(state.block_size as usize) + .map_err(|err| { + // log the error to the bus + gst_element_error!( + element, + gst::ResourceError::Write, + ["Failed to decrypt buffer"] + ); + err + })?, + ); + + drop(state_guard); + + for buffer in buffers { + self.srcpad.push(buffer).map_err(|err| { + gst_error!(CAT, obj: element, "Failed to push buffer {:?}", err); + err + })?; + } + + Ok(gst::FlowSuccess::Ok) + } + + fn sink_event(&self, pad: &gst::Pad, element: &gst::Element, event: gst::Event) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + match event.view() { + EventView::Caps(_) => { + // We send our own caps downstream + let caps = gst::Caps::builder("application/x-sodium-encrypted").build(); + self.srcpad.push_event(gst::Event::new_caps(&caps).build()) + } + EventView::Eos(_) => { + let mut state_mutex = self.state.lock().unwrap(); + let mut buffers = BufferVec::new(); + // This will only be run after READY state, + // and will be guaranted to be initialized + let state = state_mutex.as_mut().unwrap(); + + // Now that all the full size blocks are pushed, drain the + // rest of the adapter and push whatever is left. + let avail = state.adapter.available(); + // logic error, all the complete blocks that can be pushed + // should have been done in the sink_chain call. + assert!(avail < state.block_size as usize); + + if avail > 0 { + match state.encrypt_blocks(avail) { + Err(_) => { + gst_element_error!( + element, + gst::ResourceError::Write, + ["Failed to encrypt buffers at EOS"] + ); + return false; + } + Ok(b) => buffers.extend(b), + } + } + + // drop the lock before pushing into the pad + drop(state_mutex); + + for buffer in buffers { + if let Err(err) = self.srcpad.push(buffer) { + gst_error!(CAT, obj: element, "Failed to push buffer at EOS {:?}", err); + return false; + } + } + + pad.event_default(Some(element), event) + } + _ => pad.event_default(Some(element), event), + } + } + + fn src_event(&self, pad: &gst::Pad, element: &gst::Element, event: gst::Event) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + match event.view() { + EventView::Seek(_) => false, + _ => pad.event_default(Some(element), event), + } + } + + fn src_query(&self, pad: &gst::Pad, element: &gst::Element, query: &mut gst::QueryRef) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + + match query.view_mut() { + QueryView::Seeking(mut q) => { + let format = q.get_format(); + q.set( + false, + gst::GenericFormattedValue::Other(format, -1), + gst::GenericFormattedValue::Other(format, -1), + ); + gst_log!(CAT, obj: pad, "Returning {:?}", q.get_mut_query()); + true + } + QueryView::Duration(ref mut q) => { + use std::convert::TryInto; + + if q.get_format() != gst::Format::Bytes { + return pad.query_default(Some(element), query); + } + + /* First let's query the bytes duration upstream */ + let mut peer_query = gst::query::Query::new_duration(gst::Format::Bytes); + + if !self.sinkpad.peer_query(&mut peer_query) { + gst_error!(CAT, "Failed to query upstream duration"); + return false; + } + + let size = match peer_query.get_result().try_into().unwrap() { + gst::format::Bytes(Some(size)) => size, + gst::format::Bytes(None) => { + gst_error!(CAT, "Failed to query upstream duration"); + + return false; + } + }; + + let state = self.state.lock().unwrap(); + let state = match state.as_ref() { + // If state isn't set, it means that the + // element hasn't been activated yet. + None => return false, + Some(s) => s, + }; + + // calculate the number of chunks that exist in the stream + let total_chunks = (size + state.block_size as u64 - 1) / state.block_size as u64; + // add the MAC of each block + let size = size + total_chunks * box_::MACBYTES as u64; + + // add static offsets + let size = size + super::HEADERS_SIZE as u64; + + gst_debug!(CAT, obj: pad, "Setting duration bytes: {}", size); + q.set(gst::format::Bytes::from(size)); + + true + } + _ => pad.query_default(Some(element), query), + } + } +} + +impl ObjectSubclass for Encrypter { + const NAME: &'static str = "RsSodiumEncrypter"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad = gst::Pad::new_from_template(&templ, Some("sink")); + let templ = klass.get_pad_template("src").unwrap(); + let srcpad = gst::Pad::new_from_template(&templ, Some("src")); + + Encrypter::set_pad_functions(&sinkpad, &srcpad); + let props = Mutex::new(Props::default()); + let state = Mutex::new(None); + + Self { + srcpad, + sinkpad, + props, + state, + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Encrypter", + "Generic", + "libsodium-based file encrypter", + "Jordan Petridis <jordan@centricular.com>", + ); + + let src_caps = gst::Caps::builder("application/x-sodium-encrypted").build(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &gst::Caps::new_any(), + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for Encrypter { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(&self.sinkpad).unwrap(); + element.add_pad(&self.srcpad).unwrap(); + } + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("sender-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.sender_key = value.get().expect("type checked upstream"); + } + + subclass::Property("receiver-key", ..) => { + let mut props = self.props.lock().unwrap(); + props.receiver_key = value.get().expect("type checked upstream"); + } + + subclass::Property("block-size", ..) => { + let mut props = self.props.lock().unwrap(); + props.block_size = value.get_some().expect("type checked upstream"); + } + + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("receiver-key", ..) => { + let props = self.props.lock().unwrap(); + Ok(props.receiver_key.to_value()) + } + + subclass::Property("block-size", ..) => { + let props = self.props.lock().unwrap(); + Ok(props.block_size.to_value()) + } + + _ => unimplemented!(), + } + } +} + +impl ElementImpl for Encrypter { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_debug!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + let props = self.props.lock().unwrap().clone(); + + // Create an internal state struct from the provided properties or + // refuse to change state + let state_ = State::from_props(&props).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + + let mut state = self.state.lock().unwrap(); + *state = Some(state_); + } + gst::StateChange::ReadyToNull => { + let _ = self.state.lock().unwrap().take(); + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToNull { + let _ = self.state.lock().unwrap().take(); + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "sodiumencrypter", + gst::Rank::None, + Encrypter::get_type(), + ) +} diff --git a/generic/gst-plugin-sodium/src/lib.rs b/generic/gst-plugin-sodium/src/lib.rs new file mode 100644 index 000000000..3961ddc7c --- /dev/null +++ b/generic/gst-plugin-sodium/src/lib.rs @@ -0,0 +1,83 @@ +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +#![crate_type = "cdylib"] + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gst; +#[macro_use] +extern crate lazy_static; + +const TYPEFIND_HEADER: &[u8; 12] = b"gst-sodium10"; +// `core::slice::<impl [T]>::len` is not yet stable as a const fn +// const TYPEFIND_HEADER_SIZE: usize = TYPEFIND_HEADER.len(); +const TYPEFIND_HEADER_SIZE: usize = 12; +/// Encryted steams use an offset at the start to store the block_size +/// and the nonce that was used. +const HEADERS_SIZE: usize = + TYPEFIND_HEADER_SIZE + sodiumoxide::crypto::box_::NONCEBYTES + std::mem::size_of::<u32>(); + +mod decrypter; +mod encrypter; + +fn typefind_register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + use gst::{Caps, TypeFind, TypeFindProbability}; + + TypeFind::register( + Some(plugin), + "sodium_encrypted_typefind", + gst::Rank::None, + None, + Some(&Caps::new_simple("application/x-sodium-encrypted", &[])), + |typefind| { + if let Some(data) = typefind.peek(0, TYPEFIND_HEADER_SIZE as u32) { + if data == TYPEFIND_HEADER { + typefind.suggest( + TypeFindProbability::Maximum, + &Caps::new_simple("application/x-sodium-encrypted", &[]), + ); + } + } + }, + ) +} + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + encrypter::register(plugin)?; + decrypter::register(plugin)?; + typefind_register(plugin)?; + Ok(()) +} + +gst_plugin_define!( + sodium, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MIT/X11", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); diff --git a/generic/gst-plugin-sodium/tests/decrypter.rs b/generic/gst-plugin-sodium/tests/decrypter.rs new file mode 100644 index 000000000..2fbb91e7a --- /dev/null +++ b/generic/gst-plugin-sodium/tests/decrypter.rs @@ -0,0 +1,323 @@ +// decrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate pretty_assertions; + +extern crate gstsodium; + +use glib::prelude::*; +use gst::prelude::*; + +use std::sync::{Arc, Mutex}; + +use std::path::PathBuf; + +lazy_static! { + static ref SENDER_PUBLIC: glib::Bytes = { + let public = [ + 66, 248, 199, 74, 216, 55, 228, 116, 52, 17, 147, 56, 65, 130, 134, 148, 157, 153, 235, + 171, 179, 147, 120, 71, 100, 243, 133, 120, 160, 14, 111, 65, + ]; + glib::Bytes::from_owned(public) + }; + static ref RECEIVER_PRIVATE: glib::Bytes = { + let secret = [ + 54, 221, 217, 54, 94, 235, 167, 2, 187, 249, 71, 31, 59, 27, 19, 166, 78, 236, 102, 48, + 29, 142, 41, 189, 22, 146, 218, 69, 147, 165, 240, 235, + ]; + glib::Bytes::from_owned(secret) + }; +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstsodium::plugin_register_static().unwrap(); + }); +} + +#[test] +fn test_pipeline() { + init(); + + let pipeline = gst::Pipeline::new(Some("sodium-decrypter-test")); + + let input_path = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("tests"); + r.push("encrypted_sample"); + r.set_extension("enc"); + r + }; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + filesrc + .set_property("location", &input_path.to_str().unwrap()) + .expect("failed to set property"); + + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + + // the typefind element here is cause the decrypter only supports + // operating in pull mode bu the filesink wants push-mode. + let typefind = gst::ElementFactory::make("typefind", None).unwrap(); + let sink = gst::ElementFactory::make("appsink", None).unwrap(); + + pipeline + .add_many(&[&filesrc, &dec, &typefind, &sink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &dec, &typefind, &sink]) + .expect("failed to link the elements"); + + let adapter = Arc::new(Mutex::new(gst_base::UniqueAdapter::new())); + + let sink = sink.downcast::<gst_app::AppSink>().unwrap(); + let adapter_clone = adapter.clone(); + sink.set_callbacks( + gst_app::AppSinkCallbacks::new() + // Add a handler to the "new-sample" signal. + .new_sample(move |appsink| { + // Pull the sample in question out of the appsink's buffer. + let sample = appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + let buffer = sample.get_buffer().ok_or(gst::FlowError::Error)?; + + let mut adapter = adapter_clone.lock().unwrap(); + adapter.push(buffer.to_owned()); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + unreachable!(); + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Playing` state"); + + let expected_output = include_bytes!("sample.mp3"); + + let mut adapter = adapter.lock().unwrap(); + let available = adapter.available(); + assert_eq!(available, expected_output.len()); + let output_buffer = adapter.take_buffer(available).unwrap(); + let output = output_buffer.map_readable().unwrap(); + assert_eq!(expected_output.as_ref(), output.as_ref()); +} + +#[test] +fn test_pull_range() { + init(); + + let pipeline = gst::Pipeline::new(Some("sodium-decrypter-pull-range-test")); + let input_path = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("tests"); + r.push("encrypted_sample"); + r.set_extension("enc"); + r + }; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + filesrc + .set_property("location", &input_path.to_str().unwrap()) + .expect("failed to set property"); + + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + + pipeline + .add_many(&[&filesrc, &dec]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &dec]).expect("failed to link the elements"); + + // Activate in the pad in pull mode + pipeline + .set_state(gst::State::Ready) + .expect("Unable to set the pipeline to the `Playing` state"); + let srcpad = dec.get_static_pad("src").unwrap(); + srcpad.activate_mode(gst::PadMode::Pull, true).unwrap(); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + // Test that the decryptor is seekable + let mut q = gst::query::Query::new_seeking(gst::Format::Bytes); + srcpad.query(&mut q); + + // get the seeking capabilities + let (seekable, start, stop) = q.get_result(); + assert_eq!(seekable, true); + assert_eq!( + start, + gst::GenericFormattedValue::Bytes(gst::format::Bytes(Some(0))) + ); + assert_eq!( + stop, + gst::GenericFormattedValue::Bytes(gst::format::Bytes(Some(6043))) + ); + + // do pulls + let expected_array_1 = [ + 255, 251, 192, 196, 0, 0, 13, 160, 37, 86, 116, 240, 0, 42, 73, 33, 43, 63, 61, 16, 128, 5, + 53, 37, 220, 28, 225, 35, 16, 243, 140, 220, 4, 192, 2, 64, 14, 3, 144, 203, 67, 208, 244, + 61, 70, 175, 103, 127, 28, 0, + ]; + let buf1 = srcpad.get_range(0, 50).unwrap(); + assert_eq!(buf1.get_size(), 50); + let map1 = buf1.map_readable().unwrap(); + assert_eq!(&map1[..], &expected_array_1[..]); + + let expected_array_2 = [ + 255, 251, 192, 196, 0, 0, 13, 160, 37, 86, 116, 240, 0, 42, 73, 33, 43, 63, 61, 16, 128, 5, + 53, 37, 220, 28, 225, 35, 16, 243, 140, 220, 4, 192, 2, 64, 14, 3, 144, 203, 67, 208, 244, + 61, 70, 175, 103, 127, 28, 0, 0, 0, 0, 12, 60, 60, 60, 122, 0, 0, 0, 12, 195, 195, 195, + 207, 192, 0, 0, 3, 113, 195, 199, 255, 255, 254, 97, 225, 225, 231, 160, 0, 0, 49, 24, 120, + 120, 121, 232, 0, 0, 12, 252, 195, 195, 199, 128, 0, 0, 0, + ]; + let buf2 = srcpad.get_range(0, 100).unwrap(); + assert_eq!(buf2.get_size(), 100); + let map2 = buf2.map_readable().unwrap(); + assert_eq!(&map2[..], &expected_array_2[..]); + + // compare the first 50 bytes of the two slices + // they should match + assert_eq!(&map1[..], &map2[..map1.len()]); + + // request in the middle of a block + let buf = srcpad.get_range(853, 100).unwrap(); + // result size doesn't include the block macs, + assert_eq!(buf.get_size(), 100); + + // read till eos, this also will pull multiple blocks + let buf = srcpad.get_range(853, 42000).unwrap(); + // 6031 (size of file) - 883 (requersted offset) - headers size - (numbler of blcks * block mac) + assert_eq!(buf.get_size(), 5054); + + // read 0 bytes from the start + let buf = srcpad.get_range(0, 0).unwrap(); + assert_eq!(buf.get_size(), 0); + + // read 0 bytes somewhere in the middle + let buf = srcpad.get_range(4242, 0).unwrap(); + assert_eq!(buf.get_size(), 0); + + // read 0 bytes to eos + let res = srcpad.get_range(6003, 0); + assert_eq!(res, Err(gst::FlowError::Eos)); + + // read 100 bytes at eos + let res = srcpad.get_range(6003, 100); + assert_eq!(res, Err(gst::FlowError::Eos)); + + // read 100 bytes way past eos + let res = srcpad.get_range(424_242, 100); + assert_eq!(res, Err(gst::FlowError::Eos)); + + // read 10 bytes at eos -1, should return a single byte + let buf = srcpad.get_range(5906, 10).unwrap(); + assert_eq!(buf.get_size(), 1); + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Playing` state"); +} + +#[test] +fn test_state_changes() { + init(); + + // NullToReady without keys provided + { + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + assert!(dec.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only receiver key + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only sender key + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_err()); + } + + // NullToReady, no nonce provided + { + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_ok()); + } + + // ReadyToNull + { + let dec = gst::ElementFactory::make("sodiumdecrypter", None).unwrap(); + dec.set_property("sender-key", &*SENDER_PUBLIC) + .expect("failed to set property"); + dec.set_property("receiver-key", &*RECEIVER_PRIVATE) + .expect("failed to set property"); + assert!(dec.change_state(gst::StateChange::NullToReady).is_ok()); + } +} diff --git a/generic/gst-plugin-sodium/tests/encrypted_sample.enc b/generic/gst-plugin-sodium/tests/encrypted_sample.enc Binary files differnew file mode 100644 index 000000000..f63b133a2 --- /dev/null +++ b/generic/gst-plugin-sodium/tests/encrypted_sample.enc diff --git a/generic/gst-plugin-sodium/tests/encrypter.rs b/generic/gst-plugin-sodium/tests/encrypter.rs new file mode 100644 index 000000000..e2f6dc83a --- /dev/null +++ b/generic/gst-plugin-sodium/tests/encrypter.rs @@ -0,0 +1,152 @@ +// encrypter.rs +// +// Copyright 2019 Jordan Petridis <jordan@centricular.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// +// SPDX-License-Identifier: MIT + +#[macro_use] +extern crate lazy_static; +extern crate gstsodium; + +use glib::prelude::*; +use gst::prelude::*; + +lazy_static! { + static ref RECEIVER_PUBLIC: glib::Bytes = { + let public = [ + 28, 95, 33, 124, 28, 103, 80, 78, 7, 28, 234, 40, 226, 179, 253, 166, 169, 64, 78, 5, + 57, 92, 151, 179, 221, 89, 68, 70, 44, 225, 219, 19, + ]; + + glib::Bytes::from_owned(public) + }; + static ref SENDER_PRIVATE: glib::Bytes = { + let secret = [ + 154, 227, 90, 239, 206, 184, 202, 234, 176, 161, 14, 91, 218, 98, 142, 13, 145, 223, + 210, 222, 224, 240, 98, 51, 142, 165, 255, 1, 159, 100, 242, 162, + ]; + glib::Bytes::from_owned(secret) + }; + static ref NONCE: glib::Bytes = { + let nonce = [ + 144, 187, 179, 230, 15, 4, 241, 15, 37, 133, 22, 30, 50, 106, 70, 159, 243, 218, 173, + 22, 18, 36, 4, 45, + ]; + glib::Bytes::from_owned(nonce) + }; +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstsodium::plugin_register_static().unwrap(); + // set the nonce + std::env::set_var("GST_SODIUM_ENCRYPT_NONCE", hex::encode(&*NONCE)); + }); +} + +#[test] +fn encrypt_file() { + init(); + + let input = include_bytes!("sample.mp3"); + let expected_output = include_bytes!("encrypted_sample.enc"); + + let mut adapter = gst_base::UniqueAdapter::new(); + + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + enc.set_property("block-size", &1024u32) + .expect("failed to set property"); + + let mut h = gst_check::Harness::new_with_element(&enc, None, None); + h.add_element_src_pad(&enc.get_static_pad("src").expect("failed to get src pad")); + h.add_element_sink_pad(&enc.get_static_pad("sink").expect("failed to get src pad")); + h.set_src_caps_str("application/x-sodium-encrypted"); + + let buf = gst::Buffer::from_mut_slice(Vec::from(&input[..])); + + assert_eq!(h.push(buf), Ok(gst::FlowSuccess::Ok)); + h.push_event(gst::Event::new_eos().build()); + + println!("Pulling buffer..."); + while let Ok(buf) = h.pull() { + adapter.push(buf); + if adapter.available() >= expected_output.len() { + break; + } + } + + let buf = adapter + .take_buffer(adapter.available()) + .expect("failed to take buffer"); + let map = buf.map_readable().expect("Couldn't map buffer readable"); + assert_eq!(map.as_ref(), expected_output.as_ref()); +} + +#[test] +fn test_state_changes() { + init(); + + // NullToReady without keys provided + { + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + assert!(enc.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only receiver key + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_err()); + + // Set only sender key + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_err()); + } + + // NullToReady + { + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_ok()); + } + + // ReadyToNull + { + let enc = gst::ElementFactory::make("sodiumencrypter", None).unwrap(); + enc.set_property("sender-key", &*SENDER_PRIVATE) + .expect("failed to set property"); + enc.set_property("receiver-key", &*RECEIVER_PUBLIC) + .expect("failed to set property"); + assert!(enc.change_state(gst::StateChange::NullToReady).is_ok()); + } +} diff --git a/generic/gst-plugin-sodium/tests/sample.mp3 b/generic/gst-plugin-sodium/tests/sample.mp3 Binary files differnew file mode 100644 index 000000000..839e90d59 --- /dev/null +++ b/generic/gst-plugin-sodium/tests/sample.mp3 diff --git a/generic/gst-plugin-threadshare/Cargo.toml b/generic/gst-plugin-threadshare/Cargo.toml new file mode 100644 index 000000000..3186d56a6 --- /dev/null +++ b/generic/gst-plugin-threadshare/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "gst-plugin-threadshare" +version = "0.6.0" +authors = ["Sebastian Dröge <sebastian@centricular.com>"] +license = "LGPL-2.1+" +description = "Threadshare Plugin" +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +edition = "2018" + +[dependencies] +libc = "0.2" +glib-sys = { git = "https://github.com/gtk-rs/sys" } +gobject-sys = { git = "https://github.com/gtk-rs/sys" } +gio-sys = { git = "https://github.com/gtk-rs/sys" } +glib = { git = "https://github.com/gtk-rs/glib" } +gio = { git = "https://github.com/gtk-rs/gio" } +gst = { package = "gstreamer", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features=["v1_10"] } +gst-app = { package = "gstreamer-app", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-check = { package = "gstreamer-check", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-net = { package = "gstreamer-net", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gst-rtp = { package = "gstreamer-rtp", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gstreamer-sys = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs-sys" } +pin-project = "0.4" +tokio = { git = "https://github.com/fengalin/tokio", tag = "tokio-0.2.12-throttling", features = ["io-util", "macros", "rt-core", "sync", "stream", "time", "tcp", "udp", "rt-util"] } +futures = "0.3" +lazy_static = "1.0" +rand = "0.7" +net2 = "0.2" + +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["winsock2", "processthreadsapi"] } + +[lib] +name = "gstthreadshare" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[[example]] +name = "benchmark" +path = "examples/benchmark.rs" + +[[example]] +name = "udpsrc-benchmark-sender" +path = "examples/udpsrc_benchmark_sender.rs" + +[[example]] +name = "tcpclientsrc-benchmark-sender" +path = "examples/tcpclientsrc_benchmark_sender.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../gst-plugin-version-helper" } +cc = "1.0.38" +pkg-config = "0.3.15" diff --git a/generic/gst-plugin-threadshare/build.rs b/generic/gst-plugin-threadshare/build.rs new file mode 100644 index 000000000..fd089efd5 --- /dev/null +++ b/generic/gst-plugin-threadshare/build.rs @@ -0,0 +1,35 @@ +extern crate cc; +extern crate gst_plugin_version_helper; +extern crate pkg_config; + +fn main() { + let gstreamer = pkg_config::probe_library("gstreamer-1.0").unwrap(); + let gstrtp = pkg_config::probe_library("gstreamer-rtp-1.0").unwrap(); + let includes = [gstreamer.include_paths, gstrtp.include_paths]; + + let mut build = cc::Build::new(); + + for p in includes.iter().flatten() { + build.include(p); + } + + build.file("src/jitterbuffer/rtpjitterbuffer.c"); + build.file("src/jitterbuffer/rtpstats.c"); + + build.define("RTPJitterBuffer", "TsRTPJitterBuffer"); + build.define("RTPJitterBufferClass", "TsRTPJitterBufferClass"); + build.define("RTPJitterBufferPrivate", "TsRTPJitterBufferClass"); + + build.compile("libthreadshare-c.a"); + + println!("cargo:rustc-link-lib=dylib=gstrtp-1.0"); + + for path in gstrtp.link_paths.iter() { + println!( + "cargo:rustc-link-search=native={}", + path.to_str().expect("library path doesn't exist") + ); + } + + gst_plugin_version_helper::get_info() +} diff --git a/generic/gst-plugin-threadshare/examples/benchmark.rs b/generic/gst-plugin-threadshare/examples/benchmark.rs new file mode 100644 index 000000000..f7d164ed6 --- /dev/null +++ b/generic/gst-plugin-threadshare/examples/benchmark.rs @@ -0,0 +1,173 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib; +use glib::prelude::*; + +use gst; +use gst::prelude::*; + +use std::env; + +fn main() { + gst::init().unwrap(); + + #[cfg(debug_assertions)] + { + use std::path::Path; + + let mut path = Path::new("target/debug"); + if !path.exists() { + path = Path::new("../target/debug"); + } + + gst::Registry::get().scan_path(path); + } + #[cfg(not(debug_assertions))] + { + use std::path::Path; + + let mut path = Path::new("target/release"); + if !path.exists() { + path = Path::new("../target/release"); + } + + gst::Registry::get().scan_path(path); + } + + let args = env::args().collect::<Vec<_>>(); + assert_eq!(args.len(), 6); + let n_streams: u16 = args[1].parse().unwrap(); + let source = &args[2]; + let n_groups: u32 = args[3].parse().unwrap(); + let wait: u32 = args[4].parse().unwrap(); + + let l = glib::MainLoop::new(None, false); + let pipeline = gst::Pipeline::new(None); + + for i in 0..n_streams { + let sink = + gst::ElementFactory::make("fakesink", Some(format!("sink-{}", i).as_str())).unwrap(); + sink.set_property("sync", &false).unwrap(); + sink.set_property("async", &false).unwrap(); + + let source = match source.as_str() { + "udpsrc" => { + let source = + gst::ElementFactory::make("udpsrc", Some(format!("source-{}", i).as_str())) + .unwrap(); + source + .set_property("port", &(40000i32 + (i as i32))) + .unwrap(); + source + .set_property("retrieve-sender-address", &false) + .unwrap(); + + source + } + "ts-udpsrc" => { + let source = + gst::ElementFactory::make("ts-udpsrc", Some(format!("source-{}", i).as_str())) + .unwrap(); + source + .set_property("port", &(40000u32 + (i as u32))) + .unwrap(); + source + .set_property("context", &format!("context-{}", (i as u32) % n_groups)) + .unwrap(); + source.set_property("context-wait", &wait).unwrap(); + + source + } + "ts-tcpclientsrc" => { + let source = gst::ElementFactory::make( + "ts-tcpclientsrc", + Some(format!("source-{}", i).as_str()), + ) + .unwrap(); + source.set_property("port", &(40000u32)).unwrap(); + source + .set_property("context", &format!("context-{}", (i as u32) % n_groups)) + .unwrap(); + source.set_property("context-wait", &wait).unwrap(); + + source + } + "tonegeneratesrc" => { + let source = gst::ElementFactory::make( + "tonegeneratesrc", + Some(format!("source-{}", i).as_str()), + ) + .unwrap(); + source + .set_property("samplesperbuffer", &((wait as i32) * 8000 / 1000)) + .unwrap(); + + sink.set_property("sync", &true).unwrap(); + + source + } + "ts-tonesrc" => { + let source = + gst::ElementFactory::make("ts-tonesrc", Some(format!("source-{}", i).as_str())) + .unwrap(); + source + .set_property("samples-per-buffer", &((wait as u32) * 8000 / 1000)) + .unwrap(); + source + .set_property("context", &format!("context-{}", (i as u32) % n_groups)) + .unwrap(); + source.set_property("context-wait", &wait).unwrap(); + + source + } + _ => unimplemented!(), + }; + + pipeline.add_many(&[&source, &sink]).unwrap(); + source.link(&sink).unwrap(); + } + + let bus = pipeline.get_bus().unwrap(); + let l_clone = l.clone(); + bus.add_watch(move |_, msg| { + use gst::MessageView; + + match msg.view() { + MessageView::Eos(..) => l_clone.quit(), + MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + err.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + l_clone.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .expect("Failed to add bus watch"); + + pipeline.set_state(gst::State::Playing).unwrap(); + + println!("started"); + + l.run(); +} diff --git a/generic/gst-plugin-threadshare/examples/tcpclientsrc_benchmark_sender.rs b/generic/gst-plugin-threadshare/examples/tcpclientsrc_benchmark_sender.rs new file mode 100644 index 000000000..435ba2c2a --- /dev/null +++ b/generic/gst-plugin-threadshare/examples/tcpclientsrc_benchmark_sender.rs @@ -0,0 +1,48 @@ +// Copyright (C) 2018 LEE Dongjun <redongjun@gmail.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use std::io::Write; +use std::sync::{Arc, Mutex}; +use std::{net, thread, time}; + +fn main() { + let sockets: Arc<Mutex<Vec<net::TcpStream>>> = Arc::new(Mutex::new(vec![])); + + let streams = sockets.clone(); + let _handler = thread::spawn(move || { + let listener = net::TcpListener::bind("0.0.0.0:40000").unwrap(); + for stream in listener.incoming() { + streams.lock().unwrap().push(stream.unwrap()); + } + }); + + let buffer = [0; 160]; + let wait = time::Duration::from_millis(20); + + loop { + let now = time::Instant::now(); + + for mut socket in sockets.lock().unwrap().iter() { + let _ = socket.write(&buffer); + } + + let elapsed = now.elapsed(); + if elapsed < wait { + thread::sleep(wait - elapsed); + } + } +} diff --git a/generic/gst-plugin-threadshare/examples/udpsrc_benchmark_sender.rs b/generic/gst-plugin-threadshare/examples/udpsrc_benchmark_sender.rs new file mode 100644 index 000000000..2993b4759 --- /dev/null +++ b/generic/gst-plugin-threadshare/examples/udpsrc_benchmark_sender.rs @@ -0,0 +1,51 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use std::net; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::{env, thread, time}; + +fn main() { + let args = env::args().collect::<Vec<_>>(); + assert_eq!(args.len(), 2); + let n_streams: u16 = args[1].parse().unwrap(); + + let buffer = [0; 160]; + let socket = net::UdpSocket::bind("0.0.0.0:0").unwrap(); + + let ipaddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let destinations = (40000..(40000 + n_streams)) + .map(|port| SocketAddr::new(ipaddr, port)) + .collect::<Vec<_>>(); + + let wait = time::Duration::from_millis(20); + + thread::sleep(time::Duration::from_millis(1000)); + + loop { + let now = time::Instant::now(); + + for dest in &destinations { + socket.send_to(&buffer, dest).unwrap(); + } + + let elapsed = now.elapsed(); + if elapsed < wait { + thread::sleep(wait - elapsed); + } + } +} diff --git a/generic/gst-plugin-threadshare/src/appsrc.rs b/generic/gst-plugin-threadshare/src/appsrc.rs new file mode 100644 index 000000000..689b276bf --- /dev/null +++ b/generic/gst-plugin-threadshare/src/appsrc.rs @@ -0,0 +1,794 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// Copyright (C) 2019-2020 François Laignel <fengalin@free.fr> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::channel::mpsc; +use futures::lock::Mutex as FutMutex; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_element_error, gst_error, gst_error_msg, gst_log, gst_trace}; + +use lazy_static::lazy_static; + +use rand; + +use std::convert::TryInto; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::u32; + +use crate::runtime::prelude::*; +use crate::runtime::{Context, PadSrc, PadSrcRef, Task}; + +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; +const DEFAULT_CAPS: Option<gst::Caps> = None; +const DEFAULT_MAX_BUFFERS: u32 = 10; +const DEFAULT_DO_TIMESTAMP: bool = false; + +#[derive(Debug, Clone)] +struct Settings { + context: String, + context_wait: u32, + caps: Option<gst::Caps>, + max_buffers: u32, + do_timestamp: bool, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + caps: DEFAULT_CAPS, + max_buffers: DEFAULT_MAX_BUFFERS, + do_timestamp: DEFAULT_DO_TIMESTAMP, + } + } +} + +static PROPERTIES: [subclass::Property; 5] = [ + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-buffers", |name| { + glib::ParamSpec::uint( + name, + "Max Buffers", + "Maximum number of buffers to queue up", + 1, + u32::MAX, + DEFAULT_MAX_BUFFERS, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("caps", |name| { + glib::ParamSpec::boxed( + name, + "Caps", + "Caps to use", + gst::Caps::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("do-timestamp", |name| { + glib::ParamSpec::boolean( + name, + "Do Timestamp", + "Timestamp buffers with the current running time on arrival", + DEFAULT_DO_TIMESTAMP, + glib::ParamFlags::READWRITE, + ) + }), +]; + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-appsrc", + gst::DebugColorFlags::empty(), + Some("Thread-sharing app source"), + ); +} + +#[derive(Debug)] +enum StreamItem { + Buffer(gst::Buffer), + Event(gst::Event), +} + +#[derive(Debug)] +struct AppSrcPadHandlerState { + need_initial_events: bool, + need_segment: bool, + caps: Option<gst::Caps>, +} + +impl Default for AppSrcPadHandlerState { + fn default() -> Self { + AppSrcPadHandlerState { + need_initial_events: true, + need_segment: true, + caps: None, + } + } +} + +#[derive(Debug, Default)] +struct AppSrcPadHandlerInner { + state: FutMutex<AppSrcPadHandlerState>, + configured_caps: StdMutex<Option<gst::Caps>>, +} + +#[derive(Clone, Debug, Default)] +struct AppSrcPadHandler(Arc<AppSrcPadHandlerInner>); + +impl AppSrcPadHandler { + fn prepare(&self, caps: Option<gst::Caps>) { + self.0 + .state + .try_lock() + .expect("State locked elsewhere") + .caps = caps; + } + + fn reset(&self) { + *self.0.state.try_lock().expect("State locked elsewhere") = Default::default(); + *self.0.configured_caps.lock().unwrap() = None; + } + + fn set_need_segment(&self) { + self.0 + .state + .try_lock() + .expect("State locked elsewhere") + .need_segment = true; + } + + async fn push_prelude(&self, pad: &PadSrcRef<'_>, _element: &gst::Element) { + let mut state = self.0.state.lock().await; + if state.need_initial_events { + gst_debug!(CAT, obj: pad.gst_pad(), "Pushing initial events"); + + let stream_id = format!("{:08x}{:08x}", rand::random::<u32>(), rand::random::<u32>()); + let stream_start_evt = gst::Event::new_stream_start(&stream_id) + .group_id(gst::GroupId::next()) + .build(); + pad.push_event(stream_start_evt).await; + + if let Some(ref caps) = state.caps { + let caps_evt = gst::Event::new_caps(&caps).build(); + pad.push_event(caps_evt).await; + *self.0.configured_caps.lock().unwrap() = Some(caps.clone()); + } + + state.need_initial_events = false; + } + + if state.need_segment { + let segment_evt = + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(); + pad.push_event(segment_evt).await; + + state.need_segment = false; + } + } + + async fn push_item( + self, + pad: &PadSrcRef<'_>, + element: &gst::Element, + item: StreamItem, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", item); + + self.push_prelude(pad, element).await; + + match item { + StreamItem::Buffer(buffer) => { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", buffer); + pad.push(buffer).await + } + StreamItem::Event(event) => { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + pad.push_event(event).await; + Ok(gst::FlowSuccess::Ok) + } + } + } +} + +impl PadSrcHandler for AppSrcPadHandler { + type ElementImpl = AppSrc; + + fn src_event( + &self, + pad: &PadSrcRef, + appsrc: &AppSrc, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + let ret = match event.view() { + EventView::FlushStart(..) => { + appsrc.flush_start(element); + true + } + EventView::FlushStop(..) => { + appsrc.flush_stop(element); + true + } + EventView::Reconfigure(..) => true, + EventView::Latency(..) => true, + _ => false, + }; + + if ret { + gst_log!(CAT, obj: pad.gst_pad(), "Handled {:?}", event); + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Didn't handle {:?}", event); + } + + ret + } + + fn src_query( + &self, + pad: &PadSrcRef, + _appsrc: &AppSrc, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", query); + let ret = match query.view_mut() { + QueryView::Latency(ref mut q) => { + q.set(true, 0.into(), gst::CLOCK_TIME_NONE); + true + } + QueryView::Scheduling(ref mut q) => { + q.set(gst::SchedulingFlags::SEQUENTIAL, 1, -1, 0); + q.add_scheduling_modes(&[gst::PadMode::Push]); + true + } + QueryView::Caps(ref mut q) => { + let caps = if let Some(caps) = self.0.configured_caps.lock().unwrap().as_ref() { + q.get_filter() + .map(|f| f.intersect_with_mode(caps, gst::CapsIntersectMode::First)) + .unwrap_or_else(|| caps.clone()) + } else { + q.get_filter() + .map(|f| f.to_owned()) + .unwrap_or_else(gst::Caps::new_any) + }; + + q.set_result(&caps); + + true + } + _ => false, + }; + + if ret { + gst_log!(CAT, obj: pad.gst_pad(), "Handled {:?}", query); + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Didn't handle {:?}", query); + } + ret + } +} + +#[derive(Debug, Eq, PartialEq)] +enum AppSrcState { + Paused, + RejectBuffers, + Started, +} + +#[derive(Debug)] +struct AppSrc { + src_pad: PadSrc, + src_pad_handler: AppSrcPadHandler, + task: Task, + state: StdMutex<AppSrcState>, + sender: StdMutex<Option<mpsc::Sender<StreamItem>>>, + receiver: StdMutex<Option<Arc<FutMutex<mpsc::Receiver<StreamItem>>>>>, + settings: StdMutex<Settings>, +} + +impl AppSrc { + fn push_buffer(&self, element: &gst::Element, mut buffer: gst::Buffer) -> bool { + let state = self.state.lock().unwrap(); + if *state == AppSrcState::RejectBuffers { + gst_debug!(CAT, obj: element, "Rejecting buffer due to pad state"); + return false; + } + + let do_timestamp = self.settings.lock().unwrap().do_timestamp; + if do_timestamp { + if let Some(clock) = element.get_clock() { + let base_time = element.get_base_time(); + let now = clock.get_time(); + + let buffer = buffer.make_mut(); + buffer.set_dts(now - base_time); + buffer.set_pts(gst::CLOCK_TIME_NONE); + } else { + gst_error!(CAT, obj: element, "Don't have a clock yet"); + return false; + } + } + + match self + .sender + .lock() + .unwrap() + .as_mut() + .unwrap() + .try_send(StreamItem::Buffer(buffer)) + { + Ok(_) => true, + Err(err) => { + gst_error!(CAT, obj: element, "Failed to queue buffer: {}", err); + false + } + } + } + + fn end_of_stream(&self, element: &gst::Element) -> bool { + let mut sender = self.sender.lock().unwrap(); + let sender = match sender.as_mut() { + Some(sender) => sender, + None => return false, + }; + + let eos = StreamItem::Event(gst::Event::new_eos().build()); + match sender.try_send(eos) { + Ok(_) => true, + Err(err) => { + gst_error!(CAT, obj: element, "Failed to queue EOS: {}", err); + false + } + } + } + + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + let settings = self.settings.lock().unwrap(); + gst_debug!(CAT, obj: element, "Preparing"); + + let context = + Context::acquire(&settings.context, settings.context_wait).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to acquire Context: {}", err] + ) + })?; + + let max_buffers = settings.max_buffers.try_into().map_err(|err| { + gst_error_msg!( + gst::ResourceError::Settings, + ["Invalid max-buffers: {}, {}", settings.max_buffers, err] + ) + })?; + + let (sender, receiver) = mpsc::channel(max_buffers); + *self.sender.lock().unwrap() = Some(sender); + *self.receiver.lock().unwrap() = Some(Arc::new(FutMutex::new(receiver))); + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + self.src_pad_handler.prepare(settings.caps.clone()); + self.src_pad.prepare(&self.src_pad_handler); + + gst_debug!(CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Unpreparing"); + + *self.sender.lock().unwrap() = None; + *self.receiver.lock().unwrap() = None; + + self.task.unprepare().unwrap(); + self.src_pad.unprepare(); + + gst_debug!(CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + gst_debug!(CAT, obj: element, "Stopping"); + + self.flush(element); + self.src_pad_handler.reset(); + *state = AppSrcState::RejectBuffers; + + gst_debug!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn flush(&self, element: &gst::Element) { + gst_log!(CAT, obj: element, "Flushing"); + + self.task.stop(); + + let receiver = self.receiver.lock().unwrap(); + let mut receiver = receiver + .as_ref() + .unwrap() + .try_lock() + .expect("receiver locked elsewhere"); + + // Purge the channel + loop { + match receiver.try_next() { + Ok(Some(_item)) => { + gst_log!(CAT, obj: element, "Dropping pending item"); + } + Err(_) => { + gst_log!(CAT, obj: element, "No more pending item"); + break; + } + Ok(None) => { + panic!("Channel sender dropped"); + } + } + } + + self.src_pad_handler.set_need_segment(); + + gst_log!(CAT, obj: element, "Flushed"); + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + if *state == AppSrcState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return Ok(()); + } + + gst_debug!(CAT, obj: element, "Starting"); + + self.start_task(element); + *state = AppSrcState::Started; + + gst_debug!(CAT, obj: element, "Started"); + + Ok(()) + } + + fn start_task(&self, element: &gst::Element) { + let src_pad_handler = self.src_pad_handler.clone(); + let pad_weak = self.src_pad.downgrade(); + let element = element.clone(); + let receiver = Arc::clone(self.receiver.lock().unwrap().as_ref().expect("No receiver")); + + self.task.start(move || { + let src_pad_handler = src_pad_handler.clone(); + let pad_weak = pad_weak.clone(); + let element = element.clone(); + let receiver = Arc::clone(&receiver); + + async move { + let item = receiver.lock().await.next().await; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + + let item = match item { + Some(item) => item, + None => { + gst_log!(CAT, obj: pad.gst_pad(), "SrcPad channel aborted"); + return glib::Continue(false); + } + }; + + match src_pad_handler.push_item(&pad, &element, item).await { + Ok(_) => { + gst_log!(CAT, obj: pad.gst_pad(), "Successfully pushed item"); + glib::Continue(true) + } + Err(gst::FlowError::Eos) => { + gst_debug!(CAT, obj: pad.gst_pad(), "EOS"); + let eos = gst::Event::new_eos().build(); + pad.push_event(eos).await; + glib::Continue(false) + } + Err(gst::FlowError::Flushing) => { + gst_debug!(CAT, obj: pad.gst_pad(), "Flushing"); + glib::Continue(false) + } + Err(err) => { + gst_error!(CAT, obj: pad.gst_pad(), "Got error {}", err); + gst_element_error!( + &element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + glib::Continue(false) + } + } + } + }); + } + + fn flush_stop(&self, element: &gst::Element) { + let mut state = self.state.lock().unwrap(); + if *state == AppSrcState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return; + } + + gst_debug!(CAT, obj: element, "Stopping Flush"); + + self.flush(element); + self.start_task(element); + *state = AppSrcState::Started; + + gst_debug!(CAT, obj: element, "Flush Stopped"); + } + + fn flush_start(&self, element: &gst::Element) { + let mut state = self.state.lock().unwrap(); + gst_debug!(CAT, obj: element, "Starting Flush"); + + self.task.cancel(); + *state = AppSrcState::RejectBuffers; + + gst_debug!(CAT, obj: element, "Flush Started"); + } + + fn pause(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + gst_debug!(CAT, obj: element, "Pausing"); + + self.task.pause(); + *state = AppSrcState::Paused; + + gst_debug!(CAT, obj: element, "Paused"); + + Ok(()) + } +} + +impl ObjectSubclass for AppSrc { + const NAME: &'static str = "RsTsAppSrc"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing app source", + "Source/Generic", + "Thread-sharing app source", + "Sebastian Dröge <sebastian@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES); + + klass.add_signal_with_class_handler( + "push-buffer", + glib::SignalFlags::RUN_LAST | glib::SignalFlags::ACTION, + &[gst::Buffer::static_type()], + bool::static_type(), + |_, args| { + let element = args[0] + .get::<gst::Element>() + .expect("signal arg") + .expect("missing signal arg"); + let buffer = args[1] + .get::<gst::Buffer>() + .expect("signal arg") + .expect("missing signal arg"); + let appsrc = Self::from_instance(&element); + + Some(appsrc.push_buffer(&element, buffer).to_value()) + }, + ); + + klass.add_signal_with_class_handler( + "end-of-stream", + glib::SignalFlags::RUN_LAST | glib::SignalFlags::ACTION, + &[], + bool::static_type(), + |_, args| { + let element = args[0] + .get::<gst::Element>() + .expect("signal arg") + .expect("missing signal arg"); + let appsrc = Self::from_instance(&element); + Some(appsrc.end_of_stream(&element).to_value()) + }, + ); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + src_pad_handler: AppSrcPadHandler::default(), + task: Task::default(), + state: StdMutex::new(AppSrcState::RejectBuffers), + sender: StdMutex::new(None), + receiver: StdMutex::new(None), + settings: StdMutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for AppSrc { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("context", ..) => { + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + settings.context_wait = value.get_some().expect("type checked upstream"); + } + subclass::Property("caps", ..) => { + settings.caps = value.get().expect("type checked upstream"); + } + subclass::Property("max-buffers", ..) => { + settings.max_buffers = value.get_some().expect("type checked upstream"); + } + subclass::Property("do-timestamp", ..) => { + settings.do_timestamp = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("context", ..) => Ok(settings.context.to_value()), + subclass::Property("context-wait", ..) => Ok(settings.context_wait.to_value()), + subclass::Property("caps", ..) => Ok(settings.caps.to_value()), + subclass::Property("max-buffers", ..) => Ok(settings.max_buffers.to_value()), + subclass::Property("do-timestamp", ..) => Ok(settings.do_timestamp.to_value()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + + super::set_element_flags(element, gst::ElementFlags::SOURCE); + } +} + +impl ElementImpl for AppSrc { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PlayingToPaused => { + self.pause(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::ReadyToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToPlaying => { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-appsrc", + gst::Rank::None, + AppSrc::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/src/dataqueue.rs b/generic/gst-plugin-threadshare/src/dataqueue.rs new file mode 100644 index 000000000..9f77d9664 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/dataqueue.rs @@ -0,0 +1,282 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::future::{self, abortable, AbortHandle}; + +use gst; +use gst::gst_debug; +use gst::prelude::*; + +use lazy_static::lazy_static; + +use std::collections::VecDeque; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::{u32, u64}; + +lazy_static! { + static ref DATA_QUEUE_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-dataqueue", + gst::DebugColorFlags::empty(), + Some("Thread-sharing queue"), + ); +} + +#[derive(Debug)] +pub enum DataQueueItem { + Buffer(gst::Buffer), + BufferList(gst::BufferList), + Event(gst::Event), +} + +impl DataQueueItem { + fn size(&self) -> (u32, u32) { + match *self { + DataQueueItem::Buffer(ref buffer) => (1, buffer.get_size() as u32), + DataQueueItem::BufferList(ref list) => ( + list.len() as u32, + list.iter().map(|b| b.get_size() as u32).sum::<u32>(), + ), + DataQueueItem::Event(_) => (0, 0), + } + } + + fn timestamp(&self) -> Option<u64> { + match *self { + DataQueueItem::Buffer(ref buffer) => buffer.get_dts_or_pts().0, + DataQueueItem::BufferList(ref list) => { + list.iter().filter_map(|b| b.get_dts_or_pts().0).next() + } + DataQueueItem::Event(_) => None, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DataQueueState { + Paused, + Started, + Stopped, +} + +#[derive(Clone, Debug)] +pub struct DataQueue(Arc<StdMutex<DataQueueInner>>); + +#[derive(Debug)] +struct DataQueueInner { + element: gst::Element, + src_pad: gst::Pad, + + state: DataQueueState, + queue: VecDeque<DataQueueItem>, + + cur_size_buffers: u32, + cur_size_bytes: u32, + max_size_buffers: Option<u32>, + max_size_bytes: Option<u32>, + max_size_time: Option<u64>, + + pending_handle: Option<AbortHandle>, +} + +impl DataQueueInner { + fn wake(&mut self) { + if let Some(pending_handle) = self.pending_handle.take() { + pending_handle.abort(); + } + } +} + +impl DataQueue { + pub fn new( + element: &gst::Element, + src_pad: &gst::Pad, + max_size_buffers: Option<u32>, + max_size_bytes: Option<u32>, + max_size_time: Option<u64>, + ) -> DataQueue { + DataQueue(Arc::new(StdMutex::new(DataQueueInner { + element: element.clone(), + src_pad: src_pad.clone(), + state: DataQueueState::Stopped, + queue: VecDeque::new(), + cur_size_buffers: 0, + cur_size_bytes: 0, + max_size_buffers, + max_size_bytes, + max_size_time, + pending_handle: None, + }))) + } + + pub fn state(&self) -> DataQueueState { + self.0.lock().unwrap().state + } + + pub fn start(&self) { + let mut inner = self.0.lock().unwrap(); + if inner.state == DataQueueState::Started { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue already Started"); + return; + } + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Starting data queue"); + inner.state = DataQueueState::Started; + inner.wake(); + } + + pub fn pause(&self) { + let mut inner = self.0.lock().unwrap(); + if inner.state == DataQueueState::Paused { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue already Paused"); + return; + } + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Pausing data queue"); + assert_eq!(DataQueueState::Started, inner.state); + inner.state = DataQueueState::Paused; + inner.wake(); + } + + pub fn stop(&self) { + let mut inner = self.0.lock().unwrap(); + if inner.state == DataQueueState::Stopped { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue already Stopped"); + return; + } + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Stopping data queue"); + inner.state = DataQueueState::Stopped; + inner.wake(); + } + + pub fn clear(&self) { + let mut inner = self.0.lock().unwrap(); + + assert_eq!(inner.state, DataQueueState::Paused); + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Clearing data queue"); + + let src_pad = inner.src_pad.clone(); + for item in inner.queue.drain(..) { + if let DataQueueItem::Event(event) = item { + if event.is_sticky() + && event.get_type() != gst::EventType::Segment + && event.get_type() != gst::EventType::Eos + { + let _ = src_pad.store_sticky_event(&event); + } + } + } + + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue cleared"); + } + + pub fn push(&self, item: DataQueueItem) -> Result<(), DataQueueItem> { + let mut inner = self.0.lock().unwrap(); + + if inner.state == DataQueueState::Stopped { + gst_debug!( + DATA_QUEUE_CAT, + obj: &inner.element, + "Rejecting item {:?} in state {:?}", + item, + inner.state + ); + return Err(item); + } + + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Pushing item {:?}", item); + + let (count, bytes) = item.size(); + let queue_ts = inner.queue.iter().filter_map(|i| i.timestamp()).next(); + let ts = item.timestamp(); + + if let Some(max) = inner.max_size_buffers { + if max <= inner.cur_size_buffers { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Queue is full (buffers): {} <= {}", max, inner.cur_size_buffers); + return Err(item); + } + } + + if let Some(max) = inner.max_size_bytes { + if max <= inner.cur_size_bytes { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Queue is full (bytes): {} <= {}", max, inner.cur_size_bytes); + return Err(item); + } + } + + // FIXME: Use running time + if let (Some(max), Some(queue_ts), Some(ts)) = (inner.max_size_time, queue_ts, ts) { + let level = if queue_ts > ts { + queue_ts - ts + } else { + ts - queue_ts + }; + + if max <= level { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Queue is full (time): {} <= {}", max, level); + return Err(item); + } + } + + inner.queue.push_back(item); + inner.cur_size_buffers += count; + inner.cur_size_bytes += bytes; + + inner.wake(); + + Ok(()) + } + + // TODO: implement as a Stream now that we use a StdMutex + #[allow(clippy::should_implement_trait)] + pub async fn next(&mut self) -> Option<DataQueueItem> { + loop { + let pending_fut = { + let mut inner = self.0.lock().unwrap(); + match inner.state { + DataQueueState::Started => match inner.queue.pop_front() { + None => { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue is empty"); + } + Some(item) => { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Popped item {:?}", item); + + let (count, bytes) = item.size(); + inner.cur_size_buffers -= count; + inner.cur_size_bytes -= bytes; + + return Some(item); + } + }, + DataQueueState::Paused => { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue Paused"); + return None; + } + DataQueueState::Stopped => { + gst_debug!(DATA_QUEUE_CAT, obj: &inner.element, "Data queue Stopped"); + return None; + } + } + + let (pending_fut, abort_handle) = abortable(future::pending::<()>()); + inner.pending_handle = Some(abort_handle); + + pending_fut + }; + + let _ = pending_fut.await; + } + } +} diff --git a/generic/gst-plugin-threadshare/src/inputselector.rs b/generic/gst-plugin-threadshare/src/inputselector.rs new file mode 100644 index 000000000..47e3ba6f3 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/inputselector.rs @@ -0,0 +1,684 @@ +// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::future::BoxFuture; +use futures::future::{abortable, AbortHandle}; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_log, gst_trace}; + +use lazy_static::lazy_static; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::u32; + +use crate::get_current_running_time; +use crate::runtime::prelude::*; +use crate::runtime::{self, PadSink, PadSinkRef, PadSrc, PadSrcRef}; + +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; + +#[derive(Debug, Clone)] +struct Settings { + context: String, + context_wait: u32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + } + } +} + +static PROPERTIES: [subclass::Property; 3] = [ + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("active-pad", |name| { + glib::ParamSpec::object( + name, + "Active Pad", + "Currently active pad", + gst::Pad::static_type(), + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Debug)] +struct InputSelectorPadSinkHandlerInner { + segment: Option<gst::Segment>, + send_sticky: bool, + abort_handle: Option<AbortHandle>, +} + +impl Default for InputSelectorPadSinkHandlerInner { + fn default() -> Self { + InputSelectorPadSinkHandlerInner { + segment: None, + send_sticky: true, + abort_handle: None, + } + } +} + +#[derive(Clone, Debug)] +struct InputSelectorPadSinkHandler(Arc<Mutex<InputSelectorPadSinkHandlerInner>>); + +impl InputSelectorPadSinkHandler { + fn new() -> Self { + InputSelectorPadSinkHandler(Arc::new(Mutex::new( + InputSelectorPadSinkHandlerInner::default(), + ))) + } + + /* Wait until specified time */ + async fn sync(&self, element: &gst::Element, running_time: gst::ClockTime) { + let now = get_current_running_time(&element); + + if now.is_some() && now < running_time { + let delay = running_time - now; + runtime::time::delay_for(Duration::from_nanos(delay.nseconds().unwrap())).await; + } + } + + async fn handle_item( + &self, + pad: &PadSinkRef<'_>, + element: &gst::Element, + mut buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let inputselector = InputSelector::from_instance(element); + + let (stickies, is_active, sync_future, switched_pad) = { + let mut state = inputselector.state.lock().unwrap(); + let mut inner = self.0.lock().unwrap(); + let mut stickies = vec![]; + let mut sync_future = None; + let switched_pad = state.switched_pad; + + if let Some(segment) = &inner.segment { + if let Some(segment) = segment.downcast_ref::<gst::format::Time>() { + let rtime = segment.to_running_time(buffer.get_pts()); + let (sync_fut, abort_handle) = abortable(self.sync(&element, rtime)); + inner.abort_handle = Some(abort_handle); + sync_future = Some(sync_fut.map_err(|_| gst::FlowError::Flushing)); + } + } + + let is_active = { + if state.active_sinkpad.as_ref() == Some(pad.gst_pad()) { + if inner.send_sticky || state.switched_pad { + pad.gst_pad().sticky_events_foreach(|event| { + stickies.push(event.clone()); + Ok(Some(event)) + }); + + inner.send_sticky = false; + state.switched_pad = false; + } + true + } else { + false + } + }; + + (stickies, is_active, sync_future, switched_pad) + }; + + if let Some(sync_fut) = sync_future { + sync_fut.await?; + } + + for event in stickies { + inputselector.src_pad.push_event(event).await; + } + + if is_active { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", buffer); + + if switched_pad && !buffer.get_flags().contains(gst::BufferFlags::DISCONT) { + let buffer = buffer.make_mut(); + buffer.set_flags(gst::BufferFlags::DISCONT); + } + + inputselector.src_pad.push(buffer).await + } else { + Ok(gst::FlowSuccess::Ok) + } + } +} + +impl PadSinkHandler for InputSelectorPadSinkHandler { + type ElementImpl = InputSelector; + + fn sink_chain( + &self, + pad: &PadSinkRef, + _inputselector: &InputSelector, + element: &gst::Element, + buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let this = self.clone(); + let element = element.clone(); + let pad_weak = pad.downgrade(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + this.handle_item(&pad, &element, buffer).await + } + .boxed() + } + + fn sink_chain_list( + &self, + pad: &PadSinkRef, + _inputselector: &InputSelector, + element: &gst::Element, + list: gst::BufferList, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let this = self.clone(); + let element = element.clone(); + let pad_weak = pad.downgrade(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + gst_log!(CAT, obj: pad.gst_pad(), "Handling buffer list {:?}", list); + // TODO: Ideally we would keep the list intact and forward it in one go + for buffer in list.iter_owned() { + this.handle_item(&pad, &element, buffer).await?; + } + + Ok(gst::FlowSuccess::Ok) + } + .boxed() + } + + fn sink_event_serialized( + &self, + _pad: &PadSinkRef, + _inputselector: &InputSelector, + _element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + let this = self.clone(); + + async move { + let mut inner = this.0.lock().unwrap(); + + // Remember the segment for later use + if let gst::EventView::Segment(e) = event.view() { + inner.segment = Some(e.get_segment().clone()); + } + + // We sent sticky events together with the next buffer once it becomes + // the active pad. + // + // TODO: Other serialized events for the active pad can also be forwarded + // here, and sticky events could be forwarded directly. Needs forwarding of + // all other sticky events first! + if event.is_sticky() { + inner.send_sticky = true; + true + } else { + true + } + } + .boxed() + } + + fn sink_event( + &self, + _pad: &PadSinkRef, + inputselector: &InputSelector, + _element: &gst::Element, + event: gst::Event, + ) -> bool { + /* Drop all events for now */ + if let gst::EventView::FlushStart(..) = event.view() { + /* Unblock downstream */ + inputselector.src_pad.gst_pad().push_event(event.clone()); + + let mut inner = self.0.lock().unwrap(); + + if let Some(abort_handle) = inner.abort_handle.take() { + abort_handle.abort(); + } + } + true + } + + fn sink_query( + &self, + pad: &PadSinkRef, + inputselector: &InputSelector, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + gst_log!(CAT, obj: pad.gst_pad(), "Handling query {:?}", query); + + if query.is_serialized() { + // FIXME: How can we do this (drops ALLOCATION and DRAIN)? + gst_log!(CAT, obj: pad.gst_pad(), "Dropping serialized query {:?}", query); + false + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding query {:?}", query); + inputselector.src_pad.gst_pad().peer_query(query) + } + } +} + +#[derive(Clone, Debug)] +struct InputSelectorPadSrcHandler; + +impl InputSelectorPadSrcHandler {} + +impl PadSrcHandler for InputSelectorPadSrcHandler { + type ElementImpl = InputSelector; + + fn src_query( + &self, + pad: &PadSrcRef, + inputselector: &InputSelector, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", query); + + match query.view_mut() { + QueryView::Latency(ref mut q) => { + let mut ret = true; + let mut min_latency = 0.into(); + let mut max_latency = gst::CLOCK_TIME_NONE; + let pads = { + let pads = inputselector.pads.lock().unwrap(); + pads.sink_pads + .iter() + .map(|p| p.0.clone()) + .collect::<Vec<_>>() + }; + + for pad in pads { + let mut peer_query = gst::query::Query::new_latency(); + + ret = pad.peer_query(&mut peer_query); + + if ret { + let (live, min, max) = peer_query.get_result(); + if live { + min_latency = std::cmp::max(min, min_latency); + if max_latency.is_none() && max.is_some() { + max_latency = max; + } else if max_latency.is_some() && max.is_some() { + max_latency = std::cmp::min(max, max_latency); + } + } + } + } + + q.set(true, min_latency, max_latency); + + ret + } + _ => { + let sinkpad = { + let state = inputselector.state.lock().unwrap(); + state.active_sinkpad.clone() + }; + + if let Some(sinkpad) = sinkpad { + sinkpad.peer_query(query) + } else { + true + } + } + } + } +} + +#[derive(Debug)] +struct State { + active_sinkpad: Option<gst::Pad>, + switched_pad: bool, +} + +impl Default for State { + fn default() -> State { + State { + active_sinkpad: None, + switched_pad: true, + } + } +} + +#[derive(Debug)] +struct Pads { + pad_serial: u32, + sink_pads: HashMap<gst::Pad, PadSink>, +} + +impl Default for Pads { + fn default() -> Pads { + Pads { + pad_serial: 0, + sink_pads: HashMap::new(), + } + } +} + +#[derive(Debug)] +struct InputSelector { + src_pad: PadSrc, + state: Mutex<State>, + settings: Mutex<Settings>, + pads: Mutex<Pads>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-input-selector", + gst::DebugColorFlags::empty(), + Some("Thread-sharing input selector"), + ); +} + +impl InputSelector { + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_debug!(CAT, obj: element, "Preparing"); + + self.src_pad.prepare(&InputSelectorPadSrcHandler); + + let pads = self.pads.lock().unwrap(); + for pad in pads.sink_pads.values() { + pad.prepare(&InputSelectorPadSinkHandler::new()); + } + + gst_debug!(CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + gst_debug!(CAT, obj: element, "Unpreparing"); + + self.src_pad.unprepare(); + + let pads = self.pads.lock().unwrap(); + for pad in pads.sink_pads.values() { + pad.unprepare(); + } + + *state = State::default(); + + gst_debug!(CAT, obj: element, "Unprepared"); + + Ok(()) + } +} + +impl ObjectSubclass for InputSelector { + const NAME: &'static str = "RsTsInputSelector"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing input selector", + "Generic", + "Simple input selector element", + "Mathieu Duponchelle <mathieu@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let sink_pad_template = gst::PadTemplate::new( + "sink_%u", + gst::PadDirection::Sink, + gst::PadPresence::Request, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + state: Mutex::new(State::default()), + settings: Mutex::new(Settings::default()), + pads: Mutex::new(Pads::default()), + } + } +} + +impl ObjectImpl for InputSelector { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("context", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.context_wait = value.get_some().expect("type checked upstream"); + } + subclass::Property("active-pad", ..) => { + let pad = value.get::<gst::Pad>().expect("type checked upstream"); + let mut state = self.state.lock().unwrap(); + let pads = self.pads.lock().unwrap(); + let mut old_pad = None; + if let Some(ref pad) = pad { + if pads.sink_pads.get(&pad).is_some() { + old_pad = state.active_sinkpad.clone(); + state.active_sinkpad = Some(pad.clone()); + state.switched_pad = true; + } + } else { + state.active_sinkpad = None; + } + + drop(pads); + drop(state); + + if let Some(old_pad) = old_pad { + if Some(&old_pad) != pad.as_ref() { + let _ = old_pad.push_event(gst::Event::new_reconfigure().build()); + } + } + + if let Some(pad) = pad { + let _ = pad.push_event(gst::Event::new_reconfigure().build()); + } + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("context", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.context.to_value()) + } + subclass::Property("context-wait", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.context_wait.to_value()) + } + subclass::Property("active-pad", ..) => { + let state = self.state.lock().unwrap(); + let active_pad = state.active_sinkpad.clone(); + Ok(active_pad.to_value()) + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + } +} + +impl ElementImpl for InputSelector { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::ReadyToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + _ => (), + } + + Ok(success) + } + + fn request_new_pad( + &self, + element: &gst::Element, + templ: &gst::PadTemplate, + _name: Option<String>, + _caps: Option<&gst::Caps>, + ) -> Option<gst::Pad> { + let mut state = self.state.lock().unwrap(); + let mut pads = self.pads.lock().unwrap(); + let sink_pad = + gst::Pad::new_from_template(&templ, Some(format!("sink_{}", pads.pad_serial).as_str())); + pads.pad_serial += 1; + sink_pad.set_active(true).unwrap(); + element.add_pad(&sink_pad).unwrap(); + let sink_pad = PadSink::new(sink_pad); + let ret = sink_pad.gst_pad().clone(); + + sink_pad.prepare(&InputSelectorPadSinkHandler::new()); + + if state.active_sinkpad.is_none() { + state.active_sinkpad = Some(ret.clone()); + state.switched_pad = true; + } + + pads.sink_pads.insert(ret.clone(), sink_pad); + drop(pads); + drop(state); + + let _ = element.post_message(&gst::Message::new_latency().src(Some(element)).build()); + + Some(ret) + } + + fn release_pad(&self, element: &gst::Element, pad: &gst::Pad) { + let mut pads = self.pads.lock().unwrap(); + let sink_pad = pads.sink_pads.remove(pad).unwrap(); + sink_pad.unprepare(); + element.remove_pad(pad).unwrap(); + drop(pads); + + let _ = element.post_message(&gst::Message::new_latency().src(Some(element)).build()); + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-input-selector", + gst::Rank::None, + InputSelector::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/src/jitterbuffer/jitterbuffer.rs b/generic/gst-plugin-threadshare/src/jitterbuffer/jitterbuffer.rs new file mode 100644 index 000000000..6e2a2244e --- /dev/null +++ b/generic/gst-plugin-threadshare/src/jitterbuffer/jitterbuffer.rs @@ -0,0 +1,1584 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::future::BoxFuture; +use futures::future::{abortable, AbortHandle, Aborted}; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_error, gst_error_msg, gst_info, gst_log, gst_trace}; +use gst_rtp::RTPBuffer; + +use lazy_static::lazy_static; + +use std::cmp::{max, min, Ordering}; +use std::collections::{BTreeSet, VecDeque}; +use std::mem; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::time::Duration; + +use crate::get_current_running_time; +use crate::runtime::prelude::*; +use crate::runtime::{self, Context, PadSink, PadSinkRef, PadSrc, PadSrcRef, Task}; + +use super::{RTPJitterBuffer, RTPJitterBufferItem, RTPPacketRateCtx}; + +const DEFAULT_LATENCY_MS: u32 = 200; +const DEFAULT_DO_LOST: bool = false; +const DEFAULT_MAX_DROPOUT_TIME: u32 = 60000; +const DEFAULT_MAX_MISORDER_TIME: u32 = 2000; +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; + +#[derive(Debug, Clone)] +struct Settings { + latency_ms: u32, + do_lost: bool, + max_dropout_time: u32, + max_misorder_time: u32, + context: String, + context_wait: u32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + latency_ms: DEFAULT_LATENCY_MS, + do_lost: DEFAULT_DO_LOST, + max_dropout_time: DEFAULT_MAX_DROPOUT_TIME, + max_misorder_time: DEFAULT_MAX_MISORDER_TIME, + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + } + } +} + +static PROPERTIES: [subclass::Property; 7] = [ + subclass::Property("latency", |name| { + glib::ParamSpec::uint( + name, + "Buffer latency in ms", + "Amount of ms to buffer", + 0, + std::u32::MAX, + DEFAULT_LATENCY_MS, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("do-lost", |name| { + glib::ParamSpec::boolean( + name, + "Do Lost", + "Send an event downstream when a packet is lost", + DEFAULT_DO_LOST, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-dropout-time", |name| { + glib::ParamSpec::uint( + name, + "Max dropout time", + "The maximum time (milliseconds) of missing packets tolerated.", + 0, + std::u32::MAX, + DEFAULT_MAX_DROPOUT_TIME, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-misorder-time", |name| { + glib::ParamSpec::uint( + name, + "Max misorder time", + "The maximum time (milliseconds) of misordered packets tolerated.", + 0, + std::u32::MAX, + DEFAULT_MAX_MISORDER_TIME, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("stats", |name| { + glib::ParamSpec::boxed( + name, + "Statistics", + "Various statistics", + gst::Structure::static_type(), + glib::ParamFlags::READABLE, + ) + }), + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Eq)] +struct GapPacket { + buffer: gst::Buffer, + seq: u16, + pt: u8, +} + +impl GapPacket { + fn new(buffer: gst::Buffer) -> Self { + let mut rtp_buffer = RTPBuffer::from_buffer_readable(&buffer).unwrap(); + let seq = rtp_buffer.get_seq(); + let pt = rtp_buffer.get_payload_type(); + drop(rtp_buffer); + + Self { buffer, seq, pt } + } +} + +impl Ord for GapPacket { + fn cmp(&self, other: &Self) -> Ordering { + 0.cmp(&gst_rtp::compare_seqnum(self.seq, other.seq)) + } +} + +impl PartialOrd for GapPacket { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl PartialEq for GapPacket { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +struct SinkHandlerInner { + packet_rate_ctx: RTPPacketRateCtx, + ips_rtptime: Option<u32>, + ips_pts: gst::ClockTime, + + gap_packets: BTreeSet<GapPacket>, + + last_pt: Option<u8>, + + last_in_seqnum: Option<u16>, + last_rtptime: Option<u32>, +} + +impl Default for SinkHandlerInner { + fn default() -> Self { + SinkHandlerInner { + packet_rate_ctx: RTPPacketRateCtx::new(), + ips_rtptime: None, + ips_pts: gst::CLOCK_TIME_NONE, + gap_packets: BTreeSet::new(), + last_pt: None, + last_in_seqnum: None, + last_rtptime: None, + } + } +} + +#[derive(Clone)] +struct SinkHandler(Arc<StdMutex<SinkHandlerInner>>); + +impl SinkHandler { + fn new() -> Self { + SinkHandler(Arc::new(StdMutex::new(SinkHandlerInner::default()))) + } + + fn clear(&self) { + let mut inner = self.0.lock().unwrap(); + *inner = SinkHandlerInner::default(); + } + + // For resetting if seqnum discontinuities + fn reset( + &self, + inner: &mut SinkHandlerInner, + state: &mut State, + element: &gst::Element, + ) -> BTreeSet<GapPacket> { + gst_info!(CAT, obj: element, "Resetting"); + + state.jbuf.borrow().flush(); + state.jbuf.borrow().reset_skew(); + state.discont = true; + + state.last_popped_seqnum = None; + state.last_popped_pts = gst::CLOCK_TIME_NONE; + + inner.last_in_seqnum = None; + inner.last_rtptime = None; + + state.earliest_pts = gst::CLOCK_TIME_NONE; + state.earliest_seqnum = None; + + inner.ips_rtptime = None; + inner.ips_pts = gst::CLOCK_TIME_NONE; + + mem::replace(&mut inner.gap_packets, BTreeSet::new()) + } + + fn parse_caps( + &self, + inner: &mut SinkHandlerInner, + state: &mut State, + element: &gst::Element, + caps: &gst::Caps, + pt: u8, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let s = caps.get_structure(0).ok_or(gst::FlowError::Error)?; + + gst_info!(CAT, obj: element, "Parsing {:?}", caps); + + let payload = s + .get_some::<i32>("payload") + .map_err(|_| gst::FlowError::Error)?; + + if pt != 0 && payload as u8 != pt { + return Err(gst::FlowError::Error); + } + + inner.last_pt = Some(pt); + let clock_rate = s + .get_some::<i32>("clock-rate") + .map_err(|_| gst::FlowError::Error)?; + + if clock_rate <= 0 { + return Err(gst::FlowError::Error); + } + state.clock_rate = Some(clock_rate as u32); + + inner.packet_rate_ctx.reset(clock_rate); + state.jbuf.borrow().set_clock_rate(clock_rate as u32); + + Ok(gst::FlowSuccess::Ok) + } + + fn calculate_packet_spacing( + &self, + inner: &mut SinkHandlerInner, + state: &mut State, + rtptime: u32, + pts: gst::ClockTime, + ) { + if inner.ips_rtptime != Some(rtptime) { + if inner.ips_pts.is_some() && pts.is_some() { + let new_packet_spacing = pts - inner.ips_pts; + let old_packet_spacing = state.packet_spacing; + + if old_packet_spacing > new_packet_spacing { + state.packet_spacing = (new_packet_spacing + 3 * old_packet_spacing) / 4; + } else if old_packet_spacing > gst::ClockTime(Some(0)) { + state.packet_spacing = (3 * new_packet_spacing + old_packet_spacing) / 4; + } else { + state.packet_spacing = new_packet_spacing; + } + + gst_debug!( + CAT, + "new packet spacing {}, old packet spacing {} combined to {}", + new_packet_spacing, + old_packet_spacing, + state.packet_spacing + ); + } + inner.ips_rtptime = Some(rtptime); + inner.ips_pts = pts; + } + } + + fn handle_big_gap_buffer( + &self, + inner: &mut SinkHandlerInner, + element: &gst::Element, + buffer: gst::Buffer, + pt: u8, + ) -> bool { + let gap_packets_length = inner.gap_packets.len(); + let mut reset = false; + + gst_debug!( + CAT, + obj: element, + "Handling big gap, gap packets length: {}", + gap_packets_length + ); + + inner.gap_packets.insert(GapPacket::new(buffer)); + + if gap_packets_length > 0 { + let mut prev_gap_seq = std::u32::MAX; + let mut all_consecutive = true; + + for gap_packet in inner.gap_packets.iter() { + gst_log!( + CAT, + obj: element, + "Looking at gap packet with seq {}", + gap_packet.seq, + ); + + all_consecutive = gap_packet.pt == pt; + + if prev_gap_seq == std::u32::MAX { + prev_gap_seq = gap_packet.seq as u32; + } else if gst_rtp::compare_seqnum(gap_packet.seq, prev_gap_seq as u16) != -1 { + all_consecutive = false; + } else { + prev_gap_seq = gap_packet.seq as u32; + } + + if !all_consecutive { + break; + } + } + + gst_debug!(CAT, obj: element, "all consecutive: {}", all_consecutive); + + if all_consecutive && gap_packets_length > 3 { + reset = true; + } else if !all_consecutive { + inner.gap_packets.clear(); + } + } + + reset + } + + fn store( + &self, + inner: &mut SinkHandlerInner, + pad: &gst::Pad, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let jb = JitterBuffer::from_instance(element); + let mut state = jb.state.lock().unwrap(); + + let (max_misorder_time, max_dropout_time) = { + let settings = jb.settings.lock().unwrap(); + (settings.max_misorder_time, settings.max_dropout_time) + }; + + let (seq, rtptime, pt) = { + let mut rtp_buffer = + RTPBuffer::from_buffer_readable(&buffer).map_err(|_| gst::FlowError::Error)?; + ( + rtp_buffer.get_seq(), + rtp_buffer.get_timestamp(), + rtp_buffer.get_payload_type(), + ) + }; + + let mut pts = buffer.get_pts(); + let mut dts = buffer.get_dts(); + let mut estimated_dts = false; + + gst_log!( + CAT, + obj: element, + "Storing buffer, seq: {}, rtptime: {}, pt: {}", + seq, + rtptime, + pt + ); + + if dts == gst::CLOCK_TIME_NONE { + dts = pts; + } else if pts == gst::CLOCK_TIME_NONE { + pts = dts; + } + + if dts == gst::CLOCK_TIME_NONE { + dts = get_current_running_time(element); + pts = dts; + + estimated_dts = state.clock_rate.is_some(); + } else { + dts = state.segment.to_running_time(dts); + } + + if state.clock_rate.is_none() { + inner.ips_rtptime = Some(rtptime); + inner.ips_pts = pts; + } + + if inner.last_pt != Some(pt) { + inner.last_pt = Some(pt); + state.clock_rate = None; + + gst_debug!(CAT, obj: pad, "New payload type: {}", pt); + + if let Some(caps) = pad.get_current_caps() { + /* Ignore errors at this point, as we want to emit request-pt-map */ + let _ = self.parse_caps(inner, &mut state, element, &caps, pt); + } + } + + if state.clock_rate.is_none() { + let caps = element + .emit("request-pt-map", &[&(pt as u32)]) + .map_err(|_| gst::FlowError::Error)? + .ok_or(gst::FlowError::Error)? + .get::<gst::Caps>() + .map_err(|_| gst::FlowError::Error)? + .ok_or(gst::FlowError::Error)?; + self.parse_caps(inner, &mut state, element, &caps, pt)?; + } + + inner.packet_rate_ctx.update(seq, rtptime); + + let max_dropout = inner + .packet_rate_ctx + .get_max_dropout(max_dropout_time as i32); + let max_misorder = inner + .packet_rate_ctx + .get_max_dropout(max_misorder_time as i32); + + pts = state.jbuf.borrow().calculate_pts( + dts, + estimated_dts, + rtptime, + element.get_base_time(), + 0, + false, + ); + + if pts.is_none() { + gst_debug!( + CAT, + obj: element, + "cannot calculate a valid pts for #{}, discard", + seq + ); + return Ok(gst::FlowSuccess::Ok); + } + + if let Some(last_in_seqnum) = inner.last_in_seqnum { + let gap = gst_rtp::compare_seqnum(last_in_seqnum as u16, seq); + if gap == 1 { + self.calculate_packet_spacing(inner, &mut state, rtptime, pts); + } else { + if (gap != -1 && gap < -(max_misorder as i32)) || (gap >= max_dropout as i32) { + let reset = self.handle_big_gap_buffer(inner, element, buffer, pt); + if reset { + // Handle reset in `enqueue_item` to avoid recursion + return Err(gst::FlowError::CustomError); + } else { + return Ok(gst::FlowSuccess::Ok); + } + } + inner.ips_pts = gst::CLOCK_TIME_NONE; + inner.ips_rtptime = None; + } + + inner.gap_packets.clear(); + } + + if let Some(last_popped_seqnum) = state.last_popped_seqnum { + let gap = gst_rtp::compare_seqnum(last_popped_seqnum, seq); + + if gap <= 0 { + state.stats.num_late += 1; + gst_debug!(CAT, obj: element, "Dropping late {}", seq); + return Ok(gst::FlowSuccess::Ok); + } + } + + inner.last_in_seqnum = Some(seq); + + let jb_item = if estimated_dts { + RTPJitterBufferItem::new(buffer, gst::CLOCK_TIME_NONE, pts, Some(seq), rtptime) + } else { + RTPJitterBufferItem::new(buffer, dts, pts, Some(seq), rtptime) + }; + + let (success, _, _) = state.jbuf.borrow().insert(jb_item); + + if !success { + /* duplicate */ + return Ok(gst::FlowSuccess::Ok); + } + + if Some(rtptime) == inner.last_rtptime { + state.equidistant -= 2; + } else { + state.equidistant += 1; + } + + state.equidistant = min(max(state.equidistant, -7), 7); + + inner.last_rtptime = Some(rtptime); + + if state.earliest_pts.is_none() + || (pts.is_some() + && (pts < state.earliest_pts + || (pts == state.earliest_pts + && state + .earliest_seqnum + .map(|earliest_seqnum| seq > earliest_seqnum) + .unwrap_or(false)))) + { + state.earliest_pts = pts; + state.earliest_seqnum = Some(seq); + } + + gst_log!(CAT, obj: pad, "Stored buffer"); + + Ok(gst::FlowSuccess::Ok) + } + + fn enqueue_item( + &self, + pad: &gst::Pad, + element: &gst::Element, + buffer: Option<gst::Buffer>, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let mut inner = self.0.lock().unwrap(); + + let mut buffers = VecDeque::new(); + if let Some(buf) = buffer { + buffers.push_back(buf); + } + + // This is to avoid recursion with `store`, `reset` and `enqueue_item` + while let Some(buf) = buffers.pop_front() { + if let Err(err) = self.store(&mut inner, pad, element, buf) { + match err { + gst::FlowError::CustomError => { + let jb = JitterBuffer::from_instance(element); + let mut state = jb.state.lock().unwrap(); + for gap_packet in self.reset(&mut inner, &mut state, element) { + buffers.push_back(gap_packet.buffer); + } + } + other => return Err(other), + } + } + } + + let jb = JitterBuffer::from_instance(element); + let mut state = jb.state.lock().unwrap(); + + let (latency, context_wait) = { + let settings = jb.settings.lock().unwrap(); + ( + settings.latency_ms as u64 * gst::MSECOND, + settings.context_wait as u64 * gst::MSECOND, + ) + }; + + // Reschedule if needed + let (_, next_wakeup) = + jb.src_pad_handler + .get_next_wakeup(&element, &state, latency, context_wait); + if let Some((next_wakeup, _)) = next_wakeup { + if let Some((previous_next_wakeup, ref abort_handle)) = state.wait_handle { + if previous_next_wakeup.is_none() || previous_next_wakeup > next_wakeup { + gst_debug!( + CAT, + obj: pad, + "Rescheduling for new item {} < {}", + next_wakeup, + previous_next_wakeup + ); + abort_handle.abort(); + state.wait_handle = None; + } + } + } + state.last_res + } +} + +impl PadSinkHandler for SinkHandler { + type ElementImpl = JitterBuffer; + + fn sink_chain( + &self, + pad: &PadSinkRef, + _jb: &JitterBuffer, + element: &gst::Element, + buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let pad_weak = pad.downgrade(); + let element = element.clone(); + let this = self.clone(); + + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + + gst_debug!(CAT, obj: pad.gst_pad(), "Handling {:?}", buffer); + this.enqueue_item(pad.gst_pad(), &element, Some(buffer)) + } + .boxed() + } + + fn sink_event( + &self, + pad: &PadSinkRef, + jb: &JitterBuffer, + _element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + if let EventView::FlushStart(..) = event.view() { + jb.task.cancel(); + } + + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + jb.src_pad.gst_pad().push_event(event) + } + + fn sink_event_serialized( + &self, + pad: &PadSinkRef, + _jb: &JitterBuffer, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + use gst::EventView; + + let pad_weak = pad.downgrade(); + let element = element.clone(); + + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + let jb = JitterBuffer::from_instance(&element); + + let mut forward = true; + match event.view() { + EventView::Segment(e) => { + let mut state = jb.state.lock().unwrap(); + state.segment = e + .get_segment() + .clone() + .downcast::<gst::format::Time>() + .unwrap(); + } + EventView::FlushStop(..) => { + jb.flush_stop(&element); + } + EventView::Eos(..) => { + let mut state = jb.state.lock().unwrap(); + state.eos = true; + if let Some((_, abort_handle)) = state.wait_handle.take() { + abort_handle.abort(); + } + forward = false; + } + _ => (), + }; + + if forward { + // FIXME: These events should really be queued up and stay in order + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding serialized {:?}", event); + jb.src_pad.push_event(event).await + } else { + true + } + } + .boxed() + } +} + +#[derive(Clone)] +struct SrcHandler; + +impl SrcHandler { + fn new() -> Self { + SrcHandler + } + + fn clear(&self) {} + + fn generate_lost_events( + &self, + state: &mut State, + element: &gst::Element, + seqnum: u16, + pts: gst::ClockTime, + discont: &mut bool, + ) -> Vec<gst::Event> { + let (latency_ns, do_lost) = { + let jb = JitterBuffer::from_instance(element); + let settings = jb.settings.lock().unwrap(); + ( + settings.latency_ms as u64 * gst::MSECOND.nseconds().unwrap(), + settings.do_lost, + ) + }; + + let mut events = vec![]; + + let last_popped_seqnum = match state.last_popped_seqnum { + None => return events, + Some(seq) => seq, + }; + + gst_debug!( + CAT, + obj: element, + "Generating lost events seq: {}, last popped seq: {:?}", + seqnum, + last_popped_seqnum, + ); + + let mut lost_seqnum = last_popped_seqnum.wrapping_add(1); + let gap = gst_rtp::compare_seqnum(lost_seqnum, seqnum) as i64; + + if gap > 0 { + let interval = + pts.nseconds().unwrap() as i64 - state.last_popped_pts.nseconds().unwrap() as i64; + let gap = gap as u64; + let spacing = if interval >= 0 { + interval as u64 / (gap + 1) + } else { + 0 + }; + + *discont = true; + + if state.equidistant > 0 && gap > 1 && gap * spacing > latency_ns { + let n_packets = gap - latency_ns / spacing; + + if do_lost { + let s = gst::Structure::new( + "GstRTPPacketLost", + &[ + ("seqnum", &(lost_seqnum as u32)), + ( + "timestamp", + &(state.last_popped_pts + gst::ClockTime(Some(spacing))), + ), + ("duration", &(n_packets * spacing)), + ("retry", &0), + ], + ); + + events.push(gst::Event::new_custom_downstream(s).build()); + } + + lost_seqnum = lost_seqnum.wrapping_add(n_packets as u16); + state.last_popped_pts += gst::ClockTime(Some(n_packets * spacing)); + state.stats.num_lost += n_packets; + } + + while lost_seqnum != seqnum { + let timestamp = state.last_popped_pts + gst::ClockTime(Some(spacing)); + let duration = if state.equidistant > 0 { spacing } else { 0 }; + + state.last_popped_pts = timestamp; + + if do_lost { + let s = gst::Structure::new( + "GstRTPPacketLost", + &[ + ("seqnum", &(lost_seqnum as u32)), + ("timestamp", ×tamp), + ("duration", &duration), + ("retry", &0), + ], + ); + + events.push(gst::Event::new_custom_downstream(s).build()); + } + + state.stats.num_lost += 1; + + lost_seqnum = lost_seqnum.wrapping_add(1); + } + } + + events + } + + async fn pop_and_push( + &self, + element: &gst::Element, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let jb = JitterBuffer::from_instance(element); + + let (lost_events, buffer, seq) = { + let mut state = jb.state.lock().unwrap(); + + let mut discont = false; + let (jb_item, _) = state.jbuf.borrow().pop(); + + let jb_item = match jb_item { + None => { + if state.eos { + return Err(gst::FlowError::Eos); + } else { + return Ok(gst::FlowSuccess::Ok); + } + } + Some(item) => item, + }; + + let dts = jb_item.get_dts(); + let pts = jb_item.get_pts(); + let seq = jb_item.get_seqnum(); + let mut buffer = jb_item.into_buffer(); + + let lost_events = { + let buffer = buffer.make_mut(); + + buffer.set_dts(state.segment.to_running_time(dts)); + buffer.set_pts(state.segment.to_running_time(pts)); + + if state.last_popped_pts.is_some() && buffer.get_pts() < state.last_popped_pts { + buffer.set_pts(state.last_popped_pts) + } + + let lost_events = if let Some(seq) = seq { + self.generate_lost_events(&mut state, element, seq, pts, &mut discont) + } else { + vec![] + }; + + if state.discont { + discont = true; + state.discont = false; + } + + if discont { + buffer.set_flags(gst::BufferFlags::DISCONT); + } + + lost_events + }; + + state.last_popped_pts = buffer.get_pts(); + if let Some(pts) = state.last_popped_pts.nseconds() { + state.position = pts.into(); + } + state.last_popped_seqnum = seq; + + state.stats.num_pushed += 1; + + (lost_events, buffer, seq) + }; + + for event in lost_events { + gst_debug!(CAT, obj: jb.src_pad.gst_pad(), "Pushing lost event {:?}", event); + let _ = jb.src_pad.push_event(event).await; + } + + gst_debug!(CAT, obj: jb.src_pad.gst_pad(), "Pushing {:?} with seq {:?}", buffer, seq); + + jb.src_pad.push(buffer).await + } + + fn get_next_wakeup( + &self, + element: &gst::Element, + state: &State, + latency: gst::ClockTime, + context_wait: gst::ClockTime, + ) -> (gst::ClockTime, Option<(gst::ClockTime, Duration)>) { + let now = get_current_running_time(element); + + gst_debug!( + CAT, + obj: element, + "Now is {}, EOS {}, earliest pts is {}, packet_spacing {} and latency {}", + now, + state.eos, + state.earliest_pts, + state.packet_spacing, + latency + ); + + if state.eos { + gst_debug!(CAT, obj: element, "EOS, not waiting"); + return (now, Some((now, Duration::from_nanos(0)))); + } + + if state.earliest_pts.is_none() { + return (now, None); + } + + let next_wakeup = state.earliest_pts + latency - state.packet_spacing - context_wait / 2; + + let delay = { + if next_wakeup > now { + (next_wakeup - now).nseconds().unwrap() + } else { + 0 + } + }; + + gst_debug!( + CAT, + obj: element, + "Next wakeup at {} with delay {}", + next_wakeup, + delay + ); + + (now, Some((next_wakeup, Duration::from_nanos(delay)))) + } +} + +impl PadSrcHandler for SrcHandler { + type ElementImpl = JitterBuffer; + + fn src_event( + &self, + pad: &PadSrcRef, + jb: &JitterBuffer, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + match event.view() { + EventView::FlushStart(..) => { + jb.task.cancel(); + } + EventView::FlushStop(..) => { + jb.flush_stop(element); + } + _ => (), + } + + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + jb.sink_pad.gst_pad().push_event(event) + } + + fn src_query( + &self, + pad: &PadSrcRef, + jb: &JitterBuffer, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", query); + + match query.view_mut() { + QueryView::Latency(ref mut q) => { + let mut peer_query = gst::query::Query::new_latency(); + + let ret = jb.sink_pad.gst_pad().peer_query(&mut peer_query); + + if ret { + let settings = jb.settings.lock().unwrap(); + let (_, mut min_latency, _) = peer_query.get_result(); + min_latency += (settings.latency_ms as u64) * gst::SECOND; + let max_latency = gst::CLOCK_TIME_NONE; + + q.set(true, min_latency, max_latency); + } + + ret + } + QueryView::Position(ref mut q) => { + if q.get_format() != gst::Format::Time { + jb.sink_pad.gst_pad().peer_query(query) + } else { + let state = jb.state.lock().unwrap(); + let position = state.position; + q.set(position); + true + } + } + _ => jb.sink_pad.gst_pad().peer_query(query), + } + } +} + +#[derive(Debug)] +struct Stats { + num_pushed: u64, + num_lost: u64, + num_late: u64, +} + +impl Default for Stats { + fn default() -> Self { + Self { + num_pushed: 0, + num_lost: 0, + num_late: 0, + } + } +} + +// Shared state between element, sink and source pad +struct State { + jbuf: glib::SendUniqueCell<RTPJitterBuffer>, + + last_res: Result<gst::FlowSuccess, gst::FlowError>, + position: gst::ClockTime, + + segment: gst::FormattedSegment<gst::ClockTime>, + clock_rate: Option<u32>, + + packet_spacing: gst::ClockTime, + equidistant: i32, + + discont: bool, + eos: bool, + + last_popped_seqnum: Option<u16>, + last_popped_pts: gst::ClockTime, + + stats: Stats, + + earliest_pts: gst::ClockTime, + earliest_seqnum: Option<u16>, + + wait_handle: Option<(gst::ClockTime, AbortHandle)>, +} + +impl Default for State { + fn default() -> State { + State { + jbuf: glib::SendUniqueCell::new(RTPJitterBuffer::new()).unwrap(), + + last_res: Ok(gst::FlowSuccess::Ok), + position: gst::CLOCK_TIME_NONE, + + segment: gst::FormattedSegment::<gst::ClockTime>::new(), + clock_rate: None, + + packet_spacing: gst::ClockTime(Some(0)), + equidistant: 0, + + discont: true, + eos: false, + + last_popped_seqnum: None, + last_popped_pts: gst::CLOCK_TIME_NONE, + + stats: Stats::default(), + + earliest_pts: gst::CLOCK_TIME_NONE, + earliest_seqnum: None, + + wait_handle: None, + } + } +} + +struct JitterBuffer { + sink_pad: PadSink, + src_pad: PadSrc, + sink_pad_handler: SinkHandler, + src_pad_handler: SrcHandler, + task: Task, + state: StdMutex<State>, + settings: StdMutex<Settings>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-jitterbuffer", + gst::DebugColorFlags::empty(), + Some("Thread-sharing jitterbuffer"), + ); +} + +impl JitterBuffer { + fn clear_pt_map(&self, element: &gst::Element) { + gst_info!(CAT, obj: element, "Clearing PT map"); + + let mut state = self.state.lock().unwrap(); + state.clock_rate = None; + state.jbuf.borrow().reset_skew(); + } + + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_info!(CAT, obj: element, "Preparing"); + + let context = { + let settings = self.settings.lock().unwrap(); + Context::acquire(&settings.context, settings.context_wait).unwrap() + }; + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + + self.src_pad.prepare(&self.src_pad_handler); + self.sink_pad.prepare(&self.sink_pad_handler); + + gst_info!(CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) { + gst_debug!(CAT, obj: element, "Unpreparing"); + + self.task.unprepare().unwrap(); + self.sink_pad.unprepare(); + self.src_pad.unprepare(); + + gst_debug!(CAT, obj: element, "Unprepared"); + } + + fn start(&self, element: &gst::Element) { + gst_debug!(CAT, obj: element, "Starting"); + + *self.state.lock().unwrap() = State::default(); + self.sink_pad_handler.clear(); + self.src_pad_handler.clear(); + + self.start_task(element); + + gst_debug!(CAT, obj: element, "Started"); + } + + fn start_task(&self, element: &gst::Element) { + let src_pad_handler = self.src_pad_handler.clone(); + let element = element.clone(); + + self.task.start(move || { + let src_pad_handler = src_pad_handler.clone(); + let element = element.clone(); + + async move { + let jb = JitterBuffer::from_instance(&element); + let (latency, context_wait) = { + let settings = jb.settings.lock().unwrap(); + ( + settings.latency_ms as u64 * gst::MSECOND, + settings.context_wait as u64 * gst::MSECOND, + ) + }; + + loop { + let delay_fut = { + let mut state = jb.state.lock().unwrap(); + let (_, next_wakeup) = src_pad_handler.get_next_wakeup( + &element, + &state, + latency, + context_wait, + ); + + let (delay_fut, abort_handle) = match next_wakeup { + Some((_, delay)) if delay == Duration::from_nanos(0) => (None, None), + _ => { + let (delay_fut, abort_handle) = abortable(async move { + match next_wakeup { + Some((_, delay)) => { + runtime::time::delay_for(delay).await; + } + None => { + future::pending::<()>().await; + } + }; + }); + + let next_wakeup = + next_wakeup.map(|w| w.0).unwrap_or(gst::CLOCK_TIME_NONE); + (Some(delay_fut), Some((next_wakeup, abort_handle))) + } + }; + + state.wait_handle = abort_handle; + + delay_fut + }; + + // Got aborted, reschedule if needed + if let Some(delay_fut) = delay_fut { + gst_debug!(CAT, obj: &element, "Waiting"); + if let Err(Aborted) = delay_fut.await { + gst_debug!(CAT, obj: &element, "Waiting aborted"); + return glib::Continue(true); + } + } + + let (head_pts, head_seq) = { + let state = jb.state.lock().unwrap(); + // + // Check earliest PTS as we have just taken the lock + let (now, next_wakeup) = src_pad_handler.get_next_wakeup( + &element, + &state, + latency, + context_wait, + ); + + gst_debug!( + CAT, + obj: &element, + "Woke up at {}, earliest_pts {}", + now, + state.earliest_pts + ); + + if let Some((next_wakeup, _)) = next_wakeup { + if next_wakeup > now { + // Reschedule and wait a bit longer in the next iteration + return glib::Continue(true); + } + } else { + return glib::Continue(true); + } + + let (head_pts, head_seq) = state.jbuf.borrow().peek(); + + (head_pts, head_seq) + }; + + let res = src_pad_handler.pop_and_push(&element).await; + + { + let mut state = jb.state.lock().unwrap(); + + state.last_res = res; + + if head_pts == state.earliest_pts && head_seq == state.earliest_seqnum { + let (earliest_pts, earliest_seqnum) = + state.jbuf.borrow().find_earliest(); + state.earliest_pts = earliest_pts; + state.earliest_seqnum = earliest_seqnum; + } + + if res.is_ok() { + // Return and reschedule if the next packet would be in the future + // Check earliest PTS as we have just taken the lock + let (now, next_wakeup) = src_pad_handler.get_next_wakeup( + &element, + &state, + latency, + context_wait, + ); + if let Some((next_wakeup, _)) = next_wakeup { + if next_wakeup > now { + // Reschedule and wait a bit longer in the next iteration + return glib::Continue(true); + } + } else { + return glib::Continue(true); + } + } + } + + match res { + Ok(_) => (), + Err(gst::FlowError::Eos) => { + gst_debug!(CAT, obj: &element, "Pushing EOS event",); + let event = gst::Event::new_eos().build(); + let _ = jb.src_pad.push_event(event).await; + return glib::Continue(false); + } + Err(gst::FlowError::Flushing) => { + gst_debug!(CAT, obj: &element, "Flushing",); + return glib::Continue(false); + } + Err(err) => { + gst_error!(CAT, obj: &element, "Error {}", err,); + return glib::Continue(false); + } + } + } + } + }); + } + + fn stop(&self, element: &gst::Element) { + gst_debug!(CAT, obj: element, "Stopping"); + + if let Some((_, abort_handle)) = self.state.lock().unwrap().wait_handle.take() { + abort_handle.abort(); + } + + self.task.stop(); + + self.src_pad_handler.clear(); + self.sink_pad_handler.clear(); + *self.state.lock().unwrap() = State::default(); + + gst_debug!(CAT, obj: element, "Stopped"); + } + + fn flush_stop(&self, element: &gst::Element) { + self.task.stop(); + self.start(element); + } +} + +impl ObjectSubclass for JitterBuffer { + const NAME: &'static str = "RsTsJitterBuffer"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing jitterbuffer", + "Generic", + "Simple jitterbuffer", + "Mathieu Duponchelle <mathieu@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + klass.add_signal( + "request-pt-map", + glib::SignalFlags::RUN_LAST, + &[u32::static_type()], + gst::Caps::static_type(), + ); + + klass.add_signal_with_class_handler( + "clear-pt-map", + glib::SignalFlags::RUN_LAST | glib::SignalFlags::ACTION, + &[], + glib::types::Type::Unit, + |_, args| { + let element = args[0] + .get::<gst::Element>() + .expect("signal arg") + .expect("missing signal arg"); + let jb = Self::from_instance(&element); + jb.clear_pt_map(&element); + None + }, + ); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + klass.install_properties(&PROPERTIES); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + sink_pad: PadSink::new(gst::Pad::new_from_template( + &klass.get_pad_template("sink").unwrap(), + Some("sink"), + )), + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + sink_pad_handler: SinkHandler::new(), + src_pad_handler: SrcHandler::new(), + task: Task::default(), + state: StdMutex::new(State::default()), + settings: StdMutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for JitterBuffer { + glib_object_impl!(); + + fn set_property(&self, obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("latency", ..) => { + let latency_ms = { + let mut settings = self.settings.lock().unwrap(); + settings.latency_ms = value.get_some().expect("type checked upstream"); + settings.latency_ms as u64 + }; + + let state = self.state.lock().unwrap(); + state.jbuf.borrow().set_delay(latency_ms * gst::MSECOND); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + let _ = + element.post_message(&gst::Message::new_latency().src(Some(element)).build()); + } + subclass::Property("do-lost", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.do_lost = value.get_some().expect("type checked upstream"); + } + subclass::Property("max-dropout-time", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.max_dropout_time = value.get_some().expect("type checked upstream"); + } + subclass::Property("max-misorder-time", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.max_misorder_time = value.get_some().expect("type checked upstream"); + } + subclass::Property("context", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + let mut settings = self.settings.lock().unwrap(); + settings.context_wait = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("latency", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.latency_ms.to_value()) + } + subclass::Property("do-lost", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.do_lost.to_value()) + } + subclass::Property("max-dropout-time", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.max_dropout_time.to_value()) + } + subclass::Property("max-misorder-time", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.max_misorder_time.to_value()) + } + subclass::Property("stats", ..) => { + let state = self.state.lock().unwrap(); + let s = gst::Structure::new( + "application/x-rtp-jitterbuffer-stats", + &[ + ("num-pushed", &state.stats.num_pushed), + ("num-lost", &state.stats.num_lost), + ("num-late", &state.stats.num_late), + ], + ); + Ok(s.to_value()) + } + subclass::Property("context", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.context.to_value()) + } + subclass::Property("context-wait", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.context_wait.to_value()) + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.sink_pad.gst_pad()).unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + } +} + +impl ElementImpl for JitterBuffer { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PausedToReady => { + self.stop(element); + } + gst::StateChange::ReadyToNull => { + self.unprepare(element); + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::ReadyToPaused => { + self.start(element); + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + _ => (), + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-jitterbuffer", + gst::Rank::None, + JitterBuffer::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/src/jitterbuffer/mod.rs b/generic/gst-plugin-threadshare/src/jitterbuffer/mod.rs new file mode 100644 index 000000000..81873205d --- /dev/null +++ b/generic/gst-plugin-threadshare/src/jitterbuffer/mod.rs @@ -0,0 +1,458 @@ +// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib_sys as glib_ffi; +use gstreamer_sys as gst_ffi; + +use std::u32; + +#[allow(clippy::module_inception)] +pub mod jitterbuffer; + +pub mod ffi { + use glib_ffi::{gboolean, gpointer, GList, GType}; + use glib_sys as glib_ffi; + + use gst_ffi::GstClockTime; + use gstreamer_sys as gst_ffi; + use libc::{c_int, c_uint, c_ulonglong, c_ushort, c_void}; + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct RTPJitterBufferItem { + pub data: gpointer, + pub next: *mut GList, + pub prev: *mut GList, + pub r#type: c_uint, + pub dts: GstClockTime, + pub pts: GstClockTime, + pub seqnum: c_uint, + pub count: c_uint, + pub rtptime: c_uint, + } + + #[repr(C)] + pub struct RTPJitterBuffer(c_void); + + #[repr(C)] + #[derive(Copy, Clone)] + pub struct RTPPacketRateCtx { + probed: gboolean, + clock_rate: c_int, + last_seqnum: c_ushort, + last_ts: c_ulonglong, + avg_packet_rate: c_uint, + } + + pub type RTPJitterBufferMode = c_int; + pub const RTP_JITTER_BUFFER_MODE_NONE: RTPJitterBufferMode = 0; + pub const RTP_JITTER_BUFFER_MODE_SLAVE: RTPJitterBufferMode = 1; + pub const RTP_JITTER_BUFFER_MODE_BUFFER: RTPJitterBufferMode = 2; + pub const RTP_JITTER_BUFFER_MODE_SYNCED: RTPJitterBufferMode = 4; + + extern "C" { + pub fn rtp_jitter_buffer_new() -> *mut RTPJitterBuffer; + pub fn rtp_jitter_buffer_get_type() -> GType; + #[allow(dead_code)] + pub fn rtp_jitter_buffer_get_mode(jbuf: *mut RTPJitterBuffer) -> RTPJitterBufferMode; + #[allow(dead_code)] + pub fn rtp_jitter_buffer_set_mode(jbuf: *mut RTPJitterBuffer, mode: RTPJitterBufferMode); + #[allow(dead_code)] + pub fn rtp_jitter_buffer_get_delay(jbuf: *mut RTPJitterBuffer) -> GstClockTime; + pub fn rtp_jitter_buffer_set_delay(jbuf: *mut RTPJitterBuffer, delay: GstClockTime); + pub fn rtp_jitter_buffer_set_clock_rate(jbuf: *mut RTPJitterBuffer, clock_rate: c_uint); + #[allow(dead_code)] + pub fn rtp_jitter_buffer_get_clock_rate(jbuf: *mut RTPJitterBuffer) -> c_uint; + pub fn rtp_jitter_buffer_reset_skew(jbuf: *mut RTPJitterBuffer); + + pub fn rtp_jitter_buffer_flush(jbuf: *mut RTPJitterBuffer, free_func: glib_ffi::GFunc); + pub fn rtp_jitter_buffer_find_earliest( + jbuf: *mut RTPJitterBuffer, + pts: *mut GstClockTime, + seqnum: *mut c_uint, + ); + pub fn rtp_jitter_buffer_calculate_pts( + jbuf: *mut RTPJitterBuffer, + dts: GstClockTime, + estimated_dts: gboolean, + rtptime: c_uint, + base_time: GstClockTime, + gap: c_int, + is_rtx: gboolean, + ) -> GstClockTime; + pub fn rtp_jitter_buffer_insert( + jbuf: *mut RTPJitterBuffer, + item: *mut RTPJitterBufferItem, + head: *mut gboolean, + percent: *mut c_int, + ) -> gboolean; + pub fn rtp_jitter_buffer_pop( + jbuf: *mut RTPJitterBuffer, + percent: *mut c_int, + ) -> *mut RTPJitterBufferItem; + pub fn rtp_jitter_buffer_peek(jbuf: *mut RTPJitterBuffer) -> *mut RTPJitterBufferItem; + + pub fn gst_rtp_packet_rate_ctx_reset(ctx: *mut RTPPacketRateCtx, clock_rate: c_int); + pub fn gst_rtp_packet_rate_ctx_update( + ctx: *mut RTPPacketRateCtx, + seqnum: c_ushort, + ts: c_uint, + ) -> c_uint; + pub fn gst_rtp_packet_rate_ctx_get_max_dropout( + ctx: *mut RTPPacketRateCtx, + time_ms: c_int, + ) -> c_uint; + #[allow(dead_code)] + pub fn gst_rtp_packet_rate_ctx_get_max_disorder( + ctx: *mut RTPPacketRateCtx, + time_ms: c_int, + ) -> c_uint; + } +} + +use glib::prelude::*; +use glib::translate::*; +use glib::{glib_object_wrapper, glib_wrapper}; + +use std::mem; +use std::ptr; + +glib_wrapper! { + pub struct RTPJitterBuffer(Object<ffi::RTPJitterBuffer, RTPJitterBufferClass>); + + match fn { + get_type => || ffi::rtp_jitter_buffer_get_type(), + } +} + +unsafe impl glib::SendUnique for RTPJitterBuffer { + fn is_unique(&self) -> bool { + self.ref_count() == 1 + } +} + +impl ToGlib for RTPJitterBufferMode { + type GlibType = ffi::RTPJitterBufferMode; + + fn to_glib(&self) -> ffi::RTPJitterBufferMode { + match *self { + RTPJitterBufferMode::None => ffi::RTP_JITTER_BUFFER_MODE_NONE, + RTPJitterBufferMode::Slave => ffi::RTP_JITTER_BUFFER_MODE_SLAVE, + RTPJitterBufferMode::Buffer => ffi::RTP_JITTER_BUFFER_MODE_BUFFER, + RTPJitterBufferMode::Synced => ffi::RTP_JITTER_BUFFER_MODE_SYNCED, + RTPJitterBufferMode::__Unknown(value) => value, + } + } +} + +impl FromGlib<ffi::RTPJitterBufferMode> for RTPJitterBufferMode { + fn from_glib(value: ffi::RTPJitterBufferMode) -> Self { + match value { + 0 => RTPJitterBufferMode::None, + 1 => RTPJitterBufferMode::Slave, + 2 => RTPJitterBufferMode::Buffer, + 4 => RTPJitterBufferMode::Synced, + value => RTPJitterBufferMode::__Unknown(value), + } + } +} + +pub struct RTPJitterBufferItem(Option<Box<ffi::RTPJitterBufferItem>>); + +unsafe impl Send for RTPJitterBufferItem {} + +impl RTPJitterBufferItem { + pub fn new( + buffer: gst::Buffer, + dts: gst::ClockTime, + pts: gst::ClockTime, + seqnum: Option<u16>, + rtptime: u32, + ) -> RTPJitterBufferItem { + unsafe { + RTPJitterBufferItem(Some(Box::new(ffi::RTPJitterBufferItem { + data: buffer.into_ptr() as *mut _, + next: ptr::null_mut(), + prev: ptr::null_mut(), + r#type: 0, + dts: dts.to_glib(), + pts: pts.to_glib(), + seqnum: seqnum.map(|s| s as u32).unwrap_or(u32::MAX), + count: 1, + rtptime, + }))) + } + } + + pub fn into_buffer(mut self) -> gst::Buffer { + unsafe { + let item = self.0.take().expect("Invalid wrapper"); + let buf = item.data as *mut gst_ffi::GstBuffer; + from_glib_full(buf) + } + } + + pub fn get_dts(&self) -> gst::ClockTime { + let item = self.0.as_ref().expect("Invalid wrapper"); + if item.dts == gst_ffi::GST_CLOCK_TIME_NONE { + gst::CLOCK_TIME_NONE + } else { + gst::ClockTime(Some(item.dts)) + } + } + + pub fn get_pts(&self) -> gst::ClockTime { + let item = self.0.as_ref().expect("Invalid wrapper"); + if item.pts == gst_ffi::GST_CLOCK_TIME_NONE { + gst::CLOCK_TIME_NONE + } else { + gst::ClockTime(Some(item.pts)) + } + } + + pub fn get_seqnum(&self) -> Option<u16> { + let item = self.0.as_ref().expect("Invalid wrapper"); + if item.seqnum == u32::MAX { + None + } else { + Some(item.seqnum as u16) + } + } + + #[allow(dead_code)] + pub fn get_rtptime(&self) -> u32 { + let item = self.0.as_ref().expect("Invalid wrapper"); + item.rtptime + } +} + +impl Drop for RTPJitterBufferItem { + fn drop(&mut self) { + unsafe { + if let Some(ref item) = self.0 { + gst_ffi::gst_mini_object_unref(item.data as *mut _) + } + } + } +} + +pub struct RTPPacketRateCtx(Box<ffi::RTPPacketRateCtx>); + +unsafe impl Send for RTPPacketRateCtx {} + +impl RTPPacketRateCtx { + pub fn new() -> RTPPacketRateCtx { + unsafe { + let mut ptr = std::mem::MaybeUninit::uninit(); + ffi::gst_rtp_packet_rate_ctx_reset(ptr.as_mut_ptr(), -1); + RTPPacketRateCtx(Box::new(ptr.assume_init())) + } + } + + pub fn reset(&mut self, clock_rate: i32) { + unsafe { ffi::gst_rtp_packet_rate_ctx_reset(&mut *self.0, clock_rate) } + } + + pub fn update(&mut self, seqnum: u16, ts: u32) -> u32 { + unsafe { ffi::gst_rtp_packet_rate_ctx_update(&mut *self.0, seqnum, ts) } + } + + pub fn get_max_dropout(&mut self, time_ms: i32) -> u32 { + unsafe { ffi::gst_rtp_packet_rate_ctx_get_max_dropout(&mut *self.0, time_ms) } + } + + #[allow(dead_code)] + pub fn get_max_disorder(&mut self, time_ms: i32) -> u32 { + unsafe { ffi::gst_rtp_packet_rate_ctx_get_max_disorder(&mut *self.0, time_ms) } + } +} + +impl Default for RTPPacketRateCtx { + fn default() -> Self { + RTPPacketRateCtx::new() + } +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)] +pub enum RTPJitterBufferMode { + r#None, + Slave, + Buffer, + Synced, + __Unknown(i32), +} + +impl RTPJitterBuffer { + pub fn new() -> RTPJitterBuffer { + unsafe { from_glib_full(ffi::rtp_jitter_buffer_new()) } + } + + #[allow(dead_code)] + pub fn get_mode(&self) -> RTPJitterBufferMode { + unsafe { from_glib(ffi::rtp_jitter_buffer_get_mode(self.to_glib_none().0)) } + } + + #[allow(dead_code)] + pub fn set_mode(&self, mode: RTPJitterBufferMode) { + unsafe { ffi::rtp_jitter_buffer_set_mode(self.to_glib_none().0, mode.to_glib()) } + } + + #[allow(dead_code)] + pub fn get_delay(&self) -> gst::ClockTime { + unsafe { from_glib(ffi::rtp_jitter_buffer_get_delay(self.to_glib_none().0)) } + } + + pub fn set_delay(&self, delay: gst::ClockTime) { + unsafe { ffi::rtp_jitter_buffer_set_delay(self.to_glib_none().0, delay.to_glib()) } + } + + pub fn set_clock_rate(&self, clock_rate: u32) { + unsafe { ffi::rtp_jitter_buffer_set_clock_rate(self.to_glib_none().0, clock_rate) } + } + + #[allow(dead_code)] + pub fn get_clock_rate(&self) -> u32 { + unsafe { ffi::rtp_jitter_buffer_get_clock_rate(self.to_glib_none().0) } + } + + pub fn calculate_pts( + &self, + dts: gst::ClockTime, + estimated_dts: bool, + rtptime: u32, + base_time: gst::ClockTime, + gap: i32, + is_rtx: bool, + ) -> gst::ClockTime { + unsafe { + let pts = ffi::rtp_jitter_buffer_calculate_pts( + self.to_glib_none().0, + dts.to_glib(), + estimated_dts.to_glib(), + rtptime, + base_time.to_glib(), + gap, + is_rtx.to_glib(), + ); + + if pts == gst_ffi::GST_CLOCK_TIME_NONE { + gst::CLOCK_TIME_NONE + } else { + pts.into() + } + } + } + + pub fn insert(&self, mut item: RTPJitterBufferItem) -> (bool, bool, i32) { + unsafe { + let mut head = mem::MaybeUninit::uninit(); + let mut percent = mem::MaybeUninit::uninit(); + let box_ = item.0.take().expect("Invalid wrapper"); + let ptr = Box::into_raw(box_); + let ret: bool = from_glib(ffi::rtp_jitter_buffer_insert( + self.to_glib_none().0, + ptr, + head.as_mut_ptr(), + percent.as_mut_ptr(), + )); + if !ret { + item.0 = Some(Box::from_raw(ptr)); + } + (ret, from_glib(head.assume_init()), percent.assume_init()) + } + } + + pub fn find_earliest(&self) -> (gst::ClockTime, Option<u16>) { + unsafe { + let mut pts = mem::MaybeUninit::uninit(); + let mut seqnum = mem::MaybeUninit::uninit(); + + ffi::rtp_jitter_buffer_find_earliest( + self.to_glib_none().0, + pts.as_mut_ptr(), + seqnum.as_mut_ptr(), + ); + let pts = pts.assume_init(); + let seqnum = seqnum.assume_init(); + + let seqnum = if seqnum == u32::MAX { + None + } else { + Some(seqnum as u16) + }; + + if pts == gst_ffi::GST_CLOCK_TIME_NONE { + (gst::CLOCK_TIME_NONE, seqnum) + } else { + (pts.into(), seqnum) + } + } + } + + pub fn pop(&self) -> (Option<RTPJitterBufferItem>, i32) { + unsafe { + let mut percent = mem::MaybeUninit::uninit(); + let item = ffi::rtp_jitter_buffer_pop(self.to_glib_none().0, percent.as_mut_ptr()); + + ( + if item.is_null() { + None + } else { + Some(RTPJitterBufferItem(Some(Box::from_raw(item)))) + }, + percent.assume_init(), + ) + } + } + + pub fn peek(&self) -> (gst::ClockTime, Option<u16>) { + unsafe { + let item = ffi::rtp_jitter_buffer_peek(self.to_glib_none().0); + if item.is_null() { + (gst::CLOCK_TIME_NONE, None) + } else { + let seqnum = (*item).seqnum; + let seqnum = if seqnum == u32::MAX { + None + } else { + Some(seqnum as u16) + }; + ((*item).pts.into(), seqnum) + } + } + } + + pub fn flush(&self) { + unsafe extern "C" fn free_item(item: glib_ffi::gpointer, _: glib_ffi::gpointer) { + let _ = RTPJitterBufferItem(Some(Box::from_raw(item as *mut _))); + } + + unsafe { + ffi::rtp_jitter_buffer_flush(self.to_glib_none().0, Some(free_item)); + } + } + + pub fn reset_skew(&self) { + unsafe { ffi::rtp_jitter_buffer_reset_skew(self.to_glib_none().0) } + } +} + +impl Default for RTPJitterBuffer { + fn default() -> Self { + RTPJitterBuffer::new() + } +} diff --git a/generic/gst-plugin-threadshare/src/jitterbuffer/rtpjitterbuffer.c b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpjitterbuffer.c new file mode 100644 index 000000000..eee284065 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpjitterbuffer.c @@ -0,0 +1,1409 @@ +/* GStreamer + * Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#include <string.h> +#include <stdlib.h> + +#include <gst/rtp/gstrtpbuffer.h> +#include <gst/rtp/gstrtcpbuffer.h> + +#include "rtpjitterbuffer.h" + +GST_DEBUG_CATEGORY_STATIC (rtp_jitter_buffer_debug); +#define GST_CAT_DEFAULT rtp_jitter_buffer_debug + +#define MAX_WINDOW RTP_JITTER_BUFFER_MAX_WINDOW +#define MAX_TIME (2 * GST_SECOND) + +/* signals and args */ +enum +{ + LAST_SIGNAL +}; + +enum +{ + PROP_0 +}; + +/* GObject vmethods */ +static void rtp_jitter_buffer_finalize (GObject * object); + +GType +rtp_jitter_buffer_mode_get_type (void) +{ + static GType jitter_buffer_mode_type = 0; + static const GEnumValue jitter_buffer_modes[] = { + {RTP_JITTER_BUFFER_MODE_NONE, "Only use RTP timestamps", "none"}, + {RTP_JITTER_BUFFER_MODE_SLAVE, "Slave receiver to sender clock", "slave"}, + {RTP_JITTER_BUFFER_MODE_BUFFER, "Do low/high watermark buffering", + "buffer"}, + {RTP_JITTER_BUFFER_MODE_SYNCED, "Synchronized sender and receiver clocks", + "synced"}, + {0, NULL, NULL}, + }; + + if (!jitter_buffer_mode_type) { + jitter_buffer_mode_type = + g_enum_register_static ("TsRTPJitterBufferMode", jitter_buffer_modes); + } + return jitter_buffer_mode_type; +} + +/* static guint rtp_jitter_buffer_signals[LAST_SIGNAL] = { 0 }; */ + +G_DEFINE_TYPE (RTPJitterBuffer, rtp_jitter_buffer, G_TYPE_OBJECT); + +static void +rtp_jitter_buffer_class_init (RTPJitterBufferClass * klass) +{ + GObjectClass *gobject_class; + + gobject_class = (GObjectClass *) klass; + + gobject_class->finalize = rtp_jitter_buffer_finalize; + + GST_DEBUG_CATEGORY_INIT (rtp_jitter_buffer_debug, "rtpjitterbuffer", 0, + "RTP Jitter Buffer"); +} + +static void +rtp_jitter_buffer_init (RTPJitterBuffer * jbuf) +{ + g_mutex_init (&jbuf->clock_lock); + + jbuf->packets = g_queue_new (); + jbuf->mode = RTP_JITTER_BUFFER_MODE_SLAVE; + + rtp_jitter_buffer_reset_skew (jbuf); +} + +static void +rtp_jitter_buffer_finalize (GObject * object) +{ + RTPJitterBuffer *jbuf; + + jbuf = RTP_JITTER_BUFFER_CAST (object); + + if (jbuf->media_clock_synced_id) + g_signal_handler_disconnect (jbuf->media_clock, + jbuf->media_clock_synced_id); + if (jbuf->media_clock) { + /* Make sure to clear any clock master before releasing the clock */ + gst_clock_set_master (jbuf->media_clock, NULL); + gst_object_unref (jbuf->media_clock); + } + + if (jbuf->pipeline_clock) + gst_object_unref (jbuf->pipeline_clock); + + g_queue_free (jbuf->packets); + + g_mutex_clear (&jbuf->clock_lock); + + G_OBJECT_CLASS (rtp_jitter_buffer_parent_class)->finalize (object); +} + +/** + * rtp_jitter_buffer_new: + * + * Create an #RTPJitterBuffer. + * + * Returns: a new #RTPJitterBuffer. Use g_object_unref() after usage. + */ +RTPJitterBuffer * +rtp_jitter_buffer_new (void) +{ + RTPJitterBuffer *jbuf; + + jbuf = g_object_new (RTP_TYPE_JITTER_BUFFER, NULL); + + return jbuf; +} + +/** + * rtp_jitter_buffer_get_mode: + * @jbuf: an #RTPJitterBuffer + * + * Get the current jitterbuffer mode. + * + * Returns: the current jitterbuffer mode. + */ +RTPJitterBufferMode +rtp_jitter_buffer_get_mode (RTPJitterBuffer * jbuf) +{ + return jbuf->mode; +} + +/** + * rtp_jitter_buffer_set_mode: + * @jbuf: an #RTPJitterBuffer + * @mode: a #RTPJitterBufferMode + * + * Set the buffering and clock slaving algorithm used in the @jbuf. + */ +void +rtp_jitter_buffer_set_mode (RTPJitterBuffer * jbuf, RTPJitterBufferMode mode) +{ + jbuf->mode = mode; +} + +GstClockTime +rtp_jitter_buffer_get_delay (RTPJitterBuffer * jbuf) +{ + g_print ("%p getting delay (%" G_GUINT64_FORMAT ")\n", jbuf, jbuf->delay); + return jbuf->delay; +} + +void +rtp_jitter_buffer_set_delay (RTPJitterBuffer * jbuf, GstClockTime delay) +{ + jbuf->delay = delay; + jbuf->low_level = (delay * 15) / 100; + /* the high level is at 90% in order to release packets before we fill up the + * buffer up to the latency */ + jbuf->high_level = (delay * 90) / 100; + + GST_DEBUG ("delay %" GST_TIME_FORMAT ", min %" GST_TIME_FORMAT ", max %" + GST_TIME_FORMAT, GST_TIME_ARGS (jbuf->delay), + GST_TIME_ARGS (jbuf->low_level), GST_TIME_ARGS (jbuf->high_level)); +} + +/** + * rtp_jitter_buffer_set_clock_rate: + * @jbuf: an #RTPJitterBuffer + * @clock_rate: the new clock rate + * + * Set the clock rate in the jitterbuffer. + */ +void +rtp_jitter_buffer_set_clock_rate (RTPJitterBuffer * jbuf, guint32 clock_rate) +{ + if (jbuf->clock_rate != clock_rate) { + GST_DEBUG ("Clock rate changed from %" G_GUINT32_FORMAT " to %" + G_GUINT32_FORMAT, jbuf->clock_rate, clock_rate); + jbuf->clock_rate = clock_rate; + rtp_jitter_buffer_reset_skew (jbuf); + } +} + +/** + * rtp_jitter_buffer_get_clock_rate: + * @jbuf: an #RTPJitterBuffer + * + * Get the currently configure clock rate in @jbuf. + * + * Returns: the current clock-rate + */ +guint32 +rtp_jitter_buffer_get_clock_rate (RTPJitterBuffer * jbuf) +{ + return jbuf->clock_rate; +} + +static void +media_clock_synced_cb (GstClock * clock G_GNUC_UNUSED, + gboolean synced G_GNUC_UNUSED, RTPJitterBuffer * jbuf) +{ + GstClockTime internal, external; + + g_mutex_lock (&jbuf->clock_lock); + if (jbuf->pipeline_clock) { + internal = gst_clock_get_internal_time (jbuf->media_clock); + external = gst_clock_get_time (jbuf->pipeline_clock); + + gst_clock_set_calibration (jbuf->media_clock, internal, external, 1, 1); + } + g_mutex_unlock (&jbuf->clock_lock); +} + +/** + * rtp_jitter_buffer_set_media_clock: + * @jbuf: an #RTPJitterBuffer + * @clock: (transfer full): media #GstClock + * @clock_offset: RTP time at clock epoch or -1 + * + * Sets the media clock for the media and the clock offset + * + */ +void +rtp_jitter_buffer_set_media_clock (RTPJitterBuffer * jbuf, GstClock * clock, + guint64 clock_offset) +{ + g_mutex_lock (&jbuf->clock_lock); + if (jbuf->media_clock) { + if (jbuf->media_clock_synced_id) + g_signal_handler_disconnect (jbuf->media_clock, + jbuf->media_clock_synced_id); + jbuf->media_clock_synced_id = 0; + gst_object_unref (jbuf->media_clock); + } + jbuf->media_clock = clock; + jbuf->media_clock_offset = clock_offset; + + if (jbuf->pipeline_clock && jbuf->media_clock && + jbuf->pipeline_clock != jbuf->media_clock) { + jbuf->media_clock_synced_id = + g_signal_connect (jbuf->media_clock, "synced", + G_CALLBACK (media_clock_synced_cb), jbuf); + if (gst_clock_is_synced (jbuf->media_clock)) { + GstClockTime internal, external; + + internal = gst_clock_get_internal_time (jbuf->media_clock); + external = gst_clock_get_time (jbuf->pipeline_clock); + + gst_clock_set_calibration (jbuf->media_clock, internal, external, 1, 1); + } + + gst_clock_set_master (jbuf->media_clock, jbuf->pipeline_clock); + } + g_mutex_unlock (&jbuf->clock_lock); +} + +/** + * rtp_jitter_buffer_set_pipeline_clock: + * @jbuf: an #RTPJitterBuffer + * @clock: pipeline #GstClock + * + * Sets the pipeline clock + * + */ +void +rtp_jitter_buffer_set_pipeline_clock (RTPJitterBuffer * jbuf, GstClock * clock) +{ + g_mutex_lock (&jbuf->clock_lock); + if (jbuf->pipeline_clock) + gst_object_unref (jbuf->pipeline_clock); + jbuf->pipeline_clock = clock ? gst_object_ref (clock) : NULL; + + if (jbuf->pipeline_clock && jbuf->media_clock && + jbuf->pipeline_clock != jbuf->media_clock) { + if (gst_clock_is_synced (jbuf->media_clock)) { + GstClockTime internal, external; + + internal = gst_clock_get_internal_time (jbuf->media_clock); + external = gst_clock_get_time (jbuf->pipeline_clock); + + gst_clock_set_calibration (jbuf->media_clock, internal, external, 1, 1); + } + + gst_clock_set_master (jbuf->media_clock, jbuf->pipeline_clock); + } + g_mutex_unlock (&jbuf->clock_lock); +} + +gboolean +rtp_jitter_buffer_get_rfc7273_sync (RTPJitterBuffer * jbuf) +{ + return jbuf->rfc7273_sync; +} + +void +rtp_jitter_buffer_set_rfc7273_sync (RTPJitterBuffer * jbuf, + gboolean rfc7273_sync) +{ + jbuf->rfc7273_sync = rfc7273_sync; +} + +/** + * rtp_jitter_buffer_reset_skew: + * @jbuf: an #RTPJitterBuffer + * + * Reset the skew calculations in @jbuf. + */ +void +rtp_jitter_buffer_reset_skew (RTPJitterBuffer * jbuf) +{ + jbuf->base_time = -1; + jbuf->base_rtptime = -1; + jbuf->base_extrtp = -1; + jbuf->media_clock_base_time = -1; + jbuf->ext_rtptime = -1; + jbuf->last_rtptime = -1; + jbuf->window_pos = 0; + jbuf->window_filling = TRUE; + jbuf->window_min = 0; + jbuf->skew = 0; + jbuf->prev_send_diff = -1; + jbuf->prev_out_time = -1; + jbuf->need_resync = TRUE; + + GST_DEBUG ("reset skew correction"); +} + +/** + * rtp_jitter_buffer_disable_buffering: + * @jbuf: an #RTPJitterBuffer + * @disabled: the new state + * + * Enable or disable buffering on @jbuf. + */ +void +rtp_jitter_buffer_disable_buffering (RTPJitterBuffer * jbuf, gboolean disabled) +{ + jbuf->buffering_disabled = disabled; +} + +static void +rtp_jitter_buffer_resync (RTPJitterBuffer * jbuf, GstClockTime time, + GstClockTime gstrtptime, guint64 ext_rtptime, gboolean reset_skew) +{ + jbuf->base_time = time; + jbuf->media_clock_base_time = -1; + jbuf->base_rtptime = gstrtptime; + jbuf->base_extrtp = ext_rtptime; + jbuf->prev_out_time = -1; + jbuf->prev_send_diff = -1; + if (reset_skew) { + jbuf->window_filling = TRUE; + jbuf->window_pos = 0; + jbuf->window_min = 0; + jbuf->window_size = 0; + jbuf->skew = 0; + } + jbuf->need_resync = FALSE; +} + +static guint64 +get_buffer_level (RTPJitterBuffer * jbuf) +{ + RTPJitterBufferItem *high_buf = NULL, *low_buf = NULL; + guint64 level; + + /* first buffer with timestamp */ + high_buf = (RTPJitterBufferItem *) g_queue_peek_tail_link (jbuf->packets); + while (high_buf) { + if (high_buf->dts != GST_CLOCK_TIME_NONE + || high_buf->pts != GST_CLOCK_TIME_NONE) + break; + + high_buf = (RTPJitterBufferItem *) g_list_previous (high_buf); + } + + low_buf = (RTPJitterBufferItem *) g_queue_peek_head_link (jbuf->packets); + while (low_buf) { + if (low_buf->dts != GST_CLOCK_TIME_NONE + || low_buf->pts != GST_CLOCK_TIME_NONE) + break; + + low_buf = (RTPJitterBufferItem *) g_list_next (low_buf); + } + + if (!high_buf || !low_buf || high_buf == low_buf) { + level = 0; + } else { + guint64 high_ts, low_ts; + + high_ts = + high_buf->dts != GST_CLOCK_TIME_NONE ? high_buf->dts : high_buf->pts; + low_ts = low_buf->dts != GST_CLOCK_TIME_NONE ? low_buf->dts : low_buf->pts; + + if (high_ts > low_ts) + level = high_ts - low_ts; + else + level = 0; + + GST_LOG_OBJECT (jbuf, + "low %" GST_TIME_FORMAT " high %" GST_TIME_FORMAT " level %" + G_GUINT64_FORMAT, GST_TIME_ARGS (low_ts), GST_TIME_ARGS (high_ts), + level); + } + return level; +} + +static void +update_buffer_level (RTPJitterBuffer * jbuf, gint * percent) +{ + gboolean post = FALSE; + guint64 level; + + level = get_buffer_level (jbuf); + GST_DEBUG ("buffer level %" GST_TIME_FORMAT, GST_TIME_ARGS (level)); + + if (jbuf->buffering_disabled) { + GST_DEBUG ("buffering is disabled"); + level = jbuf->high_level; + } + + if (jbuf->buffering) { + post = TRUE; + if (level >= jbuf->high_level) { + GST_DEBUG ("buffering finished"); + jbuf->buffering = FALSE; + } + } else { + if (level < jbuf->low_level) { + GST_DEBUG ("buffering started"); + jbuf->buffering = TRUE; + post = TRUE; + } + } + if (post) { + gint perc; + + if (jbuf->buffering && (jbuf->high_level != 0)) { + perc = (level * 100 / jbuf->high_level); + perc = MIN (perc, 100); + } else { + perc = 100; + } + + if (percent) + *percent = perc; + + GST_DEBUG ("buffering %d", perc); + } +} + +/* For the clock skew we use a windowed low point averaging algorithm as can be + * found in Fober, Orlarey and Letz, 2005, "Real Time Clock Skew Estimation + * over Network Delays": + * http://www.grame.fr/Ressources/pub/TR-050601.pdf + * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.102.1546 + * + * The idea is that the jitter is composed of: + * + * J = N + n + * + * N : a constant network delay. + * n : random added noise. The noise is concentrated around 0 + * + * In the receiver we can track the elapsed time at the sender with: + * + * send_diff(i) = (Tsi - Ts0); + * + * Tsi : The time at the sender at packet i + * Ts0 : The time at the sender at the first packet + * + * This is the difference between the RTP timestamp in the first received packet + * and the current packet. + * + * At the receiver we have to deal with the jitter introduced by the network. + * + * recv_diff(i) = (Tri - Tr0) + * + * Tri : The time at the receiver at packet i + * Tr0 : The time at the receiver at the first packet + * + * Both of these values contain a jitter Ji, a jitter for packet i, so we can + * write: + * + * recv_diff(i) = (Cri + D + ni) - (Cr0 + D + n0)) + * + * Cri : The time of the clock at the receiver for packet i + * D + ni : The jitter when receiving packet i + * + * We see that the network delay is irrelevant here as we can elliminate D: + * + * recv_diff(i) = (Cri + ni) - (Cr0 + n0)) + * + * The drift is now expressed as: + * + * Drift(i) = recv_diff(i) - send_diff(i); + * + * We now keep the W latest values of Drift and find the minimum (this is the + * one with the lowest network jitter and thus the one which is least affected + * by it). We average this lowest value to smooth out the resulting network skew. + * + * Both the window and the weighting used for averaging influence the accuracy + * of the drift estimation. Finding the correct parameters turns out to be a + * compromise between accuracy and inertia. + * + * We use a 2 second window or up to 512 data points, which is statistically big + * enough to catch spikes (FIXME, detect spikes). + * We also use a rather large weighting factor (125) to smoothly adapt. During + * startup, when filling the window, we use a parabolic weighting factor, the + * more the window is filled, the faster we move to the detected possible skew. + * + * Returns: @time adjusted with the clock skew. + */ +static GstClockTime +calculate_skew (RTPJitterBuffer * jbuf, guint64 ext_rtptime, + GstClockTime gstrtptime, GstClockTime time, gint gap, gboolean is_rtx) +{ + guint64 send_diff, recv_diff; + gint64 delta; + gint64 old; + guint pos, i; + GstClockTime out_time; + guint64 slope; + + /* elapsed time at sender */ + send_diff = gstrtptime - jbuf->base_rtptime; + + /* we don't have an arrival timestamp so we can't do skew detection. we + * should still apply a timestamp based on RTP timestamp and base_time */ + if (time == GST_CLOCK_TIME_NONE || jbuf->base_time == GST_CLOCK_TIME_NONE + || is_rtx) + goto no_skew; + + /* elapsed time at receiver, includes the jitter */ + recv_diff = time - jbuf->base_time; + + /* measure the diff */ + delta = ((gint64) recv_diff) - ((gint64) send_diff); + + /* measure the slope, this gives a rought estimate between the sender speed + * and the receiver speed. This should be approximately 8, higher values + * indicate a burst (especially when the connection starts) */ + if (recv_diff > 0) + slope = (send_diff * 8) / recv_diff; + else + slope = 8; + + GST_DEBUG ("time %" GST_TIME_FORMAT ", base %" GST_TIME_FORMAT ", recv_diff %" + GST_TIME_FORMAT ", slope %" G_GUINT64_FORMAT, GST_TIME_ARGS (time), + GST_TIME_ARGS (jbuf->base_time), GST_TIME_ARGS (recv_diff), slope); + + /* if the difference between the sender timeline and the receiver timeline + * changed too quickly we have to resync because the server likely restarted + * its timestamps. */ + if (ABS (delta - jbuf->skew) > GST_SECOND) { + GST_WARNING ("delta - skew: %" GST_TIME_FORMAT " too big, reset skew", + GST_TIME_ARGS (ABS (delta - jbuf->skew))); + rtp_jitter_buffer_resync (jbuf, time, gstrtptime, ext_rtptime, TRUE); + send_diff = 0; + delta = 0; + gap = 0; + } + + /* only do skew calculations if we didn't have a gap. if too much time + * has elapsed despite there being a gap, we resynced already. */ + if (G_UNLIKELY (gap != 0)) + goto no_skew; + + pos = jbuf->window_pos; + + if (G_UNLIKELY (jbuf->window_filling)) { + /* we are filling the window */ + GST_DEBUG ("filling %d, delta %" G_GINT64_FORMAT, pos, delta); + jbuf->window[pos++] = delta; + /* calc the min delta we observed */ + if (G_UNLIKELY (pos == 1 || delta < jbuf->window_min)) + jbuf->window_min = delta; + + if (G_UNLIKELY (send_diff >= MAX_TIME || pos >= MAX_WINDOW)) { + jbuf->window_size = pos; + + /* window filled */ + GST_DEBUG ("min %" G_GINT64_FORMAT, jbuf->window_min); + + /* the skew is now the min */ + jbuf->skew = jbuf->window_min; + jbuf->window_filling = FALSE; + } else { + gint perc_time, perc_window, perc; + + /* figure out how much we filled the window, this depends on the amount of + * time we have or the max number of points we keep. */ + perc_time = send_diff * 100 / MAX_TIME; + perc_window = pos * 100 / MAX_WINDOW; + perc = MAX (perc_time, perc_window); + + /* make a parabolic function, the closer we get to the MAX, the more value + * we give to the scaling factor of the new value */ + perc = perc * perc; + + /* quickly go to the min value when we are filling up, slowly when we are + * just starting because we're not sure it's a good value yet. */ + jbuf->skew = + (perc * jbuf->window_min + ((10000 - perc) * jbuf->skew)) / 10000; + jbuf->window_size = pos + 1; + } + } else { + /* pick old value and store new value. We keep the previous value in order + * to quickly check if the min of the window changed */ + old = jbuf->window[pos]; + jbuf->window[pos++] = delta; + + if (G_UNLIKELY (delta <= jbuf->window_min)) { + /* if the new value we inserted is smaller or equal to the current min, + * it becomes the new min */ + jbuf->window_min = delta; + } else if (G_UNLIKELY (old == jbuf->window_min)) { + gint64 min = G_MAXINT64; + + /* if we removed the old min, we have to find a new min */ + for (i = 0; i < jbuf->window_size; i++) { + /* we found another value equal to the old min, we can stop searching now */ + if (jbuf->window[i] == old) { + min = old; + break; + } + if (jbuf->window[i] < min) + min = jbuf->window[i]; + } + jbuf->window_min = min; + } + /* average the min values */ + jbuf->skew = (jbuf->window_min + (124 * jbuf->skew)) / 125; + GST_DEBUG ("delta %" G_GINT64_FORMAT ", new min: %" G_GINT64_FORMAT, + delta, jbuf->window_min); + } + /* wrap around in the window */ + if (G_UNLIKELY (pos >= jbuf->window_size)) + pos = 0; + jbuf->window_pos = pos; + +no_skew: + /* the output time is defined as the base timestamp plus the RTP time + * adjusted for the clock skew .*/ + if (jbuf->base_time != GST_CLOCK_TIME_NONE) { + out_time = jbuf->base_time + send_diff; + /* skew can be negative and we don't want to make invalid timestamps */ + if (jbuf->skew < 0 && out_time < (GstClockTime) - jbuf->skew) { + out_time = 0; + } else { + out_time += jbuf->skew; + } + } else + out_time = -1; + + GST_DEBUG ("skew %" G_GINT64_FORMAT ", out %" GST_TIME_FORMAT, + jbuf->skew, GST_TIME_ARGS (out_time)); + + return out_time; +} + +static void +queue_do_insert (RTPJitterBuffer * jbuf, GList * list, GList * item) +{ + GQueue *queue = jbuf->packets; + + /* It's more likely that the packet was inserted at the tail of the queue */ + if (G_LIKELY (list)) { + item->prev = list; + item->next = list->next; + list->next = item; + } else { + item->prev = NULL; + item->next = queue->head; + queue->head = item; + } + if (item->next) + item->next->prev = item; + else + queue->tail = item; + queue->length++; +} + +GstClockTime +rtp_jitter_buffer_calculate_pts (RTPJitterBuffer * jbuf, GstClockTime dts, + gboolean estimated_dts, guint32 rtptime, GstClockTime base_time, + gint gap, gboolean is_rtx) +{ + guint64 ext_rtptime; + GstClockTime gstrtptime, pts; + GstClock *media_clock, *pipeline_clock; + guint64 media_clock_offset; + gboolean rfc7273_mode; + + /* rtp time jumps are checked for during skew calculation, but bypassed + * in other mode, so mind those here and reset jb if needed. + * Only reset if valid input time, which is likely for UDP input + * where we expect this might happen due to async thread effects + * (in seek and state change cycles), but not so much for TCP input */ + if (GST_CLOCK_TIME_IS_VALID (dts) && !estimated_dts && + jbuf->mode != RTP_JITTER_BUFFER_MODE_SLAVE && + jbuf->base_time != GST_CLOCK_TIME_NONE + && jbuf->last_rtptime != GST_CLOCK_TIME_NONE) { + GstClockTime ext_rtptime = jbuf->ext_rtptime; + + ext_rtptime = gst_rtp_buffer_ext_timestamp (&ext_rtptime, rtptime); + if (ext_rtptime > jbuf->last_rtptime + 3 * jbuf->clock_rate || + ext_rtptime + 3 * jbuf->clock_rate < jbuf->last_rtptime) { + if (!is_rtx) { + /* reset even if we don't have valid incoming time; + * still better than producing possibly very bogus output timestamp */ + GST_WARNING ("rtp delta too big, reset skew"); + rtp_jitter_buffer_reset_skew (jbuf); + } else { + GST_WARNING ("rtp delta too big: ignore rtx packet"); + media_clock = NULL; + pipeline_clock = NULL; + pts = GST_CLOCK_TIME_NONE; + goto done; + } + } + } + + /* Return the last time if we got the same RTP timestamp again */ + ext_rtptime = gst_rtp_buffer_ext_timestamp (&jbuf->ext_rtptime, rtptime); + if (jbuf->last_rtptime != GST_CLOCK_TIME_NONE + && ext_rtptime == jbuf->last_rtptime) { + return jbuf->prev_out_time; + } + + /* keep track of the last extended rtptime */ + jbuf->last_rtptime = ext_rtptime; + + g_mutex_lock (&jbuf->clock_lock); + media_clock = jbuf->media_clock ? gst_object_ref (jbuf->media_clock) : NULL; + pipeline_clock = + jbuf->pipeline_clock ? gst_object_ref (jbuf->pipeline_clock) : NULL; + media_clock_offset = jbuf->media_clock_offset; + g_mutex_unlock (&jbuf->clock_lock); + + gstrtptime = + gst_util_uint64_scale_int (ext_rtptime, GST_SECOND, jbuf->clock_rate); + + if (G_LIKELY (jbuf->base_rtptime != GST_CLOCK_TIME_NONE)) { + /* check elapsed time in RTP units */ + if (gstrtptime < jbuf->base_rtptime) { + if (!is_rtx) { + /* elapsed time at sender, timestamps can go backwards and thus be + * smaller than our base time, schedule to take a new base time in + * that case. */ + GST_WARNING ("backward timestamps at server, schedule resync"); + jbuf->need_resync = TRUE; + } else { + GST_WARNING ("backward timestamps: ignore rtx packet"); + pts = GST_CLOCK_TIME_NONE; + goto done; + } + } + } + + switch (jbuf->mode) { + case RTP_JITTER_BUFFER_MODE_NONE: + case RTP_JITTER_BUFFER_MODE_BUFFER: + /* send 0 as the first timestamp and -1 for the other ones. This will + * interpolate them from the RTP timestamps with a 0 origin. In buffering + * mode we will adjust the outgoing timestamps according to the amount of + * time we spent buffering. */ + if (jbuf->base_time == GST_CLOCK_TIME_NONE) + dts = 0; + else + dts = -1; + break; + case RTP_JITTER_BUFFER_MODE_SYNCED: + /* synchronized clocks, take first timestamp as base, use RTP timestamps + * to interpolate */ + if (jbuf->base_time != GST_CLOCK_TIME_NONE && !jbuf->need_resync) + dts = -1; + break; + case RTP_JITTER_BUFFER_MODE_SLAVE: + default: + break; + } + + /* need resync, lock on to time and gstrtptime if we can, otherwise we + * do with the previous values */ + if (G_UNLIKELY (jbuf->need_resync && dts != GST_CLOCK_TIME_NONE)) { + if (is_rtx) { + GST_DEBUG ("not resyncing on rtx packet, discard"); + pts = GST_CLOCK_TIME_NONE; + goto done; + } + GST_INFO ("resync to time %" GST_TIME_FORMAT ", rtptime %" + GST_TIME_FORMAT, GST_TIME_ARGS (dts), GST_TIME_ARGS (gstrtptime)); + rtp_jitter_buffer_resync (jbuf, dts, gstrtptime, ext_rtptime, FALSE); + } + + GST_DEBUG ("extrtp %" G_GUINT64_FORMAT ", gstrtp %" GST_TIME_FORMAT ", base %" + GST_TIME_FORMAT ", send_diff %" GST_TIME_FORMAT, ext_rtptime, + GST_TIME_ARGS (gstrtptime), GST_TIME_ARGS (jbuf->base_rtptime), + GST_TIME_ARGS (gstrtptime - jbuf->base_rtptime)); + + rfc7273_mode = media_clock && pipeline_clock + && gst_clock_is_synced (media_clock); + + if (rfc7273_mode && jbuf->mode == RTP_JITTER_BUFFER_MODE_SLAVE + && (media_clock_offset == GST_CLOCK_TIME_NONE || !jbuf->rfc7273_sync)) { + GstClockTime internal, external; + GstClockTime rate_num, rate_denom; + GstClockTime nsrtptimediff, rtpntptime, rtpsystime; + + gst_clock_get_calibration (media_clock, &internal, &external, &rate_num, + &rate_denom); + + /* Slave to the RFC7273 media clock instead of trying to estimate it + * based on receive times and RTP timestamps */ + + if (jbuf->media_clock_base_time == GST_CLOCK_TIME_NONE) { + if (jbuf->base_time != GST_CLOCK_TIME_NONE) { + jbuf->media_clock_base_time = + gst_clock_unadjust_with_calibration (media_clock, + jbuf->base_time + base_time, internal, external, rate_num, + rate_denom); + } else { + if (dts != GST_CLOCK_TIME_NONE) + jbuf->media_clock_base_time = + gst_clock_unadjust_with_calibration (media_clock, dts + base_time, + internal, external, rate_num, rate_denom); + else + jbuf->media_clock_base_time = + gst_clock_get_internal_time (media_clock); + jbuf->base_rtptime = gstrtptime; + } + } + + if (gstrtptime > jbuf->base_rtptime) + nsrtptimediff = gstrtptime - jbuf->base_rtptime; + else + nsrtptimediff = 0; + + rtpntptime = nsrtptimediff + jbuf->media_clock_base_time; + + rtpsystime = + gst_clock_adjust_with_calibration (media_clock, rtpntptime, internal, + external, rate_num, rate_denom); + + if (rtpsystime > base_time) + pts = rtpsystime - base_time; + else + pts = 0; + + GST_DEBUG ("RFC7273 clock time %" GST_TIME_FORMAT ", out %" GST_TIME_FORMAT, + GST_TIME_ARGS (rtpsystime), GST_TIME_ARGS (pts)); + } else if (rfc7273_mode && (jbuf->mode == RTP_JITTER_BUFFER_MODE_SLAVE + || jbuf->mode == RTP_JITTER_BUFFER_MODE_SYNCED) + && media_clock_offset != GST_CLOCK_TIME_NONE && jbuf->rfc7273_sync) { + GstClockTime ntptime, rtptime_tmp; + GstClockTime ntprtptime, rtpsystime; + GstClockTime internal, external; + GstClockTime rate_num, rate_denom; + + /* Don't do any of the dts related adjustments further down */ + dts = -1; + + /* Calculate the actual clock time on the sender side based on the + * RFC7273 clock and convert it to our pipeline clock + */ + + gst_clock_get_calibration (media_clock, &internal, &external, &rate_num, + &rate_denom); + + ntptime = gst_clock_get_internal_time (media_clock); + + ntprtptime = gst_util_uint64_scale (ntptime, jbuf->clock_rate, GST_SECOND); + ntprtptime += media_clock_offset; + ntprtptime &= 0xffffffff; + + rtptime_tmp = rtptime; + /* Check for wraparounds, we assume that the diff between current RTP + * timestamp and current media clock time can't be bigger than + * 2**31 clock units */ + if (ntprtptime > rtptime_tmp && ntprtptime - rtptime_tmp >= 0x80000000) + rtptime_tmp += G_GUINT64_CONSTANT (0x100000000); + else if (rtptime_tmp > ntprtptime && rtptime_tmp - ntprtptime >= 0x80000000) + ntprtptime += G_GUINT64_CONSTANT (0x100000000); + + if (ntprtptime > rtptime_tmp) + ntptime -= + gst_util_uint64_scale (ntprtptime - rtptime_tmp, jbuf->clock_rate, + GST_SECOND); + else + ntptime += + gst_util_uint64_scale (rtptime_tmp - ntprtptime, jbuf->clock_rate, + GST_SECOND); + + rtpsystime = + gst_clock_adjust_with_calibration (media_clock, ntptime, internal, + external, rate_num, rate_denom); + /* All this assumes that the pipeline has enough additional + * latency to cover for the network delay */ + if (rtpsystime > base_time) + pts = rtpsystime - base_time; + else + pts = 0; + + GST_DEBUG ("RFC7273 clock time %" GST_TIME_FORMAT ", out %" GST_TIME_FORMAT, + GST_TIME_ARGS (rtpsystime), GST_TIME_ARGS (pts)); + } else { + /* If we used the RFC7273 clock before and not anymore, + * we need to resync it later again */ + jbuf->media_clock_base_time = -1; + + /* do skew calculation by measuring the difference between rtptime and the + * receive dts, this function will return the skew corrected rtptime. */ + pts = calculate_skew (jbuf, ext_rtptime, gstrtptime, dts, gap, is_rtx); + } + + /* check if timestamps are not going backwards, we can only check this if we + * have a previous out time and a previous send_diff */ + if (G_LIKELY (pts != GST_CLOCK_TIME_NONE + && jbuf->prev_out_time != GST_CLOCK_TIME_NONE + && jbuf->prev_send_diff != -1)) { + /* now check for backwards timestamps */ + if (G_UNLIKELY ( + /* if the server timestamps went up and the out_time backwards */ + ((gint64) (gstrtptime - jbuf->base_rtptime) > jbuf->prev_send_diff + && pts < jbuf->prev_out_time) || + /* if the server timestamps went backwards and the out_time forwards */ + ((gint64) (gstrtptime - jbuf->base_rtptime) < jbuf->prev_send_diff + && pts > jbuf->prev_out_time) || + /* if the server timestamps did not change */ + (gint64) (gstrtptime - jbuf->base_rtptime) == jbuf->prev_send_diff)) { + GST_DEBUG ("backwards timestamps, using previous time"); + pts = jbuf->prev_out_time; + } + } + + if (gap == 0 && dts != GST_CLOCK_TIME_NONE && pts + jbuf->delay < dts) { + /* if we are going to produce a timestamp that is later than the input + * timestamp, we need to reset the jitterbuffer. Likely the server paused + * temporarily */ + GST_DEBUG ("out %" GST_TIME_FORMAT " + %" G_GUINT64_FORMAT " < time %" + GST_TIME_FORMAT ", reset jitterbuffer and discard", GST_TIME_ARGS (pts), + jbuf->delay, GST_TIME_ARGS (dts)); + rtp_jitter_buffer_reset_skew (jbuf); + pts = GST_CLOCK_TIME_NONE; + goto done; + } + + jbuf->prev_out_time = pts; + jbuf->prev_send_diff = gstrtptime - jbuf->base_rtptime; + +done: + if (media_clock) + gst_object_unref (media_clock); + if (pipeline_clock) + gst_object_unref (pipeline_clock); + + return pts; +} + + +/** + * rtp_jitter_buffer_insert: + * @jbuf: an #RTPJitterBuffer + * @item: an #RTPJitterBufferItem to insert + * @head: TRUE when the head element changed. + * @percent: the buffering percent after insertion + * + * Inserts @item into the packet queue of @jbuf. The sequence number of the + * packet will be used to sort the packets. This function takes ownerhip of + * @buf when the function returns %TRUE. + * + * When @head is %TRUE, the new packet was added at the head of the queue and + * will be available with the next call to rtp_jitter_buffer_pop() and + * rtp_jitter_buffer_peek(). + * + * Returns: %FALSE if a packet with the same number already existed. + */ +gboolean +rtp_jitter_buffer_insert (RTPJitterBuffer * jbuf, RTPJitterBufferItem * item, + gboolean * head, gint * percent) +{ + GList *list, *event = NULL; + guint16 seqnum; + + g_return_val_if_fail (jbuf != NULL, FALSE); + g_return_val_if_fail (item != NULL, FALSE); + + list = jbuf->packets->tail; + + /* no seqnum, simply append then */ + if (item->seqnum == G_MAXUINT) + goto append; + + seqnum = item->seqnum; + + /* loop the list to skip strictly larger seqnum buffers */ + for (; list; list = g_list_previous (list)) { + guint16 qseq; + gint gap; + RTPJitterBufferItem *qitem = (RTPJitterBufferItem *) list; + + if (qitem->seqnum == G_MAXUINT) { + /* keep a pointer to the first consecutive event if not already + * set. we will insert the packet after the event if we can't find + * a packet with lower sequence number before the event. */ + if (event == NULL) + event = list; + continue; + } + + qseq = qitem->seqnum; + + /* compare the new seqnum to the one in the buffer */ + gap = gst_rtp_buffer_compare_seqnum (seqnum, qseq); + + /* we hit a packet with the same seqnum, notify a duplicate */ + if (G_UNLIKELY (gap == 0)) + goto duplicate; + + /* seqnum > qseq, we can stop looking */ + if (G_LIKELY (gap < 0)) + break; + + /* if we've found a packet with greater sequence number, cleanup the + * event pointer as the packet will be inserted before the event */ + event = NULL; + } + + /* if event is set it means that packets before the event had smaller + * sequence number, so we will insert our packet after the event */ + if (event) + list = event; + +append: + queue_do_insert (jbuf, list, (GList *) item); + + /* buffering mode, update buffer stats */ + if (jbuf->mode == RTP_JITTER_BUFFER_MODE_BUFFER) + update_buffer_level (jbuf, percent); + else if (percent) + *percent = -1; + + /* head was changed when we did not find a previous packet, we set the return + * flag when requested. */ + if (G_LIKELY (head)) + *head = (list == NULL); + + return TRUE; + + /* ERRORS */ +duplicate: + { + GST_DEBUG ("duplicate packet %d found", (gint) seqnum); + if (G_LIKELY (head)) + *head = FALSE; + return FALSE; + } +} + +/** + * rtp_jitter_buffer_pop: + * @jbuf: an #RTPJitterBuffer + * @percent: the buffering percent + * + * Pops the oldest buffer from the packet queue of @jbuf. The popped buffer will + * have its timestamp adjusted with the incoming running_time and the detected + * clock skew. + * + * Returns: a #GstBuffer or %NULL when there was no packet in the queue. + */ +RTPJitterBufferItem * +rtp_jitter_buffer_pop (RTPJitterBuffer * jbuf, gint * percent) +{ + GList *item = NULL; + GQueue *queue; + + g_return_val_if_fail (jbuf != NULL, NULL); + + queue = jbuf->packets; + + item = queue->head; + if (item) { + queue->head = item->next; + if (queue->head) + queue->head->prev = NULL; + else + queue->tail = NULL; + queue->length--; + } + + /* buffering mode, update buffer stats */ + if (jbuf->mode == RTP_JITTER_BUFFER_MODE_BUFFER) + update_buffer_level (jbuf, percent); + else if (percent) + *percent = -1; + + return (RTPJitterBufferItem *) item; +} + +/** + * rtp_jitter_buffer_peek: + * @jbuf: an #RTPJitterBuffer + * + * Peek the oldest buffer from the packet queue of @jbuf. + * + * See rtp_jitter_buffer_insert() to check when an older packet was + * added. + * + * Returns: a #GstBuffer or %NULL when there was no packet in the queue. + */ +RTPJitterBufferItem * +rtp_jitter_buffer_peek (RTPJitterBuffer * jbuf) +{ + g_return_val_if_fail (jbuf != NULL, NULL); + + return (RTPJitterBufferItem *) jbuf->packets->head; +} + +/** + * rtp_jitter_buffer_flush: + * @jbuf: an #RTPJitterBuffer + * @free_func: function to free each item + * @user_data: user data passed to @free_func + * + * Flush all packets from the jitterbuffer. + */ +void +rtp_jitter_buffer_flush (RTPJitterBuffer * jbuf, GFunc free_func, + gpointer user_data) +{ + GList *item; + + g_return_if_fail (jbuf != NULL); + g_return_if_fail (free_func != NULL); + + while ((item = g_queue_pop_head_link (jbuf->packets))) + free_func ((RTPJitterBufferItem *) item, user_data); +} + +/** + * rtp_jitter_buffer_is_buffering: + * @jbuf: an #RTPJitterBuffer + * + * Check if @jbuf is buffering currently. Users of the jitterbuffer should not + * pop packets while in buffering mode. + * + * Returns: the buffering state of @jbuf + */ +gboolean +rtp_jitter_buffer_is_buffering (RTPJitterBuffer * jbuf) +{ + return jbuf->buffering && !jbuf->buffering_disabled; +} + +/** + * rtp_jitter_buffer_set_buffering: + * @jbuf: an #RTPJitterBuffer + * @buffering: the new buffering state + * + * Forces @jbuf to go into the buffering state. + */ +void +rtp_jitter_buffer_set_buffering (RTPJitterBuffer * jbuf, gboolean buffering) +{ + jbuf->buffering = buffering; +} + +/** + * rtp_jitter_buffer_get_percent: + * @jbuf: an #RTPJitterBuffer + * + * Get the buffering percent of the jitterbuffer. + * + * Returns: the buffering percent + */ +gint +rtp_jitter_buffer_get_percent (RTPJitterBuffer * jbuf) +{ + gint percent; + guint64 level; + + if (G_UNLIKELY (jbuf->high_level == 0)) + return 100; + + if (G_UNLIKELY (jbuf->buffering_disabled)) + return 100; + + level = get_buffer_level (jbuf); + percent = (level * 100 / jbuf->high_level); + percent = MIN (percent, 100); + + return percent; +} + +/** + * rtp_jitter_buffer_num_packets: + * @jbuf: an #RTPJitterBuffer + * + * Get the number of packets currently in "jbuf. + * + * Returns: The number of packets in @jbuf. + */ +guint +rtp_jitter_buffer_num_packets (RTPJitterBuffer * jbuf) +{ + g_return_val_if_fail (jbuf != NULL, 0); + + return jbuf->packets->length; +} + +/** + * rtp_jitter_buffer_get_ts_diff: + * @jbuf: an #RTPJitterBuffer + * + * Get the difference between the timestamps of first and last packet in the + * jitterbuffer. + * + * Returns: The difference expressed in the timestamp units of the packets. + */ +guint32 +rtp_jitter_buffer_get_ts_diff (RTPJitterBuffer * jbuf) +{ + guint64 high_ts, low_ts; + RTPJitterBufferItem *high_buf, *low_buf; + guint32 result; + + g_return_val_if_fail (jbuf != NULL, 0); + + high_buf = (RTPJitterBufferItem *) g_queue_peek_tail_link (jbuf->packets); + low_buf = (RTPJitterBufferItem *) g_queue_peek_head_link (jbuf->packets); + + if (!high_buf || !low_buf || high_buf == low_buf) + return 0; + + high_ts = high_buf->rtptime; + low_ts = low_buf->rtptime; + + /* it needs to work if ts wraps */ + if (high_ts >= low_ts) { + result = (guint32) (high_ts - low_ts); + } else { + result = (guint32) (high_ts + G_MAXUINT32 + 1 - low_ts); + } + return result; +} + + +/* + * rtp_jitter_buffer_get_seqnum_diff: + * @jbuf: an #RTPJitterBuffer + * + * Get the difference between the seqnum of first and last packet in the + * jitterbuffer. + * + * Returns: The difference expressed in seqnum. + */ +static guint16 +rtp_jitter_buffer_get_seqnum_diff (RTPJitterBuffer * jbuf) +{ + guint32 high_seqnum, low_seqnum; + RTPJitterBufferItem *high_buf, *low_buf; + guint16 result; + + g_return_val_if_fail (jbuf != NULL, 0); + + high_buf = (RTPJitterBufferItem *) g_queue_peek_tail_link (jbuf->packets); + low_buf = (RTPJitterBufferItem *) g_queue_peek_head_link (jbuf->packets); + + while (high_buf && high_buf->seqnum == G_MAXUINT) + high_buf = (RTPJitterBufferItem *) high_buf->prev; + + while (low_buf && low_buf->seqnum == G_MAXUINT) + low_buf = (RTPJitterBufferItem *) low_buf->next; + + if (!high_buf || !low_buf || high_buf == low_buf) + return 0; + + high_seqnum = high_buf->seqnum; + low_seqnum = low_buf->seqnum; + + /* it needs to work if ts wraps */ + if (high_seqnum >= low_seqnum) { + result = (guint32) (high_seqnum - low_seqnum); + } else { + result = (guint32) (high_seqnum + G_MAXUINT16 + 1 - low_seqnum); + } + return result; +} + +/** + * rtp_jitter_buffer_get_sync: + * @jbuf: an #RTPJitterBuffer + * @rtptime: result RTP time + * @timestamp: result GStreamer timestamp + * @clock_rate: clock-rate of @rtptime + * @last_rtptime: last seen rtptime. + * + * Calculates the relation between the RTP timestamp and the GStreamer timestamp + * used for constructing timestamps. + * + * For extended RTP timestamp @rtptime with a clock-rate of @clock_rate, + * the GStreamer timestamp is currently @timestamp. + * + * The last seen extended RTP timestamp with clock-rate @clock-rate is returned in + * @last_rtptime. + */ +void +rtp_jitter_buffer_get_sync (RTPJitterBuffer * jbuf, guint64 * rtptime, + guint64 * timestamp, guint32 * clock_rate, guint64 * last_rtptime) +{ + if (rtptime) + *rtptime = jbuf->base_extrtp; + if (timestamp) + *timestamp = jbuf->base_time + jbuf->skew; + if (clock_rate) + *clock_rate = jbuf->clock_rate; + if (last_rtptime) + *last_rtptime = jbuf->last_rtptime; +} + +/** + * rtp_jitter_buffer_can_fast_start: + * @jbuf: an #RTPJitterBuffer + * @num_packets: Number of consecutive packets needed + * + * Check if in the queue if there is enough packets with consecutive seqnum in + * order to start delivering them. + * + * Returns: %TRUE if the required number of consecutive packets was found. + */ +gboolean +rtp_jitter_buffer_can_fast_start (RTPJitterBuffer * jbuf, gint num_packet) +{ + gboolean ret = TRUE; + RTPJitterBufferItem *last_item = NULL, *item; + gint i; + + if (rtp_jitter_buffer_num_packets (jbuf) < (guint) num_packet) + return FALSE; + + item = rtp_jitter_buffer_peek (jbuf); + for (i = 0; i < num_packet; i++) { + if (G_LIKELY (last_item)) { + guint16 expected_seqnum = last_item->seqnum + 1; + + if (expected_seqnum != item->seqnum) { + ret = FALSE; + break; + } + } + + last_item = item; + item = (RTPJitterBufferItem *) last_item->next; + } + + return ret; +} + +gboolean +rtp_jitter_buffer_is_full (RTPJitterBuffer * jbuf) +{ + return rtp_jitter_buffer_get_seqnum_diff (jbuf) >= 32765 && + rtp_jitter_buffer_num_packets (jbuf) > 10000; +} + +void +rtp_jitter_buffer_find_earliest (RTPJitterBuffer * jbuf, GstClockTime * pts, + guint * seqnum) +{ + GList *tmp; + RTPJitterBufferItem *earliest = NULL; + + *pts = GST_CLOCK_TIME_NONE; + *seqnum = 0; + + for (tmp = jbuf->packets->head; tmp; tmp = tmp->next) { + RTPJitterBufferItem *item = (RTPJitterBufferItem *) tmp; + + if (!earliest || item->pts <= earliest->pts) + earliest = item; + } + + if (earliest) { + *pts = earliest->pts; + *seqnum = earliest->seqnum; + } +} diff --git a/generic/gst-plugin-threadshare/src/jitterbuffer/rtpjitterbuffer.h b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpjitterbuffer.h new file mode 100644 index 000000000..31fbb747a --- /dev/null +++ b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpjitterbuffer.h @@ -0,0 +1,201 @@ +/* GStreamer + * Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __RTP_JITTER_BUFFER_H__ +#define __RTP_JITTER_BUFFER_H__ + +#include <gst/gst.h> +#include <gst/rtp/gstrtcpbuffer.h> + +typedef struct _RTPJitterBuffer RTPJitterBuffer; +typedef struct _RTPJitterBufferClass RTPJitterBufferClass; +typedef struct _RTPJitterBufferItem RTPJitterBufferItem; + +#define RTP_TYPE_JITTER_BUFFER (rtp_jitter_buffer_get_type()) +#define RTP_JITTER_BUFFER(src) (G_TYPE_CHECK_INSTANCE_CAST((src),RTP_TYPE_JITTER_BUFFER,RTPJitterBuffer)) +#define RTP_JITTER_BUFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),RTP_TYPE_JITTER_BUFFER,RTPJitterBufferClass)) +#define RTP_IS_JITTER_BUFFER(src) (G_TYPE_CHECK_INSTANCE_TYPE((src),RTP_TYPE_JITTER_BUFFER)) +#define RTP_IS_JITTER_BUFFER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),RTP_TYPE_JITTER_BUFFER)) +#define RTP_JITTER_BUFFER_CAST(src) ((RTPJitterBuffer *)(src)) + +/** + * RTPJitterBufferMode: + * @RTP_JITTER_BUFFER_MODE_NONE: don't do any skew correction, outgoing + * timestamps are calculated directly from the RTP timestamps. This mode is + * good for recording but not for real-time applications. + * @RTP_JITTER_BUFFER_MODE_SLAVE: calculate the skew between sender and receiver + * and produce smoothed adjusted outgoing timestamps. This mode is good for + * low latency communications. + * @RTP_JITTER_BUFFER_MODE_BUFFER: buffer packets between low/high watermarks. + * This mode is good for streaming communication. + * @RTP_JITTER_BUFFER_MODE_SYNCED: sender and receiver clocks are synchronized, + * like #RTP_JITTER_BUFFER_MODE_SLAVE but skew is assumed to be 0. Good for + * low latency communication when sender and receiver clocks are + * synchronized and there is thus no clock skew. + * @RTP_JITTER_BUFFER_MODE_LAST: last buffer mode. + * + * The different buffer modes for a jitterbuffer. + */ +typedef enum { + RTP_JITTER_BUFFER_MODE_NONE = 0, + RTP_JITTER_BUFFER_MODE_SLAVE = 1, + RTP_JITTER_BUFFER_MODE_BUFFER = 2, + /* FIXME 3 is missing because it was used for 'auto' in jitterbuffer */ + RTP_JITTER_BUFFER_MODE_SYNCED = 4, + RTP_JITTER_BUFFER_MODE_LAST +} RTPJitterBufferMode; + +#define RTP_TYPE_JITTER_BUFFER_MODE (rtp_jitter_buffer_mode_get_type()) +GType rtp_jitter_buffer_mode_get_type (void); + +#define RTP_JITTER_BUFFER_MAX_WINDOW 512 +/** + * RTPJitterBuffer: + * + * A JitterBuffer in the #RTPSession + */ +struct _RTPJitterBuffer { + GObject object; + + GQueue *packets; + + RTPJitterBufferMode mode; + + GstClockTime delay; + + /* for buffering */ + gboolean buffering; + guint64 low_level; + guint64 high_level; + + /* for calculating skew */ + gboolean need_resync; + GstClockTime base_time; + GstClockTime base_rtptime; + GstClockTime media_clock_base_time; + guint32 clock_rate; + GstClockTime base_extrtp; + GstClockTime prev_out_time; + guint64 ext_rtptime; + guint64 last_rtptime; + gint64 window[RTP_JITTER_BUFFER_MAX_WINDOW]; + guint window_pos; + guint window_size; + gboolean window_filling; + gint64 window_min; + gint64 skew; + gint64 prev_send_diff; + gboolean buffering_disabled; + + GMutex clock_lock; + GstClock *pipeline_clock; + GstClock *media_clock; + gulong media_clock_synced_id; + guint64 media_clock_offset; + + gboolean rfc7273_sync; +}; + +struct _RTPJitterBufferClass { + GObjectClass parent_class; +}; + +/** + * RTPJitterBufferItem: + * @data: the data of the item + * @next: pointer to next item + * @prev: pointer to previous item + * @type: the type of @data, used freely by caller + * @dts: input DTS + * @pts: output PTS + * @seqnum: seqnum, the seqnum is used to insert the item in the + * right position in the jitterbuffer and detect duplicates. Use -1 to + * append. + * @count: amount of seqnum in this item + * @rtptime: rtp timestamp + * + * An object containing an RTP packet or event. + */ +struct _RTPJitterBufferItem { + gpointer data; + GList *next; + GList *prev; + guint type; + GstClockTime dts; + GstClockTime pts; + guint seqnum; + guint count; + guint rtptime; +}; + +GType rtp_jitter_buffer_get_type (void); + +/* managing lifetime */ +RTPJitterBuffer* rtp_jitter_buffer_new (void); + +RTPJitterBufferMode rtp_jitter_buffer_get_mode (RTPJitterBuffer *jbuf); +void rtp_jitter_buffer_set_mode (RTPJitterBuffer *jbuf, RTPJitterBufferMode mode); + +GstClockTime rtp_jitter_buffer_get_delay (RTPJitterBuffer *jbuf); +void rtp_jitter_buffer_set_delay (RTPJitterBuffer *jbuf, GstClockTime delay); + +void rtp_jitter_buffer_set_clock_rate (RTPJitterBuffer *jbuf, guint32 clock_rate); +guint32 rtp_jitter_buffer_get_clock_rate (RTPJitterBuffer *jbuf); + +void rtp_jitter_buffer_set_media_clock (RTPJitterBuffer *jbuf, GstClock * clock, guint64 clock_offset); +void rtp_jitter_buffer_set_pipeline_clock (RTPJitterBuffer *jbuf, GstClock * clock); + +gboolean rtp_jitter_buffer_get_rfc7273_sync (RTPJitterBuffer *jbuf); +void rtp_jitter_buffer_set_rfc7273_sync (RTPJitterBuffer *jbuf, gboolean rfc7273_sync); + +void rtp_jitter_buffer_reset_skew (RTPJitterBuffer *jbuf); + +gboolean rtp_jitter_buffer_insert (RTPJitterBuffer *jbuf, + RTPJitterBufferItem *item, + gboolean *head, gint *percent); + +void rtp_jitter_buffer_disable_buffering (RTPJitterBuffer *jbuf, gboolean disabled); + +RTPJitterBufferItem * rtp_jitter_buffer_peek (RTPJitterBuffer *jbuf); +RTPJitterBufferItem * rtp_jitter_buffer_pop (RTPJitterBuffer *jbuf, gint *percent); + +void rtp_jitter_buffer_flush (RTPJitterBuffer *jbuf, + GFunc free_func, gpointer user_data); + +gboolean rtp_jitter_buffer_is_buffering (RTPJitterBuffer * jbuf); +void rtp_jitter_buffer_set_buffering (RTPJitterBuffer * jbuf, gboolean buffering); +gint rtp_jitter_buffer_get_percent (RTPJitterBuffer * jbuf); + +guint rtp_jitter_buffer_num_packets (RTPJitterBuffer *jbuf); +guint32 rtp_jitter_buffer_get_ts_diff (RTPJitterBuffer *jbuf); + +void rtp_jitter_buffer_get_sync (RTPJitterBuffer *jbuf, guint64 *rtptime, + guint64 *timestamp, guint32 *clock_rate, + guint64 *last_rtptime); + +GstClockTime rtp_jitter_buffer_calculate_pts (RTPJitterBuffer * jbuf, GstClockTime dts, gboolean estimated_dts, + guint32 rtptime, GstClockTime base_time, gint gap, + gboolean is_rtx); + +gboolean rtp_jitter_buffer_can_fast_start (RTPJitterBuffer * jbuf, gint num_packet); + +gboolean rtp_jitter_buffer_is_full (RTPJitterBuffer * jbuf); +void rtp_jitter_buffer_find_earliest (RTPJitterBuffer * jbuf, GstClockTime *pts, guint * seqnum); + +#endif /* __RTP_JITTER_BUFFER_H__ */ diff --git a/generic/gst-plugin-threadshare/src/jitterbuffer/rtpstats.c b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpstats.c new file mode 100644 index 000000000..3cad086c4 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpstats.c @@ -0,0 +1,430 @@ +/* GStreamer + * Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.com> + * Copyright (C) 2015 Kurento (http://kurento.org/) + * @author: Miguel París <mparisdiaz@gmail.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "rtpstats.h" + +void +gst_rtp_packet_rate_ctx_reset (RTPPacketRateCtx * ctx, gint32 clock_rate) +{ + ctx->clock_rate = clock_rate; + ctx->probed = FALSE; + ctx->avg_packet_rate = -1; + ctx->last_ts = -1; +} + +guint32 +gst_rtp_packet_rate_ctx_update (RTPPacketRateCtx * ctx, guint16 seqnum, + guint32 ts) +{ + guint64 new_ts, diff_ts; + gint diff_seqnum; + gint32 new_packet_rate; + + if (ctx->clock_rate <= 0) { + return ctx->avg_packet_rate; + } + + new_ts = ctx->last_ts; + gst_rtp_buffer_ext_timestamp (&new_ts, ts); + + if (!ctx->probed) { + ctx->probed = TRUE; + goto done; + } + + diff_seqnum = gst_rtp_buffer_compare_seqnum (ctx->last_seqnum, seqnum); + if (diff_seqnum <= 0 || new_ts <= ctx->last_ts || diff_seqnum > 1) { + goto done; + } + + diff_ts = new_ts - ctx->last_ts; + diff_ts = gst_util_uint64_scale_int (diff_ts, GST_SECOND, ctx->clock_rate); + new_packet_rate = gst_util_uint64_scale (diff_seqnum, GST_SECOND, diff_ts); + + /* The goal is that higher packet rates "win". + * If there's a sudden burst, the average will go up fast, + * but it will go down again slowly. + * This is useful for bursty cases, where a lot of packets are close + * to each other and should allow a higher reorder/dropout there. + * Round up the new average. + */ + if ((gint32) ctx->avg_packet_rate > new_packet_rate) { + ctx->avg_packet_rate = (7 * ctx->avg_packet_rate + new_packet_rate + 7) / 8; + } else { + ctx->avg_packet_rate = (ctx->avg_packet_rate + new_packet_rate + 1) / 2; + } + +done: + ctx->last_seqnum = seqnum; + ctx->last_ts = new_ts; + + return ctx->avg_packet_rate; +} + +guint32 +gst_rtp_packet_rate_ctx_get (RTPPacketRateCtx * ctx) +{ + return ctx->avg_packet_rate; +} + +guint32 +gst_rtp_packet_rate_ctx_get_max_dropout (RTPPacketRateCtx * ctx, gint32 time_ms) +{ + if (time_ms <= 0 || !ctx->probed || ctx->avg_packet_rate == G_MAXUINT32) { + return RTP_DEF_DROPOUT; + } + + return MAX (RTP_MIN_DROPOUT, ctx->avg_packet_rate * time_ms / 1000); +} + +guint32 +gst_rtp_packet_rate_ctx_get_max_misorder (RTPPacketRateCtx * ctx, + gint32 time_ms) +{ + if (time_ms <= 0 || !ctx->probed || ctx->avg_packet_rate == G_MAXUINT32) { + return RTP_DEF_MISORDER; + } + + return MAX (RTP_MIN_MISORDER, ctx->avg_packet_rate * time_ms / 1000); +} + +/** + * rtp_stats_init_defaults: + * @stats: an #RTPSessionStats struct + * + * Initialize @stats with its default values. + */ +void +rtp_stats_init_defaults (RTPSessionStats * stats) +{ + rtp_stats_set_bandwidths (stats, -1, -1, -1, -1); + stats->min_interval = RTP_STATS_MIN_INTERVAL; + stats->bye_timeout = RTP_STATS_BYE_TIMEOUT; + stats->nacks_dropped = 0; + stats->nacks_sent = 0; + stats->nacks_received = 0; +} + +/** + * rtp_stats_set_bandwidths: + * @stats: an #RTPSessionStats struct + * @rtp_bw: RTP bandwidth + * @rtcp_bw: RTCP bandwidth + * @rs: sender RTCP bandwidth + * @rr: receiver RTCP bandwidth + * + * Configure the bandwidth parameters in the stats. When an input variable is + * set to -1, it will be calculated from the other input variables and from the + * defaults. + */ +void +rtp_stats_set_bandwidths (RTPSessionStats * stats, guint rtp_bw, + gdouble rtcp_bw, guint rs, guint rr) +{ + GST_DEBUG ("recalc bandwidths: RTP %u, RTCP %f, RS %u, RR %u", rtp_bw, + rtcp_bw, rs, rr); + + /* when given, sender and receive bandwidth add up to the total + * rtcp bandwidth */ + if (rs != G_MAXUINT && rr != G_MAXUINT) + rtcp_bw = rs + rr; + + /* If rtcp_bw is between 0 and 1, it is a fraction of rtp_bw */ + if (rtcp_bw > 0.0 && rtcp_bw < 1.0) { + if (rtp_bw > 0.0) + rtcp_bw = rtp_bw * rtcp_bw; + else + rtcp_bw = -1.0; + } + + /* RTCP is 5% of the RTP bandwidth */ + if (rtp_bw == G_MAXUINT && rtcp_bw > 1.0) + rtp_bw = rtcp_bw * 20; + else if (rtp_bw != G_MAXUINT && rtcp_bw < 0.0) + rtcp_bw = rtp_bw / 20; + else if (rtp_bw == G_MAXUINT && rtcp_bw < 0.0) { + /* nothing given, take defaults */ + rtp_bw = RTP_STATS_BANDWIDTH; + rtcp_bw = rtp_bw * RTP_STATS_RTCP_FRACTION; + } + + stats->bandwidth = rtp_bw; + stats->rtcp_bandwidth = rtcp_bw; + + /* now figure out the fractions */ + if (rs == G_MAXUINT) { + /* rs unknown */ + if (rr == G_MAXUINT) { + /* both not given, use defaults */ + rs = stats->rtcp_bandwidth * RTP_STATS_SENDER_FRACTION; + rr = stats->rtcp_bandwidth * RTP_STATS_RECEIVER_FRACTION; + } else { + /* rr known, calculate rs */ + if (stats->rtcp_bandwidth > rr) + rs = stats->rtcp_bandwidth - rr; + else + rs = 0; + } + } else if (rr == G_MAXUINT) { + /* rs known, calculate rr */ + if (stats->rtcp_bandwidth > rs) + rr = stats->rtcp_bandwidth - rs; + else + rr = 0; + } + + if (stats->rtcp_bandwidth > 0) { + stats->sender_fraction = ((gdouble) rs) / ((gdouble) stats->rtcp_bandwidth); + stats->receiver_fraction = 1.0 - stats->sender_fraction; + } else { + /* no RTCP bandwidth, set dummy values */ + stats->sender_fraction = 0.0; + stats->receiver_fraction = 0.0; + } + GST_DEBUG ("bandwidths: RTP %u, RTCP %u, RS %f, RR %f", stats->bandwidth, + stats->rtcp_bandwidth, stats->sender_fraction, stats->receiver_fraction); +} + +/** + * rtp_stats_calculate_rtcp_interval: + * @stats: an #RTPSessionStats struct + * @sender: if we are a sender + * @profile: RTP profile of this session + * @ptp: if this session is a point-to-point session + * @first: if this is the first time + * + * Calculate the RTCP interval. The result of this function is the amount of + * time to wait (in nanoseconds) before sending a new RTCP message. + * + * Returns: the RTCP interval. + */ +GstClockTime +rtp_stats_calculate_rtcp_interval (RTPSessionStats * stats, gboolean we_send, + GstRTPProfile profile, gboolean ptp, gboolean first) +{ + gdouble members, senders, n; + gdouble avg_rtcp_size, rtcp_bw; + gdouble interval; + gdouble rtcp_min_time; + + if (profile == GST_RTP_PROFILE_AVPF || profile == GST_RTP_PROFILE_SAVPF) { + /* RFC 4585 3.4d), 3.5.1 */ + + if (first && !ptp) + rtcp_min_time = 1.0; + else + rtcp_min_time = 0.0; + } else { + /* Very first call at application start-up uses half the min + * delay for quicker notification while still allowing some time + * before reporting for randomization and to learn about other + * sources so the report interval will converge to the correct + * interval more quickly. + */ + rtcp_min_time = stats->min_interval; + if (first) + rtcp_min_time /= 2.0; + } + + /* Dedicate a fraction of the RTCP bandwidth to senders unless + * the number of senders is large enough that their share is + * more than that fraction. + */ + n = members = stats->active_sources; + senders = (gdouble) stats->sender_sources; + rtcp_bw = stats->rtcp_bandwidth; + + if (senders <= members * stats->sender_fraction) { + if (we_send) { + rtcp_bw *= stats->sender_fraction; + n = senders; + } else { + rtcp_bw *= stats->receiver_fraction; + n -= senders; + } + } + + /* no bandwidth for RTCP, return NONE to signal that we don't want to send + * RTCP packets */ + if (rtcp_bw <= 0.0001) + return GST_CLOCK_TIME_NONE; + + avg_rtcp_size = 8.0 * stats->avg_rtcp_packet_size; + /* + * The effective number of sites times the average packet size is + * the total number of octets sent when each site sends a report. + * Dividing this by the effective bandwidth gives the time + * interval over which those packets must be sent in order to + * meet the bandwidth target, with a minimum enforced. In that + * time interval we send one report so this time is also our + * average time between reports. + */ + GST_DEBUG ("avg size %f, n %f, rtcp_bw %f", avg_rtcp_size, n, rtcp_bw); + interval = avg_rtcp_size * n / rtcp_bw; + if (interval < rtcp_min_time) + interval = rtcp_min_time; + + return interval * GST_SECOND; +} + +/** + * rtp_stats_add_rtcp_jitter: + * @stats: an #RTPSessionStats struct + * @interval: an RTCP interval + * + * Apply a random jitter to the @interval. @interval is typically obtained with + * rtp_stats_calculate_rtcp_interval(). + * + * Returns: the new RTCP interval. + */ +GstClockTime +rtp_stats_add_rtcp_jitter (RTPSessionStats * stats G_GNUC_UNUSED, + GstClockTime interval) +{ + gdouble temp; + + /* see RFC 3550 p 30 + * To compensate for "unconditional reconsideration" converging to a + * value below the intended average. + */ +#define COMPENSATION (2.71828 - 1.5); + + temp = (interval * g_random_double_range (0.5, 1.5)) / COMPENSATION; + + return (GstClockTime) temp; +} + + +/** + * rtp_stats_calculate_bye_interval: + * @stats: an #RTPSessionStats struct + * + * Calculate the BYE interval. The result of this function is the amount of + * time to wait (in nanoseconds) before sending a BYE message. + * + * Returns: the BYE interval. + */ +GstClockTime +rtp_stats_calculate_bye_interval (RTPSessionStats * stats) +{ + gdouble members; + gdouble avg_rtcp_size, rtcp_bw; + gdouble interval; + gdouble rtcp_min_time; + + /* no interval when we have less than 50 members */ + if (stats->active_sources < 50) + return 0; + + rtcp_min_time = (stats->min_interval) / 2.0; + + /* Dedicate a fraction of the RTCP bandwidth to senders unless + * the number of senders is large enough that their share is + * more than that fraction. + */ + members = stats->bye_members; + rtcp_bw = stats->rtcp_bandwidth * stats->receiver_fraction; + + /* no bandwidth for RTCP, return NONE to signal that we don't want to send + * RTCP packets */ + if (rtcp_bw <= 0.0001) + return GST_CLOCK_TIME_NONE; + + avg_rtcp_size = 8.0 * stats->avg_rtcp_packet_size; + /* + * The effective number of sites times the average packet size is + * the total number of octets sent when each site sends a report. + * Dividing this by the effective bandwidth gives the time + * interval over which those packets must be sent in order to + * meet the bandwidth target, with a minimum enforced. In that + * time interval we send one report so this time is also our + * average time between reports. + */ + interval = avg_rtcp_size * members / rtcp_bw; + if (interval < rtcp_min_time) + interval = rtcp_min_time; + + return interval * GST_SECOND; +} + +/** + * rtp_stats_get_packets_lost: + * @stats: an #RTPSourceStats struct + * + * Calculate the total number of RTP packets lost since beginning of + * reception. Packets that arrive late are not considered lost, and + * duplicates are not taken into account. Hence, the loss may be negative + * if there are duplicates. + * + * Returns: total RTP packets lost. + */ +gint64 +rtp_stats_get_packets_lost (const RTPSourceStats * stats) +{ + gint64 lost; + guint64 extended_max, expected; + + extended_max = stats->cycles + stats->max_seq; + expected = extended_max - stats->base_seq + 1; + lost = expected - stats->packets_received; + + return lost; +} + +void +rtp_stats_set_min_interval (RTPSessionStats * stats, gdouble min_interval) +{ + stats->min_interval = min_interval; +} + +gboolean +__g_socket_address_equal (GSocketAddress * a, GSocketAddress * b) +{ + GInetSocketAddress *ia, *ib; + GInetAddress *iaa, *iab; + + ia = G_INET_SOCKET_ADDRESS (a); + ib = G_INET_SOCKET_ADDRESS (b); + + if (g_inet_socket_address_get_port (ia) != + g_inet_socket_address_get_port (ib)) + return FALSE; + + iaa = g_inet_socket_address_get_address (ia); + iab = g_inet_socket_address_get_address (ib); + + return g_inet_address_equal (iaa, iab); +} + +gchar * +__g_socket_address_to_string (GSocketAddress * addr) +{ + GInetSocketAddress *ia; + gchar *ret, *tmp; + + ia = G_INET_SOCKET_ADDRESS (addr); + + tmp = g_inet_address_to_string (g_inet_socket_address_get_address (ia)); + ret = g_strdup_printf ("%s:%u", tmp, g_inet_socket_address_get_port (ia)); + g_free (tmp); + + return ret; +} diff --git a/generic/gst-plugin-threadshare/src/jitterbuffer/rtpstats.h b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpstats.h new file mode 100644 index 000000000..bd3a54efe --- /dev/null +++ b/generic/gst-plugin-threadshare/src/jitterbuffer/rtpstats.h @@ -0,0 +1,267 @@ +/* GStreamer + * Copyright (C) <2007> Wim Taymans <wim.taymans@gmail.com> + * Copyright (C) 2015 Kurento (http://kurento.org/) + * @author: Miguel París <mparisdiaz@gmail.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __RTP_STATS_H__ +#define __RTP_STATS_H__ + +#include <gst/gst.h> +#include <gst/net/gstnetaddressmeta.h> +#include <gst/rtp/rtp.h> +#include <gio/gio.h> + +/* UDP/IP is assumed for bandwidth calculation */ +#define UDP_IP_HEADER_OVERHEAD 28 + +/** + * RTPSenderReport: + * + * A sender report structure. + */ +typedef struct { + gboolean is_valid; + guint64 ntptime; + guint32 rtptime; + guint32 packet_count; + guint32 octet_count; + GstClockTime time; +} RTPSenderReport; + +/** + * RTPReceiverReport: + * + * A receiver report structure. + */ +typedef struct { + gboolean is_valid; + guint32 ssrc; /* who the report is from */ + guint8 fractionlost; + guint32 packetslost; + guint32 exthighestseq; + guint32 jitter; + guint32 lsr; + guint32 dlsr; + guint32 round_trip; +} RTPReceiverReport; + +/** + * RTPPacketInfo: + * @send: if this is a packet for sending + * @rtp: if this info is about an RTP packet + * @is_list: if this is a bufferlist + * @data: a #GstBuffer or #GstBufferList + * @address: address of the sender of the packet + * @current_time: current time according to the system clock + * @running_time: time of a packet as buffer running_time + * @ntpnstime: time of a packet NTP time in nanoseconds + * @header_len: number of overhead bytes per packet + * @bytes: bytes of the packet including lowlevel overhead + * @payload_len: bytes of the RTP payload + * @seqnum: the seqnum of the packet + * @pt: the payload type of the packet + * @rtptime: the RTP time of the packet + * + * Structure holding information about the packet. + */ +typedef struct { + gboolean send; + gboolean rtp; + gboolean is_list; + gpointer data; + GSocketAddress *address; + GstClockTime current_time; + GstClockTime running_time; + guint64 ntpnstime; + guint header_len; + guint bytes; + guint packets; + guint payload_len; + guint32 ssrc; + guint16 seqnum; + guint8 pt; + guint32 rtptime; + guint32 csrc_count; + guint32 csrcs[16]; +} RTPPacketInfo; + +/** + * RTPSourceStats: + * @packets_received: number of received packets in total + * @prev_received: number of packets received in previous reporting + * interval + * @octets_received: number of payload bytes received + * @bytes_received: number of total bytes received including headers and lower + * protocol level overhead + * @max_seqnr: highest sequence number received + * @transit: previous transit time used for calculating @jitter + * @jitter: current jitter (in clock rate units scaled by 16 for precision) + * @prev_rtptime: previous time when an RTP packet was received + * @prev_rtcptime: previous time when an RTCP packet was received + * @last_rtptime: time when last RTP packet received + * @last_rtcptime: time when last RTCP packet received + * @curr_rr: index of current @rr block + * @rr: previous and current receiver report block + * @curr_sr: index of current @sr block + * @sr: previous and current sender report block + * + * Stats about a source. + */ +typedef struct { + guint64 packets_received; + guint64 octets_received; + guint64 bytes_received; + + guint32 prev_expected; + guint32 prev_received; + + guint16 max_seq; + guint64 cycles; + guint32 base_seq; + guint32 bad_seq; + guint32 transit; + guint32 jitter; + + guint64 packets_sent; + guint64 octets_sent; + + guint sent_pli_count; + guint recv_pli_count; + guint sent_fir_count; + guint recv_fir_count; + guint sent_nack_count; + guint recv_nack_count; + + /* when we received stuff */ + GstClockTime prev_rtptime; + GstClockTime prev_rtcptime; + GstClockTime last_rtptime; + GstClockTime last_rtcptime; + + /* sender and receiver reports */ + gint curr_rr; + RTPReceiverReport rr[2]; + gint curr_sr; + RTPSenderReport sr[2]; +} RTPSourceStats; + +#define RTP_STATS_BANDWIDTH 64000 +#define RTP_STATS_RTCP_FRACTION 0.05 +/* + * Minimum average time between RTCP packets from this site (in + * seconds). This time prevents the reports from `clumping' when + * sessions are small and the law of large numbers isn't helping + * to smooth out the traffic. It also keeps the report interval + * from becoming ridiculously small during transient outages like + * a network partition. + */ +#define RTP_STATS_MIN_INTERVAL 5.0 +/* + * Fraction of the RTCP bandwidth to be shared among active + * senders. (This fraction was chosen so that in a typical + * session with one or two active senders, the computed report + * time would be roughly equal to the minimum report time so that + * we don't unnecessarily slow down receiver reports.) The + * receiver fraction must be 1 - the sender fraction. + */ +#define RTP_STATS_SENDER_FRACTION (0.25) +#define RTP_STATS_RECEIVER_FRACTION (1.0 - RTP_STATS_SENDER_FRACTION) + +/* + * When receiving a BYE from a source, remove the source from the database + * after this timeout. + */ +#define RTP_STATS_BYE_TIMEOUT (2 * GST_SECOND) + +/* + * The default and minimum values of the maximum number of missing packets we tolerate. + * These are packets with asequence number bigger than the last seen packet. + */ +#define RTP_DEF_DROPOUT 3000 +#define RTP_MIN_DROPOUT 30 + +/* + * The default and minimum values of the maximum number of misordered packets we tolerate. + * These are packets with a sequence number smaller than the last seen packet. + */ +#define RTP_DEF_MISORDER 100 +#define RTP_MIN_MISORDER 10 + +/** + * RTPPacketRateCtx: + * + * Context to calculate the pseudo-average packet rate. + */ +typedef struct { + gboolean probed; + gint32 clock_rate; + guint16 last_seqnum; + guint64 last_ts; + guint32 avg_packet_rate; +} RTPPacketRateCtx; + +void gst_rtp_packet_rate_ctx_reset (RTPPacketRateCtx * ctx, gint32 clock_rate); +guint32 gst_rtp_packet_rate_ctx_update (RTPPacketRateCtx *ctx, guint16 seqnum, guint32 ts); +guint32 gst_rtp_packet_rate_ctx_get (RTPPacketRateCtx *ctx); +guint32 gst_rtp_packet_rate_ctx_get_max_dropout (RTPPacketRateCtx *ctx, gint32 time_ms); +guint32 gst_rtp_packet_rate_ctx_get_max_misorder (RTPPacketRateCtx *ctx, gint32 time_ms); + +/** + * RTPSessionStats: + * + * Stats kept for a session and used to produce RTCP packet timeouts. + */ +typedef struct { + guint bandwidth; + guint rtcp_bandwidth; + gdouble sender_fraction; + gdouble receiver_fraction; + gdouble min_interval; + GstClockTime bye_timeout; + guint internal_sources; + guint sender_sources; + guint internal_sender_sources; + guint active_sources; + guint avg_rtcp_packet_size; + guint bye_members; + guint nacks_dropped; + guint nacks_sent; + guint nacks_received; +} RTPSessionStats; + +void rtp_stats_init_defaults (RTPSessionStats *stats); + +void rtp_stats_set_bandwidths (RTPSessionStats *stats, + guint rtp_bw, + gdouble rtcp_bw, + guint rs, guint rr); + +GstClockTime rtp_stats_calculate_rtcp_interval (RTPSessionStats *stats, gboolean sender, GstRTPProfile profile, gboolean ptp, gboolean first); +GstClockTime rtp_stats_add_rtcp_jitter (RTPSessionStats *stats, GstClockTime interval); +GstClockTime rtp_stats_calculate_bye_interval (RTPSessionStats *stats); +gint64 rtp_stats_get_packets_lost (const RTPSourceStats *stats); + +void rtp_stats_set_min_interval (RTPSessionStats *stats, + gdouble min_interval); + + +gboolean __g_socket_address_equal (GSocketAddress *a, GSocketAddress *b); +gchar * __g_socket_address_to_string (GSocketAddress * addr); + +#endif /* __RTP_STATS_H__ */ diff --git a/generic/gst-plugin-threadshare/src/lib.rs b/generic/gst-plugin-threadshare/src/lib.rs new file mode 100644 index 000000000..42ecc114d --- /dev/null +++ b/generic/gst-plugin-threadshare/src/lib.rs @@ -0,0 +1,117 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +//! A collection of GStreamer plugins which leverage the `threadshare` [`runtime`]. +//! +//! [`runtime`]: runtime/index.html + +// Needed for `select!` in `Socket::next` +// see https://docs.rs/futures/0.3.1/futures/macro.select.html +#![recursion_limit = "1024"] +#![crate_type = "cdylib"] + +pub use tokio; + +#[macro_use] +pub mod runtime; + +pub mod socket; +mod tcpclientsrc; +mod udpsink; +mod udpsrc; + +mod appsrc; +pub mod dataqueue; +mod inputselector; +mod jitterbuffer; +mod proxy; +mod queue; + +use glib::translate::*; +use glib_sys as glib_ffi; + +use gst; +use gst::gst_plugin_define; +use gst::prelude::*; +use gstreamer_sys as gst_ffi; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + udpsrc::register(plugin)?; + udpsink::register(plugin)?; + tcpclientsrc::register(plugin)?; + queue::register(plugin)?; + proxy::register(plugin)?; + appsrc::register(plugin)?; + jitterbuffer::jitterbuffer::register(plugin)?; + inputselector::register(plugin)?; + + Ok(()) +} + +gst_plugin_define!( + threadshare, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "LGPL", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); + +pub fn set_element_flags<T: glib::IsA<gst::Object> + glib::IsA<gst::Element>>( + element: &T, + flags: gst::ElementFlags, +) { + unsafe { + let ptr: *mut gst_ffi::GstObject = element.as_ptr() as *mut _; + let _guard = MutexGuard::lock(&(*ptr).lock); + (*ptr).flags |= flags.to_glib(); + } +} + +struct MutexGuard<'a>(&'a glib_ffi::GMutex); + +impl<'a> MutexGuard<'a> { + pub fn lock(mutex: &'a glib_ffi::GMutex) -> Self { + unsafe { + glib_ffi::g_mutex_lock(mut_override(mutex)); + } + MutexGuard(mutex) + } +} + +impl<'a> Drop for MutexGuard<'a> { + fn drop(&mut self) { + unsafe { + glib_ffi::g_mutex_unlock(mut_override(self.0)); + } + } +} + +pub fn get_current_running_time(element: &gst::Element) -> gst::ClockTime { + if let Some(clock) = element.get_clock() { + if clock.get_time() > element.get_base_time() { + clock.get_time() - element.get_base_time() + } else { + 0.into() + } + } else { + gst::CLOCK_TIME_NONE + } +} diff --git a/generic/gst-plugin-threadshare/src/proxy.rs b/generic/gst-plugin-threadshare/src/proxy.rs new file mode 100644 index 000000000..19145451a --- /dev/null +++ b/generic/gst-plugin-threadshare/src/proxy.rs @@ -0,0 +1,1332 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::channel::oneshot; +use futures::future::BoxFuture; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_element_error, gst_error, gst_error_msg, gst_log, gst_trace}; + +use lazy_static::lazy_static; + +use std::collections::{HashMap, VecDeque}; +use std::sync::Mutex as StdMutex; +use std::sync::MutexGuard as StdMutexGuard; +use std::sync::{Arc, Weak}; +use std::{u32, u64}; + +use crate::runtime::prelude::*; +use crate::runtime::{ + Context, PadSink, PadSinkRef, PadSinkWeak, PadSrc, PadSrcRef, PadSrcWeak, Task, +}; + +use super::dataqueue::{DataQueue, DataQueueItem, DataQueueState}; + +lazy_static! { + static ref PROXY_CONTEXTS: StdMutex<HashMap<String, Weak<StdMutex<ProxyContextInner>>>> = + StdMutex::new(HashMap::new()); + static ref PROXY_SRC_PADS: StdMutex<HashMap<String, PadSrcWeak>> = + StdMutex::new(HashMap::new()); + static ref PROXY_SINK_PADS: StdMutex<HashMap<String, PadSinkWeak>> = + StdMutex::new(HashMap::new()); +} + +const DEFAULT_PROXY_CONTEXT: &str = ""; + +const DEFAULT_MAX_SIZE_BUFFERS: u32 = 200; +const DEFAULT_MAX_SIZE_BYTES: u32 = 1024 * 1024; +const DEFAULT_MAX_SIZE_TIME: u64 = gst::SECOND_VAL; +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; + +#[derive(Debug, Clone)] +struct SettingsSink { + proxy_context: String, +} + +impl Default for SettingsSink { + fn default() -> Self { + SettingsSink { + proxy_context: DEFAULT_PROXY_CONTEXT.into(), + } + } +} + +#[derive(Debug, Clone)] +struct SettingsSrc { + max_size_buffers: u32, + max_size_bytes: u32, + max_size_time: u64, + context: String, + context_wait: u32, + proxy_context: String, +} + +impl Default for SettingsSrc { + fn default() -> Self { + SettingsSrc { + max_size_buffers: DEFAULT_MAX_SIZE_BUFFERS, + max_size_bytes: DEFAULT_MAX_SIZE_BYTES, + max_size_time: DEFAULT_MAX_SIZE_TIME, + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + proxy_context: DEFAULT_PROXY_CONTEXT.into(), + } + } +} + +static PROPERTIES_SRC: [subclass::Property; 6] = [ + subclass::Property("max-size-buffers", |name| { + glib::ParamSpec::uint( + name, + "Max Size Buffers", + "Maximum number of buffers to queue (0=unlimited)", + 0, + u32::MAX, + DEFAULT_MAX_SIZE_BUFFERS, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-size-bytes", |name| { + glib::ParamSpec::uint( + name, + "Max Size Bytes", + "Maximum number of bytes to queue (0=unlimited)", + 0, + u32::MAX, + DEFAULT_MAX_SIZE_BYTES, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-size-time", |name| { + glib::ParamSpec::uint64( + name, + "Max Size Time", + "Maximum number of nanoseconds to queue (0=unlimited)", + 0, + u64::MAX - 1, + DEFAULT_MAX_SIZE_TIME, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("proxy-context", |name| { + glib::ParamSpec::string( + name, + "Proxy Context", + "Context name of the proxy to share with", + Some(DEFAULT_PROXY_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), +]; + +static PROPERTIES_SINK: [subclass::Property; 1] = [subclass::Property("proxy-context", |name| { + glib::ParamSpec::string( + name, + "Proxy Context", + "Context name of the proxy to share with", + Some(DEFAULT_PROXY_CONTEXT), + glib::ParamFlags::READWRITE, + ) +})]; + +// TODO: Refactor into a Sender and Receiver instead of the have_ booleans + +#[derive(Debug, Default)] +struct PendingQueue { + more_queue_space_sender: Option<oneshot::Sender<()>>, + scheduled: bool, + items: VecDeque<DataQueueItem>, +} + +impl PendingQueue { + fn notify_more_queue_space(&mut self) { + self.more_queue_space_sender.take(); + } +} + +#[derive(Debug)] +struct ProxyContextInner { + name: String, + dataqueue: Option<DataQueue>, + last_res: Result<gst::FlowSuccess, gst::FlowError>, + pending_queue: Option<PendingQueue>, + have_sink: bool, + have_src: bool, +} + +impl Drop for ProxyContextInner { + fn drop(&mut self) { + let mut proxy_ctxs = PROXY_CONTEXTS.lock().unwrap(); + proxy_ctxs.remove(&self.name); + } +} + +#[derive(Debug)] +struct ProxyContext { + shared: Arc<StdMutex<ProxyContextInner>>, + as_sink: bool, + name: String, +} + +impl ProxyContext { + #[inline] + fn lock_shared(&self) -> StdMutexGuard<'_, ProxyContextInner> { + self.shared.lock().unwrap() + } + + fn get(name: &str, as_sink: bool) -> Option<Self> { + let mut proxy_ctxs = PROXY_CONTEXTS.lock().unwrap(); + + let mut proxy_ctx = None; + if let Some(shared_weak) = proxy_ctxs.get(name) { + if let Some(shared) = shared_weak.upgrade() { + { + let shared = shared.lock().unwrap(); + if (shared.have_sink && as_sink) || (shared.have_src && !as_sink) { + return None; + } + } + + proxy_ctx = Some({ + let proxy_ctx = ProxyContext { + shared, + as_sink, + name: name.into(), + }; + { + let mut shared = proxy_ctx.lock_shared(); + if as_sink { + shared.have_sink = true; + } else { + shared.have_src = true; + } + } + + proxy_ctx + }); + } + } + + if proxy_ctx.is_none() { + let shared = Arc::new(StdMutex::new(ProxyContextInner { + name: name.into(), + dataqueue: None, + last_res: Err(gst::FlowError::Flushing), + pending_queue: None, + have_sink: as_sink, + have_src: !as_sink, + })); + + proxy_ctxs.insert(name.into(), Arc::downgrade(&shared)); + + proxy_ctx = Some(ProxyContext { + shared, + as_sink, + name: name.into(), + }); + } + + proxy_ctx + } +} + +impl Drop for ProxyContext { + fn drop(&mut self) { + let mut shared_ctx = self.lock_shared(); + if self.as_sink { + assert!(shared_ctx.have_sink); + shared_ctx.have_sink = false; + let _ = shared_ctx.pending_queue.take(); + } else { + assert!(shared_ctx.have_src); + shared_ctx.have_src = false; + let _ = shared_ctx.dataqueue.take(); + } + } +} + +#[derive(Clone, Debug)] +struct ProxySinkPadHandler; + +impl PadSinkHandler for ProxySinkPadHandler { + type ElementImpl = ProxySink; + + fn sink_chain( + &self, + pad: &PadSinkRef, + _proxysink: &ProxySink, + element: &gst::Element, + buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let pad_weak = pad.downgrade(); + let element = element.clone(); + + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + gst_log!(SINK_CAT, obj: pad.gst_pad(), "Handling {:?}", buffer); + let proxysink = ProxySink::from_instance(&element); + proxysink + .enqueue_item(&element, DataQueueItem::Buffer(buffer)) + .await + } + .boxed() + } + + fn sink_chain_list( + &self, + pad: &PadSinkRef, + _proxysink: &ProxySink, + element: &gst::Element, + list: gst::BufferList, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let pad_weak = pad.downgrade(); + let element = element.clone(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + gst_log!(SINK_CAT, obj: pad.gst_pad(), "Handling {:?}", list); + let proxysink = ProxySink::from_instance(&element); + proxysink + .enqueue_item(&element, DataQueueItem::BufferList(list)) + .await + } + .boxed() + } + + fn sink_event( + &self, + pad: &PadSinkRef, + proxysink: &ProxySink, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_debug!(SINK_CAT, obj: pad.gst_pad(), "Handling non-serialized {:?}", event); + + let src_pad = { + let proxy_ctx = proxysink.proxy_ctx.lock().unwrap(); + + PROXY_SRC_PADS + .lock() + .unwrap() + .get(&proxy_ctx.as_ref().unwrap().name) + .and_then(|src_pad| src_pad.upgrade()) + .map(|src_pad| src_pad.gst_pad().clone()) + }; + + if let EventView::FlushStart(..) = event.view() { + proxysink.stop(&element).unwrap(); + } + + if let Some(src_pad) = src_pad { + gst_log!(SINK_CAT, obj: pad.gst_pad(), "Forwarding non-serialized {:?}", event); + src_pad.push_event(event) + } else { + gst_error!(SINK_CAT, obj: pad.gst_pad(), "No src pad to forward non-serialized {:?} to", event); + true + } + } + + fn sink_event_serialized( + &self, + pad: &PadSinkRef, + _proxysink: &ProxySink, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + use gst::EventView; + + gst_log!(SINK_CAT, obj: pad.gst_pad(), "Handling serialized {:?}", event); + + let pad_weak = pad.downgrade(); + let element = element.clone(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + let proxysink = ProxySink::from_instance(&element); + + match event.view() { + EventView::Eos(..) => { + let _ = + element.post_message(&gst::Message::new_eos().src(Some(&element)).build()); + } + EventView::FlushStop(..) => proxysink.start(&element).unwrap(), + _ => (), + } + + gst_log!(SINK_CAT, obj: pad.gst_pad(), "Queuing serialized {:?}", event); + proxysink + .enqueue_item(&element, DataQueueItem::Event(event)) + .await + .is_ok() + } + .boxed() + } +} + +#[derive(Debug)] +struct ProxySink { + sink_pad: PadSink, + proxy_ctx: StdMutex<Option<ProxyContext>>, + settings: StdMutex<SettingsSink>, +} + +lazy_static! { + static ref SINK_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-proxysink", + gst::DebugColorFlags::empty(), + Some("Thread-sharing proxy sink"), + ); +} + +impl ProxySink { + fn schedule_pending_queue( + &self, + element: &gst::Element, + pending_queue: &mut PendingQueue, + ) -> impl Future<Output = ()> { + gst_log!(SINK_CAT, obj: element, "Scheduling pending queue now"); + + pending_queue.scheduled = true; + + let element = element.clone(); + async move { + let sink = Self::from_instance(&element); + + loop { + let more_queue_space_receiver = { + let proxy_ctx = sink.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + + gst_log!(SINK_CAT, obj: &element, "Trying to empty pending queue"); + + let ProxyContextInner { + pending_queue: ref mut pq, + ref dataqueue, + .. + } = *shared_ctx; + + if let Some(ref mut pending_queue) = *pq { + if let Some(ref dataqueue) = dataqueue { + let mut failed_item = None; + while let Some(item) = pending_queue.items.pop_front() { + if let Err(item) = dataqueue.push(item) { + failed_item = Some(item); + break; + } + } + + if let Some(failed_item) = failed_item { + pending_queue.items.push_front(failed_item); + let (sender, receiver) = oneshot::channel(); + pending_queue.more_queue_space_sender = Some(sender); + + receiver + } else { + gst_log!(SINK_CAT, obj: &element, "Pending queue is empty now"); + *pq = None; + return; + } + } else { + let (sender, receiver) = oneshot::channel(); + pending_queue.more_queue_space_sender = Some(sender); + + receiver + } + } else { + gst_log!(SINK_CAT, obj: &element, "Flushing, dropping pending queue"); + *pq = None; + return; + } + }; + + gst_log!(SINK_CAT, obj: &element, "Waiting for more queue space"); + let _ = more_queue_space_receiver.await; + } + } + } + + async fn enqueue_item( + &self, + element: &gst::Element, + item: DataQueueItem, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let wait_fut = { + let proxy_ctx = self.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + + /* We've taken the lock again, make sure not to recreate + * a pending queue if tearing down */ + shared_ctx.last_res?; + + let item = { + let ProxyContextInner { + ref mut pending_queue, + ref dataqueue, + .. + } = *shared_ctx; + + match (pending_queue, dataqueue) { + (None, Some(ref dataqueue)) => dataqueue.push(item), + (Some(ref mut pending_queue), Some(ref dataqueue)) => { + if !pending_queue.scheduled { + let mut failed_item = None; + while let Some(item) = pending_queue.items.pop_front() { + if let Err(item) = dataqueue.push(item) { + failed_item = Some(item); + break; + } + } + + if let Some(failed_item) = failed_item { + pending_queue.items.push_front(failed_item); + + Err(item) + } else { + dataqueue.push(item) + } + } else { + Err(item) + } + } + _ => Err(item), + } + }; + + if let Err(item) = item { + if shared_ctx + .pending_queue + .as_ref() + .map(|pending_queue| !pending_queue.scheduled) + .unwrap_or(true) + { + if shared_ctx.pending_queue.is_none() { + shared_ctx.pending_queue = Some(PendingQueue::default()); + } + + let pending_queue = shared_ctx.pending_queue.as_mut().unwrap(); + + let schedule_now = match item { + DataQueueItem::Event(ref ev) if ev.get_type() != gst::EventType::Eos => { + false + } + _ => true, + }; + + pending_queue.items.push_back(item); + + gst_log!( + SINK_CAT, + obj: element, + "Proxy is full - Pushing first item on pending queue" + ); + + if schedule_now { + let wait_fut = self.schedule_pending_queue(element, pending_queue); + Some(wait_fut) + } else { + gst_log!(SINK_CAT, obj: element, "Scheduling pending queue later"); + + None + } + } else { + shared_ctx + .pending_queue + .as_mut() + .unwrap() + .items + .push_back(item); + + None + } + } else { + None + } + }; + + if let Some(wait_fut) = wait_fut { + gst_log!( + SINK_CAT, + obj: element, + "Blocking until queue has space again" + ); + wait_fut.await; + } + + let proxy_ctx = self.proxy_ctx.lock().unwrap(); + let shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + shared_ctx.last_res + } + + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_debug!(SINK_CAT, obj: element, "Preparing"); + + let proxy_context = self.settings.lock().unwrap().proxy_context.to_string(); + + let proxy_ctx = ProxyContext::get(&proxy_context, true).ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to create or get ProxyContext"] + ) + })?; + + { + let mut proxy_sink_pads = PROXY_SINK_PADS.lock().unwrap(); + assert!(!proxy_sink_pads.contains_key(&proxy_context)); + proxy_sink_pads.insert(proxy_context, self.sink_pad.downgrade()); + } + + *self.proxy_ctx.lock().unwrap() = Some(proxy_ctx); + + self.sink_pad.prepare(&ProxySinkPadHandler); + + gst_debug!(SINK_CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(SINK_CAT, obj: element, "Unpreparing"); + + self.sink_pad.unprepare(); + *self.proxy_ctx.lock().unwrap() = None; + + gst_debug!(SINK_CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let proxy_ctx = self.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + + gst_debug!(SINK_CAT, obj: element, "Starting"); + + { + let settings = self.settings.lock().unwrap(); + let mut proxy_sink_pads = PROXY_SINK_PADS.lock().unwrap(); + proxy_sink_pads.remove(&settings.proxy_context); + } + + shared_ctx.last_res = Ok(gst::FlowSuccess::Ok); + + gst_debug!(SINK_CAT, obj: element, "Started"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + let proxy_ctx = self.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + + gst_debug!(SINK_CAT, obj: element, "Stopping"); + + let _ = shared_ctx.pending_queue.take(); + shared_ctx.last_res = Err(gst::FlowError::Flushing); + + gst_debug!(SINK_CAT, obj: element, "Stopped"); + + Ok(()) + } +} + +impl ObjectSubclass for ProxySink { + const NAME: &'static str = "RsTsProxySink"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing proxy sink", + "Sink/Generic", + "Thread-sharing proxy sink", + "Sebastian Dröge <sebastian@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + klass.install_properties(&PROPERTIES_SINK); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + sink_pad: PadSink::new(gst::Pad::new_from_template( + &klass.get_pad_template("sink").unwrap(), + Some("sink"), + )), + proxy_ctx: StdMutex::new(None), + settings: StdMutex::new(SettingsSink::default()), + } + } +} + +impl ObjectImpl for ProxySink { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES_SINK[id]; + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("proxy-context", ..) => { + settings.proxy_context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES_SINK[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("proxy-context", ..) => Ok(settings.proxy_context.to_value()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.sink_pad.gst_pad()).unwrap(); + + super::set_element_flags(element, gst::ElementFlags::SINK); + } +} + +impl ElementImpl for ProxySink { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(SINK_CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToPaused { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + + Ok(success) + } +} + +#[derive(Clone, Debug)] +struct ProxySrcPadHandler; + +impl ProxySrcPadHandler { + async fn push_item( + pad: &PadSrcRef<'_>, + proxysrc: &ProxySrc, + item: DataQueueItem, + ) -> Result<(), gst::FlowError> { + { + let proxy_ctx = proxysrc.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + if let Some(ref mut pending_queue) = shared_ctx.pending_queue { + pending_queue.notify_more_queue_space(); + } + } + + match item { + DataQueueItem::Buffer(buffer) => { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Forwarding {:?}", buffer); + pad.push(buffer).await.map(drop) + } + DataQueueItem::BufferList(list) => { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Forwarding {:?}", list); + pad.push_list(list).await.map(drop) + } + DataQueueItem::Event(event) => { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + pad.push_event(event).await; + Ok(()) + } + } + } +} + +impl PadSrcHandler for ProxySrcPadHandler { + type ElementImpl = ProxySrc; + + fn src_event( + &self, + pad: &PadSrcRef, + proxysrc: &ProxySrc, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + let sink_pad = { + let proxy_ctx = proxysrc.proxy_ctx.lock().unwrap(); + + PROXY_SINK_PADS + .lock() + .unwrap() + .get(&proxy_ctx.as_ref().unwrap().name) + .and_then(|sink_pad| sink_pad.upgrade()) + .map(|sink_pad| sink_pad.gst_pad().clone()) + }; + + match event.view() { + EventView::FlushStart(..) => proxysrc.stop(element).unwrap(), + EventView::FlushStop(..) => proxysrc.flush_stop(element), + _ => (), + } + + if let Some(sink_pad) = sink_pad { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + sink_pad.push_event(event) + } else { + gst_error!(SRC_CAT, obj: pad.gst_pad(), "No sink pad to forward {:?} to", event); + false + } + } + + fn src_query( + &self, + pad: &PadSrcRef, + _proxysrc: &ProxySrc, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Handling {:?}", query); + let ret = match query.view_mut() { + QueryView::Latency(ref mut q) => { + q.set(true, 0.into(), gst::CLOCK_TIME_NONE); + true + } + QueryView::Scheduling(ref mut q) => { + q.set(gst::SchedulingFlags::SEQUENTIAL, 1, -1, 0); + q.add_scheduling_modes(&[gst::PadMode::Push]); + true + } + QueryView::Caps(ref mut q) => { + let caps = if let Some(ref caps) = pad.gst_pad().get_current_caps() { + q.get_filter() + .map(|f| f.intersect_with_mode(caps, gst::CapsIntersectMode::First)) + .unwrap_or_else(|| caps.clone()) + } else { + q.get_filter() + .map(|f| f.to_owned()) + .unwrap_or_else(gst::Caps::new_any) + }; + + q.set_result(&caps); + + true + } + _ => false, + }; + + if ret { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Handled {:?}", query); + } else { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Didn't handle {:?}", query); + } + + ret + } +} + +#[derive(Debug)] +struct ProxySrc { + src_pad: PadSrc, + task: Task, + proxy_ctx: StdMutex<Option<ProxyContext>>, + dataqueue: StdMutex<Option<DataQueue>>, + settings: StdMutex<SettingsSrc>, +} + +lazy_static! { + static ref SRC_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-proxysrc", + gst::DebugColorFlags::empty(), + Some("Thread-sharing proxy source"), + ); +} + +impl ProxySrc { + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_debug!(SRC_CAT, obj: element, "Preparing"); + + let settings = self.settings.lock().unwrap().clone(); + + let proxy_ctx = ProxyContext::get(&settings.proxy_context, false).ok_or_else(|| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to create get shared_state"] + ) + })?; + + let ts_ctx = Context::acquire(&settings.context, settings.context_wait).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to acquire Context: {}", err] + ) + })?; + + let dataqueue = DataQueue::new( + &element.clone().upcast(), + self.src_pad.gst_pad(), + if settings.max_size_buffers == 0 { + None + } else { + Some(settings.max_size_buffers) + }, + if settings.max_size_bytes == 0 { + None + } else { + Some(settings.max_size_bytes) + }, + if settings.max_size_time == 0 { + None + } else { + Some(settings.max_size_time) + }, + ); + + { + let mut shared_ctx = proxy_ctx.lock_shared(); + shared_ctx.dataqueue = Some(dataqueue.clone()); + + let mut proxy_src_pads = PROXY_SRC_PADS.lock().unwrap(); + assert!(!proxy_src_pads.contains_key(&settings.proxy_context)); + proxy_src_pads.insert(settings.proxy_context, self.src_pad.downgrade()); + } + + *self.proxy_ctx.lock().unwrap() = Some(proxy_ctx); + + *self.dataqueue.lock().unwrap() = Some(dataqueue); + + self.src_pad.prepare(&ProxySrcPadHandler); + + self.task.prepare(ts_ctx).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + + gst_debug!(SRC_CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(SRC_CAT, obj: element, "Unpreparing"); + + { + let settings = self.settings.lock().unwrap(); + let mut proxy_src_pads = PROXY_SRC_PADS.lock().unwrap(); + proxy_src_pads.remove(&settings.proxy_context); + } + + self.task.unprepare().unwrap(); + self.src_pad.unprepare(); + + *self.dataqueue.lock().unwrap() = None; + *self.proxy_ctx.lock().unwrap() = None; + + gst_debug!(SRC_CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + // Keep the lock on the `dataqueue` until `stop` is complete + // so as to prevent race conditions due to concurrent FlushStart/Stop. + // Note that this won't deadlock as `ProxySrc::dataqueue` guards a pointer to + // the `dataqueue` used within the `src_pad`'s `Task`. + let dataqueue = self.dataqueue.lock().unwrap(); + gst_debug!(SRC_CAT, obj: element, "Stopping"); + + self.task.stop(); + + let dataqueue = dataqueue.as_ref().unwrap(); + dataqueue.clear(); + dataqueue.stop(); + + gst_debug!(SRC_CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let dataqueue = self.dataqueue.lock().unwrap(); + let dataqueue = dataqueue.as_ref().unwrap(); + if dataqueue.state() == DataQueueState::Started { + gst_debug!(SRC_CAT, obj: element, "Already started"); + return Ok(()); + } + + gst_debug!(SRC_CAT, obj: element, "Starting"); + + self.start_unchecked(element, dataqueue); + + gst_debug!(SRC_CAT, obj: element, "Started"); + + Ok(()) + } + + fn start_unchecked(&self, element: &gst::Element, dataqueue: &DataQueue) { + dataqueue.start(); + + { + let proxy_ctx = self.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + if let Some(pending_queue) = shared_ctx.pending_queue.as_mut() { + pending_queue.notify_more_queue_space(); + } + } + + self.start_task(element, dataqueue); + } + + fn start_task(&self, element: &gst::Element, dataqueue: &DataQueue) { + let pad_weak = self.src_pad.downgrade(); + let dataqueue = dataqueue.clone(); + let element = element.clone(); + + self.task.start(move || { + let pad_weak = pad_weak.clone(); + let mut dataqueue = dataqueue.clone(); + let element = element.clone(); + + async move { + let item = dataqueue.next().await; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + let item = match item { + Some(item) => item, + None => { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "DataQueue Stopped or Paused"); + return glib::Continue(false); + } + }; + + let proxysrc = ProxySrc::from_instance(&element); + + match ProxySrcPadHandler::push_item(&pad, &proxysrc, item).await { + Ok(_) => { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Successfully pushed item"); + let proxy_ctx = proxysrc.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + shared_ctx.last_res = Ok(gst::FlowSuccess::Ok); + glib::Continue(true) + } + Err(gst::FlowError::Flushing) => { + gst_debug!(SRC_CAT, obj: pad.gst_pad(), "Flushing"); + let proxy_ctx = proxysrc.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + shared_ctx.last_res = Err(gst::FlowError::Flushing); + glib::Continue(false) + } + Err(gst::FlowError::Eos) => { + gst_debug!(SRC_CAT, obj: pad.gst_pad(), "EOS"); + let proxy_ctx = proxysrc.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + shared_ctx.last_res = Err(gst::FlowError::Eos); + glib::Continue(false) + } + Err(err) => { + gst_error!(SRC_CAT, obj: pad.gst_pad(), "Got error {}", err); + gst_element_error!( + element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + let proxy_ctx = proxysrc.proxy_ctx.lock().unwrap(); + let mut shared_ctx = proxy_ctx.as_ref().unwrap().lock_shared(); + shared_ctx.last_res = Err(err); + glib::Continue(false) + } + } + } + }); + } + + fn flush_stop(&self, element: &gst::Element) { + // Keep the lock on the `dataqueue` until `flush_stop` is complete + // so as to prevent race conditions due to concurrent state transitions. + // Note that this won't deadlock as `ProxySrc::dataqueue` guards a pointer to + // the `dataqueue` used within the `src_pad`'s `Task`. + let dataqueue = self.dataqueue.lock().unwrap(); + let dataqueue = dataqueue.as_ref().unwrap(); + if dataqueue.state() == DataQueueState::Started { + gst_debug!(SRC_CAT, obj: element, "Already started"); + return; + } + + gst_debug!(SRC_CAT, obj: element, "Stopping Flush"); + + self.task.stop(); + self.start_unchecked(element, dataqueue); + + gst_debug!(SRC_CAT, obj: element, "Stopped Flush"); + } + + fn pause(&self, element: &gst::Element) -> Result<(), ()> { + let dataqueue = self.dataqueue.lock().unwrap(); + gst_debug!(SRC_CAT, obj: element, "Pausing"); + + dataqueue.as_ref().unwrap().pause(); + self.task.pause(); + + gst_debug!(SRC_CAT, obj: element, "Paused"); + + Ok(()) + } +} + +impl ObjectSubclass for ProxySrc { + const NAME: &'static str = "RsTsProxySrc"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing proxy source", + "Source/Generic", + "Thread-sharing proxy source", + "Sebastian Dröge <sebastian@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES_SRC); + } + + fn new() -> Self { + unreachable!() + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + task: Task::default(), + proxy_ctx: StdMutex::new(None), + dataqueue: StdMutex::new(None), + settings: StdMutex::new(SettingsSrc::default()), + } + } +} + +impl ObjectImpl for ProxySrc { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES_SRC[id]; + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("max-size-buffers", ..) => { + settings.max_size_buffers = value.get_some().expect("type checked upstream"); + } + subclass::Property("max-size-bytes", ..) => { + settings.max_size_bytes = value.get_some().expect("type checked upstream"); + } + subclass::Property("max-size-time", ..) => { + settings.max_size_time = value.get_some().expect("type checked upstream"); + } + subclass::Property("context", ..) => { + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + settings.context_wait = value.get_some().expect("type checked upstream"); + } + subclass::Property("proxy-context", ..) => { + settings.proxy_context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES_SRC[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("max-size-buffers", ..) => Ok(settings.max_size_buffers.to_value()), + subclass::Property("max-size-bytes", ..) => Ok(settings.max_size_bytes.to_value()), + subclass::Property("max-size-time", ..) => Ok(settings.max_size_time.to_value()), + subclass::Property("context", ..) => Ok(settings.context.to_value()), + subclass::Property("context-wait", ..) => Ok(settings.context_wait.to_value()), + subclass::Property("proxy-context", ..) => Ok(settings.proxy_context.to_value()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + + super::set_element_flags(element, gst::ElementFlags::SOURCE); + } +} + +impl ElementImpl for ProxySrc { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(SRC_CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PlayingToPaused => { + self.pause(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::ReadyToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToPlaying => { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-proxysink", + gst::Rank::None, + ProxySink::get_type(), + )?; + gst::Element::register( + Some(plugin), + "ts-proxysrc", + gst::Rank::None, + ProxySrc::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/src/queue.rs b/generic/gst-plugin-threadshare/src/queue.rs new file mode 100644 index 000000000..7c02602ff --- /dev/null +++ b/generic/gst-plugin-threadshare/src/queue.rs @@ -0,0 +1,867 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::channel::oneshot; +use futures::future::BoxFuture; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_element_error, gst_error, gst_error_msg, gst_log, gst_trace}; + +use lazy_static::lazy_static; + +use std::collections::VecDeque; +use std::sync::Mutex as StdMutex; +use std::{u32, u64}; + +use crate::runtime::prelude::*; +use crate::runtime::{Context, PadSink, PadSinkRef, PadSrc, PadSrcRef, Task}; + +use super::dataqueue::{DataQueue, DataQueueItem, DataQueueState}; + +const DEFAULT_MAX_SIZE_BUFFERS: u32 = 200; +const DEFAULT_MAX_SIZE_BYTES: u32 = 1024 * 1024; +const DEFAULT_MAX_SIZE_TIME: u64 = gst::SECOND_VAL; +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; + +#[derive(Debug, Clone)] +struct Settings { + max_size_buffers: u32, + max_size_bytes: u32, + max_size_time: u64, + context: String, + context_wait: u32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + max_size_buffers: DEFAULT_MAX_SIZE_BUFFERS, + max_size_bytes: DEFAULT_MAX_SIZE_BYTES, + max_size_time: DEFAULT_MAX_SIZE_TIME, + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + } + } +} + +static PROPERTIES: [subclass::Property; 5] = [ + subclass::Property("max-size-buffers", |name| { + glib::ParamSpec::uint( + name, + "Max Size Buffers", + "Maximum number of buffers to queue (0=unlimited)", + 0, + u32::MAX, + DEFAULT_MAX_SIZE_BUFFERS, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-size-bytes", |name| { + glib::ParamSpec::uint( + name, + "Max Size Bytes", + "Maximum number of bytes to queue (0=unlimited)", + 0, + u32::MAX, + DEFAULT_MAX_SIZE_BYTES, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("max-size-time", |name| { + glib::ParamSpec::uint64( + name, + "Max Size Time", + "Maximum number of nanoseconds to queue (0=unlimited)", + 0, + u64::MAX - 1, + DEFAULT_MAX_SIZE_TIME, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Debug)] +struct PendingQueue { + more_queue_space_sender: Option<oneshot::Sender<()>>, + scheduled: bool, + items: VecDeque<DataQueueItem>, +} + +impl PendingQueue { + fn notify_more_queue_space(&mut self) { + self.more_queue_space_sender.take(); + } +} + +#[derive(Clone)] +struct QueuePadSinkHandler; + +impl PadSinkHandler for QueuePadSinkHandler { + type ElementImpl = Queue; + + fn sink_chain( + &self, + pad: &PadSinkRef, + _queue: &Queue, + element: &gst::Element, + buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let pad_weak = pad.downgrade(); + let element = element.clone(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", buffer); + let queue = Queue::from_instance(&element); + queue + .enqueue_item(&element, DataQueueItem::Buffer(buffer)) + .await + } + .boxed() + } + + fn sink_chain_list( + &self, + pad: &PadSinkRef, + _queue: &Queue, + element: &gst::Element, + list: gst::BufferList, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let pad_weak = pad.downgrade(); + let element = element.clone(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", list); + let queue = Queue::from_instance(&element); + queue + .enqueue_item(&element, DataQueueItem::BufferList(list)) + .await + } + .boxed() + } + + fn sink_event( + &self, + pad: &PadSinkRef, + queue: &Queue, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_debug!(CAT, obj: pad.gst_pad(), "Handling non-serialized {:?}", event); + + if let EventView::FlushStart(..) = event.view() { + queue.stop(&element).unwrap(); + } + + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding non-serialized {:?}", event); + queue.src_pad.gst_pad().push_event(event) + } + + fn sink_event_serialized( + &self, + pad: &PadSinkRef, + _queue: &Queue, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling serialized {:?}", event); + + let pad_weak = pad.downgrade(); + let element = element.clone(); + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + let queue = Queue::from_instance(&element); + + if let EventView::FlushStop(..) = event.view() { + queue.flush_stop(&element); + } + + gst_log!(CAT, obj: pad.gst_pad(), "Queuing serialized {:?}", event); + queue + .enqueue_item(&element, DataQueueItem::Event(event)) + .await + .is_ok() + } + .boxed() + } + + fn sink_query( + &self, + pad: &PadSinkRef, + queue: &Queue, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", query); + + if query.is_serialized() { + // FIXME: How can we do this? + gst_log!(CAT, obj: pad.gst_pad(), "Dropping serialized {:?}", query); + false + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", query); + queue.src_pad.gst_pad().peer_query(query) + } + } +} + +#[derive(Clone, Debug)] +struct QueuePadSrcHandler; + +impl QueuePadSrcHandler { + async fn push_item( + pad: &PadSrcRef<'_>, + element: &gst::Element, + item: DataQueueItem, + ) -> Result<(), gst::FlowError> { + let queue = Queue::from_instance(element); + + if let Some(pending_queue) = queue.pending_queue.lock().unwrap().as_mut() { + pending_queue.notify_more_queue_space(); + } + + match item { + DataQueueItem::Buffer(buffer) => { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", buffer); + pad.push(buffer).await.map(drop) + } + DataQueueItem::BufferList(list) => { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", list); + pad.push_list(list).await.map(drop) + } + DataQueueItem::Event(event) => { + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + pad.push_event(event).await; + Ok(()) + } + } + } +} + +impl PadSrcHandler for QueuePadSrcHandler { + type ElementImpl = Queue; + + fn src_event( + &self, + pad: &PadSrcRef, + queue: &Queue, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + match event.view() { + EventView::FlushStart(..) => queue.stop(element).unwrap(), + EventView::FlushStop(..) => queue.flush_stop(element), + _ => (), + } + + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", event); + queue.sink_pad.gst_pad().push_event(event) + } + + fn src_query( + &self, + pad: &PadSrcRef, + queue: &Queue, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", query); + + if let QueryView::Scheduling(ref mut q) = query.view_mut() { + let mut new_query = gst::Query::new_scheduling(); + let res = queue.sink_pad.gst_pad().peer_query(&mut new_query); + if !res { + return res; + } + + gst_log!(CAT, obj: pad.gst_pad(), "Upstream returned {:?}", new_query); + + let (flags, min, max, align) = new_query.get_result(); + q.set(flags, min, max, align); + q.add_scheduling_modes( + &new_query + .get_scheduling_modes() + .iter() + .cloned() + .filter(|m| m != &gst::PadMode::Pull) + .collect::<Vec<_>>(), + ); + gst_log!(CAT, obj: pad.gst_pad(), "Returning {:?}", q.get_mut_query()); + return true; + } + + gst_log!(CAT, obj: pad.gst_pad(), "Forwarding {:?}", query); + queue.sink_pad.gst_pad().peer_query(query) + } +} + +#[derive(Debug)] +struct Queue { + sink_pad: PadSink, + src_pad: PadSrc, + task: Task, + dataqueue: StdMutex<Option<DataQueue>>, + pending_queue: StdMutex<Option<PendingQueue>>, + last_res: StdMutex<Result<gst::FlowSuccess, gst::FlowError>>, + settings: StdMutex<Settings>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-queue", + gst::DebugColorFlags::empty(), + Some("Thread-sharing queue"), + ); +} + +impl Queue { + /* Try transfering all the items from the pending queue to the DataQueue, then + * the current item. Errors out if the DataQueue was full, or the pending queue + * is already scheduled, in which case the current item should be added to the + * pending queue */ + fn queue_until_full( + &self, + dataqueue: &DataQueue, + pending_queue: &mut Option<PendingQueue>, + item: DataQueueItem, + ) -> Result<(), DataQueueItem> { + match pending_queue { + None => dataqueue.push(item), + Some(PendingQueue { + scheduled: false, + ref mut items, + .. + }) => { + let mut failed_item = None; + while let Some(item) = items.pop_front() { + if let Err(item) = dataqueue.push(item) { + failed_item = Some(item); + } + } + + if let Some(failed_item) = failed_item { + items.push_front(failed_item); + + Err(item) + } else { + dataqueue.push(item) + } + } + _ => Err(item), + } + } + + /* Schedules emptying of the pending queue. If there is an upstream + * TaskContext, the new task is spawned, it is otherwise + * returned, for the caller to block on */ + fn schedule_pending_queue( + &self, + element: &gst::Element, + pending_queue: &mut Option<PendingQueue>, + ) -> impl Future<Output = ()> { + gst_log!(CAT, obj: element, "Scheduling pending queue now"); + + pending_queue.as_mut().unwrap().scheduled = true; + + let element = element.clone(); + async move { + let queue = Self::from_instance(&element); + + loop { + let more_queue_space_receiver = { + let dataqueue = queue.dataqueue.lock().unwrap(); + if dataqueue.is_none() { + return; + } + let mut pending_queue_grd = queue.pending_queue.lock().unwrap(); + + gst_log!(CAT, obj: &element, "Trying to empty pending queue"); + + if let Some(pending_queue) = pending_queue_grd.as_mut() { + let mut failed_item = None; + while let Some(item) = pending_queue.items.pop_front() { + if let Err(item) = dataqueue.as_ref().unwrap().push(item) { + failed_item = Some(item); + } + } + + if let Some(failed_item) = failed_item { + pending_queue.items.push_front(failed_item); + let (sender, receiver) = oneshot::channel(); + pending_queue.more_queue_space_sender = Some(sender); + + receiver + } else { + gst_log!(CAT, obj: &element, "Pending queue is empty now"); + *pending_queue_grd = None; + return; + } + } else { + gst_log!(CAT, obj: &element, "Flushing, dropping pending queue"); + return; + } + }; + + gst_log!(CAT, obj: &element, "Waiting for more queue space"); + let _ = more_queue_space_receiver.await; + } + } + } + + async fn enqueue_item( + &self, + element: &gst::Element, + item: DataQueueItem, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let wait_fut = { + let dataqueue = self.dataqueue.lock().unwrap(); + let dataqueue = dataqueue.as_ref().ok_or_else(|| { + gst_error!(CAT, obj: element, "No DataQueue"); + gst::FlowError::Error + })?; + + let mut pending_queue = self.pending_queue.lock().unwrap(); + + if let Err(item) = self.queue_until_full(&dataqueue, &mut pending_queue, item) { + if pending_queue + .as_ref() + .map(|pq| !pq.scheduled) + .unwrap_or(true) + { + if pending_queue.is_none() { + *pending_queue = Some(PendingQueue { + more_queue_space_sender: None, + scheduled: false, + items: VecDeque::new(), + }); + } + + let schedule_now = match item { + DataQueueItem::Event(ref ev) if ev.get_type() != gst::EventType::Eos => { + false + } + _ => true, + }; + + pending_queue.as_mut().unwrap().items.push_back(item); + + gst_log!( + CAT, + obj: element, + "Queue is full - Pushing first item on pending queue" + ); + + if schedule_now { + let wait_fut = self.schedule_pending_queue(element, &mut pending_queue); + Some(wait_fut) + } else { + gst_log!(CAT, obj: element, "Scheduling pending queue later"); + None + } + } else { + pending_queue.as_mut().unwrap().items.push_back(item); + None + } + } else { + None + } + }; + + if let Some(wait_fut) = wait_fut { + gst_log!(CAT, obj: element, "Blocking until queue has space again"); + wait_fut.await; + } + + *self.last_res.lock().unwrap() + } + + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_debug!(CAT, obj: element, "Preparing"); + + let settings = self.settings.lock().unwrap().clone(); + + let dataqueue = DataQueue::new( + &element.clone().upcast(), + self.src_pad.gst_pad(), + if settings.max_size_buffers == 0 { + None + } else { + Some(settings.max_size_buffers) + }, + if settings.max_size_bytes == 0 { + None + } else { + Some(settings.max_size_bytes) + }, + if settings.max_size_time == 0 { + None + } else { + Some(settings.max_size_time) + }, + ); + + *self.dataqueue.lock().unwrap() = Some(dataqueue); + + let context = + Context::acquire(&settings.context, settings.context_wait).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to acquire Context: {}", err] + ) + })?; + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + + self.src_pad.prepare(&QueuePadSrcHandler); + self.sink_pad.prepare(&QueuePadSinkHandler); + + gst_debug!(CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Unpreparing"); + + self.sink_pad.unprepare(); + self.task.unprepare().unwrap(); + self.src_pad.unprepare(); + + *self.dataqueue.lock().unwrap() = None; + *self.pending_queue.lock().unwrap() = None; + + *self.last_res.lock().unwrap() = Ok(gst::FlowSuccess::Ok); + + gst_debug!(CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + let dataqueue = self.dataqueue.lock().unwrap(); + gst_debug!(CAT, obj: element, "Stopping"); + + *self.last_res.lock().unwrap() = Err(gst::FlowError::Flushing); + + self.task.stop(); + + if let Some(dataqueue) = dataqueue.as_ref() { + dataqueue.pause(); + dataqueue.clear(); + dataqueue.stop(); + } + + if let Some(mut pending_queue) = self.pending_queue.lock().unwrap().take() { + pending_queue.notify_more_queue_space(); + } + + gst_debug!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let dataqueue = self.dataqueue.lock().unwrap(); + let dataqueue = dataqueue.as_ref().unwrap(); + if dataqueue.state() == DataQueueState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return Ok(()); + } + + gst_debug!(CAT, obj: element, "Starting"); + + dataqueue.start(); + *self.last_res.lock().unwrap() = Ok(gst::FlowSuccess::Ok); + + self.start_task(element, dataqueue); + + gst_debug!(CAT, obj: element, "Started"); + + Ok(()) + } + + fn start_task(&self, element: &gst::Element, dataqueue: &DataQueue) { + let pad_weak = self.src_pad.downgrade(); + let dataqueue = dataqueue.clone(); + let element = element.clone(); + + self.task.start(move || { + let pad_weak = pad_weak.clone(); + let mut dataqueue = dataqueue.clone(); + let element = element.clone(); + + async move { + let item = dataqueue.next().await; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + let item = match item { + Some(item) => item, + None => { + gst_log!(CAT, obj: pad.gst_pad(), "DataQueue Stopped"); + return glib::Continue(false); + } + }; + + let queue = Queue::from_instance(&element); + + match QueuePadSrcHandler::push_item(&pad, &element, item).await { + Ok(()) => { + gst_log!(CAT, obj: pad.gst_pad(), "Successfully pushed item"); + *queue.last_res.lock().unwrap() = Ok(gst::FlowSuccess::Ok); + glib::Continue(true) + } + Err(gst::FlowError::Flushing) => { + gst_debug!(CAT, obj: pad.gst_pad(), "Flushing"); + *queue.last_res.lock().unwrap() = Err(gst::FlowError::Flushing); + glib::Continue(false) + } + Err(gst::FlowError::Eos) => { + gst_debug!(CAT, obj: pad.gst_pad(), "EOS"); + *queue.last_res.lock().unwrap() = Err(gst::FlowError::Eos); + let eos = gst::Event::new_eos().build(); + pad.push_event(eos).await; + glib::Continue(false) + } + Err(err) => { + gst_error!(CAT, obj: pad.gst_pad(), "Got error {}", err); + gst_element_error!( + element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + *queue.last_res.lock().unwrap() = Err(err); + glib::Continue(false) + } + } + } + }); + } + + fn flush_stop(&self, element: &gst::Element) { + // Keep the lock on the `dataqueue` until `flush_stop` is complete + // so as to prevent race conditions due to concurrent state transitions. + // Note that this won't deadlock as `Queue::dataqueue` guards a pointer to + // the `dataqueue` used within the `src_pad`'s `Task`. + let dataqueue = self.dataqueue.lock().unwrap(); + let dataqueue = dataqueue.as_ref().unwrap(); + if dataqueue.state() == DataQueueState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return; + } + + gst_debug!(CAT, obj: element, "Stopping Flush"); + + dataqueue.start(); + self.start_task(element, dataqueue); + + gst_debug!(CAT, obj: element, "Stopped Flush"); + } +} + +impl ObjectSubclass for Queue { + const NAME: &'static str = "RsTsQueue"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing queue", + "Generic", + "Simple data queue", + "Sebastian Dröge <sebastian@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + sink_pad: PadSink::new(gst::Pad::new_from_template( + &klass.get_pad_template("sink").unwrap(), + Some("sink"), + )), + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + task: Task::default(), + dataqueue: StdMutex::new(None), + pending_queue: StdMutex::new(None), + last_res: StdMutex::new(Ok(gst::FlowSuccess::Ok)), + settings: StdMutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for Queue { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("max-size-buffers", ..) => { + settings.max_size_buffers = value.get_some().expect("type checked upstream"); + } + subclass::Property("max-size-bytes", ..) => { + settings.max_size_bytes = value.get_some().expect("type checked upstream"); + } + subclass::Property("max-size-time", ..) => { + settings.max_size_time = value.get_some().expect("type checked upstream"); + } + subclass::Property("context", ..) => { + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + settings.context_wait = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("max-size-buffers", ..) => Ok(settings.max_size_buffers.to_value()), + subclass::Property("max-size-bytes", ..) => Ok(settings.max_size_bytes.to_value()), + subclass::Property("max-size-time", ..) => Ok(settings.max_size_time.to_value()), + subclass::Property("context", ..) => Ok(settings.context.to_value()), + subclass::Property("context-wait", ..) => Ok(settings.context_wait.to_value()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.sink_pad.gst_pad()).unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + } +} + +impl ElementImpl for Queue { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToPaused { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register(Some(plugin), "ts-queue", gst::Rank::None, Queue::get_type()) +} diff --git a/generic/gst-plugin-threadshare/src/runtime/executor.rs b/generic/gst-plugin-threadshare/src/runtime/executor.rs new file mode 100644 index 000000000..9ee0ce617 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/runtime/executor.rs @@ -0,0 +1,853 @@ +// Copyright (C) 2018-2020 Sebastian Dröge <sebastian@centricular.com> +// Copyright (C) 2019-2020 François Laignel <fengalin@free.fr> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +//! The `Executor` for the `threadshare` GStreamer plugins framework. +//! +//! The [`threadshare`]'s `Executor` consists in a set of [`Context`]s. Each [`Context`] is +//! identified by a `name` and runs a loop in a dedicated `thread`. Users can use the [`Context`] +//! to spawn `Future`s. `Future`s are asynchronous processings which allow waiting for resources +//! in a non-blocking way. Examples of non-blocking operations are: +//! +//! * Waiting for an incoming packet on a Socket. +//! * Waiting for an asynchronous `Mutex` `lock` to succeed. +//! * Waiting for a time related `Future`. +//! +//! `Element` implementations should use [`PadSrc`] & [`PadSink`] which provides high-level features. +//! +//! [`threadshare`]: ../../index.html +//! [`Context`]: struct.Context.html +//! [`PadSrc`]: ../pad/struct.PadSrc.html +//! [`PadSink`]: ../pad/struct.PadSink.html + +use futures::channel::oneshot; +use futures::future::BoxFuture; +use futures::prelude::*; + +use gst; +use gst::{gst_debug, gst_log, gst_trace, gst_warning}; + +use lazy_static::lazy_static; + +use std::cell::RefCell; +use std::collections::{HashMap, VecDeque}; +use std::fmt; +use std::io; +use std::mem; +use std::pin::Pin; +use std::sync::mpsc as sync_mpsc; +use std::sync::{Arc, Mutex, Weak}; +use std::task::Poll; +use std::thread; +use std::time::Duration; + +use super::RUNTIME_CAT; + +// We are bound to using `sync` for the `runtime` `Mutex`es. Attempts to use `async` `Mutex`es +// lead to the following issues: +// +// * `CONTEXTS`: can't `spawn` a `Future` when called from a `Context` thread via `ffi`. +// * `timers`: can't automatically `remove` the timer from `BinaryHeap` because `async drop` +// is not available. +// * `task_queues`: can't `add` a pending task when called from a `Context` thread via `ffi`. +// +// Also, we want to be able to `acquire` a `Context` outside of an `async` context. +// These `Mutex`es must be `lock`ed for a short period. +lazy_static! { + static ref CONTEXTS: Mutex<HashMap<String, Weak<ContextInner>>> = Mutex::new(HashMap::new()); +} + +thread_local!(static CURRENT_THREAD_CONTEXT: RefCell<Option<ContextWeak>> = RefCell::new(None)); + +tokio::task_local! { + static CURRENT_TASK_ID: TaskId; +} + +/// Blocks on `future` in one way or another if possible. +/// +/// IO & time related `Future`s must be handled within their own [`Context`]. +/// Wait for the result using a [`JoinHandle`] or a `channel`. +/// +/// If there's currently an active `Context` with a task, then the future is only queued up as a +/// pending sub task for that task. +/// +/// Otherwise the current thread is blocking and the passed in future is executed. +/// +/// Note that you must not pass any futures here that wait for the currently active task in one way +/// or another as this would deadlock! +pub fn block_on_or_add_sub_task<Fut: Future + Send + 'static>(future: Fut) -> Option<Fut::Output> { + if let Some((cur_context, cur_task_id)) = Context::current_task() { + gst_debug!( + RUNTIME_CAT, + "Adding subtask to task {:?} on context {}", + cur_task_id, + cur_context.name() + ); + let _ = Context::add_sub_task(async move { + future.await; + Ok(()) + }); + return None; + } + + // Not running in a Context thread so we can block + Some(block_on(future)) +} + +/// Blocks on `future`. +/// +/// IO & time related `Future`s must be handled within their own [`Context`]. +/// Wait for the result using a [`JoinHandle`] or a `channel`. +/// +/// The current thread is blocking and the passed in future is executed. +/// +/// # Panics +/// +/// This function panics if called within a [`Context`] thread. +pub fn block_on<Fut: Future>(future: Fut) -> Fut::Output { + assert!(!Context::is_context_thread()); + + // Not running in a Context thread so we can block + gst_debug!(RUNTIME_CAT, "Blocking on new dummy context"); + + let context = Context(Arc::new(ContextInner { + real: None, + task_queues: Mutex::new((0, HashMap::new())), + })); + + CURRENT_THREAD_CONTEXT.with(move |cur_ctx| { + *cur_ctx.borrow_mut() = Some(context.downgrade()); + + let res = futures::executor::block_on(async move { + CURRENT_TASK_ID + .scope(TaskId(0), async move { + let task_id = CURRENT_TASK_ID.try_with(|task_id| *task_id).ok(); + assert_eq!(task_id, Some(TaskId(0))); + + let res = future.await; + + while Context::current_has_sub_tasks() { + if Context::drain_sub_tasks().await.is_err() { + break; + } + } + + res + }) + .await + }); + + *cur_ctx.borrow_mut() = None; + + res + }) +} + +/// Yields execution back to the runtime +#[inline] +pub async fn yield_now() { + tokio::task::yield_now().await; +} + +struct ContextThread { + name: String, +} + +impl ContextThread { + fn start(name: &str, wait: u32) -> Context { + let context_thread = ContextThread { name: name.into() }; + let (context_sender, context_receiver) = sync_mpsc::channel(); + let join = thread::spawn(move || { + context_thread.spawn(wait, context_sender); + }); + + let context = context_receiver.recv().expect("Context thread init failed"); + *context + .0 + .real + .as_ref() + .unwrap() + .shutdown + .join + .lock() + .unwrap() = Some(join); + + context + } + + fn spawn(&self, wait: u32, context_sender: sync_mpsc::Sender<Context>) { + gst_debug!(RUNTIME_CAT, "Started context thread '{}'", self.name); + + let mut runtime = tokio::runtime::Builder::new() + .basic_scheduler() + .thread_name(self.name.clone()) + .enable_all() + .max_throttling(Duration::from_millis(wait as u64)) + .build() + .expect("Couldn't build the runtime"); + + let (shutdown_sender, shutdown_receiver) = oneshot::channel(); + + let shutdown = ContextShutdown { + name: self.name.clone(), + shutdown: Some(shutdown_sender), + join: Mutex::new(None), + }; + + let context = Context(Arc::new(ContextInner { + real: Some(ContextRealInner { + name: self.name.clone(), + handle: Mutex::new(runtime.handle().clone()), + shutdown, + }), + task_queues: Mutex::new((0, HashMap::new())), + })); + + CURRENT_THREAD_CONTEXT.with(|cur_ctx| { + *cur_ctx.borrow_mut() = Some(context.downgrade()); + }); + + context_sender.send(context).unwrap(); + + let _ = runtime.block_on(shutdown_receiver); + } +} + +impl Drop for ContextThread { + fn drop(&mut self) { + gst_debug!(RUNTIME_CAT, "Terminated: context thread '{}'", self.name); + } +} + +#[derive(Debug)] +struct ContextShutdown { + name: String, + shutdown: Option<oneshot::Sender<()>>, + join: Mutex<Option<thread::JoinHandle<()>>>, +} + +impl Drop for ContextShutdown { + fn drop(&mut self) { + gst_debug!( + RUNTIME_CAT, + "Shutting down context thread thread '{}'", + self.name + ); + self.shutdown.take().unwrap(); + + gst_trace!( + RUNTIME_CAT, + "Waiting for context thread '{}' to shutdown", + self.name + ); + let join_handle = self.join.lock().unwrap().take().unwrap(); + let _ = join_handle.join(); + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] +pub struct TaskId(u64); + +pub type SubTaskOutput = Result<(), gst::FlowError>; +pub struct SubTaskQueue(VecDeque<BoxFuture<'static, SubTaskOutput>>); + +impl fmt::Debug for SubTaskQueue { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_tuple("SubTaskQueue").finish() + } +} + +pub struct JoinError(tokio::task::JoinError); + +impl fmt::Display for JoinError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, fmt) + } +} + +impl fmt::Debug for JoinError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.0, fmt) + } +} + +impl std::error::Error for JoinError {} + +impl From<tokio::task::JoinError> for JoinError { + fn from(src: tokio::task::JoinError) -> Self { + JoinError(src) + } +} + +/// Wrapper for the underlying runtime JoinHandle implementation. +pub struct JoinHandle<T> { + join_handle: tokio::task::JoinHandle<T>, + context: ContextWeak, + task_id: TaskId, +} + +unsafe impl<T: Send> Send for JoinHandle<T> {} +unsafe impl<T: Send> Sync for JoinHandle<T> {} + +impl<T> JoinHandle<T> { + pub fn is_current(&self) -> bool { + if let Some((context, task_id)) = Context::current_task() { + let self_context = self.context.upgrade(); + self_context.map(|c| c == context).unwrap_or(false) && task_id == self.task_id + } else { + false + } + } + + pub fn context(&self) -> Option<Context> { + self.context.upgrade() + } + + pub fn task_id(&self) -> TaskId { + self.task_id + } +} + +impl<T> Unpin for JoinHandle<T> {} + +impl<T> Future for JoinHandle<T> { + type Output = Result<T, JoinError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> { + if self.as_ref().is_current() { + panic!("Trying to join task {:?} from itself", self.as_ref()); + } + + self.as_mut() + .join_handle + .poll_unpin(cx) + .map_err(JoinError::from) + } +} + +impl<T> fmt::Debug for JoinHandle<T> { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let context_name = self.context.upgrade().map(|c| String::from(c.name())); + + fmt.debug_struct("JoinHandle") + .field("context", &context_name) + .field("task_id", &self.task_id) + .finish() + } +} + +#[derive(Debug)] +struct ContextRealInner { + name: String, + handle: Mutex<tokio::runtime::Handle>, + // Only used for dropping + shutdown: ContextShutdown, +} + +#[derive(Debug)] +struct ContextInner { + // Otherwise a dummy context + real: Option<ContextRealInner>, + task_queues: Mutex<(u64, HashMap<u64, SubTaskQueue>)>, +} + +impl Drop for ContextInner { + fn drop(&mut self) { + if let Some(ref real) = self.real { + let mut contexts = CONTEXTS.lock().unwrap(); + gst_debug!(RUNTIME_CAT, "Finalizing context '{}'", real.name); + contexts.remove(&real.name); + } + } +} + +#[derive(Clone, Debug)] +pub struct ContextWeak(Weak<ContextInner>); + +impl ContextWeak { + pub fn upgrade(&self) -> Option<Context> { + self.0.upgrade().map(Context) + } +} + +/// A `threadshare` `runtime` `Context`. +/// +/// The `Context` provides low-level asynchronous processing features to +/// multiplex task execution on a single thread. +/// +/// `Element` implementations should use [`PadSrc`] and [`PadSink`] which +/// provide high-level features. +/// +/// See the [module-level documentation](index.html) for more. +/// +/// [`PadSrc`]: ../pad/struct.PadSrc.html +/// [`PadSink`]: ../pad/struct.PadSink.html +#[derive(Clone, Debug)] +pub struct Context(Arc<ContextInner>); + +impl PartialEq for Context { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for Context {} + +impl Context { + pub fn acquire(context_name: &str, wait: u32) -> Result<Self, io::Error> { + assert_ne!(context_name, "DUMMY"); + + let mut contexts = CONTEXTS.lock().unwrap(); + + if let Some(inner_weak) = contexts.get(context_name) { + if let Some(inner_strong) = inner_weak.upgrade() { + gst_debug!( + RUNTIME_CAT, + "Joining Context '{}'", + inner_strong.real.as_ref().unwrap().name + ); + return Ok(Context(inner_strong)); + } + } + + let context = ContextThread::start(context_name, wait); + contexts.insert(context_name.into(), Arc::downgrade(&context.0)); + + gst_debug!( + RUNTIME_CAT, + "New Context '{}'", + context.0.real.as_ref().unwrap().name + ); + Ok(context) + } + + pub fn downgrade(&self) -> ContextWeak { + ContextWeak(Arc::downgrade(&self.0)) + } + + pub fn name(&self) -> &str { + match self.0.real { + Some(ref real) => real.name.as_str(), + None => "DUMMY", + } + } + + /// Returns `true` if a `Context` is running on current thread. + pub fn is_context_thread() -> bool { + CURRENT_THREAD_CONTEXT.with(|cur_ctx| cur_ctx.borrow().is_some()) + } + + /// Returns the `Context` running on current thread, if any. + pub fn current() -> Option<Context> { + CURRENT_THREAD_CONTEXT.with(|cur_ctx| { + cur_ctx + .borrow() + .as_ref() + .and_then(|ctx_weak| ctx_weak.upgrade()) + }) + } + + /// Returns the `TaskId` running on current thread, if any. + pub fn current_task() -> Option<(Context, TaskId)> { + CURRENT_THREAD_CONTEXT.with(|cur_ctx| { + cur_ctx + .borrow() + .as_ref() + .and_then(|ctx_weak| ctx_weak.upgrade()) + .and_then(|ctx| { + let task_id = CURRENT_TASK_ID.try_with(|task_id| *task_id).ok(); + + task_id.map(move |task_id| (ctx, task_id)) + }) + }) + } + + pub fn enter<F, R>(&self, f: F) -> R + where + F: FnOnce() -> R, + { + let real = match self.0.real { + Some(ref real) => real, + None => panic!("Can't enter on dummy context"), + }; + + real.handle.lock().unwrap().enter(f) + } + + pub fn spawn<Fut>(&self, future: Fut) -> JoinHandle<Fut::Output> + where + Fut: Future + Send + 'static, + Fut::Output: Send + 'static, + { + let real = match self.0.real { + Some(ref real) => real, + None => panic!("Can't spawn new tasks on dummy context"), + }; + + let mut task_queues = self.0.task_queues.lock().unwrap(); + let id = task_queues.0; + task_queues.0 += 1; + task_queues.1.insert(id, SubTaskQueue(VecDeque::new())); + + let id = TaskId(id); + gst_trace!( + RUNTIME_CAT, + "Spawning new task {:?} on context {}", + id, + real.name + ); + + let join_handle = real.handle.lock().unwrap().spawn(async move { + let ctx = Context::current().unwrap(); + let real = ctx.0.real.as_ref().unwrap(); + + gst_trace!( + RUNTIME_CAT, + "Running task {:?} on context {}", + id, + real.name + ); + let res = CURRENT_TASK_ID.scope(id, future).await; + + // Remove task from the list + { + let mut task_queues = ctx.0.task_queues.lock().unwrap(); + if let Some(task_queue) = task_queues.1.remove(&id.0) { + let l = task_queue.0.len(); + if l > 0 { + gst_warning!( + RUNTIME_CAT, + "Task {:?} on context {} has {} pending sub tasks", + id, + real.name, + l + ); + } + } + } + + gst_trace!(RUNTIME_CAT, "Task {:?} on context {} done", id, real.name); + + res + }); + + JoinHandle { + join_handle, + context: self.downgrade(), + task_id: id, + } + } + + pub fn current_has_sub_tasks() -> bool { + let (ctx, task_id) = match Context::current_task() { + Some(task) => task, + None => { + gst_trace!(RUNTIME_CAT, "No current task"); + return false; + } + }; + + let task_queues = ctx.0.task_queues.lock().unwrap(); + task_queues + .1 + .get(&task_id.0) + .map(|t| !t.0.is_empty()) + .unwrap_or(false) + } + + pub fn add_sub_task<T>(sub_task: T) -> Result<(), T> + where + T: Future<Output = SubTaskOutput> + Send + 'static, + { + let (ctx, task_id) = match Context::current_task() { + Some(task) => task, + None => { + gst_trace!(RUNTIME_CAT, "No current task"); + return Err(sub_task); + } + }; + + let mut task_queues = ctx.0.task_queues.lock().unwrap(); + match task_queues.1.get_mut(&task_id.0) { + Some(task_queue) => { + if let Some(ref real) = ctx.0.real { + gst_trace!( + RUNTIME_CAT, + "Adding subtask to {:?} on context {}", + task_id, + real.name + ); + } else { + gst_trace!( + RUNTIME_CAT, + "Adding subtask to {:?} on dummy context", + task_id, + ); + } + task_queue.0.push_back(sub_task.boxed()); + Ok(()) + } + None => { + gst_trace!(RUNTIME_CAT, "Task was removed in the meantime"); + Err(sub_task) + } + } + } + + pub fn drain_sub_tasks() -> impl Future<Output = SubTaskOutput> + Send + 'static { + async { + let (ctx, task_id) = match Context::current_task() { + Some(task) => task, + None => return Ok(()), + }; + + ctx.drain_sub_tasks_internal(task_id).await + } + } + + fn drain_sub_tasks_internal( + &self, + id: TaskId, + ) -> impl Future<Output = SubTaskOutput> + Send + 'static { + let mut task_queue = { + let mut task_queues = self.0.task_queues.lock().unwrap(); + if let Some(task_queue) = task_queues.1.get_mut(&id.0) { + mem::replace(task_queue, SubTaskQueue(VecDeque::new())) + } else { + SubTaskQueue(VecDeque::new()) + } + }; + + let name = self + .0 + .real + .as_ref() + .map(|r| r.name.clone()) + .unwrap_or_else(|| String::from("DUMMY")); + async move { + if !task_queue.0.is_empty() { + gst_log!( + RUNTIME_CAT, + "Scheduling draining {} sub tasks from {:?} on '{}'", + task_queue.0.len(), + id, + &name, + ); + + for task in task_queue.0.drain(..) { + task.await?; + } + } + + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use futures; + use futures::channel::mpsc; + use futures::lock::Mutex; + use futures::prelude::*; + + use gst; + + use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; + use std::sync::Arc; + use std::time::{Duration, Instant}; + + use super::Context; + + type Item = i32; + + const SLEEP_DURATION_MS: u32 = 2; + const SLEEP_DURATION: Duration = Duration::from_millis(SLEEP_DURATION_MS as u64); + const DELAY: Duration = Duration::from_millis(SLEEP_DURATION_MS as u64 * 10); + + #[tokio::test] + async fn drain_sub_tasks() { + // Setup + gst::init().unwrap(); + + let context = Context::acquire("drain_sub_tasks", SLEEP_DURATION_MS).unwrap(); + + let join_handle = context.spawn(async move { + let (sender, mut receiver) = mpsc::channel(1); + let sender: Arc<Mutex<mpsc::Sender<Item>>> = Arc::new(Mutex::new(sender)); + + let add_sub_task = move |item| { + let sender = sender.clone(); + Context::add_sub_task(async move { + sender + .lock() + .await + .send(item) + .await + .map_err(|_| gst::FlowError::Error) + }) + }; + + // Tests + + // Drain empty queue + let drain_fut = Context::drain_sub_tasks(); + drain_fut.await.unwrap(); + + // Add a subtask + add_sub_task(0).map_err(drop).unwrap(); + + // Check that it was not executed yet + receiver.try_next().unwrap_err(); + + // Drain it now and check that it was executed + let drain_fut = Context::drain_sub_tasks(); + drain_fut.await.unwrap(); + assert_eq!(receiver.try_next().unwrap(), Some(0)); + + // Add another task and check that it's not executed yet + add_sub_task(1).map_err(drop).unwrap(); + receiver.try_next().unwrap_err(); + + // Return the receiver + receiver + }); + + let mut receiver = join_handle.await.unwrap(); + + // The last sub task should be simply dropped at this point + assert_eq!(receiver.try_next().unwrap(), None); + } + + #[tokio::test] + async fn block_on_within_tokio() { + let context = Context::acquire("block_on_within_tokio", SLEEP_DURATION_MS).unwrap(); + + let bytes_sent = crate::runtime::executor::block_on(context.spawn(async { + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5000); + let socket = UdpSocket::bind(saddr).unwrap(); + let mut socket = tokio::net::UdpSocket::from_std(socket).unwrap(); + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4000); + socket.send_to(&[0; 10], saddr).await.unwrap() + })) + .unwrap(); + assert_eq!(bytes_sent, 10); + + let elapsed = crate::runtime::executor::block_on(context.spawn(async { + let now = Instant::now(); + crate::runtime::time::delay_for(DELAY).await; + now.elapsed() + })) + .unwrap(); + // Due to throttling, `Delay` may be fired earlier + assert!(elapsed + SLEEP_DURATION / 2 >= DELAY); + } + + #[test] + fn block_on_from_sync() { + let context = Context::acquire("block_on_from_sync", SLEEP_DURATION_MS).unwrap(); + + let bytes_sent = crate::runtime::executor::block_on(context.spawn(async { + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5001); + let socket = UdpSocket::bind(saddr).unwrap(); + let mut socket = tokio::net::UdpSocket::from_std(socket).unwrap(); + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4000); + socket.send_to(&[0; 10], saddr).await.unwrap() + })) + .unwrap(); + assert_eq!(bytes_sent, 10); + + let elapsed = crate::runtime::executor::block_on(context.spawn(async { + let now = Instant::now(); + crate::runtime::time::delay_for(DELAY).await; + now.elapsed() + })) + .unwrap(); + // Due to throttling, `Delay` may be fired earlier + assert!(elapsed + SLEEP_DURATION / 2 >= DELAY); + } + + #[test] + fn block_on_from_context() { + gst::init().unwrap(); + + let context = Context::acquire("block_on_from_context", SLEEP_DURATION_MS).unwrap(); + let join_handle = context.spawn(async { + crate::runtime::executor::block_on(async { + crate::runtime::time::delay_for(DELAY).await; + }); + }); + // Panic: attempt to `runtime::executor::block_on` within a `Context` thread + futures::executor::block_on(join_handle).unwrap_err(); + } + + #[tokio::test] + async fn enter_context_from_tokio() { + gst::init().unwrap(); + + let context = Context::acquire("enter_context_from_tokio", SLEEP_DURATION_MS).unwrap(); + let mut socket = context + .enter(|| { + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5002); + let socket = UdpSocket::bind(saddr).unwrap(); + tokio::net::UdpSocket::from_std(socket) + }) + .unwrap(); + + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4000); + let bytes_sent = socket.send_to(&[0; 10], saddr).await.unwrap(); + assert_eq!(bytes_sent, 10); + + let elapsed = context.enter(|| { + futures::executor::block_on(async { + let now = Instant::now(); + crate::runtime::time::delay_for(DELAY).await; + now.elapsed() + }) + }); + // Due to throttling, `Delay` may be fired earlier + assert!(elapsed + SLEEP_DURATION / 2 >= DELAY); + } + + #[test] + fn enter_context_from_sync() { + gst::init().unwrap(); + + let context = Context::acquire("enter_context_from_sync", SLEEP_DURATION_MS).unwrap(); + let mut socket = context + .enter(|| { + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 5003); + let socket = UdpSocket::bind(saddr).unwrap(); + tokio::net::UdpSocket::from_std(socket) + }) + .unwrap(); + + let saddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4000); + let bytes_sent = futures::executor::block_on(socket.send_to(&[0; 10], saddr)).unwrap(); + assert_eq!(bytes_sent, 10); + + let elapsed = context.enter(|| { + futures::executor::block_on(async { + let now = Instant::now(); + crate::runtime::time::delay_for(DELAY).await; + now.elapsed() + }) + }); + // Due to throttling, `Delay` may be fired earlier + assert!(elapsed + SLEEP_DURATION / 2 >= DELAY); + } +} diff --git a/generic/gst-plugin-threadshare/src/runtime/mod.rs b/generic/gst-plugin-threadshare/src/runtime/mod.rs new file mode 100644 index 000000000..39923a4e1 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/runtime/mod.rs @@ -0,0 +1,70 @@ +// Copyright (C) 2019 François Laignel <fengalin@free.fr> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +//! A `runtime` for the `threadshare` GStreamer plugins framework. +//! +//! Many `GStreamer` `Element`s internally spawn OS `thread`s. For most applications, this is not an +//! issue. However, in applications which process many `Stream`s in parallel, the high number of +//! `threads` leads to reduced efficiency due to: +//! +//! * context switches, +//! * scheduler overhead, +//! * most of the threads waiting for some resources to be available. +//! +//! The `threadshare` `runtime` is a framework to build `Element`s for such applications. It +//! uses light-weight threading to allow multiple `Element`s share a reduced number of OS `thread`s. +//! +//! See this [talk] ([slides]) for a presentation of the motivations and principles, +//! and this [blog post]. +//! +//! Current implementation uses the crate [`tokio`]. +//! +//! Most `Element`s implementations should use the high-level features provided by [`PadSrc`] & +//! [`PadSink`]. +//! +//! [talk]: https://gstconf.ubicast.tv/videos/when-adding-more-threads-adds-more-problems-thread-sharing-between-elements-in-gstreamer/ +//! [slides]: https://gstreamer.freedesktop.org/data/events/gstreamer-conference/2018/Sebastian%20Dr%C3%B6ge%20-%20When%20adding%20more%20threads%20adds%20more%20problems:%20Thread-sharing%20between%20elements%20in%20GStreamer.pdf +//! [blog post]: https://coaxion.net/blog/2018/04/improving-gstreamer-performance-on-a-high-number-of-network-streams-by-sharing-threads-between-elements-with-rusts-tokio-crate +//! [`tokio`]: https://crates.io/crates/tokio +//! [`PadSrc`]: pad/struct.PadSrc.html +//! [`PadSink`]: pad/struct.PadSink.html + +pub mod executor; +pub use executor::{Context, JoinHandle, SubTaskOutput}; + +pub mod pad; +pub use pad::{PadSink, PadSinkRef, PadSinkWeak, PadSrc, PadSrcRef, PadSrcWeak}; + +pub mod task; +pub use task::{Task, TaskState}; + +pub mod prelude { + pub use super::pad::{PadSinkHandler, PadSrcHandler}; +} + +pub mod time; + +use gst; +use lazy_static::lazy_static; + +lazy_static! { + static ref RUNTIME_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-runtime", + gst::DebugColorFlags::empty(), + Some("Thread-sharing Runtime"), + ); +} diff --git a/generic/gst-plugin-threadshare/src/runtime/pad.rs b/generic/gst-plugin-threadshare/src/runtime/pad.rs new file mode 100644 index 000000000..fde5ed3bf --- /dev/null +++ b/generic/gst-plugin-threadshare/src/runtime/pad.rs @@ -0,0 +1,1130 @@ +// Copyright (C) 2019-2020 François Laignel <fengalin@free.fr> +// Copyright (C) 2020 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +//! An implementation of `Pad`s to run asynchronous processings. +//! +//! [`PadSink`] & [`PadSrc`] provide an asynchronous API to ease the development of `Element`s in +//! the `threadshare` GStreamer plugins framework. +//! +//! The diagram below shows how the [`PadSrc`] & [`PadSink`] and the related `struct`s integrate in +//! `ts` `Element`s. +//! +//! Note: [`PadSrc`] & [`PadSink`] only support `gst::PadMode::Push` at the moment. +//! +//! ```text +//! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +//! Element A ┃ ┃ Element B +//! ┃ ┃ +//! ╭─────────────────╮ ┃ ┃ ╭──────────────────╮ +//! │ PadSrc │ ┃ ┃ │ PadSink │ +//! │ Handler │ ┃ ┃ │ Handler │ +//! │─────────────────│ ┃ ┃ │──────────────────│ +//! │ - src_activate* │ ╭──┸──╮ ╭──┸──╮ │ - sink_activate* │ +//! │ - src_event* │<────│ │<╌╌╌│ │───>│ - sink_chain* │ +//! │ - src_query │<────│ gst │ │ gst │───>│ - sink_event* │ +//! │─────────────────│ │ │ │ │───>│ - sink_query │ +//! │ - task fn │ │ Pad │ │ Pad │ ╰──────────────────╯ +//! ╰─────────────────╯ ╭─>│ │╌╌╌>│ │─╮ │ +//! ╭───────╯ │ │ ╰──┰──╯ ╰──┰──╯ ╰───────╮ │ +//! ╭────────────╮ ╭────────╮ push* │ ┃ ┃ ╭─────────╮ +//! │ Pad Task ↺ │<──│ PadSrc │───────╯ ┃ ┃ │ PadSink │ +//! ╰────────────╯ ╰────────╯ ┃ ┃ ╰─────────╯ +//! ━━━━━━━━━━━━━━━━━━━━━━│━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━│━━━━━━━━━━━━ +//! ╰───────────────────╮ ╭─────────────────╯ +//! ╭──────────────╮ +//! │ PadContext │ +//! │╭────────────╮│ +//! ││ Context ↺ ││ +//! ╰╰────────────╯╯ +//! ``` +//! +//! Asynchronous operations for both [`PadSrc`] in `Element A` and [`PadSink`] in `Element B` run on +//! the same [`Context`], which can also be shared by other `Element`s or instances of the same +//! `Element`s in multiple `Pipeline`s. +//! +//! `Element A` & `Element B` can also be linked to non-threadshare `Element`s in which case, they +//! operate in a regular synchronous way. +//! +//! Note that only operations on the streaming thread (serialized events, buffers, serialized +//! queries) are handled from the `PadContext` and asynchronously, everything else operates +//! blocking. +//! +//! [`PadSink`]: struct.PadSink.html +//! [`PadSrc`]: struct.PadSrc.html +//! [`Context`]: ../executor/struct.Context.html + +use futures::future; +use futures::future::BoxFuture; +use futures::prelude::*; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_error, gst_fixme, gst_log, gst_loggable_error}; +use gst::{FlowError, FlowSuccess}; + +use std::marker::PhantomData; +use std::sync::{Arc, Weak}; + +use super::executor::{block_on_or_add_sub_task, Context}; +use super::RUNTIME_CAT; + +#[inline] +fn event_ret_to_event_full_res( + ret: bool, + event_type: gst::EventType, +) -> Result<FlowSuccess, FlowError> { + if ret { + Ok(FlowSuccess::Ok) + } else if event_type == gst::EventType::Caps { + Err(FlowError::NotNegotiated) + } else { + Err(FlowError::Error) + } +} + +#[inline] +fn event_to_event_full(ret: bool, event_type: gst::EventType) -> Result<FlowSuccess, FlowError> { + event_ret_to_event_full_res(ret, event_type) +} + +#[inline] +fn event_to_event_full_serialized( + ret: BoxFuture<'static, bool>, + event_type: gst::EventType, +) -> BoxFuture<'static, Result<FlowSuccess, FlowError>> { + ret.map(move |ret| event_ret_to_event_full_res(ret, event_type)) + .boxed() +} + +/// A trait to define `handler`s for [`PadSrc`] callbacks. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`PadSrc`]: struct.PadSrc.html +/// [`pad` module]: index.html +pub trait PadSrcHandler: Clone + Send + Sync + 'static { + type ElementImpl: ElementImpl + ObjectSubclass; + + fn src_activate( + &self, + pad: &PadSrcRef, + _imp: &Self::ElementImpl, + _element: &gst::Element, + ) -> Result<(), gst::LoggableError> { + let gst_pad = pad.gst_pad(); + if gst_pad.is_active() { + gst_debug!( + RUNTIME_CAT, + obj: gst_pad, + "Already activated in {:?} mode ", + gst_pad.get_mode() + ); + return Ok(()); + } + + gst_pad + .activate_mode(gst::PadMode::Push, true) + .map_err(|err| { + gst_error!( + RUNTIME_CAT, + obj: gst_pad, + "Error in PadSrc activate: {:?}", + err + ); + gst_loggable_error!(RUNTIME_CAT, "Error in PadSrc activate: {:?}", err) + }) + } + + fn src_activatemode( + &self, + _pad: &PadSrcRef, + _imp: &Self::ElementImpl, + _element: &gst::Element, + _mode: gst::PadMode, + _active: bool, + ) -> Result<(), gst::LoggableError> { + Ok(()) + } + + fn src_event( + &self, + pad: &PadSrcRef, + _imp: &Self::ElementImpl, + element: &gst::Element, + event: gst::Event, + ) -> bool { + gst_log!(RUNTIME_CAT, obj: pad.gst_pad(), "Handling {:?}", event); + pad.gst_pad().event_default(Some(element), event) + } + + fn src_event_full( + &self, + pad: &PadSrcRef, + imp: &Self::ElementImpl, + element: &gst::Element, + event: gst::Event, + ) -> Result<FlowSuccess, FlowError> { + // default is to dispatch to `src_event` + // (as implemented in `gst_pad_send_event_unchecked`) + let event_type = event.get_type(); + event_to_event_full(self.src_event(pad, imp, element, event), event_type) + } + + fn src_query( + &self, + pad: &PadSrcRef, + _imp: &Self::ElementImpl, + element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + gst_log!(RUNTIME_CAT, obj: pad.gst_pad(), "Handling {:?}", query); + if query.is_serialized() { + // FIXME serialized queries should be handled with the dataflow + // but we can't return a `Future` because we couldn't honor QueryRef's lifetime + false + } else { + pad.gst_pad().query_default(Some(element), query) + } + } +} + +#[derive(Debug)] +struct PadSrcInner { + gst_pad: gst::Pad, +} + +impl PadSrcInner { + fn new(gst_pad: gst::Pad) -> Self { + if gst_pad.get_direction() != gst::PadDirection::Src { + panic!("Wrong pad direction for PadSrc"); + } + + PadSrcInner { gst_pad } + } +} + +/// A [`PadSrc`] which can be moved in [`handler`]s functions and `Future`s. +/// +/// Call [`upgrade`] to use the [`PadSrc`]. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`PadSrc`]: struct.PadSrc.html +/// [`handler`]: trait.PadSrcHandler.html +/// [`upgrade`]: struct.PadSrcWeak.html#method.upgrade +/// [`pad` module]: index.html +#[derive(Clone, Debug)] +pub struct PadSrcWeak(Weak<PadSrcInner>); + +impl PadSrcWeak { + pub fn upgrade(&self) -> Option<PadSrcRef<'_>> { + self.0.upgrade().map(PadSrcRef::new) + } +} + +/// A [`PadSrc`] to be used in `Handler`s functions and `Future`s. +/// +/// Call [`downgrade`] if you need to `clone` the [`PadSrc`]. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`PadSrc`]: struct.PadSrc.html +/// [`PadSrcWeak`]: struct.PadSrcWeak.html +/// [`downgrade`]: struct.PadSrcRef.html#method.downgrade +/// [`pad` module]: index.html +#[derive(Debug)] +pub struct PadSrcRef<'a> { + strong: PadSrcStrong, + phantom: PhantomData<&'a PadSrcStrong>, +} + +impl<'a> PadSrcRef<'a> { + fn new(inner_arc: Arc<PadSrcInner>) -> Self { + PadSrcRef { + strong: PadSrcStrong(inner_arc), + phantom: PhantomData, + } + } + + pub fn gst_pad(&self) -> &gst::Pad { + self.strong.gst_pad() + } + + ///// Spawns `future` using current [`PadContext`]. + ///// + ///// # Panics + ///// + ///// This function panics if the `PadSrc` is not prepared. + ///// + ///// [`PadContext`]: ../struct.PadContext.html + //pub fn spawn<Fut>(&self, future: Fut) -> JoinHandle<Fut::Output> + //where + // Fut: Future + Send + 'static, + // Fut::Output: Send + 'static, + //{ + // self.strong.spawn(future) + //} + + pub fn downgrade(&self) -> PadSrcWeak { + self.strong.downgrade() + } + + pub async fn push(&self, buffer: gst::Buffer) -> Result<FlowSuccess, FlowError> { + self.strong.push(buffer).await + } + + pub async fn push_list(&self, list: gst::BufferList) -> Result<FlowSuccess, FlowError> { + self.strong.push_list(list).await + } + + pub async fn push_event(&self, event: gst::Event) -> bool { + self.strong.push_event(event).await + } + + fn activate_mode_hook( + &self, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError> { + // Important: don't panic here as the hook is used without `catch_panic_pad_function` + // in the default `activatemode` handling + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "ActivateMode {:?}, {}", mode, active); + + if mode == gst::PadMode::Pull { + gst_error!(RUNTIME_CAT, obj: self.gst_pad(), "Pull mode not supported by PadSrc"); + return Err(gst_loggable_error!( + RUNTIME_CAT, + "Pull mode not supported by PadSrc" + )); + } + + Ok(()) + } +} + +#[derive(Debug)] +struct PadSrcStrong(Arc<PadSrcInner>); + +impl PadSrcStrong { + fn new(gst_pad: gst::Pad) -> Self { + PadSrcStrong(Arc::new(PadSrcInner::new(gst_pad))) + } + + #[inline] + fn gst_pad(&self) -> &gst::Pad { + &self.0.gst_pad + } + + //#[inline] + //fn spawn<Fut>(&self, future: Fut) -> JoinHandle<Fut::Output> + //where + // Fut: Future + Send + 'static, + // Fut::Output: Send + 'static, + //{ + // let pad_ctx = self.pad_context_priv(); + // pad_ctx + // .as_ref() + // .expect("PadContext not initialized") + // .spawn(future) + //} + + #[inline] + fn downgrade(&self) -> PadSrcWeak { + PadSrcWeak(Arc::downgrade(&self.0)) + } + + #[inline] + async fn push(&self, buffer: gst::Buffer) -> Result<FlowSuccess, FlowError> { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Pushing {:?}", buffer); + + let success = self.gst_pad().push(buffer).map_err(|err| { + gst_error!(RUNTIME_CAT, + obj: self.gst_pad(), + "Failed to push Buffer to PadSrc: {:?}", + err, + ); + err + })?; + + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Processing any pending sub tasks"); + while Context::current_has_sub_tasks() { + Context::drain_sub_tasks().await?; + } + + Ok(success) + } + + #[inline] + async fn push_list(&self, list: gst::BufferList) -> Result<FlowSuccess, FlowError> { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Pushing {:?}", list); + + let success = self.gst_pad().push_list(list).map_err(|err| { + gst_error!( + RUNTIME_CAT, + obj: self.gst_pad(), + "Failed to push BufferList to PadSrc: {:?}", + err, + ); + err + })?; + + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Processing any pending sub tasks"); + while Context::current_has_sub_tasks() { + Context::drain_sub_tasks().await?; + } + + Ok(success) + } + + #[inline] + async fn push_event(&self, event: gst::Event) -> bool { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Pushing {:?}", event); + + let was_handled = self.gst_pad().push_event(event); + + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Processing any pending sub tasks"); + while Context::current_has_sub_tasks() { + if Context::drain_sub_tasks().await.is_err() { + return false; + } + } + + was_handled + } +} + +/// The `PadSrc` which `Element`s must own. +/// +/// Call [`downgrade`] if you need to `clone` the `PadSrc`. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`downgrade`]: struct.PadSrc.html#method.downgrade +/// [`pad` module]: index.html +#[derive(Debug)] +pub struct PadSrc(PadSrcStrong); + +impl PadSrc { + pub fn new(gst_pad: gst::Pad) -> Self { + let this = PadSrc(PadSrcStrong::new(gst_pad)); + this.set_default_activatemode_function(); + + this + } + + pub fn as_ref(&self) -> PadSrcRef<'_> { + PadSrcRef::new(Arc::clone(&(self.0).0)) + } + + pub fn gst_pad(&self) -> &gst::Pad { + self.0.gst_pad() + } + + pub fn downgrade(&self) -> PadSrcWeak { + self.0.downgrade() + } + + pub fn check_reconfigure(&self) -> bool { + self.gst_pad().check_reconfigure() + } + + fn set_default_activatemode_function(&self) { + let this_weak = self.downgrade(); + self.gst_pad() + .set_activatemode_function(move |gst_pad, _parent, mode, active| { + // Important: don't panic here as we operate without `catch_panic_pad_function` + // because we may not know which element the PadSrc is associated to yet + this_weak + .upgrade() + .ok_or_else(|| { + gst_error!(RUNTIME_CAT, obj: gst_pad, "PadSrc no longer exists"); + gst_loggable_error!(RUNTIME_CAT, "PadSrc no longer exists") + })? + .activate_mode_hook(mode, active) + }); + } + + ///// Spawns `future` using current [`PadContext`]. + ///// + ///// # Panics + ///// + ///// This function panics if the `PadSrc` is not prepared. + ///// + ///// [`PadContext`]: ../struct.PadContext.html + //pub fn spawn<Fut>(&self, future: Fut) -> JoinHandle<Fut::Output> + //where + // Fut: Future + Send + 'static, + // Fut::Output: Send + 'static, + //{ + // self.0.spawn(future) + //} + + fn init_pad_functions<H: PadSrcHandler>(&self, handler: &H) { + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_activate_function(move |gst_pad, parent| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || { + gst_error!(RUNTIME_CAT, obj: gst_pad, "Panic in PadSrc activate"); + Err(gst_loggable_error!(RUNTIME_CAT, "Panic in PadSrc activate")) + }, + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSrc no longer exists"); + handler.src_activate(&this_ref, imp, element) + }, + ) + }); + + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_activatemode_function(move |gst_pad, parent, mode, active| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || { + gst_error!(RUNTIME_CAT, obj: gst_pad, "Panic in PadSrc activatemode"); + Err(gst_loggable_error!( + RUNTIME_CAT, + "Panic in PadSrc activatemode" + )) + }, + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSrc no longer exists"); + this_ref.activate_mode_hook(mode, active)?; + handler.src_activatemode(&this_ref, imp, element, mode, active) + }, + ) + }); + + // No need to `set_event_function` since `set_event_full_function` + // overrides it and dispatches to `src_event` when necessary + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_event_full_function(move |_gst_pad, parent, event| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || Err(FlowError::Error), + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSrc no longer exists"); + handler.src_event_full(&this_ref, imp, &element, event) + }, + ) + }); + + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_query_function(move |_gst_pad, parent, query| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || false, + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSrc no longer exists"); + if !query.is_serialized() { + handler.src_query(&this_ref, imp, &element, query) + } else { + gst_fixme!(RUNTIME_CAT, obj: this_ref.gst_pad(), "Serialized Query not supported"); + false + } + }, + ) + }); + } + + pub fn prepare<H: PadSrcHandler>(&self, handler: &H) { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Preparing"); + + self.init_pad_functions(handler); + } + + /// Releases the resources held by this `PadSrc`. + pub fn unprepare(&self) { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Unpreparing"); + + self.gst_pad() + .set_activate_function(move |_gst_pad, _parent| { + Err(gst_loggable_error!(RUNTIME_CAT, "PadSrc unprepared")) + }); + self.set_default_activatemode_function(); + self.gst_pad() + .set_event_function(move |_gst_pad, _parent, _event| false); + self.gst_pad() + .set_event_full_function(move |_gst_pad, _parent, _event| Err(FlowError::Flushing)); + self.gst_pad() + .set_query_function(move |_gst_pad, _parent, _query| false); + } + + pub async fn push(&self, buffer: gst::Buffer) -> Result<FlowSuccess, FlowError> { + self.0.push(buffer).await + } + + pub async fn push_list(&self, list: gst::BufferList) -> Result<FlowSuccess, FlowError> { + self.0.push_list(list).await + } + + pub async fn push_event(&self, event: gst::Event) -> bool { + self.0.push_event(event).await + } +} + +/// A trait to define `handler`s for [`PadSink`] callbacks. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`PadSink`]: struct.PadSink.html +/// [`pad` module]: index.html +pub trait PadSinkHandler: Clone + Send + Sync + 'static { + type ElementImpl: ElementImpl + ObjectSubclass; + + fn sink_activate( + &self, + pad: &PadSinkRef, + _imp: &Self::ElementImpl, + _element: &gst::Element, + ) -> Result<(), gst::LoggableError> { + let gst_pad = pad.gst_pad(); + if gst_pad.is_active() { + gst_debug!( + RUNTIME_CAT, + obj: gst_pad, + "Already activated in {:?} mode ", + gst_pad.get_mode() + ); + return Ok(()); + } + + gst_pad + .activate_mode(gst::PadMode::Push, true) + .map_err(|err| { + gst_error!( + RUNTIME_CAT, + obj: gst_pad, + "Error in PadSink activate: {:?}", + err + ); + gst_loggable_error!(RUNTIME_CAT, "Error in PadSink activate: {:?}", err) + }) + } + + fn sink_activatemode( + &self, + _pad: &PadSinkRef, + _imp: &Self::ElementImpl, + _element: &gst::Element, + _mode: gst::PadMode, + _active: bool, + ) -> Result<(), gst::LoggableError> { + Ok(()) + } + + fn sink_chain( + &self, + _pad: &PadSinkRef, + _imp: &Self::ElementImpl, + _element: &gst::Element, + _buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<FlowSuccess, FlowError>> { + future::err(FlowError::NotSupported).boxed() + } + + fn sink_chain_list( + &self, + _pad: &PadSinkRef, + _imp: &Self::ElementImpl, + _element: &gst::Element, + _buffer_list: gst::BufferList, + ) -> BoxFuture<'static, Result<FlowSuccess, FlowError>> { + future::err(FlowError::NotSupported).boxed() + } + + fn sink_event( + &self, + pad: &PadSinkRef, + _imp: &Self::ElementImpl, + element: &gst::Element, + event: gst::Event, + ) -> bool { + assert!(!event.is_serialized()); + gst_log!(RUNTIME_CAT, obj: pad.gst_pad(), "Handling {:?}", event); + pad.gst_pad().event_default(Some(element), event) + } + + fn sink_event_serialized( + &self, + pad: &PadSinkRef, + _imp: &Self::ElementImpl, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + assert!(event.is_serialized()); + let pad_weak = pad.downgrade(); + let element = element.clone(); + + async move { + let pad = pad_weak.upgrade().expect("PadSink no longer exists"); + gst_log!(RUNTIME_CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + pad.gst_pad().event_default(Some(&element), event) + } + .boxed() + } + + fn sink_event_full( + &self, + pad: &PadSinkRef, + imp: &Self::ElementImpl, + element: &gst::Element, + event: gst::Event, + ) -> Result<FlowSuccess, FlowError> { + assert!(!event.is_serialized()); + // default is to dispatch to `sink_event` + // (as implemented in `gst_pad_send_event_unchecked`) + let event_type = event.get_type(); + event_to_event_full(self.sink_event(pad, imp, element, event), event_type) + } + + fn sink_event_full_serialized( + &self, + pad: &PadSinkRef, + imp: &Self::ElementImpl, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, Result<FlowSuccess, FlowError>> { + assert!(event.is_serialized()); + // default is to dispatch to `sink_event` + // (as implemented in `gst_pad_send_event_unchecked`) + let event_type = event.get_type(); + event_to_event_full_serialized( + self.sink_event_serialized(pad, imp, element, event), + event_type, + ) + } + + fn sink_query( + &self, + pad: &PadSinkRef, + _imp: &Self::ElementImpl, + element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + if query.is_serialized() { + gst_log!(RUNTIME_CAT, obj: pad.gst_pad(), "Dropping {:?}", query); + // FIXME serialized queries should be handled with the dataflow + // but we can't return a `Future` because we couldn't honor QueryRef's lifetime + false + } else { + gst_log!(RUNTIME_CAT, obj: pad.gst_pad(), "Handling {:?}", query); + pad.gst_pad().query_default(Some(element), query) + } + } +} + +#[derive(Debug)] +struct PadSinkInner { + gst_pad: gst::Pad, +} + +impl PadSinkInner { + fn new(gst_pad: gst::Pad) -> Self { + if gst_pad.get_direction() != gst::PadDirection::Sink { + panic!("Wrong pad direction for PadSink"); + } + + PadSinkInner { gst_pad } + } +} + +/// A [`PadSink`] which can be moved in `Handler`s functions and `Future`s. +/// +/// Call [`upgrade`] to use the [`PadSink`]. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`PadSink`]: struct.PadSink.html +/// [`upgrade`]: struct.PadSinkWeak.html#method.upgrade +/// [`pad` module]: index.html +#[derive(Clone, Debug)] +pub struct PadSinkWeak(Weak<PadSinkInner>); + +impl PadSinkWeak { + pub fn upgrade(&self) -> Option<PadSinkRef<'_>> { + self.0.upgrade().map(PadSinkRef::new) + } +} + +/// A [`PadSink`] to be used in [`handler`]s functions and `Future`s. +/// +/// Call [`downgrade`] if you need to `clone` the [`PadSink`]. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`PadSink`]: struct.PadSink.html +/// [`handler`]: trait.PadSinkHandler.html +/// [`downgrade`]: struct.PadSinkRef.html#method.downgrade +/// [`pad` module]: index.html +#[derive(Debug)] +pub struct PadSinkRef<'a> { + strong: PadSinkStrong, + phantom: PhantomData<&'a PadSrcStrong>, +} + +impl<'a> PadSinkRef<'a> { + fn new(inner_arc: Arc<PadSinkInner>) -> Self { + PadSinkRef { + strong: PadSinkStrong(inner_arc), + phantom: PhantomData, + } + } + + pub fn gst_pad(&self) -> &gst::Pad { + self.strong.gst_pad() + } + + pub fn downgrade(&self) -> PadSinkWeak { + self.strong.downgrade() + } + + fn activate_mode_hook( + &self, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError> { + // Important: don't panic here as the hook is used without `catch_panic_pad_function` + // in the default `activatemode` handling + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "ActivateMode {:?}, {}", mode, active); + + if mode == gst::PadMode::Pull { + gst_error!(RUNTIME_CAT, obj: self.gst_pad(), "Pull mode not supported by PadSink"); + return Err(gst_loggable_error!( + RUNTIME_CAT, + "Pull mode not supported by PadSink" + )); + } + + Ok(()) + } + + fn handle_future( + &self, + fut: impl Future<Output = Result<FlowSuccess, FlowError>> + Send + 'static, + ) -> Result<FlowSuccess, FlowError> { + // First try to add it as a sub task to the current task, if any + if let Err(fut) = Context::add_sub_task(fut.map(|res| res.map(drop))) { + // FIXME: update comments below + // Not on a context thread: execute the Future immediately. + // + // - If there is no PadContext, we don't have any other options. + // - If there is a PadContext, it means that we received it from + // an upstream element, but there is at least one non-ts element + // operating on another thread in between, so we can't take + // advantage of the task queue. + // + // Note: we don't use `crate::runtime::executor::block_on` here + // because `Context::is_context_thread()` is checked in the `if` + // statement above. + block_on_or_add_sub_task(fut.map(|res| res.map(|_| gst::FlowSuccess::Ok))) + .unwrap_or(Ok(gst::FlowSuccess::Ok)) + } else { + Ok(gst::FlowSuccess::Ok) + } + } +} + +#[derive(Debug)] +struct PadSinkStrong(Arc<PadSinkInner>); + +impl PadSinkStrong { + fn new(gst_pad: gst::Pad) -> Self { + PadSinkStrong(Arc::new(PadSinkInner::new(gst_pad))) + } + + fn gst_pad(&self) -> &gst::Pad { + &self.0.gst_pad + } + + fn downgrade(&self) -> PadSinkWeak { + PadSinkWeak(Arc::downgrade(&self.0)) + } +} + +/// The `PadSink` which `Element`s must own. +/// +/// Call [`downgrade`] if you need to `clone` the `PadSink`. +/// +/// *See the [`pad` module] documentation for a description of the model.* +/// +/// [`downgrade`]: struct.PadSink.html#method.downgrade +/// [`pad` module]: index.html +#[derive(Debug)] +pub struct PadSink(PadSinkStrong); + +impl PadSink { + pub fn new(gst_pad: gst::Pad) -> Self { + let this = PadSink(PadSinkStrong::new(gst_pad)); + this.set_default_activatemode_function(); + + this + } + + pub fn as_ref(&self) -> PadSinkRef<'_> { + PadSinkRef::new(Arc::clone(&(self.0).0)) + } + + pub fn gst_pad(&self) -> &gst::Pad { + self.0.gst_pad() + } + + pub fn downgrade(&self) -> PadSinkWeak { + self.0.downgrade() + } + + fn set_default_activatemode_function(&self) { + let this_weak = self.downgrade(); + self.gst_pad() + .set_activatemode_function(move |gst_pad, _parent, mode, active| { + // Important: don't panic here as we operate without `catch_panic_pad_function` + // because we may not know which element the PadSrc is associated to yet + this_weak + .upgrade() + .ok_or_else(|| { + gst_error!(RUNTIME_CAT, obj: gst_pad, "PadSink no longer exists"); + gst_loggable_error!(RUNTIME_CAT, "PadSink no longer exists") + })? + .activate_mode_hook(mode, active) + }); + } + + fn init_pad_functions<H: PadSinkHandler>(&self, handler: &H) { + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_activate_function(move |gst_pad, parent| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || { + gst_error!(RUNTIME_CAT, obj: gst_pad, "Panic in PadSink activate"); + Err(gst_loggable_error!( + RUNTIME_CAT, + "Panic in PadSink activate" + )) + }, + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSink no longer exists"); + handler.sink_activate(&this_ref, imp, element) + }, + ) + }); + + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_activatemode_function(move |gst_pad, parent, mode, active| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || { + gst_error!(RUNTIME_CAT, obj: gst_pad, "Panic in PadSink activatemode"); + Err(gst_loggable_error!( + RUNTIME_CAT, + "Panic in PadSink activatemode" + )) + }, + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSink no longer exists"); + this_ref.activate_mode_hook(mode, active)?; + + handler.sink_activatemode(&this_ref, imp, element, mode, active) + }, + ) + }); + + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_chain_function(move |_gst_pad, parent, buffer| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || Err(FlowError::Error), + move |imp, element| { + if Context::current_has_sub_tasks() { + let this_weak = this_weak.clone(); + let handler = handler.clone(); + let element = element.clone(); + let delayed_fut = async move { + let imp = + <H::ElementImpl as ObjectSubclass>::from_instance(&element); + let this_ref = + this_weak.upgrade().ok_or(gst::FlowError::Flushing)?; + handler.sink_chain(&this_ref, imp, &element, buffer).await + }; + let _ = Context::add_sub_task(delayed_fut.map(|res| res.map(drop))); + + Ok(gst::FlowSuccess::Ok) + } else { + let this_ref = this_weak.upgrade().expect("PadSink no longer exists"); + let chain_fut = handler.sink_chain(&this_ref, imp, &element, buffer); + this_ref.handle_future(chain_fut) + } + }, + ) + }); + + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_chain_list_function(move |_gst_pad, parent, list| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || Err(FlowError::Error), + move |imp, element| { + if Context::current_has_sub_tasks() { + let this_weak = this_weak.clone(); + let handler = handler.clone(); + let element = element.clone(); + let delayed_fut = async move { + let imp = + <H::ElementImpl as ObjectSubclass>::from_instance(&element); + let this_ref = + this_weak.upgrade().ok_or(gst::FlowError::Flushing)?; + handler + .sink_chain_list(&this_ref, imp, &element, list) + .await + }; + let _ = Context::add_sub_task(delayed_fut.map(|res| res.map(drop))); + + Ok(gst::FlowSuccess::Ok) + } else { + let this_ref = this_weak.upgrade().expect("PadSink no longer exists"); + let chain_list_fut = + handler.sink_chain_list(&this_ref, imp, &element, list); + this_ref.handle_future(chain_list_fut) + } + }, + ) + }); + + // No need to `set_event_function` since `set_event_full_function` + // overrides it and dispatches to `sink_event` when necessary + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_event_full_function(move |_gst_pad, parent, event| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || Err(FlowError::Error), + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSink no longer exists"); + if event.is_serialized() { + if Context::current_has_sub_tasks() { + let this_weak = this_weak.clone(); + let handler = handler.clone(); + let element = element.clone(); + let delayed_fut = async move { + let imp = + <H::ElementImpl as ObjectSubclass>::from_instance(&element); + let this_ref = + this_weak.upgrade().ok_or(gst::FlowError::Flushing)?; + + handler + .sink_event_full_serialized(&this_ref, imp, &element, event) + .await + }; + let _ = Context::add_sub_task(delayed_fut.map(|res| res.map(drop))); + + Ok(gst::FlowSuccess::Ok) + } else { + let event_fut = handler + .sink_event_full_serialized(&this_ref, imp, &element, event); + this_ref.handle_future(event_fut) + } + } else { + handler.sink_event_full(&this_ref, imp, &element, event) + } + }, + ) + }); + + let handler_clone = handler.clone(); + let this_weak = self.downgrade(); + self.gst_pad() + .set_query_function(move |_gst_pad, parent, query| { + let handler = handler_clone.clone(); + let this_weak = this_weak.clone(); + H::ElementImpl::catch_panic_pad_function( + parent, + || false, + move |imp, element| { + let this_ref = this_weak.upgrade().expect("PadSink no longer exists"); + if !query.is_serialized() { + handler.sink_query(&this_ref, imp, &element, query) + } else { + gst_fixme!(RUNTIME_CAT, obj: this_ref.gst_pad(), "Serialized Query not supported"); + false + } + }, + ) + }); + } + + pub fn prepare<H: PadSinkHandler>(&self, handler: &H) { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Preparing"); + self.init_pad_functions(handler); + } + + /// Releases the resources held by this `PadSink`. + pub fn unprepare(&self) { + gst_log!(RUNTIME_CAT, obj: self.gst_pad(), "Unpreparing"); + + self.gst_pad() + .set_activate_function(move |_gst_pad, _parent| { + Err(gst_loggable_error!(RUNTIME_CAT, "PadSink unprepared")) + }); + self.set_default_activatemode_function(); + self.gst_pad() + .set_chain_function(move |_gst_pad, _parent, _buffer| Err(FlowError::Flushing)); + self.gst_pad() + .set_chain_list_function(move |_gst_pad, _parent, _list| Err(FlowError::Flushing)); + self.gst_pad() + .set_event_function(move |_gst_pad, _parent, _event| false); + self.gst_pad() + .set_event_full_function(move |_gst_pad, _parent, _event| Err(FlowError::Flushing)); + self.gst_pad() + .set_query_function(move |_gst_pad, _parent, _query| false); + } +} diff --git a/generic/gst-plugin-threadshare/src/runtime/task.rs b/generic/gst-plugin-threadshare/src/runtime/task.rs new file mode 100644 index 000000000..c48327cce --- /dev/null +++ b/generic/gst-plugin-threadshare/src/runtime/task.rs @@ -0,0 +1,725 @@ +// Copyright (C) 2019-2020 François Laignel <fengalin@free.fr> +// Copyright (C) 2020 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +//! An execution loop to run asynchronous processing. + +use futures::channel::oneshot; +use futures::future::{abortable, AbortHandle, Aborted}; +use futures::prelude::*; + +use gst::{gst_debug, gst_error, gst_log, gst_trace, gst_warning}; + +use std::fmt; +use std::sync::{Arc, Mutex}; + +use super::executor::{block_on, yield_now}; +use super::{Context, JoinHandle, RUNTIME_CAT}; + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy)] +pub enum TaskState { + Cancelled, + Started, + Stopped, + Paused, + Pausing, + Preparing, + Unprepared, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TaskError { + ActiveTask, +} + +impl fmt::Display for TaskError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TaskError::ActiveTask => write!(f, "The task is still active"), + } + } +} + +impl std::error::Error for TaskError {} + +#[derive(Debug)] +struct TaskInner { + context: Option<Context>, + state: TaskState, + prepare_handle: Option<JoinHandle<Result<Result<(), gst::FlowError>, Aborted>>>, + prepare_abort_handle: Option<AbortHandle>, + abort_handle: Option<AbortHandle>, + loop_handle: Option<JoinHandle<Result<(), Aborted>>>, + resume_sender: Option<oneshot::Sender<()>>, +} + +impl Default for TaskInner { + fn default() -> Self { + TaskInner { + context: None, + state: TaskState::Unprepared, + prepare_handle: None, + prepare_abort_handle: None, + abort_handle: None, + loop_handle: None, + resume_sender: None, + } + } +} + +impl Drop for TaskInner { + fn drop(&mut self) { + if self.state != TaskState::Unprepared { + panic!("Missing call to `Task::unprepared`"); + } + } +} + +/// A `Task` operating on a `threadshare` [`Context`]. +/// +/// [`Context`]: ../executor/struct.Context.html +#[derive(Debug)] +pub struct Task(Arc<Mutex<TaskInner>>); + +impl Default for Task { + fn default() -> Self { + Task(Arc::new(Mutex::new(TaskInner::default()))) + } +} + +impl Task { + pub fn prepare_with_func<F, Fut>( + &self, + context: Context, + prepare_func: F, + ) -> Result<(), TaskError> + where + F: (FnOnce() -> Fut) + Send + 'static, + Fut: Future<Output = ()> + Send + 'static, + { + gst_debug!(RUNTIME_CAT, "Preparing task"); + + let mut inner = self.0.lock().unwrap(); + if inner.state != TaskState::Unprepared { + return Err(TaskError::ActiveTask); + } + + // Spawn prepare function in the background + let task_weak = Arc::downgrade(&self.0); + let (prepare_fut, prepare_abort_handle) = abortable(async move { + gst_trace!(RUNTIME_CAT, "Calling task prepare function"); + + prepare_func().await; + + gst_trace!(RUNTIME_CAT, "Task prepare function finished"); + + while Context::current_has_sub_tasks() { + Context::drain_sub_tasks().await?; + } + + // Once the prepare function is finished we can forget the corresponding + // handles so that unprepare and friends don't have to block on it anymore + if let Some(task_inner) = task_weak.upgrade() { + let mut inner = task_inner.lock().unwrap(); + inner.prepare_abort_handle = None; + inner.prepare_handle = None; + } + + gst_trace!(RUNTIME_CAT, "Task fully prepared"); + + Ok(()) + }); + let prepare_handle = context.spawn(prepare_fut); + inner.prepare_handle = Some(prepare_handle); + inner.prepare_abort_handle = Some(prepare_abort_handle); + + inner.context = Some(context); + + inner.state = TaskState::Preparing; + gst_debug!(RUNTIME_CAT, "Task prepared"); + + Ok(()) + } + + pub fn prepare(&self, context: Context) -> Result<(), TaskError> { + gst_debug!(RUNTIME_CAT, "Preparing task"); + + let mut inner = self.0.lock().unwrap(); + if inner.state != TaskState::Unprepared { + return Err(TaskError::ActiveTask); + } + + inner.prepare_handle = None; + inner.prepare_abort_handle = None; + + inner.context = Some(context); + + inner.state = TaskState::Stopped; + gst_debug!(RUNTIME_CAT, "Task prepared"); + + Ok(()) + } + + pub fn unprepare(&self) -> Result<(), TaskError> { + let mut inner = self.0.lock().unwrap(); + if inner.state != TaskState::Stopped { + gst_error!( + RUNTIME_CAT, + "Attempt to Unprepare a task in state {:?}", + inner.state + ); + return Err(TaskError::ActiveTask); + } + + gst_debug!(RUNTIME_CAT, "Unpreparing task"); + + // Abort any pending preparation + if let Some(abort_handle) = inner.prepare_abort_handle.take() { + abort_handle.abort(); + } + let prepare_handle = inner.prepare_handle.take(); + + let context = inner.context.take().unwrap(); + + inner.state = TaskState::Unprepared; + + drop(inner); + + if let Some(prepare_handle) = prepare_handle { + if let Some((cur_context, cur_task_id)) = Context::current_task() { + if prepare_handle.is_current() { + // This would deadlock! + gst_warning!( + RUNTIME_CAT, + "Trying to stop task {:?} from itself, not waiting", + prepare_handle + ); + } else if cur_context == context { + // This is ok: as we're on the same thread and the prepare function is aborted + // this means that it won't ever be called, and we're not inside it here + gst_debug!( + RUNTIME_CAT, + "Asynchronously waiting for task {:?} on the same context", + prepare_handle + ); + + let _ = Context::add_sub_task(async move { + let _ = prepare_handle.await; + Ok(()) + }); + } else { + // This is suboptimal but we can't really do this asynchronously as otherwise + // it might be started again before it's actually stopped. + gst_warning!( + RUNTIME_CAT, + "Synchronously waiting for task {:?} on task {:?} on context {}", + prepare_handle, + cur_task_id, + cur_context.name() + ); + let _ = block_on(prepare_handle); + } + } else { + gst_debug!( + RUNTIME_CAT, + "Synchronously waiting for task {:?}", + prepare_handle + ); + let _ = block_on(prepare_handle); + } + } + + gst_debug!(RUNTIME_CAT, "Task unprepared"); + + Ok(()) + } + + pub fn state(&self) -> TaskState { + self.0.lock().unwrap().state + } + + pub fn context(&self) -> Option<Context> { + self.0.lock().unwrap().context.as_ref().cloned() + } + + /// `Starts` the `Task`. + /// + /// The `Task` will loop on the provided @func. + /// The execution occurs on the `Task`'s context. + pub fn start<F, Fut>(&self, mut func: F) + where + F: (FnMut() -> Fut) + Send + 'static, + Fut: Future<Output = glib::Continue> + Send + 'static, + { + let inner_clone = Arc::clone(&self.0); + let mut inner = self.0.lock().unwrap(); + match inner.state { + TaskState::Started => { + gst_log!(RUNTIME_CAT, "Task already Started"); + return; + } + TaskState::Pausing => { + gst_debug!(RUNTIME_CAT, "Re-starting a Pausing task"); + + assert!(inner.resume_sender.is_none()); + + inner.state = TaskState::Started; + return; + } + TaskState::Paused => { + inner + .resume_sender + .take() + .expect("Task Paused but the resume_sender is already taken") + .send(()) + .expect("Task Paused but the resume_receiver was dropped"); + + gst_log!(RUNTIME_CAT, "Resume requested"); + return; + } + TaskState::Stopped | TaskState::Cancelled | TaskState::Preparing => (), + TaskState::Unprepared => panic!("Attempt to start an unprepared Task"), + } + + gst_debug!(RUNTIME_CAT, "Starting Task"); + + let prepare_handle = inner.prepare_handle.take(); + + // If the task was only cancelled and not actually stopped yet then + // wait for that to happen as first thing in the new task. + let loop_handle = inner.loop_handle.take(); + + let (loop_fut, abort_handle) = abortable(async move { + let task_id = Context::current_task().unwrap().1; + + if let Some(loop_handle) = loop_handle { + gst_trace!( + RUNTIME_CAT, + "Waiting for previous loop to finish before starting" + ); + let _ = loop_handle.await; + } + + // First await on the prepare function, if any + if let Some(prepare_handle) = prepare_handle { + gst_trace!(RUNTIME_CAT, "Waiting for prepare before starting"); + let res = prepare_handle.await; + if res.is_err() { + gst_warning!(RUNTIME_CAT, "Preparing failed"); + inner_clone.lock().unwrap().state = TaskState::Unprepared; + + return; + } + + inner_clone.lock().unwrap().state = TaskState::Stopped; + } + + gst_trace!(RUNTIME_CAT, "Starting task loop"); + + // Then loop as long as we're actually running + loop { + let mut resume_receiver = { + let mut inner = inner_clone.lock().unwrap(); + match inner.state { + TaskState::Started => None, + TaskState::Pausing => { + let (sender, receiver) = oneshot::channel(); + inner.resume_sender = Some(sender); + + inner.state = TaskState::Paused; + + Some(receiver) + } + TaskState::Stopped | TaskState::Cancelled => { + gst_trace!(RUNTIME_CAT, "Stopping task loop"); + break; + } + TaskState::Paused => { + unreachable!("The Paused state is controlled by the loop"); + } + other => { + unreachable!("Task loop iteration in state {:?}", other); + } + } + }; + + if let Some(resume_receiver) = resume_receiver.take() { + gst_trace!(RUNTIME_CAT, "Task loop paused"); + + match resume_receiver.await { + Ok(_) => { + gst_trace!(RUNTIME_CAT, "Resuming task loop"); + inner_clone.lock().unwrap().state = TaskState::Started; + } + Err(_) => { + gst_trace!(RUNTIME_CAT, "Resume cancelled"); + break; + } + } + } + + if func().await == glib::Continue(false) { + let mut inner = inner_clone.lock().unwrap(); + + // Make sure to only reset the state if this is still the correct task + // and no new task was started in the meantime + if inner.state == TaskState::Started + && inner + .loop_handle + .as_ref() + .map(|h| h.task_id() == task_id) + .unwrap_or(false) + { + gst_trace!(RUNTIME_CAT, "Exiting task loop"); + inner.state = TaskState::Cancelled; + } + + break; + } + + // Make sure the loop can be aborted even if `func` never goes `Pending`. + yield_now().await; + } + + // Once the loop function is finished we can forget the corresponding + // handles so that unprepare and friends don't have to block on it anymore + { + let mut inner = inner_clone.lock().unwrap(); + + // Make sure to only reset the state if this is still the correct task + // and no new task was started in the meantime + if inner + .loop_handle + .as_ref() + .map(|h| h.task_id() == task_id) + .unwrap_or(false) + { + inner.abort_handle = None; + inner.loop_handle = None; + inner.state = TaskState::Stopped; + } + } + + gst_trace!(RUNTIME_CAT, "Task loop finished"); + }); + + let loop_handle = inner + .context + .as_ref() + .expect("Context not set") + .spawn(loop_fut); + + inner.abort_handle = Some(abort_handle); + inner.loop_handle = Some(loop_handle); + inner.state = TaskState::Started; + + gst_debug!(RUNTIME_CAT, "Task Started"); + } + + /// Requests the `Task` loop to pause. + /// + /// If an iteration is in progress, it will run to completion, + /// then no more iteration will be executed before `start` is called again. + pub fn pause(&self) { + let mut inner = self.0.lock().unwrap(); + if inner.state != TaskState::Started { + gst_log!(RUNTIME_CAT, "Task not started"); + return; + } + + inner.state = TaskState::Pausing; + gst_debug!(RUNTIME_CAT, "Pause requested"); + } + + /// Cancels the `Task` so that it stops running as soon as possible. + pub fn cancel(&self) { + let mut inner = self.0.lock().unwrap(); + if inner.state != TaskState::Started + && inner.state != TaskState::Paused + && inner.state != TaskState::Pausing + { + gst_log!(RUNTIME_CAT, "Task not Started nor Paused"); + return; + } + + gst_debug!(RUNTIME_CAT, "Cancelling Task"); + + // Abort any still running loop function + if let Some(abort_handle) = inner.abort_handle.take() { + abort_handle.abort(); + } + + inner.resume_sender = None; + + inner.state = TaskState::Cancelled; + } + + /// Stops the `Started` `Task` and wait for it to finish. + pub fn stop(&self) { + let mut inner = self.0.lock().unwrap(); + if inner.state == TaskState::Stopped || inner.state == TaskState::Preparing { + gst_log!(RUNTIME_CAT, "Task loop already stopped"); + return; + } + + gst_debug!(RUNTIME_CAT, "Stopping Task"); + + inner.state = TaskState::Stopped; + + // Abort any still running loop function + if let Some(abort_handle) = inner.abort_handle.take() { + abort_handle.abort(); + } + + // And now wait for it to actually stop + let loop_handle = inner.loop_handle.take(); + + inner.resume_sender = None; + + let context = inner.context.as_ref().unwrap().clone(); + drop(inner); + + if let Some(loop_handle) = loop_handle { + if let Some((cur_context, cur_task_id)) = Context::current_task() { + if loop_handle.is_current() { + // This would deadlock! + gst_warning!( + RUNTIME_CAT, + "Trying to stop task {:?} from itself, not waiting", + loop_handle + ); + } else if cur_context == context { + // This is ok: as we're on the same thread and the loop function is aborted + // this means that it won't ever be called, and we're not inside it here + gst_debug!( + RUNTIME_CAT, + "Asynchronously waiting for task {:?} on the same context", + loop_handle + ); + + let _ = Context::add_sub_task(async move { + let _ = loop_handle.await; + Ok(()) + }); + } else { + // This is suboptimal but we can't really do this asynchronously as otherwise + // it might be started again before it's actually stopped. + gst_warning!( + RUNTIME_CAT, + "Synchronously waiting for task {:?} on task {:?} on context {}", + loop_handle, + cur_task_id, + cur_context.name() + ); + let _ = block_on(loop_handle); + } + } else { + gst_debug!( + RUNTIME_CAT, + "Synchronously waiting for task {:?}", + loop_handle + ); + let _ = block_on(loop_handle); + } + } + + gst_debug!(RUNTIME_CAT, "Task stopped"); + } +} + +#[cfg(test)] +mod tests { + use futures::channel::{mpsc, oneshot}; + use futures::lock::Mutex; + + use std::sync::Arc; + + use crate::runtime::Context; + + use super::*; + + #[tokio::test] + async fn task() { + gst::init().unwrap(); + + let context = Context::acquire("task", 2).unwrap(); + + let task = Task::default(); + task.prepare(context).unwrap(); + + let (mut sender, receiver) = mpsc::channel(0); + let receiver = Arc::new(Mutex::new(receiver)); + + gst_debug!(RUNTIME_CAT, "task test: starting"); + task.start(move || { + let receiver = Arc::clone(&receiver); + async move { + gst_debug!(RUNTIME_CAT, "task test: awaiting receiver"); + match receiver.lock().await.next().await { + Some(_) => { + gst_debug!(RUNTIME_CAT, "task test: item received"); + glib::Continue(true) + } + None => { + gst_debug!(RUNTIME_CAT, "task test: channel complete"); + glib::Continue(false) + } + } + } + }); + + gst_debug!(RUNTIME_CAT, "task test: sending item"); + sender.send(()).await.unwrap(); + gst_debug!(RUNTIME_CAT, "task test: item sent"); + + gst_debug!(RUNTIME_CAT, "task test: dropping sender"); + drop(sender); + + gst_debug!(RUNTIME_CAT, "task test: stopping"); + task.stop(); + gst_debug!(RUNTIME_CAT, "task test: stopped"); + + task.unprepare().unwrap(); + gst_debug!(RUNTIME_CAT, "task test: unprepared"); + } + + #[tokio::test] + async fn task_with_prepare_func() { + gst::init().unwrap(); + + let context = Context::acquire("task_with_prepare_func", 2).unwrap(); + + let task = Task::default(); + + let (prepare_sender, prepare_receiver) = oneshot::channel(); + task.prepare_with_func(context, move || async move { + prepare_sender.send(()).unwrap(); + }) + .unwrap(); + + let (mut sender, receiver) = mpsc::channel(0); + let receiver = Arc::new(Mutex::new(receiver)); + + let mut prepare_receiver = Some(prepare_receiver); + + gst_debug!(RUNTIME_CAT, "task test: starting"); + task.start(move || { + if let Some(mut prepare_receiver) = prepare_receiver.take() { + assert_eq!(prepare_receiver.try_recv().unwrap(), Some(())); + } + + let receiver = Arc::clone(&receiver); + async move { + gst_debug!(RUNTIME_CAT, "task test: awaiting receiver"); + match receiver.lock().await.next().await { + Some(_) => { + gst_debug!(RUNTIME_CAT, "task test: item received"); + glib::Continue(true) + } + None => { + gst_debug!(RUNTIME_CAT, "task test: channel complete"); + glib::Continue(false) + } + } + } + }); + + gst_debug!(RUNTIME_CAT, "task test: sending item"); + sender.send(()).await.unwrap(); + gst_debug!(RUNTIME_CAT, "task test: item sent"); + + gst_debug!(RUNTIME_CAT, "task test: dropping sender"); + drop(sender); + + gst_debug!(RUNTIME_CAT, "task test: stopping"); + task.stop(); + gst_debug!(RUNTIME_CAT, "task test: stopped"); + + task.unprepare().unwrap(); + gst_debug!(RUNTIME_CAT, "task test: unprepared"); + } + + #[tokio::test] + async fn pause_start() { + use gst::gst_error; + + gst::init().unwrap(); + + let context = Context::acquire("task_pause_start", 2).unwrap(); + + let task = Task::default(); + task.prepare(context).unwrap(); + + let (iter_sender, mut iter_receiver) = mpsc::channel(0); + let iter_sender = Arc::new(Mutex::new(iter_sender)); + + let (mut complete_sender, complete_receiver) = mpsc::channel(0); + let complete_receiver = Arc::new(Mutex::new(complete_receiver)); + + gst_debug!(RUNTIME_CAT, "task_pause_start: starting"); + task.start(move || { + let iter_sender = Arc::clone(&iter_sender); + let complete_receiver = Arc::clone(&complete_receiver); + async move { + gst_debug!(RUNTIME_CAT, "task_pause_start: entering iteration"); + iter_sender.lock().await.send(()).await.unwrap(); + + gst_debug!( + RUNTIME_CAT, + "task_pause_start: iteration awaiting completion" + ); + complete_receiver.lock().await.next().await.unwrap(); + gst_debug!(RUNTIME_CAT, "task_pause_start: iteration complete"); + glib::Continue(true) + } + }); + + gst_debug!(RUNTIME_CAT, "task_pause_start: awaiting 1st iteration"); + iter_receiver.next().await.unwrap(); + + task.pause(); + + gst_debug!( + RUNTIME_CAT, + "task_pause_start: sending 1st iteration completion" + ); + complete_sender.send(()).await.unwrap(); + + // Loop held on + iter_receiver.try_next().unwrap_err(); + + task.start(|| { + gst_error!( + RUNTIME_CAT, + "task_pause_start: reached start to resume closure" + ); + future::pending() + }); + + gst_debug!(RUNTIME_CAT, "task_pause_start: awaiting 2d iteration"); + iter_receiver.next().await.unwrap(); + + gst_debug!( + RUNTIME_CAT, + "task_pause_start: sending 2d iteration completion" + ); + complete_sender.send(()).await.unwrap(); + + task.stop(); + task.unprepare().unwrap(); + } +} diff --git a/generic/gst-plugin-threadshare/src/runtime/time.rs b/generic/gst-plugin-threadshare/src/runtime/time.rs new file mode 100644 index 000000000..0ea857b0c --- /dev/null +++ b/generic/gst-plugin-threadshare/src/runtime/time.rs @@ -0,0 +1,37 @@ +// Copyright (C) 2020 François Laignel <fengalin@free.fr> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +//! Wrappers for the underlying runtime specific time related Futures. + +use futures::prelude::*; +use futures::stream::StreamExt; + +use std::time::Duration; + +/// Wait until the given `delay` has elapsed. +/// +/// This must be called from within the target runtime environment. +pub async fn delay_for(delay: Duration) { + tokio::time::delay_for(delay).map(drop).await; +} + +/// Builds a `Stream` that yields at `interval. +/// +/// This must be called from within the target runtime environment. +pub fn interval(interval: Duration) -> impl Stream<Item = ()> { + tokio::time::interval(interval).map(drop) +} diff --git a/generic/gst-plugin-threadshare/src/socket.rs b/generic/gst-plugin-threadshare/src/socket.rs new file mode 100644 index 000000000..790e977bc --- /dev/null +++ b/generic/gst-plugin-threadshare/src/socket.rs @@ -0,0 +1,489 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// Copyright (C) 2018 LEE Dongjun <redongjun@gmail.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::future::{abortable, AbortHandle, Aborted, BoxFuture}; +use futures::prelude::*; + +use gst; +use gst::prelude::*; +use gst::{gst_debug, gst_error, gst_error_msg}; + +use lazy_static::lazy_static; + +use std::io; +use std::sync::{Arc, Mutex}; + +use gio; +use gio::prelude::*; +use gio_sys as gio_ffi; +use gobject_sys as gobject_ffi; + +#[cfg(unix)] +use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; + +#[cfg(windows)] +use std::os::windows::io::{AsRawSocket, FromRawSocket, IntoRawSocket, RawSocket}; + +lazy_static! { + static ref SOCKET_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-socket", + gst::DebugColorFlags::empty(), + Some("Thread-sharing Socket"), + ); +} + +pub struct Socket<T: SocketRead + 'static>(Arc<Mutex<SocketInner<T>>>); + +pub trait SocketRead: Send + Unpin { + const DO_TIMESTAMP: bool; + + fn read<'buf>( + &self, + buffer: &'buf mut [u8], + ) -> BoxFuture<'buf, io::Result<(usize, Option<std::net::SocketAddr>)>>; +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SocketState { + Paused, + Prepared, + Started, + Unprepared, +} + +struct SocketInner<T: SocketRead + 'static> { + state: SocketState, + element: gst::Element, + buffer_pool: gst::BufferPool, + clock: Option<gst::Clock>, + base_time: Option<gst::ClockTime>, + create_read_handle: Option<AbortHandle>, + create_reader_fut: Option<BoxFuture<'static, Result<T, SocketError>>>, + read_handle: Option<AbortHandle>, + reader: Option<T>, +} + +impl<T: SocketRead + 'static> Socket<T> { + pub fn new<F>( + element: &gst::Element, + buffer_pool: gst::BufferPool, + create_reader_fut: F, + ) -> Result<Self, ()> + where + F: Future<Output = Result<T, SocketError>> + Send + 'static, + { + let socket = Socket(Arc::new(Mutex::new(SocketInner::<T> { + state: SocketState::Unprepared, + element: element.clone(), + buffer_pool, + clock: None, + base_time: None, + create_read_handle: None, + create_reader_fut: Some(create_reader_fut.boxed()), + read_handle: None, + reader: None, + }))); + + let mut inner = socket.0.lock().unwrap(); + if inner.state != SocketState::Unprepared { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Socket already prepared"); + return Err(()); + } + gst_debug!(SOCKET_CAT, obj: &inner.element, "Preparing socket"); + + inner.buffer_pool.set_active(true).map_err(|err| { + gst_error!(SOCKET_CAT, obj: &inner.element, "Failed to prepare socket: {}", err); + })?; + inner.state = SocketState::Prepared; + drop(inner); + + Ok(socket) + } + + pub fn state(&self) -> SocketState { + self.0.lock().unwrap().state + } + + pub fn start( + &self, + clock: Option<gst::Clock>, + base_time: Option<gst::ClockTime>, + ) -> Result<SocketStream<T>, ()> { + // Paused->Playing + let mut inner = self.0.lock().unwrap(); + assert_ne!(SocketState::Unprepared, inner.state); + if inner.state == SocketState::Started { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Socket already started"); + return Err(()); + } + + gst_debug!(SOCKET_CAT, obj: &inner.element, "Starting socket"); + inner.clock = clock; + inner.base_time = base_time; + inner.state = SocketState::Started; + + Ok(SocketStream::<T>::new(self)) + } + + pub fn pause(&self) { + // Playing->Paused + let mut inner = self.0.lock().unwrap(); + assert_ne!(SocketState::Unprepared, inner.state); + if inner.state != SocketState::Started { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Socket not started"); + return; + } + + gst_debug!(SOCKET_CAT, obj: &inner.element, "Pausing socket"); + inner.clock = None; + inner.base_time = None; + inner.state = SocketState::Paused; + if let Some(read_handle) = inner.read_handle.take() { + read_handle.abort(); + } + } +} + +impl<T: SocketRead> Drop for Socket<T> { + fn drop(&mut self) { + // Ready->Null + let mut inner = self.0.lock().unwrap(); + assert_ne!(SocketState::Started, inner.state); + if inner.state == SocketState::Unprepared { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Socket already unprepared"); + return; + } + + if let Some(create_read_handle_handle) = inner.create_read_handle.take() { + create_read_handle_handle.abort(); + } + + if let Err(err) = inner.buffer_pool.set_active(false) { + gst_error!(SOCKET_CAT, obj: &inner.element, "Failed to unprepare socket: {}", err); + } + inner.state = SocketState::Unprepared; + } +} + +impl<T: SocketRead + Unpin + 'static> Clone for Socket<T> { + fn clone(&self) -> Self { + Socket::<T>(self.0.clone()) + } +} + +pub type SocketStreamItem = Result<(gst::Buffer, Option<std::net::SocketAddr>), SocketError>; + +#[derive(Debug)] +pub enum SocketError { + Gst(gst::FlowError), + Io(io::Error), +} + +pub struct SocketStream<T: SocketRead + 'static> { + socket: Socket<T>, + mapped_buffer: Option<gst::MappedBuffer<gst::buffer::Writable>>, +} + +impl<T: SocketRead + 'static> SocketStream<T> { + fn new(socket: &Socket<T>) -> Self { + SocketStream { + socket: socket.clone(), + mapped_buffer: None, + } + } + + #[allow(clippy::should_implement_trait)] + pub async fn next(&mut self) -> Option<SocketStreamItem> { + // First create if needed + let (create_reader_fut, element) = { + let mut inner = self.socket.0.lock().unwrap(); + + if let Some(create_reader_fut) = inner.create_reader_fut.take() { + let (create_reader_fut, abort_handle) = abortable(create_reader_fut); + inner.create_read_handle = Some(abort_handle); + (Some(create_reader_fut), inner.element.clone()) + } else { + (None, inner.element.clone()) + } + }; + + if let Some(create_reader_fut) = create_reader_fut { + match create_reader_fut.await { + Ok(Ok(read)) => { + let mut inner = self.socket.0.lock().unwrap(); + inner.create_read_handle = None; + inner.reader = Some(read); + } + Ok(Err(err)) => { + gst_debug!(SOCKET_CAT, obj: &element, "Create reader error {:?}", err); + + return Some(Err(err)); + } + Err(Aborted) => { + gst_debug!(SOCKET_CAT, obj: &element, "Create reader Aborted"); + + return None; + } + } + } + + // take the mapped_buffer before locking the socket so as to please the mighty borrow checker + let (read_fut, clock, base_time) = { + let mut inner = self.socket.0.lock().unwrap(); + if inner.state != SocketState::Started { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Socket is not Started"); + return None; + } + + let reader = match inner.reader { + None => { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Have no reader"); + return None; + } + Some(ref reader) => reader, + }; + + gst_debug!(SOCKET_CAT, obj: &inner.element, "Trying to read data"); + if self.mapped_buffer.is_none() { + match inner.buffer_pool.acquire_buffer(None) { + Ok(buffer) => { + self.mapped_buffer = Some(buffer.into_mapped_buffer_writable().unwrap()); + } + Err(err) => { + gst_debug!(SOCKET_CAT, obj: &inner.element, "Failed to acquire buffer {:?}", err); + return Some(Err(SocketError::Gst(err))); + } + } + } + + let (read_fut, abort_handle) = + abortable(reader.read(self.mapped_buffer.as_mut().unwrap().as_mut_slice())); + inner.read_handle = Some(abort_handle); + + (read_fut, inner.clock.clone(), inner.base_time) + }; + + match read_fut.await { + Ok(Ok((len, saddr))) => { + let dts = if T::DO_TIMESTAMP { + let time = clock.as_ref().unwrap().get_time(); + let running_time = time - base_time.unwrap(); + gst_debug!( + SOCKET_CAT, + obj: &element, + "Read {} bytes at {} (clock {})", + len, + running_time, + time + ); + running_time + } else { + gst_debug!(SOCKET_CAT, obj: &element, "Read {} bytes", len); + gst::CLOCK_TIME_NONE + }; + + let mut buffer = self.mapped_buffer.take().unwrap().into_buffer(); + { + let buffer = buffer.get_mut().unwrap(); + if len < buffer.get_size() { + buffer.set_size(len); + } + buffer.set_dts(dts); + } + + Some(Ok((buffer, saddr))) + } + Ok(Err(err)) => { + gst_debug!(SOCKET_CAT, obj: &element, "Read error {:?}", err); + + Some(Err(SocketError::Io(err))) + } + Err(Aborted) => { + gst_debug!(SOCKET_CAT, obj: &element, "Read Aborted"); + + None + } + } + } +} + +// Send/Sync struct for passing around a gio::Socket +// and getting the raw fd from it +// +// gio::Socket is not Send/Sync as it's generally unsafe +// to access it from multiple threads. Getting the underlying raw +// fd is safe though, as is receiving/sending from two different threads +#[derive(Debug)] +pub struct GioSocketWrapper { + socket: *mut gio_ffi::GSocket, +} + +unsafe impl Send for GioSocketWrapper {} +unsafe impl Sync for GioSocketWrapper {} + +impl GioSocketWrapper { + pub fn new(socket: &gio::Socket) -> Self { + use glib::translate::*; + + Self { + socket: socket.to_glib_full(), + } + } + + pub fn as_socket(&self) -> gio::Socket { + unsafe { + use glib::translate::*; + + from_glib_none(self.socket) + } + } + + #[cfg(unix)] + pub fn set_tos(&self, qos_dscp: i32) -> Result<(), glib::error::Error> { + use libc::{IPPROTO_IP, IPPROTO_IPV6, IPV6_TCLASS, IP_TOS}; + + let tos = (qos_dscp & 0x3f) << 2; + + let socket = self.as_socket(); + + socket.set_option(IPPROTO_IP, IP_TOS, tos)?; + + if socket.get_family() == gio::SocketFamily::Ipv6 { + socket.set_option(IPPROTO_IPV6, IPV6_TCLASS, tos)?; + } + + Ok(()) + } + + #[cfg(not(unix))] + pub fn set_tos(&self, qos_dscp: i32) -> Result<(), Error> { + Ok(()) + } + + #[cfg(unix)] + pub fn get<T: FromRawFd>(&self) -> T { + unsafe { FromRawFd::from_raw_fd(libc::dup(gio_ffi::g_socket_get_fd(self.socket))) } + } + + #[cfg(windows)] + pub fn get<T: FromRawSocket>(&self) -> T { + unsafe { + FromRawSocket::from_raw_socket( + dup_socket(gio_ffi::g_socket_get_fd(self.socket) as _) as _ + ) + } + } +} + +impl Clone for GioSocketWrapper { + fn clone(&self) -> Self { + Self { + socket: unsafe { gobject_ffi::g_object_ref(self.socket as *mut _) as *mut _ }, + } + } +} + +impl Drop for GioSocketWrapper { + fn drop(&mut self) { + unsafe { + gobject_ffi::g_object_unref(self.socket as *mut _); + } + } +} + +#[cfg(windows)] +unsafe fn dup_socket(socket: usize) -> usize { + use std::mem; + use winapi::shared::ws2def; + use winapi::um::processthreadsapi; + use winapi::um::winsock2; + + let mut proto_info = mem::MaybeUninit::uninit(); + let ret = winsock2::WSADuplicateSocketA( + socket, + processthreadsapi::GetCurrentProcessId(), + proto_info.as_mut_ptr(), + ); + assert_eq!(ret, 0); + let mut proto_info = proto_info.assume_init(); + + let socket = winsock2::WSASocketA( + ws2def::AF_INET, + ws2def::SOCK_DGRAM, + ws2def::IPPROTO_UDP as i32, + &mut proto_info, + 0, + 0, + ); + + assert_ne!(socket, winsock2::INVALID_SOCKET); + + socket +} + +pub fn wrap_socket(socket: &tokio::net::UdpSocket) -> Result<GioSocketWrapper, gst::ErrorMessage> { + #[cfg(unix)] + unsafe { + let fd = libc::dup(socket.as_raw_fd()); + + // This is unsafe because it allows us to share the fd between the socket and the + // GIO socket below, but safety of this is the job of the application + struct FdConverter(RawFd); + impl IntoRawFd for FdConverter { + fn into_raw_fd(self) -> RawFd { + self.0 + } + } + + let fd = FdConverter(fd); + + let gio_socket = gio::Socket::new_from_fd(fd).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to create wrapped GIO socket: {}", err] + ) + })?; + Ok(GioSocketWrapper::new(&gio_socket)) + } + #[cfg(windows)] + unsafe { + // FIXME: Needs https://github.com/tokio-rs/tokio/pull/806 + // and https://github.com/carllerche/mio/pull/859 + let fd = unreachable!(); //dup_socket(socket.as_raw_socket() as _) as _; + + // This is unsafe because it allows us to share the fd between the socket and the + // GIO socket below, but safety of this is the job of the application + struct SocketConverter(RawSocket); + impl IntoRawSocket for SocketConverter { + fn into_raw_socket(self) -> RawSocket { + self.0 + } + } + + let fd = SocketConverter(fd); + + let gio_socket = gio::Socket::new_from_socket(fd).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to create wrapped GIO socket: {}", err] + ) + })?; + + Ok(GioSocketWrapper::new(&gio_socket)) + } +} diff --git a/generic/gst-plugin-threadshare/src/tcpclientsrc.rs b/generic/gst-plugin-threadshare/src/tcpclientsrc.rs new file mode 100644 index 000000000..1a35a668a --- /dev/null +++ b/generic/gst-plugin-threadshare/src/tcpclientsrc.rs @@ -0,0 +1,768 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// Copyright (C) 2018 LEE Dongjun <redongjun@gmail.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::future::BoxFuture; +use futures::lock::Mutex as FutMutex; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_element_error, gst_error, gst_error_msg, gst_log, gst_trace}; + +use lazy_static::lazy_static; + +use rand; + +use std::io; +use std::net::{IpAddr, SocketAddr}; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::u16; + +use tokio::io::AsyncReadExt; + +use crate::runtime::prelude::*; +use crate::runtime::{Context, PadSrc, PadSrcRef, Task}; + +use super::socket::{Socket, SocketError, SocketRead, SocketState}; + +const DEFAULT_ADDRESS: Option<&str> = Some("127.0.0.1"); +const DEFAULT_PORT: u32 = 5000; +const DEFAULT_CAPS: Option<gst::Caps> = None; +const DEFAULT_CHUNK_SIZE: u32 = 4096; +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; + +#[derive(Debug, Clone)] +struct Settings { + address: Option<String>, + port: u32, + caps: Option<gst::Caps>, + chunk_size: u32, + context: String, + context_wait: u32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + address: DEFAULT_ADDRESS.map(Into::into), + port: DEFAULT_PORT, + caps: DEFAULT_CAPS, + chunk_size: DEFAULT_CHUNK_SIZE, + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + } + } +} + +static PROPERTIES: [subclass::Property; 6] = [ + subclass::Property("address", |name| { + glib::ParamSpec::string( + name, + "Address", + "Address to receive packets from", + DEFAULT_ADDRESS, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("port", |name| { + glib::ParamSpec::uint( + name, + "Port", + "Port to receive packets from", + 0, + u16::MAX as u32, + DEFAULT_PORT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("caps", |name| { + glib::ParamSpec::boxed( + name, + "Caps", + "Caps to use", + gst::Caps::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("chunk-size", |name| { + glib::ParamSpec::uint( + name, + "Chunk Size", + "Chunk Size", + 0, + u16::MAX as u32, + DEFAULT_CHUNK_SIZE, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), +]; + +struct TcpClientReaderInner { + socket: tokio::net::TcpStream, +} + +pub struct TcpClientReader(Arc<FutMutex<TcpClientReaderInner>>); + +impl TcpClientReader { + pub fn new(socket: tokio::net::TcpStream) -> Self { + TcpClientReader(Arc::new(FutMutex::new(TcpClientReaderInner { socket }))) + } +} + +impl SocketRead for TcpClientReader { + const DO_TIMESTAMP: bool = false; + + fn read<'buf>( + &self, + buffer: &'buf mut [u8], + ) -> BoxFuture<'buf, io::Result<(usize, Option<std::net::SocketAddr>)>> { + let this = Arc::clone(&self.0); + + async move { + this.lock() + .await + .socket + .read(buffer) + .await + .map(|read_size| (read_size, None)) + } + .boxed() + } +} + +#[derive(Debug)] +struct TcpClientSrcPadHandlerState { + need_initial_events: bool, + need_segment: bool, + caps: Option<gst::Caps>, +} + +impl Default for TcpClientSrcPadHandlerState { + fn default() -> Self { + TcpClientSrcPadHandlerState { + need_initial_events: true, + need_segment: true, + caps: None, + } + } +} + +#[derive(Debug, Default)] +struct TcpClientSrcPadHandlerInner { + state: FutMutex<TcpClientSrcPadHandlerState>, + configured_caps: StdMutex<Option<gst::Caps>>, +} + +#[derive(Clone, Debug, Default)] +struct TcpClientSrcPadHandler(Arc<TcpClientSrcPadHandlerInner>); + +impl TcpClientSrcPadHandler { + fn prepare(&self, caps: Option<gst::Caps>) { + self.0 + .state + .try_lock() + .expect("State locked elsewhere") + .caps = caps; + } + + fn reset(&self) { + *self.0.state.try_lock().expect("State locked elsewhere") = Default::default(); + *self.0.configured_caps.lock().unwrap() = None; + } + + fn set_need_segment(&self) { + self.0 + .state + .try_lock() + .expect("State locked elsewhere") + .need_segment = true; + } + + async fn push_prelude(&self, pad: &PadSrcRef<'_>, _element: &gst::Element) { + let mut state = self.0.state.lock().await; + if state.need_initial_events { + gst_debug!(CAT, obj: pad.gst_pad(), "Pushing initial events"); + + let stream_id = format!("{:08x}{:08x}", rand::random::<u32>(), rand::random::<u32>()); + let stream_start_evt = gst::Event::new_stream_start(&stream_id) + .group_id(gst::GroupId::next()) + .build(); + pad.push_event(stream_start_evt).await; + + if let Some(ref caps) = state.caps { + let caps_evt = gst::Event::new_caps(&caps).build(); + pad.push_event(caps_evt).await; + *self.0.configured_caps.lock().unwrap() = Some(caps.clone()); + } + + state.need_initial_events = false; + } + + if state.need_segment { + let segment_evt = + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(); + pad.push_event(segment_evt).await; + + state.need_segment = false; + } + } + + async fn push_buffer( + &self, + pad: &PadSrcRef<'_>, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", buffer); + + self.push_prelude(pad, element).await; + + if buffer.get_size() == 0 { + let event = gst::Event::new_eos().build(); + pad.push_event(event).await; + return Ok(gst::FlowSuccess::Ok); + } + + pad.push(buffer).await + } +} + +impl PadSrcHandler for TcpClientSrcPadHandler { + type ElementImpl = TcpClientSrc; + + fn src_event( + &self, + pad: &PadSrcRef, + tcpclientsrc: &TcpClientSrc, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + let ret = match event.view() { + EventView::FlushStart(..) => { + tcpclientsrc.flush_start(element); + + true + } + EventView::FlushStop(..) => { + tcpclientsrc.flush_stop(element); + + true + } + EventView::Reconfigure(..) => true, + EventView::Latency(..) => true, + _ => false, + }; + + if ret { + gst_log!(CAT, obj: pad.gst_pad(), "Handled {:?}", event); + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Didn't handle {:?}", event); + } + + ret + } + + fn src_query( + &self, + pad: &PadSrcRef, + _tcpclientsrc: &TcpClientSrc, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", query); + let ret = match query.view_mut() { + QueryView::Latency(ref mut q) => { + q.set(false, 0.into(), gst::CLOCK_TIME_NONE); + true + } + QueryView::Scheduling(ref mut q) => { + q.set(gst::SchedulingFlags::SEQUENTIAL, 1, -1, 0); + q.add_scheduling_modes(&[gst::PadMode::Push]); + true + } + QueryView::Caps(ref mut q) => { + let caps = if let Some(caps) = self.0.configured_caps.lock().unwrap().as_ref() { + q.get_filter() + .map(|f| f.intersect_with_mode(caps, gst::CapsIntersectMode::First)) + .unwrap_or_else(|| caps.clone()) + } else { + q.get_filter() + .map(|f| f.to_owned()) + .unwrap_or_else(gst::Caps::new_any) + }; + + q.set_result(&caps); + + true + } + _ => false, + }; + + if ret { + gst_log!(CAT, obj: pad.gst_pad(), "Handled {:?}", query); + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Didn't handle {:?}", query); + } + + ret + } +} + +struct TcpClientSrc { + src_pad: PadSrc, + src_pad_handler: TcpClientSrcPadHandler, + task: Task, + socket: StdMutex<Option<Socket<TcpClientReader>>>, + settings: StdMutex<Settings>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-tcpclientsrc", + gst::DebugColorFlags::empty(), + Some("Thread-sharing TCP Client source"), + ); +} + +impl TcpClientSrc { + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + let settings = self.settings.lock().unwrap().clone(); + + gst_debug!(CAT, obj: element, "Preparing"); + + let context = + Context::acquire(&settings.context, settings.context_wait).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to acquire Context: {}", err] + ) + })?; + + let addr: IpAddr = match settings.address { + None => { + return Err(gst_error_msg!( + gst::ResourceError::Settings, + ["No address set"] + )); + } + Some(ref addr) => match addr.parse() { + Err(err) => { + return Err(gst_error_msg!( + gst::ResourceError::Settings, + ["Invalid address '{}' set: {}", addr, err] + )); + } + Ok(addr) => addr, + }, + }; + let port = settings.port; + + let buffer_pool = gst::BufferPool::new(); + let mut config = buffer_pool.get_config(); + config.set_params(None, settings.chunk_size, 0, 0); + buffer_pool.set_config(config).map_err(|_| { + gst_error_msg!( + gst::ResourceError::Settings, + ["Failed to configure buffer pool"] + ) + })?; + + let saddr = SocketAddr::new(addr, port as u16); + let element_clone = element.clone(); + let socket = Socket::new(element.upcast_ref(), buffer_pool, async move { + gst_debug!(CAT, obj: &element_clone, "Connecting to {:?}", saddr); + let socket = tokio::net::TcpStream::connect(saddr) + .await + .map_err(SocketError::Io)?; + Ok(TcpClientReader::new(socket)) + }) + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to prepare socket {:?}", err] + ) + })?; + + *self.socket.lock().unwrap() = Some(socket); + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + self.src_pad_handler.prepare(settings.caps); + self.src_pad.prepare(&self.src_pad_handler); + + gst_debug!(CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Unpreparing"); + + *self.socket.lock().unwrap() = None; + + self.task.unprepare().unwrap(); + self.src_pad.unprepare(); + + gst_debug!(CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Stopping"); + + self.task.stop(); + self.src_pad_handler.reset(); + + gst_debug!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let socket = self.socket.lock().unwrap(); + let socket = socket.as_ref().unwrap(); + if socket.state() == SocketState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return Ok(()); + } + + gst_debug!(CAT, obj: element, "Starting"); + self.start_task(element, socket); + gst_debug!(CAT, obj: element, "Started"); + + Ok(()) + } + + fn start_task(&self, element: &gst::Element, socket: &Socket<TcpClientReader>) { + let socket_stream = socket + .start(element.get_clock(), Some(element.get_base_time())) + .unwrap(); + let socket_stream = Arc::new(FutMutex::new(socket_stream)); + + let src_pad_handler = self.src_pad_handler.clone(); + let pad_weak = self.src_pad.downgrade(); + let element = element.clone(); + + self.task.start(move || { + let src_pad_handler = src_pad_handler.clone(); + let pad_weak = pad_weak.clone(); + let element = element.clone(); + let socket_stream = socket_stream.clone(); + + async move { + let item = socket_stream.lock().await.next().await; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + let buffer = match item { + Some(Ok((buffer, _))) => buffer, + Some(Err(err)) => { + gst_error!(CAT, obj: &element, "Got error {:?}", err); + match err { + SocketError::Gst(err) => { + gst_element_error!( + element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + } + SocketError::Io(err) => { + gst_element_error!( + element, + gst::StreamError::Failed, + ("I/O error"), + ["streaming stopped, I/O error {}", err] + ); + } + } + return glib::Continue(false); + } + None => { + gst_log!(CAT, obj: pad.gst_pad(), "SocketStream Stopped"); + return glib::Continue(false); + } + }; + + match src_pad_handler.push_buffer(&pad, &element, buffer).await { + Ok(_) => { + gst_log!(CAT, obj: pad.gst_pad(), "Successfully pushed buffer"); + glib::Continue(true) + } + Err(gst::FlowError::Flushing) => { + gst_debug!(CAT, obj: pad.gst_pad(), "Flushing"); + glib::Continue(false) + } + Err(gst::FlowError::Eos) => { + gst_debug!(CAT, obj: pad.gst_pad(), "EOS"); + let eos = gst::Event::new_eos().build(); + pad.push_event(eos).await; + glib::Continue(false) + } + Err(err) => { + gst_error!(CAT, obj: pad.gst_pad(), "Got error {}", err); + gst_element_error!( + element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + glib::Continue(false) + } + } + } + }); + } + + fn flush_stop(&self, element: &gst::Element) { + let socket = self.socket.lock().unwrap(); + if let Some(socket) = socket.as_ref() { + if socket.state() == SocketState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return; + } + + gst_debug!(CAT, obj: element, "Stopping Flush"); + + self.src_pad_handler.set_need_segment(); + self.start_task(element, socket); + + gst_debug!(CAT, obj: element, "Stopped Flush"); + } else { + gst_debug!(CAT, obj: element, "Socket not available"); + } + } + + fn flush_start(&self, element: &gst::Element) { + let socket = self.socket.lock().unwrap(); + gst_debug!(CAT, obj: element, "Starting Flush"); + + if let Some(socket) = socket.as_ref() { + socket.pause(); + } + + self.task.cancel(); + + gst_debug!(CAT, obj: element, "Flush Started"); + } + + fn pause(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Pausing"); + + self.socket.lock().unwrap().as_ref().unwrap().pause(); + self.task.pause(); + + gst_debug!(CAT, obj: element, "Paused"); + + Ok(()) + } +} + +impl ObjectSubclass for TcpClientSrc { + const NAME: &'static str = "RsTsTcpClientSrc"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing TCP client source", + "Source/Network", + "Receives data over the network via TCP", + "Sebastian Dröge <sebastian@centricular.com>, LEE Dongjun <redongjun@gmail.com>", + ); + + let caps = gst::Caps::new_any(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&PROPERTIES); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + let templ = klass.get_pad_template("src").unwrap(); + + Self { + src_pad: PadSrc::new(gst::Pad::new_from_template(&templ, Some("src"))), + src_pad_handler: TcpClientSrcPadHandler::default(), + task: Task::default(), + socket: StdMutex::new(None), + settings: StdMutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for TcpClientSrc { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("address", ..) => { + settings.address = value.get().expect("type checked upstream"); + } + subclass::Property("port", ..) => { + settings.port = value.get_some().expect("type checked upstream"); + } + subclass::Property("caps", ..) => { + settings.caps = value.get().expect("type checked upstream"); + } + subclass::Property("chunk-size", ..) => { + settings.chunk_size = value.get_some().expect("type checked upstream"); + } + subclass::Property("context", ..) => { + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + settings.context_wait = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("address", ..) => Ok(settings.address.to_value()), + subclass::Property("port", ..) => Ok(settings.port.to_value()), + subclass::Property("caps", ..) => Ok(settings.caps.to_value()), + subclass::Property("chunk-size", ..) => Ok(settings.chunk_size.to_value()), + subclass::Property("context", ..) => Ok(settings.context.to_value()), + subclass::Property("context-wait", ..) => Ok(settings.context_wait.to_value()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + + super::set_element_flags(element, gst::ElementFlags::SOURCE); + } +} + +impl ElementImpl for TcpClientSrc { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PlayingToPaused => { + self.pause(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::ReadyToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToPlaying => { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-tcpclientsrc", + gst::Rank::None, + TcpClientSrc::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/src/udpsink.rs b/generic/gst-plugin-threadshare/src/udpsink.rs new file mode 100644 index 000000000..3c95f25c7 --- /dev/null +++ b/generic/gst-plugin-threadshare/src/udpsink.rs @@ -0,0 +1,1521 @@ +// Copyright (C) 2019 Mathieu Duponchelle <mathieu@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::channel::mpsc; +use futures::future::BoxFuture; +use futures::lock::Mutex; +use futures::prelude::*; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::EventView; +use gst::{ + gst_debug, gst_element_error, gst_error, gst_error_msg, gst_info, gst_log, gst_trace, + gst_warning, +}; + +use lazy_static::lazy_static; + +use crate::runtime::prelude::*; +use crate::runtime::{self, Context, PadSink, PadSinkRef, Task}; +use crate::socket::{wrap_socket, GioSocketWrapper}; + +use std::convert::TryInto; +use std::mem; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::string::ToString; +use std::sync::Mutex as StdMutex; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use std::u16; +use std::u8; + +const DEFAULT_HOST: Option<&str> = Some("127.0.0.1"); +const DEFAULT_PORT: u32 = 5000; +const DEFAULT_SYNC: bool = true; +const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0"; +const DEFAULT_BIND_PORT: u32 = 0; +const DEFAULT_BIND_ADDRESS_V6: &str = "::"; +const DEFAULT_BIND_PORT_V6: u32 = 0; +const DEFAULT_SOCKET: Option<GioSocketWrapper> = None; +const DEFAULT_USED_SOCKET: Option<GioSocketWrapper> = None; +const DEFAULT_SOCKET_V6: Option<GioSocketWrapper> = None; +const DEFAULT_USED_SOCKET_V6: Option<GioSocketWrapper> = None; +const DEFAULT_AUTO_MULTICAST: bool = true; +const DEFAULT_LOOP: bool = true; +const DEFAULT_TTL: u32 = 64; +const DEFAULT_TTL_MC: u32 = 1; +const DEFAULT_QOS_DSCP: i32 = -1; +const DEFAULT_CLIENTS: &str = ""; +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; + +#[derive(Debug, Clone)] +struct Settings { + host: Option<String>, + port: u32, + sync: bool, + bind_address: String, + bind_port: u32, + bind_address_v6: String, + bind_port_v6: u32, + socket: Option<GioSocketWrapper>, + used_socket: Option<GioSocketWrapper>, + socket_v6: Option<GioSocketWrapper>, + used_socket_v6: Option<GioSocketWrapper>, + auto_multicast: bool, + multicast_loop: bool, + ttl: u32, + ttl_mc: u32, + qos_dscp: i32, + context: String, + context_wait: u32, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + host: DEFAULT_HOST.map(Into::into), + port: DEFAULT_PORT, + sync: DEFAULT_SYNC, + bind_address: DEFAULT_BIND_ADDRESS.into(), + bind_port: DEFAULT_BIND_PORT, + bind_address_v6: DEFAULT_BIND_ADDRESS_V6.into(), + bind_port_v6: DEFAULT_BIND_PORT_V6, + socket: DEFAULT_SOCKET, + used_socket: DEFAULT_USED_SOCKET, + socket_v6: DEFAULT_SOCKET_V6, + used_socket_v6: DEFAULT_USED_SOCKET_V6, + auto_multicast: DEFAULT_AUTO_MULTICAST, + multicast_loop: DEFAULT_LOOP, + ttl: DEFAULT_TTL, + ttl_mc: DEFAULT_TTL_MC, + qos_dscp: DEFAULT_QOS_DSCP, + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + } + } +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-udpsink", + gst::DebugColorFlags::empty(), + Some("Thread-sharing UDP sink"), + ); +} + +static PROPERTIES: [subclass::Property; 19] = [ + subclass::Property("host", |name| { + glib::ParamSpec::string( + name, + "Host", + "The host/IP/Multicast group to send the packets to", + DEFAULT_HOST, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("port", |name| { + glib::ParamSpec::uint( + name, + "Port", + "The port to send the packets to", + 0, + u16::MAX as u32, + DEFAULT_PORT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("sync", |name| { + glib::ParamSpec::boolean( + name, + "Sync", + "Sync on the clock", + DEFAULT_SYNC, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("bind-address", |name| { + glib::ParamSpec::string( + name, + "Bind Address", + "Address to bind the socket to", + Some(DEFAULT_BIND_ADDRESS), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("bind-port", |name| { + glib::ParamSpec::uint( + name, + "Bind Port", + "Port to bind the socket to", + 0, + u16::MAX as u32, + DEFAULT_BIND_PORT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("bind-address-v6", |name| { + glib::ParamSpec::string( + name, + "Bind Address V6", + "Address to bind the V6 socket to", + Some(DEFAULT_BIND_ADDRESS_V6), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("bind-port-v6", |name| { + glib::ParamSpec::uint( + name, + "Bind Port", + "Port to bind the V6 socket to", + 0, + u16::MAX as u32, + DEFAULT_BIND_PORT_V6, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("socket", |name| { + glib::ParamSpec::object( + name, + "Socket", + "Socket to use for UDP transmission. (None == allocate)", + gio::Socket::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("used-socket", |name| { + glib::ParamSpec::object( + name, + "Used Socket", + "Socket currently in use for UDP transmission. (None = no socket)", + gio::Socket::static_type(), + glib::ParamFlags::READABLE, + ) + }), + subclass::Property("socket-v6", |name| { + glib::ParamSpec::object( + name, + "Socket V6", + "IPV6 Socket to use for UDP transmission. (None == allocate)", + gio::Socket::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("used-socket-v6", |name| { + glib::ParamSpec::object( + name, + "Used Socket V6", + "V6 Socket currently in use for UDP transmission. (None = no socket)", + gio::Socket::static_type(), + glib::ParamFlags::READABLE, + ) + }), + subclass::Property("auto-multicast", |name| { + glib::ParamSpec::boolean( + name, + "Auto multicast", + "Automatically join/leave the multicast groups, FALSE means user has to do it himself", + DEFAULT_AUTO_MULTICAST, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("loop", |name| { + glib::ParamSpec::boolean( + name, + "Loop", + "Set the multicast loop parameter.", + DEFAULT_LOOP, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("ttl", |name| { + glib::ParamSpec::uint( + name, + "Time To Live", + "Used for setting the unicast TTL parameter", + 0, + u8::MAX as u32, + DEFAULT_TTL, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("ttl-mc", |name| { + glib::ParamSpec::uint( + name, + "Time To Live Multicast", + "Used for setting the multicast TTL parameter", + 0, + u8::MAX as u32, + DEFAULT_TTL_MC, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("qos-dscp", |name| { + glib::ParamSpec::int( + name, + "QoS DSCP", + "Quality of Service, differentiated services code point (-1 default)", + -1, + 63, + DEFAULT_QOS_DSCP, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("clients", |name| { + glib::ParamSpec::string( + name, + "Clients", + "A comma separated list of host:port pairs with destinations", + Some(DEFAULT_CLIENTS), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Debug)] +enum TaskItem { + Buffer(gst::Buffer), + Event(gst::Event), +} + +#[derive(Debug)] +struct UdpSinkPadHandlerInner { + sync: bool, + segment: Option<gst::Segment>, + latency: gst::ClockTime, + socket: Arc<Mutex<Option<tokio::net::UdpSocket>>>, + socket_v6: Arc<Mutex<Option<tokio::net::UdpSocket>>>, + clients: Arc<Vec<SocketAddr>>, + clients_to_configure: Vec<SocketAddr>, + clients_to_unconfigure: Vec<SocketAddr>, + sender: Arc<Mutex<Option<mpsc::Sender<TaskItem>>>>, + settings: Arc<StdMutex<Settings>>, +} + +impl UdpSinkPadHandlerInner { + fn new(settings: Arc<StdMutex<Settings>>) -> Self { + UdpSinkPadHandlerInner { + sync: DEFAULT_SYNC, + segment: None, + latency: gst::CLOCK_TIME_NONE, + socket: Arc::new(Mutex::new(None)), + socket_v6: Arc::new(Mutex::new(None)), + clients: Arc::new(vec![SocketAddr::new( + DEFAULT_HOST.unwrap().parse().unwrap(), + DEFAULT_PORT as u16, + )]), + clients_to_configure: vec![], + clients_to_unconfigure: vec![], + sender: Arc::new(Mutex::new(None)), + settings, + } + } + + fn clear_clients( + &mut self, + gst_pad: &gst::Pad, + clients_to_add: impl Iterator<Item = SocketAddr>, + ) { + let old_clients = mem::replace(&mut *Arc::make_mut(&mut self.clients), vec![]); + + self.clients_to_configure = vec![]; + self.clients_to_unconfigure = vec![]; + + for addr in clients_to_add { + if !old_clients.contains(&addr) { + self.clients_to_unconfigure.push(addr); + } + self.add_client(gst_pad, addr); + } + } + + fn remove_client(&mut self, gst_pad: &gst::Pad, addr: SocketAddr) { + if !self.clients.contains(&addr) { + gst_warning!(CAT, obj: gst_pad, "Not removing unknown client {:?}", &addr); + return; + } + + gst_info!(CAT, obj: gst_pad, "Removing client {:?}", addr); + + Arc::make_mut(&mut self.clients).retain(|addr2| addr != *addr2); + + self.clients_to_unconfigure.push(addr); + self.clients_to_configure.retain(|addr2| addr != *addr2); + } + + fn replace_client( + &mut self, + gst_pad: &gst::Pad, + addr: Option<SocketAddr>, + new_addr: Option<SocketAddr>, + ) { + if let Some(addr) = addr { + self.remove_client(gst_pad, addr); + } + + if let Some(new_addr) = new_addr { + self.add_client(gst_pad, new_addr); + } + } + + fn add_client(&mut self, gst_pad: &gst::Pad, addr: SocketAddr) { + if self.clients.contains(&addr) { + gst_warning!(CAT, obj: gst_pad, "Not adding client {:?} again", &addr); + return; + } + + gst_info!(CAT, obj: gst_pad, "Adding client {:?}", addr); + + Arc::make_mut(&mut self.clients).push(addr); + + self.clients_to_configure.push(addr); + self.clients_to_unconfigure.retain(|addr2| addr != *addr2); + } +} + +#[derive(Debug)] +enum SocketQualified { + Ipv4(tokio::net::UdpSocket), + Ipv6(tokio::net::UdpSocket), +} + +#[derive(Clone, Debug)] +struct UdpSinkPadHandler(Arc<RwLock<UdpSinkPadHandlerInner>>); + +impl UdpSinkPadHandler { + fn new(settings: Arc<StdMutex<Settings>>) -> UdpSinkPadHandler { + Self(Arc::new(RwLock::new(UdpSinkPadHandlerInner::new(settings)))) + } + + fn set_latency(&self, latency: gst::ClockTime) { + self.0.write().unwrap().latency = latency; + } + + fn prepare(&self) { + let mut inner = self.0.write().unwrap(); + inner.clients_to_configure = inner.clients.to_vec(); + } + + fn prepare_socket(&self, socket: SocketQualified) { + let mut inner = self.0.write().unwrap(); + + match socket { + SocketQualified::Ipv4(socket) => inner.socket = Arc::new(Mutex::new(Some(socket))), + SocketQualified::Ipv6(socket) => inner.socket_v6 = Arc::new(Mutex::new(Some(socket))), + } + } + + fn unprepare(&self) { + let mut inner = self.0.write().unwrap(); + *inner = UdpSinkPadHandlerInner::new(Arc::clone(&inner.settings)) + } + + fn clear_clients(&self, gst_pad: &gst::Pad, clients_to_add: impl Iterator<Item = SocketAddr>) { + self.0 + .write() + .unwrap() + .clear_clients(gst_pad, clients_to_add); + } + + fn remove_client(&self, gst_pad: &gst::Pad, addr: SocketAddr) { + self.0.write().unwrap().remove_client(gst_pad, addr); + } + + fn replace_client( + &self, + gst_pad: &gst::Pad, + addr: Option<SocketAddr>, + new_addr: Option<SocketAddr>, + ) { + self.0 + .write() + .unwrap() + .replace_client(gst_pad, addr, new_addr); + } + + fn add_client(&self, gst_pad: &gst::Pad, addr: SocketAddr) { + self.0.write().unwrap().add_client(gst_pad, addr); + } + + fn get_clients(&self) -> Vec<SocketAddr> { + (*self.0.read().unwrap().clients).clone() + } + + fn configure_client( + &self, + settings: &Settings, + socket: &mut Option<tokio::net::UdpSocket>, + socket_v6: &mut Option<tokio::net::UdpSocket>, + client: &SocketAddr, + ) -> Result<(), gst::ErrorMessage> { + if client.ip().is_multicast() { + match client.ip() { + IpAddr::V4(addr) => { + if let Some(socket) = socket.as_mut() { + if settings.auto_multicast { + socket + .join_multicast_v4(addr, Ipv4Addr::new(0, 0, 0, 0)) + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to join multicast group: {}", err] + ) + })?; + } + if settings.multicast_loop { + socket.set_multicast_loop_v4(true).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to set multicast loop: {}", err] + ) + })?; + } + socket + .set_multicast_ttl_v4(settings.ttl_mc) + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to set multicast ttl: {}", err] + ) + })?; + } + } + IpAddr::V6(addr) => { + if let Some(socket) = socket_v6.as_mut() { + if settings.auto_multicast { + socket.join_multicast_v6(&addr, 0).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to join multicast group: {}", err] + ) + })?; + } + if settings.multicast_loop { + socket.set_multicast_loop_v6(true).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to set multicast loop: {}", err] + ) + })?; + } + /* FIXME no API for set_multicast_ttl_v6 ? */ + } + } + } + } else { + match client.ip() { + IpAddr::V4(_) => { + if let Some(socket) = socket.as_mut() { + socket.set_ttl(settings.ttl).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to set unicast ttl: {}", err] + ) + })?; + } + } + IpAddr::V6(_) => { + if let Some(socket) = socket_v6.as_mut() { + socket.set_ttl(settings.ttl).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to set unicast ttl: {}", err] + ) + })?; + } + } + } + } + + Ok(()) + } + + fn unconfigure_client( + &self, + settings: &Settings, + socket: &mut Option<tokio::net::UdpSocket>, + socket_v6: &mut Option<tokio::net::UdpSocket>, + client: &SocketAddr, + ) -> Result<(), gst::ErrorMessage> { + if client.ip().is_multicast() { + match client.ip() { + IpAddr::V4(addr) => { + if let Some(socket) = socket.as_mut() { + if settings.auto_multicast { + socket + .leave_multicast_v4(addr, Ipv4Addr::new(0, 0, 0, 0)) + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to join multicast group: {}", err] + ) + })?; + } + } + } + IpAddr::V6(addr) => { + if let Some(socket) = socket_v6.as_mut() { + if settings.auto_multicast { + socket.leave_multicast_v6(&addr, 0).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to join multicast group: {}", err] + ) + })?; + } + } + } + } + } + + Ok(()) + } + + async fn render( + &self, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + let ( + do_sync, + rtime, + clients, + clients_to_configure, + clients_to_unconfigure, + socket, + socket_v6, + settings, + ) = { + let mut inner = self.0.write().unwrap(); + let do_sync = inner.sync; + let mut rtime: gst::ClockTime = 0.into(); + + if let Some(segment) = &inner.segment { + if let Some(segment) = segment.downcast_ref::<gst::format::Time>() { + rtime = segment.to_running_time(buffer.get_pts()); + if inner.latency.is_some() { + rtime += inner.latency; + } + } + } + + let clients_to_configure = mem::replace(&mut inner.clients_to_configure, vec![]); + let clients_to_unconfigure = mem::replace(&mut inner.clients_to_unconfigure, vec![]); + + let settings = inner.settings.lock().unwrap().clone(); + + ( + do_sync, + rtime, + Arc::clone(&inner.clients), + clients_to_configure, + clients_to_unconfigure, + Arc::clone(&inner.socket), + Arc::clone(&inner.socket_v6), + settings, + ) + }; + + let mut socket = socket.lock().await; + let mut socket_v6 = socket_v6.lock().await; + + if !clients_to_configure.is_empty() { + for client in &clients_to_configure { + self.configure_client(&settings, &mut socket, &mut socket_v6, &client) + .map_err(|err| { + gst_element_error!( + element, + gst::StreamError::Failed, + ["Failed to configure client {:?}: {}", client, err] + ); + + gst::FlowError::Error + })?; + } + } + + if !clients_to_unconfigure.is_empty() { + for client in &clients_to_unconfigure { + self.unconfigure_client(&settings, &mut socket, &mut socket_v6, &client) + .map_err(|err| { + gst_element_error!( + element, + gst::StreamError::Failed, + ["Failed to unconfigure client {:?}: {}", client, err] + ); + + gst::FlowError::Error + })?; + } + } + + if do_sync { + self.sync(&element, rtime).await; + } + + let data = buffer.map_readable().map_err(|_| { + gst_element_error!( + element, + gst::StreamError::Format, + ["Failed to map buffer readable"] + ); + + gst::FlowError::Error + })?; + + for client in clients.iter() { + let socket = match client.ip() { + IpAddr::V4(_) => &mut socket, + IpAddr::V6(_) => &mut socket_v6, + }; + + if let Some(socket) = socket.as_mut() { + gst_log!(CAT, obj: element, "Sending to {:?}", &client); + socket.send_to(&data, client).await.map_err(|err| { + gst_element_error!( + element, + gst::StreamError::Failed, + ("I/O error"), + ["streaming stopped, I/O error {}", err] + ); + gst::FlowError::Error + })?; + } else { + gst_element_error!( + element, + gst::StreamError::Failed, + ("I/O error"), + ["No socket available for sending to {}", client] + ); + return Err(gst::FlowError::Error); + } + } + + gst_log!( + CAT, + obj: element, + "Sent buffer {:?} to all clients", + &buffer + ); + + Ok(gst::FlowSuccess::Ok) + } + + /* Wait until specified time */ + async fn sync(&self, element: &gst::Element, running_time: gst::ClockTime) { + let now = super::get_current_running_time(&element); + + if now < running_time { + let delay = running_time - now; + runtime::time::delay_for(Duration::from_nanos(delay.nseconds().unwrap())).await; + } + } + + async fn handle_event(&self, element: &gst::Element, event: gst::Event) { + match event.view() { + EventView::Eos(_) => { + let _ = element.post_message(&gst::Message::new_eos().src(Some(element)).build()); + } + EventView::Segment(e) => { + self.0.write().unwrap().segment = Some(e.get_segment().clone()); + } + _ => (), + } + } +} + +impl PadSinkHandler for UdpSinkPadHandler { + type ElementImpl = UdpSink; + + fn sink_chain( + &self, + _pad: &PadSinkRef, + _udpsink: &UdpSink, + _element: &gst::Element, + buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let sender = Arc::clone(&self.0.read().unwrap().sender); + + async move { + if let Some(sender) = sender.lock().await.as_mut() { + sender.send(TaskItem::Buffer(buffer)).await.unwrap(); + } + Ok(gst::FlowSuccess::Ok) + } + .boxed() + } + + fn sink_chain_list( + &self, + _pad: &PadSinkRef, + _udpsink: &UdpSink, + _element: &gst::Element, + list: gst::BufferList, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let sender = Arc::clone(&self.0.read().unwrap().sender); + + async move { + if let Some(sender) = sender.lock().await.as_mut() { + for buffer in list.iter_owned() { + sender.send(TaskItem::Buffer(buffer)).await.unwrap(); + } + } + + Ok(gst::FlowSuccess::Ok) + } + .boxed() + } + + fn sink_event_serialized( + &self, + _pad: &PadSinkRef, + _udpsink: &UdpSink, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + let sender = Arc::clone(&self.0.read().unwrap().sender); + let element = element.clone(); + + async move { + if let EventView::FlushStop(_) = event.view() { + let udpsink = UdpSink::from_instance(&element); + let _ = udpsink.start(&element); + } else if let Some(sender) = sender.lock().await.as_mut() { + sender.send(TaskItem::Event(event)).await.unwrap(); + } + + true + } + .boxed() + } + + fn sink_event( + &self, + _pad: &PadSinkRef, + udpsink: &UdpSink, + element: &gst::Element, + event: gst::Event, + ) -> bool { + if let EventView::FlushStart(..) = event.view() { + let _ = udpsink.stop(&element); + } + + true + } +} + +#[derive(Debug)] +enum SocketFamily { + Ipv4, + Ipv6, +} + +#[derive(Debug)] +struct UdpSink { + sink_pad: PadSink, + sink_pad_handler: UdpSinkPadHandler, + task: Task, + settings: Arc<StdMutex<Settings>>, +} + +impl UdpSink { + fn prepare_socket( + &self, + family: SocketFamily, + context: &Context, + element: &gst::Element, + ) -> Result<(), gst::ErrorMessage> { + let mut settings = self.settings.lock().unwrap(); + + let wrapped_socket = match family { + SocketFamily::Ipv4 => &settings.socket, + SocketFamily::Ipv6 => &settings.socket_v6, + }; + + let socket_qualified: SocketQualified; + + if let Some(ref wrapped_socket) = wrapped_socket { + use std::net::UdpSocket; + + let socket: UdpSocket; + + #[cfg(unix)] + { + socket = wrapped_socket.get() + } + #[cfg(windows)] + { + socket = wrapped_socket.get() + } + + let socket = context.enter(|| { + tokio::net::UdpSocket::from_std(socket).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to setup socket for tokio: {}", err] + ) + }) + })?; + + match family { + SocketFamily::Ipv4 => { + settings.used_socket = Some(wrapped_socket.clone()); + socket_qualified = SocketQualified::Ipv4(socket); + } + SocketFamily::Ipv6 => { + settings.used_socket_v6 = Some(wrapped_socket.clone()); + socket_qualified = SocketQualified::Ipv6(socket); + } + } + } else { + let bind_addr = match family { + SocketFamily::Ipv4 => &settings.bind_address, + SocketFamily::Ipv6 => &settings.bind_address_v6, + }; + + let bind_addr: IpAddr = bind_addr.parse().map_err(|err| { + gst_error_msg!( + gst::ResourceError::Settings, + ["Invalid address '{}' set: {}", bind_addr, err] + ) + })?; + + let bind_port = match family { + SocketFamily::Ipv4 => settings.bind_port, + SocketFamily::Ipv6 => settings.bind_port_v6, + }; + + let saddr = SocketAddr::new(bind_addr, bind_port as u16); + gst_debug!(CAT, obj: element, "Binding to {:?}", saddr); + + let builder = match family { + SocketFamily::Ipv4 => net2::UdpBuilder::new_v4(), + SocketFamily::Ipv6 => net2::UdpBuilder::new_v6(), + }; + + let builder = match builder { + Ok(builder) => builder, + Err(err) => { + gst_warning!( + CAT, + obj: element, + "Failed to create {} socket builder: {}", + match family { + SocketFamily::Ipv4 => "IPv4", + SocketFamily::Ipv6 => "IPv6", + }, + err + ); + return Ok(()); + } + }; + + let socket = builder.bind(&saddr).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to bind socket: {}", err] + ) + })?; + + let socket = context.enter(|| { + tokio::net::UdpSocket::from_std(socket).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to setup socket for tokio: {}", err] + ) + }) + })?; + + let wrapper = wrap_socket(&socket)?; + + if settings.qos_dscp != -1 { + wrapper.set_tos(settings.qos_dscp).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to set QoS DSCP: {}", err] + ) + })?; + } + + match family { + SocketFamily::Ipv4 => { + settings.used_socket = Some(wrapper); + socket_qualified = SocketQualified::Ipv4(socket) + } + SocketFamily::Ipv6 => { + settings.used_socket_v6 = Some(wrapper); + socket_qualified = SocketQualified::Ipv6(socket) + } + } + } + + self.sink_pad_handler.prepare_socket(socket_qualified); + + Ok(()) + } + + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_debug!(CAT, obj: element, "Preparing"); + + let context = { + let settings = self.settings.lock().unwrap(); + + Context::acquire(&settings.context, settings.context_wait).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenWrite, + ["Failed to acquire Context: {}", err] + ) + })? + }; + + self.sink_pad_handler.prepare(); + self.prepare_socket(SocketFamily::Ipv4, &context, element)?; + self.prepare_socket(SocketFamily::Ipv6, &context, element)?; + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + + self.sink_pad.prepare(&self.sink_pad_handler); + + gst_debug!(CAT, obj: element, "Started preparing"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Unpreparing"); + + self.task.unprepare().unwrap(); + self.sink_pad_handler.unprepare(); + self.sink_pad.unprepare(); + + gst_debug!(CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Stopping"); + self.task.stop(); + gst_debug!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Starting"); + + let sink_pad_handler = self.sink_pad_handler.clone(); + let element_clone = element.clone(); + + let (sender, receiver) = mpsc::channel(0); + let receiver = Arc::new(Mutex::new(receiver)); + + sink_pad_handler.0.write().unwrap().sender = Arc::new(Mutex::new(Some(sender))); + + self.task.start(move || { + let receiver = Arc::clone(&receiver); + let element = element_clone.clone(); + let sink_pad_handler = sink_pad_handler.clone(); + + async move { + match receiver.lock().await.next().await { + Some(TaskItem::Buffer(buffer)) => { + match sink_pad_handler.render(&element, buffer).await { + Err(err) => { + gst_element_error!( + element, + gst::StreamError::Failed, + ["Failed to render item, stopping task: {}", err] + ); + + glib::Continue(false) + } + _ => glib::Continue(true), + } + } + Some(TaskItem::Event(event)) => { + sink_pad_handler.handle_event(&element, event).await; + glib::Continue(true) + } + None => glib::Continue(false), + } + } + }); + + Ok(()) + } +} + +impl UdpSink { + fn clear_clients(&self, clients_to_add: impl Iterator<Item = SocketAddr>) { + self.sink_pad_handler + .clear_clients(&self.sink_pad.gst_pad(), clients_to_add); + } + + fn remove_client(&self, addr: SocketAddr) { + self.sink_pad_handler + .remove_client(&self.sink_pad.gst_pad(), addr); + } + + fn replace_client(&self, addr: Option<SocketAddr>, new_addr: Option<SocketAddr>) { + self.sink_pad_handler + .replace_client(&self.sink_pad.gst_pad(), addr, new_addr); + } + + fn add_client(&self, addr: SocketAddr) { + self.sink_pad_handler + .add_client(&self.sink_pad.gst_pad(), addr); + } +} + +fn try_into_socket_addr(element: &gst::Element, host: &str, port: u32) -> Result<SocketAddr, ()> { + let addr: IpAddr = match host.parse() { + Err(err) => { + gst_error!(CAT, obj: element, "Failed to parse host {}: {}", host, err); + return Err(()); + } + Ok(addr) => addr, + }; + + let port: u16 = match port.try_into() { + Err(err) => { + gst_error!(CAT, obj: element, "Invalid port {}: {}", port, err); + return Err(()); + } + Ok(port) => port, + }; + + Ok(SocketAddr::new(addr, port)) +} + +impl ObjectSubclass for UdpSink { + const NAME: &'static str = "RsTsUdpSink"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing UDP sink", + "Sink/Network", + "Thread-sharing UDP sink", + "Mathieu <mathieu@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + klass.add_signal_with_class_handler( + "add", + glib::SignalFlags::RUN_LAST | glib::SignalFlags::ACTION, + &[String::static_type(), i32::static_type()], + glib::types::Type::Unit, + |_, args| { + let element = args[0] + .get::<gst::Element>() + .expect("signal arg") + .expect("missing signal arg"); + let host = args[1] + .get::<String>() + .expect("signal arg") + .expect("missing signal arg"); + let port = args[2] + .get::<i32>() + .expect("signal arg") + .expect("missing signal arg") as u32; + + if let Ok(addr) = try_into_socket_addr(&element, &host, port) { + let udpsink = Self::from_instance(&element); + udpsink.add_client(addr); + } + + None + }, + ); + + klass.add_signal_with_class_handler( + "remove", + glib::SignalFlags::RUN_LAST | glib::SignalFlags::ACTION, + &[String::static_type(), i32::static_type()], + glib::types::Type::Unit, + |_, args| { + let element = args[0] + .get::<gst::Element>() + .expect("signal arg") + .expect("missing signal arg"); + let host = args[1] + .get::<String>() + .expect("signal arg") + .expect("missing signal arg"); + let port = args[2] + .get::<i32>() + .expect("signal arg") + .expect("missing signal arg") as u32; + + let udpsink = Self::from_instance(&element); + let settings = udpsink.settings.lock().unwrap(); + + if Some(&host) != settings.host.as_ref() || port != settings.port { + if let Ok(addr) = try_into_socket_addr(&element, &host, port) { + udpsink.remove_client(addr); + } + } + + None + }, + ); + + klass.add_signal_with_class_handler( + "clear", + glib::SignalFlags::RUN_LAST | glib::SignalFlags::ACTION, + &[], + glib::types::Type::Unit, + |_, args| { + let element = args[0] + .get::<gst::Element>() + .expect("signal arg") + .expect("missing signal arg"); + + let udpsink = Self::from_instance(&element); + let settings = udpsink.settings.lock().unwrap(); + let current_client = settings + .host + .iter() + .filter_map(|host| try_into_socket_addr(&element, host, settings.port).ok()); + + udpsink.clear_clients(current_client); + + None + }, + ); + + klass.install_properties(&PROPERTIES); + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + let settings = Arc::new(StdMutex::new(Settings::default())); + + Self { + sink_pad: PadSink::new(gst::Pad::new_from_template( + &klass.get_pad_template("sink").unwrap(), + Some("sink"), + )), + sink_pad_handler: UdpSinkPadHandler::new(Arc::clone(&settings)), + task: Task::default(), + settings, + } + } +} + +impl ObjectImpl for UdpSink { + glib_object_impl!(); + + fn set_property(&self, obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + let element = obj.downcast_ref::<gst::Element>().unwrap(); + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("host", ..) => { + let current_client = settings + .host + .as_ref() + .and_then(|host| try_into_socket_addr(&element, host, settings.port).ok()); + + let new_host = value.get().expect("type checked upstream"); + + let new_client = new_host + .and_then(|host| try_into_socket_addr(&element, host, settings.port).ok()); + + self.replace_client(current_client, new_client); + + settings.host = new_host.map(ToString::to_string); + } + subclass::Property("port", ..) => { + let current_client = settings + .host + .as_ref() + .and_then(|host| try_into_socket_addr(&element, host, settings.port).ok()); + + let new_port = value.get_some().expect("type checked upstream"); + + let new_client = settings + .host + .as_ref() + .and_then(|host| try_into_socket_addr(&element, host, new_port).ok()); + + self.replace_client(current_client, new_client); + + settings.port = new_port; + } + subclass::Property("sync", ..) => { + settings.sync = value.get_some().expect("type checked upstream"); + } + subclass::Property("bind-address", ..) => { + settings.bind_address = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("bind-port", ..) => { + settings.bind_port = value.get_some().expect("type checked upstream"); + } + subclass::Property("bind-address-v6", ..) => { + settings.bind_address_v6 = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("bind-port-v6", ..) => { + settings.bind_port_v6 = value.get_some().expect("type checked upstream"); + } + subclass::Property("socket", ..) => { + settings.socket = value + .get::<gio::Socket>() + .expect("type checked upstream") + .map(|socket| GioSocketWrapper::new(&socket)); + } + subclass::Property("used-socket", ..) => { + unreachable!(); + } + subclass::Property("socket-v6", ..) => { + settings.socket_v6 = value + .get::<gio::Socket>() + .expect("type checked upstream") + .map(|socket| GioSocketWrapper::new(&socket)); + } + subclass::Property("used-socket-v6", ..) => { + unreachable!(); + } + subclass::Property("auto-multicast", ..) => { + settings.auto_multicast = value.get_some().expect("type checked upstream"); + } + subclass::Property("loop", ..) => { + settings.multicast_loop = value.get_some().expect("type checked upstream"); + } + subclass::Property("ttl", ..) => { + settings.ttl = value.get_some().expect("type checked upstream"); + } + subclass::Property("ttl-mc", ..) => { + settings.ttl_mc = value.get_some().expect("type checked upstream"); + } + subclass::Property("qos-dscp", ..) => { + settings.qos_dscp = value.get_some().expect("type checked upstream"); + } + subclass::Property("clients", ..) => { + let clients: String = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + + let current_client = settings + .host + .iter() + .filter_map(|host| try_into_socket_addr(&element, host, settings.port).ok()); + + let clients_iter = current_client.chain(clients.split(',').filter_map(|client| { + let rsplit: Vec<&str> = client.rsplitn(2, ':').collect(); + + if rsplit.len() == 2 { + rsplit[0] + .parse::<u32>() + .map_err(|err| { + gst_error!( + CAT, + obj: element, + "Invalid port {}: {}", + rsplit[0], + err + ); + }) + .and_then(|port| try_into_socket_addr(&element, rsplit[1], port)) + .ok() + } else { + None + } + })); + + self.clear_clients(clients_iter); + } + subclass::Property("context", ..) => { + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + settings.context_wait = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("host", ..) => Ok(settings.host.to_value()), + subclass::Property("port", ..) => Ok(settings.port.to_value()), + subclass::Property("sync", ..) => Ok(settings.sync.to_value()), + subclass::Property("bind-address", ..) => Ok(settings.bind_address.to_value()), + subclass::Property("bind-port", ..) => Ok(settings.bind_port.to_value()), + subclass::Property("bind-address-v6", ..) => Ok(settings.bind_address_v6.to_value()), + subclass::Property("bind-port-v6", ..) => Ok(settings.bind_port_v6.to_value()), + subclass::Property("socket", ..) => Ok(settings + .socket + .as_ref() + .map(GioSocketWrapper::as_socket) + .to_value()), + subclass::Property("used-socket", ..) => Ok(settings + .used_socket + .as_ref() + .map(GioSocketWrapper::as_socket) + .to_value()), + subclass::Property("socket-v6", ..) => Ok(settings + .socket_v6 + .as_ref() + .map(GioSocketWrapper::as_socket) + .to_value()), + subclass::Property("used-socket-v6", ..) => Ok(settings + .used_socket_v6 + .as_ref() + .map(GioSocketWrapper::as_socket) + .to_value()), + subclass::Property("auto-multicast", ..) => Ok(settings.sync.to_value()), + subclass::Property("loop", ..) => Ok(settings.multicast_loop.to_value()), + subclass::Property("ttl", ..) => Ok(settings.ttl.to_value()), + subclass::Property("ttl-mc", ..) => Ok(settings.ttl_mc.to_value()), + subclass::Property("qos-dscp", ..) => Ok(settings.qos_dscp.to_value()), + subclass::Property("clients", ..) => { + let clients: Vec<String> = self + .sink_pad_handler + .get_clients() + .iter() + .map(ToString::to_string) + .collect(); + + Ok(clients.join(",").to_value()) + } + subclass::Property("context", ..) => Ok(settings.context.to_value()), + subclass::Property("context-wait", ..) => Ok(settings.context_wait.to_value()), + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.sink_pad.gst_pad()).unwrap(); + + super::set_element_flags(element, gst::ElementFlags::SINK); + } +} + +impl ElementImpl for UdpSink { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::ReadyToPaused => { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + self.parent_change_state(element, transition) + } + + fn send_event(&self, _element: &gst::Element, event: gst::Event) -> bool { + match event.view() { + EventView::Latency(ev) => { + self.sink_pad_handler.set_latency(ev.get_latency()); + self.sink_pad.gst_pad().push_event(event) + } + EventView::Step(..) => false, + _ => self.sink_pad.gst_pad().push_event(event), + } + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-udpsink", + gst::Rank::None, + UdpSink::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/src/udpsrc.rs b/generic/gst-plugin-threadshare/src/udpsrc.rs new file mode 100644 index 000000000..6542b0baa --- /dev/null +++ b/generic/gst-plugin-threadshare/src/udpsrc.rs @@ -0,0 +1,1002 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::future::BoxFuture; +use futures::lock::Mutex as FutMutex; +use futures::prelude::*; + +use gio; + +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::{gst_debug, gst_element_error, gst_error, gst_error_msg, gst_log, gst_trace}; +use gst_net::*; + +use lazy_static::lazy_static; + +use rand; + +use std::io; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; +use std::u16; + +use crate::runtime::prelude::*; +use crate::runtime::{Context, PadSrc, PadSrcRef, Task}; + +use super::socket::{wrap_socket, GioSocketWrapper, Socket, SocketError, SocketRead, SocketState}; + +const DEFAULT_ADDRESS: Option<&str> = Some("127.0.0.1"); +const DEFAULT_PORT: u32 = 5000; +const DEFAULT_REUSE: bool = true; +const DEFAULT_CAPS: Option<gst::Caps> = None; +const DEFAULT_MTU: u32 = 1500; +const DEFAULT_SOCKET: Option<GioSocketWrapper> = None; +const DEFAULT_USED_SOCKET: Option<GioSocketWrapper> = None; +const DEFAULT_CONTEXT: &str = ""; +const DEFAULT_CONTEXT_WAIT: u32 = 0; +const DEFAULT_RETRIEVE_SENDER_ADDRESS: bool = true; + +#[derive(Debug, Clone)] +struct Settings { + address: Option<String>, + port: u32, + reuse: bool, + caps: Option<gst::Caps>, + mtu: u32, + socket: Option<GioSocketWrapper>, + used_socket: Option<GioSocketWrapper>, + context: String, + context_wait: u32, + retrieve_sender_address: bool, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + address: DEFAULT_ADDRESS.map(Into::into), + port: DEFAULT_PORT, + reuse: DEFAULT_REUSE, + caps: DEFAULT_CAPS, + mtu: DEFAULT_MTU, + socket: DEFAULT_SOCKET, + used_socket: DEFAULT_USED_SOCKET, + context: DEFAULT_CONTEXT.into(), + context_wait: DEFAULT_CONTEXT_WAIT, + retrieve_sender_address: DEFAULT_RETRIEVE_SENDER_ADDRESS, + } + } +} + +static PROPERTIES: [subclass::Property; 10] = [ + subclass::Property("address", |name| { + glib::ParamSpec::string( + name, + "Address", + "Address/multicast group to listen on", + DEFAULT_ADDRESS, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("port", |name| { + glib::ParamSpec::uint( + name, + "Port", + "Port to listen on", + 0, + u16::MAX as u32, + DEFAULT_PORT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("reuse", |name| { + glib::ParamSpec::boolean( + name, + "Reuse", + "Allow reuse of the port", + DEFAULT_REUSE, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("caps", |name| { + glib::ParamSpec::boxed( + name, + "Caps", + "Caps to use", + gst::Caps::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("mtu", |name| { + glib::ParamSpec::uint( + name, + "MTU", + "MTU", + 0, + u16::MAX as u32, + DEFAULT_MTU, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("socket", |name| { + glib::ParamSpec::object( + name, + "Socket", + "Socket to use for UDP reception. (None == allocate)", + gio::Socket::static_type(), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("used-socket", |name| { + glib::ParamSpec::object( + name, + "Used Socket", + "Socket currently in use for UDP reception. (None = no socket)", + gio::Socket::static_type(), + glib::ParamFlags::READABLE, + ) + }), + subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("context-wait", |name| { + glib::ParamSpec::uint( + name, + "Context Wait", + "Throttle poll loop to run at most once every this many ms", + 0, + 1000, + DEFAULT_CONTEXT_WAIT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("retrieve-sender-address", |name| { + glib::ParamSpec::boolean( + name, + "Retrieve sender address", + "Whether to retrieve the sender address and add it to buffers as meta. Disabling this might result in minor performance improvements in certain scenarios", + DEFAULT_REUSE, + glib::ParamFlags::READWRITE, + ) + }), +]; + +#[derive(Debug)] +struct UdpReaderInner { + socket: tokio::net::UdpSocket, +} + +#[derive(Debug)] +pub struct UdpReader(Arc<FutMutex<UdpReaderInner>>); + +impl UdpReader { + fn new(socket: tokio::net::UdpSocket) -> Self { + UdpReader(Arc::new(FutMutex::new(UdpReaderInner { socket }))) + } +} + +impl SocketRead for UdpReader { + const DO_TIMESTAMP: bool = true; + + fn read<'buf>( + &self, + buffer: &'buf mut [u8], + ) -> BoxFuture<'buf, io::Result<(usize, Option<std::net::SocketAddr>)>> { + let this = Arc::clone(&self.0); + + async move { + this.lock() + .await + .socket + .recv_from(buffer) + .await + .map(|(read_size, saddr)| (read_size, Some(saddr))) + } + .boxed() + } +} + +#[derive(Debug)] +struct UdpSrcPadHandlerState { + retrieve_sender_address: bool, + need_initial_events: bool, + need_segment: bool, + caps: Option<gst::Caps>, +} + +impl Default for UdpSrcPadHandlerState { + fn default() -> Self { + UdpSrcPadHandlerState { + retrieve_sender_address: true, + need_initial_events: true, + need_segment: true, + caps: None, + } + } +} + +#[derive(Debug, Default)] +struct UdpSrcPadHandlerInner { + state: FutMutex<UdpSrcPadHandlerState>, + configured_caps: StdMutex<Option<gst::Caps>>, +} + +#[derive(Clone, Debug, Default)] +struct UdpSrcPadHandler(Arc<UdpSrcPadHandlerInner>); + +impl UdpSrcPadHandler { + fn prepare(&self, caps: Option<gst::Caps>, retrieve_sender_address: bool) { + let mut state = self.0.state.try_lock().expect("State locked elsewhere"); + + state.caps = caps; + state.retrieve_sender_address = retrieve_sender_address; + } + + fn reset(&self) { + *self.0.state.try_lock().expect("State locked elsewhere") = Default::default(); + *self.0.configured_caps.lock().unwrap() = None; + } + + fn set_need_segment(&self) { + self.0 + .state + .try_lock() + .expect("State locked elsewhere") + .need_segment = true; + } + + async fn push_prelude(&self, pad: &PadSrcRef<'_>, _element: &gst::Element) { + let mut state = self.0.state.lock().await; + if state.need_initial_events { + gst_debug!(CAT, obj: pad.gst_pad(), "Pushing initial events"); + + let stream_id = format!("{:08x}{:08x}", rand::random::<u32>(), rand::random::<u32>()); + let stream_start_evt = gst::Event::new_stream_start(&stream_id) + .group_id(gst::GroupId::next()) + .build(); + pad.push_event(stream_start_evt).await; + + if let Some(ref caps) = state.caps { + let caps_evt = gst::Event::new_caps(&caps).build(); + pad.push_event(caps_evt).await; + *self.0.configured_caps.lock().unwrap() = Some(caps.clone()); + } + + state.need_initial_events = false; + } + + if state.need_segment { + let segment_evt = + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(); + pad.push_event(segment_evt).await; + + state.need_segment = false; + } + } + + async fn push_buffer( + &self, + pad: &PadSrcRef<'_>, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", buffer); + + self.push_prelude(pad, element).await; + + pad.push(buffer).await + } +} + +impl PadSrcHandler for UdpSrcPadHandler { + type ElementImpl = UdpSrc; + + fn src_event( + &self, + pad: &PadSrcRef, + udpsrc: &UdpSrc, + element: &gst::Element, + event: gst::Event, + ) -> bool { + use gst::EventView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + let ret = match event.view() { + EventView::FlushStart(..) => { + udpsrc.flush_start(element); + + true + } + EventView::FlushStop(..) => { + udpsrc.flush_stop(element); + + true + } + EventView::Reconfigure(..) => true, + EventView::Latency(..) => true, + _ => false, + }; + + if ret { + gst_log!(CAT, obj: pad.gst_pad(), "Handled {:?}", event); + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Didn't handle {:?}", event); + } + + ret + } + + fn src_query( + &self, + pad: &PadSrcRef, + _udpsrc: &UdpSrc, + _element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + use gst::QueryView; + + gst_log!(CAT, obj: pad.gst_pad(), "Handling {:?}", query); + + let ret = match query.view_mut() { + QueryView::Latency(ref mut q) => { + q.set(true, 0.into(), gst::CLOCK_TIME_NONE); + true + } + QueryView::Scheduling(ref mut q) => { + q.set(gst::SchedulingFlags::SEQUENTIAL, 1, -1, 0); + q.add_scheduling_modes(&[gst::PadMode::Push]); + true + } + QueryView::Caps(ref mut q) => { + let caps = if let Some(caps) = self.0.configured_caps.lock().unwrap().as_ref() { + q.get_filter() + .map(|f| f.intersect_with_mode(caps, gst::CapsIntersectMode::First)) + .unwrap_or_else(|| caps.clone()) + } else { + q.get_filter() + .map(|f| f.to_owned()) + .unwrap_or_else(gst::Caps::new_any) + }; + + q.set_result(&caps); + + true + } + _ => false, + }; + + if ret { + gst_log!(CAT, obj: pad.gst_pad(), "Handled {:?}", query); + } else { + gst_log!(CAT, obj: pad.gst_pad(), "Didn't handle {:?}", query); + } + + ret + } +} + +struct UdpSrc { + src_pad: PadSrc, + src_pad_handler: UdpSrcPadHandler, + task: Task, + socket: StdMutex<Option<Socket<UdpReader>>>, + settings: StdMutex<Settings>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-udpsrc", + gst::DebugColorFlags::empty(), + Some("Thread-sharing UDP source"), + ); +} + +impl UdpSrc { + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + let mut settings = self.settings.lock().unwrap().clone(); + + gst_debug!(CAT, obj: element, "Preparing"); + + let context = + Context::acquire(&settings.context, settings.context_wait).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to acquire Context: {}", err] + ) + })?; + + let socket = if let Some(ref wrapped_socket) = settings.socket { + use std::net::UdpSocket; + + let socket: UdpSocket; + + #[cfg(unix)] + { + socket = wrapped_socket.get() + } + #[cfg(windows)] + { + socket = wrapped_socket.get() + } + + let socket = context.enter(|| { + tokio::net::UdpSocket::from_std(socket).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to setup socket for tokio: {}", err] + ) + }) + })?; + + settings.used_socket = Some(wrapped_socket.clone()); + + socket + } else { + let addr: IpAddr = match settings.address { + None => { + return Err(gst_error_msg!( + gst::ResourceError::Settings, + ["No address set"] + )); + } + Some(ref addr) => match addr.parse() { + Err(err) => { + return Err(gst_error_msg!( + gst::ResourceError::Settings, + ["Invalid address '{}' set: {}", addr, err] + )); + } + Ok(addr) => addr, + }, + }; + let port = settings.port; + + // TODO: TTL, multicast loopback, etc + let saddr = if addr.is_multicast() { + let bind_addr = if addr.is_ipv4() { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + } else { + IpAddr::V6(Ipv6Addr::UNSPECIFIED) + }; + + let saddr = SocketAddr::new(bind_addr, port as u16); + gst_debug!( + CAT, + obj: element, + "Binding to {:?} for multicast group {:?}", + saddr, + addr + ); + + saddr + } else { + let saddr = SocketAddr::new(addr, port as u16); + gst_debug!(CAT, obj: element, "Binding to {:?}", saddr); + + saddr + }; + + let builder = if addr.is_ipv4() { + net2::UdpBuilder::new_v4() + } else { + net2::UdpBuilder::new_v6() + } + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to create socket: {}", err] + ) + })?; + + builder.reuse_address(settings.reuse).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to set reuse_address: {}", err] + ) + })?; + + #[cfg(unix)] + { + use net2::unix::UnixUdpBuilderExt; + + builder.reuse_port(settings.reuse).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to set reuse_port: {}", err] + ) + })?; + } + + let socket = builder.bind(&saddr).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to bind socket: {}", err] + ) + })?; + + let socket = context.enter(|| { + tokio::net::UdpSocket::from_std(socket).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to setup socket for tokio: {}", err] + ) + }) + })?; + + if addr.is_multicast() { + // TODO: Multicast interface configuration, going to be tricky + match addr { + IpAddr::V4(addr) => { + socket + .join_multicast_v4(addr, Ipv4Addr::new(0, 0, 0, 0)) + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to join multicast group: {}", err] + ) + })?; + } + IpAddr::V6(addr) => { + socket.join_multicast_v6(&addr, 0).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to join multicast group: {}", err] + ) + })?; + } + } + } + + settings.used_socket = Some(wrap_socket(&socket)?); + + socket + }; + + let buffer_pool = gst::BufferPool::new(); + let mut config = buffer_pool.get_config(); + config.set_params(None, settings.mtu, 0, 0); + buffer_pool.set_config(config).map_err(|err| { + gst_error_msg!( + gst::ResourceError::Settings, + ["Failed to configure buffer pool {:?}", err] + ) + })?; + + let socket = Socket::new(element.upcast_ref(), buffer_pool, async move { + Ok(UdpReader::new(socket)) + }) + .map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to prepare socket {:?}", err] + ) + })?; + + *self.socket.lock().unwrap() = Some(socket); + element.notify("used-socket"); + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + self.src_pad_handler + .prepare(settings.caps, settings.retrieve_sender_address); + self.src_pad.prepare(&self.src_pad_handler); + + gst_debug!(CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Unpreparing"); + + *self.socket.lock().unwrap() = None; + self.settings.lock().unwrap().used_socket = None; + element.notify("used-socket"); + + self.task.unprepare().unwrap(); + self.src_pad.unprepare(); + + gst_debug!(CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Stopping"); + + self.task.stop(); + self.src_pad_handler.reset(); + + gst_debug!(CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let socket = self.socket.lock().unwrap(); + let socket = socket.as_ref().unwrap(); + if socket.state() == SocketState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return Ok(()); + } + + gst_debug!(CAT, obj: element, "Starting"); + self.start_task(element, socket); + gst_debug!(CAT, obj: element, "Started"); + + Ok(()) + } + + fn start_task(&self, element: &gst::Element, socket: &Socket<UdpReader>) { + let socket_stream = socket + .start(element.get_clock(), Some(element.get_base_time())) + .unwrap(); + let socket_stream = Arc::new(FutMutex::new(socket_stream)); + + let src_pad_handler = self.src_pad_handler.clone(); + let pad_weak = self.src_pad.downgrade(); + let element = element.clone(); + + self.task.start(move || { + let src_pad_handler = src_pad_handler.clone(); + let pad_weak = pad_weak.clone(); + let element = element.clone(); + let socket_stream = socket_stream.clone(); + + async move { + let item = socket_stream.lock().await.next().await; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + let (mut buffer, saddr) = match item { + Some(Ok((buffer, saddr))) => (buffer, saddr), + Some(Err(err)) => { + gst_error!(CAT, obj: &element, "Got error {:?}", err); + match err { + SocketError::Gst(err) => { + gst_element_error!( + element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + } + SocketError::Io(err) => { + gst_element_error!( + element, + gst::StreamError::Failed, + ("I/O error"), + ["streaming stopped, I/O error {}", err] + ); + } + } + return glib::Continue(false); + } + None => { + gst_log!(CAT, obj: pad.gst_pad(), "SocketStream Stopped"); + return glib::Continue(false); + } + }; + + if let Some(saddr) = saddr { + if src_pad_handler.0.state.lock().await.retrieve_sender_address { + let inet_addr = match saddr.ip() { + IpAddr::V4(ip) => gio::InetAddress::new_from_bytes( + gio::InetAddressBytes::V4(&ip.octets()), + ), + IpAddr::V6(ip) => gio::InetAddress::new_from_bytes( + gio::InetAddressBytes::V6(&ip.octets()), + ), + }; + let inet_socket_addr = + &gio::InetSocketAddress::new(&inet_addr, saddr.port()); + NetAddressMeta::add(buffer.get_mut().unwrap(), inet_socket_addr); + } + } + + match src_pad_handler.push_buffer(&pad, &element, buffer).await { + Ok(_) => { + gst_log!(CAT, obj: pad.gst_pad(), "Successfully pushed buffer"); + glib::Continue(true) + } + Err(gst::FlowError::Flushing) => { + gst_debug!(CAT, obj: pad.gst_pad(), "Flushing"); + glib::Continue(false) + } + Err(gst::FlowError::Eos) => { + gst_debug!(CAT, obj: pad.gst_pad(), "EOS"); + let eos = gst::Event::new_eos().build(); + pad.push_event(eos).await; + glib::Continue(false) + } + Err(err) => { + gst_error!(CAT, obj: pad.gst_pad(), "Got error {}", err); + gst_element_error!( + element, + gst::StreamError::Failed, + ("Internal data stream error"), + ["streaming stopped, reason {}", err] + ); + glib::Continue(false) + } + } + } + }); + } + + fn flush_stop(&self, element: &gst::Element) { + let socket = self.socket.lock().unwrap(); + if let Some(socket) = socket.as_ref() { + if socket.state() == SocketState::Started { + gst_debug!(CAT, obj: element, "Already started"); + return; + } + + gst_debug!(CAT, obj: element, "Stopping Flush"); + + self.src_pad_handler.set_need_segment(); + self.start_task(element, socket); + + gst_debug!(CAT, obj: element, "Stopped Flush"); + } else { + gst_debug!(CAT, obj: element, "Socket not available"); + } + } + + fn flush_start(&self, element: &gst::Element) { + let socket = self.socket.lock().unwrap(); + gst_debug!(CAT, obj: element, "Starting Flush"); + + if let Some(socket) = socket.as_ref() { + socket.pause(); + } + + self.task.cancel(); + + gst_debug!(CAT, obj: element, "Flush Started"); + } + + fn pause(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(CAT, obj: element, "Pausing"); + + self.socket.lock().unwrap().as_ref().unwrap().pause(); + self.task.pause(); + + gst_debug!(CAT, obj: element, "Paused"); + + Ok(()) + } +} + +impl ObjectSubclass for UdpSrc { + const NAME: &'static str = "RsTsUdpSrc"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing UDP source", + "Source/Network", + "Receives data over the network via UDP", + "Sebastian Dröge <sebastian@centricular.com>", + ); + + let caps = gst::Caps::new_any(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + #[cfg(not(windows))] + { + klass.install_properties(&PROPERTIES); + } + #[cfg(windows)] + { + let properties = PROPERTIES + .iter() + .filter(|p| match *p { + subclass::Property("socket", ..) | subclass::Property("used-socket", ..) => { + false + } + _ => true, + }) + .collect::<Vec<_>>(); + klass.install_properties(properties.as_slice()); + } + } + + fn new_with_class(klass: &subclass::simple::ClassStruct<Self>) -> Self { + Self { + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + src_pad_handler: UdpSrcPadHandler::default(), + task: Task::default(), + socket: StdMutex::new(None), + settings: StdMutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for UdpSrc { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + + let mut settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("address", ..) => { + settings.address = value.get().expect("type checked upstream"); + } + subclass::Property("port", ..) => { + settings.port = value.get_some().expect("type checked upstream"); + } + subclass::Property("reuse", ..) => { + settings.reuse = value.get_some().expect("type checked upstream"); + } + subclass::Property("caps", ..) => { + settings.caps = value.get().expect("type checked upstream"); + } + subclass::Property("mtu", ..) => { + settings.mtu = value.get_some().expect("type checked upstream"); + } + subclass::Property("socket", ..) => { + settings.socket = value + .get::<gio::Socket>() + .expect("type checked upstream") + .map(|socket| GioSocketWrapper::new(&socket)); + } + subclass::Property("used-socket", ..) => { + unreachable!(); + } + subclass::Property("context", ..) => { + settings.context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + } + subclass::Property("context-wait", ..) => { + settings.context_wait = value.get_some().expect("type checked upstream"); + } + subclass::Property("retrieve-sender-address", ..) => { + settings.retrieve_sender_address = value.get_some().expect("type checked upstream"); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result<glib::Value, ()> { + let prop = &PROPERTIES[id]; + + let settings = self.settings.lock().unwrap(); + match *prop { + subclass::Property("address", ..) => Ok(settings.address.to_value()), + subclass::Property("port", ..) => Ok(settings.port.to_value()), + subclass::Property("reuse", ..) => Ok(settings.reuse.to_value()), + subclass::Property("caps", ..) => Ok(settings.caps.to_value()), + subclass::Property("mtu", ..) => Ok(settings.mtu.to_value()), + subclass::Property("socket", ..) => Ok(settings + .socket + .as_ref() + .map(GioSocketWrapper::as_socket) + .to_value()), + subclass::Property("used-socket", ..) => Ok(settings + .used_socket + .as_ref() + .map(GioSocketWrapper::as_socket) + .to_value()), + subclass::Property("context", ..) => Ok(settings.context.to_value()), + subclass::Property("context-wait", ..) => Ok(settings.context_wait.to_value()), + subclass::Property("retrieve-sender-address", ..) => { + Ok(settings.retrieve_sender_address.to_value()) + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + + super::set_element_flags(element, gst::ElementFlags::SOURCE); + } +} + +impl ElementImpl for UdpSrc { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PlayingToPaused => { + self.pause(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::ReadyToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToPlaying => { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + Ok(success) + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "ts-udpsrc", + gst::Rank::None, + UdpSrc::get_type(), + ) +} diff --git a/generic/gst-plugin-threadshare/tests/appsrc.rs b/generic/gst-plugin-threadshare/tests/appsrc.rs new file mode 100644 index 000000000..8610fcb47 --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/appsrc.rs @@ -0,0 +1,303 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib::prelude::*; + +use gst; +use gst::prelude::*; + +use gst_check; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare appsrc test"); + }); +} + +#[test] +fn push() { + init(); + + let mut h = gst_check::Harness::new("ts-appsrc"); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + { + let appsrc = h.get_element().unwrap(); + appsrc.set_property("caps", &caps).unwrap(); + appsrc.set_property("do-timestamp", &true).unwrap(); + appsrc.set_property("context", &"appsrc-push").unwrap(); + } + + h.play(); + + { + let appsrc = h.get_element().unwrap(); + for _ in 0..3 { + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::new()]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + } + + assert!(appsrc + .emit("end-of-stream", &[]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + } + + for _ in 0..3 { + let _buffer = h.pull().unwrap(); + } + + let mut n_events = 0; + loop { + use gst::EventView; + + let event = h.pull_event().unwrap(); + match event.view() { + EventView::StreamStart(..) => { + assert_eq!(n_events, 0); + } + EventView::Caps(ev) => { + assert_eq!(n_events, 1); + let event_caps = ev.get_caps(); + assert_eq!(caps.as_ref(), event_caps); + } + EventView::Segment(..) => { + assert_eq!(n_events, 2); + } + EventView::Eos(..) => { + break; + } + _ => (), + } + n_events += 1; + } + assert!(n_events >= 2); +} + +#[test] +fn pause() { + init(); + + let mut h = gst_check::Harness::new("ts-appsrc"); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + { + let appsrc = h.get_element().unwrap(); + appsrc.set_property("caps", &caps).unwrap(); + appsrc.set_property("do-timestamp", &true).unwrap(); + appsrc.set_property("context", &"appsrc-pause").unwrap(); + } + + h.play(); + + let appsrc = h.get_element().unwrap(); + + // Initial buffer + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::from_slice(vec![1, 2, 3, 4])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + let _ = h.pull().unwrap(); + + appsrc + .change_state(gst::StateChange::PlayingToPaused) + .unwrap(); + + // Pre-pause buffer + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::from_slice(vec![5, 6, 7])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + appsrc + .change_state(gst::StateChange::PlayingToPaused) + .unwrap(); + + // Buffer is queued during Paused + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::from_slice(vec![8, 9])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + appsrc + .change_state(gst::StateChange::PausedToPlaying) + .unwrap(); + + // Pull Pre-pause buffer + let _ = h.pull().unwrap(); + + // Pull buffer queued while Paused + let _ = h.pull().unwrap(); + + // Can push again + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::new()]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + let _ = h.pull().unwrap(); + assert!(h.try_pull().is_none()); +} + +#[test] +fn flush() { + init(); + + let mut h = gst_check::Harness::new("ts-appsrc"); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + { + let appsrc = h.get_element().unwrap(); + appsrc.set_property("caps", &caps).unwrap(); + appsrc.set_property("do-timestamp", &true).unwrap(); + appsrc.set_property("context", &"appsrc-flush").unwrap(); + } + + h.play(); + + let appsrc = h.get_element().unwrap(); + + // Initial buffer + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::from_slice(vec![1, 2, 3, 4])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + let _ = h.pull().unwrap(); + + // FlushStart + assert!(h.push_upstream_event(gst::Event::new_flush_start().build())); + + // Can't push buffer while flushing + assert!(!appsrc + .emit("push-buffer", &[&gst::Buffer::new()]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + assert!(h.try_pull().is_none()); + + // FlushStop + assert!(h.push_upstream_event(gst::Event::new_flush_stop(true).build())); + + // No buffer available due to flush + assert!(h.try_pull().is_none()); + + // Can push again + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::new()]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + let _ = h.pull().unwrap(); + assert!(h.try_pull().is_none()); +} + +#[test] +fn pause_flush() { + init(); + + let mut h = gst_check::Harness::new("ts-appsrc"); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + { + let appsrc = h.get_element().unwrap(); + appsrc.set_property("caps", &caps).unwrap(); + appsrc.set_property("do-timestamp", &true).unwrap(); + appsrc + .set_property("context", &"appsrc-pause_flush") + .unwrap(); + } + + h.play(); + + let appsrc = h.get_element().unwrap(); + + // Initial buffer + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::from_slice(vec![1, 2, 3, 4])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + let _ = h.pull().unwrap(); + + appsrc + .change_state(gst::StateChange::PlayingToPaused) + .unwrap(); + + // FlushStart + assert!(h.push_upstream_event(gst::Event::new_flush_start().build())); + + // Can't push buffer while flushing + assert!(!appsrc + .emit("push-buffer", &[&gst::Buffer::new()]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + assert!(h.try_pull().is_none()); + + // FlushStop + assert!(h.push_upstream_event(gst::Event::new_flush_stop(true).build())); + + appsrc + .change_state(gst::StateChange::PausedToPlaying) + .unwrap(); + + // No buffer available due to flush + assert!(h.try_pull().is_none()); + + // Can push again + assert!(appsrc + .emit("push-buffer", &[&gst::Buffer::new()]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + let _ = h.pull().unwrap(); + assert!(h.try_pull().is_none()); +} diff --git a/generic/gst-plugin-threadshare/tests/inputselector.rs b/generic/gst-plugin-threadshare/tests/inputselector.rs new file mode 100644 index 000000000..965c266c1 --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/inputselector.rs @@ -0,0 +1,108 @@ +// Copyright (C) 2020 Mathieu Duponchelle <mathieu@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib; +use glib::prelude::*; + +use gst; +use gst::prelude::*; +use gst_check; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare inputselector test"); + }); +} + +#[test] +fn test_active_pad() { + init(); + + let is = gst::ElementFactory::make("ts-input-selector", None).unwrap(); + + let mut h1 = gst_check::Harness::new_with_element(&is, Some("sink_%u"), Some("src")); + let mut h2 = gst_check::Harness::new_with_element(&is, Some("sink_%u"), None); + + let active_pad = is + .get_property("active-pad") + .unwrap() + .get::<gst::Pad>() + .unwrap(); + assert_eq!(active_pad, h1.get_srcpad().unwrap().get_peer()); + + is.set_property("active-pad", &h2.get_srcpad().unwrap().get_peer()) + .unwrap(); + let active_pad = is + .get_property("active-pad") + .unwrap() + .get::<gst::Pad>() + .unwrap(); + assert_eq!(active_pad, h2.get_srcpad().unwrap().get_peer()); + + h1.set_src_caps_str("foo/bar"); + h2.set_src_caps_str("foo/bar"); + + h1.play(); + + /* Push buffer on inactive pad, we should not receive anything */ + let buf = gst::Buffer::new(); + assert_eq!(h1.push(buf), Ok(gst::FlowSuccess::Ok)); + assert_eq!(h1.buffers_received(), 0); + + /* Buffers pushed on the active pad should be received */ + let buf = gst::Buffer::new(); + assert_eq!(h2.push(buf), Ok(gst::FlowSuccess::Ok)); + assert_eq!(h1.buffers_received(), 1); + + assert_eq!(h1.events_received(), 3); + + let event = h1.pull_event().unwrap(); + assert_eq!(event.get_type(), gst::EventType::StreamStart); + let event = h1.pull_event().unwrap(); + assert_eq!(event.get_type(), gst::EventType::Caps); + let event = h1.pull_event().unwrap(); + assert_eq!(event.get_type(), gst::EventType::Segment); + + /* Push another buffer on the active pad, there should be no new events */ + let buf = gst::Buffer::new(); + assert_eq!(h2.push(buf), Ok(gst::FlowSuccess::Ok)); + assert_eq!(h1.buffers_received(), 2); + assert_eq!(h1.events_received(), 3); + + /* Switch the active pad and push a buffer, we should receive stream-start, segment and caps + * again */ + let buf = gst::Buffer::new(); + is.set_property("active-pad", &h1.get_srcpad().unwrap().get_peer()) + .unwrap(); + assert_eq!(h1.push(buf), Ok(gst::FlowSuccess::Ok)); + assert_eq!(h1.buffers_received(), 3); + assert_eq!(h1.events_received(), 6); + let event = h1.pull_event().unwrap(); + assert_eq!(event.get_type(), gst::EventType::StreamStart); + let event = h1.pull_event().unwrap(); + assert_eq!(event.get_type(), gst::EventType::Caps); + let event = h1.pull_event().unwrap(); + assert_eq!(event.get_type(), gst::EventType::Segment); + + let _ = is.set_state(gst::State::Null); +} diff --git a/generic/gst-plugin-threadshare/tests/jitterbuffer.rs b/generic/gst-plugin-threadshare/tests/jitterbuffer.rs new file mode 100644 index 000000000..cdc227672 --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/jitterbuffer.rs @@ -0,0 +1,172 @@ +// Copyright (C) 2020 François Laignel <fengalin@free.fr> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use gst; +use gst::gst_debug; +use gst::prelude::*; + +use lazy_static::lazy_static; + +use std::sync::mpsc; + +use gstthreadshare; + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-test", + gst::DebugColorFlags::empty(), + Some("Thread-sharing test"), + ); +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare jitterbuffer test"); + }); +} + +#[test] +fn jb_pipeline() { + init(); + + const CONTEXT_WAIT: u32 = 20; + const LATENCY: u32 = 20; + const BUFFER_NB: i32 = 3; + + let pipeline = gst::Pipeline::new(None); + + let src = gst::ElementFactory::make("audiotestsrc", Some("audiotestsrc")).unwrap(); + src.set_property("is-live", &true).unwrap(); + src.set_property("num-buffers", &BUFFER_NB).unwrap(); + + let enc = gst::ElementFactory::make("alawenc", Some("alawenc")).unwrap(); + let pay = gst::ElementFactory::make("rtppcmapay", Some("rtppcmapay")).unwrap(); + + let jb = gst::ElementFactory::make("ts-jitterbuffer", Some("ts-jitterbuffer")).unwrap(); + jb.set_property("context", &"jb_pipeline").unwrap(); + jb.set_property("context-wait", &CONTEXT_WAIT).unwrap(); + jb.set_property("latency", &LATENCY).unwrap(); + + let depay = gst::ElementFactory::make("rtppcmadepay", Some("rtppcmadepay")).unwrap(); + let dec = gst::ElementFactory::make("alawdec", Some("alawdec")).unwrap(); + + let sink = gst::ElementFactory::make("appsink", Some("appsink")).unwrap(); + sink.set_property("sync", &false).unwrap(); + sink.set_property("async", &false).unwrap(); + sink.set_property("emit-signals", &true).unwrap(); + + pipeline + .add_many(&[&src, &enc, &pay, &jb, &depay, &dec, &sink]) + .unwrap(); + gst::Element::link_many(&[&src, &enc, &pay, &jb, &depay, &dec, &sink]).unwrap(); + + let appsink = sink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let (sender, receiver) = mpsc::channel(); + appsink.connect_new_sample(move |appsink| { + let _sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + sender.send(()).unwrap(); + Ok(gst::FlowSuccess::Ok) + }); + + pipeline.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "jb_pipeline: waiting for {} buffers", BUFFER_NB); + for idx in 0..BUFFER_NB { + receiver.recv().unwrap(); + gst_debug!(CAT, "jb_pipeline: received buffer #{}", idx); + } + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn jb_ts_pipeline() { + init(); + + const CONTEXT_WAIT: u32 = 20; + const LATENCY: u32 = 20; + const BUFFER_NB: i32 = 3; + + let pipeline = gst::Pipeline::new(None); + + let src = gst::ElementFactory::make("audiotestsrc", Some("audiotestsrc")).unwrap(); + src.set_property("is-live", &true).unwrap(); + src.set_property("num-buffers", &BUFFER_NB).unwrap(); + + let queue = gst::ElementFactory::make("ts-queue", Some("ts-queue")).unwrap(); + queue + .set_property("context", &"jb_ts_pipeline_queue") + .unwrap(); + queue.set_property("context-wait", &CONTEXT_WAIT).unwrap(); + + let enc = gst::ElementFactory::make("alawenc", Some("alawenc")).unwrap(); + let pay = gst::ElementFactory::make("rtppcmapay", Some("rtppcmapay")).unwrap(); + + let jb = gst::ElementFactory::make("ts-jitterbuffer", Some("ts-jitterbuffer")).unwrap(); + jb.set_property("context", &"jb_ts_pipeline").unwrap(); + jb.set_property("context-wait", &CONTEXT_WAIT).unwrap(); + jb.set_property("latency", &LATENCY).unwrap(); + + let depay = gst::ElementFactory::make("rtppcmadepay", Some("rtppcmadepay")).unwrap(); + let dec = gst::ElementFactory::make("alawdec", Some("alawdec")).unwrap(); + + let sink = gst::ElementFactory::make("appsink", Some("appsink")).unwrap(); + sink.set_property("sync", &false).unwrap(); + sink.set_property("async", &false).unwrap(); + sink.set_property("emit-signals", &true).unwrap(); + + pipeline + .add_many(&[&src, &queue, &enc, &pay, &jb, &depay, &dec, &sink]) + .unwrap(); + gst::Element::link_many(&[&src, &queue, &enc, &pay, &jb, &depay, &dec, &sink]).unwrap(); + + let appsink = sink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let (sender, receiver) = mpsc::channel(); + appsink.connect_new_sample(move |appsink| { + let _sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + sender.send(()).unwrap(); + Ok(gst::FlowSuccess::Ok) + }); + + pipeline.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "jb_ts_pipeline: waiting for {} buffers", BUFFER_NB); + for idx in 0..BUFFER_NB { + receiver.recv().unwrap(); + gst_debug!(CAT, "jb_ts_pipeline: received buffer #{}", idx); + } + + pipeline.set_state(gst::State::Null).unwrap(); +} diff --git a/generic/gst-plugin-threadshare/tests/pad.rs b/generic/gst-plugin-threadshare/tests/pad.rs new file mode 100644 index 000000000..8bf680c04 --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/pad.rs @@ -0,0 +1,1322 @@ +// Copyright (C) 2019-2020 François Laignel <fengalin@free.fr> +// Copyright (C) 2020 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use futures::channel::mpsc; +use futures::future::BoxFuture; +use futures::lock::Mutex as FutMutex; +use futures::prelude::*; + +use glib; +use glib::GBoxed; +use glib::{glib_object_impl, glib_object_subclass}; + +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst::EventView; +use gst::{gst_debug, gst_error_msg, gst_info, gst_log}; + +use lazy_static::lazy_static; + +use std::boxed::Box; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::sync::Mutex as StdMutex; + +use gstthreadshare::runtime::prelude::*; +use gstthreadshare::runtime::{Context, PadSink, PadSinkRef, PadSrc, PadSrcRef, Task}; + +const DEFAULT_CONTEXT: &str = ""; +const THROTTLING_DURATION: u32 = 2; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare pad test"); + }); +} + +// Src + +static SRC_PROPERTIES: [glib::subclass::Property; 1] = + [glib::subclass::Property("context", |name| { + glib::ParamSpec::string( + name, + "Context", + "Context name to share threads with", + Some(DEFAULT_CONTEXT), + glib::ParamFlags::READWRITE, + ) + })]; + +#[derive(Clone, Debug, Default)] +struct Settings { + context: String, +} + +lazy_static! { + static ref SRC_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-element-src-test", + gst::DebugColorFlags::empty(), + Some("Thread-sharing Test Src Element"), + ); +} + +#[derive(Clone, Debug)] +struct PadSrcTestHandler; + +impl PadSrcTestHandler { + async fn push_item(pad: PadSrcRef<'_>, item: Item) -> Result<gst::FlowSuccess, gst::FlowError> { + gst_debug!(SRC_CAT, obj: pad.gst_pad(), "Handling {:?}", item); + + match item { + Item::Event(event) => { + pad.push_event(event).await; + + Ok(gst::FlowSuccess::Ok) + } + Item::Buffer(buffer) => pad.push(buffer).await, + Item::BufferList(list) => pad.push_list(list).await, + } + } +} + +impl PadSrcHandler for PadSrcTestHandler { + type ElementImpl = ElementSrcTest; + + fn src_event( + &self, + pad: &PadSrcRef, + elem_src_test: &ElementSrcTest, + element: &gst::Element, + event: gst::Event, + ) -> bool { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Handling {:?}", event); + + let ret = match event.view() { + EventView::FlushStart(..) => { + elem_src_test.flush_start(element); + true + } + EventView::Qos(..) | EventView::Reconfigure(..) | EventView::Latency(..) => true, + EventView::FlushStop(..) => { + elem_src_test.flush_stop(&element); + true + } + _ => false, + }; + + if ret { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Handled {:?}", event); + } else { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "Didn't handle {:?}", event); + } + + ret + } +} + +#[derive(Debug, Eq, PartialEq)] +enum ElementSrcTestState { + Paused, + RejectItems, + Started, +} + +#[derive(Debug)] +struct ElementSrcTest { + src_pad: PadSrc, + task: Task, + state: StdMutex<ElementSrcTestState>, + sender: StdMutex<Option<mpsc::Sender<Item>>>, + receiver: StdMutex<Option<Arc<FutMutex<mpsc::Receiver<Item>>>>>, + settings: StdMutex<Settings>, +} + +impl ElementSrcTest { + fn try_push(&self, item: Item) -> Result<(), Item> { + let state = self.state.lock().unwrap(); + if *state == ElementSrcTestState::RejectItems { + gst_debug!(SRC_CAT, "ElementSrcTest rejecting item due to pad state"); + + return Err(item); + } + + match self.sender.lock().unwrap().as_mut() { + Some(sender) => sender + .try_send(item) + .map_err(mpsc::TrySendError::into_inner), + None => Err(item), + } + } + + fn prepare(&self, element: &gst::Element) -> Result<(), gst::ErrorMessage> { + gst_debug!(SRC_CAT, obj: element, "Preparing"); + + let settings = self.settings.lock().unwrap().clone(); + let context = Context::acquire(&settings.context, THROTTLING_DURATION).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Failed to acquire Context: {}", err] + ) + })?; + + self.task.prepare(context).map_err(|err| { + gst_error_msg!( + gst::ResourceError::OpenRead, + ["Error preparing Task: {:?}", err] + ) + })?; + self.src_pad.prepare(&PadSrcTestHandler); + + let (sender, receiver) = mpsc::channel(1); + *self.sender.lock().unwrap() = Some(sender); + *self.receiver.lock().unwrap() = Some(Arc::new(FutMutex::new(receiver))); + + gst_debug!(SRC_CAT, obj: element, "Prepared"); + + Ok(()) + } + + fn unprepare(&self, element: &gst::Element) -> Result<(), ()> { + gst_debug!(SRC_CAT, obj: element, "Unpreparing"); + + self.task.unprepare().unwrap(); + self.src_pad.unprepare(); + + *self.sender.lock().unwrap() = None; + *self.receiver.lock().unwrap() = None; + + gst_debug!(SRC_CAT, obj: element, "Unprepared"); + + Ok(()) + } + + fn stop(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + gst_debug!(SRC_CAT, obj: element, "Stopping"); + + self.flush(element); + *state = ElementSrcTestState::RejectItems; + + gst_debug!(SRC_CAT, obj: element, "Stopped"); + + Ok(()) + } + + fn flush(&self, element: &gst::Element) { + gst_debug!(SRC_CAT, obj: element, "Flushing"); + + self.task.stop(); + + let receiver = self.receiver.lock().unwrap(); + let mut receiver = receiver + .as_ref() + .unwrap() + .try_lock() + .expect("receiver locked elsewhere"); + + // Purge the channel + loop { + match receiver.try_next() { + Ok(Some(_item)) => { + gst_debug!(SRC_CAT, obj: element, "Dropping pending item"); + } + Err(_) => { + gst_debug!(SRC_CAT, obj: element, "No more pending item"); + break; + } + Ok(None) => { + panic!("Channel sender dropped"); + } + } + } + + gst_debug!(SRC_CAT, obj: element, "Flushed"); + } + + fn start(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + if *state == ElementSrcTestState::Started { + gst_debug!(SRC_CAT, obj: element, "Already started"); + return Err(()); + } + + gst_debug!(SRC_CAT, obj: element, "Starting"); + + self.start_task(); + *state = ElementSrcTestState::Started; + + gst_debug!(SRC_CAT, obj: element, "Started"); + + Ok(()) + } + + fn start_task(&self) { + let pad_weak = self.src_pad.downgrade(); + let receiver = Arc::clone(self.receiver.lock().unwrap().as_ref().expect("No receiver")); + + self.task.start(move || { + let pad_weak = pad_weak.clone(); + let receiver = Arc::clone(&receiver); + + async move { + let item = receiver.lock().await.next().await; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + + let item = match item { + Some(item) => item, + None => { + gst_log!(SRC_CAT, obj: pad.gst_pad(), "SrcPad channel aborted"); + return glib::Continue(false); + } + }; + + let pad = pad_weak.upgrade().expect("PadSrc no longer exists"); + match PadSrcTestHandler::push_item(pad, item).await { + Ok(_) => glib::Continue(true), + Err(gst::FlowError::Flushing) => glib::Continue(false), + Err(err) => panic!("Got error {:?}", err), + } + } + }); + } + + fn flush_stop(&self, element: &gst::Element) { + let mut state = self.state.lock().unwrap(); + if *state == ElementSrcTestState::Started { + gst_debug!(SRC_CAT, obj: element, "Already started"); + return; + } + + gst_debug!(SRC_CAT, obj: element, "Stopping Flush"); + + self.flush(element); + self.start_task(); + *state = ElementSrcTestState::Started; + + gst_debug!(SRC_CAT, obj: element, "Stopped Flush"); + } + + fn flush_start(&self, element: &gst::Element) { + let mut state = self.state.lock().unwrap(); + gst_debug!(SRC_CAT, obj: element, "Starting Flush"); + + self.task.cancel(); + *state = ElementSrcTestState::RejectItems; + + gst_debug!(SRC_CAT, obj: element, "Flush Started"); + } + + fn pause(&self, element: &gst::Element) -> Result<(), ()> { + let mut state = self.state.lock().unwrap(); + gst_debug!(SRC_CAT, obj: element, "Pausing"); + + self.task.pause(); + *state = ElementSrcTestState::Paused; + + gst_debug!(SRC_CAT, obj: element, "Paused"); + + Ok(()) + } +} + +impl ObjectSubclass for ElementSrcTest { + const NAME: &'static str = "TsElementSrcTest"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = glib::subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut glib::subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing Test Src Element", + "Generic", + "Src Element for Pad Src Test", + "François Laignel <fengalin@free.fr>", + ); + + let caps = gst::Caps::new_any(); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + klass.install_properties(&SRC_PROPERTIES); + } + + fn new_with_class(klass: &glib::subclass::simple::ClassStruct<Self>) -> Self { + ElementSrcTest { + src_pad: PadSrc::new(gst::Pad::new_from_template( + &klass.get_pad_template("src").unwrap(), + Some("src"), + )), + task: Task::default(), + state: StdMutex::new(ElementSrcTestState::RejectItems), + sender: StdMutex::new(None), + receiver: StdMutex::new(None), + settings: StdMutex::new(Settings::default()), + } + } +} + +impl ObjectImpl for ElementSrcTest { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &SRC_PROPERTIES[id]; + + match *prop { + glib::subclass::Property("context", ..) => { + let context = value + .get() + .expect("type checked upstream") + .unwrap_or_else(|| "".into()); + + self.settings.lock().unwrap().context = context; + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.src_pad.gst_pad()).unwrap(); + } +} + +impl ElementImpl for ElementSrcTest { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_log!(SRC_CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.prepare(element).map_err(|err| { + element.post_error_message(&err); + gst::StateChangeError + })?; + } + gst::StateChange::PlayingToPaused => { + self.pause(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToNull => { + self.unprepare(element).map_err(|_| gst::StateChangeError)?; + } + _ => (), + } + + let mut success = self.parent_change_state(element, transition)?; + + match transition { + gst::StateChange::PausedToReady => { + self.stop(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::PausedToPlaying => { + self.start(element).map_err(|_| gst::StateChangeError)?; + } + gst::StateChange::ReadyToPaused | gst::StateChange::PlayingToPaused => { + success = gst::StateChangeSuccess::NoPreroll; + } + _ => (), + } + + Ok(success) + } + + fn send_event(&self, element: &gst::Element, event: gst::Event) -> bool { + match event.view() { + EventView::FlushStart(..) => { + self.flush_start(element); + } + EventView::FlushStop(..) => { + self.flush_stop(element); + } + _ => (), + } + + if !event.is_serialized() { + self.src_pad.gst_pad().push_event(event) + } else { + self.try_push(Item::Event(event)).is_ok() + } + } +} + +// Sink + +#[derive(Debug)] +enum Item { + Buffer(gst::Buffer), + BufferList(gst::BufferList), + Event(gst::Event), +} + +#[derive(Clone, Debug, GBoxed)] +#[gboxed(type_name = "TsTestItemSender")] +struct ItemSender { + sender: mpsc::Sender<Item>, +} + +static SINK_PROPERTIES: [glib::subclass::Property; 1] = + [glib::subclass::Property("sender", |name| { + glib::ParamSpec::boxed( + name, + "Sender", + "Channel sender to forward the incoming items to", + ItemSender::get_type(), + glib::ParamFlags::WRITABLE, + ) + })]; + +#[derive(Clone, Debug, Default)] +struct PadSinkTestHandler; + +impl PadSinkHandler for PadSinkTestHandler { + type ElementImpl = ElementSinkTest; + + fn sink_chain( + &self, + _pad: &PadSinkRef, + _elem_sink_test: &ElementSinkTest, + element: &gst::Element, + buffer: gst::Buffer, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let element = element.clone(); + async move { + let elem_sink_test = ElementSinkTest::from_instance(&element); + elem_sink_test + .forward_item(&element, Item::Buffer(buffer)) + .await + } + .boxed() + } + + fn sink_chain_list( + &self, + _pad: &PadSinkRef, + _elem_sink_test: &ElementSinkTest, + element: &gst::Element, + list: gst::BufferList, + ) -> BoxFuture<'static, Result<gst::FlowSuccess, gst::FlowError>> { + let element = element.clone(); + async move { + let elem_sink_test = ElementSinkTest::from_instance(&element); + elem_sink_test + .forward_item(&element, Item::BufferList(list)) + .await + } + .boxed() + } + + fn sink_event( + &self, + pad: &PadSinkRef, + elem_sink_test: &ElementSinkTest, + element: &gst::Element, + event: gst::Event, + ) -> bool { + gst_debug!(SINK_CAT, obj: pad.gst_pad(), "Handling non-serialized {:?}", event); + + match event.view() { + EventView::FlushStart(..) => { + elem_sink_test.stop(&element); + true + } + _ => false, + } + } + + fn sink_event_serialized( + &self, + pad: &PadSinkRef, + _elem_sink_test: &ElementSinkTest, + element: &gst::Element, + event: gst::Event, + ) -> BoxFuture<'static, bool> { + gst_log!(SINK_CAT, obj: pad.gst_pad(), "Handling serialized {:?}", event); + + let element = element.clone(); + async move { + let elem_sink_test = ElementSinkTest::from_instance(&element); + + if let EventView::FlushStop(..) = event.view() { + elem_sink_test.start(&element); + } + + elem_sink_test + .forward_item(&element, Item::Event(event)) + .await + .is_ok() + } + .boxed() + } +} + +#[derive(Debug)] +struct ElementSinkTest { + sink_pad: PadSink, + flushing: AtomicBool, + sender: FutMutex<Option<mpsc::Sender<Item>>>, +} + +impl ElementSinkTest { + async fn forward_item( + &self, + element: &gst::Element, + item: Item, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + if !self.flushing.load(Ordering::SeqCst) { + gst_debug!(SINK_CAT, obj: element, "Fowarding {:?}", item); + self.sender + .lock() + .await + .as_mut() + .expect("Item Sender not set") + .send(item) + .await + .map(|_| gst::FlowSuccess::Ok) + .map_err(|_| gst::FlowError::Error) + } else { + gst_debug!( + SINK_CAT, + obj: element, + "Not fowarding {:?} due to flushing", + item + ); + Err(gst::FlowError::Flushing) + } + } + + fn start(&self, element: &gst::Element) { + gst_debug!(SINK_CAT, obj: element, "Starting"); + self.flushing.store(false, Ordering::SeqCst); + gst_debug!(SINK_CAT, obj: element, "Started"); + } + + fn stop(&self, element: &gst::Element) { + gst_debug!(SINK_CAT, obj: element, "Stopping"); + self.flushing.store(true, Ordering::SeqCst); + gst_debug!(SINK_CAT, obj: element, "Stopped"); + } +} + +impl ElementSinkTest { + fn push_flush_start(&self, element: &gst::Element) { + gst_debug!(SINK_CAT, obj: element, "Pushing FlushStart"); + self.sink_pad + .gst_pad() + .push_event(gst::Event::new_flush_start().build()); + gst_debug!(SINK_CAT, obj: element, "FlushStart pushed"); + } + + fn push_flush_stop(&self, element: &gst::Element) { + gst_debug!(SINK_CAT, obj: element, "Pushing FlushStop"); + self.sink_pad + .gst_pad() + .push_event(gst::Event::new_flush_stop(true).build()); + gst_debug!(SINK_CAT, obj: element, "FlushStop pushed"); + } +} + +lazy_static! { + static ref SINK_CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-element-sink-test", + gst::DebugColorFlags::empty(), + Some("Thread-sharing Test Sink Element"), + ); +} + +impl ObjectSubclass for ElementSinkTest { + const NAME: &'static str = "TsElementSinkTest"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = glib::subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn class_init(klass: &mut glib::subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "Thread-sharing Test Sink Element", + "Generic", + "Sink Element for Pad Test", + "François Laignel <fengalin@free.fr>", + ); + + let caps = gst::Caps::new_any(); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + klass.install_properties(&SINK_PROPERTIES); + } + + fn new_with_class(klass: &glib::subclass::simple::ClassStruct<Self>) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let gst_pad = gst::Pad::new_from_template(&templ, Some("sink")); + + ElementSinkTest { + sink_pad: PadSink::new(gst_pad), + flushing: AtomicBool::new(true), + sender: FutMutex::new(None), + } + } +} + +impl ObjectImpl for ElementSinkTest { + glib_object_impl!(); + + fn set_property(&self, _obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &SINK_PROPERTIES[id]; + + match *prop { + glib::subclass::Property("sender", ..) => { + let ItemSender { sender } = value + .get::<&ItemSender>() + .expect("type checked upstream") + .expect("ItemSender not found") + .clone(); + *futures::executor::block_on(self.sender.lock()) = Some(sender); + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::<gst::Element>().unwrap(); + element.add_pad(self.sink_pad.gst_pad()).unwrap(); + } +} + +impl ElementImpl for ElementSinkTest { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { + gst_log!(SINK_CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::NullToReady => { + self.sink_pad.prepare(&PadSinkTestHandler::default()); + } + gst::StateChange::PausedToReady => { + self.stop(element); + } + gst::StateChange::ReadyToNull => { + self.sink_pad.unprepare(); + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::ReadyToPaused { + self.start(element); + } + + Ok(success) + } +} + +fn setup( + context_name: &str, + mut middle_element_1: Option<gst::Element>, + mut middle_element_2: Option<gst::Element>, +) -> ( + gst::Pipeline, + gst::Element, + gst::Element, + mpsc::Receiver<Item>, +) { + init(); + + let pipeline = gst::Pipeline::new(None); + + // Src + let src_element = glib::Object::new(ElementSrcTest::get_type(), &[]) + .unwrap() + .downcast::<gst::Element>() + .unwrap(); + src_element.set_property("context", &context_name).unwrap(); + pipeline.add(&src_element).unwrap(); + + let mut last_element = src_element.clone(); + + if let Some(middle_element) = middle_element_1.take() { + pipeline.add(&middle_element).unwrap(); + last_element.link(&middle_element).unwrap(); + last_element = middle_element; + } + + if let Some(middle_element) = middle_element_2.take() { + // Don't link the 2 middle elements: this is used for ts-proxy + pipeline.add(&middle_element).unwrap(); + last_element = middle_element; + } + + // Sink + let sink_element = glib::Object::new(ElementSinkTest::get_type(), &[]) + .unwrap() + .downcast::<gst::Element>() + .unwrap(); + pipeline.add(&sink_element).unwrap(); + last_element.link(&sink_element).unwrap(); + + let (sender, receiver) = mpsc::channel::<Item>(10); + sink_element + .set_property("sender", &ItemSender { sender }) + .unwrap(); + + (pipeline, src_element, sink_element, receiver) +} + +fn nominal_scenario( + scenario_name: &str, + pipeline: gst::Pipeline, + src_element: gst::Element, + mut receiver: mpsc::Receiver<Item>, +) { + let elem_src_test = ElementSrcTest::from_instance(&src_element); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Initial events + elem_src_test + .try_push(Item::Event( + gst::Event::new_stream_start(scenario_name) + .group_id(gst::GroupId::next()) + .build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::StreamStart(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + // Buffer + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![1, 2, 3, 4]))) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![1, 2, 3, 4].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + // BufferList + let mut list = gst::BufferList::new(); + list.get_mut() + .unwrap() + .add(gst::Buffer::from_slice(vec![1, 2, 3, 4])); + elem_src_test.try_push(Item::BufferList(list)).unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::BufferList(_) => (), + other => panic!("Unexpected item {:?}", other), + } + + // Pause the Pad task + pipeline.set_state(gst::State::Paused).unwrap(); + + // Item accepted, but not processed before switching to Playing again + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![5, 6, 7]))) + .unwrap(); + + // Nothing forwarded + receiver.try_next().unwrap_err(); + + // Switch back the Pad task to Started + pipeline.set_state(gst::State::Playing).unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![5, 6, 7].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + // Flush + src_element.send_event(gst::Event::new_flush_start().build()); + src_element.send_event(gst::Event::new_flush_stop(true).build()); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::FlushStop(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + // Buffer + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![8, 9]))) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![8, 9].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + // EOS + elem_src_test + .try_push(Item::Event(gst::Event::new_eos().build())) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Eos(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + pipeline.set_state(gst::State::Ready).unwrap(); + + // Receiver was dropped when stopping => can't send anymore + elem_src_test + .try_push(Item::Event( + gst::Event::new_stream_start(&format!("{}_past_stop", scenario_name)) + .group_id(gst::GroupId::next()) + .build(), + )) + .unwrap_err(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn src_sink_nominal() { + let name = "src_sink_nominal"; + + let (pipeline, src_element, _sink_element, receiver) = setup(&name, None, None); + + nominal_scenario(&name, pipeline, src_element, receiver); +} + +#[test] +fn src_tsqueue_sink_nominal() { + init(); + + let name = "src_tsqueue_sink"; + + let ts_queue = gst::ElementFactory::make("ts-queue", Some("ts-queue")).unwrap(); + ts_queue + .set_property("context", &format!("{}_queue", name)) + .unwrap(); + ts_queue + .set_property("context-wait", &THROTTLING_DURATION) + .unwrap(); + + let (pipeline, src_element, _sink_element, receiver) = setup(name, Some(ts_queue), None); + + nominal_scenario(&name, pipeline, src_element, receiver); +} + +#[test] +fn src_queue_sink_nominal() { + init(); + + let name = "src_queue_sink"; + + let queue = gst::ElementFactory::make("queue", Some("queue")).unwrap(); + let (pipeline, src_element, _sink_element, receiver) = setup(name, Some(queue), None); + + nominal_scenario(&name, pipeline, src_element, receiver); +} + +#[test] +fn src_tsproxy_sink_nominal() { + init(); + + let name = "src_tsproxy_sink"; + + let ts_proxy_sink = gst::ElementFactory::make("ts-proxysink", Some("ts-proxysink")).unwrap(); + ts_proxy_sink + .set_property("proxy-context", &format!("{}_proxy_context", name)) + .unwrap(); + + let ts_proxy_src = gst::ElementFactory::make("ts-proxysrc", Some("ts-proxysrc")).unwrap(); + ts_proxy_src + .set_property("proxy-context", &format!("{}_proxy_context", name)) + .unwrap(); + ts_proxy_src + .set_property("context", &format!("{}_context", name)) + .unwrap(); + ts_proxy_src + .set_property("context-wait", &THROTTLING_DURATION) + .unwrap(); + + let (pipeline, src_element, _sink_element, receiver) = + setup(name, Some(ts_proxy_sink), Some(ts_proxy_src)); + + nominal_scenario(&name, pipeline, src_element, receiver); +} + +#[test] +fn start_pause_start() { + init(); + + let scenario_name = "start_pause_start"; + + let (pipeline, src_element, _sink_element, mut receiver) = setup(&scenario_name, None, None); + + let elem_src_test = ElementSrcTest::from_instance(&src_element); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Initial events + elem_src_test + .try_push(Item::Event( + gst::Event::new_stream_start(scenario_name) + .group_id(gst::GroupId::next()) + .build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::StreamStart(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + // Buffer + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![1, 2, 3, 4]))) + .unwrap(); + + pipeline.set_state(gst::State::Paused).unwrap(); + + pipeline.set_state(gst::State::Playing).unwrap(); + + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![5, 6, 7]))) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![1, 2, 3, 4].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![5, 6, 7].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + // Nothing else forwarded + receiver.try_next().unwrap_err(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn start_stop_start() { + init(); + + let scenario_name = "start_stop_start"; + + let (pipeline, src_element, _sink_element, mut receiver) = setup(&scenario_name, None, None); + + let elem_src_test = ElementSrcTest::from_instance(&src_element); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Initial events + elem_src_test + .try_push(Item::Event( + gst::Event::new_stream_start(&format!("{}-after_stop", scenario_name)) + .group_id(gst::GroupId::next()) + .build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::StreamStart(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + // Buffer + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![1, 2, 3, 4]))) + .unwrap(); + + pipeline.set_state(gst::State::Ready).unwrap(); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Initial events again + elem_src_test + .try_push(Item::Event( + gst::Event::new_stream_start(scenario_name) + .group_id(gst::GroupId::next()) + .build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(_buffer) => { + gst_info!( + SRC_CAT, + "{}: initial buffer went through, don't expect any pending item to be dropped", + scenario_name + ); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::StreamStart(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + } + Item::Event(event) => match event.view() { + EventView::StreamStart(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![5, 6, 7]))) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![5, 6, 7].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn start_flush() { + init(); + + let scenario_name = "start_flush"; + + let (pipeline, src_element, sink_element, mut receiver) = setup(&scenario_name, None, None); + + let elem_src_test = ElementSrcTest::from_instance(&src_element); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Initial events + elem_src_test + .try_push(Item::Event( + gst::Event::new_stream_start(&format!("{}-after_stop", scenario_name)) + .group_id(gst::GroupId::next()) + .build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::StreamStart(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + + // Buffer + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![1, 2, 3, 4]))) + .unwrap(); + + let elem_sink_test = ElementSinkTest::from_instance(&sink_element); + + elem_sink_test.push_flush_start(&sink_element); + + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![5, 6, 7]))) + .unwrap_err(); + + elem_sink_test.push_flush_stop(&sink_element); + + elem_src_test + .try_push(Item::Event( + gst::Event::new_segment(&gst::FormattedSegment::<gst::format::Time>::new()).build(), + )) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + Item::Buffer(buffer) => { + // In some cases, the first Buffer might be processed before FlushStart + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![1, 2, 3, 4].as_slice()); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Event(event) => match event.view() { + EventView::Segment(_) => (), + other => panic!("Unexpected event {:?}", other), + }, + other => panic!("Unexpected item {:?}", other), + } + } + other => panic!("Unexpected item {:?}", other), + } + + // Post flush buffer + elem_src_test + .try_push(Item::Buffer(gst::Buffer::from_slice(vec![8, 9]))) + .unwrap(); + + match futures::executor::block_on(receiver.next()).unwrap() { + Item::Buffer(buffer) => { + let data = buffer.map_readable().unwrap(); + assert_eq!(data.as_slice(), vec![8, 9].as_slice()); + } + other => panic!("Unexpected item {:?}", other), + } + + pipeline.set_state(gst::State::Null).unwrap(); +} diff --git a/generic/gst-plugin-threadshare/tests/pipeline.rs b/generic/gst-plugin-threadshare/tests/pipeline.rs new file mode 100644 index 000000000..61a7851aa --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/pipeline.rs @@ -0,0 +1,627 @@ +// Copyright (C) 2019 François Laignel <fengalin@free.fr> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use gst; +use gst::prelude::*; +use gst::{gst_debug, gst_error}; + +use lazy_static::lazy_static; + +use std::sync::mpsc; + +use gstthreadshare; + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "ts-test", + gst::DebugColorFlags::empty(), + Some("Thread-sharing test"), + ); +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare pipeline test"); + }); +} + +#[test] +fn multiple_contexts_queue() { + use std::net; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::sync::mpsc; + + init(); + + const CONTEXT_NB: u32 = 2; + const SRC_NB: u16 = 4; + const CONTEXT_WAIT: u32 = 1; + const BUFFER_NB: u32 = 3; + const FIRST_PORT: u16 = 40000; + + let l = glib::MainLoop::new(None, false); + let pipeline = gst::Pipeline::new(None); + + let (sender, receiver) = mpsc::channel(); + + for i in 0..SRC_NB { + let src = + gst::ElementFactory::make("ts-udpsrc", Some(format!("src-{}", i).as_str())).unwrap(); + src.set_property("context", &format!("context-{}", (i as u32) % CONTEXT_NB)) + .unwrap(); + src.set_property("context-wait", &CONTEXT_WAIT).unwrap(); + src.set_property("port", &((FIRST_PORT + i) as u32)) + .unwrap(); + + let queue = + gst::ElementFactory::make("ts-queue", Some(format!("queue-{}", i).as_str())).unwrap(); + queue + .set_property("context", &format!("context-{}", (i as u32) % CONTEXT_NB)) + .unwrap(); + queue.set_property("context-wait", &CONTEXT_WAIT).unwrap(); + + let sink = + gst::ElementFactory::make("appsink", Some(format!("sink-{}", i).as_str())).unwrap(); + sink.set_property("sync", &false).unwrap(); + sink.set_property("async", &false).unwrap(); + sink.set_property("emit-signals", &true).unwrap(); + + pipeline.add_many(&[&src, &queue, &sink]).unwrap(); + gst::Element::link_many(&[&src, &queue, &sink]).unwrap(); + + let appsink = sink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let sender_clone = sender.clone(); + appsink.connect_new_sample(move |appsink| { + let _sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + sender_clone.send(()).unwrap(); + Ok(gst::FlowSuccess::Ok) + }); + } + + let pipeline_clone = pipeline.clone(); + let l_clone = l.clone(); + let mut test_scenario = Some(move || { + let buffer = [0; 160]; + let socket = net::UdpSocket::bind("0.0.0.0:0").unwrap(); + + let ipaddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let destinations = (FIRST_PORT..(FIRST_PORT + SRC_NB)) + .map(|port| SocketAddr::new(ipaddr, port)) + .collect::<Vec<_>>(); + + for _ in 0..BUFFER_NB { + for dest in &destinations { + gst_debug!(CAT, "multiple_contexts_queue: sending buffer to {:?}", dest); + socket.send_to(&buffer, dest).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(CONTEXT_WAIT as u64)); + } + } + + gst_debug!( + CAT, + "multiple_contexts_queue: waiting for all buffers notifications" + ); + for _ in 0..(BUFFER_NB * (SRC_NB as u32)) { + receiver.recv().unwrap(); + } + + pipeline_clone.set_state(gst::State::Null).unwrap(); + l_clone.quit(); + }); + + let bus = pipeline.get_bus().unwrap(); + let l_clone = l.clone(); + bus.add_watch(move |_, msg| { + use gst::MessageView; + + match msg.view() { + MessageView::StateChanged(state_changed) => { + if let Some(source) = state_changed.get_src() { + if source.get_type() == gst::Pipeline::static_type() + && state_changed.get_old() == gst::State::Paused + && state_changed.get_current() == gst::State::Playing + { + if let Some(test_scenario) = test_scenario.take() { + std::thread::spawn(test_scenario); + } + } + } + } + MessageView::Error(err) => { + gst_error!( + CAT, + "multiple_contexts_queue: Error from {:?}: {} ({:?})", + err.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + l_clone.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .unwrap(); + + pipeline.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "Starting main loop for multiple_contexts_queue..."); + l.run(); + gst_debug!(CAT, "Stopping main loop for multiple_contexts_queue..."); +} + +#[test] +fn multiple_contexts_proxy() { + use std::net; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + + init(); + + const CONTEXT_NB: u32 = 2; + const SRC_NB: u16 = 4; + const CONTEXT_WAIT: u32 = 1; + const BUFFER_NB: u32 = 3; + // Don't overlap with `multiple_contexts_queue` + const OFFSET: u16 = 10; + const FIRST_PORT: u16 = 40000 + OFFSET; + + let l = glib::MainLoop::new(None, false); + let pipeline = gst::Pipeline::new(None); + + let (sender, receiver) = mpsc::channel(); + + for i in 0..SRC_NB { + let pipeline_index = i + OFFSET; + + let src = gst::ElementFactory::make( + "ts-udpsrc", + Some(format!("src-{}", pipeline_index).as_str()), + ) + .unwrap(); + src.set_property("context", &format!("context-{}", (i as u32) % CONTEXT_NB)) + .unwrap(); + src.set_property("context-wait", &CONTEXT_WAIT).unwrap(); + src.set_property("port", &((FIRST_PORT + i) as u32)) + .unwrap(); + + let proxysink = gst::ElementFactory::make( + "ts-proxysink", + Some(format!("proxysink-{}", pipeline_index).as_str()), + ) + .unwrap(); + proxysink + .set_property("proxy-context", &format!("proxy-{}", pipeline_index)) + .unwrap(); + let proxysrc = gst::ElementFactory::make( + "ts-proxysrc", + Some(format!("proxysrc-{}", pipeline_index).as_str()), + ) + .unwrap(); + proxysrc + .set_property( + "context", + &format!("context-{}", (pipeline_index as u32) % CONTEXT_NB), + ) + .unwrap(); + proxysrc + .set_property("proxy-context", &format!("proxy-{}", pipeline_index)) + .unwrap(); + + let sink = + gst::ElementFactory::make("appsink", Some(format!("sink-{}", pipeline_index).as_str())) + .unwrap(); + sink.set_property("sync", &false).unwrap(); + sink.set_property("async", &false).unwrap(); + sink.set_property("emit-signals", &true).unwrap(); + + pipeline + .add_many(&[&src, &proxysink, &proxysrc, &sink]) + .unwrap(); + src.link(&proxysink).unwrap(); + proxysrc.link(&sink).unwrap(); + + let appsink = sink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let sender_clone = sender.clone(); + appsink.connect_new_sample(move |appsink| { + let _sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + sender_clone.send(()).unwrap(); + Ok(gst::FlowSuccess::Ok) + }); + } + + let pipeline_clone = pipeline.clone(); + let l_clone = l.clone(); + let mut test_scenario = Some(move || { + let buffer = [0; 160]; + let socket = net::UdpSocket::bind("0.0.0.0:0").unwrap(); + + let ipaddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let destinations = (FIRST_PORT..(FIRST_PORT + SRC_NB)) + .map(|port| SocketAddr::new(ipaddr, port)) + .collect::<Vec<_>>(); + + for _ in 0..BUFFER_NB { + for dest in &destinations { + gst_debug!(CAT, "multiple_contexts_proxy: sending buffer to {:?}", dest); + socket.send_to(&buffer, dest).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(CONTEXT_WAIT as u64)); + } + } + + gst_debug!( + CAT, + "multiple_contexts_proxy: waiting for all buffers notifications" + ); + for _ in 0..(BUFFER_NB * (SRC_NB as u32)) { + receiver.recv().unwrap(); + } + + pipeline_clone.set_state(gst::State::Null).unwrap(); + l_clone.quit(); + }); + + let bus = pipeline.get_bus().unwrap(); + let l_clone = l.clone(); + bus.add_watch(move |_, msg| { + use gst::MessageView; + + match msg.view() { + MessageView::StateChanged(state_changed) => { + if let Some(source) = state_changed.get_src() { + if source.get_type() == gst::Pipeline::static_type() + && state_changed.get_old() == gst::State::Paused + && state_changed.get_current() == gst::State::Playing + { + if let Some(test_scenario) = test_scenario.take() { + std::thread::spawn(test_scenario); + } + } + } + } + MessageView::Error(err) => { + gst_error!( + CAT, + "multiple_contexts_proxy: Error from {:?}: {} ({:?})", + err.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + l_clone.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .unwrap(); + + pipeline.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "Starting main loop for multiple_contexts_proxy..."); + l.run(); + gst_debug!(CAT, "Stopping main loop for multiple_contexts_proxy..."); +} + +#[test] +fn eos() { + const CONTEXT: &str = "test_eos"; + + init(); + + let l = glib::MainLoop::new(None, false); + let pipeline = gst::Pipeline::new(None); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + + let src = gst::ElementFactory::make("ts-appsrc", Some("src-eos")).unwrap(); + src.set_property("caps", &caps).unwrap(); + src.set_property("do-timestamp", &true).unwrap(); + src.set_property("context", &CONTEXT).unwrap(); + + let queue = gst::ElementFactory::make("ts-queue", Some("queue-eos")).unwrap(); + queue.set_property("context", &CONTEXT).unwrap(); + + let appsink = gst::ElementFactory::make("appsink", Some("sink-eos")).unwrap(); + + pipeline.add_many(&[&src, &queue, &appsink]).unwrap(); + gst::Element::link_many(&[&src, &queue, &appsink]).unwrap(); + + appsink.set_property("sync", &false).unwrap(); + appsink.set_property("async", &false).unwrap(); + + appsink.set_property("emit-signals", &true).unwrap(); + let (sample_notifier, sample_notif_rcv) = mpsc::channel(); + let (eos_notifier, eos_notif_rcv) = mpsc::channel(); + let appsink = appsink.dynamic_cast::<gst_app::AppSink>().unwrap(); + appsink.connect_new_sample(move |appsink| { + gst_debug!(CAT, obj: appsink, "eos: pulling sample"); + let _ = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + sample_notifier.send(()).unwrap(); + + Ok(gst::FlowSuccess::Ok) + }); + + appsink.connect_eos(move |_appsink| eos_notifier.send(()).unwrap()); + + fn push_buffer(src: &gst::Element) -> bool { + gst_debug!(CAT, obj: src, "eos: pushing buffer"); + src.emit("push-buffer", &[&gst::Buffer::from_slice(vec![0; 1024])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap() + } + + let pipeline_clone = pipeline.clone(); + let l_clone = l.clone(); + let mut scenario = Some(move || { + // Initialize the dataflow + assert!(push_buffer(&src)); + + sample_notif_rcv.recv().unwrap(); + + assert!(src + .emit("end-of-stream", &[]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap()); + + eos_notif_rcv.recv().unwrap(); + + assert!(push_buffer(&src)); + std::thread::sleep(std::time::Duration::from_millis(50)); + assert_eq!( + sample_notif_rcv.try_recv().unwrap_err(), + mpsc::TryRecvError::Empty + ); + + pipeline_clone.set_state(gst::State::Null).unwrap(); + l_clone.quit(); + }); + + let l_clone = l.clone(); + pipeline + .get_bus() + .unwrap() + .add_watch(move |_, msg| { + use gst::MessageView; + + match msg.view() { + MessageView::StateChanged(state_changed) => { + if let Some(source) = state_changed.get_src() { + if source.get_type() != gst::Pipeline::static_type() { + return glib::Continue(true); + } + if state_changed.get_old() == gst::State::Paused + && state_changed.get_current() == gst::State::Playing + { + if let Some(scenario) = scenario.take() { + std::thread::spawn(scenario); + } + } + } + } + MessageView::Error(err) => { + gst_error!( + CAT, + "eos: Error from {:?}: {} ({:?})", + err.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + l_clone.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .unwrap(); + + pipeline.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "Starting main loop for eos..."); + l.run(); + gst_debug!(CAT, "Stopping main loop for eos..."); +} + +#[test] +fn premature_shutdown() { + init(); + + const APPSRC_CONTEXT_WAIT: u32 = 0; + const QUEUE_CONTEXT_WAIT: u32 = 1; + const QUEUE_ITEMS_CAPACITY: u32 = 1; + + let l = glib::MainLoop::new(None, false); + let pipeline = gst::Pipeline::new(None); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + + let src = gst::ElementFactory::make("ts-appsrc", Some("src-ps")).unwrap(); + src.set_property("caps", &caps).unwrap(); + src.set_property("do-timestamp", &true).unwrap(); + src.set_property("context", &"appsrc-context").unwrap(); + src.set_property("context-wait", &APPSRC_CONTEXT_WAIT) + .unwrap(); + + let queue = gst::ElementFactory::make("ts-queue", Some("queue-ps")).unwrap(); + queue.set_property("context", &"queue-context").unwrap(); + queue + .set_property("context-wait", &QUEUE_CONTEXT_WAIT) + .unwrap(); + queue + .set_property("max-size-buffers", &QUEUE_ITEMS_CAPACITY) + .unwrap(); + + let appsink = gst::ElementFactory::make("appsink", Some("sink-ps")).unwrap(); + + pipeline.add_many(&[&src, &queue, &appsink]).unwrap(); + gst::Element::link_many(&[&src, &queue, &appsink]).unwrap(); + + appsink.set_property("emit-signals", &true).unwrap(); + appsink.set_property("sync", &false).unwrap(); + appsink.set_property("async", &false).unwrap(); + + let (appsink_sender, appsink_receiver) = mpsc::channel(); + + let appsink = appsink.dynamic_cast::<gst_app::AppSink>().unwrap(); + appsink.connect_new_sample(move |appsink| { + gst_debug!(CAT, obj: appsink, "premature_shutdown: pulling sample"); + let _sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + appsink_sender.send(()).unwrap(); + + Ok(gst::FlowSuccess::Ok) + }); + + fn push_buffer(src: &gst::Element, intent: &str) -> bool { + gst_debug!( + CAT, + obj: src, + "premature_shutdown: pushing buffer {}", + intent + ); + src.emit("push-buffer", &[&gst::Buffer::from_slice(vec![0; 1024])]) + .unwrap() + .unwrap() + .get_some::<bool>() + .unwrap() + } + + let pipeline_clone = pipeline.clone(); + let l_clone = l.clone(); + let mut scenario = Some(move || { + gst_debug!(CAT, "premature_shutdown: STEP 1: Playing"); + // Initialize the dataflow + assert!(push_buffer(&src, "(initial)")); + + // Wait for the buffer to reach AppSink + appsink_receiver.recv().unwrap(); + assert_eq!( + appsink_receiver.try_recv().unwrap_err(), + mpsc::TryRecvError::Empty + ); + + assert!(push_buffer(&src, "before Playing -> Paused")); + + gst_debug!(CAT, "premature_shutdown: STEP 2: Playing -> Paused"); + pipeline_clone.set_state(gst::State::Paused).unwrap(); + + gst_debug!(CAT, "premature_shutdown: STEP 3: Paused -> Playing"); + pipeline_clone.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "premature_shutdown: Playing again"); + + gst_debug!(CAT, "Waiting for buffer sent before Playing -> Paused"); + appsink_receiver.recv().unwrap(); + + assert!(push_buffer(&src, "after Paused -> Playing")); + gst_debug!(CAT, "Waiting for buffer sent after Paused -> Playing"); + appsink_receiver.recv().unwrap(); + + // Fill up the (dataqueue) and abruptly shutdown + assert!(push_buffer(&src, "filling 1")); + assert!(push_buffer(&src, "filling 2")); + + gst_debug!(CAT, "premature_shutdown: STEP 4: Playing -> Null"); + + pipeline_clone.set_state(gst::State::Null).unwrap(); + + assert!(!push_buffer(&src, "after Null")); + + l_clone.quit(); + }); + + let l_clone = l.clone(); + pipeline + .get_bus() + .unwrap() + .add_watch(move |_, msg| { + use gst::MessageView; + + match msg.view() { + MessageView::StateChanged(state_changed) => { + if let Some(source) = state_changed.get_src() { + if source.get_type() != gst::Pipeline::static_type() { + return glib::Continue(true); + } + if state_changed.get_old() == gst::State::Paused + && state_changed.get_current() == gst::State::Playing + { + if let Some(scenario) = scenario.take() { + std::thread::spawn(scenario); + } + } + } + } + MessageView::Error(err) => { + gst_error!( + CAT, + "premature_shutdown: Error from {:?}: {} ({:?})", + err.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + l_clone.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .unwrap(); + + pipeline.set_state(gst::State::Playing).unwrap(); + + gst_debug!(CAT, "Starting main loop for premature_shutdown..."); + l.run(); + gst_debug!(CAT, "Stopped main loop for premature_shutdown..."); +} diff --git a/generic/gst-plugin-threadshare/tests/proxy.rs b/generic/gst-plugin-threadshare/tests/proxy.rs new file mode 100644 index 000000000..b593b16eb --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/proxy.rs @@ -0,0 +1,167 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib::prelude::*; + +use gst; +use gst::prelude::*; + +use gst_app; + +use std::sync::{Arc, Mutex}; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare proxy test"); + }); +} + +#[test] +fn test_push() { + init(); + + let pipeline = gst::Pipeline::new(None); + let fakesrc = gst::ElementFactory::make("fakesrc", None).unwrap(); + let proxysink = gst::ElementFactory::make("ts-proxysink", None).unwrap(); + let proxysrc = gst::ElementFactory::make("ts-proxysrc", None).unwrap(); + let appsink = gst::ElementFactory::make("appsink", None).unwrap(); + + pipeline + .add_many(&[&fakesrc, &proxysink, &proxysrc, &appsink]) + .unwrap(); + fakesrc.link(&proxysink).unwrap(); + proxysrc.link(&appsink).unwrap(); + + fakesrc.set_property("num-buffers", &3i32).unwrap(); + proxysink.set_property("proxy-context", &"test1").unwrap(); + proxysrc.set_property("proxy-context", &"test1").unwrap(); + + appsink.set_property("emit-signals", &true).unwrap(); + + let samples = Arc::new(Mutex::new(Vec::new())); + + let appsink = appsink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let samples_clone = samples.clone(); + appsink.connect_new_sample(move |appsink| { + let sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + samples_clone.lock().unwrap().push(sample); + + Ok(gst::FlowSuccess::Ok) + }); + + pipeline.set_state(gst::State::Playing).unwrap(); + + let mut eos = false; + let bus = pipeline.get_bus().unwrap(); + while let Some(msg) = bus.timed_pop(5 * gst::SECOND) { + use gst::MessageView; + match msg.view() { + MessageView::Eos(..) => { + eos = true; + break; + } + MessageView::Error(err) => unreachable!("proxy::test_push {:?}", err), + _ => (), + } + } + + assert!(eos); + let samples = samples.lock().unwrap(); + assert_eq!(samples.len(), 3); + + for sample in samples.iter() { + assert!(sample.get_buffer().is_some()); + } + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_from_pipeline_to_pipeline() { + init(); + + let pipe_1 = gst::Pipeline::new(None); + let fakesrc = gst::ElementFactory::make("fakesrc", None).unwrap(); + let pxsink = gst::ElementFactory::make("ts-proxysink", None).unwrap(); + + let pipe_2 = gst::Pipeline::new(None); + let pxsrc = gst::ElementFactory::make("ts-proxysrc", None).unwrap(); + let fakesink = gst::ElementFactory::make("fakesink", None).unwrap(); + + pipe_1.add_many(&[&fakesrc, &pxsink]).unwrap(); + fakesrc.link(&pxsink).unwrap(); + + pipe_2.add_many(&[&pxsrc, &fakesink]).unwrap(); + pxsrc.link(&fakesink).unwrap(); + + pxsink.set_property("proxy-context", &"test2").unwrap(); + pxsrc.set_property("proxy-context", &"test2").unwrap(); + + pipe_1.set_state(gst::State::Paused).unwrap(); + pipe_2.set_state(gst::State::Paused).unwrap(); + + let _ = pipe_1.get_state(gst::CLOCK_TIME_NONE); + let _ = pipe_2.get_state(gst::CLOCK_TIME_NONE); + + pipe_1.set_state(gst::State::Null).unwrap(); + + pipe_2.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_from_pipeline_to_pipeline_and_back() { + init(); + + let pipe_1 = gst::Pipeline::new(None); + let pxsrc_1 = gst::ElementFactory::make("ts-proxysrc", None).unwrap(); + let pxsink_1 = gst::ElementFactory::make("ts-proxysink", None).unwrap(); + + let pipe_2 = gst::Pipeline::new(None); + let pxsrc_2 = gst::ElementFactory::make("ts-proxysrc", None).unwrap(); + let pxsink_2 = gst::ElementFactory::make("ts-proxysink", None).unwrap(); + + pipe_1.add_many(&[&pxsrc_1, &pxsink_1]).unwrap(); + pxsrc_1.link(&pxsink_1).unwrap(); + + pipe_2.add_many(&[&pxsrc_2, &pxsink_2]).unwrap(); + pxsrc_2.link(&pxsink_2).unwrap(); + + pxsrc_1.set_property("proxy-context", &"test3").unwrap(); + pxsink_2.set_property("proxy-context", &"test3").unwrap(); + + pxsrc_2.set_property("proxy-context", &"test4").unwrap(); + pxsink_1.set_property("proxy-context", &"test4").unwrap(); + + pipe_1.set_state(gst::State::Paused).unwrap(); + pipe_2.set_state(gst::State::Paused).unwrap(); + + pipe_1.set_state(gst::State::Null).unwrap(); + pipe_2.set_state(gst::State::Null).unwrap(); +} diff --git a/generic/gst-plugin-threadshare/tests/queue.rs b/generic/gst-plugin-threadshare/tests/queue.rs new file mode 100644 index 000000000..c170516fe --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/queue.rs @@ -0,0 +1,100 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib; +use glib::prelude::*; + +use gst; +use gst::prelude::*; + +use gst_app; + +use std::sync::{Arc, Mutex}; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare queue test"); + }); +} + +#[test] +fn test_push() { + init(); + + let pipeline = gst::Pipeline::new(None); + let fakesrc = gst::ElementFactory::make("fakesrc", None).unwrap(); + let queue = gst::ElementFactory::make("ts-queue", None).unwrap(); + let appsink = gst::ElementFactory::make("appsink", None).unwrap(); + + pipeline.add_many(&[&fakesrc, &queue, &appsink]).unwrap(); + fakesrc.link(&queue).unwrap(); + queue.link(&appsink).unwrap(); + + fakesrc.set_property("num-buffers", &3i32).unwrap(); + + appsink.set_property("emit-signals", &true).unwrap(); + + let samples = Arc::new(Mutex::new(Vec::new())); + + let appsink = appsink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let samples_clone = samples.clone(); + appsink.connect_new_sample(move |appsink| { + let sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + samples_clone.lock().unwrap().push(sample); + + Ok(gst::FlowSuccess::Ok) + }); + + pipeline.set_state(gst::State::Playing).unwrap(); + + let mut eos = false; + let bus = pipeline.get_bus().unwrap(); + while let Some(msg) = bus.timed_pop(5 * gst::SECOND) { + use gst::MessageView; + match msg.view() { + MessageView::Eos(..) => { + eos = true; + break; + } + MessageView::Error(..) => unreachable!(), + _ => (), + } + } + + assert!(eos); + let samples = samples.lock().unwrap(); + assert_eq!(samples.len(), 3); + + for sample in samples.iter() { + assert!(sample.get_buffer().is_some()); + } + + pipeline.set_state(gst::State::Null).unwrap(); +} diff --git a/generic/gst-plugin-threadshare/tests/tcpclientsrc.rs b/generic/gst-plugin-threadshare/tests/tcpclientsrc.rs new file mode 100644 index 000000000..7498dcc7e --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/tcpclientsrc.rs @@ -0,0 +1,124 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// Copyright (C) 2018 LEE Dongjun <redongjun@gmail.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib; +use glib::prelude::*; + +use gst; +use gst::prelude::*; + +use gst_app; + +use std::io::Write; +use std::sync::{Arc, Mutex}; +use std::{thread, time}; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare tcpclientsrc test"); + }); +} + +#[test] +fn test_push() { + init(); + + let handler = thread::spawn(move || { + use std::net; + + let listener = net::TcpListener::bind("0.0.0.0:5000").unwrap(); + let stream = listener.incoming().next().unwrap(); + let buffer = [0; 160]; + let mut socket = stream.unwrap(); + for _ in 0..3 { + let _ = socket.write(&buffer); + thread::sleep(time::Duration::from_millis(20)); + } + }); + + let pipeline = gst::Pipeline::new(None); + + let tcpclientsrc = gst::ElementFactory::make("ts-tcpclientsrc", None).unwrap(); + let appsink = gst::ElementFactory::make("appsink", None).unwrap(); + appsink.set_property("sync", &false).unwrap(); + appsink.set_property("async", &false).unwrap(); + + pipeline.add_many(&[&tcpclientsrc, &appsink]).unwrap(); + tcpclientsrc.link(&appsink).unwrap(); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + tcpclientsrc.set_property("caps", &caps).unwrap(); + tcpclientsrc.set_property("port", &(5000u32)).unwrap(); + + appsink.set_property("emit-signals", &true).unwrap(); + + let samples = Arc::new(Mutex::new(Vec::new())); + + let appsink = appsink.dynamic_cast::<gst_app::AppSink>().unwrap(); + let samples_clone = samples.clone(); + appsink.connect_new_sample(move |appsink| { + let sample = appsink + .emit("pull-sample", &[]) + .unwrap() + .unwrap() + .get::<gst::Sample>() + .unwrap() + .unwrap(); + + let mut samples = samples_clone.lock().unwrap(); + samples.push(sample); + Ok(gst::FlowSuccess::Ok) + }); + + pipeline.set_state(gst::State::Playing).unwrap(); + + let mut eos = false; + let bus = pipeline.get_bus().unwrap(); + while let Some(msg) = bus.timed_pop(5 * gst::SECOND) { + use gst::MessageView; + match msg.view() { + MessageView::Eos(..) => { + eos = true; + break; + } + MessageView::Error(..) => unreachable!(), + _ => (), + } + } + + assert!(eos); + let samples = samples.lock().unwrap(); + for sample in samples.iter() { + assert_eq!(Some(caps.as_ref()), sample.get_caps()); + } + + let total_received_size = samples.iter().fold(0, |acc, sample| { + acc + sample.get_buffer().unwrap().get_size() + }); + assert_eq!(total_received_size, 3 * 160); + + pipeline.set_state(gst::State::Null).unwrap(); + + handler.join().unwrap(); +} diff --git a/generic/gst-plugin-threadshare/tests/udpsink.rs b/generic/gst-plugin-threadshare/tests/udpsink.rs new file mode 100644 index 000000000..420b34fe2 --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/udpsink.rs @@ -0,0 +1,160 @@ +// Copyright (C) 2019 Mathieu Duponchelle <mathieu@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use std::thread; + +use glib; +use glib::prelude::*; + +use gst; +use gst_check; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare udpsrc test"); + }); +} + +#[test] +fn test_client_management() { + init(); + + let h = gst_check::Harness::new("ts-udpsink"); + let udpsink = h.get_element().unwrap(); + + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + + assert_eq!(clients, "127.0.0.1:5000"); + + udpsink.emit("add", &[&"192.168.1.1", &57]).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, "127.0.0.1:5000,192.168.1.1:57"); + + /* Adding a client twice is not supported */ + udpsink.emit("add", &[&"192.168.1.1", &57]).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, "127.0.0.1:5000,192.168.1.1:57"); + + udpsink.emit("remove", &[&"192.168.1.1", &57]).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, "127.0.0.1:5000"); + + /* Removing a non-existing client should not be a problem */ + udpsink.emit("remove", &[&"192.168.1.1", &57]).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, "127.0.0.1:5000"); + + /* While the default host:address client is listed in clients, + * it can't be removed with the remove signal */ + udpsink.emit("remove", &[&"127.0.0.1", &5000]).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, "127.0.0.1:5000"); + + /* It is however possible to remove the default client by setting + * host to None */ + let host: Option<String> = None; + udpsink.set_property("host", &host).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, ""); + + /* The client properties is writable too */ + udpsink + .set_property("clients", &"127.0.0.1:5000,192.168.1.1:57") + .unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, "127.0.0.1:5000,192.168.1.1:57"); + + udpsink.emit("clear", &[]).unwrap(); + let clients = udpsink + .get_property("clients") + .unwrap() + .get::<String>() + .unwrap() + .unwrap(); + assert_eq!(clients, ""); +} + +#[test] +fn test_chain() { + init(); + + let mut h = gst_check::Harness::new("ts-udpsink"); + h.set_src_caps_str(&"foo/bar"); + + thread::spawn(move || { + use std::net; + use std::time; + + thread::sleep(time::Duration::from_millis(50)); + + let socket = net::UdpSocket::bind("127.0.0.1:5000").unwrap(); + let mut buf = [0; 5]; + let (amt, _) = socket.recv_from(&mut buf).unwrap(); + + assert!(amt == 4); + assert!(buf == [42, 43, 44, 45, 0]); + }); + + let buf = gst::Buffer::from_slice(&[42, 43, 44, 45]); + assert!(h.push(buf) == Ok(gst::FlowSuccess::Ok)); +} diff --git a/generic/gst-plugin-threadshare/tests/udpsrc.rs b/generic/gst-plugin-threadshare/tests/udpsrc.rs new file mode 100644 index 000000000..eadfac743 --- /dev/null +++ b/generic/gst-plugin-threadshare/tests/udpsrc.rs @@ -0,0 +1,174 @@ +// Copyright (C) 2018 Sebastian Dröge <sebastian@centricular.com> +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Library General Public +// License as published by the Free Software Foundation; either +// version 2 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Library General Public License for more details. +// +// You should have received a copy of the GNU Library General Public +// License along with this library; if not, write to the +// Free Software Foundation, Inc., 51 Franklin Street, Suite 500, +// Boston, MA 02110-1335, USA. + +use glib; +use glib::prelude::*; + +use gio; + +use gst; +use gst_check; + +use std::thread; + +use gstthreadshare; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstthreadshare::plugin_register_static().expect("gstthreadshare udpsrc test"); + }); +} + +#[test] +fn test_push() { + init(); + + let mut h = gst_check::Harness::new("ts-udpsrc"); + + let caps = gst::Caps::new_simple("foo/bar", &[]); + { + let udpsrc = h.get_element().unwrap(); + udpsrc.set_property("caps", &caps).unwrap(); + udpsrc.set_property("port", &(5000 as u32)).unwrap(); + udpsrc.set_property("context", &"test-push").unwrap(); + } + + h.play(); + + thread::spawn(move || { + use std::net; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::time; + + // Sleep 50ms to allow for the udpsrc to be ready to actually receive data + thread::sleep(time::Duration::from_millis(50)); + + let buffer = [0; 160]; + let socket = net::UdpSocket::bind("0.0.0.0:0").unwrap(); + + let ipaddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let dest = SocketAddr::new(ipaddr, 5000 as u16); + + for _ in 0..3 { + socket.send_to(&buffer, dest).unwrap(); + } + }); + + for _ in 0..3 { + let buffer = h.pull().unwrap(); + assert_eq!(buffer.get_size(), 160); + } + + let mut n_events = 0; + loop { + use gst::EventView; + + let event = h.pull_event().unwrap(); + match event.view() { + EventView::StreamStart(..) => { + assert_eq!(n_events, 0); + } + EventView::Caps(ev) => { + assert_eq!(n_events, 1); + let event_caps = ev.get_caps(); + assert_eq!(caps.as_ref(), event_caps); + } + EventView::Segment(..) => { + assert_eq!(n_events, 2); + break; + } + _ => (), + } + n_events += 1; + } + assert!(n_events >= 2); +} + +#[test] +#[cfg(not(windows))] +fn test_socket_reuse() { + init(); + + let mut ts_src_h = gst_check::Harness::new("ts-udpsrc"); + let mut sink_h = gst_check::Harness::new("udpsink"); + let mut ts_src_h2 = gst_check::Harness::new("ts-udpsrc"); + + { + let udpsrc = ts_src_h.get_element().unwrap(); + udpsrc.set_property("port", &(6000 as u32)).unwrap(); + udpsrc + .set_property("context", &"test-socket-reuse") + .unwrap(); + } + ts_src_h.play(); + + { + let udpsrc = ts_src_h.get_element().unwrap(); + let socket = udpsrc + .get_property("used-socket") + .unwrap() + .get::<gio::Socket>() + .unwrap(); + + let udpsink = sink_h.get_element().unwrap(); + udpsink.set_property("socket", &socket).unwrap(); + udpsink.set_property("host", &"127.0.0.1").unwrap(); + udpsink.set_property("port", &6001i32).unwrap(); + } + sink_h.play(); + sink_h.set_src_caps_str("application/test"); + + { + let udpsrc = ts_src_h2.get_element().unwrap(); + udpsrc.set_property("port", &(6001 as u32)).unwrap(); + udpsrc + .set_property("context", &"test-socket-reuse") + .unwrap(); + } + ts_src_h2.play(); + + thread::spawn(move || { + use std::net; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::time; + + // Sleep 50ms to allow for the udpsrc to be ready to actually receive data + thread::sleep(time::Duration::from_millis(50)); + + let buffer = [0; 160]; + let socket = net::UdpSocket::bind("0.0.0.0:0").unwrap(); + + let ipaddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + let dest = SocketAddr::new(ipaddr, 6000 as u16); + + for _ in 0..3 { + socket.send_to(&buffer, dest).unwrap(); + } + }); + + for _ in 0..3 { + let buffer = ts_src_h.pull().unwrap(); + sink_h.push(buffer).unwrap(); + let buffer = ts_src_h2.pull().unwrap(); + + assert_eq!(buffer.get_size(), 160); + } +} |