use derive_more::IsVariant;
use mediatime::TimeRange;
use smol_str::SmolStr;
use crate::domain::{SceneDetector, Uuid7};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scene<Id = Uuid7> {
id: Id,
video_track_id: Id,
index: u32,
span: TimeRange,
detector: SceneDetector,
keyframes: std::vec::Vec<Id>,
description: SmolStr,
}
impl Scene<Uuid7> {
pub fn try_new(
id: Uuid7,
video_track_id: Uuid7,
index: u32,
span: TimeRange,
detector: SceneDetector,
) -> Result<Self, SceneError> {
if id.is_nil() {
return Err(SceneError::NilId);
}
if video_track_id.is_nil() {
return Err(SceneError::NilVideoTrackId);
}
if span.start_pts() > span.end_pts() {
return Err(SceneError::InvertedSpan);
}
Ok(Self {
id,
video_track_id,
index,
span,
detector,
keyframes: std::vec::Vec::new(),
description: SmolStr::default(),
})
}
#[must_use]
#[inline(always)]
pub fn with_keyframes(mut self, kfs: impl Into<std::vec::Vec<Uuid7>>) -> Self {
self.keyframes = kfs.into();
self
}
#[inline(always)]
pub fn set_keyframes(&mut self, kfs: impl Into<std::vec::Vec<Uuid7>>) -> &mut Self {
self.keyframes = kfs.into();
self
}
}
impl<Id> Scene<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn video_track_id_ref(&self) -> &Id {
&self.video_track_id
}
#[inline(always)]
pub const fn index(&self) -> u32 {
self.index
}
#[inline(always)]
pub const fn span_ref(&self) -> &TimeRange {
&self.span
}
#[inline(always)]
pub const fn detector(&self) -> SceneDetector {
self.detector
}
#[inline(always)]
pub const fn keyframes_slice(&self) -> &[Id] {
self.keyframes.as_slice()
}
#[inline(always)]
pub fn description(&self) -> &str {
self.description.as_str()
}
#[must_use]
#[inline(always)]
pub const fn with_index(mut self, index: u32) -> Self {
self.index = index;
self
}
#[inline]
pub fn try_with_span(mut self, span: TimeRange) -> Result<Self, SceneError> {
if span.start_pts() > span.end_pts() {
return Err(SceneError::InvertedSpan);
}
self.span = span;
Ok(self)
}
#[inline]
pub const fn try_set_span(&mut self, span: TimeRange) -> Result<&mut Self, SceneError> {
if span.start_pts() > span.end_pts() {
return Err(SceneError::InvertedSpan);
}
self.span = span;
Ok(self)
}
#[must_use]
#[inline(always)]
pub const fn with_detector(mut self, detector: SceneDetector) -> Self {
self.detector = detector;
self
}
#[must_use]
#[inline(always)]
pub fn with_description(mut self, description: impl Into<SmolStr>) -> Self {
self.description = description.into();
self
}
#[inline(always)]
pub const fn set_index(&mut self, index: u32) -> &mut Self {
self.index = index;
self
}
#[inline(always)]
pub const fn set_detector(&mut self, detector: SceneDetector) -> &mut Self {
self.detector = detector;
self
}
#[inline(always)]
pub fn set_description(&mut self, description: impl Into<SmolStr>) -> &mut Self {
self.description = description.into();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum SceneError {
#[error("Scene id must not be the nil UUID")]
NilId,
#[error("Scene `video_track_id` (FK → VideoTrack) must not be the nil UUID")]
NilVideoTrackId,
#[error("Scene span must not be inverted (start_pts <= end_pts)")]
InvertedSpan,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use core::num::NonZeroU32;
use mediatime::Timebase;
fn tb() -> Timebase {
Timebase::new(1, NonZeroU32::new(1000).unwrap())
}
#[test]
fn try_new_happy_path() {
let video_track_id = Uuid7::new();
let span = TimeRange::new(5_000, 10_000, tb());
let s = Scene::try_new(
Uuid7::new(),
video_track_id,
0,
span,
SceneDetector::Adaptive,
)
.unwrap();
assert_eq!(s.video_track_id_ref(), &video_track_id);
assert_eq!(s.index(), 0);
assert_eq!(s.span_ref(), &span);
assert!(s.detector().is_adaptive());
assert!(s.keyframes_slice().is_empty());
assert!(s.description().is_empty());
}
#[test]
fn try_new_rejects_nil_id_or_parent() {
let span = TimeRange::new(0, 100, tb());
assert_eq!(
Scene::try_new(Uuid7::nil(), Uuid7::new(), 0, span, SceneDetector::Manual).err(),
Some(SceneError::NilId)
);
assert_eq!(
Scene::try_new(Uuid7::new(), Uuid7::nil(), 0, span, SceneDetector::Manual).err(),
Some(SceneError::NilVideoTrackId)
);
assert!(SceneError::NilId.is_nil_id());
assert!(SceneError::NilVideoTrackId.is_nil_video_track_id());
}
#[test]
fn try_new_rejects_inverted_span() {
assert!(TimeRange::try_new(2000, 1000, tb()).is_none());
let inverted = TimeRange::new(1_000, 5_000, tb()).with_end(0);
assert!(inverted.start_pts() > inverted.end_pts());
assert_eq!(
Scene::try_new(
Uuid7::new(),
Uuid7::new(),
0,
inverted,
SceneDetector::Manual
)
.err(),
Some(SceneError::InvertedSpan)
);
assert!(SceneError::InvertedSpan.is_inverted_span());
}
#[test]
fn try_set_span_rejects_post_construction_inversion() {
let span = TimeRange::new(0, 5_000, tb());
let mut s = Scene::try_new(Uuid7::new(), Uuid7::new(), 0, span, SceneDetector::Manual).unwrap();
let mut inverted = TimeRange::new(2_000, 8_000, tb());
inverted.set_start(9_000);
assert_eq!(
s.try_set_span(inverted).err(),
Some(SceneError::InvertedSpan)
);
assert_eq!(s.span_ref(), &span);
let next = TimeRange::new(100, 200, tb());
s.try_set_span(next).unwrap();
assert_eq!(s.span_ref(), &next);
let inverted2 = TimeRange::new(3_000, 9_000, tb()).with_start(10_000);
assert_eq!(
s.clone().try_with_span(inverted2).err(),
Some(SceneError::InvertedSpan)
);
}
#[test]
fn instantaneous_span_is_allowed() {
let span = TimeRange::new(7_000, 7_000, tb());
let s = Scene::try_new(Uuid7::new(), Uuid7::new(), 0, span, SceneDetector::Manual)
.expect("instantaneous span allowed");
assert_eq!(s.span_ref().start_pts(), s.span_ref().end_pts());
}
#[test]
fn builders_and_setters_chain() {
let span = TimeRange::new(0, 5_000, tb());
let kf = Uuid7::new();
let s = Scene::try_new(
Uuid7::new(),
Uuid7::new(),
0,
span,
SceneDetector::Histogram,
)
.unwrap()
.with_index(3)
.with_detector(SceneDetector::Content)
.with_keyframes(std::vec![kf])
.with_description("Jane is eating");
assert_eq!(s.index(), 3);
assert!(s.detector().is_content());
assert_eq!(s.keyframes_slice(), &[kf]);
assert_eq!(s.description(), "Jane is eating");
let mut s = s;
s.set_index(0);
s.set_description("");
s.set_keyframes(std::vec::Vec::<Uuid7>::new());
s.set_detector(SceneDetector::Manual);
assert_eq!(s.index(), 0);
assert!(s.description().is_empty());
assert!(s.keyframes_slice().is_empty());
assert!(s.detector().is_manual());
}
}