#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum StreamType {
Mpeg1Video = 0x01,
Mpeg2Video = 0x02,
Mpeg1Audio = 0x03,
Mpeg2Audio = 0x04,
H264 = 0x1B,
H265 = 0x24,
Aac = 0x0F,
AacLatm = 0x11,
Ac3 = 0x81,
Other(u8),
}
impl From<u8> for StreamType {
fn from(v: u8) -> Self {
match v {
0x01 => Self::Mpeg1Video,
0x02 => Self::Mpeg2Video,
0x03 => Self::Mpeg1Audio,
0x04 => Self::Mpeg2Audio,
0x0F => Self::Aac,
0x11 => Self::AacLatm,
0x1B => Self::H264,
0x24 => Self::H265,
0x81 => Self::Ac3,
other => Self::Other(other),
}
}
}
impl StreamType {
pub fn is_video(&self) -> bool {
matches!(
self,
Self::Mpeg1Video | Self::Mpeg2Video | Self::H264 | Self::H265
)
}
pub fn is_audio(&self) -> bool {
matches!(
self,
Self::Mpeg1Audio | Self::Mpeg2Audio | Self::Aac | Self::AacLatm | Self::Ac3
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PmtEntry {
pub stream_type: StreamType,
pub elementary_pid: u16,
pub descriptors: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramMapTable {
pub program_number: u16,
pub version_number: u8,
pub current_next_indicator: bool,
pub section_number: u8,
pub last_section_number: u8,
pub pcr_pid: u16,
pub program_info: Vec<u8>,
pub entries: Vec<PmtEntry>,
}
impl ProgramMapTable {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 12 {
return None;
}
let table_id = data[0];
if table_id != 0x02 {
return None;
}
let section_length = ((data[1] as usize & 0x0F) << 8) | data[2] as usize;
let section_end = 3 + section_length;
if data.len() < section_end {
return None;
}
let program_number = (data[3] as u16) << 8 | data[4] as u16;
let version_number = (data[5] >> 1) & 0x1F;
let current_next_indicator = data[5] & 1 != 0;
let section_number = data[6];
let last_section_number = data[7];
let pcr_pid = ((data[8] as u16) & 0x1F) << 8 | data[9] as u16;
let program_info_length = ((data[10] as usize) & 0x0F) << 8 | data[11] as usize;
let pi_start = 12;
let pi_end = pi_start + program_info_length;
if pi_end > section_end.saturating_sub(4) {
return None;
}
let program_info = data[pi_start..pi_end].to_vec();
let entries_end = section_end.saturating_sub(4); let mut entries = Vec::new();
let mut i = pi_end;
while i + 5 <= entries_end {
let stream_type = StreamType::from(data[i]);
let elementary_pid = ((data[i + 1] as u16) & 0x1F) << 8 | data[i + 2] as u16;
let es_info_length = ((data[i + 3] as usize) & 0x0F) << 8 | data[i + 4] as usize;
i += 5;
let desc_end = (i + es_info_length).min(entries_end);
let descriptors = data[i..desc_end].to_vec();
i = desc_end;
entries.push(PmtEntry {
stream_type,
elementary_pid,
descriptors,
});
}
Some(Self {
program_number,
version_number,
current_next_indicator,
section_number,
last_section_number,
pcr_pid,
program_info,
entries,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_pmt_section(program_number: u16, pcr_pid: u16, streams: &[(u8, u16)]) -> Vec<u8> {
let stream_bytes: usize = streams.len() * 5; let section_length = 9 + stream_bytes + 4; let mut data = vec![
0x02, ];
data.push(0xB0 | ((section_length >> 8) as u8 & 0x0F));
data.push(section_length as u8);
data.push((program_number >> 8) as u8);
data.push(program_number as u8);
data.push(0xC1);
data.push(0x00);
data.push(0x00);
data.push(0xE0 | ((pcr_pid >> 8) as u8 & 0x1F));
data.push(pcr_pid as u8);
data.push(0xF0);
data.push(0x00);
for &(st, pid) in streams {
data.push(st);
data.push(0xE0 | ((pid >> 8) as u8 & 0x1F));
data.push(pid as u8);
data.push(0xF0);
data.push(0x00);
}
data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
data
}
#[test]
fn test_pmt_basic() {
let data = build_pmt_section(1, 0x100, &[(0x1B, 0x100), (0x0F, 0x101)]);
let pmt = ProgramMapTable::from_bytes(&data).unwrap();
assert_eq!(pmt.program_number, 1);
assert_eq!(pmt.pcr_pid, 0x100);
assert_eq!(pmt.entries.len(), 2);
assert_eq!(pmt.entries[0].stream_type, StreamType::H264);
assert!(pmt.entries[0].stream_type.is_video());
assert_eq!(pmt.entries[0].elementary_pid, 0x100);
assert_eq!(pmt.entries[1].stream_type, StreamType::Aac);
assert!(pmt.entries[1].stream_type.is_audio());
assert_eq!(pmt.entries[1].elementary_pid, 0x101);
}
#[test]
fn test_pmt_empty_streams() {
let data = build_pmt_section(5, 0x1FF, &[]);
let pmt = ProgramMapTable::from_bytes(&data).unwrap();
assert!(pmt.entries.is_empty());
assert_eq!(pmt.pcr_pid, 0x1FF);
}
#[test]
fn test_pmt_wrong_table_id() {
let mut data = build_pmt_section(1, 0x100, &[]);
data[0] = 0x00;
assert!(ProgramMapTable::from_bytes(&data).is_none());
}
#[test]
fn test_pmt_too_short() {
assert!(ProgramMapTable::from_bytes(&[0x02; 5]).is_none());
}
#[test]
fn test_stream_type_other() {
let data = build_pmt_section(1, 0x100, &[(0xFF, 0x200)]);
let pmt = ProgramMapTable::from_bytes(&data).unwrap();
assert_eq!(pmt.entries[0].stream_type, StreamType::Other(0xFF));
assert!(!pmt.entries[0].stream_type.is_video());
assert!(!pmt.entries[0].stream_type.is_audio());
}
}