#[derive(Debug, Clone)]
pub struct Scte35Marker {
pub time_seconds: f64,
pub out_of_network: bool,
pub event_id: Option<u32>,
pub break_duration_seconds: Option<f64>,
}
impl Scte35Marker {
pub fn cue_out(time_seconds: f64) -> Self {
Self { time_seconds, out_of_network: true, event_id: None, break_duration_seconds: None }
}
pub fn cue_in(time_seconds: f64) -> Self {
Self { time_seconds, out_of_network: false, event_id: None, break_duration_seconds: None }
}
}
pub fn build_splice_insert(marker: &Scte35Marker) -> Vec<u8> {
let event_id = marker.event_id.unwrap_or_else(|| {
(marker.time_seconds * 1000.0).round() as u32
});
let pts = (marker.time_seconds * 90_000.0).round() as u64 & 0x1_ffff_ffff;
let mut body = Vec::new();
body.extend_from_slice(&event_id.to_be_bytes());
body.push(0x7f);
let duration_flag = marker.break_duration_seconds.is_some() && marker.out_of_network;
let mut flags: u8 = 0b0100_1111; if marker.out_of_network {
flags |= 0b1000_0000;
}
if duration_flag {
flags |= 0b0010_0000;
}
body.push(flags);
let time_word: u64 = (1u64 << 39) | (0x3f << 33) | pts;
body.push(((time_word >> 32) & 0xff) as u8);
body.push(((time_word >> 24) & 0xff) as u8);
body.push(((time_word >> 16) & 0xff) as u8);
body.push(((time_word >> 8) & 0xff) as u8);
body.push((time_word & 0xff) as u8);
if duration_flag {
let dur_pts = (marker.break_duration_seconds.unwrap_or(0.0) * 90_000.0).round() as u64
& 0x1_ffff_ffff;
let dur_word: u64 = (1u64 << 39) | (0x3f << 33) | dur_pts;
body.push(((dur_word >> 32) & 0xff) as u8);
body.push(((dur_word >> 24) & 0xff) as u8);
body.push(((dur_word >> 16) & 0xff) as u8);
body.push(((dur_word >> 8) & 0xff) as u8);
body.push((dur_word & 0xff) as u8);
}
body.extend_from_slice(&0u16.to_be_bytes());
body.push(0);
body.push(0);
let command_type: u8 = 5; let command_len = body.len() as u16;
let section_payload_len = 1 + 5 + 1 + 3 + 1 + body.len() + 2;
let section_length = (section_payload_len + 4) as u16;
let mut section = Vec::with_capacity(3 + section_payload_len + 4);
section.push(0xFC); let sl = (0b0011_0000_0000_0000) | (section_length & 0x0fff);
section.extend_from_slice(&sl.to_be_bytes());
section.push(0); section.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00]);
section.push(0); let tier_cmd = (0x0fffu32 << 12) | (u32::from(command_len) & 0x0fff);
section.push(((tier_cmd >> 16) & 0xff) as u8);
section.push(((tier_cmd >> 8) & 0xff) as u8);
section.push((tier_cmd & 0xff) as u8);
section.push(command_type);
section.extend_from_slice(&body);
section.extend_from_slice(&0u16.to_be_bytes());
let crc = mpeg_crc32(§ion);
section.extend_from_slice(&crc.to_be_bytes());
section
}
pub fn to_hex_0x(bytes: &[u8]) -> String {
let mut s = String::from("0x");
for b in bytes {
s.push_str(&format!("{b:02X}"));
}
s
}
pub fn to_base64(bytes: &[u8]) -> String {
const T: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::new();
let mut i = 0;
while i + 3 <= bytes.len() {
let n =
(u32::from(bytes[i]) << 16) | (u32::from(bytes[i + 1]) << 8) | u32::from(bytes[i + 2]);
out.push(T[((n >> 18) & 63) as usize] as char);
out.push(T[((n >> 12) & 63) as usize] as char);
out.push(T[((n >> 6) & 63) as usize] as char);
out.push(T[(n & 63) as usize] as char);
i += 3;
}
let rest = bytes.len() - i;
if rest == 1 {
let n = u32::from(bytes[i]) << 16;
out.push(T[((n >> 18) & 63) as usize] as char);
out.push(T[((n >> 12) & 63) as usize] as char);
out.push('=');
out.push('=');
} else if rest == 2 {
let n = (u32::from(bytes[i]) << 16) | (u32::from(bytes[i + 1]) << 8);
out.push(T[((n >> 18) & 63) as usize] as char);
out.push(T[((n >> 12) & 63) as usize] as char);
out.push(T[((n >> 6) & 63) as usize] as char);
out.push('=');
}
out
}
fn mpeg_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) ^ 0x04C1_1DB7;
} else {
crc <<= 1;
}
}
}
crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splice_insert_has_table_id_and_crc_length() {
let bytes = build_splice_insert(&Scte35Marker::cue_out(12.0));
assert_eq!(bytes[0], 0xFC);
let section_length = u16::from_be_bytes([bytes[1] & 0x0f, bytes[2]]) as usize;
assert_eq!(bytes.len(), 3 + section_length);
assert!(bytes.contains(&5));
}
#[test]
fn hex_and_base64_round_shapes() {
let bytes = build_splice_insert(&Scte35Marker::cue_in(30.0));
let hex = to_hex_0x(&bytes);
assert!(hex.starts_with("0xFC"));
let b64 = to_base64(&bytes);
assert!(!b64.is_empty());
assert!(b64.is_ascii());
}
}