mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-timeline: FCPXML on the neutral model (#987)
* craft-timeline: a neutral timeline model and the import builder model: a Project of bins and items (media, sequences, subclips, and items no interchange format carries), sequences of tracks with clips, transitions, markers and effects with keyframes. Cut down from FilmCraft's project model to what Final Cut Pro 7 XML, FCPXML and OpenTimelineIO describe; EffectCraft already uses that shape for its interchange. Three things stay the application's: effects (looked up through a Catalog the caller supplies), generators beyond the common ones (Generator::Other with an opaque payload), and per-object extension JSON (ext) that a format can carry unread. fx: the effect and parameter ids the formats name themselves, and Standard, a catalog of just those. build: FilmCraft's import Builder and shared helpers (filmcraft-interchange common.rs at 4bec22bd) on the model, with the effect registry replaced by the catalog, and the bounds every import ends with (Imported::bound, from lib.rs). * craft-timeline: Final Cut Pro 7 XML (xmeml) on the neutral model fcp7: FilmCraft's xmeml reader and writer (filmcraft-interchange fcp7.rs at 4bec22bd) moved onto craft_timeline::model, behind feature xml. - XML is read with craft-xml, strictly: at most 256 levels of nesting (the limit EffectCraft checked with its own depth scan before handing documents over), 2 000 000 nodes, 256 MiB; a document type declaration with an internal subset and entities XML does not define are refused. It is written with craft-xml's Writer. - Effects other than motion, opacity, time remap, audio levels and pan are looked up through the caller's Catalog, by id and by name, and written with the catalog's stable name and category. - An application generator round-trips as <namespace>:<payload> in effectid (FilmCraft wrote filmcraft:<json>); transparent video, counting leader and bars and tone are written by name, which the reader already took. - Finding the sequence a clip item is in is one walk of the document instead of an ancestors() lookup per clip item. tests/it: FilmCraft's FCP7 integration tests (the hand-written Premiere-style document, the rich round trip, the 48-case property round trip, split mono and stereo channels, sub-frame spans, absurd timebases and start timecodes, the link-group bound) on the model, with a test catalog for the four application effects they use. * craft-timeline: FCPXML on the neutral model fcpxml: FilmCraft's FCPXML 1.9-1.11 reader and writer (filmcraft-interchange fcpxml.rs at 4bec22bd) moved onto craft_timeline::model, behind feature xml. XML is read with craft-xml as fcp7 is; filters other than the transform, blend and volume adjustments go through the caller's Catalog by name, and an effect resource's uid is <namespace>.<effect id> (FilmCraft wrote filmcraft.<id>). ExportOptions gains fcpxml_version. tests/it/fcpxml.rs: FilmCraft's FCPXML integration tests on the model (the hand-written connected-clips document, hostile lane numbers, standalone video stills, self-referencing compound media, round trips at 1.9, 1.10 and 1.11, the property round trip with its regression seed, percent-decoded relative URLs). The FCP7 tests export to FCPXML again where FilmCraft's did.
This commit is contained in:
1 parent
d197b1e72f
commit
d59bcf56c9
9 files changed
+11
-25
No files matched your search
Generated
-1
@@ -1859,7 +1859,6 @@ dependencies = [
|
||||
"craft-xml",
|
||||
"proptest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -15,18 +15,14 @@ workspace = true
|
||||
serde = ["dep:serde"]
|
||||
# Final Cut Pro 7 XML (`fcp7`), read with craft-xml's bounded parser.
|
||||
xml = ["dep:craft-xml"]
|
||||
# OpenTimelineIO JSON (`otio`), with serde_json.
|
||||
otio = ["dep:serde_json"]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, optional = true }
|
||||
craft-xml = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[[test]]
|
||||
name = "it"
|
||||
required-features = ["xml", "otio"]
|
||||
required-features = ["xml"]
|
||||
@@ -16,7 +16,6 @@ text formats that are documents of their own.
|
||||
| `edl` | CMX 3600 edit decision lists: `parse` to the list as written (events, their lines and timecode fields, `M2`, `AUD`, clip-name, source-file, effect-name and locator comments), `write` for events the caller built, channel codes, reel names, the frame-rate guess. |
|
||||
| `fcp7` (feature `xml`) | Final Cut Pro 7 XML (`xmeml` 4 and 5, the dialect Premiere Pro reads and writes) to and from `model`. Read with craft-xml, strictly and bounded. |
|
||||
| `fcpxml` (feature `xml`) | FCPXML 1.9–1.11 to and from `model`: the primary storyline as V1, connected clips by lane, `ref-clip` compounds as nested sequences, rational times exact, transforms, opacity, volume and their keyframes, retiming through `timeMap`. |
|
||||
| `otio` (feature `otio`) | OpenTimelineIO JSON to and from `model`: timelines, collections, stacks as nested sequences, gaps, transitions, markers, linear time warps and freeze frames. What OTIO has no schema for goes in `metadata.<namespace>`, and the model's `ext` is merged into it, so an application's own data round-trips. |
|
||||
| crate root | `Format` (EDL, xmeml, FCPXML, OTIO, AAF, OMF), `detect_text`, `decode_text`. |
|
||||
|
||||
No file I/O. EDL and ALE give the document as the format writes it. The formats that describe a
|
||||
@@ -24,7 +23,7 @@ whole edit read into and write from `model`; mapping `model` onto an application
|
||||
stays in the application.
|
||||
AAF and OMF are in `craft-aaf`.
|
||||
|
||||
Feature `xml`: the XML formats, on craft-xml. Feature `otio`: OpenTimelineIO, on serde_json. Feature `serde`: `Serialize` / `Deserialize` for the time, report and `Format` types.
|
||||
Feature `xml`: the XML formats, on craft-xml. Feature `serde`: `Serialize` / `Deserialize` for the time, report and `Format` types.
|
||||
|
||||
## Specifications
|
||||
|
||||
|
||||
@@ -1325,7 +1325,7 @@ mod tests {
|
||||
<asset-clip ref="a1" offset="3600s" duration="5s"><marker start="1s" duration="1/25s" value="m"/></asset-clip></spine></sequence></project></event></library></fcpxml>"#
|
||||
);
|
||||
let mut report = Report::default();
|
||||
let iopts = ImportOptions { base_dir: None, name: None, catalog: &crate::fx::Standard, namespace: "test" };
|
||||
let iopts = ImportOptions { base_dir: None, catalog: &crate::fx::Standard, namespace: "test" };
|
||||
let imported = import(&xml, &iopts, &mut report).expect("import succeeds");
|
||||
let xopts = ExportOptions {
|
||||
relative_to: None,
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
//! - [`edl`]: CMX 3600 edit decision lists.
|
||||
//! - [`fcp7`] (feature `xml`): Final Cut Pro 7 XML (`xmeml`).
|
||||
//! - [`fcpxml`] (feature `xml`): FCPXML 1.9–1.11.
|
||||
//! - [`otio`] (feature `otio`): OpenTimelineIO JSON.
|
||||
//! - [`Format`], [`detect_text`], [`decode_text`]: which interchange format a document is.
|
||||
//!
|
||||
//! No file I/O: documents are text or bytes, and media references are strings. Each format module
|
||||
@@ -31,8 +30,6 @@ pub mod fcp7;
|
||||
pub mod fcpxml;
|
||||
pub mod fx;
|
||||
pub mod model;
|
||||
#[cfg(feature = "otio")]
|
||||
pub mod otio;
|
||||
pub mod path;
|
||||
pub mod rate;
|
||||
pub mod report;
|
||||
@@ -119,7 +116,7 @@ pub enum Error {
|
||||
}
|
||||
|
||||
impl Error {
|
||||
#[cfg_attr(not(any(feature = "xml", feature = "otio")), allow(dead_code))]
|
||||
#[cfg_attr(not(feature = "xml"), allow(dead_code))]
|
||||
pub(crate) fn parse(format: Format, message: impl Into<String>) -> Error {
|
||||
Error::Parse { format: format.name(), message: message.into() }
|
||||
}
|
||||
@@ -145,8 +142,6 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub struct ImportOptions<'a> {
|
||||
/// Directory relative media paths are resolved against (normally the document's directory).
|
||||
pub base_dir: Option<&'a str>,
|
||||
/// Name for a timeline the document leaves unnamed.
|
||||
pub name: Option<&'a str>,
|
||||
/// The application's effects and generators (see [`model::Catalog`]; [`fx::Standard`] knows
|
||||
/// only what the formats name themselves).
|
||||
pub catalog: &'a dyn model::Catalog,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
//! supplies.
|
||||
//! - **Generators** other than the ones every editor has: [`Generator::Other`] carries a name and
|
||||
//! an opaque payload.
|
||||
//! - **Extensions.** `ext` on items (a sequence's is its item's), tracks, clips, transitions and markers is JSON
|
||||
//! - **Extensions.** `ext` on items, sequences, tracks, clips, transitions and markers is JSON
|
||||
//! text the application owns. A format that can carry application metadata (OpenTimelineIO)
|
||||
//! writes it under the application's namespace and gives it back on reading; the others ignore it.
|
||||
|
||||
@@ -540,11 +540,12 @@ pub struct Sequence {
|
||||
pub markers: Vec<Marker>,
|
||||
/// Timecode of the first frame, in frames.
|
||||
pub start_timecode: i64,
|
||||
pub ext: Ext,
|
||||
}
|
||||
|
||||
impl Sequence {
|
||||
pub fn new(settings: SequenceSettings) -> Self {
|
||||
Self { settings, video_tracks: Vec::new(), audio_tracks: Vec::new(), markers: Vec::new(), start_timecode: 0 }
|
||||
Self { settings, video_tracks: Vec::new(), audio_tracks: Vec::new(), markers: Vec::new(), start_timecode: 0, ext: None }
|
||||
}
|
||||
/// The end of the last clip.
|
||||
pub fn duration(&self) -> Tick {
|
||||
|
||||
@@ -723,7 +723,7 @@ fn absurd_timebase_is_bounded() {
|
||||
);
|
||||
let (imported, _) = fcp7_import(&doc, None);
|
||||
for seq in &imported.sequences {
|
||||
for format in [Format::Fcp7Xml, Format::Fcpxml, Format::Otio] {
|
||||
for format in [Format::Fcp7Xml, Format::Fcpxml] {
|
||||
export(&imported.project, *seq, format, &xopts()).unwrap_or_else(|e| panic!("{format:?}: {e}"));
|
||||
}
|
||||
}
|
||||
@@ -742,7 +742,7 @@ fn absurd_start_timecode_is_bounded() {
|
||||
</clipitem></track></video></media></sequence></xmeml>"#;
|
||||
let (imported, report) = fcp7_import(doc, None);
|
||||
for seq in &imported.sequences {
|
||||
for format in [Format::Fcp7Xml, Format::Fcpxml, Format::Otio] {
|
||||
for format in [Format::Fcp7Xml, Format::Fcpxml] {
|
||||
export(&imported.project, *seq, format, &xopts()).unwrap_or_else(|e| panic!("{format:?}: {e}"));
|
||||
}
|
||||
assert_eq!(imported.project.sequence(*seq).map(|s| s.start_timecode), Some(0));
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
mod fcp7;
|
||||
mod fcpxml;
|
||||
mod otio;
|
||||
mod support;
|
||||
@@ -11,7 +11,7 @@ use craft_timeline::model::{
|
||||
TransitionId, VideoStreamInfo,
|
||||
};
|
||||
use craft_timeline::time::{FrameRate, TICKS_PER_SECOND, Tick, TimeRange};
|
||||
use craft_timeline::{Error, ExportOptions, Format, ImportOptions, Imported, Report, fcp7, fcpxml, otio};
|
||||
use craft_timeline::{Error, ExportOptions, Format, ImportOptions, Imported, Report, fcp7, fcpxml};
|
||||
|
||||
/// [`Standard`] and a few effects an application adds: a blur, a grade, two transitions.
|
||||
pub struct TestCatalog;
|
||||
@@ -31,7 +31,6 @@ fn extra() -> Vec<EffectDef> {
|
||||
def("lumetri", "Lumetri Color", EffectKind::Video, "Color Correction", vec![float("exposure", "Exposure", 0.0)]),
|
||||
def("dip_to_white", "Dip to White", EffectKind::VideoTransition, "Dissolve", vec![]),
|
||||
def("push", "Push", EffectKind::VideoTransition, "Slide", vec![]),
|
||||
def("iris_round", "Iris Round", EffectKind::VideoTransition, "Iris", vec![]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ impl Catalog for TestCatalog {
|
||||
pub const NS: &str = "testapp";
|
||||
|
||||
pub fn iopts(base_dir: Option<&str>) -> ImportOptions<'_> {
|
||||
ImportOptions { base_dir, name: None, catalog: &TestCatalog, namespace: NS }
|
||||
ImportOptions { base_dir, catalog: &TestCatalog, namespace: NS }
|
||||
}
|
||||
|
||||
pub fn xopts() -> ExportOptions<'static> {
|
||||
@@ -308,7 +307,6 @@ pub fn import(bytes: &[u8], format: Format, base_dir: Option<&str>) -> Result<(I
|
||||
let imported = match format {
|
||||
Format::Fcp7Xml => fcp7::import(&text, &opts, &mut report),
|
||||
Format::Fcpxml => fcpxml::import(&text, &opts, &mut report),
|
||||
Format::Otio => otio::import(&text, &opts, &mut report),
|
||||
_ => Err(Error::Other(format!("{} is not read here", format.name()))),
|
||||
}?;
|
||||
Ok((imported, report))
|
||||
@@ -320,7 +318,6 @@ pub fn export(p: &Project, seq: ItemId, format: Format, opts: &ExportOptions) ->
|
||||
let text = match format {
|
||||
Format::Fcp7Xml => fcp7::export(p, seq, opts, &mut report),
|
||||
Format::Fcpxml => fcpxml::export(p, seq, opts, &mut report),
|
||||
Format::Otio => otio::export(p, seq, opts, &mut report),
|
||||
_ => Err(Error::Other(format!("{} is not written here", format.name()))),
|
||||
}?;
|
||||
Ok((text.into_bytes(), report))
|
||||
|
||||
Reference in new issue
Block a user