Video Studio SDKv0.0.3
Getting started

Configuration

Every prop on VideoStudioProvider, explained — what it does, when to set it, the type shape, defaults, gotchas, and worked recipes for each.

VideoStudioProvider is the root of the SDK. Everything below it has access to the store, the plugin registry, the icon map, and the config context. This page is the complete prop reference — every option, its type, its default, when to set it, common gotchas, and at least one worked example.

The full shape

interface VideoStudioProviderProps {
  // ─── Required ────────────────────────────────────────────────────────────
  children: ReactNode

  // ─── Project ──────────────────────────────────────────────────────────────
  initialBundle?: ProjectBundle
  plugins?:       PluginDefinition[]

  // ─── Appearance ───────────────────────────────────────────────────────────
  theme?:          'light' | 'dark' | 'system'
  themeOverrides?: ThemeOverrides
  icons?:          Partial<StudioIconMap>
  logo?:           ReactNode
  className?:      string
  rootClassName?:  string

  // ─── Layout extensions ────────────────────────────────────────────────────
  defaultActivePanel?: string
  sidebarButtons?:     CustomSidebarButton[]

  // ─── Callbacks ────────────────────────────────────────────────────────────
  onSave?:        (bundle: ProjectBundle)        => void | Promise<void>
  onExport?:      (payload: ExportPayload)       => void | Promise<void>
  onExportAbort?: (payload: ExportAbortPayload)  => void | Promise<void>

  // ─── Subsystems ───────────────────────────────────────────────────────────
  masking?: MaskingConfig
  routes?:  ApiRoutesConfig

  // ─── License (required in production) ─────────────────────────────────────
  licenseKey?:       string
  licenseServerUrl?: string
}

Everything except children is optional. The rest of this page walks through each group, in the order you'll typically reach for them.


Project

initialBundle

A serialized project to load on mount. If omitted, the provider creates an empty project (one "Main" track, default 1920×1080 @ 30fps, 30s duration).

<VideoStudioProvider initialBundle={savedBundle}>
  <StudioEditor />
</VideoStudioProvider>

ProjectBundle shape (abbreviated — see Project bundle for full):

interface ProjectBundle {
  schemaVersion: number
  metadata: {
    id:        string | null
    name:      string
    createdAt: string
    updatedAt: string
  }
  settings:    ProjectSettings
  tracks:      Track[]
  items:       TrackItem[]
  markers:     Marker[]
  regions:     Region[]
  transitions?: Record<string, unknown>[]
}

For async loading (fetch by id, show a spinner, etc.) see the Quickstart → Loading a project recipe.

initialBundle is only read on mount. Changing it later does nothing — the SDK doesn't track it after the initial load. To load a new project at runtime, call loadBundle(bundle) via useStudioStoreApi.

plugins

An array of PluginDefinition objects, typically created by calling each plugin's factory function. Plugins are registered in array order.

import { filtersPlugin } from '@studio-dev/vsdk-plugin-filters'
import { typographyPlugin } from '@studio-dev/vsdk-plugin-typography'

<VideoStudioProvider
  plugins={[filtersPlugin(), typographyPlugin()]}
>
  <StudioEditor />
</VideoStudioProvider>

The provider compares plugins by reference identity. Pass a stable reference — either a useMemo or a module-level constant — or you'll re-register every plugin on every render.

const plugins = useMemo(() => [filtersPlugin(), typographyPlugin()], [])

See Plugin authoring to write your own.


Appearance

theme

ValueBehavior
'light'Always light mode
'dark'Always dark mode
'system' (default)Follow OS preference, reactive to changes

The provider applies .dark to its root <div> when dark mode is active. Tailwind classes like dark:bg-… work as expected. You can also read the resolved theme via the data-theme attribute on the root: data-theme="light" or data-theme="dark".

themeOverrides

Override individual theme tokens. Values are raw RGB channel strings (e.g. "124 58 237") so the SDK can compose them with opacity utilities.

Single-mode (applies to both light and dark):

<VideoStudioProvider
  themeOverrides={{
    primary:      '124 58 237',
    studioAccent: '124 58 237',
    studioAppBg:  '10 10 12',
  }}
