examples: Add an example for subclassing and providing Rust API on the subclass

This commit is contained in:
Sebastian Dröge 2019-10-30 16:07:23 +01:00
parent 452ea45c1b
commit ccd01b93bf
2 changed files with 418 additions and 0 deletions

View file

@ -10,6 +10,7 @@ gstreamer = { path = "../gstreamer" }
gstreamer-gl = { path = "../gstreamer-gl", optional = true } gstreamer-gl = { path = "../gstreamer-gl", optional = true }
gstreamer-app = { path = "../gstreamer-app" } gstreamer-app = { path = "../gstreamer-app" }
gstreamer-audio = { path = "../gstreamer-audio" } gstreamer-audio = { path = "../gstreamer-audio" }
gstreamer-base = { path = "../gstreamer-base" }
gstreamer-video = { path = "../gstreamer-video" } gstreamer-video = { path = "../gstreamer-video" }
gstreamer-pbutils = { path = "../gstreamer-pbutils" } gstreamer-pbutils = { path = "../gstreamer-pbutils" }
gstreamer-player = { path = "../gstreamer-player", optional = true } gstreamer-player = { path = "../gstreamer-player", optional = true }
@ -29,6 +30,7 @@ pango = { git = "https://github.com/gtk-rs/pango", optional = true }
pangocairo = { git = "https://github.com/gtk-rs/pangocairo", optional = true } pangocairo = { git = "https://github.com/gtk-rs/pangocairo", optional = true }
glutin = { version = "0.21", optional = true } glutin = { version = "0.21", optional = true }
winit = { version = "0.19", optional = true } winit = { version = "0.19", optional = true }
lazy_static = "1.0"
[build-dependencies] [build-dependencies]
gl_generator = { version = "0.14", optional = true } gl_generator = { version = "0.14", optional = true }
@ -144,3 +146,7 @@ name = "glupload"
required-features = ["gl"] required-features = ["gl"]
features = ["gl-egl", "gl-x11", "gl-wayland"] features = ["gl-egl", "gl-x11", "gl-wayland"]
edition = "2018" edition = "2018"
[[bin]]
name = "subclass"
edition = "2018"

View file

