1#![allow(dead_code)]
2use crate::{FrameRate, Timecode, TimecodeError};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum TimecodeSource {
14 Ltc,
16 Vitc,
18 Mtc,
20 Ntp,
22 Ptp,
24 FreeRun,
26 FileMetadata,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct RecordDate {
33 pub year: u16,
35 pub month: u8,
37 pub day: u8,
39}
40
41impl RecordDate {
42 pub fn new(year: u16, month: u8, day: u8) -> Result<Self, TimecodeError> {
48 if month == 0 || month > 12 {
49 return Err(TimecodeError::InvalidConfiguration);
50 }
51 if day == 0 || day > 31 {
52 return Err(TimecodeError::InvalidConfiguration);
53 }
54 Ok(Self { year, month, day })
55 }
56
57 pub fn to_iso_string(&self) -> String {
59 format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct UserBitsPayload {
66 pub raw: u32,
68 pub is_date: bool,
70}
71
72impl UserBitsPayload {
73 pub fn new(raw: u32, is_date: bool) -> Self {
75 Self { raw, is_date }
76 }
77
78 pub fn nibble(&self, index: u8) -> u8 {
80 if index > 7 {
81 return 0;
82 }
83 ((self.raw >> (index * 4)) & 0x0F) as u8
84 }
85
86 pub fn set_nibble(&mut self, index: u8, value: u8) {
88 if index > 7 {
89 return;
90 }
91 let shift = index * 4;
92 self.raw &= !(0x0F << shift);
93 self.raw |= ((value & 0x0F) as u32) << shift;
94 }
95
96 pub fn decode_date(&self) -> Option<RecordDate> {
100 if !self.is_date {
101 return None;
102 }
103 let day = self.nibble(0) * 10 + self.nibble(1);
104 let month = self.nibble(2) * 10 + self.nibble(3);
105 let year_hi = self.nibble(4) as u16 * 10 + self.nibble(5) as u16;
106 let year_lo = self.nibble(6) as u16 * 10 + self.nibble(7) as u16;
107 let year = year_hi * 100 + year_lo;
108 RecordDate::new(year, month, day).ok()
109 }
110
111 pub fn encode_date(date: &RecordDate) -> Self {
113 let mut payload = Self::new(0, true);
114 let day_hi = date.day / 10;
115 let day_lo = date.day % 10;
116 let month_hi = date.month / 10;
117 let month_lo = date.month % 10;
118 let year_hi_hi = (date.year / 1000) as u8;
119 let year_hi_lo = ((date.year / 100) % 10) as u8;
120 let year_lo_hi = ((date.year / 10) % 10) as u8;
121 let year_lo_lo = (date.year % 10) as u8;
122 payload.set_nibble(0, day_hi);
123 payload.set_nibble(1, day_lo);
124 payload.set_nibble(2, month_hi);
125 payload.set_nibble(3, month_lo);
126 payload.set_nibble(4, year_hi_hi);
127 payload.set_nibble(5, year_hi_lo);
128 payload.set_nibble(6, year_lo_hi);
129 payload.set_nibble(7, year_lo_lo);
130 payload
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct ReelId {
137 pub name: String,
139 pub sequence: Option<u32>,
141}
142
143impl ReelId {
144 pub fn new(name: impl Into<String>) -> Self {
146 Self {
147 name: name.into(),
148 sequence: None,
149 }
150 }
151
152 pub fn with_sequence(mut self, seq: u32) -> Self {
154 self.sequence = Some(seq);
155 self
156 }
157}
158
159#[derive(Debug, Clone)]
164pub struct TcMetadata {
165 pub timecode: Timecode,
167 pub frame_rate: FrameRate,
169 pub source: TimecodeSource,
171 pub reel: Option<ReelId>,
173 pub record_date: Option<RecordDate>,
175 pub user_bits: Option<UserBitsPayload>,
177 pub tags: HashMap<String, String>,
179 pub scene: Option<String>,
181 pub take: Option<u32>,
183}
184
185impl TcMetadata {
186 pub fn new(timecode: Timecode, frame_rate: FrameRate, source: TimecodeSource) -> Self {
188 Self {
189 timecode,
190 frame_rate,
191 source,
192 reel: None,
193 record_date: None,
194 user_bits: None,
195 tags: HashMap::new(),
196 scene: None,
197 take: None,
198 }
199 }
200
201 pub fn with_reel(mut self, reel: ReelId) -> Self {
203 self.reel = Some(reel);
204 self
205 }
206
207 pub fn with_record_date(mut self, date: RecordDate) -> Self {
209 self.record_date = Some(date);
210 self
211 }
212
213 pub fn with_user_bits(mut self, ub: UserBitsPayload) -> Self {
215 self.user_bits = Some(ub);
216 self
217 }
218
219 pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
221 self.tags.insert(key.into(), value.into());
222 self
223 }
224
225 pub fn with_scene(mut self, scene: impl Into<String>) -> Self {
227 self.scene = Some(scene.into());
228 self
229 }
230
231 pub fn with_take(mut self, take: u32) -> Self {
233 self.take = Some(take);
234 self
235 }
236
237 pub fn summary(&self) -> String {
239 let mut parts = vec![format!("TC={}", self.timecode)];
240 parts.push(format!("src={:?}", self.source));
241 if let Some(ref reel) = self.reel {
242 parts.push(format!("reel={}", reel.name));
243 }
244 if let Some(ref date) = self.record_date {
245 parts.push(format!("date={}", date.to_iso_string()));
246 }
247 if let Some(ref scene) = self.scene {
248 parts.push(format!("scene={scene}"));
249 }
250 if let Some(take) = self.take {
251 parts.push(format!("take={take}"));
252 }
253 parts.join(" | ")
254 }
255
256 pub fn validate(&self) -> Result<(), TimecodeError> {
262 let expected_fps = self.frame_rate.frames_per_second() as u8;
263 if self.timecode.frame_rate.fps != expected_fps {
264 return Err(TimecodeError::InvalidConfiguration);
265 }
266 if self.timecode.frame_rate.drop_frame != self.frame_rate.is_drop_frame() {
267 return Err(TimecodeError::InvalidConfiguration);
268 }
269 Ok(())
270 }
271}
272
273#[derive(Debug, Clone)]
275pub struct MetadataTimeline {
276 entries: Vec<(u64, TcMetadata)>,
278}
279
280impl MetadataTimeline {
281 pub fn new() -> Self {
283 Self {
284 entries: Vec::new(),
285 }
286 }
287
288 pub fn insert(&mut self, frame: u64, meta: TcMetadata) {
290 let pos = self.entries.partition_point(|(f, _)| *f < frame);
291 self.entries.insert(pos, (frame, meta));
292 }
293
294 pub fn lookup(&self, frame: u64) -> Option<&TcMetadata> {
296 let pos = self.entries.partition_point(|(f, _)| *f <= frame);
297 if pos == 0 {
298 return None;
299 }
300 Some(&self.entries[pos - 1].1)
301 }
302
303 pub fn len(&self) -> usize {
305 self.entries.len()
306 }
307
308 pub fn is_empty(&self) -> bool {
310 self.entries.is_empty()
311 }
312
313 pub fn entries(&self) -> &[(u64, TcMetadata)] {
315 &self.entries
316 }
317}
318
319impl Default for MetadataTimeline {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 fn make_tc() -> Timecode {
330 Timecode::new(1, 2, 3, 4, FrameRate::Fps25).expect("valid timecode")
331 }
332
333 #[test]
334 fn test_record_date_valid() {
335 let d = RecordDate::new(2026, 3, 2).expect("valid record date");
336 assert_eq!(d.to_iso_string(), "2026-03-02");
337 }
338
339 #[test]
340 fn test_record_date_invalid_month() {
341 assert!(RecordDate::new(2026, 13, 1).is_err());
342 }
343
344 #[test]
345 fn test_record_date_invalid_day() {
346 assert!(RecordDate::new(2026, 1, 0).is_err());
347 }
348
349 #[test]
350 fn test_user_bits_nibble() {
351 let mut ub = UserBitsPayload::new(0, false);
352 ub.set_nibble(0, 0x0A);
353 assert_eq!(ub.nibble(0), 0x0A);
354 assert_eq!(ub.nibble(1), 0);
355 }
356
357 #[test]
358 fn test_user_bits_date_encode_decode() {
359 let date = RecordDate::new(2026, 3, 15).expect("valid record date");
360 let ub = UserBitsPayload::encode_date(&date);
361 let decoded = ub.decode_date().expect("decode should succeed");
362 assert_eq!(decoded.year, 2026);
363 assert_eq!(decoded.month, 3);
364 assert_eq!(decoded.day, 15);
365 }
366
367 #[test]
368 fn test_user_bits_no_date() {
369 let ub = UserBitsPayload::new(0x12345678, false);
370 assert!(ub.decode_date().is_none());
371 }
372
373 #[test]
374 fn test_reel_id() {
375 let reel = ReelId::new("A001").with_sequence(1);
376 assert_eq!(reel.name, "A001");
377 assert_eq!(reel.sequence, Some(1));
378 }
379
380 #[test]
381 fn test_tc_metadata_new() {
382 let tc = make_tc();
383 let meta = TcMetadata::new(tc, FrameRate::Fps25, TimecodeSource::Ltc);
384 assert_eq!(meta.source, TimecodeSource::Ltc);
385 assert!(meta.reel.is_none());
386 }
387
388 #[test]
389 fn test_tc_metadata_with_builders() {
390 let tc = make_tc();
391 let meta = TcMetadata::new(tc, FrameRate::Fps25, TimecodeSource::Vitc)
392 .with_reel(ReelId::new("B002"))
393 .with_scene("42A")
394 .with_take(3)
395 .with_tag("camera", "A");
396 assert_eq!(meta.scene.as_deref(), Some("42A"));
397 assert_eq!(meta.take, Some(3));
398 assert_eq!(meta.tags.get("camera").expect("key should exist"), "A");
399 }
400
401 #[test]
402 fn test_tc_metadata_summary() {
403 let tc = make_tc();
404 let meta = TcMetadata::new(tc, FrameRate::Fps25, TimecodeSource::Ltc).with_scene("1A");
405 let s = meta.summary();
406 assert!(s.contains("TC=01:02:03:04"));
407 assert!(s.contains("scene=1A"));
408 }
409
410 #[test]
411 fn test_tc_metadata_validate_ok() {
412 let tc = make_tc();
413 let meta = TcMetadata::new(tc, FrameRate::Fps25, TimecodeSource::Ltc);
414 assert!(meta.validate().is_ok());
415 }
416
417 #[test]
418 fn test_tc_metadata_validate_mismatch() {
419 let tc = make_tc();
420 let meta = TcMetadata::new(tc, FrameRate::Fps30, TimecodeSource::Ltc);
421 assert!(meta.validate().is_err());
422 }
423
424 #[test]
425 fn test_metadata_timeline_insert_and_lookup() {
426 let tc = make_tc();
427 let meta = TcMetadata::new(tc, FrameRate::Fps25, TimecodeSource::FreeRun);
428 let mut tl = MetadataTimeline::new();
429 tl.insert(100, meta.clone());
430 tl.insert(200, meta);
431 assert_eq!(tl.len(), 2);
432 let found = tl.lookup(150).expect("lookup should succeed");
433 assert_eq!(found.timecode.hours, 1);
434 }
435
436 #[test]
437 fn test_metadata_timeline_empty_lookup() {
438 let tl = MetadataTimeline::new();
439 assert!(tl.lookup(0).is_none());
440 assert!(tl.is_empty());
441 }
442
443 #[test]
444 fn test_timecode_source_variants() {
445 let sources = [
446 TimecodeSource::Ltc,
447 TimecodeSource::Vitc,
448 TimecodeSource::Mtc,
449 TimecodeSource::Ntp,
450 TimecodeSource::Ptp,
451 TimecodeSource::FreeRun,
452 TimecodeSource::FileMetadata,
453 ];
454 assert_eq!(sources.len(), 7);
455 }
456}