Skip to main content

Projects and media

A project describes an edit: which media plays when, with what effects, text and audio. This page explains how it refers to media, so projects can live wherever your app keeps them and media can come from wherever you store it.

The editor and its project​

An editor holds one project, plus its undo history and the current selection. The preview and the editor controls show whichever editor you give them, and follow every change, whether it comes from the user or from your code.

A project has a fixed size and frame rate, e.g. 1080×1920 at 30 fps for vertical video. Choose them when you create it; export can render smaller.

Media ids​

A project never stores files, URLs or device paths. Each piece of media in it has a media id: your app's own id for that file, for example its key in your storage or its id in your database. The SDK treats ids as opaque text.

Two things follow:

  • Projects travel. The project's JSON contains only ids, so you can store it on the device, sync it to your backend, or open it on another device.
  • Your app decides where media lives. When the SDK needs a file it doesn't have, it asks your app to resolve the id.

Adding media​

When the user adds a file, pass its id together with the file you already have. The SDK keeps a copy, so editing starts right away; it doesn't wait for your upload.

PlatformCall
iOSeditor.addMedia(id:file:name:)
Androideditor.addMedia(file, id, name)
React Nativeeditor.addMedia(id, path, name)
Webawait editor.addMedia(id, file)

The result is a source: the project now knows the file, its kind, duration and size. Place it on a track as a clip to show it in the timeline. One source can back many clips.

The SDK reads video, audio and images.

Resolving media ids​

Set a resolver once. The SDK calls it with a media id whenever it needs that file and doesn't have it, typically after opening a project on a device that hasn't seen it before. Return where the file is:

  • a URL: plain, signed (e.g. a pre-signed S3 URL from your backend), or on your CDN. The SDK downloads it into its cache and shows the progress on the clips;
  • a local file your app already has.
NitrakitMedia.resolve = { id in
.remote(try await api.signedURL(for: id))
}

On iOS, Android and React Native, files are kept in the SDK's cache, so an id is downloaded once per device. Without a resolver, media stays on the device it was added on, which suits apps that keep projects on the device only.

Opening a project​

Opening a saved project shows it at once. Its media becomes available in the background: from the device when it's there, otherwise through your resolver.

  • While a file downloads, its clips show the progress.
  • If a file can't be resolved (deleted, no access, not uploaded yet), the project still opens and its clips are marked missing. Retry later with retryMedia.
  • Export waits for media that's still downloading, and fails if some is missing.

Uploads​

Uploading is your app's job, so it fits your storage and your backend. While it runs, show its progress on the clips with setMediaProgress(id, fraction), and clear it when done. The user keeps editing meanwhile.

Tracks​

KindHolds
VisualVideo, images, stickers, text and colour solids. Higher tracks draw on top.
AudioMusic, voice-over, sound effects.
EffectsEffect clips that change everything below them while they play.

A magnetic track keeps its clips back to back: removing or trimming a clip closes the gap. Use one magnetic visual track as the main story and free tracks for overlays, titles and music.

Time​

All times are microseconds: one second is 1_000_000. Clip times are on the timeline; keyframe times are relative to their clip.

Saving​

A project saves as JSON. Store it however you like, and save as the user works: the SDK tells you after every change.

iOSAndroidReact NativeWeb
Saveeditor.toJson()editor.toJson()editor.toJson()editor.toJson()
OpenEditor(open: json), then loadMedia()Editor(json), then loadMedia()openEditor(json), then loadMedia()editor.openProject(json)
ChangessetListener(_:)setListener { … }setListener(fn)onChange option