use oximedia_core::{OxiError, OxiResult};
pub const SCTE35_DEFAULT_PID: u16 = 0x1FFF;
const SCTE35_TABLE_ID: u8 = 0xFC;
#[derive(Debug, Clone)]
pub struct Scte35Config {
pub pid: u16,
pub verify_crc: bool,
}
impl Default for Scte35Config {
fn default() -> Self {
Self {
pid: SCTE35_DEFAULT_PID,
verify_crc: true,
}
}
}
impl Scte35Config {
#[must_use]
pub const fn with_pid(mut self, pid: u16) -> Self {
self.pid = pid;
self
}
#[must_use]
pub const fn without_crc_check(mut self) -> Self {
self.verify_crc = false;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpliceTime {
pub time_specified: bool,
pub pts_time: Option<u64>,
}
impl SpliceTime {
pub const IMMEDIATE: Self = Self {
time_specified: false,
pts_time: None,
};
#[must_use]
pub fn at_pts(pts: u64) -> Self {
Self {
time_specified: true,
pts_time: Some(pts),
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn seconds(&self) -> Option<f64> {
self.pts_time.map(|pts| pts as f64 / 90000.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BreakDuration {
pub auto_return: bool,
pub duration: u64,
}
impl BreakDuration {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn seconds(&self) -> f64 {
self.duration as f64 / 90000.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpliceCommand {
Null,
Schedule {
event_count: u8,
},
Insert(SpliceInsert),
TimeSignal(SpliceTime),
BandwidthReservation,
Private {
identifier: u32,
data: Vec<u8>,
},
Unknown {
command_type: u8,
data: Vec<u8>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpliceInsert {
pub event_id: u32,
pub event_cancel: bool,
pub out_of_network: bool,
pub program_splice: bool,
pub duration: Option<BreakDuration>,
pub immediate: bool,
pub splice_time: Option<SpliceTime>,
pub unique_program_id: u16,
pub avail_num: u8,
pub avails_expected: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpliceDescriptor {
pub tag: u8,
pub length: u8,
pub identifier: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpliceInfoSection {
pub protocol_version: u8,
pub encrypted: bool,
pub encryption_algorithm: u8,
pub pts_adjustment: u64,
pub cw_index: u8,
pub tier: u16,
pub splice_command: SpliceCommand,
pub descriptors: Vec<SpliceDescriptor>,
pub crc32: u32,
}
#[derive(Debug, Clone)]
pub struct Scte35Parser {
config: Scte35Config,
parse_count: u64,
}
impl Scte35Parser {
#[must_use]
pub fn new(config: Scte35Config) -> Self {
Self {
config,
parse_count: 0,
}
}
#[must_use]
pub fn default_parser() -> Self {
Self::new(Scte35Config::default())
}
#[must_use]
pub const fn pid(&self) -> u16 {
self.config.pid
}
#[must_use]
pub const fn parse_count(&self) -> u64 {
self.parse_count
}
pub fn parse(&mut self, data: &[u8]) -> OxiResult<SpliceInfoSection> {
if data.len() < 15 {
return Err(OxiError::InvalidData(
"SCTE-35 section too short".to_string(),
));
}
if data[0] != SCTE35_TABLE_ID {
return Err(OxiError::InvalidData(format!(
"Invalid SCTE-35 table ID: expected 0xFC, got 0x{:02X}",
data[0]
)));
}
let section_length = (((u16::from(data[1]) & 0x0F) << 8) | u16::from(data[2])) as usize;
if data.len() < section_length + 3 {
return Err(OxiError::InvalidData(format!(
"SCTE-35 section length mismatch: declared {}, available {}",
section_length + 3,
data.len()
)));
}
let section_data = &data[..section_length + 3];
if self.config.verify_crc && section_data.len() >= 4 {
let crc_offset = section_data.len() - 4;
let expected_crc = u32::from_be_bytes([
section_data[crc_offset],
section_data[crc_offset + 1],
section_data[crc_offset + 2],
section_data[crc_offset + 3],
]);
let computed_crc = compute_crc32(§ion_data[..crc_offset]);
if computed_crc != expected_crc {
return Err(OxiError::InvalidData(format!(
"SCTE-35 CRC mismatch: expected 0x{expected_crc:08X}, computed 0x{computed_crc:08X}"
)));
}
}
let protocol_version = data[3];
let encrypted = (data[4] & 0x80) != 0;
let encryption_algorithm = (data[4] >> 1) & 0x3F;
let pts_adjustment = (u64::from(data[4] & 0x01) << 32)
| (u64::from(data[5]) << 24)
| (u64::from(data[6]) << 16)
| (u64::from(data[7]) << 8)
| u64::from(data[8]);
let cw_index = data[9];
let tier = ((u16::from(data[10]) << 4) | (u16::from(data[11]) >> 4)) & 0x0FFF;
let splice_command_length =
(((u16::from(data[11]) & 0x0F) << 8) | u16::from(data[12])) as usize;
let splice_command_type = data[13];
let cmd_start = 14;
let cmd_end = if splice_command_length == 0xFFF {
section_data.len().saturating_sub(4)
} else {
(cmd_start + splice_command_length).min(section_data.len().saturating_sub(4))
};
let cmd_data = if cmd_end > cmd_start && cmd_end <= section_data.len() {
§ion_data[cmd_start..cmd_end]
} else {
&[]
};
let splice_command = self.parse_command(splice_command_type, cmd_data)?;
let desc_start = cmd_end;
let descriptors = if desc_start + 2 <= section_data.len().saturating_sub(4) {
let desc_loop_length =
u16::from_be_bytes([section_data[desc_start], section_data[desc_start + 1]])
as usize;
self.parse_descriptors(§ion_data[desc_start + 2..], desc_loop_length)
} else {
Vec::new()
};
let crc_offset = section_data.len().saturating_sub(4);
let crc32 = if crc_offset + 4 <= section_data.len() {
u32::from_be_bytes([
section_data[crc_offset],
section_data[crc_offset + 1],
section_data[crc_offset + 2],
section_data[crc_offset + 3],
])
} else {
0
};
self.parse_count += 1;
Ok(SpliceInfoSection {
protocol_version,
encrypted,
encryption_algorithm,
pts_adjustment,
cw_index,
tier,
splice_command,
descriptors,
crc32,
})
}
fn parse_command(&self, command_type: u8, data: &[u8]) -> OxiResult<SpliceCommand> {
match command_type {
0x00 => Ok(SpliceCommand::Null),
0x04 => {
let count = data.first().copied().unwrap_or_default();
Ok(SpliceCommand::Schedule { event_count: count })
}
0x05 => self.parse_splice_insert(data),
0x06 => {
let time = self.parse_splice_time(data);
Ok(SpliceCommand::TimeSignal(time))
}
0x07 => Ok(SpliceCommand::BandwidthReservation),
0xFF => {
let identifier = if data.len() >= 4 {
u32::from_be_bytes([data[0], data[1], data[2], data[3]])
} else {
0
};
let payload = if data.len() > 4 {
data[4..].to_vec()
} else {
Vec::new()
};
Ok(SpliceCommand::Private {
identifier,
data: payload,
})
}
_ => Ok(SpliceCommand::Unknown {
command_type,
data: data.to_vec(),
}),
}
}
fn parse_splice_insert(&self, data: &[u8]) -> OxiResult<SpliceCommand> {
if data.len() < 5 {
return Err(OxiError::InvalidData("splice_insert too short".to_string()));
}
let event_id = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let event_cancel = (data[4] & 0x80) != 0;
if event_cancel {
return Ok(SpliceCommand::Insert(SpliceInsert {
event_id,
event_cancel: true,
out_of_network: false,
program_splice: false,
duration: None,
immediate: false,
splice_time: None,
unique_program_id: 0,
avail_num: 0,
avails_expected: 0,
}));
}
if data.len() < 10 {
return Err(OxiError::InvalidData(
"splice_insert body too short".to_string(),
));
}
let out_of_network = (data[5] & 0x80) != 0;
let program_splice = (data[5] & 0x40) != 0;
let duration_flag = (data[5] & 0x20) != 0;
let immediate = (data[5] & 0x10) != 0;
let mut offset = 6;
let splice_time = if program_splice && !immediate {
if offset < data.len() {
let time = self.parse_splice_time(&data[offset..]);
let time_len = if time.time_specified { 5 } else { 1 };
offset += time_len;
Some(time)
} else {
None
}
} else {
None
};
let duration = if duration_flag && offset + 5 <= data.len() {
let auto_return = (data[offset] & 0x80) != 0;
let dur = (u64::from(data[offset] & 0x01) << 32)
| (u64::from(data[offset + 1]) << 24)
| (u64::from(data[offset + 2]) << 16)
| (u64::from(data[offset + 3]) << 8)
| u64::from(data[offset + 4]);
offset += 5;
Some(BreakDuration {
auto_return,
duration: dur,
})
} else {
None
};
let unique_program_id = if offset + 2 <= data.len() {
let v = u16::from_be_bytes([data[offset], data[offset + 1]]);
offset += 2;
v
} else {
0
};
let avail_num = if offset < data.len() {
let v = data[offset];
offset += 1;
v
} else {
0
};
let avails_expected = if offset < data.len() { data[offset] } else { 0 };
Ok(SpliceCommand::Insert(SpliceInsert {
event_id,
event_cancel: false,
out_of_network,
program_splice,
duration,
immediate,
splice_time,
unique_program_id,
avail_num,
avails_expected,
}))
}
fn parse_splice_time(&self, data: &[u8]) -> SpliceTime {
if data.is_empty() {
return SpliceTime::IMMEDIATE;
}
let time_specified = (data[0] & 0x80) != 0;
if time_specified && data.len() >= 5 {
let pts = (u64::from(data[0] & 0x01) << 32)
| (u64::from(data[1]) << 24)
| (u64::from(data[2]) << 16)
| (u64::from(data[3]) << 8)
| u64::from(data[4]);
SpliceTime::at_pts(pts)
} else {
SpliceTime::IMMEDIATE
}
}
fn parse_descriptors(&self, data: &[u8], loop_length: usize) -> Vec<SpliceDescriptor> {
let mut descriptors = Vec::new();
let mut offset = 0;
let end = loop_length.min(data.len());
while offset + 6 <= end {
let tag = data[offset];
let length = data[offset + 1];
let total_len = 2 + length as usize;
if offset + total_len > end {
break;
}
let identifier = if length >= 4 {
u32::from_be_bytes([
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
])
} else {
0
};
let desc_data = if length > 4 {
data[offset + 6..offset + total_len].to_vec()
} else {
Vec::new()
};
descriptors.push(SpliceDescriptor {
tag,
length,
identifier,
data: desc_data,
});
offset += total_len;
}
descriptors
}
}
const CRC32_POLY: u32 = 0x04C1_1DB7;
fn compute_crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= u32::from(byte) << 24;
for _ in 0..8 {
if crc & 0x8000_0000 != 0 {
crc = (crc << 1) ^ CRC32_POLY;
} else {
crc <<= 1;
}
}
}
crc
}
pub fn parse_splice_info_section(data: &[u8]) -> OxiResult<SpliceInfoSection> {
Scte35Parser::default_parser().parse(data)
}
#[cfg(test)]
mod tests {
use super::*;
fn build_minimal_scte35_section(command_type: u8, command_data: &[u8]) -> Vec<u8> {
let mut section = Vec::new();
section.push(SCTE35_TABLE_ID);
section.push(0x00);
section.push(0x00);
section.push(0x00);
section.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00]);
section.push(0x00);
let cmd_len = command_data.len() as u16;
let tier: u16 = 0x0FFF; section.push(((tier >> 4) & 0xFF) as u8);
section.push((((tier & 0x0F) << 4) | ((cmd_len >> 8) & 0x0F)) as u8);
section.push((cmd_len & 0xFF) as u8);
section.push(command_type);
section.extend_from_slice(command_data);
section.extend_from_slice(&[0x00, 0x00]);
let section_length = section.len() - 3 + 4;
section[1] = ((section_length >> 8) & 0x0F) as u8;
section[2] = (section_length & 0xFF) as u8;
let crc = compute_crc32(§ion);
section.extend_from_slice(&crc.to_be_bytes());
section
}
#[test]
fn test_scte35_config_default() {
let cfg = Scte35Config::default();
assert_eq!(cfg.pid, SCTE35_DEFAULT_PID);
assert!(cfg.verify_crc);
}
#[test]
fn test_scte35_config_with_pid() {
let cfg = Scte35Config::default().with_pid(0x100);
assert_eq!(cfg.pid, 0x100);
}
#[test]
fn test_scte35_config_without_crc() {
let cfg = Scte35Config::default().without_crc_check();
assert!(!cfg.verify_crc);
}
#[test]
fn test_splice_time_immediate() {
let t = SpliceTime::IMMEDIATE;
assert!(!t.time_specified);
assert!(t.pts_time.is_none());
assert!(t.seconds().is_none());
}
#[test]
fn test_splice_time_at_pts() {
let t = SpliceTime::at_pts(90000);
assert!(t.time_specified);
assert_eq!(t.pts_time, Some(90000));
let secs = t.seconds().expect("should have seconds");
assert!((secs - 1.0).abs() < 0.001);
}
#[test]
fn test_break_duration_seconds() {
let bd = BreakDuration {
auto_return: true,
duration: 2_700_000, };
assert!((bd.seconds() - 30.0).abs() < 0.001);
}
#[test]
fn test_parse_null_command() {
let data = build_minimal_scte35_section(0x00, &[]);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
assert_eq!(section.splice_command, SpliceCommand::Null);
assert_eq!(section.protocol_version, 0);
assert!(!section.encrypted);
assert_eq!(parser.parse_count(), 1);
}
#[test]
fn test_parse_bandwidth_reservation() {
let data = build_minimal_scte35_section(0x07, &[]);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
assert_eq!(section.splice_command, SpliceCommand::BandwidthReservation);
}
#[test]
fn test_parse_time_signal() {
let cmd = [0x80 | 0x00, 0x00, 0x01, 0x5F, 0x90]; let data = build_minimal_scte35_section(0x06, &cmd);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
if let SpliceCommand::TimeSignal(time) = §ion.splice_command {
assert!(time.time_specified);
assert_eq!(time.pts_time, Some(90000));
} else {
panic!("Expected TimeSignal");
}
}
#[test]
fn test_parse_splice_insert_cancel() {
let mut cmd = Vec::new();
cmd.extend_from_slice(&0x12345678u32.to_be_bytes()); cmd.push(0x80); let data = build_minimal_scte35_section(0x05, &cmd);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
if let SpliceCommand::Insert(insert) = §ion.splice_command {
assert_eq!(insert.event_id, 0x12345678);
assert!(insert.event_cancel);
} else {
panic!("Expected Insert");
}
}
#[test]
fn test_parse_splice_insert_immediate() {
let mut cmd = Vec::new();
cmd.extend_from_slice(&1u32.to_be_bytes()); cmd.push(0x00); cmd.push(0xD0); cmd.extend_from_slice(&100u16.to_be_bytes());
cmd.push(0);
cmd.push(1);
let data = build_minimal_scte35_section(0x05, &cmd);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
if let SpliceCommand::Insert(insert) = §ion.splice_command {
assert_eq!(insert.event_id, 1);
assert!(!insert.event_cancel);
assert!(insert.out_of_network);
assert!(insert.program_splice);
assert!(insert.immediate);
assert!(insert.splice_time.is_none());
assert_eq!(insert.unique_program_id, 100);
assert_eq!(insert.avails_expected, 1);
} else {
panic!("Expected Insert");
}
}
#[test]
fn test_parse_private_command() {
let mut cmd = Vec::new();
cmd.extend_from_slice(b"CUEI"); cmd.extend_from_slice(&[0x01, 0x02, 0x03]); let data = build_minimal_scte35_section(0xFF, &cmd);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
if let SpliceCommand::Private { identifier, data } = §ion.splice_command {
assert_eq!(*identifier, 0x43554549); assert_eq!(data, &[0x01, 0x02, 0x03]);
} else {
panic!("Expected Private");
}
}
#[test]
fn test_parse_unknown_command() {
let data = build_minimal_scte35_section(0x42, &[0xAA, 0xBB]);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
if let SpliceCommand::Unknown { command_type, data } = §ion.splice_command {
assert_eq!(*command_type, 0x42);
assert_eq!(data, &[0xAA, 0xBB]);
} else {
panic!("Expected Unknown");
}
}
#[test]
fn test_parse_too_short() {
let mut parser = Scte35Parser::default_parser();
let result = parser.parse(&[0xFC, 0x00]);
assert!(result.is_err());
}
#[test]
fn test_parse_wrong_table_id() {
let mut data = build_minimal_scte35_section(0x00, &[]);
data[0] = 0x00; let mut parser = Scte35Parser::default_parser();
let result = parser.parse(&data);
assert!(result.is_err());
}
#[test]
fn test_parse_crc_failure() {
let mut data = build_minimal_scte35_section(0x00, &[]);
let len = data.len();
data[len - 1] ^= 0xFF;
let mut parser = Scte35Parser::new(Scte35Config::default());
let result = parser.parse(&data);
assert!(result.is_err());
}
#[test]
fn test_parse_no_crc_check() {
let mut data = build_minimal_scte35_section(0x00, &[]);
let len = data.len();
data[len - 1] ^= 0xFF; let mut parser = Scte35Parser::new(Scte35Config::default().without_crc_check());
let result = parser.parse(&data);
assert!(result.is_ok());
}
#[test]
fn test_compute_crc32_empty() {
let crc = compute_crc32(&[]);
assert_eq!(crc, 0xFFFF_FFFF);
}
#[test]
fn test_compute_crc32_known() {
let crc = compute_crc32(b"123456789");
assert_eq!(crc, 0x0376_E6E7);
}
#[test]
fn test_tier_parsing() {
let data = build_minimal_scte35_section(0x00, &[]);
let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
assert_eq!(section.tier, 0x0FFF);
}
#[test]
fn test_schedule_command() {
let data = build_minimal_scte35_section(0x04, &[3]); let mut parser = Scte35Parser::default_parser();
let section = parser.parse(&data).expect("should parse");
if let SpliceCommand::Schedule { event_count } = §ion.splice_command {
assert_eq!(*event_count, 3);
} else {
panic!("Expected Schedule");
}
}
#[test]
fn test_parser_count_increments() {
let mut parser = Scte35Parser::default_parser();
assert_eq!(parser.parse_count(), 0);
let data = build_minimal_scte35_section(0x00, &[]);
let _ = parser.parse(&data);
assert_eq!(parser.parse_count(), 1);
let _ = parser.parse(&data);
assert_eq!(parser.parse_count(), 2);
}
#[test]
fn test_splice_descriptor_parsing() {
let mut section = Vec::new();
section.push(SCTE35_TABLE_ID);
section.push(0x00);
section.push(0x00);
section.push(0x00); section.extend_from_slice(&[0x00; 5]); section.push(0x00); let tier: u16 = 0x0FFF;
let cmd_len: u16 = 0;
section.push(((tier >> 4) & 0xFF) as u8);
section.push((((tier & 0x0F) << 4) | ((cmd_len >> 8) & 0x0F)) as u8);
section.push((cmd_len & 0xFF) as u8);
section.push(0x00);
let desc_data = [0x00, 0x08, 0x43, 0x55, 0x45, 0x49, 0xAA, 0xBB, 0xCC, 0xDD];
let desc_len = desc_data.len() as u16;
section.extend_from_slice(&desc_len.to_be_bytes());
section.extend_from_slice(&desc_data);
let section_length = section.len() - 3 + 4;
section[1] = ((section_length >> 8) & 0x0F) as u8;
section[2] = (section_length & 0xFF) as u8;
let crc = compute_crc32(§ion);
section.extend_from_slice(&crc.to_be_bytes());
let mut parser = Scte35Parser::default_parser();
let result = parser.parse(§ion).expect("should parse");
assert_eq!(result.descriptors.len(), 1);
assert_eq!(result.descriptors[0].tag, 0x00);
assert_eq!(result.descriptors[0].identifier, 0x43554549); assert_eq!(result.descriptors[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]);
}
}