filmcraft/docs/project-files.md

8.4 KiB
Raw Blame History

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

{
  "format": "filmcraft.project",
  "schema_version": 2,
  "generator": "FilmCraft 0.1.0",
  "project": { "name": "…", "settings": { … }, "root": { … }, "items": { … }, "next_id": 48 }
}
  • 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.

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

Migrations

Older files are upgraded on load, in memory, by a chain of single-step functions (filmcraft_format::MIGRATIONS, where entry i upgrades vi+1 to vi+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.

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 v3); this build reads up to v2. 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 keeps the snapshot, marked cleanExit, and FilmCraft offers it on the next launch ("FilmCraft was closed while … had unsaved changes"). There is no "Save changes?" prompt on quit yet, so this keeps the work.

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

` (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.

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 preferences

UI automation ids: prefs.autoSave.enabled, prefs.autoSave.intervalMinutes, prefs.autoSave.maxVersions, prefs.autoSave.saveCurrentProject, prefs.autoSave.recoveryJournal, prefs.autoSave.recoveryIntervalSeconds, prefs.ok, prefs.cancel, prefs.reset; recovery.recover, recovery.later, recovery.discard, recovery.item.<n>; revert.yes, revert.no (File ▸ Revert asks first when invoked without params).