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