Skip to content

DVR Chapters

Chapters are how a recording is saved: they turn one continuous recording into smaller playback windows that stay replayable after the broadcast ends, while the live rewind window only serves viewers during the broadcast. Each chapter is finalized as a canonical .mkv VOD artifact with a matching .dtsh seek index and fresh poster/sprite thumbnails for the chapter timeline. Chapters use the standard VOD playback path, and their processing pass uses the lifecycle-specific dvr_finalize process snapshot resolved by Commodore.

The chapter mode is a property of the source Stream, snapshotted onto each recording when it starts. Changes apply from the next broadcast; a recording in progress keeps its setting.

New streams use WINDOW_SIZED: each chapter is as long as the recording’s own live rewind window, starting at the broadcast’s start. NONE keeps live rewind only. The recording still serves its rolling playback surface (dvr+<dvr_internal_name>) for rewinding while the broadcast is live, but nothing is saved: once media leaves that window it is gone, no chapter exists, and the recording ends with recording.stopped but never recording.ready.

mutation EnableChapters {
updateStream(
id: "stream-id"
input: { record: true, dvrChapterMode: FIXED_INTERVAL, dvrChapterIntervalSeconds: 3600 }
) {
... on Stream {
id
dvrChapterMode
dvrChapterIntervalSeconds
}
}
}
ModeBest for
WINDOW_SIZEDDefault. Parts as long as the recording’s live rewind window, from the broadcast’s start.
FIXED_INTERVALClock-aligned UTC parts such as hourly, 6-hour, or 24-hour chapters.
NONELive rewind only: viewers can rewind while live, and nothing is kept after the broadcast.

FIXED_INTERVAL requires dvrChapterIntervalSeconds ≥ 3600 (1 hour). The interval is used only by FIXED_INTERVAL; send 0 with another mode to clear it.

A stopped recording’s own playback ID plays its most recent finalized chapter. Before any chapter has finalized, playback by that ID answers 409 DVR_CHAPTERS_PENDING.

All chapter boundaries use UTC epoch milliseconds. Civil-time bucketing belongs in your app: convert “yesterday in Europe/Amsterdam” to a UTC range there, then list the chapters in that range with dvrChapters(rangeStartMs:, rangeEndMs:).

OPEN → CLOSED → FINALIZING → FINALIZED → FROZEN → RECLAIMED
│ │
└──────────┴─→ FAILED_SOURCE_MISSING | FAILED_PERMANENT

A chapter fails from CLOSED or FINALIZING, before it reaches FINALIZED.

A chapter becomes playable at FINALIZED (canonical .mkv produced on the recording origin). FROZEN means the artifact + .dtsh are durably on S3 and source TS segments are safe to delete. RECLAIMED is the steady state — the row remains as range metadata; the chapter artifact is the only thing serving playback.

playableNow is true when state ∈ {FINALIZED, FROZEN, RECLAIMED}.

dvrChapter returns a chapter only when startMs and endMs are exactly that chapter’s boundaries; any other range returns not found. It does not search for the chapter that covers a range. Take the boundaries from dvrChapters (its startMs and endMs), or compute them from the configured mode. With the hourly FIXED_INTERVAL configuration above, chapters start on UTC hour boundaries and last 3,600,000 ms:

query DVRChapter {
dvrChapter(dvrId: "dvr-hash", startMs: 1715126400000, endMs: 1715130000000) {
chapterId
state
playbackId
isCurrent
hasGaps
segmentCount
wallClockStartUnixMs
wallClockEndUnixMs
playableNow
lastFailureReason
}
}

playbackId is the Commodore-minted, externally shareable playback key for the chapter’s VOD artifact. It is a locator, not an authorization grant: public/JWT/webhook policy is snapshotted from the parent DVR and enforced normally. Resolve playback through resolveViewerEndpoint against that ID; the resulting endpoint is a standard VOD edge URL. There is no dvr+chapter_id token — chapter playback uses the same path as any other VOD.

wallClockStartUnixMs and wallClockEndUnixMs are absolute Unix epoch milliseconds. Use them to render the chapter’s calendar time and to map player timeline position (videoCurrentTime * 1000 + wallClockStartUnixMs) back to absolute time. Mist preserves DateUTC end-to-end through the remux, so the artifact itself also carries unixoffset natively — but the API metadata is the product contract.

query DVRChapters {
dvrChapters(dvrId: "dvr-hash", rangeStartMs: 1715126400000, rangeEndMs: 1715212800000) {
chapters {
chapterId
state
playbackId
startMs
endMs
segmentCount
hasGaps
lastFailureReason
}
nextPageToken
}
}

rangeStartMs and rangeEndMs limit the list to a UTC time range (here one day); omit them or pass 0 for no bound. Paginate with pageSize (default 200, max 1000) + nextPageToken. Use this for chapter pickers, timeline markers, or quick links into a long archive.

If a segment was lost from both local disk AND the recovery-bridge S3 (severe storage pressure during recording), chapter finalization marks the chapter FAILED_SOURCE_MISSING. lastFailureReason carries the operator-facing explanation. The chapter row remains addressable as range metadata; the artifact does not exist.

hasGaps=true on a successful chapter means the output media has missing timeline coverage. Recovery from the temporary S3 segment object produces the original bytes and does not count as a gap.