use std::time::Duration;
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
}
const TIER_ALL: u16 = 0x0FFF;
const TABLE_ID: u8 = 0xFC;
fn write_section_header(buf: &mut Vec<u8>, command_type: u8, command_len: u16) {
buf.push(TABLE_ID);
buf.push(0x30); buf.push(0x00); buf.push(0x00); buf.extend_from_slice(&[0x00; 5]); buf.push(0x00); let tier = TIER_ALL;
buf.push(((tier >> 4) & 0xFF) as u8);
buf.push((((tier & 0x0F) << 4) | ((command_len >> 8) & 0x0F)) as u8);
buf.push((command_len & 0xFF) as u8);
buf.push(command_type);
}
fn backpatch_section_length(buf: &mut Vec<u8>) {
let section_length = buf.len() - 3 + 4;
buf[1] = 0x30 | ((section_length >> 8) as u8 & 0x0F);
buf[2] = (section_length & 0xFF) as u8;
}
fn append_crc(buf: &mut Vec<u8>) {
let crc = compute_crc32(buf);
buf.extend_from_slice(&crc.to_be_bytes());
}
fn encode_splice_time(pts: Option<Duration>) -> [u8; 5] {
match pts {
Some(d) => {
let ticks = (d.as_secs_f64() * 90_000.0) as u64 & 0x1_FFFF_FFFF;
[
0x80 | ((ticks >> 32) as u8 & 0x01), ((ticks >> 24) & 0xFF) as u8,
((ticks >> 16) & 0xFF) as u8,
((ticks >> 8) & 0xFF) as u8,
(ticks & 0xFF) as u8,
]
}
None => [
0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
],
}
}
#[must_use]
pub fn emit_time_signal(pts: Option<Duration>) -> Vec<u8> {
let splice_time = encode_splice_time(pts);
let command_len = splice_time.len() as u16;
let mut buf = Vec::with_capacity(24);
write_section_header(&mut buf, 0x06, command_len);
buf.extend_from_slice(&splice_time);
buf.extend_from_slice(&[0x00, 0x00]);
backpatch_section_length(&mut buf);
append_crc(&mut buf);
buf
}
#[must_use]
pub fn emit_splice_null() -> Vec<u8> {
let mut buf = Vec::with_capacity(20);
write_section_header(&mut buf, 0x00, 0x000);
buf.extend_from_slice(&[0x00, 0x00]);
backpatch_section_length(&mut buf);
append_crc(&mut buf);
buf
}
#[derive(Debug, Clone)]
pub struct SpliceInsertConfig {
pub event_id: u32,
pub out_of_network: bool,
pub duration: Option<Duration>,
pub auto_return: bool,
pub splice_pts: Option<Duration>,
pub unique_program_id: u16,
}
#[must_use]
pub fn emit_splice_insert(cfg: &SpliceInsertConfig) -> Vec<u8> {
let mut cmd = Vec::with_capacity(16);
cmd.extend_from_slice(&cfg.event_id.to_be_bytes());
cmd.push(0x7F);
let immediate = cfg.splice_pts.is_none();
let duration_flag = cfg.duration.is_some();
let flags = (if cfg.out_of_network { 0x80 } else { 0x00 })
| 0x40 | (if duration_flag { 0x20 } else { 0x00 })
| (if immediate { 0x10 } else { 0x00 });
cmd.push(flags);
if !immediate {
let splice_time = encode_splice_time(cfg.splice_pts);
cmd.extend_from_slice(&splice_time);
}
if let Some(dur) = cfg.duration {
let ticks = (dur.as_secs_f64() * 90_000.0) as u64 & 0x1_FFFF_FFFF;
let auto_return_flag: u8 = if cfg.auto_return { 0x80 } else { 0x00 };
cmd.push(auto_return_flag | 0x7E | ((ticks >> 32) as u8 & 0x01));
cmd.extend_from_slice(&((ticks & 0xFFFF_FFFF) as u32).to_be_bytes());
}
cmd.extend_from_slice(&cfg.unique_program_id.to_be_bytes());
cmd.push(0x00);
cmd.push(0x01);
let command_len = cmd.len() as u16;
let mut buf = Vec::with_capacity(32);
write_section_header(&mut buf, 0x05, command_len);
buf.extend_from_slice(&cmd);
buf.extend_from_slice(&[0x00, 0x00]);
backpatch_section_length(&mut buf);
append_crc(&mut buf);
buf
}
#[cfg(test)]
mod tests {
use super::*;
use crate::demux::mpegts::scte35::{Scte35Config, Scte35Parser};
fn parse_section(bytes: &[u8]) -> crate::demux::mpegts::scte35::SpliceInfoSection {
let mut parser = Scte35Parser::new(Scte35Config::default());
parser.parse(bytes).expect("should parse emitted section")
}
#[test]
fn test_emit_null_table_id() {
let bytes = emit_splice_null();
assert_eq!(bytes[0], TABLE_ID, "table_id must be 0xFC");
}
#[test]
fn test_emit_null_crc_valid() {
let bytes = emit_splice_null();
let section = parse_section(&bytes);
assert_eq!(
section.splice_command,
crate::demux::mpegts::scte35::SpliceCommand::Null
);
}
#[test]
fn test_emit_null_minimum_length() {
let bytes = emit_splice_null();
assert!(
bytes.len() >= 19,
"section too short: {} bytes",
bytes.len()
);
}
#[test]
fn test_emit_time_signal_no_pts() {
let bytes = emit_time_signal(None);
let section = parse_section(&bytes);
if let crate::demux::mpegts::scte35::SpliceCommand::TimeSignal(t) = section.splice_command {
assert!(!t.time_specified, "time_specified_flag should be 0");
assert!(t.pts_time.is_none());
} else {
panic!("expected TimeSignal, got {:?}", section.splice_command);
}
}
#[test]
fn test_emit_time_signal_with_pts_roundtrip() {
let pts = Duration::from_secs(5);
let bytes = emit_time_signal(Some(pts));
let section = parse_section(&bytes);
if let crate::demux::mpegts::scte35::SpliceCommand::TimeSignal(t) = section.splice_command {
assert!(t.time_specified, "time_specified_flag should be 1");
let ticks = t.pts_time.expect("pts_time must be Some");
assert_eq!(ticks, 450_000, "expected 450 000, got {ticks}");
} else {
panic!("expected TimeSignal, got {:?}", section.splice_command);
}
}
#[test]
fn test_emit_time_signal_pts_30s() {
let bytes = emit_time_signal(Some(Duration::from_secs(30)));
let section = parse_section(&bytes);
if let crate::demux::mpegts::scte35::SpliceCommand::TimeSignal(t) = section.splice_command {
let ticks = t.pts_time.expect("pts_time must be Some");
assert_eq!(ticks, 2_700_000, "30 s × 90 000 Hz = 2 700 000 ticks");
} else {
panic!("expected TimeSignal");
}
}
#[test]
fn test_emit_time_signal_crc_field_matches() {
let bytes = emit_time_signal(Some(Duration::from_millis(100)));
let stored_crc = u32::from_be_bytes([
bytes[bytes.len() - 4],
bytes[bytes.len() - 3],
bytes[bytes.len() - 2],
bytes[bytes.len() - 1],
]);
let computed = compute_crc32(&bytes[..bytes.len() - 4]);
assert_eq!(stored_crc, computed, "stored CRC must match recomputed CRC");
}
#[test]
fn test_emit_splice_insert_immediate_out_of_network() {
let cfg = SpliceInsertConfig {
event_id: 0xDEAD_BEEF,
out_of_network: true,
duration: None,
auto_return: false,
splice_pts: None, unique_program_id: 7,
};
let bytes = emit_splice_insert(&cfg);
let section = parse_section(&bytes);
if let crate::demux::mpegts::scte35::SpliceCommand::Insert(ins) = section.splice_command {
assert_eq!(ins.event_id, 0xDEAD_BEEF);
assert!(ins.out_of_network);
assert!(ins.program_splice);
assert!(ins.immediate);
assert!(ins.duration.is_none());
assert_eq!(ins.unique_program_id, 7);
} else {
panic!("expected Insert, got {:?}", section.splice_command);
}
}
#[test]
fn test_emit_splice_insert_with_duration_and_pts() {
let cfg = SpliceInsertConfig {
event_id: 1,
out_of_network: true,
duration: Some(Duration::from_secs(30)),
auto_return: true,
splice_pts: Some(Duration::from_secs(10)),
unique_program_id: 100,
};
let bytes = emit_splice_insert(&cfg);
let section = parse_section(&bytes);
if let crate::demux::mpegts::scte35::SpliceCommand::Insert(ins) = section.splice_command {
assert_eq!(ins.event_id, 1);
assert!(ins.out_of_network);
assert!(!ins.immediate);
let dur = ins.duration.expect("duration must be present");
assert!(dur.auto_return, "auto_return should be set");
assert_eq!(dur.duration, 2_700_000);
} else {
panic!("expected Insert, got {:?}", section.splice_command);
}
}
#[test]
fn test_emit_splice_insert_crc_valid() {
let cfg = SpliceInsertConfig {
event_id: 42,
out_of_network: false,
duration: None,
auto_return: false,
splice_pts: None,
unique_program_id: 0,
};
let bytes = emit_splice_insert(&cfg);
let stored = u32::from_be_bytes([
bytes[bytes.len() - 4],
bytes[bytes.len() - 3],
bytes[bytes.len() - 2],
bytes[bytes.len() - 1],
]);
let computed = compute_crc32(&bytes[..bytes.len() - 4]);
assert_eq!(stored, computed);
}
#[test]
fn test_crc32_empty_input() {
assert_eq!(compute_crc32(&[]), 0xFFFF_FFFF);
}
#[test]
fn test_crc32_known_value() {
assert_eq!(compute_crc32(b"123456789"), 0x0376_E6E7);
}
#[test]
fn test_encode_splice_time_none() {
let bytes = encode_splice_time(None);
assert_eq!(bytes[0], 0x7F, "time_specified_flag should be 0");
}
#[test]
fn test_encode_splice_time_1s() {
let bytes = encode_splice_time(Some(Duration::from_secs(1)));
assert_eq!(bytes[0] & 0x80, 0x80, "time_specified_flag should be 1");
let pts = (u64::from(bytes[0] & 0x01) << 32)
| (u64::from(bytes[1]) << 24)
| (u64::from(bytes[2]) << 16)
| (u64::from(bytes[3]) << 8)
| u64::from(bytes[4]);
assert_eq!(pts, 90_000, "1 s × 90 000 Hz = 90 000 ticks");
}
}