>

Per-mode (different values for light vs dark):

<VideoStudioProvider
  themeOverrides={{
    light: { primary: '30 80 50',    studioAppBg: '245 245 245' },
    dark:  { primary: '155 120 255', studioAppBg: '20 18 28' },
  }}
>

The full list of tokens and their meanings is in UI → Color system. Recipes for common rebrands live in Theming & customization.

icons

Override any icon used by the SDK. Each key is a semantic name from StudioIconName; each value is a React component that accepts { className?: string }.

import { Play, Pause, Save } from 'lucide-react'

// Define OUTSIDE the component (or wrap in useMemo) to keep the reference stable.
const ICONS = {
  play:  Play,
  pause: Pause,
  save:  Save,
}

export default function Editor() {
  return (
    <VideoStudioProvider icons={ICONS}>
      <StudioEditor />
    </VideoStudioProvider>
  )
}

Override precedence: defaults < plugin-registered icons < icons prop. The user always wins.

The full icon-key list and override patterns are in UI → Icon system.

Custom logo for the toolbar. Accepts any ReactNode:

<VideoStudioProvider logo={<img src="/logo.svg" className="h-5 w-auto" />}>
<VideoStudioProvider logo={<MyAnimatedLogo />}>
<VideoStudioProvider logo="Acme Studio">                                {/* string → text */}
<VideoStudioProvider logo={null}>                                       {/* default VideoIcon */}

The toolbar reserves a logo slot of ~24px height — design assets to fit within that.

className / rootClassName

Both apply to the provider's root <div> (where the .dark class is also applied). Both exist for historical reasons — use whichever fits your code-base style.

<VideoStudioProvider className="h-screen w-screen" />
<VideoStudioProvider rootClassName="bg-zinc-950" />     {/* equivalent for class purposes */}

If you set both, they're concatenated. The root also has the immovable vsdk-root class and the data-theme attribute.


Layout extensions

defaultActivePanel

The id of the plugin panel to open by default in the sidebar. If omitted, the user starts with no panel selected.

<VideoStudioProvider defaultActivePanel="stock">

The id must match a registered panel's id. Built-in plugins use their package's short name (filters, gif, stock, typography, etc.).

sidebarButtons

Custom buttons rendered at the bottom of the desktop icon sidebar and at the start of the mobile bottom bar. Use this for app-level navigation (e.g. "Back to dashboard", account menu) that needs to live alongside the editor.

import { LogOutIcon, SettingsIcon } from 'lucide-react'

const BUTTONS = [
  { id: 'back',     label: 'Dashboard', icon: LogOutIcon,   onClick: () => router.push('/') },
  { id: 'settings', label: 'Settings',  icon: SettingsIcon, onClick: () => openSettings() },
]

<VideoStudioProvider sidebarButtons={BUTTONS}>

CustomSidebarButton shape:

interface CustomSidebarButton {
  id:      string                                          // stable React key
  label:   string                                          // tooltip + mobile caption
  icon:    ComponentType<{ className?: string }>
  onClick: () => void
}

Memoize the array (or define outside the component) for stable references.


Callbacks

onSave

Called when the user clicks Save. Receives the current ProjectBundle. Your job: persist it.

<VideoStudioProvider
  onSave={async (bundle) => {
    await fetch(`/api/projects/${bundle.metadata.id}`, {
      method: 'PUT',
      body: JSON.stringify(bundle),
    })
    // The SDK marks the project clean automatically once the promise resolves.
  }}
>

Behavioral details:

  • The SDK calls setSyncing(true) before invoking onSave and setSyncing(false) after the promise resolves or rejects.
  • The SDK calls markClean() after the promise resolves successfully.
  • If onSave throws or rejects, the project stays dirty and the toolbar shows the syncing indicator going off — but the SDK won't surface the error to the user. Catch and toast yourself.
  • If onSave is not provided, clicking Save emits the project:save-request event on the event bus — useful when you want to handle saving from outside the provider tree.

onExport

Called when the user starts an export. Receives an ExportPayload:

interface ExportPayload {
  projectId: string
  profileId: string
  profile:   RenderProfile             // width, height, fps, bitrate, codec
  bundle:    ProjectBundle
}

Your job: kick off a render on your backend and drive the SDK's export UI by calling store actions as the render progresses:

<VideoStudioProvider
  onExport={async (payload) => {
    const storeApi = /* obtain via useStudioStoreApi() inside the tree */
    const job = await startRender(payload)

    job.on('progress', ({ progress, stepLabel }) => {
      storeApi.getState().setExportProgress(progress, stepLabel)
    })
    job.on('done', ({ downloadUrl }) => {
      storeApi.getState().setExportCompleted(downloadUrl)
    })
    job.on('error', ({ message }) => {
      storeApi.getState().setExportFailed(message)
    })
  }}
>

The export-related store actions are: startExport, setExportPhase, setExportProgress, setExportCompleted, setExportFailed, abortExport, resetExport. See Core → Store → Export actions for the full signatures and Save & Export for the full flow.

onExportAbort

Called when the user clicks Cancel during an active export. The SDK has already set the phase to 'aborting' — your job is to tell your backend to actually stop the render job, then call resetExport() once the backend acknowledges.

<VideoStudioProvider
  onExportAbort={async ({ projectId, profileId, reason }) => {
    await fetch(`/api/renders/${projectId}/cancel`, { method: 'POST' })
    storeApi.getState().resetExport()
  }}
>

ExportAbortPayload:

interface ExportAbortPayload {
  projectId: string
  profileId: string | null           // may be null if aborted before profile was set
  profile:   RenderProfile | null
  reason:    string | null           // optional reason passed to abortExport()
}

Subsystems

masking

Configure the masking system (rectangle / ellipse / polygon / split / filmstrip masks on image and video items).

interface MaskingConfig {
  enabled?:          boolean                              // default: true
  enabledMaskTypes?: MaskType[]                           // default: all 5
  maxMasksPerItem?:  number                               // default: 10
  defaultFeather?:   number                               // default: 0
  defaultExpansion?: number                               // default: 0
}
<VideoStudioProvider
  masking={{
    enabledMaskTypes: ['rectangle', 'ellipse'],
    maxMasksPerItem:  3,
  }}
>

Set enabled: false to hide the masking UI entirely.

Allowed MaskType values: 'rectangle', 'ellipse', 'polygon', 'split', 'filmstrip'.

routes

Override API route paths used by built-in modules. Currently only storage is configurable. All defaults match the reference backend.

interface ApiRoutesConfig {
  storage?: {
    list?:                   string  // default: '/v1/user/storage'
    get?:                    string  // default: '/v1/user/storage/{id}'
    delete?:                 string  // default: '/v1/user/storage/{id}'
    uploadRegular?:          string  // default: '/v1/user/storage/upload/regular'
    uploadChunkedInit?:      string  // default: '/v1/user/storage/upload/chunked/init'
    uploadChunkedChunk?:     string  // default: '/v1/user/storage/upload/chunked/chunk'
    uploadChunkedComplete?:  string  // default: '/v1/user/storage/upload/chunked/complete'
    uploadChunkedAbort?:     string  // default: '/v1/user/storage/upload/chunked/abort'
  }
}

{id} is a placeholder the SDK substitutes at request time.

<VideoStudioProvider
  routes={{
    storage: {
      list:   '/api/assets',
      get:    '/api/assets/{id}',
      delete: '/api/assets/{id}',
    },
  }}
>

License

licenseKey

Your production license key (stdk_live_…). The editor renders a license overlay when no valid key is present. Set this in production builds.

<VideoStudioProvider licenseKey={process.env.NEXT_PUBLIC_VSDK_LICENSE_KEY}>

Development keys (stdk_test_…) work without a server round-trip and are intended for local / preview builds.

licenseServerUrl

Override the license validation endpoint. Defaults to the public Studio license server. Set this if you self-host the validation service:

<VideoStudioProvider
  licenseKey="stdk_live_…"
  licenseServerUrl="https://license.acme.com"
>

Read license state inside the tree via useLicense():

