use derive_more::IsVariant;
use mediatime::Timestamp;
use smol_str::SmolStr;
use indexmap::IndexMap;
use mediaframe::{
codec::VideoCodec,
color::{DolbyVisionConfig, HdrStaticMetadata, Info as ColorInfo},
disposition::TrackDisposition,
frame::{Dimensions, FieldOrder, FrameRate, Rect, Rotation, SampleAspectRatio, StereoMode},
pixel_format::PixelFormat,
};
use crate::domain::{bitflags::VideoIndexStatus, primitives::ErrorInfo, vo::Provenance, Uuid7};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoTrack<Id = Uuid7> {
id: Id,
video_id: Id,
stream_index: Option<u32>,
container_track_id: Option<u64>,
start_pts: Option<Timestamp>,
duration: Option<Timestamp>,
codec: VideoCodec,
profile: Option<SmolStr>,
level: Option<u16>,
bit_rate: u64,
nb_frames: Option<u64>,
has_b_frames: bool,
closed_gop: Option<bool>,
bits_per_raw_sample: Option<u8>,
dimensions: Dimensions,
visible_rect: Option<Rect>,
sample_aspect_ratio: SampleAspectRatio,
pixel_format: PixelFormat,
color: ColorInfo,
hdr_static: Option<HdrStaticMetadata>,
rotation: Rotation,
frame_rate: FrameRate,
avg_frame_rate: FrameRate,
field_order: FieldOrder,
stereo_mode: Option<StereoMode>,
dovi: Option<DolbyVisionConfig>,
has_embedded_captions: bool,
disposition: TrackDisposition,
is_primary: bool,
auto_selected: bool,
scenes: std::vec::Vec<Id>,
metadata: IndexMap<SmolStr, SmolStr>,
index_status: VideoIndexStatus,
index_errors: std::vec::Vec<ErrorInfo>,
provenance: Provenance,
}
impl VideoTrack<Uuid7> {
pub fn try_new(id: Uuid7, video_id: Uuid7) -> Result<Self, VideoTrackError> {
if id.is_nil() {
return Err(VideoTrackError::NilId);
}
if video_id.is_nil() {
return Err(VideoTrackError::NilVideoId);
}
Ok(Self {
id,
video_id,
stream_index: None,
container_track_id: None,
start_pts: None,
duration: None,
codec: VideoCodec::Other(SmolStr::default()),
profile: None,
level: None,
bit_rate: 0,
nb_frames: None,
has_b_frames: false,
closed_gop: None,
bits_per_raw_sample: None,
dimensions: Dimensions::new(0, 0),
visible_rect: None,
sample_aspect_ratio: SampleAspectRatio::default(),
pixel_format: PixelFormat::default(),
color: ColorInfo::default(),
hdr_static: None,
rotation: Rotation::default(),
frame_rate: FrameRate::default(),
avg_frame_rate: FrameRate::default(),
field_order: FieldOrder::default(),
stereo_mode: None,
dovi: None,
has_embedded_captions: false,
disposition: TrackDisposition::empty(),
is_primary: false,
auto_selected: false,
scenes: std::vec::Vec::new(),
metadata: IndexMap::new(),
index_status: VideoIndexStatus::empty(),
index_errors: std::vec::Vec::new(),
provenance: Provenance::new(),
})
}
#[must_use]
#[inline(always)]
pub fn with_scenes(mut self, v: impl Into<std::vec::Vec<Uuid7>>) -> Self {
self.scenes = v.into();
self
}
#[inline(always)]
pub fn set_scenes(&mut self, v: impl Into<std::vec::Vec<Uuid7>>) -> &mut Self {
self.scenes = v.into();
self
}
}
impl<Id> VideoTrack<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn video_id_ref(&self) -> &Id {
&self.video_id
}
#[inline(always)]
pub const fn stream_index(&self) -> Option<u32> {
self.stream_index
}
#[inline(always)]
pub const fn container_track_id(&self) -> Option<u64> {
self.container_track_id
}
#[inline(always)]
pub const fn start_pts_ref(&self) -> Option<&Timestamp> {
self.start_pts.as_ref()
}
#[inline(always)]
pub const fn duration_ref(&self) -> Option<&Timestamp> {
self.duration.as_ref()
}
#[inline(always)]
pub const fn codec_ref(&self) -> &VideoCodec {
&self.codec
}
#[inline(always)]
pub fn profile(&self) -> Option<&str> {
self.profile.as_deref()
}
#[inline(always)]
pub const fn level(&self) -> Option<u16> {
self.level
}
#[inline(always)]
pub const fn bit_rate(&self) -> u64 {
self.bit_rate
}
#[inline(always)]
pub const fn nb_frames(&self) -> Option<u64> {
self.nb_frames
}
#[inline(always)]
pub const fn has_b_frames(&self) -> bool {
self.has_b_frames
}
#[inline(always)]
pub const fn closed_gop(&self) -> Option<bool> {
self.closed_gop
}
#[inline(always)]
pub const fn bits_per_raw_sample(&self) -> Option<u8> {
self.bits_per_raw_sample
}
#[inline(always)]
pub const fn dimensions(&self) -> Dimensions {
self.dimensions
}
#[inline(always)]
pub const fn visible_rect(&self) -> Option<Rect> {
self.visible_rect
}
#[inline(always)]
pub const fn sample_aspect_ratio(&self) -> SampleAspectRatio {
self.sample_aspect_ratio
}
#[inline(always)]
pub const fn pixel_format(&self) -> PixelFormat {
self.pixel_format
}
#[inline(always)]
pub const fn color_ref(&self) -> &ColorInfo {
&self.color
}
#[inline(always)]
pub const fn hdr_static_ref(&self) -> Option<&HdrStaticMetadata> {
self.hdr_static.as_ref()
}
#[inline(always)]
pub const fn rotation(&self) -> Rotation {
self.rotation
}
#[inline(always)]
pub const fn frame_rate(&self) -> FrameRate {
self.frame_rate
}
#[inline(always)]
pub const fn avg_frame_rate(&self) -> FrameRate {
self.avg_frame_rate
}
#[inline(always)]
pub const fn metadata_ref(&self) -> &IndexMap<SmolStr, SmolStr> {
&self.metadata
}
#[inline(always)]
pub const fn field_order(&self) -> FieldOrder {
self.field_order
}
#[inline(always)]
pub const fn stereo_mode(&self) -> Option<StereoMode> {
self.stereo_mode
}
#[inline(always)]
pub const fn dovi(&self) -> Option<DolbyVisionConfig> {
self.dovi
}
#[inline(always)]
pub const fn has_embedded_captions(&self) -> bool {
self.has_embedded_captions
}
#[inline(always)]
pub const fn disposition(&self) -> TrackDisposition {
self.disposition
}
#[inline(always)]
pub const fn is_primary(&self) -> bool {
self.is_primary
}
#[inline(always)]
pub const fn auto_selected(&self) -> bool {
self.auto_selected
}
#[inline(always)]
pub const fn scenes_slice(&self) -> &[Id] {
self.scenes.as_slice()
}
#[inline(always)]
pub const fn index_status(&self) -> VideoIndexStatus {
self.index_status
}
#[inline(always)]
pub const fn index_errors_slice(&self) -> &[ErrorInfo] {
self.index_errors.as_slice()
}
#[inline(always)]
pub const fn provenance_ref(&self) -> &Provenance {
&self.provenance
}
}
impl<Id> VideoTrack<Id> {
#[must_use]
#[inline(always)]
pub const fn with_stream_index(mut self, v: Option<u32>) -> Self {
self.stream_index = v;
self
}
#[inline(always)]
pub const fn set_stream_index(&mut self, v: Option<u32>) -> &mut Self {
self.stream_index = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_container_track_id(mut self, v: Option<u64>) -> Self {
self.container_track_id = v;
self
}
#[inline(always)]
pub const fn set_container_track_id(&mut self, v: Option<u64>) -> &mut Self {
self.container_track_id = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_start_pts(mut self, v: Option<Timestamp>) -> Self {
self.start_pts = v;
self
}
#[inline(always)]
pub fn set_start_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
self.start_pts = v;
self
}
#[inline]
pub fn try_with_duration(mut self, v: Option<Timestamp>) -> Result<Self, VideoTrackError> {
if let Some(t) = v {
if t.pts() < 0 {
return Err(VideoTrackError::NegativeDuration);
}
}
self.duration = v;
Ok(self)
}
#[inline]
pub fn try_set_duration(&mut self, v: Option<Timestamp>) -> Result<&mut Self, VideoTrackError> {
if let Some(t) = v {
if t.pts() < 0 {
return Err(VideoTrackError::NegativeDuration);
}
}
self.duration = v;
Ok(self)
}
#[must_use]
#[inline(always)]
pub fn with_codec(mut self, v: VideoCodec) -> Self {
self.codec = v;
self
}
#[inline(always)]
pub fn set_codec(&mut self, v: VideoCodec) -> &mut Self {
self.codec = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_profile(mut self, v: Option<SmolStr>) -> Self {
self.profile = v;
self
}
#[inline(always)]
pub fn set_profile(&mut self, v: Option<SmolStr>) -> &mut Self {
self.profile = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_level(mut self, v: Option<u16>) -> Self {
self.level = v;
self
}
#[inline(always)]
pub const fn set_level(&mut self, v: Option<u16>) -> &mut Self {
self.level = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_bit_rate(mut self, v: u64) -> Self {
self.bit_rate = v;
self
}
#[inline(always)]
pub const fn set_bit_rate(&mut self, v: u64) -> &mut Self {
self.bit_rate = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_nb_frames(mut self, v: Option<u64>) -> Self {
self.nb_frames = v;
self
}
#[inline(always)]
pub const fn set_nb_frames(&mut self, v: Option<u64>) -> &mut Self {
self.nb_frames = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_has_b_frames(mut self, v: bool) -> Self {
self.has_b_frames = v;
self
}
#[inline(always)]
pub const fn set_has_b_frames(&mut self, v: bool) -> &mut Self {
self.has_b_frames = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_closed_gop(mut self, v: Option<bool>) -> Self {
self.closed_gop = v;
self
}
#[inline(always)]
pub const fn set_closed_gop(&mut self, v: Option<bool>) -> &mut Self {
self.closed_gop = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_bits_per_raw_sample(mut self, v: Option<u8>) -> Self {
self.bits_per_raw_sample = v;
self
}
#[inline(always)]
pub const fn set_bits_per_raw_sample(&mut self, v: Option<u8>) -> &mut Self {
self.bits_per_raw_sample = v;
self
}
#[inline]
pub fn try_with_dimensions(mut self, v: Dimensions) -> Result<Self, VideoTrackError> {
self.try_set_dimensions(v)?;
Ok(self)
}
#[inline]
pub fn try_set_dimensions(&mut self, v: Dimensions) -> Result<&mut Self, VideoTrackError> {
if !dimensions_valid(v) {
return Err(VideoTrackError::PartialZeroDimensions);
}
if let Some(rect) = self.visible_rect {
if !rect_fits_dimensions(rect, v) {
return Err(VideoTrackError::CropExceedsDimensions);
}
}
self.dimensions = v;
Ok(self)
}
#[inline]
pub fn try_with_visible_rect(mut self, v: Option<Rect>) -> Result<Self, VideoTrackError> {
self.try_set_visible_rect(v)?;
Ok(self)
}
#[inline]
pub fn try_set_visible_rect(&mut self, v: Option<Rect>) -> Result<&mut Self, VideoTrackError> {
if let Some(rect) = v {
if rect.width() == 0 || rect.height() == 0 {
return Err(VideoTrackError::ZeroExtentCrop);
}
if !dimensions_known(self.dimensions) {
return Err(VideoTrackError::CropWithoutDimensions);
}
if !rect_fits_dimensions(rect, self.dimensions) {
return Err(VideoTrackError::CropExceedsDimensions);
}
}
self.visible_rect = v;
Ok(self)
}
#[must_use]
#[inline(always)]
pub const fn with_sample_aspect_ratio(mut self, v: SampleAspectRatio) -> Self {
self.sample_aspect_ratio = v;
self
}
#[inline(always)]
pub const fn set_sample_aspect_ratio(&mut self, v: SampleAspectRatio) -> &mut Self {
self.sample_aspect_ratio = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_pixel_format(mut self, v: PixelFormat) -> Self {
self.pixel_format = v;
self
}
#[inline(always)]
pub const fn set_pixel_format(&mut self, v: PixelFormat) -> &mut Self {
self.pixel_format = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_color(mut self, v: ColorInfo) -> Self {
self.color = v;
self
}
#[inline(always)]
pub const fn set_color(&mut self, v: ColorInfo) -> &mut Self {
self.color = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_hdr_static(mut self, v: Option<HdrStaticMetadata>) -> Self {
self.hdr_static = v;
self
}
#[inline(always)]
pub const fn set_hdr_static(&mut self, v: Option<HdrStaticMetadata>) -> &mut Self {
self.hdr_static = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_rotation(mut self, v: Rotation) -> Self {
self.rotation = v;
self
}
#[inline(always)]
pub const fn set_rotation(&mut self, v: Rotation) -> &mut Self {
self.rotation = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_frame_rate(mut self, v: FrameRate) -> Self {
self.frame_rate = v;
self
}
#[inline(always)]
pub const fn set_frame_rate(&mut self, v: FrameRate) -> &mut Self {
self.frame_rate = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_avg_frame_rate(mut self, v: FrameRate) -> Self {
self.avg_frame_rate = v;
self
}
#[inline(always)]
pub const fn set_avg_frame_rate(&mut self, v: FrameRate) -> &mut Self {
self.avg_frame_rate = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_metadata(mut self, v: IndexMap<SmolStr, SmolStr>) -> Self {
self.metadata = v;
self
}
#[inline(always)]
pub fn set_metadata(&mut self, v: IndexMap<SmolStr, SmolStr>) -> &mut Self {
self.metadata = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_field_order(mut self, v: FieldOrder) -> Self {
self.field_order = v;
self
}
#[inline(always)]
pub const fn set_field_order(&mut self, v: FieldOrder) -> &mut Self {
self.field_order = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_stereo_mode(mut self, v: Option<StereoMode>) -> Self {
self.stereo_mode = v;
self
}
#[inline(always)]
pub const fn set_stereo_mode(&mut self, v: Option<StereoMode>) -> &mut Self {
self.stereo_mode = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_dovi(mut self, v: Option<DolbyVisionConfig>) -> Self {
self.dovi = v;
self
}
#[inline(always)]
pub const fn set_dovi(&mut self, v: Option<DolbyVisionConfig>) -> &mut Self {
self.dovi = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_has_embedded_captions(mut self, v: bool) -> Self {
self.has_embedded_captions = v;
self
}
#[inline(always)]
pub const fn set_has_embedded_captions(&mut self, v: bool) -> &mut Self {
self.has_embedded_captions = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_disposition(mut self, v: TrackDisposition) -> Self {
self.disposition = v;
self
}
#[inline(always)]
pub const fn set_disposition(&mut self, v: TrackDisposition) -> &mut Self {
self.disposition = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_is_primary(mut self, v: bool) -> Self {
self.is_primary = v;
self
}
#[inline(always)]
pub const fn set_is_primary(&mut self, v: bool) -> &mut Self {
self.is_primary = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_auto_selected(mut self, v: bool) -> Self {
self.auto_selected = v;
self
}
#[inline(always)]
pub const fn set_auto_selected(&mut self, v: bool) -> &mut Self {
self.auto_selected = v;
self
}
#[must_use]
#[inline(always)]
pub const fn with_index_status(mut self, v: VideoIndexStatus) -> Self {
self.index_status = v;
self
}
#[inline(always)]
pub const fn set_index_status(&mut self, v: VideoIndexStatus) -> &mut Self {
self.index_status = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_index_errors(mut self, v: impl Into<std::vec::Vec<ErrorInfo>>) -> Self {
self.index_errors = v.into();
self
}
#[inline(always)]
pub fn set_index_errors(&mut self, v: impl Into<std::vec::Vec<ErrorInfo>>) -> &mut Self {
self.index_errors = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_provenance(mut self, v: Provenance) -> Self {
self.provenance = v;
self
}
#[inline(always)]
pub fn set_provenance(&mut self, v: Provenance) -> &mut Self {
self.provenance = v;
self
}
}
#[inline]
const fn dimensions_valid(dims: Dimensions) -> bool {
let (w, h) = (dims.width(), dims.height());
(w == 0 && h == 0) || (w != 0 && h != 0)
}
#[inline]
const fn dimensions_known(dims: Dimensions) -> bool {
dims.width() != 0 && dims.height() != 0
}
#[inline]
const fn rect_fits_dimensions(rect: Rect, dims: Dimensions) -> bool {
let right = match rect.x().checked_add(rect.width()) {
Some(v) => v,
None => return false,
};
let bottom = match rect.y().checked_add(rect.height()) {
Some(v) => v,
None => return false,
};
right <= dims.width() && bottom <= dims.height()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum VideoTrackError {
#[error("VideoTrack id must not be the nil UUID")]
NilId,
#[error("VideoTrack video_id (Video facet) must not be the nil UUID")]
NilVideoId,
#[error("VideoTrack duration must not be negative")]
NegativeDuration,
#[error("VideoTrack visible_rect crop must fit within the coded dimensions")]
CropExceedsDimensions,
#[error("VideoTrack dimensions must be 0x0 (unknown) or have both axes non-zero")]
PartialZeroDimensions,
#[error("VideoTrack visible_rect crop must have non-zero width and height")]
ZeroExtentCrop,
#[error("VideoTrack visible_rect crop requires known (non-zero) dimensions")]
CropWithoutDimensions,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::domain::ErrorCode;
use core::num::NonZeroU32;
use mediaframe::frame::Rational;
use mediatime::Timebase;
#[test]
fn try_new_happy_path() {
let id = Uuid7::new();
let video_id = Uuid7::new();
let t = VideoTrack::try_new(id, video_id).unwrap();
assert_eq!(t.id_ref(), &id);
assert_eq!(t.video_id_ref(), &video_id);
assert_eq!(t.bit_rate(), 0);
assert!(t.codec_ref().is_other());
assert_eq!(t.dimensions(), Dimensions::new(0, 0));
assert!(t.scenes_slice().is_empty());
assert!(t.index_status().is_empty());
assert!(t.index_errors_slice().is_empty());
assert!(t.provenance_ref().is_empty());
}
#[test]
fn try_new_rejects_nil_id_and_parent() {
assert_eq!(
VideoTrack::try_new(Uuid7::nil(), Uuid7::new()).err(),
Some(VideoTrackError::NilId)
);
assert_eq!(
VideoTrack::try_new(Uuid7::new(), Uuid7::nil()).err(),
Some(VideoTrackError::NilVideoId)
);
assert!(VideoTrackError::NilId.is_nil_id());
assert!(VideoTrackError::NilVideoId.is_nil_video_id());
}
#[test]
fn builders_and_setters_chain() {
let s1 = Uuid7::new();
let s2 = Uuid7::new();
let fr = FrameRate::new(Rational::new(24000, NonZeroU32::new(1001).unwrap()), false);
let t = VideoTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_stream_index(Some(0))
.with_codec(VideoCodec::Hevc)
.with_profile(Some(SmolStr::new("Main10")))
.with_level(Some(150))
.with_bit_rate(8_000_000)
.try_with_dimensions(Dimensions::new(3840, 2160))
.unwrap()
.with_frame_rate(fr)
.with_pixel_format(PixelFormat::from_u32(0x0a)) .with_has_b_frames(true)
.with_is_primary(true)
.with_scenes(std::vec![s1, s2])
.with_index_status(VideoIndexStatus::PROBED)
.with_index_errors(std::vec![ErrorInfo::code_only(ErrorCode::SceneDetectionFailed)])
.with_provenance(Provenance::from_parts("qwen2-vl-7b", "v0.3.0", "p@1", "idx-0.1.0"));
assert_eq!(t.stream_index(), Some(0));
assert!(matches!(t.codec_ref(), VideoCodec::Hevc));
assert_eq!(t.profile(), Some("Main10"));
assert_eq!(t.level(), Some(150));
assert_eq!(t.bit_rate(), 8_000_000);
assert_eq!(t.dimensions(), Dimensions::new(3840, 2160));
assert_eq!(t.frame_rate(), fr);
assert!(t.has_b_frames());
assert!(t.is_primary());
assert_eq!(t.scenes_slice().len(), 2);
assert!(t.scenes_slice().contains(&s1));
assert_eq!(t.index_status(), VideoIndexStatus::PROBED);
assert_eq!(t.index_errors_slice().len(), 1);
assert_eq!(t.provenance_ref().model_name(), "qwen2-vl-7b");
let mut t = t;
t.set_bit_rate(0);
t.try_set_dimensions(Dimensions::new(0, 0)).unwrap();
t.set_is_primary(false);
t.set_index_status(VideoIndexStatus::empty());
t.set_scenes(std::vec::Vec::<Uuid7>::new());
assert_eq!(t.bit_rate(), 0);
assert_eq!(t.dimensions(), Dimensions::new(0, 0));
assert!(!t.is_primary());
assert!(t.index_status().is_empty());
assert!(t.scenes_slice().is_empty());
}
#[test]
fn duration_rejects_negative_timestamp() {
let tb = Timebase::new(1, NonZeroU32::new(1000).unwrap());
let t = VideoTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
assert_eq!(
t.clone()
.try_with_duration(Some(Timestamp::new(-1, tb)))
.err(),
Some(VideoTrackError::NegativeDuration)
);
let mut t = t;
assert_eq!(
t.try_set_duration(Some(Timestamp::new(-5_000, tb))).err(),
Some(VideoTrackError::NegativeDuration)
);
assert!(t.duration_ref().is_none());
assert!(VideoTrackError::NegativeDuration.is_negative_duration());
let t = t
.try_with_duration(Some(Timestamp::new(0, tb)))
.unwrap()
.try_with_duration(Some(Timestamp::new(48_000, tb)))
.unwrap();
assert_eq!(t.duration_ref().map(Timestamp::pts), Some(48_000));
let mut t = t;
t.try_set_duration(None).unwrap();
assert!(t.duration_ref().is_none());
}
#[test]
fn codec_other_preserves_wire_string() {
let c = VideoCodec::Other(SmolStr::new("xyz-codec"));
assert!(c.is_other());
assert_eq!(c.as_str(), "xyz-codec");
}
#[test]
fn mediaframe_descriptors_flow_through() {
let t = VideoTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.try_with_dimensions(Dimensions::new(1920, 1080))
.unwrap()
.try_with_visible_rect(Some(Rect::new(0, 0, 1920, 1080)))
.unwrap()
.with_sample_aspect_ratio(SampleAspectRatio::new(16, NonZeroU32::new(9).unwrap()))
.with_rotation(Rotation::D90)
.with_field_order(FieldOrder::Progressive)
.with_stereo_mode(Some(StereoMode::SideBySide))
.with_color(ColorInfo::UNSPECIFIED)
.with_hdr_static(Some(HdrStaticMetadata::new(None, None)))
.with_dovi(Some(DolbyVisionConfig::new(8, 9, true, false, 1)))
.with_disposition(TrackDisposition::empty());
assert_eq!(t.visible_rect().unwrap().width(), 1920);
assert_eq!(t.sample_aspect_ratio().num(), 16);
assert!(matches!(t.rotation(), Rotation::D90));
assert!(matches!(t.field_order(), FieldOrder::Progressive));
assert!(matches!(t.stereo_mode(), Some(StereoMode::SideBySide)));
assert_eq!(t.color_ref(), &ColorInfo::UNSPECIFIED);
assert!(t.hdr_static_ref().is_some());
assert_eq!(t.dovi().unwrap().profile(), 8);
assert!(t.disposition().is_empty());
}
#[test]
fn crop_must_fit_within_coded_dimensions() {
let t = VideoTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.try_with_dimensions(Dimensions::new(1920, 1080))
.unwrap();
assert_eq!(
t.clone()
.try_with_visible_rect(Some(Rect::new(100, 0, 1900, 1080)))
.err(),
Some(VideoTrackError::CropExceedsDimensions)
);
assert_eq!(
t.clone()
.try_with_visible_rect(Some(Rect::new(0, 100, 1920, 1000)))
.err(),
Some(VideoTrackError::CropExceedsDimensions)
);
assert!(VideoTrackError::CropExceedsDimensions.is_crop_exceeds_dimensions());
let t = t
.try_with_visible_rect(Some(Rect::new(0, 0, 1920, 1080)))
.unwrap();
let mut t = t;
assert_eq!(
t.try_set_dimensions(Dimensions::new(1280, 720)).err(),
Some(VideoTrackError::CropExceedsDimensions)
);
assert_eq!(t.dimensions(), Dimensions::new(1920, 1080));
assert_eq!(t.visible_rect(), Some(Rect::new(0, 0, 1920, 1080)));
assert_eq!(
t.try_set_visible_rect(Some(Rect::new(0, 0, 4000, 4000)))
.err(),
Some(VideoTrackError::CropExceedsDimensions)
);
assert_eq!(t.visible_rect(), Some(Rect::new(0, 0, 1920, 1080)));
t.try_set_dimensions(Dimensions::new(3840, 2160)).unwrap();
t.try_set_visible_rect(None).unwrap();
assert!(t.visible_rect().is_none());
t.try_set_dimensions(Dimensions::new(0, 0)).unwrap();
}
#[test]
fn dimensions_reject_partial_zero() {
let mut t = VideoTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
assert_eq!(
t.try_set_dimensions(Dimensions::new(0, 1080)).err(),
Some(VideoTrackError::PartialZeroDimensions)
);
assert_eq!(
t.try_set_dimensions(Dimensions::new(1920, 0)).err(),
Some(VideoTrackError::PartialZeroDimensions)
);
assert_eq!(t.dimensions(), Dimensions::new(0, 0));
assert!(VideoTrackError::PartialZeroDimensions.is_partial_zero_dimensions());
t.try_set_dimensions(Dimensions::new(0, 0)).unwrap();
t.try_set_dimensions(Dimensions::new(1920, 1080)).unwrap();
}
#[test]
fn crop_rejects_zero_extent_and_unknown_dimensions() {
let t = VideoTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
assert_eq!(
t.clone()
.try_with_visible_rect(Some(Rect::new(0, 0, 100, 100)))
.err(),
Some(VideoTrackError::CropWithoutDimensions)
);
assert!(VideoTrackError::CropWithoutDimensions.is_crop_without_dimensions());
let t = t.try_with_dimensions(Dimensions::new(1920, 1080)).unwrap();
assert_eq!(
t.clone()
.try_with_visible_rect(Some(Rect::new(0, 0, 0, 1080)))
.err(),
Some(VideoTrackError::ZeroExtentCrop)
);
assert_eq!(
t.clone()
.try_with_visible_rect(Some(Rect::new(0, 0, 1920, 0)))
.err(),
Some(VideoTrackError::ZeroExtentCrop)
);
assert!(VideoTrackError::ZeroExtentCrop.is_zero_extent_crop());
let t = t
.try_with_visible_rect(Some(Rect::new(10, 10, 100, 100)))
.unwrap();
assert_eq!(t.visible_rect(), Some(Rect::new(10, 10, 100, 100)));
}
}