OpenVibe.Live/Docs
Source on GitHub

VODs & Clips#

Finished media analysis#

LIVE_MEDIA_ANALYSIS=local is the default: Live's existing frame and transcript jobs produce VOD and clip overviews and timed transcripts. Set LIVE_MEDIA_ANALYSIS=ai to send each finished VOD or clip's signed Media URL to OpenVibe.AI's media.analyze workflow (AI runs one analysis at a time at nice 15). Live fills a missing overview (an overview it already has is kept), derives the short overview, and stores timed speech segments in vod_ai_state or clip_ai_state. Live waits up to a minute for the run (LIVE_MEDIA_ANALYSIS_WAIT_MS, 60000); a longer analysis is re-joined 15 minutes later by its idempotency key (live.media.analyze:<kind>:<id>:<attempt>), so the same recording is never analysed twice at once and no loop waits for it. AI's signals, scenes, and highlights have no Live state columns and are not stored. Failed or unavailable analyses retry with backoff; Live does not run its VOD/clip decoder in this mode. Live stream captions and sound events continue using their real-time paths. The live_media_analysis_retry_total metric counts deferred analyses by media kind.

Written before the media split. Storage, cutting, thumbnails and files now live in OpenVibe.Media; Live proxies to it (server/media-proxy/) and starts/stops recordings (server/streaming/recorder.js). Pastes live in OpenVibe.Community. Live's old local vods/clips/pastes tables are frozen: nothing writes or reads them (below); the PostgreSQL schema has no copies of them. Implementation details below may be out of date.

Recording System#

OpenVibe.Live automatically records your streams as VODs (Video on Demand).

Protocol-Specific Recording#

ProtocolRecording MethodFormat
WebRTC (Browser)Browser-side MediaRecorder, chunks uploaded to serverWebM (VP8/VP9 + Opus)
RTMPServer-side FFmpeg captureWebM (VP8 + Vorbis)
JSMPEGServer-side FFmpeg via WebSocket relayWebM (VP8 + Vorbis)

Recording Lifecycle#

  1. Start: Recording begins when the stream goes live
  2. Live DVR: A seekable sidecar file is generated periodically (every 60s for server-side, every 2 chunks for browser uploads) so viewers can rewind
  3. Finalize: When the stream ends, the recording is remuxed for proper seeking, duration is probed, and a thumbnail is generated
  4. Auto-cleanup: Recordings shorter than 10 seconds are automatically deleted (test streams, accidental go-lives)

Browser Tab Close Safety#

If you close the browser tab during a WebRTC stream, sendBeacon attempts to upload any remaining chunks. The server will auto-finalize when it detects the stream has ended.

DVR / Live Seeking#

Viewers can seek backwards in a live stream using the DVR controls:

DVR availability appears after ~30 seconds of recording.

Clips#

Viewers can create clips from live streams:

  1. Click the Clip button during a stream
  2. Set the clip duration (default: 30 seconds)
  3. The clip is saved from the server-side recording

Clips are unlisted by default — the stream owner can publish or delete them from the dashboard.

VOD Management#

From the dashboard:

The canonical page is on OpenVibe.Video#

A public, finished recording's and a person's public clip's canonical page is on OpenVibe.Video (https://openvibe.video/v/<id>, /c/<id>; server/web/video-url.js, VIDEO_PUBLIC_URL overrides the origin). Live's own /vod/<id> and /clip/<id> pages keep working and keep their own og:url, but their <link rel="canonical"> names the Video page, Live's sitemap leaves recordings and clips out (Video's sitemap lists them), the documents Live sends to OpenVibe.Search link there, and an AI clip's canonical moment is the Video page at that second. Unlisted items, recordings in progress and private items keep their Live canonical (they are not indexed anywhere).

Playback URLs#

Every VOD and clip row Live answers (/api/vods, /api/vods/:id, /api/clips, /api/clips/:id and the per-user lists) carries three URLs, set by setPlaybackUrls() in server/media-client.js:

The watch pages play hls_url when there is one and fall back to the file. The server-rendered pages name no playback URL at all: their schema.org contentUrl is the page and their og:video tags carry only the duration.

Views#

A view is one content_views row per item and address: Live's detail read (GET /api/vods/:id, GET /api/clips/:id) adds it. openvibe.video reads those over loopback, so its watch page counts its viewers with a beacon instead: POST /api/views/vod/:id or /api/views/clip/:id (server/media-proxy/view-beacon.js), sent from the viewer's browser when playback starts. It answers only https://openvibe.video's origin, reads no cookie, counts only a public or unlisted finished item (Live's visibility rule, remembered for a minute), is limited to 60 a minute per address, and answers 204 whether or not it counted.

Who sees a private VOD or clip#

A private item (visibility private, or a legacy row with no visibility and is_public = 0) is visible only to its owners — the uploader or clipper, the clipped channel's streamer, the streamer whose stream it came from — and to staff (admin, global mod). Everyone else gets exactly the answer an unknown id gets: the same 404 and body on /api/vods/:id and its live-info/memories/context, /api/clips/:id, comments (including a comment's replies, edit and delete), thumbnail regeneration and the write routes (a 403 would confirm the id exists). It is also left out of /api/chat-ai/vod-transcripts, the server-rendered /vod/:id and /clip/:id pages, and /api/streams/recent. The rule lives in server/media-proxy/access.js; Media applies the same rule to openvibe.media/v/:id and /c/:id. Unlisted items stay reachable by direct link.

