use crate::{SubtitleError, SubtitleResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VideoField {
Field1,
Field2,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VideoFormat {
NTSC,
PAL,
HD720p,
HD1080i,
HD1080p,
UHD4K,
}
impl VideoFormat {
#[must_use]
pub const fn line_count(&self) -> u32 {
match self {
Self::NTSC => 525,
Self::PAL => 625,
Self::HD720p => 720,
Self::HD1080i | Self::HD1080p => 1080,
Self::UHD4K => 2160,
}
}
#[must_use]
pub const fn is_interlaced(&self) -> bool {
matches!(self, Self::NTSC | Self::PAL | Self::HD1080i)
}
#[must_use]
pub const fn line21_field1(&self) -> Option<u32> {
match self {
Self::NTSC => Some(21),
Self::PAL => Some(22),
_ => None, }
}
#[must_use]
pub const fn line21_field2(&self) -> Option<u32> {
match self {
Self::NTSC => Some(284), Self::PAL => Some(335),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FrameRate {
pub num: u32,
pub den: u32,
}
impl FrameRate {
#[must_use]
pub const fn new(num: u32, den: u32) -> Self {
Self { num, den }
}
#[must_use]
pub fn as_float(&self) -> f64 {
self.num as f64 / self.den as f64
}
#[must_use]
pub const fn ntsc() -> Self {
Self::new(30000, 1001)
}
#[must_use]
pub const fn pal() -> Self {
Self::new(25, 1)
}
#[must_use]
pub const fn film() -> Self {
Self::new(24000, 1001)
}
#[must_use]
pub const fn fps24() -> Self {
Self::new(24, 1)
}
#[must_use]
pub const fn fps30() -> Self {
Self::new(30, 1)
}
#[must_use]
pub const fn fps60() -> Self {
Self::new(60, 1)
}
}
pub struct Line21Encoder {
format: VideoFormat,
}
impl Line21Encoder {
pub fn new(format: VideoFormat) -> SubtitleResult<Self> {
if format.line21_field1().is_none() {
return Err(SubtitleError::InvalidFormat(
"Format does not support Line 21 encoding".to_string(),
));
}
Ok(Self { format })
}
#[must_use]
pub fn encode_line21(&self, byte1: u8, byte2: u8, field: VideoField) -> Vec<u8> {
let mut waveform = Vec::with_capacity(512);
for _ in 0..14 {
waveform.push(0x00); waveform.push(0xFF); }
self.encode_bit(&mut waveform, false);
self.encode_bit(&mut waveform, false);
self.encode_bit(&mut waveform, true);
self.encode_byte(&mut waveform, byte1);
self.encode_byte(&mut waveform, byte2);
while waveform.len() < 512 {
waveform.push(0x10); }
waveform
}
fn encode_bit(&self, waveform: &mut Vec<u8>, bit: bool) {
let value = if bit { 0xFF } else { 0x00 };
for _ in 0..4 {
waveform.push(value);
}
}
fn encode_byte(&self, waveform: &mut Vec<u8>, byte: u8) {
for i in 0..8 {
let bit = (byte & (1 << i)) != 0;
self.encode_bit(waveform, bit);
}
}
#[must_use]
pub fn get_line_number(&self, field: VideoField) -> Option<u32> {
match field {
VideoField::Field1 => self.format.line21_field1(),
VideoField::Field2 => self.format.line21_field2(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SeiMessageType {
UserDataRegistered = 4,
UserDataUnregistered = 5,
}
pub struct SeiNalBuilder {
is_h265: bool,
}
impl SeiNalBuilder {
#[must_use]
pub const fn new(is_h265: bool) -> Self {
Self { is_h265 }
}
#[must_use]
pub fn build_cea708_sei(&self, cea708_data: &[u8]) -> Vec<u8> {
let mut sei = Vec::new();
if self.is_h265 {
sei.push(0x4E); sei.push(0x01);
} else {
sei.push(0x06);
}
self.write_sei_size(&mut sei, 4);
let payload_size = 1 + 2 + 1 + cea708_data.len(); self.write_sei_size(&mut sei, payload_size);
sei.push(0xB5);
sei.push(0x00);
sei.push(0x31);
sei.push(0x47);
sei.extend_from_slice(cea708_data);
sei.push(0x80);
self.add_emulation_prevention(&mut sei);
sei
}
fn write_sei_size(&self, buffer: &mut Vec<u8>, mut size: usize) {
while size >= 255 {
buffer.push(0xFF);
size -= 255;
}
buffer.push(size as u8);
}
fn add_emulation_prevention(&self, data: &mut Vec<u8>) {
let mut i = 2;
while i < data.len() {
if data[i - 2] == 0x00 && data[i - 1] == 0x00 && data[i] <= 0x03 {
data.insert(i, 0x03);
i += 1;
}
i += 1;
}
}
}
pub struct Mpeg2UserDataBuilder;
impl Mpeg2UserDataBuilder {
#[must_use]
pub fn build_cea608_user_data(byte1: u8, byte2: u8) -> Vec<u8> {
vec![
0x00, 0x00, 0x01, 0xB2, b'G', b'A', b'9', b'4', 0x03,
0xC1, 0xFF,
0xFC, byte1, byte2, 0xFF,
]
}
#[must_use]
pub fn build_cea708_user_data(cdp: &[u8]) -> Vec<u8> {
let mut user_data = vec![
0x00, 0x00, 0x01, 0xB2, b'G', b'A', b'9', b'4', 0x03,
];
user_data.extend_from_slice(cdp);
user_data
}
}
pub struct A53Validator;
impl A53Validator {
pub fn validate_cea608_pair(byte1: u8, byte2: u8) -> SubtitleResult<()> {
if !Self::check_parity(byte1) {
return Err(SubtitleError::InvalidFormat(
"CEA-608 byte 1 parity error".to_string(),
));
}
if byte2 != 0x80 && !Self::check_parity(byte2) {
return Err(SubtitleError::InvalidFormat(
"CEA-608 byte 2 parity error".to_string(),
));
}
Ok(())
}
fn check_parity(byte: u8) -> bool {
let mut parity = 0u8;
let mut b = byte;
for _ in 0..8 {
parity ^= b & 1;
b >>= 1;
}
parity == 1
}
pub fn validate_cdp(cdp: &[u8]) -> SubtitleResult<()> {
if cdp.len() < 6 {
return Err(SubtitleError::InvalidFormat("CDP too short".to_string()));
}
if cdp[0] != 0x96 {
return Err(SubtitleError::InvalidFormat(
"Invalid CDP identifier".to_string(),
));
}
let stated_length = cdp[1] as usize;
if stated_length + 2 != cdp.len() {
return Err(SubtitleError::InvalidFormat(format!(
"CDP length mismatch: stated={stated_length}, actual={}",
cdp.len() - 2
)));
}
let mut sum = 0u8;
for &byte in cdp {
sum = sum.wrapping_add(byte);
}
if sum != 0 {
return Err(SubtitleError::InvalidFormat(format!(
"CDP checksum error: sum={sum:#04x}"
)));
}
Ok(())
}
}
pub struct FrameRateAdapter {
source_fps: f64,
target_fps: f64,
accumulator: f64,
}
impl FrameRateAdapter {
#[must_use]
pub fn new(source_fps: f64, target_fps: f64) -> Self {
Self {
source_fps,
target_fps,
accumulator: 0.0,
}
}
#[must_use]
pub fn should_output(&mut self) -> bool {
self.accumulator += self.source_fps;
if self.accumulator >= self.target_fps {
self.accumulator -= self.target_fps;
true
} else {
false
}
}
pub fn reset(&mut self) {
self.accumulator = 0.0;
}
#[must_use]
pub fn convert_frame_number(&self, source_frame: u64) -> u64 {
((source_frame as f64) * self.target_fps / self.source_fps).round() as u64
}
#[must_use]
pub fn timestamp_to_frame(timestamp_ms: i64, fps: f64) -> u64 {
((timestamp_ms as f64 / 1000.0) * fps).round() as u64
}
#[must_use]
pub fn frame_to_timestamp(frame: u64, fps: f64) -> i64 {
((frame as f64 / fps) * 1000.0).round() as i64
}
}
pub struct CaptionEmbedder {
format: VideoFormat,
frame_rate: FrameRate,
}
impl CaptionEmbedder {
#[must_use]
pub const fn new(format: VideoFormat, frame_rate: FrameRate) -> Self {
Self { format, frame_rate }
}
pub fn embed_cea608_line21(
&self,
frame_data: &mut [u8],
byte1: u8,
byte2: u8,
field: VideoField,
) -> SubtitleResult<()> {
A53Validator::validate_cea608_pair(byte1, byte2)?;
let encoder = Line21Encoder::new(self.format)?;
let waveform = encoder.encode_line21(byte1, byte2, field);
let line_num = encoder
.get_line_number(field)
.ok_or_else(|| SubtitleError::InvalidFormat("No Line 21 support".to_string()))?;
let line_offset = (line_num as usize) * self.get_line_stride();
let end_offset = (line_offset + waveform.len()).min(frame_data.len());
if line_offset < frame_data.len() {
let copy_len = end_offset - line_offset;
frame_data[line_offset..end_offset].copy_from_slice(&waveform[..copy_len]);
}
Ok(())
}
#[must_use]
fn get_line_stride(&self) -> usize {
match self.format {
VideoFormat::NTSC | VideoFormat::PAL => 720, VideoFormat::HD720p => 1280,
VideoFormat::HD1080i | VideoFormat::HD1080p => 1920,
VideoFormat::UHD4K => 3840,
}
}
#[must_use]
pub fn create_sei_nal(&self, cdp: &[u8], is_h265: bool) -> Vec<u8> {
let builder = SeiNalBuilder::new(is_h265);
builder.build_cea708_sei(cdp)
}
#[must_use]
pub fn create_mpeg2_user_data_608(&self, byte1: u8, byte2: u8) -> Vec<u8> {
Mpeg2UserDataBuilder::build_cea608_user_data(byte1, byte2)
}
#[must_use]
pub fn create_mpeg2_user_data_708(&self, cdp: &[u8]) -> Vec<u8> {
Mpeg2UserDataBuilder::build_cea708_user_data(cdp)
}
#[must_use]
pub fn get_fps(&self) -> f64 {
self.frame_rate.as_float()
}
}
pub struct TimecodeCalculator {
frame_rate: FrameRate,
drop_frame: bool,
}
impl TimecodeCalculator {
#[must_use]
pub const fn new(frame_rate: FrameRate, drop_frame: bool) -> Self {
Self {
frame_rate,
drop_frame,
}
}
#[must_use]
pub fn frame_to_timecode(&self, frame: u64) -> (u8, u8, u8, u8) {
let fps = self.frame_rate.as_float().round() as u64;
let frames_per_minute = fps * 60;
let frames_per_hour = frames_per_minute * 60;
let mut frame_num = frame;
if self.drop_frame && (self.frame_rate.num == 30000 && self.frame_rate.den == 1001) {
let frames_per_10min = frames_per_minute * 10 - 18; let ten_min_groups = frame_num / frames_per_10min;
let remaining = frame_num % frames_per_10min;
if remaining >= 2 {
let minutes_in_group = (remaining - 2) / (frames_per_minute - 2);
frame_num += ten_min_groups * 18 + minutes_in_group * 2;
}
}
let hours = (frame_num / frames_per_hour) as u8;
frame_num %= frames_per_hour;
let minutes = (frame_num / frames_per_minute) as u8;
frame_num %= frames_per_minute;
let seconds = (frame_num / fps) as u8;
let frames = (frame_num % fps) as u8;
(hours, minutes, seconds, frames)
}
#[must_use]
pub fn encode_timecode(&self, hours: u8, minutes: u8, seconds: u8, frames: u8) -> u32 {
let mut tc = 0u32;
tc |= u32::from(Self::to_bcd(frames));
tc |= u32::from(Self::to_bcd(seconds)) << 8;
tc |= u32::from(Self::to_bcd(minutes)) << 16;
tc |= u32::from(Self::to_bcd(hours)) << 24;
if self.drop_frame {
tc |= 0x8000_0000;
}
tc
}
fn to_bcd(value: u8) -> u8 {
((value / 10) << 4) | (value % 10)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line21_encoding() {
let encoder = Line21Encoder::new(VideoFormat::NTSC).expect("should succeed in test");
let waveform = encoder.encode_line21(0xB5, 0xA1, VideoField::Field1);
assert!(!waveform.is_empty());
assert_eq!(waveform.len(), 512);
}
#[test]
fn test_sei_builder() {
let builder = SeiNalBuilder::new(false);
let sei = builder.build_cea708_sei(&[0x96, 0x69]);
assert!(!sei.is_empty());
assert_eq!(sei[0], 0x06); }
#[test]
fn test_parity_check() {
assert!(A53Validator::check_parity(0xB5));
assert!(!A53Validator::check_parity(0x00));
}
#[test]
fn test_frame_rate_conversion() {
let adapter = FrameRateAdapter::new(30.0, 25.0);
assert_eq!(adapter.convert_frame_number(30), 25);
assert_eq!(adapter.convert_frame_number(60), 50);
}
#[test]
fn test_timecode_calculation() {
let calc = TimecodeCalculator::new(FrameRate::ntsc(), false);
let (h, m, s, f) = calc.frame_to_timecode(108000); assert_eq!(h, 1);
assert_eq!(m, 0);
assert_eq!(s, 0);
assert_eq!(f, 0);
}
}