Video Studio SDKv0.0.3
Getting started

Quickstart

Minimal working editor, then add plugins, then load a real project.

This page is three escalating examples: a smoke-test editor with the defaults, the same editor with two official plugins wired up, and the production pattern for loading a project asynchronously.

1. Minimal editor

The smallest amount of code that gets you a working editor on screen. The provider creates an empty 1920×1080 @ 30fps project under the hood — you can add tracks and items immediately.

app/editor/editor.tsx
'use client'

import { StudioEditor, VideoStudioProvider } from '@studio-dev/vsdk'
import '@studio-dev/vsdk/styles.css'

export default function Editor() {
  return (
    <VideoStudioProvider theme="dark">
      <StudioEditor className="h-screen" />
    </VideoStudioProvider>
  )
}

That's it. No callbacks, no plugins, no bundle. The editor mounts, the toolbar/timeline/inspector render, hotkeys work (Space, Cmd+Z, Delete, …). Save and Export buttons appear but do nothing yet — the next sections wire them up.

2. With plugins

Plugins are installed independently and registered via the plugins prop. Each plugin's factory takes optional config — most have sensible defaults.

app/editor/editor.tsx
'use client'

import { useMemo } from 'react'
import { StudioEditor, VideoStudioProvider } from '@studio-dev/vsdk'
import { filtersPlugin } from '@studio-dev/vsdk-plugin-filters'
import { typographyPlugin } from '@studio-dev/vsdk-plugin-typography'
import { gifPlugin, createGifAdapter } from '@studio-dev/vsdk-plugin-gif'
import {
  stockAssetPlugin,
  createStockAssetAdapter,
} from '@studio-dev/vsdk-plugin-stock'
import '@studio-dev/vsdk/styles.css'

export default function Editor() {
  // Memoize: plugin factories build a fresh definition each call.
  const plugins = useMemo(
    () => [
      filtersPlugin(),
      typographyPlugin(),
      gifPlugin({
        adapter: createGifAdapter({
          giphy: { apiKey: process.env.NEXT_PUBLIC_GIPHY_KEY! },
        }),
      }),
      stockAssetPlugin({
        adapter: createStockAssetAdapter({
          pexels: { apiKey: process.env.NEXT_PUBLIC_PEXELS_KEY!, mediaType: 'mixed' },
          freesound: { apiKey: process.env.NEXT_PUBLIC_FREESOUND_KEY!, minDuration: 10 },
        }),
      }),
    ],
    [],
  )

  return (
    <VideoStudioProvider plugins={plugins} theme="dark" defaultActivePanel="filters">
      <StudioEditor className="h-screen" />
    </VideoStudioProvider>
  )
}

Each plugin contributes one or more sidebar panels and (for filters, typography) inspector sections — they appear automatically without any extra wiring on your side.

3. Loading a project asynchronously

The provider accepts an initialBundle synchronously, but real apps fetch projects by ID. Two patterns are common:

Pattern A — fetch outside, mount with the bundle

Suspense or a top-level loading state on your page, then render the provider once you have a bundle:

app/editor/page.tsx
import { Editor } from './editor'

async function loadProject(id: string) {
  const res = await fetch(`${process.env.API_URL}/projects/${id}`, {
    next: { revalidate: 0 },
  })
  return res.json()
}

export default async function Page({ params }: { params: { id: string } }) {
  const bundle = await loadProject(params.id)
  return <Editor bundle={bundle} />
}
app/editor/editor.tsx
'use client'
import { StudioEditor, VideoStudioProvider, type ProjectBundle } from '@studio-dev/vsdk'
import '@studio-dev/vsdk/styles.css'

export function Editor({ bundle }: { bundle: ProjectBundle }) {
  return (
    <VideoStudioProvider initialBundle={bundle}>
      <StudioEditor className="h-screen" />
    </VideoStudioProvider>
  )
}

Pattern B — mount empty, load with setLoading overlay

When the bundle isn't ready at mount time, use the store's setLoading action to show a blurred loading overlay over the editor:

'use client'

import { useEffect } from 'react'
import {
  StudioEditor,
  VideoStudioProvider,
  useStudioStoreApi,
} from '@studio-dev/vsdk'
import '@studio-dev/vsdk/styles.css'

function ProjectLoader({ projectId }: { projectId: string }) {
  const storeApi = useStudioStoreApi()

  useEffect(() => {
    storeApi.getState().setLoading(true)
    fetch(`/api/projects/${projectId}`)
      .then((r) => r.json())
      .then((bundle) => storeApi.getState().loadBundle(bundle))
      .finally(() => storeApi.getState().setLoading(false))
  }, [projectId, storeApi])

  return null
}

export default function Editor({ projectId }: { projectId: string }) {
  return (
    <VideoStudioProvider>
      <ProjectLoader projectId={projectId} />
      <StudioEditor className="h-screen" />
    </VideoStudioProvider>
  )
}

The setLoading(true) overlay covers the whole editor with a spinner. setLoading(false) removes it. Calling loadBundle resets selection, history, and playback — it's a hard load.

Saving and exporting

Wire onSave and onExport once you have a backend:

<VideoStudioProvider
  initialBundle={bundle}
  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) })
    // Drive the SDK's export UI via store actions as your render job progresses
    // (setExportProgress / setExportCompleted / setExportFailed).
  }}
>

See Configuration for the full callback signatures.

What's next

On this page