alpha2_model/
fingerprint.rs1use alpha2_media::ColorMetadata;
5use alpha2_time::FrameRate;
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[serde(deny_unknown_fields)]
15pub struct MediaFingerprint {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub content_sha256: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub byte_length: Option<u64>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub modified_ms: Option<i64>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub streams: Option<StreamTopology>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub duration_ticks: Option<i64>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub color: Option<ColorMetadata>,
35}
36
37impl MediaFingerprint {
38 #[must_use]
40 pub fn is_valid(&self) -> bool {
41 self.malformed_part().is_none()
42 }
43
44 #[must_use]
47 pub fn malformed_part(&self) -> Option<&'static str> {
48 if self
49 .content_sha256
50 .as_deref()
51 .is_some_and(|hash| !is_sha256_hex(hash))
52 {
53 Some("content_sha256")
54 } else if self.duration_ticks.is_some_and(|ticks| ticks < 0) {
55 Some("duration_ticks")
56 } else if self
57 .streams
58 .as_ref()
59 .is_some_and(|streams| !streams.is_valid())
60 {
61 Some("streams")
62 } else if self.color.is_some_and(|color| !color.is_consistent()) {
63 Some("color")
64 } else {
65 None
66 }
67 }
68}
69
70#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
72#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[serde(deny_unknown_fields)]
74pub struct StreamTopology {
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub video: Option<VideoStreamShape>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub audio: Option<AudioStreamShape>,
81}
82
83impl StreamTopology {
84 fn is_valid(&self) -> bool {
85 self.video
86 .as_ref()
87 .is_none_or(|video| video.width > 0 && video.height > 0)
88 && self
89 .audio
90 .as_ref()
91 .is_none_or(|audio| audio.channels > 0 && audio.sample_rate > 0)
92 }
93}
94
95#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[serde(deny_unknown_fields)]
99#[non_exhaustive]
100pub struct VideoStreamShape {
101 pub width: u32,
103 pub height: u32,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub frame_rate: Option<FrameRate>,
110}
111
112impl VideoStreamShape {
113 #[must_use]
115 pub const fn new(width: u32, height: u32, frame_rate: Option<FrameRate>) -> Self {
116 Self {
117 width,
118 height,
119 frame_rate,
120 }
121 }
122}
123
124#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
126#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
127#[serde(deny_unknown_fields)]
128#[non_exhaustive]
129pub struct AudioStreamShape {
130 pub channels: u32,
132 pub sample_rate: u32,
134}
135
136impl AudioStreamShape {
137 #[must_use]
139 pub const fn new(channels: u32, sample_rate: u32) -> Self {
140 Self {
141 channels,
142 sample_rate,
143 }
144 }
145}
146
147fn is_sha256_hex(hash: &str) -> bool {
148 hash.len() == 64
149 && hash
150 .bytes()
151 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
152}