Clipping & VOD from Archived Streams
Once your live streams are being recorded to S3 (see Recording), Native Frame can produce on-demand assets from those archived stream sessions:
- HLS playlist — stitch archived sessions into a single VOD or clip
m3u8. - MP4 clip — remux a time range into a single downloadable MP4.
- Thumbnail — extract a single still image at a timestamp.
All three are GET endpoints under /program/api/v2/archive/…, share the same auth and recording prerequisite, and read what is already archived — they never trigger recording.
Prerequisites
These apply to all three endpoints:
- Recording on, set to HLS. These features stitch archived HLS segments — configure an S3 archiving destination first (see Recording) with the format set to
hls. A non-HLS recording config returns 400; a stream with no archiving config at all returns 404. - The segments for your range must already be archived — but the session doesn't need to be finished. The endpoints read whatever is currently in S3 and listed in the session's
index.m3u8at request time, so you can clip or grab a thumbnail from an in-progress recording's already-written portion. They never trigger recording or wait for it: a range running up to the live edge just returns the segments written so far. Stay a segment or two back from the live edge to avoid the still-being-written tail. - Authentication: a
BearerJWT whoseaud/rolesincludeservice-account(orinternal-account). The project is taken from the token — astream_keybelonging to another project returns 403.
Datetimes are ISO-8601 / RFC 3339 (e.g. 2026-06-10T18:00:00Z). Time ranges use the archived sessions' timestamps.
HLS playlist (VOD & clips)
GET /program/api/v2/archive/hls-playlist — see it in the API reference.
Finds every archived session within the time range and merges their segments into one unified HLS playlist. A wide range gives a full-event VOD; a narrow range gives a clip. It can also rewrite segment URLs (e.g. S3 → CDN) and save the playlist back to S3 as a servable index.m3u8.
| Query param | Required | Description |
|---|---|---|
stream_key | ✅ | The stream's ID (PublicKey). |
start_datetime | ✅ | Start of the range. |
end_datetime | ✅ | End of the range. |
url_rewrite_match | – | Go-regexp pattern matched against each full segment URL (used with url_rewrite_replace). |
url_rewrite_replace | – | Replacement string; supports capture groups ($1, $2, …). Must be sent with url_rewrite_match. |
output_path | – | S3 path (relative to the archive bucket root) to also save the generated index.m3u8 (created if needed, overwrites existing). |
response | – | body (default) returns the m3u8 inline; metadata returns JSON about the S3 write (requires output_path). |
height | – | Target rendition height when the recording was archived at multiple resolutions; the nearest available variant is used (e.g. 720). 0 targets a source/passthrough archive. Default 1080. See Multi-resolution recordings. |
prefer | – | How to choose among overlapping renditions: coverage (default) takes the most complete coverage of your range; height honours the requested resolution, accepting gaps. |
Default response: application/vnd.apple.mpegurl.
Full-event VOD — the event's full range:
GET /program/api/v2/archive/hls-playlist?stream_key=<streamId>&start_datetime=2026-06-10T18:00:00Z&end_datetime=2026-06-10T20:30:00Z
Authorization: Bearer <jwt>
Clip a moment — a narrow range:
GET /program/api/v2/archive/hls-playlist?stream_key=<streamId>&start_datetime=2026-06-10T18:42:00Z&end_datetime=2026-06-10T18:42:30Z
Authorization: Bearer <jwt>
Serve via CDN — rewrite the segment URLs (Go regexp, capture groups):
GET /program/api/v2/archive/hls-playlist
?stream_key=<streamId>
&start_datetime=2026-06-10T18:00:00Z
&end_datetime=2026-06-10T20:30:00Z
&url_rewrite_match=^https://my-bucket\.s3\.amazonaws\.com/(.*)$
&url_rewrite_replace=https://cdn.example.com/$1
Persist to S3 — also save a servable index.m3u8 (still returned in the response too):
GET /program/api/v2/archive/hls-playlist?stream_key=<streamId>&start_datetime=...&end_datetime=...&output_path=vod/2026-06-10/event-finals
There's no hard cap on how many sessions are merged — a wide range over a long event produces a correspondingly long playlist.
Status codes: 200 playlist (or metadata) · 400 bad params, invalid url_rewrite_match regex, url_rewrite_replace without url_rewrite_match, or source not HLS · 403 wrong project · 404 no archiving config · 500.
MP4 clip
GET /program/api/v2/archive/mp4-clip
Remuxes (does not transcode) the archived HLS segments in the range into a single MP4 with +faststart (moov before mdat, for progressive playback). Source is assumed H.264 video + AAC audio. It always writes to S3 and always returns metadata — there is no inline body mode, and output_path is required.
| Query param | Required | Description |
|---|---|---|
stream_key | ✅ | Stream ID. |
start_datetime | ✅ | Start of the clip range. |
end_datetime | ✅ | Must be > start, and end − start must not exceed the max clip duration (default 3 min). |
output_path | ✅ | S3 path; final key is {output_path}/clip{startUnixMs}-{endUnixMs}.mp4. |
kf_accurate | – | true tightens start trimming to the nearest GOP keyframe instead of the segment boundary (see below). Default false. |
height | – | Target rendition height when the recording was archived at multiple resolutions; the nearest available variant is used (e.g. 720). 0 targets a source/passthrough archive. Default 1080. See Multi-resolution recordings. |
prefer | – | How to choose among overlapping renditions: coverage (default) takes the most complete coverage of your range; height honours the requested resolution, accepting gaps. |
GET /program/api/v2/archive/mp4-clip?stream_key=<streamId>&start_datetime=2026-06-10T18:42:00Z&end_datetime=2026-06-10T18:42:30Z&output_path=clips/<streamId>
Authorization: Bearer <jwt>
Returns application/json (ArchiveAssetMetadata) with an mp4 sub-object: durationSeconds, width, height, videoCodec (e.g. h264), audioCodec (e.g. aac), plus optional fps and bitrateKbps.
Clip boundaries (head-padding): because this is a pure remux (no transcode), the start snaps back to a keyframe at-or-before start_datetime; the end is frame-accurate. The extra leading content is always at the head, never the tail, and mp4.durationSeconds reports the actual produced length. How far back the start snaps depends on kf_accurate:
- default (
kf_accurateoff): snaps to the archive segment boundary — head-padding up to ~one segment (e.g. ~7–10 s with 10 s segments). kf_accurate=true: snaps to the nearest GOP keyframe — head-padding bounded by ~one keyframe interval (e.g. ~2 s with a 2 s GOP), much tighter. Still a pure remux. Single-session ranges only; a range spanning multiple archive sessions automatically falls back to segment-granular trimming (no error).
GET /program/api/v2/archive/mp4-clip?stream_key=<streamId>&start_datetime=2026-06-10T18:42:00Z&end_datetime=2026-06-10T18:42:30Z&output_path=clips/<streamId>&kf_accurate=true
Authorization: Bearer <jwt>
Clients needing an exact-frame start should still read mp4.durationSeconds and skip any residual head-padding client-side.
Status codes: 200 metadata · 400 missing/invalid params, output_path missing, duration over the cap, zero/negative range, or source not HLS · 403 wrong project · 404 no archiving config or no sessions in range · 500.
Multi-resolution recordings
If a stream is archived at more than one resolution, the archive holds a separate rendition for each. Both hls-playlist and mp4-clip always return a single rendition, so a playlist or clip never mixes resolutions partway through your range. By default they serve the rendition closest to 1080p with the most complete coverage of the window.
Two optional parameters control the choice:
height— the resolution you want. The nearest available variant wins, soheight=720still returns something sensible if only 1080p and 480p were archived.height=0targets a source/passthrough archive. Omit it to accept the default.prefer— the trade-off when several renditions overlap your range:coverage(default) — take the rendition with the fewest gaps, usingheightonly to break ties.height— honour the requested resolution even if that rendition has gaps.
GET /program/api/v2/archive/hls-playlist?stream_key=<streamId>&start_datetime=2026-06-10T18:00:00Z&end_datetime=2026-06-10T20:30:00Z&height=720
Authorization: Bearer <jwt>
Ask for prefer=height when the output resolution matters more than continuity — a clip destined for a fixed-size player, say. Leave it at coverage when a complete, gap-free result matters more.
Thumbnails select a rendition too, but automatically: the source variant nearest your requested output size is used, so a small thumbnail doesn't decode a 4K frame. There is no height/prefer on thumbnail — use width/height to size the image itself.
Thumbnail
GET /program/api/v2/archive/thumbnail
Seeks into the archived stream at one timestamp and returns a single encoded image (jpg/png/webp). If the timestamp falls in a gap between sessions, the closest session by time is used (ties prefer the session before).
| Query param | Required | Description |
|---|---|---|
stream_key | ✅ | Stream ID. |
datetime | ✅ | Timestamp to extract a frame at. |
format | – | jpg | png | webp — default jpg. |
quality | – | 0–100. jpg/webp = perceptual quality (higher is better); png maps to compression level (png is lossless either way). Default 63. |
width | – | 1–4096. If only one of width/height is given, the other is computed to preserve aspect. Both omitted → source variant's natural resolution. |
height | – | 1–4096; see width. |
aspect_method | – | center-cut | pillar-box — used only when both width & height are given. Default center-cut. |
output_path | – | S3 path; also writes the image to {output_path}/thumb{unixMillisOfDatetime}.{ext}. |
playback_url | – | Overrides the project's stored s3BaseUrl for this request only — useful when the archiving config has no playback URL recorded yet. |
response | – | body (default) returns the image; metadata returns JSON about the S3 write (requires output_path). |
Default response: the binary image (image/jpeg, image/png, or image/webp).
Basic (defaults → jpeg, quality 63, source resolution):
GET /program/api/v2/archive/thumbnail?stream_key=<streamId>&datetime=2026-06-10T18:42:00Z
Authorization: Bearer <jwt>
Resized PNG, aspect-preserving (only width given):
GET /program/api/v2/archive/thumbnail?stream_key=<streamId>&datetime=2026-06-10T18:42:00Z&format=png&width=640
Authorization: Bearer <jwt>
Fixed box with explicit aspect handling (both dimensions — center-cut crops to fill 640×360; pillar-box letter/pillar-boxes without cropping):
GET /program/api/v2/archive/thumbnail?stream_key=<streamId>&datetime=2026-06-10T18:42:00Z&width=640&height=360&aspect_method=pillar-box
Authorization: Bearer <jwt>
Persist to S3 and/or return metadata instead of the image (response=metadata requires output_path):
GET /program/api/v2/archive/thumbnail?stream_key=<streamId>&datetime=2026-06-10T18:42:00Z&output_path=thumbs/<streamId>&response=metadata
Authorization: Bearer <jwt>
With response=metadata, returns application/json (ArchiveAssetMetadata) including an image sub-object (width, height, format).
Status codes: 200 image (or metadata) · 400 bad params (e.g. width/height out of 1–4096, or response=metadata without output_path), or source not HLS · 403 wrong project · 404 no archiving config or no sessions for the stream key · 500.
ArchiveAssetMetadata (shared response shape)
Returned by mp4-clip always, and by hls-playlist / thumbnail when response=metadata:
| Field | Description |
|---|---|
s3Bucket | Bucket the asset was written to. |
s3ObjectKey | Object key within the bucket. |
s3Uri | Canonical s3://bucket/key URI. |
contentType | MIME type of the uploaded asset. |
bytes | Size in bytes. |
etag | S3 ETag (optional). |
playbackUrl | Public URL (project s3BaseUrl + key); omitted if none configured. |
hls | { totalDurationSeconds, sessionCount } — playlist writes. |
image | { width, height, format } — thumbnail writes. |
mp4 | { durationSeconds, width, height, videoCodec, audioCodec, fps?, bitrateKbps? } — mp4 clips. |
Notes
url_rewrite_replacemust be sent together withurl_rewrite_match.- A playlist or clip is always built from one rendition — see Multi-resolution recordings.
- These endpoints read what's already archived; they don't trigger recording. Make sure the sessions you want have finished archiving (or stay back from the live edge for in-progress recordings).
- For automatically-captured live clips (rather than on-demand assets from the archive), see Snapshots.