use std::collections::{BTreeMap, HashMap};
use std::time::Duration;
use bytes::Bytes;
use moq_net::Timestamp;
use super::catalog;
use super::psi::{self, CRC};
const SDT_PID: u16 = 0x0011;
const SDT_ACTUAL: u8 = 0x42;
const EIT_PID: u16 = 0x0012;
const DEBOUNCE: Duration = Duration::from_secs(1);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(super) enum Identity {
Sub { ext: u16, scope: u32 },
Table,
}
pub(super) fn identity(section: &[u8]) -> Identity {
if !long_form(section) {
return Identity::Table;
}
let ext = u16::from_be_bytes([section[3], section[4]]);
Identity::Sub {
ext,
scope: scope(section),
}
}
fn scope(section: &[u8]) -> u32 {
match section[0] {
0x46 if section.len() >= 10 => u32::from(u16::from_be_bytes([section[8], section[9]])),
0x4E..=0x6F if section.len() >= 12 => u32::from_be_bytes([section[8], section[9], section[10], section[11]]),
_ => 0,
}
}
fn long_form(section: &[u8]) -> bool {
section.len() >= 8 && section[1] & 0x80 != 0
}
pub(super) fn split_sections(payload: &Bytes) -> Vec<Bytes> {
let mut out = Vec::new();
let mut off = 0;
while payload.len() >= off + 3 {
let len = 3 + ((((payload[off + 1] & 0x0f) as usize) << 8) | payload[off + 2] as usize);
if payload.len() < off + len {
break;
}
out.push(payload.slice(off..off + len));
off += len;
}
out
}
fn sdt_service<'a>(sections: impl IntoIterator<Item = &'a Bytes>, service: u16) -> Option<Vec<u8>> {
for section in sections {
let end = section.len().saturating_sub(4);
let mut off = 11;
while off + 5 <= end {
let len = 5 + ((usize::from(section[off + 3] & 0x0f) << 8) | usize::from(section[off + 4]));
if off + len > end {
break;
}
if u16::from_be_bytes([section[off], section[off + 1]]) == service {
let mut out = section[..11].to_vec();
out.extend_from_slice(§ion[off..off + len]);
let section_length = out.len() - 3 + 4;
out[1] = (out[1] & 0xf0) | (section_length >> 8) as u8;
out[2] = section_length as u8;
out[6] = 0;
out[7] = 0;
out.extend_from_slice(&CRC.checksum(&out).to_be_bytes());
return Some(out);
}
off += len;
}
}
None
}
#[derive(Default, PartialEq)]
struct Generation {
version: Option<u8>,
sections: BTreeMap<u8, Bytes>,
}
#[derive(Default)]
struct Pending {
last: u8,
sections: BTreeMap<u8, Bytes>,
}
enum Complete {
Contiguous,
Wrapped { last: u8 },
}
impl Pending {
fn contiguous(&self) -> bool {
self.sections.len() == self.last as usize + 1
}
}
struct Entry {
track: Option<moq_net::track::Producer>,
interval: Option<Duration>,
service: Option<u16>,
active: BTreeMap<Identity, Generation>,
pending: HashMap<(Identity, u8), Pending>,
dirty: bool,
populated: bool,
advertised: bool,
last_cut: Option<u128>,
}
impl Entry {
fn section(&mut self, section: Bytes) {
let id = identity(§ion);
let Identity::Sub { .. } = id else {
let changed = self
.active
.get(&id)
.is_none_or(|current| current.sections.get(&0) != Some(§ion));
if changed {
self.active.insert(
id,
Generation {
version: None,
sections: BTreeMap::from([(0u8, section)]),
},
);
self.dirty = true;
}
return;
};
let version = (section[5] >> 1) & 0x1f;
let number = section[6];
let last = section[7];
if let Some(current) = self.active.get(&id)
&& current.version == Some(version)
&& current.sections.get(&number) == Some(§ion)
{
return;
}
let pending = self.pending.entry((id, version)).or_default();
pending.last = pending.last.max(last).max(number);
if let Some(prior) = pending.sections.get(&number) {
if *prior == section {
if self.service.is_some() {
return;
}
let last = pending.last;
let sections = std::mem::take(&mut pending.sections);
self.commit(id, version, sections, Complete::Wrapped { last });
return;
}
pending.sections.insert(number, section);
return;
}
pending.sections.insert(number, section);
if pending.contiguous() {
let sections = std::mem::take(&mut pending.sections);
self.commit(id, version, sections, Complete::Contiguous);
}
}
fn commit(&mut self, id: Identity, version: u8, mut sections: BTreeMap<u8, Bytes>, complete: Complete) {
self.pending.retain(|(pid, _), _| *pid != id);
if let Complete::Wrapped { last } = complete
&& let Some(current) = self.active.get(&id)
&& current.version == Some(version)
{
for (number, section) in current.sections.iter() {
if *number <= last {
sections.entry(*number).or_insert_with(|| section.clone());
}
}
}
let changed = self.active.get(&id).is_none_or(|current| current.sections != sections);
self.active.insert(
id,
Generation {
version: Some(version),
sections,
},
);
if changed {
self.dirty = true;
}
}
fn cut(
&mut self,
broadcast: &moq_net::broadcast::Producer,
(pid, table_id): (u16, u8),
pts: Timestamp,
now: u128,
) -> anyhow::Result<()> {
let frames: Vec<Vec<u8>> = self
.active
.values()
.filter_map(|generation| match self.service {
Some(service) => sdt_service(generation.sections.values(), service),
None => Some(generation.sections.values().flatten().copied().collect()),
})
.collect();
self.dirty = false;
self.populated = !frames.is_empty();
let track = match &mut self.track {
Some(track) => track,
None if frames.is_empty() => return Ok(()),
None => self.track.insert(
match broadcast.create_track(format!("{pid:#06x}-{table_id:#04x}.si"), None) {
Ok(track) => track,
Err(_) => broadcast.unique_track(".si", None)?,
},
),
};
let mut group = track.append_group()?;
for payload in frames {
group.write_frame(pts, payload)?;
}
group.finish()?;
self.last_cut = Some(now);
Ok(())
}
}
pub(super) struct Capture<E: catalog::Catalog> {
broadcast: moq_net::broadcast::Producer,
catalog: crate::catalog::Producer<E>,
entries: BTreeMap<(u16, u8), Entry>,
clock: crate::Clock,
program: Option<u16>,
}
impl<E: catalog::Catalog> Capture<E> {
pub fn new(broadcast: moq_net::broadcast::Producer, catalog: crate::catalog::Producer<E>) -> Self {
Self {
broadcast,
catalog,
entries: BTreeMap::new(),
clock: crate::Clock::new(),
program: None,
}
}
pub fn select(&mut self, program: u16) {
self.program = Some(program);
}
pub fn section(&mut self, pid: u16, section: Vec<u8>) -> anyhow::Result<()> {
let Some(&table_id) = section.first() else {
return Ok(());
};
let section = Bytes::from(section);
if long_form(§ion) && section[5] & 0x01 == 0 {
return Ok(());
}
if let Some(program) = self.program
&& pid == EIT_PID
&& matches!(table_id, 0x4E | 0x50..=0x5F)
&& !matches!(identity(§ion), Identity::Sub { ext, .. } if ext == program)
{
return Ok(());
}
let service = self.program.filter(|_| (pid, table_id) == (SDT_PID, SDT_ACTUAL));
if service.is_some() && !psi::crc_ok(§ion) {
return Ok(());
}
let entry = self.entries.entry((pid, table_id)).or_insert_with(|| Entry {
track: None,
interval: catalog::si_interval(table_id),
service,
active: BTreeMap::new(),
pending: HashMap::new(),
dirty: false,
populated: false,
advertised: false,
last_cut: None,
});
entry.section(section);
Ok(())
}
pub fn flush(&mut self, pts: Timestamp, force: bool) -> anyhow::Result<()> {
let now = self.clock.now().as_micros();
for (key, entry) in self.entries.iter_mut() {
if !entry.dirty {
continue;
}
if !force
&& let Some(last) = entry.last_cut
&& now.saturating_sub(last) < DEBOUNCE.as_micros()
{
continue;
}
entry.cut(&self.broadcast, *key, pts, now)?;
}
if self.entries.values().any(|e| e.advertised != e.populated) {
let mut guard = self.catalog.modify()?;
let Some(mpegts) = guard.ext.mpegts_mut() else {
anyhow::bail!("catalog extension no longer carries an mpegts section");
};
for ((pid, table_id), entry) in self.entries.iter_mut() {
let Some(track) = &entry.track else { continue };
if entry.advertised == entry.populated {
continue;
}
if entry.populated {
mpegts.si.entry(*pid).or_default().insert(
*table_id,
catalog::SiEntry {
track: track.name().to_string(),
interval: entry.interval,
..Default::default()
},
);
} else {
retract(&mut mpegts.si, *pid, *table_id, track.name());
}
entry.advertised = entry.populated;
}
}
Ok(())
}
pub fn finish(&mut self, pts: Timestamp) -> anyhow::Result<()> {
self.flush(pts, true)?;
for track in self.entries.values_mut().filter_map(|entry| entry.track.as_mut()) {
track.finish()?;
}
Ok(())
}
pub fn abort(mut self, err: moq_net::Error) {
self.unadvertise();
for (_, entry) in std::mem::take(&mut self.entries) {
if let Some(track) = entry.track {
let _ = track.abort(err.clone());
}
}
}
fn unadvertise(&mut self) {
if !self.entries.values().any(|e| e.advertised) {
return;
}
let Ok(mut guard) = self.catalog.modify() else {
return;
};
let Some(mpegts) = guard.ext.mpegts_mut() else {
return;
};
for ((pid, table_id), entry) in self.entries.iter_mut() {
let Some(track) = entry.track.as_ref().filter(|_| entry.advertised) else {
continue;
};
entry.advertised = false;
retract(&mut mpegts.si, *pid, *table_id, track.name());
}
}
}
fn retract(si: &mut BTreeMap<u16, BTreeMap<u8, catalog::SiEntry>>, pid: u16, table_id: u8, track: &str) {
let Some(tables) = si.get_mut(&pid) else {
return;
};
if tables.get(&table_id).is_some_and(|e| e.track == track) {
tables.remove(&table_id);
}
if tables.is_empty() {
si.remove(&pid);
}
}
impl<E: catalog::Catalog> Drop for Capture<E> {
fn drop(&mut self) {
self.unadvertise();
}
}
#[derive(Default, PartialEq)]
pub(super) struct Snapshot {
subs: BTreeMap<Identity, Vec<Bytes>>,
}
impl Snapshot {
pub fn apply(&mut self, payload: &Bytes) {
let sections = split_sections(payload);
let Some(first) = sections.first() else {
return;
};
self.subs.insert(identity(first), sections);
}
pub fn sections(&self) -> impl Iterator<Item = &Bytes> {
self.subs.values().flatten()
}
pub fn is_empty(&self) -> bool {
self.subs.is_empty()
}
}