Publish to object storage (S3, Azure Blob)
Available in: UI · API
Use this how-to to send the segmented output of a Live stream into object storage instead of a CDN ingest. Two providers cover it: S3 for any S3-compatible bucket and Azure Blob Storage for an Azure container. Both are ordinary Destinations — there is no separate form and no extra entity to create.
When to use this
When you want to own the origin. The bucket holds the manifests and segments the Encoder produces, a CDN in front of it serves the audience, and the storage bill replaces the CDN ingest contract. It is also the shortest path to an archive that a player can read directly, and to a self-hosted origin on premises.
S3 is not AWS-specific. The same signed upload surface works against AWS S3, MinIO, Ceph RGW, Wasabi, Backblaze B2, DigitalOcean Spaces and Cloudflare R2, including endpoints on a non-default port.
Prerequisites
- A bucket or container, already created. C21 Live Control does not create it. For S3, note the regional endpoint, the bucket name and the key prefix you want the output to live under; for Azure, the account endpoint and the container.
- Credentials scoped to that prefix. For S3, an Access Key ID and a Secret Access Key belonging to an identity that can write and delete under the prefix. For Azure, a container-scoped SAS token (
sr=c) with create and write permission. - A CDN in front of the bucket — recommended for anything an audience will watch. Its public URL is what goes into Playback URL.
- A configured Live stream whose Encoding produces HLS / TS, DASH or CMAF output.
- Required role. System Administrator to register the Destination; Operator (or System Administrator) to bind it to a Live stream and run the broadcast.
Constraints to be aware of
- Segmented formats only. Object storage Destinations accept
HLS / TS,DASHandCMAF. The provider is not offered on RTMP, Enhanced RTMP, SRT, STREAM, SDIOUT or Record. - No region field on S3, by design. The signing region is derived from the endpoint and corrected automatically — see Region (S3).
- Temporary STS credentials are not supported. Use long-lived keys.
- Per-object SSE-KMS keys are not supported. Bucket-level default encryption (SSE-S3 or SSE-KMS) applies transparently and needs nothing here.
- Storage class is the bucket default. Use a lifecycle rule to transition objects to a cheaper class.
- Azure Blob Storage is not purged by the Encoder. Old segments stay in the container until an Azure lifecycle management rule expires them.
- DASH and CMAF orphan one session's objects per Encoder restart. Their segment names carry a per-session timestamp, so a restart begins a new naming session and the previous session's objects are left behind — nothing in the product tracks them afterwards. A lifecycle rule on the prefix is required, not optional. HLS is unaffected: its segments are a continuous counter under a fixed path, so the window keeps rolling across restarts.
Via UI
Create the Destination
Navigate: Destinations → Add destination. Set Name, pick the Type (HLS / TS, DASH or CMAF) and set Provider to S3 or Azure Blob Storage. The rest of the form relabels itself around the provider.
For S3:
| UI label | API field | Value |
|---|---|---|
| Endpoint and bucket | settings_common.urls.primary_server | Endpoint, bucket and key prefix in one URL, for example https://s3.eu-west-1.amazonaws.com/my-bucket/live. |
| Stream name | settings_common.stream | Appended to the prefix — it becomes the key's next path segment. |
| Access Key ID | settings_common.username | The access key id. |
| Secret Access Key | settings_common.password | The matching secret. Stored encrypted; reads return the sentinel. |
| Playback URL | settings_common.urls.player | The CDN URL players should use. |
For Azure Blob Storage:
| UI label | API field | Value |
|---|---|---|
| Primary server | settings_common.urls.primary_server | https://<account>.blob.core.windows.net/<container>/<prefix>. |
| Stream name | settings_common.stream | Appended to the prefix, as above. |
| SAS token | settings_common.password | The container SAS. With or without its leading ? — both are accepted. There is no username: the SAS is the whole authorisation. |
| Playback URL | settings_common.urls.player | The CDN URL players should use. |
Get the URL right
Both S3 addressing styles are accepted and need nothing to distinguish them:
path-style https://s3.eu-west-1.amazonaws.com/my-bucket/live
virtual-hosted-style https://my-bucket.s3.eu-west-1.amazonaws.com/live
self-hosted https://minio.example.com:9000/my-bucket/live
With Stream name channel-1, the first form writes my-bucket/live/channel-1/index.m3u8 and its segments alongside.
Use a regional endpoint. The global s3.amazonaws.com answers with a redirect to the bucket's own region, and a redirected request no longer matches its signature. The editor warns about this under the field, and about an endpoint that is not https or that carries no bucket in its path. The warnings do not block the save.
Bind the Destination to a Live stream
Open the target Live stream in the Live streams editor and add the new Destination under Destinations — directly, or through a Destination group. Save.
Start the Live stream
Start the broadcast from On air. The Destination turns active on the per-Destination indicator of the on-air row, and objects begin to appear under the prefix.
Via API
Every call uses the standard bearer header Authorization: Bearer <YOUR_API_TOKEN>.
| Action | Method + path | operationId |
|---|---|---|
| List the provider catalogue | GET /c21apiv2/publishing/providers | getPublishingProviders |
| Create a Destination | POST /c21apiv2/publishings | addPublishing |
| Update a Destination | PUT /c21apiv2/publishings/{publishingId} | updatePublishing |
curl -X POST "https://<your-host>/c21apiv2/publishings" \
-H "Authorization: Bearer <YOUR_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "Origin bucket — Channel 1",
"type": "HLS / TS",
"settings_common": {
"provider": "S3",
"stream": "channel-1",
"username": "<Access Key ID>",
"password": "<Secret Access Key>",
"urls": {
"primary_server": "https://s3.eu-west-1.amazonaws.com/my-bucket/live",
"player": "https://cdn.example.com/live/channel-1/index.m3u8"
}
}
}'
The Azure variant is the same call with "provider": "Azure Blob Storage", the container URL in primary_server, the SAS token in password and no username.
Credentials are write-only. Every read path returns the sentinel •••••••• when a value is stored and an empty string when it is not; echoing the sentinel back on a write keeps the stored value, so a PUT that only changes the stream name does not need the secret in hand.
See API → Overview for the standard envelope and Pagination and errors for error handling.
Region (S3)
SigV4 binds a signature to a region, and the Encoder works the region out rather than asking for something the endpoint can state itself. In order:
- The endpoint's own answer. An unsigned probe on the bucket returns the region alongside its denial, so this resolves correctly even with credentials that may only write objects.
- The hostname, for the vendors that encode it in a label —
s3.<region>.amazonaws.com,s3.<region>.wasabisys.com,<region>.digitaloceanspaces.com, and*.r2.cloudflarestorage.com(which signs asauto). us-east-1, which is what an S3 implementation with no configured region accepts.- Correction. If the storage rejects the request as a malformed authorisation header, its answer names the region it expects; the Encoder adopts it, records the correction and re-signs. A misconfigured region therefore costs one request, not an outage.
The region in use is reported in the Encoder log when publishing starts.
Bucket setup
IAM policy (AWS S3)
Give the Destination its own identity with nothing but the verbs it uses, scoped to the prefix it writes. Do not reuse an administrative key.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublishSegments",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::my-bucket/live/*"
},
{
"Sid": "RegionDiscovery",
"Effect": "Allow",
"Action": "s3:GetBucketLocation",
"Resource": "arn:aws:s3:::my-bucket"
}
]
}
s3:DeleteObject is not optional in practice: without it the live window is never purged and the bucket grows for as long as the channel runs. A refused delete is reported in the Encoder log.
Public access
No ACL header is sent, so objects are private and how they are served is a bucket decision:
- Recommended: keep the bucket private and put CloudFront in front of it with Origin Access Control.
- Alternatively, a bucket policy granting
s3:GetObjecton the prefix.
Buckets created since April 2023 have ACLs disabled, so a public-read ACL would be rejected outright — which is why none is sent.
CORS
Browser playback needs CORS on the bucket, or on the CDN. A minimal policy allows GET and HEAD from the player's origin.
Lifecycle rule
The live window is maintained by the Encoder itself: as each segment leaves the window, it is deleted from the bucket. That keeps a running HLS channel bounded — measured on a reference channel at a steady 81 .ts objects, flat over time.
A lifecycle rule on the prefix is still required for DASH and CMAF, whose per-session segment names orphan the previous session's objects on every restart, and for Azure Blob Storage, which is never purged by the Encoder. Expire the prefix after a few days; it is also cheaper than a delete per object.
What each object carries
Object storage keeps exactly what the upload declared and serves it back for the life of the object, so the media type and the cache lifetime are set at upload time:
| Object | Content-Type | Cache-Control |
|---|---|---|
.m3u8 | application/vnd.apple.mpegurl | no-cache, max-age=1 |
.mpd | application/dash+xml | no-cache, max-age=1 |
.ts | video/MP2T | public, max-age=31536000, immutable |
.m4s, .mp4, .cmfv | video/mp4 | public, max-age=31536000, immutable |
.cmfa | audio/mp4 | public, max-age=31536000, immutable |
.vtt | text/vtt | public, max-age=31536000, immutable |
Manifests are rewritten every segment and must not be cached. Segments and init segments are immutable — their names carry the segment number or the session timestamp — so the long lifetime is what makes bucket-plus-CDN affordable.
Cost
Every segment of every rendition is one upload, and every segment leaving the window is one delete. With 2-second segments and five renditions that is roughly 9,000 uploads per hour per Destination, scaling linearly with the number of renditions. On a major cloud provider that is cents per hour, but it is recurring — size it before pointing a 24/7 channel at a bucket. Longer segments cut it proportionally.
Verify
- Objects appear under
<prefix>/<stream name>/within a segment duration or two of the start. - The object count stabilises instead of growing without bound: on a reference channel, 81
.tsobjects for HLS and 351 per rendition for DASH, flat over time. - The manifest is reachable through the CDN URL stored in Playback URL, and a player opened on that URL plays the live edge.
- Stopping the Live stream stops the uploads; the objects already written stay until the lifecycle rule expires them.
Troubleshooting
The storage provider's own error code is surfaced in the Encoder log rather than a bare HTTP status.
| Symptom | Cause |
|---|---|
403 SignatureDoesNotMatch | Wrong Secret Access Key. |
403 AccessDenied | The key is valid; the policy does not allow writes on that prefix. |
404 NoSuchBucket | Bucket name wrong, or the URL path is missing the bucket segment. |
| A logged region correction | Informational: the signing region was corrected automatically on the first request. |
| The bucket keeps growing, with delete failures logged | The policy is missing s3:DeleteObject. |
Azure: every upload returns 404 ResourceNotFound | The SAS arrived truncated, or is blob-scoped instead of container-scoped (sr=c). |
| Objects arrive but playback fails | Check CORS on the bucket or the CDN, and that Playback URL points at the CDN. |
| Playback serves a stale manifest | A cache in front of the bucket is ignoring Cache-Control on the manifest. |
An Azure container SAS runs to roughly 160 characters and its signature is the last thing in it, so any truncation on the way in silently removes the part that authorises the request — and every upload comes back 404 ResourceNotFound. Paste it whole.
FAQ
us-east-1, and if the storage rejects the signature naming a different region it adopts that one and re-signs. A wrong guess costs one request.Real Time streaming to Dolby Millicast
Broadcast with sub-second, end-to-end latency — Real time encoding on the Live stream, RTMP or Enhanced RTMP contribution, and Dolby Millicast WebRTC distribution.
Protect a Live stream with multi-DRM
Register a DRM provider and attach it to a Publishing — through the UI, through the API, or both.