Skip to content

About

Exemple of video editor with react-native-skia-video

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Island Studio — a tiny React Native video editor

A compact, readable video editor built with Expo, React Native Skia and @azzapp/react-native-skia-video. It is meant to be read as a tutorial: every moving part of a mobile video editor — timeline, GPU compositing, shader transitions, draggable overlays, audio mixing, hardware-encoded export — in ~15 small files.

Island Studio demo

Pick clips → add music, text & images → choose a transition → export to your photo library.
▶ Full demo video (with sound)

Features

  • 🎞 Montage of up to 6 photos/videos from your gallery — or from 5 bundled island-themed samples (photos get a slow Ken Burns zoom)
  • 🎵 Soundtrack: 3 bundled royalty-free tracks, looped over the montage and mixed into the export (gallery videos keep their own sound)
  • ✍️ Text & image overlays: drag, pinch and rotate them right on the preview, pick font/color, give them an enter/exit animation
  • ✨ Transitions between clips, written as Skia (SkSL) shaders: fade, slide, wipe, cross-zoom
  • 📤 Export to a real 1080×1920@30 H.264 file, saved to the photo library

Running it

The app uses native modules (Skia, skia-video), so it needs a development build — Expo Go won't work:

npm install
npx expo prebuild
npx expo run:ios      # or: npx expo run:android

The one idea worth stealing

Everything on screen is drawn by a single worklet, src/editor/drawFrame.ts:

drawFrame({ canvas, currentTime, frames, state })
   ├─ clips + transition        (video frames arrive as GPU textures)
   ├─ image overlays
   └─ text overlays
  • The preview calls it on every vsync via useVideoCompositionPlayer.
  • The export calls it for every encoded frame on a background thread via exportVideoComposition, feeding the resulting texture straight to the hardware encoder.

Same function, same state → what you preview is exactly what you export. No duplicated rendering logic to keep in sync, ever.

Project tour

src/
├── editor/               the engine (no UI)
│   ├── types.ts          the data model — geometry stored as canvas fractions
│   ├── state.ts          plain useReducer, ~10 actions
│   ├── timeline.ts       clip timing + VideoComposition builder
│   ├── transitions.ts    SkSL transition shaders
│   ├── layerAnimations.ts overlay enter/exit poses
│   ├── drawFrame.ts      ★ the renderer (preview AND export)
│   ├── skiaCache.ts      per-runtime decode cache (images, paragraphs)
│   ├── assetRegistry.ts  raw image bytes, shared across runtimes
│   ├── fonts.ts          the shared Skia typeface provider
│   ├── assets.ts         bundled samples, music and fonts
│   └── useExport.ts      export + save to the photo library
├── components/
│   ├── VideoPreview.tsx  player + overlay gestures + playback bar
│   ├── ClipStrip.tsx     reorder/remove clips
│   ├── Toolbar.tsx       tools / contextual layer actions
│   ├── sheets/           one bottom sheet per tool
│   └── ui/               small shadcn-style primitives (button, text, sheet…)
└── screens/EditorScreen.tsx

How the tricky parts work

Timeline with transitions — clips have durations, not start times. Start times are derived by an accumulator where each clip overlaps the previous one by the transition duration. Inside the overlap two clips are live: the drawer hands both to the transition shader, which blends them (timeline.ts, drawFrame.ts).

Transitions are shaders — each one is a few lines of SkSL taking the two frames as child shaders and a progress uniform. Adding a new transition is copy-pasting a shader from gl-transitions into transitions.ts.

Gestures never re-render React — dragging writes {dx, dy, scale, rotation} into a reanimated shared value that drawFrame merges over the selected layer on the UI thread; React state is only updated once, when the fingers lift (VideoPreview.tsx). Layer hit-testing reuses the bounds that drawFrame computed while drawing.

Resolution independence — all layer geometry lives in fractions of the canvas, so the ~200pt preview and the 1080×1920 export render the exact same composition.

Assets across threads — images are kept as Uint8Arrays (assetRegistry.ts) and each runtime decodes and caches its own SkImages; the typeface provider is created once on the JS thread and captured by the worklets (fonts.ts). Paragraphs are laid out per runtime and cached — never rebuilt per frame (skiaCache.ts).

Audio — the soundtrack is just kind: 'audio' items in the VideoComposition; the native module plays them during preview and mixes them into the exported file. Tracks shorter than the montage are chained back-to-back (timeline.ts).

Bundled assets

All bundled content is original and free to reuse: the island artwork and sample videos are procedurally generated vector scenes, and the three music tracks were synthesized for this project (no samples). Fonts (Poppins, Pacifico, Archivo Black) come from Google Fonts under the SIL Open Font License; icons are Remix Icon (Apache 2.0).

Credits

Heavily inspired by the cover editor of Azzapp, which pioneered the "one worklet for preview and export" architecture on top of @azzapp/react-native-skia-video.

MIT — see LICENSE.

About

Exemple of video editor with react-native-skia-video

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages