Documentation
Architecture

Media Pipeline

For developers

Direct-to-Bunny resumable uploads, encoding webhooks and short-lived signed playback URLs — video never passes through the application servers.

Video never passes through the Orochia servers: the browser edits it, sends it straight to Bunny Stream over Tus, Bunny encodes it and reports back through a signed webhook, and every play is a short-lived signed URL issued only after the app has decided the viewer may watch.


1. Edit in the browser (optional)

Before an upload the creator can open the editor — trim with a filmstrip and two handles, speed 0.5–2×, 14 looks, brightness / contrast / colour, crop to 9:16 or 1:1 by dragging the picture, sound volume up to 200 %, fades, noise reduction and a music track mixed in. The preview is live (CSS, Web Audio); Apply renders the exact result with ffmpeg.wasm on the device (H.264 / AAC MP4, 1080 px on the short side at most). Nothing is uploaded to do it.

Editing is non-destructive: the original stays on the device and reopens with its last settings. Stories are always vertical 9:16 and at most 60 seconds — a story clip goes through the editor before it can be shared.

Drafts. Save draft keeps the original at Bunny (drafts collection, sent once over Tus), the settings and the form in video_drafts and the music privately in storage, so the edit continues on any device. Saving again only updates the settings. A draft is removed with its files when published, deleted, or after DRAFT_RETENTION_DAYS (30).

2. Limits

One table, packages/media/src/limits.ts, read by the browser and the server:

WhatSizeLength
Video4 GB3 hours
Story clip250 MB60 seconds
Draft original (what the editor opens)400 MB—
Draft music25 MB—

The browser refuses before sending; the upload session refuses a larger declared size; the webhook reads the encoded length and deletes at Bunny a video or story longer than allowed (status FAILED).

3. Direct-to-Bunny Tus upload

Story videos (/api/stories/upload-session) and draft originals (/api/me/drafts) take the same path into their own collections.

4. Encoding webhook

POST /api/webhooks/bunny — signature v1 (HMAC-SHA256 over the raw body with the library's Read-Only key, BUNNY_WEBHOOK_SECRET), compared in constant time.

Bunny statusOrochia
0 queued · 1 processing · 2 encoding · 4 resolution finished · 6–7 presigned upload started / finishedPROCESSING
3 finishedREADY — length, renditions and thumbnail read from the Stream API
5 failed · 8 presigned upload failedFAILED
9–10 captions / title generatedignored

Events can arrive late, twice or out of order: a READY video never goes back to PROCESSING, and an event that changes nothing is ignored. The GUID finds a video, else a story (its 24 hours start at READY), else a draft.

When the webhook never comes (stories)

A library without a webhook URL, an app Bunny cannot reach (localhost, a second environment on the same library) or a lost delivery used to leave a story PENDING_UPLOAD forever — invisible, even to its author. Now:

  • The author always sees their story. The web rail asks /api/stories?pending=1: the author's own video stories come with state: "processing" (badge, no playback, no view counted) or "failed", and the rail refreshes every 10 s until the story is playable. Other viewers only ever receive READY stories.
  • Reconciliation (reconcileStoryVideos, lib/stories.ts): each rail request asks the Stream API (GET /videos/{guid}) about video stories unsettled for more than 20 s — the viewer's own before answering, everyone else's after the response — and applies the answer through the same settleStoryVideo as the webhook (length check, READY, 24 hours from then). A story is claimed before the call (its updated_at moves), so instances never ask twice. The API numbers states differently from the webhook: 0 created · 1–3, 7, 8 processing · 4 finished · 5–6 failed (mapBunnyApiStatusToOrochia); a video Bunny no longer has, or holds no bytes for after the 2-hour Tus window, fails.

5. Signed playback

The library has CDN token authentication on and only allows the app's domains, so an unsigned URL answers 403. After evaluateVideoAccess allows the viewer, /api/videos/[id]/stream signs a token for the video's directory, valid 300 seconds:

https://{hostname}/bcdn_token={token}&expires={expires}&token_path=%2F{guid}%2F/{guid}/playlist.m3u8
token = base64url(sha256(tokenAuthKey + "/{guid}/" + expires + "token_path=/{guid}/"))

The token is in the path, not the query, because HLS players request renditions and segments by relative URL — the path prefix is kept, a query string would be dropped. Thumbnails and preview animations are signed one file at a time (?token=…&expires=…, 6-hour windows), which never opens the renditions.

Product overview →

On this page