Recordings, Clips & VOD
Always-on venues, sports, conferences, classrooms, public meetings, monitoring feeds: anywhere the live moment and the long archive both matter. FrameWorks keeps a single DVR session running per channel for as long as the stream is live, and segments that session into chapter artifacts you can replay by window, civil-time range, or explicit range. This page covers DVR replay, clips from the buffer, and direct VOD uploads.
DVR Recording
Section titled “DVR Recording”DVR records your live stream and preserves the broadcast for later playback. DVR content is playable while the stream is still live: viewers can seek back through the live DVR window, pause, and catch up. After that window, the same recording remains available as a continuous archive, split into bounded chapter artifacts for replay.
The value prop is the split between capture, live UX, and replay UX:
- 24/7 capture — one stream session can keep recording continuously instead of rolling into arbitrary fixed-length assets.
- Fast live playlists — the live DVR window stays bounded so viewers get seekback without enormous manifests.
- Archive replay — chapters turn a long recording into hidden VOD-shaped artifacts with normal playback IDs.
- Cost control — retention policies decide how long completed DVR, clips, and VOD assets stay in storage.
That split matters:
| Concept | What it controls |
|---|---|
| Live DVR window | How far back live viewers can seek while the stream is still recording. This is tier- and cluster-bounded so live playlists stay small. |
| Continuous archive | The full DVR segment timeline for one stream session. It can span hours, days, or months if the ingest stays live. |
| DVR chapters | Finalized VOD-shaped artifacts for historical replay. Each chapter is linked to its parent DVR and source stream, but stays out of the tenant-wide VOD library by default. |
| Retention | How long a completed recording is kept after the stream session ends. Retention is not the live seekback window. |
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 { startDVR(streamId: "stream-global-id") { __typename ... on DVRRequest { dvrHash playbackId createdAt } ... on ValidationError { message } }}
# Stop recordingmutation { stopDVR(dvrHash: "dvr-hash") { __typename ... on DeleteSuccess { success } }}Playback
Section titled “Playback”DVR 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 DVR window (
dvrPlaybackId) — seekable playback while the stream is recording, bounded by the tier/cluster DVR window. - Archive chapters — optional finalized VOD artifacts generated from the recording timeline for historical replay.
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 DVR window. The player enables seeking, skip, and speed controls within the active window:
<Player contentType="dvr" contentId="pk_dvr_..." />Live Time-Shift
Section titled “Live Time-Shift”While a stream is live, the DVR recording grows continuously. Viewers using the DVR playback ID can:
- Seek back through the tier-bounded live DVR 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 DVR window.
Archive Chapters
Section titled “Archive Chapters”For historical replay, FrameWorks splits the DVR archive into 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 public playbackId — pass it to resolveViewerEndpoint like any other VOD.
For the full chapter-policy story (modes, defaults, resolution semantics) see DVR Chapters. Storage cost behavior is covered in Storage & retention.
Common chapter shapes:
| Shape | Use |
|---|---|
| Window-sized chapters | Sequential chapters sized to the stream’s DVR window. Useful default for long recordings. |
| Fixed UTC intervals | Regular UTC buckets such as 6-hour or 24-hour spans. Minimum interval is 1 hour (enforced by Commodore + DB). |
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 resolve those boundaries before requesting the chapter.
Chapter API
Section titled “Chapter API”Chapters become playable at boundary close once finalization produces the .mkv. Retrieve a chapter to get its public playbackId; pass that to resolveViewerEndpoint like any other VOD.
query { dvrChapter(dvrId: "dvr-hash", startMs: 1715126400000, endMs: 1715212800000) { 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 }}For stream-scoped media views, chapter artifacts also surface through vodAssetsConnection(streamId: ...) with originType: "dvr_chapter". Omit streamId to get the normal tenant VOD library, which only returns library-visible user uploads.
query StreamVodArtifacts { vodAssetsConnection(streamId: "stream-global-id", page: { first: 20 }) { edges { node { id playbackId streamId originType originId title status createdAt } } }}Historical chapter mode is configured on the Stream and snapshotted onto each recording at StartDVR. Changes apply to the next recording, not in-flight ones. NONE keeps DVR recording and the active rolling DVR window enabled, but skips finalized chapter artifacts for historical replay:
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 { dvrRecordingsConnection(streamId: "stream-global-id", page: { first: 10 }) { edges { node { dvrHash playbackId title createdAt expiresAt durationSeconds sizeBytes status isExpired } } }}Clips are short video segments extracted from a live stream’s DVR buffer. Use them to capture highlights, key moments, or shareable previews.
Creating Clips from the Dashboard
Section titled “Creating Clips from the Dashboard”On a live stream’s detail page, open the Clips section and click Create Clip. Choose your time range and hit save.
Creating Clips via the API
Section titled “Creating Clips via the API”The createClip mutation supports four time modes:
| Mode | Use Case | Required Fields |
|---|---|---|
CLIP_NOW | Clip the last N seconds from live | duration |
DURATION | Start time + duration | startUnix + duration |
ABSOLUTE | Exact start and stop timestamps | startUnix + stopUnix |
RELATIVE | Seconds from stream start | startMedia + stopMedia |
Clip the last 30 seconds (most common):
mutation { createClip( input: { streamId: "stream-global-id", title: "Great moment", mode: CLIP_NOW, duration: 30 } ) { __typename ... on Clip { id clipHash playbackId status duration } ... on ValidationError { message field } ... on NotFoundError { message } }}Clip with absolute timestamps:
mutation { createClip( input: { streamId: "stream-global-id" title: "Keynote intro" mode: ABSOLUTE startUnix: 1706000000 stopUnix: 1706000060 } ) { __typename ... on Clip { id playbackId status } }}Clip Processing
Section titled “Clip Processing”After creation, clips go through a processing pipeline:
- queued — Waiting to be processed
- processing — Extracting and encoding the segment
- ready — Available for playback
- failed — Something went wrong (check
errorMessage)
Track progress in real time with the liveClipLifecycle subscription:
subscription { liveClipLifecycle(streamId: "stream-global-id") { clipHash stage playbackId progressPercent error }}Clip Playback
Section titled “Clip Playback”Once ready, clips are playable at the same URLs as any other content:
https://foghorn.frameworks.network/play/{playbackId}/hls/index.m3u8https://foghorn.frameworks.network/play/{playbackId}.mp4Managing Clips
Section titled “Managing Clips”# List clips for a streamquery { clipsConnection(streamId: "stream-global-id", page: { first: 20 }) { edges { node { id title playbackId duration status createdAt sizeBytes } } }}
# Delete a clipmutation { deleteClip(id: "clip-global-id") { __typename ... on DeleteSuccess { success } }}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
# 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 } }}VOD Processing
Section titled “VOD Processing”After upload completion, the asset goes through validation:
- UPLOADING → PROCESSING → READY
- If validation fails: FAILED
Track progress with the liveVodLifecycle subscription.
VOD Playback
Section titled “VOD Playback”Same URL pattern as all other content:
<Player contentType="vod" contentId="pk_..." />Managing VOD Assets
Section titled “Managing VOD Assets”# List all library VOD uploadsquery { vodAssetsConnection(page: { first: 20 }) { edges { node { id playbackId title status sizeBytes durationSeconds createdAt } } }}
# List VOD-shaped artifacts derived from a stream, including DVR chaptersquery { vodAssetsConnection(streamId: "stream-global-id", page: { first: 20 }) { edges { node { id playbackId streamId originType originId title status createdAt } } }}
# Delete a VOD assetmutation { deleteVodAsset(id: "vod-global-id") { __typename ... on DeleteSuccess { success } }}
# Abort an in-progress uploadmutation { abortVodUpload(uploadId: "upload-session-id") { __typename ... on DeleteSuccess { success } }}