1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//! What a source's media was when the project recorded it: evidence a host
//! compares with the bytes it finds, never a locator.
use alpha2_media::ColorMetadata;
use alpha2_time::FrameRate;
use serde::{Deserialize, Serialize};
/// Evidence of a source's media, each part recorded only when it was known.
///
/// A matching content hash proves the same bytes. Without one, the other
/// parts can only show a difference. `modified_ms` narrows a search for
/// candidates and never decides whether a candidate is the same media.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MediaFingerprint {
/// SHA-256 of the whole file, as 64 lowercase hexadecimal digits.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_sha256: Option<String>,
/// The file's length in bytes.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub byte_length: Option<u64>,
/// The file's modification time, in milliseconds since the Unix epoch.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modified_ms: Option<i64>,
/// The streams the file carries.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub streams: Option<StreamTopology>,
/// Where the media ends on its own timeline, in ticks: its first
/// presentation plus its length, the end of any source range that reads
/// it. A transport stream that starts at 1.4 s and lasts 2 s ends at
/// 3.4 s. Non-negative.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ticks: Option<i64>,
/// The color the decoded pictures carry.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: Option<ColorMetadata>,
}
impl MediaFingerprint {
/// Whether every recorded part is well formed.
#[must_use]
pub fn is_valid(&self) -> bool {
self.malformed_part().is_none()
}
/// The first recorded part that is not well formed, by its field name:
/// a hash that is not 64 lowercase hexadecimal digits, a negative
/// duration, a stream without size, channels or rate, or a color whose
/// dynamic range contradicts its transfer.
#[must_use]
pub fn malformed_part(&self) -> Option<&'static str> {
if self
.content_sha256
.as_deref()
.is_some_and(|hash| !is_sha256_hex(hash))
{
Some("content_sha256")
} else if self.duration_ticks.is_some_and(|ticks| ticks < 0) {
Some("duration_ticks")
} else if self
.streams
.as_ref()
.is_some_and(|streams| !streams.is_valid())
{
Some("streams")
} else if self.color.is_some_and(|color| !color.is_consistent()) {
Some("color")
} else {
None
}
}
}
/// The streams a media file carries.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct StreamTopology {
/// The primary video stream, if there is one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub video: Option<VideoStreamShape>,
/// The primary audio stream, if there is one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioStreamShape>,
}
impl StreamTopology {
fn is_valid(&self) -> bool {
self.video
.as_ref()
.is_none_or(|video| video.width > 0 && video.height > 0)
&& self
.audio
.as_ref()
.is_none_or(|audio| audio.channels > 0 && audio.sample_rate > 0)
}
}
/// A video stream's displayed size and frame rate.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct VideoStreamShape {
/// Displayed width in square pixels: the stored width stretched by a
/// pixel aspect ratio wider than tall, then turned by the stream's
/// orientation.
pub width: u32,
/// Displayed height in square pixels: the stored height stretched by a
/// pixel aspect ratio taller than wide, then turned.
pub height: u32,
/// The rate its first frames are presented at, as every host measures
/// it from packet times and rounds it (`alpha2_assets::FrameRateEvidence`),
/// when it can be measured. Not the rate a stream declares: a declared
/// rate is a guess for variable-rate media, and browsers report none.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frame_rate: Option<FrameRate>,
}
impl VideoStreamShape {
/// A stream of `width` x `height` pictures at `frame_rate`.
#[must_use]
pub const fn new(width: u32, height: u32, frame_rate: Option<FrameRate>) -> Self {
Self {
width,
height,
frame_rate,
}
}
}
/// An audio stream's channel count and sample rate.
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct AudioStreamShape {
/// Channel count.
pub channels: u32,
/// Samples per second per channel.
pub sample_rate: u32,
}
impl AudioStreamShape {
/// A stream of `channels` channels at `sample_rate`.
#[must_use]
pub const fn new(channels: u32, sample_rate: u32) -> Self {
Self {
channels,
sample_rate,
}
}
}
fn is_sha256_hex(hash: &str) -> bool {
hash.len() == 64
&& hash
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}