Skip to main content

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.m3u8 at 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 Bearer JWT whose aud/roles include service-account (or internal-account). The project is taken from the token — a stream_key belonging 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 paramRequiredDescription
stream_keyThe stream's ID (PublicKey).
start_datetimeStart of the range.
end_datetimeEnd of the range.
url_rewrite_matchGo-regexp pattern matched against each full segment URL (used with url_rewrite_replace).
url_rewrite_replaceReplacement string; supports capture groups ($1, $2, …). Must be sent with url_rewrite_match.
output_pathS3 path (relative to the archive bucket root) to also save the generated index.m3u8 (created if needed, overwrites existing).
responsebody (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 paramRequiredDescription
stream_keyStream ID.
start_datetimeStart of the clip range.
end_datetimeMust be > start, and end − start must not exceed the max clip duration (default 3 min).
output_pathS3 path; final key is {output_path}/clip{startUnixMs}-{endUnixMs}.mp4.
kf_accuratetrue 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_accurate off): 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 paramRequiredDescription
stream_keyStream ID.
datetimeTimestamp to extract a frame at.
formatjpg | png | webp — default jpg.
quality0–100. jpg/webp = perceptual quality (higher is better); png maps to compression level (png is lossless either way). Default 63.
width1–4096. If only one of width/height is given, the other is computed to preserve aspect. Both omitted → source variant's natural resolution.
height1–4096; see width.
aspect_methodcenter-cut | pillar-box — used only when both width & height are given. Default center-cut.
output_pathS3 path; also writes the image to {output_path}/thumb{unixMillisOfDatetime}.{ext}.
responsebody (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:

FieldDescription
s3BucketBucket the asset was written to.
s3ObjectKeyObject key within the bucket.
s3UriCanonical s3://bucket/key URI.
contentTypeMIME type of the uploaded asset.
bytesSize in bytes.
etagS3 ETag (optional).
playbackUrlPublic 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_replace must be sent together with url_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.