const { status, plan } = useLicense()
// status: 'valid' | 'invalid' | 'unknown' | 'loading'
// plan:   'free' | 'pro' | 'enterprise' | null

Putting it together — production patterns

Pattern A — Single-project editor with backend

'use client'

import { useMemo } from 'react'
import {
  StudioEditor,
  VideoStudioProvider,
  type ProjectBundle,
} from '@studio-dev/vsdk'
import { filtersPlugin } from '@studio-dev/vsdk-plugin-filters'
import { typographyPlugin } from '@studio-dev/vsdk-plugin-typography'
import '@studio-dev/vsdk/styles.css'

import { Logo } from './logo'
import { ICONS } from './icons'

export function Editor({ bundle }: { bundle: ProjectBundle }) {
  const plugins = useMemo(() => [filtersPlugin(), typographyPlugin()], [])

  return (
    <VideoStudioProvider
      initialBundle={bundle}
      plugins={plugins}
      theme="dark"
      logo={<Logo />}
      icons={ICONS}
      defaultActivePanel="filters"
      licenseKey={process.env.NEXT_PUBLIC_VSDK_LICENSE_KEY}
      onSave={async (b) => {
        await fetch(`/api/projects/${b.metadata.id}`, {
          method: 'PUT',
          body: JSON.stringify(b),
        })
      }}
      onExport={async (payload) => {
        await fetch('/api/renders', {
          method: 'POST',
          body: JSON.stringify(payload),
        })
        // The render job will call back via webhooks to drive setExportProgress/Completed/Failed.
      }}
    >
      <StudioEditor className="h-screen" />
    </VideoStudioProvider>
  )
}

Pattern B — Multi-tenant editor (per-tenant theme)

function tenantOverrides(tenantId: string): ThemeOverrides {
  return TENANT_THEMES[tenantId] ?? {}
}

<VideoStudioProvider
  theme="system"
  themeOverrides={tenantOverrides(tenantId)}
  icons={tenantIcons(tenantId)}
  logo={<TenantLogo tenantId={tenantId} />}
  defaultActivePanel="brand-kit"
  plugins={tenantPlugins(tenantId)}
>

Pattern C — Read-only embed (e.g. preview iframe)

<VideoStudioProvider
  initialBundle={bundle}
  theme="dark"
  /* no onSave — Save button emits an event your host can choose to ignore */
  /* no onExport — Export button does nothing visible */
  plugins={[]}                            // strip down to the bare editor
>
  <StudioEditor className="h-full pointer-events-none select-none" />
</VideoStudioProvider>

For true read-only — disabling timeline interactions — combine the above with track.locked: true on every track via setTrackFlags.


Defaults at a glance

PropDefault
theme'system'
themeOverridesnone
iconsnone (uses DEFAULT_ICON_MAP)
logodefault VideoIcon
plugins[]
defaultActivePanelnone (no panel open)
sidebarButtons[]
masking{ enabled: true, enabledMaskTypes: all-five, maxMasksPerItem: 10, defaultFeather: 0, defaultExpansion: 0 }
routes.storagethe eight /v1/user/storage/* defaults listed above
licenseKeynone (license overlay shown until set)
licenseServerUrlpublic Studio license server
onSave / onExport / onExportAbortnone (Save / Export buttons emit events instead)

Common gotchas

ProblemFix
Plugins re-register on every renderuseMemo(() => [...], []) for the plugins array, or hoist to module scope.
icons prop is ignoredSame — wrap in useMemo. Identity-based comparison.
Theme tokens have no visible effectWrong format — values are raw RGB triplets like "124 58 237", not "#7c3aed".
Editor renders unstyledForgot import '@studio-dev/vsdk/styles.css' somewhere in the app.
defaultActivePanel="stock" doesn't open anythingThe matching plugin isn't registered. The id must match a registered panel's id.
onSave finishes but the project stays "dirty"onSave threw / rejected — the SDK won't catch it. Wrap in try/catch and toast yourself.
License overlay appears in productionlicenseKey is undefined — check env var name and NEXT_PUBLIC_ prefix.

What's next

On this page