Recordings, Clips & VOD
Always-on venues, sports, conferences, classrooms, public meetings, monitoring feeds: anywhere the live moment and the long archive both matter. A stream that records broadcasts keeps one recording running per broadcast for as long as the stream is live, and saves it as chapters you can replay after the broadcast ends. You find chapters by listing a UTC time range and replay each one by its playback ID. This page covers recordings, clips from the buffer, and direct VOD uploads.
flowchart LR
LIVE[Live stream] --> DVR[Live rewind window]
DVR --> CH[Saved recording chapters]
DVR --> CLIP[Clip export]
UPLOAD[Direct upload] --> VOD[VOD asset]
CH --> PLAY[Playback ID]
CLIP --> PLAY
VOD --> PLAY
Recording broadcasts
Section titled “Recording broadcasts”A stream with Record broadcasts on (record: true) records every broadcast. Two things come
out of one recording:
- Live rewind window: while the broadcast is live, viewers can pause, rewind up to the window’s length, and jump back to live. The window’s length comes from your tier and cluster, so live playlists stay small.
- Saved recording: the broadcast is kept after it ends, split into chapters. Each chapter is a finalized VOD-shaped artifact with its own playback ID, replayable until retention removes it.
By default a saved recording is split into parts as long as the live rewind window, starting at
the broadcast’s start (WINDOW_SIZED). You can instead split it into clock-aligned UTC parts every
N hours (FIXED_INTERVAL). A stream set to NONE keeps live rewind only: viewers can rewind while
live, and nothing is kept once media leaves the window.
| Concept | What it controls |
|---|---|
| Live rewind window | How far back live viewers can seek while the broadcast is live. Tier- and cluster-bounded. |
| Saved recording | The broadcast after it ends, as chapters. One recording can span hours, days, or months if the ingest stays live. |
| Chapters | The saved parts. Each is linked to its recording and source stream, and stays out of the tenant-wide VOD library by default. |
| Retention | How long a saved recording is kept after the broadcast ends. Retention is not the live rewind window. |
A stream’s chapter setting is snapshotted when a recording starts, so a change applies from the next broadcast; a recording in progress keeps its setting.
Enabling Recording
Section titled “Enabling Recording”When creating a stream:
Set record: true in the create mutation:
mutation { createStream(input: { name: "My Stream", record: true }) { __typename ... on Stream { id streamKey playbackId record } ... on ValidationError { message field } }}On an existing stream:
Toggle recording in the dashboard on the stream detail page, or update via the API:
mutation { updateStream(input: { record: true }, id: "stream-global-id") { __typename ... on Stream { id record } }}Starting and Stopping DVR
Section titled “Starting and Stopping DVR”Recording begins automatically when a stream with record: true goes live. You can also start and stop recording manually during a broadcast:
# Start recordingmutation StartRecording { startDVR(streamId: "stream-global-id") { __typename ... on DVRRequest { dvrHash playbackId createdAt } ... on ValidationError { message } }}
# Stop recordingmutation StopRecording { stopDVR(dvrHash: "dvr-hash") { __typename ... on DeleteSuccess { success } }}Playback
Section titled “Playback”Recordings get their own playbackId, separate from the live stream’s. This means the same stream supports three viewing modes:
- Live (
streamPlaybackId) — real-time playback at the live edge. - Live rewind window (
dvrPlaybackId) — seekable playback while the broadcast is live, bounded by the tier/cluster window. - Saved recording — the recording’s chapters, each a finalized VOD artifact with its own
playbackId, for replay after the broadcast.
After the broadcast ends, the recording’s own dvrPlaybackId plays its most recent finalized
chapter. Until the first chapter has finalized (or for a live-rewind-only recording, which has no
chapters) /play/{dvrPlaybackId} answers 409 with code DVR_CHAPTERS_PENDING; list the
recording’s chapters with dvrChapters to play a specific one.
DVR playback URLs follow the same /play/{playbackId} pattern:
HLS: https://foghorn.frameworks.network/play/{dvrPlaybackId}/hls/index.m3u8DASH: https://foghorn.frameworks.network/play/{dvrPlaybackId}/cmaf/index.mpdMP4: https://foghorn.frameworks.network/play/{dvrPlaybackId}.mp4With the Player SDK, use contentType="dvr" for the live rewind window. The player enables seeking, skip, and speed controls within the active window:
<Player contentType="dvr" contentId="a1b2c3d4e5f6g7h8" />Live Time-Shift
Section titled “Live Time-Shift”While a stream is live, the recording grows continuously. Viewers using the recording’s playback ID can:
- Seek back through the tier-bounded live rewind window
- Pause and resume without losing their position
- Jump to live to catch up to the real-time edge
The trade-off is latency — DVR playback goes through HLS or DASH segments, so it’s higher latency than a direct live WebRTC connection. But for live events, conferences, and always-on streams this is a feature: viewers can rewind a goal, replay a slide, or start watching late and catch up within the live rewind window.
Archive Chapters
Section titled “Archive Chapters”For replay after the broadcast, FrameWorks saves the recording as chapter VOD artifacts. Each chapter is its own canonical .mkv produced by the chapter finalization pipeline at boundary close and addressed by a Commodore-minted, shareable playbackId — pass it to resolveViewerEndpoint like any other VOD. The parent DVR’s playback policy is snapshotted onto the chapter and still applies.
For the full chapter-policy story (modes, defaults, resolution semantics) see DVR Chapters. Storage cost behavior is covered in Storage & retention.
A new stream saves window-sized chapters (WINDOW_SIZED). Chapter shapes:
| Shape | Use |
|---|---|
| Window-sized chapters | Default. Sequential parts as long as the recording’s live rewind window, from the broadcast’s start (WINDOW_SIZED). |
| Fixed UTC intervals | Regular UTC buckets such as 6-hour or 24-hour spans. Minimum interval is 1 hour (enforced by Commodore + DB). |
| Live rewind only | NONE: nothing is saved. Viewers can rewind while live; no chapters are produced and recording.ready never fires. |
All chapter time fields are UTC epoch milliseconds. The API does not store timezone names or offsets; clients that care about civil-day boundaries should convert them to a UTC range and list the chapters in that range with dvrChapters(rangeStartMs:, rangeEndMs:).
Chapter API
Section titled “Chapter API”Chapters become playable at boundary close once finalization produces the .mkv. Retrieve a chapter to get its playbackId; pass that to resolveViewerEndpoint like any other VOD and supply whatever authorization the parent DVR policy requires.
dvrChapter finds a chapter only when startMs and endMs are exactly its boundaries; it does not search for the chapter covering a range. Take the boundaries from dvrChapters, or compute them from the configured mode. With the 6-hour FIXED_INTERVAL configuration shown below, chapters start on UTC multiples of 6 hours and last 21,600,000 ms:
query { dvrChapter(dvrId: "dvr-hash", startMs: 1715126400000, endMs: 1715148000000) { chapterId state playbackId isCurrent hasGaps segmentCount wallClockStartUnixMs wallClockEndUnixMs playableNow lastFailureReason }}playbackId is non-null once state reaches FINALIZED. Use wallClockStartUnixMs / wallClockEndUnixMs for player timeline mapping — they reflect the chapter’s actual MKV span (set at finalization to the first/last owned segment’s media times) so video.currentTime maps to wall-clock without drift.
Build a chapter index for player navigation with paginated listing:
query { dvrChapters( dvrId: "dvr-hash" rangeStartMs: 1715040000000 rangeEndMs: 1715212800000 pageSize: 50 ) { chapters { chapterId state playbackId startMs endMs isCurrent hasGaps segmentCount lastFailureReason } nextPageToken }}Chapter artifacts also surface through storageArtifactsConnection with kind CHAPTER. Pass a streamId for one stream’s uploads and chapters. Omitting streamId does not hide chapters: kinds: [VOD, CHAPTER] then returns the chapters of every stream in your account along with your uploads. Request kinds: [VOD] for the upload library only.
query StreamVodArtifacts { storageArtifactsConnection( input: { kinds: [VOD, CHAPTER], streamId: "stream-global-id", first: 20, offset: 0 } ) { nodes { id kind playbackId streamId title status createdAt } totalCount hasNextPage }}The chapter mode is configured on the Stream and snapshotted onto each recording when it starts. Changes apply from the next broadcast, not to a recording in progress. Send dvrChapterIntervalSeconds: 0 with WINDOW_SIZED or NONE to clear a stored interval. To split into 6-hour UTC parts:
mutation { updateStream( id: "stream-id" input: { dvrChapterMode: FIXED_INTERVAL, dvrChapterIntervalSeconds: 21600 } ) { ... on Stream { id dvrChapterMode dvrChapterIntervalSeconds } }}Retention
Section titled “Retention”DVR retention starts after the recording finalizes; an active 24/7 recording is not deleted while live, regardless of age. The resolved horizon is snapshotted onto the artifact at StartDVR — the tier policy in force at that moment is what applies, even if the tenant’s plan changes mid-stream.
The DVR system default is 30 days. A tenant can lift or shorten it per-class via setMediaRetentionPolicy(targetType: DVR), per-stream via setStreamRetentionOverrides, or per-recording via updateMediaRetention once the artifact is finalized. The full cascade and the tier cap behavior are covered in Storage & retention.
Listing Recordings
Section titled “Listing Recordings”query { storageArtifactsConnection( input: { kinds: [DVR], streamId: "stream-global-id", first: 10, offset: 0 } ) { nodes { hash playbackId title createdAt expiresAt durationSeconds sizeBytes status } totalCount hasNextPage }}A clip is a time range from the live stream, its DVR window, or a finalized chapter. There are two kinds:
- Stored clips: create one in the dashboard (the Clips section of a stream’s page) or with
createClip. It is processed, saved to your account with its ownplaybackId, and counts toward storage and retention. - Ad-hoc cuts: add
startunix,stopunix, ordurationto a progressive playback URL such as/play/{dvrPlaybackId}.mp4. The edge returns the range as a file and nothing is saved.
Clips covers both: the four createClip time modes, which source a range is
read from, events, the URL parameters, and how far back each playback ID reaches.
The tenantEvents subscription with types: ["recording.ready", "recording.failed"] reports
saved and failed recordings. A live-rewind-only (NONE) recording ends with recording.stopped
and no recording.ready, because nothing replayable is kept. See Events for
every event type.
VOD Uploads
Section titled “VOD Uploads”Upload pre-recorded video files for on-demand playback. VOD uploads use S3-compatible multipart uploads for reliable handling of large files.
Upload Flow
Section titled “Upload Flow”- Initiate — Create an upload session to get presigned URLs
- Upload parts — PUT each file chunk to its presigned URL
- Complete — Finalize the upload with part ETags
The SDKs run all three steps with one call, including part retries and aborting the session on failure. The raw GraphQL flow:
# Step 1: Create upload sessionmutation { createVodUpload( input: { filename: "keynote.mp4", sizeBytes: 524288000, title: "Keynote Recording" } ) { __typename ... on VodUploadSession { id playbackId partSize expiresAt parts { partNumber presignedUrl } } ... on ValidationError { message } }}Upload each part with an HTTP PUT to the presigned URL, then complete:
# Step 3: Complete uploadmutation { completeVodUpload( input: { uploadId: "upload-session-id" parts: [{ partNumber: 1, etag: "\"abc123\"" }, { partNumber: 2, etag: "\"def456\"" }] } ) { __typename ... on VodAsset { id playbackId status } }}Import a video from a URL
Section titled “Import a video from a URL”When the file already sits on a web server, a CDN, or an S3 bucket behind a presigned link, let FrameWorks read it from there instead of uploading it yourself:
mutation { importVodAsset(input: { url: "https://media.example.com/talks/keynote.mp4", title: "Keynote" }) { __typename ... on VodAsset { id playbackId status } ... on ValidationError { message } }}The asset comes back at once with its playbackId and status PROCESSING. The processing node
reads the file straight from the URL, with no separate upload step, and processes it like an
upload: upload.created fires on acceptance, upload.completed once the node has fetched the
source (with its size), then upload.ready or upload.failed. A source that cannot be fetched
emits upload.failed without upload.completed (see Events).
- Sources:
https://andhttp://URLs. For content on IPFS or Arweave, pass a public gateway’s https link. - Range requests: the server must support HTTP range requests, as S3, CDNs, and IPFS gateways do. The file is read in blocks, not in one download.
- Filename: the extension tells FrameWorks how to read the file:
mp4,mov,mkv,webm, orts. When the URL path does not end in one, passfilename. Formats that must be read from a local file (avi,flv,m4v) are not importable; upload them instead. - Reachability: the URL must be publicly reachable. Private, loopback, and cloud-metadata addresses are refused on every connection, including after a redirect. Up to 5 http(s) redirects are followed.
- Failures: a source that cannot be read or processed fails the asset with
upload.failed; the asset’serrorMessagehas the detail.
VOD Processing
Section titled “VOD Processing”After upload completion, the asset goes through validation:
- UPLOADING → PROCESSING → READY
- If validation fails: FAILED
Poll vodUploadStatus(uploadId) for the state of an upload, or subscribe to tenantEvents with
types: ["upload.completed", "upload.ready", "upload.failed"] to learn when processing finishes.
Events carry no progress percentage; see Events.
VOD Playback
Section titled “VOD Playback”Same URL pattern as all other content:
<Player contentType="vod" contentId="a1b2c3d4e5f6g7h8" />Managing VOD Assets
Section titled “Managing VOD Assets”# List all library VOD uploads (kinds: [VOD] excludes DVR chapters)query LibraryUploads { storageArtifactsConnection(input: { kinds: [VOD], first: 20, offset: 0 }) { nodes { id playbackId title status sizeBytes durationSeconds createdAt } totalCount hasNextPage }}
# List VOD-shaped artifacts derived from a stream, including DVR chaptersquery StreamArtifacts { storageArtifactsConnection( input: { kinds: [VOD, CHAPTER], streamId: "stream-global-id", first: 20, offset: 0 } ) { nodes { id kind playbackId streamId title status createdAt } totalCount hasNextPage }}
# Delete a VOD assetmutation DeleteUpload { deleteVodAsset(id: "vod-global-id") { __typename ... on DeleteSuccess { success } }}
# Abort an in-progress uploadmutation AbortUpload { abortVodUpload(uploadId: "upload-session-id") { __typename ... on DeleteSuccess { success } }}