use crate::event::MediaTime;
use alloc::string::String;
#[cfg(any(feature = "cc-data", feature = "teletext"))]
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cue {
pub start: MediaTime,
pub end: MediaTime,
pub text: String,
}
#[cfg(any(feature = "cc-data", feature = "teletext"))]
struct DiffState {
last_pts: Option<u64>,
epoch: u64,
open: Option<(u64, String)>,
}
#[cfg(any(feature = "cc-data", feature = "teletext"))]
impl DiffState {
fn new() -> Self {
DiffState {
last_pts: None,
epoch: 0,
open: None,
}
}
fn unroll(&mut self, pts33: u64) -> u64 {
crate::timeline::unroll_pts(&mut self.last_pts, &mut self.epoch, pts33)
}
fn observe(&mut self, ticks: u64, text: String, cues: &mut Vec<Cue>) {
let changed = match &self.open {
Some((_, cur)) => *cur != text,
None => !text.is_empty(),
};
if !changed {
return;
}
if let Some((start, prev)) = self.open.take() {
if !prev.is_empty() {
cues.push(Cue {
start: MediaTime(start),
end: MediaTime(ticks),
text: prev,
});
}
}
if !text.is_empty() {
self.open = Some((ticks, text));
}
}
fn finalize(&mut self, ticks: u64, cues: &mut Vec<Cue>) {
if let Some((start, prev)) = self.open.take() {
if !prev.is_empty() {
cues.push(Cue {
start: MediaTime(start),
end: MediaTime(ticks),
text: prev,
});
}
}
}
}
#[cfg(feature = "cc-data")]
#[cfg_attr(docsrs, doc(cfg(feature = "cc-data")))]
pub struct Cea608CueExtractor {
decoder: cc_data::decode::Cea608Decoder,
channel: cc_data::decode::Cea608Channel,
state: DiffState,
cues: Vec<Cue>,
}
#[cfg(feature = "cc-data")]
#[cfg_attr(docsrs, doc(cfg(feature = "cc-data")))]
impl Cea608CueExtractor {
#[must_use]
pub fn new(channel: cc_data::decode::Cea608Channel) -> Self {
Cea608CueExtractor {
decoder: cc_data::decode::Cea608Decoder::new(),
channel,
state: DiffState::new(),
cues: Vec::new(),
}
}
pub fn push_frame(&mut self, pts33: u64, triplets: &[cc_data::CcTriplet]) {
let ticks = self.state.unroll(pts33);
self.decoder
.push_triplets(triplets.iter().filter(|t| t.cc_type.is_cea608()));
let text = self.decoder.channel_text(self.channel);
self.state.observe(ticks, text, &mut self.cues);
}
pub fn finalize(&mut self, end_pts33: u64) {
let ticks = self.state.unroll(end_pts33);
self.state.finalize(ticks, &mut self.cues);
}
#[must_use]
pub fn cues(&self) -> &[Cue] {
&self.cues
}
#[must_use]
pub fn into_cues(self) -> Vec<Cue> {
self.cues
}
}
#[cfg(feature = "cc-data")]
#[cfg_attr(docsrs, doc(cfg(feature = "cc-data")))]
pub struct Cea708CueExtractor {
decoder: cc_data::decode::Cea708Decoder,
service_number: usize,
state: DiffState,
cues: Vec<Cue>,
}
#[cfg(feature = "cc-data")]
#[cfg_attr(docsrs, doc(cfg(feature = "cc-data")))]
impl Cea708CueExtractor {
#[must_use]
pub fn new(service_number: usize) -> Self {
Cea708CueExtractor {
decoder: cc_data::decode::Cea708Decoder::new(),
service_number,
state: DiffState::new(),
cues: Vec::new(),
}
}
pub fn push_frame(&mut self, pts33: u64, triplets: &[cc_data::CcTriplet]) {
let ticks = self.state.unroll(pts33);
self.decoder
.push_triplets(triplets.iter().filter(|t| t.cc_type.is_cea708()));
let text = self.decoder.service_text(self.service_number);
self.state.observe(ticks, text, &mut self.cues);
}
pub fn finalize(&mut self, end_pts33: u64) {
let ticks = self.state.unroll(end_pts33);
self.state.finalize(ticks, &mut self.cues);
}
#[must_use]
pub fn cues(&self) -> &[Cue] {
&self.cues
}
#[must_use]
pub fn into_cues(self) -> Vec<Cue> {
self.cues
}
}
#[cfg(feature = "teletext")]
#[cfg_attr(docsrs, doc(cfg(feature = "teletext")))]
pub struct TeletextCueExtractor {
assembler: crate::webvtt::teletext::PageAssembler,
state: DiffState,
cues: Vec<Cue>,
}
#[cfg(feature = "teletext")]
#[cfg_attr(docsrs, doc(cfg(feature = "teletext")))]
impl TeletextCueExtractor {
#[must_use]
pub fn new(magazine: u8, page: u8) -> Self {
TeletextCueExtractor {
assembler: crate::webvtt::teletext::PageAssembler::new(magazine, page),
state: DiffState::new(),
cues: Vec::new(),
}
}
pub fn push_frame(&mut self, pts33: u64, fields: &[dvb_vbi::TeletextDataField]) {
let ticks = self.state.unroll(pts33);
for field in fields {
self.assembler.push(field);
}
let text = self.assembler.display_text();
self.state.observe(ticks, text, &mut self.cues);
}
pub fn finalize(&mut self, end_pts33: u64) {
let ticks = self.state.unroll(end_pts33);
self.state.finalize(ticks, &mut self.cues);
}
#[must_use]
pub fn cues(&self) -> &[Cue] {
&self.cues
}
#[must_use]
pub fn into_cues(self) -> Vec<Cue> {
self.cues
}
}
#[cfg(all(test, feature = "cc-data"))]
mod tests {
use super::*;
use cc_data::{CcTriplet, CcType};
fn t608(b1: u8, b2: u8) -> CcTriplet {
CcTriplet {
cc_valid: true,
cc_type: CcType::Ntsc608Field1,
cc_data_1: b1,
cc_data_2: b2,
}
}
#[test]
fn pop_on_boundary_is_eoc_and_edm() {
let mut ex = Cea608CueExtractor::new(cc_data::decode::Cea608Channel::Cc1);
ex.push_frame(0, &[t608(0x14, 0x20)]); ex.push_frame(1, &[t608(0x14, 0x70)]); assert!(ex.cues().is_empty(), "composing must not yet emit a cue");
ex.push_frame(2, &[t608(b'H', b'I')]);
assert!(
ex.cues().is_empty(),
"non-displayed writes must not emit a cue"
);
ex.push_frame(3, &[t608(0x14, 0x2F)]); assert!(
ex.cues().is_empty(),
"the cue is open, not yet closed by an erase/replace"
);
ex.push_frame(4, &[t608(0x14, 0x2C)]); assert_eq!(ex.cues().len(), 1);
assert_eq!(ex.cues()[0].start, MediaTime(3));
assert_eq!(ex.cues()[0].end, MediaTime(4));
assert_eq!(ex.cues()[0].text, "HI");
}
#[test]
fn finalize_closes_open_cue() {
let mut ex = Cea608CueExtractor::new(cc_data::decode::Cea608Channel::Cc1);
ex.push_frame(0, &[t608(0x14, 0x20)]);
ex.push_frame(1, &[t608(0x14, 0x70)]);
ex.push_frame(2, &[t608(b'O', b'K')]);
ex.push_frame(3, &[t608(0x14, 0x2F)]); assert!(ex.cues().is_empty());
ex.finalize(10);
assert_eq!(ex.cues().len(), 1);
assert_eq!(ex.cues()[0].start, MediaTime(3));
assert_eq!(ex.cues()[0].end, MediaTime(10));
assert_eq!(ex.cues()[0].text, "OK");
}
#[test]
fn cea708_service1_basic_window() {
let mut ex = Cea708CueExtractor::new(1);
let packet: [u8; 9] = [0x05, 0x27, 0x9A, 0x38, 0x4A, 0xD1, 0x8B, 0x0F, 0x11];
let t0 = CcTriplet {
cc_valid: true,
cc_type: CcType::Dtvcc708Start,
cc_data_1: packet[0],
cc_data_2: packet[1],
};
let rest: alloc::vec::Vec<CcTriplet> = packet[2..]
.chunks(2)
.map(|c| CcTriplet {
cc_valid: true,
cc_type: CcType::Dtvcc708Data,
cc_data_1: c[0],
cc_data_2: *c.get(1).unwrap_or(&0),
})
.collect();
let mut triplets = alloc::vec![t0];
triplets.extend(rest);
ex.push_frame(0, &triplets);
assert!(ex.cues().is_empty());
}
}