use std::collections::BTreeMap;
use std::time::Duration;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use serde_with::base64::Base64;
use serde_with::{DisplayFromStr, DurationMilliSeconds, serde_as};
use crate::catalog::hang::CatalogExt;
pub(super) const SI_PIDS: &[(u16, Duration)] = &[
(0x0010, Duration::from_secs(10)),
(0x0011, Duration::from_secs(2)),
];
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Mpegts {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tracks: BTreeMap<String, Track>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub program_descriptors: Vec<Descriptor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program: Option<Program>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde_as(as = "BTreeMap<DisplayFromStr, _>")]
pub si: BTreeMap<u16, Si>,
}
impl Mpegts {
pub fn is_empty(&self) -> bool {
self.tracks.is_empty() && self.program_descriptors.is_empty() && self.program.is_none() && self.si.is_empty()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Program {
pub transport_stream_id: u16,
pub program_number: u16,
pub pmt_pid: u16,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Si {
#[serde_as(as = "Vec<Base64>")]
pub sections: Vec<Bytes>,
#[serde_as(as = "Option<DurationMilliSeconds<u64>>")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval: Option<Duration>,
}
impl Si {
pub fn upsert(&mut self, section: Bytes) -> bool {
let key = section_key(§ion);
match self.sections.iter_mut().find(|s| section_key(s) == key) {
Some(existing) if *existing == section => false,
Some(existing) => {
*existing = section;
true
}
None => {
self.sections.push(section);
true
}
}
}
}
fn section_key(section: &[u8]) -> (u8, u16, u8) {
let table_id = section.first().copied().unwrap_or(0);
let long_form = section.get(1).is_some_and(|b| b & 0x80 != 0);
if !long_form || section.len() < 8 {
return (table_id, 0, 0);
}
let extension = ((section[3] as u16) << 8) | section[4] as u16;
(table_id, extension, section[6])
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Track {
pub pid: u16,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub descriptors: Vec<Descriptor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verbatim: Option<Verbatim>,
}
impl Track {
pub fn new(pid: u16) -> Self {
Self {
pid,
descriptors: Vec::new(),
verbatim: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Verbatim {
pub stream_type: u8,
#[serde(default)]
pub framing: Framing,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream_id: Option<u8>,
}
impl Verbatim {
pub fn new(stream_type: u8, framing: Framing) -> Self {
Self {
stream_type,
framing,
stream_id: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum Framing {
#[default]
Pes,
Section,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Descriptor {
pub tag: u8,
#[serde_as(as = "Base64")]
pub data: Bytes,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct Ext {
#[serde(default, skip_serializing_if = "Mpegts::is_empty")]
pub mpegts: Mpegts,
}
impl CatalogExt for Ext {}
pub trait Catalog: CatalogExt {
fn mpegts_mut(&mut self) -> Option<&mut Mpegts>;
}
impl Catalog for () {
fn mpegts_mut(&mut self) -> Option<&mut Mpegts> {
None
}
}
impl Catalog for crate::catalog::hang::Extra {
fn mpegts_mut(&mut self) -> Option<&mut Mpegts> {
None
}
}
impl Catalog for Ext {
fn mpegts_mut(&mut self) -> Option<&mut Mpegts> {
Some(&mut self.mpegts)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_section_omitted() {
let ext = Ext::default();
assert_eq!(serde_json::to_string(&ext).unwrap(), "{}");
}
#[test]
fn section_roundtrip() {
let mut mpegts = Mpegts::default();
mpegts.tracks.insert(
"audio".to_string(),
Track {
pid: 0x101,
descriptors: vec![Descriptor {
tag: 0x0a,
data: Bytes::from_static(b"eng\x00"),
}],
verbatim: None,
},
);
mpegts.tracks.insert(
".scte35".to_string(),
Track {
pid: 0x102,
descriptors: Vec::new(),
verbatim: Some(Verbatim::new(0x86, Framing::Section)),
},
);
mpegts.program_descriptors.push(Descriptor {
tag: 0x05,
data: Bytes::from_static(b"CUEI"),
});
let json = serde_json::to_string(&Ext { mpegts: mpegts.clone() }).unwrap();
assert!(json.contains("\"Q1VFSQ==\""), "descriptor data is base64: {json}");
let parsed: Ext = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.mpegts, mpegts, "mpegts section round-trips");
}
#[test]
fn program_and_si_roundtrip() {
let mut si = Si {
interval: Some(Duration::from_secs(2)),
..Default::default()
};
assert!(
si.upsert(Bytes::from_static(b"\x42\xf0\x25")),
"first section is a change"
);
let mpegts = Mpegts {
program: Some(Program {
transport_stream_id: 0x1234,
program_number: 1,
pmt_pid: 0x0064,
..Default::default()
}),
si: BTreeMap::from([(0x0011, si)]),
..Default::default()
};
assert!(!mpegts.is_empty(), "a program record is not empty");
let json = serde_json::to_string(&Ext { mpegts: mpegts.clone() }).unwrap();
assert!(json.contains("\"sections\""), "sections present: {json}");
assert!(json.contains("\"pmtPid\":100"), "identity stays structured: {json}");
assert!(json.contains("\"17\":"), "PID key written as a string: {json}");
assert!(json.contains("\"interval\":2000"), "interval is bare millis: {json}");
let parsed: Ext = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.mpegts, mpegts, "program and SI round-trip");
}
#[test]
fn si_upsert_dedupes_by_section_identity() {
let section = |section_number: u8, fill: u8| {
Bytes::from(vec![
0x42,
0xf0,
0x0b,
0x00,
0x01,
0xc1,
section_number,
0x01,
fill,
fill,
fill,
fill,
fill,
fill,
])
};
let mut si = Si::default();
assert!(si.upsert(section(0, 0xaa)), "first section");
assert!(!si.upsert(section(0, 0xaa)), "a byte-identical repeat is not a change");
assert_eq!(si.sections.len(), 1);
assert!(si.upsert(section(0, 0xbb)), "revised section 0 is a change");
assert_eq!(si.sections.len(), 1, "the revision replaced in place");
assert_eq!(si.sections[0], section(0, 0xbb));
assert!(si.upsert(section(1, 0xcc)), "section 1 is a sibling, not a replacement");
assert_eq!(si.sections.len(), 2, "both sections of the table are held");
}
}