Skip to content

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 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:

ConceptWhat it controls
Live DVR windowHow far back live viewers can seek while the stream is still recording. This is tier- and cluster-bounded so live playlists stay small.
Continuous archiveThe full DVR segment timeline for one stream session. It can span hours, days, or months if the ingest stays live.
DVR chaptersFinalized 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.
RetentionHow long a completed recording is kept after the stream session ends. Retention is not the live seekback window.

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
}
}
}

Recording begins automatically when a stream with record: true goes live. You can also start and stop recording manually during a broadcast:

# Start recording
mutation {
startDVR(streamId: "stream-global-id") {
__typename
... on DVRRequest {
dvrHash
playbackId
createdAt
}
... on ValidationError {
message
}
}
}
# Stop recording
mutation {
stopDVR(dvrHash: "dvr-hash") {
__typename
... on DeleteSuccess {
success
}
}
}

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.m3u8
DASH: https://foghorn.frameworks.network/play/{dvrPlaybackId}/cmaf/index.mpd
MP4: https://foghorn.frameworks.network/play/{dvrPlaybackId}.mp4

With 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_..." />

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.

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:

ShapeUse
Window-sized chaptersSequential chapters sized to the stream’s DVR window. Useful default for long recordings.
Fixed UTC intervalsRegular 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.

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
}
}
}

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.

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.

On a live stream’s detail page, open the Clips section and click Create Clip. Choose your time range and hit save.

The createClip mutation supports four time modes:

ModeUse CaseRequired Fields
CLIP_NOWClip the last N seconds from liveduration
DURATIONStart time + durationstartUnix + duration
ABSOLUTEExact start and stop timestampsstartUnix + stopUnix
RELATIVESeconds from stream startstartMedia + 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
}
}
}

After creation, clips go through a processing pipeline:

  1. queued — Waiting to be processed
  2. processing — Extracting and encoding the segment
  3. ready — Available for playback
  4. 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
}
}

Once ready, clips are playable at the same URLs as any other content:

https://foghorn.frameworks.network/play/{playbackId}/hls/index.m3u8
https://foghorn.frameworks.network/play/{playbackId}.mp4
# List clips for a stream
query {
clipsConnection(streamId: "stream-global-id", page: { first: 20 }) {
edges {
node {
id
title
playbackId
duration
status
createdAt
sizeBytes
}
}
}
}
# Delete a clip
mutation {
deleteClip(id: "clip-global-id") {
__typename
... on DeleteSuccess {
success
}
}
}

Upload pre-recorded video files for on-demand playback. VOD uploads use S3-compatible multipart uploads for reliable handling of large files.

  1. Initiate — Create an upload session to get presigned URLs
  2. Upload parts — PUT each file chunk to its presigned URL
  3. Complete — Finalize the upload with part ETags
# Step 1: Create upload session
mutation {
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 upload
mutation {
completeVodUpload(
input: {
uploadId: "upload-session-id"
parts: [{ partNumber: 1, etag: "\"abc123\"" }, { partNumber: 2, etag: "\"def456\"" }]
}
) {
__typename
... on VodAsset {
id
playbackId
status
}
}
}

After upload completion, the asset goes through validation:

  • UPLOADINGPROCESSINGREADY
  • If validation fails: FAILED

Track progress with the liveVodLifecycle subscription.

Same URL pattern as all other content:

<Player contentType="vod" contentId="pk_..." />
# List all library VOD uploads
query {
vodAssetsConnection(page: { first: 20 }) {
edges {
node {
id
playbackId
title
status
sizeBytes
durationSeconds
createdAt
}
}
}
}
# List VOD-shaped artifacts derived from a stream, including DVR chapters
query {
vodAssetsConnection(streamId: "stream-global-id", page: { first: 20 }) {
edges {
node {
id
playbackId
streamId
originType
originId
title
status
createdAt
}
}
}
}
# Delete a VOD asset
mutation {
deleteVodAsset(id: "vod-global-id") {
__typename
... on DeleteSuccess {
success
}
}
}
# Abort an in-progress upload
mutation {
abortVodUpload(uploadId: "upload-session-id") {
__typename
... on DeleteSuccess {
success
}
}
}