Skip to main content

ffcine_timeline/
schema.rs

1//! Serde types for the OTIO core schemas.
2//!
3//! Every object keeps the `OTIO_SCHEMA` string it was read with and an `extra` map for fields this
4//! model doesn't name (newer OTIO fields such as `color`, `comment` or `enabled` on effects,
5//! adapter fields), so a read/write cycle loses nothing. Objects of unknown schemas inside
6//! polymorphic slots (`children`, `effects`, `media_references`) are kept verbatim as `Unknown`.
7//! The only changes on read are OTIO's own: `Clip.1` -> `Clip.2`, and `Sequence.1` (the old
8//! name) -> `Track.1`.
9
10use 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
18/// `active_media_reference_key` of a clip with a single reference.
19pub const DEFAULT_MEDIA_KEY: &str = "DEFAULT_MEDIA";
20
21/// Defines `deserialize_with` functions that accept the `OTIO_SCHEMA` strings of some families.
22macro_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
57/// `"Clip.2"` -> `("Clip", 2)`.
58pub fn split_schema(s: &str) -> Option<(&str, u32)> {
59    let (name, v) = s.rsplit_once('.')?;
60    Some((name, v.parse().ok()?))
61}
62
63/// OTIO writes `"name": null` for unnamed objects in some files; read it as `""`.
64fn 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/// `Timeline.1`: the root object.
97#[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/// `Stack.1`: children play at the same time, first at the bottom.
126#[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
164/// `Track.kind` values.
165pub mod track_kind {
166    pub const VIDEO: &str = "Video";
167    pub const AUDIO: &str = "Audio";
168}
169
170/// `Track.1`: children play one after another.
171#[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    /// [`track_kind::VIDEO`] or [`track_kind::AUDIO`].
190    #[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/// `Clip.2`: a range of one media reference (of several alternatives).
214#[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/// `Gap.1`: empty time in a track.
269#[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
305/// `Transition.transition_type` values.
306pub mod transition_type {
307    pub const SMPTE_DISSOLVE: &str = "SMPTE_Dissolve";
308    pub const CUSTOM: &str = "Custom_Transition";
309}
310
311/// `Transition.1`: overlaps its neighbours by `in_offset` / `out_offset`.
312#[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/// A child of a [`Track`] or [`Stack`].
343#[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    /// Any other schema, verbatim.
352    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
379/// OTIO's upgrade: `media_reference` becomes `media_references[DEFAULT_MEDIA]`.
380fn 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/// An entry of an item's `effects`.
404#[derive(Debug, Clone, PartialEq, Serialize)]
405#[serde(untagged)]
406pub enum Effect {
407    /// `Effect.1` (also used for `TimeEffect.1`).
408    Effect(GenericEffect),
409    /// `LinearTimeWarp.1`.
410    LinearTimeWarp(LinearTimeWarp),
411    /// `FreezeFrame.1` (a `LinearTimeWarp` with `time_scalar` 0).
412    FreezeFrame(LinearTimeWarp),
413    /// Any other schema, verbatim.
414    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/// `Effect.1`: a named effect; ffcine's parameters live in `metadata.ffcine`.
445#[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/// `LinearTimeWarp.1` / `FreezeFrame.1`: constant speed change.
472#[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/// `Marker.2` / `Marker.3`. `color` (a name in 2, an object in 3) and `comment` (3) stay in
514/// `extra`.
515#[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    /// A `Marker.2` (readable by OTIO 0.14+) with a colour name such as `"GREEN"`.
530    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/// A clip's media.
542#[derive(Debug, Clone, PartialEq, Serialize)]
543#[serde(untagged)]
544pub enum MediaReference {
545    External(ExternalReference),
546    Missing(MissingReference),
547    Generator(GeneratorReference),
548    /// Any other schema (`ImageSequenceReference`, ...), verbatim.
549    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/// `ExternalReference.1`: a file or URL.
583#[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    /// A `file://` URL for an absolute path (`%`, space, `#` and `?` percent-encoded).
614    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    /// The local path of a `file://` URL (percent-decoded) or of a plain path.
629    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/// `MissingReference.1`: media that isn't known.
656#[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/// `GeneratorReference.1`: synthesised media (`ffcine.text`, solids, ...).
683#[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}