forked from mirrors/gstreamer-rs
examples: Add an example for subclassing and providing Rust API on the subclass
This commit is contained in:
parent
452ea45c1b
commit
ccd01b93bf
2 changed files with 418 additions and 0 deletions
|
@ -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"
|
||||||
|
|
412
examples/src/bin/subclass.rs
Normal file
412
examples/src/bin/subclass.rs
Normal 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);
|
||||||
|
}
|
Loading…
Reference in a new issue