use crate::halt::Halt;
use crossbeam_channel::{Receiver, Sender, bounded};
use std::thread::JoinHandle;
const DEMUX_CHANNEL_DEPTH: usize = 2;
pub enum DemuxBatch {
Ts(Vec<super::ts::PesPacket>),
Ps(Vec<super::ps::PsPacket>),
Err(std::io::Error),
Eof,
}
pub struct DemuxThread {
handle: Option<JoinHandle<()>>,
#[allow(dead_code)]
producer_shell: Option<Box<dyn Send>>,
}
impl DemuxThread {
pub fn spawn_zero_copy<S: Send + 'static>(
prefetch_rx: Receiver<std::io::Result<Vec<u8>>>,
recycle_tx: Sender<Vec<u8>>,
producer_shell: S,
halt: Option<Halt>,
ts: Option<super::ts::TsDemuxer>,
ps: Option<super::ps::PsDemuxer>,
) -> crate::error::Result<(Self, Receiver<DemuxBatch>)> {
let (tx, rx) = bounded::<DemuxBatch>(DEMUX_CHANNEL_DEPTH);
let mut ts = ts;
let mut ps = ps;
let handle = std::thread::Builder::new()
.name("freemkv-demux".into())
.spawn(move || {
let prof = std::env::var_os("FREEMKV_PROFILE").is_some();
let mut prof_started = std::time::Instant::now();
let mut prof_last_dump = prof_started;
let mut prof_read_ns: u128 = 0;
let mut prof_feed_ns: u128 = 0;
let mut prof_bytes: u64 = 0;
loop {
if halt.as_ref().map(|h| h.is_cancelled()).unwrap_or(false) {
let _ = tx.send(DemuxBatch::Eof);
return;
}
let t0 = if prof {
Some(std::time::Instant::now())
} else {
None
};
let buf = match prefetch_rx.recv() {
Ok(Ok(b)) => b,
Ok(Err(e)) => {
let _ = tx.send(DemuxBatch::Err(e));
return;
}
Err(_) => break, };
let t1 = if prof {
Some(std::time::Instant::now())
} else {
None
};
let n = buf.len();
if let Some(ref mut d) = ts {
let pkts = d.feed(&buf);
let t2 = if prof {
Some(std::time::Instant::now())
} else {
None
};
let _ = recycle_tx.send(buf);
if !pkts.is_empty() && tx.send(DemuxBatch::Ts(pkts)).is_err() {
return;
}
if prof {
prof_read_ns += t1.unwrap().duration_since(t0.unwrap()).as_nanos();
prof_feed_ns += t2.unwrap().duration_since(t1.unwrap()).as_nanos();
prof_bytes += n as u64;
let now = std::time::Instant::now();
if now.duration_since(prof_last_dump)
>= std::time::Duration::from_secs(5)
{
let el = now.duration_since(prof_started).as_millis().max(1);
let mbps = prof_bytes as u128 * 1000 / 1_000_000 / el;
eprintln!(
"[demux] elapsed={}ms in={}MB/s read={}% feed={}%",
el,
mbps,
prof_read_ns / 10_000 / el,
prof_feed_ns / 10_000 / el,
);
prof_last_dump = now;
prof_started = now;
prof_read_ns = 0;
prof_feed_ns = 0;
prof_bytes = 0;
}
}
} else if let Some(ref mut d) = ps {
let pkts = d.feed(&buf);
let _ = recycle_tx.send(buf);
if !pkts.is_empty() && tx.send(DemuxBatch::Ps(pkts)).is_err() {
return;
}
} else {
let _ = recycle_tx.send(buf);
}
}
if let Some(ref mut d) = ts {
let tail = d.flush();
if !tail.is_empty() {
let _ = tx.send(DemuxBatch::Ts(tail));
}
} else if let Some(ref mut d) = ps {
let tail = d.flush();
if !tail.is_empty() {
let _ = tx.send(DemuxBatch::Ps(tail));
}
}
let _ = tx.send(DemuxBatch::Eof);
})
.map_err(|e| crate::error::Error::IoError { source: e })?;
Ok((
Self {
handle: Some(handle),
producer_shell: Some(Box::new(producer_shell)),
},
rx,
))
}
}
impl Drop for DemuxThread {
fn drop(&mut self) {
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::halt::Halt;
use crossbeam_channel::bounded;
use std::time::Duration;
fn bdts_pes_packet(pid: u16, payload: &[u8]) -> Vec<u8> {
const SYNC: u8 = 0x47;
const TS_PAYLOAD: usize = 184;
let mut pes = vec![0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x00, 0x00];
pes.extend_from_slice(payload);
assert!(pes.len() <= TS_PAYLOAD);
let mut pkt = vec![0u8; 192];
pkt[4] = SYNC;
pkt[5] = (((pid >> 8) as u8) & 0x1F) | 0x40; pkt[6] = (pid & 0xFF) as u8;
let pad = TS_PAYLOAD - pes.len();
if pad == 0 {
pkt[7] = 0x10; pkt[8..8 + pes.len()].copy_from_slice(&pes);
} else {
pkt[7] = 0x30; let af_field_len = pad - 1;
pkt[8] = af_field_len as u8;
if af_field_len >= 1 {
pkt[9] = 0x00; for b in pkt.iter_mut().skip(10).take(af_field_len - 1) {
*b = 0xFF;
}
}
let off = 8 + pad;
pkt[off..off + pes.len()].copy_from_slice(&pes);
}
pkt
}
fn collect_batches(rx: &Receiver<DemuxBatch>, budget: Duration) -> Vec<DemuxBatch> {
let mut out = Vec::new();
let deadline = std::time::Instant::now() + budget;
loop {
let now = std::time::Instant::now();
if now >= deadline {
break;
}
match rx.recv_timeout(deadline - now) {
Ok(b) => {
let is_terminal = matches!(b, DemuxBatch::Eof | DemuxBatch::Err(_));
out.push(b);
if is_terminal {
break;
}
}
Err(_) => break,
}
}
out
}
#[test]
fn clean_eof_sentinel_sent_after_input_exhausted() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, _rc_rx) = bounded::<Vec<u8>>(4);
let pid = 0x1011;
let ts = super::super::ts::TsDemuxer::new(&[pid]);
let (_dt, rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, Some(ts), None).unwrap();
pf_tx.send(Ok(bdts_pes_packet(pid, &[0xDE, 0xAD]))).unwrap();
drop(pf_tx);
let batches = collect_batches(&rx, Duration::from_secs(5));
assert!(
matches!(batches.last(), Some(DemuxBatch::Eof)),
"stream must terminate with the Eof sentinel"
);
let saw_pes = batches.iter().any(|b| match b {
DemuxBatch::Ts(p) => p.iter().any(|pes| pes.data == vec![0xDE, 0xAD]),
_ => false,
});
assert!(saw_pes, "the demuxed PES must be delivered");
}
#[test]
fn flush_tail_emitted_before_eof() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, _rc_rx) = bounded::<Vec<u8>>(4);
let pid = 0x1011;
let ts = super::super::ts::TsDemuxer::new(&[pid]);
let (_dt, rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, Some(ts), None).unwrap();
pf_tx
.send(Ok(bdts_pes_packet(pid, &[0x11, 0x22, 0x33])))
.unwrap();
drop(pf_tx);
let batches = collect_batches(&rx, Duration::from_secs(5));
let pes_idx = batches.iter().position(|b| {
matches!(b, DemuxBatch::Ts(p) if p.iter().any(|x| x.data == vec![0x11, 0x22, 0x33]))
});
let eof_idx = batches.iter().position(|b| matches!(b, DemuxBatch::Eof));
assert!(pes_idx.is_some(), "flushed tail PES delivered");
assert!(eof_idx.is_some(), "Eof delivered");
assert!(pes_idx.unwrap() < eof_idx.unwrap(), "tail before Eof");
}
#[test]
fn halt_cancellation_sends_eof_not_panic() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, _rc_rx) = bounded::<Vec<u8>>(4);
let halt = Halt::new();
halt.cancel(); let ts = super::super::ts::TsDemuxer::new(&[0x1011]);
let (_dt, rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), Some(halt), Some(ts), None).unwrap();
let batches = collect_batches(&rx, Duration::from_secs(5));
drop(pf_tx);
assert!(
matches!(batches.last(), Some(DemuxBatch::Eof)),
"halt cancellation must yield a clean Eof sentinel"
);
}
#[test]
fn upstream_error_is_propagated_as_err_terminal() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, _rc_rx) = bounded::<Vec<u8>>(4);
let ts = super::super::ts::TsDemuxer::new(&[0x1011]);
let (_dt, rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, Some(ts), None).unwrap();
pf_tx
.send(Err(std::io::Error::new(std::io::ErrorKind::Other, "boom")))
.unwrap();
drop(pf_tx);
let batches = collect_batches(&rx, Duration::from_secs(5));
assert!(
matches!(batches.last(), Some(DemuxBatch::Err(_))),
"upstream error must terminate the stream with Err"
);
assert!(
!batches.iter().any(|b| matches!(b, DemuxBatch::Eof)),
"Err is terminal; no Eof after it"
);
}
#[test]
fn buffers_are_recycled_to_producer() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, rc_rx) = bounded::<Vec<u8>>(4);
let pid = 0x1011;
let ts = super::super::ts::TsDemuxer::new(&[pid]);
let (_dt, _rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, Some(ts), None).unwrap();
pf_tx.send(Ok(bdts_pes_packet(pid, &[0xAA]))).unwrap();
let recycled = rc_rx.recv_timeout(Duration::from_secs(5));
assert!(recycled.is_ok(), "consumed buffer must be recycled");
assert_eq!(recycled.unwrap().len(), 192, "the original buffer returned");
drop(pf_tx);
}
#[test]
fn ps_path_demuxes_and_eofs() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, _rc_rx) = bounded::<Vec<u8>>(4);
let ps = super::super::ps::PsDemuxer::new();
let (_dt, rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, None, Some(ps)).unwrap();
let mut buf = vec![
0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x77, 0x88,
];
buf.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); pf_tx.send(Ok(buf)).unwrap();
drop(pf_tx);
let batches = collect_batches(&rx, Duration::from_secs(5));
assert!(
matches!(batches.last(), Some(DemuxBatch::Eof)),
"PS path sends Eof"
);
let saw = batches.iter().any(|b| match b {
DemuxBatch::Ps(p) => p.iter().any(|x| x.data == vec![0x77, 0x88]),
_ => false,
});
assert!(saw, "PS PES must be demuxed and delivered");
}
#[test]
fn no_demuxer_configured_still_recycles_and_eofs() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, rc_rx) = bounded::<Vec<u8>>(4);
let (_dt, rx) = DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, None, None).unwrap();
pf_tx.send(Ok(vec![0u8; 192])).unwrap();
assert!(
rc_rx.recv_timeout(Duration::from_secs(5)).is_ok(),
"buffer recycled"
);
drop(pf_tx);
let batches = collect_batches(&rx, Duration::from_secs(5));
assert_eq!(batches.len(), 1, "only the Eof sentinel");
assert!(matches!(batches[0], DemuxBatch::Eof));
}
#[test]
fn empty_batches_are_not_forwarded() {
let (pf_tx, pf_rx) = bounded::<std::io::Result<Vec<u8>>>(4);
let (rc_tx, _rc_rx) = bounded::<Vec<u8>>(4);
let pid = 0x1011;
let ts = super::super::ts::TsDemuxer::new(&[pid]);
let (_dt, rx) =
DemuxThread::spawn_zero_copy(pf_rx, rc_tx, (), None, Some(ts), None).unwrap();
const SYNC: u8 = 0x47;
let mut pkt = vec![0u8; 192];
pkt[4] = SYNC;
pkt[5] = ((pid >> 8) as u8) & 0x1F; pkt[6] = (pid & 0xFF) as u8;
pkt[7] = 0x10; pf_tx.send(Ok(pkt)).unwrap();
drop(pf_tx);
let batches = collect_batches(&rx, Duration::from_secs(5));
assert_eq!(batches.len(), 1, "only Eof; no empty Ts batch forwarded");
assert!(matches!(batches[0], DemuxBatch::Eof));
}
}