Make videos programmatically with Vue 3 — a Vue-native port of Remotion.
Status:
0.1.0candidate. Vue primitives, scrubber preview, headless renderer with worker pool,<Sequence>,<Audio>,delayRender, tests, CI, and a docs site are in. Not yet published to npm.
pnpm install
pnpm dev:hello # scrubber preview of examples/hello-world
pnpm --filter @vumo/example-hello-world render # → out.mp4pnpm dev:hello opens a Vite dev server with a scrubber, play/pause, and keyboard shortcuts (space, ←/→, J/K/L). The render script produces a deterministic h264 MP4.
import { useCurrentFrame, useVideoConfig } from '@vumo/core';
const frame = useCurrentFrame(); // Ref<number>
const { width, height, fps, durationInFrames } = useVideoConfig();Anything that reads frame.value re-renders as the timeline advances. The video config is provided by whoever mounts the composition (preview UI or render harness).
import { defineComposition } from '@vumo/core';
import MyComposition from './MyComposition.vue';
defineComposition({
id: 'hello-world',
component: MyComposition,
width: 1280,
height: 720,
fps: 30,
durationInFrames: 150,
});Compositions live in a registry that both the preview UI and the renderer read from. Each project's src/main.ts becomes a one-liner:
import { vumoMount } from '@vumo/preview';
import './compositions';
vumoMount('#app');vumoMount checks the URL: ?vumoRender=1 switches to a headless harness that the renderer drives via window.__vumoSelectComposition / __vumoSetFrame. Otherwise it shows the scrubber preview.
<script setup lang="ts">
import { Sequence } from '@vumo/core';
</script>
<template>
<Sequence :from="0" :duration-in-frames="60"><Intro /></Sequence>
<Sequence :from="60" :duration-in-frames="60"><Middle /></Sequence>
<Sequence :from="120" :duration-in-frames="60"><Outro /></Sequence>
</template>Inside each <Sequence>, useCurrentFrame() returns a frame relative to that sequence's from. Children only mount while from <= globalFrame < from + durationInFrames.
<script setup lang="ts">
import { Audio } from '@vumo/core';
</script>
<template>
<Audio src="/bg.wav" :from="0" :duration-in-frames="180" :volume="0.4" />
<Audio src="/beep-a.wav" :from="30" :duration-in-frames="6" :volume="0.9" />
<Audio src="/beep-b.wav" :from="90" :duration-in-frames="6" :volume="0.9" />
</template><Audio> renders nothing visible — it registers an audio cue (src + global frame window + volume + optional loop/startOffset) into a per-page registry. The scrubber preview drives HTML5 <audio> elements that sync to the frame timer. The renderer collects cues from every worker page after capture, downloads each source from the dev server, and muxes them into the MP4 via FFmpeg's adelay + amix filters.
import { onMounted } from 'vue';
import { delayRender, continueRender } from '@vumo/core';
onMounted(async () => {
const handle = delayRender('load profile image');
await fetch('/assets/avatar.png'); // or font loading, etc.
continueRender(handle);
});The renderer won't capture a frame until every pending handle has been continued.
vumo render <compositionId> [options]
-p, --project <path> Project root (default: cwd)
-o, --output <path> Output MP4 (default: out.mp4)
--crf <number> H.264 CRF, lower = better (default: 18)
--workers <number> Parallel render workers (default: min(cpu, 4))
- Spin up a Vite dev server in the project root (programmatic API, HMR disabled).
- Launch headless Chromium via Puppeteer. Inject a determinism shim that seeds
Math.randomand clampsDate.now/performance.nowto frame time. - Probe the page to enumerate registered compositions.
- Spawn N worker pages, each navigating to
?vumoRender=1and callingwindow.__vumoSelectComposition(id). - Round-robin distribute frame indices across workers. Each capture:
__vumoReseed(frame)+__vumoSetFrame(frame)inpage.evaluate, flush Vue's microtask queuewaitForFunction(() => __vumoReadyForCapture())— synchronous check fordocument.fonts.statusand pendingdelayRenderhandlespage.screenshot({ clip: ..., type: 'png' })
- Pipe frames into FFmpeg (
ffmpeg-static) for H.264 encode (libx264 -pix_fmt yuv420p -crf 18).
packages/
core/ Vue primitives — useCurrentFrame, useVideoConfig,
defineComposition, Sequence, delayRender/continueRender
preview/ Browser UI — VumoPreview scrubber + vumoMount router
+ render harness
renderer/ Node — Vite + Puppeteer + FFmpeg pipeline
cli/ `vumo render` command
examples/
hello-world/ Single composition, rotating pulsing square
sequences-demo/ Three Sequence clips + delayRender
audio-demo/ Background tone + four staggered SFX beeps
packages/
skills/ Agent skill files (Claude Code / Cursor / Codex)
describing vumo best practices. Published as
@vumo/skills.
- Phase 1 — Vue primitives + scrubber preview
- Phase 2 — Puppeteer + FFmpeg → MP4
- Phase 3 —
<Sequence>,delayRender, worker pool - Phase 4 —
<Audio>+ audio mux - Phase 5 — Docs, Vitest suite, GitHub Actions CI, Changesets
- Production-bundle render path (skip Vite dev server → real worker-count scaling)
<Video>for embedded clips- Sequence-aware
<Audio>(auto-shiftfromby parent Sequence offset) interpolate/springanimation helpers
Heavily inspired by Remotion — the React framework that pioneered "videos as code." vumo aims to bring the same model to Vue 3.
MIT.