1use mkv_parser_kit::{BoxData, EbmlElement, Tag, ebml_parse};
5
6const STRAND_MKV_FILENAME_TEMPLATE: &str = "movie%Y%m%d_%H%M%S.%f";
7
8#[derive(thiserror::Error, Debug)]
9pub enum Error {
10 #[error("parse error: {source}")]
11 Parser {
12 #[from]
13 source: mkv_parser_kit::Error,
14 },
15 #[error("missing data: {missing}")]
16 MissingData { missing: String },
17 #[error("no cluster timestamp")]
18 NoClusterTimestamp,
19 #[error("could not determine filename")]
20 CouldNotDetermineFilename,
21 #[error("filename is not valid UTF-8")]
22 FilenameNotUtf8,
23 #[error("filename is could not be parsed to datetime")]
24 DatetimeInFilenameNotParsedCorrectly,
25}
26
27fn missing(what: &str) -> Error {
28 Error::MissingData {
29 missing: what.to_string(),
30 }
31}
32
33pub type Result<T> = std::result::Result<T, Error>;
34
35#[derive(Debug, Clone)]
36pub struct BlockData {
37 pub pts: std::time::Duration,
38 pub start_idx: u64,
39 pub size: usize,
40 pub is_keyframe: bool,
41}
42
43#[derive(Debug)]
44pub struct StrandCamMkvMetadata {
45 pub creation_time: chrono::DateTime<chrono::FixedOffset>,
46 pub camera_name: Option<String>,
47 pub gamma: Option<f32>,
48 pub writing_app: String,
49}
50
51#[derive(Debug)]
52pub struct ParsedStrandCamMkv {
53 pub width: u32,
54 pub height: u32,
55 pub timestep_nanos: u32,
56 pub metadata: StrandCamMkvMetadata,
57 pub block_data: Vec<BlockData>,
58 pub uncompressed_fourcc: Option<String>,
59 pub codec: String,
60}
61
62impl TryFrom<Accum> for ParsedStrandCamMkv {
63 type Error = Error;
64 fn try_from(a: Accum) -> Result<Self> {
65 let width = a.width.ok_or(missing("width"))?;
66 let height = a.height.ok_or(missing("height"))?;
67 let metadata = StrandCamMkvMetadata {
68 creation_time: a.creation_time.ok_or(missing("creation time"))?,
69 gamma: a.gamma,
70 camera_name: a.title,
72 writing_app: a.writing_app.ok_or(missing("writing app"))?,
73 };
74 Ok(Self {
75 width,
76 height,
77 timestep_nanos: a.timestep_nanos.ok_or(missing("timestep"))?,
78 block_data: a.block_data,
79 codec: a.codec.ok_or(missing("codec"))?,
80 metadata,
81 uncompressed_fourcc: a.uncompressed_fourcc,
82 })
83 }
84}
85
86#[derive(Debug, Default)]
87struct Accum {
88 n_segments: u8,
89 width: Option<u32>,
90 height: Option<u32>,
91 timestep_nanos: Option<u32>,
92 creation_time: Option<chrono::DateTime<chrono::FixedOffset>>,
93 title: Option<String>,
94 gamma: Option<f32>,
95 writing_app: Option<String>,
96 block_data: Vec<BlockData>,
97 pts: Option<std::time::Duration>,
100 uncompressed_fourcc: Option<String>,
101 codec: Option<String>,
102}
103
104fn line_summary(e: &EbmlElement) -> String {
105 let name = format!("{:?}", e.tag());
106 let position = e.position();
107 let full_size = e.full_size();
108 let data_size = e.data_size();
109 format!("{name} at {position} size {full_size} data size {data_size}")
110}
111
112const PRINT_ALL: bool = false;
113
114fn do_parse(
115 element: &EbmlElement,
116 depth: u8,
117 accum: &mut Accum,
118 tag_path: &[Tag],
119 verbose: bool,
120 filename: Option<&str>,
121) -> Result<()> {
122 let verbose_prefix = if verbose {
123 let mut prefix = String::new();
124 for idx in 0..depth {
125 if idx == 0 {
126 prefix.push('|');
127 } else {
128 prefix.push(' ');
129 }
130 }
131 Some(prefix)
132 } else {
133 None
134 };
135 if let Some(prefix) = &verbose_prefix
136 && PRINT_ALL
137 {
138 println!("{}+ {}", prefix, line_summary(element));
139 }
140 for child in element.children().iter() {
141 let mut child_tag_path = tag_path.to_vec();
142 child_tag_path.push(child.tag());
143 do_parse(child, depth + 1, accum, &child_tag_path, verbose, filename)?;
144 }
145
146 if let Some(prefix) = &verbose_prefix
147 && let Some(bd) = &element.box_data()
148 {
149 if !PRINT_ALL {
150 println!("{}+ {}", prefix, line_summary(element));
151 }
152 println!("{prefix}+ {bd:?}");
153 }
154
155 match tag_path {
156 [Tag::Segment] => {
157 accum.n_segments += 1;
158 assert_eq!(accum.n_segments, 1); }
160 [Tag::Segment, Tag::Info, Tag::DateUTC] => {
161 if let Some(BoxData::DateTime(creation_time_utc)) = element.box_data() {
162 let creation_time = infer_timezone(creation_time_utc, filename)?;
163 accum.creation_time = Some(creation_time);
164 } else {
165 panic!("need DateUTC");
166 }
167 }
168 [Tag::Segment, Tag::Info, Tag::TimestampScale] => {
169 accum.timestep_nanos = Some(get_uint(element));
170 }
171 [Tag::Segment, Tag::Info, Tag::WritingApp] => {
172 accum.writing_app = Some(get_string(element));
173 }
174 [Tag::Segment, Tag::Info, Tag::Title] => {
175 accum.title = Some(get_string(element));
176 }
177 [Tag::Segment, Tag::Info, Tag::GammaValue] => {
178 accum.gamma = Some(get_float(element));
179 }
180 [Tag::Segment, Tag::Cluster] => {
181 accum.pts = None;
183 }
184 [Tag::Segment, Tag::Cluster, Tag::Timestamp] => {
185 assert!(accum.pts.is_none());
187 let n_timesteps: u64 = get_uint(element).into();
189 let timestep_nanos: u64 = accum.timestep_nanos.unwrap().into();
190 let pts_total_nanos = n_timesteps * timestep_nanos;
191
192 let pts = std::time::Duration::from_nanos(pts_total_nanos);
193 accum.pts = Some(pts);
196 }
197 [Tag::Segment, Tag::Cluster, Tag::SimpleBlock] => {
198 if let Some(cluster_pts) = accum.pts {
200 let x = if let Some(BoxData::SimpleBlockData(block_data)) = &element.box_data() {
201 let n_timesteps: u64 = block_data.timestamp.try_into().unwrap();
202 let timestep_nanos: u64 = accum.timestep_nanos.unwrap().into();
203 let cluster_offset_nanos = n_timesteps * timestep_nanos;
204 let cluster_offset = std::time::Duration::from_nanos(cluster_offset_nanos);
205 let pts = cluster_pts + cluster_offset;
206 BlockData {
207 pts,
208 start_idx: block_data.start,
209 is_keyframe: block_data.is_keyframe,
210 size: block_data.size.try_into().unwrap(),
211 }
212 } else {
213 panic!("expected UncompressedFourCC in {:?}", element.tag());
214 };
215 accum.block_data.push(x);
216 } else {
217 return Err(Error::NoClusterTimestamp);
218 }
219 }
220 [Tag::Segment, Tag::Tracks, Tag::TrackEntry, Tag::CodecID] => {
221 accum.codec = Some(get_ascii_string(element));
222 }
223 [
224 Tag::Segment,
225 Tag::Tracks,
226 Tag::TrackEntry,
227 Tag::Video,
228 Tag::UncompressedFourCC,
229 ] => {
230 accum.uncompressed_fourcc =
231 if let Some(BoxData::UncompressedFourCC(s)) = &element.box_data() {
232 Some(s.clone())
233 } else {
234 panic!("expected UncompressedFourCC in {:?}", element.tag());
235 }
236 }
237 [
238 Tag::Segment,
239 Tag::Tracks,
240 Tag::TrackEntry,
241 Tag::Video,
242 Tag::PixelWidth,
243 ] => {
244 accum.width = Some(get_uint(element));
245 }
246 [
247 Tag::Segment,
248 Tag::Tracks,
249 Tag::TrackEntry,
250 Tag::Video,
251 Tag::PixelHeight,
252 ] => {
253 accum.height = Some(get_uint(element));
254 }
255 _ => {
256 }
258 }
259 Ok(())
260}
261
262fn get_ascii_string(element: &EbmlElement) -> String {
263 if let Some(BoxData::AsciiString(s)) = &element.box_data() {
264 s.clone()
265 } else {
266 panic!("expected ascii string in {:?}", element.tag());
267 }
268}
269
270fn get_string(element: &EbmlElement) -> String {
271 if let Some(BoxData::String(s)) = &element.box_data() {
272 s.clone()
273 } else {
274 panic!("expected string in {:?}", element.tag());
275 }
276}
277
278fn get_float(element: &EbmlElement) -> f32 {
279 if let Some(BoxData::Float(f)) = &element.box_data() {
280 *f
281 } else {
282 panic!("expected string");
283 }
284}
285
286fn get_uint(element: &EbmlElement) -> u32 {
287 if let Some(BoxData::UnsignedInt(v)) = &element.box_data() {
288 *v
289 } else {
290 panic!("expected string");
291 }
292}
293
294pub fn parse_strand_cam_mkv<R, P>(
295 rdr: R,
296 verbose: bool,
297 path: Option<P>,
298) -> Result<(ParsedStrandCamMkv, R)>
299where
300 R: std::io::Read + std::io::Seek,
301 P: AsRef<std::path::Path>,
302{
303 let filename = path.map(|p| format!("{}", p.as_ref().display()));
304 let (parsed, rdr) = ebml_parse(rdr)?;
305 let mut accum = Accum::default();
307 for element in parsed.iter() {
308 do_parse(
309 element,
310 0,
311 &mut accum,
312 &[element.tag()],
313 verbose,
314 filename.as_deref(),
315 )?;
316 }
317 Ok((accum.try_into()?, rdr))
318}
319
320pub fn infer_timezone(
327 creation_time_utc: &chrono::DateTime<chrono::Utc>,
328 filename: Option<&str>,
329) -> Result<chrono::DateTime<chrono::FixedOffset>> {
330 let zero_offset = chrono::FixedOffset::east_opt(0).unwrap();
331 let mut creation_time = creation_time_utc.with_timezone(&zero_offset);
332 if let Some(filename) = filename {
333 let path_buf = std::path::PathBuf::from(filename);
334 let filename = path_buf
335 .file_name()
336 .ok_or(Error::CouldNotDetermineFilename)?;
337 let filename = filename.to_str().ok_or(Error::FilenameNotUtf8)?;
338
339 let underscores: Vec<_> = filename.split('_').collect();
342 if underscores.len() > 2 {
343 let joined = format!("{}_{}", underscores[0], underscores[1]);
344
345 match chrono::NaiveDateTime::parse_from_str(&joined, STRAND_MKV_FILENAME_TEMPLATE) {
346 Ok(naive) => {
347 let offset_dur = naive - creation_time_utc.naive_utc();
348 let offset =
349 chrono::FixedOffset::east_opt(offset_dur.num_seconds().try_into().unwrap())
350 .unwrap();
351 creation_time = creation_time_utc.with_timezone(&offset);
352 let test_str = creation_time
353 .format(STRAND_MKV_FILENAME_TEMPLATE)
354 .to_string();
355 if joined != test_str {
359 return Err(Error::DatetimeInFilenameNotParsedCorrectly);
360 }
361 }
362 Err(_e) => {}
363 }
364 }
365 };
366 Ok(creation_time)
367}