use oximedia_core::{CodecId, OxiError, OxiResult};
use std::collections::HashMap;
const CRC32_POLYNOMIAL: u32 = 0x04C1_1DB7;
#[derive(Debug, Clone)]
pub struct ProgramAssociationTable {
#[allow(dead_code)]
pub transport_stream_id: u16,
#[allow(dead_code)]
pub version: u8,
pub programs: HashMap<u16, u16>,
}
impl ProgramAssociationTable {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < 8 {
return Err(OxiError::InvalidData("PAT too short".to_string()));
}
if data[0] != 0x00 {
return Err(OxiError::InvalidData(format!(
"Invalid PAT table ID: expected 0x00, 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!(
"PAT section too short: expected {}, got {}",
section_length + 3,
data.len()
)));
}
verify_crc32(&data[..section_length + 3])?;
let transport_stream_id = (u16::from(data[3]) << 8) | u16::from(data[4]);
let version = (data[5] >> 1) & 0x1F;
let mut programs = HashMap::new();
let entries_end = section_length + 3 - 4;
let mut offset = 8;
while offset + 4 <= entries_end {
let program_number = (u16::from(data[offset]) << 8) | u16::from(data[offset + 1]);
let pmt_pid = (u16::from(data[offset + 2] & 0x1F) << 8) | u16::from(data[offset + 3]);
if program_number != 0 {
programs.insert(program_number, pmt_pid);
}
offset += 4;
}
Ok(Self {
transport_stream_id,
version,
programs,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamType {
Mpeg2Video,
H264,
H265,
Av1,
Vp9,
Vp8,
Mpeg1Audio,
AacAudio,
Opus,
Flac,
Pcm,
PrivateStream,
Unknown(u8),
}
impl StreamType {
#[must_use]
pub const fn from_type_id(type_id: u8) -> Self {
match type_id {
0x01 | 0x02 => Self::Mpeg2Video,
0x1B => Self::H264,
0x24 => Self::H265,
0x03 | 0x04 => Self::Mpeg1Audio,
0x0F | 0x11 => Self::AacAudio,
0x06 => Self::PrivateStream, 0x80 => Self::Pcm, 0x81 => Self::Opus, 0x82 => Self::Flac, 0x83 => Self::Vp8, 0x84 => Self::Vp9, 0x85 => Self::Av1, _ => Self::Unknown(type_id),
}
}
#[must_use]
pub const fn to_codec_id(self) -> Option<CodecId> {
match self {
Self::Av1 => Some(CodecId::Av1),
Self::Vp9 => Some(CodecId::Vp9),
Self::Vp8 => Some(CodecId::Vp8),
Self::Opus => Some(CodecId::Opus),
Self::Flac => Some(CodecId::Flac),
Self::Pcm => Some(CodecId::Pcm),
_ => None,
}
}
#[must_use]
pub const fn is_patent_encumbered(self) -> bool {
matches!(
self,
Self::Mpeg2Video | Self::H264 | Self::H265 | Self::Mpeg1Audio | Self::AacAudio
)
}
}
#[derive(Debug, Clone)]
pub struct ElementaryStreamInfo {
#[allow(dead_code)]
pub stream_type: StreamType,
pub pid: u16,
pub codec_id: Option<CodecId>,
#[allow(dead_code)]
pub descriptors: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct ProgramMapTable {
pub program_number: u16,
#[allow(dead_code)]
pub version: u8,
#[allow(dead_code)]
pub pcr_pid: u16,
pub streams: Vec<ElementaryStreamInfo>,
}
impl ProgramMapTable {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < 12 {
return Err(OxiError::InvalidData("PMT too short".to_string()));
}
if data[0] != 0x02 {
return Err(OxiError::InvalidData(format!(
"Invalid PMT table ID: expected 0x02, 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!(
"PMT section too short: expected {}, got {}",
section_length + 3,
data.len()
)));
}
verify_crc32(&data[..section_length + 3])?;
let program_number = (u16::from(data[3]) << 8) | u16::from(data[4]);
let version = (data[5] >> 1) & 0x1F;
let pcr_pid = (u16::from(data[8] & 0x1F) << 8) | u16::from(data[9]);
let program_info_length =
(((u16::from(data[10]) & 0x0F) << 8) | u16::from(data[11])) as usize;
let mut offset = 12 + program_info_length;
let streams_end = section_length + 3 - 4;
let mut streams = Vec::new();
while offset + 5 <= streams_end {
let stream_type_id = data[offset];
let stream_type = StreamType::from_type_id(stream_type_id);
if stream_type.is_patent_encumbered() {
return Err(OxiError::PatentViolation(format!(
"Patent-encumbered stream type detected: {stream_type:?} (0x{stream_type_id:02X})"
)));
}
let elementary_pid =
(u16::from(data[offset + 1] & 0x1F) << 8) | u16::from(data[offset + 2]);
let es_info_length = (((u16::from(data[offset + 3]) & 0x0F) << 8)
| u16::from(data[offset + 4])) as usize;
let descriptors = if es_info_length > 0 && offset + 5 + es_info_length <= streams_end {
data[offset + 5..offset + 5 + es_info_length].to_vec()
} else {
Vec::new()
};
streams.push(ElementaryStreamInfo {
stream_type,
pid: elementary_pid,
codec_id: stream_type.to_codec_id(),
descriptors,
});
offset += 5 + es_info_length;
}
Ok(Self {
program_number,
version,
pcr_pid,
streams,
})
}
}
#[derive(Debug, Clone)]
pub struct ServiceDescription {
#[allow(dead_code)]
pub service_id: u16,
#[allow(dead_code)]
pub service_name: Option<String>,
#[allow(dead_code)]
pub provider_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ServiceDescriptionTable {
#[allow(dead_code)]
pub transport_stream_id: u16,
#[allow(dead_code)]
pub version: u8,
#[allow(dead_code)]
pub services: Vec<ServiceDescription>,
}
impl ServiceDescriptionTable {
#[allow(dead_code)]
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < 11 {
return Err(OxiError::InvalidData("SDT too short".to_string()));
}
if data[0] != 0x42 {
return Err(OxiError::InvalidData(format!(
"Invalid SDT table ID: expected 0x42, 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("SDT section too short".to_string()));
}
verify_crc32(&data[..section_length + 3])?;
let transport_stream_id = (u16::from(data[3]) << 8) | u16::from(data[4]);
let version = (data[5] >> 1) & 0x1F;
let mut services = Vec::new();
let mut offset = 11;
let services_end = section_length + 3 - 4;
while offset + 5 <= services_end {
let service_id = (u16::from(data[offset]) << 8) | u16::from(data[offset + 1]);
let descriptors_loop_length = (((u16::from(data[offset + 3]) & 0x0F) << 8)
| u16::from(data[offset + 4])) as usize;
let mut service_name = None;
let mut provider_name = None;
let desc_end = offset + 5 + descriptors_loop_length;
let mut desc_offset = offset + 5;
while desc_offset + 2 <= desc_end {
let descriptor_tag = data[desc_offset];
let descriptor_length = data[desc_offset + 1] as usize;
if descriptor_tag == 0x48 && desc_offset + 2 + descriptor_length <= desc_end {
let desc_data = &data[desc_offset + 2..desc_offset + 2 + descriptor_length];
if desc_data.len() >= 3 {
let provider_name_length = desc_data[1] as usize;
if desc_data.len() > 2 + provider_name_length {
provider_name =
String::from_utf8(desc_data[2..2 + provider_name_length].to_vec())
.ok();
let service_name_length = desc_data[2 + provider_name_length] as usize;
if desc_data.len() >= 3 + provider_name_length + service_name_length {
service_name = String::from_utf8(
desc_data[3 + provider_name_length
..3 + provider_name_length + service_name_length]
.to_vec(),
)
.ok();
}
}
}
}
desc_offset += 2 + descriptor_length;
}
services.push(ServiceDescription {
service_id,
service_name,
provider_name,
});
offset = desc_end;
}
Ok(Self {
transport_stream_id,
version,
services,
})
}
}
#[derive(Debug, Clone)]
pub struct SectionAssembler {
data: Vec<u8>,
expected_length: Option<usize>,
}
impl Default for SectionAssembler {
fn default() -> Self {
Self::new()
}
}
impl SectionAssembler {
#[must_use]
pub fn new() -> Self {
Self {
data: Vec::new(),
expected_length: None,
}
}
pub fn push(&mut self, payload: &[u8], payload_unit_start: bool) -> Option<Vec<u8>> {
if payload_unit_start {
self.data.clear();
self.expected_length = None;
if payload.is_empty() {
return None;
}
let pointer = payload[0] as usize;
if pointer + 1 >= payload.len() {
return None;
}
self.data.extend_from_slice(&payload[pointer + 1..]);
} else {
self.data.extend_from_slice(payload);
}
if self.expected_length.is_none() && self.data.len() >= 3 {
let section_length =
(((u16::from(self.data[1]) & 0x0F) << 8) | u16::from(self.data[2])) as usize;
self.expected_length = Some(section_length + 3);
}
if let Some(expected) = self.expected_length {
if self.data.len() >= expected {
let section = self.data[..expected].to_vec();
self.data.clear();
self.expected_length = None;
return Some(section);
}
}
None
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.data.clear();
self.expected_length = None;
}
}
fn verify_crc32(data: &[u8]) -> OxiResult<()> {
if data.len() < 4 {
return Err(OxiError::InvalidData(
"Section too short for CRC".to_string(),
));
}
let computed_crc = compute_crc32(data);
if computed_crc != 0 {
return Err(OxiError::InvalidData(format!(
"CRC check failed: computed 0x{computed_crc:08X}"
)));
}
Ok(())
}
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_POLYNOMIAL;
} else {
crc <<= 1;
}
}
}
crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_type_from_id() {
assert_eq!(StreamType::from_type_id(0x1B), StreamType::H264);
assert_eq!(StreamType::from_type_id(0x24), StreamType::H265);
assert_eq!(StreamType::from_type_id(0x81), StreamType::Opus);
assert_eq!(StreamType::from_type_id(0x85), StreamType::Av1);
}
#[test]
fn test_stream_type_patent_check() {
assert!(StreamType::H264.is_patent_encumbered());
assert!(StreamType::H265.is_patent_encumbered());
assert!(StreamType::AacAudio.is_patent_encumbered());
assert!(!StreamType::Av1.is_patent_encumbered());
assert!(!StreamType::Opus.is_patent_encumbered());
}
#[test]
fn test_stream_type_to_codec_id() {
assert_eq!(StreamType::Av1.to_codec_id(), Some(CodecId::Av1));
assert_eq!(StreamType::Opus.to_codec_id(), Some(CodecId::Opus));
assert_eq!(StreamType::H264.to_codec_id(), None);
}
#[test]
fn test_crc32_computation() {
let test_data = vec![0x00, 0xB0, 0x0D, 0x00, 0x01, 0xC1, 0x00, 0x00, 0x00];
let crc = compute_crc32(&test_data);
let mut data_with_crc = test_data;
data_with_crc.extend_from_slice(&crc.to_be_bytes());
assert_eq!(compute_crc32(&data_with_crc), 0);
}
#[test]
fn test_section_assembler() {
let mut assembler = SectionAssembler::new();
let section_data = vec![
0x00, 0xB0, 0x0D, 0x00, 0x01, 0xC1, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x20, 0x00, 0x00, 0x00, 0x00, ];
let mut packet1 = vec![0x00]; packet1.extend_from_slice(§ion_data[..8]);
let result = assembler.push(&packet1, true);
assert!(result.is_none());
let packet2 = section_data[8..].to_vec();
let result = assembler.push(&packet2, false);
assert!(result.is_some()); }
}