Skip to main content

ffcine_timeline/
lib.rs

1//! The ffcine timeline: plain OTIO JSON (`Timeline.1`, core schemas only), see `docs/design.md`.
2//!
3//! [`schema`] has serde types for the core OTIO objects, [`metadata`] the `ffcine` metadata
4//! namespace. Reading and writing a file keeps everything, including fields and schemas the
5//! model doesn't know (see [`schema`] for the one upgrade OTIO itself applies).
6//!
7//! ```
8//! use ffcine_timeline::{Clip, ExternalReference, HasMetadata, Item, MediaReference, TimeRange,
9//!     Timeline, Track, track_kind};
10//!
11//! let mut track = Track::new("V1", track_kind::VIDEO);
12//! let media = MediaReference::External(ExternalReference::new("file:///a.mov", None));
13//! let mut clip = Clip::new("a", media, Some(TimeRange::from_frames(10.0, 58.0, 24.0)));
14//! clip.set_ffcine_id("a");
15//! track.children.push(Item::Clip(clip));
16//! let mut t = Timeline::default();
17//! t.tracks.children.push(Item::Track(track));
18//! let json = t.to_json()?;
19//! assert_eq!(Timeline::from_json(&json)?, t);
20//! # Ok::<(), ffcine_core::Error>(())
21//! ```
22
23pub mod metadata;
24pub mod place;
25pub mod project;
26pub mod schema;
27pub mod time;
28
29use std::path::Path;
30
31pub use metadata::{FFCINE, FFCINE_VERSION, HasMetadata};
32pub use place::{Placed, PlacedMarker};
33pub use project::{
34    BusDuck, BusParams, BusReverb, BusSend, ClipGrade, GradeParams, MixParams, Output, OutputMode,
35    Project, SourceKind,
36};
37pub use schema::{
38    Clip, DEFAULT_MEDIA_KEY, Effect, ExternalReference, Gap, GeneratorReference, GenericEffect,
39    Item, LinearTimeWarp, Marker, MediaReference, MissingReference, Stack, Timeline, Track,
40    Transition, track_kind, transition_type,
41};
42pub use time::{RationalTime, TimeRange};
43
44use ffcine_core::{Error, Result};
45
46impl Timeline {
47    pub fn new(name: impl Into<String>) -> Self {
48        Self {
49            name: name.into(),
50            ..Self::default()
51        }
52    }
53
54    pub fn from_json(json: &str) -> Result<Self> {
55        Ok(serde_json::from_str(json)?)
56    }
57
58    /// Pretty-printed with 4-space indents, like OTIO.
59    pub fn to_json(&self) -> Result<String> {
60        let mut out = Vec::new();
61        let fmt = serde_json::ser::PrettyFormatter::with_indent(b"    ");
62        let mut ser = serde_json::Serializer::with_formatter(&mut out, fmt);
63        serde::Serialize::serialize(self, &mut ser)?;
64        out.push(b'\n');
65        Ok(String::from_utf8(out).unwrap_or_default())
66    }
67
68    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
69        let path = path.as_ref();
70        let json = std::fs::read_to_string(path).map_err(|source| Error::Io {
71            path: path.to_owned(),
72            source,
73        })?;
74        Self::from_json(&json)
75    }
76
77    pub fn to_file(&self, path: impl AsRef<Path>) -> Result<()> {
78        let path = path.as_ref();
79        std::fs::write(path, self.to_json()?).map_err(|source| Error::Io {
80            path: path.to_owned(),
81            source,
82        })
83    }
84
85    /// `metadata.ffcine.project`; `None` if absent.
86    pub fn project(&self) -> Option<Result<Project>> {
87        self.ffcine_get("project")
88    }
89
90    /// Sets `metadata.ffcine.project` and the namespace version `metadata.ffcine.v`.
91    pub fn set_project(&mut self, project: &Project) -> Result<()> {
92        self.ffcine_mut().insert("v".into(), FFCINE_VERSION.into());
93        self.ffcine_set("project", project)
94    }
95
96    /// The top-level tracks of `tracks`, bottom first.
97    pub fn track_list(&self) -> impl Iterator<Item = &Track> {
98        self.tracks.children.iter().filter_map(|c| match c {
99            Item::Track(t) => Some(t),
100            _ => None,
101        })
102    }
103
104    /// Every clip, depth first, in timeline order within each track.
105    pub fn clips(&self) -> Vec<&Clip> {
106        let mut out = Vec::new();
107        collect_clips(&self.tracks.children, &mut out);
108        out
109    }
110}
111
112fn collect_clips<'a>(items: &'a [Item], out: &mut Vec<&'a Clip>) {
113    for item in items {
114        match item {
115            Item::Clip(c) => out.push(c),
116            Item::Stack(s) => collect_clips(&s.children, out),
117            Item::Track(t) => collect_clips(&t.children, out),
118            Item::Gap(_) | Item::Transition(_) | Item::Unknown(_) => {}
119        }
120    }
121}
122
123impl Item {
124    pub fn name(&self) -> &str {
125        match self {
126            Item::Clip(x) => &x.name,
127            Item::Gap(x) => &x.name,
128            Item::Stack(x) => &x.name,
129            Item::Track(x) => &x.name,
130            Item::Transition(x) => &x.name,
131            Item::Unknown(m) => m
132                .get("name")
133                .and_then(serde_json::Value::as_str)
134                .unwrap_or(""),
135        }
136    }
137
138    pub fn source_range(&self) -> Option<&TimeRange> {
139        match self {
140            Item::Clip(x) => x.source_range.as_ref(),
141            Item::Gap(x) => x.source_range.as_ref(),
142            Item::Stack(x) => x.source_range.as_ref(),
143            Item::Track(x) => x.source_range.as_ref(),
144            Item::Transition(_) | Item::Unknown(_) => None,
145        }
146    }
147}