From 205b6040fbb918c0fa736874b09f8e3f3f261e44 Mon Sep 17 00:00:00 2001 From: Arun Raghavan Date: Fri, 3 Apr 2020 19:26:41 -0400 Subject: 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 --- utils/gst-plugin-fallbackswitch/Cargo.toml | 46 + utils/gst-plugin-fallbackswitch/build.rs | 42 + .../examples/gtk_fallbackswitch.rs | 235 ++ .../src/base/aggregator.rs | 95 + .../src/base/aggregator_pad.rs | 28 + .../src/base/auto/aggregator.rs | 190 ++ .../src/base/auto/aggregator_pad.rs | 182 + .../gst-plugin-fallbackswitch/src/base/auto/mod.rs | 13 + .../src/base/gstaggregator.c | 3465 ++++++++++++++++++++ .../src/base/gstaggregator.h | 393 +++ utils/gst-plugin-fallbackswitch/src/base/mod.rs | 27 + .../src/base/subclass/aggregator.rs | 1042 ++++++ .../src/base/subclass/aggregator_pad.rs | 147 + .../src/base/subclass/mod.rs | 17 + utils/gst-plugin-fallbackswitch/src/base/sys.rs | 235 ++ utils/gst-plugin-fallbackswitch/src/base/utils.rs | 30 + .../src/fallbackswitch.rs | 794 +++++ utils/gst-plugin-fallbackswitch/src/lib.rs | 62 + .../tests/fallbackswitch.rs | 570 ++++ utils/gst-plugin-togglerecord/Cargo.toml | 34 + utils/gst-plugin-togglerecord/LICENSE | 502 +++ utils/gst-plugin-togglerecord/build.rs | 5 + .../examples/gtk_recording.rs | 358 ++ utils/gst-plugin-togglerecord/src/lib.rs | 48 + utils/gst-plugin-togglerecord/src/togglerecord.rs | 1749 ++++++++++ utils/gst-plugin-togglerecord/tests/tests.rs | 1173 +++++++ 26 files changed, 11482 insertions(+) create mode 100644 utils/gst-plugin-fallbackswitch/Cargo.toml create mode 100644 utils/gst-plugin-fallbackswitch/build.rs create mode 100644 utils/gst-plugin-fallbackswitch/examples/gtk_fallbackswitch.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/aggregator.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/aggregator_pad.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/auto/aggregator.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/auto/aggregator_pad.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/auto/mod.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/gstaggregator.c create mode 100644 utils/gst-plugin-fallbackswitch/src/base/gstaggregator.h create mode 100644 utils/gst-plugin-fallbackswitch/src/base/mod.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator_pad.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/subclass/mod.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/sys.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/base/utils.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/fallbackswitch.rs create mode 100644 utils/gst-plugin-fallbackswitch/src/lib.rs create mode 100644 utils/gst-plugin-fallbackswitch/tests/fallbackswitch.rs create mode 100644 utils/gst-plugin-togglerecord/Cargo.toml create mode 100644 utils/gst-plugin-togglerecord/LICENSE create mode 100644 utils/gst-plugin-togglerecord/build.rs create mode 100644 utils/gst-plugin-togglerecord/examples/gtk_recording.rs create mode 100644 utils/gst-plugin-togglerecord/src/lib.rs create mode 100644 utils/gst-plugin-togglerecord/src/togglerecord.rs create mode 100644 utils/gst-plugin-togglerecord/tests/tests.rs (limited to 'utils') diff --git a/utils/gst-plugin-fallbackswitch/Cargo.toml b/utils/gst-plugin-fallbackswitch/Cargo.toml new file mode 100644 index 000000000..bb6a8749a --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "gst-plugin-fallbackswitch" +version = "0.6.0" +authors = ["Sebastian Dröge "] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +license = "LGPL-2.1+" +edition = "2018" +description = "Fallback Switcher Plugin" + +[dependencies] +libc = "0.2" +glib = { git = "https://github.com/gtk-rs/glib" } +glib-sys = { git = "https://github.com/gtk-rs/sys" } +gobject-sys = { git = "https://github.com/gtk-rs/sys" } +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"] } +gstreamer-base = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_16"], optional = true } +gstreamer-sys = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs-sys" } +gstreamer-audio = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"] } +gstreamer-video = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"] } +gtk = { git = "https://github.com/gtk-rs/gtk", optional = true } +gio = { git = "https://github.com/gtk-rs/gio", optional = true } +lazy_static = "1.0" + +[dev-dependencies] +gstreamer-check = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"]} +gstreamer-app = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_14"]} +lazy_static = "1.2" + +[lib] +name = "gstfallbackswitch" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[[example]] +name = "gtk-fallbackswitch" +path = "examples/gtk_fallbackswitch.rs" +required-features = ["gtk", "gio"] + +[build-dependencies] +gst-plugin-version-helper = { path="../../gst-plugin-version-helper" } +cc = "1.0" +pkg-config = "0.3" + +[features] +default = [] +#v1_18 = ["gstreamer/v1_18", "gstreamer-base"] diff --git a/utils/gst-plugin-fallbackswitch/build.rs b/utils/gst-plugin-fallbackswitch/build.rs new file mode 100644 index 000000000..0ff81ccba --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/build.rs @@ -0,0 +1,42 @@ +extern crate cc; +extern crate gst_plugin_version_helper; +extern crate pkg_config; + +fn main() { + gst_plugin_version_helper::get_info(); + + if cfg!(feature = "v1_18") { + return; + } + + let gstreamer = pkg_config::probe_library("gstreamer-1.0").unwrap(); + let includes = [gstreamer.include_paths]; + + let files = ["src/base/gstaggregator.c"]; + + let mut build = cc::Build::new(); + build.include("src/base"); + + for f in files.iter() { + build.file(f); + } + + for p in includes.iter().flatten() { + build.include(p); + } + + build.define( + "PACKAGE_BUGREPORT", + "\"https://gitlab.freedesktop.org/gstreamer/gstreamer/issues/new\"", + ); + build.extra_warnings(false); + build.define("GstAggregator", "GstAggregatorFallback"); + build.define("GstAggregatorClass", "GstAggregatorFallbackClass"); + build.define("GstAggregatorPrivate", "GstAggregatorFallbackPrivate"); + build.define("GstAggregatorPad", "GstAggregatorFallbackPad"); + build.define("GstAggregatorPadClass", "GstAggregatorFallbackPadClass"); + build.define("GstAggregatorPadPrivate", "GstAggregatorFallbackPadPrivate"); + build.define("GST_BASE_API", "G_GNUC_INTERNAL"); + + build.compile("libgstaggregator-c.a"); +} diff --git a/utils/gst-plugin-fallbackswitch/examples/gtk_fallbackswitch.rs b/utils/gst-plugin-fallbackswitch/examples/gtk_fallbackswitch.rs new file mode 100644 index 000000000..ffb645422 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/examples/gtk_fallbackswitch.rs @@ -0,0 +1,235 @@ +// Copyright (C) 2019 Sebastian Dröge +// +// 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. + +extern crate glib; +use glib::prelude::*; +extern crate gio; +use gio::prelude::*; + +extern crate gstreamer as gst; +use gst::prelude::*; + +extern crate gstfallbackswitch; + +extern crate gtk; +use gtk::prelude::*; +use std::cell::RefCell; +use std::env; + +const MAIN_PIPELINE: &str = "videotestsrc is-live=true pattern=ball"; +const FALLBACK_PIPELINE: &str = "videotestsrc is-live=true pattern=snow"; + +//const MAIN_PIPELINE: &str = "videotestsrc is-live=true pattern=ball ! x264enc tune=zerolatency"; +//const FALLBACK_PIPELINE: &str = "videotestsrc is-live=true pattern=snow ! x264enc tune=zerolatency"; + +fn create_pipeline() -> (gst::Pipeline, gst::Pad, gst::Element, gtk::Widget) { + let pipeline = gst::Pipeline::new(None); + + let video_src = gst::parse_bin_from_description(MAIN_PIPELINE, true) + .unwrap() + .upcast(); + let fallback_video_src = gst::parse_bin_from_description(&FALLBACK_PIPELINE, true) + .unwrap() + .upcast(); + + let fallbackswitch = gst::ElementFactory::make("fallbackswitch", None).unwrap(); + fallbackswitch + .set_property("timeout", &gst::SECOND) + .unwrap(); + + let decodebin = gst::ElementFactory::make("decodebin", None).unwrap(); + let videoconvert = gst::ElementFactory::make("videoconvert", None).unwrap(); + + let videoconvert_clone = videoconvert.clone(); + decodebin.connect_pad_added(move |_, pad| { + let caps = pad.get_current_caps().unwrap(); + let s = caps.get_structure(0).unwrap(); + + let sinkpad = videoconvert_clone.get_static_pad("sink").unwrap(); + + if s.get_name() == "video/x-raw" && !sinkpad.is_linked() { + pad.link(&sinkpad).unwrap(); + } + }); + + let (video_sink, video_widget) = + //if let Some(gtkglsink) = gst::ElementFactory::make("gtkglsink", None) { + // let glsinkbin = gst::ElementFactory::make("glsinkbin", None).unwrap(); + // glsinkbin.set_property("sink", >kglsink).unwrap(); + + // let widget = gtkglsink.get_property("widget").unwrap(); + // (glsinkbin, widget.get::().unwrap().unwrap()) + //} else + { + let sink = gst::ElementFactory::make("gtksink", None).unwrap(); + let widget = sink.get_property("widget").unwrap(); + (sink, widget.get::().unwrap().unwrap()) + }; + + pipeline + .add_many(&[ + &video_src, + &fallback_video_src, + &fallbackswitch, + &decodebin, + &videoconvert, + &video_sink, + ]) + .unwrap(); + + video_src + .link_pads(Some("src"), &fallbackswitch, Some("sink")) + .unwrap(); + fallback_video_src + .link_pads(Some("src"), &fallbackswitch, Some("fallback_sink")) + .unwrap(); + fallbackswitch + .link_pads(Some("src"), &decodebin, Some("sink")) + .unwrap(); + videoconvert + .link_pads(Some("src"), &video_sink, Some("sink")) + .unwrap(); + + ( + pipeline, + video_src.get_static_pad("src").unwrap(), + video_sink, + video_widget, + ) +} + +fn create_ui(app: >k::Application) { + let (pipeline, video_src_pad, video_sink, video_widget) = create_pipeline(); + + let window = gtk::Window::new(gtk::WindowType::Toplevel); + window.set_default_size(320, 240); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.pack_start(&video_widget, true, true, 0); + + let position_label = gtk::Label::new(Some("Position: 00:00:00")); + vbox.pack_start(&position_label, true, true, 5); + + let drop_button = gtk::ToggleButton::new_with_label("Drop Signal"); + vbox.pack_start(&drop_button, true, true, 5); + + window.add(&vbox); + window.show_all(); + + app.add_window(&window); + + let video_sink_weak = video_sink.downgrade(); + let timeout_id = gtk::timeout_add(100, move || { + let video_sink = match video_sink_weak.upgrade() { + Some(video_sink) => video_sink, + None => return glib::Continue(true), + }; + + let position = video_sink + .query_position::() + .unwrap_or_else(|| 0.into()); + position_label.set_text(&format!("Position: {:.1}", position)); + + glib::Continue(true) + }); + + let video_src_pad_weak = video_src_pad.downgrade(); + let drop_id = RefCell::new(None); + drop_button.connect_toggled(move |drop_button| { + let video_src_pad = match video_src_pad_weak.upgrade() { + Some(video_src_pad) => video_src_pad, + None => return, + }; + + let drop = drop_button.get_active(); + if drop { + let mut drop_id = drop_id.borrow_mut(); + if drop_id.is_none() { + *drop_id = video_src_pad + .add_probe(gst::PadProbeType::BUFFER, |_, _| gst::PadProbeReturn::Drop); + } + } else if let Some(drop_id) = drop_id.borrow_mut().take() { + video_src_pad.remove_probe(drop_id); + } + }); + + let app_weak = app.downgrade(); + window.connect_delete_event(move |_, _| { + let app = match app_weak.upgrade() { + Some(app) => app, + None => return Inhibit(false), + }; + + app.quit(); + Inhibit(false) + }); + + let bus = pipeline.get_bus().unwrap(); + let app_weak = app.downgrade(); + bus.add_watch_local(move |_, msg| { + use gst::MessageView; + + let app = match app_weak.upgrade() { + Some(app) => app, + None => return glib::Continue(false), + }; + + match msg.view() { + MessageView::Eos(..) => app.quit(), + MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + msg.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + app.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .expect("Failed to add bus watch"); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Pipeline reference is owned by the closure below, so will be + // destroyed once the app is destroyed + let timeout_id = RefCell::new(Some(timeout_id)); + app.connect_shutdown(move |_| { + pipeline.set_state(gst::State::Null).unwrap(); + + bus.remove_watch().unwrap(); + + if let Some(timeout_id) = timeout_id.borrow_mut().take() { + glib::source_remove(timeout_id); + } + }); +} + +fn main() { + gst::init().unwrap(); + gtk::init().unwrap(); + + gstfallbackswitch::plugin_register_static().expect("Failed to register fallbackswitch plugin"); + + let app = gtk::Application::new(None, gio::ApplicationFlags::FLAGS_NONE).unwrap(); + + app.connect_activate(create_ui); + let args = env::args().collect::>(); + app.run(&args); +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/aggregator.rs b/utils/gst-plugin-fallbackswitch/src/base/aggregator.rs new file mode 100644 index 000000000..d6ac7fa60 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/aggregator.rs @@ -0,0 +1,95 @@ +// Copyright (C) 2018 Sebastian Dröge +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use super::gst_base_sys; +use super::Aggregator; +use glib::prelude::*; +use glib::signal::{connect_raw, SignalHandlerId}; +use glib::translate::*; +use glib::IsA; +use glib::Value; +use gst; +use std::boxed::Box as Box_; +use std::mem::transmute; + +pub trait AggregatorExtManual: 'static { + fn finish_buffer(&self, buffer: gst::Buffer) -> Result; + fn get_property_min_upstream_latency(&self) -> gst::ClockTime; + + fn set_property_min_upstream_latency(&self, min_upstream_latency: gst::ClockTime); + + fn connect_property_min_upstream_latency_notify( + &self, + f: F, + ) -> SignalHandlerId; +} + +impl> AggregatorExtManual for O { + fn finish_buffer(&self, buffer: gst::Buffer) -> Result { + let ret: gst::FlowReturn = unsafe { + from_glib(gst_base_sys::gst_aggregator_finish_buffer( + self.as_ref().to_glib_none().0, + buffer.into_ptr(), + )) + }; + ret.into_result() + } + + fn get_property_min_upstream_latency(&self) -> gst::ClockTime { + unsafe { + let mut value = Value::from_type(::static_type()); + gobject_sys::g_object_get_property( + self.to_glib_none().0 as *mut gobject_sys::GObject, + b"min-upstream-latency\0".as_ptr() as *const _, + value.to_glib_none_mut().0, + ); + value + .get() + .expect("AggregatorExtManual::get_property_min_upstream_latency") + .unwrap() + } + } + + fn set_property_min_upstream_latency(&self, min_upstream_latency: gst::ClockTime) { + unsafe { + gobject_sys::g_object_set_property( + self.to_glib_none().0 as *mut gobject_sys::GObject, + b"min-upstream-latency\0".as_ptr() as *const _, + Value::from(&min_upstream_latency).to_glib_none().0, + ); + } + } + + fn connect_property_min_upstream_latency_notify( + &self, + f: F, + ) -> SignalHandlerId { + unsafe { + let f: Box_ = Box_::new(f); + connect_raw( + self.as_ptr() as *mut _, + b"notify::min-upstream-latency\0".as_ptr() as *const _, + Some(transmute( + notify_min_upstream_latency_trampoline:: as usize, + )), + Box_::into_raw(f), + ) + } + } +} + +unsafe extern "C" fn notify_min_upstream_latency_trampoline( + this: *mut gst_base_sys::GstAggregator, + _param_spec: glib_sys::gpointer, + f: glib_sys::gpointer, +) where + P: IsA, +{ + let f: &F = &*(f as *const F); + f(&Aggregator::from_glib_borrow(this).unsafe_cast_ref()) +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/aggregator_pad.rs b/utils/gst-plugin-fallbackswitch/src/base/aggregator_pad.rs new file mode 100644 index 000000000..eb57f6143 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/aggregator_pad.rs @@ -0,0 +1,28 @@ +// Copyright (C) 2018 Sebastian Dröge +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use super::gst_base_sys; +use super::AggregatorPad; +use glib::object::IsA; +use glib::translate::*; +use gst; +use gst_sys; + +pub trait AggregatorPadExtManual: 'static { + fn get_segment(&self) -> gst::Segment; +} + +impl> AggregatorPadExtManual for O { + fn get_segment(&self) -> gst::Segment { + unsafe { + let ptr: &gst_base_sys::GstAggregatorPad = &*(self.as_ptr() as *const _); + super::utils::MutexGuard::lock(&ptr.parent.object.lock); + from_glib_none(&ptr.segment as *const gst_sys::GstSegment) + } + } +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/auto/aggregator.rs b/utils/gst-plugin-fallbackswitch/src/base/auto/aggregator.rs new file mode 100644 index 000000000..912892eef --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/auto/aggregator.rs @@ -0,0 +1,190 @@ +// This file was generated by gir (https://github.com/gtk-rs/gir) +// from gir-files (https://github.com/gtk-rs/gir-files) +// DO NOT EDIT + +use super::super::gst_base_sys; +use glib::object::Cast; +use glib::object::IsA; +use glib::signal::connect_raw; +use glib::signal::SignalHandlerId; +use glib::translate::*; +use glib::StaticType; +use glib::Value; +use glib_sys; +use gobject_sys; +use gst; +use std::boxed::Box as Box_; +use std::mem::transmute; + +glib_wrapper! { + pub struct Aggregator(Object) @extends gst::Element, gst::Object; + + match fn { + get_type => || gst_base_sys::gst_aggregator_get_type(), + } +} + +unsafe impl Send for Aggregator {} +unsafe impl Sync for Aggregator {} + +pub const NONE_AGGREGATOR: Option<&Aggregator> = None; + +pub trait AggregatorExt: 'static { + fn get_buffer_pool(&self) -> Option; + + fn get_latency(&self) -> gst::ClockTime; + + fn set_latency(&self, min_latency: gst::ClockTime, max_latency: gst::ClockTime); + + fn set_src_caps(&self, caps: &gst::Caps); + + fn simple_get_next_time(&self) -> gst::ClockTime; + + fn get_property_start_time(&self) -> u64; + + fn set_property_start_time(&self, start_time: u64); + + fn connect_property_latency_notify( + &self, + f: F, + ) -> SignalHandlerId; + + fn connect_property_start_time_notify( + &self, + f: F, + ) -> SignalHandlerId; + + fn negotiate(&self) -> bool; +} + +impl> AggregatorExt for O { + //fn get_allocator(&self, allocator: /*Ignored*/gst::Allocator, params: /*Ignored*/gst::AllocationParams) { + // unsafe { TODO: call gst_base_sys:gst_aggregator_get_allocator() } + //} + + fn get_buffer_pool(&self) -> Option { + unsafe { + from_glib_full(gst_base_sys::gst_aggregator_get_buffer_pool( + self.as_ref().to_glib_none().0, + )) + } + } + + fn get_latency(&self) -> gst::ClockTime { + unsafe { + from_glib(gst_base_sys::gst_aggregator_get_latency( + self.as_ref().to_glib_none().0, + )) + } + } + + fn set_latency(&self, min_latency: gst::ClockTime, max_latency: gst::ClockTime) { + unsafe { + gst_base_sys::gst_aggregator_set_latency( + self.as_ref().to_glib_none().0, + min_latency.to_glib(), + max_latency.to_glib(), + ); + } + } + + fn set_src_caps(&self, caps: &gst::Caps) { + unsafe { + gst_base_sys::gst_aggregator_set_src_caps( + self.as_ref().to_glib_none().0, + caps.to_glib_none().0, + ); + } + } + + fn simple_get_next_time(&self) -> gst::ClockTime { + unsafe { + from_glib(gst_base_sys::gst_aggregator_simple_get_next_time( + self.as_ref().to_glib_none().0, + )) + } + } + + fn get_property_start_time(&self) -> u64 { + unsafe { + let mut value = Value::from_type(::static_type()); + gobject_sys::g_object_get_property( + self.to_glib_none().0 as *mut gobject_sys::GObject, + b"start-time\0".as_ptr() as *const _, + value.to_glib_none_mut().0, + ); + value + .get() + .expect("Return Value for property `start-time` getter") + .unwrap() + } + } + + fn set_property_start_time(&self, start_time: u64) { + unsafe { + gobject_sys::g_object_set_property( + self.to_glib_none().0 as *mut gobject_sys::GObject, + b"start-time\0".as_ptr() as *const _, + Value::from(&start_time).to_glib_none().0, + ); + } + } + + fn connect_property_latency_notify( + &self, + f: F, + ) -> SignalHandlerId { + unsafe extern "C" fn notify_latency_trampoline( + this: *mut gst_base_sys::GstAggregator, + _param_spec: glib_sys::gpointer, + f: glib_sys::gpointer, + ) where + P: IsA, + { + let f: &F = &*(f as *const F); + f(&Aggregator::from_glib_borrow(this).unsafe_cast_ref()) + } + unsafe { + let f: Box_ = Box_::new(f); + connect_raw( + self.as_ptr() as *mut _, + b"notify::latency\0".as_ptr() as *const _, + Some(transmute(notify_latency_trampoline:: as usize)), + Box_::into_raw(f), + ) + } + } + + fn connect_property_start_time_notify( + &self, + f: F, + ) -> SignalHandlerId { + unsafe extern "C" fn notify_start_time_trampoline( + this: *mut gst_base_sys::GstAggregator, + _param_spec: glib_sys::gpointer, + f: glib_sys::gpointer, + ) where + P: IsA, + { + let f: &F = &*(f as *const F); + f(&Aggregator::from_glib_borrow(this).unsafe_cast_ref()) + } + unsafe { + let f: Box_ = Box_::new(f); + connect_raw( + self.as_ptr() as *mut _, + b"notify::start-time\0".as_ptr() as *const _, + Some(transmute(notify_start_time_trampoline:: as usize)), + Box_::into_raw(f), + ) + } + } + + fn negotiate(&self) -> bool { + unsafe { + from_glib(gst_base_sys::gst_aggregator_negotiate( + self.as_ref().to_glib_none().0, + )) + } + } +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/auto/aggregator_pad.rs b/utils/gst-plugin-fallbackswitch/src/base/auto/aggregator_pad.rs new file mode 100644 index 000000000..7c8df57e5 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/auto/aggregator_pad.rs @@ -0,0 +1,182 @@ +// This file was generated by gir (https://github.com/gtk-rs/gir) +// from gir-files (https://github.com/gtk-rs/gir-files) +// DO NOT EDIT + +use super::super::gst_base_sys; +use glib::object::Cast; +use glib::object::IsA; +use glib::signal::connect_raw; +use glib::signal::SignalHandlerId; +use glib::translate::*; +use glib::StaticType; +use glib::Value; +use glib_sys; +use gobject_sys; +use gst; +use gst_sys; +use std::boxed::Box as Box_; +use std::mem::transmute; + +glib_wrapper! { + pub struct AggregatorPad(Object) @extends gst::Pad, gst::Object; + + match fn { + get_type => || gst_base_sys::gst_aggregator_pad_get_type(), + } +} + +unsafe impl Send for AggregatorPad {} +unsafe impl Sync for AggregatorPad {} + +pub const NONE_AGGREGATOR_PAD: Option<&AggregatorPad> = None; + +pub trait AggregatorPadExt: 'static { + fn drop_buffer(&self) -> bool; + + fn has_buffer(&self) -> bool; + + fn is_eos(&self) -> bool; + + fn peek_buffer(&self) -> Option; + + fn pop_buffer(&self) -> Option; + + fn get_property_emit_signals(&self) -> bool; + + fn set_property_emit_signals(&self, emit_signals: bool); + + fn connect_buffer_consumed( + &self, + f: F, + ) -> SignalHandlerId; + + fn connect_property_emit_signals_notify( + &self, + f: F, + ) -> SignalHandlerId; +} + +impl> AggregatorPadExt for O { + fn drop_buffer(&self) -> bool { + unsafe { + from_glib(gst_base_sys::gst_aggregator_pad_drop_buffer( + self.as_ref().to_glib_none().0, + )) + } + } + + fn has_buffer(&self) -> bool { + unsafe { + from_glib(gst_base_sys::gst_aggregator_pad_has_buffer( + self.as_ref().to_glib_none().0, + )) + } + } + + fn is_eos(&self) -> bool { + unsafe { + from_glib(gst_base_sys::gst_aggregator_pad_is_eos( + self.as_ref().to_glib_none().0, + )) + } + } + + fn peek_buffer(&self) -> Option { + unsafe { + from_glib_full(gst_base_sys::gst_aggregator_pad_peek_buffer( + self.as_ref().to_glib_none().0, + )) + } + } + + fn pop_buffer(&self) -> Option { + unsafe { + from_glib_full(gst_base_sys::gst_aggregator_pad_pop_buffer( + self.as_ref().to_glib_none().0, + )) + } + } + + fn get_property_emit_signals(&self) -> bool { + unsafe { + let mut value = Value::from_type(::static_type()); + gobject_sys::g_object_get_property( + self.to_glib_none().0 as *mut gobject_sys::GObject, + b"emit-signals\0".as_ptr() as *const _, + value.to_glib_none_mut().0, + ); + value + .get() + .expect("Return Value for property `emit-signals` getter") + .unwrap() + } + } + + fn set_property_emit_signals(&self, emit_signals: bool) { + unsafe { + gobject_sys::g_object_set_property( + self.to_glib_none().0 as *mut gobject_sys::GObject, + b"emit-signals\0".as_ptr() as *const _, + Value::from(&emit_signals).to_glib_none().0, + ); + } + } + + fn connect_buffer_consumed( + &self, + f: F, + ) -> SignalHandlerId { + unsafe extern "C" fn buffer_consumed_trampoline< + P, + F: Fn(&P, &gst::Buffer) + Send + Sync + 'static, + >( + this: *mut gst_base_sys::GstAggregatorPad, + object: *mut gst_sys::GstBuffer, + f: glib_sys::gpointer, + ) where + P: IsA, + { + let f: &F = &*(f as *const F); + f( + &AggregatorPad::from_glib_borrow(this).unsafe_cast_ref(), + &from_glib_borrow(object), + ) + } + unsafe { + let f: Box_ = Box_::new(f); + connect_raw( + self.as_ptr() as *mut _, + b"buffer-consumed\0".as_ptr() as *const _, + Some(transmute(buffer_consumed_trampoline:: as usize)), + Box_::into_raw(f), + ) + } + } + + fn connect_property_emit_signals_notify( + &self, + f: F, + ) -> SignalHandlerId { + unsafe extern "C" fn notify_emit_signals_trampoline( + this: *mut gst_base_sys::GstAggregatorPad, + _param_spec: glib_sys::gpointer, + f: glib_sys::gpointer, + ) where + P: IsA, + { + let f: &F = &*(f as *const F); + f(&AggregatorPad::from_glib_borrow(this).unsafe_cast_ref()) + } + unsafe { + let f: Box_ = Box_::new(f); + connect_raw( + self.as_ptr() as *mut _, + b"notify::emit-signals\0".as_ptr() as *const _, + Some(transmute( + notify_emit_signals_trampoline:: as usize, + )), + Box_::into_raw(f), + ) + } + } +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/auto/mod.rs b/utils/gst-plugin-fallbackswitch/src/base/auto/mod.rs new file mode 100644 index 000000000..0081d7c4f --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/auto/mod.rs @@ -0,0 +1,13 @@ +mod aggregator; +pub use self::aggregator::AggregatorExt; +pub use self::aggregator::{Aggregator, AggregatorClass, NONE_AGGREGATOR}; + +mod aggregator_pad; +pub use self::aggregator_pad::AggregatorPadExt; +pub use self::aggregator_pad::{AggregatorPad, AggregatorPadClass, NONE_AGGREGATOR_PAD}; + +#[doc(hidden)] +pub mod traits { + pub use super::AggregatorExt; + pub use super::AggregatorPadExt; +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/gstaggregator.c b/utils/gst-plugin-fallbackswitch/src/base/gstaggregator.c new file mode 100644 index 000000000..06389895e --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/gstaggregator.c @@ -0,0 +1,3465 @@ +/* GStreamer aggregator base class + * Copyright (C) 2014 Mathieu Duponchelle + * Copyright (C) 2014 Thibault Saunier + * + * gstaggregator.c: + * + * 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. + */ +/** + * SECTION: gstaggregator + * @title: GstAggregator + * @short_description: Base class for mixers and muxers, manages a set of input + * pads and aggregates their streams + * @see_also: gstcollectpads for historical reasons. + * + * Manages a set of pads with the purpose of aggregating their buffers. + * Control is given to the subclass when all pads have data. + * + * * Base class for mixers and muxers. Subclasses should at least implement + * the #GstAggregatorClass.aggregate() virtual method. + * + * * Installs a #GstPadChainFunction, a #GstPadEventFullFunction and a + * #GstPadQueryFunction to queue all serialized data packets per sink pad. + * Subclasses should not overwrite those, but instead implement + * #GstAggregatorClass.sink_event() and #GstAggregatorClass.sink_query() as + * needed. + * + * * When data is queued on all pads, the aggregate vmethod is called. + * + * * One can peek at the data on any given GstAggregatorPad with the + * gst_aggregator_pad_peek_buffer () method, and remove it from the pad + * with the gst_aggregator_pad_pop_buffer () method. When a buffer + * has been taken with pop_buffer (), a new buffer can be queued + * on that pad. + * + * * If the subclass wishes to push a buffer downstream in its aggregate + * implementation, it should do so through the + * gst_aggregator_finish_buffer () method. This method will take care + * of sending and ordering mandatory events such as stream start, caps + * and segment. + * + * * Same goes for EOS events, which should not be pushed directly by the + * subclass, it should instead return GST_FLOW_EOS in its aggregate + * implementation. + * + * * Note that the aggregator logic regarding gap event handling is to turn + * these into gap buffers with matching PTS and duration. It will also + * flag these buffers with GST_BUFFER_FLAG_GAP and GST_BUFFER_FLAG_DROPPABLE + * to ease their identification and subsequent processing. + * + * * Subclasses must use (a subclass of) #GstAggregatorPad for both their + * sink and source pads. + * See gst_element_class_add_static_pad_template_with_gtype(). + * + * This class used to live in gst-plugins-bad and was moved to core. + * + * Since: 1.14 + */ + +/** + * SECTION: gstaggregatorpad + * @title: GstAggregatorPad + * @short_description: #GstPad subclass for pads managed by #GstAggregator + * @see_also: gstcollectpads for historical reasons. + * + * Pads managed by a #GstAggregator subclass. + * + * This class used to live in gst-plugins-bad and was moved to core. + * + * Since: 1.14 + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include /* strlen */ + +#include "gstaggregator.h" + +typedef enum +{ + GST_AGGREGATOR_START_TIME_SELECTION_ZERO, + GST_AGGREGATOR_START_TIME_SELECTION_FIRST, + GST_AGGREGATOR_START_TIME_SELECTION_SET +} GstAggregatorStartTimeSelection; + +static GType +gst_aggregator_start_time_selection_get_type (void) +{ + static GType gtype = 0; + + if (gtype == 0) { + static const GEnumValue values[] = { + {GST_AGGREGATOR_START_TIME_SELECTION_ZERO, + "Start at 0 running time (default)", "zero"}, + {GST_AGGREGATOR_START_TIME_SELECTION_FIRST, + "Start at first observed input running time", "first"}, + {GST_AGGREGATOR_START_TIME_SELECTION_SET, + "Set start time with start-time property", "set"}, + {0, NULL, NULL} + }; + + gtype = + g_enum_register_static ("GstAggregatorFallbackStartTimeSelection", + values); + } + return gtype; +} + +/* Might become API */ +#if 0 +static void gst_aggregator_merge_tags (GstAggregator * aggregator, + const GstTagList * tags, GstTagMergeMode mode); +#endif +static void gst_aggregator_set_latency_property (GstAggregator * agg, + GstClockTime latency); +static GstClockTime gst_aggregator_get_latency_property (GstAggregator * agg); + +static GstClockTime gst_aggregator_get_latency_unlocked (GstAggregator * self); + +static void gst_aggregator_pad_buffer_consumed (GstAggregatorPad * pad, + GstBuffer * buffer); + +GST_DEBUG_CATEGORY_STATIC (aggregator_debug); +#define GST_CAT_DEFAULT aggregator_debug + +/* Locking order, locks in this element must always be taken in this order + * + * standard sink pad stream lock -> GST_PAD_STREAM_LOCK (aggpad) + * Aggregator pad flush lock -> PAD_FLUSH_LOCK(aggpad) + * standard src pad stream lock -> GST_PAD_STREAM_LOCK (srcpad) + * Aggregator src lock -> SRC_LOCK(agg) w/ SRC_WAIT/BROADCAST + * standard element object lock -> GST_OBJECT_LOCK(agg) + * Aggregator pad lock -> PAD_LOCK (aggpad) w/ PAD_WAIT/BROADCAST_EVENT(aggpad) + * standard src pad object lock -> GST_OBJECT_LOCK(srcpad) + * standard sink pad object lock -> GST_OBJECT_LOCK(aggpad) + */ + +/* GstAggregatorPad definitions */ +#define PAD_LOCK(pad) G_STMT_START { \ + GST_TRACE_OBJECT (pad, "Taking PAD lock from thread %p", \ + g_thread_self()); \ + g_mutex_lock(&pad->priv->lock); \ + GST_TRACE_OBJECT (pad, "Took PAD lock from thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define PAD_UNLOCK(pad) G_STMT_START { \ + GST_TRACE_OBJECT (pad, "Releasing PAD lock from thread %p", \ + g_thread_self()); \ + g_mutex_unlock(&pad->priv->lock); \ + GST_TRACE_OBJECT (pad, "Release PAD lock from thread %p", \ + g_thread_self()); \ + } G_STMT_END + + +#define PAD_WAIT_EVENT(pad) G_STMT_START { \ + GST_LOG_OBJECT (pad, "Waiting for buffer to be consumed thread %p", \ + g_thread_self()); \ + g_cond_wait(&(((GstAggregatorPad* )pad)->priv->event_cond), \ + (&((GstAggregatorPad*)pad)->priv->lock)); \ + GST_LOG_OBJECT (pad, "DONE Waiting for buffer to be consumed on thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define PAD_BROADCAST_EVENT(pad) G_STMT_START { \ + GST_LOG_OBJECT (pad, "Signaling buffer consumed from thread %p", \ + g_thread_self()); \ + g_cond_broadcast(&(((GstAggregatorPad* )pad)->priv->event_cond)); \ + } G_STMT_END + + +#define PAD_FLUSH_LOCK(pad) G_STMT_START { \ + GST_TRACE_OBJECT (pad, "Taking lock from thread %p", \ + g_thread_self()); \ + g_mutex_lock(&pad->priv->flush_lock); \ + GST_TRACE_OBJECT (pad, "Took lock from thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define PAD_FLUSH_UNLOCK(pad) G_STMT_START { \ + GST_TRACE_OBJECT (pad, "Releasing lock from thread %p", \ + g_thread_self()); \ + g_mutex_unlock(&pad->priv->flush_lock); \ + GST_TRACE_OBJECT (pad, "Release lock from thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define SRC_LOCK(self) G_STMT_START { \ + GST_TRACE_OBJECT (self, "Taking src lock from thread %p", \ + g_thread_self()); \ + g_mutex_lock(&self->priv->src_lock); \ + GST_TRACE_OBJECT (self, "Took src lock from thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define SRC_UNLOCK(self) G_STMT_START { \ + GST_TRACE_OBJECT (self, "Releasing src lock from thread %p", \ + g_thread_self()); \ + g_mutex_unlock(&self->priv->src_lock); \ + GST_TRACE_OBJECT (self, "Released src lock from thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define SRC_WAIT(self) G_STMT_START { \ + GST_LOG_OBJECT (self, "Waiting for src on thread %p", \ + g_thread_self()); \ + g_cond_wait(&(self->priv->src_cond), &(self->priv->src_lock)); \ + GST_LOG_OBJECT (self, "DONE Waiting for src on thread %p", \ + g_thread_self()); \ + } G_STMT_END + +#define SRC_BROADCAST(self) G_STMT_START { \ + GST_LOG_OBJECT (self, "Signaling src from thread %p", \ + g_thread_self()); \ + if (self->priv->aggregate_id) \ + gst_clock_id_unschedule (self->priv->aggregate_id); \ + g_cond_broadcast(&(self->priv->src_cond)); \ + } G_STMT_END + +struct _GstAggregatorPadPrivate +{ + /* Following fields are protected by the PAD_LOCK */ + GstFlowReturn flow_return; + + guint32 last_flush_start_seqnum; + guint32 last_flush_stop_seqnum; + + gboolean first_buffer; + + GQueue data; /* buffers, events and queries */ + GstBuffer *clipped_buffer; + guint num_buffers; + + /* used to track fill state of queues, only used with live-src and when + * latency property is set to > 0 */ + GstClockTime head_position; + GstClockTime tail_position; + GstClockTime head_time; /* running time */ + GstClockTime tail_time; + GstClockTime time_level; /* how much head is ahead of tail */ + GstSegment head_segment; /* segment before the queue */ + + gboolean negotiated; + + gboolean eos; + + GMutex lock; + GCond event_cond; + /* This lock prevents a flush start processing happening while + * the chain function is also happening. + */ + GMutex flush_lock; + + /* properties */ + gboolean emit_signals; +}; + +/* Must be called with PAD_LOCK held */ +static void +gst_aggregator_pad_reset_unlocked (GstAggregatorPad * aggpad) +{ + aggpad->priv->eos = FALSE; + aggpad->priv->flow_return = GST_FLOW_OK; + GST_OBJECT_LOCK (aggpad); + gst_segment_init (&aggpad->segment, GST_FORMAT_UNDEFINED); + gst_segment_init (&aggpad->priv->head_segment, GST_FORMAT_UNDEFINED); + GST_OBJECT_UNLOCK (aggpad); + aggpad->priv->head_position = GST_CLOCK_TIME_NONE; + aggpad->priv->tail_position = GST_CLOCK_TIME_NONE; + aggpad->priv->head_time = GST_CLOCK_TIME_NONE; + aggpad->priv->tail_time = GST_CLOCK_TIME_NONE; + aggpad->priv->time_level = 0; + aggpad->priv->first_buffer = TRUE; +} + +static gboolean +gst_aggregator_pad_flush (GstAggregatorPad * aggpad, GstAggregator * agg) +{ + GstAggregatorPadClass *klass = GST_AGGREGATOR_PAD_GET_CLASS (aggpad); + + PAD_LOCK (aggpad); + gst_aggregator_pad_reset_unlocked (aggpad); + PAD_UNLOCK (aggpad); + + if (klass->flush) + return (klass->flush (aggpad, agg) == GST_FLOW_OK); + + return TRUE; +} + +/************************************* + * GstAggregator implementation * + *************************************/ +static GstElementClass *aggregator_parent_class = NULL; +static gint aggregator_private_offset = 0; + +/* All members are protected by the object lock unless otherwise noted */ + +struct _GstAggregatorPrivate +{ + gint max_padserial; + + /* Our state is >= PAUSED */ + gboolean running; /* protected by src_lock */ + + /* seqnum from last seek or common seqnum to flush start events received + * on all pads, for flushing without a seek */ + guint32 next_seqnum; + /* seqnum to apply to synthetic segment/eos events */ + guint32 seqnum; + gboolean send_stream_start; /* protected by srcpad stream lock */ + gboolean send_segment; + gboolean flushing; + gboolean send_eos; /* protected by srcpad stream lock */ + + GstCaps *srccaps; /* protected by the srcpad stream lock */ + + GstTagList *tags; + gboolean tags_changed; + + gboolean peer_latency_live; /* protected by src_lock */ + GstClockTime peer_latency_min; /* protected by src_lock */ + GstClockTime peer_latency_max; /* protected by src_lock */ + gboolean has_peer_latency; /* protected by src_lock */ + + GstClockTime sub_latency_min; /* protected by src_lock */ + GstClockTime sub_latency_max; /* protected by src_lock */ + + GstClockTime upstream_latency_min; /* protected by src_lock */ + + /* aggregate */ + GstClockID aggregate_id; /* protected by src_lock */ + GMutex src_lock; + GCond src_cond; + + gboolean first_buffer; /* protected by object lock */ + GstAggregatorStartTimeSelection start_time_selection; + GstClockTime start_time; + + /* protected by the object lock */ + GstQuery *allocation_query; + GstAllocator *allocator; + GstBufferPool *pool; + GstAllocationParams allocation_params; + + /* properties */ + gint64 latency; /* protected by both src_lock and all pad locks */ +}; + +/* Seek event forwarding helper */ +typedef struct +{ + /* parameters */ + GstEvent *event; + gboolean flush; + gboolean only_to_active_pads; + + /* results */ + gboolean result; + gboolean one_actually_seeked; +} EventData; + +#define DEFAULT_LATENCY 0 +#define DEFAULT_MIN_UPSTREAM_LATENCY 0 +#define DEFAULT_START_TIME_SELECTION GST_AGGREGATOR_START_TIME_SELECTION_ZERO +#define DEFAULT_START_TIME (-1) + +enum +{ + PROP_0, + PROP_LATENCY, + PROP_MIN_UPSTREAM_LATENCY, + PROP_START_TIME_SELECTION, + PROP_START_TIME, + PROP_LAST +}; + +static GstFlowReturn gst_aggregator_pad_chain_internal (GstAggregator * self, + GstAggregatorPad * aggpad, GstBuffer * buffer, gboolean head); + +static gboolean +gst_aggregator_pad_queue_is_empty (GstAggregatorPad * pad) +{ + return (g_queue_peek_tail (&pad->priv->data) == NULL && + pad->priv->clipped_buffer == NULL); +} + +/* Will return FALSE if there's no buffer available on every non-EOS pad, or + * if at least one of the pads has an event or query at the top of its queue. + * + * Only returns TRUE if all non-EOS pads have a buffer available at the top of + * their queue + */ +static gboolean +gst_aggregator_check_pads_ready (GstAggregator * self, + gboolean * have_event_or_query_ret) +{ + GstAggregatorPad *pad = NULL; + GList *l, *sinkpads; + gboolean have_buffer = TRUE; + gboolean have_event_or_query = FALSE; + + GST_LOG_OBJECT (self, "checking pads"); + + GST_OBJECT_LOCK (self); + + sinkpads = GST_ELEMENT_CAST (self)->sinkpads; + if (sinkpads == NULL) + goto no_sinkpads; + + for (l = sinkpads; l != NULL; l = l->next) { + pad = l->data; + + PAD_LOCK (pad); + + /* If there's an event or query at the top of the queue and we don't yet + * have taken the top buffer out and stored it as clip_buffer, remember + * that and exit the loop. We first have to handle all events/queries + * before we handle any buffers. */ + if (!pad->priv->clipped_buffer + && (GST_IS_EVENT (g_queue_peek_tail (&pad->priv->data)) + || GST_IS_QUERY (g_queue_peek_tail (&pad->priv->data)))) { + PAD_UNLOCK (pad); + have_event_or_query = TRUE; + break; + } + + /* Otherwise check if we have a clipped buffer or a buffer at the top of + * the queue, and if not then this pad is not ready unless it is also EOS */ + if (!pad->priv->clipped_buffer + && !GST_IS_BUFFER (g_queue_peek_tail (&pad->priv->data))) { + /* We must not have any buffers at all in this pad then as otherwise we + * would've had an event/query at the top of the queue */ + g_assert (pad->priv->num_buffers == 0); + + /* Only consider this pad as worth waiting for if it's not already EOS. + * There's no point in waiting for buffers on EOS pads */ + if (!pad->priv->eos) + have_buffer = FALSE; + } else if (self->priv->peer_latency_live) { + /* In live mode, having a single pad with buffers is enough to + * generate a start time from it. In non-live mode all pads need + * to have a buffer + */ + self->priv->first_buffer = FALSE; + } + + PAD_UNLOCK (pad); + } + + if (have_event_or_query) + goto pad_not_ready_but_event_or_query; + + if (!have_buffer) + goto pad_not_ready; + + if (have_buffer) + self->priv->first_buffer = FALSE; + + GST_OBJECT_UNLOCK (self); + GST_LOG_OBJECT (self, "pads are ready"); + + if (have_event_or_query_ret) + *have_event_or_query_ret = have_event_or_query; + + return TRUE; + +no_sinkpads: + { + GST_LOG_OBJECT (self, "pads not ready: no sink pads"); + GST_OBJECT_UNLOCK (self); + + if (have_event_or_query_ret) + *have_event_or_query_ret = have_event_or_query; + + return FALSE; + } +pad_not_ready: + { + GST_LOG_OBJECT (pad, "pad not ready to be aggregated yet"); + GST_OBJECT_UNLOCK (self); + + if (have_event_or_query_ret) + *have_event_or_query_ret = have_event_or_query; + + return FALSE; + } +pad_not_ready_but_event_or_query: + { + GST_LOG_OBJECT (pad, + "pad not ready to be aggregated yet, need to handle serialized event or query first"); + GST_OBJECT_UNLOCK (self); + + if (have_event_or_query_ret) + *have_event_or_query_ret = have_event_or_query; + + return FALSE; + } +} + +static void +gst_aggregator_reset_flow_values (GstAggregator * self) +{ + GST_OBJECT_LOCK (self); + self->priv->send_stream_start = TRUE; + self->priv->send_segment = TRUE; + gst_segment_init (&GST_AGGREGATOR_PAD (self->srcpad)->segment, + GST_FORMAT_TIME); + self->priv->first_buffer = TRUE; + GST_OBJECT_UNLOCK (self); +} + +static inline void +gst_aggregator_push_mandatory_events (GstAggregator * self) +{ + GstAggregatorPrivate *priv = self->priv; + GstEvent *segment = NULL; + GstEvent *tags = NULL; + + if (self->priv->send_stream_start) { + gchar s_id[32]; + + GST_INFO_OBJECT (self, "pushing stream start"); + /* stream-start (FIXME: create id based on input ids) */ + g_snprintf (s_id, sizeof (s_id), "agg-%08x", g_random_int ()); + if (!gst_pad_push_event (GST_PAD (self->srcpad), + gst_event_new_stream_start (s_id))) { + GST_WARNING_OBJECT (self->srcpad, "Sending stream start event failed"); + } + self->priv->send_stream_start = FALSE; + } + + if (self->priv->srccaps) { + + GST_INFO_OBJECT (self, "pushing caps: %" GST_PTR_FORMAT, + self->priv->srccaps); + if (!gst_pad_push_event (GST_PAD (self->srcpad), + gst_event_new_caps (self->priv->srccaps))) { + GST_WARNING_OBJECT (self->srcpad, "Sending caps event failed"); + } + gst_caps_unref (self->priv->srccaps); + self->priv->srccaps = NULL; + } + + GST_OBJECT_LOCK (self); + if (self->priv->send_segment && !self->priv->flushing) { + segment = + gst_event_new_segment (&GST_AGGREGATOR_PAD (self->srcpad)->segment); + + if (!self->priv->seqnum) + /* This code-path is in preparation to be able to run without a source + * connected. Then we won't have a seq-num from a segment event. */ + self->priv->seqnum = gst_event_get_seqnum (segment); + else + gst_event_set_seqnum (segment, self->priv->seqnum); + self->priv->send_segment = FALSE; + + GST_DEBUG_OBJECT (self, "pushing segment %" GST_PTR_FORMAT, segment); + } + + if (priv->tags && priv->tags_changed && !self->priv->flushing) { + tags = gst_event_new_tag (gst_tag_list_ref (priv->tags)); + priv->tags_changed = FALSE; + } + GST_OBJECT_UNLOCK (self); + + if (segment) + gst_pad_push_event (self->srcpad, segment); + if (tags) + gst_pad_push_event (self->srcpad, tags); + +} + +/** + * gst_aggregator_set_src_caps: + * @self: The #GstAggregator + * @caps: The #GstCaps to set on the src pad. + * + * Sets the caps to be used on the src pad. + */ +void +gst_aggregator_set_src_caps (GstAggregator * self, GstCaps * caps) +{ + GST_PAD_STREAM_LOCK (self->srcpad); + gst_caps_replace (&self->priv->srccaps, caps); + gst_aggregator_push_mandatory_events (self); + GST_PAD_STREAM_UNLOCK (self->srcpad); +} + +static GstFlowReturn +gst_aggregator_default_finish_buffer (GstAggregator * self, GstBuffer * buffer) +{ + gst_aggregator_push_mandatory_events (self); + + GST_OBJECT_LOCK (self); + if (!self->priv->flushing && gst_pad_is_active (self->srcpad)) { + GST_TRACE_OBJECT (self, "pushing buffer %" GST_PTR_FORMAT, buffer); + GST_OBJECT_UNLOCK (self); + return gst_pad_push (self->srcpad, buffer); + } else { + GST_INFO_OBJECT (self, "Not pushing (active: %i, flushing: %i)", + self->priv->flushing, gst_pad_is_active (self->srcpad)); + GST_OBJECT_UNLOCK (self); + gst_buffer_unref (buffer); + return GST_FLOW_OK; + } +} + +/** + * gst_aggregator_finish_buffer: + * @aggregator: The #GstAggregator + * @buffer: (transfer full): the #GstBuffer to push. + * + * This method will push the provided output buffer downstream. If needed, + * mandatory events such as stream-start, caps, and segment events will be + * sent before pushing the buffer. + */ +GstFlowReturn +gst_aggregator_finish_buffer (GstAggregator * aggregator, GstBuffer * buffer) +{ + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (aggregator); + + g_assert (klass->finish_buffer != NULL); + + return klass->finish_buffer (aggregator, buffer); +} + +static void +gst_aggregator_push_eos (GstAggregator * self) +{ + GstEvent *event; + gst_aggregator_push_mandatory_events (self); + + event = gst_event_new_eos (); + + GST_OBJECT_LOCK (self); + self->priv->send_eos = FALSE; + gst_event_set_seqnum (event, self->priv->seqnum); + GST_OBJECT_UNLOCK (self); + + gst_pad_push_event (self->srcpad, event); +} + +static GstClockTime +gst_aggregator_get_next_time (GstAggregator * self) +{ + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + + if (klass->get_next_time) + return klass->get_next_time (self); + + return GST_CLOCK_TIME_NONE; +} + +static gboolean +gst_aggregator_wait_and_check (GstAggregator * self, gboolean * timeout) +{ + GstClockTime latency; + GstClockTime start; + gboolean res; + gboolean have_event_or_query = FALSE; + + *timeout = FALSE; + + SRC_LOCK (self); + + latency = gst_aggregator_get_latency_unlocked (self); + + if (gst_aggregator_check_pads_ready (self, &have_event_or_query)) { + GST_DEBUG_OBJECT (self, "all pads have data"); + SRC_UNLOCK (self); + + return TRUE; + } + + /* If we have an event or query, immediately return FALSE instead of waiting + * and handle it immediately */ + if (have_event_or_query) { + GST_DEBUG_OBJECT (self, "Have serialized event or query to handle first"); + SRC_UNLOCK (self); + return FALSE; + } + + /* Before waiting, check if we're actually still running */ + if (!self->priv->running || !self->priv->send_eos) { + SRC_UNLOCK (self); + + return FALSE; + } + + start = gst_aggregator_get_next_time (self); + + /* If we're not live, or if we use the running time + * of the first buffer as start time, we wait until + * all pads have buffers. + * Otherwise (i.e. if we are live!), we wait on the clock + * and if a pad does not have a buffer in time we ignore + * that pad. + */ + GST_OBJECT_LOCK (self); + if (!GST_CLOCK_TIME_IS_VALID (latency) || + !GST_IS_CLOCK (GST_ELEMENT_CLOCK (self)) || + !GST_CLOCK_TIME_IS_VALID (start) || + (self->priv->first_buffer + && self->priv->start_time_selection == + GST_AGGREGATOR_START_TIME_SELECTION_FIRST)) { + /* We wake up here when something happened, and below + * then check if we're ready now. If we return FALSE, + * we will be directly called again. + */ + GST_OBJECT_UNLOCK (self); + SRC_WAIT (self); + } else { + GstClockTime base_time, time; + GstClock *clock; + GstClockReturn status; + GstClockTimeDiff jitter; + + GST_DEBUG_OBJECT (self, "got subclass start time: %" GST_TIME_FORMAT, + GST_TIME_ARGS (start)); + + base_time = GST_ELEMENT_CAST (self)->base_time; + clock = gst_object_ref (GST_ELEMENT_CLOCK (self)); + GST_OBJECT_UNLOCK (self); + + time = base_time + start; + time += latency; + + GST_DEBUG_OBJECT (self, "possibly waiting for clock to reach %" + GST_TIME_FORMAT " (base %" GST_TIME_FORMAT " start %" GST_TIME_FORMAT + " latency %" GST_TIME_FORMAT " current %" GST_TIME_FORMAT ")", + GST_TIME_ARGS (time), + GST_TIME_ARGS (base_time), + GST_TIME_ARGS (start), GST_TIME_ARGS (latency), + GST_TIME_ARGS (gst_clock_get_time (clock))); + + self->priv->aggregate_id = gst_clock_new_single_shot_id (clock, time); + gst_object_unref (clock); + SRC_UNLOCK (self); + + jitter = 0; + status = gst_clock_id_wait (self->priv->aggregate_id, &jitter); + + SRC_LOCK (self); + if (self->priv->aggregate_id) { + gst_clock_id_unref (self->priv->aggregate_id); + self->priv->aggregate_id = NULL; + } + + GST_DEBUG_OBJECT (self, + "clock returned %d (jitter: %" GST_STIME_FORMAT ")", + status, GST_STIME_ARGS (jitter)); + + /* we timed out */ + if (status == GST_CLOCK_OK || status == GST_CLOCK_EARLY) { + SRC_UNLOCK (self); + *timeout = TRUE; + return TRUE; + } + } + + res = gst_aggregator_check_pads_ready (self, &have_event_or_query); + SRC_UNLOCK (self); + + return res; +} + +typedef struct +{ + gboolean processed_event; + GstFlowReturn flow_ret; +} DoHandleEventsAndQueriesData; + +static gboolean +gst_aggregator_do_events_and_queries (GstElement * self, GstPad * epad, + gpointer user_data) +{ + GstAggregatorPad *pad = GST_AGGREGATOR_PAD_CAST (epad); + GstAggregator *aggregator = GST_AGGREGATOR_CAST (self); + GstEvent *event = NULL; + GstQuery *query = NULL; + GstAggregatorClass *klass = NULL; + DoHandleEventsAndQueriesData *data = user_data; + + do { + event = NULL; + query = NULL; + + PAD_LOCK (pad); + if (pad->priv->clipped_buffer == NULL && + !GST_IS_BUFFER (g_queue_peek_tail (&pad->priv->data))) { + if (GST_IS_EVENT (g_queue_peek_tail (&pad->priv->data))) + event = gst_event_ref (g_queue_peek_tail (&pad->priv->data)); + if (GST_IS_QUERY (g_queue_peek_tail (&pad->priv->data))) + query = g_queue_peek_tail (&pad->priv->data); + } + PAD_UNLOCK (pad); + if (event || query) { + gboolean ret; + + data->processed_event = TRUE; + if (klass == NULL) + klass = GST_AGGREGATOR_GET_CLASS (self); + + if (event) { + GST_LOG_OBJECT (pad, "Processing %" GST_PTR_FORMAT, event); + gst_event_ref (event); + ret = klass->sink_event (aggregator, pad, event); + + PAD_LOCK (pad); + if (GST_EVENT_TYPE (event) == GST_EVENT_CAPS) { + pad->priv->negotiated = ret; + if (!ret) + pad->priv->flow_return = data->flow_ret = GST_FLOW_NOT_NEGOTIATED; + } + if (g_queue_peek_tail (&pad->priv->data) == event) + gst_event_unref (g_queue_pop_tail (&pad->priv->data)); + gst_event_unref (event); + } else if (query) { + GST_LOG_OBJECT (pad, "Processing %" GST_PTR_FORMAT, query); + ret = klass->sink_query (aggregator, pad, query); + + PAD_LOCK (pad); + if (g_queue_peek_tail (&pad->priv->data) == query) { + GstStructure *s; + + s = gst_query_writable_structure (query); + gst_structure_set (s, "gst-aggregator-retval", G_TYPE_BOOLEAN, ret, + NULL); + g_queue_pop_tail (&pad->priv->data); + } + } + + PAD_BROADCAST_EVENT (pad); + PAD_UNLOCK (pad); + } + } while (event || query); + + return TRUE; +} + +static gboolean +gst_aggregator_pad_skip_buffers (GstElement * self, GstPad * epad, + gpointer user_data) +{ + GList *item; + GstAggregatorPad *aggpad = (GstAggregatorPad *) epad; + GstAggregator *agg = (GstAggregator *) self; + GstAggregatorPadClass *klass = GST_AGGREGATOR_PAD_GET_CLASS (aggpad); + + if (!klass->skip_buffer) + return FALSE; + + PAD_LOCK (aggpad); + + item = g_queue_peek_head_link (&aggpad->priv->data); + while (item) { + GList *next = item->next; + + if (GST_IS_BUFFER (item->data) + && klass->skip_buffer (aggpad, agg, item->data)) { + GST_LOG_OBJECT (aggpad, "Skipping %" GST_PTR_FORMAT, item->data); + gst_aggregator_pad_buffer_consumed (aggpad, GST_BUFFER (item->data)); + gst_buffer_unref (item->data); + g_queue_delete_link (&aggpad->priv->data, item); + } else { + break; + } + + item = next; + } + + PAD_UNLOCK (aggpad); + + return TRUE; +} + +static void +gst_aggregator_pad_set_flushing (GstAggregatorPad * aggpad, + GstFlowReturn flow_return, gboolean full) +{ + GList *item; + + PAD_LOCK (aggpad); + if (flow_return == GST_FLOW_NOT_LINKED) + aggpad->priv->flow_return = MIN (flow_return, aggpad->priv->flow_return); + else + aggpad->priv->flow_return = flow_return; + + item = g_queue_peek_head_link (&aggpad->priv->data); + while (item) { + GList *next = item->next; + + /* In partial flush, we do like the pad, we get rid of non-sticky events + * and EOS/SEGMENT. + */ + if (full || GST_IS_BUFFER (item->data) || + GST_EVENT_TYPE (item->data) == GST_EVENT_EOS || + GST_EVENT_TYPE (item->data) == GST_EVENT_SEGMENT || + !GST_EVENT_IS_STICKY (item->data)) { + if (!GST_IS_QUERY (item->data)) + gst_mini_object_unref (item->data); + g_queue_delete_link (&aggpad->priv->data, item); + } + item = next; + } + aggpad->priv->num_buffers = 0; + gst_buffer_replace (&aggpad->priv->clipped_buffer, NULL); + + PAD_BROADCAST_EVENT (aggpad); + PAD_UNLOCK (aggpad); +} + +static GstFlowReturn +gst_aggregator_default_update_src_caps (GstAggregator * agg, GstCaps * caps, + GstCaps ** ret) +{ + *ret = gst_caps_ref (caps); + + return GST_FLOW_OK; +} + +static GstCaps * +gst_aggregator_default_fixate_src_caps (GstAggregator * agg, GstCaps * caps) +{ + caps = gst_caps_fixate (caps); + + return caps; +} + +static gboolean +gst_aggregator_default_negotiated_src_caps (GstAggregator * agg, GstCaps * caps) +{ + return TRUE; +} + + +/* takes ownership of the pool, allocator and query */ +static gboolean +gst_aggregator_set_allocation (GstAggregator * self, + GstBufferPool * pool, GstAllocator * allocator, + GstAllocationParams * params, GstQuery * query) +{ + GstAllocator *oldalloc; + GstBufferPool *oldpool; + GstQuery *oldquery; + + GST_DEBUG ("storing allocation query"); + + GST_OBJECT_LOCK (self); + oldpool = self->priv->pool; + self->priv->pool = pool; + + oldalloc = self->priv->allocator; + self->priv->allocator = allocator; + + oldquery = self->priv->allocation_query; + self->priv->allocation_query = query; + + if (params) + self->priv->allocation_params = *params; + else + gst_allocation_params_init (&self->priv->allocation_params); + GST_OBJECT_UNLOCK (self); + + if (oldpool) { + GST_DEBUG_OBJECT (self, "deactivating old pool %p", oldpool); + gst_buffer_pool_set_active (oldpool, FALSE); + gst_object_unref (oldpool); + } + if (oldalloc) { + gst_object_unref (oldalloc); + } + if (oldquery) { + gst_query_unref (oldquery); + } + return TRUE; +} + + +static gboolean +gst_aggregator_decide_allocation (GstAggregator * self, GstQuery * query) +{ + GstAggregatorClass *aggclass = GST_AGGREGATOR_GET_CLASS (self); + + if (aggclass->decide_allocation) + if (!aggclass->decide_allocation (self, query)) + return FALSE; + + return TRUE; +} + +static gboolean +gst_aggregator_do_allocation (GstAggregator * self, GstCaps * caps) +{ + GstQuery *query; + gboolean result = TRUE; + GstBufferPool *pool = NULL; + GstAllocator *allocator; + GstAllocationParams params; + + /* find a pool for the negotiated caps now */ + GST_DEBUG_OBJECT (self, "doing allocation query"); + query = gst_query_new_allocation (caps, TRUE); + if (!gst_pad_peer_query (self->srcpad, query)) { + /* not a problem, just debug a little */ + GST_DEBUG_OBJECT (self, "peer ALLOCATION query failed"); + } + + GST_DEBUG_OBJECT (self, "calling decide_allocation"); + result = gst_aggregator_decide_allocation (self, query); + + GST_DEBUG_OBJECT (self, "ALLOCATION (%d) params: %" GST_PTR_FORMAT, result, + query); + + if (!result) + goto no_decide_allocation; + + /* we got configuration from our peer or the decide_allocation method, + * parse them */ + if (gst_query_get_n_allocation_params (query) > 0) { + gst_query_parse_nth_allocation_param (query, 0, &allocator, ¶ms); + } else { + allocator = NULL; + gst_allocation_params_init (¶ms); + } + + if (gst_query_get_n_allocation_pools (query) > 0) + gst_query_parse_nth_allocation_pool (query, 0, &pool, NULL, NULL, NULL); + + /* now store */ + result = + gst_aggregator_set_allocation (self, pool, allocator, ¶ms, query); + + return result; + + /* Errors */ +no_decide_allocation: + { + GST_WARNING_OBJECT (self, "Failed to decide allocation"); + gst_query_unref (query); + + return result; + } + +} + +static gboolean +gst_aggregator_default_negotiate (GstAggregator * self) +{ + GstAggregatorClass *agg_klass = GST_AGGREGATOR_GET_CLASS (self); + GstCaps *downstream_caps, *template_caps, *caps = NULL; + GstFlowReturn ret = GST_FLOW_OK; + + template_caps = gst_pad_get_pad_template_caps (self->srcpad); + downstream_caps = gst_pad_peer_query_caps (self->srcpad, template_caps); + + if (gst_caps_is_empty (downstream_caps)) { + GST_INFO_OBJECT (self, "Downstream caps (%" + GST_PTR_FORMAT ") not compatible with pad template caps (%" + GST_PTR_FORMAT ")", downstream_caps, template_caps); + ret = GST_FLOW_NOT_NEGOTIATED; + goto done; + } + + g_assert (agg_klass->update_src_caps); + GST_DEBUG_OBJECT (self, "updating caps from %" GST_PTR_FORMAT, + downstream_caps); + ret = agg_klass->update_src_caps (self, downstream_caps, &caps); + if (ret < GST_FLOW_OK) { + GST_WARNING_OBJECT (self, "Subclass failed to update provided caps"); + goto done; + } else if (ret == GST_AGGREGATOR_FLOW_NEED_DATA) { + GST_DEBUG_OBJECT (self, "Subclass needs more data to decide on caps"); + goto done; + } + if ((caps == NULL || gst_caps_is_empty (caps)) && ret >= GST_FLOW_OK) { + ret = GST_FLOW_NOT_NEGOTIATED; + goto done; + } + GST_DEBUG_OBJECT (self, " to %" GST_PTR_FORMAT, caps); + +#ifdef GST_ENABLE_EXTRA_CHECKS + if (!gst_caps_is_subset (caps, template_caps)) { + GstCaps *intersection; + + GST_ERROR_OBJECT (self, + "update_src_caps returned caps %" GST_PTR_FORMAT + " which are not a real subset of the template caps %" + GST_PTR_FORMAT, caps, template_caps); + g_warning ("%s: update_src_caps returned caps which are not a real " + "subset of the filter caps", GST_ELEMENT_NAME (self)); + + intersection = + gst_caps_intersect_full (template_caps, caps, GST_CAPS_INTERSECT_FIRST); + gst_caps_unref (caps); + caps = intersection; + } +#endif + + if (gst_caps_is_any (caps)) { + goto done; + } + + if (!gst_caps_is_fixed (caps)) { + g_assert (agg_klass->fixate_src_caps); + + GST_DEBUG_OBJECT (self, "fixate caps from %" GST_PTR_FORMAT, caps); + if (!(caps = agg_klass->fixate_src_caps (self, caps))) { + GST_WARNING_OBJECT (self, "Subclass failed to fixate provided caps"); + ret = GST_FLOW_NOT_NEGOTIATED; + goto done; + } + GST_DEBUG_OBJECT (self, " to %" GST_PTR_FORMAT, caps); + } + + if (agg_klass->negotiated_src_caps) { + if (!agg_klass->negotiated_src_caps (self, caps)) { + GST_WARNING_OBJECT (self, "Subclass failed to accept negotiated caps"); + ret = GST_FLOW_NOT_NEGOTIATED; + goto done; + } + } + + gst_aggregator_set_src_caps (self, caps); + + if (!gst_aggregator_do_allocation (self, caps)) { + GST_WARNING_OBJECT (self, "Allocation negotiation failed"); + ret = GST_FLOW_NOT_NEGOTIATED; + } + +done: + gst_caps_unref (downstream_caps); + gst_caps_unref (template_caps); + + if (caps) + gst_caps_unref (caps); + + return ret >= GST_FLOW_OK || ret == GST_AGGREGATOR_FLOW_NEED_DATA; +} + +/* WITH SRC_LOCK held */ +static gboolean +gst_aggregator_negotiate_unlocked (GstAggregator * self) +{ + GstAggregatorClass *agg_klass = GST_AGGREGATOR_GET_CLASS (self); + + if (agg_klass->negotiate) + return agg_klass->negotiate (self); + + return TRUE; +} + +/** + * gst_aggregator_negotiate: + * @self: a #GstAggregator + * + * Negotiates src pad caps with downstream elements. + * Unmarks GST_PAD_FLAG_NEED_RECONFIGURE in any case. But marks it again + * if #GstAggregatorClass.negotiate() fails. + * + * Returns: %TRUE if the negotiation succeeded, else %FALSE. + * + * Since: 1.18 + */ +gboolean +gst_aggregator_negotiate (GstAggregator * self) +{ + gboolean ret = TRUE; + + g_return_val_if_fail (GST_IS_AGGREGATOR (self), FALSE); + + GST_PAD_STREAM_LOCK (GST_AGGREGATOR_SRC_PAD (self)); + gst_pad_check_reconfigure (GST_AGGREGATOR_SRC_PAD (self)); + ret = gst_aggregator_negotiate_unlocked (self); + if (!ret) + gst_pad_mark_reconfigure (GST_AGGREGATOR_SRC_PAD (self)); + GST_PAD_STREAM_UNLOCK (GST_AGGREGATOR_SRC_PAD (self)); + + return ret; +} + +static void +gst_aggregator_aggregate_func (GstAggregator * self) +{ + GstAggregatorPrivate *priv = self->priv; + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + gboolean timeout = FALSE; + + if (self->priv->running == FALSE) { + GST_DEBUG_OBJECT (self, "Not running anymore"); + return; + } + + GST_LOG_OBJECT (self, "Checking aggregate"); + while (priv->send_eos && priv->running) { + GstFlowReturn flow_return = GST_FLOW_OK; + DoHandleEventsAndQueriesData events_query_data = { FALSE, GST_FLOW_OK }; + + gst_element_foreach_sink_pad (GST_ELEMENT_CAST (self), + gst_aggregator_do_events_and_queries, &events_query_data); + + if ((flow_return = events_query_data.flow_ret) != GST_FLOW_OK) + goto handle_error; + + if (self->priv->peer_latency_live) + gst_element_foreach_sink_pad (GST_ELEMENT_CAST (self), + gst_aggregator_pad_skip_buffers, NULL); + + /* Ensure we have buffers ready (either in clipped_buffer or at the head of + * the queue */ + if (!gst_aggregator_wait_and_check (self, &timeout)) + continue; + + if (gst_pad_check_reconfigure (GST_AGGREGATOR_SRC_PAD (self))) { + if (!gst_aggregator_negotiate_unlocked (self)) { + gst_pad_mark_reconfigure (GST_AGGREGATOR_SRC_PAD (self)); + if (GST_PAD_IS_FLUSHING (GST_AGGREGATOR_SRC_PAD (self))) { + flow_return = GST_FLOW_FLUSHING; + } else { + flow_return = GST_FLOW_NOT_NEGOTIATED; + } + } + } + + if (timeout || flow_return >= GST_FLOW_OK) { + GST_TRACE_OBJECT (self, "Actually aggregating!"); + flow_return = klass->aggregate (self, timeout); + } + + if (flow_return == GST_AGGREGATOR_FLOW_NEED_DATA) + continue; + + GST_OBJECT_LOCK (self); + if (flow_return == GST_FLOW_FLUSHING && priv->flushing) { + /* We don't want to set the pads to flushing, but we want to + * stop the thread, so just break here */ + GST_OBJECT_UNLOCK (self); + break; + } + GST_OBJECT_UNLOCK (self); + + if (flow_return == GST_FLOW_EOS || flow_return == GST_FLOW_ERROR) { + gst_aggregator_push_eos (self); + } + + handle_error: + GST_LOG_OBJECT (self, "flow return is %s", gst_flow_get_name (flow_return)); + + if (flow_return != GST_FLOW_OK) { + GList *item; + + GST_OBJECT_LOCK (self); + for (item = GST_ELEMENT (self)->sinkpads; item; item = item->next) { + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (item->data); + + gst_aggregator_pad_set_flushing (aggpad, flow_return, TRUE); + } + GST_OBJECT_UNLOCK (self); + break; + } + } + + /* Pause the task here, the only ways to get here are: + * 1) We're stopping, in which case the task is stopped anyway + * 2) We got a flow error above, in which case it might take + * some time to forward the flow return upstream and we + * would otherwise call the task function over and over + * again without doing anything + */ + gst_pad_pause_task (self->srcpad); +} + +static gboolean +gst_aggregator_start (GstAggregator * self) +{ + GstAggregatorClass *klass; + gboolean result; + + self->priv->send_stream_start = TRUE; + self->priv->send_segment = TRUE; + self->priv->send_eos = TRUE; + self->priv->srccaps = NULL; + + gst_aggregator_set_allocation (self, NULL, NULL, NULL, NULL); + + klass = GST_AGGREGATOR_GET_CLASS (self); + + if (klass->start) + result = klass->start (self); + else + result = TRUE; + + return result; +} + +static gboolean +gst_aggregator_stop_srcpad_task (GstAggregator * self, GstEvent * flush_start) +{ + gboolean res = TRUE; + + GST_INFO_OBJECT (self, "%s srcpad task", + flush_start ? "Pausing" : "Stopping"); + + SRC_LOCK (self); + self->priv->running = FALSE; + SRC_BROADCAST (self); + SRC_UNLOCK (self); + + if (flush_start) { + res = gst_pad_push_event (self->srcpad, flush_start); + } + + gst_pad_stop_task (self->srcpad); + + return res; +} + +static void +gst_aggregator_start_srcpad_task (GstAggregator * self) +{ + GST_INFO_OBJECT (self, "Starting srcpad task"); + + self->priv->running = TRUE; + gst_pad_start_task (GST_PAD (self->srcpad), + (GstTaskFunction) gst_aggregator_aggregate_func, self, NULL); +} + +static GstFlowReturn +gst_aggregator_flush (GstAggregator * self) +{ + GstFlowReturn ret = GST_FLOW_OK; + GstAggregatorPrivate *priv = self->priv; + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + + GST_DEBUG_OBJECT (self, "Flushing everything"); + GST_OBJECT_LOCK (self); + priv->send_segment = TRUE; + priv->flushing = FALSE; + priv->tags_changed = FALSE; + GST_OBJECT_UNLOCK (self); + if (klass->flush) + ret = klass->flush (self); + + return ret; +} + + +/* Called with GstAggregator's object lock held */ + +static gboolean +gst_aggregator_all_flush_stop_received (GstAggregator * self, guint32 seqnum) +{ + GList *tmp; + GstAggregatorPad *tmppad; + + for (tmp = GST_ELEMENT (self)->sinkpads; tmp; tmp = tmp->next) { + tmppad = (GstAggregatorPad *) tmp->data; + + if (tmppad->priv->last_flush_stop_seqnum != seqnum) + return FALSE; + } + + return TRUE; +} + +/* Called with GstAggregator's object lock held */ + +static gboolean +gst_aggregator_all_flush_start_received (GstAggregator * self, guint32 seqnum) +{ + GList *tmp; + GstAggregatorPad *tmppad; + + for (tmp = GST_ELEMENT (self)->sinkpads; tmp; tmp = tmp->next) { + tmppad = (GstAggregatorPad *) tmp->data; + + if (tmppad->priv->last_flush_start_seqnum != seqnum) { + return FALSE; + } + } + + return TRUE; +} + +static void +gst_aggregator_flush_start (GstAggregator * self, GstAggregatorPad * aggpad, + GstEvent * event) +{ + GstAggregatorPrivate *priv = self->priv; + GstAggregatorPadPrivate *padpriv = aggpad->priv; + guint32 seqnum = gst_event_get_seqnum (event); + + gst_aggregator_pad_set_flushing (aggpad, GST_FLOW_FLUSHING, FALSE); + + PAD_FLUSH_LOCK (aggpad); + PAD_LOCK (aggpad); + padpriv->last_flush_start_seqnum = seqnum; + PAD_UNLOCK (aggpad); + + GST_OBJECT_LOCK (self); + + if (!priv->flushing && gst_aggregator_all_flush_start_received (self, seqnum)) { + /* Make sure we don't forward more than one FLUSH_START */ + priv->flushing = TRUE; + priv->next_seqnum = seqnum; + GST_OBJECT_UNLOCK (self); + + GST_INFO_OBJECT (self, "Flushing, pausing srcpad task"); + gst_aggregator_stop_srcpad_task (self, event); + + event = NULL; + } else { + gst_event_unref (event); + GST_OBJECT_UNLOCK (self); + } + + PAD_FLUSH_UNLOCK (aggpad); +} + +/* Must be called with the the PAD_LOCK held */ +static void +update_time_level (GstAggregatorPad * aggpad, gboolean head) +{ + GstAggregatorPadPrivate *priv = aggpad->priv; + + if (head) { + if (GST_CLOCK_TIME_IS_VALID (priv->head_position) && + priv->head_segment.format == GST_FORMAT_TIME) + priv->head_time = gst_segment_to_running_time (&priv->head_segment, + GST_FORMAT_TIME, priv->head_position); + else + priv->head_time = GST_CLOCK_TIME_NONE; + + if (!GST_CLOCK_TIME_IS_VALID (priv->tail_time)) + priv->tail_time = priv->head_time; + } else { + if (GST_CLOCK_TIME_IS_VALID (priv->tail_position) && + aggpad->segment.format == GST_FORMAT_TIME) + priv->tail_time = gst_segment_to_running_time (&aggpad->segment, + GST_FORMAT_TIME, priv->tail_position); + else + priv->tail_time = priv->head_time; + } + + if (priv->head_time == GST_CLOCK_TIME_NONE || + priv->tail_time == GST_CLOCK_TIME_NONE) { + priv->time_level = 0; + return; + } + + if (priv->tail_time > priv->head_time) + priv->time_level = 0; + else + priv->time_level = priv->head_time - priv->tail_time; +} + + +/* GstAggregator vmethods default implementations */ +static gboolean +gst_aggregator_default_sink_event (GstAggregator * self, + GstAggregatorPad * aggpad, GstEvent * event) +{ + gboolean res = TRUE; + GstPad *pad = GST_PAD (aggpad); + GstAggregatorPrivate *priv = self->priv; + + GST_DEBUG_OBJECT (aggpad, "Got event: %" GST_PTR_FORMAT, event); + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_FLUSH_START: + { + gst_aggregator_flush_start (self, aggpad, event); + /* We forward only in one case: right after flushing */ + event = NULL; + goto eat; + } + case GST_EVENT_FLUSH_STOP: + { + guint32 seqnum = gst_event_get_seqnum (event); + + PAD_FLUSH_LOCK (aggpad); + PAD_LOCK (aggpad); + aggpad->priv->last_flush_stop_seqnum = seqnum; + PAD_UNLOCK (aggpad); + + gst_aggregator_pad_flush (aggpad, self); + + GST_OBJECT_LOCK (self); + if (priv->flushing + && gst_aggregator_all_flush_stop_received (self, seqnum)) { + GST_OBJECT_UNLOCK (self); + /* That means we received FLUSH_STOP/FLUSH_STOP on + * all sinkpads -- Seeking is Done... sending FLUSH_STOP */ + gst_aggregator_flush (self); + gst_pad_push_event (self->srcpad, event); + event = NULL; + SRC_LOCK (self); + priv->send_eos = TRUE; + SRC_BROADCAST (self); + SRC_UNLOCK (self); + + GST_INFO_OBJECT (self, "Flush stopped"); + + gst_aggregator_start_srcpad_task (self); + } else { + GST_OBJECT_UNLOCK (self); + } + + PAD_FLUSH_UNLOCK (aggpad); + + /* We never forward the event */ + goto eat; + } + case GST_EVENT_EOS: + { + SRC_LOCK (self); + PAD_LOCK (aggpad); + g_assert (aggpad->priv->num_buffers == 0); + aggpad->priv->eos = TRUE; + PAD_UNLOCK (aggpad); + SRC_BROADCAST (self); + SRC_UNLOCK (self); + goto eat; + } + case GST_EVENT_SEGMENT: + { + PAD_LOCK (aggpad); + GST_OBJECT_LOCK (aggpad); + gst_event_copy_segment (event, &aggpad->segment); + /* We've got a new segment, tail_position is now meaningless + * and may interfere with the time_level calculation + */ + aggpad->priv->tail_position = GST_CLOCK_TIME_NONE; + update_time_level (aggpad, FALSE); + GST_OBJECT_UNLOCK (aggpad); + PAD_UNLOCK (aggpad); + + GST_OBJECT_LOCK (self); + self->priv->seqnum = gst_event_get_seqnum (event); + GST_OBJECT_UNLOCK (self); + goto eat; + } + case GST_EVENT_STREAM_START: + { + goto eat; + } + case GST_EVENT_GAP: + { + GstClockTime pts, endpts; + GstClockTime duration; + GstBuffer *gapbuf; + + gst_event_parse_gap (event, &pts, &duration); + + if (GST_CLOCK_TIME_IS_VALID (duration)) + endpts = pts + duration; + else + endpts = GST_CLOCK_TIME_NONE; + + GST_OBJECT_LOCK (aggpad); + res = gst_segment_clip (&aggpad->segment, GST_FORMAT_TIME, pts, endpts, + &pts, &endpts); + GST_OBJECT_UNLOCK (aggpad); + + if (!res) { + GST_WARNING_OBJECT (self, "GAP event outside segment, dropping"); + goto eat; + } + + if (GST_CLOCK_TIME_IS_VALID (endpts) && GST_CLOCK_TIME_IS_VALID (pts)) + duration = endpts - pts; + else + duration = GST_CLOCK_TIME_NONE; + + gapbuf = gst_buffer_new (); + GST_BUFFER_PTS (gapbuf) = pts; + GST_BUFFER_DURATION (gapbuf) = duration; + GST_BUFFER_FLAG_SET (gapbuf, GST_BUFFER_FLAG_GAP); + GST_BUFFER_FLAG_SET (gapbuf, GST_BUFFER_FLAG_DROPPABLE); + + /* Remove GAP event so we can replace it with the buffer */ + PAD_LOCK (aggpad); + if (g_queue_peek_tail (&aggpad->priv->data) == event) + gst_event_unref (g_queue_pop_tail (&aggpad->priv->data)); + PAD_UNLOCK (aggpad); + + if (gst_aggregator_pad_chain_internal (self, aggpad, gapbuf, FALSE) != + GST_FLOW_OK) { + GST_WARNING_OBJECT (self, "Failed to chain gap buffer"); + res = FALSE; + } + + goto eat; + } + case GST_EVENT_TAG: + goto eat; + default: + { + break; + } + } + + GST_DEBUG_OBJECT (pad, "Forwarding event: %" GST_PTR_FORMAT, event); + return gst_pad_event_default (pad, GST_OBJECT (self), event); + +eat: + GST_DEBUG_OBJECT (pad, "Eating event: %" GST_PTR_FORMAT, event); + if (event) + gst_event_unref (event); + + return res; +} + +/* Queue serialized events and let the others go through directly. + * The queued events with be handled from the src-pad task in + * gst_aggregator_do_events_and_queries(). + */ +static gboolean +gst_aggregator_default_sink_event_pre_queue (GstAggregator * self, + GstAggregatorPad * aggpad, GstEvent * event) +{ + GstFlowReturn ret = GST_FLOW_OK; + + if (GST_EVENT_IS_SERIALIZED (event) + && GST_EVENT_TYPE (event) != GST_EVENT_FLUSH_STOP) { + SRC_LOCK (self); + PAD_LOCK (aggpad); + + if (aggpad->priv->flow_return != GST_FLOW_OK) + goto flushing; + + if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) { + GST_OBJECT_LOCK (aggpad); + gst_event_copy_segment (event, &aggpad->priv->head_segment); + aggpad->priv->head_position = aggpad->priv->head_segment.position; + update_time_level (aggpad, TRUE); + GST_OBJECT_UNLOCK (aggpad); + } + + GST_DEBUG_OBJECT (aggpad, "Store event in queue: %" GST_PTR_FORMAT, event); + g_queue_push_head (&aggpad->priv->data, event); + SRC_BROADCAST (self); + PAD_UNLOCK (aggpad); + SRC_UNLOCK (self); + } else { + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + + if (!klass->sink_event (self, aggpad, event)) { + /* Copied from GstPad to convert boolean to a GstFlowReturn in + * the event handling func */ + ret = GST_FLOW_ERROR; + } + } + + return ret; + +flushing: + GST_DEBUG_OBJECT (aggpad, "Pad is %s, dropping event", + gst_flow_get_name (aggpad->priv->flow_return)); + PAD_UNLOCK (aggpad); + SRC_UNLOCK (self); + if (GST_EVENT_IS_STICKY (event)) + gst_pad_store_sticky_event (GST_PAD (aggpad), event); + gst_event_unref (event); + + return aggpad->priv->flow_return; +} + +static gboolean +gst_aggregator_stop_pad (GstElement * self, GstPad * epad, gpointer user_data) +{ + GstAggregatorPad *pad = GST_AGGREGATOR_PAD_CAST (epad); + GstAggregator *agg = GST_AGGREGATOR_CAST (self); + + gst_aggregator_pad_flush (pad, agg); + + PAD_LOCK (pad); + pad->priv->flow_return = GST_FLOW_FLUSHING; + pad->priv->negotiated = FALSE; + PAD_BROADCAST_EVENT (pad); + PAD_UNLOCK (pad); + + return TRUE; +} + +static gboolean +gst_aggregator_stop (GstAggregator * agg) +{ + GstAggregatorClass *klass; + gboolean result; + + gst_aggregator_reset_flow_values (agg); + + /* Application needs to make sure no pads are added while it shuts us down */ + gst_element_foreach_sink_pad (GST_ELEMENT_CAST (agg), + gst_aggregator_stop_pad, NULL); + + klass = GST_AGGREGATOR_GET_CLASS (agg); + + if (klass->stop) + result = klass->stop (agg); + else + result = TRUE; + + agg->priv->has_peer_latency = FALSE; + agg->priv->peer_latency_live = FALSE; + agg->priv->peer_latency_min = agg->priv->peer_latency_max = 0; + + if (agg->priv->tags) + gst_tag_list_unref (agg->priv->tags); + agg->priv->tags = NULL; + + gst_aggregator_set_allocation (agg, NULL, NULL, NULL, NULL); + + if (agg->priv->running) { + /* As sinkpads get deactivated after the src pad, we + * may have restarted the source pad task after receiving + * flush events on one of our sinkpads. Stop our src pad + * task again if that is the case */ + gst_aggregator_stop_srcpad_task (agg, NULL); + } + + return result; +} + +/* GstElement vmethods implementations */ +static GstStateChangeReturn +gst_aggregator_change_state (GstElement * element, GstStateChange transition) +{ + GstStateChangeReturn ret; + GstAggregator *self = GST_AGGREGATOR (element); + + switch (transition) { + case GST_STATE_CHANGE_READY_TO_PAUSED: + if (!gst_aggregator_start (self)) + goto error_start; + break; + default: + break; + } + + if ((ret = + GST_ELEMENT_CLASS (aggregator_parent_class)->change_state (element, + transition)) == GST_STATE_CHANGE_FAILURE) + goto failure; + + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + if (!gst_aggregator_stop (self)) { + /* What to do in this case? Error out? */ + GST_ERROR_OBJECT (self, "Subclass failed to stop."); + } + break; + default: + break; + } + + return ret; + +/* ERRORS */ +failure: + { + GST_ERROR_OBJECT (element, "parent failed state change"); + return ret; + } +error_start: + { + GST_ERROR_OBJECT (element, "Subclass failed to start"); + return GST_STATE_CHANGE_FAILURE; + } +} + +static void +gst_aggregator_release_pad (GstElement * element, GstPad * pad) +{ + GstAggregator *self = GST_AGGREGATOR (element); + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (pad); + + GST_INFO_OBJECT (pad, "Removing pad"); + + SRC_LOCK (self); + gst_aggregator_pad_set_flushing (aggpad, GST_FLOW_FLUSHING, TRUE); + gst_element_remove_pad (element, pad); + + self->priv->has_peer_latency = FALSE; + SRC_BROADCAST (self); + SRC_UNLOCK (self); +} + +static GstAggregatorPad * +gst_aggregator_default_create_new_pad (GstAggregator * self, + GstPadTemplate * templ, const gchar * req_name, const GstCaps * caps) +{ + GstAggregatorPad *agg_pad; + GstAggregatorPrivate *priv = self->priv; + gint serial = 0; + gchar *name = NULL; + GType pad_type = + GST_PAD_TEMPLATE_GTYPE (templ) == + G_TYPE_NONE ? GST_TYPE_AGGREGATOR_PAD : GST_PAD_TEMPLATE_GTYPE (templ); + + if (templ->direction != GST_PAD_SINK) + goto not_sink; + + if (templ->presence != GST_PAD_REQUEST) + goto not_request; + + GST_OBJECT_LOCK (self); + if (req_name == NULL || strlen (req_name) < 6 + || !g_str_has_prefix (req_name, "sink_") + || strrchr (req_name, '%') != NULL) { + /* no name given when requesting the pad, use next available int */ + serial = ++priv->max_padserial; + } else { + gchar *endptr = NULL; + + /* parse serial number from requested padname */ + serial = g_ascii_strtoull (&req_name[5], &endptr, 10); + if (endptr != NULL && *endptr == '\0') { + if (serial > priv->max_padserial) { + priv->max_padserial = serial; + } + } else { + serial = ++priv->max_padserial; + } + } + + name = g_strdup_printf ("sink_%u", serial); + g_assert (g_type_is_a (pad_type, GST_TYPE_AGGREGATOR_PAD)); + agg_pad = g_object_new (pad_type, + "name", name, "direction", GST_PAD_SINK, "template", templ, NULL); + g_free (name); + + GST_OBJECT_UNLOCK (self); + + return agg_pad; + + /* errors */ +not_sink: + { + GST_WARNING_OBJECT (self, "request new pad that is not a SINK pad"); + return NULL; + } +not_request: + { + GST_WARNING_OBJECT (self, "request new pad that is not a REQUEST pad"); + return NULL; + } +} + +static GstPad * +gst_aggregator_request_new_pad (GstElement * element, + GstPadTemplate * templ, const gchar * req_name, const GstCaps * caps) +{ + GstAggregator *self; + GstAggregatorPad *agg_pad; + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (element); + GstAggregatorPrivate *priv = GST_AGGREGATOR (element)->priv; + + self = GST_AGGREGATOR (element); + + agg_pad = klass->create_new_pad (self, templ, req_name, caps); + if (!agg_pad) { + GST_ERROR_OBJECT (element, "Couldn't create new pad"); + return NULL; + } + + GST_DEBUG_OBJECT (element, "Adding pad %s", GST_PAD_NAME (agg_pad)); + + if (priv->running) + gst_pad_set_active (GST_PAD (agg_pad), TRUE); + + /* add the pad to the element */ + gst_element_add_pad (element, GST_PAD (agg_pad)); + + return GST_PAD (agg_pad); +} + +/* Must be called with SRC_LOCK held */ + +static gboolean +gst_aggregator_query_latency_unlocked (GstAggregator * self, GstQuery * query) +{ + gboolean query_ret, live; + GstClockTime our_latency, min, max; + + query_ret = gst_pad_query_default (self->srcpad, GST_OBJECT (self), query); + + if (!query_ret) { + GST_WARNING_OBJECT (self, "Latency query failed"); + return FALSE; + } + + gst_query_parse_latency (query, &live, &min, &max); + + if (G_UNLIKELY (!GST_CLOCK_TIME_IS_VALID (min))) { + GST_ERROR_OBJECT (self, "Invalid minimum latency %" GST_TIME_FORMAT + ". Please file a bug at " PACKAGE_BUGREPORT ".", GST_TIME_ARGS (min)); + return FALSE; + } + + if (self->priv->upstream_latency_min > min) { + GstClockTimeDiff diff = + GST_CLOCK_DIFF (min, self->priv->upstream_latency_min); + + min += diff; + if (GST_CLOCK_TIME_IS_VALID (max)) { + max += diff; + } + } + + if (min > max && GST_CLOCK_TIME_IS_VALID (max)) { + GST_ELEMENT_WARNING (self, CORE, CLOCK, (NULL), + ("Impossible to configure latency: max %" GST_TIME_FORMAT " < min %" + GST_TIME_FORMAT ". Add queues or other buffering elements.", + GST_TIME_ARGS (max), GST_TIME_ARGS (min))); + return FALSE; + } + + our_latency = self->priv->latency; + + self->priv->peer_latency_live = live; + self->priv->peer_latency_min = min; + self->priv->peer_latency_max = max; + self->priv->has_peer_latency = TRUE; + + /* add our own */ + min += our_latency; + min += self->priv->sub_latency_min; + if (GST_CLOCK_TIME_IS_VALID (self->priv->sub_latency_max) + && GST_CLOCK_TIME_IS_VALID (max)) + max += self->priv->sub_latency_max + our_latency; + else + max = GST_CLOCK_TIME_NONE; + + SRC_BROADCAST (self); + + GST_DEBUG_OBJECT (self, "configured latency live:%s min:%" G_GINT64_FORMAT + " max:%" G_GINT64_FORMAT, live ? "true" : "false", min, max); + + gst_query_set_latency (query, live, min, max); + + return query_ret; +} + +/* + * MUST be called with the src_lock held. + * + * See gst_aggregator_get_latency() for doc + */ +static GstClockTime +gst_aggregator_get_latency_unlocked (GstAggregator * self) +{ + GstClockTime latency; + + g_return_val_if_fail (GST_IS_AGGREGATOR (self), 0); + + if (!self->priv->has_peer_latency) { + GstQuery *query = gst_query_new_latency (); + gboolean ret; + + ret = gst_aggregator_query_latency_unlocked (self, query); + gst_query_unref (query); + if (!ret) + return GST_CLOCK_TIME_NONE; + } + + if (!self->priv->has_peer_latency || !self->priv->peer_latency_live) + return GST_CLOCK_TIME_NONE; + + /* latency_min is never GST_CLOCK_TIME_NONE by construction */ + latency = self->priv->peer_latency_min; + + /* add our own */ + latency += self->priv->latency; + latency += self->priv->sub_latency_min; + + return latency; +} + +/** + * gst_aggregator_get_latency: + * @self: a #GstAggregator + * + * Retrieves the latency values reported by @self in response to the latency + * query, or %GST_CLOCK_TIME_NONE if there is not live source connected and the element + * will not wait for the clock. + * + * Typically only called by subclasses. + * + * Returns: The latency or %GST_CLOCK_TIME_NONE if the element does not sync + */ +GstClockTime +gst_aggregator_get_latency (GstAggregator * self) +{ + GstClockTime ret; + + SRC_LOCK (self); + ret = gst_aggregator_get_latency_unlocked (self); + SRC_UNLOCK (self); + + return ret; +} + +static gboolean +gst_aggregator_send_event (GstElement * element, GstEvent * event) +{ + GstAggregator *self = GST_AGGREGATOR (element); + + GST_STATE_LOCK (element); + if (GST_EVENT_TYPE (event) == GST_EVENT_SEEK && + GST_STATE (element) < GST_STATE_PAUSED) { + gdouble rate; + GstFormat fmt; + GstSeekFlags flags; + GstSeekType start_type, stop_type; + gint64 start, stop; + + gst_event_parse_seek (event, &rate, &fmt, &flags, &start_type, + &start, &stop_type, &stop); + + GST_OBJECT_LOCK (self); + gst_segment_do_seek (&GST_AGGREGATOR_PAD (self->srcpad)->segment, rate, fmt, + flags, start_type, start, stop_type, stop, NULL); + self->priv->next_seqnum = gst_event_get_seqnum (event); + self->priv->first_buffer = FALSE; + GST_OBJECT_UNLOCK (self); + + GST_DEBUG_OBJECT (element, "Storing segment %" GST_PTR_FORMAT, event); + } + GST_STATE_UNLOCK (element); + + return GST_ELEMENT_CLASS (aggregator_parent_class)->send_event (element, + event); +} + +static gboolean +gst_aggregator_default_src_query (GstAggregator * self, GstQuery * query) +{ + gboolean res = TRUE; + + switch (GST_QUERY_TYPE (query)) { + case GST_QUERY_SEEKING: + { + GstFormat format; + + /* don't pass it along as some (file)sink might claim it does + * whereas with a collectpads in between that will not likely work */ + gst_query_parse_seeking (query, &format, NULL, NULL, NULL); + gst_query_set_seeking (query, format, FALSE, 0, -1); + res = TRUE; + + break; + } + case GST_QUERY_LATENCY: + SRC_LOCK (self); + res = gst_aggregator_query_latency_unlocked (self, query); + SRC_UNLOCK (self); + break; + default: + return gst_pad_query_default (self->srcpad, GST_OBJECT (self), query); + } + + return res; +} + +static gboolean +gst_aggregator_event_forward_func (GstPad * pad, gpointer user_data) +{ + EventData *evdata = user_data; + gboolean ret = TRUE; + GstPad *peer = gst_pad_get_peer (pad); + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (pad); + + if (peer) { + if (evdata->only_to_active_pads && aggpad->priv->first_buffer) { + GST_DEBUG_OBJECT (pad, "not sending event to inactive pad"); + ret = TRUE; + } else { + ret = gst_pad_send_event (peer, gst_event_ref (evdata->event)); + GST_DEBUG_OBJECT (pad, "return of event push is %d", ret); + } + } + + if (ret == FALSE) { + if (GST_EVENT_TYPE (evdata->event) == GST_EVENT_SEEK) { + GstQuery *seeking = gst_query_new_seeking (GST_FORMAT_TIME); + + GST_DEBUG_OBJECT (pad, "Event %" GST_PTR_FORMAT " failed", evdata->event); + + if (gst_pad_query (peer, seeking)) { + gboolean seekable; + + gst_query_parse_seeking (seeking, NULL, &seekable, NULL, NULL); + + if (seekable == FALSE) { + GST_INFO_OBJECT (pad, + "Source not seekable, We failed but it does not matter!"); + + ret = TRUE; + } + } else { + GST_ERROR_OBJECT (pad, "Query seeking FAILED"); + } + + gst_query_unref (seeking); + } + } else { + evdata->one_actually_seeked = TRUE; + } + + evdata->result &= ret; + + if (peer) + gst_object_unref (peer); + + /* Always send to all pads */ + return FALSE; +} + +static void +gst_aggregator_forward_event_to_all_sinkpads (GstAggregator * self, + EventData * evdata) +{ + evdata->result = TRUE; + evdata->one_actually_seeked = FALSE; + + gst_pad_forward (self->srcpad, gst_aggregator_event_forward_func, evdata); + + gst_event_unref (evdata->event); +} + +static gboolean +gst_aggregator_do_seek (GstAggregator * self, GstEvent * event) +{ + gdouble rate; + GstFormat fmt; + GstSeekFlags flags; + GstSeekType start_type, stop_type; + gint64 start, stop; + gboolean flush; + EventData evdata = { 0, }; + GstAggregatorPrivate *priv = self->priv; + + gst_event_parse_seek (event, &rate, &fmt, &flags, &start_type, + &start, &stop_type, &stop); + + GST_INFO_OBJECT (self, "starting SEEK"); + + flush = flags & GST_SEEK_FLAG_FLUSH; + + GST_OBJECT_LOCK (self); + + if (gst_event_get_seqnum (event) == self->priv->next_seqnum) { + evdata.result = TRUE; + GST_DEBUG_OBJECT (self, "Dropping duplicated seek event with seqnum %d", + self->priv->next_seqnum); + GST_OBJECT_UNLOCK (self); + goto done; + } + + self->priv->next_seqnum = gst_event_get_seqnum (event); + + gst_segment_do_seek (&GST_AGGREGATOR_PAD (self->srcpad)->segment, rate, fmt, + flags, start_type, start, stop_type, stop, NULL); + + /* Seeking sets a position */ + self->priv->first_buffer = FALSE; + + if (flush) + priv->flushing = TRUE; + + GST_OBJECT_UNLOCK (self); + + if (flush) { + GstEvent *event = gst_event_new_flush_start (); + + gst_event_set_seqnum (event, self->priv->next_seqnum); + gst_aggregator_stop_srcpad_task (self, event); + } + + /* forward the seek upstream */ + evdata.event = event; + evdata.flush = flush; + evdata.only_to_active_pads = FALSE; + gst_aggregator_forward_event_to_all_sinkpads (self, &evdata); + event = NULL; + + if (!evdata.result || !evdata.one_actually_seeked) { + GST_OBJECT_LOCK (self); + priv->flushing = FALSE; + GST_OBJECT_UNLOCK (self); + + /* No flush stop is inbound for us to forward */ + if (flush) { + GstEvent *event = gst_event_new_flush_stop (TRUE); + + gst_event_set_seqnum (event, self->priv->next_seqnum); + gst_pad_push_event (self->srcpad, event); + } + } + +done: + GST_INFO_OBJECT (self, "seek done, result: %d", evdata.result); + + return evdata.result; +} + +static gboolean +gst_aggregator_default_src_event (GstAggregator * self, GstEvent * event) +{ + EventData evdata = { 0, }; + + switch (GST_EVENT_TYPE (event)) { + case GST_EVENT_SEEK: + /* _do_seek() unrefs the event. */ + return gst_aggregator_do_seek (self, event); + case GST_EVENT_NAVIGATION: + /* navigation is rather pointless. */ + gst_event_unref (event); + return FALSE; + default: + break; + } + + /* Don't forward QOS events to pads that had no active buffer yet. Otherwise + * they will receive a QOS event that has earliest_time=0 (because we can't + * have negative timestamps), and consider their buffer as too late */ + evdata.event = event; + evdata.flush = FALSE; + evdata.only_to_active_pads = GST_EVENT_TYPE (event) == GST_EVENT_QOS; + gst_aggregator_forward_event_to_all_sinkpads (self, &evdata); + return evdata.result; +} + +static gboolean +gst_aggregator_src_pad_event_func (GstPad * pad, GstObject * parent, + GstEvent * event) +{ + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (parent); + + return klass->src_event (GST_AGGREGATOR (parent), event); +} + +static gboolean +gst_aggregator_src_pad_query_func (GstPad * pad, GstObject * parent, + GstQuery * query) +{ + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (parent); + + return klass->src_query (GST_AGGREGATOR (parent), query); +} + +static gboolean +gst_aggregator_src_pad_activate_mode_func (GstPad * pad, + GstObject * parent, GstPadMode mode, gboolean active) +{ + GstAggregator *self = GST_AGGREGATOR (parent); + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (parent); + + if (klass->src_activate) { + if (klass->src_activate (self, mode, active) == FALSE) { + return FALSE; + } + } + + if (active == TRUE) { + switch (mode) { + case GST_PAD_MODE_PUSH: + { + GST_INFO_OBJECT (pad, "Activating pad!"); + gst_aggregator_start_srcpad_task (self); + return TRUE; + } + default: + { + GST_ERROR_OBJECT (pad, "Only supported mode is PUSH"); + return FALSE; + } + } + } + + /* deactivating */ + GST_INFO_OBJECT (self, "Deactivating srcpad"); + + gst_aggregator_stop_srcpad_task (self, FALSE); + + return TRUE; +} + +static gboolean +gst_aggregator_default_sink_query (GstAggregator * self, + GstAggregatorPad * aggpad, GstQuery * query) +{ + GstPad *pad = GST_PAD (aggpad); + + if (GST_QUERY_TYPE (query) == GST_QUERY_ALLOCATION) { + GstQuery *decide_query = NULL; + GstAggregatorClass *agg_class; + gboolean ret; + + GST_OBJECT_LOCK (self); + PAD_LOCK (aggpad); + if (G_UNLIKELY (!aggpad->priv->negotiated)) { + GST_DEBUG_OBJECT (self, + "not negotiated yet, can't answer ALLOCATION query"); + PAD_UNLOCK (aggpad); + GST_OBJECT_UNLOCK (self); + + return FALSE; + } + + if ((decide_query = self->priv->allocation_query)) + gst_query_ref (decide_query); + PAD_UNLOCK (aggpad); + GST_OBJECT_UNLOCK (self); + + GST_DEBUG_OBJECT (self, + "calling propose allocation with query %" GST_PTR_FORMAT, decide_query); + + agg_class = GST_AGGREGATOR_GET_CLASS (self); + + /* pass the query to the propose_allocation vmethod if any */ + if (agg_class->propose_allocation) + ret = agg_class->propose_allocation (self, aggpad, decide_query, query); + else + ret = FALSE; + + if (decide_query) + gst_query_unref (decide_query); + + GST_DEBUG_OBJECT (self, "ALLOCATION ret %d, %" GST_PTR_FORMAT, ret, query); + return ret; + } + + return gst_pad_query_default (pad, GST_OBJECT (self), query); +} + +static gboolean +gst_aggregator_default_sink_query_pre_queue (GstAggregator * self, + GstAggregatorPad * aggpad, GstQuery * query) +{ + if (GST_QUERY_IS_SERIALIZED (query)) { + GstStructure *s; + gboolean ret = FALSE; + + SRC_LOCK (self); + PAD_LOCK (aggpad); + + if (aggpad->priv->flow_return != GST_FLOW_OK) { + SRC_UNLOCK (self); + goto flushing; + } + + g_queue_push_head (&aggpad->priv->data, query); + SRC_BROADCAST (self); + SRC_UNLOCK (self); + + while (!gst_aggregator_pad_queue_is_empty (aggpad) + && aggpad->priv->flow_return == GST_FLOW_OK) { + GST_DEBUG_OBJECT (aggpad, "Waiting for buffer to be consumed"); + PAD_WAIT_EVENT (aggpad); + } + + s = gst_query_writable_structure (query); + if (gst_structure_get_boolean (s, "gst-aggregator-retval", &ret)) + gst_structure_remove_field (s, "gst-aggregator-retval"); + else + g_queue_remove (&aggpad->priv->data, query); + + if (aggpad->priv->flow_return != GST_FLOW_OK) + goto flushing; + + PAD_UNLOCK (aggpad); + + return ret; + } else { + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + + return klass->sink_query (self, aggpad, query); + } + +flushing: + GST_DEBUG_OBJECT (aggpad, "Pad is %s, dropping query", + gst_flow_get_name (aggpad->priv->flow_return)); + PAD_UNLOCK (aggpad); + + return FALSE; +} + +static void +gst_aggregator_finalize (GObject * object) +{ + GstAggregator *self = (GstAggregator *) object; + + g_mutex_clear (&self->priv->src_lock); + g_cond_clear (&self->priv->src_cond); + + G_OBJECT_CLASS (aggregator_parent_class)->finalize (object); +} + +/* + * gst_aggregator_set_latency_property: + * @agg: a #GstAggregator + * @latency: the new latency value (in nanoseconds). + * + * Sets the new latency value to @latency. This value is used to limit the + * amount of time a pad waits for data to appear before considering the pad + * as unresponsive. + */ +static void +gst_aggregator_set_latency_property (GstAggregator * self, GstClockTime latency) +{ + gboolean changed; + + g_return_if_fail (GST_IS_AGGREGATOR (self)); + g_return_if_fail (GST_CLOCK_TIME_IS_VALID (latency)); + + SRC_LOCK (self); + changed = (self->priv->latency != latency); + + if (changed) { + GList *item; + + GST_OBJECT_LOCK (self); + /* First lock all the pads */ + for (item = GST_ELEMENT_CAST (self)->sinkpads; item; item = item->next) { + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (item->data); + PAD_LOCK (aggpad); + } + + self->priv->latency = latency; + + SRC_BROADCAST (self); + + /* Now wake up the pads */ + for (item = GST_ELEMENT_CAST (self)->sinkpads; item; item = item->next) { + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (item->data); + PAD_BROADCAST_EVENT (aggpad); + PAD_UNLOCK (aggpad); + } + GST_OBJECT_UNLOCK (self); + } + + SRC_UNLOCK (self); + + if (changed) + gst_element_post_message (GST_ELEMENT_CAST (self), + gst_message_new_latency (GST_OBJECT_CAST (self))); +} + +/* + * gst_aggregator_get_latency_property: + * @agg: a #GstAggregator + * + * Gets the latency value. See gst_aggregator_set_latency for + * more details. + * + * Returns: The time in nanoseconds to wait for data to arrive on a sink pad + * before a pad is deemed unresponsive. A value of -1 means an + * unlimited time. + */ +static GstClockTime +gst_aggregator_get_latency_property (GstAggregator * agg) +{ + GstClockTime res; + + g_return_val_if_fail (GST_IS_AGGREGATOR (agg), GST_CLOCK_TIME_NONE); + + GST_OBJECT_LOCK (agg); + res = agg->priv->latency; + GST_OBJECT_UNLOCK (agg); + + return res; +} + +static void +gst_aggregator_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAggregator *agg = GST_AGGREGATOR (object); + + switch (prop_id) { + case PROP_LATENCY: + gst_aggregator_set_latency_property (agg, g_value_get_uint64 (value)); + break; + case PROP_MIN_UPSTREAM_LATENCY: + SRC_LOCK (agg); + agg->priv->upstream_latency_min = g_value_get_uint64 (value); + SRC_UNLOCK (agg); + break; + case PROP_START_TIME_SELECTION: + agg->priv->start_time_selection = g_value_get_enum (value); + break; + case PROP_START_TIME: + agg->priv->start_time = g_value_get_uint64 (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_aggregator_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstAggregator *agg = GST_AGGREGATOR (object); + + switch (prop_id) { + case PROP_LATENCY: + g_value_set_uint64 (value, gst_aggregator_get_latency_property (agg)); + break; + case PROP_MIN_UPSTREAM_LATENCY: + SRC_LOCK (agg); + g_value_set_uint64 (value, agg->priv->upstream_latency_min); + SRC_UNLOCK (agg); + break; + case PROP_START_TIME_SELECTION: + g_value_set_enum (value, agg->priv->start_time_selection); + break; + case PROP_START_TIME: + g_value_set_uint64 (value, agg->priv->start_time); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/* GObject vmethods implementations */ +static void +gst_aggregator_class_init (GstAggregatorClass * klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + GstElementClass *gstelement_class = (GstElementClass *) klass; + + aggregator_parent_class = g_type_class_peek_parent (klass); + + GST_DEBUG_CATEGORY_INIT (aggregator_debug, "aggregator", + GST_DEBUG_FG_MAGENTA, "GstAggregator"); + + if (aggregator_private_offset != 0) + g_type_class_adjust_private_offset (klass, &aggregator_private_offset); + + klass->finish_buffer = gst_aggregator_default_finish_buffer; + + klass->sink_event = gst_aggregator_default_sink_event; + klass->sink_query = gst_aggregator_default_sink_query; + + klass->src_event = gst_aggregator_default_src_event; + klass->src_query = gst_aggregator_default_src_query; + + klass->create_new_pad = gst_aggregator_default_create_new_pad; + klass->update_src_caps = gst_aggregator_default_update_src_caps; + klass->fixate_src_caps = gst_aggregator_default_fixate_src_caps; + klass->negotiated_src_caps = gst_aggregator_default_negotiated_src_caps; + + klass->negotiate = gst_aggregator_default_negotiate; + + klass->sink_event_pre_queue = gst_aggregator_default_sink_event_pre_queue; + klass->sink_query_pre_queue = gst_aggregator_default_sink_query_pre_queue; + + gstelement_class->request_new_pad = + GST_DEBUG_FUNCPTR (gst_aggregator_request_new_pad); + gstelement_class->send_event = GST_DEBUG_FUNCPTR (gst_aggregator_send_event); + gstelement_class->release_pad = + GST_DEBUG_FUNCPTR (gst_aggregator_release_pad); + gstelement_class->change_state = + GST_DEBUG_FUNCPTR (gst_aggregator_change_state); + + gobject_class->set_property = gst_aggregator_set_property; + gobject_class->get_property = gst_aggregator_get_property; + gobject_class->finalize = gst_aggregator_finalize; + + g_object_class_install_property (gobject_class, PROP_LATENCY, + g_param_spec_uint64 ("latency", "Buffer latency", + "Additional latency in live mode to allow upstream " + "to take longer to produce buffers for the current " + "position (in nanoseconds)", 0, G_MAXUINT64, + DEFAULT_LATENCY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + /** + * GstAggregator:min-upstream-latency: + * + * Force minimum upstream latency (in nanoseconds). When sources with a + * higher latency are expected to be plugged in dynamically after the + * aggregator has started playing, this allows overriding the minimum + * latency reported by the initial source(s). This is only taken into + * account when larger than the actually reported minimum latency. + * + * Since: 1.16 + */ + g_object_class_install_property (gobject_class, PROP_MIN_UPSTREAM_LATENCY, + g_param_spec_uint64 ("min-upstream-latency", "Buffer latency", + "When sources with a higher latency are expected to be plugged " + "in dynamically after the aggregator has started playing, " + "this allows overriding the minimum latency reported by the " + "initial source(s). This is only taken into account when larger " + "than the actually reported minimum latency. (nanoseconds)", + 0, G_MAXUINT64, + DEFAULT_LATENCY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, PROP_START_TIME_SELECTION, + g_param_spec_enum ("start-time-selection", "Start Time Selection", + "Decides which start time is output", + gst_aggregator_start_time_selection_get_type (), + DEFAULT_START_TIME_SELECTION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, PROP_START_TIME, + g_param_spec_uint64 ("start-time", "Start Time", + "Start time to use if start-time-selection=set", 0, + G_MAXUINT64, + DEFAULT_START_TIME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + +static inline gpointer +gst_aggregator_get_instance_private (GstAggregator * self) +{ + return (G_STRUCT_MEMBER_P (self, aggregator_private_offset)); +} + +static void +gst_aggregator_init (GstAggregator * self, GstAggregatorClass * klass) +{ + GstPadTemplate *pad_template; + GstAggregatorPrivate *priv; + GType pad_type; + + g_return_if_fail (klass->aggregate != NULL); + + self->priv = gst_aggregator_get_instance_private (self); + + priv = self->priv; + + pad_template = + gst_element_class_get_pad_template (GST_ELEMENT_CLASS (klass), "src"); + g_return_if_fail (pad_template != NULL); + + priv->max_padserial = -1; + priv->tags_changed = FALSE; + + self->priv->peer_latency_live = FALSE; + self->priv->peer_latency_min = self->priv->sub_latency_min = 0; + self->priv->peer_latency_max = self->priv->sub_latency_max = 0; + self->priv->has_peer_latency = FALSE; + + pad_type = + GST_PAD_TEMPLATE_GTYPE (pad_template) == + G_TYPE_NONE ? GST_TYPE_AGGREGATOR_PAD : + GST_PAD_TEMPLATE_GTYPE (pad_template); + g_assert (g_type_is_a (pad_type, GST_TYPE_AGGREGATOR_PAD)); + self->srcpad = + g_object_new (pad_type, "name", "src", "direction", GST_PAD_SRC, + "template", pad_template, NULL); + + gst_aggregator_reset_flow_values (self); + + gst_pad_set_event_function (self->srcpad, + GST_DEBUG_FUNCPTR (gst_aggregator_src_pad_event_func)); + gst_pad_set_query_function (self->srcpad, + GST_DEBUG_FUNCPTR (gst_aggregator_src_pad_query_func)); + gst_pad_set_activatemode_function (self->srcpad, + GST_DEBUG_FUNCPTR (gst_aggregator_src_pad_activate_mode_func)); + + gst_element_add_pad (GST_ELEMENT (self), self->srcpad); + + self->priv->upstream_latency_min = DEFAULT_MIN_UPSTREAM_LATENCY; + self->priv->latency = DEFAULT_LATENCY; + self->priv->start_time_selection = DEFAULT_START_TIME_SELECTION; + self->priv->start_time = DEFAULT_START_TIME; + + g_mutex_init (&self->priv->src_lock); + g_cond_init (&self->priv->src_cond); +} + +/* we can't use G_DEFINE_ABSTRACT_TYPE because we need the klass in the _init + * method to get to the padtemplates */ +GType +gst_aggregator_get_type (void) +{ + static volatile gsize type = 0; + + if (g_once_init_enter (&type)) { + GType _type; + static const GTypeInfo info = { + sizeof (GstAggregatorClass), + NULL, + NULL, + (GClassInitFunc) gst_aggregator_class_init, + NULL, + NULL, + sizeof (GstAggregator), + 0, + (GInstanceInitFunc) gst_aggregator_init, + }; + + _type = g_type_register_static (GST_TYPE_ELEMENT, + "GstAggregatorFallback", &info, G_TYPE_FLAG_ABSTRACT); + + aggregator_private_offset = + g_type_add_instance_private (_type, sizeof (GstAggregatorPrivate)); + + g_once_init_leave (&type, _type); + } + return type; +} + +/* Must be called with SRC lock and PAD lock held */ +static gboolean +gst_aggregator_pad_has_space (GstAggregator * self, GstAggregatorPad * aggpad) +{ + /* Empty queue always has space */ + if (aggpad->priv->num_buffers == 0 && aggpad->priv->clipped_buffer == NULL) + return TRUE; + + /* We also want at least two buffers, one is being processed and one is ready + * for the next iteration when we operate in live mode. */ + if (self->priv->peer_latency_live && aggpad->priv->num_buffers < 2) + return TRUE; + + /* zero latency, if there is a buffer, it's full */ + if (self->priv->latency == 0) + return FALSE; + + /* Allow no more buffers than the latency */ + return (aggpad->priv->time_level <= self->priv->latency); +} + +/* Must be called with the PAD_LOCK held */ +static void +apply_buffer (GstAggregatorPad * aggpad, GstBuffer * buffer, gboolean head) +{ + GstClockTime timestamp; + + if (GST_BUFFER_DTS_IS_VALID (buffer)) + timestamp = GST_BUFFER_DTS (buffer); + else + timestamp = GST_BUFFER_PTS (buffer); + + if (timestamp == GST_CLOCK_TIME_NONE) { + if (head) + timestamp = aggpad->priv->head_position; + else + timestamp = aggpad->priv->tail_position; + } + + /* add duration */ + if (GST_BUFFER_DURATION_IS_VALID (buffer)) + timestamp += GST_BUFFER_DURATION (buffer); + + if (head) + aggpad->priv->head_position = timestamp; + else + aggpad->priv->tail_position = timestamp; + + update_time_level (aggpad, head); +} + +/* + * Can be called either from the sinkpad's chain function or from the srcpad's + * thread in the case of a buffer synthetized from a GAP event. + * Because of this second case, FLUSH_LOCK can't be used here. + */ + +static GstFlowReturn +gst_aggregator_pad_chain_internal (GstAggregator * self, + GstAggregatorPad * aggpad, GstBuffer * buffer, gboolean head) +{ + GstFlowReturn flow_return; + GstClockTime buf_pts; + + PAD_LOCK (aggpad); + flow_return = aggpad->priv->flow_return; + if (flow_return != GST_FLOW_OK) + goto flushing; + + PAD_UNLOCK (aggpad); + + buf_pts = GST_BUFFER_PTS (buffer); + + for (;;) { + SRC_LOCK (self); + GST_OBJECT_LOCK (self); + PAD_LOCK (aggpad); + + if (aggpad->priv->first_buffer) { + self->priv->has_peer_latency = FALSE; + aggpad->priv->first_buffer = FALSE; + } + + if ((gst_aggregator_pad_has_space (self, aggpad) || !head) + && aggpad->priv->flow_return == GST_FLOW_OK) { + if (head) + g_queue_push_head (&aggpad->priv->data, buffer); + else + g_queue_push_tail (&aggpad->priv->data, buffer); + apply_buffer (aggpad, buffer, head); + aggpad->priv->num_buffers++; + buffer = NULL; + SRC_BROADCAST (self); + break; + } + + flow_return = aggpad->priv->flow_return; + if (flow_return != GST_FLOW_OK) { + GST_OBJECT_UNLOCK (self); + SRC_UNLOCK (self); + goto flushing; + } + GST_DEBUG_OBJECT (aggpad, "Waiting for buffer to be consumed"); + GST_OBJECT_UNLOCK (self); + SRC_UNLOCK (self); + PAD_WAIT_EVENT (aggpad); + + PAD_UNLOCK (aggpad); + } + + if (self->priv->first_buffer) { + GstClockTime start_time; + GstAggregatorPad *srcpad = GST_AGGREGATOR_PAD (self->srcpad); + + switch (self->priv->start_time_selection) { + case GST_AGGREGATOR_START_TIME_SELECTION_ZERO: + default: + start_time = 0; + break; + case GST_AGGREGATOR_START_TIME_SELECTION_FIRST: + GST_OBJECT_LOCK (aggpad); + if (aggpad->priv->head_segment.format == GST_FORMAT_TIME) { + start_time = buf_pts; + if (start_time != -1) { + start_time = MAX (start_time, aggpad->priv->head_segment.start); + start_time = + gst_segment_to_running_time (&aggpad->priv->head_segment, + GST_FORMAT_TIME, start_time); + } + } else { + start_time = 0; + GST_WARNING_OBJECT (aggpad, + "Ignoring request of selecting the first start time " + "as the segment is a %s segment instead of a time segment", + gst_format_get_name (aggpad->segment.format)); + } + GST_OBJECT_UNLOCK (aggpad); + break; + case GST_AGGREGATOR_START_TIME_SELECTION_SET: + start_time = self->priv->start_time; + if (start_time == -1) + start_time = 0; + break; + } + + if (start_time != -1) { + if (srcpad->segment.position == -1) + srcpad->segment.position = start_time; + else + srcpad->segment.position = MIN (start_time, srcpad->segment.position); + + GST_DEBUG_OBJECT (self, "Selecting start time %" GST_TIME_FORMAT, + GST_TIME_ARGS (start_time)); + } + } + + PAD_UNLOCK (aggpad); + GST_OBJECT_UNLOCK (self); + SRC_UNLOCK (self); + + GST_DEBUG_OBJECT (aggpad, "Done chaining"); + + return flow_return; + +flushing: + PAD_UNLOCK (aggpad); + + GST_DEBUG_OBJECT (aggpad, "Pad is %s, dropping buffer", + gst_flow_get_name (flow_return)); + if (buffer) + gst_buffer_unref (buffer); + + return flow_return; +} + +static GstFlowReturn +gst_aggregator_pad_chain (GstPad * pad, GstObject * object, GstBuffer * buffer) +{ + GstFlowReturn ret; + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (pad); + + PAD_FLUSH_LOCK (aggpad); + + ret = gst_aggregator_pad_chain_internal (GST_AGGREGATOR_CAST (object), + aggpad, buffer, TRUE); + + PAD_FLUSH_UNLOCK (aggpad); + + return ret; +} + +static gboolean +gst_aggregator_pad_query_func (GstPad * pad, GstObject * parent, + GstQuery * query) +{ + GstAggregator *self = GST_AGGREGATOR (parent); + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (pad); + + g_assert (klass->sink_query_pre_queue); + return klass->sink_query_pre_queue (self, aggpad, query); +} + +static GstFlowReturn +gst_aggregator_pad_event_func (GstPad * pad, GstObject * parent, + GstEvent * event) +{ + GstAggregator *self = GST_AGGREGATOR (parent); + GstAggregatorClass *klass = GST_AGGREGATOR_GET_CLASS (self); + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (pad); + + g_assert (klass->sink_event_pre_queue); + return klass->sink_event_pre_queue (self, aggpad, event); +} + +static gboolean +gst_aggregator_pad_activate_mode_func (GstPad * pad, + GstObject * parent, GstPadMode mode, gboolean active) +{ + GstAggregator *self = GST_AGGREGATOR (parent); + GstAggregatorPad *aggpad = GST_AGGREGATOR_PAD (pad); + + if (active == FALSE) { + SRC_LOCK (self); + gst_aggregator_pad_set_flushing (aggpad, GST_FLOW_FLUSHING, TRUE); + SRC_BROADCAST (self); + SRC_UNLOCK (self); + } else { + PAD_LOCK (aggpad); + aggpad->priv->flow_return = GST_FLOW_OK; + PAD_BROADCAST_EVENT (aggpad); + PAD_UNLOCK (aggpad); + } + + return TRUE; +} + +/*********************************** + * GstAggregatorPad implementation * + ************************************/ +G_DEFINE_TYPE_WITH_PRIVATE (GstAggregatorPad, gst_aggregator_pad, GST_TYPE_PAD); + +#define DEFAULT_PAD_EMIT_SIGNALS FALSE + +enum +{ + PAD_PROP_0, + PAD_PROP_EMIT_SIGNALS, +}; + +enum +{ + PAD_SIGNAL_BUFFER_CONSUMED, + PAD_LAST_SIGNAL, +}; + +static guint gst_aggregator_pad_signals[PAD_LAST_SIGNAL] = { 0 }; + +static void +gst_aggregator_pad_constructed (GObject * object) +{ + GstPad *pad = GST_PAD (object); + + if (GST_PAD_IS_SINK (pad)) { + gst_pad_set_chain_function (pad, + GST_DEBUG_FUNCPTR (gst_aggregator_pad_chain)); + gst_pad_set_event_full_function_full (pad, + GST_DEBUG_FUNCPTR (gst_aggregator_pad_event_func), NULL, NULL); + gst_pad_set_query_function (pad, + GST_DEBUG_FUNCPTR (gst_aggregator_pad_query_func)); + gst_pad_set_activatemode_function (pad, + GST_DEBUG_FUNCPTR (gst_aggregator_pad_activate_mode_func)); + } +} + +static void +gst_aggregator_pad_finalize (GObject * object) +{ + GstAggregatorPad *pad = (GstAggregatorPad *) object; + + g_cond_clear (&pad->priv->event_cond); + g_mutex_clear (&pad->priv->flush_lock); + g_mutex_clear (&pad->priv->lock); + + G_OBJECT_CLASS (gst_aggregator_pad_parent_class)->finalize (object); +} + +static void +gst_aggregator_pad_dispose (GObject * object) +{ + GstAggregatorPad *pad = (GstAggregatorPad *) object; + + gst_aggregator_pad_set_flushing (pad, GST_FLOW_FLUSHING, TRUE); + + G_OBJECT_CLASS (gst_aggregator_pad_parent_class)->dispose (object); +} + +static void +gst_aggregator_pad_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstAggregatorPad *pad = GST_AGGREGATOR_PAD (object); + + switch (prop_id) { + case PAD_PROP_EMIT_SIGNALS: + pad->priv->emit_signals = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_aggregator_pad_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstAggregatorPad *pad = GST_AGGREGATOR_PAD (object); + + switch (prop_id) { + case PAD_PROP_EMIT_SIGNALS: + g_value_set_boolean (value, pad->priv->emit_signals); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_aggregator_pad_class_init (GstAggregatorPadClass * klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->constructed = gst_aggregator_pad_constructed; + gobject_class->finalize = gst_aggregator_pad_finalize; + gobject_class->dispose = gst_aggregator_pad_dispose; + gobject_class->set_property = gst_aggregator_pad_set_property; + gobject_class->get_property = gst_aggregator_pad_get_property; + + /** + * GstAggregatorPad:buffer-consumed: + * @buffer: The buffer that was consumed + * + * Signals that a buffer was consumed. As aggregator pads store buffers + * in an internal queue, there is no direct match between input and output + * buffers at any given time. This signal can be useful to forward metas + * such as #GstVideoTimeCodeMeta or #GstVideoCaptionMeta at the right time. + * + * Since: 1.16 + */ + gst_aggregator_pad_signals[PAD_SIGNAL_BUFFER_CONSUMED] = + g_signal_new ("buffer-consumed", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 1, GST_TYPE_BUFFER); + + /** + * GstAggregatorPad:emit-signals: + * + * Enables the emission of signals such as #GstAggregatorPad::buffer-consumed + * + * Since: 1.16 + */ + g_object_class_install_property (gobject_class, PAD_PROP_EMIT_SIGNALS, + g_param_spec_boolean ("emit-signals", "Emit signals", + "Send signals to signal data consumption", DEFAULT_PAD_EMIT_SIGNALS, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + +static void +gst_aggregator_pad_init (GstAggregatorPad * pad) +{ + pad->priv = gst_aggregator_pad_get_instance_private (pad); + + g_queue_init (&pad->priv->data); + g_cond_init (&pad->priv->event_cond); + + g_mutex_init (&pad->priv->flush_lock); + g_mutex_init (&pad->priv->lock); + + gst_aggregator_pad_reset_unlocked (pad); + pad->priv->negotiated = FALSE; + pad->priv->emit_signals = DEFAULT_PAD_EMIT_SIGNALS; +} + +/* Must be called with the PAD_LOCK held */ +static void +gst_aggregator_pad_buffer_consumed (GstAggregatorPad * pad, GstBuffer * buffer) +{ + pad->priv->num_buffers--; + GST_TRACE_OBJECT (pad, "Consuming buffer %" GST_PTR_FORMAT, buffer); + if (buffer && pad->priv->emit_signals) { + g_signal_emit (pad, gst_aggregator_pad_signals[PAD_SIGNAL_BUFFER_CONSUMED], + 0, buffer); + } + PAD_BROADCAST_EVENT (pad); +} + +/* Must be called with the PAD_LOCK held */ +static void +gst_aggregator_pad_clip_buffer_unlocked (GstAggregatorPad * pad) +{ + GstAggregator *self = NULL; + GstAggregatorClass *aggclass = NULL; + GstBuffer *buffer = NULL; + + while (pad->priv->clipped_buffer == NULL && + GST_IS_BUFFER (g_queue_peek_tail (&pad->priv->data))) { + buffer = g_queue_pop_tail (&pad->priv->data); + + apply_buffer (pad, buffer, FALSE); + + /* We only take the parent here so that it's not taken if the buffer is + * already clipped or if the queue is empty. + */ + if (self == NULL) { + self = GST_AGGREGATOR (gst_pad_get_parent_element (GST_PAD (pad))); + if (self == NULL) { + gst_buffer_unref (buffer); + return; + } + + aggclass = GST_AGGREGATOR_GET_CLASS (self); + } + + if (aggclass->clip) { + GST_TRACE_OBJECT (pad, "Clipping: %" GST_PTR_FORMAT, buffer); + + buffer = aggclass->clip (self, pad, buffer); + + if (buffer == NULL) { + gst_aggregator_pad_buffer_consumed (pad, buffer); + GST_TRACE_OBJECT (pad, "Clipping consumed the buffer"); + } + } + + pad->priv->clipped_buffer = buffer; + } + + if (self) + gst_object_unref (self); +} + +/** + * gst_aggregator_pad_pop_buffer: + * @pad: the pad to get buffer from + * + * Steal the ref to the buffer currently queued in @pad. + * + * Returns: (transfer full): The buffer in @pad or NULL if no buffer was + * queued. You should unref the buffer after usage. + */ +GstBuffer * +gst_aggregator_pad_pop_buffer (GstAggregatorPad * pad) +{ + GstBuffer *buffer; + + PAD_LOCK (pad); + + if (pad->priv->flow_return != GST_FLOW_OK) { + PAD_UNLOCK (pad); + return NULL; + } + + gst_aggregator_pad_clip_buffer_unlocked (pad); + + buffer = pad->priv->clipped_buffer; + + if (buffer) { + pad->priv->clipped_buffer = NULL; + gst_aggregator_pad_buffer_consumed (pad, buffer); + GST_DEBUG_OBJECT (pad, "Consumed: %" GST_PTR_FORMAT, buffer); + } + + PAD_UNLOCK (pad); + + return buffer; +} + +/** + * gst_aggregator_pad_drop_buffer: + * @pad: the pad where to drop any pending buffer + * + * Drop the buffer currently queued in @pad. + * + * Returns: TRUE if there was a buffer queued in @pad, or FALSE if not. + */ +gboolean +gst_aggregator_pad_drop_buffer (GstAggregatorPad * pad) +{ + GstBuffer *buf; + + buf = gst_aggregator_pad_pop_buffer (pad); + + if (buf == NULL) + return FALSE; + + gst_buffer_unref (buf); + return TRUE; +} + +/** + * gst_aggregator_pad_peek_buffer: + * @pad: the pad to get buffer from + * + * Returns: (transfer full): A reference to the buffer in @pad or + * NULL if no buffer was queued. You should unref the buffer after + * usage. + */ +GstBuffer * +gst_aggregator_pad_peek_buffer (GstAggregatorPad * pad) +{ + GstBuffer *buffer; + + PAD_LOCK (pad); + + if (pad->priv->flow_return != GST_FLOW_OK) { + PAD_UNLOCK (pad); + return NULL; + } + + gst_aggregator_pad_clip_buffer_unlocked (pad); + + if (pad->priv->clipped_buffer) { + buffer = gst_buffer_ref (pad->priv->clipped_buffer); + } else { + buffer = NULL; + } + PAD_UNLOCK (pad); + + return buffer; +} + +/** + * gst_aggregator_pad_has_buffer: + * @pad: the pad to check the buffer on + * + * This checks if a pad has a buffer available that will be returned by + * a call to gst_aggregator_pad_peek_buffer() or + * gst_aggregator_pad_pop_buffer(). + * + * Returns: %TRUE if the pad has a buffer available as the next thing. + * + * Since: 1.14.1 + */ +gboolean +gst_aggregator_pad_has_buffer (GstAggregatorPad * pad) +{ + gboolean has_buffer; + + PAD_LOCK (pad); + gst_aggregator_pad_clip_buffer_unlocked (pad); + has_buffer = (pad->priv->clipped_buffer != NULL); + PAD_UNLOCK (pad); + + return has_buffer; +} + +/** + * gst_aggregator_pad_is_eos: + * @pad: an aggregator pad + * + * Returns: %TRUE if the pad is EOS, otherwise %FALSE. + */ +gboolean +gst_aggregator_pad_is_eos (GstAggregatorPad * pad) +{ + gboolean is_eos; + + PAD_LOCK (pad); + is_eos = pad->priv->eos; + PAD_UNLOCK (pad); + + return is_eos; +} + +#if 0 +/* + * gst_aggregator_merge_tags: + * @self: a #GstAggregator + * @tags: a #GstTagList to merge + * @mode: the #GstTagMergeMode to use + * + * Adds tags to so-called pending tags, which will be processed + * before pushing out data downstream. + * + * Note that this is provided for convenience, and the subclass is + * not required to use this and can still do tag handling on its own. + * + * MT safe. + */ +void +gst_aggregator_merge_tags (GstAggregator * self, + const GstTagList * tags, GstTagMergeMode mode) +{ + GstTagList *otags; + + g_return_if_fail (GST_IS_AGGREGATOR (self)); + g_return_if_fail (tags == NULL || GST_IS_TAG_LIST (tags)); + + /* FIXME Check if we can use OBJECT lock here! */ + GST_OBJECT_LOCK (self); + if (tags) + GST_DEBUG_OBJECT (self, "merging tags %" GST_PTR_FORMAT, tags); + otags = self->priv->tags; + self->priv->tags = gst_tag_list_merge (self->priv->tags, tags, mode); + if (otags) + gst_tag_list_unref (otags); + self->priv->tags_changed = TRUE; + GST_OBJECT_UNLOCK (self); +} +#endif + +/** + * gst_aggregator_set_latency: + * @self: a #GstAggregator + * @min_latency: minimum latency + * @max_latency: maximum latency + * + * Lets #GstAggregator sub-classes tell the baseclass what their internal + * latency is. Will also post a LATENCY message on the bus so the pipeline + * can reconfigure its global latency. + */ +void +gst_aggregator_set_latency (GstAggregator * self, + GstClockTime min_latency, GstClockTime max_latency) +{ + gboolean changed = FALSE; + + g_return_if_fail (GST_IS_AGGREGATOR (self)); + g_return_if_fail (GST_CLOCK_TIME_IS_VALID (min_latency)); + g_return_if_fail (max_latency >= min_latency); + + SRC_LOCK (self); + if (self->priv->sub_latency_min != min_latency) { + self->priv->sub_latency_min = min_latency; + changed = TRUE; + } + if (self->priv->sub_latency_max != max_latency) { + self->priv->sub_latency_max = max_latency; + changed = TRUE; + } + + if (changed) + SRC_BROADCAST (self); + SRC_UNLOCK (self); + + if (changed) { + gst_element_post_message (GST_ELEMENT_CAST (self), + gst_message_new_latency (GST_OBJECT_CAST (self))); + } +} + +/** + * gst_aggregator_get_buffer_pool: + * @self: a #GstAggregator + * + * Returns: (transfer full): the instance of the #GstBufferPool used + * by @trans; free it after use it + */ +GstBufferPool * +gst_aggregator_get_buffer_pool (GstAggregator * self) +{ + GstBufferPool *pool; + + g_return_val_if_fail (GST_IS_AGGREGATOR (self), NULL); + + GST_OBJECT_LOCK (self); + pool = self->priv->pool; + if (pool) + gst_object_ref (pool); + GST_OBJECT_UNLOCK (self); + + return pool; +} + +/** + * gst_aggregator_get_allocator: + * @self: a #GstAggregator + * @allocator: (out) (allow-none) (transfer full): the #GstAllocator + * used + * @params: (out) (allow-none) (transfer full): the + * #GstAllocationParams of @allocator + * + * Lets #GstAggregator sub-classes get the memory @allocator + * acquired by the base class and its @params. + * + * Unref the @allocator after use it. + */ +void +gst_aggregator_get_allocator (GstAggregator * self, + GstAllocator ** allocator, GstAllocationParams * params) +{ + g_return_if_fail (GST_IS_AGGREGATOR (self)); + + if (allocator) + *allocator = self->priv->allocator ? + gst_object_ref (self->priv->allocator) : NULL; + + if (params) + *params = self->priv->allocation_params; +} + +/** + * gst_aggregator_simple_get_next_time: + * @self: A #GstAggregator + * + * This is a simple #GstAggregatorClass.get_next_time() implementation that + * just looks at the #GstSegment on the srcpad of the aggregator and bases + * the next time on the running time there. + * + * This is the desired behaviour in most cases where you have a live source + * and you have a dead line based aggregator subclass. + * + * Returns: The running time based on the position + * + * Since: 1.16 + */ +GstClockTime +gst_aggregator_simple_get_next_time (GstAggregator * self) +{ + GstClockTime next_time; + GstAggregatorPad *srcpad = GST_AGGREGATOR_PAD (self->srcpad); + GstSegment *segment = &srcpad->segment; + + GST_OBJECT_LOCK (self); + if (segment->position == -1 || segment->position < segment->start) + next_time = segment->start; + else + next_time = segment->position; + + if (segment->stop != -1 && next_time > segment->stop) + next_time = segment->stop; + + next_time = gst_segment_to_running_time (segment, GST_FORMAT_TIME, next_time); + GST_OBJECT_UNLOCK (self); + + return next_time; +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/gstaggregator.h b/utils/gst-plugin-fallbackswitch/src/base/gstaggregator.h new file mode 100644 index 000000000..b8f4216e1 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/gstaggregator.h @@ -0,0 +1,393 @@ +/* GStreamer aggregator base class + * Copyright (C) 2014 Mathieu Duponchelle + * Copyright (C) 2014 Thibault Saunier + * + * 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 __GST_AGGREGATOR_H__ +#define __GST_AGGREGATOR_H__ + +#include + +G_BEGIN_DECLS + +/************************** + * GstAggregator Structs * + *************************/ + +typedef struct _GstAggregator GstAggregator; +typedef struct _GstAggregatorPrivate GstAggregatorPrivate; +typedef struct _GstAggregatorClass GstAggregatorClass; + +/************************ + * GstAggregatorPad API * + ***********************/ + +#define GST_TYPE_AGGREGATOR_PAD (gst_aggregator_pad_get_type()) +#define GST_AGGREGATOR_PAD(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AGGREGATOR_PAD, GstAggregatorPad)) +#define GST_AGGREGATOR_PAD_CAST(obj) ((GstAggregatorPad *)(obj)) +#define GST_AGGREGATOR_PAD_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AGGREGATOR_PAD, GstAggregatorPadClass)) +#define GST_AGGREGATOR_PAD_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj),GST_TYPE_AGGREGATOR_PAD, GstAggregatorPadClass)) +#define GST_IS_AGGREGATOR_PAD(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AGGREGATOR_PAD)) +#define GST_IS_AGGREGATOR_PAD_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AGGREGATOR_PAD)) + +/**************************** + * GstAggregatorPad Structs * + ***************************/ + +typedef struct _GstAggregatorPad GstAggregatorPad; +typedef struct _GstAggregatorPadClass GstAggregatorPadClass; +typedef struct _GstAggregatorPadPrivate GstAggregatorPadPrivate; + +/** + * GstAggregatorPad: + * @segment: last segment received. + * + * The implementation the GstPad to use with #GstAggregator + * + * Since: 1.14 + */ +struct _GstAggregatorPad +{ + GstPad parent; + + /*< public >*/ + /* Protected by the OBJECT_LOCK */ + GstSegment segment; + + /* < private > */ + GstAggregatorPadPrivate * priv; + + gpointer _gst_reserved[GST_PADDING]; +}; + +/** + * GstAggregatorPadClass: + * @flush: Optional + * Called when the pad has received a flush stop, this is the place + * to flush any information specific to the pad, it allows for individual + * pads to be flushed while others might not be. + * @skip_buffer: Optional + * Called before input buffers are queued in the pad, return %TRUE + * if the buffer should be skipped. + * + * Since: 1.14 + */ +struct _GstAggregatorPadClass +{ + GstPadClass parent_class; + + GstFlowReturn (*flush) (GstAggregatorPad * aggpad, GstAggregator * aggregator); + gboolean (*skip_buffer) (GstAggregatorPad * aggpad, GstAggregator * aggregator, GstBuffer * buffer); + + /*< private >*/ + gpointer _gst_reserved[GST_PADDING_LARGE]; +}; + +GST_BASE_API +GType gst_aggregator_pad_get_type (void); + +/**************************** + * GstAggregatorPad methods * + ***************************/ + +GST_BASE_API +GstBuffer * gst_aggregator_pad_pop_buffer (GstAggregatorPad * pad); + +GST_BASE_API +GstBuffer * gst_aggregator_pad_peek_buffer (GstAggregatorPad * pad); + +GST_BASE_API +gboolean gst_aggregator_pad_drop_buffer (GstAggregatorPad * pad); + +GST_BASE_API +gboolean gst_aggregator_pad_has_buffer (GstAggregatorPad * pad); + +GST_BASE_API +gboolean gst_aggregator_pad_is_eos (GstAggregatorPad * pad); + +/********************* + * GstAggregator API * + ********************/ + +#define GST_TYPE_AGGREGATOR (gst_aggregator_get_type()) +#define GST_AGGREGATOR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_AGGREGATOR,GstAggregator)) +#define GST_AGGREGATOR_CAST(obj) ((GstAggregator *)(obj)) +#define GST_AGGREGATOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_AGGREGATOR,GstAggregatorClass)) +#define GST_AGGREGATOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj),GST_TYPE_AGGREGATOR,GstAggregatorClass)) +#define GST_IS_AGGREGATOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_AGGREGATOR)) +#define GST_IS_AGGREGATOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_AGGREGATOR)) + +#define GST_AGGREGATOR_FLOW_NEED_DATA GST_FLOW_CUSTOM_ERROR + +/** + * GstAggregator: + * @srcpad: the aggregator's source pad + * + * Aggregator base class object structure. + * + * Since: 1.14 + */ +struct _GstAggregator +{ + GstElement parent; + + /*< public >*/ + GstPad * srcpad; + + /*< private >*/ + GstAggregatorPrivate * priv; + + gpointer _gst_reserved[GST_PADDING_LARGE]; +}; + +/** + * GstAggregatorClass: + * @flush: Optional. + * Called after a successful flushing seek, once all the flush + * stops have been received. Flush pad-specific data in + * #GstAggregatorPad->flush. + * @clip: Optional. + * Called when a buffer is received on a sink pad, the task of + * clipping it and translating it to the current segment falls + * on the subclass. The function should use the segment of data + * and the negotiated media type on the pad to perform + * clipping of input buffer. This function takes ownership of + * buf and should output a buffer or return NULL in + * if the buffer should be dropped. + * @finish_buffer: Optional. + * Called when a subclass calls gst_aggregator_finish_buffer() + * from their aggregate function to push out a buffer. + * Subclasses can override this to modify or decorate buffers + * before they get pushed out. This function takes ownership + * of the buffer passed. Subclasses that override this method + * should always chain up to the parent class virtual method. + * @sink_event: Optional. + * Called when an event is received on a sink pad, the subclass + * should always chain up. + * @sink_query: Optional. + * Called when a query is received on a sink pad, the subclass + * should always chain up. + * @src_event: Optional. + * Called when an event is received on the src pad, the subclass + * should always chain up. + * @src_query: Optional. + * Called when a query is received on the src pad, the subclass + * should always chain up. + * @src_activate: Optional. + * Called when the src pad is activated, it will start/stop its + * pad task right after that call. + * @aggregate: Mandatory. + * Called when buffers are queued on all sinkpads. Classes + * should iterate the GstElement->sinkpads and peek or steal + * buffers from the #GstAggregatorPads. If the subclass returns + * GST_FLOW_EOS, sending of the eos event will be taken care + * of. Once / if a buffer has been constructed from the + * aggregated buffers, the subclass should call _finish_buffer. + * @stop: Optional. + * Called when the element goes from PAUSED to READY. + * The subclass should free all resources and reset its state. + * @start: Optional. + * Called when the element goes from READY to PAUSED. + * The subclass should get ready to process + * aggregated buffers. + * @get_next_time: Optional. + * Called when the element needs to know the running time of the next + * rendered buffer for live pipelines. This causes deadline + * based aggregation to occur. Defaults to returning + * GST_CLOCK_TIME_NONE causing the element to wait for buffers + * on all sink pads before aggregating. + * @create_new_pad: Optional. + * Called when a new pad needs to be created. Allows subclass that + * don't have a single sink pad template to provide a pad based + * on the provided information. + * @update_src_caps: Lets subclasses update the #GstCaps representing + * the src pad caps before usage. The result should end up + * in @ret. Return %GST_AGGREGATOR_FLOW_NEED_DATA to indicate that the + * element needs more information (caps, a buffer, etc) to + * choose the correct caps. Should return ANY caps if the + * stream has not caps at all. + * @fixate_src_caps: Optional. + * Fixate and return the src pad caps provided. The function takes + * ownership of @caps and returns a fixated version of + * @caps. @caps is not guaranteed to be writable. + * @negotiated_src_caps: Optional. + * Notifies subclasses what caps format has been negotiated + * @decide_allocation: Optional. + * Allows the subclass to influence the allocation choices. + * Setup the allocation parameters for allocating output + * buffers. The passed in query contains the result of the + * downstream allocation query. + * @propose_allocation: Optional. + * Allows the subclass to handle the allocation query from upstream. + * @negotiate: Optional. + * Negotiate the caps with the peer (Since: 1.18). + * @sink_event_pre_queue: Optional. + * Called when an event is received on a sink pad before queueing up + * serialized events. The subclass should always chain up (Since: 1.18). + * @sink_query_pre_queue: Optional. + * Called when a query is received on a sink pad before queueing up + * serialized queries. The subclass should always chain up (Since: 1.18). + * + * The aggregator base class will handle in a thread-safe way all manners of + * concurrent flushes, seeks, pad additions and removals, leaving to the + * subclass the responsibility of clipping buffers, and aggregating buffers in + * the way the implementor sees fit. + * + * It will also take care of event ordering (stream-start, segment, eos). + * + * Basically, a simple implementation will override @aggregate, and call + * _finish_buffer from inside that function. + * + * Since: 1.14 + */ +struct _GstAggregatorClass { + GstElementClass parent_class; + + GstFlowReturn (*flush) (GstAggregator * aggregator); + + GstBuffer * (*clip) (GstAggregator * aggregator, + GstAggregatorPad * aggregator_pad, + GstBuffer * buf); + + GstFlowReturn (*finish_buffer) (GstAggregator * aggregator, + GstBuffer * buffer); + + /* sinkpads virtual methods */ + gboolean (*sink_event) (GstAggregator * aggregator, + GstAggregatorPad * aggregator_pad, + GstEvent * event); + + gboolean (*sink_query) (GstAggregator * aggregator, + GstAggregatorPad * aggregator_pad, + GstQuery * query); + + /* srcpad virtual methods */ + gboolean (*src_event) (GstAggregator * aggregator, + GstEvent * event); + + gboolean (*src_query) (GstAggregator * aggregator, + GstQuery * query); + + gboolean (*src_activate) (GstAggregator * aggregator, + GstPadMode mode, + gboolean active); + + GstFlowReturn (*aggregate) (GstAggregator * aggregator, + gboolean timeout); + + gboolean (*stop) (GstAggregator * aggregator); + + gboolean (*start) (GstAggregator * aggregator); + + GstClockTime (*get_next_time) (GstAggregator * aggregator); + + GstAggregatorPad * (*create_new_pad) (GstAggregator * self, + GstPadTemplate * templ, + const gchar * req_name, + const GstCaps * caps); + + /** + * GstAggregatorClass::update_src_caps: + * @ret: (out) (allow-none): + */ + GstFlowReturn (*update_src_caps) (GstAggregator * self, + GstCaps * caps, + GstCaps ** ret); + GstCaps * (*fixate_src_caps) (GstAggregator * self, + GstCaps * caps); + gboolean (*negotiated_src_caps) (GstAggregator * self, + GstCaps * caps); + gboolean (*decide_allocation) (GstAggregator * self, + GstQuery * query); + gboolean (*propose_allocation) (GstAggregator * self, + GstAggregatorPad * pad, + GstQuery * decide_query, + GstQuery * query); + + gboolean (*negotiate) (GstAggregator * self); + + gboolean (*sink_event_pre_queue) (GstAggregator * aggregator, + GstAggregatorPad * aggregator_pad, + GstEvent * event); + + gboolean (*sink_query_pre_queue) (GstAggregator * aggregator, + GstAggregatorPad * aggregator_pad, + GstQuery * query); + + /*< private >*/ + gpointer _gst_reserved[GST_PADDING_LARGE-3]; +}; + +/************************************ + * GstAggregator convenience macros * + ***********************************/ + +/** + * GST_AGGREGATOR_SRC_PAD: + * @agg: a #GstAggregator + * + * Convenience macro to access the source pad of #GstAggregator + * + * Since: 1.6 + */ +#define GST_AGGREGATOR_SRC_PAD(agg) (((GstAggregator *)(agg))->srcpad) + +/************************* + * GstAggregator methods * + ************************/ + +GST_BASE_API +GstFlowReturn gst_aggregator_finish_buffer (GstAggregator * aggregator, + GstBuffer * buffer); + +GST_BASE_API +void gst_aggregator_set_src_caps (GstAggregator * self, + GstCaps * caps); + +GST_BASE_API +gboolean gst_aggregator_negotiate (GstAggregator * self); + +GST_BASE_API +void gst_aggregator_set_latency (GstAggregator * self, + GstClockTime min_latency, + GstClockTime max_latency); + +GST_BASE_API +GType gst_aggregator_get_type(void); + +GST_BASE_API +GstClockTime gst_aggregator_get_latency (GstAggregator * self); + +GST_BASE_API +GstBufferPool * gst_aggregator_get_buffer_pool (GstAggregator * self); + +GST_BASE_API +void gst_aggregator_get_allocator (GstAggregator * self, + GstAllocator ** allocator, + GstAllocationParams * params); + +GST_BASE_API +GstClockTime gst_aggregator_simple_get_next_time (GstAggregator * self); + + +G_END_DECLS + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstAggregator, gst_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstAggregatorPad, gst_object_unref) + +#endif /* __GST_AGGREGATOR_H__ */ diff --git a/utils/gst-plugin-fallbackswitch/src/base/mod.rs b/utils/gst-plugin-fallbackswitch/src/base/mod.rs new file mode 100644 index 000000000..2d0973560 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/mod.rs @@ -0,0 +1,27 @@ +#[allow(clippy::unreadable_literal)] +#[allow(clippy::too_many_arguments)] +#[allow(clippy::match_same_arms)] +#[allow(clippy::type_complexity)] +mod auto; +pub use auto::*; + +mod utils; + +mod aggregator; +mod aggregator_pad; + +pub mod prelude { + pub use glib::prelude::*; + pub use gst::prelude::*; + + pub use super::aggregator::AggregatorExtManual; + pub use super::aggregator_pad::AggregatorPadExtManual; + pub use super::auto::traits::*; +} + +pub mod subclass; + +mod sys; +use sys as gst_base_sys; + +pub const AGGREGATOR_FLOW_NEED_DATA: gst::FlowError = gst::FlowError::CustomError; diff --git a/utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator.rs b/utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator.rs new file mode 100644 index 000000000..5f8e27444 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator.rs @@ -0,0 +1,1042 @@ +// Copyright (C) 2017-2019 Sebastian Dröge +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use libc; + +use super::super::gst_base_sys; +use glib_sys; +use gst_sys; + +use glib::translate::*; + +use glib::subclass::prelude::*; +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; + +use std::ptr; + +use super::super::Aggregator; +use super::super::AggregatorClass; +use super::super::AggregatorPad; + +pub trait AggregatorImpl: AggregatorImplExt + ElementImpl + Send + Sync + 'static { + fn flush(&self, aggregator: &Aggregator) -> Result { + self.parent_flush(aggregator) + } + + fn clip( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + buffer: gst::Buffer, + ) -> Option { + self.parent_clip(aggregator, aggregator_pad, buffer) + } + + fn finish_buffer( + &self, + aggregator: &Aggregator, + buffer: gst::Buffer, + ) -> Result { + self.parent_finish_buffer(aggregator, buffer) + } + + fn sink_event( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + event: gst::Event, + ) -> bool { + self.parent_sink_event(aggregator, aggregator_pad, event) + } + + fn sink_event_pre_queue( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + event: gst::Event, + ) -> bool { + self.parent_sink_event_pre_queue(aggregator, aggregator_pad, event) + } + + fn sink_query( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool { + self.parent_sink_query(aggregator, aggregator_pad, query) + } + + fn sink_query_pre_queue( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool { + self.parent_sink_query_pre_queue(aggregator, aggregator_pad, query) + } + + fn src_event(&self, aggregator: &Aggregator, event: gst::Event) -> bool { + self.parent_src_event(aggregator, event) + } + + fn src_query(&self, aggregator: &Aggregator, query: &mut gst::QueryRef) -> bool { + self.parent_src_query(aggregator, query) + } + + fn src_activate( + &self, + aggregator: &Aggregator, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError> { + self.parent_src_activate(aggregator, mode, active) + } + + fn aggregate( + &self, + aggregator: &Aggregator, + timeout: bool, + ) -> Result; + + fn start(&self, aggregator: &Aggregator) -> Result<(), gst::ErrorMessage> { + self.parent_start(aggregator) + } + + fn stop(&self, aggregator: &Aggregator) -> Result<(), gst::ErrorMessage> { + self.parent_stop(aggregator) + } + + fn get_next_time(&self, aggregator: &Aggregator) -> gst::ClockTime { + self.parent_get_next_time(aggregator) + } + + fn create_new_pad( + &self, + aggregator: &Aggregator, + templ: &gst::PadTemplate, + req_name: Option<&str>, + caps: Option<&gst::Caps>, + ) -> Option { + self.parent_create_new_pad(aggregator, templ, req_name, caps) + } + + fn update_src_caps( + &self, + aggregator: &Aggregator, + caps: &gst::Caps, + ) -> Result { + self.parent_update_src_caps(aggregator, caps) + } + + fn fixate_src_caps(&self, aggregator: &Aggregator, caps: gst::Caps) -> gst::Caps { + self.parent_fixate_src_caps(aggregator, caps) + } + + fn negotiated_src_caps( + &self, + aggregator: &Aggregator, + caps: &gst::Caps, + ) -> Result<(), gst::LoggableError> { + self.parent_negotiated_src_caps(aggregator, caps) + } + + fn negotiate(&self, aggregator: &Aggregator) -> bool { + self.parent_negotiate(aggregator) + } +} + +pub trait AggregatorImplExt { + fn parent_flush(&self, aggregator: &Aggregator) -> Result; + + fn parent_clip( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + buffer: gst::Buffer, + ) -> Option; + + fn parent_finish_buffer( + &self, + aggregator: &Aggregator, + buffer: gst::Buffer, + ) -> Result; + + fn parent_sink_event( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + event: gst::Event, + ) -> bool; + + fn parent_sink_event_pre_queue( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + event: gst::Event, + ) -> bool; + + fn parent_sink_query( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool; + + fn parent_sink_query_pre_queue( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool; + + fn parent_src_event(&self, aggregator: &Aggregator, event: gst::Event) -> bool; + + fn parent_src_query(&self, aggregator: &Aggregator, query: &mut gst::QueryRef) -> bool; + + fn parent_src_activate( + &self, + aggregator: &Aggregator, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError>; + + fn parent_aggregate( + &self, + aggregator: &Aggregator, + timeout: bool, + ) -> Result; + + fn parent_start(&self, aggregator: &Aggregator) -> Result<(), gst::ErrorMessage>; + + fn parent_stop(&self, aggregator: &Aggregator) -> Result<(), gst::ErrorMessage>; + + fn parent_get_next_time(&self, aggregator: &Aggregator) -> gst::ClockTime; + + fn parent_create_new_pad( + &self, + aggregator: &Aggregator, + templ: &gst::PadTemplate, + req_name: Option<&str>, + caps: Option<&gst::Caps>, + ) -> Option; + + fn parent_update_src_caps( + &self, + aggregator: &Aggregator, + caps: &gst::Caps, + ) -> Result; + + fn parent_fixate_src_caps(&self, aggregator: &Aggregator, caps: gst::Caps) -> gst::Caps; + + fn parent_negotiated_src_caps( + &self, + aggregator: &Aggregator, + caps: &gst::Caps, + ) -> Result<(), gst::LoggableError>; + + fn parent_negotiate(&self, aggregator: &Aggregator) -> bool; +} + +impl AggregatorImplExt for T { + fn parent_flush(&self, aggregator: &Aggregator) -> Result { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + (*parent_class) + .flush + .map(|f| from_glib(f(aggregator.to_glib_none().0))) + .unwrap_or(gst::FlowReturn::Ok) + .into_result() + } + } + + fn parent_clip( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + buffer: gst::Buffer, + ) -> Option { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + match (*parent_class).clip { + None => Some(buffer), + Some(ref func) => from_glib_full(func( + aggregator.to_glib_none().0, + aggregator_pad.to_glib_none().0, + buffer.into_ptr(), + )), + } + } + } + + fn parent_finish_buffer( + &self, + aggregator: &Aggregator, + buffer: gst::Buffer, + ) -> Result { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .finish_buffer + .expect("Missing parent function `finish_buffer`"); + gst::FlowReturn::from_glib(f(aggregator.to_glib_none().0, buffer.into_ptr())) + .into_result() + } + } + + fn parent_sink_event( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + event: gst::Event, + ) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .sink_event + .expect("Missing parent function `sink_event`"); + from_glib(f( + aggregator.to_glib_none().0, + aggregator_pad.to_glib_none().0, + event.into_ptr(), + )) + } + } + + fn parent_sink_event_pre_queue( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + event: gst::Event, + ) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .sink_event_pre_queue + .expect("Missing parent function `sink_event_pre_queue`"); + from_glib(f( + aggregator.to_glib_none().0, + aggregator_pad.to_glib_none().0, + event.into_ptr(), + )) + } + } + + fn parent_sink_query( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .sink_query + .expect("Missing parent function `sink_query`"); + from_glib(f( + aggregator.to_glib_none().0, + aggregator_pad.to_glib_none().0, + query.as_mut_ptr(), + )) + } + } + + fn parent_sink_query_pre_queue( + &self, + aggregator: &Aggregator, + aggregator_pad: &AggregatorPad, + query: &mut gst::QueryRef, + ) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .sink_query_pre_queue + .expect("Missing parent function `sink_query`"); + from_glib(f( + aggregator.to_glib_none().0, + aggregator_pad.to_glib_none().0, + query.as_mut_ptr(), + )) + } + } + + fn parent_src_event(&self, aggregator: &Aggregator, event: gst::Event) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .src_event + .expect("Missing parent function `src_event`"); + from_glib(f(aggregator.to_glib_none().0, event.into_ptr())) + } + } + + fn parent_src_query(&self, aggregator: &Aggregator, query: &mut gst::QueryRef) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .src_query + .expect("Missing parent function `src_query`"); + from_glib(f(aggregator.to_glib_none().0, query.as_mut_ptr())) + } + } + + fn parent_src_activate( + &self, + aggregator: &Aggregator, + mode: gst::PadMode, + active: bool, + ) -> Result<(), gst::LoggableError> { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + match (*parent_class).src_activate { + None => Ok(()), + Some(f) => gst_result_from_gboolean!( + f( + aggregator.to_glib_none().0, + mode.to_glib(), + active.to_glib() + ), + gst::CAT_RUST, + "Parent function `src_activate` failed" + ), + } + } + } + + fn parent_aggregate( + &self, + aggregator: &Aggregator, + timeout: bool, + ) -> Result { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .aggregate + .expect("Missing parent function `aggregate`"); + gst::FlowReturn::from_glib(f(aggregator.to_glib_none().0, timeout.to_glib())) + .into_result() + } + } + + fn parent_start(&self, aggregator: &Aggregator) -> Result<(), gst::ErrorMessage> { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + (*parent_class) + .start + .map(|f| { + if from_glib(f(aggregator.to_glib_none().0)) { + Ok(()) + } else { + Err(gst_error_msg!( + gst::CoreError::Failed, + ["Parent function `start` failed"] + )) + } + }) + .unwrap_or(Ok(())) + } + } + + fn parent_stop(&self, aggregator: &Aggregator) -> Result<(), gst::ErrorMessage> { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + (*parent_class) + .stop + .map(|f| { + if from_glib(f(aggregator.to_glib_none().0)) { + Ok(()) + } else { + Err(gst_error_msg!( + gst::CoreError::Failed, + ["Parent function `stop` failed"] + )) + } + }) + .unwrap_or(Ok(())) + } + } + + fn parent_get_next_time(&self, aggregator: &Aggregator) -> gst::ClockTime { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + (*parent_class) + .get_next_time + .map(|f| from_glib(f(aggregator.to_glib_none().0))) + .unwrap_or(gst::CLOCK_TIME_NONE) + } + } + + fn parent_create_new_pad( + &self, + aggregator: &Aggregator, + templ: &gst::PadTemplate, + req_name: Option<&str>, + caps: Option<&gst::Caps>, + ) -> Option { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .create_new_pad + .expect("Missing parent function `create_new_pad`"); + from_glib_full(f( + aggregator.to_glib_none().0, + templ.to_glib_none().0, + req_name.to_glib_none().0, + caps.to_glib_none().0, + )) + } + } + + fn parent_update_src_caps( + &self, + aggregator: &Aggregator, + caps: &gst::Caps, + ) -> Result { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + let f = (*parent_class) + .update_src_caps + .expect("Missing parent function `update_src_caps`"); + + let mut out_caps = ptr::null_mut(); + gst::FlowReturn::from_glib(f( + aggregator.to_glib_none().0, + caps.as_mut_ptr(), + &mut out_caps, + )) + .into_result_value(|| from_glib_full(out_caps)) + } + } + + fn parent_fixate_src_caps(&self, aggregator: &Aggregator, caps: gst::Caps) -> gst::Caps { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + + let f = (*parent_class) + .fixate_src_caps + .expect("Missing parent function `fixate_src_caps`"); + from_glib_full(f(aggregator.to_glib_none().0, caps.into_ptr())) + } + } + + fn parent_negotiated_src_caps( + &self, + aggregator: &Aggregator, + caps: &gst::Caps, + ) -> Result<(), gst::LoggableError> { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + (*parent_class) + .negotiated_src_caps + .map(|f| { + gst_result_from_gboolean!( + f(aggregator.to_glib_none().0, caps.to_glib_none().0), + gst::CAT_RUST, + "Parent function `negotiated_src_caps` failed" + ) + }) + .unwrap_or(Ok(())) + } + } + + fn parent_negotiate(&self, aggregator: &Aggregator) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorClass; + (*parent_class) + .negotiate + .map(|f| from_glib(f(aggregator.to_glib_none().0))) + .unwrap_or(true) + } + } +} + +unsafe impl IsSubclassable for AggregatorClass +where + ::Instance: PanicPoison, +{ + fn override_vfuncs(&mut self) { + >::override_vfuncs(self); + unsafe { + let klass = &mut *(self as *mut Self as *mut gst_base_sys::GstAggregatorClass); + klass.flush = Some(aggregator_flush::); + klass.clip = Some(aggregator_clip::); + klass.finish_buffer = Some(aggregator_finish_buffer::); + klass.sink_event = Some(aggregator_sink_event::); + klass.sink_event_pre_queue = Some(aggregator_sink_event_pre_queue::); + klass.sink_query = Some(aggregator_sink_query::); + klass.sink_query_pre_queue = Some(aggregator_sink_query_pre_queue::); + klass.src_event = Some(aggregator_src_event::); + klass.src_query = Some(aggregator_src_query::); + klass.src_activate = Some(aggregator_src_activate::); + klass.aggregate = Some(aggregator_aggregate::); + klass.start = Some(aggregator_start::); + klass.stop = Some(aggregator_stop::); + klass.get_next_time = Some(aggregator_get_next_time::); + klass.create_new_pad = Some(aggregator_create_new_pad::); + klass.update_src_caps = Some(aggregator_update_src_caps::); + klass.fixate_src_caps = Some(aggregator_fixate_src_caps::); + klass.negotiated_src_caps = Some(aggregator_negotiated_src_caps::); + klass.negotiate = Some(aggregator_negotiate::); + } + } +} + +unsafe extern "C" fn aggregator_flush( + ptr: *mut gst_base_sys::GstAggregator, +) -> gst_sys::GstFlowReturn +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), gst::FlowReturn::Error, { + imp.flush(&wrap).into() + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_clip( + ptr: *mut gst_base_sys::GstAggregator, + aggregator_pad: *mut gst_base_sys::GstAggregatorPad, + buffer: *mut gst_sys::GstBuffer, +) -> *mut gst_sys::GstBuffer +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + let ret = gst_panic_to_error!(&wrap, &instance.panicked(), None, { + imp.clip( + &wrap, + &from_glib_borrow(aggregator_pad), + from_glib_full(buffer), + ) + }); + + ret.map(|r| r.into_ptr()).unwrap_or(ptr::null_mut()) +} + +unsafe extern "C" fn aggregator_finish_buffer( + ptr: *mut gst_base_sys::GstAggregator, + buffer: *mut gst_sys::GstBuffer, +) -> gst_sys::GstFlowReturn +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), gst::FlowReturn::Error, { + imp.finish_buffer(&wrap, from_glib_full(buffer)).into() + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_sink_event( + ptr: *mut gst_base_sys::GstAggregator, + aggregator_pad: *mut gst_base_sys::GstAggregatorPad, + event: *mut gst_sys::GstEvent, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + imp.sink_event( + &wrap, + &from_glib_borrow(aggregator_pad), + from_glib_full(event), + ) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_sink_event_pre_queue( + ptr: *mut gst_base_sys::GstAggregator, + aggregator_pad: *mut gst_base_sys::GstAggregatorPad, + event: *mut gst_sys::GstEvent, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + imp.sink_event_pre_queue( + &wrap, + &from_glib_borrow(aggregator_pad), + from_glib_full(event), + ) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_sink_query( + ptr: *mut gst_base_sys::GstAggregator, + aggregator_pad: *mut gst_base_sys::GstAggregatorPad, + query: *mut gst_sys::GstQuery, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + imp.sink_query( + &wrap, + &from_glib_borrow(aggregator_pad), + gst::QueryRef::from_mut_ptr(query), + ) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_sink_query_pre_queue( + ptr: *mut gst_base_sys::GstAggregator, + aggregator_pad: *mut gst_base_sys::GstAggregatorPad, + query: *mut gst_sys::GstQuery, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + imp.sink_query_pre_queue( + &wrap, + &from_glib_borrow(aggregator_pad), + gst::QueryRef::from_mut_ptr(query), + ) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_src_event( + ptr: *mut gst_base_sys::GstAggregator, + event: *mut gst_sys::GstEvent, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + imp.src_event(&wrap, from_glib_full(event)) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_src_query( + ptr: *mut gst_base_sys::GstAggregator, + query: *mut gst_sys::GstQuery, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + imp.src_query(&wrap, gst::QueryRef::from_mut_ptr(query)) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_src_activate( + ptr: *mut gst_base_sys::GstAggregator, + mode: gst_sys::GstPadMode, + active: glib_sys::gboolean, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + match imp.src_activate(&wrap, from_glib(mode), from_glib(active)) { + Ok(()) => true, + Err(err) => { + err.log_with_object(&*wrap); + false + } + } + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_aggregate( + ptr: *mut gst_base_sys::GstAggregator, + timeout: glib_sys::gboolean, +) -> gst_sys::GstFlowReturn +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), gst::FlowReturn::Error, { + imp.aggregate(&wrap, from_glib(timeout)).into() + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_start( + ptr: *mut gst_base_sys::GstAggregator, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + match imp.start(&wrap) { + Ok(()) => true, + Err(err) => { + wrap.post_error_message(&err); + false + } + } + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_stop( + ptr: *mut gst_base_sys::GstAggregator, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + match imp.stop(&wrap) { + Ok(()) => true, + Err(err) => { + wrap.post_error_message(&err); + false + } + } + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_get_next_time( + ptr: *mut gst_base_sys::GstAggregator, +) -> gst_sys::GstClockTime +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), gst::CLOCK_TIME_NONE, { + imp.get_next_time(&wrap) + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_create_new_pad( + ptr: *mut gst_base_sys::GstAggregator, + templ: *mut gst_sys::GstPadTemplate, + req_name: *const libc::c_char, + caps: *const gst_sys::GstCaps, +) -> *mut gst_base_sys::GstAggregatorPad +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), None, { + let req_name: Option = from_glib_none(req_name); + + // FIXME: Easier way to convert Option to Option<&str>? + let mut _tmp = String::new(); + let req_name = match req_name { + Some(n) => { + _tmp = n; + Some(_tmp.as_str()) + } + None => None, + }; + + imp.create_new_pad( + &wrap, + &from_glib_borrow(templ), + req_name, + Option::::from_glib_borrow(caps) + .as_ref() + .as_ref(), + ) + }) + .to_glib_full() +} + +unsafe extern "C" fn aggregator_update_src_caps( + ptr: *mut gst_base_sys::GstAggregator, + caps: *mut gst_sys::GstCaps, + res: *mut *mut gst_sys::GstCaps, +) -> gst_sys::GstFlowReturn +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + *res = ptr::null_mut(); + + gst_panic_to_error!(&wrap, &instance.panicked(), gst::FlowReturn::Error, { + match imp.update_src_caps(&wrap, &from_glib_borrow(caps)) { + Ok(res_caps) => { + *res = res_caps.into_ptr(); + gst::FlowReturn::Ok + } + Err(err) => err.into(), + } + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_fixate_src_caps( + ptr: *mut gst_base_sys::GstAggregator, + caps: *mut gst_sys::GstCaps, +) -> *mut gst_sys::GstCaps +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), gst::Caps::new_empty(), { + imp.fixate_src_caps(&wrap, from_glib_full(caps)) + }) + .into_ptr() +} + +unsafe extern "C" fn aggregator_negotiated_src_caps( + ptr: *mut gst_base_sys::GstAggregator, + caps: *mut gst_sys::GstCaps, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { + match imp.negotiated_src_caps(&wrap, &from_glib_borrow(caps)) { + Ok(()) => true, + Err(err) => { + err.log_with_object(&*wrap); + false + } + } + }) + .to_glib() +} + +unsafe extern "C" fn aggregator_negotiate( + ptr: *mut gst_base_sys::GstAggregator, +) -> glib_sys::gboolean +where + T: AggregatorImpl, + T::Instance: PanicPoison, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + gst_panic_to_error!(&wrap, &instance.panicked(), false, { imp.negotiate(&wrap) }).to_glib() +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator_pad.rs b/utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator_pad.rs new file mode 100644 index 000000000..c09c4a36d --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/subclass/aggregator_pad.rs @@ -0,0 +1,147 @@ +// Copyright (C) 2018 Sebastian Dröge +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use super::super::gst_base_sys; +use glib_sys; +use gst_sys; + +use glib::translate::*; +use gst; + +use glib::subclass::prelude::*; +use gst::subclass::prelude::*; + +use super::super::Aggregator; +use super::super::AggregatorPad; +use super::super::AggregatorPadClass; + +pub trait AggregatorPadImpl: AggregatorPadImplExt + PadImpl + Send + Sync + 'static { + fn flush( + &self, + aggregator_pad: &AggregatorPad, + aggregator: &Aggregator, + ) -> Result { + self.parent_flush(aggregator_pad, aggregator) + } + + fn skip_buffer( + &self, + aggregator_pad: &AggregatorPad, + aggregator: &Aggregator, + buffer: &gst::Buffer, + ) -> bool { + self.parent_skip_buffer(aggregator_pad, aggregator, buffer) + } +} + +pub trait AggregatorPadImplExt { + fn parent_flush( + &self, + aggregator_pad: &AggregatorPad, + aggregator: &Aggregator, + ) -> Result; + + fn parent_skip_buffer( + &self, + aggregator_pad: &AggregatorPad, + aggregator: &Aggregator, + buffer: &gst::Buffer, + ) -> bool; +} + +impl AggregatorPadImplExt for T { + fn parent_flush( + &self, + aggregator_pad: &AggregatorPad, + aggregator: &Aggregator, + ) -> Result { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorPadClass; + (*parent_class) + .flush + .map(|f| { + from_glib(f( + aggregator_pad.to_glib_none().0, + aggregator.to_glib_none().0, + )) + }) + .unwrap_or(gst::FlowReturn::Ok) + .into_result() + } + } + + fn parent_skip_buffer( + &self, + aggregator_pad: &AggregatorPad, + aggregator: &Aggregator, + buffer: &gst::Buffer, + ) -> bool { + unsafe { + let data = self.get_type_data(); + let parent_class = + data.as_ref().get_parent_class() as *mut gst_base_sys::GstAggregatorPadClass; + (*parent_class) + .skip_buffer + .map(|f| { + from_glib(f( + aggregator_pad.to_glib_none().0, + aggregator.to_glib_none().0, + buffer.to_glib_none().0, + )) + }) + .unwrap_or(false) + } + } +} +unsafe impl IsSubclassable for AggregatorPadClass { + fn override_vfuncs(&mut self) { + >::override_vfuncs(self); + unsafe { + let klass = &mut *(self as *mut Self as *mut gst_base_sys::GstAggregatorPadClass); + klass.flush = Some(aggregator_pad_flush::); + klass.skip_buffer = Some(aggregator_pad_skip_buffer::); + } + } +} + +unsafe extern "C" fn aggregator_pad_flush( + ptr: *mut gst_base_sys::GstAggregatorPad, + aggregator: *mut gst_base_sys::GstAggregator, +) -> gst_sys::GstFlowReturn +where + T: AggregatorPadImpl, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + let res: gst::FlowReturn = imp.flush(&wrap, &from_glib_borrow(aggregator)).into(); + res.to_glib() +} + +unsafe extern "C" fn aggregator_pad_skip_buffer( + ptr: *mut gst_base_sys::GstAggregatorPad, + aggregator: *mut gst_base_sys::GstAggregator, + buffer: *mut gst_sys::GstBuffer, +) -> glib_sys::gboolean +where + T: AggregatorPadImpl, +{ + let instance = &*(ptr as *mut T::Instance); + let imp = instance.get_impl(); + let wrap: Borrowed = from_glib_borrow(ptr); + + imp.skip_buffer( + &wrap, + &from_glib_borrow(aggregator), + &from_glib_borrow(buffer), + ) + .to_glib() +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/subclass/mod.rs b/utils/gst-plugin-fallbackswitch/src/base/subclass/mod.rs new file mode 100644 index 000000000..319dc1311 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/subclass/mod.rs @@ -0,0 +1,17 @@ +// Copyright (C) 2016-2018 Sebastian Dröge +// 2016 Luis de Bethencourt +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. +#![allow(clippy::cast_ptr_alignment)] + +pub mod aggregator; +pub mod aggregator_pad; + +pub mod prelude { + pub use super::aggregator::{AggregatorImpl, AggregatorImplExt}; + pub use super::aggregator_pad::{AggregatorPadImpl, AggregatorPadImplExt}; +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/sys.rs b/utils/gst-plugin-fallbackswitch/src/base/sys.rs new file mode 100644 index 000000000..ac3e8ffcd --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/sys.rs @@ -0,0 +1,235 @@ +#![allow(non_camel_case_types, non_upper_case_globals, non_snake_case)] +#![allow( + clippy::approx_constant, + clippy::type_complexity, + clippy::unreadable_literal +)] + +extern crate gstreamer_sys as gst; + +#[allow(unused_imports)] +use libc::{ + c_char, c_double, c_float, c_int, c_long, c_short, c_uchar, c_uint, c_ulong, c_ushort, c_void, + intptr_t, size_t, ssize_t, time_t, uintptr_t, FILE, +}; + +#[allow(unused_imports)] +use glib_sys::{gboolean, gconstpointer, gpointer, GType}; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct GstAggregatorClass { + pub parent_class: gst::GstElementClass, + pub flush: Option gst::GstFlowReturn>, + pub clip: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut GstAggregatorPad, + *mut gst::GstBuffer, + ) -> *mut gst::GstBuffer, + >, + pub finish_buffer: + Option gst::GstFlowReturn>, + pub sink_event: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut GstAggregatorPad, + *mut gst::GstEvent, + ) -> gboolean, + >, + pub sink_query: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut GstAggregatorPad, + *mut gst::GstQuery, + ) -> gboolean, + >, + pub src_event: Option gboolean>, + pub src_query: Option gboolean>, + pub src_activate: + Option gboolean>, + pub aggregate: Option gst::GstFlowReturn>, + pub stop: Option gboolean>, + pub start: Option gboolean>, + pub get_next_time: Option gst::GstClockTime>, + pub create_new_pad: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut gst::GstPadTemplate, + *const c_char, + *const gst::GstCaps, + ) -> *mut GstAggregatorPad, + >, + pub update_src_caps: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut gst::GstCaps, + *mut *mut gst::GstCaps, + ) -> gst::GstFlowReturn, + >, + pub fixate_src_caps: + Option *mut gst::GstCaps>, + pub negotiated_src_caps: + Option gboolean>, + pub decide_allocation: + Option gboolean>, + pub propose_allocation: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut GstAggregatorPad, + *mut gst::GstQuery, + *mut gst::GstQuery, + ) -> gboolean, + >, + pub negotiate: Option gboolean>, + pub sink_event_pre_queue: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut GstAggregatorPad, + *mut gst::GstEvent, + ) -> gboolean, + >, + pub sink_query_pre_queue: Option< + unsafe extern "C" fn( + *mut GstAggregator, + *mut GstAggregatorPad, + *mut gst::GstQuery, + ) -> gboolean, + >, + pub _gst_reserved: [gpointer; 17], +} + +impl ::std::fmt::Debug for GstAggregatorClass { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct(&format!("GstAggregatorClass @ {:?}", self as *const _)) + .field("parent_class", &self.parent_class) + .field("flush", &self.flush) + .field("clip", &self.clip) + .field("finish_buffer", &self.finish_buffer) + .field("sink_event", &self.sink_event) + .field("sink_query", &self.sink_query) + .field("src_event", &self.src_event) + .field("src_query", &self.src_query) + .field("src_activate", &self.src_activate) + .field("aggregate", &self.aggregate) + .field("stop", &self.stop) + .field("start", &self.start) + .field("get_next_time", &self.get_next_time) + .field("create_new_pad", &self.create_new_pad) + .field("update_src_caps", &self.update_src_caps) + .field("fixate_src_caps", &self.fixate_src_caps) + .field("negotiated_src_caps", &self.negotiated_src_caps) + .field("decide_allocation", &self.decide_allocation) + .field("propose_allocation", &self.propose_allocation) + .finish() + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct GstAggregatorPadClass { + pub parent_class: gst::GstPadClass, + pub flush: Option< + unsafe extern "C" fn(*mut GstAggregatorPad, *mut GstAggregator) -> gst::GstFlowReturn, + >, + pub skip_buffer: Option< + unsafe extern "C" fn( + *mut GstAggregatorPad, + *mut GstAggregator, + *mut gst::GstBuffer, + ) -> gboolean, + >, + pub _gst_reserved: [gpointer; 20], +} + +impl ::std::fmt::Debug for GstAggregatorPadClass { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct(&format!("GstAggregatorPadClass @ {:?}", self as *const _)) + .field("parent_class", &self.parent_class) + .field("flush", &self.flush) + .field("skip_buffer", &self.skip_buffer) + .finish() + } +} + +#[repr(C)] +pub struct _GstAggregatorPadPrivate(c_void); + +pub type GstAggregatorPadPrivate = *mut _GstAggregatorPadPrivate; + +#[repr(C)] +pub struct _GstAggregatorPrivate(c_void); + +pub type GstAggregatorPrivate = *mut _GstAggregatorPrivate; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct GstAggregator { + pub parent: gst::GstElement, + pub srcpad: *mut gst::GstPad, + pub priv_: *mut GstAggregatorPrivate, + pub _gst_reserved: [gpointer; 20], +} + +impl ::std::fmt::Debug for GstAggregator { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct(&format!("GstAggregator @ {:?}", self as *const _)) + .field("parent", &self.parent) + .field("srcpad", &self.srcpad) + .finish() + } +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct GstAggregatorPad { + pub parent: gst::GstPad, + pub segment: gst::GstSegment, + pub priv_: *mut GstAggregatorPadPrivate, + pub _gst_reserved: [gpointer; 4], +} + +impl ::std::fmt::Debug for GstAggregatorPad { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + f.debug_struct(&format!("GstAggregatorPad @ {:?}", self as *const _)) + .field("parent", &self.parent) + .field("segment", &self.segment) + .finish() + } +} + +extern "C" { + //========================================================================= + // GstAggregator + //========================================================================= + pub fn gst_aggregator_get_type() -> GType; + pub fn gst_aggregator_finish_buffer( + aggregator: *mut GstAggregator, + buffer: *mut gst::GstBuffer, + ) -> gst::GstFlowReturn; + pub fn gst_aggregator_negotiate(aggregator: *mut GstAggregator) -> gboolean; + pub fn gst_aggregator_get_allocator( + self_: *mut GstAggregator, + allocator: *mut *mut gst::GstAllocator, + params: *mut gst::GstAllocationParams, + ); + pub fn gst_aggregator_get_buffer_pool(self_: *mut GstAggregator) -> *mut gst::GstBufferPool; + pub fn gst_aggregator_get_latency(self_: *mut GstAggregator) -> gst::GstClockTime; + pub fn gst_aggregator_set_latency( + self_: *mut GstAggregator, + min_latency: gst::GstClockTime, + max_latency: gst::GstClockTime, + ); + pub fn gst_aggregator_set_src_caps(self_: *mut GstAggregator, caps: *mut gst::GstCaps); + pub fn gst_aggregator_simple_get_next_time(self_: *mut GstAggregator) -> gst::GstClockTime; + + //========================================================================= + // GstAggregatorPad + //========================================================================= + pub fn gst_aggregator_pad_get_type() -> GType; + pub fn gst_aggregator_pad_drop_buffer(pad: *mut GstAggregatorPad) -> gboolean; + pub fn gst_aggregator_pad_has_buffer(pad: *mut GstAggregatorPad) -> gboolean; + pub fn gst_aggregator_pad_is_eos(pad: *mut GstAggregatorPad) -> gboolean; + pub fn gst_aggregator_pad_peek_buffer(pad: *mut GstAggregatorPad) -> *mut gst::GstBuffer; + pub fn gst_aggregator_pad_pop_buffer(pad: *mut GstAggregatorPad) -> *mut gst::GstBuffer; +} diff --git a/utils/gst-plugin-fallbackswitch/src/base/utils.rs b/utils/gst-plugin-fallbackswitch/src/base/utils.rs new file mode 100644 index 000000000..dca8cf691 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/base/utils.rs @@ -0,0 +1,30 @@ +// Copyright (C) 2017 Sebastian Dröge +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use glib::translate::mut_override; +use glib_sys; + +pub struct MutexGuard<'a>(&'a glib_sys::GMutex); + +impl<'a> MutexGuard<'a> { + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn lock(mutex: &'a glib_sys::GMutex) -> Self { + unsafe { + glib_sys::g_mutex_lock(mut_override(mutex)); + } + MutexGuard(mutex) + } +} + +impl<'a> Drop for MutexGuard<'a> { + fn drop(&mut self) { + unsafe { + glib_sys::g_mutex_unlock(mut_override(self.0)); + } + } +} diff --git a/utils/gst-plugin-fallbackswitch/src/fallbackswitch.rs b/utils/gst-plugin-fallbackswitch/src/fallbackswitch.rs new file mode 100644 index 000000000..ddf4cca43 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/fallbackswitch.rs @@ -0,0 +1,794 @@ +// Copyright (C) 2019 Sebastian Dröge +// +// 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. + +#[cfg(not(feature = "v1_18"))] +use super::base as gst_base; +use glib; +use glib::prelude::*; +use glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst_audio; +#[cfg(feature = "v1_18")] +use gst_base; +use gst_base::prelude::*; +use gst_base::subclass::prelude::*; +use gst_video; + +use std::sync::{Mutex, RwLock}; + +struct FallbackSwitch { + sinkpad: gst_base::AggregatorPad, + fallback_sinkpad: RwLock>, + active_sinkpad: Mutex, + output_state: Mutex, + pad_states: RwLock, + settings: Mutex, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "fallbackswitch", + gst::DebugColorFlags::empty(), + Some("Fallback switch Element"), + ); +} + +#[derive(Debug)] +struct OutputState { + last_sinkpad_time: gst::ClockTime, +} + +#[derive(Debug, Default)] +struct PadStates { + sinkpad: PadState, + fallback_sinkpad: Option, +} + +#[derive(Debug, Default)] +struct PadState { + caps: Option, + audio_info: Option, + video_info: Option, +} + +const DEFAULT_TIMEOUT: u64 = 5 * gst::SECOND_VAL; + +#[derive(Debug, Clone)] +struct Settings { + timeout: gst::ClockTime, +} + +impl Default for OutputState { + fn default() -> Self { + OutputState { + last_sinkpad_time: gst::CLOCK_TIME_NONE, + } + } +} + +impl Default for Settings { + fn default() -> Self { + Settings { + timeout: DEFAULT_TIMEOUT.into(), + } + } +} + +static PROPERTIES: [subclass::Property; 2] = [ + subclass::Property("timeout", |name| { + glib::ParamSpec::uint64( + name, + "Timeout", + "Timeout in nanoseconds", + 0, + std::u64::MAX, + DEFAULT_TIMEOUT, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("active-pad", |name| { + glib::ParamSpec::object( + name, + "Active Pad", + "Currently active pad", + gst::Pad::static_type(), + glib::ParamFlags::READABLE, + ) + }), +]; + +impl FallbackSwitch { + fn handle_main_buffer( + &self, + agg: &gst_base::Aggregator, + state: &mut OutputState, + mut buffer: gst::Buffer, + fallback_sinkpad: Option<&gst_base::AggregatorPad>, + ) -> Result, gst::FlowError> { + // If we got a buffer on the sinkpad just handle it + gst_debug!(CAT, obj: agg, "Got buffer on sinkpad {:?}", buffer); + + if buffer.get_pts().is_none() { + gst_error!(CAT, obj: agg, "Only buffers with PTS supported"); + return Err(gst::FlowError::Error); + } + + let segment = self + .sinkpad + .get_segment() + .downcast::() + .map_err(|_| { + gst_error!(CAT, obj: agg, "Only TIME segments supported"); + gst::FlowError::Error + })?; + + { + // FIXME: This will not work correctly for negative DTS + let buffer = buffer.make_mut(); + buffer.set_pts(segment.to_running_time(buffer.get_pts())); + buffer.set_dts(segment.to_running_time(buffer.get_dts())); + } + + let mut active_sinkpad = self.active_sinkpad.lock().unwrap(); + let pad_change = &*active_sinkpad != self.sinkpad.upcast_ref::(); + if pad_change { + if buffer.get_flags().contains(gst::BufferFlags::DELTA_UNIT) { + gst_info!( + CAT, + obj: agg, + "Can't change back to sinkpad, waiting for keyframe" + ); + self.sinkpad.push_event( + gst_video::new_upstream_force_key_unit_event() + .all_headers(true) + .build(), + ); + return Ok(None); + } + + gst_info!(CAT, obj: agg, "Active pad changed to sinkpad"); + *active_sinkpad = self.sinkpad.clone().upcast(); + } + drop(active_sinkpad); + + state.last_sinkpad_time = segment.to_running_time(buffer.get_dts_or_pts()); + + // Drop all older buffers from the fallback sinkpad + if let Some(fallback_sinkpad) = fallback_sinkpad { + let fallback_segment = self + .sinkpad + .get_segment() + .downcast::() + .map_err(|_| { + gst_error!(CAT, obj: agg, "Only TIME segments supported"); + gst::FlowError::Error + })?; + + while let Some(fallback_buffer) = fallback_sinkpad.peek_buffer() { + let fallback_pts = fallback_buffer.get_dts_or_pts(); + if fallback_pts.is_none() + || fallback_segment.to_running_time(fallback_pts) <= state.last_sinkpad_time + { + gst_debug!( + CAT, + obj: agg, + "Dropping fallback buffer {:?}", + fallback_buffer + ); + fallback_sinkpad.drop_buffer(); + } else { + break; + } + } + } + + let pad_states = self.pad_states.read().unwrap(); + let active_caps = pad_states.sinkpad.caps.as_ref().unwrap().clone(); + drop(pad_states); + + Ok(Some((buffer, active_caps, pad_change))) + } + + fn get_fallback_buffer( + &self, + agg: &gst_base::Aggregator, + state: &mut OutputState, + settings: &Settings, + fallback_sinkpad: &gst_base::AggregatorPad, + ) -> Result<(gst::Buffer, gst::Caps, bool), gst::FlowError> { + // If we have a fallback sinkpad and timeout, try to get a fallback buffer from here + // and drop all too old buffers in the process + loop { + let mut buffer = fallback_sinkpad + .pop_buffer() + .ok_or(gst_base::AGGREGATOR_FLOW_NEED_DATA)?; + + gst_debug!(CAT, obj: agg, "Got buffer on fallback sinkpad {:?}", buffer); + + if buffer.get_pts().is_none() { + gst_error!(CAT, obj: agg, "Only buffers with PTS supported"); + return Err(gst::FlowError::Error); + } + + let fallback_segment = fallback_sinkpad + .get_segment() + .downcast::() + .map_err(|_| { + gst_error!(CAT, obj: agg, "Only TIME segments supported"); + gst::FlowError::Error + })?; + let running_time = fallback_segment.to_running_time(buffer.get_dts_or_pts()); + + { + // FIXME: This will not work correctly for negative DTS + let buffer = buffer.make_mut(); + buffer.set_pts(fallback_segment.to_running_time(buffer.get_pts())); + buffer.set_dts(fallback_segment.to_running_time(buffer.get_dts())); + } + + // If we never had a real buffer, initialize with the running time of the fallback + // sinkpad so that we still output fallback buffers after the timeout + if state.last_sinkpad_time.is_none() { + state.last_sinkpad_time = running_time; + } + + // Get the next one if this one is before the timeout + if state.last_sinkpad_time + settings.timeout > running_time { + gst_debug!( + CAT, + obj: agg, + "Timeout not reached yet: {} + {} > {}", + state.last_sinkpad_time, + settings.timeout, + running_time + ); + + continue; + } + + gst_debug!( + CAT, + obj: agg, + "Timeout reached: {} + {} <= {}", + state.last_sinkpad_time, + settings.timeout, + running_time + ); + + let mut active_sinkpad = self.active_sinkpad.lock().unwrap(); + let pad_change = &*active_sinkpad != fallback_sinkpad.upcast_ref::(); + if pad_change { + if buffer.get_flags().contains(gst::BufferFlags::DELTA_UNIT) { + gst_info!( + CAT, + obj: agg, + "Can't change to fallback sinkpad yet, waiting for keyframe" + ); + fallback_sinkpad.push_event( + gst_video::new_upstream_force_key_unit_event() + .all_headers(true) + .build(), + ); + continue; + } + + gst_info!(CAT, obj: agg, "Active pad changed to fallback sinkpad"); + *active_sinkpad = fallback_sinkpad.clone().upcast(); + } + drop(active_sinkpad); + + let pad_states = self.pad_states.read().unwrap(); + let active_caps = match pad_states.fallback_sinkpad { + None => { + // This can happen if the pad is removed in the meantime, + // not a problem really + return Err(gst_base::AGGREGATOR_FLOW_NEED_DATA); + } + Some(ref fallback_sinkpad) => fallback_sinkpad.caps.as_ref().unwrap().clone(), + }; + drop(pad_states); + + break Ok((buffer, active_caps, pad_change)); + } + } + + fn get_next_buffer( + &self, + agg: &gst_base::Aggregator, + timeout: bool, + ) -> Result<(gst::Buffer, gst::Caps, bool), gst::FlowError> { + let settings = self.settings.lock().unwrap().clone(); + let mut state = self.output_state.lock().unwrap(); + let fallback_sinkpad = self.fallback_sinkpad.read().unwrap(); + + gst_debug!(CAT, obj: agg, "Aggregate called: timeout {}", timeout); + + if let Some(buffer) = self.sinkpad.pop_buffer() { + if let Some(res) = + self.handle_main_buffer(agg, &mut *state, buffer, fallback_sinkpad.as_ref())? + { + return Ok(res); + } + } else if self.sinkpad.is_eos() { + gst_log!(CAT, obj: agg, "Sinkpad is EOS"); + return Err(gst::FlowError::Eos); + } + + if let (false, Some(_)) = (timeout, &*fallback_sinkpad) { + gst_debug!(CAT, obj: agg, "Have fallback sinkpad but no timeout yet"); + + Err(gst_base::AGGREGATOR_FLOW_NEED_DATA) + } else if let (true, Some(fallback_sinkpad)) = (timeout, &*fallback_sinkpad) { + self.get_fallback_buffer(agg, &mut *state, &settings, fallback_sinkpad) + } else { + // Otherwise there's not much we can do at this point + gst_debug!( + CAT, + obj: agg, + "Got no buffer on sinkpad and have no fallback sinkpad" + ); + Err(gst_base::AGGREGATOR_FLOW_NEED_DATA) + } + } +} + +impl ObjectSubclass for FallbackSwitch { + const NAME: &'static str = "FallbackSwitch"; + type ParentType = gst_base::Aggregator; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn new_with_class(klass: &subclass::simple::ClassStruct) -> Self { + let templ = klass.get_pad_template("sink").unwrap(); + let sinkpad: gst_base::AggregatorPad = glib::Object::new( + gst_base::AggregatorPad::static_type(), + &[ + ("name", &"sink"), + ("direction", &gst::PadDirection::Sink), + ("template", &templ), + ], + ) + .unwrap() + .downcast() + .unwrap(); + + Self { + sinkpad: sinkpad.clone(), + fallback_sinkpad: RwLock::new(None), + active_sinkpad: Mutex::new(sinkpad.upcast()), + output_state: Mutex::new(OutputState::default()), + pad_states: RwLock::new(PadStates::default()), + settings: Mutex::new(Settings::default()), + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + klass.set_metadata( + "Fallback Switch", + "Generic", + "Allows switching to a fallback input after a given timeout", + "Sebastian Dröge ", + ); + + let caps = gst::Caps::new_any(); + let src_pad_template = gst::PadTemplate::new_with_gtype( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &caps, + gst_base::AggregatorPad::static_type(), + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_pad_template = gst::PadTemplate::new_with_gtype( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &caps, + gst_base::AggregatorPad::static_type(), + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let fallbacksink_pad_template = gst::PadTemplate::new_with_gtype( + "fallback_sink", + gst::PadDirection::Sink, + gst::PadPresence::Request, + &caps, + gst_base::AggregatorPad::static_type(), + ) + .unwrap(); + klass.add_pad_template(fallbacksink_pad_template); + + klass.install_properties(&PROPERTIES); + } +} + +impl ObjectImpl for FallbackSwitch { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let agg = obj.downcast_ref::().unwrap(); + agg.add_pad(&self.sinkpad).unwrap(); + } + + fn set_property(&self, obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + let agg = obj.downcast_ref::().unwrap(); + + match *prop { + subclass::Property("timeout", ..) => { + let mut settings = self.settings.lock().unwrap(); + let timeout = value.get_some().expect("type checked upstream"); + gst_info!( + CAT, + obj: agg, + "Changing timeout from {} to {}", + settings.timeout, + timeout + ); + settings.timeout = timeout; + drop(settings); + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("timeout", ..) => { + let settings = self.settings.lock().unwrap(); + Ok(settings.timeout.to_value()) + } + subclass::Property("active-pad", ..) => { + let active_pad = self.active_sinkpad.lock().unwrap().clone(); + Ok(active_pad.to_value()) + } + _ => unimplemented!(), + } + } +} + +impl ElementImpl for FallbackSwitch { + fn request_new_pad( + &self, + element: &gst::Element, + templ: &gst::PadTemplate, + name: Option, + _caps: Option<&gst::Caps>, + ) -> Option { + let agg = element.downcast_ref::().unwrap(); + let fallback_sink_templ = agg.get_pad_template("fallback_sink").unwrap(); + if templ != &fallback_sink_templ + || (name.is_some() && name.as_deref() != Some("fallback_sink")) + { + gst_error!(CAT, obj: agg, "Wrong pad template or name"); + return None; + } + + let mut fallback_sinkpad = self.fallback_sinkpad.write().unwrap(); + if fallback_sinkpad.is_some() { + gst_error!(CAT, obj: agg, "Already have a fallback sinkpad"); + return None; + } + + let sinkpad: gst_base::AggregatorPad = glib::Object::new( + gst_base::AggregatorPad::static_type(), + &[ + ("name", &"fallback_sink"), + ("direction", &gst::PadDirection::Sink), + ("template", templ), + ], + ) + .unwrap() + .downcast() + .unwrap(); + *fallback_sinkpad = Some(sinkpad.clone()); + drop(fallback_sinkpad); + + agg.add_pad(&sinkpad).unwrap(); + + Some(sinkpad.upcast()) + } + + fn release_pad(&self, element: &gst::Element, pad: &gst::Pad) { + let agg = element.downcast_ref::().unwrap(); + let mut fallback_sinkpad = self.fallback_sinkpad.write().unwrap(); + let mut pad_states = self.pad_states.write().unwrap(); + + if fallback_sinkpad.as_ref().map(|p| p.upcast_ref()) == Some(pad) { + *fallback_sinkpad = None; + pad_states.fallback_sinkpad = None; + drop(pad_states); + drop(fallback_sinkpad); + agg.remove_pad(pad).unwrap(); + gst_debug!(CAT, obj: agg, "Removed fallback sinkpad {:?}", pad); + } + } +} + +impl AggregatorImpl for FallbackSwitch { + fn start(&self, _agg: &gst_base::Aggregator) -> Result<(), gst::ErrorMessage> { + *self.active_sinkpad.lock().unwrap() = self.sinkpad.clone().upcast(); + *self.output_state.lock().unwrap() = OutputState::default(); + *self.pad_states.write().unwrap() = PadStates::default(); + + Ok(()) + } + + fn sink_event_pre_queue( + &self, + agg: &gst_base::Aggregator, + agg_pad: &gst_base::AggregatorPad, + event: gst::Event, + ) -> bool { + use gst::EventView; + + match event.view() { + EventView::Gap(_) => { + gst_debug!(CAT, obj: agg_pad, "Dropping gap event"); + true + } + _ => self.parent_sink_event_pre_queue(agg, agg_pad, event), + } + } + + fn sink_event( + &self, + agg: &gst_base::Aggregator, + agg_pad: &gst_base::AggregatorPad, + event: gst::Event, + ) -> bool { + use gst::EventView; + + match event.view() { + EventView::Caps(caps) => { + let caps = caps.get_caps_owned(); + gst_debug!(CAT, obj: agg_pad, "Received caps {}", caps); + + let audio_info; + let video_info; + if caps.get_structure(0).unwrap().get_name() == "audio/x-raw" { + audio_info = gst_audio::AudioInfo::from_caps(&caps).ok(); + video_info = None; + } else if caps.get_structure(0).unwrap().get_name() == "video/x-raw" { + audio_info = None; + video_info = gst_video::VideoInfo::from_caps(&caps).ok(); + } else { + audio_info = None; + video_info = None; + } + + let new_pad_state = PadState { + caps: Some(caps), + audio_info, + video_info, + }; + + let mut pad_states = self.pad_states.write().unwrap(); + if agg_pad == &self.sinkpad { + pad_states.sinkpad = new_pad_state; + } else if Some(agg_pad) == self.fallback_sinkpad.read().unwrap().as_ref() { + pad_states.fallback_sinkpad = Some(new_pad_state); + } + drop(pad_states); + + self.parent_sink_event(agg, agg_pad, event) + } + _ => self.parent_sink_event(agg, agg_pad, event), + } + } + + fn get_next_time(&self, agg: &gst_base::Aggregator) -> gst::ClockTime { + // If we have a buffer on the sinkpad then the timeout is always going to be immediately, + // i.e. 0. We want to output that buffer immediately, no matter what. + // + // Otherwise if we have a fallback sinkpad and it has a buffer, then the timeout is going + // to be its running time. We will then either output the buffer or drop it, depending on + // its distance from the last sinkpad time + if self.sinkpad.peek_buffer().is_some() { + gst_debug!(CAT, obj: agg, "Have buffer on sinkpad, immediate timeout"); + 0.into() + } else if self.sinkpad.is_eos() { + gst_debug!(CAT, obj: agg, "Sinkpad is EOS, immediate timeout"); + 0.into() + } else if let Some((buffer, fallback_sinkpad)) = self + .fallback_sinkpad + .read() + .unwrap() + .as_ref() + .and_then(|p| p.peek_buffer().map(|buffer| (buffer, p))) + { + if buffer.get_pts().is_none() { + gst_error!(CAT, obj: agg, "Only buffers with PTS supported"); + // Trigger aggregate immediately to error out immediately + return 0.into(); + } + + let segment = match fallback_sinkpad.get_segment().downcast::() { + Ok(segment) => segment, + Err(_) => { + gst_error!(CAT, obj: agg, "Only TIME segments supported"); + // Trigger aggregate immediately to error out immediately + return 0.into(); + } + }; + + let running_time = segment.to_running_time(buffer.get_dts_or_pts()); + gst_debug!( + CAT, + obj: agg, + "Have buffer on fallback sinkpad, timeout at {}", + running_time + ); + running_time + } else { + gst_debug!(CAT, obj: agg, "Have no buffer at all yet"); + gst::CLOCK_TIME_NONE + } + } + + // Clip the raw audio/video buffers we have to the segment boundaries to ensure that + // calculating the running times later works correctly + fn clip( + &self, + agg: &gst_base::Aggregator, + agg_pad: &gst_base::AggregatorPad, + mut buffer: gst::Buffer, + ) -> Option { + let segment = match agg_pad.get_segment().downcast::() { + Ok(segment) => segment, + Err(_) => { + gst_error!(CAT, obj: agg, "Only TIME segments supported"); + return Some(buffer); + } + }; + + let pts = buffer.get_pts(); + if pts.is_none() { + gst_error!(CAT, obj: agg, "Only buffers with PTS supported"); + return Some(buffer); + } + + let pad_states = self.pad_states.read().unwrap(); + let pad_state = if agg_pad == &self.sinkpad { + &pad_states.sinkpad + } else if Some(agg_pad) == self.fallback_sinkpad.read().unwrap().as_ref() { + if let Some(ref pad_state) = pad_states.fallback_sinkpad { + pad_state + } else { + return Some(buffer); + } + } else { + unreachable!() + }; + + if pad_state.audio_info.is_none() && pad_state.video_info.is_none() { + // No clipping possible for non-raw formats + return Some(buffer); + } + + let duration = if buffer.get_duration().is_some() { + buffer.get_duration() + } else if let Some(ref audio_info) = pad_state.audio_info { + gst::SECOND + .mul_div_floor( + buffer.get_size() as u64, + audio_info.rate() as u64 * audio_info.bpf() as u64, + ) + .unwrap() + } else if let Some(ref video_info) = pad_state.video_info { + if *video_info.fps().numer() > 0 { + gst::SECOND + .mul_div_floor( + *video_info.fps().denom() as u64, + *video_info.fps().numer() as u64, + ) + .unwrap() + } else { + gst::CLOCK_TIME_NONE + } + } else { + unreachable!() + }; + + gst_debug!( + CAT, + obj: agg_pad, + "Clipping buffer {:?} with PTS {} and duration {}", + buffer, + pts, + duration + ); + if let Some(ref audio_info) = pad_state.audio_info { + gst_audio::audio_buffer_clip( + buffer, + segment.upcast_ref(), + audio_info.rate(), + audio_info.bpf(), + ) + } else if pad_state.video_info.is_some() { + segment.clip(pts, pts + duration).map(|(start, stop)| { + { + let buffer = buffer.make_mut(); + buffer.set_pts(start); + buffer.set_dts(start); + if duration.is_some() { + buffer.set_duration(stop - start); + } + } + + buffer + }) + } else { + unreachable!(); + } + } + + fn aggregate( + &self, + agg: &gst_base::Aggregator, + timeout: bool, + ) -> Result { + gst_debug!(CAT, obj: agg, "Aggregate called: timeout {}", timeout); + + let (mut buffer, active_caps, pad_change) = self.get_next_buffer(agg, timeout)?; + + let current_src_caps = agg.get_static_pad("src").unwrap().get_current_caps(); + if Some(&active_caps) != current_src_caps.as_ref() { + gst_info!( + CAT, + obj: agg, + "Caps change from {:?} to {:?}", + current_src_caps, + active_caps + ); + agg.set_src_caps(&active_caps); + } + + if pad_change { + agg.notify("active-pad"); + buffer.make_mut().set_flags(gst::BufferFlags::DISCONT); + } + + gst_debug!(CAT, obj: agg, "Finishing buffer {:?}", buffer); + agg.finish_buffer(buffer) + } + + fn negotiate(&self, _agg: &gst_base::Aggregator) -> bool { + true + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "fallbackswitch", + gst::Rank::None, + FallbackSwitch::get_type(), + ) +} diff --git a/utils/gst-plugin-fallbackswitch/src/lib.rs b/utils/gst-plugin-fallbackswitch/src/lib.rs new file mode 100644 index 000000000..c2b2b0fdf --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/src/lib.rs @@ -0,0 +1,62 @@ +// Copyright (C) 2019 Sebastian Dröge +// +// 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. + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gstreamer as gst; + +extern crate gstreamer_audio as gst_audio; +extern crate gstreamer_video as gst_video; + +#[cfg(not(feature = "v1_18"))] +extern crate glib_sys; +#[cfg(not(feature = "v1_18"))] +extern crate gobject_sys; +#[cfg(feature = "v1_18")] +extern crate gstreamer_base as gst_base; +#[cfg(not(feature = "v1_18"))] +extern crate gstreamer_sys as gst_sys; +#[cfg(not(feature = "v1_18"))] +#[allow(dead_code)] +mod base; +#[cfg(not(feature = "v1_18"))] +mod gst_base { + pub use super::base::*; +} + +#[macro_use] +extern crate lazy_static; + +mod fallbackswitch; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + fallbackswitch::register(plugin)?; + Ok(()) +} + +gst_plugin_define!( + fallbackswitch, + 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/utils/gst-plugin-fallbackswitch/tests/fallbackswitch.rs b/utils/gst-plugin-fallbackswitch/tests/fallbackswitch.rs new file mode 100644 index 000000000..20d45ae70 --- /dev/null +++ b/utils/gst-plugin-fallbackswitch/tests/fallbackswitch.rs @@ -0,0 +1,570 @@ +// Copyright (C) 2019 Sebastian Dröge +// +// 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. + +extern crate glib; +use glib::prelude::*; + +#[macro_use] +extern crate gstreamer as gst; +use gst::prelude::*; +extern crate gstreamer_app as gst_app; +extern crate gstreamer_check as gst_check; + +extern crate gstfallbackswitch; + +#[macro_use] +extern crate lazy_static; + +lazy_static! { + static ref TEST_CAT: gst::DebugCategory = gst::DebugCategory::new( + "fallbackswitch-test", + gst::DebugColorFlags::empty(), + Some("fallbackswitch test"), + ); +} + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstfallbackswitch::plugin_register_static().expect("gstfallbackswitch test"); + }); +} + +macro_rules! assert_fallback_buffer { + ($buffer:expr, $ts:expr) => { + assert_eq!($buffer.get_pts(), $ts); + assert_eq!($buffer.get_size(), 160 * 120 * 4); + }; +} + +macro_rules! assert_buffer { + ($buffer:expr, $ts:expr) => { + assert_eq!($buffer.get_pts(), $ts); + assert_eq!($buffer.get_size(), 320 * 240 * 4); + }; +} + +#[test] +fn test_no_fallback_no_drops() { + let pipeline = setup_pipeline(None); + + push_buffer(&pipeline, 0.into()); + set_time(&pipeline, 0.into()); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 0.into()); + + push_buffer(&pipeline, gst::SECOND); + set_time(&pipeline, gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, gst::SECOND); + + push_buffer(&pipeline, 2 * gst::SECOND); + set_time(&pipeline, 2 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 2 * gst::SECOND); + + push_eos(&pipeline); + wait_eos(&pipeline); + + stop_pipeline(pipeline); +} + +#[test] +fn test_no_drops_live() { + test_no_drops(true); +} + +#[test] +fn test_no_drops_not_live() { + test_no_drops(false); +} + +fn test_no_drops(live: bool) { + let pipeline = setup_pipeline(Some(live)); + + push_buffer(&pipeline, 0.into()); + push_fallback_buffer(&pipeline, 0.into()); + set_time(&pipeline, 0.into()); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 0.into()); + + push_fallback_buffer(&pipeline, gst::SECOND); + push_buffer(&pipeline, gst::SECOND); + set_time(&pipeline, gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, gst::SECOND); + + push_buffer(&pipeline, 2 * gst::SECOND); + push_fallback_buffer(&pipeline, 2 * gst::SECOND); + set_time(&pipeline, 2 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 2 * gst::SECOND); + + // EOS on the fallback should not be required + push_eos(&pipeline); + wait_eos(&pipeline); + + stop_pipeline(pipeline); +} + +#[test] +fn test_no_drops_but_no_fallback_frames_live() { + test_no_drops_but_no_fallback_frames(true); +} + +#[test] +fn test_no_drops_but_no_fallback_frames_not_live() { + test_no_drops_but_no_fallback_frames(false); +} + +fn test_no_drops_but_no_fallback_frames(live: bool) { + let pipeline = setup_pipeline(Some(live)); + + push_buffer(&pipeline, 0.into()); + // +10ms needed here because the immediate timeout will be always at running time 0, but + // aggregator also adds the latency to it so we end up at 10ms instead. + set_time(&pipeline, 10 * gst::MSECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 0.into()); + + push_buffer(&pipeline, gst::SECOND); + set_time(&pipeline, gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, gst::SECOND); + + push_buffer(&pipeline, 2 * gst::SECOND); + set_time(&pipeline, 2 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 2 * gst::SECOND); + + // EOS on the fallback should not be required + push_eos(&pipeline); + wait_eos(&pipeline); + + stop_pipeline(pipeline); +} + +#[test] +fn test_short_drop_live() { + test_short_drop(true); +} + +#[test] +fn test_short_drop_not_live() { + test_short_drop(false); +} + +fn test_short_drop(live: bool) { + let pipeline = setup_pipeline(Some(live)); + + push_buffer(&pipeline, 0.into()); + push_fallback_buffer(&pipeline, 0.into()); + set_time(&pipeline, 0.into()); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 0.into()); + + // A timeout at 1s will get rid of the fallback buffer + // but not output anything + push_fallback_buffer(&pipeline, gst::SECOND); + // Time out the fallback buffer at +10ms + set_time(&pipeline, gst::SECOND + 10 * gst::MSECOND); + + push_fallback_buffer(&pipeline, 2 * gst::SECOND); + push_buffer(&pipeline, 2 * gst::SECOND); + set_time(&pipeline, 2 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 2 * gst::SECOND); + + push_eos(&pipeline); + push_fallback_eos(&pipeline); + wait_eos(&pipeline); + + stop_pipeline(pipeline); +} + +#[test] +fn test_long_drop_and_eos_live() { + test_long_drop_and_eos(true); +} + +#[test] +fn test_long_drop_and_eos_not_live() { + test_long_drop_and_eos(false); +} + +fn test_long_drop_and_eos(live: bool) { + let pipeline = setup_pipeline(Some(live)); + + // Produce the first frame + push_buffer(&pipeline, 0.into()); + push_fallback_buffer(&pipeline, 0.into()); + set_time(&pipeline, 0.into()); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 0.into()); + + // Produce a second frame but only from the fallback source + push_fallback_buffer(&pipeline, gst::SECOND); + set_time(&pipeline, gst::SECOND + 10 * gst::MSECOND); + + // Produce a third frame but only from the fallback source + push_fallback_buffer(&pipeline, 2 * gst::SECOND); + set_time(&pipeline, 2 * gst::SECOND + 10 * gst::MSECOND); + + // Produce a fourth frame but only from the fallback source + // This should be output now + push_fallback_buffer(&pipeline, 3 * gst::SECOND); + set_time(&pipeline, 3 * gst::SECOND + 10 * gst::MSECOND); + let buffer = pull_buffer(&pipeline); + assert_fallback_buffer!(buffer, 3 * gst::SECOND); + + // Produce a fifth frame but only from the fallback source + // This should be output now + push_fallback_buffer(&pipeline, 4 * gst::SECOND); + set_time(&pipeline, 4 * gst::SECOND + 10 * gst::MSECOND); + let buffer = pull_buffer(&pipeline); + assert_fallback_buffer!(buffer, 4 * gst::SECOND); + + // Wait for EOS to arrive at appsink + push_eos(&pipeline); + push_fallback_eos(&pipeline); + wait_eos(&pipeline); + + stop_pipeline(pipeline); +} + +#[test] +fn test_long_drop_and_recover_live() { + test_long_drop_and_recover(true); +} + +#[test] +fn test_long_drop_and_recover_not_live() { + test_long_drop_and_recover(false); +} + +fn test_long_drop_and_recover(live: bool) { + let pipeline = setup_pipeline(Some(live)); + + // Produce the first frame + push_buffer(&pipeline, 0.into()); + push_fallback_buffer(&pipeline, 0.into()); + set_time(&pipeline, 0.into()); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 0.into()); + + // Produce a second frame but only from the fallback source + push_fallback_buffer(&pipeline, gst::SECOND); + set_time(&pipeline, gst::SECOND + 10 * gst::MSECOND); + + // Produce a third frame but only from the fallback source + push_fallback_buffer(&pipeline, 2 * gst::SECOND); + set_time(&pipeline, 2 * gst::SECOND + 10 * gst::MSECOND); + + // Produce a fourth frame but only from the fallback source + // This should be output now + push_fallback_buffer(&pipeline, 3 * gst::SECOND); + set_time(&pipeline, 3 * gst::SECOND + 10 * gst::MSECOND); + let buffer = pull_buffer(&pipeline); + assert_fallback_buffer!(buffer, 3 * gst::SECOND); + + // Produce a fifth frame but only from the fallback source + // This should be output now + push_fallback_buffer(&pipeline, 4 * gst::SECOND); + set_time(&pipeline, 4 * gst::SECOND + 10 * gst::MSECOND); + let buffer = pull_buffer(&pipeline); + assert_fallback_buffer!(buffer, 4 * gst::SECOND); + + // Produce a sixth frame from the normal source + push_buffer(&pipeline, 5 * gst::SECOND); + push_fallback_buffer(&pipeline, 5 * gst::SECOND); + set_time(&pipeline, 5 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 5 * gst::SECOND); + + // Produce a seventh frame from the normal source but no fallback. + // This should still be output immediately + push_buffer(&pipeline, 6 * gst::SECOND); + set_time(&pipeline, 6 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 6 * gst::SECOND); + + // Produce a eight frame from the normal source + push_buffer(&pipeline, 7 * gst::SECOND); + push_fallback_buffer(&pipeline, 7 * gst::SECOND); + set_time(&pipeline, 7 * gst::SECOND); + let buffer = pull_buffer(&pipeline); + assert_buffer!(buffer, 7 * gst::SECOND); + + // Wait for EOS to arrive at appsink + push_eos(&pipeline); + push_fallback_eos(&pipeline); + wait_eos(&pipeline); + + stop_pipeline(pipeline); +} + +struct Pipeline { + pipeline: gst::Pipeline, + clock_join_handle: Option>, +} + +impl std::ops::Deref for Pipeline { + type Target = gst::Pipeline; + + fn deref(&self) -> &gst::Pipeline { + &self.pipeline + } +} + +fn setup_pipeline(with_live_fallback: Option) -> Pipeline { + init(); + + gst_debug!(TEST_CAT, "Setting up pipeline"); + + let clock = gst_check::TestClock::new(); + clock.set_time(0.into()); + let pipeline = gst::Pipeline::new(None); + + // Running time 0 in our pipeline is going to be clock time 1s. All + // clock ids before 1s are used for signalling to our clock advancing + // thread. + pipeline.use_clock(Some(&clock)); + pipeline.set_base_time(gst::SECOND); + pipeline.set_start_time(gst::CLOCK_TIME_NONE); + + let src = gst::ElementFactory::make("appsrc", Some("src")) + .unwrap() + .downcast::() + .unwrap(); + src.set_property("is-live", &true).unwrap(); + src.set_property("format", &gst::Format::Time).unwrap(); + src.set_property("min-latency", &(10i64)).unwrap(); + src.set_property( + "caps", + &gst::Caps::builder("video/x-raw") + .field("format", &"ARGB") + .field("width", &320) + .field("height", &240) + .field("framerate", &gst::Fraction::new(1, 1)) + .build(), + ) + .unwrap(); + + let switch = gst::ElementFactory::make("fallbackswitch", Some("switch")).unwrap(); + switch.set_property("timeout", &(3 * gst::SECOND)).unwrap(); + + let sink = gst::ElementFactory::make("appsink", Some("sink")) + .unwrap() + .downcast::() + .unwrap(); + sink.set_property("sync", &false).unwrap(); + + let queue = gst::ElementFactory::make("queue", None).unwrap(); + + pipeline + .add_many(&[src.upcast_ref(), &switch, &queue, &sink.upcast_ref()]) + .unwrap(); + src.link_pads(Some("src"), &switch, Some("sink")).unwrap(); + switch.link_pads(Some("src"), &queue, Some("sink")).unwrap(); + queue.link_pads(Some("src"), &sink, Some("sink")).unwrap(); + + if let Some(live) = with_live_fallback { + let fallback_src = gst::ElementFactory::make("appsrc", Some("fallback-src")) + .unwrap() + .downcast::() + .unwrap(); + fallback_src.set_property("is-live", &live).unwrap(); + fallback_src + .set_property("format", &gst::Format::Time) + .unwrap(); + fallback_src.set_property("min-latency", &(10i64)).unwrap(); + fallback_src + .set_property( + "caps", + &gst::Caps::builder("video/x-raw") + .field("format", &"ARGB") + .field("width", &160) + .field("height", &120) + .field("framerate", &gst::Fraction::new(1, 1)) + .build(), + ) + .unwrap(); + + pipeline.add(&fallback_src).unwrap(); + + fallback_src + .link_pads(Some("src"), &switch, Some("fallback_sink")) + .unwrap(); + } + + pipeline.set_state(gst::State::Playing).unwrap(); + + let clock_join_handle = std::thread::spawn(move || { + loop { + while let Some(clock_id) = clock.peek_next_pending_id().and_then(|clock_id| { + // Process if the clock ID is in the past or now + if clock.get_time() >= clock_id.get_time() { + Some(clock_id) + } else { + None + } + }) { + gst_debug!(TEST_CAT, "Processing clock ID at {}", clock_id.get_time()); + if let Some(clock_id) = clock.process_next_clock_id() { + gst_debug!(TEST_CAT, "Processed clock ID at {}", clock_id.get_time()); + if clock_id.get_time() == 0.into() { + gst_debug!(TEST_CAT, "Stopping clock thread"); + return; + } + } + } + + // Sleep for 5ms as long as we have pending clock IDs that are in the future + // at the top of the queue. We don't want to do a busy loop here. + while clock.peek_next_pending_id().iter().any(|clock_id| { + // Sleep if the clock ID is in the future + clock.get_time() < clock_id.get_time() + }) { + use std::{thread, time}; + + thread::sleep(time::Duration::from_millis(10)); + } + + // Otherwise if there are none (or they are ready now) wait until there are + // clock ids again + let _ = clock.wait_for_next_pending_id(); + } + }); + + Pipeline { + pipeline, + clock_join_handle: Some(clock_join_handle), + } +} + +fn push_buffer(pipeline: &Pipeline, time: gst::ClockTime) { + let src = pipeline + .get_by_name("src") + .unwrap() + .downcast::() + .unwrap(); + let mut buffer = gst::Buffer::with_size(320 * 240 * 4).unwrap(); + { + let buffer = buffer.get_mut().unwrap(); + buffer.set_pts(time); + } + src.push_buffer(buffer).unwrap(); +} + +fn push_fallback_buffer(pipeline: &Pipeline, time: gst::ClockTime) { + let src = pipeline + .get_by_name("fallback-src") + .unwrap() + .downcast::() + .unwrap(); + let mut buffer = gst::Buffer::with_size(160 * 120 * 4).unwrap(); + { + let buffer = buffer.get_mut().unwrap(); + buffer.set_pts(time); + } + src.push_buffer(buffer).unwrap(); +} + +fn push_eos(pipeline: &Pipeline) { + let src = pipeline + .get_by_name("src") + .unwrap() + .downcast::() + .unwrap(); + src.end_of_stream().unwrap(); +} + +fn push_fallback_eos(pipeline: &Pipeline) { + let src = pipeline + .get_by_name("fallback-src") + .unwrap() + .downcast::() + .unwrap(); + src.end_of_stream().unwrap(); +} + +fn pull_buffer(pipeline: &Pipeline) -> gst::Buffer { + let sink = pipeline + .get_by_name("sink") + .unwrap() + .downcast::() + .unwrap(); + let sample = sink.pull_sample().unwrap(); + sample.get_buffer_owned().unwrap() +} + +fn set_time(pipeline: &Pipeline, time: gst::ClockTime) { + let clock = pipeline + .get_clock() + .unwrap() + .downcast::() + .unwrap(); + + gst_debug!(TEST_CAT, "Setting time to {}", time); + clock.set_time(gst::SECOND + time); +} + +fn wait_eos(pipeline: &Pipeline) { + let sink = pipeline + .get_by_name("sink") + .unwrap() + .downcast::() + .unwrap(); + // FIXME: Ideally without a sleep + loop { + use std::{thread, time}; + + if sink.is_eos() { + gst_debug!(TEST_CAT, "Waited for EOS"); + break; + } + thread::sleep(time::Duration::from_millis(10)); + } +} + +fn stop_pipeline(mut pipeline: Pipeline) { + pipeline.set_state(gst::State::Null).unwrap(); + + let clock = pipeline + .get_clock() + .unwrap() + .downcast::() + .unwrap(); + + // Signal shutdown to the clock thread + let clock_id = clock.new_single_shot_id(0.into()).unwrap(); + let _ = clock_id.wait(); + + let switch = pipeline.get_by_name("switch").unwrap(); + let switch_weak = switch.downgrade(); + drop(switch); + let pipeline_weak = pipeline.downgrade(); + + pipeline.clock_join_handle.take().unwrap().join().unwrap(); + drop(pipeline); + + assert!(switch_weak.upgrade().is_none()); + assert!(pipeline_weak.upgrade().is_none()); +} diff --git a/utils/gst-plugin-togglerecord/Cargo.toml b/utils/gst-plugin-togglerecord/Cargo.toml new file mode 100644 index 000000000..ac25dba01 --- /dev/null +++ b/utils/gst-plugin-togglerecord/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "gst-plugin-togglerecord" +version = "0.6.0" +authors = ["Sebastian Dröge "] +license = "LGPL-2.1+" +description = "Toggle Record Plugin" +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" +edition = "2018" + +[dependencies] +glib = { git = "https://github.com/gtk-rs/glib" } +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gstreamer-audio = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gstreamer-video = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +gtk = { git = "https://github.com/gtk-rs/gtk", optional = true } +gio = { git = "https://github.com/gtk-rs/gio", optional = true } +parking_lot = "0.10" +lazy_static = "1.0" + +[dev-dependencies] +either = "1.0" + +[lib] +name = "gsttogglerecord" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[[example]] +name = "gtk-recording" +path = "examples/gtk_recording.rs" +required-features = ["gtk", "gio"] + +[build-dependencies] +gst-plugin-version-helper = { path="../../gst-plugin-version-helper" } diff --git a/utils/gst-plugin-togglerecord/LICENSE b/utils/gst-plugin-togglerecord/LICENSE new file mode 100644 index 000000000..4362b4915 --- /dev/null +++ b/utils/gst-plugin-togglerecord/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/utils/gst-plugin-togglerecord/build.rs b/utils/gst-plugin-togglerecord/build.rs new file mode 100644 index 000000000..0d1ddb61d --- /dev/null +++ b/utils/gst-plugin-togglerecord/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/utils/gst-plugin-togglerecord/examples/gtk_recording.rs b/utils/gst-plugin-togglerecord/examples/gtk_recording.rs new file mode 100644 index 000000000..8f0bc0bb1 --- /dev/null +++ b/utils/gst-plugin-togglerecord/examples/gtk_recording.rs @@ -0,0 +1,358 @@ +// Copyright (C) 2017 Sebastian Dröge +// +// 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. + +extern crate glib; +use glib::prelude::*; +extern crate gio; +use gio::prelude::*; + +extern crate gstreamer as gst; +use gst::prelude::*; + +extern crate gsttogglerecord; + +extern crate gtk; +use gtk::prelude::*; +use std::cell::RefCell; +use std::env; + +fn create_pipeline() -> ( + gst::Pipeline, + gst::Pad, + gst::Pad, + gst::Element, + gst::Element, + gtk::Widget, +) { + let pipeline = gst::Pipeline::new(None); + + let video_src = gst::ElementFactory::make("videotestsrc", None).unwrap(); + video_src.set_property("is-live", &true).unwrap(); + video_src.set_property_from_str("pattern", "ball"); + + let timeoverlay = gst::ElementFactory::make("timeoverlay", None).unwrap(); + timeoverlay + .set_property("font-desc", &"Monospace 20") + .unwrap(); + + let video_tee = gst::ElementFactory::make("tee", None).unwrap(); + let video_queue1 = gst::ElementFactory::make("queue", None).unwrap(); + let video_queue2 = gst::ElementFactory::make("queue", None).unwrap(); + + let video_convert1 = gst::ElementFactory::make("videoconvert", None).unwrap(); + let video_convert2 = gst::ElementFactory::make("videoconvert", None).unwrap(); + + let (video_sink, video_widget) = + if let Ok(gtkglsink) = gst::ElementFactory::make("gtkglsink", None) { + let glsinkbin = gst::ElementFactory::make("glsinkbin", None).unwrap(); + glsinkbin.set_property("sink", >kglsink).unwrap(); + + let widget = gtkglsink.get_property("widget").unwrap(); + (glsinkbin, widget.get::().unwrap().unwrap()) + } else { + let sink = gst::ElementFactory::make("gtksink", None).unwrap(); + let widget = sink.get_property("widget").unwrap(); + (sink, widget.get::().unwrap().unwrap()) + }; + + let video_enc = gst::ElementFactory::make("x264enc", None).unwrap(); + video_enc.set_property("rc-lookahead", &10i32).unwrap(); + video_enc.set_property("key-int-max", &30u32).unwrap(); + let video_parse = gst::ElementFactory::make("h264parse", None).unwrap(); + + let audio_src = gst::ElementFactory::make("audiotestsrc", None).unwrap(); + audio_src.set_property("is-live", &true).unwrap(); + audio_src.set_property_from_str("wave", "ticks"); + + let audio_tee = gst::ElementFactory::make("tee", None).unwrap(); + let audio_queue1 = gst::ElementFactory::make("queue", None).unwrap(); + let audio_queue2 = gst::ElementFactory::make("queue", None).unwrap(); + + let audio_convert1 = gst::ElementFactory::make("audioconvert", None).unwrap(); + let audio_convert2 = gst::ElementFactory::make("audioconvert", None).unwrap(); + + let audio_sink = gst::ElementFactory::make("autoaudiosink", None).unwrap(); + + let audio_enc = gst::ElementFactory::make("lamemp3enc", None).unwrap(); + let audio_parse = gst::ElementFactory::make("mpegaudioparse", None).unwrap(); + + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + + let mux_queue1 = gst::ElementFactory::make("queue", None).unwrap(); + let mux_queue2 = gst::ElementFactory::make("queue", None).unwrap(); + + let mux = gst::ElementFactory::make("mp4mux", None).unwrap(); + + let file_sink = gst::ElementFactory::make("filesink", None).unwrap(); + file_sink + .set_property("location", &"recording.mp4") + .unwrap(); + file_sink.set_property("async", &false).unwrap(); + file_sink.set_property("sync", &false).unwrap(); + + pipeline + .add_many(&[ + &video_src, + &timeoverlay, + &video_tee, + &video_queue1, + &video_queue2, + &video_convert1, + &video_convert2, + &video_sink, + &video_enc, + &video_parse, + &audio_src, + &audio_tee, + &audio_queue1, + &audio_queue2, + &audio_convert1, + &audio_convert2, + &audio_sink, + &audio_enc, + &audio_parse, + &togglerecord, + &mux_queue1, + &mux_queue2, + &mux, + &file_sink, + ]) + .unwrap(); + + gst::Element::link_many(&[ + &video_src, + &timeoverlay, + &video_tee, + &video_queue1, + &video_convert1, + &video_sink, + ]) + .unwrap(); + + gst::Element::link_many(&[ + &video_tee, + &video_queue2, + &video_convert2, + &video_enc, + &video_parse, + ]) + .unwrap(); + + video_parse + .link_pads(Some("src"), &togglerecord, Some("sink")) + .unwrap(); + togglerecord + .link_pads(Some("src"), &mux_queue1, Some("sink")) + .unwrap(); + mux_queue1 + .link_pads(Some("src"), &mux, Some("video_%u")) + .unwrap(); + + gst::Element::link_many(&[ + &audio_src, + &audio_tee, + &audio_queue1, + &audio_convert1, + &audio_sink, + ]) + .unwrap(); + + gst::Element::link_many(&[ + &audio_tee, + &audio_queue2, + &audio_convert2, + &audio_enc, + &audio_parse, + ]) + .unwrap(); + + audio_parse + .link_pads(Some("src"), &togglerecord, Some("sink_0")) + .unwrap(); + togglerecord + .link_pads(Some("src_0"), &mux_queue2, Some("sink")) + .unwrap(); + mux_queue2 + .link_pads(Some("src"), &mux, Some("audio_%u")) + .unwrap(); + + gst::Element::link_many(&[&mux, &file_sink]).unwrap(); + + ( + pipeline, + video_queue2.get_static_pad("sink").unwrap(), + audio_queue2.get_static_pad("sink").unwrap(), + togglerecord, + video_sink, + video_widget, + ) +} + +fn create_ui(app: >k::Application) { + let (pipeline, video_pad, audio_pad, togglerecord, video_sink, video_widget) = + create_pipeline(); + + let window = gtk::Window::new(gtk::WindowType::Toplevel); + window.set_default_size(320, 240); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + vbox.pack_start(&video_widget, true, true, 0); + + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 0); + let position_label = gtk::Label::new(Some("Position: 00:00:00")); + hbox.pack_start(&position_label, true, true, 5); + let recorded_duration_label = gtk::Label::new(Some("Recorded: 00:00:00")); + hbox.pack_start(&recorded_duration_label, true, true, 5); + vbox.pack_start(&hbox, false, false, 5); + + let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 0); + let record_button = gtk::Button::new_with_label("Record"); + hbox.pack_start(&record_button, true, true, 5); + let finish_button = gtk::Button::new_with_label("Finish"); + hbox.pack_start(&finish_button, true, true, 5); + vbox.pack_start(&hbox, false, false, 5); + + window.add(&vbox); + window.show_all(); + + app.add_window(&window); + + let video_sink_weak = video_sink.downgrade(); + let togglerecord_weak = togglerecord.downgrade(); + let timeout_id = gtk::timeout_add(100, move || { + let video_sink = match video_sink_weak.upgrade() { + Some(video_sink) => video_sink, + None => return glib::Continue(true), + }; + + let togglerecord = match togglerecord_weak.upgrade() { + Some(togglerecord) => togglerecord, + None => return glib::Continue(true), + }; + + let position = video_sink + .query_position::() + .unwrap_or_else(|| 0.into()); + position_label.set_text(&format!("Position: {:.1}", position)); + + let recording_duration = togglerecord + .get_static_pad("src") + .unwrap() + .query_position::() + .unwrap_or_else(|| 0.into()); + recorded_duration_label.set_text(&format!("Recorded: {:.1}", recording_duration)); + + glib::Continue(true) + }); + + let togglerecord_weak = togglerecord.downgrade(); + record_button.connect_clicked(move |button| { + let togglerecord = match togglerecord_weak.upgrade() { + Some(togglerecord) => togglerecord, + None => return, + }; + + let recording = !togglerecord + .get_property("record") + .unwrap() + .get_some::() + .unwrap(); + togglerecord.set_property("record", &recording).unwrap(); + + button.set_label(if recording { "Stop" } else { "Record" }); + }); + + let record_button_weak = record_button.downgrade(); + finish_button.connect_clicked(move |button| { + let record_button = match record_button_weak.upgrade() { + Some(record_button) => record_button, + None => return, + }; + + record_button.set_sensitive(false); + button.set_sensitive(false); + + video_pad.send_event(gst::Event::new_eos().build()); + audio_pad.send_event(gst::Event::new_eos().build()); + }); + + let app_weak = app.downgrade(); + window.connect_delete_event(move |_, _| { + let app = match app_weak.upgrade() { + Some(app) => app, + None => return Inhibit(false), + }; + + app.quit(); + Inhibit(false) + }); + + let bus = pipeline.get_bus().unwrap(); + let app_weak = app.downgrade(); + bus.add_watch_local(move |_, msg| { + use gst::MessageView; + + let app = match app_weak.upgrade() { + Some(app) => app, + None => return glib::Continue(false), + }; + + match msg.view() { + MessageView::Eos(..) => app.quit(), + MessageView::Error(err) => { + println!( + "Error from {:?}: {} ({:?})", + msg.get_src().map(|s| s.get_path_string()), + err.get_error(), + err.get_debug() + ); + app.quit(); + } + _ => (), + }; + + glib::Continue(true) + }) + .expect("Failed to add bus watch"); + + pipeline.set_state(gst::State::Playing).unwrap(); + + // Pipeline reference is owned by the closure below, so will be + // destroyed once the app is destroyed + let timeout_id = RefCell::new(Some(timeout_id)); + app.connect_shutdown(move |_| { + pipeline.set_state(gst::State::Null).unwrap(); + + bus.remove_watch().unwrap(); + + if let Some(timeout_id) = timeout_id.borrow_mut().take() { + glib::source_remove(timeout_id); + } + }); +} + +fn main() { + gst::init().unwrap(); + gtk::init().unwrap(); + + gsttogglerecord::plugin_register_static().expect("Failed to register togglerecord plugin"); + + let app = gtk::Application::new(None, gio::ApplicationFlags::FLAGS_NONE).unwrap(); + + app.connect_activate(create_ui); + let args = env::args().collect::>(); + app.run(&args); +} diff --git a/utils/gst-plugin-togglerecord/src/lib.rs b/utils/gst-plugin-togglerecord/src/lib.rs new file mode 100644 index 000000000..9f2154662 --- /dev/null +++ b/utils/gst-plugin-togglerecord/src/lib.rs @@ -0,0 +1,48 @@ +// Copyright (C) 2017 Sebastian Dröge +// +// 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. + +#![crate_type = "cdylib"] + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gstreamer as gst; +extern crate gstreamer_audio as gst_audio; +extern crate gstreamer_video as gst_video; + +#[macro_use] +extern crate lazy_static; + +extern crate parking_lot; + +mod togglerecord; + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + togglerecord::register(plugin) +} + +gst_plugin_define!( + togglerecord, + 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") +); diff --git a/utils/gst-plugin-togglerecord/src/togglerecord.rs b/utils/gst-plugin-togglerecord/src/togglerecord.rs new file mode 100644 index 000000000..07872b73d --- /dev/null +++ b/utils/gst-plugin-togglerecord/src/togglerecord.rs @@ -0,0 +1,1749 @@ +// Copyright (C) 2017 Sebastian Dröge +// +// 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 glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::prelude::*; +use gst::subclass::prelude::*; +use gst_audio; +use gst_video; + +use parking_lot::{Condvar, Mutex}; +use std::cmp; +use std::collections::HashMap; +use std::f64; +use std::iter; +use std::sync::Arc; + +const DEFAULT_RECORD: bool = false; + +#[derive(Debug, Clone, Copy)] +struct Settings { + record: bool, +} + +impl Default for Settings { + fn default() -> Self { + Settings { + record: DEFAULT_RECORD, + } + } +} + +static PROPERTIES: [subclass::Property; 2] = [ + subclass::Property("record", |name| { + glib::ParamSpec::boolean( + name, + "Record", + "Enable/disable recording", + DEFAULT_RECORD, + glib::ParamFlags::READWRITE, + ) + }), + subclass::Property("recording", |name| { + glib::ParamSpec::boolean( + name, + "Recording", + "Whether recording is currently taking place", + DEFAULT_RECORD, + glib::ParamFlags::READABLE, + ) + }), +]; + +#[derive(Clone)] +struct Stream { + sinkpad: gst::Pad, + srcpad: gst::Pad, + state: Arc>, +} + +impl PartialEq for Stream { + fn eq(&self, other: &Self) -> bool { + self.sinkpad == other.sinkpad && self.srcpad == other.srcpad + } +} + +impl Eq for Stream {} + +impl Stream { + fn new(sinkpad: gst::Pad, srcpad: gst::Pad) -> Self { + Self { + sinkpad, + srcpad, + state: Arc::new(Mutex::new(StreamState::default())), + } + } +} + +struct StreamState { + in_segment: gst::FormattedSegment, + out_segment: gst::FormattedSegment, + segment_seqnum: gst::Seqnum, + current_running_time: gst::ClockTime, + eos: bool, + flushing: bool, + segment_pending: bool, + pending_events: Vec, + audio_info: Option, + video_info: Option, +} + +impl Default for StreamState { + fn default() -> Self { + Self { + in_segment: gst::FormattedSegment::new(), + out_segment: gst::FormattedSegment::new(), + segment_seqnum: gst::Seqnum::next(), + current_running_time: gst::CLOCK_TIME_NONE, + eos: false, + flushing: false, + segment_pending: false, + pending_events: Vec::new(), + audio_info: None, + video_info: None, + } + } +} + +// Recording behaviour: +// +// Secondary streams are *always* behind main stream +// Main stream EOS stops recording (-> Stopping), makes secondary streams go EOS +// +// Recording: Passing through all data +// Stopping: Main stream remembering current last_recording_stop, waiting for all +// other streams to reach this position +// Stopped: Dropping all data +// Starting: Main stream waiting until next keyframe and setting last_recording_start, waiting +// for all other streams to reach this position +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RecordingState { + Recording, + Stopping, + Stopped, + Starting, +} + +#[derive(Debug)] +struct State { + recording_state: RecordingState, + last_recording_start: gst::ClockTime, + last_recording_stop: gst::ClockTime, + // Accumulated duration of previous recording segments, + // updated whenever going to Stopped + recording_duration: gst::ClockTime, + // Updated whenever going to Recording + running_time_offset: gst::ClockTime, +} + +impl Default for State { + fn default() -> Self { + Self { + recording_state: RecordingState::Stopped, + last_recording_start: gst::CLOCK_TIME_NONE, + last_recording_stop: gst::CLOCK_TIME_NONE, + recording_duration: 0.into(), + running_time_offset: gst::CLOCK_TIME_NONE, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum HandleResult { + Pass(T), + Drop, + Eos, + Flushing, +} + +trait HandleData: Sized { + fn get_pts(&self) -> gst::ClockTime; + fn get_dts(&self) -> gst::ClockTime; + fn get_dts_or_pts(&self) -> gst::ClockTime { + let dts = self.get_dts(); + if dts.is_some() { + dts + } else { + self.get_pts() + } + } + fn get_duration(&self, state: &StreamState) -> gst::ClockTime; + fn is_keyframe(&self) -> bool; + fn can_clip(&self, state: &StreamState) -> bool; + fn clip( + self, + state: &StreamState, + segment: &gst::FormattedSegment, + ) -> Option; +} + +impl HandleData for (gst::ClockTime, gst::ClockTime) { + fn get_pts(&self) -> gst::ClockTime { + self.0 + } + + fn get_dts(&self) -> gst::ClockTime { + self.0 + } + + fn get_duration(&self, _state: &StreamState) -> gst::ClockTime { + self.1 + } + + fn is_keyframe(&self) -> bool { + true + } + + fn can_clip(&self, _state: &StreamState) -> bool { + true + } + + fn clip( + self, + _state: &StreamState, + segment: &gst::FormattedSegment, + ) -> Option { + let stop = if self.1.is_some() { + self.0 + self.1 + } else { + self.0 + }; + + segment + .clip(self.0, stop) + .map(|(start, stop)| (start, stop - start)) + } +} + +impl HandleData for gst::Buffer { + fn get_pts(&self) -> gst::ClockTime { + gst::BufferRef::get_pts(self) + } + + fn get_dts(&self) -> gst::ClockTime { + gst::BufferRef::get_dts(self) + } + + fn get_duration(&self, state: &StreamState) -> gst::ClockTime { + let duration = gst::BufferRef::get_duration(self); + + if duration.is_some() { + duration + } else if let Some(ref video_info) = state.video_info { + if video_info.fps() != 0.into() { + gst::SECOND + .mul_div_floor( + *video_info.fps().denom() as u64, + *video_info.fps().numer() as u64, + ) + .unwrap_or(gst::CLOCK_TIME_NONE) + } else { + gst::CLOCK_TIME_NONE + } + } else if let Some(ref audio_info) = state.audio_info { + if audio_info.bpf() == 0 || audio_info.rate() == 0 { + return gst::CLOCK_TIME_NONE; + } + + let size = self.get_size() as u64; + let num_samples = size / audio_info.bpf() as u64; + gst::SECOND + .mul_div_floor(num_samples, audio_info.rate() as u64) + .unwrap_or(gst::CLOCK_TIME_NONE) + } else { + gst::CLOCK_TIME_NONE + } + } + + fn is_keyframe(&self) -> bool { + !gst::BufferRef::get_flags(self).contains(gst::BufferFlags::DELTA_UNIT) + } + + fn can_clip(&self, state: &StreamState) -> bool { + // Only do actual clipping for raw audio/video + if let Some(ref audio_info) = state.audio_info { + if audio_info.format() == gst_audio::AudioFormat::Unknown + || audio_info.format() == gst_audio::AudioFormat::Encoded + || audio_info.rate() == 0 + || audio_info.bpf() == 0 + { + return false; + } + } else if let Some(ref video_info) = state.video_info { + if video_info.format() == gst_video::VideoFormat::Unknown + || video_info.format() == gst_video::VideoFormat::Encoded + || self.get_dts_or_pts() != self.get_pts() + { + return false; + } + } else { + return false; + } + + true + } + + fn clip( + mut self, + state: &StreamState, + segment: &gst::FormattedSegment, + ) -> Option { + // Only do actual clipping for raw audio/video + if !self.can_clip(state) { + return Some(self); + } + + let pts = HandleData::get_pts(&self); + let duration = HandleData::get_duration(&self, state); + let stop = if duration.is_some() { + pts + duration + } else { + pts + }; + + if let Some(ref audio_info) = state.audio_info { + gst_audio::audio_buffer_clip( + self, + segment.upcast_ref(), + audio_info.rate(), + audio_info.bpf(), + ) + } else if state.video_info.is_some() { + segment.clip(pts, stop).map(move |(start, stop)| { + { + let buffer = self.make_mut(); + buffer.set_pts(start); + buffer.set_dts(start); + buffer.set_duration(stop - start); + } + + self + }) + } else { + unreachable!(); + } + } +} + +struct ToggleRecord { + settings: Mutex, + state: Mutex, + main_stream: Stream, + // Always must have main_stream.state locked! + // If multiple stream states have to be locked, the + // main_stream always comes first + main_stream_cond: Condvar, + other_streams: Mutex<(Vec, u32)>, + pads: Mutex>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "togglerecord", + gst::DebugColorFlags::empty(), + Some("Toggle Record Element"), + ); +} + +impl ToggleRecord { + fn set_pad_functions(sinkpad: &gst::Pad, srcpad: &gst::Pad) { + sinkpad.set_chain_function(|pad, parent, buffer| { + ToggleRecord::catch_panic_pad_function( + parent, + || Err(gst::FlowError::Error), + |togglerecord, element| togglerecord.sink_chain(pad, element, buffer), + ) + }); + sinkpad.set_event_function(|pad, parent, event| { + ToggleRecord::catch_panic_pad_function( + parent, + || false, + |togglerecord, element| togglerecord.sink_event(pad, element, event), + ) + }); + sinkpad.set_query_function(|pad, parent, query| { + ToggleRecord::catch_panic_pad_function( + parent, + || false, + |togglerecord, element| togglerecord.sink_query(pad, element, query), + ) + }); + sinkpad.set_iterate_internal_links_function(|pad, parent| { + ToggleRecord::catch_panic_pad_function( + parent, + || gst::Iterator::from_vec(vec![]), + |togglerecord, element| togglerecord.iterate_internal_links(pad, element), + ) + }); + + srcpad.set_event_function(|pad, parent, event| { + ToggleRecord::catch_panic_pad_function( + parent, + || false, + |togglerecord, element| togglerecord.src_event(pad, element, event), + ) + }); + srcpad.set_query_function(|pad, parent, query| { + ToggleRecord::catch_panic_pad_function( + parent, + || false, + |togglerecord, element| togglerecord.src_query(pad, element, query), + ) + }); + srcpad.set_iterate_internal_links_function(|pad, parent| { + ToggleRecord::catch_panic_pad_function( + parent, + || gst::Iterator::from_vec(vec![]), + |togglerecord, element| togglerecord.iterate_internal_links(pad, element), + ) + }); + } + + fn handle_main_stream( + &self, + element: &gst::Element, + pad: &gst::Pad, + stream: &Stream, + data: T, + ) -> Result, gst::FlowError> { + let mut state = stream.state.lock(); + + let mut dts_or_pts = data.get_dts_or_pts(); + let duration = data.get_duration(&state); + + if !dts_or_pts.is_some() { + gst_element_error!( + element, + gst::StreamError::Format, + ["Buffer without DTS or PTS"] + ); + return Err(gst::FlowError::Error); + } + + let mut dts_or_pts_end = if duration.is_some() { + dts_or_pts + duration + } else { + dts_or_pts + }; + + let data = match data.clip(&state, &state.in_segment) { + None => { + gst_log!(CAT, obj: pad, "Dropping raw data outside segment"); + return Ok(HandleResult::Drop); + } + Some(data) => data, + }; + + // This will only do anything for non-raw data + dts_or_pts = cmp::max(state.in_segment.get_start(), dts_or_pts); + dts_or_pts_end = cmp::max(state.in_segment.get_start(), dts_or_pts_end); + if state.in_segment.get_stop().is_some() { + dts_or_pts = cmp::min(state.in_segment.get_stop(), dts_or_pts); + dts_or_pts_end = cmp::min(state.in_segment.get_stop(), dts_or_pts_end); + } + + let current_running_time = state.in_segment.to_running_time(dts_or_pts); + let current_running_time_end = state.in_segment.to_running_time(dts_or_pts_end); + state.current_running_time = cmp::max(current_running_time_end, state.current_running_time); + + // Wake up everybody, we advanced a bit + // Important: They will only be able to advance once we're done with this + // function or waiting for them to catch up below, otherwise they might + // get the wrong state + self.main_stream_cond.notify_all(); + + gst_log!( + CAT, + obj: pad, + "Main stream current running time {}-{} (position: {}-{})", + current_running_time, + current_running_time_end, + dts_or_pts, + dts_or_pts_end + ); + + let settings = *self.settings.lock(); + + // First check if we have to update our recording state + let mut rec_state = self.state.lock(); + let settings_changed = match rec_state.recording_state { + RecordingState::Recording if !settings.record => { + gst_debug!(CAT, obj: pad, "Stopping recording"); + rec_state.recording_state = RecordingState::Stopping; + true + } + RecordingState::Stopped if settings.record => { + gst_debug!(CAT, obj: pad, "Starting recording"); + rec_state.recording_state = RecordingState::Starting; + true + } + _ => false, + }; + + match rec_state.recording_state { + RecordingState::Recording => { + // Remember where we stopped last, in case of EOS + rec_state.last_recording_stop = current_running_time_end; + gst_log!(CAT, obj: pad, "Passing buffer (recording)"); + Ok(HandleResult::Pass(data)) + } + RecordingState::Stopping => { + if !data.is_keyframe() { + // Remember where we stopped last, in case of EOS + rec_state.last_recording_stop = current_running_time_end; + gst_log!(CAT, obj: pad, "Passing non-keyframe buffer (stopping)"); + + drop(rec_state); + drop(state); + if settings_changed { + gst_debug!(CAT, obj: pad, "Requesting a new keyframe"); + stream + .sinkpad + .push_event(gst_video::new_upstream_force_key_unit_event().build()); + } + + return Ok(HandleResult::Pass(data)); + } + + // Remember the time when we stopped: now, i.e. right before the current buffer! + rec_state.last_recording_stop = current_running_time; + gst_debug!(CAT, obj: pad, "Stopping at {}", current_running_time); + + // Then unlock and wait for all other streams to reach it or go EOS instead. + drop(rec_state); + + while !self.other_streams.lock().0.iter().all(|s| { + let s = s.state.lock(); + s.eos + || (s.current_running_time.is_some() + && s.current_running_time >= current_running_time_end) + }) { + gst_log!(CAT, obj: pad, "Waiting for other streams to stop"); + self.main_stream_cond.wait(&mut state); + } + + if state.flushing { + gst_debug!(CAT, obj: pad, "Flushing"); + return Ok(HandleResult::Flushing); + } + + let mut rec_state = self.state.lock(); + rec_state.recording_state = RecordingState::Stopped; + let advance_by = rec_state.last_recording_stop - rec_state.last_recording_start; + rec_state.recording_duration += advance_by; + rec_state.last_recording_start = gst::CLOCK_TIME_NONE; + rec_state.last_recording_stop = gst::CLOCK_TIME_NONE; + + gst_debug!( + CAT, + obj: pad, + "Stopped at {}, recording duration {}", + current_running_time, + rec_state.recording_duration + ); + + // Then become Stopped and drop this buffer. We always stop right before + // a keyframe + gst_log!(CAT, obj: pad, "Dropping buffer (stopped)"); + + drop(rec_state); + drop(state); + element.notify("recording"); + + Ok(HandleResult::Drop) + } + RecordingState::Stopped => { + gst_log!(CAT, obj: pad, "Dropping buffer (stopped)"); + Ok(HandleResult::Drop) + } + RecordingState::Starting => { + // If this is no keyframe, we can directly go out again here and drop the frame + if !data.is_keyframe() { + gst_log!(CAT, obj: pad, "Dropping non-keyframe buffer (starting)"); + + drop(rec_state); + drop(state); + if settings_changed { + gst_debug!(CAT, obj: pad, "Requesting a new keyframe"); + stream + .sinkpad + .push_event(gst_video::new_upstream_force_key_unit_event().build()); + } + + return Ok(HandleResult::Drop); + } + + // Remember the time when we started: now! + rec_state.last_recording_start = current_running_time; + rec_state.running_time_offset = current_running_time - rec_state.recording_duration; + gst_debug!(CAT, obj: pad, "Starting at {}", current_running_time); + + state.segment_pending = true; + for other_stream in &self.other_streams.lock().0 { + other_stream.state.lock().segment_pending = true; + } + + // Then unlock and wait for all other streams to reach + // it or go EOS instead + drop(rec_state); + + while !self.other_streams.lock().0.iter().all(|s| { + let s = s.state.lock(); + s.eos + || (s.current_running_time.is_some() + && s.current_running_time >= current_running_time_end) + }) { + gst_log!(CAT, obj: pad, "Waiting for other streams to start"); + self.main_stream_cond.wait(&mut state); + } + + if state.flushing { + gst_debug!(CAT, obj: pad, "Flushing"); + return Ok(HandleResult::Flushing); + } + + let mut rec_state = self.state.lock(); + rec_state.recording_state = RecordingState::Recording; + gst_debug!( + CAT, + obj: pad, + "Started at {}, recording duration {}", + current_running_time, + rec_state.recording_duration + ); + + gst_log!(CAT, obj: pad, "Passing buffer (recording)"); + + drop(rec_state); + drop(state); + element.notify("recording"); + + Ok(HandleResult::Pass(data)) + } + } + } + + fn handle_secondary_stream( + &self, + element: &gst::Element, + pad: &gst::Pad, + stream: &Stream, + data: T, + ) -> Result, gst::FlowError> { + // Calculate end pts & current running time and make sure we stay in the segment + let mut state = stream.state.lock(); + + let mut pts = data.get_pts(); + let duration = data.get_duration(&state); + + if pts.is_none() { + gst_element_error!(element, gst::StreamError::Format, ["Buffer without PTS"]); + return Err(gst::FlowError::Error); + } + + let dts = data.get_dts(); + if dts.is_some() && pts.is_some() && dts != pts { + gst_element_error!( + element, + gst::StreamError::Format, + ["DTS != PTS not supported for secondary streams"] + ); + return Err(gst::FlowError::Error); + } + + if !data.is_keyframe() { + gst_element_error!( + element, + gst::StreamError::Format, + ["Delta-units not supported for secondary streams"] + ); + return Err(gst::FlowError::Error); + } + + let mut pts_end = if duration.is_some() { + pts + duration + } else { + pts + }; + + let data = match data.clip(&state, &state.in_segment) { + None => { + gst_log!(CAT, obj: pad, "Dropping raw data outside segment"); + return Ok(HandleResult::Drop); + } + Some(data) => data, + }; + + // This will only do anything for non-raw data + pts = cmp::max(state.in_segment.get_start(), pts); + pts_end = cmp::max(state.in_segment.get_start(), pts_end); + if state.in_segment.get_stop().is_some() { + pts = cmp::min(state.in_segment.get_stop(), pts); + pts_end = cmp::min(state.in_segment.get_stop(), pts_end); + } + + let current_running_time = state.in_segment.to_running_time(pts); + let current_running_time_end = state.in_segment.to_running_time(pts_end); + state.current_running_time = cmp::max(current_running_time_end, state.current_running_time); + + gst_log!( + CAT, + obj: pad, + "Secondary stream current running time {}-{} (position: {}-{}", + current_running_time, + current_running_time_end, + pts, + pts_end + ); + + drop(state); + + let mut main_state = self.main_stream.state.lock(); + + // Wake up, in case the main stream is waiting for us to progress up to here. We progressed + // above but all notifying must happen while the main_stream state is locked as per above. + self.main_stream_cond.notify_all(); + + while (main_state.current_running_time == gst::CLOCK_TIME_NONE + || main_state.current_running_time < current_running_time_end) + && !main_state.eos + && !stream.state.lock().flushing + { + gst_log!( + CAT, + obj: pad, + "Waiting for reaching {} / EOS / flushing, main stream at {}", + current_running_time, + main_state.current_running_time + ); + + self.main_stream_cond.wait(&mut main_state); + } + + state = stream.state.lock(); + + if state.flushing { + gst_debug!(CAT, obj: pad, "Flushing"); + return Ok(HandleResult::Flushing); + } + + let rec_state = self.state.lock(); + + // If the main stream is EOS, we are also EOS unless we are + // before the final last recording stop running time + if main_state.eos { + // If we have no start or stop position (we never recorded) then we're EOS too now + if rec_state.last_recording_stop.is_none() || rec_state.last_recording_start.is_none() { + gst_debug!(CAT, obj: pad, "Main stream EOS and recording never started",); + return Ok(HandleResult::Eos); + } else if data.can_clip(&*state) + && current_running_time < rec_state.last_recording_start + && current_running_time_end > rec_state.last_recording_start + { + // Otherwise if we're before the recording start but the end of the buffer is after + // the start and we can clip, clip the buffer and pass it onwards. + gst_debug!( + CAT, + obj: pad, + "Main stream EOS and we're not EOS yet (overlapping recording start, {} < {} < {})", + current_running_time, + rec_state.last_recording_start, + current_running_time_end + ); + + let mut clip_start = state + .in_segment + .position_from_running_time(rec_state.last_recording_start); + if clip_start.is_none() { + clip_start = state.in_segment.get_start(); + } + let mut clip_stop = state + .in_segment + .position_from_running_time(rec_state.last_recording_stop); + if clip_stop.is_none() { + clip_stop = state.in_segment.get_stop(); + } + let mut segment = state.in_segment.clone(); + segment.set_start(clip_start); + segment.set_stop(clip_stop); + + gst_log!(CAT, obj: pad, "Clipping to segment {:?}", segment,); + + if let Some(data) = data.clip(&*state, &segment) { + return Ok(HandleResult::Pass(data)); + } else { + gst_warning!(CAT, obj: pad, "Complete buffer clipped!"); + return Ok(HandleResult::Drop); + } + } else if current_running_time < rec_state.last_recording_start { + // Otherwise if the buffer starts before the recording start, drop it. This + // means that we either can't clip, or that the end is also before the + // recording start + gst_debug!( + CAT, + obj: pad, + "Main stream EOS and we're not EOS yet (before recording start, {} < {})", + current_running_time, + rec_state.last_recording_start + ); + return Ok(HandleResult::Drop); + } else if data.can_clip(&*state) + && current_running_time < rec_state.last_recording_stop + && current_running_time_end > rec_state.last_recording_stop + { + // Similarly if the end is after the recording stop but the start is before and we + // can clip, clip the buffer and pass it through. + gst_debug!( + CAT, + obj: pad, + "Main stream EOS and we're not EOS yet (overlapping recording end, {} < {} < {})", + current_running_time, + rec_state.last_recording_stop, + current_running_time_end + ); + + let mut clip_start = state + .in_segment + .position_from_running_time(rec_state.last_recording_start); + if clip_start.is_none() { + clip_start = state.in_segment.get_start(); + } + let mut clip_stop = state + .in_segment + .position_from_running_time(rec_state.last_recording_stop); + if clip_stop.is_none() { + clip_stop = state.in_segment.get_stop(); + } + let mut segment = state.in_segment.clone(); + segment.set_start(clip_start); + segment.set_stop(clip_stop); + + gst_log!(CAT, obj: pad, "Clipping to segment {:?}", segment,); + + if let Some(data) = data.clip(&*state, &segment) { + return Ok(HandleResult::Pass(data)); + } else { + gst_warning!(CAT, obj: pad, "Complete buffer clipped!"); + return Ok(HandleResult::Eos); + } + } else if current_running_time_end > rec_state.last_recording_stop { + // Otherwise if the end of the buffer is after the recording stop, we're EOS + // now. This means that we either couldn't clip or that the start is also after + // the recording stop + gst_debug!( + CAT, + obj: pad, + "Main stream EOS and we're EOS too (after recording end, {} > {})", + current_running_time_end, + rec_state.last_recording_stop + ); + return Ok(HandleResult::Eos); + } else { + // In all other cases the buffer is fully between recording start and end and + // can be passed through as is + assert!(current_running_time >= rec_state.last_recording_start); + assert!(current_running_time_end <= rec_state.last_recording_stop); + + gst_debug!( + CAT, + obj: pad, + "Main stream EOS and we're not EOS yet (before recording end, {} <= {} <= {})", + rec_state.last_recording_start, + current_running_time, + rec_state.last_recording_stop + ); + return Ok(HandleResult::Pass(data)); + } + } + + // The end of our buffer is before the end of the previous buffer of the main stream + assert!(main_state.current_running_time >= current_running_time_end); + + match rec_state.recording_state { + RecordingState::Recording => { + // We're properly started, must have a start position and + // be actually after that start position + assert!(rec_state.last_recording_start.is_some()); + assert!(current_running_time >= rec_state.last_recording_start); + gst_log!(CAT, obj: pad, "Passing buffer (recording)"); + Ok(HandleResult::Pass(data)) + } + RecordingState::Stopping => { + // If we have no start position yet, the main stream is waiting for a key-frame + if rec_state.last_recording_stop.is_none() { + gst_log!( + CAT, + obj: pad, + "Passing buffer (stopping: waiting for keyframe)", + ); + Ok(HandleResult::Pass(data)) + } else if current_running_time_end <= rec_state.last_recording_stop { + gst_log!( + CAT, + obj: pad, + "Passing buffer (stopping: {} <= {})", + current_running_time_end, + rec_state.last_recording_stop + ); + Ok(HandleResult::Pass(data)) + } else if data.can_clip(&*state) + && current_running_time < rec_state.last_recording_stop + && current_running_time_end > rec_state.last_recording_stop + { + gst_log!( + CAT, + obj: pad, + "Passing buffer (stopping: {} < {} < {})", + current_running_time, + rec_state.last_recording_stop, + current_running_time_end, + ); + + let mut clip_stop = state + .in_segment + .position_from_running_time(rec_state.last_recording_stop); + if clip_stop.is_none() { + clip_stop = state.in_segment.get_stop(); + } + let mut segment = state.in_segment.clone(); + segment.set_stop(clip_stop); + + gst_log!(CAT, obj: pad, "Clipping to segment {:?}", segment,); + + if let Some(data) = data.clip(&*state, &segment) { + Ok(HandleResult::Pass(data)) + } else { + gst_warning!(CAT, obj: pad, "Complete buffer clipped!"); + Ok(HandleResult::Drop) + } + } else { + gst_log!( + CAT, + obj: pad, + "Dropping buffer (stopping: {} > {})", + current_running_time_end, + rec_state.last_recording_stop + ); + Ok(HandleResult::Drop) + } + } + RecordingState::Stopped => { + // We're properly stopped + gst_log!(CAT, obj: pad, "Dropping buffer (stopped)"); + Ok(HandleResult::Drop) + } + RecordingState::Starting => { + // If we have no start position yet, the main stream is waiting for a key-frame + if rec_state.last_recording_start.is_none() { + gst_log!( + CAT, + obj: pad, + "Dropping buffer (starting: waiting for keyframe)", + ); + Ok(HandleResult::Drop) + } else if current_running_time >= rec_state.last_recording_start { + gst_log!( + CAT, + obj: pad, + "Passing buffer (starting: {} >= {})", + current_running_time, + rec_state.last_recording_start + ); + Ok(HandleResult::Pass(data)) + } else if data.can_clip(&*state) + && current_running_time < rec_state.last_recording_start + && current_running_time_end > rec_state.last_recording_start + { + gst_log!( + CAT, + obj: pad, + "Passing buffer (starting: {} < {} < {})", + current_running_time, + rec_state.last_recording_start, + current_running_time_end, + ); + + let mut clip_start = state + .in_segment + .position_from_running_time(rec_state.last_recording_start); + if clip_start.is_none() { + clip_start = state.in_segment.get_start(); + } + let mut segment = state.in_segment.clone(); + segment.set_start(clip_start); + + gst_log!(CAT, obj: pad, "Clipping to segment {:?}", segment,); + + if let Some(data) = data.clip(&*state, &segment) { + Ok(HandleResult::Pass(data)) + } else { + gst_warning!(CAT, obj: pad, "Complete buffer clipped!"); + Ok(HandleResult::Drop) + } + } else { + gst_log!( + CAT, + obj: pad, + "Dropping buffer (starting: {} < {})", + current_running_time, + rec_state.last_recording_start + ); + Ok(HandleResult::Drop) + } + } + } + } + + fn sink_chain( + &self, + pad: &gst::Pad, + element: &gst::Element, + buffer: gst::Buffer, + ) -> Result { + let stream = self.pads.lock().get(pad).cloned().ok_or_else(|| { + gst_element_error!( + element, + gst::CoreError::Pad, + ["Unknown pad {:?}", pad.get_name()] + ); + gst::FlowError::Error + })?; + + gst_log!(CAT, obj: pad, "Handling buffer {:?}", buffer); + + { + let state = stream.state.lock(); + if state.eos { + return Err(gst::FlowError::Eos); + } + } + + let handle_result = if stream != self.main_stream { + self.handle_secondary_stream(element, pad, &stream, buffer) + } else { + self.handle_main_stream(element, pad, &stream, buffer) + }?; + + let buffer = match handle_result { + HandleResult::Drop => { + return Ok(gst::FlowSuccess::Ok); + } + HandleResult::Flushing => { + return Err(gst::FlowError::Flushing); + } + HandleResult::Eos => { + stream.srcpad.push_event( + gst::Event::new_eos() + .seqnum(stream.state.lock().segment_seqnum) + .build(), + ); + return Err(gst::FlowError::Eos); + } + HandleResult::Pass(buffer) => { + // Pass through and actually push the buffer + buffer + } + }; + + let out_running_time = { + let mut state = stream.state.lock(); + let mut events = Vec::with_capacity(state.pending_events.len() + 1); + + if state.segment_pending { + let rec_state = self.state.lock(); + + // Adjust so that last_recording_start has running time of + // recording_duration + + state.out_segment = state.in_segment.clone(); + let offset = rec_state.running_time_offset.unwrap_or(0); + state + .out_segment + .offset_running_time(-(offset as i64)) + .expect("Adjusting record duration"); + events.push( + gst::Event::new_segment(&state.out_segment) + .seqnum(state.segment_seqnum) + .build(), + ); + state.segment_pending = false; + gst_debug!(CAT, obj: pad, "Pending Segment {:?}", &state.out_segment); + } + + if !state.pending_events.is_empty() { + gst_debug!(CAT, obj: pad, "Pushing pending events"); + } + + events.append(&mut state.pending_events); + + let out_running_time = state.out_segment.to_running_time(buffer.get_pts()); + + // Unlock before pushing + drop(state); + + for e in events.drain(..) { + stream.srcpad.push_event(e); + } + + out_running_time + }; + + gst_log!( + CAT, + obj: pad, + "Pushing buffer with running time {}: {:?}", + out_running_time, + buffer + ); + stream.srcpad.push(buffer) + } + + fn sink_event(&self, pad: &gst::Pad, element: &gst::Element, mut event: gst::Event) -> bool { + use gst::EventView; + + let stream = match self.pads.lock().get(pad) { + None => { + gst_element_error!( + element, + gst::CoreError::Pad, + ["Unknown pad {:?}", pad.get_name()] + ); + return false; + } + Some(stream) => stream.clone(), + }; + + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + let mut forward = true; + let mut send_pending = false; + + match event.view() { + EventView::FlushStart(..) => { + let _main_state = if stream != self.main_stream { + Some(self.main_stream.state.lock()) + } else { + None + }; + let mut state = stream.state.lock(); + + state.flushing = true; + self.main_stream_cond.notify_all(); + } + EventView::FlushStop(..) => { + let mut state = stream.state.lock(); + + state.eos = false; + state.flushing = false; + state.segment_pending = false; + state.current_running_time = gst::CLOCK_TIME_NONE; + } + EventView::Caps(c) => { + let mut state = stream.state.lock(); + let caps = c.get_caps(); + let s = caps.get_structure(0).unwrap(); + if s.get_name().starts_with("audio/") { + state.audio_info = gst_audio::AudioInfo::from_caps(caps).ok(); + gst_log!(CAT, obj: pad, "Got audio caps {:?}", state.audio_info); + state.video_info = None; + } else if s.get_name().starts_with("video/") { + state.audio_info = None; + state.video_info = gst_video::VideoInfo::from_caps(caps).ok(); + gst_log!(CAT, obj: pad, "Got video caps {:?}", state.video_info); + } else { + state.audio_info = None; + state.video_info = None; + } + } + EventView::Segment(e) => { + let mut state = stream.state.lock(); + + let segment = match e.get_segment().clone().downcast::() { + Err(segment) => { + gst_element_error!( + element, + gst::StreamError::Format, + [ + "Only Time segments supported, got {:?}", + segment.get_format(), + ] + ); + return false; + } + Ok(segment) => segment, + }; + + if (segment.get_rate() - 1.0).abs() > f64::EPSILON { + gst_element_error!( + element, + gst::StreamError::Format, + [ + "Only rate==1.0 segments supported, got {:?}", + segment.get_rate(), + ] + ); + return false; + } + + state.in_segment = segment; + state.segment_seqnum = event.get_seqnum(); + state.segment_pending = true; + state.current_running_time = gst::CLOCK_TIME_NONE; + + gst_debug!(CAT, obj: pad, "Got new Segment {:?}", state.in_segment); + + forward = false; + } + EventView::Gap(e) => { + gst_debug!(CAT, obj: pad, "Handling Gap event {:?}", event); + let (pts, duration) = e.get(); + let handle_result = if stream == self.main_stream { + self.handle_main_stream(element, pad, &stream, (pts, duration)) + } else { + self.handle_secondary_stream(element, pad, &stream, (pts, duration)) + }; + + forward = match handle_result { + Ok(HandleResult::Pass((new_pts, new_duration))) if new_pts.is_some() => { + if new_pts != pts || new_duration != duration { + event = gst::Event::new_gap(new_pts, new_duration).build(); + } + true + } + Ok(_) => false, + Err(_) => false, + }; + } + EventView::Eos(..) => { + let _main_state = if stream != self.main_stream { + Some(self.main_stream.state.lock()) + } else { + None + }; + let mut state = stream.state.lock(); + + state.eos = true; + self.main_stream_cond.notify_all(); + gst_debug!( + CAT, + obj: pad, + "Stream is EOS now, sending any pending events" + ); + + send_pending = true; + } + _ => (), + }; + + // If a serialized event and coming after Segment and a new Segment is pending, + // queue up and send at a later time (buffer/gap) after we sent the Segment + let type_ = event.get_type(); + if forward + && type_ != gst::EventType::Eos + && type_.is_serialized() + && type_.partial_cmp(&gst::EventType::Segment) == Some(cmp::Ordering::Greater) + { + let mut state = stream.state.lock(); + if state.segment_pending { + gst_log!(CAT, obj: pad, "Storing event for later pushing"); + state.pending_events.push(event); + return true; + } + } + + if send_pending { + let mut state = stream.state.lock(); + let mut events = Vec::with_capacity(state.pending_events.len() + 1); + + // Got not a single buffer on this stream before EOS, forward + // the input segment + if state.segment_pending { + events.push( + gst::Event::new_segment(&state.in_segment) + .seqnum(state.segment_seqnum) + .build(), + ); + } + events.append(&mut state.pending_events); + drop(state); + + for e in events.drain(..) { + stream.srcpad.push_event(e); + } + } + + if forward { + gst_log!(CAT, obj: pad, "Forwarding event {:?}", event); + stream.srcpad.push_event(event) + } else { + gst_log!(CAT, obj: pad, "Dropping event {:?}", event); + true + } + } + + fn sink_query( + &self, + pad: &gst::Pad, + element: &gst::Element, + query: &mut gst::QueryRef, + ) -> bool { + let stream = match self.pads.lock().get(pad) { + None => { + gst_element_error!( + element, + gst::CoreError::Pad, + ["Unknown pad {:?}", pad.get_name()] + ); + return false; + } + Some(stream) => stream.clone(), + }; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + + stream.srcpad.peer_query(query) + } + + fn src_event(&self, pad: &gst::Pad, element: &gst::Element, mut event: gst::Event) -> bool { + use gst::EventView; + + let stream = match self.pads.lock().get(pad) { + None => { + gst_element_error!( + element, + gst::CoreError::Pad, + ["Unknown pad {:?}", pad.get_name()] + ); + return false; + } + Some(stream) => stream.clone(), + }; + + gst_log!(CAT, obj: pad, "Handling event {:?}", event); + + let forward = match event.view() { + EventView::Seek(..) => false, + _ => true, + }; + + let rec_state = self.state.lock(); + let running_time_offset = rec_state.running_time_offset.unwrap_or(0) as i64; + let offset = event.get_running_time_offset(); + event + .make_mut() + .set_running_time_offset(offset + running_time_offset); + drop(rec_state); + + if forward { + gst_log!(CAT, obj: pad, "Forwarding event {:?}", event); + stream.sinkpad.push_event(event) + } else { + gst_log!(CAT, obj: pad, "Dropping event {:?}", event); + false + } + } + + fn src_query(&self, pad: &gst::Pad, element: &gst::Element, query: &mut gst::QueryRef) -> bool { + use gst::QueryView; + + let stream = match self.pads.lock().get(pad) { + None => { + gst_element_error!( + element, + gst::CoreError::Pad, + ["Unknown pad {:?}", pad.get_name()] + ); + return false; + } + Some(stream) => stream.clone(), + }; + + gst_log!(CAT, obj: pad, "Handling query {:?}", query); + match query.view_mut() { + QueryView::Scheduling(ref mut q) => { + let mut new_query = gst::Query::new_scheduling(); + let res = stream.sinkpad.peer_query(&mut new_query); + if !res { + return res; + } + + gst_log!(CAT, obj: pad, "Downstream 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::>(), + ); + gst_log!(CAT, obj: pad, "Returning {:?}", q.get_mut_query()); + true + } + QueryView::Seeking(ref mut q) => { + // Seeking is not possible here + let format = q.get_format(); + q.set( + false, + gst::GenericFormattedValue::new(format, -1), + gst::GenericFormattedValue::new(format, -1), + ); + + gst_log!(CAT, obj: pad, "Returning {:?}", q.get_mut_query()); + true + } + // Position and duration is always the current recording position + QueryView::Position(ref mut q) => { + if q.get_format() == gst::Format::Time { + let state = stream.state.lock(); + let rec_state = self.state.lock(); + let mut recording_duration = rec_state.recording_duration; + if rec_state.recording_state == RecordingState::Recording + || rec_state.recording_state == RecordingState::Stopping + { + recording_duration += + state.current_running_time - rec_state.last_recording_start; + } + q.set(recording_duration); + true + } else { + false + } + } + QueryView::Duration(ref mut q) => { + if q.get_format() == gst::Format::Time { + let state = stream.state.lock(); + let rec_state = self.state.lock(); + let mut recording_duration = rec_state.recording_duration; + if rec_state.recording_state == RecordingState::Recording + || rec_state.recording_state == RecordingState::Stopping + { + recording_duration += + state.current_running_time - rec_state.last_recording_start; + } + q.set(recording_duration); + true + } else { + false + } + } + _ => { + gst_log!(CAT, obj: pad, "Forwarding query {:?}", query); + stream.sinkpad.peer_query(query) + } + } + } + + fn iterate_internal_links( + &self, + pad: &gst::Pad, + element: &gst::Element, + ) -> gst::Iterator { + let stream = match self.pads.lock().get(pad) { + None => { + gst_element_error!( + element, + gst::CoreError::Pad, + ["Unknown pad {:?}", pad.get_name()] + ); + return gst::Iterator::from_vec(vec![]); + } + Some(stream) => stream.clone(), + }; + + if pad == &stream.srcpad { + gst::Iterator::from_vec(vec![stream.sinkpad]) + } else { + gst::Iterator::from_vec(vec![stream.srcpad]) + } + } +} + +impl ObjectSubclass for ToggleRecord { + const NAME: &'static str = "RsToggleRecord"; + type ParentType = gst::Element; + type Instance = gst::subclass::ElementInstanceStruct; + type Class = subclass::simple::ClassStruct; + + glib_object_subclass!(); + + fn new_with_class(klass: &subclass::simple::ClassStruct) -> 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")); + + ToggleRecord::set_pad_functions(&sinkpad, &srcpad); + + let main_stream = Stream::new(sinkpad, srcpad); + + let mut pads = HashMap::new(); + pads.insert(main_stream.sinkpad.clone(), main_stream.clone()); + pads.insert(main_stream.srcpad.clone(), main_stream.clone()); + + Self { + settings: Mutex::new(Settings::default()), + state: Mutex::new(State::default()), + main_stream, + main_stream_cond: Condvar::new(), + other_streams: Mutex::new((Vec::new(), 0)), + pads: Mutex::new(pads), + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct) { + klass.install_properties(&PROPERTIES); + + klass.set_metadata( + "Toggle Record", + "Generic", + "Valve that ensures multiple streams start/end at the same time", + "Sebastian Dröge ", + ); + + 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); + + 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_%u", + gst::PadDirection::Src, + gst::PadPresence::Sometimes, + &caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + + let sink_pad_template = gst::PadTemplate::new( + "sink_%u", + gst::PadDirection::Sink, + gst::PadPresence::Request, + &caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + } +} + +impl ObjectImpl for ToggleRecord { + glib_object_impl!(); + + fn set_property(&self, obj: &glib::Object, id: usize, value: &glib::Value) { + let prop = &PROPERTIES[id]; + let element = obj.downcast_ref::().unwrap(); + + match *prop { + subclass::Property("record", ..) => { + let mut settings = self.settings.lock(); + let record = value.get_some().expect("type checked upstream"); + gst_debug!( + CAT, + obj: element, + "Setting record from {:?} to {:?}", + settings.record, + record + ); + settings.record = record; + } + _ => unimplemented!(), + } + } + + fn get_property(&self, _obj: &glib::Object, id: usize) -> Result { + let prop = &PROPERTIES[id]; + + match *prop { + subclass::Property("record", ..) => { + let settings = self.settings.lock(); + Ok(settings.record.to_value()) + } + subclass::Property("recording", ..) => { + let rec_state = self.state.lock(); + Ok((rec_state.recording_state == RecordingState::Recording).to_value()) + } + _ => unimplemented!(), + } + } + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + + let element = obj.downcast_ref::().unwrap(); + element.add_pad(&self.main_stream.sinkpad).unwrap(); + element.add_pad(&self.main_stream.srcpad).unwrap(); + } +} + +impl ElementImpl for ToggleRecord { + fn change_state( + &self, + element: &gst::Element, + transition: gst::StateChange, + ) -> Result { + gst_trace!(CAT, obj: element, "Changing state {:?}", transition); + + match transition { + gst::StateChange::ReadyToPaused => { + for s in self + .other_streams + .lock() + .0 + .iter() + .chain(iter::once(&self.main_stream)) + { + let mut state = s.state.lock(); + *state = StreamState::default(); + } + + let mut rec_state = self.state.lock(); + *rec_state = State::default(); + } + gst::StateChange::PausedToReady => { + for s in &self.other_streams.lock().0 { + let mut state = s.state.lock(); + state.flushing = true; + } + + let mut state = self.main_stream.state.lock(); + state.flushing = true; + self.main_stream_cond.notify_all(); + } + _ => (), + } + + let success = self.parent_change_state(element, transition)?; + + if transition == gst::StateChange::PausedToReady { + for s in self + .other_streams + .lock() + .0 + .iter() + .chain(iter::once(&self.main_stream)) + { + let mut state = s.state.lock(); + + state.pending_events.clear(); + } + + let mut rec_state = self.state.lock(); + *rec_state = State::default(); + drop(rec_state); + element.notify("recording"); + } + + Ok(success) + } + + fn request_new_pad( + &self, + element: &gst::Element, + _templ: &gst::PadTemplate, + _name: Option, + _caps: Option<&gst::Caps>, + ) -> Option { + let mut other_streams_guard = self.other_streams.lock(); + let (ref mut other_streams, ref mut pad_count) = *other_streams_guard; + let mut pads = self.pads.lock(); + + let id = *pad_count; + *pad_count += 1; + + let templ = element.get_pad_template("sink_%u").unwrap(); + let sinkpad = gst::Pad::new_from_template(&templ, Some(format!("sink_{}", id).as_str())); + + let templ = element.get_pad_template("src_%u").unwrap(); + let srcpad = gst::Pad::new_from_template(&templ, Some(format!("src_{}", id).as_str())); + + ToggleRecord::set_pad_functions(&sinkpad, &srcpad); + + sinkpad.set_active(true).unwrap(); + srcpad.set_active(true).unwrap(); + + let stream = Stream::new(sinkpad.clone(), srcpad.clone()); + + pads.insert(stream.sinkpad.clone(), stream.clone()); + pads.insert(stream.srcpad.clone(), stream.clone()); + + other_streams.push(stream); + + drop(pads); + drop(other_streams_guard); + + element.add_pad(&sinkpad).unwrap(); + element.add_pad(&srcpad).unwrap(); + + Some(sinkpad) + } + + fn release_pad(&self, element: &gst::Element, pad: &gst::Pad) { + let mut other_streams_guard = self.other_streams.lock(); + let (ref mut other_streams, _) = *other_streams_guard; + let mut pads = self.pads.lock(); + + let stream = match pads.get(pad) { + None => return, + Some(stream) => stream.clone(), + }; + + stream.srcpad.set_active(false).unwrap(); + stream.sinkpad.set_active(false).unwrap(); + + pads.remove(&stream.sinkpad).unwrap(); + pads.remove(&stream.srcpad).unwrap(); + + // TODO: Replace with Vec::remove_item() once stable + let pos = other_streams.iter().position(|x| *x == stream); + pos.map(|pos| other_streams.swap_remove(pos)); + + drop(pads); + drop(other_streams_guard); + + element.remove_pad(&stream.sinkpad).unwrap(); + element.remove_pad(&stream.srcpad).unwrap(); + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "togglerecord", + gst::Rank::None, + ToggleRecord::get_type(), + ) +} diff --git a/utils/gst-plugin-togglerecord/tests/tests.rs b/utils/gst-plugin-togglerecord/tests/tests.rs new file mode 100644 index 000000000..c0eaf6745 --- /dev/null +++ b/utils/gst-plugin-togglerecord/tests/tests.rs @@ -0,0 +1,1173 @@ +// Copyright (C) 2017 Sebastian Dröge +// +// 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. + +extern crate glib; +use glib::prelude::*; + +extern crate gstreamer as gst; +use gst::prelude::*; + +extern crate either; +use either::*; + +use std::sync::{mpsc, Mutex}; +use std::thread; + +extern crate gsttogglerecord; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gsttogglerecord::plugin_register_static().expect("gsttogglerecord tests"); + }); +} + +enum SendData { + Buffers(usize), + BuffersDelta(usize), + Gaps(usize), + Eos, +} + +#[allow(clippy::type_complexity)] +fn setup_sender_receiver( + pipeline: &gst::Pipeline, + togglerecord: &gst::Element, + pad: &str, + offset: gst::ClockTime, +) -> ( + mpsc::Sender, + mpsc::Receiver<()>, + mpsc::Receiver>, + thread::JoinHandle<()>, +) { + let fakesink = gst::ElementFactory::make("fakesink", None).unwrap(); + fakesink.set_property("async", &false).unwrap(); + pipeline.add(&fakesink).unwrap(); + + let main_stream = pad == "src"; + + let (srcpad, sinkpad) = if main_stream { + ( + togglerecord.get_static_pad("src").unwrap(), + togglerecord.get_static_pad("sink").unwrap(), + ) + } else { + let sinkpad = togglerecord.get_request_pad("sink_%u").unwrap(); + let srcpad = sinkpad.iterate_internal_links().next().unwrap().unwrap(); + (srcpad, sinkpad) + }; + + let fakesink_sinkpad = fakesink.get_static_pad("sink").unwrap(); + srcpad.link(&fakesink_sinkpad).unwrap(); + + let (sender_output, receiver_output) = mpsc::channel::>(); + let sender_output = Mutex::new(sender_output); + srcpad.add_probe( + gst::PadProbeType::BUFFER | gst::PadProbeType::EVENT_DOWNSTREAM, + move |_, ref probe_info| { + match probe_info.data { + Some(gst::PadProbeData::Buffer(ref buffer)) => { + sender_output + .lock() + .unwrap() + .send(Left(buffer.clone())) + .unwrap(); + } + Some(gst::PadProbeData::Event(ref event)) => { + sender_output + .lock() + .unwrap() + .send(Right(event.clone())) + .unwrap(); + } + _ => { + unreachable!(); + } + } + + gst::PadProbeReturn::Ok + }, + ); + + let (sender_input, receiver_input) = mpsc::channel::(); + let (sender_input_done, receiver_input_done) = mpsc::channel::<()>(); + let thread = thread::spawn(move || { + let mut i = 0; + let mut first = true; + while let Ok(send_data) = receiver_input.recv() { + if first { + assert!(sinkpad.send_event(gst::Event::new_stream_start("test").build())); + let caps = if main_stream { + gst::Caps::builder("video/x-raw") + .field("format", &"ARGB") + .field("width", &320i32) + .field("height", &240i32) + .field("framerate", &gst::Fraction::new(50, 1)) + .build() + } else { + gst::Caps::builder("audio/x-raw") + .field("format", &"U8") + .field("layout", &"interleaved") + .field("rate", &8000i32) + .field("channels", &1i32) + .build() + }; + assert!(sinkpad.send_event(gst::Event::new_caps(&caps).build())); + + let segment = gst::FormattedSegment::::new(); + assert!(sinkpad.send_event(gst::Event::new_segment(&segment).build())); + + let mut tags = gst::TagList::new(); + tags.get_mut() + .unwrap() + .add::(&"some title", gst::TagMergeMode::Append); + assert!(sinkpad.send_event(gst::Event::new_tag(tags).build())); + + first = false; + } + + let buffer = if main_stream { + gst::Buffer::with_size(320 * 240 * 4).unwrap() + } else { + gst::Buffer::with_size(160).unwrap() + }; + + match send_data { + SendData::Eos => { + break; + } + SendData::Buffers(n) => { + for _ in 0..n { + let mut buffer = buffer.clone(); + { + let buffer = buffer.make_mut(); + buffer.set_pts(offset + i * 20 * gst::MSECOND); + buffer.set_duration(20 * gst::MSECOND); + } + let _ = sinkpad.chain(buffer); + i += 1; + } + } + SendData::BuffersDelta(n) => { + for _ in 0..n { + let mut buffer = gst::Buffer::new(); + buffer + .get_mut() + .unwrap() + .set_pts(offset + i * 20 * gst::MSECOND); + buffer.get_mut().unwrap().set_duration(20 * gst::MSECOND); + buffer + .get_mut() + .unwrap() + .set_flags(gst::BufferFlags::DELTA_UNIT); + let _ = sinkpad.chain(buffer); + i += 1; + } + } + SendData::Gaps(n) => { + for _ in 0..n { + let event = + gst::Event::new_gap(offset + i * 20 * gst::MSECOND, 20 * gst::MSECOND) + .build(); + let _ = sinkpad.send_event(event); + i += 1; + } + } + } + + let _ = sender_input_done.send(()); + } + + let _ = sinkpad.send_event(gst::Event::new_eos().build()); + let _ = sender_input_done.send(()); + }); + + (sender_input, receiver_input_done, receiver_output, thread) +} + +fn recv_buffers( + receiver_output: &mpsc::Receiver>, + segment: &mut gst::FormattedSegment, + wait_buffers: usize, +) -> Vec<(gst::ClockTime, gst::ClockTime, gst::ClockTime)> { + let mut res = Vec::new(); + let mut n_buffers = 0; + while let Ok(val) = receiver_output.recv() { + match val { + Left(buffer) => { + res.push(( + segment.to_running_time(buffer.get_pts()), + buffer.get_pts(), + buffer.get_duration(), + )); + n_buffers += 1; + if wait_buffers > 0 && n_buffers == wait_buffers { + return res; + } + } + Right(event) => { + use gst::EventView; + + match event.view() { + EventView::Gap(ref e) => { + let (ts, duration) = e.get(); + + res.push((segment.to_running_time(ts), ts, duration)); + n_buffers += 1; + if wait_buffers > 0 && n_buffers == wait_buffers { + return res; + } + } + EventView::Eos(..) => { + return res; + } + EventView::Segment(ref e) => { + *segment = e.get_segment().clone().downcast().unwrap(); + } + _ => (), + } + } + } + } + + res +} + +#[test] +fn test_create() { + init(); + assert!(gst::ElementFactory::make("togglerecord", None).is_ok()); +} + +#[test] +fn test_create_pads() { + init(); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + + let sinkpad = togglerecord.get_request_pad("sink_%u").unwrap(); + let srcpad = sinkpad.iterate_internal_links().next().unwrap().unwrap(); + + assert_eq!(sinkpad.get_name(), "sink_0"); + assert_eq!(srcpad.get_name(), "src_0"); + + togglerecord.release_request_pad(&sinkpad); + assert!(sinkpad.get_parent().is_none()); + assert!(srcpad.get_parent().is_none()); +} + +#[test] +fn test_one_stream_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input, _, receiver_output, thread) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + drop(sender_input); + + let mut segment = gst::FormattedSegment::::new(); + let buffers = recv_buffers(&receiver_output, &mut segment, 0); + assert_eq!(buffers.len(), 10); + for (index, &(running_time, pts, duration)) in buffers.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + + thread.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_one_stream_gaps_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input, _, receiver_output, thread) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + sender_input.send(SendData::Buffers(5)).unwrap(); + sender_input.send(SendData::Gaps(5)).unwrap(); + drop(sender_input); + + let mut segment = gst::FormattedSegment::::new(); + let buffers = recv_buffers(&receiver_output, &mut segment, 0); + assert_eq!(buffers.len(), 10); + for (index, &(running_time, pts, duration)) in buffers.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + + thread.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_one_stream_close_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input, receiver_input_done, receiver_output, thread) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + sender_input.send(SendData::Buffers(10)).unwrap(); + receiver_input_done.recv().unwrap(); + togglerecord.set_property("record", &true).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + drop(sender_input); + + let mut segment = gst::FormattedSegment::::new(); + let buffers = recv_buffers(&receiver_output, &mut segment, 0); + assert_eq!(buffers.len(), 10); + for (index, &(running_time, pts, duration)) in buffers.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, (10 + index) * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + + thread.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_one_stream_open_close() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input, receiver_input_done, receiver_output, thread) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + receiver_input_done.recv().unwrap(); + togglerecord.set_property("record", &false).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + drop(sender_input); + + let mut segment = gst::FormattedSegment::::new(); + let buffers = recv_buffers(&receiver_output, &mut segment, 0); + assert_eq!(buffers.len(), 10); + for (index, &(running_time, pts, duration)) in buffers.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + + thread.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_one_stream_open_close_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input, receiver_input_done, receiver_output, thread) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + receiver_input_done.recv().unwrap(); + togglerecord.set_property("record", &false).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + receiver_input_done.recv().unwrap(); + togglerecord.set_property("record", &true).unwrap(); + sender_input.send(SendData::Buffers(10)).unwrap(); + drop(sender_input); + + let mut segment = gst::FormattedSegment::::new(); + let buffers = recv_buffers(&receiver_output, &mut segment, 0); + assert_eq!(buffers.len(), 20); + for (index, &(running_time, pts, duration)) in buffers.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + + thread.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + receiver_input_done_1.recv().unwrap(); + sender_input_1.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 10); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 10); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_open_shift() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 5 * gst::MSECOND); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + receiver_input_done_1.recv().unwrap(); + sender_input_1.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 10); + + // Second to last buffer should be clipped from second stream, last should be dropped + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, 5 * gst::MSECOND + index * 20 * gst::MSECOND); + assert_eq!(pts, 5 * gst::MSECOND + index * 20 * gst::MSECOND); + if index == 9 { + assert_eq!(duration, 15 * gst::MSECOND); + } else { + assert_eq!(duration, 20 * gst::MSECOND); + } + } + assert_eq!(buffers_2.len(), 10); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_open_shift_main() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 5 * gst::MSECOND); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(12)).unwrap(); + receiver_input_done_1.recv().unwrap(); + sender_input_1.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + // PTS 5 maps to running time 0 now + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, 5 * gst::MSECOND + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 10); + + // First and second last buffer should be clipped from second stream, + // last buffer should be dropped + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let index = index as u64; + if index == 0 { + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, 5 * gst::MSECOND + index * 20 * gst::MSECOND); + assert_eq!(duration, 15 * gst::MSECOND); + } else if index == 10 { + assert_eq!(running_time, index * 20 * gst::MSECOND - 5 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 5 * gst::MSECOND); + } else { + assert_eq!(running_time, index * 20 * gst::MSECOND - 5 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + } + assert_eq!(buffers_2.len(), 11); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_open_close() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + + // Sender 2 is waiting for sender 1 to continue, sender 1 is finished + receiver_input_done_1.recv().unwrap(); + + // Stop recording and push new buffers to sender 1, which will advance + // it and release the 11th buffer of sender 2 above + togglerecord.set_property("record", &false).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another 9 buffers to sender 2, both are the same position now + sender_input_2.send(SendData::Buffers(9)).unwrap(); + + // Wait until all 20 buffers of both senders are done + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send EOS and wait for it to be handled + sender_input_1.send(SendData::Eos).unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 10); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 10); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_close_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &false).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + + // Sender 2 is waiting for sender 1 to continue, sender 1 is finished + receiver_input_done_1.recv().unwrap(); + + // Start recording and push new buffers to sender 1, which will advance + // it and release the 11th buffer of sender 2 above + togglerecord.set_property("record", &true).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another 9 buffers to sender 2, both are the same position now + sender_input_2.send(SendData::Buffers(9)).unwrap(); + + // Wait until all 20 buffers of both senders are done + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send EOS and wait for it to be handled + sender_input_1.send(SendData::Eos).unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, (10 + index) * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 10); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, (10 + index) * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 10); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_open_close_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + + // Sender 2 is waiting for sender 1 to continue, sender 1 is finished + receiver_input_done_1.recv().unwrap(); + + // Stop recording and push new buffers to sender 1, which will advance + // it and release the 11th buffer of sender 2 above + togglerecord.set_property("record", &false).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another 9 buffers to sender 2, both are the same position now + sender_input_2.send(SendData::Buffers(9)).unwrap(); + + // Wait until all 20 buffers of both senders are done + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another buffer to sender 2, this will block until sender 1 advances + // but must not be dropped, although we're not recording (yet) + sender_input_2.send(SendData::Buffers(1)).unwrap(); + + // Start recording again and send another set of buffers to both senders + togglerecord.set_property("record", &true).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_1.recv().unwrap(); + // The single buffer above for sender 1 should be handled now + receiver_input_done_2.recv().unwrap(); + + // Send EOS and wait for it to be handled + sender_input_1.send(SendData::Eos).unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 20); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 20); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_open_close_open_gaps() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(3)).unwrap(); + sender_input_1.send(SendData::Gaps(3)).unwrap(); + sender_input_1.send(SendData::Buffers(4)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + + // Sender 2 is waiting for sender 1 to continue, sender 1 is finished + receiver_input_done_1.recv().unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_1.recv().unwrap(); + + // Stop recording and push new buffers to sender 1, which will advance + // it and release the 11th buffer of sender 2 above + togglerecord.set_property("record", &false).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another 4 gaps and 5 buffers to sender 2, both are the same position now + sender_input_2.send(SendData::Gaps(4)).unwrap(); + sender_input_2.send(SendData::Buffers(5)).unwrap(); + + // Wait until all 20 buffers of both senders are done + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another gap to sender 2, this will block until sender 1 advances + // but must not be dropped, although we're not recording (yet) + sender_input_2.send(SendData::Gaps(1)).unwrap(); + + // Start recording again and send another set of buffers to both senders + togglerecord.set_property("record", &true).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_1.recv().unwrap(); + // The single buffer above for sender 1 should be handled now + receiver_input_done_2.recv().unwrap(); + + // Send EOS and wait for it to be handled + sender_input_1.send(SendData::Eos).unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 20); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 20); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_two_stream_close_open_close_delta() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &false).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + + // Sender 2 is waiting for sender 1 to continue, sender 1 is finished + receiver_input_done_1.recv().unwrap(); + + // Start recording and push new buffers to sender 1. The first one is a delta frame, + // so will be dropped, and as such the next frame of sender 2 will also be dropped + // Sender 2 is empty now + togglerecord.set_property("record", &true).unwrap(); + sender_input_1.send(SendData::BuffersDelta(1)).unwrap(); + sender_input_1.send(SendData::Buffers(9)).unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another 9 buffers to sender 2, both are the same position now + sender_input_2.send(SendData::Buffers(9)).unwrap(); + + // Wait until all 20 buffers of both senders are done + receiver_input_done_1.recv().unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another buffer to sender 2, this will block until sender 1 advances + // but must not be dropped, and we're still recording + sender_input_2.send(SendData::Buffers(1)).unwrap(); + + // Stop recording again and send another set of buffers to both senders + // The first one is a delta frame, so we only actually stop recording + // after recording another frame + togglerecord.set_property("record", &false).unwrap(); + sender_input_1.send(SendData::BuffersDelta(1)).unwrap(); + sender_input_1.send(SendData::Buffers(9)).unwrap(); + sender_input_2.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_1.recv().unwrap(); + // The single buffer above for sender 1 should be handled now + receiver_input_done_2.recv().unwrap(); + + // Send EOS and wait for it to be handled + sender_input_1.send(SendData::Eos).unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, (11 + index) * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 10); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, (11 + index) * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 10); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} + +#[test] +fn test_three_stream_open_close_open() { + init(); + + let pipeline = gst::Pipeline::new(None); + let togglerecord = gst::ElementFactory::make("togglerecord", None).unwrap(); + pipeline.add(&togglerecord).unwrap(); + + let (sender_input_1, receiver_input_done_1, receiver_output_1, thread_1) = + setup_sender_receiver(&pipeline, &togglerecord, "src", 0.into()); + let (sender_input_2, receiver_input_done_2, receiver_output_2, thread_2) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + let (sender_input_3, receiver_input_done_3, receiver_output_3, thread_3) = + setup_sender_receiver(&pipeline, &togglerecord, "src_%u", 0.into()); + + pipeline.set_state(gst::State::Playing).unwrap(); + + togglerecord.set_property("record", &true).unwrap(); + + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(11)).unwrap(); + sender_input_3.send(SendData::Buffers(10)).unwrap(); + + // Sender 2 is waiting for sender 1 to continue, sender 1/3 are finished + receiver_input_done_1.recv().unwrap(); + receiver_input_done_3.recv().unwrap(); + + // Stop recording and push new buffers to sender 1, which will advance + // it and release the 11th buffer of sender 2 above + togglerecord.set_property("record", &false).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + receiver_input_done_2.recv().unwrap(); + + // Send another 9 buffers to sender 2, 1/2 are at the same position now + sender_input_2.send(SendData::Buffers(9)).unwrap(); + + // Send the remaining 10 buffers to sender 3, all are at the same position now + sender_input_3.send(SendData::Buffers(10)).unwrap(); + + // Wait until all 20 buffers of all senders are done + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_3.recv().unwrap(); + + // Send another buffer to sender 2, this will block until sender 1 advances + // but must not be dropped, although we're not recording (yet) + sender_input_2.send(SendData::Buffers(1)).unwrap(); + + // Start recording again and send another set of buffers to both senders + togglerecord.set_property("record", &true).unwrap(); + sender_input_1.send(SendData::Buffers(10)).unwrap(); + sender_input_2.send(SendData::Buffers(10)).unwrap(); + sender_input_3.send(SendData::Buffers(5)).unwrap(); + receiver_input_done_1.recv().unwrap(); + // The single buffer above for sender 1 should be handled now + receiver_input_done_2.recv().unwrap(); + receiver_input_done_3.recv().unwrap(); + + sender_input_3.send(SendData::Buffers(5)).unwrap(); + receiver_input_done_3.recv().unwrap(); + + // Send EOS and wait for it to be handled + sender_input_1.send(SendData::Eos).unwrap(); + sender_input_2.send(SendData::Eos).unwrap(); + sender_input_3.send(SendData::Eos).unwrap(); + receiver_input_done_1.recv().unwrap(); + receiver_input_done_2.recv().unwrap(); + receiver_input_done_3.recv().unwrap(); + + let mut segment_1 = gst::FormattedSegment::::new(); + let buffers_1 = recv_buffers(&receiver_output_1, &mut segment_1, 0); + for (index, &(running_time, pts, duration)) in buffers_1.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_1.len(), 20); + + // Last buffer should be dropped from second stream + let mut segment_2 = gst::FormattedSegment::::new(); + let buffers_2 = recv_buffers(&receiver_output_2, &mut segment_2, 0); + for (index, &(running_time, pts, duration)) in buffers_2.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_2.len(), 20); + + let mut segment_3 = gst::FormattedSegment::::new(); + let buffers_3 = recv_buffers(&receiver_output_3, &mut segment_3, 0); + for (index, &(running_time, pts, duration)) in buffers_3.iter().enumerate() { + let pts_off = if index >= 10 { + 10 * 20 * gst::MSECOND + } else { + 0.into() + }; + + let index = index as u64; + assert_eq!(running_time, index * 20 * gst::MSECOND); + assert_eq!(pts, pts_off + index * 20 * gst::MSECOND); + assert_eq!(duration, 20 * gst::MSECOND); + } + assert_eq!(buffers_3.len(), 20); + + thread_1.join().unwrap(); + thread_2.join().unwrap(); + thread_3.join().unwrap(); + + pipeline.set_state(gst::State::Null).unwrap(); +} -- cgit v1.2.3