diff --git a/gstreamer-video/Cargo.toml b/gstreamer-video/Cargo.toml index 0bebf23b8..438ccaa52 100644 --- a/gstreamer-video/Cargo.toml +++ b/gstreamer-video/Cargo.toml @@ -22,6 +22,7 @@ gst = { package = "gstreamer", path = "../gstreamer" } gst-base = { package = "gstreamer-base", path = "../gstreamer-base" } futures-channel = "0.3" serde = { version = "1.0", optional = true, features = ["derive"] } +thiserror = "1.0.49" [dev-dependencies] itertools = "0.11" diff --git a/gstreamer-video/src/lib.rs b/gstreamer-video/src/lib.rs index 8b9134e9e..609684901 100644 --- a/gstreamer-video/src/lib.rs +++ b/gstreamer-video/src/lib.rs @@ -133,6 +133,27 @@ mod video_aggregator_convert_pad; #[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] mod video_aggregator_pad; +#[cfg(feature = "v1_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] +mod video_vbi; +#[cfg(feature = "v1_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] +pub use video_vbi::*; + +#[cfg(feature = "v1_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] +mod video_vbi_encoder; +#[cfg(feature = "v1_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] +pub use video_vbi_encoder::*; + +#[cfg(feature = "v1_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] +mod video_vbi_parser; +#[cfg(feature = "v1_16")] +#[cfg_attr(docsrs, doc(cfg(feature = "v1_16")))] +pub use video_vbi_parser::*; + pub const VIDEO_ENCODER_FLOW_NEED_DATA: gst::FlowSuccess = gst::FlowSuccess::CustomSuccess; pub const VIDEO_DECODER_FLOW_NEED_DATA: gst::FlowSuccess = gst::FlowSuccess::CustomSuccess; diff --git a/gstreamer-video/src/video_vbi.rs b/gstreamer-video/src/video_vbi.rs new file mode 100644 index 000000000..439f724c5 --- /dev/null +++ b/gstreamer-video/src/video_vbi.rs @@ -0,0 +1,35 @@ +use crate::VideoFormat; +pub(super) const VBI_HD_MIN_PIXEL_WIDTH: u32 = 1280; + +// rustdoc-stripper-ignore-next +/// Video Vertical Blanking Interval related Errors. +#[derive(thiserror::Error, Clone, Copy, Debug, Eq, PartialEq)] +pub enum VideoVBIError { + #[error("Format and/or pixel_width is not supported")] + Unsupported, + + #[error("Not enough space left in the current line")] + NotEnoughSpace, + + #[error("Not enough data left in the current line")] + NotEnoughData, + + #[error("Insufficient line buffer length {found}. Expected: {expected}")] + InsufficientLineBufLen { found: usize, expected: usize }, +} + +// rustdoc-stripper-ignore-next +/// Returns the buffer length needed to store the line. +pub(super) fn line_buffer_len(format: VideoFormat, width: u32) -> usize { + skip_assert_initialized!(); + // Taken from gst-plugins-base/gst-libs/gst/video/video-info.c:fill_planes + match format { + VideoFormat::V210 => ((width as usize + 47) / 48) * 128, + VideoFormat::Uyvy => { + // round up width to the next multiple of 4 + // FIXME: {integer}::next_multiple_of was stabilised in rustc 1.73.0 + ((width as usize * 2) + 3) & !3 + } + _ => unreachable!(), + } +} diff --git a/gstreamer-video/src/video_vbi_encoder.rs b/gstreamer-video/src/video_vbi_encoder.rs new file mode 100644 index 000000000..cde6f6be9 --- /dev/null +++ b/gstreamer-video/src/video_vbi_encoder.rs @@ -0,0 +1,543 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use crate::VideoFormat; +use glib::translate::*; + +use crate::video_vbi::line_buffer_len; +use crate::{VideoAncillaryDID, VideoAncillaryDID16, VideoVBIError, VBI_HD_MIN_PIXEL_WIDTH}; + +glib::wrapper! { + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct VideoVBIEncoderInner(Boxed); + + match fn { + copy => |ptr| ffi::gst_video_vbi_encoder_copy(ptr), + free => |ptr| ffi::gst_video_vbi_encoder_free(ptr), + type_ => || ffi::gst_video_vbi_encoder_get_type(), + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VideoVBIEncoder { + inner: VideoVBIEncoderInner, + format: VideoFormat, + pixel_width: u32, + line_buffer_len: usize, + anc_len: usize, +} + +unsafe impl Send for VideoVBIEncoder {} +unsafe impl Sync for VideoVBIEncoder {} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VideoAFDDescriptionMode { + Composite, + Component, +} + +impl VideoAFDDescriptionMode { + pub fn is_composite(&self) -> bool { + matches!(self, VideoAFDDescriptionMode::Composite) + } + + pub fn is_component(&self) -> bool { + matches!(self, VideoAFDDescriptionMode::Component) + } +} + +impl VideoVBIEncoder { + #[doc(alias = "gst_video_vbi_encoder_new")] + fn try_new(format: VideoFormat, pixel_width: u32) -> Result { + skip_assert_initialized!(); + let res: Option = unsafe { + from_glib_full(ffi::gst_video_vbi_encoder_new( + format.into_glib(), + pixel_width, + )) + }; + + Ok(VideoVBIEncoder { + inner: res.ok_or(VideoVBIError::Unsupported)?, + format, + pixel_width, + line_buffer_len: line_buffer_len(format, pixel_width), + anc_len: 0, + }) + } + + // rustdoc-stripper-ignore-next + /// Adds the provided ancillary data as a DID and block number AFD. + pub fn add_did_ancillary( + &mut self, + adf_mode: VideoAFDDescriptionMode, + did: VideoAncillaryDID, + block_number: u8, + data: &[u8], + ) -> Result<(), VideoVBIError> { + self.add_ancillary(adf_mode, did.into_glib() as u8, block_number, data) + } + + // rustdoc-stripper-ignore-next + /// Adds the provided ancillary data as a DID16 (DID & SDID) AFD. + pub fn add_did16_ancillary( + &mut self, + adf_mode: VideoAFDDescriptionMode, + did16: VideoAncillaryDID16, + data: &[u8], + ) -> Result<(), VideoVBIError> { + let did16 = did16.into_glib(); + + self.add_ancillary( + adf_mode, + ((did16 & 0xff00) >> 8) as u8, + (did16 & 0xff) as u8, + data, + ) + } + + #[doc(alias = "gst_video_vbi_encoder_add_ancillary")] + pub fn add_ancillary( + &mut self, + adf_mode: VideoAFDDescriptionMode, + did: u8, + sdid_block_number: u8, + data: &[u8], + ) -> Result<(), VideoVBIError> { + let data_count = data.len() as _; + let res: bool = unsafe { + from_glib(ffi::gst_video_vbi_encoder_add_ancillary( + self.inner.to_glib_none_mut().0, + adf_mode.is_composite().into_glib(), + did, + sdid_block_number, + data.to_glib_none().0, + data_count, + )) + }; + + if !res { + return Err(VideoVBIError::NotEnoughSpace); + } + + // AFD: 1 byte (+2 if component) + // DID + SDID_block_number + Data Count: 3 bytes + // DATA: data_count bytes + // Checksum: 1 byte + let mut len = 1 + 3 + (data_count as usize) + 1; + if adf_mode.is_component() { + len += 2; + } + + if matches!(self.format, VideoFormat::V210) { + // 10bits payload on 16bits for now: will be packed when writing the line + len *= 2; + } + + self.anc_len += len; + + Ok(()) + } + + // rustdoc-stripper-ignore-next + /// Returns the buffer length needed to store the line. + pub fn line_buffer_len(&self) -> usize { + self.line_buffer_len + } + + // rustdoc-stripper-ignore-next + /// Writes the ancillaries encoded for VBI to the provided buffer. + /// + /// Use [`Self::line_buffer_len`] to get the expected buffer length. + /// + /// Resets the internal state, so this [`VideoVBIEncoder`] can be reused for + /// subsequent VBI encodings. + /// + /// # Returns + /// + /// - `Ok` with the written length in bytes in the line buffer containing the encoded + /// ancilliaries previously added using [`VideoVBIEncoder::add_ancillary`], + /// [`VideoVBIEncoder::add_did_ancillary`] or [`VideoVBIEncoder::add_did16_ancillary`]. + /// - `Err` if the ancillary could not be added. + #[doc(alias = "gst_video_vbi_encoder_write_line")] + pub fn write_line(&mut self, data: &mut [u8]) -> Result { + if data.len() < self.line_buffer_len { + return Err(VideoVBIError::InsufficientLineBufLen { + found: data.len(), + expected: self.line_buffer_len, + }); + } + + unsafe { + let dest = data.as_mut_ptr(); + ffi::gst_video_vbi_encoder_write_line(self.inner.to_glib_none_mut().0, dest); + } + + let mut anc_len = std::mem::take(&mut self.anc_len); + match self.format { + VideoFormat::V210 => { + // Anc data consists in 10bits stored in 16bits word + let word_count = anc_len / 2; + + if self.pixel_width < VBI_HD_MIN_PIXEL_WIDTH { + // SD: Packs 12x 10bits data in 4x 32bits word + anc_len = + 4 * 4 * ((word_count / 12) + if word_count % 12 == 0 { 0 } else { 1 }); + } else { + // HD: Packs 3x 10bits data in 1x 32bits word interleaving UV and Y components + // (where Y starts at buffer offset 0 and UV starts at buffer offset pixel_width) + // so we get 6 (uv,y) pairs every 4x 32bits word in the resulting line + // FIXME: {integer}::div_ceil was stabilised in rustc 1.73.0 + let pair_count = usize::min(word_count, self.pixel_width as usize); + anc_len = 4 * 4 * ((pair_count / 6) + if pair_count % 6 == 0 { 0 } else { 1 }); + } + } + VideoFormat::Uyvy => { + // Anc data stored as bytes + + if self.pixel_width < VBI_HD_MIN_PIXEL_WIDTH { + // SD: Stores 4x bytes in 4x bytes let's keep 32 bits alignment + anc_len = 4 * ((anc_len / 4) + if anc_len % 4 == 0 { 0 } else { 1 }); + } else { + // HD: Stores 4x bytes in 4x bytes interleaving UV and Y components + // (where Y starts at buffer offset 0 and UV starts at buffer offset pixel_width) + // so we get 2 (uv,y) pairs every 4x bytes in the resulting line + // let's keep 32 bits alignment + // FIXME: {integer}::div_ceil was stabilised in rustc 1.73.0 + let pair_count = usize::min(anc_len, self.pixel_width as usize); + anc_len = 4 * ((pair_count / 2) + if pair_count % 2 == 0 { 0 } else { 1 }); + } + } + _ => unreachable!(), + } + + assert!(anc_len < self.line_buffer_len); + + Ok(anc_len) + } +} + +impl<'a> TryFrom<&'a crate::VideoInfo> for VideoVBIEncoder { + type Error = VideoVBIError; + + fn try_from(info: &'a crate::VideoInfo) -> Result { + skip_assert_initialized!(); + VideoVBIEncoder::try_new(info.format(), info.width()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cea608_component() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia608, + &[0x80, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(32, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xf0, 0x3f, 0x00, 0x84, 0x05, 0x00, 0x02, 0x01, + 0x30, 0x20, 0x00, 0x00, 0x06, 0x00, 0x94, 0x01, 0xc0, 0x12, 0x00, 0x98, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00 + ] + ); + } + + #[test] + fn cea608_component_sd() { + let mut encoder = VideoVBIEncoder::try_new(VideoFormat::V210, 768).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia608, + &[0x80, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(16, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0xfc, 0xff, 0x3f, 0x61, 0x09, 0x34, 0x20, 0x80, 0x51, 0xc6, 0x12, 0xa6, 0x02, + 0x00, 0x00 + ] + ); + } + + #[test] + fn cea608_composite() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Composite, + VideoAncillaryDID16::S334Eia608, + &[0x15, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(32, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0xf0, 0x0f, 0x00, 0x61, 0x01, 0x20, 0x10, 0x00, 0x0c, 0x08, 0x00, 0x15, 0x01, + 0x40, 0x19, 0x00, 0xb0, 0x04, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ] + ); + } + + #[test] + fn cea608_composite_sd() { + let mut encoder = VideoVBIEncoder::try_new(VideoFormat::V210, 768).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Composite, + VideoAncillaryDID16::S334Eia608, + &[0x15, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(16, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0xfc, 0x87, 0x25, 0x10, 0x03, 0x56, 0x44, 0x19, 0x2c, 0xed, 0x08, 0x00, 0x00, 0x00, + 0x00, 0x00 + ] + ); + } + + #[test] + fn cea608_component_uyvy() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::Uyvy, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia608, + &[0x80, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(20, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0x00, 0x61, 0x00, 0x02, 0x00, 0x03, 0x00, 0x80, + 0x00, 0x94, 0x00, 0x2c, 0x00, 0xa6 + ] + ); + } + + #[test] + fn cea608_component_sd_uyvy() { + let mut encoder = VideoVBIEncoder::try_new(VideoFormat::Uyvy, 768).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia608, + &[0x80, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(12, anc_len); + assert_eq!( + buf[0..anc_len], + [0x00, 0xff, 0xff, 0x61, 0x02, 0x03, 0x80, 0x94, 0x2c, 0xa6, 0x00, 0x00] + ); + } + + #[test] + fn cea608_composite_uyvy() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::Uyvy, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Composite, + VideoAncillaryDID16::S334Eia608, + &[0x15, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(16, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0xfc, 0x00, 0x61, 0x00, 0x02, 0x00, 0x03, 0x00, 0x15, 0x00, 0x94, 0x00, 0x2c, + 0x00, 0x3b + ] + ); + } + + #[test] + fn cea608_composite_sd_uyvy() { + let mut encoder = VideoVBIEncoder::try_new(VideoFormat::Uyvy, 768).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Composite, + VideoAncillaryDID16::S334Eia608, + &[0x15, 0x94, 0x2c], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(8, anc_len); + assert_eq!( + buf[0..anc_len], + [0xfc, 0x61, 0x02, 0x03, 0x15, 0x94, 0x2c, 0x3b] + ); + } + + #[test] + fn insufficient_line_buf_len() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia608, + &[0x80, 0x94, 0x2c], + ) + .unwrap(); + let mut buf = vec![0; 10]; + assert_eq!( + encoder.write_line(buf.as_mut_slice()).unwrap_err(), + VideoVBIError::InsufficientLineBufLen { + found: 10, + expected: encoder.line_buffer_len() + }, + ); + } + + #[test] + fn cea708_component() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia708, + &[ + 0x96, 0x69, 0x55, 0x3f, 0x43, 0x00, 0x00, 0x72, 0xf8, 0xfc, 0x94, 0x2c, 0xf9, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0x74, 0x00, 0x00, 0x1b, + ], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(256, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xf0, 0x3f, 0x00, 0x84, 0x05, 0x00, 0x01, 0x01, + 0x50, 0x25, 0x00, 0x58, 0x0a, 0x00, 0x69, 0x02, 0x50, 0x25, 0x00, 0xfc, 0x08, 0x00, + 0x43, 0x01, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0x72, 0x02, 0x80, 0x1f, 0x00, 0xf0, + 0x0b, 0x00, 0x94, 0x01, 0xc0, 0x12, 0x00, 0xe4, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, + 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, + 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, + 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, + 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, + 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, + 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xd0, 0x09, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0x6c, 0x08, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ] + ); + } + + #[test] + fn cea608_and_cea708_component() { + let mut encoder = + VideoVBIEncoder::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia608, + &[0x80, 0x94, 0x2c], + ) + .unwrap(); + + encoder + .add_did16_ancillary( + VideoAFDDescriptionMode::Component, + VideoAncillaryDID16::S334Eia708, + &[ + 0x96, 0x69, 0x55, 0x3f, 0x43, 0x00, 0x00, 0x72, 0xf8, 0xfc, 0x94, 0x2c, 0xf9, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0x74, 0x00, 0x00, 0x1b, + ], + ) + .unwrap(); + + let mut buf = vec![0; encoder.line_buffer_len()]; + let anc_len = encoder.write_line(buf.as_mut_slice()).unwrap(); + assert_eq!(272, anc_len); + assert_eq!( + buf[0..anc_len], + [ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xf0, 0x3f, 0x00, 0x84, 0x05, 0x00, 0x02, 0x01, + 0x30, 0x20, 0x00, 0x00, 0x06, 0x00, 0x94, 0x01, 0xc0, 0x12, 0x00, 0x98, 0x0a, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0x00, 0xfc, 0x0f, 0x00, 0x61, 0x01, 0x10, 0x10, 0x00, 0x54, + 0x09, 0x00, 0x96, 0x02, 0x90, 0x26, 0x00, 0x54, 0x09, 0x00, 0x3f, 0x02, 0x30, 0x14, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x02, 0x20, 0x27, 0x00, 0xe0, 0x07, 0x00, 0xfc, 0x02, + 0x40, 0x19, 0x00, 0xb0, 0x04, 0x00, 0xf9, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, + 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, + 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, + 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, + 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, + 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, + 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0x74, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0x1b, 0x02, 0x70, 0x2b + ] + ); + } +} diff --git a/gstreamer-video/src/video_vbi_parser.rs b/gstreamer-video/src/video_vbi_parser.rs new file mode 100644 index 000000000..618cbb2e7 --- /dev/null +++ b/gstreamer-video/src/video_vbi_parser.rs @@ -0,0 +1,348 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use crate::VideoFormat; +use glib::translate::*; + +use std::fmt; + +use crate::video_vbi::line_buffer_len; +use crate::{VideoAncillaryDID, VideoAncillaryDID16, VideoVBIError}; + +glib::wrapper! { + #[doc(alias = "GstVideoAncillary")] + pub struct VideoAncillary(BoxedInline); +} + +impl VideoAncillary { + pub fn did_u8(&self) -> u8 { + self.inner.DID + } + + pub fn did(&self) -> VideoAncillaryDID { + unsafe { VideoAncillaryDID::from_glib(self.inner.DID as ffi::GstVideoAncillaryDID) } + } + + pub fn sdid_block_number(&self) -> u8 { + self.inner.SDID_block_number + } + + pub fn did16(&self) -> VideoAncillaryDID16 { + unsafe { + VideoAncillaryDID16::from_glib( + (((self.inner.DID as u16) << 8) + self.inner.SDID_block_number as u16) + as ffi::GstVideoAncillaryDID16, + ) + } + } + + pub fn len(&self) -> usize { + self.inner.data_count as usize + } + + pub fn is_empty(&self) -> bool { + self.inner.data_count == 0 + } + + pub fn data(&self) -> &[u8] { + &self.inner.data[0..(self.inner.data_count as usize)] + } +} + +impl fmt::Debug for VideoAncillary { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("VideoAncillary") + .field("did", &self.did()) + .field("sdid_block_number", &self.sdid_block_number()) + .field("did16", &self.did16()) + .field("data_count", &self.inner.data_count) + .finish() + } +} + +glib::wrapper! { + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct VideoVBIParserInner(Boxed); + + match fn { + copy => |ptr| ffi::gst_video_vbi_parser_copy(ptr), + free => |ptr| ffi::gst_video_vbi_parser_free(ptr), + type_ => || ffi::gst_video_vbi_parser_get_type(), + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VideoVBIParser { + inner: VideoVBIParserInner, + line_buffer_len: usize, +} + +impl VideoVBIParser { + #[doc(alias = "gst_video_vbi_parser_new")] + pub fn try_new(format: VideoFormat, pixel_width: u32) -> Result { + skip_assert_initialized!(); + let res: Option = unsafe { + from_glib_full(ffi::gst_video_vbi_parser_new( + format.into_glib(), + pixel_width, + )) + }; + + Ok(VideoVBIParser { + inner: res.ok_or(VideoVBIError::Unsupported)?, + line_buffer_len: line_buffer_len(format, pixel_width), + }) + } + + // rustdoc-stripper-ignore-next + /// Returns the buffer length needed to store the line. + pub fn line_buffer_len(&self) -> usize { + self.line_buffer_len + } + + #[doc(alias = "gst_video_vbi_parser_add_line")] + pub fn add_line(&mut self, data: &[u8]) -> Result<(), VideoVBIError> { + if data.len() < self.line_buffer_len { + return Err(VideoVBIError::InsufficientLineBufLen { + found: data.len(), + expected: self.line_buffer_len, + }); + } + unsafe { + let data = data.as_ptr(); + ffi::gst_video_vbi_parser_add_line(self.inner.to_glib_none_mut().0, data); + } + + Ok(()) + } + + pub fn iter(&mut self) -> AncillaryIter { + AncillaryIter { parser: self } + } + + #[doc(alias = "gst_video_vbi_parser_get_ancillary")] + pub fn next_ancillary(&mut self) -> Option> { + unsafe { + let mut video_anc = std::mem::MaybeUninit::uninit(); + let res = ffi::gst_video_vbi_parser_get_ancillary( + self.inner.to_glib_none_mut().0, + video_anc.as_mut_ptr(), + ); + + match res { + ffi::GST_VIDEO_VBI_PARSER_RESULT_OK => Some(Ok(VideoAncillary { + inner: video_anc.assume_init(), + })), + ffi::GST_VIDEO_VBI_PARSER_RESULT_DONE => None, + ffi::GST_VIDEO_VBI_PARSER_RESULT_ERROR => Some(Err(VideoVBIError::NotEnoughData)), + _ => unreachable!(), + } + } + } +} + +unsafe impl Send for VideoVBIParser {} +unsafe impl Sync for VideoVBIParser {} + +impl<'a> TryFrom<&'a crate::VideoInfo> for VideoVBIParser { + type Error = VideoVBIError; + + fn try_from(info: &'a crate::VideoInfo) -> Result { + skip_assert_initialized!(); + VideoVBIParser::try_new(info.format(), info.width()) + } +} + +#[derive(Debug)] +pub struct AncillaryIter<'a> { + parser: &'a mut VideoVBIParser, +} + +impl<'a> Iterator for AncillaryIter<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + self.parser.next_ancillary() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::VBI_HD_MIN_PIXEL_WIDTH; + + fn init_line_buf(parser: &VideoVBIParser, anc_buf: &[u8]) -> Vec { + skip_assert_initialized!(); + let mut line_buf = vec![0; parser.line_buffer_len()]; + line_buf[0..anc_buf.len()].copy_from_slice(anc_buf); + line_buf + } + + #[test] + fn cea608_component() { + let mut parser = + VideoVBIParser::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + let line_buf = init_line_buf( + &parser, + &[ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xf0, 0x3f, 0x00, 0x84, 0x05, 0x00, 0x02, 0x01, + 0x30, 0x20, 0x00, 0x00, 0x06, 0x00, 0x94, 0x01, 0xc0, 0x12, 0x00, 0x98, 0x0a, 0x00, + 0x00, 0x00, 0x00, 0x00, + ], + ); + parser.add_line(&line_buf).unwrap(); + + let video_anc = parser.next_ancillary().unwrap().unwrap(); + assert_eq!(video_anc.did16(), VideoAncillaryDID16::S334Eia608); + assert_eq!(video_anc.data(), [0x80, 0x94, 0x2c]); + + assert!(parser.next_ancillary().is_none()); + } + + #[test] + fn cea608_composite() { + let mut parser = + VideoVBIParser::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + let line_buf = init_line_buf( + &parser, + &[ + 0x00, 0xf0, 0x0f, 0x00, 0x61, 0x01, 0x20, 0x10, 0x00, 0x0c, 0x08, 0x00, 0x15, 0x01, + 0x40, 0x19, 0x00, 0xb0, 0x04, 0x00, 0x3b, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ], + ); + parser.add_line(&line_buf).unwrap(); + + let video_anc = parser.next_ancillary().unwrap().unwrap(); + assert_eq!(video_anc.did16(), VideoAncillaryDID16::S334Eia608); + assert_eq!(video_anc.data(), [0x15, 0x94, 0x2c]); + + assert!(parser.next_ancillary().is_none()); + } + + #[test] + fn cea608_can_not_parse() { + let mut parser = + VideoVBIParser::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + let line_buf = init_line_buf(&parser, &[0x00, 0xf0, 0x0f, 0x00, 0x61, 0x01, 0x20, 0x10]); + parser.add_line(&line_buf).unwrap(); + + assert!(parser.next_ancillary().is_none()); + } + + #[test] + fn cea608_insufficient_line_buf_len() { + let mut parser = + VideoVBIParser::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + let line_buf = vec![0; 10]; + + assert_eq!( + parser.add_line(&line_buf).unwrap_err(), + VideoVBIError::InsufficientLineBufLen { + found: 10, + expected: parser.line_buffer_len() + }, + ); + } + + #[test] + fn cea708_component() { + let mut parser = + VideoVBIParser::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + let line_buf = init_line_buf( + &parser, + &[ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xf0, 0x3f, 0x00, 0x84, 0x05, 0x00, 0x01, 0x01, + 0x50, 0x25, 0x00, 0x58, 0x0a, 0x00, 0x69, 0x02, 0x50, 0x25, 0x00, 0xfc, 0x08, 0x00, + 0x43, 0x01, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0x72, 0x02, 0x80, 0x1f, 0x00, 0xf0, + 0x0b, 0x00, 0x94, 0x01, 0xc0, 0x12, 0x00, 0xe4, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, + 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, + 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, + 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, + 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, + 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, + 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, + 0x00, 0xe8, 0x0b, 0x00, 0x00, 0x02, 0x00, 0x20, 0x00, 0xd0, 0x09, 0x00, 0x00, 0x02, + 0x00, 0x20, 0x00, 0x6c, 0x08, 0x00, 0xb7, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ], + ); + parser.add_line(&line_buf).unwrap(); + + let video_anc = parser.next_ancillary().unwrap().unwrap(); + assert_eq!(video_anc.did16(), VideoAncillaryDID16::S334Eia708); + assert_eq!( + video_anc.data(), + [ + 0x96, 0x69, 0x55, 0x3f, 0x43, 0x00, 0x00, 0x72, 0xf8, 0xfc, 0x94, 0x2c, 0xf9, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x74, 0x00, 0x00, + 0x1b, + ] + ); + + assert!(parser.next_ancillary().is_none()); + } + + #[test] + fn cea608_and_cea708_component() { + let mut parser = + VideoVBIParser::try_new(VideoFormat::V210, VBI_HD_MIN_PIXEL_WIDTH).unwrap(); + let mut line_buf = vec![0; parser.line_buffer_len()]; + let anc_buf = [ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0xf0, 0x3f, 0x00, 0x84, 0x05, 0x00, 0x02, 0x01, + 0x30, 0x20, 0x00, 0x00, 0x06, 0x00, 0x94, 0x01, 0xc0, 0x12, 0x00, 0x98, 0x0a, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0x00, 0xfc, 0x0f, 0x00, 0x61, 0x01, 0x10, 0x10, 0x00, 0x54, + 0x09, 0x00, 0x96, 0x02, 0x90, 0x26, 0x00, 0x54, 0x09, 0x00, 0x3f, 0x02, 0x30, 0x14, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x02, 0x20, 0x27, 0x00, 0xe0, 0x07, 0x00, 0xfc, 0x02, + 0x40, 0x19, 0x00, 0xb0, 0x04, 0x00, 0xf9, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, + 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, + 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, + 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, + 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, + 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, + 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, + 0xfa, 0x02, 0x00, 0x20, 0x00, 0x00, 0x08, 0x00, 0x74, 0x02, 0x00, 0x20, 0x00, 0x00, + 0x08, 0x00, 0x1b, 0x02, 0x70, 0x2b, + ]; + line_buf[0..anc_buf.len()].copy_from_slice(&anc_buf); + parser.add_line(&line_buf).unwrap(); + + let mut anc_iter = parser.iter(); + + let video_anc = anc_iter.next().unwrap().unwrap(); + assert_eq!(video_anc.did16(), VideoAncillaryDID16::S334Eia608); + assert_eq!(video_anc.data(), [0x80, 0x94, 0x2c]); + + let video_anc = anc_iter.next().unwrap().unwrap(); + assert_eq!(video_anc.did16(), VideoAncillaryDID16::S334Eia708); + assert_eq!( + video_anc.data(), + [ + 0x96, 0x69, 0x55, 0x3f, 0x43, 0x00, 0x00, 0x72, 0xf8, 0xfc, 0x94, 0x2c, 0xf9, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, + 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, + 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, + 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x74, 0x00, 0x00, + 0x1b, + ] + ); + + assert!(anc_iter.next().is_none()); + } +}