1use std::collections::BTreeMap;
11
12use serde::de::{self, Deserializer};
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16use super::time::{RationalTime, TimeRange};
17
18pub const DEFAULT_MEDIA_KEY: &str = "DEFAULT_MEDIA";
20
21macro_rules! schema_check {
23 ($($fn:ident => [$($family:literal),+], max $max:literal;)*) => {$(
24 pub(super) fn $fn<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
25 let s = String::deserialize(d)?;
26 match split_schema(&s) {
27 Some((name, v)) if [$($family),+].contains(&name) && (1..=$max).contains(&v) => {
28 Ok(s)
29 }
30 _ => Err(de::Error::custom(format_args!(
31 "expected {} (version 1..={}), found {s:?}",
32 [$($family),+].join(" or "),
33 $max
34 ))),
35 }
36 }
37 )*};
38}
39
40schema_check! {
41 rational_time => ["RationalTime"], max 1;
42 time_range => ["TimeRange"], max 1;
43 timeline => ["Timeline"], max 1;
44 stack => ["Stack"], max 1;
45 track => ["Track"], max 1;
46 clip => ["Clip"], max 2;
47 gap => ["Gap"], max 1;
48 transition => ["Transition"], max 1;
49 effect => ["Effect", "TimeEffect"], max 1;
50 warp => ["LinearTimeWarp", "FreezeFrame"], max 1;
51 marker => ["Marker"], max 3;
52 external_reference => ["ExternalReference"], max 1;
53 missing_reference => ["MissingReference"], max 1;
54 generator_reference => ["GeneratorReference"], max 1;
55}
56
57pub fn split_schema(s: &str) -> Option<(&str, u32)> {
59 let (name, v) = s.rsplit_once('.')?;
60 Some((name, v.parse().ok()?))
61}
62
63fn name<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
65 Ok(Option::<String>::deserialize(d)?.unwrap_or_default())
66}
67
68fn yes() -> bool {
69 true
70}
71
72macro_rules! schema_default {
73 ($($fn:ident => $s:literal;)*) => {$(
74 fn $fn() -> String {
75 $s.into()
76 }
77 )*};
78}
79
80schema_default! {
81 timeline_v => "Timeline.1";
82 stack_v => "Stack.1";
83 track_v => "Track.1";
84 clip_v => "Clip.2";
85 gap_v => "Gap.1";
86 transition_v => "Transition.1";
87 effect_v => "Effect.1";
88 linear_time_warp_v => "LinearTimeWarp.1";
89 freeze_frame_v => "FreezeFrame.1";
90 marker_v => "Marker.2";
91 external_reference_v => "ExternalReference.1";
92 missing_reference_v => "MissingReference.1";
93 generator_reference_v => "GeneratorReference.1";
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
98pub struct Timeline {
99 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "timeline")]
100 pub otio_schema: String,
101 #[serde(default, deserialize_with = "name")]
102 pub name: String,
103 #[serde(default)]
104 pub metadata: Map<String, Value>,
105 #[serde(default)]
106 pub global_start_time: Option<RationalTime>,
107 pub tracks: Stack,
108 #[serde(flatten)]
109 pub extra: Map<String, Value>,
110}
111
112impl Default for Timeline {
113 fn default() -> Self {
114 Self {
115 otio_schema: timeline_v(),
116 name: String::new(),
117 metadata: Map::new(),
118 global_start_time: None,
119 tracks: Stack::default(),
120 extra: Map::new(),
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct Stack {
128 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "stack")]
129 pub otio_schema: String,
130 #[serde(default, deserialize_with = "name")]
131 pub name: String,
132 #[serde(default)]
133 pub metadata: Map<String, Value>,
134 #[serde(default)]
135 pub source_range: Option<TimeRange>,
136 #[serde(default)]
137 pub effects: Vec<Effect>,
138 #[serde(default)]
139 pub markers: Vec<Marker>,
140 #[serde(default = "yes")]
141 pub enabled: bool,
142 #[serde(default)]
143 pub children: Vec<Item>,
144 #[serde(flatten)]
145 pub extra: Map<String, Value>,
146}
147
148impl Default for Stack {
149 fn default() -> Self {
150 Self {
151 otio_schema: stack_v(),
152 name: String::new(),
153 metadata: Map::new(),
154 source_range: None,
155 effects: Vec::new(),
156 markers: Vec::new(),
157 enabled: true,
158 children: Vec::new(),
159 extra: Map::new(),
160 }
161 }
162}
163
164pub mod track_kind {
166 pub const VIDEO: &str = "Video";
167 pub const AUDIO: &str = "Audio";
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub struct Track {
173 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "track")]
174 pub otio_schema: String,
175 #[serde(default, deserialize_with = "name")]
176 pub name: String,
177 #[serde(default)]
178 pub metadata: Map<String, Value>,
179 #[serde(default)]
180 pub source_range: Option<TimeRange>,
181 #[serde(default)]
182 pub effects: Vec<Effect>,
183 #[serde(default)]
184 pub markers: Vec<Marker>,
185 #[serde(default = "yes")]
186 pub enabled: bool,
187 #[serde(default)]
188 pub children: Vec<Item>,
189 #[serde(default)]
191 pub kind: String,
192 #[serde(flatten)]
193 pub extra: Map<String, Value>,
194}
195
196impl Track {
197 pub fn new(name: impl Into<String>, kind: &str) -> Self {
198 Self {
199 otio_schema: track_v(),
200 name: name.into(),
201 metadata: Map::new(),
202 source_range: None,
203 effects: Vec::new(),
204 markers: Vec::new(),
205 enabled: true,
206 children: Vec::new(),
207 kind: kind.into(),
208 extra: Map::new(),
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub struct Clip {
216 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "clip")]
217 pub otio_schema: String,
218 #[serde(default, deserialize_with = "name")]
219 pub name: String,
220 #[serde(default)]
221 pub metadata: Map<String, Value>,
222 #[serde(default)]
223 pub source_range: Option<TimeRange>,
224 #[serde(default)]
225 pub effects: Vec<Effect>,
226 #[serde(default)]
227 pub markers: Vec<Marker>,
228 #[serde(default = "yes")]
229 pub enabled: bool,
230 #[serde(default)]
231 pub media_references: BTreeMap<String, MediaReference>,
232 #[serde(default)]
233 pub active_media_reference_key: String,
234 #[serde(flatten)]
235 pub extra: Map<String, Value>,
236}
237
238impl Clip {
239 pub fn new(
240 name: impl Into<String>,
241 media: MediaReference,
242 source_range: Option<TimeRange>,
243 ) -> Self {
244 Self {
245 otio_schema: clip_v(),
246 name: name.into(),
247 metadata: Map::new(),
248 source_range,
249 effects: Vec::new(),
250 markers: Vec::new(),
251 enabled: true,
252 media_references: BTreeMap::from([(DEFAULT_MEDIA_KEY.to_owned(), media)]),
253 active_media_reference_key: DEFAULT_MEDIA_KEY.into(),
254 extra: Map::new(),
255 }
256 }
257
258 pub fn media_reference(&self) -> Option<&MediaReference> {
259 self.media_references.get(&self.active_media_reference_key)
260 }
261
262 pub fn media_reference_mut(&mut self) -> Option<&mut MediaReference> {
263 self.media_references
264 .get_mut(&self.active_media_reference_key)
265 }
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct Gap {
271 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "gap")]
272 pub otio_schema: String,
273 #[serde(default, deserialize_with = "name")]
274 pub name: String,
275 #[serde(default)]
276 pub metadata: Map<String, Value>,
277 #[serde(default)]
278 pub source_range: Option<TimeRange>,
279 #[serde(default)]
280 pub effects: Vec<Effect>,
281 #[serde(default)]
282 pub markers: Vec<Marker>,
283 #[serde(default = "yes")]
284 pub enabled: bool,
285 #[serde(flatten)]
286 pub extra: Map<String, Value>,
287}
288
289impl Gap {
290 pub fn new(duration: RationalTime) -> Self {
291 let start = RationalTime::new(0.0, duration.rate);
292 Self {
293 otio_schema: gap_v(),
294 name: String::new(),
295 metadata: Map::new(),
296 source_range: Some(TimeRange::new(start, duration)),
297 effects: Vec::new(),
298 markers: Vec::new(),
299 enabled: true,
300 extra: Map::new(),
301 }
302 }
303}
304
305pub mod transition_type {
307 pub const SMPTE_DISSOLVE: &str = "SMPTE_Dissolve";
308 pub const CUSTOM: &str = "Custom_Transition";
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct Transition {
314 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "transition")]
315 pub otio_schema: String,
316 #[serde(default, deserialize_with = "name")]
317 pub name: String,
318 #[serde(default)]
319 pub metadata: Map<String, Value>,
320 #[serde(default)]
321 pub transition_type: String,
322 pub in_offset: RationalTime,
323 pub out_offset: RationalTime,
324 #[serde(flatten)]
325 pub extra: Map<String, Value>,
326}
327
328impl Transition {
329 pub fn dissolve(in_offset: RationalTime, out_offset: RationalTime) -> Self {
330 Self {
331 otio_schema: transition_v(),
332 name: String::new(),
333 metadata: Map::new(),
334 transition_type: transition_type::SMPTE_DISSOLVE.into(),
335 in_offset,
336 out_offset,
337 extra: Map::new(),
338 }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Serialize)]
344#[serde(untagged)]
345pub enum Item {
346 Clip(Clip),
347 Gap(Gap),
348 Stack(Stack),
349 Track(Track),
350 Transition(Transition),
351 Unknown(Map<String, Value>),
353}
354
355impl<'de> Deserialize<'de> for Item {
356 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
357 let mut obj = Map::deserialize(d)?;
358 let family = schema_of(&obj);
359 let r = match family {
360 Some(("Clip", 1)) => {
361 upgrade_clip_1(&mut obj);
362 serde_json::from_value(obj.into()).map(Item::Clip)
363 }
364 Some(("Clip", _)) => serde_json::from_value(obj.into()).map(Item::Clip),
365 Some(("Gap", _)) => serde_json::from_value(obj.into()).map(Item::Gap),
366 Some(("Stack", _)) => serde_json::from_value(obj.into()).map(Item::Stack),
367 Some(("Track", _)) => serde_json::from_value(obj.into()).map(Item::Track),
368 Some(("Sequence", 1)) => {
369 obj.insert("OTIO_SCHEMA".into(), "Track.1".into());
370 serde_json::from_value(obj.into()).map(Item::Track)
371 }
372 Some(("Transition", _)) => serde_json::from_value(obj.into()).map(Item::Transition),
373 _ => Ok(Item::Unknown(obj)),
374 };
375 r.map_err(de::Error::custom)
376 }
377}
378
379fn upgrade_clip_1(obj: &mut Map<String, Value>) {
381 let media = match obj.shift_remove("media_reference") {
382 Some(v @ Value::Object(_)) => v,
383 _ => serde_json::to_value(MediaReference::Missing(MissingReference::default()))
384 .unwrap_or_default(),
385 };
386 obj.insert("OTIO_SCHEMA".into(), "Clip.2".into());
387 obj.insert(
388 "media_references".into(),
389 Value::Object(Map::from_iter([(DEFAULT_MEDIA_KEY.to_owned(), media)])),
390 );
391 obj.insert(
392 "active_media_reference_key".into(),
393 DEFAULT_MEDIA_KEY.into(),
394 );
395}
396
397fn schema_of(obj: &Map<String, Value>) -> Option<(&str, u32)> {
398 obj.get("OTIO_SCHEMA")
399 .and_then(Value::as_str)
400 .and_then(split_schema)
401}
402
403#[derive(Debug, Clone, PartialEq, Serialize)]
405#[serde(untagged)]
406pub enum Effect {
407 Effect(GenericEffect),
409 LinearTimeWarp(LinearTimeWarp),
411 FreezeFrame(LinearTimeWarp),
413 Unknown(Map<String, Value>),
415}
416
417impl Effect {
418 pub fn effect_name(&self) -> Option<&str> {
419 match self {
420 Effect::Effect(e) => Some(&e.effect_name),
421 Effect::LinearTimeWarp(e) | Effect::FreezeFrame(e) => Some(&e.effect_name),
422 Effect::Unknown(m) => m.get("effect_name").and_then(Value::as_str),
423 }
424 }
425}
426
427impl<'de> Deserialize<'de> for Effect {
428 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
429 let obj = Map::deserialize(d)?;
430 let r = match schema_of(&obj) {
431 Some(("Effect" | "TimeEffect", _)) => {
432 serde_json::from_value(obj.into()).map(Effect::Effect)
433 }
434 Some(("LinearTimeWarp", _)) => {
435 serde_json::from_value(obj.into()).map(Effect::LinearTimeWarp)
436 }
437 Some(("FreezeFrame", _)) => serde_json::from_value(obj.into()).map(Effect::FreezeFrame),
438 _ => Ok(Effect::Unknown(obj)),
439 };
440 r.map_err(de::Error::custom)
441 }
442}
443
444#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
446pub struct GenericEffect {
447 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "effect")]
448 pub otio_schema: String,
449 #[serde(default, deserialize_with = "name")]
450 pub name: String,
451 #[serde(default)]
452 pub metadata: Map<String, Value>,
453 #[serde(default)]
454 pub effect_name: String,
455 #[serde(flatten)]
456 pub extra: Map<String, Value>,
457}
458
459impl GenericEffect {
460 pub fn new(name: impl Into<String>, effect_name: impl Into<String>) -> Self {
461 Self {
462 otio_schema: effect_v(),
463 name: name.into(),
464 metadata: Map::new(),
465 effect_name: effect_name.into(),
466 extra: Map::new(),
467 }
468 }
469}
470
471#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
473pub struct LinearTimeWarp {
474 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "warp")]
475 pub otio_schema: String,
476 #[serde(default, deserialize_with = "name")]
477 pub name: String,
478 #[serde(default)]
479 pub metadata: Map<String, Value>,
480 #[serde(default)]
481 pub effect_name: String,
482 #[serde(default = "one")]
483 pub time_scalar: f64,
484 #[serde(flatten)]
485 pub extra: Map<String, Value>,
486}
487
488fn one() -> f64 {
489 1.0
490}
491
492impl LinearTimeWarp {
493 pub fn new(time_scalar: f64) -> Self {
494 Self {
495 otio_schema: linear_time_warp_v(),
496 name: String::new(),
497 metadata: Map::new(),
498 effect_name: "LinearTimeWarp".into(),
499 time_scalar,
500 extra: Map::new(),
501 }
502 }
503
504 pub fn freeze_frame() -> Self {
505 Self {
506 otio_schema: freeze_frame_v(),
507 effect_name: "FreezeFrame".into(),
508 ..Self::new(0.0)
509 }
510 }
511}
512
513#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
516pub struct Marker {
517 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "marker")]
518 pub otio_schema: String,
519 #[serde(default, deserialize_with = "name")]
520 pub name: String,
521 #[serde(default)]
522 pub metadata: Map<String, Value>,
523 pub marked_range: TimeRange,
524 #[serde(flatten)]
525 pub extra: Map<String, Value>,
526}
527
528impl Marker {
529 pub fn new(name: impl Into<String>, marked_range: TimeRange, color: &str) -> Self {
531 Self {
532 otio_schema: marker_v(),
533 name: name.into(),
534 metadata: Map::new(),
535 marked_range,
536 extra: Map::from_iter([("color".to_owned(), color.into())]),
537 }
538 }
539}
540
541#[derive(Debug, Clone, PartialEq, Serialize)]
543#[serde(untagged)]
544pub enum MediaReference {
545 External(ExternalReference),
546 Missing(MissingReference),
547 Generator(GeneratorReference),
548 Unknown(Map<String, Value>),
550}
551
552impl MediaReference {
553 pub fn available_range(&self) -> Option<&TimeRange> {
554 match self {
555 MediaReference::External(r) => r.available_range.as_ref(),
556 MediaReference::Missing(r) => r.available_range.as_ref(),
557 MediaReference::Generator(r) => r.available_range.as_ref(),
558 MediaReference::Unknown(_) => None,
559 }
560 }
561}
562
563impl<'de> Deserialize<'de> for MediaReference {
564 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
565 let obj = Map::deserialize(d)?;
566 let r = match schema_of(&obj) {
567 Some(("ExternalReference", _)) => {
568 serde_json::from_value(obj.into()).map(MediaReference::External)
569 }
570 Some(("MissingReference", _)) => {
571 serde_json::from_value(obj.into()).map(MediaReference::Missing)
572 }
573 Some(("GeneratorReference", _)) => {
574 serde_json::from_value(obj.into()).map(MediaReference::Generator)
575 }
576 _ => Ok(MediaReference::Unknown(obj)),
577 };
578 r.map_err(de::Error::custom)
579 }
580}
581
582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
584pub struct ExternalReference {
585 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "external_reference")]
586 pub otio_schema: String,
587 #[serde(default, deserialize_with = "name")]
588 pub name: String,
589 #[serde(default)]
590 pub metadata: Map<String, Value>,
591 #[serde(default)]
592 pub available_range: Option<TimeRange>,
593 #[serde(default)]
594 pub target_url: String,
595 #[serde(flatten)]
596 pub extra: Map<String, Value>,
597}
598
599impl ExternalReference {
600 pub fn new(target_url: impl Into<String>, available_range: Option<TimeRange>) -> Self {
601 Self {
602 otio_schema: external_reference_v(),
603 name: String::new(),
604 metadata: Map::new(),
605 available_range,
606 target_url: target_url.into(),
607 extra: Map::new(),
608 }
609 }
610}
611
612impl ExternalReference {
613 pub fn from_path(path: &std::path::Path, available_range: Option<TimeRange>) -> Self {
615 let mut url = String::from("file://");
616 for c in path.to_string_lossy().chars() {
617 match c {
618 '%' => url.push_str("%25"),
619 ' ' => url.push_str("%20"),
620 '#' => url.push_str("%23"),
621 '?' => url.push_str("%3F"),
622 c => url.push(c),
623 }
624 }
625 Self::new(url, available_range)
626 }
627
628 pub fn path(&self) -> Option<std::path::PathBuf> {
630 let rest = match self.target_url.strip_prefix("file://") {
631 Some(r) => r.strip_prefix("localhost").unwrap_or(r),
632 None if self.target_url.contains("://") => return None,
633 None => return Some(self.target_url.clone().into()),
634 };
635 let bytes = rest.as_bytes();
636 let mut out = Vec::with_capacity(bytes.len());
637 let mut i = 0;
638 while i < bytes.len() {
639 let hex = |b: u8| char::from(b).to_digit(16);
640 if bytes[i] == b'%'
641 && i + 2 < bytes.len()
642 && let (Some(h), Some(l)) = (hex(bytes[i + 1]), hex(bytes[i + 2]))
643 {
644 out.push(u8::try_from(h * 16 + l).unwrap_or(b'?'));
645 i += 3;
646 continue;
647 }
648 out.push(bytes[i]);
649 i += 1;
650 }
651 Some(String::from_utf8_lossy(&out).into_owned().into())
652 }
653}
654
655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
657pub struct MissingReference {
658 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "missing_reference")]
659 pub otio_schema: String,
660 #[serde(default, deserialize_with = "name")]
661 pub name: String,
662 #[serde(default)]
663 pub metadata: Map<String, Value>,
664 #[serde(default)]
665 pub available_range: Option<TimeRange>,
666 #[serde(flatten)]
667 pub extra: Map<String, Value>,
668}
669
670impl Default for MissingReference {
671 fn default() -> Self {
672 Self {
673 otio_schema: missing_reference_v(),
674 name: String::new(),
675 metadata: Map::new(),
676 available_range: None,
677 extra: Map::new(),
678 }
679 }
680}
681
682#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
684pub struct GeneratorReference {
685 #[serde(rename = "OTIO_SCHEMA", deserialize_with = "generator_reference")]
686 pub otio_schema: String,
687 #[serde(default, deserialize_with = "name")]
688 pub name: String,
689 #[serde(default)]
690 pub metadata: Map<String, Value>,
691 #[serde(default)]
692 pub available_range: Option<TimeRange>,
693 #[serde(default)]
694 pub generator_kind: String,
695 #[serde(default)]
696 pub parameters: Map<String, Value>,
697 #[serde(flatten)]
698 pub extra: Map<String, Value>,
699}
700
701impl GeneratorReference {
702 pub fn new(generator_kind: impl Into<String>, parameters: Map<String, Value>) -> Self {
703 Self {
704 otio_schema: generator_reference_v(),
705 name: String::new(),
706 metadata: Map::new(),
707 available_range: None,
708 generator_kind: generator_kind.into(),
709 parameters,
710 extra: Map::new(),
711 }
712 }
713}