Streaming audio and video
StreamingPreviewStreaming prepares an adaptive HLS stream of an audio or video asset once it is decided, so playback starts quickly, adapts to the viewer's connection and seeks straight away. Turn it on for a project or an upload, then play the CDN's hls resource in your own player or the asset component, or show the embed page in an iframe on any site.
Turn streaming on
Streaming is off by default. Turn it on in one of three places:
| Where | Applies to |
|---|---|
| Prepare streams for new uploads | Every new audio and video upload to the project. |
processingOptions.features.streaming.enabled | One upload. true or false wins over the project setting. |
| Upload presets | Uploads through the preset, including browser uploads. |
A stream is prepared from the content CalmLens keeps, so the upload also needs processingOptions.keepContentAfterProcessing: true:
import { readFile } from "node:fs/promises";
const asset = await client.assets.create({
name: "clip.mp4",
content: { buffer: await readFile("./clip.mp4"), type: "video/mp4" },
processingOptions: {
keepContentAfterProcessing: true,
features: { streaming: { enabled: true } },
},
});
// content.hls is null until the stream is prepared, after the decision and the browser-safe file.
console.log(asset.id, asset.content.hls);
Turning the project setting on doesn't change media that is already stored. To stream it too, prepare streams for existing media from Prepare streams for new uploads: the portal shows how many files and billed minutes of video that covers, and what it costs, before you confirm.
Without billing, an organisation's video streams use its free streaming allowance of 60 started minutes a calendar month (UTC), so you can try streaming and the embed page before setting up billing; audio-only streams don't use it. A stream that doesn't fit in the minutes left isn't prepared until the next month or until billing is set up, and the portal shows how many minutes are left before it prepares streams for existing media. With billing, a stream whose expected cost would pass a spend limit set to pause uploads isn't prepared. Either way, the asset is moderated and served as usual, without a stream.
What a stream contains
Each stream is HLS with fragmented MP4 (CMAF) segments of six seconds:
| Asset | Renditions |
|---|---|
| Video | H.264 video at the source's size, up to 1080p, and at each standard size below it: 1080p, 720p, 480p and 360p. Sizes refer to the picture's shorter side, and a source is never enlarged. One AAC audio rendition plays with every picture size; the source's AAC audio is copied without re-encoding. |
| Audio | One AAC audio rendition. |
Players choose a picture size from the viewer's screen and connection, and switch between them as playback continues.
When a stream is ready
The stream is prepared from the asset's browser-safe file after the decision, in jobs of its own, so it never delays the decision.
Until the stream is ready, content.hls is null and the hls resource returns 404 asset/no-rendered-version. Once it is, content.hls.renditions lists every rendition, video from the largest down and then audio, with its kind, width, height, bandwidth and codecs. Retrieve the asset with client.assets.retrieve({ assetId }) to check. Until then, play the rendered file, which every browser can play; the asset component and the embed page do this for you.
content.hls is also null while a safety or copyright hold is on, and once the content is removed.
content.hlsStatus says what became of a stream that was asked for:
status | Meaning |
|---|---|
preparing | The stream is on its way. |
ready | content.hls lists it. |
unavailable | It won't be prepared; reason says why. |
reason | Why the stream isn't prepared | What to do |
|---|---|---|
allowance-used | This month's free streaming minutes don't have room for it. | Set up billing, or prepare it for existing media next month. |
billing-required | It is longer than a whole month's free minutes. | Set up billing. |
spend-limit | Its expected cost would pass a spend limit set to pause uploads. | Raise the limit, then prepare streams for existing media. |
processing-failed | Preparing it failed. Nothing is billed. | Upload the media again, or prepare streams for existing media. |
content-not-kept | The upload doesn't keep its content, so there is nothing to stream. | Upload it with keepContentAfterProcessing. |
reason is null unless status is unavailable. content.hlsStatus is null when no stream was asked for, while a hold is on and once the content is removed. When only the project's setting asks for streams, it stays null until the asset's browser-safe file exists. Either way the asset plays its rendered file.
Embed media in any site
The embed resource is a small page that shows the asset, made for an <iframe> on your own site or someone else's. For audio and video it plays the stream when one is ready, natively where the browser plays HLS and with a small player it loads otherwise, and the browser-safe file until then, with the video's poster or the audio's waveform. Other assets show in the asset viewer for their type: an image, a website's screenshot, text, a PDF or a document rendered as one, or a spreadsheet's sheets. Any site may frame it.
For a private asset, sign its asset URL on your server and append /embed, with client.cdn.signAssetUrl({ assetId, resource: "embed" }):
const embed = await client.cdn.signAssetUrl({ assetId, resource: "embed", expiresIn: 3600 });
// Use embed.url as the src of an <iframe> in your page.
console.log(embed.url); // …/sig_…/embed
Then use the URL as the frame's source:
<iframe
src="https://cdn.calmlens.com/v1/assets/proj_…/sig_…/embed"
title="Product demo"
allow="fullscreen; picture-in-picture"
style="width: 100%; aspect-ratio: 16 / 9; border: 0"
></iframe>The page fills its frame and fits the asset inside it, so size the frame for the asset, such as to a video's aspect ratio, from content.rendered.width and height. allow="fullscreen; picture-in-picture" lets viewers use the player's full-screen and picture-in-picture buttons. A public asset's embed page works from its plain asset URL, …/assets/proj_…/asset_…/embed.
The page passes the same delivery checks as every other resource, and shows a status message instead of the asset when they fail:
| The asset | The embed page shows |
|---|---|
age-restricted, without viewerClaims.ageConfirmed: true in the URL | Its blurred placeholder, with an age-restricted message |
| blocked or purged, held, refused in the viewer's region, or behind an expired or invalid signed asset URL | An unavailable message |
| Nothing to show yet, such as a document before its PDF is ready | A message that it can't be shown in the page |
Add the viewer's viewerClaims to the embed URL, such as ageConfirmed: true once your app has confirmed the viewer's age; see Age-restricted assets. The files the page loads carry the same claims, under a grant that expires with the embed URL's own, and don't count as reads again.
Play the stream in your own player
The hls resource is the stream's master playlist, at /hls/index.m3u8 beneath the asset's CDN path. Any HLS player can play it. For a private asset, sign its asset URL and append /hls/index.m3u8, with client.cdn.signAssetUrl({ assetId, resource: "hls" }):
// The grant covers the master playlist and every playlist and segment beneath it.
const stream = await client.cdn.signAssetUrl({ assetId, resource: "hls", expiresIn: 3600 });
console.log(stream.url); // …/sig_…/hls/index.m3u8
The playlists list their files by relative paths, so the player requests every media playlist and segment beneath the same signed asset URL; pass the master playlist's URL to the player unchanged. Choose an expiry that covers the viewing session: once it passes, the player's next requests fail with 403 signed-url/expired, and it needs a new URL to continue.
Stream files are served with Access-Control-Allow-Origin: *, so players on any site can fetch them. Segments are cached by the CDN like other files; playlists carry the request's viewer claims, so only the viewer's browser keeps them, for up to a minute. The stream passes the same delivery checks as the other resources, on every playlist and segment request.
Play the stream with the asset component
The JavaScript SDK's asset component plays content.hls automatically when the asset has it, and the browser-safe file otherwise. For a private asset, pass its signed asset URL as src: the stream is a path beneath it, like every other resource, and onExpired returns a fresh URL when it expires. Set video.stream to false to play the file instead.
Billing and limits
| Usage | Billed as |
|---|---|
| Preparing a video's stream with billing | Stream encoding at £10.00 per 1,000 minutes, counting each started minute of video, at least one, once per content revision. The video's audio rendition is included in its minutes. A preparation that fails isn't billed. |
| Preparing a video's stream without billing | Free, from the monthly allowance of 60 started minutes, counted the same way. |
| Preparing an audio-only stream | Free. |
| Keeping the stream | Its files count towards retained storage, with the asset's other content. |
| Playing the stream | One CDN read for each six-second media segment the player loads. A video plays its picture and audio as separate segments, so each six seconds of video usually counts two reads. Playlists and each rendition's initialisation segment aren't counted. |
| Loading the embed page | One CDN read per page load. The files the page loads aren't counted again. |
The meters and how usage is billed are described in Pricing.
- Streams are prepared for audio and video within the long media limits: up to 30 minutes and 2 GiB.
- Replacing an asset's content prepares a new stream for the new content, billed again, and deletes the previous one.
- Removing the content or deleting the asset deletes its stream files.
- Streaming is in preview, so the renditions it prepares and what the embed page shows may change.