Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/video/cdg
diff options
context:
space:
mode:
authorArun Raghavan <arun@asymptotic.io>2020-04-05 17:38:52 +0300
committerArun Raghavan <arun@arunraghavan.net>2020-04-05 22:10:47 +0300
commitdc3c8fd0494056ae5e5f87aa716b4c3866af6591 (patch)
tree3226cac3439fd29b7b02f6456f3162d6a3d1e00a /video/cdg
parent205b6040fbb918c0fa736874b09f8e3f3f261e44 (diff)
Drop gst-plugin- prefix in plugin directory name
Diffstat (limited to 'video/cdg')
-rw-r--r--video/cdg/Cargo.toml28
-rw-r--r--video/cdg/build.rs5
-rw-r--r--video/cdg/src/cdgdec.rs242
-rw-r--r--video/cdg/src/cdgparse.rs245
-rw-r--r--video/cdg/src/constants.rs18
-rw-r--r--video/cdg/src/lib.rs114
-rw-r--r--video/cdg/tests/BrotherJohn.cdgbin0 -> 583200 bytes
-rw-r--r--video/cdg/tests/cdgdec.rs107
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
new file mode 100644
index 000000000..9ec6ec153
--- /dev/null
+++ b/video/cdg/tests/BrotherJohn.cdg
Binary files differ
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");
+}