use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::pin::Pin;
use std::task::Poll;
use std::time::Duration;
use anyhow::Context;
use bytes::Bytes;
use hang::catalog::{AudioCodec, AudioConfig, Container, VideoCodec, VideoConfig};
use mpeg2ts::es::StreamId;
use mpeg2ts::es::StreamType;
use mpeg2ts::time::Timestamp as TsTimestamp;
use mpeg2ts::ts::payload::{Bytes as TsBytes, Pat, Pes, Pmt, Section};
use mpeg2ts::ts::{
AdaptationField, ContinuityCounter, Descriptor, EsInfo, Pid, ProgramAssociation, TransportScramblingControl,
TsHeader, TsPacket, TsPacketWriter, TsPayload, VersionNumber, WriteTsPacket,
};
use moq_net::Timestamp;
use crate::catalog::hang::Catalog;
use crate::catalog::{CatalogFormat, Stream};
use crate::codec::video::Reorder;
use crate::codec::{aac, annexb};
use crate::container::{ExportSource, Frame};
use super::adts;
use super::catalog;
const PMT_PID: u16 = 0x1000;
const FIRST_ES_PID: u16 = 0x1001;
const PSI_INTERVAL: Duration = Duration::from_millis(500);
const SI_REVISION_INTERVAL: Duration = Duration::from_secs(1);
pub(super) const PCR_INTERVAL: Duration = Duration::from_millis(25);
const PCR_BACKFILL: u128 = 40;
pub(super) const NULL_PACKET: [u8; TsPacket::SIZE] = {
let mut packet = [0xff; TsPacket::SIZE];
packet[0] = 0x47;
packet[1] = 0x1f;
packet[2] = 0xff;
packet[3] = 0x10;
packet
};
const SLOTS_PER_SECOND: i64 = (Duration::from_secs(1).as_nanos() / PCR_INTERVAL.as_nanos()) as i64;
const _: () = assert!(
Duration::from_secs(1)
.as_nanos()
.is_multiple_of(PCR_INTERVAL.as_nanos())
);
const STUFFING_UNIT: i64 = TsPacket::SIZE as i64 * 8 * SLOTS_PER_SECOND;
const MAX_MUX_RATE: u64 = 1_000_000_000;
fn sanitize_mux_rate(rate: u64) -> Option<u64> {
(1..=MAX_MUX_RATE).contains(&rate).then_some(rate)
}
pub struct Export<E: catalog::Catalog = ()> {
source: crate::Source,
catalog: Option<crate::catalog::Consumer<E>>,
catalog_format: CatalogFormat,
stale: HashSet<String>,
max_age: Duration,
tracks: HashMap<String, Track>,
counters: HashMap<u16, ContinuityCounter>,
span_counters: Option<HashMap<u16, ContinuityCounter>>,
program_descriptors: Vec<catalog::Descriptor>,
program: Option<catalog::Program>,
si: BTreeMap<(u16, u8), SiTrack>,
last_timestamp: Option<Timestamp>,
si_flushed: bool,
psi: Option<Psi>,
last_psi: Option<Timestamp>,
last_pcr: Option<u128>,
epoch: u64,
emitted_epoch: u64,
pcr_discontinuity: bool,
pcr_reserve: Option<u64>,
pending: Vec<u8>,
keyframes: Vec<usize>,
queue: VecDeque<(Frame, Tally)>,
pcr_cc: Option<u8>,
clock: Option<Timestamp>,
low: Option<Timestamp>,
watermark: Option<Timestamp>,
stall: Option<web_async::time::Instant>,
hold: Option<Pin<Box<web_async::time::Sleep>>>,
mux_rate: Option<u64>,
mux_rate_override: Option<u64>,
stuffing: Stuffing,
video_start: Option<Timestamp>,
units: Vec<(usize, u16)>,
tally: Tally,
liveness: super::import::Liveness,
}
#[derive(Default)]
struct Tally {
pcrs: Vec<(u64, bool)>,
units: Vec<u16>,
}
struct Pending {
frame: Frame,
discontinuity: u64,
arrived: web_async::time::Instant,
}
struct Track {
source: ExportSource,
pending: Option<Pending>,
discontinuity: u64,
epoch: u64,
finished: bool,
pid: u16,
kind: Kind,
descriptors: Vec<catalog::Descriptor>,
last_dts: Option<u64>,
timeline: Option<Timestamp>,
reserve: Reserve,
}
impl Track {
fn admit(&mut self, pending: Pending, epoch: u64) -> Option<Pending> {
(self.epoch == epoch).then_some(pending)
}
fn shown(&self, timestamp: Timestamp, pid: u16) -> bool {
if self.pending.is_some() || self.finished {
return true;
}
let Some(timeline) = self.timeline else {
return false;
};
let reorder = match self.kind {
Kind::Video(_) => u128::from(self.reserve.ticks) * 1_000_000_000 / 90_000,
_ => 0,
};
let floor = timeline.as_nanos().saturating_sub(reorder);
(floor, self.pid) > (timestamp.as_nanos(), pid)
}
}
struct Reserve {
jitter: Option<u64>,
framerate: Option<f64>,
description: Option<Bytes>,
declared: Option<Reorder>,
observed: u64,
since_peak: u64,
ticks: u64,
}
impl Default for Reserve {
fn default() -> Self {
Self {
jitter: None,
framerate: None,
description: None,
declared: None,
observed: 0,
since_peak: 0,
ticks: DEFAULT_DTS_RESERVE,
}
}
}
impl Reserve {
fn configure(&mut self, config: &VideoConfig, name: &str) {
self.jitter = config
.jitter
.map(|t| (t.as_micros() * 90_000 / 1_000_000) as u64)
.filter(|&ticks| ticks > 0);
self.framerate = config.framerate.filter(|fps| fps.is_finite() && *fps > 0.0);
self.settle(name, "catalog");
}
fn describe(&mut self, stream_type: StreamType, description: Option<&Bytes>, name: &str) {
if self.description.as_ref() == description {
return;
}
self.description = description.cloned();
self.declared = description.and_then(|d| declared_reorder(stream_type, d));
self.settle(name, "sps");
}
fn observe(&mut self, gap: u64, name: &str) {
self.since_peak += 1;
let covering = gap.saturating_add(self.since_peak).min(MAX_DTS_RESERVE);
if covering > self.observed {
self.observed = covering;
self.settle(name, "observed");
}
}
fn peak(&mut self) {
self.since_peak = 0;
}
fn settle(&mut self, name: &str, source: &'static str) {
let declared = self.declared.and_then(|reorder| {
let period = match reorder.period {
Some((units, scale)) => units.saturating_mul(90_000).div_ceil(scale),
None => (90_000.0 / self.framerate?).ceil() as u64,
};
let depth = u64::from(reorder.depth);
let ticks = depth.saturating_mul(period).saturating_add(depth.max(1));
Some(ticks.min(MAX_DTS_RESERVE))
});
let target = self
.jitter
.or(declared)
.unwrap_or(DEFAULT_DTS_RESERVE)
.max(self.observed);
if target > self.ticks {
if source == "observed" {
tracing::warn!(
track = %name,
from = self.ticks,
to = target,
"reordering deeper than the catalog or SPS declares; raising the video DTS reserve"
);
} else {
tracing::info!(track = %name, from = self.ticks, to = target, source, "raising the video DTS reserve");
}
self.ticks = target;
}
}
}
#[derive(Clone)]
enum Kind {
Video(StreamType),
Aac(aac::InBand),
Opus { channel_count: u32 },
Mp2 { sample_rate: u32 },
Ac3,
Eac3,
Verbatim {
stream_type: u8,
framing: catalog::Framing,
stream_id: Option<u8>,
},
}
impl Kind {
fn suffix(&self) -> &'static str {
match self {
Kind::Video(StreamType::H265) => ".hev1",
Kind::Video(_) => ".avc3",
Kind::Aac { .. } => ".aac",
Kind::Opus { .. } => ".opus",
Kind::Mp2 { .. } => ".mp2",
Kind::Ac3 => ".ac3",
Kind::Eac3 => ".eac3",
Kind::Verbatim { .. } => ".ts",
}
}
}
#[derive(Default)]
struct Stuffing {
balance: i64,
since_pcr: u64,
overrun: bool,
}
struct Psi {
pat: Pat,
pmt: Pmt,
pcr_pid: u16,
pmt_pid: u16,
}
struct PesUnit {
pid: u16,
is_video: bool,
keyframe: bool,
timestamp: Timestamp,
dts: Option<u64>,
stream_id: Option<u8>,
}
struct SiTrack {
track: String,
interval: Option<Duration>,
state: SiState,
active: super::si::Snapshot,
pending: Option<(moq_net::group::Consumer, super::si::Snapshot)>,
dirty: bool,
last_emit: Option<Timestamp>,
max_age: Duration,
}
enum SiState {
Requesting(kio::Pending<moq_net::origin::Requesting>, String),
Subscribing(kio::Pending<moq_net::track::Subscribing>),
Active(moq_net::track::Ordered),
Done,
}
impl SiTrack {
fn new(source: &crate::Source, entry: &catalog::SiEntry, max_age: Duration) -> Self {
let (state, active) = if entry.sections.is_empty() {
(
SiState::Requesting(source.request_catalog(), entry.track.clone()),
Default::default(),
)
} else {
(SiState::Done, Self::inline(entry))
};
Self {
track: entry.track.clone(),
interval: entry.interval,
state,
dirty: !active.is_empty(),
active,
pending: None,
last_emit: None,
max_age,
}
}
fn inline(entry: &catalog::SiEntry) -> super::si::Snapshot {
let mut snapshot = super::si::Snapshot::default();
for section in &entry.sections {
snapshot.apply(section);
}
snapshot
}
fn poll(&mut self, waiter: &kio::Waiter) {
if matches!(self.state, SiState::Requesting(..)) {
let resolved = {
let SiState::Requesting(pending, name) = &self.state else {
unreachable!("just matched Requesting");
};
match pending.poll_ok(waiter) {
Poll::Ready(Ok(broadcast)) => Ok((broadcast, name.clone())),
Poll::Ready(Err(err)) => Err(err),
Poll::Pending => return,
}
};
self.state = match resolved {
Ok((broadcast, name)) => match broadcast.track(&name) {
Ok(track) => SiState::Subscribing(
track.subscribe(moq_net::track::Subscription::default().with_max_age(self.max_age)),
),
Err(err) => {
tracing::warn!(%err, track = %name, "SI track unavailable; carrying the last snapshot");
SiState::Done
}
},
Err(err) => {
tracing::warn!(%err, "SI broadcast unavailable; carrying the last snapshot");
SiState::Done
}
};
}
if matches!(self.state, SiState::Subscribing(_)) {
let resolved = {
let SiState::Subscribing(pending) = &self.state else {
unreachable!("just matched Subscribing");
};
match pending.poll_ok(waiter) {
Poll::Ready(result) => result,
Poll::Pending => return,
}
};
self.state = match resolved {
Ok(track) => SiState::Active(track.ordered()),
Err(err) => {
tracing::warn!(%err, "SI subscription failed; carrying the last snapshot");
SiState::Done
}
};
}
let mut ended = false;
if let SiState::Active(track) = &mut self.state {
loop {
match track.poll_next_group(waiter) {
Poll::Ready(Ok(Some(group))) => {
self.pending = Some((group, Default::default()));
}
Poll::Ready(Ok(None)) => {
ended = true;
break;
}
Poll::Ready(Err(err)) => {
tracing::warn!(%err, "SI track failed; carrying the last snapshot");
ended = true;
break;
}
Poll::Pending => break,
}
}
}
if ended {
self.state = SiState::Done;
}
while let Some((group, snapshot)) = &mut self.pending {
let (promote, drop_pending) = match group.poll_read_frame(waiter) {
Poll::Ready(Ok(Some(frame))) => {
snapshot.apply(&frame.payload);
(false, false)
}
Poll::Ready(Ok(None)) => (true, false),
Poll::Ready(Err(err)) => {
tracing::warn!(%err, "SI group failed; carrying the last snapshot");
(false, true)
}
Poll::Pending => break,
};
if promote {
let (_, snapshot) = self.pending.take().unwrap();
self.dirty |= snapshot != self.active;
self.active = snapshot;
} else if drop_pending {
self.pending = None;
}
}
}
}
impl Export {
pub async fn new(source: crate::Source) -> Result<Self, crate::Error> {
Self::with_catalog_format(source, CatalogFormat::default()).await
}
pub async fn with_catalog_format(
source: crate::Source,
catalog_format: CatalogFormat,
) -> Result<Self, crate::Error> {
Self::build(source, catalog_format).await
}
}
impl Export<catalog::Ext> {
pub async fn with_ts(source: crate::Source, catalog_format: CatalogFormat) -> Result<Self, crate::Error> {
Self::build(source, catalog_format).await
}
}
impl<E: catalog::Catalog> Export<E> {
async fn build(source: crate::Source, catalog_format: CatalogFormat) -> Result<Self, crate::Error> {
let catalog = source.catalog::<E>(catalog_format).await?;
Ok(Self {
source,
catalog: Some(catalog),
catalog_format,
stale: HashSet::new(),
max_age: Duration::ZERO,
tracks: HashMap::new(),
counters: HashMap::new(),
span_counters: None,
program_descriptors: Vec::new(),
program: None,
si: BTreeMap::new(),
last_timestamp: None,
si_flushed: false,
psi: None,
last_psi: None,
last_pcr: None,
epoch: 0,
emitted_epoch: 0,
pcr_discontinuity: false,
pcr_reserve: None,
pending: Vec::new(),
keyframes: Vec::new(),
queue: VecDeque::new(),
pcr_cc: None,
clock: None,
low: None,
watermark: None,
stall: None,
hold: None,
video_start: None,
mux_rate: None,
mux_rate_override: None,
stuffing: Stuffing::default(),
units: Vec::new(),
tally: Tally::default(),
liveness: Default::default(),
})
}
pub fn with_mux_rate(mut self, mux_rate: u64) -> Self {
if sanitize_mux_rate(mux_rate).is_none() {
tracing::warn!(mux_rate, "ignoring invalid MPEG-TS multiplex rate override");
}
self.mux_rate_override = Some(mux_rate);
self.mux_rate = sanitize_mux_rate(mux_rate);
self
}
pub fn with_max_age(mut self, max_age: Duration) -> Self {
self.max_age = max_age;
self
}
pub async fn next(&mut self) -> crate::Result<Option<Frame>> {
kio::wait(|waiter| self.poll_next(waiter)).await
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<crate::Result<Option<Frame>>> {
while let Some(catalog) = self.catalog.as_mut() {
match catalog.poll_next(waiter)? {
Poll::Ready(Some(snapshot)) => self.update_catalog(snapshot)?,
Poll::Ready(None) => {
self.catalog = None;
break;
}
Poll::Pending => break,
}
}
for si in self.si.values_mut() {
si.poll(waiter);
}
self.fill(waiter)?;
if self.psi.is_none() {
if self.tracks.is_empty() {
if self.catalog.is_none() {
return Poll::Ready(Ok(None));
}
return Poll::Pending;
}
if !self.header_ready() || !self.video_ready() {
if self.catalog.is_none() && self.tracks.values().all(|t| t.finished) {
return Poll::Ready(Ok(None));
}
return Poll::Pending;
}
self.build_psi()?;
self.video_start = self.first_video_pts();
if let Some(start) = self.video_start {
for track in self.tracks.values_mut() {
if !matches!(track.kind, Kind::Video(_))
&& track.pending.as_ref().is_some_and(|p| p.frame.timestamp < start)
{
track.pending = None;
}
}
self.fill(waiter)?;
}
}
loop {
if let Some(out) = self.pop() {
return Poll::Ready(Ok(Some(out)));
}
let Some(name) = self.pick_next_track(waiter) else {
break;
};
let pending = self.tracks.get_mut(&name).unwrap().pending.take().unwrap();
let changed = pending.discontinuity != self.tracks[&name].discontinuity;
if changed {
let joined = self.tracks[&name].epoch == self.epoch;
if joined {
if !self.pending.is_empty() {
self.emit(None)?;
self.tracks.get_mut(&name).unwrap().pending = Some(pending);
continue;
}
self.rewind();
}
let track = self.tracks.get_mut(&name).unwrap();
track.discontinuity = pending.discontinuity;
track.epoch = self.epoch;
track.last_dts = None;
track.timeline = None;
}
let frame = pending.frame;
let track = self.tracks.get_mut(&name).unwrap();
if let Kind::Video(stream_type) = track.kind {
if frame.keyframe {
track.reserve.describe(stream_type, track.source.description(), &name);
}
if let Some(timeline) = track.timeline
&& frame.timestamp < timeline
{
let gap = to_ticks(timeline) - to_ticks(frame.timestamp);
track.reserve.observe(gap, &name);
} else {
track.reserve.peak();
}
}
track.timeline = Some(track.timeline.map_or(frame.timestamp, |last| last.max(frame.timestamp)));
self.last_timestamp = Some(frame.timestamp);
self.advance(frame.timestamp)?;
self.mux(&name, frame)?;
self.fill(waiter)?;
}
let drained = !self.tracks.is_empty() && self.tracks.values().all(|t| t.finished);
if drained {
self.emit(None)?;
if let Some(out) = self.pop() {
return Poll::Ready(Ok(Some(out)));
}
if self.catalog.is_none() && !self.si_flushed {
self.si_flushed = true;
if let Some(out) = self.write_si_tail()? {
return Poll::Ready(Ok(Some(out)));
}
}
}
if self.catalog.is_none() && (drained || self.tracks.is_empty()) {
return Poll::Ready(Ok(None));
}
Poll::Pending
}
fn pop(&mut self) -> Option<Frame> {
let (out, tally) = self.queue.pop_front()?;
self.emitted_epoch = self.epoch;
for (pcr, discontinuity) in tally.pcrs {
if discontinuity {
self.liveness.discontinuity();
}
self.liveness.written_pcr(pcr);
}
for pid in tally.units {
self.liveness.delivered(pid, 1);
}
Some(out)
}
fn write_si_tail(&mut self) -> anyhow::Result<Option<Frame>> {
if self.psi.is_none() {
return Ok(None);
}
let pending: Vec<(u16, Vec<Bytes>)> = self
.si
.iter()
.filter(|(_, si)| !si.active.is_empty())
.map(|((pid, _), si)| (*pid, si.active.sections().cloned().collect()))
.collect();
if pending.is_empty() {
return Ok(None);
}
let timestamp = self.last_timestamp.unwrap_or(Timestamp::ZERO);
let mut out = Vec::new();
for (pid, sections) in pending {
for section in §ions {
self.write_section(&mut out, pid, section)?;
}
}
Ok(Some(Frame {
timestamp,
duration: None,
payload: Bytes::from(out),
keyframe: false,
}))
}
fn fill(&mut self, waiter: &kio::Waiter) -> crate::Result<()> {
let waiting_for_header = self.psi.is_none();
let video_start = self.video_start;
for track in self.tracks.values_mut() {
if track.pending.is_some() || track.finished {
continue;
}
let is_video = matches!(track.kind, Kind::Video(_));
loop {
match track.source.poll_read(waiter)? {
Poll::Ready(Some(frame)) => {
if waiting_for_header && !track.source.header_ready() {
continue;
}
let pending = Pending {
frame,
discontinuity: track.source.discontinuity(),
arrived: web_async::time::Instant::now(),
};
let Some(pending) = track.admit(pending, self.epoch) else {
continue;
};
let changed = pending.discontinuity != track.discontinuity;
if let Some(start) = video_start
&& !is_video && !changed
&& pending.frame.timestamp < start
{
continue;
}
track.pending = Some(pending);
break;
}
Poll::Ready(None) => {
track.finished = true;
break;
}
Poll::Pending => break,
}
}
}
Ok(())
}
fn update_catalog(&mut self, mut catalog: Catalog<E>) -> anyhow::Result<()> {
self.source.retain_valid(&mut catalog);
let mpegts = catalog.ext.mpegts_mut().cloned().unwrap_or_default();
self.program_descriptors = mpegts.program_descriptors.clone();
self.program = mpegts.program.clone();
let mux_rate = match self.mux_rate_override {
Some(override_rate) => sanitize_mux_rate(override_rate),
None => mpegts.mux_rate.and_then(|rate| {
sanitize_mux_rate(rate).or_else(|| {
tracing::warn!(mux_rate = rate, "ignoring invalid MPEG-TS multiplex rate in catalog");
None
})
}),
};
if self.mux_rate != mux_rate {
self.mux_rate = mux_rate;
self.stuffing = Stuffing::default();
}
self.si
.retain(|key, _| mpegts.si.get(&key.0).is_some_and(|t| t.contains_key(&key.1)));
reject_colliding_si_pids(&mpegts, self.pmt_pid(), &[])?;
for (pid, tables) in mpegts.si.iter() {
for (table_id, entry) in tables.iter() {
match self.si.get_mut(&(*pid, *table_id)) {
Some(existing) if existing.track != entry.track => {
let mut replacement = SiTrack::new(&self.source, entry, self.max_age);
if replacement.active.is_empty() {
replacement.active = std::mem::take(&mut existing.active);
replacement.dirty = existing.dirty;
} else {
replacement.dirty = replacement.active != existing.active;
}
replacement.last_emit = existing.last_emit;
*existing = replacement;
}
Some(existing) => {
existing.interval = entry.interval;
if !entry.sections.is_empty() {
let snapshot = SiTrack::inline(entry);
if existing.active != snapshot {
existing.active = snapshot;
existing.dirty = true;
}
}
}
None => {
self.si
.insert((*pid, *table_id), SiTrack::new(&self.source, entry, self.max_age));
}
}
}
}
let mut active: BTreeMap<String, ()> = BTreeMap::new();
for name in catalog.video.renditions.keys() {
active.insert(name.clone(), ());
}
for name in catalog.audio.renditions.keys() {
active.insert(name.clone(), ());
}
for (name, track) in mpegts.tracks.iter() {
if track.verbatim.is_some() {
active.insert(name.clone(), ());
}
}
if self.psi.is_some() {
for name in active.keys() {
anyhow::ensure!(
self.tracks.contains_key(name),
"TS track layout changed after PAT/PMT was emitted: '{name}' added"
);
}
if !self.stale.is_empty() {
self.resubscribe(&catalog, &mpegts)?;
}
let es_pids: Vec<u16> = self.tracks.values().map(|t| t.pid).collect();
reject_colliding_si_pids(&mpegts, self.pmt_pid(), &es_pids)?;
for (name, config) in catalog.video.renditions.iter() {
if let Some(track) = self.tracks.get_mut(name) {
track.reserve.configure(config, name);
}
}
return Ok(());
}
let mut used: Vec<u16> = vec![0x0000, self.pmt_pid(), 0x1FFF];
let mut pids: BTreeMap<String, u16> = BTreeMap::new();
for name in active.keys() {
if let Some(pid) = mpegts.tracks.get(name).map(|t| t.pid)
&& !used.contains(&pid)
{
used.push(pid);
pids.insert(name.clone(), pid);
}
}
for name in active.keys() {
if !pids.contains_key(name) {
let mut pid = FIRST_ES_PID;
while used.contains(&pid) {
pid += 1;
}
used.push(pid);
pids.insert(name.clone(), pid);
}
}
let es_pids: Vec<u16> = pids.values().copied().collect();
reject_colliding_si_pids(&mpegts, self.pmt_pid(), &es_pids)?;
let mut old = std::mem::take(&mut self.tracks);
for (name, config) in catalog.video.renditions.iter() {
let kind = video_kind(config, name)?;
let descriptors = track_descriptors(&mpegts, name);
let pid = pids[name];
match old.remove(name) {
Some(mut track) => {
track.pid = pid;
track.kind = kind;
track.descriptors = descriptors;
track.reserve.configure(config, name);
self.tracks.insert(name.clone(), track);
}
None => {
let Some(source) = ExportSource::for_video(&self.source, name, config, self.max_age)? else {
continue;
};
let mut reserve = Reserve::default();
reserve.configure(config, name);
self.insert_track(name, source, pid, kind, descriptors, reserve);
}
}
}
for (name, config) in catalog.audio.renditions.iter() {
let kind = audio_kind(config, name)?;
let descriptors = track_descriptors(&mpegts, name);
let pid = pids[name];
match old.remove(name) {
Some(mut track) => {
track.pid = pid;
track.kind = kind;
track.descriptors = descriptors;
self.tracks.insert(name.clone(), track);
}
None => {
let Some(source) = ExportSource::for_audio(&self.source, name, config, self.max_age)? else {
continue;
};
self.insert_track(name, source, pid, kind, descriptors, Reserve::default());
}
}
}
for (name, track) in mpegts.tracks.iter() {
let Some(verbatim) = &track.verbatim else {
continue;
};
let kind = Kind::Verbatim {
stream_type: verbatim.stream_type,
framing: verbatim.framing,
stream_id: verbatim.stream_id,
};
let descriptors = track.descriptors.clone();
let pid = pids[name];
match old.remove(name) {
Some(mut existing) => {
existing.pid = pid;
existing.kind = kind;
existing.descriptors = descriptors;
self.tracks.insert(name.clone(), existing);
}
None => {
let source = ExportSource::for_stream(&self.source, name, self.max_age)?;
self.insert_track(name, source, pid, kind, descriptors, Reserve::default());
}
}
}
Ok(())
}
fn resubscribe(&mut self, catalog: &Catalog<E>, mpegts: &catalog::Mpegts) -> anyhow::Result<()> {
for (name, track) in self.tracks.iter_mut() {
if !self.stale.contains(name) {
continue;
}
let source = if let Some(config) = catalog.video.renditions.get(name) {
ExportSource::for_video(&self.source, name, config, self.max_age)?
} else if let Some(config) = catalog.audio.renditions.get(name) {
ExportSource::for_audio(&self.source, name, config, self.max_age)?
} else if mpegts.tracks.get(name).is_some_and(|t| t.verbatim.is_some()) {
Some(ExportSource::for_stream(&self.source, name, self.max_age)?)
} else {
None
};
let Some(source) = source else {
continue;
};
self.stale.remove(name);
track.source = source;
track.finished = false;
}
Ok(())
}
fn insert_track(
&mut self,
name: &str,
source: ExportSource,
pid: u16,
kind: Kind,
descriptors: Vec<catalog::Descriptor>,
reserve: Reserve,
) {
self.tracks.insert(
name.to_string(),
Track {
source,
pending: None,
discontinuity: 0,
epoch: self.epoch,
finished: false,
pid,
kind,
descriptors,
last_dts: None,
timeline: None,
reserve,
},
);
}
pub fn discontinuity(&self) -> u64 {
self.emitted_epoch
}
pub fn stats(&self) -> super::Stats {
let mut stats = super::Stats::default();
if self.psi.is_none() {
return stats;
}
for track in self.tracks.values() {
let (units, quiet) = self.liveness.stream(track.pid);
let row = super::StreamStats {
track: track.kind.suffix(),
units,
quiet,
..Default::default()
};
stats.streams.insert(track.pid, row);
}
stats
}
pub async fn resume(&mut self) -> Result<(), crate::Error> {
self.catalog = Some(self.source.catalog::<E>(self.catalog_format).await?);
self.si_flushed = false;
for si in self.si.values_mut() {
si.track.clear();
}
if self.psi.is_none() {
self.tracks.clear();
}
for (name, track) in self.tracks.iter_mut() {
track.finished = true;
track.pending = None;
track.discontinuity = 0;
self.stale.insert(name.clone());
}
self.stall = None;
self.hold = None;
self.rewind();
Ok(())
}
fn rewind(&mut self) {
self.epoch += 1;
if let Some(counters) = self.span_counters.take() {
self.counters = counters;
}
self.pending.clear();
self.keyframes.clear();
self.units.clear();
self.tally = Tally::default();
self.queue.clear();
self.watermark = None;
self.clock = None;
self.low = None;
self.last_pcr = None;
self.last_psi = None;
for si in self.si.values_mut() {
si.last_emit = None;
}
self.video_start = None;
self.pcr_discontinuity = true;
self.stuffing = Stuffing::default();
for track in self.tracks.values_mut() {
track.last_dts = None;
track.timeline = None;
track.epoch = self.epoch;
if let Some(pending) = track.pending.as_ref() {
track.discontinuity = pending.discontinuity;
}
if let Some(pending) = track.pending.take() {
track.pending = track.admit(pending, self.epoch);
}
}
}
fn header_ready(&self) -> bool {
self.tracks.values().all(|t| t.source.header_ready())
}
fn video_ready(&self) -> bool {
self.tracks
.values()
.filter(|t| matches!(t.kind, Kind::Video(_)))
.all(|t| t.pending.is_some() || t.finished)
}
fn first_video_pts(&self) -> Option<Timestamp> {
self.tracks
.values()
.filter(|t| matches!(t.kind, Kind::Video(_)))
.filter_map(|t| t.pending.as_ref().map(|p| p.frame.timestamp))
.min()
}
fn pmt_pid(&self) -> u16 {
self.program
.as_ref()
.map(|s| s.pmt_pid)
.filter(|&pid| pid != 0)
.unwrap_or(PMT_PID)
}
fn build_psi(&mut self) -> anyhow::Result<()> {
let mut tracks: Vec<&Track> = self.tracks.values().collect();
tracks.sort_by_key(|t| t.pid);
let needs_clock = tracks.iter().any(|t| {
matches!(
&t.kind,
Kind::Verbatim {
framing: catalog::Framing::Section,
..
}
)
});
let video = tracks.iter().find(|t| matches!(t.kind, Kind::Video(_)));
anyhow::ensure!(
!needs_clock || video.is_some(),
"TS export of section-framed verbatim streams (e.g. SCTE-35) requires a video track for the program clock"
);
let pcr_pid = video
.or_else(|| {
tracks.iter().find(|t| {
matches!(
t.kind,
Kind::Aac(_) | Kind::Opus { .. } | Kind::Mp2 { .. } | Kind::Ac3 | Kind::Eac3
)
})
})
.map(|t| t.pid)
.context("TS export requires a video or audio track for the PCR")?;
let es_info = tracks
.iter()
.map(|t| {
let stream_type = match &t.kind {
Kind::Video(stream_type) => *stream_type,
Kind::Aac(_) => StreamType::AdtsAac,
Kind::Opus { .. } => StreamType::from_u8(0x06).map_err(anyhow::Error::msg)?,
Kind::Mp2 { sample_rate } if *sample_rate < 32000 => StreamType::Mpeg2HalvedSampleRateAudio,
Kind::Mp2 { .. } => StreamType::Mpeg1Audio,
Kind::Ac3 => StreamType::DolbyDigitalUpToSixChannelAudio,
Kind::Eac3 => StreamType::DolbyDigitalPlusUpTo16ChannelAudioForAtsc,
Kind::Verbatim { stream_type, .. } => {
StreamType::from_u8(*stream_type).map_err(anyhow::Error::msg)?
}
};
let descriptors = if !t.descriptors.is_empty() {
to_pmt_descriptors(&t.descriptors)
} else {
match &t.kind {
Kind::Ac3 => vec![Descriptor {
tag: 0x05,
data: b"AC-3".to_vec(),
}],
Kind::Eac3 => vec![Descriptor {
tag: 0x05,
data: b"EAC3".to_vec(),
}],
Kind::Opus { channel_count } => opus_descriptors(*channel_count),
_ => Vec::new(),
}
};
Ok(EsInfo {
stream_type,
elementary_pid: Pid::new(t.pid)?,
descriptors,
})
})
.collect::<anyhow::Result<Vec<_>>>()?;
let program_info = if !self.program_descriptors.is_empty() {
to_pmt_descriptors(&self.program_descriptors)
} else if tracks.iter().any(|t| {
matches!(
&t.kind,
Kind::Verbatim {
stream_type: 0x86,
framing: catalog::Framing::Section,
..
}
)
}) {
vec![Descriptor {
tag: 0x05,
data: b"CUEI".to_vec(),
}]
} else {
Vec::new()
};
let pmt_pid = self.pmt_pid();
let transport_stream_id = self.program.as_ref().map(|s| s.transport_stream_id).unwrap_or(1);
let program_number = self
.program
.as_ref()
.map(|s| s.program_number)
.filter(|&id| id != 0)
.unwrap_or(1);
let pat = Pat {
transport_stream_id,
version_number: VersionNumber::default(),
table: vec![ProgramAssociation {
program_num: program_number,
program_map_pid: Pid::new(pmt_pid)?,
}],
};
let pmt = Pmt {
program_num: program_number,
pcr_pid: Some(Pid::new(pcr_pid)?),
version_number: VersionNumber::default(),
program_info,
es_info,
};
for track in &tracks {
self.liveness.register(track.pid);
}
self.psi = Some(Psi {
pat,
pmt,
pcr_pid,
pmt_pid,
});
Ok(())
}
fn pick_next_track(&mut self, waiter: &kio::Waiter) -> Option<String> {
let (timestamp, pid, name, arrived) = self
.tracks
.iter()
.filter_map(|(n, t)| t.pending.as_ref().map(|p| (p.frame.timestamp, t.pid, n, p.arrived)))
.min_by_key(|(timestamp, pid, name, _)| (*timestamp, *pid, *name))?;
let name = name.clone();
if self.max_age.is_zero() {
return Some(name);
}
if self.tracks.values().all(|t| t.shown(timestamp, pid)) {
self.stall = None;
return Some(name);
}
let deadline = *self.stall.get_or_insert(arrived) + self.max_age;
let hold = self
.hold
.get_or_insert_with(|| Box::pin(web_async::time::sleep_until(deadline)));
if hold.deadline() != deadline {
hold.as_mut().reset(deadline);
}
waiter.poll_future(hold.as_mut()).is_ready().then_some(name)
}
fn mux(&mut self, name: &str, frame: Frame) -> anyhow::Result<()> {
if self.span_counters.is_none() {
self.span_counters = Some(self.counters.clone());
}
let track = self.tracks.get_mut(name).context("missing track")?;
let pid = track.pid;
let kind = track.kind.clone();
if let Kind::Aac(aac) = &mut track.kind {
aac.program_config.take();
}
let is_video = matches!(kind, Kind::Video(_));
let timestamp = frame.timestamp;
let keyframe = frame.keyframe;
let es_payload = match &kind {
Kind::Video(stream_type) => Some(video_es_payload(*stream_type, track.source.description(), &frame)?),
Kind::Aac(aac) => {
let pce = aac.program_config.as_deref().unwrap_or_default();
let raw_len = pce.len() + frame.payload.len();
let header = adts::write_header(aac.object_type, aac.sample_rate, aac.channel_config, raw_len)?;
let mut framed = Vec::with_capacity(header.len() + raw_len);
framed.extend_from_slice(&header);
framed.extend_from_slice(pce);
framed.extend_from_slice(&frame.payload);
Some(framed)
}
Kind::Opus { .. } => Some(opus_es_payload(&frame.payload)),
Kind::Mp2 { .. } | Kind::Ac3 | Kind::Eac3 => Some(frame.payload.to_vec()),
Kind::Verbatim {
framing: catalog::Framing::Pes,
..
} => Some(frame.payload.to_vec()),
Kind::Verbatim {
framing: catalog::Framing::Section,
..
} => None,
};
let dts = if is_video {
let pts = to_ticks(frame.timestamp);
let track = self.tracks.get_mut(name).context("missing track")?;
author_dts(pts, track.reserve.ticks, &mut track.last_dts)
} else {
None
};
let mut out = Vec::with_capacity(TsPacket::SIZE);
if (is_video && frame.keyframe) || due(frame.timestamp, self.last_psi, PSI_INTERVAL) {
let psi = self.psi.as_ref().context("PSI not built")?;
let pmt_pid = psi.pmt_pid;
let pat = TsPayload::Pat(psi.pat.clone());
let pmt = TsPayload::Pmt(psi.pmt.clone());
self.write_packet(&mut out, Pid::PAT, None, pat)?;
self.write_packet(&mut out, pmt_pid, None, pmt)?;
self.last_psi = Some(frame.timestamp);
}
let pending: Vec<(u16, Vec<Bytes>)> = self
.si
.iter_mut()
.filter(|(_, si)| !si.active.is_empty())
.filter(|((_, table_id), si)| {
let interval = si.interval.unwrap_or(PSI_INTERVAL);
let revision = si.dirty && si_due(frame.timestamp, si.last_emit, SI_REVISION_INTERVAL.min(interval));
let repeat = !is_clock_table(*table_id) && due(frame.timestamp, si.last_emit, interval);
revision || repeat
})
.map(|((pid, _), si)| {
si.dirty = false;
if si.last_emit.is_none_or(|last| frame.timestamp > last) {
si.last_emit = Some(frame.timestamp);
}
(*pid, si.active.sections().cloned().collect())
})
.collect();
for (pid, sections) in pending {
for section in §ions {
self.write_section(&mut out, pid, section)?;
}
}
let written = out.len();
match es_payload {
None => self.write_section(&mut out, pid, &frame.payload)?,
Some(es_payload) => {
let stream_id = match &kind {
Kind::Verbatim { stream_id, .. } => Some(stream_id.unwrap_or(StreamId::PRIVATE_STREAM_1)),
Kind::Opus { .. } => Some(StreamId::PRIVATE_STREAM_1),
_ => None,
};
let unit = PesUnit {
pid,
is_video,
keyframe: frame.keyframe,
timestamp: frame.timestamp,
dts,
stream_id,
};
self.write_pes(&mut out, &unit, &es_payload)?;
}
}
if out.len() > written {
self.units.push((self.pending.len() + written, pid));
}
if keyframe {
self.keyframes.push(self.pending.len());
}
self.pending.extend_from_slice(&out);
self.low = Some(self.low.map_or(timestamp, |low| low.min(timestamp)));
Ok(())
}
fn advance(&mut self, ts: Timestamp) -> anyhow::Result<()> {
let Some(watermark) = self.watermark else {
self.watermark = Some(ts);
return Ok(());
};
if ts <= watermark {
return Ok(());
}
self.watermark = Some(ts);
let span = ts.as_nanos().saturating_sub(watermark.as_nanos());
self.emit(Some(span))
}
fn emit(&mut self, span: Option<u128>) -> anyhow::Result<()> {
self.span_counters = None;
let bytes = std::mem::take(&mut self.pending);
let keyframes = std::mem::take(&mut self.keyframes);
let units = std::mem::take(&mut self.units);
let Some(to) = self.low.take() else { return Ok(()) };
let packets = bytes.len() / TsPacket::SIZE;
let start = match self.clock {
Some(clock) => clock.as_nanos(),
None => to.as_nanos().saturating_sub(span.unwrap_or(0)),
};
let end = to.as_nanos().max(start);
let from = stamp(start)?;
self.clock = Some(stamp(end)?);
let open = start / PCR_INTERVAL.as_nanos();
let last = slot_before(end, PCR_INTERVAL).max(open);
let first = self
.last_pcr
.map_or(open, |l| l + 1)
.max((last + 1).saturating_sub(PCR_BACKFILL));
let spread = last - open < PCR_BACKFILL;
let width = end - start;
let pcr_pid = self.psi.as_ref().context("PSI not built")?.pcr_pid;
let mut payload = Vec::new();
for index in first..=open {
self.stuff(index, &mut payload);
let before = counter_before(&bytes, 0, pcr_pid, self.pcr_cc);
let clock = self.pcr_at(index, before)?;
self.send(&mut payload, &clock);
}
let mut cut = 0;
let mut at = from;
for index in (open + 1).max(first)..=last {
let boundary = slot_stamp(index)?;
let next = if spread && width > 0 {
(packets as u128 * (boundary.as_nanos() - start) / width) as usize
} else {
packets
};
self.send(&mut payload, &bytes[cut * TsPacket::SIZE..next * TsPacket::SIZE]);
self.stuff(index, &mut payload);
self.push(at, payload, &keyframes, &units, cut, next);
cut = next;
at = boundary;
let before = counter_before(&bytes, cut * TsPacket::SIZE, pcr_pid, self.pcr_cc);
let clock = self.pcr_at(index, before)?;
payload = Vec::new();
self.send(&mut payload, &clock);
}
self.send(&mut payload, &bytes[cut * TsPacket::SIZE..]);
self.push(at, payload, &keyframes, &units, cut, packets);
if let Some(cc) = counter_before(&bytes, bytes.len(), pcr_pid, None) {
self.pcr_cc = Some(cc);
}
Ok(())
}
fn send(&mut self, payload: &mut Vec<u8>, packets: &[u8]) {
payload.extend_from_slice(packets);
self.stuffing.since_pcr += (packets.len() / TsPacket::SIZE) as u64;
}
fn stuff(&mut self, index: u128, payload: &mut Vec<u8>) {
let since = std::mem::take(&mut self.stuffing.since_pcr);
let (Some(rate), Some(last)) = (self.mux_rate, self.last_pcr) else {
return;
};
let Ok(rate) = i64::try_from(rate) else {
return;
};
let slots = index.saturating_sub(last).min(PCR_BACKFILL) as i64;
let stuffing = &mut self.stuffing;
stuffing.balance = stuffing
.balance
.saturating_add(slots.saturating_mul(rate))
.saturating_sub((since as i64).saturating_mul(STUFFING_UNIT));
let floor = rate.saturating_mul(-SLOTS_PER_SECOND);
if stuffing.balance < floor {
if !std::mem::replace(&mut stuffing.overrun, true) {
tracing::warn!(mux_rate = rate, "MPEG-TS output exceeds the multiplex rate");
}
stuffing.balance = floor;
} else if stuffing.balance >= 0 {
stuffing.overrun = false;
}
let nulls = (stuffing.balance / STUFFING_UNIT).max(0);
let ceiling = slots.saturating_mul(rate).saturating_add(STUFFING_UNIT - 1) / STUFFING_UNIT;
let nulls = nulls.min(ceiling);
stuffing.balance -= nulls * STUFFING_UNIT;
payload.reserve(nulls as usize * TsPacket::SIZE);
for _ in 0..nulls {
payload.extend_from_slice(&NULL_PACKET);
}
}
fn push(
&mut self,
timestamp: Timestamp,
payload: Vec<u8>,
keyframes: &[usize],
units: &[(usize, u16)],
from: usize,
to: usize,
) {
if payload.is_empty() {
return;
}
let (from, to) = (from * TsPacket::SIZE, to * TsPacket::SIZE);
let keyframe = keyframes.iter().any(|&at| at >= from && at < to);
let mut tally = std::mem::take(&mut self.tally);
tally.units = units
.iter()
.filter(|&&(at, _)| at >= from && at < to)
.map(|&(_, pid)| pid)
.collect();
let frame = Frame {
timestamp,
duration: None,
payload: Bytes::from(payload),
keyframe,
};
self.queue.push_back((frame, tally));
}
fn pcr_at(&mut self, index: u128, before: Option<u8>) -> anyhow::Result<Vec<u8>> {
let pcr_pid = self.psi.as_ref().context("PSI not built")?.pcr_pid;
let reserve = self
.tracks
.values()
.map(|t| t.reserve.ticks)
.max()
.unwrap_or(DEFAULT_DTS_RESERVE);
let ticks = slot_ticks(index, PCR_INTERVAL).wrapping_sub(reserve);
if self.pcr_reserve.replace(reserve).is_some_and(|last| reserve > last) {
self.pcr_discontinuity = true;
}
let cc = match before {
Some(cc) => cc,
None => self.counters.entry(pcr_pid).or_default().as_u8().wrapping_sub(1),
};
self.last_pcr = Some(index);
let mut packet = pcr_packet(pcr_pid, ticks, cc)?;
let discontinuity = std::mem::take(&mut self.pcr_discontinuity);
if discontinuity {
packet[5] |= 0x80;
}
self.tally.pcrs.push(((ticks & TS_TIMESTAMP_MASK) * 300, discontinuity));
Ok(packet)
}
fn write_pes(&mut self, out: &mut Vec<u8>, unit: &PesUnit, payload: &[u8]) -> anyhow::Result<()> {
let pts = to_ts_timestamp(unit.timestamp)?;
let dts = unit
.dts
.map(|t| TsTimestamp::new(t & TS_TIMESTAMP_MASK).map_err(anyhow::Error::msg))
.transpose()?;
let stream_id = match unit.stream_id {
Some(id) => StreamId::new(id),
None if unit.is_video => StreamId::new(StreamId::VIDEO_MIN),
None => StreamId::new(StreamId::AUDIO_MIN),
};
let header = mpeg2ts::pes::PesHeader {
stream_id,
priority: false,
data_alignment_indicator: true,
copyright: false,
original_or_copy: false,
pts: Some(pts),
dts,
escr: None,
};
let optional_len = PES_OPTIONAL_LEN + if dts.is_some() { PES_DTS_LEN } else { 0 };
let pes_packet_len = if unit.is_video {
0
} else {
u16::try_from(optional_len + payload.len()).unwrap_or(0)
};
let mut offset = 0;
let mut first = true;
loop {
let adaptation = if first && unit.keyframe {
Some(AdaptationField {
discontinuity_indicator: false,
random_access_indicator: true,
es_priority_indicator: false,
pcr: None,
opcr: None,
splice_countdown: None,
transport_private_data: Vec::new(),
extension: None,
})
} else {
None
};
let header_len = if first { 6 + optional_len } else { 0 };
let af_len = adaptation.as_ref().map(adaptation_size).unwrap_or(0);
let avail = TsBytes::MAX_SIZE - header_len - af_len;
let take = avail.min(payload.len() - offset);
let chunk = &payload[offset..offset + take];
let ts_payload = if first {
TsPayload::PesStart(Pes {
header: header.clone(),
pes_packet_len,
data: TsBytes::new(chunk).map_err(anyhow::Error::msg)?,
})
} else {
TsPayload::PesContinuation(TsBytes::new(chunk).map_err(anyhow::Error::msg)?)
};
self.write_packet(out, unit.pid, adaptation, ts_payload)?;
offset += take;
first = false;
if offset >= payload.len() {
break;
}
}
Ok(())
}
fn write_section(&mut self, out: &mut Vec<u8>, pid: u16, section: &[u8]) -> anyhow::Result<()> {
if !is_complete_section(section) {
tracing::warn!(pid, len = section.len(), "dropping malformed private section on export");
return Ok(());
}
let mut offset = 0;
let mut first = true;
loop {
let payload = if first {
let take = (TsBytes::MAX_SIZE - 1).min(section.len());
let chunk = §ion[..take];
offset = take;
TsPayload::Section(Section {
pointer_field: 0,
data: TsBytes::new(chunk).map_err(anyhow::Error::msg)?,
})
} else {
let take = TsBytes::MAX_SIZE.min(section.len() - offset);
let chunk = §ion[offset..offset + take];
offset += take;
TsPayload::Raw(TsBytes::new(chunk).map_err(anyhow::Error::msg)?)
};
self.write_packet(out, pid, None, payload)?;
first = false;
if offset >= section.len() {
break;
}
}
Ok(())
}
fn write_packet(
&mut self,
out: &mut Vec<u8>,
pid: u16,
adaptation_field: Option<AdaptationField>,
payload: TsPayload,
) -> anyhow::Result<()> {
let counter = self.counters.entry(pid).or_default();
let continuity_counter = *counter;
counter.increment();
let packet = TsPacket {
header: TsHeader {
transport_error_indicator: false,
transport_priority: false,
pid: Pid::new(pid)?,
transport_scrambling_control: TransportScramblingControl::NotScrambled,
continuity_counter,
},
adaptation_field,
payload: Some(payload),
};
let mut writer = TsPacketWriter::new(out);
writer.write_ts_packet(&packet).map_err(anyhow::Error::msg)?;
Ok(())
}
}
fn pcr_packet(pid: u16, ticks: u64, cc: u8) -> anyhow::Result<Vec<u8>> {
anyhow::ensure!(pid <= 0x1FFF, "PID out of range: {pid}");
let cc = cc & ContinuityCounter::MAX;
let base = ticks & TS_TIMESTAMP_MASK;
let mut p = Vec::with_capacity(TsPacket::SIZE);
p.push(0x47);
p.push((pid >> 8) as u8);
p.push(pid as u8);
p.push(0x20 | cc);
p.push(183);
p.push(0x10);
p.push((base >> 25) as u8);
p.push((base >> 17) as u8);
p.push((base >> 9) as u8);
p.push((base >> 1) as u8);
p.push(((base as u8) << 7) | 0x7e);
p.push(0x00);
p.resize(TsPacket::SIZE, 0xff);
Ok(p)
}
fn counter_before(bytes: &[u8], cut: usize, pid: u16, carried: Option<u8>) -> Option<u8> {
let on_pid = |p: &[u8]| (u16::from(p[1] & 0x1f) << 8 | u16::from(p[2])) == pid;
let counter = |p: &[u8]| p[3] & ContinuityCounter::MAX;
bytes[..cut]
.rchunks_exact(TsPacket::SIZE)
.find(|p| on_pid(p))
.map(counter)
.or(carried)
.or_else(|| {
bytes[cut..]
.as_chunks::<{ TsPacket::SIZE }>()
.0
.iter()
.find(|p| on_pid(p.as_slice()))
.map(|p| counter(p).wrapping_sub(1) & ContinuityCounter::MAX)
})
}
const PES_OPTIONAL_LEN: usize = 3 + 5;
const PES_DTS_LEN: usize = 5;
const DEFAULT_DTS_RESERVE: u64 = 16;
const MAX_DTS_RESERVE: u64 = 2 * 90_000;
fn due(timestamp: Timestamp, last: Option<Timestamp>, interval: Duration) -> bool {
if interval.is_zero() {
return true;
}
let Some(last) = last else {
return true;
};
slot(timestamp, interval) > slot(last, interval)
}
fn si_due(timestamp: Timestamp, last: Option<Timestamp>, interval: Duration) -> bool {
let Some(last) = last else {
return true;
};
Duration::from(timestamp).saturating_sub(Duration::from(last)) >= interval
}
fn is_clock_table(table_id: u8) -> bool {
matches!(table_id, 0x70 | 0x73)
}
fn slot(timestamp: Timestamp, interval: Duration) -> u128 {
Duration::from(timestamp).as_nanos() / interval.as_nanos()
}
fn slot_before(nanos: u128, interval: Duration) -> u128 {
if nanos == 0 {
return 0;
}
(nanos - 1) / interval.as_nanos()
}
fn slot_stamp(index: u128) -> anyhow::Result<Timestamp> {
stamp(index * PCR_INTERVAL.as_micros() * 1_000)
}
fn stamp(nanos: u128) -> anyhow::Result<Timestamp> {
let micros = (nanos / 1_000).try_into().context("media timeline out of range")?;
Ok(Timestamp::from_micros(micros)?)
}
fn slot_ticks(index: u128, interval: Duration) -> u64 {
(index * interval.as_nanos() * 90_000 / 1_000_000_000) as u64
}
fn adaptation_size(af: &AdaptationField) -> usize {
2 + if af.pcr.is_some() { 6 } else { 0 }
}
const TS_TIMESTAMP_MASK: u64 = (1 << 33) - 1;
fn to_ticks(timestamp: Timestamp) -> u64 {
(timestamp.as_micros() * 90_000 / 1_000_000) as u64
}
fn to_ts_timestamp(timestamp: Timestamp) -> anyhow::Result<TsTimestamp> {
TsTimestamp::new(to_ticks(timestamp) & TS_TIMESTAMP_MASK).map_err(anyhow::Error::msg)
}
fn video_kind(config: &VideoConfig, name: &str) -> anyhow::Result<Kind> {
ensure_raw(&config.container, "video", name)?;
match &config.codec {
VideoCodec::H264(_) => Ok(Kind::Video(StreamType::H264)),
VideoCodec::H265(_) => Ok(Kind::Video(StreamType::H265)),
other => anyhow::bail!("TS export does not support video codec {other:?} (track '{name}')"),
}
}
fn video_es_payload(stream_type: StreamType, description: Option<&Bytes>, frame: &Frame) -> anyhow::Result<Vec<u8>> {
let description = description.context("video codec config (avcC/hvcC) not resolved")?;
let (length_size, params) = match stream_type {
StreamType::H264 => crate::codec::h264::avcc_params(description)?,
StreamType::H265 => crate::codec::h265::hvcc_params(description)?,
other => anyhow::bail!("unsupported TS video stream type {other:?}"),
};
let mut out = Vec::with_capacity(frame.payload.len() + 64);
if frame.keyframe {
for nal in ¶ms {
out.extend_from_slice(&annexb::START_CODE);
out.extend_from_slice(nal);
}
}
annexb::length_prefixed_to_annexb(&frame.payload, length_size, &mut out)?;
Ok(out)
}
fn audio_kind(config: &AudioConfig, name: &str) -> anyhow::Result<Kind> {
ensure_raw(&config.container, "audio", name)?;
match &config.codec {
AudioCodec::AAC(codec) => {
Ok(Kind::Aac(match &config.description {
Some(asc) => aac::in_band(asc)?,
None => aac::InBand {
object_type: codec.profile,
sample_rate: config.sample_rate,
channel_config: adts::channel_config_from_count(config.channel_count)?,
program_config: None,
},
}))
}
AudioCodec::Mp2 => Ok(Kind::Mp2 {
sample_rate: config.sample_rate,
}),
AudioCodec::Opus => Ok(Kind::Opus {
channel_count: config.channel_count,
}),
AudioCodec::Ac3 => Ok(Kind::Ac3),
AudioCodec::Ec3 => Ok(Kind::Eac3),
other => anyhow::bail!("TS export does not support audio codec {other:?} (track '{name}')"),
}
}
fn opus_descriptors(channel_count: u32) -> Vec<Descriptor> {
vec![
Descriptor {
tag: 0x05,
data: b"Opus".to_vec(),
},
Descriptor {
tag: 0x7f,
data: vec![0x80, channel_count.clamp(1, 8) as u8],
},
]
}
fn opus_es_payload(packet: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(packet.len() + 4);
out.push(0x7f);
out.push(0xe0);
let mut n = packet.len();
loop {
out.push(n.min(255) as u8);
if n < 255 {
break;
}
n -= 255;
}
out.extend_from_slice(packet);
out
}
fn reject_colliding_si_pids(mpegts: &catalog::Mpegts, pmt_pid: u16, es_pids: &[u16]) -> anyhow::Result<()> {
for pid in mpegts.si.keys() {
anyhow::ensure!(
(1..0x1FFF).contains(pid) && *pid != pmt_pid && !es_pids.contains(pid),
"mpegts.si PID {pid} collides with PAT, PMT, null, or an elementary stream"
);
}
Ok(())
}
fn track_descriptors(mpegts: &catalog::Mpegts, name: &str) -> Vec<catalog::Descriptor> {
mpegts
.tracks
.get(name)
.map(|t| t.descriptors.clone())
.unwrap_or_default()
}
fn to_pmt_descriptors(descriptors: &[catalog::Descriptor]) -> Vec<Descriptor> {
descriptors
.iter()
.map(|d| Descriptor {
tag: d.tag,
data: d.data.to_vec(),
})
.collect()
}
fn is_complete_section(section: &[u8]) -> bool {
section.len() >= 3 && section.len() == 3 + ((((section[1] & 0x0f) as usize) << 8) | section[2] as usize)
}
fn ensure_raw(container: &Container, kind: &str, name: &str) -> anyhow::Result<()> {
match container {
Container::Legacy | Container::Loc => Ok(()),
Container::Cmaf { .. } => anyhow::bail!("TS export does not support CMAF {kind} track '{name}'"),
Container::Unknown(unknown) => anyhow::bail!(
"TS export does not support container '{}' on {kind} track '{name}'",
unknown.kind().unwrap_or("<missing>")
),
}
}
fn author_dts(pts: u64, reserve: u64, last: &mut Option<u64>) -> Option<u64> {
let mut dts = pts.saturating_sub(reserve);
if let Some(prev) = *last
&& dts <= prev
{
dts = prev + 1;
}
*last = Some(dts);
(dts != pts).then_some(dts)
}
fn declared_reorder(stream_type: StreamType, description: &[u8]) -> Option<Reorder> {
match stream_type {
StreamType::H264 => {
crate::codec::h264::sps_reorder(crate::codec::h264::Avcc::parse(description).ok()?.sps.first()?)
}
StreamType::H265 => {
crate::codec::h265::sps_reorder(crate::codec::h265::Hvcc::parse(description).ok()?.sps.first()?)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use moq_net::Timestamp;
use super::{
DEFAULT_DTS_RESERVE, PCR_INTERVAL, PSI_INTERVAL, author_dts, due, is_complete_section, si_due, slot, slot_ticks,
};
fn ms(value: u64) -> Timestamp {
Timestamp::from_millis(value).unwrap()
}
fn run_clock(pts: &[u64], reserve: u64) -> Vec<u64> {
let mut last = None;
pts.iter()
.map(|&p| author_dts(p, reserve, &mut last).unwrap_or(p))
.collect()
}
fn decode_order(refs: usize, b: usize, dur: u64, base: u64) -> Vec<u64> {
let pts = |display: usize| base + display as u64 * dur;
let span = b + 1;
let mut out = vec![pts(0)]; for g in 1..refs {
let reference = g * span;
out.push(pts(reference)); for j in 1..=b {
out.push(pts(reference - span + j)); }
}
out
}
#[test]
fn dts_is_monotonic_across_reorder() {
for b in [1, 3, 5] {
let pts = decode_order(40, b, 3_600, 10_000_000);
let dts = run_clock(&pts, DEFAULT_DTS_RESERVE);
assert!(pts.windows(2).any(|w| w[1] < w[0]), "b={b}: stream must reorder PTS");
for (i, win) in dts.windows(2).enumerate() {
assert!(win[1] > win[0], "b={b}: DTS not strictly increasing at {i}: {win:?}");
}
}
}
#[test]
fn sufficient_reserve_keeps_dts_under_pts() {
let dur = 3_600;
for b in [1, 3, 5] {
let reserve = (b as u64 + 1) * dur; let pts = decode_order(40, b, dur, 10_000_000);
let dts = run_clock(&pts, reserve);
for (i, win) in dts.windows(2).enumerate() {
assert!(win[1] > win[0], "b={b}: DTS not strictly increasing at {i}: {win:?}");
}
for (i, (&d, &p)) in dts.iter().zip(pts.iter()).enumerate() {
assert!(d <= p, "b={b}: DTS {d} after PTS {p} at {i}");
}
}
}
#[test]
fn dts_clock_survives_33bit_wrap() {
let wrap = 1u64 << 33;
let pts = decode_order(40, 3, 3_600, wrap - 20 * 3_600);
let dts = run_clock(&pts, DEFAULT_DTS_RESERVE);
assert!(pts.iter().any(|&p| p >= wrap), "test must cross the wrap boundary");
for (i, win) in dts.windows(2).enumerate() {
assert!(
win[1] > win[0],
"DTS not strictly increasing across wrap at {i}: {win:?}"
);
}
}
#[test]
fn dts_without_reorder_trails_pts_by_the_reserve() {
let pts: Vec<u64> = (0..40).map(|i| 10_000_000 + i * 3_600).collect();
let dts = run_clock(&pts, DEFAULT_DTS_RESERVE);
for (i, win) in dts.windows(2).enumerate() {
assert!(win[1] > win[0], "DTS not strictly increasing at {i}: {win:?}");
}
for (i, (&d, &p)) in dts.iter().zip(pts.iter()).enumerate() {
assert_eq!(d, p - DEFAULT_DTS_RESERVE, "DTS should trail PTS by the reserve at {i}");
}
}
#[test]
fn author_dts_is_join_independent_at_a_peak() {
for b in [1, 3, 5] {
let pts = decode_order(40, b, 3_600, 10_000_000);
let running = run_clock(&pts, DEFAULT_DTS_RESERVE);
let mut peaks = 0;
for k in 1..pts.len() {
if pts[..k].iter().any(|&p| p >= pts[k]) {
continue;
}
peaks += 1;
let fresh = run_clock(&pts[k..], DEFAULT_DTS_RESERVE);
assert_eq!(
&running[k..],
&fresh[..],
"b={b}: joining at {k} authored a different clock"
);
}
assert!(peaks > 10, "b={b}: fixture must have peaks to join at, got {peaks}");
}
}
#[test]
fn due_crosses_an_absolute_slot() {
assert!(due(ms(1_000), None, PSI_INTERVAL));
assert!(!due(ms(1_250), Some(ms(1_000)), PSI_INTERVAL));
assert!(due(ms(1_500), Some(ms(1_000)), PSI_INTERVAL));
assert!(!due(ms(750), Some(ms(1_000)), PSI_INTERVAL));
let coarse = Duration::from_millis(2_000);
assert!(!due(ms(1_500), Some(ms(1_000)), coarse));
assert!(due(ms(3_000), Some(ms(1_000)), coarse));
}
#[test]
fn si_due_floors_repeats_from_the_last_emission() {
assert!(si_due(ms(1_000), None, PSI_INTERVAL));
let interval = Duration::from_millis(2_000);
assert!(!si_due(ms(2_000), Some(ms(1_400)), interval));
assert!(!si_due(ms(3_399), Some(ms(1_400)), interval));
assert!(si_due(ms(3_400), Some(ms(1_400)), interval));
assert!(!si_due(ms(1_000), Some(ms(1_400)), interval));
assert!(si_due(ms(1_400), Some(ms(1_400)), Duration::ZERO));
}
fn run_cadence(stamps: &[Timestamp], interval: Duration) -> usize {
let mut last = None;
let mut emissions = 0;
for &ts in stamps {
if due(ts, last, interval) {
emissions += 1;
last = Some(ts);
}
}
emissions
}
#[test]
fn due_ignores_reorder() {
let ticks = decode_order(40, 3, 3_600, 10_000_000);
let stamps: Vec<Timestamp> = ticks
.iter()
.map(|&t| Timestamp::from_micros(t * 1_000_000 / 90_000).unwrap())
.collect();
assert!(stamps.windows(2).any(|w| w[1] < w[0]), "fixture must reorder its PTS");
let first = slot(stamps[0], PSI_INTERVAL);
let last = slot(*stamps.iter().max().unwrap(), PSI_INTERVAL);
assert_eq!(run_cadence(&stamps, PSI_INTERVAL), (last - first + 1) as usize);
}
#[test]
fn due_zero_interval_emits_every_frame() {
let stamps: Vec<Timestamp> = [0, 0, 40, 40, 80].iter().map(|&t| ms(t)).collect();
assert_eq!(run_cadence(&stamps, Duration::ZERO), stamps.len());
}
#[test]
fn due_survives_a_sub_microsecond_interval() {
let interval = Duration::from_nanos(500);
assert!(
!interval.is_zero() && interval.as_micros() == 0,
"fixture must be sub-microsecond"
);
assert!(due(ms(1), Some(ms(0)), interval));
assert!(!due(ms(0), Some(ms(0)), interval));
}
#[test]
fn due_ignores_when_the_last_emission_landed_in_its_slot() {
for last in [1_000, 1_100, 1_499] {
assert!(!due(ms(1_499), Some(ms(last)), PSI_INTERVAL), "last={last}");
assert!(due(ms(1_500), Some(ms(last)), PSI_INTERVAL), "last={last}");
}
}
#[test]
fn pcr_slots_are_exact_ticks() {
let step = slot_ticks(1, PCR_INTERVAL);
assert_eq!(step, 2250);
for index in [0u128, 1, 7, 1_000_000, u32::MAX as u128] {
assert_eq!(slot_ticks(index, PCR_INTERVAL), index as u64 * step);
}
}
#[test]
fn section_validation() {
let mut ok = vec![0xfc, 0x30, 0x1b];
ok.resize(30, 0x00);
assert!(is_complete_section(&ok));
assert!(is_complete_section(&[0xfc, 0x00, 0x00]));
assert!(is_complete_section(&[0x00, 0x00, 0x00]));
assert!(!is_complete_section(&[0xfc, 0x00]));
assert!(!is_complete_section(&[0xfc, 0x30, 0x1b]));
}
}