diff options
author | Arun Raghavan <arun@asymptotic.io> | 2020-04-05 17:38:52 +0300 |
---|---|---|
committer | Arun Raghavan <arun@arunraghavan.net> | 2020-04-05 22:10:47 +0300 |
commit | dc3c8fd0494056ae5e5f87aa716b4c3866af6591 (patch) | |
tree | 3226cac3439fd29b7b02f6456f3162d6a3d1e00a /video/cdg | |
parent | 205b6040fbb918c0fa736874b09f8e3f3f261e44 (diff) |
Drop gst-plugin- prefix in plugin directory name
Diffstat (limited to 'video/cdg')
-rw-r--r-- | video/cdg/Cargo.toml | 28 | ||||
-rw-r--r-- | video/cdg/build.rs | 5 | ||||
-rw-r--r-- | video/cdg/src/cdgdec.rs | 242 | ||||
-rw-r--r-- | video/cdg/src/cdgparse.rs | 245 | ||||
-rw-r--r-- | video/cdg/src/constants.rs | 18 | ||||
-rw-r--r-- | video/cdg/src/lib.rs | 114 | ||||
-rw-r--r-- | video/cdg/tests/BrotherJohn.cdg | bin | 0 -> 583200 bytes | |||
-rw-r--r-- | video/cdg/tests/cdgdec.rs | 107 |
8 files changed, 759 insertions, 0 deletions
diff --git a/video/cdg/Cargo.toml b/video/cdg/Cargo.toml new file mode 100644 index 000000000..4ce1a51b6 --- /dev/null +++ b/video/cdg/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "gst-plugin-cdg" +version = "0.6.0" +authors = ["Guillaume Desmottes <guillaume.desmottes@collabora.com>"] +repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugin-rs" +license = "MIT/Apache-2.0" +description = "CDG Plugin" +edition = "2018" + +[dependencies] +glib = { git = "https://github.com/gtk-rs/glib" } +gstreamer = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_12"] } +gstreamer-base = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_12"] } +gstreamer-video = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs", features = ["v1_12"] } +gstreamer-app = { git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } +cdg = "0.1" +cdg_renderer = "0.4" +image = "0.22" +muldiv = "0.2" +lazy_static = "1.0" + +[lib] +name = "gstcdg" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[build-dependencies] +gst-plugin-version-helper = { path="../../version-helper" } diff --git a/video/cdg/build.rs b/video/cdg/build.rs new file mode 100644 index 000000000..0d1ddb61d --- /dev/null +++ b/video/cdg/build.rs @@ -0,0 +1,5 @@ +extern crate gst_plugin_version_helper; + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/video/cdg/src/cdgdec.rs b/video/cdg/src/cdgdec.rs new file mode 100644 index 000000000..6ea7b59d2 --- /dev/null +++ b/video/cdg/src/cdgdec.rs @@ -0,0 +1,242 @@ +// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use cdg; +use cdg_renderer; +use glib; +use glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::subclass::prelude::*; +use gst_video::prelude::VideoDecoderExtManual; +use gst_video::prelude::*; +use gst_video::subclass::prelude::*; +use gstreamer_video as gst_video; +use image::GenericImageView; +use std::sync::Mutex; + +use crate::constants::{CDG_HEIGHT, CDG_WIDTH}; + +struct CdgDec { + cdg_inter: Mutex<cdg_renderer::CdgInterpreter>, + output_info: Mutex<Option<gst_video::VideoInfo>>, +} + +lazy_static! { + static ref CAT: gst::DebugCategory = + gst::DebugCategory::new("cdgdec", gst::DebugColorFlags::empty(), Some("CDG decoder"),); +} + +impl ObjectSubclass for CdgDec { + const NAME: &'static str = "CdgDec"; + type ParentType = gst_video::VideoDecoder; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new() -> Self { + Self { + cdg_inter: Mutex::new(cdg_renderer::CdgInterpreter::new()), + output_info: Mutex::new(None), + } + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "CDG decoder", + "Decoder/Video", + "CDG decoder", + "Guillaume Desmottes <guillaume.desmottes@collabora.com>", + ); + + let sink_caps = gst::Caps::new_simple("video/x-cdg", &[("parsed", &true)]); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let src_caps = gst::Caps::new_simple( + "video/x-raw", + &[ + ("format", &gst_video::VideoFormat::Rgba.to_str()), + ("width", &(CDG_WIDTH as i32)), + ("height", &(CDG_HEIGHT as i32)), + ("framerate", &gst::Fraction::new(0, 1)), + ], + ); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + } +} + +impl ObjectImpl for CdgDec { + glib_object_impl!(); +} + +impl ElementImpl for CdgDec {} + +impl VideoDecoderImpl for CdgDec { + fn start(&self, element: &gst_video::VideoDecoder) -> Result<(), gst::ErrorMessage> { + let mut out_info = self.output_info.lock().unwrap(); + *out_info = None; + + self.parent_start(element) + } + + fn stop(&self, element: &gst_video::VideoDecoder) -> Result<(), gst::ErrorMessage> { + { + let mut cdg_inter = self.cdg_inter.lock().unwrap(); + cdg_inter.reset(true); + } + self.parent_stop(element) + } + + fn handle_frame( + &self, + element: &gst_video::VideoDecoder, + mut frame: gst_video::VideoCodecFrame, + ) -> Result<gst::FlowSuccess, gst::FlowError> { + { + let mut out_info = self.output_info.lock().unwrap(); + if out_info.is_none() { + let output_state = element.set_output_state( + gst_video::VideoFormat::Rgba, + CDG_WIDTH, + CDG_HEIGHT, + None, + )?; + + element.negotiate(output_state)?; + + let out_state = element.get_output_state().unwrap(); + *out_info = Some(out_state.get_info()); + } + } + + let cmd = { + let input = frame.get_input_buffer().unwrap(); + let map = input.map_readable().map_err(|_| { + gst_element_error!( + element, + gst::CoreError::Failed, + ["Failed to map input buffer readable"] + ); + gst::FlowError::Error + })?; + let data = map.as_slice(); + + cdg::decode_subchannel_cmd(&data) + }; + + let cmd = match cmd { + Some(cmd) => cmd, + None => { + // Not a CDG command + element.release_frame(frame); + return Ok(gst::FlowSuccess::Ok); + } + }; + + let mut cdg_inter = self.cdg_inter.lock().unwrap(); + cdg_inter.handle_cmd(cmd); + + element.allocate_output_frame(&mut frame, None)?; + { + let output = frame.get_output_buffer_mut().unwrap(); + let info = self.output_info.lock().unwrap(); + + let mut out_frame = + gst_video::VideoFrameRef::from_buffer_ref_writable(output, info.as_ref().unwrap()) + .map_err(|_| { + gst_element_error!( + element, + gst::CoreError::Failed, + ["Failed to map output buffer writable"] + ); + gst::FlowError::Error + })?; + + let out_stride = out_frame.plane_stride()[0] as usize; + + for (y, line) in out_frame + .plane_data_mut(0) + .unwrap() + .chunks_exact_mut(out_stride) + .take(CDG_HEIGHT as usize) + .enumerate() + { + for (x, pixel) in line + .chunks_exact_mut(4) + .take(CDG_WIDTH as usize) + .enumerate() + { + let p = cdg_inter.get_pixel(x as u32, y as u32); + pixel.copy_from_slice(&p.0); + } + } + } + + gst_debug!(CAT, obj: element, "Finish frame pts={}", frame.get_pts()); + + element.finish_frame(frame) + } + + fn decide_allocation( + &self, + element: &gst_video::VideoDecoder, + query: &mut gst::QueryRef, + ) -> Result<(), gst::ErrorMessage> { + if let gst::query::QueryView::Allocation(allocation) = query.view() { + if allocation + .find_allocation_meta::<gst_video::VideoMeta>() + .is_some() + { + let pools = allocation.get_allocation_pools(); + if let Some((ref pool, _, _, _)) = pools.first() { + if let Some(pool) = pool { + let mut config = pool.get_config(); + config.add_option(&gst_video::BUFFER_POOL_OPTION_VIDEO_META); + pool.set_config(config).map_err(|e| { + gst::gst_error_msg!(gst::CoreError::Negotiation, [&e.message]) + })?; + } + } + } + } + + self.parent_decide_allocation(element, query) + } + + fn flush(&self, element: &gst_video::VideoDecoder) -> bool { + gst_debug!(CAT, obj: element, "flushing, reset CDG interpreter"); + + let mut cdg_inter = self.cdg_inter.lock().unwrap(); + cdg_inter.reset(false); + true + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "cdgdec", + gst::Rank::Primary, + CdgDec::get_type(), + ) +} diff --git a/video/cdg/src/cdgparse.rs b/video/cdg/src/cdgparse.rs new file mode 100644 index 000000000..a23910e63 --- /dev/null +++ b/video/cdg/src/cdgparse.rs @@ -0,0 +1,245 @@ +// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use glib; +use glib::subclass; +use glib::subclass::prelude::*; +use gst; +use gst::subclass::prelude::*; +use gst::SECOND_VAL; +use gst_base::prelude::*; +use gst_base::subclass::prelude::*; +use gstreamer::format::Bytes; +use gstreamer_base as gst_base; +use std::convert::TryInto; + +use crate::constants::{ + CDG_COMMAND, CDG_HEIGHT, CDG_MASK, CDG_PACKET_PERIOD, CDG_PACKET_SIZE, CDG_WIDTH, +}; + +const CDG_CMD_MEMORY_PRESET: u8 = 1; +const CDG_CMD_MEMORY_LOAD_COLOR_TABLE_1: u8 = 30; +const CDG_CMD_MEMORY_LOAD_COLOR_TABLE_2: u8 = 31; + +struct CdgParse; + +lazy_static! { + static ref CAT: gst::DebugCategory = gst::DebugCategory::new( + "cdgparse", + gst::DebugColorFlags::empty(), + Some("CDG parser"), + ); +} + +impl ObjectSubclass for CdgParse { + const NAME: &'static str = "CdgParse"; + type ParentType = gst_base::BaseParse; + type Instance = gst::subclass::ElementInstanceStruct<Self>; + type Class = subclass::simple::ClassStruct<Self>; + + glib_object_subclass!(); + + fn new() -> Self { + Self + } + + fn class_init(klass: &mut subclass::simple::ClassStruct<Self>) { + klass.set_metadata( + "CDG parser", + "Codec/Parser/Video", + "CDG parser", + "Guillaume Desmottes <guillaume.desmottes@collabora.com>", + ); + + let sink_caps = gst::Caps::new_simple("video/x-cdg", &[]); + let sink_pad_template = gst::PadTemplate::new( + "sink", + gst::PadDirection::Sink, + gst::PadPresence::Always, + &sink_caps, + ) + .unwrap(); + klass.add_pad_template(sink_pad_template); + + let src_caps = gst::Caps::new_simple( + "video/x-cdg", + &[ + ("width", &(CDG_WIDTH as i32)), + ("height", &(CDG_HEIGHT as i32)), + ("framerate", &gst::Fraction::new(0, 1)), + ("parsed", &true), + ], + ); + let src_pad_template = gst::PadTemplate::new( + "src", + gst::PadDirection::Src, + gst::PadPresence::Always, + &src_caps, + ) + .unwrap(); + klass.add_pad_template(src_pad_template); + } +} + +impl ObjectImpl for CdgParse { + glib_object_impl!(); +} + +impl ElementImpl for CdgParse {} + +fn bytes_to_time(bytes: Bytes) -> gst::ClockTime { + match bytes { + Bytes(Some(bytes)) => { + let nb = bytes / CDG_PACKET_SIZE as u64; + gst::ClockTime(nb.mul_div_round(SECOND_VAL, CDG_PACKET_PERIOD)) + } + Bytes(None) => gst::ClockTime::none(), + } +} + +fn time_to_bytes(time: gst::ClockTime) -> Bytes { + match time.nseconds() { + Some(time) => { + let bytes = time.mul_div_round(CDG_PACKET_PERIOD * CDG_PACKET_SIZE as u64, SECOND_VAL); + Bytes(bytes) + } + None => Bytes(None), + } +} + +impl BaseParseImpl for CdgParse { + fn start(&self, element: &gst_base::BaseParse) -> Result<(), gst::ErrorMessage> { + element.set_min_frame_size(CDG_PACKET_SIZE as u32); + + /* Set duration */ + let mut query = gst::Query::new_duration(gst::Format::Bytes); + let pad = element.get_src_pad(); + if pad.query(&mut query) { + let size = query.get_result(); + let duration = bytes_to_time(size.try_into().unwrap()); + element.set_duration(duration, 0); + } + + Ok(()) + } + + fn handle_frame( + &self, + element: &gst_base::BaseParse, + mut frame: gst_base::BaseParseFrame, + ) -> Result<(gst::FlowSuccess, u32), gst::FlowError> { + let pad = element.get_src_pad(); + if pad.get_current_caps().is_none() { + // Set src pad caps + let src_caps = gst::Caps::new_simple( + "video/x-cdg", + &[ + ("width", &(CDG_WIDTH as i32)), + ("height", &(CDG_HEIGHT as i32)), + ("framerate", &gst::Fraction::new(0, 1)), + ("parsed", &true), + ], + ); + + pad.push_event(gst::Event::new_caps(&src_caps).build()); + } + + // Scan for CDG instruction + let input = frame.get_buffer().unwrap(); + let skip = { + let map = input.map_readable().map_err(|_| { + gst_element_error!( + element, + gst::CoreError::Failed, + ["Failed to map input buffer readable"] + ); + gst::FlowError::Error + })?; + let data = map.as_slice(); + + data.iter() + .enumerate() + .find(|(_, byte)| (*byte & CDG_MASK == CDG_COMMAND)) + .map(|(i, _)| i) + .unwrap_or_else(|| input.get_size()) // skip the whole buffer + as u32 + }; + + if skip != 0 { + // Skip to the start of the CDG packet + return Ok((gst::FlowSuccess::Ok, skip)); + } + + let (keyframe, header) = { + let map = input.map_readable().map_err(|_| { + gst_element_error!( + element, + gst::CoreError::Failed, + ["Failed to map input buffer readable"] + ); + gst::FlowError::Error + })?; + let data = map.as_slice(); + + match data[1] & CDG_MASK { + // consider memory preset as keyframe as it clears the screen + CDG_CMD_MEMORY_PRESET => (true, false), + // mark palette commands as headers + CDG_CMD_MEMORY_LOAD_COLOR_TABLE_1 | CDG_CMD_MEMORY_LOAD_COLOR_TABLE_2 => { + (false, true) + } + _ => (false, false), + } + }; + + let pts = bytes_to_time(Bytes(Some(frame.get_offset()))); + let buffer = frame.get_buffer_mut().unwrap(); + buffer.set_pts(pts); + + if !keyframe { + buffer.set_flags(gst::BufferFlags::DELTA_UNIT); + } + if header { + buffer.set_flags(gst::BufferFlags::HEADER); + } + + gst_debug!(CAT, obj: element, "Found frame pts={}", pts); + + element.finish_frame(frame, CDG_PACKET_SIZE as u32)?; + + Ok((gst::FlowSuccess::Ok, skip)) + } + + fn convert<V: Into<gst::GenericFormattedValue>>( + &self, + _element: &gst_base::BaseParse, + src_val: V, + dest_format: gst::Format, + ) -> Option<gst::GenericFormattedValue> { + let src_val = src_val.into(); + + match (src_val, dest_format) { + (gst::GenericFormattedValue::Bytes(bytes), gst::Format::Time) => { + Some(bytes_to_time(bytes).into()) + } + (gst::GenericFormattedValue::Time(time), gst::Format::Bytes) => { + Some(time_to_bytes(time).into()) + } + _ => None, + } + } +} + +pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + gst::Element::register( + Some(plugin), + "cdgparse", + gst::Rank::Primary, + CdgParse::get_type(), + ) +} diff --git a/video/cdg/src/constants.rs b/video/cdg/src/constants.rs new file mode 100644 index 000000000..5f8d80000 --- /dev/null +++ b/video/cdg/src/constants.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +pub(crate) const CDG_PACKET_SIZE: i32 = 24; + +// 75 sectors/sec * 4 packets/sector = 300 packets/sec +pub(crate) const CDG_PACKET_PERIOD: u64 = 300; + +pub(crate) const CDG_WIDTH: u32 = 300; +pub(crate) const CDG_HEIGHT: u32 = 216; + +pub(crate) const CDG_MASK: u8 = 0x3F; +pub(crate) const CDG_COMMAND: u8 = 0x09; diff --git a/video/cdg/src/lib.rs b/video/cdg/src/lib.rs new file mode 100644 index 000000000..433f6fdec --- /dev/null +++ b/video/cdg/src/lib.rs @@ -0,0 +1,114 @@ +// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![crate_type = "cdylib"] + +#[macro_use] +extern crate glib; +#[macro_use] +extern crate gstreamer as gst; +#[macro_use] +extern crate lazy_static; + +mod cdgdec; +mod cdgparse; +mod constants; + +use constants::{CDG_COMMAND, CDG_MASK, CDG_PACKET_PERIOD, CDG_PACKET_SIZE}; +use gst::{Caps, TypeFind, TypeFindProbability}; +use std::cmp; + +const NB_WINDOWS: u64 = 8; +const TYPEFIND_SEARCH_WINDOW_SEC: i64 = 4; +const TYPEFIND_SEARCH_WINDOW: i64 = + TYPEFIND_SEARCH_WINDOW_SEC * (CDG_PACKET_SIZE as i64 * CDG_PACKET_PERIOD as i64); /* in bytes */ + +/* Return the percentage of CDG packets in the first @len bytes of @typefind */ +fn cdg_packets_ratio(typefind: &mut TypeFind, start: i64, len: i64) -> i64 { + let mut count = 0; + let total = len / CDG_PACKET_SIZE as i64; + + for offset in (0..len).step_by(CDG_PACKET_SIZE as usize) { + match typefind.peek(start + offset, CDG_PACKET_SIZE as u32) { + Some(data) => { + if data[0] & CDG_MASK == CDG_COMMAND { + count += 1; + } + } + None => break, + } + } + (count * 100) / total +} + +/* Some CDG files starts drawing right away and then pause for a while + * (typically because of the song intro) while other wait for a few + * seconds before starting to draw. + * In order to support all variants, scan through all the file per block + * of size TYPEFIND_SEARCH_WINDOW and keep the highest ratio of CDG packets + * detected. */ +fn compute_probability(typefind: &mut TypeFind) -> TypeFindProbability { + let mut best = TypeFindProbability::None; + // Try looking at the start of the file if its length isn't available + let len = typefind + .get_length() + .unwrap_or(TYPEFIND_SEARCH_WINDOW as u64 * NB_WINDOWS); + let step = len / NB_WINDOWS; + + for offset in (0..len).step_by(step as usize) { + let proba = match cdg_packets_ratio(typefind, offset as i64, TYPEFIND_SEARCH_WINDOW) { + 0..=5 => TypeFindProbability::None, + 6..=10 => TypeFindProbability::Possible, + _ => TypeFindProbability::Likely, + }; + + if proba == TypeFindProbability::Likely { + return proba; + } + + best = cmp::max(best, proba); + } + + best +} + +fn typefind_register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + TypeFind::register( + Some(plugin), + "cdg_typefind", + gst::Rank::None, + Some("cdg"), + Some(&Caps::new_simple("video/x-cdg", &[])), + |mut typefind| { + let proba = compute_probability(&mut typefind); + + if proba != gst::TypeFindProbability::None { + typefind.suggest(proba, &Caps::new_simple("video/x-cdg", &[])); + } + }, + ) +} + +fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { + cdgdec::register(plugin)?; + cdgparse::register(plugin)?; + typefind_register(plugin)?; + Ok(()) +} + +gst_plugin_define!( + cdg, + 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/video/cdg/tests/BrotherJohn.cdg b/video/cdg/tests/BrotherJohn.cdg Binary files differnew file mode 100644 index 000000000..9ec6ec153 --- /dev/null +++ b/video/cdg/tests/BrotherJohn.cdg diff --git a/video/cdg/tests/cdgdec.rs b/video/cdg/tests/cdgdec.rs new file mode 100644 index 000000000..69b9d3ff8 --- /dev/null +++ b/video/cdg/tests/cdgdec.rs @@ -0,0 +1,107 @@ +// Copyright (C) 2019 Guillaume Desmottes <guillaume.desmottes@collabora.com> +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use gst_app::prelude::*; +use gstreamer as gst; +use gstreamer_app as gst_app; +use std::path::PathBuf; + +use gstcdg; + +fn init() { + use std::sync::Once; + static INIT: Once = Once::new(); + + INIT.call_once(|| { + gst::init().unwrap(); + gstcdg::plugin_register_static().expect("cdgdec tests"); + }); +} + +#[test] +fn test_cdgdec() { + init(); + + let pipeline = gst::Pipeline::new(Some("cdgdec-test")); + + let input_path = { + let mut r = PathBuf::new(); + r.push(env!("CARGO_MANIFEST_DIR")); + r.push("tests"); + r.push("BrotherJohn"); + r.set_extension("cdg"); + r + }; + + let filesrc = gst::ElementFactory::make("filesrc", None).unwrap(); + filesrc + .set_property("location", &input_path.to_str().unwrap()) + .expect("failed to set 'location' property"); + filesrc + .set_property("num-buffers", &1) + .expect("failed to set 'num-buffers' property"); + let blocksize: u32 = 24; // One CDG instruction + filesrc + .set_property("blocksize", &blocksize) + .expect("failed to set 'blocksize' property"); + + let parse = gst::ElementFactory::make("cdgparse", None).unwrap(); + let dec = gst::ElementFactory::make("cdgdec", None).unwrap(); + let sink = gst::ElementFactory::make("appsink", None).unwrap(); + + pipeline + .add_many(&[&filesrc, &parse, &dec, &sink]) + .expect("failed to add elements to the pipeline"); + gst::Element::link_many(&[&filesrc, &parse, &dec, &sink]).expect("failed to link the elements"); + + let sink = sink.downcast::<gst_app::AppSink>().unwrap(); + sink.set_callbacks( + gst_app::AppSinkCallbacks::new() + // Add a handler to the "new-sample" signal. + .new_sample(move |appsink| { + // Pull the sample in question out of the appsink's buffer. + let sample = appsink.pull_sample().map_err(|_| gst::FlowError::Eos)?; + let buffer = sample.get_buffer().ok_or(gst::FlowError::Error)?; + let map = buffer.map_readable().map_err(|_| gst::FlowError::Error)?; + + // First frame fully blue + map.as_slice() + .chunks_exact(4) + .for_each(|p| assert_eq!(p, [0, 0, 136, 255])); + + Ok(gst::FlowSuccess::Ok) + }) + .build(), + ); + + pipeline + .set_state(gst::State::Playing) + .expect("Unable to set the pipeline to the `Playing` state"); + + let bus = pipeline.get_bus().unwrap(); + for msg in bus.iter_timed(gst::CLOCK_TIME_NONE) { + use gst::MessageView; + match msg.view() { + MessageView::Error(err) => { + eprintln!( + "Error received from element {:?}: {}", + err.get_src().map(|s| s.get_path_string()), + err.get_error() + ); + eprintln!("Debugging information: {:?}", err.get_debug()); + unreachable!(); + } + MessageView::Eos(..) => break, + _ => (), + } + } + + pipeline + .set_state(gst::State::Null) + .expect("Unable to set the pipeline to the `Null` state"); +} |