2023-04-24 17:18:55 +00:00
|
|
|
use io::Read;
|
2023-05-02 18:05:21 +00:00
|
|
|
use std::collections::VecDeque;
|
|
|
|
use std::{fs, io, time};
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
use std::io::Write;
|
2023-04-24 17:18:55 +00:00
|
|
|
|
|
|
|
use anyhow;
|
2023-05-02 18:05:21 +00:00
|
|
|
use mp4;
|
2023-04-24 17:18:55 +00:00
|
|
|
|
2023-05-02 18:05:21 +00:00
|
|
|
use mp4::{ReadBox, WriteBox};
|
2023-04-24 17:18:55 +00:00
|
|
|
|
|
|
|
pub struct Source {
|
2023-05-02 18:05:05 +00:00
|
|
|
// We read the file once, in order, and don't seek backwards.
|
2023-04-24 17:18:55 +00:00
|
|
|
reader: io::BufReader<fs::File>,
|
2023-04-24 20:07:06 +00:00
|
|
|
|
2023-05-02 18:05:05 +00:00
|
|
|
// Any fragments parsed and ready to be returned by next().
|
|
|
|
fragments: VecDeque<Fragment>,
|
|
|
|
|
|
|
|
// The timestamp when the broadcast "started", so we can sleep to simulate a live stream.
|
2023-04-24 20:07:06 +00:00
|
|
|
start: time::Instant,
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
// The raw ftyp box, which we need duplicate for each track, but we don't know how many tracks exist yet.
|
|
|
|
ftyp: Vec<u8>,
|
|
|
|
|
|
|
|
// The parsed moov box, so we can look up track information later.
|
|
|
|
moov: Option<mp4::MoovBox>,
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Fragment {
|
2023-05-02 18:05:05 +00:00
|
|
|
// The track ID for the fragment.
|
|
|
|
pub track: u32,
|
|
|
|
|
|
|
|
// The type of the fragment.
|
2023-04-24 20:07:06 +00:00
|
|
|
pub typ: mp4::BoxType,
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
// The data of the fragment.
|
2023-04-24 17:18:55 +00:00
|
|
|
pub data: Vec<u8>,
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
// Whether this fragment is a keyframe.
|
2023-04-24 18:45:46 +00:00
|
|
|
pub keyframe: bool,
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
// The timestamp of the fragment, in milliseconds, to simulate a live stream.
|
2023-05-02 18:05:21 +00:00
|
|
|
pub timestamp: Option<u64>,
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Source {
|
|
|
|
pub fn new(path: &str) -> io::Result<Self> {
|
|
|
|
let f = fs::File::open(path)?;
|
|
|
|
let reader = io::BufReader::new(f);
|
|
|
|
let start = time::Instant::now();
|
|
|
|
|
2023-05-02 18:05:21 +00:00
|
|
|
Ok(Self {
|
2023-04-24 17:18:55 +00:00
|
|
|
reader,
|
|
|
|
start,
|
2023-05-02 18:05:05 +00:00
|
|
|
fragments: VecDeque::new(),
|
|
|
|
ftyp: Vec::new(),
|
|
|
|
moov: None,
|
2023-04-24 17:18:55 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-05-02 18:09:36 +00:00
|
|
|
pub fn get(&mut self) -> anyhow::Result<Option<Fragment>> {
|
2023-05-02 18:05:05 +00:00
|
|
|
if self.fragments.is_empty() {
|
|
|
|
self.parse()?;
|
2023-04-24 17:18:55 +00:00
|
|
|
};
|
|
|
|
|
2023-04-24 20:07:06 +00:00
|
|
|
if self.timeout().is_some() {
|
2023-05-02 18:05:21 +00:00
|
|
|
return Ok(None);
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
|
2023-05-02 18:05:05 +00:00
|
|
|
Ok(self.fragments.pop_front())
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
|
2023-05-02 18:05:05 +00:00
|
|
|
fn parse(&mut self) -> anyhow::Result<()> {
|
|
|
|
loop {
|
|
|
|
// Read the next full atom.
|
|
|
|
let atom = read_box(&mut self.reader)?;
|
|
|
|
|
|
|
|
// Before we return it, let's do some simple parsing.
|
|
|
|
let mut reader = io::Cursor::new(&atom);
|
|
|
|
let header = mp4::BoxHeader::read(&mut reader)?;
|
|
|
|
|
|
|
|
match header.name {
|
|
|
|
mp4::BoxType::FtypBox => {
|
|
|
|
// Don't return anything until we know the total number of tracks.
|
|
|
|
// To be honest, I didn't expect the borrow checker to allow this, but it does!
|
|
|
|
self.ftyp = atom;
|
2023-05-02 18:05:21 +00:00
|
|
|
}
|
2023-05-02 18:05:05 +00:00
|
|
|
mp4::BoxType::MoovBox => {
|
|
|
|
// We need to split the moov based on the tracks.
|
|
|
|
let moov = mp4::MoovBox::read_box(&mut reader, header.size)?;
|
|
|
|
|
|
|
|
for trak in &moov.traks {
|
|
|
|
let track_id = trak.tkhd.track_id;
|
|
|
|
|
|
|
|
// Push the styp atom for each track.
|
|
|
|
self.fragments.push_back(Fragment {
|
|
|
|
track: track_id,
|
|
|
|
typ: mp4::BoxType::FtypBox,
|
|
|
|
data: self.ftyp.clone(),
|
|
|
|
keyframe: false,
|
|
|
|
timestamp: None,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Unfortunately, we need to create a brand new moov atom for each track.
|
|
|
|
// We remove every box for other track IDs.
|
|
|
|
let mut toov = moov.clone();
|
|
|
|
toov.traks.retain(|t| t.tkhd.track_id == track_id);
|
2023-05-02 18:05:21 +00:00
|
|
|
toov.mvex
|
|
|
|
.as_mut()
|
|
|
|
.expect("missing mvex")
|
|
|
|
.trexs
|
|
|
|
.retain(|f| f.track_id == track_id);
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
// Marshal the box.
|
|
|
|
let mut toov_data = Vec::new();
|
|
|
|
toov.write_box(&mut toov_data)?;
|
|
|
|
|
|
|
|
let mut file = std::fs::File::create(format!("track{}.mp4", track_id))?;
|
2023-05-02 18:09:36 +00:00
|
|
|
file.write_all(toov_data.as_slice())?;
|
2023-05-02 18:05:05 +00:00
|
|
|
|
|
|
|
self.fragments.push_back(Fragment {
|
|
|
|
track: track_id,
|
|
|
|
typ: mp4::BoxType::MoovBox,
|
|
|
|
data: toov_data,
|
|
|
|
keyframe: false,
|
|
|
|
timestamp: None,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
self.moov = Some(moov);
|
2023-05-02 18:05:21 +00:00
|
|
|
}
|
2023-05-02 18:05:05 +00:00
|
|
|
mp4::BoxType::MoofBox => {
|
|
|
|
let moof = mp4::MoofBox::read_box(&mut reader, header.size)?;
|
|
|
|
|
|
|
|
if moof.trafs.len() != 1 {
|
|
|
|
// We can't split the mdat atom, so this is impossible to support
|
|
|
|
anyhow::bail!("multiple tracks per moof atom")
|
|
|
|
}
|
|
|
|
|
2023-05-02 18:05:21 +00:00
|
|
|
self.fragments.push_back(Fragment {
|
2023-05-02 18:05:05 +00:00
|
|
|
track: moof.trafs[0].tfhd.track_id,
|
|
|
|
typ: mp4::BoxType::MoofBox,
|
|
|
|
data: atom,
|
|
|
|
keyframe: has_keyframe(&moof),
|
|
|
|
timestamp: first_timestamp(&moof),
|
|
|
|
})
|
2023-05-02 18:05:21 +00:00
|
|
|
}
|
2023-05-02 18:05:05 +00:00
|
|
|
mp4::BoxType::MdatBox => {
|
|
|
|
let moof = self.fragments.back().expect("no atom before mdat");
|
|
|
|
assert!(moof.typ == mp4::BoxType::MoofBox, "no moof before mdat");
|
|
|
|
|
2023-05-02 18:05:21 +00:00
|
|
|
self.fragments.push_back(Fragment {
|
2023-05-02 18:05:05 +00:00
|
|
|
track: moof.track,
|
|
|
|
typ: mp4::BoxType::MoofBox,
|
|
|
|
data: atom,
|
|
|
|
keyframe: false,
|
|
|
|
timestamp: None,
|
|
|
|
});
|
|
|
|
|
|
|
|
// We have some media data, return so we can start sending it.
|
2023-05-02 18:05:21 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
2023-05-02 18:05:05 +00:00
|
|
|
_ => anyhow::bail!("unknown top-level atom: {:?}", header.name),
|
2023-04-24 20:07:06 +00:00
|
|
|
}
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-24 20:07:06 +00:00
|
|
|
|
|
|
|
// Simulate a live stream by sleeping until the next timestamp in the media.
|
|
|
|
pub fn timeout(&self) -> Option<time::Duration> {
|
2023-05-02 18:05:05 +00:00
|
|
|
let next = self.fragments.front()?;
|
|
|
|
let timestamp = next.timestamp?;
|
|
|
|
|
|
|
|
// Find the timescale for the track.
|
2023-05-02 18:05:21 +00:00
|
|
|
let track = self
|
|
|
|
.moov
|
|
|
|
.as_ref()?
|
|
|
|
.traks
|
|
|
|
.iter()
|
|
|
|
.find(|t| t.tkhd.track_id == next.track)?;
|
2023-05-02 18:05:05 +00:00
|
|
|
let timescale = track.mdia.mdhd.timescale as u64;
|
2023-04-24 20:07:06 +00:00
|
|
|
|
|
|
|
let delay = time::Duration::from_millis(1000 * timestamp / timescale);
|
|
|
|
let elapsed = self.start.elapsed();
|
|
|
|
|
|
|
|
delay.checked_sub(elapsed)
|
|
|
|
}
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Read a full MP4 atom into a vector.
|
|
|
|
fn read_box<R: io::Read>(reader: &mut R) -> anyhow::Result<Vec<u8>> {
|
|
|
|
// Read the 8 bytes for the size + type
|
|
|
|
let mut buf = [0u8; 8];
|
|
|
|
reader.read_exact(&mut buf)?;
|
|
|
|
|
|
|
|
// Convert the first 4 bytes into the size.
|
|
|
|
let size = u32::from_be_bytes(buf[0..4].try_into()?) as u64;
|
|
|
|
let mut out = buf.to_vec();
|
|
|
|
|
|
|
|
let mut limit = match size {
|
|
|
|
// Runs until the end of the file.
|
|
|
|
0 => reader.take(u64::MAX),
|
|
|
|
|
|
|
|
// The next 8 bytes are the extended size to be used instead.
|
|
|
|
1 => {
|
|
|
|
reader.read_exact(&mut buf)?;
|
|
|
|
let size_large = u64::from_be_bytes(buf);
|
2023-05-02 18:05:21 +00:00
|
|
|
anyhow::ensure!(
|
|
|
|
size_large >= 16,
|
|
|
|
"impossible extended box size: {}",
|
|
|
|
size_large
|
|
|
|
);
|
2023-04-24 17:18:55 +00:00
|
|
|
|
|
|
|
reader.take(size_large - 16)
|
2023-05-02 18:05:21 +00:00
|
|
|
}
|
2023-04-24 17:18:55 +00:00
|
|
|
|
|
|
|
2..=7 => {
|
|
|
|
anyhow::bail!("impossible box size: {}", size)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise read based on the size.
|
2023-05-02 18:05:21 +00:00
|
|
|
size => reader.take(size - 8),
|
2023-04-24 17:18:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Append to the vector and return it.
|
|
|
|
limit.read_to_end(&mut out)?;
|
|
|
|
|
|
|
|
Ok(out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn has_keyframe(moof: &mp4::MoofBox) -> bool {
|
|
|
|
for traf in &moof.trafs {
|
|
|
|
// TODO trak default flags if this is None
|
|
|
|
let default_flags = traf.tfhd.default_sample_flags.unwrap_or_default();
|
|
|
|
let trun = match &traf.trun {
|
|
|
|
Some(t) => t,
|
|
|
|
None => return false,
|
|
|
|
};
|
|
|
|
|
|
|
|
for i in 0..trun.sample_count {
|
|
|
|
let mut flags = match trun.sample_flags.get(i as usize) {
|
|
|
|
Some(f) => *f,
|
|
|
|
None => default_flags,
|
|
|
|
};
|
|
|
|
|
|
|
|
if i == 0 && trun.first_sample_flags.is_some() {
|
|
|
|
flags = trun.first_sample_flags.unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://chromium.googlesource.com/chromium/src/media/+/master/formats/mp4/track_run_iterator.cc#177
|
|
|
|
let keyframe = (flags >> 24) & 0x3 == 0x2; // kSampleDependsOnNoOther
|
|
|
|
let non_sync = (flags >> 16) & 0x1 == 0x1; // kSampleIsNonSyncSample
|
|
|
|
|
2023-04-24 18:45:46 +00:00
|
|
|
if keyframe && !non_sync {
|
2023-05-02 18:05:21 +00:00
|
|
|
return true;
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
false
|
|
|
|
}
|
|
|
|
|
2023-04-24 20:07:06 +00:00
|
|
|
fn first_timestamp(moof: &mp4::MoofBox) -> Option<u64> {
|
|
|
|
Some(moof.trafs.first()?.tfdt.as_ref()?.base_media_decode_time)
|
2023-04-24 17:18:55 +00:00
|
|
|
}
|