Video Studio SDKv0.0.3
Core

Project bundle

The serializable JSON that fully describes a project — schema, versioning, helpers, and the load/save roundtrip.

A ProjectBundle is one JSON object that captures everything about a project: settings, tracks, items, transitions, markers, regions, and metadata. It's what you persist to your database and what you ship to the renderer. The bundle is the only source of truth between sessions; the runtime StudioState is derived from it on load and serialized back to it on save.

Shape

interface ProjectBundle {
  schemaVersion: number
  metadata: {
    id: string | null
    name: string
    createdAt: string                 // ISO 8601
    updatedAt: string
  }
  settings: ProjectSettings
  tracks: Track[]
  items: TrackItem[]                  // flat list (denormalized from the store's keyed map)
  markers: Marker[]
  regions: Region[]
  transitions?: Record<string, unknown>[]
}

Metadata

  • id — your project ID. Generated by createEmptyBundle if you don't provide one. Pass it through your backend; the SDK doesn't care what it is as long as it's stable.
  • name — display name shown in the toolbar title.
  • createdAt / updatedAt — ISO timestamps. The SDK doesn't auto-update these — you control them in your save handler.

Settings (ProjectSettings)

interface ProjectSettings {
  width: number                          // default 1920
  height: number                         // default 1080
  fps: number                            // default 30
  aspectRatio: string                    // default '16:9'
  durationFrames: number                 // recalculated by the store on every mutation
  backgroundColor: string                // default '#000000'
  safeZones: { titleSafePercent: number; actionSafePercent: number }
  renderProfiles: RenderProfile[]        // see DEFAULT_RENDER_PROFILES
}

durationFrames is derived — the store updates it after every item add/move/trim/remove to be the max (startFrame + durationFrames) across all items. Don't write it manually.

renderProfiles is the list shown in the export dialog. The defaults come from DEFAULT_RENDER_PROFILES: Draft 480p, Preview 720p, Final 1080p, Final 4K.

Tracks (Track[])

interface Track {
  id: string
  name: string
  kind: 'main' | 'track' | 'audio'
  order: number
  locked: boolean
  muted: boolean
  hidden: boolean
}

Three track kinds:

  • main — primary video/image track (default created on new bundle). Enforces non-overlap.
  • track — generic overlay/text/captions/adjustment track. Allows multiple per project.
  • audio — audio track.

Items (TrackItem[])

Stored flat in the bundle (so JSON serialization stays simple), keyed by ID in the runtime store. See Timeline for the full TrackItem shape.

Transitions / markers / regions

  • Transitions — track-level transitions between adjacent items. Optional.
  • Markers{ id, frame, label?, color? } — labeled points on the timeline ruler.
  • Regions{ id, startFrame, endFrame, ... } — labeled ranges on the timeline.

Helpers

createEmptyBundle(name?, aspectRatio?)

Creates a fresh bundle with one main track, one audio track, and default settings.

import { createEmptyBundle } from '@studio-dev/vsdk'

const bundle = createEmptyBundle('My Project', '9:16')
// {
//   schemaVersion: 1,
//   metadata: { id: 'prj_...', name: 'My Project', createdAt: '...', updatedAt: '...' },
//   settings: { width: 1080, height: 1920, fps: 30, aspectRatio: '9:16', ... },
//   tracks: [
//     { id: 'trk_main', name: 'Main', kind: 'main', ... },
//     { id: 'trk_audio_1', name: 'Audio 1', kind: 'audio', ... },
//   ],
//   items: [], markers: [], regions: [], transitions: [],
// }

The main track's id is always the constant trk_main. Don't delete it; the SDK treats it as load-bearing.

normalizeBundle(partial)

Fills in missing fields with defaults. Use this when accepting bundles from older versions of your backend, hand-written test fixtures, or migrated data.

import { normalizeBundle } from '@studio-dev/vsdk'

const safe = normalizeBundle(maybeBrokenBundle)

CURRENT_SCHEMA_VERSION

import { CURRENT_SCHEMA_VERSION } from '@studio-dev/vsdk'
// → 1 (at the time of writing)

Check this before loading old bundles. If the saved bundle's schemaVersion is lower, run migrateBundle from @studio-dev/vsdk/contracts before loading.

Validation (@studio-dev/vsdk/contracts)

The contracts subpath exports Zod schemas you can use to validate a bundle at the trust boundary:

import { projectBundleSchema } from '@studio-dev/vsdk/contracts'

const parsed = projectBundleSchema.safeParse(jsonFromBackend)
if (!parsed.success) {
  // Bad bundle — refuse to load, log to your error tracker.
}

The same module exports migrateBundle(bundle) to upgrade older bundles to CURRENT_SCHEMA_VERSION.

Load and save

Load on mount

Pass initialBundle to the provider. The store calls loadBundle(bundle) once on first render.

<VideoStudioProvider initialBundle={bundle}>

Load on demand

From inside the provider tree, call loadBundle on the store API:

const storeApi = useStudioStoreApi()
storeApi.getState().loadBundle(newBundle)

loadBundle:

  • Replaces all timeline state with the bundle's data.
  • Resets selection, history (undo + redo stacks), and playback.
  • Marks the project clean.

It is not undoable — load-bundle is treated as a hard reset, not a user-level action.

Save

toBundle() serializes the current state:

const bundle = storeApi.getState().toBundle()
await saveToBackend(bundle)

The provider's onSave callback does this for you when the user clicks Save — see Configuration.

Round-trip guarantees

For any state S produced through public store actions:

loadBundle(toBundle(S)) ≡ S

…modulo selection (cleared) and history (cleared). The bundle is the canonical representation; anything the SDK can rebuild from the bundle isn't serialized.

Recipes

Create a vertical (9:16) project

const bundle = createEmptyBundle('Vertical', '9:16')
// width: 1080, height: 1920

Add an item to a bundle before loading

import { createEmptyBundle, generateItemId, createDefaultTransform } from '@studio-dev/vsdk'

const bundle = createEmptyBundle()
bundle.items.push({
  id: generateItemId(),
  trackId: 'trk_main',
  type: 'image',
  name: 'Logo',
  startFrame: 0,
  durationFrames: 90,
  source: { assetUrl: 'https://...' },
  transform: createDefaultTransform(bundle.settings),
  style: {},
  filters: [],
  zIndex: 0,
  locked: false,
  disabled: false,
  muted: false,
})

<VideoStudioProvider initialBundle={bundle}>

Validate then load

import { projectBundleSchema, migrateBundle } from '@studio-dev/vsdk/contracts'

const parsed = projectBundleSchema.safeParse(raw)
if (!parsed.success) throw new Error('Invalid project bundle')
const upgraded = migrateBundle(parsed.data)
storeApi.getState().loadBundle(upgraded)

On this page