@ -0,0 +1,412 @@
// This example demonstrates subclassing, implementing an audio filter and providing some Rust API
// on it. It operates the following pipeline:
//
// {audiotestsrc} - {our filter} - {audioconvert} - {autoaudiosink}
//
// Our filter can only handle F32 mono and acts as a FIR filter. The filter impulse response /
// coefficients are provided via Rust API on the filter as a Vec<f32>.
#[macro_use]
extern crate glib;
#[macro_use]
extern crate gstreamer as gst;
use gst::prelude::*;
use std::error::Error as StdError;
extern crate failure;
use failure::Error;
#[macro_use]
extern crate failure_derive;
#[path = "../examples-common.rs"]
mod examples_common;
// Our custom FIR filter element is defined in this module
mod fir_filter {
use super::*;
use glib::subclass;
use glib::subclass::prelude::*;
use glib::translate::*;
use gst::subclass::prelude::*;
extern crate gstreamer_base as gst_base;
use gst_base::subclass::prelude::*;
extern crate gstreamer_audio as gst_audio;
extern crate byte_slice_cast;
use byte_slice_cast::*;
use lazy_static::lazy_static;
// The debug category we use below for our filter
lazy_static! {
pub static ref CAT: gst::DebugCategory = gst::DebugCategory::new(
"rsfirfilter",
gst::DebugColorFlags::empty(),
Some("Rust FIR Filter"),
);
}
// In the imp submodule we include the actual implementation
mod imp {
use super::*;
use std::collections::VecDeque;
use std::i32;
use std::sync::Mutex;
// This is the private data of our filter
pub struct FirFilter {
pub(super) coeffs: Mutex<Vec<f32>>,
history: Mutex<VecDeque<f32>>,
}
// This trait registers our type with the GObject object system and
// provides the entry points for creating a new instance and setting
// up the class data
impl ObjectSubclass for FirFilter {
const NAME: &'static str = "RsFirFilter";
type ParentType = gst_base::BaseTransform;
type Instance = gst::subclass::ElementInstanceStruct<Self>;
type Class = subclass::simple::ClassStruct<Self>;
// This macro provides some boilerplate
glib_object_subclass!();
// Called when a new instance is to be created. We need to return an instance
// of our struct here.
fn new() -> Self {
Self {
coeffs: Mutex::new(Vec::new()),
history: Mutex::new(VecDeque::new()),
}
}
// Called exactly once when registering the type. Used for
// setting up metadata for all instances, e.g. the name and
// classification and the pad templates with their caps.
//
// Actual instances can create pads based on those pad templates
// with a subset of the caps given here. In case of basetransform,
// a "src" and "sink" pad template are required here and the base class
// will automatically instantiate pads for them.
//
// Our element here can only handle F32 mono audio.
fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) {
// Set the element specific metadata. This information is what
// is visible from gst-inspect-1.0 and can also be programatically
// retrieved from the gst::Registry after initial registration
// without having to load the plugin in memory.
klass.set_metadata(
"FIR Filter",
"Filter/Effect/Audio",
"A FIR audio filter",
"Sebastian Dröge <sebastian@centricular.com>",
);
// Create and add pad templates for our sink and source pad. These
// are later used for actually creating the pads and beforehand
// already provide information to GStreamer about all possible
// pads that could exist for this type.
// On both of pads we can only handle F32 mono at any sample rate.
let caps = gst::Caps::new_simple(
"audio/x-raw",
&[
("format", &gst_audio::AUDIO_FORMAT_F32.to_str()),
("rate", &gst::IntRange::<i32>::new(1, i32::MAX)),
("channels", &1i32),
("layout", &"interleaved"),
],
);
// The src pad template must be named "src" for basetransform
// and specific a pad that is always there
let src_pad_template = gst::PadTemplate::new(
"src",
gst::PadDirection::Src,
gst::PadPresence::Always,
&caps,
)
.unwrap();
klass.add_pad_template(src_pad_template);
// The sink pad template must be named "sink" for basetransform
// and specific a pad that is always there
let sink_pad_template = gst::PadTemplate::new(
"sink",
gst::PadDirection::Sink,
gst::PadPresence::Always,
&caps,
)
.unwrap();
klass.add_pad_template(sink_pad_template);
// Configure basetransform so that we are always running in-place,
// don't passthrough on same caps and also never call transform_ip
// in passthrough mode (which does not matter for us here).
//
// The way how our processing is implemented, in-place transformation
// is simpler.
klass.configure(
gst_base::subclass::BaseTransformMode::AlwaysInPlace,
false,
false,
);
}
}
// Implementation of glib::Object virtual methods
impl ObjectImpl for FirFilter {
// This macro provides some boilerplate.
glib_object_impl!();
}
// Implementation of gst::Element virtual methods
impl ElementImpl for FirFilter {}
// Implementation of gst_base::BaseTransform virtual methods
impl BaseTransformImpl for FirFilter {
// Returns the size of one processing unit (i.e. a frame in our case) corresponding
// to the given caps. This is used for allocating a big enough output buffer and
// sanity checking the input buffer size, among other things.
fn get_unit_size(
&self,
_element: &gst_base::BaseTransform,
caps: &gst::Caps,
) -> Option<usize> {
gst_audio::AudioInfo::from_caps(caps).map(|info| info.bpf() as usize)
}
// Called when shutting down the element so we can release all stream-related state
// There's also start(), which is called whenever starting the element again
fn stop(&self, element: &gst_base::BaseTransform) -> Result<(), gst::ErrorMessage> {
// Drop state
self.history.lock().unwrap().clear();
gst_info!(CAT, obj: element, "Stopped");
Ok(())
}
// Does the actual transformation of the input buffer to the output buffer
fn transform_ip(
&self,
element: &gst_base::BaseTransform,
buf: &mut gst::BufferRef,
) -> Result<gst::FlowSuccess, gst::FlowError> {
// Get coefficients and return directly if we have none
let coeffs = self.coeffs.lock().unwrap();
if coeffs.is_empty() {
gst_trace!(CAT, obj: element, "No coefficients set -- passthrough");
return Ok(gst::FlowSuccess::Ok);
}
// Try mapping the input buffer as writable
let mut data = buf.map_writable().ok_or_else(|| {
gst_element_error!(
element,
gst::CoreError::Failed,
["Failed to map input buffer readable"]
);
gst::FlowError::Error
})?;
// And reinterprete it as a slice of f32
let samples = data.as_mut_slice_of::<f32>().map_err(|err| {
gst_element_error!(
element,
gst::CoreError::Failed,
["Failed to cast input buffer as f32 slice: {}", err]
);
gst::FlowError::Error
})?;
let mut history = self.history.lock().unwrap();
gst_trace!(
CAT,
obj: element,
"Transforming {} samples with filter of length {}",
samples.len(),
coeffs.len()
);
// Now calculate the output for each sample by doing convolution
for sample in samples.iter_mut() {
history.push_front(*sample);
history.truncate(coeffs.len());
let val = history
.iter()
.copied()
.chain(std::iter::repeat(0.0))
.zip(coeffs.iter())
.map(|(x, a)| x * a)
.sum();
*sample = val;
}
Ok(gst::FlowSuccess::Ok)
}
}
}
// This here defines the public interface of our element and implements
// the corresponding traits so that it behaves like any other gst::Element
glib_wrapper! {
pub struct FirFilter(
Object<
gst::subclass::ElementInstanceStruct<imp::FirFilter>,
subclass::simple::ClassStruct<imp::FirFilter>,
FirFilterClass
>
) @extends gst_base::BaseTransform, gst::Element, gst::Object;
match fn {
get_type => || imp::FirFilter::get_type().to_glib(),
}
}
// GStreamer elements must be Send+Sync, and ours is
unsafe impl Send for FirFilter {}
unsafe impl Sync for FirFilter {}
impl FirFilter {
// Creates a new instance of our filter with the given name
pub fn new(name: Option<&str>) -> FirFilter {
glib::Object::new(Self::static_type(), &[("name", &name)])
.expect("Failed to create fir filter")
.downcast()
.expect("Created fir filter is of wrong type")
}
// Sets the coefficients by getting access to the private
// struct and simply setting them
pub fn set_coeffs(&self, coeffs: Vec<f32>) {
let imp = imp::FirFilter::from_instance(self);
*imp.coeffs.lock().unwrap() = coeffs;
}
}
}
#[derive(Debug, Fail)]
#[fail(display = "Missing element {}", _0)]
struct MissingElement(&'static str);
#[derive(Debug, Fail)]
#[fail(
display = "Received error from {}: {} (debug: {:?})",
src, error, debug
)]
struct ErrorMessage {
src: String,
error: String,
debug: Option<String>,
#[cause]
cause: glib::Error,
}
fn create_pipeline() -> Result<gst::Pipeline, Error> {
gst::init()?;
// Create our pipeline with the custom element
let pipeline = gst::Pipeline::new(None);
let src =
gst::ElementFactory::make("audiotestsrc", None).ok_or(MissingElement("audiotestsrc"))?;
let filter = fir_filter::FirFilter::new(None);
let conv =
gst::ElementFactory::make("audioconvert", None).ok_or(MissingElement("audioconvert"))?;
let sink =
gst::ElementFactory::make("autoaudiosink", None).ok_or(MissingElement("autoaudiosink"))?;
pipeline.add_many(&[&src, filter.upcast_ref(), &conv, &sink])?;
src.link(&filter)?;
filter.link(&conv)?;
conv.link(&sink)?;
src.set_property_from_str("wave", "white-noise");
// Create a windowed sinc lowpass filter at 1/64 sample rate,
// i.e. 689Hz for 44.1kHz sample rate
let w = 2.0 * std::f32::consts::PI / 64.0;
let len = 9;
let mut kernel = (0..len)
.map(|i| {
let v = if i == (len - 1) / 2 {
w
} else {
let p = i as f32 - (len - 1) as f32 / 2.0;
(w * p).sin() / p
};
// Hamming window
let win =
0.54 - 0.46 * (2.0 * std::f32::consts::PI * i as f32 / (len as f32 - 1.0)).cos();
v * win
})
.collect::<Vec<f32>>();
// Normalize
let sum: f32 = kernel.iter().sum();
for v in kernel.iter_mut() {
*v /= sum;
}
filter.set_coeffs(kernel);
Ok(pipeline)
}
fn main_loop(pipeline: gst::Pipeline) -> Result<(), Error> {
pipeline.set_state(gst::State::Playing)?;
let bus = pipeline
.get_bus()
.expect("Pipeline without bus. Shouldn't happen!");
for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) {
use gst::MessageView;
match msg.view() {
MessageView::Eos(..) => break,
MessageView::Error(err) => {
pipeline.set_state(gst::State::Null)?;
return Err(ErrorMessage {
src: msg
.get_src()
.map(|s| String::from(s.get_path_string()))
.unwrap_or_else(|| String::from("None")),
error: err.get_error().description().into(),
debug: Some(err.get_debug().unwrap().to_string()),
cause: err.get_error(),
}
.into());
}
_ => (),
}
}
pipeline.set_state(gst::State::Null)?;
Ok(())
}
fn example_main() {
match create_pipeline().and_then(main_loop) {
Ok(r) => r,
Err(e) => eprintln!("Error! {}", e),
}
}
fn main() {
// tutorials_common::run is only required to set up the application environent on macOS
// (but not necessary in normal Cocoa applications where this is set up autmatically)
examples_common::run(example_main);
}