mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-14 19:26:59 -04:00
421 lines
27 KiB
Markdown
421 lines
27 KiB
Markdown
# Project files
|
||
|
||
FilmCraft projects are `.fcproj` files: UTF-8 JSON with an explicit schema version. Reading and
|
||
writing them is `crates/format` (`filmcraft-format`); the engine's `file.*` commands use it.
|
||
|
||
## Format
|
||
|
||
```json
|
||
{
|
||
"format": "filmcraft.project",
|
||
"schema_version": 13,
|
||
"generator": "FilmCraft 0.1.0",
|
||
"project": { "name": "…", "settings": { … }, "root": { … }, "items": { … }, "next_id": 48 },
|
||
"view": { "open_sequences": [21, 50], "active_sequence": 50, "sequences": { "21": { "pps": 40.0, "scroll": 0.0, … } } }
|
||
}
|
||
```
|
||
|
||
- `schema_version` is the version of the whole document. This build writes
|
||
`filmcraft_format::SCHEMA_VERSION` and reads every version from 1 up to it.
|
||
- `project` is the serialized `filmcraft_project::Project` (bins, media clips, sequences, tracks,
|
||
clips, effects, keyframes, markers). Media is referenced by path, never embedded.
|
||
- Files are written compact (no indentation). A 2,000-clip project is about 1.4 MB.
|
||
- `view` (optional) is what was open when the project was saved: the Timeline's sequence tabs in
|
||
order, the active one, each sequence's zoom, scroll and track heights, and each sequence's
|
||
playhead (`filmcraft_project::ProjectView`). It is beside the project, not in it: it is not part of the
|
||
edit, never an undo step, and changing it does not mark the project as changed. Opening a
|
||
project restores it when the Timeline preference "Restore open sequences when opening projects"
|
||
is on. It is outside the schema version: older builds ignore it, ids that are not sequences and
|
||
numbers out of range are dropped or clamped (a playhead also snaps to a frame of its sequence), and a `view` that cannot be read is ignored (the
|
||
project opens on its first sequence, as it does without one or with no open sequence in it).
|
||
- A coordinate of an effect's point parameter may be `null`. Point parameters use NaN for "auto"
|
||
(the frame centre, or the source centre for `anchor`), and JSON writes NaN as `null`; it reads back
|
||
as "auto". Placing a clip resolves these points at once. An interchange import resolves them
|
||
too, except the `anchor` of a clip whose media file was not found. That one stays "auto", and
|
||
the renderer centres it, until Link Media (`media.relink`, `media.autoRelink`) reads the file
|
||
and with it the real picture size; a file that merely comes back at its old path does not
|
||
change it. `null` anywhere else is a damaged file.
|
||
|
||
### Schema history
|
||
|
||
| Version | Since | Shape |
|
||
|---|---|---|
|
||
| 1 | M0 | the bare `Project` object with a `"version": 1` field |
|
||
| 2 | M11.1 | the envelope above; the project's own `version` field is gone |
|
||
| 3 | M10.2 | graphic clips (`ItemKind::Graphic` with text/shape layers); a no-op step for older data |
|
||
| 4 | M7.4 | Essential Sound: clip audio types and settings; no-op step |
|
||
| 5 | M8.8 | colour management: working space, Interpret Footage colour space, LUT library; no-op step |
|
||
| 6 | M11.7/M11.9 | media identity fingerprints, ingest settings; no-op step |
|
||
| 7 | M5.5 | effect masks: `masks` on effect instances with keyframable Bézier `Path` values; no-op step |
|
||
| 8 | M12 | multi-camera source sequences and clips, merged clips; no-op step |
|
||
| 9 | M10.5 | transcripts of media items (`project.transcripts`); no-op step |
|
||
| 10 | M3.10 | clip time interpolation, Hold Filters, Field Options, audio source channels, Modify ▸ Audio Channels map, subclip Restrict Trims; no-op step |
|
||
| 11 | M3.11 | search bins (`project.search_bins`), Flash Cue markers, Project Settings safe areas, capture format and scratch disks; no-op step |
|
||
| 12 | M10.7 | graphics design data: `TrackItem::graphic` (roll / crawl, responsive time, template link), `EffectInstance::layer` (layer uid, per-character styles, responsive pins), `project.source_graphics`; no-op step |
|
||
| 13 | Multi-stream audio | `MediaInfo::audio_streams` and `TrackItem::audio_stream`; older `audio` objects load as stream zero, missing clip indices default to zero; no-op step |
|
||
|
||
### Migrations
|
||
|
||
Older files are upgraded on load, in memory, by a chain of single-step functions
|
||
(`filmcraft_format::MIGRATIONS`, where entry *i* upgrades v*i+1* to v*i+2*). Each step rewrites the JSON
|
||
document; after the last one the result is deserialized into the current model. To change the schema:
|
||
|
||
1. add a function `vN_to_vN1(doc: Value) -> Result<Value, String>` and append it to `MIGRATIONS`
|
||
(`SCHEMA_VERSION` follows automatically);
|
||
2. add a small hand-made fixture of the old shape to `crates/format/tests/fixtures/` and a test that it
|
||
loads;
|
||
3. never edit a migration that has shipped.
|
||
|
||
Fields added with `#[serde(default)]` don't need a migration: missing values take their defaults.
|
||
But when new data would make the file unreadable to an older build (a new enum variant such as
|
||
`ItemKind::Graphic`, a new required shape), bump the schema with a no-op step so older builds refuse
|
||
the file with the message below instead of failing with a parse error. Do the same when an older
|
||
build would load the file but silently drop new fields on its next save.
|
||
|
||
When you open an older file FilmCraft says so ("Upgraded project from schema v1 to v2"). The file
|
||
on disk is unchanged until you save. The **first save over it** keeps the original next to it as
|
||
`<name> (schema v1 backup).fcproj`, so an older FilmCraft can still open your work.
|
||
|
||
### Newer files
|
||
|
||
A file with a `schema_version` newer than the build supports is **refused**, not half-read:
|
||
|
||
> this project was saved by a newer version of FilmCraft (project schema v12); this build reads up to
|
||
> v11. Update FilmCraft to open it.
|
||
|
||
Reading it partially and saving it back would silently drop whatever the newer version added.
|
||
|
||
## Saving is atomic
|
||
|
||
Every write of a project file, auto-save, recovery snapshot or preferences file goes through
|
||
`filmcraft_format::atomic_write`:
|
||
|
||
1. write the bytes to a hidden temp file in the **same directory** (`.<name>.<pid>-<n>.tmp`);
|
||
2. `fsync` the temp file;
|
||
3. rename it over the target (an atomic replace on every supported OS);
|
||
4. `fsync` the directory so the rename itself is durable.
|
||
|
||
A crash, `kill -9` or power cut at any point leaves either the complete old file or the complete new
|
||
one. If a step fails, the temp file is removed and the old file is untouched.
|
||
|
||
## Commands
|
||
|
||
| Id | Menu | |
|
||
|---|---|---|
|
||
| `file.save {path?}` | File ▸ Save (⌘S) | write to the project's path (or `path`) |
|
||
| `file.saveAs {path}` | File ▸ Save As… (⇧⌘S) | write and adopt the new path |
|
||
| `file.saveCopy {path}` | File ▸ Save a Copy… (⌥⌘S) | write a copy; the project keeps its path and unsaved state |
|
||
| `file.revert` | File ▸ Revert | reload the last saved version, discarding changes |
|
||
| `file.open {path}` | File ▸ Open Project… (⌘O) | returns `{schemaVersion, migrated}` |
|
||
|
||
In the UI, Save / Save As / Save a Copy without a `path` show a file dialog.
|
||
|
||
## Auto Save
|
||
|
||
Preferences ▸ Auto Save (Edit ▸ Preferences ▸ Auto Save…, ⌘,; FilmCraft ▸ Settings… on macOS) works
|
||
like Premiere's:
|
||
|
||
| Setting | Key | Default |
|
||
|---|---|---|
|
||
| Automatically save projects | `autoSave.enabled` | on |
|
||
| Automatically Save Every N minute(s) | `autoSave.intervalMinutes` | 5 (1–1440) |
|
||
| Maximum Project Versions | `autoSave.maxVersions` | 20 (1–1000) |
|
||
| Auto Save also saves the current project(s) | `autoSave.saveCurrentProject` | off |
|
||
| Keep a recovery copy of unsaved changes | `autoSave.recoveryJournal` | on |
|
||
| Update it at least every N second(s) | `autoSave.recoveryIntervalSeconds` | 5 (1–600) |
|
||
|
||
At each interval, if the project changed since the last auto-save, a copy is written to the
|
||
`Auto-Save` folder next to the project as `<name>-YYYY-MM-DD_HH-MM-SS.fcproj` (local time). An
|
||
unsaved project uses `<data dir>/Auto-Save/`. After each write the oldest copies beyond the limit are
|
||
deleted. Only files named exactly `<name>-<timestamp>.fcproj` are ever deleted. An auto-save is an
|
||
ordinary project file: open it with File ▸ Open Project. `file.listAutoSaves` lists them, newest
|
||
first.
|
||
|
||
Preferences are stored in `<data dir>/preferences.json`. The `prefs.*` commands read and change them.
|
||
|
||
## Crash recovery
|
||
|
||
Premiere's auto-save can lose up to one interval of work. FilmCraft also keeps a **recovery journal**:
|
||
|
||
- Whenever the project has unsaved changes, a snapshot is written to
|
||
`<data dir>/Recovery/<started>-<pid>[-<n>]/snapshot.fcproj`, plus `session.json` (time, revision, project
|
||
name and path). It is written about 1 s after the last edit, or after at most
|
||
`recoveryIntervalSeconds` while edits keep coming. Saving the project deletes the snapshot.
|
||
- Each running FilmCraft holds an OS file lock on `<session>/lock`. When the process dies (crash,
|
||
`kill -9`, power loss) the OS releases the lock.
|
||
- At startup, every other session directory whose lock can be taken and that has a snapshot is a
|
||
recovery candidate. FilmCraft asks: *"FilmCraft quit unexpectedly while 'X' had unsaved changes.
|
||
Recover unsaved changes from 2026-10-01 00:02:09?"*, with the buttons **Recover**, **Not Now** and
|
||
**Discard**. Stale directories without a snapshot are removed. Journals of running sessions are
|
||
never touched.
|
||
- Recovering loads the snapshot as an **unsaved** project with its original path, so Save writes
|
||
over the original file. The recovered state goes into the new session's own journal before the old
|
||
journal is deleted.
|
||
- A normal quit with nothing unsaved removes the session directory. Quitting with unsaved changes
|
||
(Quit, Cmd+Q or the window's close button) first asks *"Save changes to 'X' before closing?"*
|
||
with **Cancel**, **Don't Save** and **Save** (Save on a never-saved project opens the Save dialog);
|
||
Don't Save discards the changes and their snapshot. A quit that does not ask (the control
|
||
channel's `app.quit`, the OS ending the session) keeps the snapshot, marked `cleanExit`, and FilmCraft offers it on the next launch ("FilmCraft was
|
||
closed while … had unsaved changes").
|
||
|
||
Desktop flags: `--recover` recovers the newest candidate without asking. `--no-recover` starts
|
||
without asking; the changes stay available through File ▸ Recover Unsaved Changes…. `--data-dir
|
||
<dir>` (or `FILMCRAFT_DATA_DIR`) moves the data directory. The default is `~/Library/Application
|
||
Support/FilmCraft` on macOS, `%APPDATA%\FilmCraft` on Windows, and `$XDG_DATA_HOME/filmcraft` or
|
||
`~/.local/share/filmcraft` elsewhere. A `portable.txt` file next to the executable (the Windows
|
||
portable zip ships one) makes it `data` next to the executable instead.
|
||
|
||
### Cost
|
||
|
||
The UI thread only gives the worker thread an `Arc<Project>`: the immutable snapshot the engine
|
||
already keeps for undo, passed through a channel. JSON encoding and all file I/O run on the
|
||
`filmcraft-autosave` thread. Measured on the development Mac (release build) with a 2,000-clip project
|
||
(`cargo test --release -p filmcraft-format --test fixtures -- --nocapture`):
|
||
|
||
| Step | Time | Size |
|
||
|---|---|---|
|
||
| encode, compact (what is written) | 2.5 ms | 1.4 MB |
|
||
| encode, pretty (not used) | 7.1 ms | 5.6 MB |
|
||
| decode | 4.6 ms | |
|
||
| atomic write + fsync (APFS SSD) | 30 ms | |
|
||
|
||
In the running app with the demo project, a recovery write measured 0.1 ms to encode and 27 ms to
|
||
write (two fsynced files).
|
||
|
||
## Commands
|
||
|
||
| Id | |
|
||
|---|---|
|
||
| `file.recoveryList` | candidates: `id`, `projectName`, `projectPath`, `savedAt`, `revision`, `cleanExit`… |
|
||
| `file.recover {id?}` | recover (newest when no `id`). From the UI, `{}` opens the recovery dialog |
|
||
| `file.discardRecovery {id?, all?}` | delete a candidate's journal |
|
||
| `file.autoSaveNow` | write an auto-save now (if anything is unsaved) |
|
||
| `file.autoSaveStatus` | prefs, folders, last auto-save / journal times, last encode/write ms and bytes |
|
||
| `file.listAutoSaves` | auto-save files of the current project, newest first |
|
||
| `prefs.get {key?}` / `prefs.set {key, value}` or `{values:{…}}` / `prefs.reset {category?}` | settings (below) |
|
||
|
||
UI automation ids: `recovery.recover`, `recovery.later`, `recovery.discard`, `recovery.item.<n>`;
|
||
`revert.yes`, `revert.no` (File ▸ Revert asks first when invoked without params).
|
||
|
||
## Settings
|
||
|
||
Premiere's Settings dialog (app menu ▸ Settings ▸ <category> on macOS, Edit ▸ Preferences
|
||
elsewhere; General is Cmd+,; the window is titled "Preferences"). The values are user preferences,
|
||
not project data: they live in `preferences.json` in the per-user data directory (written
|
||
atomically on every change) and never touch the `.fcproj` schema. The file carries a `version`
|
||
(currently 2); older files are migrated on load, unknown dropdown values, bad colours and
|
||
out-of-range numbers are repaired to defaults/limits, and an unreadable file falls back to the
|
||
defaults.
|
||
|
||
Every value is a dotted key whose first segment is its category: `timeline.stillImageDuration`,
|
||
`labels.colors.rose.name`. `prefs.get` / `prefs.set` / `prefs.reset {category?}` reach them from the
|
||
CLI, the control channel and MCP; `prefs.set` rejects values that are not one of a dropdown's
|
||
choices. `prefs.schema {category?}` lists every category with its fields (label, kind, choices,
|
||
range, unit, current value, and `wired`: whether FilmCraft acts on it yet). `app.settings.<category>`
|
||
opens the dialog on a page.
|
||
|
||
| Category | Takes effect |
|
||
|---|---|
|
||
| General | At Startup (Show Home = demo project, Open Most Recent, empty project; recent projects are remembered on open/save), Show Tool Tips |
|
||
| Appearance | Appearance Mode `appearanceMode` (`auto` = Sync with system / `dark` (default) / `light`; the header button and View ▸ Appearance ▸ Next Appearance Mode cycle it), Dark Theme `darkTheme` (`darkest` (default) / `dark`), Light Theme `lightTheme` (`light`), highlight colour, accessible contrast. `colorTheme` (Darkest / Dark / Light) is legacy input: setting it (older files and clients, View ▸ Appearance ▸ a theme) selects that theme and fixes the mode to its family; a file without `appearanceMode` migrates to its theme's fixed mode. It is kept in line with the choices: the theme a fixed mode shows, the dark theme in Auto |
|
||
| Audio | Automatch Time, Large Volume Adjustment, automation keyframe thinning (linear, minimum time) |
|
||
| Audio Hardware | device class (cpal host), output device, I/O buffer size, sample rate, force document rate, Output Mapping (programme L/R → device channels) |
|
||
| Auto Save | the auto-save ring and the crash-recovery journal |
|
||
| Color | stored only (display colour management, EDR monitoring, HDR graphics white) |
|
||
| Graphics | new text layers: smart quotes, ligatures, default font |
|
||
| Labels | the 16 label names and colours (timeline, Project panel, Edit ▸ Label), label defaults for imported movies / video / audio / stills and new sequences, Label Color Presets: JSON files (`{"name", "labels": [16 × {"name", "color": "#rrggbb"}]}`) in `<data dir>/Label Presets`, built in: "Default" (the labels FilmCraft ships with), "Bright" (16 saturated hues) and "Roles" (B-Roll, Footage, Dialogue… named so the label defaults land on matching roles) (`labels.presets`, `labels.importPreset {path}`, `labels.exportPreset {path}`, `labels.deletePreset {name}`; `labels.preset` is the selected one) |
|
||
| Media | Indeterminate Media Timebase (frame rate of stills), Default Media Scaling (Scale to / Set to frame size when a clip of another size is edited in), Enable proxies |
|
||
| Media Analysis & Transcription | auto-transcribe imported clips (scope "all imported"), speech model, default language / auto-detect, speaker labelling (defaults of `transcript.generate`) |
|
||
| Media Cache | location (`<data dir>/Media Cache` by default; unsaved projects' render previews live there), automatic deletion (older than N days / beyond N GB), Delete… (`mediaCache.clean {all?}`, `mediaCache.info`) |
|
||
| Memory | frame cache budget of the monitors and thumbnails |
|
||
| Playback | preroll / postroll (Play Around, trim loop), Step forward/back many |
|
||
| Plugins | stub (no plugins) |
|
||
| Timeline | video / audio transition and still image default durations (frames or seconds), playback auto-scrolling (none / page / smooth), snap playhead, return to beginning at playback end, play after rendering previews |
|
||
| Trim | Large Trim Offset, Selection tool picks roll (on the cut) / ripple (on the edge) without a modifier, playhead determines trim loop |
|
||
|
||
The remaining fields are kept for parity and shown, but have no effect yet (`wired: false`).
|
||
|
||
UI automation ids: `settings.category.<id>`; `settings.<key>` for every control (for example
|
||
`settings.timeline.autoScroll`, `settings.labels.colors.rose.color`); `settings.<key>.<value>` for
|
||
the items of an open dropdown; `settings.<key>.browse`, `settings.<key>.hex`;
|
||
`settings.mediaCache.clean`; `settings.help`, `settings.reset` (this page back to its defaults),
|
||
`settings.cancel`, `settings.ok`. The open dialog's page and draft are `ui.settings` in `ui.inspect`;
|
||
`ui.set {"settings": {"page": id, "values": {key: value}}}` edits the draft (OK applies it).
|
||
|
||
## Media: offline, relinking, proxies, ingest
|
||
|
||
Media is referenced by path. Each imported file also records its **identity**
|
||
(`MediaClip::identity`): the size and a 64-bit fingerprint of the size, the first MiB and the last
|
||
MiB. Reading 2 MiB is cheap on any file, and it tells a moved original from a different take with
|
||
the same name. The field is optional (`skip_serializing_if`); it and `ProjectSettings::ingest` came with schema
|
||
v6, so builds that would drop them on save refuse the file instead. Projects from older builds
|
||
have no identity, so relinking them skips the fingerprint check.
|
||
|
||
### Offline media
|
||
|
||
- **Opening a project** checks every file (`media.findMissing`). Missing items don't stop the open.
|
||
They render the **offline slate** (`crates/render/src/offline.rs`, our own design: a red striped
|
||
field, a warning triangle, "MEDIA NOT FOUND", the file name and a hint), their audio is silent,
|
||
and the app opens the **Link Media** dialog. `file.open` returns `missingMedia`.
|
||
- **Make Offline** (`media.makeOffline {items?, deleteFiles?}`) sets `MediaClip::offline`. The clip
|
||
then shows "MEDIA SET OFFLINE" even though the file exists, until it is linked again. Files are
|
||
deleted only with `deleteFiles: true`.
|
||
- The media pool caches sources per item **and** per reference (path, offline flag). So relinking,
|
||
Make Offline and undoing either take effect on the next frame.
|
||
|
||
### Link Media
|
||
|
||
| Command | |
|
||
|---|---|
|
||
| `media.findMissing` | `{missing: [{item, name, fileName, path, status: missing\|offline\|proxyMissing, duration, startTimecode}]}` |
|
||
| `media.status {item?}` | per item: `online`, `missing`, `offline`, `unreadable` or `generated`, plus identity and proxy |
|
||
| `media.linkMedia` | File ▸ Link Media… (rescans; the UI opens the dialog) |
|
||
| `media.relink {item, path, force?, relinkOthers?=true, alignTimecode?, match?}` | check, link, then the others |
|
||
| `media.autoRelink {from, to}` / `{folder}` | batch relink: a folder-prefix remap, or a search of a folder tree |
|
||
| `media.search {folder, item?, exactName?=true}` | ranked candidates with `ok`, `identityMatch`, `problems` |
|
||
| `media.replaceFootage {item, path}` | relink without checks (Replace Footage) |
|
||
| `media.offlineAll` | close the dialog and leave the rest offline |
|
||
|
||
`match` selects which properties must agree. Each defaults as shown:
|
||
|
||
- `fileName` (true): the file name stem;
|
||
- `extension` (true);
|
||
- `clipId` (true): the fingerprint; a mismatch is refused with "not the same file…";
|
||
- `duration` (true): within one frame;
|
||
- `mediaStart` (false): the start timecode;
|
||
- `metadata` (true): frame size, frame rate and audio channels.
|
||
|
||
`force: true` links regardless. **Relink others automatically** works out the folder remap from the
|
||
file just linked, by dropping the common trailing path components (`/Volumes/A/shoot/a.mov` →
|
||
`/Users/me/shoot/a.mov` gives `/Volumes/A` → `/Users/me`). It applies the remap to the other missing
|
||
files and checks each one. It also looks in the new file's folder. Windows and macOS separators both
|
||
work. **Align Timecode** keeps clips on the same timecode when the new file starts at a different
|
||
timecode: the clips' source in-points, keyframes, marks and markers move with it
|
||
(`Project::shift_media_time`). One relink, together with the files it pulls along, is one undo step.
|
||
|
||
Link Media dialog automation ids: `linkMedia.row.<n>`,
|
||
`linkMedia.match.<fileName|extension|clipId|duration|mediaStart|metadata>`, `linkMedia.alignTimecode`,
|
||
`linkMedia.relinkOthers`, `linkMedia.folder`, `linkMedia.browse`, `linkMedia.exactName`,
|
||
`linkMedia.search`, `linkMedia.candidate.<n>`, `linkMedia.preview`, `linkMedia.link`,
|
||
`linkMedia.locate`, `linkMedia.offline`, `linkMedia.offlineAll`, `linkMedia.cancel`.
|
||
Make Offline: `makeOffline.keep|delete|ok|cancel`. In the Project panel, offline items get a
|
||
broken-link badge (`project.item.<id>.offline` in Icon view), and items with a proxy get a **P**
|
||
badge (`project.item.<id>.proxy`).
|
||
|
||
### Proxies
|
||
|
||
| Command | Menu | |
|
||
|---|---|---|
|
||
| `media.createProxies {items?, preset?, destination?, attach?=true, wait?}` | Clip ▸ Proxy ▸ Create Proxies… | background job; attaches when done |
|
||
| `media.attachProxies {item, path}` / `{items, paths}` | Attach Proxies… | duration within one frame and the same frame rate are required; size and aspect may differ |
|
||
| `media.detachProxies {items?}` | Detach Proxies | |
|
||
| `media.reconnectFullRes {item, path}` | Reconnect Full Resolution Media… | link the full-resolution file of a clip that has a proxy |
|
||
| `media.toggleProxies {enabled?}` | View ▸ Toggle Proxies, monitor button | Preferences ▸ Media ▸ `media.enableProxies` |
|
||
| `media.proxyPresets` | | |
|
||
|
||
The presets use our own encoders:
|
||
|
||
- `prores_proxy_quarter` (default), `prores_proxy_half`, `prores_lt_half`: ProRes 422 Proxy or LT
|
||
in MOV with PCM audio;
|
||
- `h264_quarter`, `h264_half`: H.264 + AAC in MP4.
|
||
|
||
Proxies go to `<media folder>/Proxies/<name>_Proxy.<ext>`, or to the chosen folder. Job progress is
|
||
in `jobs.list`. `Session::poll_persistence`, which the app calls every frame, attaches finished
|
||
proxies.
|
||
|
||
With proxies enabled, monitors, thumbnails and playback read the proxy. The proxy source reports
|
||
the original's size. The compositor derives its pixel scale from the frame it gets, so Motion and
|
||
pixel-size effect parameters (blur radii…) give the same picture at the proxy's resolution. A
|
||
320×180 clip with Motion and Gaussian Blur and a ½-size ProRes Proxy measures 44 dB PSNR against
|
||
full resolution (48 dB at ½ playback resolution). **Export always uses full resolution**
|
||
(`MediaPool::full_res_provider`). Toggling proxies bumps the revision so caches refresh, but it is
|
||
not an edit and doesn't mark the project as modified.
|
||
|
||
**Cost.** These numbers are from `proxy_playback_perf_4k`, an ignored test: `RAYON_NUM_THREADS=1
|
||
cargo test --release -p filmcraft-engine proxy_playback_perf_4k -- --ignored --nocapture`. It plays a
|
||
2 s 3840×2160 H.264 clip (ffmpeg `testsrc2`, libx264) on one core of the development Mac and reports
|
||
milliseconds per frame:
|
||
|
||
| | decode (GPU path) | CPU composite at ½ | CPU composite at ¼ | proxy creation |
|
||
|---|---|---|---|---|
|
||
| full resolution | 9.2 | 277 | 201 | |
|
||
| ProRes Proxy ¼ | 8.9 | 150 | 35 | 17.8 s |
|
||
| H.264 ¼ | 0.7 | 131 | 48 | 8.9 s |
|
||
|
||
The test pattern is unusually cheap for H.264 to decode, so real camera footage gains more from
|
||
proxies. The CPU composite (YUV→linear conversion and resampling) is what proxies cut most:
|
||
5.7× at ¼ resolution, where the proxy needs no resampling.
|
||
|
||
Create Proxies dialog ids: `proxies.preset.<id>`, `proxies.destination`, `proxies.browse`,
|
||
`proxies.ok`, `proxies.cancel`. Monitor button: `program.transport.media.toggleProxies` (and
|
||
`source.…`). It is lit while proxies are on.
|
||
|
||
### Ingest settings
|
||
|
||
`project.ingestSettings {enabled, action: copy|transcode|createProxies|copyAndCreateProxies,
|
||
destination?, preset?}` (Project Settings ▸ Ingest) acts on every `file.import`:
|
||
|
||
- **copy** copies the file to the destination (default `<media folder>/Ingested Media`), checks the
|
||
copy's fingerprint against the original, and then uses the copy;
|
||
- **transcode** writes the preset's format (default ProRes 422 LT) in the background and switches the
|
||
clip to the result when it is done;
|
||
- **createProxies** makes proxies in the background and attaches them;
|
||
- **copyAndCreateProxies** does both.
|
||
|
||
Copied files keep their names when available; collisions receive a numbered suffix before the
|
||
extension (`A001 2.mov`) and never replace an existing file. Transcodes reserve distinct output
|
||
names for every clip in the import batch. Copies keep their source fingerprint, and each proxy
|
||
stays attached to its corresponding take.
|
||
|
||
`file.import` reports this under `ingest`.
|
||
|
||
## Project Manager
|
||
|
||
`file.projectManager` (File ▸ Project Manager…) makes a self-contained copy of a project:
|
||
|
||
```json
|
||
{"destination": "/path/Folder", "mode": "collect|consolidate", "sequences": [id], "excludeUnused": true,
|
||
"handles": 30, "preset": "prores_lt", "includeProxies": true, "includePreviews": false,
|
||
"projectName": "…", "dryRun": false, "overwrite": false, "wait": false}
|
||
```
|
||
|
||
- **collect** copies every media file that the chosen sequences use (following nested sequences and
|
||
subclips), plus their proxies and render previews if asked.
|
||
- **consolidate** writes only the used range of each movie or audio file, plus `handles` frames on
|
||
each side, with a transcode preset (`prores_lt`, `prores_hq`, `h264`). It then re-bases the clips'
|
||
media time, so every edit, keyframe and marker stays in place. Stills are copied.
|
||
- **excludeUnused** drops media items and sequences that the chosen sequences don't use.
|
||
- **dryRun** returns the plan and the disk-space estimate (`originalBytes`, `resultBytes`, `files`)
|
||
without writing anything.
|
||
|
||
The copy runs as a job. The project file (`<destination>/<name>.fcproj`) is written last, with the
|
||
new paths and fingerprints, so it opens with no missing media.
|
||
|
||
Dialog ids: `pm.seq.<id>`, `pm.mode.<collect|consolidate>`, `pm.preset.<id>`, `pm.excludeUnused`,
|
||
`pm.handles`, `pm.includeProxies`, `pm.includePreviews`, `pm.destination`, `pm.browse`,
|
||
`pm.calculate`, `pm.sizes`, `pm.ok`, `pm.cancel`.
|
||
|
||
## Effect presets
|
||
|
||
Effect presets (Effects panel ▸ Presets; Effect Controls ▸ right-click an effect ▸ Save Preset…) are
|
||
not part of the project. Built-in presets are defined in code (`filmcraft_engine::presets`); user
|
||
presets live in `<data dir>/effect-presets.json`, and `presets.export` / `presets.import` read and
|
||
write the same JSON format:
|
||
|
||
```json
|
||
{"format": "filmcraft.effect-presets", "version": 1, "presets": [
|
||
{"name": "My Vignette", "description": "", "keyframes": "AnchorToIn",
|
||
"source_duration": 2540160000000, "source_size": [1920, 1080],
|
||
"effects": [ /* EffectInstance, exactly as in .fcproj: effect, enabled, params, masks */ ]}
|
||
]}
|
||
```
|
||
|
||
Keyframe times are relative to the in point of the clip the preset was saved from
|
||
(`source_duration` is that clip's media length in ticks). Applying re-times them: `Scale` stretches
|
||
them over the target clip, `AnchorToIn` / `AnchorToOut` keep their distance from the target's in /
|
||
out point. Point parameters, mask paths, feather and expansion are scaled by the target / source
|
||
frame-size ratio. Intrinsic effects (Motion, Opacity…) replace the clip's own instance; other
|
||
effects are added. Applying is one undo step; saving, renaming, deleting and importing change the
|
||
library, not the project, and are not undoable. Files with another `format`, a newer `version` or
|
||
unknown effect ids are refused.
|