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, Copy, Debug, PartialEq, Eq)]
pub enum VideoCodec {
H264,
H265,
AV1,
VP8,
VP9,
}
impl VideoCodec {
pub fn base_webcodec_string(&self) -> &'static str {
match self {
Self::AV1 => "av01",
Self::H264 => "avc1",
Self::H265 => "hev1",
Self::VP8 => "vp8",
Self::VP9 => "vp09",
}
}
}
pub type SampleIndex = usize;
pub type KeyframeIndex = usize;
#[derive(Clone)]
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 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::AV1 => Ok(chunk.data.clone()),
VideoCodec::H264 => {
let stsd = self
.encoding_details
.as_ref()
.ok_or(SampleConversionError::MissingEncodingDetails(self.codec))?
.stsd
.as_ref()
.ok_or(SampleConversionError::MissingStsd(self.codec))?;
let re_mp4::StsdBoxContent::Avc1(avc1_box) = &stsd.contents else {
return Err(SampleConversionError::UnexpectedStsdContent {
codec: self.codec,
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(SampleConversionError::MissingEncodingDetails(self.codec))?
.stsd
.as_ref()
.ok_or(SampleConversionError::MissingStsd(self.codec))?;
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,
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::VP8 | VideoCodec::VP9 => {
Err(SampleConversionError::UnsupportedCodec(self.codec))
}
}
}
}
#[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),
#[error("Unsupported codec {0:?}")]
UnsupportedCodec(VideoCodec),
}
#[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, Debug, PartialEq, Eq)]
pub struct SamplesStatistics {
pub dts_always_equal_pts: bool,
pub has_sample_highest_pts_so_far: Option<BitVec>,
}
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,
} = 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,
};
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()
});
Self {
dts_always_equal_pts,
has_sample_highest_pts_so_far,
}
}
}
impl VideoDataDescription {
pub fn load_from_bytes(
data: &[u8],
media_type: &str,
debug_name: &str,
source_id: Tuid,
) -> 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, source_id),
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::AV1 => "AV1",
VideoCodec::H264 => "H.264 AVC1",
VideoCodec::H265 => "H.265 HEV1",
VideoCodec::VP8 => "VP8",
VideoCodec::VP9 => "VP9",
}
.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(
keyframes: &[KeyframeIndex],
samples: &StableIndexDeque<SampleMetadataState>,
decode_time: Time,
) -> Option<SampleIndex> {
let keyframe_idx = keyframes
.partition_point(|p| {
samples
.get(*p)
.map(|s| s.sample())
.inspect(|_s| {
debug_assert!(_s.is_some(), "Keyframes mentioned in the keyframe lookup list should always be loaded");
})
.flatten()
.is_some_and(|s| s.decode_timestamp <= decode_time)
})
.checked_sub(1)?;
let start = *keyframes.get(keyframe_idx)?;
let end = keyframes
.get(keyframe_idx + 1)
.copied()
.unwrap_or_else(|| samples.next_index());
let range = start..end;
let mut found_sample_idx = None;
for (idx, sample) in samples.iter_index_range_clamped(&range) {
let Some(s) = sample.sample() else {
continue;
};
if s.decode_timestamp <= decode_time {
found_sample_idx = Some(idx);
} else {
break;
}
}
found_sample_idx
}
fn latest_sample_index_at_presentation_timestamp_internal(
keyframes: &[KeyframeIndex],
samples: &StableIndexDeque<SampleMetadataState>,
sample_statistics: &SamplesStatistics,
presentation_timestamp: Time,
) -> Option<SampleIndex> {
let decode_sample_idx = Self::latest_sample_index_at_decode_timestamp(
keyframes,
samples,
presentation_timestamp,
);
let decode_sample_idx = 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 Some(decode_sample_idx);
};
debug_assert!(has_sample_highest_pts_so_far.len() == samples.next_index());
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 {
continue;
};
if sample.presentation_timestamp == presentation_timestamp {
return Some(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] {
return Some(best_index);
}
}
None
}
pub fn latest_sample_index_at_presentation_timestamp(
&self,
presentation_timestamp: Time,
) -> Option<SampleIndex> {
Self::latest_sample_index_at_presentation_timestamp_internal(
&self.keyframe_indices,
&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.keyframe_indices,
&self.samples,
&self.samples_statistics,
sample.presentation_timestamp - Time::new(1),
)?;
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_id(&self) -> Tuid {
match self {
Self::Present(sample) => sample.source_id,
Self::Unloaded { source_id, .. } => *source_id,
}
}
pub fn source_id_mut(&mut self) -> &mut Tuid {
match self {
Self::Present(sample) => &mut sample.source_id,
Self::Unloaded { source_id, .. } => source_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_source_id: Option<Tuid>) {
match self {
Self::Present(sample) => {
let dts = sample.decode_timestamp;
let source_id = new_source_id.unwrap_or(sample.source_id);
*self = Self::Unloaded {
source_id,
min_dts: dts,
}
}
Self::Unloaded {
source_id,
min_dts: _,
} => {
if let Some(new_source_id) = new_source_id {
*source_id = new_source_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)]
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_id: Tuid,
pub byte_span: Span<u32>,
}
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_buffer: &dyn Fn(Tuid) -> &'a [u8],
sample_idx: SampleIndex,
) -> Option<Chunk> {
let buffer = get_buffer(self.source_id);
let data = buffer.get(self.byte_span.range_usize())?.to_vec();
Some(Chunk {
data,
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 {
f.debug_struct("Video")
.field("codec", &self.codec)
.field("encoding_details", &self.encoding_details)
.field("timescale", &self.timescale)
.field("keyframe_indices", &self.keyframe_indices)
.field("samples", &self.samples.iter_indexed().collect::<Vec<_>>())
.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 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_id: Tuid::new(),
byte_span: Default::default(),
})
})
.collect::<StableIndexDeque<_>>();
let keyframe_indices: Vec<SampleIndex> =
(samples.min_index()..samples.next_index()).collect();
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(
&keyframe_indices,
&samples,
&sample_statistics,
pts,
)
};
for (idx, sample) in samples.iter_indexed() {
assert_eq!(
Some(idx),
query_pts(sample.sample().unwrap().presentation_timestamp)
);
}
for (idx, sample) in samples.iter_indexed() {
assert_eq!(
Some(idx),
query_pts(sample.sample().unwrap().presentation_timestamp + Time(1))
);
assert_eq!(
Some(idx),
query_pts(sample.sample().unwrap().presentation_timestamp + Time(255))
);
}
assert_eq!(None, query_pts(Time(-1)));
assert_eq!(None, query_pts(Time(-123)));
assert_eq!(Some(0), query_pts(Time(0)));
assert_eq!(Some(0), query_pts(Time(1)));
assert_eq!(Some(0), query_pts(Time(88)));
assert_eq!(Some(0), query_pts(Time(255)));
assert_eq!(Some(3), query_pts(Time(256)));
assert_eq!(Some(3), query_pts(Time(257)));
assert_eq!(Some(3), query_pts(Time(400)));
assert_eq!(Some(3), query_pts(Time(511)));
assert_eq!(Some(2), query_pts(Time(512)));
assert_eq!(Some(2), query_pts(Time(513)));
assert_eq!(Some(2), query_pts(Time(600)));
assert_eq!(Some(2), query_pts(Time(767)));
assert_eq!(Some(4), query_pts(Time(768)));
assert_eq!(Some(4), query_pts(Time(1023)));
assert_eq!(Some(48), query_pts(Time(123123123123123123)));
}
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::VP9 => "vp9",
VideoCodec::VP8 => {
panic!("We don't have test data for vp8, because Mp4 doesn't support vp8.")
}
VideoCodec::AV1 => "av1",
};
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"),
Tuid::new(),
)
.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(&|_| &data, 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] {
test_video_codec_sampling(codec, false);
}
}
}