collective

Browse / github.com/storytold/filmcraft / patch

Infer OTIO sequence and media dimensions from video references

OTIO import creates the sequence at 1920x1080 regardless of source resolution **Version / platform:** FilmCraft 0.12.0, linux-x86_64, headless CLI (`filmcraft-cli`). **Problem:** Importing an OTIO cut list whose clips come from a 1080x1920 (portrait) h264 source creates the sequence at 1920x1080. The source resolution is not used for the new sequence. **Repro:** ``` filmcraft-cli exec file.import '{"paths":["cut.otio"]}' filmcraft-cli exec sequence.settings '{}' # reports width 1920, height 1080 ``` (`cut.otio` references clips from a single 1080x1920 h264 file.) **Expected:** The imported sequence matches the source clips' resolution (or the OTIO metadata), or t

Status
open
Score
0.00
Adopters
0 other operators
Author
lisn0 (imported from pull request #3278, open)
Base
a504f3c5adc4cd60d9c1120916cddb50c9b24b86
Size
+458 −16 in 2 files
Review
pending
Shared
2026-10-28
Id
3735234313733a8fb9f01fc5

How it went

No other operator has reported using it yet.

Diff

raw · crates/interchange/src/otio.rs crates/interchange/tests/it/otio.rs

diff --git a/crates/interchange/src/otio.rs b/crates/interchange/src/otio.rs--- a/crates/interchange/src/otio.rs+++ b/crates/interchange/src/otio.rs@@ -11,7 +11,7 @@ //! state, sequence settings, media info, generators) round-trip through `metadata.filmcraft`. //! Nested sequences are `Stack` items inside tracks. -use std::collections::HashMap;+use std::collections::{HashMap, HashSet};  use filmcraft_media::{Generator, MediaInfo}; use filmcraft_project::{@@ -22,8 +22,8 @@ use filmcraft_time::{FrameRate, TICKS_PER_SECOND, Tick}; use serde_json::{Value, json};  use crate::common::{-    Builder, MediaSpec, base_item, empty_sequence, file_name, generator_of, item_media, path_to_file_url, rate_from_f64, relative_path, resolve_path,-    settings_for, transition_effect, transition_name,+    Builder, MediaSpec, base_item, empty_sequence, file_name, generator_of, item_media, media_info, path_to_file_url, rate_from_f64, relative_path,+    resolve_path, settings_for, transition_effect, transition_name, }; use crate::{Error, ExportOptions, Format, ImportOptions, Imported, Report, Result}; @@ -369,8 +369,10 @@ struct Imp<'a, 'r> {     report: &'r mut Report,     links: HashMap<u64, u64>,     groups: HashMap<u64, u64>,+    explicit_dimensions: HashSet<ItemId>, } +/// Imports OTIO timelines into one project while preserving supported source metadata. pub(crate) fn import(text: &str, opts: &ImportOptions, report: &mut Report) -> Result<Imported> {     let v: Value = serde_json::from_str(text).map_err(|e| Error::parse(Format::Otio, e.to_string()))?;     let mut timelines = Vec::new();@@ -378,7 +380,7 @@ pub(crate) fn import(text: &str, opts: &ImportOptions, report: &mut Report) -> R     if timelines.is_empty() {         return Err(Error::parse(Format::Otio, format!("no Timeline in a {} document", schema(&v))));     }-    let mut imp = Imp { b: Builder::new(&name_of(&v)), opts, report, links: HashMap::new(), groups: HashMap::new() };+    let mut imp = Imp { b: Builder::new(&name_of(&v)), opts, report, links: HashMap::new(), groups: HashMap::new(), explicit_dimensions: HashSet::new() };     if imp.b.p.name.is_empty() {         imp.b.p.name = "Imported OTIO".into();     }@@ -389,6 +391,7 @@ pub(crate) fn import(text: &str, opts: &ImportOptions, report: &mut Report) -> R     Ok(imp.b.finish()) } +/// Collects importable timelines, stacks and standalone tracks from an OTIO document. fn collect_timelines<'v>(v: &'v Value, out: &mut Vec<&'v Value>) {     match schema_name(v) {         "Timeline" | "Stack" => out.push(v),@@ -402,6 +405,7 @@ fn collect_timelines<'v>(v: &'v Value, out: &mut Vec<&'v Value>) {     } } +/// Finds the first source-range frame rate while walking a timeline's children. fn first_rate(v: &Value) -> Option<FrameRate> {     if let Some(sr) = v.get("source_range").filter(|s| !s.is_null()) {         return sr.get("duration").and_then(read_rate);@@ -409,6 +413,99 @@ fn first_rate(v: &Value) -> Option<FrameRate> {     v.get("children").and_then(Value::as_array).into_iter().flatten().find_map(first_rate) } +/// Converts a positive integral JSON number within the u32 dimension range.+fn frame_dimension(value: &Value) -> Option<u32> {+    if let Some(integer) = value.as_u64() {+        let dimension = u32::try_from(integer).ok()?;+        return (dimension > 0).then_some(dimension);+    }+    frame_dimension_number(value.as_f64()?)+}++/// Converts a finite, positive integral float within the frame-size integer range.+fn frame_dimension_number(value: f64) -> Option<u32> {+    if !value.is_finite() || value.fract() != 0.0 || value <= 0.0 || value > f64::from(u32::MAX) {+        return None;+    }+    Some(value as u32)+}++/// Reads and validates dimensions from media metadata or an OTIO image bounds box.+fn image_dimensions(v: &Value) -> Option<(u32, u32)> {+    let (width, height) = if let (Some(width), Some(height)) = (v.get("width"), v.get("height")) {+        (frame_dimension(width)?, frame_dimension(height)?)+    } else {+        let min = v.get("min")?;+        let max = v.get("max")?;+        let width = max.get("x")?.as_f64()? - min.get("x")?.as_f64()?;+        let height = max.get("y")?.as_f64()? - min.get("y")?.as_f64()?;+        (frame_dimension_number(width)?, frame_dimension_number(height)?)+    };+    filmcraft_project::validate_frame_size(width, height).ok()?;+    Some((width, height))+}++/// Prefers FilmCraft media info, then OTIO metadata, then available image bounds.+fn reference_dimensions(reference: &Value) -> Option<(u32, u32)> {+    let metadata = reference.get("metadata");+    let filmcraft = metadata.and_then(|m| m.get("filmcraft"));+    filmcraft+        .and_then(|m| m.get("info"))+        .and_then(|i| i.get("video"))+        .and_then(image_dimensions)+        .or_else(|| metadata.and_then(image_dimensions))+        .or_else(|| reference.get("available_image_bounds").and_then(image_dimensions))+}++/// Selects the active OTIO media reference, falling back to the first one if needed.+fn active_media_reference(clip: &Value) -> Option<&Value> {+    let refs = clip.get("media_references");+    let key = clip.get("active_media_reference_key").and_then(Value::as_str).unwrap_or("DEFAULT_MEDIA");+    match refs {+        Some(refs) => refs.get(key).or_else(|| refs.as_object().and_then(|o| o.values().next())),+        None => clip.get("media_reference"),+    }+}++/// Returns dimensions only for a clip import() will retain (positive source duration).+fn media_reference_dimensions(clip: &Value) -> Option<(u32, u32)> {+    let reference = active_media_reference(clip)?;+    let available = reference.get("available_range").and_then(read_tr);+    let (_, duration) = clip.get("source_range").and_then(read_tr).or(available)?;+    if duration <= Tick::ZERO {+        return None;+    }+    reference_dimensions(reference)+}++/// Finds dimensions from the first importable video clip, respecting nested stacks.+fn first_video_dimensions_at(stack: &Value, depth: usize) -> Option<(u32, u32)> {+    if depth >= 64 {+        return None;+    }+    for track in stack.get("children").and_then(Value::as_array).into_iter().flatten() {+        if schema_name(track) != "Track" || track.get("kind").and_then(Value::as_str) == Some("Audio") {+            continue;+        }+        for child in track.get("children").and_then(Value::as_array).into_iter().flatten() {+            let dimensions = match schema_name(child) {+                "Clip" => media_reference_dimensions(child),+                "Stack" => child.get("source_range").and_then(read_tr).and_then(|_| first_video_dimensions_at(child, depth + 1)),+                _ => None,+            };+            if dimensions.is_some() {+                return dimensions;+            }+        }+    }+    None+}++/// Returns the first usable video clip dimensions in a stack.+fn first_video_dimensions(stack: &Value) -> Option<(u32, u32)> {+    first_video_dimensions_at(stack, 0)+}+ impl Imp<'_, '_> {     fn timeline(&mut self, tl: &Value) -> ItemId {         let name = name_of(tl);@@ -433,9 +530,12 @@ impl Imp<'_, '_> {         id     } +    /// Imports a sequence, preferring explicit settings over inferred video dimensions.     fn sequence(&mut self, stack: &Value, name: &str, meta: Option<&Value>, rate: FrameRate) -> ItemId {-        let settings: SequenceSettings =-            meta.and_then(|m| m.get("settings")).and_then(|s| serde_json::from_value(s.clone()).ok()).unwrap_or_else(|| settings_for(rate, 1920, 1080, false));+        let settings: SequenceSettings = meta.and_then(|m| m.get("settings")).and_then(|s| serde_json::from_value(s.clone()).ok()).unwrap_or_else(|| {+            let (width, height) = first_video_dimensions(stack).unwrap_or((1920, 1080));+            settings_for(rate, width, height, false)+        });         let id = self.b.reserve_sequence(name, settings.clone(), None);         if let Some(l) = meta.and_then(|m| m.get("label")).and_then(|l| serde_json::from_value::<Label>(l.clone()).ok())             && let Some(it) = self.b.p.item_mut(id)@@ -618,16 +718,9 @@ impl Imp<'_, '_> {         }     } +    /// Imports a positive-duration clip using the same reference selection as dimension discovery.     fn clip(&mut self, c: &Value, kind: TrackKind, pos: Tick) -> Option<TrackItem> {-        let mref = match c.get("media_references") {-            Some(refs) => {-                let key = c.get("active_media_reference_key").and_then(Value::as_str).unwrap_or("DEFAULT_MEDIA");-                refs.get(key).or_else(|| refs.as_object().and_then(|o| o.values().next()))-            }-            None => c.get("media_reference"),-        }-        .cloned()-        .unwrap_or(Value::Null);+        let mref = active_media_reference(c).cloned().unwrap_or(Value::Null);         let name = name_of(c);         let avail = mref.get("available_range").and_then(read_tr);         let (src_in, dur) = c.get("source_range").and_then(read_tr).or(avail)?;@@ -674,6 +767,7 @@ impl Imp<'_, '_> {         Some(ti)     } +    /// Registers a referenced media item with dimensions inferred from its reference metadata.     fn media(&mut self, r: &Value, clip_name: &str, kind: TrackKind, avail: Option<(Tick, Tick)>) -> Option<ItemId> {         let meta = fc(r);         let info: Option<MediaInfo> = meta.and_then(|m| m.get("info")).and_then(|i| serde_json::from_value(i.clone()).ok());@@ -681,9 +775,13 @@ impl Imp<'_, '_> {             let n = name_of(r);             if n.is_empty() { clip_name.to_string() } else { n }         };+        let dimensions = reference_dimensions(r);         let spec = MediaSpec {             duration: avail.map(|a| a.1),-            video: (kind == TrackKind::Video).then(|| (1920, 1080, FrameRate::default())),+            video: (kind == TrackKind::Video).then(|| {+                let (width, height) = dimensions.unwrap_or((1920, 1080));+                (width, height, FrameRate::default())+            }),             audio: (kind == TrackKind::Audio).then_some((48_000, 2)),             start_tc: None,             kind: None,@@ -745,12 +843,33 @@ impl Imp<'_, '_> {                 id             }         };+        if kind == TrackKind::Video+            && let Some((width, height)) = dimensions+            && !self.explicit_dimensions.contains(&id)+            && let Some(media) = self.b.p.item_mut(id).and_then(|item| item.as_media_mut())+        {+            if let Some(video) = media.info.video.as_mut() {+                video.width = width;+                video.height = height;+            } else {+                media.info.video = media_info(&media.info.name, &spec).video;+            }+            if media.info.video.is_some() {+                if media.info.kind == filmcraft_media::MediaKind::AudioOnly {+                    media.info.kind = filmcraft_media::MediaKind::Movie;+                }+                self.explicit_dimensions.insert(id);+            }+        }         Some(id)     } +    /// Applies FilmCraft media info and availability metadata to an imported media item.     fn fill_media(&mut self, id: ItemId, info: Option<MediaInfo>, meta: Option<&Value>, kind: TrackKind) {         let label = meta.and_then(|m| m.get("label")).and_then(|l| serde_json::from_value::<Label>(l.clone()).ok());         let offline = meta.and_then(|m| m.get("offline")).and_then(Value::as_bool);+        let explicit_video_dimensions =+            info.as_ref().and_then(|i| i.video.as_ref()).is_some_and(|v| filmcraft_project::validate_frame_size(v.width, v.height).is_ok());         let Some(it) = self.b.p.item_mut(id) else { return };         if let Some(l) = label {             it.label = l;@@ -767,6 +886,9 @@ impl Imp<'_, '_> {                 m.offline = o;             }         }+        if explicit_video_dimensions {+            self.explicit_dimensions.insert(id);+        }     }      fn markers(&mut self, v: &Value) -> Vec<Marker> {diff --git a/crates/interchange/tests/it/otio.rs b/crates/interchange/tests/it/otio.rs--- a/crates/interchange/tests/it/otio.rs+++ b/crates/interchange/tests/it/otio.rs@@ -252,6 +252,326 @@ fn lossless_roundtrip_with_filmcraft_metadata() {     assert_eq!(structure(&imp2.project, imp2.sequences[0], TrackKind::Video), structure(&p, s, TrackKind::Video)); } +/// Explicit sequence metadata is absent; the first video's FilmCraft media info supplies the size.+#[test]+fn sequence_dimensions_default_from_first_video_clip_media_info() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"portrait",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mp4",+                    "available_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":100}},+                    "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1080, 1920));+    let media_id = sequence.video_tracks[0].items[0].item;+    let media = imp.project.item(media_id).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1080, 1920)));+}++/// Image bounds supply portrait dimensions without any FilmCraft metadata.+#[test]+fn sequence_dimensions_default_from_first_video_clip_image_bounds() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"portrait",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mp4",+                    "available_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":100}},+                    "available_image_bounds":{"OTIO_SCHEMA":"Box2d.1","min":{"x":0,"y":0},"max":{"x":1080,"y":1920}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1080, 1920));+    let media_id = sequence.video_tracks[0].items[0].item;+    let media = imp.project.item(media_id).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1080, 1920)));+}++/// Integral JSON floats preserve portrait dimensions for both the sequence and imported media.+#[test]+fn sequence_dimensions_accept_integral_float_media_metadata() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"portrait",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mp4",+                    "available_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":100}},+                    "metadata":{"width":1080.0,"height":1920.0}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1080, 1920));+    let media_id = sequence.video_tracks[0].items[0].item;+    let media = imp.project.item(media_id).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1080, 1920)));+}++/// Invalid numeric dimensions retain the HD fallback rather than admitting unsafe frame sizes.+#[test]+fn sequence_dimensions_reject_invalid_metadata_widths() {+    let template = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"portrait",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mp4",+                    "available_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":100}},+                    "metadata":{"width":WIDTH,"height":1920}}}+            ]}+        ]}+    }"#;++    for width in ["1080.5", "-1", "0", "4294967296"] {+        let document = template.replace("\"width\":WIDTH", &format!("\"width\":{width}"));+        let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+        let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+        assert_eq!((sequence.settings.width, sequence.settings.height), (1920, 1080), "width={width}");+    }+}++/// A present but empty media_references map does not fall back to the legacy field.+#[test]+fn empty_media_references_do_not_use_legacy_reference_dimensions() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"clip",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_references":{},+                 "active_media_reference_key":"DEFAULT_MEDIA",+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"legacy.mp4",+                    "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1920, 1080));+}++/// Updates deduplicated default media dimensions when a later reference supplies them.+#[test]+fn later_same_path_metadata_updates_default_media_dimensions() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"first",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"shared.mp4",+                    "metadata":{}}},+                {"OTIO_SCHEMA":"Clip.2","name":"second",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"shared.mp4",+                    "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1080, 1920));+    let media = imp.project.item(sequence.video_tracks[0].items[0].item).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1080, 1920)));+}++/// Adds a later video stream when an audio reference created the shared media item first.+#[test]+fn later_video_reference_adds_dimensions_to_audio_only_shared_media() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Audio","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"audio first",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"shared.mp4",+                    "metadata":{}}}+            ]},+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"video later",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"shared.mp4",+                    "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    let audio_item = sequence.audio_tracks[0].items[0].item;+    let video_item = sequence.video_tracks[0].items[0].item;+    assert_eq!(audio_item, video_item);+    let media = imp.project.item(audio_item).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1080, 1920)));+    assert_eq!(media.info.kind, filmcraft_media::MediaKind::Movie);+    assert_eq!(media.info.audio_streams.len(), 1);+}++/// Keeps the first explicit dimensions when later references to the same file disagree.+#[test]+fn first_explicit_same_path_dimensions_are_preserved() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"first",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"shared.mp4",+                    "metadata":{"filmcraft":{"info":{"video":{"width":1920,"height":1080}}}}}},+                {"OTIO_SCHEMA":"Clip.2","name":"second",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"shared.mp4",+                    "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1920, 1080));+    let media = imp.project.item(sequence.video_tracks[0].items[0].item).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1920, 1080)));+}++/// Preserves complete first-reference media info when it includes video on an audio track.+#[test]+fn first_audio_reference_complete_media_info_keeps_explicit_dimensions() {+    let info = serde_json::to_value(media_info("shared.mp4", true, true, FrameRate::FPS_24, 60)).unwrap();+    let document = serde_json::json!({+        "OTIO_SCHEMA": "Timeline.1",+        "tracks": {+            "OTIO_SCHEMA": "Stack.1",+            "children": [+                {+                    "OTIO_SCHEMA": "Track.1",+                    "kind": "Audio",+                    "children": [{+                        "OTIO_SCHEMA": "Clip.2",+                        "name": "audio first",+                        "source_range": {+                            "OTIO_SCHEMA": "TimeRange.1",+                            "start_time": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24, "value": 0},+                            "duration": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24, "value": 24}+                        },+                        "media_reference": {+                            "OTIO_SCHEMA": "ExternalReference.1",+                            "target_url": "shared.mp4",+                            "metadata": {"filmcraft": {"info": info}}+                        }+                    }]+                },+                {+                    "OTIO_SCHEMA": "Track.1",+                    "kind": "Video",+                    "children": [{+                        "OTIO_SCHEMA": "Clip.2",+                        "name": "video later",+                        "source_range": {+                            "OTIO_SCHEMA": "TimeRange.1",+                            "start_time": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24, "value": 0},+                            "duration": {"OTIO_SCHEMA": "RationalTime.1", "rate": 24, "value": 24}+                        },+                        "media_reference": {+                            "OTIO_SCHEMA": "ExternalReference.1",+                            "target_url": "shared.mp4",+                            "metadata": {"filmcraft": {"info": {"video": {"width": 1080, "height": 1920}}}}+                        }+                    }]+                }+            ]+        }+    });+    let bytes = serde_json::to_vec(&document).unwrap();+    let (imp, _) = import(&bytes, Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    let media = imp.project.item(sequence.video_tracks[0].items[0].item).unwrap().as_media().unwrap();+    assert_eq!(media.info.video.as_ref().map(|v| (v.width, v.height)), Some((1920, 1080)));+    assert_eq!(media.info.audio_streams.len(), 1);+}++/// Selects dimensions only from nested stacks that the importer can actually include.+#[test]+fn sequence_dimensions_skip_stacks_without_readable_source_ranges() {+    let template = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Stack.1","name":"unreadable nested",NESTED_RANGE "children":[+                    {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                        {"OTIO_SCHEMA":"Clip.2","name":"nested portrait",+                         "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                         "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mp4",+                            "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+                    ]}+                ]},+                {"OTIO_SCHEMA":"Clip.2","name":"direct landscape",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"landscape.mp4",+                    "metadata":{"filmcraft":{"info":{"video":{"width":1920,"height":1080}}}}}}+            ]}+        ]}+    }"#;++    for source_range in ["", r#""source_range":null,"#, r#""source_range":{},"#] {+        let document = template.replace("NESTED_RANGE", source_range);+        let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+        let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+        assert_eq!((sequence.settings.width, sequence.settings.height), (1920, 1080));+        assert_eq!(sequence.video_tracks[0].items.len(), 1);+    }++    let nested_range = r#""source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},"#;+    let document = template.replace("NESTED_RANGE", nested_range);+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1080, 1920));+    assert_eq!(sequence.video_tracks[0].items.len(), 2);+    let nested = sequence.video_tracks[0].items[0].item;+    let nested = imp.project.sequence(nested).unwrap();+    assert_eq!((nested.settings.width, nested.settings.height), (1080, 1920));+    assert_eq!(nested.video_tracks[0].items.len(), 1);+}++/// Empty clips are skipped when choosing the sequence's video dimensions.+#[test]+fn sequence_dimensions_skip_zero_duration_video_clips() {+    let document = r#"{+        "OTIO_SCHEMA":"Timeline.1",+        "tracks":{"OTIO_SCHEMA":"Stack.1","children":[+            {"OTIO_SCHEMA":"Track.1","kind":"Video","children":[+                {"OTIO_SCHEMA":"Clip.2","name":"empty landscape",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"empty.mp4",+                    "available_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":100}},+                    "metadata":{"filmcraft":{"info":{"video":{"width":1920,"height":1080}}}}}},+                {"OTIO_SCHEMA":"Clip.2","name":"portrait",+                 "source_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":24}},+                 "media_reference":{"OTIO_SCHEMA":"ExternalReference.1","target_url":"portrait.mp4",+                    "available_range":{"OTIO_SCHEMA":"TimeRange.1","start_time":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":0},"duration":{"OTIO_SCHEMA":"RationalTime.1","rate":24,"value":100}},+                    "metadata":{"filmcraft":{"info":{"video":{"width":1080,"height":1920}}}}}}+            ]}+        ]}+    }"#;+    let (imp, _) = import(document.as_bytes(), Format::Otio, None).unwrap();+    let sequence = imp.project.sequence(imp.sequences[0]).unwrap();+    assert_eq!((sequence.settings.width, sequence.settings.height), (1080, 1920));+    assert_eq!(sequence.video_tracks[0].items.len(), 1);+}+ #[test] fn collections_and_bare_tracks() {     let one = r#"{"OTIO_SCHEMA": "SerializableCollection.1", "name": "Bundle", "children": [