use std::collections::VecDeque;
use std::time::Duration;
use crate::demux::{self, Codec, ContainerFormat, Demuxer, StreamInfo, StreamKind};
use crate::source::{MediaSource, MemorySource};
use crate::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SegmentFormat {
Ts,
Fmp4,
PackedAudio,
}
pub(crate) fn detect(data: &[u8]) -> Option<SegmentFormat> {
if demux::ts_is_ts(data) {
return Some(SegmentFormat::Ts);
}
if data.len() >= 8 && matches!(&data[4..8], b"ftyp" | b"styp" | b"moof" | b"moov" | b"sidx" | b"free" | b"prft" | b"emsg") {
return Some(SegmentFormat::Fmp4);
}
let mut src = MemorySource::new(data[..data.len().min(256 * 1024)].to_vec(), "segment");
match demux::probe(&mut src).ok().flatten() {
Some(ContainerFormat::Adts | ContainerFormat::Mp3) => Some(SegmentFormat::PackedAudio),
_ => None,
}
}
pub(crate) fn ticks_to_duration(ticks: u64) -> Duration {
Duration::from_nanos((ticks as u128 * 1_000_000_000 / 90_000) as u64)
}
#[derive(Clone, Debug)]
pub(crate) struct RawPacket {
pub kind: StreamKind,
pub raw: Duration,
pub keyframe: bool,
pub data: Vec<u8>,
}
enum Inner {
Ts(Box<demux::TsDemuxer>),
Other(Box<dyn Demuxer>),
}
impl Inner {
fn get(&mut self) -> &mut dyn Demuxer {
match self {
Inner::Ts(d) => d.as_mut(),
Inner::Other(d) => d.as_mut(),
}
}
}
pub(crate) struct SegmentDemuxer {
pub streams: Vec<StreamInfo>,
inner: Inner,
offset: Duration,
update: Option<StreamInfo>,
peeked: VecDeque<(RawPacket, Option<StreamInfo>)>,
}
impl SegmentDemuxer {
#[cfg(test)]
pub fn open(data: Vec<u8>, init: Option<&[u8]>, ts_reference: Option<u64>) -> Result<Self> {
Self::open_with(data, init, ts_reference, None)
}
pub fn open_with(data: Vec<u8>, init: Option<&[u8]>, ts_reference: Option<u64>, params: Option<demux::VideoParams>) -> Result<Self> {
let format = detect(&data).or_else(|| init.map(|_| SegmentFormat::Fmp4));
let (inner, offset) = match format {
Some(SegmentFormat::Ts) => (Inner::Ts(Box::new(demux::TsDemuxer::open_segment_with(data, ts_reference, params)?)), Duration::ZERO),
Some(SegmentFormat::Fmp4) => {
let bytes = match init {
Some(init) => [init, &data].concat(),
None => data,
};
(Inner::Other(Box::new(demux::Mp4Demuxer::open(Box::new(MemorySource::new(bytes, "fMP4 segment")))?)), Duration::ZERO)
}
Some(SegmentFormat::PackedAudio) => {
let offset = packed_audio_timestamp(&data).unwrap_or_default();
let mut src: Box<dyn MediaSource> = Box::new(MemorySource::new(data, "packed audio segment"));
let d: Box<dyn Demuxer> = match demux::probe(src.as_mut())? {
Some(ContainerFormat::Mp3) => Box::new(demux::Mp3Demuxer::open(src)?),
_ => Box::new(demux::AdtsDemuxer::open(src)?),
};
(Inner::Other(d), offset)
}
None => return Err(Error::Demux("HLS segment of unknown format".into())),
};
let mut d = Self { streams: Vec::new(), inner, offset, update: None, peeked: VecDeque::new() };
let all = d.inner.get().streams().to_vec();
for kind in [StreamKind::Video, StreamKind::Audio] {
if let Some(s) = all.iter().find(|s| s.kind == kind && !matches!(s.codec, Codec::Other(_))) {
d.streams.push(s.clone());
}
}
if d.streams.is_empty() {
return Err(Error::Demux("HLS segment without audio or video".into()));
}
Ok(d)
}
pub fn first_raw(&mut self) -> Result<Option<Duration>> {
const LOOK_AHEAD: usize = 64;
while self.peeked.len() < LOOK_AHEAD
&& !self.streams.iter().all(|s| self.peeked.iter().any(|(p, _)| p.kind == s.kind))
{
match self.read()? {
Some(p) => {
let u = self.update.take();
self.peeked.push_back((p, u));
}
None => break,
}
}
let mut firsts = self.streams.iter().filter_map(|s| self.peeked.iter().find(|(p, _)| p.kind == s.kind).map(|(p, _)| p.raw));
Ok(firsts.by_ref().min())
}
pub fn next(&mut self) -> Result<Option<RawPacket>> {
if let Some((p, u)) = self.peeked.pop_front() {
self.update = u;
return Ok(Some(p));
}
self.read()
}
fn read(&mut self) -> Result<Option<RawPacket>> {
loop {
let Some(p) = self.inner.get().next_packet()? else { return Ok(None) };
let update = self.inner.get().take_stream_update();
let Some(i) = self.streams.iter().position(|s| s.id == p.stream) else { continue };
if let Some(info) = update.filter(|u| u.id == p.stream) {
self.streams[i] = info.clone();
self.update = Some(info);
}
return Ok(Some(RawPacket { kind: self.streams[i].kind, raw: p.pts + self.offset, keyframe: p.keyframe, data: p.data }));
}
}
pub fn take_update(&mut self) -> Option<StreamInfo> {
self.update.take()
}
pub fn ts_video_params(&self) -> Option<demux::VideoParams> {
match &self.inner {
Inner::Ts(d) => d.video_params(),
Inner::Other(_) => None,
}
}
pub fn ts_last_raw(&self) -> Option<u64> {
match &self.inner {
Inner::Ts(d) => d.last_raw_pts(),
Inner::Other(_) => None,
}
}
}
pub(crate) fn packed_audio_timestamp(data: &[u8]) -> Option<Duration> {
const OWNER: &[u8] = b"com.apple.streaming.transportStreamTimestamp\0";
if data.get(..3)? != b"ID3" {
return None;
}
let version = data[3];
let syncsafe = |b: &[u8]| b.iter().fold(0usize, |n, &x| n << 7 | (x & 0x7F) as usize);
let size = syncsafe(data.get(6..10)?);
let tag = data.get(10..10 + size)?;
let mut pos = 0;
if data[5] & 0x40 != 0 {
let len = tag.get(..4)?;
pos = if version >= 4 { syncsafe(len) } else { u32::from_be_bytes(len.try_into().ok()?) as usize + 4 };
}
while pos + 10 <= tag.len() {
let id = &tag[pos..pos + 4];
let len_bytes = &tag[pos + 4..pos + 8];
let len = if version >= 4 { syncsafe(len_bytes) } else { u32::from_be_bytes(len_bytes.try_into().ok()?) as usize };
let body = tag.get(pos + 10..pos + 10 + len)?;
if id == b"PRIV"
&& let Some(ts) = body.strip_prefix(OWNER)
{
let ticks = u64::from_be_bytes(ts.get(..8)?.try_into().ok()?) & ((1 << 33) - 1);
return Some(ticks_to_duration(ticks));
}
if id[0] == 0 {
break; }
pos += 10 + len;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> Vec<u8> {
std::fs::read(format!("{}/tests/fixtures/hls/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap()
}
fn all(mut d: SegmentDemuxer) -> Vec<RawPacket> {
std::iter::from_fn(|| d.next().unwrap()).collect()
}
#[test]
fn detects_formats() {
let ts = std::fs::read(format!("{}/tests/fixtures/h264_aac.ts", env!("CARGO_MANIFEST_DIR"))).unwrap();
assert_eq!(detect(&ts), Some(SegmentFormat::Ts));
assert_eq!(detect(&fixture("fmp4/seg0.m4s")), Some(SegmentFormat::Fmp4), "styp first");
assert_eq!(detect(&fixture("fmp4/init.mp4")), Some(SegmentFormat::Fmp4));
assert_eq!(detect(&fixture("packed/seg0.aac")), Some(SegmentFormat::PackedAudio));
assert_eq!(detect(b"<html>not media</html>"), None);
}
#[test]
fn fmp4_segment_gives_hevc_and_aac_timed_by_tfdt() {
let init = fixture("fmp4/init.mp4");
let d = SegmentDemuxer::open(fixture("fmp4/seg1.m4s"), Some(&init), None).unwrap();
let video = d.streams.iter().find(|s| s.kind == StreamKind::Video).unwrap();
assert_eq!((video.codec.clone(), video.width, video.height), (Codec::Hevc, 320, 180));
assert!(video.extradata.is_some(), "hvcC");
let audio = d.streams.iter().find(|s| s.kind == StreamKind::Audio).unwrap();
assert_eq!((audio.codec.clone(), audio.sample_rate), (Codec::Aac, 48_000));
let packets = all(d);
let first_video = packets.iter().find(|p| p.kind == StreamKind::Video).unwrap();
assert!(first_video.keyframe);
assert!(first_video.raw.abs_diff(Duration::from_millis(1880)) < Duration::from_millis(5), "{:?}", first_video.raw);
assert!(packets.iter().filter(|p| p.kind == StreamKind::Video).count() >= 50);
}
#[test]
fn packed_audio_is_timed_by_its_priv_timestamp() {
let data = fixture("packed/seg1.aac");
let start = packed_audio_timestamp(&data).unwrap();
assert_eq!(start, ticks_to_duration(1_080_480), "900000 + 94 frames of 1024 at 48 kHz");
let d = SegmentDemuxer::open(data, None, None).unwrap();
assert_eq!(d.streams.len(), 1);
assert_eq!(d.streams[0].codec, Codec::Aac);
let packets = all(d);
assert_eq!(packets[0].raw, start);
assert_eq!(packets.len(), 94);
assert!(packets.iter().all(|p| p.kind == StreamKind::Audio && p.keyframe));
}
#[test]
fn ts_segments_keep_raw_unwrapped_times() {
let ts = std::fs::read(format!("{}/tests/fixtures/h264_aac.ts", env!("CARGO_MANIFEST_DIR"))).unwrap();
let mut d = SegmentDemuxer::open(ts, None, None).unwrap();
let first = d.next().unwrap().unwrap();
assert!(first.raw > Duration::from_secs(1), "raw TS clock (ffmpeg starts at 1.4 s): {:?}", first.raw);
while d.next().unwrap().is_some() {}
assert!(d.ts_last_raw().unwrap() > 90_000 * 4, "{:?}", d.ts_last_raw());
}
#[test]
fn garbage_is_an_error() {
assert!(SegmentDemuxer::open(b"<html>404</html>".to_vec(), None, None).is_err());
}
}