Skip to main content

ff_preview/scene/
types.rs

1//! Model-agnostic description of a timeline for the real-time preview runner.
2//!
3//! A [`Scene`] is the primitive seam between an editing engine and
4//! [`SceneRunner`](super::runner::SceneRunner): it carries only the
5//! model's primitivised fields (paths, durations, opacity, blend, animation
6//! tracks), never the editing model itself. Resolving a `Scene` against the
7//! media — probing for duration, audio presence, and frame size — happens in
8//! [`ScenePlayer::open`](super::ScenePlayer::open), so a `Scene` re-derived on
9//! every edit needs no re-probe and behaviour is preserved. An engine derives a
10//! `Scene` from its own editing model; `Scene` is the only input the runner accepts.
11
12use std::path::{Path, PathBuf};
13use std::time::Duration;
14
15use ff_filter::{AnimatedValue, RealtimeLayerDescriptor, XfadeTransition};
16use ff_format::{Color, TextSpec};
17
18// SceneSource
19
20/// The video source backing a [`ScenePlacement`]: a decoded media file, or a
21/// generated (solid-colour / text) source rendered without a file — the preview
22/// counterpart of the engine's `ClipSource`. Generated sources are produced by
23/// `ff-filter`'s `SolidSource` / `TextSource` (the same `color` / `drawtext`
24/// filters the export path uses), so preview matches export.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum SceneSource {
27    /// A decoded media file at this path.
28    File(PathBuf),
29    /// A solid-colour fill for the whole canvas.
30    Solid(Color),
31    /// A text/title overlay rendered by `drawtext`.
32    Text(TextSpec),
33}
34
35impl SceneSource {
36    /// The file path when this is a [`File`](Self::File) source, else `None`
37    /// (a generated source has no path). Used by the file-only open path.
38    #[must_use]
39    pub fn as_file(&self) -> Option<&Path> {
40        match self {
41            Self::File(path) => Some(path.as_path()),
42            Self::Solid(_) | Self::Text(_) => None,
43        }
44    }
45}
46
47// Scene
48
49/// A whole timeline's worth of playback work, described without the editing model.
50#[derive(Debug, Clone)]
51pub struct Scene {
52    /// Presentation frame rate. Clamped to at least `1.0` by the runner.
53    pub fps: f64,
54    /// Explicit output canvas `(width, height)`, or `None` to size from the base track.
55    pub canvas: Option<(u32, u32)>,
56    /// Optional timeline-global `lavfi` filtergraph string (e.g.
57    /// `color=s=1920x1080:c=black@0.0,drawtext=text='Title'`) generated and composited
58    /// as the **topmost** video layer, matching the export path. `None` = no overlay.
59    pub lavfi_overlay: Option<String>,
60    /// Video tracks, composited bottom-up: index `0` is the V1 base, `1..` are overlays.
61    pub video_tracks: Vec<SceneVideoTrack>,
62    /// Dedicated audio-only tracks (A1, A2, …).
63    pub audio_tracks: Vec<SceneAudioTrack>,
64}
65
66// SceneVideoTrack
67
68/// One video track: an ordered list of clip placements along the timeline. The
69/// track's index in [`Scene::video_tracks`] is its compositing order (`0` = base).
70#[derive(Debug, Clone)]
71pub struct SceneVideoTrack {
72    /// Placements in timeline order.
73    pub placements: Vec<ScenePlacement>,
74}
75
76// ScenePlacement
77
78/// One video clip placed on the timeline.
79///
80/// Fields are pre-resolved model projections (e.g. `in_point` defaulted, `speed`
81/// clamped, `xfade_dur` computed); media-dependent resolution (the effective
82/// clip duration when `out_point` is `None`) is done by the runner at open time.
83#[derive(Debug, Clone)]
84pub struct ScenePlacement {
85    /// Video source: a media file, or a generated (solid/text) source.
86    pub source: SceneSource,
87    /// Global timeline position where this placement starts.
88    pub offset: Duration,
89    /// Source-file PTS at which playback starts (`Clip::in_point`, defaulted to zero).
90    pub in_point: Duration,
91    /// Source-file PTS at which playback ends (`None` = play to EOF).
92    pub out_point: Option<Duration>,
93    /// Playback speed multiplier (`1.0` = normal), clamped to at least `0.01`.
94    pub speed: f64,
95    /// Crossfade duration from the previous placement into this one. Meaningful on
96    /// the V1 base track only; `Duration::ZERO` = hard cut.
97    pub xfade_dur: Duration,
98    /// The `xfade` transition kind for this crossfade (V1 base track only). `None`
99    /// when there is no transition; the runner defaults to `Fade` if a duration is
100    /// set without a kind.
101    pub xfade_kind: Option<XfadeTransition>,
102    /// How far past `out_point` this placement may keep producing **video**, to feed
103    /// the crossfade into the *next* placement. `Duration::ZERO` = not at all.
104    ///
105    /// A transition occupies the incoming placement head and blends it against these
106    /// frames, so the outgoing placement has to survive that window without being
107    /// lengthened: `out_point` stays where it is (it bounds this placement audio and
108    /// its timeline extent), and only the video decode runs on.
109    ///
110    /// **Timeline time**, like `xfade_dur` — the runner converts to source time with
111    /// `speed` where it compares against a source PTS.
112    pub video_handle: Duration,
113    /// Per-clip opacity in `[0.0, 1.0]`.
114    pub opacity: f32,
115    /// The dimension-free compositing description (effects, blend, position, and the
116    /// opacity/position animation tracks). The runner realises it per frame via
117    /// [`RealtimeLayer::with_dimensions`](ff_filter::RealtimeLayer::with_dimensions).
118    pub layer: RealtimeLayerDescriptor,
119    /// Audio fade-in duration (`Duration::ZERO` = none).
120    pub fade_in: Duration,
121    /// Audio fade-out duration (`Duration::ZERO` = none).
122    pub fade_out: Duration,
123    /// Resolved audio gain (dB), static or animated. A static value is applied once at
124    /// open; an animated one is evaluated per tick.
125    pub volume: AnimatedValue<f64>,
126    /// Per-clip audio pitch shift in semitones (`0.0` = none). A set `pitch_track`
127    /// is evaluated at its `t=0` value (static, per ADR-0002); the preview applies
128    /// it duration-preserving, matching the export shift at the model level.
129    pub pitch: f64,
130    /// Resolved stereo pan (`-1.0` left .. `+1.0` right, `0.0` center), static or
131    /// animated. Applied once at open at its `t=0` value; per-sample pan automation
132    /// is a deferred capability, so an animated pan uses its initial value (matching
133    /// the export mixer).
134    pub pan: AnimatedValue<f64>,
135}
136
137// SceneAudioTrack
138
139/// One dedicated audio-only track (A1, A2, …).
140#[derive(Debug, Clone)]
141pub struct SceneAudioTrack {
142    /// Placements in timeline order.
143    pub placements: Vec<SceneAudioPlacement>,
144}
145
146// SceneAudioPlacement
147
148/// One audio-only clip placed on the timeline.
149#[derive(Debug, Clone)]
150pub struct SceneAudioPlacement {
151    /// Source media path.
152    pub source: PathBuf,
153    /// Global timeline position where this placement starts.
154    pub offset: Duration,
155    /// Source-file PTS at which playback starts (defaulted to zero).
156    pub in_point: Duration,
157    /// Source-file PTS at which playback ends (`None` = play to EOF).
158    pub out_point: Option<Duration>,
159    /// Playback speed multiplier (`1.0` = normal), clamped to at least `0.01`,
160    /// applied by resampling.
161    pub speed: f64,
162    /// Audio fade-in duration (`Duration::ZERO` = none).
163    pub fade_in: Duration,
164    /// Audio fade-out duration (`Duration::ZERO` = none).
165    pub fade_out: Duration,
166    /// Resolved audio gain (dB), static or animated. A static value is applied once at
167    /// open; an animated one is evaluated per tick.
168    pub volume: AnimatedValue<f64>,
169    /// Per-clip audio pitch shift in semitones (`0.0` = none). A set `pitch_track`
170    /// is evaluated at its `t=0` value (static, per ADR-0002); the preview applies
171    /// it duration-preserving, matching the export shift at the model level.
172    pub pitch: f64,
173    /// Resolved stereo pan (`-1.0` left .. `+1.0` right, `0.0` center), static or
174    /// animated. Applied once at open at its `t=0` value; per-sample pan automation
175    /// is a deferred capability, so an animated pan uses its initial value (matching
176    /// the export mixer).
177    pub pan: AnimatedValue<f64>,
178}