pub mod mp4;
use std::collections::BTreeMap;
use bit_vec::BitVec;
use itertools::Itertools as _;
use re_log::{debug_assert, debug_panic};
use re_span::Span;
use re_tuid::Tuid;
use web_time::Instant;
use super::{Time, Timescale};
use crate::nalu::AnnexBStreamWriteError;
use crate::{
Chunk, StableIndexDeque, TrackId, TrackKind, write_avc_chunk_to_annexb,
write_hevc_chunk_to_annexb,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChromaSubsamplingModes {
Monochrome,
Yuv444,
Yuv422,
Yuv420,
}
impl std::fmt::Display for ChromaSubsamplingModes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Monochrome => write!(f, "monochrome"),
Self::Yuv444 => write!(f, "4:4:4"),
Self::Yuv422 => write!(f, "4:2:2"),
Self::Yuv420 => write!(f, "4:2:0"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VideoCodec {
H264,
H265,
AV1,
VP8,
VP9,
ImageSequence(Option<String>),
}
pub type SampleIndex = usize;
pub type KeyframeIndex = usize;
#[derive(Clone, Debug)]
pub enum VideoDeliveryMethod {
Static { duration: Time },
Stream {
last_time_updated_samples: Instant,
},
}
impl VideoDeliveryMethod {
#[inline]
pub fn new_stream() -> Self {
Self::Stream {
last_time_updated_samples: Instant::now(),
}
}
}
#[derive(Clone)]
pub struct VideoDataDescription {
pub codec: VideoCodec,
pub encoding_details: Option<VideoEncodingDetails>,
pub timescale: Option<Timescale>,
pub delivery_method: VideoDeliveryMethod,
pub keyframe_indices: Vec<SampleIndex>,
pub samples: StableIndexDeque<SampleMetadataState>,
pub samples_statistics: SamplesStatistics,
pub mp4_tracks: BTreeMap<TrackId, Option<TrackKind>>,
}
impl re_byte_size::SizeBytes for VideoDataDescription {
fn heap_size_bytes(&self) -> u64 {
let Self {
codec: _,
encoding_details: _,
timescale: _,
delivery_method: _,
keyframe_indices,
samples,
samples_statistics,
mp4_tracks,
} = self;
keyframe_indices.heap_size_bytes()
+ samples.heap_size_bytes()
+ samples_statistics.heap_size_bytes()
+ mp4_tracks.len() as u64 * std::mem::size_of::<(TrackId, Option<TrackKind>)>() as u64
}
}
impl VideoDataDescription {
pub fn gop_sample_range_for_keyframe(
&self,
keyframe_idx: usize,
) -> Option<std::ops::Range<SampleIndex>> {
Some(
*self.keyframe_indices.get(keyframe_idx)?
..self
.keyframe_indices
.get(keyframe_idx + 1)
.copied()
.unwrap_or_else(|| self.samples.next_index()),
)
}
pub fn image_codec_mime_type(&self) -> Option<&str> {
let VideoCodec::ImageSequence(codec) = &self.codec else {
return None;
};
if let Some(details) = &self.encoding_details {
Some(&details.codec_string)
} else {
codec.as_deref()
}
}
pub fn sanity_check(&self) -> Result<(), String> {
self.sanity_check_keyframes()?;
self.sanity_check_samples()?;
if let Some(stsd) = self.encoding_details.as_ref().and_then(|e| e.stsd.as_ref()) {
let stsd_codec = match &stsd.contents {
re_mp4::StsdBoxContent::Av01(_) => crate::VideoCodec::AV1,
re_mp4::StsdBoxContent::Avc1(_) => crate::VideoCodec::H264,
re_mp4::StsdBoxContent::Hvc1(_) | re_mp4::StsdBoxContent::Hev1(_) => {
crate::VideoCodec::H265
}
re_mp4::StsdBoxContent::Vp08(_) => crate::VideoCodec::VP8,
re_mp4::StsdBoxContent::Vp09(_) => crate::VideoCodec::VP9,
_ => {
return Err(format!(
"STSD box content type {:?} doesn't have a supported codec.",
stsd.contents
));
}
};
if stsd_codec != self.codec {
return Err(format!(
"STSD box content type {:?} does not match with the internal codec {:?}.",
stsd.contents, self.codec
));
}
}
Ok(())
}
fn sanity_check_keyframes(&self) -> Result<(), String> {
if !self.keyframe_indices.is_sorted() {
return Err("Keyframes aren't sorted".to_owned());
}
for &keyframe in &self.keyframe_indices {
if keyframe < self.samples.min_index() {
return Err(format!(
"Keyframe {keyframe} refers to sample to the left of the list of samples.",
));
}
if keyframe >= self.samples.next_index() {
return Err(format!(
"Keyframe {keyframe} refers to sample to the right of the list of samples.",
));
}
match &self.samples[keyframe] {
SampleMetadataState::Present(sample_metadata) => {
if !sample_metadata.is_sync {
return Err(format!("Keyframe {keyframe} is not marked with `is_sync`."));
}
}
SampleMetadataState::Unloaded { .. } => {
return Err(format!("Keyframe {keyframe} refers to an unloaded sample"));
}
}
}
let mut keyframes = self.keyframe_indices.iter().copied();
for (sample_idx, sample) in self
.samples
.iter_indexed()
.filter_map(|(idx, s)| Some((idx, s.sample()?)))
{
if sample.is_sync && keyframes.next().is_none_or(|idx| idx != sample_idx) {
return Err(format!("Not tracking the keyframe {sample_idx}."));
}
}
Ok(())
}
fn sanity_check_samples(&self) -> Result<(), String> {
for ((a_idx, a), (b_idx, b)) in self.samples.iter_indexed().tuple_windows() {
if let SampleMetadataState::Present(a) = a
&& let SampleMetadataState::Present(b) = b
&& a.decode_timestamp > b.decode_timestamp
{
return Err(format!(
"Decode timestamps for {a_idx}..{b_idx} are not monotonically increasing: {:?} {:?}",
a.decode_timestamp, b.decode_timestamp
));
}
}
let expected_statistics = SamplesStatistics::new(&self.samples);
if expected_statistics != self.samples_statistics {
return Err(format!(
"Sample statistics are not consistent with the samples.\nExpected: {:?}\nActual: {:?}",
expected_statistics, self.samples_statistics
));
}
Ok(())
}
pub fn sample_data_in_stream_format(
&self,
chunk: &crate::Chunk,
) -> Result<Vec<u8>, SampleConversionError> {
match self.codec {
VideoCodec::H264 => {
let stsd = self
.encoding_details
.as_ref()
.ok_or_else(|| {
SampleConversionError::MissingEncodingDetails(self.codec.clone())
})?
.stsd
.as_ref()
.ok_or_else(|| SampleConversionError::MissingStsd(self.codec.clone()))?;
let re_mp4::StsdBoxContent::Avc1(avc1_box) = &stsd.contents else {
return Err(SampleConversionError::UnexpectedStsdContent {
codec: self.codec.clone(),
found: format!("{:?}", stsd.contents),
});
};
let mut output = Vec::new();
write_avc_chunk_to_annexb(avc1_box, &mut output, chunk.is_sync, chunk)
.map_err(SampleConversionError::AnnexB)?;
Ok(output)
}
VideoCodec::H265 => {
let stsd = self
.encoding_details
.as_ref()
.ok_or_else(|| {
SampleConversionError::MissingEncodingDetails(self.codec.clone())
})?
.stsd
.as_ref()
.ok_or_else(|| SampleConversionError::MissingStsd(self.codec.clone()))?;
let hvcc_box = match &stsd.contents {
re_mp4::StsdBoxContent::Hvc1(hvc1_box)
| re_mp4::StsdBoxContent::Hev1(hvc1_box) => hvc1_box,
other => {
return Err(SampleConversionError::UnexpectedStsdContent {
codec: self.codec.clone(),
found: format!("{other:?}"),
});
}
};
let mut output = Vec::new();
write_hevc_chunk_to_annexb(hvcc_box, &mut output, chunk.is_sync, chunk)
.map_err(SampleConversionError::AnnexB)?;
Ok(output)
}
VideoCodec::AV1 | VideoCodec::VP8 | VideoCodec::VP9 | VideoCodec::ImageSequence(_) => {
Ok(chunk.data.clone())
}
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum SampleConversionError {
#[error("Missing encoding details for codec {0:?}")]
MissingEncodingDetails(VideoCodec),
#[error("Missing stsd box for codec {0:?}")]
MissingStsd(VideoCodec),
#[error("Unexpected stsd contents for codec {codec:?}: {found}")]
UnexpectedStsdContent { codec: VideoCodec, found: String },
#[error("Failed converting sample to Annex-B: {0}")]
AnnexB(#[from] AnnexBStreamWriteError),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VideoEncodingDetails {
pub codec_string: String,
pub coded_dimensions: [u16; 2],
pub bit_depth: Option<u8>,
pub chroma_subsampling: Option<ChromaSubsamplingModes>,
pub stsd: Option<re_mp4::StsdBox>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GopSizes {
pub smallest: usize,
pub largest: usize,
}
impl GopSizes {
const NO_SAMPLES: Self = Self {
smallest: 0,
largest: 0,
};
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SamplesStatistics {
pub dts_always_equal_pts: bool,
pub has_sample_highest_pts_so_far: Option<BitVec>,
pub gop_sizes: GopSizes,
}
impl re_byte_size::SizeBytes for SamplesStatistics {
fn heap_size_bytes(&self) -> u64 {
let Self {
dts_always_equal_pts: _,
has_sample_highest_pts_so_far,
gop_sizes: _,
} = self;
has_sample_highest_pts_so_far
.as_ref()
.map_or(0, |bitvec| bitvec.capacity() as u64 / 8)
}
}
impl SamplesStatistics {
pub const NO_BFRAMES: Self = Self {
dts_always_equal_pts: true,
has_sample_highest_pts_so_far: None,
gop_sizes: GopSizes::NO_SAMPLES,
};
pub fn new(samples: &StableIndexDeque<SampleMetadataState>) -> Self {
re_tracing::profile_function!();
let dts_always_equal_pts = samples
.iter()
.filter_map(|s| s.sample())
.all(|s| s.decode_timestamp == s.presentation_timestamp);
let mut biggest_pts_so_far = Time::MIN;
let has_sample_highest_pts_so_far = (!dts_always_equal_pts).then(|| {
samples
.iter()
.map(move |sample| {
sample.sample().is_some_and(|sample| {
if sample.presentation_timestamp > biggest_pts_so_far {
biggest_pts_so_far = sample.presentation_timestamp;
true
} else {
false
}
})
})
.collect()
});
let gop_sizes = samples
.iter()
.fold(
(None::<GopSizes>, 0usize),
|(mut sizes, mut count), sample| {
let ends_gop = match sample {
SampleMetadataState::Present(s) => s.is_sync,
SampleMetadataState::Unloaded { .. } => true,
};
if ends_gop && count > 0 {
sizes = Some(match sizes {
Some(s) => GopSizes {
smallest: s.smallest.min(count),
largest: s.largest.max(count),
},
None => GopSizes {
smallest: count,
largest: count,
},
});
count = 0;
}
if sample.is_loaded() {
count += 1;
}
(sizes, count)
},
)
.0
.unwrap_or(GopSizes::NO_SAMPLES);
Self {
dts_always_equal_pts,
has_sample_highest_pts_so_far,
gop_sizes,
}
}
}
impl VideoDataDescription {
pub fn load_from_bytes(
data: &[u8],
media_type: &str,
debug_name: &str,
) -> Result<Self, VideoLoadError> {
if data.is_empty() {
return Err(VideoLoadError::ZeroBytes);
}
re_tracing::profile_function!();
match media_type {
"video/mp4" => Self::load_mp4(data, debug_name),
media_type => {
if media_type.starts_with("video/") {
Err(VideoLoadError::UnsupportedMimeType {
provided_or_detected_media_type: media_type.to_owned(),
})
} else {
Err(VideoLoadError::MimeTypeIsNotAVideo {
provided_or_detected_media_type: media_type.to_owned(),
})
}
}
}
}
#[inline]
pub fn human_readable_codec_string(&self) -> String {
let base_codec_string = match &self.codec {
VideoCodec::H264 => "H.264 AVC1",
VideoCodec::H265 => "H.265 HEV1",
VideoCodec::AV1 => "AV1",
VideoCodec::VP8 => "VP8",
VideoCodec::VP9 => "VP9",
VideoCodec::ImageSequence(_) => {
if let Some(codec) = self.image_codec_mime_type() {
return codec.to_owned();
} else {
return "unknown".to_owned();
}
}
}
.to_owned();
if let Some(encoding_details) = self.encoding_details.as_ref() {
format!("{base_codec_string} ({})", encoding_details.codec_string)
} else {
base_codec_string
}
}
#[inline]
pub fn num_samples(&self) -> usize {
self.samples.num_elements()
}
pub fn duration(&self) -> Option<std::time::Duration> {
let timescale = self.timescale?;
Some(match &self.delivery_method {
VideoDeliveryMethod::Static { duration } => duration.duration(timescale),
VideoDeliveryMethod::Stream { .. } => match self.samples.num_elements() {
0 => std::time::Duration::ZERO,
1 => {
let first = self.samples.iter().find_map(|s| s.sample())?;
first
.duration
.map(|d| d.duration(timescale))
.unwrap_or(std::time::Duration::ZERO)
}
_ => {
let first = self.samples.iter().find_map(|s| s.sample())?;
let last = self.samples.iter().rev().find_map(|s| s.sample())?;
let last_sample_duration = last.duration.map_or_else(
|| {
(last.presentation_timestamp - first.presentation_timestamp)
.duration(timescale)
/ (last.frame_nr - first.frame_nr)
},
|d| d.duration(timescale),
);
(last.presentation_timestamp - first.presentation_timestamp).duration(timescale)
+ last_sample_duration
}
},
})
}
#[inline]
pub fn average_fps(&self) -> Option<f32> {
self.duration().map(|duration| {
let num_frames = self.num_samples();
num_frames as f32 / duration.as_secs_f32()
})
}
pub fn frame_timestamps_nanos(&self) -> Option<impl Iterator<Item = i64> + '_> {
let timescale = self.timescale?;
Some(
self.samples
.iter()
.filter_map(|sample| Some(sample.sample()?.presentation_timestamp))
.sorted()
.map(move |pts| pts.into_nanos(timescale)),
)
}
fn latest_sample_index_at_decode_timestamp(
samples: &StableIndexDeque<SampleMetadataState>,
decode_time: Time,
) -> Option<SampleIndex> {
let idx = samples
.partition_point(|sample| sample.decode_timestamp() <= decode_time)
.checked_sub(1)?;
(idx >= samples.min_index()).then_some(idx)
}
fn latest_sample_index_at_presentation_timestamp_internal(
samples: &StableIndexDeque<SampleMetadataState>,
sample_statistics: &SamplesStatistics,
presentation_timestamp: Time,
) -> Result<SampleIndex, Option<SampleIndex>> {
let decode_sample_idx =
Self::latest_sample_index_at_decode_timestamp(samples, presentation_timestamp)
.ok_or(None)?;
if let Some(sample) = samples.get(decode_sample_idx)
&& sample.is_unloaded()
{
return Err(Some(decode_sample_idx));
}
let Some(has_sample_highest_pts_so_far) =
sample_statistics.has_sample_highest_pts_so_far.as_ref()
else {
debug_assert!(sample_statistics.dts_always_equal_pts);
return Ok(decode_sample_idx);
};
debug_assert!(has_sample_highest_pts_so_far.len() == samples.num_elements());
let mut best_index = SampleIndex::MAX;
let mut best_pts = Time::MIN;
for sample_idx in (samples.min_index()..=decode_sample_idx).rev() {
let Some(sample) = samples[sample_idx].sample() else {
return Err(Some(sample_idx));
};
if sample.presentation_timestamp == presentation_timestamp {
return Ok(sample_idx);
}
if sample.presentation_timestamp < presentation_timestamp
&& sample.presentation_timestamp > best_pts
{
best_pts = sample.presentation_timestamp;
best_index = sample_idx;
}
if best_pts != Time::MIN
&& has_sample_highest_pts_so_far[sample_idx - samples.min_index()]
{
return Ok(best_index);
}
}
Err(None)
}
pub fn latest_sample_index_at_presentation_timestamp(
&self,
presentation_timestamp: Time,
) -> Result<SampleIndex, Option<SampleIndex>> {
Self::latest_sample_index_at_presentation_timestamp_internal(
&self.samples,
&self.samples_statistics,
presentation_timestamp,
)
}
pub fn previous_presented_sample(&self, sample: &SampleMetadata) -> Option<&SampleMetadata> {
let idx = Self::latest_sample_index_at_presentation_timestamp_internal(
&self.samples,
&self.samples_statistics,
sample.presentation_timestamp - Time::new(1),
)
.ok()?;
match self.samples.get(idx) {
Some(SampleMetadataState::Present(sample)) => Some(sample),
None | Some(_) => unreachable!(),
}
}
pub fn sample_keyframe_idx(&self, sample_idx: SampleIndex) -> Option<KeyframeIndex> {
self.keyframe_indices
.partition_point(|idx| *idx <= sample_idx)
.checked_sub(1)
}
fn find_keyframe_index(
&self,
cmp_time: impl Fn(&SampleMetadata) -> bool,
) -> Option<KeyframeIndex> {
self.keyframe_indices
.partition_point(|sample_idx| {
if let Some(sample) = self.samples[*sample_idx].sample() {
cmp_time(sample)
} else {
debug_panic!("keyframe indices should always be valid");
false
}
})
.checked_sub(1)
}
pub fn decode_time_keyframe_index(&self, decode_time: Time) -> Option<KeyframeIndex> {
self.find_keyframe_index(|t| t.decode_timestamp <= decode_time)
}
pub fn presentation_time_keyframe_index(&self, pts: Time) -> Option<KeyframeIndex> {
self.find_keyframe_index(|t| t.presentation_timestamp <= pts)
}
}
#[derive(Debug, Clone)]
pub enum SampleMetadataState {
Present(SampleMetadata),
Unloaded { source_id: Tuid, min_dts: Time },
}
impl SampleMetadataState {
pub fn sample(&self) -> Option<&SampleMetadata> {
match self {
Self::Present(sample_metadata) => Some(sample_metadata),
Self::Unloaded { .. } => None,
}
}
pub fn sample_mut(&mut self) -> Option<&mut SampleMetadata> {
match self {
Self::Present(sample_metadata) => Some(sample_metadata),
Self::Unloaded { .. } => None,
}
}
pub fn source(&self) -> VideoSource {
match self {
Self::Present(sample) => sample.source,
Self::Unloaded { source_id, .. } => VideoSource::Id {
id: *source_id,
sub_id: None,
},
}
}
pub fn source_primary_id(&self) -> Option<Tuid> {
match self {
Self::Present(sample) => sample.source.primary_id(),
Self::Unloaded { source_id, .. } => Some(*source_id),
}
}
pub fn source_to_mark_in_use(&self) -> Option<VideoSource> {
self.source_primary_id()
.map(|id| VideoSource::Id { id, sub_id: None })
}
pub fn set_source_primary_id(&mut self, new_id: Tuid) {
match self {
Self::Present(sample) => match &mut sample.source {
VideoSource::Span(_) => {}
VideoSource::Id { id, .. } => *id = new_id,
},
Self::Unloaded { source_id, .. } => *source_id = new_id,
}
}
pub fn decode_timestamp(&self) -> Time {
match self {
Self::Present(sample) => sample.decode_timestamp,
Self::Unloaded { min_dts, .. } => *min_dts,
}
}
pub fn unload(&mut self, new_primary_id: Option<Tuid>) {
match self {
Self::Present(sample) => {
let dts = sample.decode_timestamp;
let id = new_primary_id
.or_else(|| sample.source.primary_id())
.unwrap_or(Tuid::ZERO);
*self = Self::Unloaded {
source_id: id,
min_dts: dts,
}
}
Self::Unloaded {
source_id,
min_dts: _,
} => {
if let Some(new_id) = new_primary_id {
*source_id = new_id;
}
}
}
}
pub fn is_loaded(&self) -> bool {
match self {
Self::Present(_) => true,
Self::Unloaded { .. } => false,
}
}
pub fn is_unloaded(&self) -> bool {
!self.is_loaded()
}
}
impl re_byte_size::SizeBytes for SampleMetadataState {
fn heap_size_bytes(&self) -> u64 {
match self {
Self::Present(sample_metadata) => sample_metadata.heap_size_bytes(),
Self::Unloaded {
source_id: _,
min_dts: _,
} => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VideoSource {
Span(Span<u32>),
Id { id: Tuid, sub_id: Option<Tuid> },
}
impl VideoSource {
#[inline]
pub fn id(id: Tuid, sub_id: Tuid) -> Self {
Self::Id {
id,
sub_id: Some(sub_id),
}
}
#[inline]
pub fn only_source(self) -> Self {
match self {
Self::Span(_) => self,
Self::Id { id, .. } => Self::Id { id, sub_id: None },
}
}
#[inline]
pub fn primary_id(&self) -> Option<Tuid> {
match self {
Self::Span(_) => None,
Self::Id { id, .. } => Some(*id),
}
}
}
impl re_byte_size::SizeBytes for VideoSource {
fn heap_size_bytes(&self) -> u64 {
0
}
fn is_pod() -> bool {
true
}
}
#[derive(Debug, Clone)]
pub struct SampleMetadata {
pub is_sync: bool,
pub frame_nr: u32,
pub decode_timestamp: Time,
pub presentation_timestamp: Time,
pub duration: Option<Time>,
pub source: VideoSource,
}
impl re_byte_size::SizeBytes for SampleMetadata {
fn heap_size_bytes(&self) -> u64 {
0
}
fn is_pod() -> bool {
true
}
}
impl SampleMetadata {
pub fn get<'a>(
&self,
get_video_chunk: &dyn Fn(VideoSource) -> &'a [u8],
sample_idx: SampleIndex,
) -> Option<Chunk> {
let bytes = get_video_chunk(self.source);
if bytes.is_empty() {
return None;
}
Some(Chunk {
data: bytes.to_vec(),
sample_idx,
frame_nr: self.frame_nr,
decode_timestamp: self.decode_timestamp,
presentation_timestamp: self.presentation_timestamp,
duration: self.duration,
is_sync: self.is_sync,
})
}
}
#[derive(thiserror::Error, Debug)]
pub enum VideoLoadError {
#[error("The video file is empty (zero bytes)")]
ZeroBytes,
#[error("MP4 error: {0}")]
ParseMp4(#[from] re_mp4::Error),
#[error("Video file has no video tracks")]
NoVideoTrack,
#[error("Video file track config is invalid")]
InvalidConfigFormat,
#[error("Video file has invalid sample entries")]
InvalidSamples,
#[error(
"Video file has no timescale, which is required to determine frame timestamps in time units"
)]
NoTimescale,
#[error("The media type of the blob is not a video: {provided_or_detected_media_type}")]
MimeTypeIsNotAVideo {
provided_or_detected_media_type: String,
},
#[error("MIME type '{provided_or_detected_media_type}' is not supported for videos")]
UnsupportedMimeType {
provided_or_detected_media_type: String,
},
#[error("Could not detect MIME type from the video contents")]
UnrecognizedMimeType,
#[error("Video track uses unsupported codec \"{0}\"")] UnsupportedCodec(re_mp4::FourCC),
#[error("Unable to determine codec string from the video contents")]
UnableToDetermineCodecString,
#[error("Failed to parse H.264 SPS from mp4: {0:?}")]
SpsParsingError(h264_reader::nal::sps::SpsError),
}
impl re_byte_size::SizeBytes for VideoLoadError {
fn heap_size_bytes(&self) -> u64 {
0 }
}
impl std::fmt::Debug for VideoDataDescription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
codec,
encoding_details,
timescale,
delivery_method,
keyframe_indices,
samples,
samples_statistics,
mp4_tracks,
} = self;
f.debug_struct("Video")
.field("codec", codec)
.field("encoding_details", encoding_details)
.field("timescale", timescale)
.field("delivery_method", delivery_method)
.field("keyframe_indices", keyframe_indices)
.field("samples", &samples.iter_indexed().collect::<Vec<_>>())
.field("samples_statistics", samples_statistics)
.field("mp4_tracks", mp4_tracks)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nalu::ANNEXB_NAL_START_CODE;
#[test]
fn test_latest_sample_index_at_presentation_timestamp() {
let pts = [
0, 1024, 512, 256, 768, 2048, 1536, 1280, 1792, 3072, 2560, 2304, 2816, 4096, 3584,
3328, 3840, 4864, 4352, 4608, 5888, 5376, 5120, 5632, 6912, 6400, 6144, 6656, 7936,
7424, 7168, 7680, 8960, 8448, 8192, 8704, 9984, 9472, 9216, 9728, 11008, 10496, 10240,
10752, 12032, 11520, 11264, 11776, 13056, 12544,
];
let dts = [
-512, -256, 0, 256, 512, 768, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072,
3328, 3584, 3840, 4096, 4352, 4608, 4864, 5120, 5376, 5632, 5888, 6144, 6400, 6656,
6912, 7168, 7424, 7680, 7936, 8192, 8448, 8704, 8960, 9216, 9472, 9728, 9984, 10240,
10496, 10752, 11008, 11264, 11520, 11776, 12032,
];
assert_eq!(pts.len(), dts.len());
assert!(pts.iter().zip(dts.iter()).all(|(pts, dts)| dts <= pts));
let mut samples = pts
.into_iter()
.zip(dts)
.map(|(pts, dts)| {
SampleMetadataState::Present(SampleMetadata {
is_sync: true,
frame_nr: 0, decode_timestamp: Time(dts),
presentation_timestamp: Time(pts),
duration: Some(Time(1)),
source: VideoSource::Span(Default::default()),
})
})
.collect::<StableIndexDeque<_>>();
let sample_statistics = SamplesStatistics::new(&samples);
assert!(!sample_statistics.dts_always_equal_pts);
let query_pts = |pts| {
VideoDataDescription::latest_sample_index_at_presentation_timestamp_internal(
&samples,
&sample_statistics,
pts,
)
};
for (idx, sample) in samples.iter_indexed() {
assert_eq!(
Ok(idx),
query_pts(sample.sample().unwrap().presentation_timestamp)
);
}
for (idx, sample) in samples.iter_indexed() {
assert_eq!(
Ok(idx),
query_pts(sample.sample().unwrap().presentation_timestamp + Time(1))
);
assert_eq!(
Ok(idx),
query_pts(sample.sample().unwrap().presentation_timestamp + Time(255))
);
}
assert_eq!(Err(None), query_pts(Time(-1)));
assert_eq!(Err(None), query_pts(Time(-123)));
assert_eq!(Ok(0), query_pts(Time(0)));
assert_eq!(Ok(0), query_pts(Time(1)));
assert_eq!(Ok(0), query_pts(Time(88)));
assert_eq!(Ok(0), query_pts(Time(255)));
assert_eq!(Ok(3), query_pts(Time(256)));
assert_eq!(Ok(3), query_pts(Time(257)));
assert_eq!(Ok(3), query_pts(Time(400)));
assert_eq!(Ok(3), query_pts(Time(511)));
assert_eq!(Ok(2), query_pts(Time(512)));
assert_eq!(Ok(2), query_pts(Time(513)));
assert_eq!(Ok(2), query_pts(Time(600)));
assert_eq!(Ok(2), query_pts(Time(767)));
assert_eq!(Ok(4), query_pts(Time(768)));
assert_eq!(Ok(4), query_pts(Time(1023)));
assert_eq!(Ok(48), query_pts(Time(123123123123123123)));
samples[4].unload(None);
let sample_statistics = SamplesStatistics::new(&samples);
let query_pts_unloaded = |pts| {
VideoDataDescription::latest_sample_index_at_presentation_timestamp_internal(
&samples,
&sample_statistics,
pts,
)
};
assert_eq!(Err(Some(4)), query_pts_unloaded(Time(768)));
assert_eq!(Ok(0), query_pts_unloaded(Time(0)));
assert_eq!(Ok(3), query_pts_unloaded(Time(256)));
}
fn has_annexb_start_codes(data: &[u8]) -> bool {
data.windows(4).any(|w| w == ANNEXB_NAL_START_CODE)
}
fn video_test_file_mp4(codec: &VideoCodec, need_dts_equal_pts: bool) -> std::path::PathBuf {
let workspace_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.unwrap()
.to_path_buf();
let codec_str = match codec {
VideoCodec::H264 => "h264",
VideoCodec::H265 => "h265",
VideoCodec::AV1 => "av1",
VideoCodec::VP8 => "vp8",
VideoCodec::VP9 => "vp9",
VideoCodec::ImageSequence(_) => panic!("mp4 won't be an image sequence"),
};
if need_dts_equal_pts && (*codec == VideoCodec::H264 || *codec == VideoCodec::H265) {
workspace_dir.join(format!(
"tests/assets/video/Big_Buck_Bunny_1080_1s_{codec_str}_nobframes.mp4",
))
} else {
workspace_dir.join(format!(
"tests/assets/video/Big_Buck_Bunny_1080_1s_{codec_str}.mp4",
))
}
}
fn test_video_codec_sampling(codec: &VideoCodec, need_dts_equal_pts: bool) {
let video_path = video_test_file_mp4(codec, need_dts_equal_pts);
let data = std::fs::read(&video_path).unwrap();
let video_data = VideoDataDescription::load_from_bytes(
&data,
"video/mp4",
&format!("test_{codec:?}_video_sampling"),
)
.unwrap();
let mut idr_count = 0;
let mut non_idr_count = 0;
for (sample_idx, sample) in video_data.samples.iter_indexed() {
let chunk = sample
.sample()
.unwrap()
.get(
&|source| match source {
VideoSource::Span(span) => &data[span.range_usize()],
VideoSource::Id { .. } => &[],
},
sample_idx,
)
.unwrap();
let converted = video_data.sample_data_in_stream_format(&chunk).unwrap();
if chunk.is_sync {
idr_count += 1;
if *codec == VideoCodec::H264 {
let has_sps = converted
.windows(5)
.any(|w| w[0..4] == *ANNEXB_NAL_START_CODE && (w[4] & 0x1F) == 7);
assert!(has_sps, "IDR frame at index {sample_idx} should have SPS");
}
} else {
non_idr_count += 1;
}
if *codec == VideoCodec::H264 || *codec == VideoCodec::H265 {
assert!(
has_annexb_start_codes(&converted),
"Frame at index {sample_idx} should have Annex B start codes",
);
}
}
assert!(idr_count > 0, "Should have at least one IDR frame");
assert!(non_idr_count > 0, "Should have at least one non-IDR frame");
}
#[test]
fn test_full_video_sampling_all_codecs() {
for codec in [
VideoCodec::H264,
VideoCodec::H265,
VideoCodec::AV1,
VideoCodec::VP8,
VideoCodec::VP9,
] {
test_video_codec_sampling(&codec, false);
}
}
fn sample(dts: i64, is_sync: bool) -> SampleMetadataState {
SampleMetadataState::Present(SampleMetadata {
is_sync,
frame_nr: 0,
decode_timestamp: Time(dts),
presentation_timestamp: Time(dts),
duration: Some(Time(1)),
source: VideoSource::Span(Default::default()),
})
}
fn unloaded(dts: i64) -> SampleMetadataState {
SampleMetadataState::Unloaded {
source_id: Tuid::new(),
min_dts: Time(dts),
}
}
#[test]
fn test_gop_sizes_empty() {
let samples = StableIndexDeque::new();
let stats = SamplesStatistics::new(&samples);
assert_eq!(stats.gop_sizes, GopSizes::NO_SAMPLES);
}
#[test]
fn test_gop_sizes_uniform() {
let samples: StableIndexDeque<_> = [
sample(0, true),
sample(1, false),
sample(2, false),
sample(3, false),
sample(4, true),
sample(5, false),
sample(6, false),
sample(7, false),
]
.into_iter()
.collect();
let stats = SamplesStatistics::new(&samples);
assert_eq!(stats.gop_sizes.smallest, 4);
assert_eq!(stats.gop_sizes.largest, 4);
}
#[test]
fn test_gop_sizes_varying() {
let samples: StableIndexDeque<_> = [
sample(0, true),
sample(1, false),
sample(2, true),
sample(3, false),
sample(4, false),
sample(5, true),
sample(6, false),
sample(7, false),
sample(8, false),
]
.into_iter()
.collect();
let stats = SamplesStatistics::new(&samples);
assert_eq!(stats.gop_sizes.smallest, 2);
assert_eq!(stats.gop_sizes.largest, 3);
}
#[test]
fn test_gop_sizes_with_unloaded() {
let samples: StableIndexDeque<_> = [
sample(0, true),
sample(1, false),
sample(2, false),
unloaded(3),
sample(4, true),
sample(5, false),
sample(6, false),
]
.into_iter()
.collect();
let stats = SamplesStatistics::new(&samples);
assert_eq!(stats.gop_sizes.smallest, 3);
assert_eq!(stats.gop_sizes.largest, 3);
}
#[test]
fn test_gop_sizes_single_keyframe() {
let samples: StableIndexDeque<_> = [sample(0, true), sample(1, false), sample(2, false)]
.into_iter()
.collect();
let stats = SamplesStatistics::new(&samples);
assert_eq!(stats.gop_sizes, GopSizes::NO_SAMPLES);
}
}