How Tos

Publish to object storage (S3, Azure Blob)

Publish the HLS / TS, DASH or CMAF output of a Live stream straight into an S3-compatible bucket or an Azure Blob container, with a CDN in front.

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.

A bucket is not a CDN. Object storage is an origin: it has no caching, no geographic distribution, and it bills every request. Serving playback straight from the bucket works, but put a CDN in front of it and give players the CDN URL through the Destination's Playback URL field.

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, DASH and CMAF. 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 labelAPI fieldValue
Endpoint and bucketsettings_common.urls.primary_serverEndpoint, bucket and key prefix in one URL, for example https://s3.eu-west-1.amazonaws.com/my-bucket/live.
Stream namesettings_common.streamAppended to the prefix — it becomes the key's next path segment.
Access Key IDsettings_common.usernameThe access key id.
Secret Access Keysettings_common.passwordThe matching secret. Stored encrypted; reads return the sentinel.
Playback URLsettings_common.urls.playerThe CDN URL players should use.

For Azure Blob Storage:

UI labelAPI fieldValue
Primary serversettings_common.urls.primary_serverhttps://<account>.blob.core.windows.net/<container>/<prefix>.
Stream namesettings_common.streamAppended to the prefix, as above.
SAS tokensettings_common.passwordThe container SAS. With or without its leading ? — both are accepted. There is no username: the SAS is the whole authorisation.
Playback URLsettings_common.urls.playerThe 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>.

ActionMethod + pathoperationId
List the provider catalogueGET /c21apiv2/publishing/providersgetPublishingProviders
Create a DestinationPOST /c21apiv2/publishingsaddPublishing
Update a DestinationPUT /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:

  1. 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.
  2. 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 as auto).
  3. us-east-1, which is what an S3 implementation with no configured region accepts.
  4. 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:GetObject on 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:

ObjectContent-TypeCache-Control
.m3u8application/vnd.apple.mpegurlno-cache, max-age=1
.mpdapplication/dash+xmlno-cache, max-age=1
.tsvideo/MP2Tpublic, max-age=31536000, immutable
.m4s, .mp4, .cmfvvideo/mp4public, max-age=31536000, immutable
.cmfaaudio/mp4public, max-age=31536000, immutable
.vtttext/vttpublic, 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 .ts objects 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.

SymptomCause
403 SignatureDoesNotMatchWrong Secret Access Key.
403 AccessDeniedThe key is valid; the policy does not allow writes on that prefix.
404 NoSuchBucketBucket name wrong, or the URL path is missing the bucket segment.
A logged region correctionInformational: the signing region was corrected automatically on the first request.
The bucket keeps growing, with delete failures loggedThe policy is missing s3:DeleteObject.
Azure: every upload returns 404 ResourceNotFoundThe SAS arrived truncated, or is blob-scoped instead of container-scoped (sr=c).
Objects arrive but playback failsCheck CORS on the bucket or the CDN, and that Playback URL points at the CDN.
Playback serves a stale manifestA 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

Copyright © 2026