# Streaming audio and video

Streaming (preview)

Streaming 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](/docs/widgets/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](/portal/proj_current/settings#streaming) | 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](/docs/guides/upload-presets#settings)                     | Uploads through the preset, including browser uploads.       |

A stream is prepared from the content CalmLens keeps, so the upload also needs `processingOptions.keepContentAfterProcessing: true`:

**JavaScript/TypeScript Server SDK**

```typescript
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);
```

**python**

```python
with open("clip.mp4", "rb") as content:
    asset = client.assets.create({
        "name": "clip.mp4",
        "content": {"data": content, "type": "video/mp4"},
        "processingOptions": {
            "keepContentAfterProcessing": True,
            "features": {"streaming": {"enabled": True}},
        },
    })
# content.hls is None until the stream is prepared, after the decision and the browser-safe file.
print(asset["id"], asset["content"]["hls"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {
    content, err := os.Open("clip.mp4")
    if err != nil { log.Fatal(err) }
    defer content.Close()
    asset, err := client.Assets.Create(context.Background(), calmlens.SubmitAssetOptions{
        Name:    "clip.mp4",
        Content: &calmlens.Content{Data: content, Type: "video/mp4"},
        ProcessingOptions: &calmlens.ProcessingOptions{
            KeepContentAfterProcessing: calmlens.Ptr(true),
            Features: &calmlens.ProcessingFeatures{Streaming: &calmlens.StreamingFeatureOptions{Enabled: true}},
        },
    })
    if err != nil { log.Fatal(err) }
    // Content.HLS is nil until the stream is prepared, after the decision and the browser-safe file.
    fmt.Println(asset.ID, asset.Content.HLS)
}
```

**ruby**

```ruby
asset = client.assets.create(
  name: 'clip.mp4',
  content: {data: File.binread('clip.mp4'), type: 'video/mp4'},
  processing_options: {
    keep_content_after_processing: true,
    features: {streaming: {enabled: true}}
  }
)
# content.hls is nil until the stream is prepared, after the decision and the browser-safe file.
puts asset['id'], asset['content']['hls'].inspect
```

**rust**

```rust
use calmlens::{models::*, ServerClient};

async fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    let bytes = std::fs::read("clip.mp4")?;
    let asset = client.assets().create(SubmitAsset {
        processing_options: Some(ProcessingOptions {
            keep_content_after_processing: Some(true),
            features: Some(ProcessingFeatures { streaming: Some(FeatureOptions::enabled()), ..Default::default() }),
            ..Default::default()
        }),
        ..SubmitAsset::new("clip.mp4", Content::bytes(bytes, "video/mp4"))
    }).await?;
    // content.hls is None until the stream is prepared, after the decision and the browser-safe file.
    println!("{} {:?}", asset.id, asset.content.hls);
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensServerClient;
import com.calmlens.Models.*;

public class Example {
  public static void example(CalmLensServerClient client) throws Exception {
    byte[] bytes = java.nio.file.Files.readAllBytes(java.nio.file.Path.of("clip.mp4"));
    Asset asset = client.assets.create(new SubmitAsset("clip.mp4", Content.bytes(bytes, "video/mp4"))
        .processingOptions(new ProcessingOptions()
            .keepContentAfterProcessing(true)
            .features(new ProcessingFeatures().streaming(new FeatureOptions(true)))));
    // content().hls() is null until the stream is prepared, after the decision and the browser-safe file.
    System.out.println(asset.id() + " " + asset.content().hls());
  }
}
```

**php**

```php
$asset = $client->assets->create([
    'name' => 'clip.mp4',
    'content' => ['data' => file_get_contents('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.
echo $asset['id'], ' ', json_encode($asset['content']['hls']), PHP_EOL;
```

**curl**

```bash
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/assets?contentType=video%2Fmp4&wait=false" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "X-CalmLens-Upload: content" \
  --form-string 'options={"name":"clip.mp4","processingOptions":{"keepContentAfterProcessing":true,"features":{"streaming":{"enabled":true}}}}' \
  -F "file=@clip.mp4;type=video/mp4"
```

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](/portal/proj_current/settings#streaming): 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](/docs/usage-limits#spend-limits-and-project-budgets) 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](/docs/guides/audio-and-video#show-audio-and-video-in-your-app) 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`](/docs/api/cdn#error-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](/docs/usage-limits#spend-limits-and-project-budgets) 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](/docs/widgets/asset-viewers) 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](/docs/guides/delivering-assets#signed-asset-urls) on your server and append `/embed`, with `client.cdn.signAssetUrl({ assetId, resource: "embed" })`:

**JavaScript/TypeScript Server SDK**

```typescript
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
```

**python**

```python
embed = client.cdn.sign_asset_url(asset_id="asset_5Gm2Rk8XqT3wLp9VbN4c", resource="embed", expires_in=3600)
# Use embed["url"] as the src of an <iframe> in your page.
print(embed["url"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"

    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {
    embed, err := client.CDN.SignAssetURL(context.Background(), calmlens.SignAssetURLOptions{AssetID: "asset_5Gm2Rk8XqT3wLp9VbN4c", Resource: calmlens.ResourceEmbed, ExpiresIn: 3600})
    if err != nil { log.Fatal(err) }
    // Use embed.URL as the src of an <iframe> in your page.
    fmt.Println(embed.URL)
}
```

**ruby**

```ruby
embed = client.cdn.sign_asset_url(asset_id: 'asset_5Gm2Rk8XqT3wLp9VbN4c', resource: 'embed', expires_in: 3600)
# Use embed['url'] as the src of an <iframe> in your page.
puts embed['url']
```

**rust**

```rust
use calmlens::{models::*, ServerClient};

async fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    let embed = client.cdn().sign_asset_url(SignAssetUrlOptions { asset_id: Some("asset_5Gm2Rk8XqT3wLp9VbN4c".into()), resource: Some(CdnResource::Embed), expires_in: Some(3600), ..Default::default() }).await?;
    // Use embed.url as the src of an <iframe> in your page.
    println!("{}", embed.url);
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensServerClient;
import com.calmlens.Models.*;

public class Example {
  public static void example(CalmLensServerClient client) {
    var embed = client.cdn.signAssetUrl(new SignAssetUrlOptions("asset_5Gm2Rk8XqT3wLp9VbN4c").resource("embed").expiresIn(3600L));
    // Use embed.url() as the src of an <iframe> in your page.
    System.out.println(embed.url());
  }
}
```

**php**

```php
$embed = $client->cdn->signAssetUrl(['assetId' => 'asset_5Gm2Rk8XqT3wLp9VbN4c', 'resource' => 'embed', 'expiresIn' => 3600]);
// Use $embed['url'] as the src of an <iframe> in your page.
echo $embed['url'], PHP_EOL;
```

**curl**

```bash
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/signed-asset-urls" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "assetId": "'"$ASSET_ID"'",
  "expiresIn": 3600
}'
```

Then use the URL as the frame's source:

```html
<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](/docs/guides/asset-lifecycle#delivery-while-moderation-runs) 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](/docs/guides/delivering-assets#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" })`:

**JavaScript/TypeScript Server SDK**

```typescript
// 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
```

**python**

```python
# The grant covers the master playlist and every playlist and segment beneath it.
stream = client.cdn.sign_asset_url(asset_id="asset_5Gm2Rk8XqT3wLp9VbN4c", resource="hls", expires_in=3600)
print(stream["url"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"

    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {
    // The grant covers the master playlist and every playlist and segment beneath it.
    stream, err := client.CDN.SignAssetURL(context.Background(), calmlens.SignAssetURLOptions{AssetID: "asset_5Gm2Rk8XqT3wLp9VbN4c", Resource: calmlens.ResourceHLS, ExpiresIn: 3600})
    if err != nil { log.Fatal(err) }
    fmt.Println(stream.URL)
}
```

**ruby**

```ruby
# The grant covers the master playlist and every playlist and segment beneath it.
stream = client.cdn.sign_asset_url(asset_id: 'asset_5Gm2Rk8XqT3wLp9VbN4c', resource: 'hls', expires_in: 3600)
puts stream['url']
```

**rust**

```rust
use calmlens::{models::*, ServerClient};

async fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    // The grant covers the master playlist and every playlist and segment beneath it.
    let stream = client.cdn().sign_asset_url(SignAssetUrlOptions { asset_id: Some("asset_5Gm2Rk8XqT3wLp9VbN4c".into()), resource: Some(CdnResource::Hls), expires_in: Some(3600), ..Default::default() }).await?;
    println!("{}", stream.url);
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensServerClient;
import com.calmlens.Models.*;

public class Example {
  public static void example(CalmLensServerClient client) {
    // The grant covers the master playlist and every playlist and segment beneath it.
    var stream = client.cdn.signAssetUrl(new SignAssetUrlOptions("asset_5Gm2Rk8XqT3wLp9VbN4c").resource("hls").expiresIn(3600L));
    System.out.println(stream.url());
  }
}
```

**php**

```php
// The grant covers the master playlist and every playlist and segment beneath it.
$stream = $client->cdn->signAssetUrl(['assetId' => 'asset_5Gm2Rk8XqT3wLp9VbN4c', 'resource' => 'hls', 'expiresIn' => 3600]);
echo $stream['url'], PHP_EOL;
```

**curl**

```bash
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/signed-asset-urls" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "assetId": "'"$ASSET_ID"'",
  "expiresIn": 3600
}'
```

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`](/docs/api/cdn#error-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](/docs/widgets/asset-component#adaptive-streams) 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](/docs/pricing#what-each-request-is-billed-for).

- Streams are prepared for audio and video within the [long media limits](/docs/guides/audio-and-video#coverage-and-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.
