Merge pull request #192 from cirocueto2013-wq/fix/editing-workflow-stability
Stabilize project lifecycle, timeline, audio and export workflows
This commit is contained in:
commit
6d904c1b37
|
|
@ -423,11 +423,7 @@ fn can_replace_from_bin(s: &Session) -> std::result::Result<(), String> {
|
|||
|
||||
/// The media item behind `id` (subclips resolve to their parent) and the subclip range.
|
||||
fn media_root(p: &Project, id: ItemId) -> Option<(ItemId, &MediaClip, Option<TimeRange>)> {
|
||||
match &p.item(id)?.kind {
|
||||
ItemKind::Media(m) => Some((id, m, None)),
|
||||
ItemKind::Subclip { parent, range, .. } => media_root(p, *parent).map(|(i, m, _)| (i, m, Some(*range))),
|
||||
_ => None,
|
||||
}
|
||||
p.resolve_media(id)
|
||||
}
|
||||
|
||||
/// What the Source monitor shows for an item: the media-time span of its time ruler, its frame
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use std::sync::{Arc, Mutex, RwLock};
|
|||
use filmcraft_frame::{AudioBuffer, VideoFrame};
|
||||
use filmcraft_media::generators::GeneratorSource;
|
||||
use filmcraft_media::{FrameRequest, MediaError, MediaInfo, MediaSource, Opener, SharedSource};
|
||||
use filmcraft_project::{ItemId, ItemKind, MediaClip, MediaRef, Project};
|
||||
use filmcraft_project::{ItemId, MediaClip, MediaRef, Project};
|
||||
use filmcraft_render::offline::OfflineReason;
|
||||
|
||||
use crate::Services;
|
||||
|
|
@ -217,12 +217,8 @@ impl MediaPool {
|
|||
}
|
||||
|
||||
fn resolve(&self, p: &Project, item: ItemId, services: &dyn Services, proxies: bool) -> Option<SharedSource> {
|
||||
let (item, m, _) = p.resolve_media(item)?;
|
||||
let it = p.item(item)?;
|
||||
let m = match &it.kind {
|
||||
ItemKind::Media(m) => m,
|
||||
ItemKind::Subclip { parent, .. } => return self.resolve(p, *parent, services, proxies),
|
||||
_ => return None,
|
||||
};
|
||||
if proxies
|
||||
&& !m.offline
|
||||
&& let Some(MediaRef::File { path }) = &m.proxy
|
||||
|
|
@ -406,6 +402,7 @@ impl MediaSource for ProxySource {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use filmcraft_project::ItemKind;
|
||||
|
||||
/// A host whose whole-file read fails: opening must go through the reader.
|
||||
struct ReaderOnly(Arc<[u8]>);
|
||||
|
|
@ -421,6 +418,48 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cyclic_subclips_do_not_recurse_in_media_or_rendering() {
|
||||
let mut project = Project::new("damaged references");
|
||||
let a = project.add_item(
|
||||
"A",
|
||||
filmcraft_project::Label::Iris,
|
||||
ItemKind::Subclip {
|
||||
parent: ItemId(999),
|
||||
range: filmcraft_time::TimeRange::new(filmcraft_time::Tick::ZERO, filmcraft_time::Tick(1000)),
|
||||
restrict_trims: false,
|
||||
},
|
||||
None,
|
||||
);
|
||||
let b = project.add_item(
|
||||
"B",
|
||||
filmcraft_project::Label::Iris,
|
||||
ItemKind::Subclip {
|
||||
parent: a,
|
||||
range: filmcraft_time::TimeRange::new(filmcraft_time::Tick::ZERO, filmcraft_time::Tick(1000)),
|
||||
restrict_trims: false,
|
||||
},
|
||||
None,
|
||||
);
|
||||
let pool = MediaPool::default();
|
||||
for parent in [a, b] {
|
||||
let ItemKind::Subclip { parent: reference, .. } = &mut project.item_mut(a).unwrap().kind else { panic!() };
|
||||
*reference = parent;
|
||||
assert!(pool.source_for(&project, a, &crate::FsServices).is_none());
|
||||
assert!(pool.full_res_source(&project, a, &crate::FsServices).is_none());
|
||||
assert!(crate::media_duration(&project, &pool, a).is_none());
|
||||
assert!(filmcraft_render::source_size(&project, a).is_none());
|
||||
assert!(project.resolve_media(a).is_none());
|
||||
assert!(filmcraft_render::colorman::override_of(&project, a).is_none());
|
||||
assert!(filmcraft_render::colorman::source_peak_nits(&project, a).is_none());
|
||||
assert!(crate::sync::project_clip(&project, a).is_none());
|
||||
let mut session = crate::Session { project: Arc::new(project.clone()), ..Default::default() };
|
||||
session.state.project_selection = vec![a];
|
||||
assert!(!session.is_enabled("file.mediaProperties"));
|
||||
assert!(session.execute("transcript.generate", serde_json::json!({"items":[a.0]})).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_file_prefers_the_hosts_reader() {
|
||||
let wav = filmcraft_media::wav::write_wav16(&[0.0, 0.5, -0.5, 0.25], 2, 48_000);
|
||||
|
|
|
|||
|
|
@ -323,11 +323,7 @@ fn placed_item(p: &mut Project, item: ItemId, kind: TrackKind, exact: Tick, rang
|
|||
}
|
||||
|
||||
fn sync_source_size(p: &Project, item: ItemId) -> Option<(u32, u32)> {
|
||||
match &p.item(item)?.kind {
|
||||
ItemKind::Media(m) => m.info.video.as_ref().map(|v| (v.width, v.height)),
|
||||
ItemKind::Subclip { parent, .. } => sync_source_size(p, *parent),
|
||||
_ => None,
|
||||
}
|
||||
p.resolve_media(item).and_then(|(_, media, _)| media.info.video.as_ref().map(|video| (video.width, video.height)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Synchronize
|
||||
|
|
|
|||
|
|
@ -370,11 +370,7 @@ fn targets(s: &Session, p: &Value) -> Vec<ItemId> {
|
|||
|
||||
/// The media clip behind an item (subclips resolve to their parent).
|
||||
fn media_of(p: &Project, id: ItemId) -> Option<(ItemId, &filmcraft_project::MediaClip)> {
|
||||
match &p.item(id)?.kind {
|
||||
ItemKind::Media(m) => Some((id, m)),
|
||||
ItemKind::Subclip { parent, .. } => media_of(p, *parent),
|
||||
_ => None,
|
||||
}
|
||||
p.resolve_media(id).map(|(root, media, _)| (root, media))
|
||||
}
|
||||
|
||||
fn file_path(m: &filmcraft_project::MediaClip) -> Option<&str> {
|
||||
|
|
|
|||
|
|
@ -81,11 +81,7 @@ fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|||
/// The media item a clip shows (a subclip resolves to its parent: subclip clips use the parent's
|
||||
/// media time).
|
||||
fn media_root(p: &filmcraft_project::Project, item: ItemId) -> Option<ItemId> {
|
||||
match &p.item(item)?.kind {
|
||||
ItemKind::Media(_) => Some(item),
|
||||
ItemKind::Subclip { parent, .. } => media_root(p, *parent),
|
||||
_ => None,
|
||||
}
|
||||
p.resolve_media(item).map(|(root, _, _)| root)
|
||||
}
|
||||
|
||||
struct Work {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
//! `offset` (frames) shifts every clip but the reference by that much (a known delay to correct).
|
||||
|
||||
use filmcraft_audio_dsp::sync::{SyncOptions, SyncResult, find_offset, mixdown};
|
||||
use filmcraft_project::{ItemId, ItemKind, Project, TrackItem};
|
||||
use filmcraft_project::{ItemId, Project, TrackItem};
|
||||
use filmcraft_time::{FrameRate, TICKS_PER_SECOND, Tick, TimeRange};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
|
|
@ -76,12 +76,7 @@ pub struct SyncClip {
|
|||
|
||||
/// The media item behind an item (subclips resolve to their parent) and its subclip range.
|
||||
fn media_of(p: &Project, item: ItemId) -> Option<(ItemId, &filmcraft_project::MediaClip, Option<TimeRange>)> {
|
||||
let it = p.item(item)?;
|
||||
match &it.kind {
|
||||
ItemKind::Media(m) => Some((item, m, None)),
|
||||
ItemKind::Subclip { parent, range, .. } => media_of(p, *parent).map(|(i, m, _)| (i, m, Some(*range))),
|
||||
_ => None,
|
||||
}
|
||||
p.resolve_media(item)
|
||||
}
|
||||
|
||||
fn start_timecode(m: &filmcraft_project::MediaClip) -> Tick {
|
||||
|
|
|
|||
|
|
@ -77,11 +77,7 @@ fn can_download(_: &Session) -> std::result::Result<(), String> {
|
|||
|
||||
/// The media item behind a project item (subclips resolve to their parent).
|
||||
fn media_item(s: &Session, item: ItemId) -> Option<ItemId> {
|
||||
match &s.project.item(item)?.kind {
|
||||
ItemKind::Media(_) => Some(item),
|
||||
ItemKind::Subclip { parent, .. } => media_item(s, *parent),
|
||||
_ => None,
|
||||
}
|
||||
s.project.resolve_media(item).map(|(root, _, _)| root)
|
||||
}
|
||||
|
||||
fn ids_p(p: &Value, k: &str) -> Option<Vec<ItemId>> {
|
||||
|
|
|
|||
|
|
@ -1491,6 +1491,24 @@ fn resolve_auto_points_sized(e: &mut EffectInstance, frame: (u32, u32), source:
|
|||
}
|
||||
|
||||
impl Project {
|
||||
/// Resolve media through at most sixteen items, preserving the outermost subclip range.
|
||||
/// Missing parents, cycles and longer chains are unavailable media.
|
||||
pub fn resolve_media(&self, item: ItemId) -> Option<(ItemId, &MediaClip, Option<TimeRange>)> {
|
||||
let mut id = item;
|
||||
let mut range = None;
|
||||
for _ in 0..16 {
|
||||
match &self.item(id)?.kind {
|
||||
ItemKind::Media(media) => return Some((id, media, range)),
|
||||
ItemKind::Subclip { parent, range: span, .. } => {
|
||||
range.get_or_insert(*span);
|
||||
id = *parent;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Size in pixels of what an item shows: a media clip's picture, a sequence's frame, an
|
||||
/// adjustment layer or graphic; a subclip has the size of its parent. `None` without
|
||||
/// picture. This is the size the renderer centres a clip's "auto" anchor in.
|
||||
|
|
@ -1630,6 +1648,22 @@ mod tests {
|
|||
assert!(serde_json::from_value::<ParamValue>(serde_json::json!({ "Path": path })).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_resolution_bounds_depth_and_preserves_the_outer_range() {
|
||||
let (mut p, media, _) = demo_project();
|
||||
assert_eq!(p.resolve_media(media).map(|(id, _, range)| (id, range)), Some((media, None)));
|
||||
let mut parent = media;
|
||||
let mut last_range = TimeRange::default();
|
||||
for n in 0..15 {
|
||||
last_range = TimeRange::new(Tick(n * 1000), Tick(100));
|
||||
parent = p.add_item("Sub", Label::Iris, ItemKind::Subclip { parent, range: last_range, restrict_trims: false }, None);
|
||||
}
|
||||
assert_eq!(p.resolve_media(parent).map(|(id, _, range)| (id, range)), Some((media, Some(last_range))));
|
||||
let too_deep = p.add_item("Too deep", Label::Iris, ItemKind::Subclip { parent, range: last_range, restrict_trims: false }, None);
|
||||
assert!(p.resolve_media(too_deep).is_none());
|
||||
assert!(p.resolve_media(ItemId(u64::MAX)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placed_auto_points_resolve_once_the_source_size_is_final() {
|
||||
let (mut p, clip, seq) = demo_project();
|
||||
|
|
|
|||
|
|
@ -10,17 +10,13 @@ use std::sync::{Arc, Mutex, OnceLock};
|
|||
|
||||
use filmcraft_color::{ColorPipeline, ColorSpace, InputTransform, OutputTransform, Range};
|
||||
use filmcraft_frame::VideoFrame;
|
||||
use filmcraft_project::{ItemId, ItemKind, Project};
|
||||
use filmcraft_project::{ItemId, Project};
|
||||
|
||||
use crate::Image;
|
||||
|
||||
/// The Interpret Footage colour-space override of an item (following subclips to their media).
|
||||
pub fn override_of(project: &Project, item: ItemId) -> Option<ColorSpace> {
|
||||
match &project.item(item)?.kind {
|
||||
ItemKind::Media(m) => m.interpret.color_space,
|
||||
ItemKind::Subclip { parent, .. } => override_of(project, *parent),
|
||||
_ => None,
|
||||
}
|
||||
project.resolve_media(item).and_then(|(_, media, _)| media.interpret.color_space)
|
||||
}
|
||||
|
||||
/// The colour space a frame of `item` is interpreted in.
|
||||
|
|
@ -39,11 +35,7 @@ type Key = (ColorSpace, Range, ColorPipeline, Option<u32>);
|
|||
/// The content peak (cd/m²) of an item's HDR metadata (MaxCLL, else the mastering display peak),
|
||||
/// following subclips to their media. Used as the tone-mapping source peak of PQ media.
|
||||
pub fn source_peak_nits(project: &Project, item: ItemId) -> Option<f32> {
|
||||
match &project.item(item)?.kind {
|
||||
ItemKind::Media(m) => m.info.video.as_ref()?.hdr.as_ref()?.peak_nits(),
|
||||
ItemKind::Subclip { parent, .. } => source_peak_nits(project, *parent),
|
||||
_ => None,
|
||||
}
|
||||
project.resolve_media(item).and_then(|(_, media, _)| media.info.video.as_ref()?.hdr.as_ref()?.peak_nits())
|
||||
}
|
||||
|
||||
/// A cached input transform (`peak_nits`: the PQ content peak from the file's HDR metadata).
|
||||
|
|
@ -115,7 +107,7 @@ mod tests {
|
|||
fn mastering_metadata_sets_the_tone_mapping_peak() {
|
||||
use filmcraft_color::HdrMetadata;
|
||||
use filmcraft_media::MediaSource;
|
||||
use filmcraft_project::{Label, MediaClip, MediaRef};
|
||||
use filmcraft_project::{ItemKind, Label, MediaClip, MediaRef};
|
||||
let pq = ColorInfo { transfer: Transfer::Pq, primaries: Primaries::Bt2020, ..ColorInfo::SRGB_FULL };
|
||||
// a PQ grey of ≈ 2000 cd/m² (code 0.83) and one of ≈ 4000 cd/m² (code 0.90)
|
||||
let px = |nits: f32| (filmcraft_color::pq_inverse_eotf(nits / 10_000.0) * 255.0).round() as u8;
|
||||
|
|
|
|||
|
|
@ -329,12 +329,7 @@ pub(crate) fn opacity_blend(item: &TrackItem, mt: Tick) -> (f32, Blend) {
|
|||
|
||||
/// Size of an item's source at full resolution.
|
||||
pub fn source_size(project: &Project, item: ItemId) -> Option<(u32, u32)> {
|
||||
match &project.item(item)?.kind {
|
||||
ItemKind::Media(m) => m.info.video.as_ref().map(|v| (v.width, v.height)),
|
||||
ItemKind::Sequence(s) => Some((s.settings.width, s.settings.height)),
|
||||
ItemKind::AdjustmentLayer { width, height, .. } | ItemKind::Graphic { width, height, .. } => Some((*width, *height)),
|
||||
ItemKind::Subclip { parent, .. } => source_size(project, *parent),
|
||||
}
|
||||
project.source_size(item)
|
||||
}
|
||||
|
||||
/// The Motion transform of an item at media time `mt`, mapping full-res source pixels to
|
||||
|
|
|
|||
Loading…
Reference in New Issue