<meta property="og:video">

og:video from a URL, composited in Rust at the edge.

URL params in, a streaming MP4 out. A Rust compositor and x264, both in wasm, render each frame inside a Cloudflare Worker and stream the file out faster than it plays. iMessage, Slack and Discord play it inline.

<!-- that's the whole integration -->
<meta property="og:video" content="https://og.video/v1/title-card.mp4?title=We+shipped+it&kicker=Launch&brand=ACME&url=example.com">
<meta property="og:video:type" content="video/mp4">
<meta property="og:image" content="https://og.video/v1/title-card.png?title=We+shipped+it&kicker=Launch&brand=ACME&url=example.com">
  • 100 to 200 msto render a 7 s card
  • ~2 mson a cache hit
  • 38 MBcompositor + encoder, in wasm
9:41
3
M
Maya
Today 9:41 AM
did the launch post go out?
just now. here's the preview
Delivered
whoa how did you do that
iMessage

Direct video compositing, in a Worker

A scenario is a pure function of (params, frame). The pipeline runs it once per video.

GET /v1/kinetic.mp4?text=…  ──► edge cache hit ───────────────────────────────► MP4, ~2 ms
                       ──► R2 hit ───────────────────────────────────────► MP4, streamed
                       ──► miss:
  1. write the prefix: opening frames, pre-encoded once per prefix key
  2. render the rest live, one fMP4 fragment per frame
       GSAP timeline ──► Rust/wasm compositor (tiny-skia + SIMD) ──► x264 (wasm) ──► moof/mdat ──► you
  3. store the finished file in R2 + the edge cache
Rust compositortiny-skia with SIMD, compiled to wasm. Rasterizes shapes once, restores cached layers by memcpy, converts to I420 and flags the 16×16 blocks that changed.
x264, in wasmEncodes only the macroblocks the compositor flagged. A still frame costs almost nothing.
Streams ahead of playbackThe prefix ships on the first byte. The live tail renders 10 to 30× faster than realtime, so the player never waits.
Renders onceCache key: content hash of the params + scenario version + engine version. Finished files live in R2 and the edge cache. iOS byte ranges come from the cached file.

Six scenarios

Each one is live. Edit the fields and the URL updates; fields left at their default stay out of it. Arrays and objects are JSON.

/kinetic

1200×630 · 7 s · H.264

Open share page

/title-card

1200×630 · 7 s · H.264

Open share page

/falling-words

1200×630 · 7 s · H.264

Open share page

/dot-matrix

1200×630 · 7 s · H.264

Open share page

/gallery

1200×630 · 7 s · H.264

Open share page

/diagram

1200×630 · 7 s · H.264

Open share page

/metric

1200×630 · 7 s · H.264

Open share page

/bars

1200×630 · 7 s · H.264

Open share page

/release

1200×630 · 7 s · H.264

Open share page

API

Everything is a GET on a query string. Keys a scenario doesn't declare (utm_source) are ignored and don't touch the cache key. Cache misses are rate limited per IP.

RequestReturns
GET /v1/<scenario>.mp4?<params>The video. Streamed on a miss, byte-ranged on a hit.
GET /v1/<scenario>.png?<params>The poster frame, for og:image.
GET /v1/<scenario>?<params>A share page with the og:video and og:image tags set.
GET /v1/<scenario>.json?<params>Resolved params and defaults, content id, links, and a packed ?s= URL about 3× shorter.
POST /v1/<scenario>JSON params in, a short /v1/<scenario>/<id>.mp4 out. The id is a content hash.

For agents

Agents: add https://og.video/mcp as a remote MCP server, or npx skills add https://og.video. The server builds these URLs from params, or designs a card from a description. The skill covers the same API.

EndpointReturns
POST /mcpA Streamable HTTP MCP server with two tools: og_video_url (params to URLs and og tags) and og_video_from_prompt (a description to a design).
GET /.well-known/agent-skills/The og-video agent skill, listed in index.json.
GET /llms.txtA Markdown overview of the site for LLMs.

Self-host it

og.video runs streaming-og-video unchanged. Deploy the same Worker on your Cloudflare account, with your own scenarios.

# one dependency, no build step
npm install github:runtypelabs/og-video
// src/index.js
import { createHandler, kvStore } from 'streaming-og-video/worker'
import { titleCard, kinetic } from 'streaming-og-video/scenarios'

export default createHandler({
  scenarios: [titleCard, kinetic],
  bucket: 'RENDERS',        // R2: videos, posters, prefixes
  store: kvStore({ kv: 'STATE', r2: 'RENDERS' }),
  secret: 'OGV_SECRET',     // signed URLs
  rateLimit: 'RENDER_LIMIT',
})

A scenario is a small program: GSAP on plain objects for the timeline, a draw function for cached layers and live overlays. npx sogv stills, render and bench iterate locally; the repo ships an agent skill that knows the API. Write a scenario.