The open-source remote recording studio.
Local per-guest recording Β· crash-proof uploads Β· separate tracks Β· text-based editing
A self-hostable, browser-based remote recording studio in the style of Riverside: a live video call for conversation, while every participant's camera and microphone are recorded locally on their own device at full quality, chunked, persisted, and uploaded progressively in the background. The host ends up with separate, synchronized, high-quality tracks per participant β plus mixed exports β no matter how rough the live connection was.
- Studios β sessions β invite links. Hosts sign up; guests join with one link, no account.
- Lobby / device check β camera preview, mic level meter, camera/mic pickers, quality preset (720p / 1080p / 4K / audio-only), "I'm wearing headphones" echo-cancellation toggle.
- Live studio room β mesh WebRTC (host + up to ~5 guests), tiles with mute/camera state, screen sharing, text chat, presence, reconnecting states, and a stall watchdog that rebuilds peer connections whose ICE agent never comes up.
- Waiting room β optional per-session lobby: guests hold until the host admits or declines; admitted guests skip the queue on refresh.
- Host controls β force-mute any guest; per-tile local volume sliders.
- Teleprompter β host-editable script synced to every participant, with per-person auto-scroll speed and font size.
- Local-first recording β host hits Record; a server-side countdown broadcasts to everyone,
then the server issues the authoritative start timestamp; each client records its own devices
with
MediaRecorder(3s chunks). Every chunk is written to IndexedDB before upload, then uploaded with retry/backoff. Live-call quality and recording quality are fully decoupled. Optional auto-record starts the take when the first guest joins. - Pause uploads β defer bandwidth use on weak connections while local recording continues.
- True 48kHz WAV capture β an AudioWorklet taps the mic and records uncompressed PCM as a separate per-participant track (no codec round-trip), used preferentially for mixes and transcription. Toggle in the lobby, on by default.
- Screen shares are their own recorded tracks, start/stoppable mid-recording.
- Upload resilience β idempotent chunk-by-index uploads, resume after refresh/offline, and a recovery banner that scans IndexedDB on any page load and finishes uploads from crashed tabs (verified by an automated kill-the-tab-mid-recording test).
- Track sync β clients estimate server clock offset over WebSocket pings; each track stores its start offset so independently recorded files align in the mixer.
- Post-production pipeline (ffmpeg) β chunks are assembled, probed, and delivered as: per-participant MP4 (H.264/AAC) + 48kHz WAV + original raw webm, and per-take mixed grid MP4 and mixed WAV with per-track offset alignment.
- Transcription + captions β per-track whisper.cpp transcription with speaker labels from
the separate tracks and offset-aligned timestamps; TXT / SRT / VTT downloads and an inline
transcript viewer. (Uses
whisper-cliβbrew install whisper-cpp; the model auto-downloads on first use.) - Session dashboard β takes, participants, per-track status (chunk counts while uploading), downloads, mixed exports, transcripts, retry-processing, auto-record and waiting-room toggles.
- Premiere/FCP XML export β one click gives you an xmeml timeline that drops the downloaded MP4/WAV tracks onto synced tracks in Premiere Pro or DaVinci Resolve, offsets applied.
- Editor β per-recording editor with synced multi-track preview, click-to-seek timeline, trim + cut ranges, and text-based editing: word-level timestamps from whisper let you select words in the transcript and cut them β the video follows the text. Edits render server-side into new mixed exports β non-destructive throughout.
- Social clips β export any edit as 16:9, 1:1, or 9:16 (the grid re-arranges to fit, e.g. vertical stacking for Shorts/Reels) with captions: a re-timed SRT sidecar always, burned-in when your ffmpeg has libass.
- TURN-ready β set
ICE_SERVERSto add a TURN relay for guests behind strict NATs; seedocs/DEPLOYMENT.mdfor HTTPS + coturn recipes. - AI-agent CLI β
tributaryexposes the whole post-production surface (transcripts, word timestamps, cuts, clips, downloads, enhancement) as JSON commands, so any agent β Claude Code, Codex, a script β can be the AI layer: highlight clips, filler-word removal, show notes. Seedocs/AI-AGENTS.md. No LLM dependency ships in the box. - SFU mode β set
LIVEKIT_URL/API_KEY/API_SECRETand the live call rides LiveKit instead of mesh, scaling past the ~6-participant ceiling. Recording/signaling unchanged. - Live streaming + watch page β one click composites a program feed (all tiles + mixed
audio) and streams it to any RTMP destination (YouTube/Twitch/custom) and/or a public
/watch/<token>page served as HLS β no third-party platform needed. - Teams β invite members to a studio by email; owners manage the team, editors do everything else.
- Audio enhancement β per-track noise reduction + loudness normalization (β16 LUFS); mixes automatically prefer enhanced audio.
- Mobile app (early) β Expo shell (
mobile/) wrapping the studio with native camera/mic permissions and keep-awake; your phone joins as a fully recorded extra camera. This is a young app shell, not a polished native app yet β expect rough edges.
Requires Node 20+, pnpm, and ffmpeg/ffprobe on PATH (brew install ffmpeg). All
configuration is optional and env-based β see .env.example for the knobs
(port, data dir, TURN, LiveKit, whisper). Contributions welcome β see
CONTRIBUTING.md.
pnpm install
pnpm dev # server on :4100, web dev server on :4110Open http://localhost:4110, create an account, a studio, and a session, then "Enter studio". Copy the invite link for guests. To test locally, open the invite link in a second browser window β camera/mic work on localhost without HTTPS.
Production-style single process (server serves the built SPA):
pnpm build
pnpm start # everything on :4100CLI (for you or your AI agents):
ln -s "$(pwd)/cli/tributary.mjs" /usr/local/bin/tributary
tributary login --email you@example.com --password 'β¦'
tributary helpHTTPS note: browsers require a secure context for camera/mic.
localhostis fine for development; to record with remote guests, put the server behind HTTPS (Caddy, nginx + certbot, Tailscale Funnel, or a tunnel likecloudflared).
web (Vite + React + TS + Tailwind)
ββ lib/rtc WS signaling client (clock sync) + mesh WebRTC (perfect negotiation)
ββ lib/recorder MediaRecorder engine β IndexedDB chunk store β resumable upload manager
ββ pages auth, dashboard, studio, lobby, room, session detail
server (Fastify + better-sqlite3, single process)
ββ REST API auth, studios, sessions, invites, recording control, tracks, chunks, exports
ββ WS /ws rooms: presence, WebRTC relay, chat, state, upload health, clock pings
ββ jobs in-process queue β ffmpeg (assemble / probe / MP4 / WAV / mixed exports)
ββ storage local disk under data/ (chunks/, media/, exports/)
Design decisions, the full product spec (based on research of Riverside's public behavior), and
the build plan live in docs/SPEC.md and docs/PLAN.md.
Why mesh WebRTC instead of an SFU? The live call only carries the conversation β final media comes from local recordings β so mesh keeps the stack dependency-free and is fine to ~6 participants. The signaling protocol is transport-agnostic; swapping in an SFU (LiveKit) is the documented upgrade path for larger rooms.
- A chunk is persisted to IndexedDB before it's queued for upload, and deleted only after a confirmed 2xx.
- Chunk PUTs are idempotent (
PUT /api/tracks/:id/chunks/:idx); duplicates are no-ops. - On any page load, a recovery scanner finds unfinished local tracks, asks the server which chunks it already has, uploads the difference, and finalizes (crashed tabs included β the participant token is stored with the track).
- A track is marked
uploadedonly when the server holds every declared chunk; processing then assembles them into a continuous file (MediaRecorder chunks concatenate losslessly).
pnpm typecheckβ both packages.scratchpadE2E suites (run during development, all passing):- API smoke β full flow via curl: register β studio β session β join β record β chunked upload (with duplicate-chunk idempotency check) β finalize β processing β byte-identical raw download β MP4/WAV probe β mixed exports with offset alignment β authz checks.
- Browser E2E β headless Chrome with fake devices: two participants, live video both ways, chat, record 12s, live chunk upload, "All uploads complete", tracks β ready.
- Crash recovery β guest forced offline mid-recording, tab killed, then reopened: recovery banner resumes from IndexedDB, track finalizes and processes to ready.
- 4K preset depends on the camera/browser actually delivering 2160p and is encoder-heavy; 1080p is the recommended ceiling for most machines.
- Editor covers trim/cuts/word-level text editing/clips; layout switching mid-timeline and AI highlight detection are the next tier (agents can do both today via the CLI).
- Caption burn-in requires ffmpeg with libass (homebrew's default build lacks it); the SRT sidecar is always generated.
- Eye-contact correction and speech regeneration (VideoDub-style) are deliberately out of scope: no production-quality open models exist to self-host β they'd be paid-API add-ons.
- SSO (SAML/OIDC) deferred until there's a real IdP to integrate against.
- Mobile app is a WebView shell (verified via typecheck + Metro bundle); native capture via expo-camera is the planned upgrade if WebView capture limits show up on older devices.
- Local-disk storage and in-process job queue β swap for S3 + a real queue to scale out.
- Device switching is locked while recording.
Tributary is an independent implementation inspired by Riverside's public product behavior. It uses no Riverside code, branding, or assets.


