Skip to main content

alpha2_model/
caption.rs

1//! Caption tracks: timed text the engine draws in a track's style.
2
3use crate::{MAX_TEXT_CONTENT_BYTES, TextStyle};
4use alpha2_time::TimeRange;
5use serde::{Deserialize, Serialize};
6
7/// What a caption track shows: its cues, in one style.
8#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[serde(deny_unknown_fields)]
11pub struct Captions {
12    /// How every cue of the track looks.
13    #[serde(default)]
14    pub style: TextStyle,
15    /// Cues in timeline order, none overlapping another: a track shows one
16    /// cue at a time. Simultaneous captions take one track each.
17    pub cues: Vec<CaptionCue>,
18}
19
20/// Text shown over one timeline interval.
21#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23#[serde(deny_unknown_fields)]
24pub struct CaptionCue {
25    /// When the cue shows. Non-empty and not before zero.
26    pub timeline_range: TimeRange,
27    /// The text; `\n` starts a line. Non-empty, at most
28    /// [`MAX_TEXT_CONTENT_BYTES`] bytes.
29    pub text: String,
30}
31
32/// Largest caption font size, as a fraction of the frame's height.
33pub const MAX_CAPTION_FONT_SIZE: f32 = 0.25;
34
35impl Captions {
36    pub(crate) fn is_valid(&self) -> bool {
37        self.style.is_valid(MAX_CAPTION_FONT_SIZE)
38            && self.cues.iter().all(|cue| {
39                !cue.timeline_range.is_empty()
40                    && cue.timeline_range.start().ticks() >= 0
41                    && !cue.text.is_empty()
42                    && cue.text.len() <= MAX_TEXT_CONTENT_BYTES
43            })
44            && self
45                .cues
46                .windows(2)
47                .all(|pair| pair[0].timeline_range.end() <= pair[1].timeline_range.start())
48    }
49}