Comments are OpenVibe.Community threads#

Comments on a VOD or clip live in OpenVibe.Community, not in Live (roadmap Wave 5: Live and a second product share one Community thread). The thread of a VOD is the one Community keeps for the EntityRef { service: 'live', type: 'vod', id: '<vod id>' } (type: 'clip' for clips). /api/comments keeps its paths and response shapes; server/media-proxy/comments.js is an adapter over server/comments-client.js:

Live's old comments table is read-only (test/frozen-tables.test.js fails if server code writes it). Its rows were moved with OpenVibe.Community's scripts/import-live-comments.js: dry run by default, --apply only with --backup of Community's database, a ledger so re-runs import nothing twice, authors mapped to Network subjects (unmapped ones held and listed), and a reconciliation read = imported + held + excluded. The exact production commands are in OpenVibe.Community's README ("Live's VOD and clip comments").

Chat Replay#

VODs include synchronized chat replay. Messages are stored in the database with timestamps relative to the stream start. Deleted messages are automatically excluded from replay (soft-delete with is_deleted flag).

AI moments: pastes and clips never overlap#

Two jobs turn stream moments into content: server/ai/ai-moments-job.js (image pastes for the home hero + pastes tab, every 6 h) and server/ai/auto-clip-job.js (live chat-spike clips + a VOD backfill). They used to pick the same second of the same VOD with the same title. Now:

AI-inferred category#

Go-live used to default every stream to irl, so every AI prompt (AI viewers, streamer overviews, VOD overviews, the Arena, sound detection) assumed everyone is an IRL streamer. Now the category selector defaults to Auto (stored as NULL), and the stream-memory rollup — the same summarise call that already runs — returns { overview, category, tags } with category from the fixed taxonomy (outdoors, travel, building, music, gaming, robot, desktop, irl, other). It is stored on streams.ai_category / streams.ai_tags and the channel inherits the latest read (channels.ai_category).

Everything reads the effective value: listing queries return COALESCE(ai_category, category) AS category (the self-selected value is still there as chosen_category), stream_category on VODs/clips, the AI viewers' session block ("judged from the stream itself"), streamer overviews, sound detection and the Arena all prefer ai_category. A streamer can still pick a category manually; the AI read wins once it exists.

Live reads nothing from the frozen tables#

Live's local vods, clips, pastes, paste_likes and paste_comments tables stopped at the Media split: nothing recorded, clipped or pasted since then was ever in them, and OpenVibe.Media (VODs and clips, same ids) and OpenVibe.Community (pastes, by slug) hold the old rows too. Since register item C-73 step 2, no Live code reads them. test/frozen-tables.test.js fails on any write or read of these tables in server/ or scripts/ (SQL text, split over lines too, and table: '…' registries). Its allow-list of kept reads is empty. A read that has to stay goes into that list with a reason, and into this page.

What used to read them, and what answers now (most of it in server/media-proxy/lookups.js):

Page or jobNow asksHidden items
Recently ended streams (/api/streams/recent): VOD id, thumbnail, durationMedia, per stream (cached 5 min)never: public VODs only
Recently online cards: slot thumbnailMedia GET /vods/latest-thumbs (the route already did)never
Channel page badges: pastes, clips takenCommunity (pastes), Media (clips, hide_self)owner and staff only
Channel offline screen: top VOD and clip per week, month, all timeMedia, newest 100 plus most-viewednever
Workspace session history: each session's VODMedia by slot (managed_stream_id)owner only (private and recording included)
Setup hub: "Publish a paste"Community, as the personthe person's own
Settings: avatar historyCommunity, as the person (screenshots tagged metadata.kind = 'avatar')the person's own
AI timeline (channel AI tab): each session's VOD linkMedia by user, public only; not cached while Media is downnever
Transcript view: the stream's VODMedia (the old local fallback is gone)never
Home hero, kiosk, SEO snapshot: VOD, clip, hour and paste countsMedia /stats; pastes from Community /api/pastes/admin/statscounts only
Home "over time" charts for VODs, clips, hoursMedia /stats/series/* (the route already did)counts only
Admin dashboard: VOD totalsMedia list totals (null when Media is down)staff
Admin AI explorer and the streamer overview job: VODs, clips, pastesMedia and Communitystaff: any visibility (the overview job reads public VODs)
AI moments and auto-clip jobs: VOD pool, VOD source, where viewers clippedMedia (no local fallback: Media down means no run)internal signal only
Stream analytics: clips per streamMedia, written into stream_analytics.clips_created right after the stream endsinternal
Arena portraits: VOD thumbnailsMedia, public VODsnever

Removed from server/db/database.js: 45 exported helpers that had no caller (VOD health, public VOD and clip lists, the old paste API, paste comments and likes, filtered channel lists), the 11 whose callers now ask Media or Community, and the boot-time migrations that still rewrote rows of the frozen vods/clips tables (short overviews, transcript status) along with the indexes that only served the removed listings. The streamer overview job now only considers streamers with stream memories; the frozen vods table was its other signal, and its rows moved to Media at the split.

API additions that would help (not blocking)#

This page is rendered from docs/vods-and-clips.md in the OpenVibe.Live repository. Found a mistake? Edit it there.