# Media sources

Set what a media element plays and how its engine plays it with the structured source property

Every media element takes a `src`. Elements that drive a playback engine also take `source`, a structured object that carries the URL alongside everything else about that source:

```ts
const video = document.querySelector('hlsjs-video');

video.source = {
  src: 'https://example.com/stream.m3u8',
  preferPlayback: 'mse',
  engine: { hlsJs: { maxBufferLength: 60 } },
};
```

## Three tiers, three homes

The shape answers one question: does this option describe the source, or how to play it?

| Tier | Home | Example |
| --- | --- | --- |
| Which source to play | `source.src`, or an element’s own identity fields | `src`, `<mux-video>`’s `playbackId` |
| How to interpret it | `source.type` | `'video/mp4'` |
| How Video.js plays it | `source`, at the top level | `preferPlayback`, `maxAutoResolution`, `capRenditionToPlayerSize` |
| How a specific engine behaves | `source.engine`, under that engine’s name | hls.js’s `maxBufferLength`, in `source.engine.hlsJs` |
| An extension’s own settings | that extension’s attributes and properties | [Mux Data](https://videojs.org/docs/framework/html/guides/mux-data), [`<google-cast>`](https://videojs.org/docs/framework/html/reference/components/google-cast) |

`type` is worth reaching for when the URL lies about its contents. Video.js infers the content type from the file extension, so a manifest served from an extensionless or signed URL may need to say so explicitly:

```ts
video.source = { src: 'https://example.com/asset?id=42', type: 'application/vnd.apple.mpegurl' };
```

The legacy HLS spelling `application/x-mpegURL` is accepted as an alias, and matching is case-insensitive. An HLS source whose `type` isn’t recognized is handed to the browser as-is rather than to hls.js, which leaves quality and audio-track selection unavailable.

## `src` and `source` stay in sync

They are two views of the same thing, and writing either updates the other. Setting `source` derives `src`. Setting `src` replaces only the identity half and **keeps the rest, such as `type` and the engine options, intact**:

```ts
video.source = { src: 'https://example.com/a.m3u8', preferPlayback: 'native' };

video.src = 'https://example.com/b.m3u8';

// playback options survive the URL change
video.source; // { src: 'https://example.com/b.m3u8', preferPlayback: 'native' }
```

A `sourcechange` event fires whenever `source` changes, from either direction.

## Assigning `source` replaces it

`source` is not merged. A new object is a fresh start, and keys you leave out are dropped:

```ts
video.source = { src, type: 'video/mp4', preferPlayback: 'native' };

// type and preferPlayback are gone, because the new object omits them
video.source = { src };
```

## Equivalent sources cost nothing

Sources are compared **structurally**, not by reference. Reassigning an object with the same values is a no-op — no reload, and no engine teardown:

```ts
video.source = { src, engine: { hlsJs: { maxBufferLength: 60 } } };
video.source = { src, engine: { hlsJs: { maxBufferLength: 60 } } }; // no-op
```

Only a change to the engine options, `preferPlayback`, or the resolved content type recreates the playback engine.

> **Note**
>
> `source` holds an object, so it’s a property, not an attribute — there is no `source=""` in HTML. `src` remains the attribute, and it’s the one to reach for when your markup only names a URL.

## Engine options

Engine options live under `source.engine`, **namespaced by engine**. Each key there holds that engine’s own configuration object, handed over untouched — there’s no Video.js wrapper around it, so whatever the engine documents works:

| Key | Elements that read it | It holds |
| --- | --- | --- |
| `engine.hlsJs` | [`<hlsjs-video>`](https://videojs.org/docs/framework/html/reference/components/hlsjs-video), [`<mux-video>`](https://videojs.org/docs/framework/html/reference/components/mux-video), [`<mux-audio>`](https://videojs.org/docs/framework/html/reference/components/mux-audio) | an [hls.js config](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) |
| `engine.nativeHls` | [`<native-hls-video>`](https://videojs.org/docs/framework/html/reference/components/native-hls-video), and the three above whenever the browser plays the manifest | options for the browser’s own HLS support (today, [DRM](#licensing-one-engine-differently)) |
| `engine.dashJs` | [`<dash-video>`](https://videojs.org/docs/framework/html/reference/components/dash-video) | [dash.js settings](https://cdn.dashjs.org/latest/jsdoc/module-Settings.html) |
| `engine.vimeo` | [`<vimeo-video>`](https://videojs.org/docs/framework/html/reference/components/vimeo-video) | [Vimeo embed parameters](https://developer.vimeo.com/player/sdk/embed) |
| `engine.youtube` | [`<youtube-video>`](https://videojs.org/docs/framework/html/reference/components/youtube-video) | [YouTube player parameters](https://developer.chrome.com/docs/youtube/iframe_api_reference#Parameters) |
| `engine.cloudflare` | [`<cloudflare-video>`](https://videojs.org/docs/framework/html/reference/components/cloudflare-video) | [Stream player parameters](https://developers.cloudflare.com/stream/viewing-videos/using-the-stream-player/) |
| `engine.spotify` | [`<spotify-audio>`](https://videojs.org/docs/framework/html/reference/components/spotify-audio) | [Spotify embed options](https://developer.spotify.com/documentation/embeds) |
| `engine.tiktok` | [`<tiktok-video>`](https://videojs.org/docs/framework/html/reference/components/tiktok-video) | [TikTok player parameters](https://developers.tiktok.com/doc/embed-player) |
| `engine.twitch` | [`<twitch-video>`](https://videojs.org/docs/framework/html/reference/components/twitch-video) | [Twitch embed parameters](https://dev.twitch.tv/docs/embed/video-and-clips/) |

Naming the engine rather than using one generic key matters where an element has more than one to choose from. `<hlsjs-video>` plays through hls.js or through the browser depending on the platform and `preferPlayback`, and only one of them runs — so a single source can describe both paths without either engine reading the other’s options.

hls.js reads its options when the engine is constructed, so changing them tears down the engine and builds a new one:

```ts
video.source = {
  src: 'https://example.com/stream.m3u8',
  engine: { hlsJs: { maxBufferLength: 60, enableWorker: false } },
};
```

dash.js takes settings on a running player, so `engine.dashJs` is applied in place and playback continues uninterrupted:

```ts
const video = document.querySelector('dash-video');

video.source = {
  src: 'https://example.com/manifest.mpd',
  engine: { dashJs: { streaming: { abr: { maxBitrate: { video: 2000 } } } } },
};
```

Because `engine.dashJs` replaces rather than merges, dropping a key restores the dash.js default instead of leaving the old value behind.

### Options Video.js normalizes

Where an option means the same thing across engines, it sits on `source` itself rather than inside an engine’s namespace.

`preferPlayback` picks between [hls.js](https://github.com/video-dev/hls.js/) and the browser’s own HLS support:

```ts
video.source = { src: 'https://example.com/stream.m3u8', preferPlayback: 'native' };
```

It’s a preference, not a demand — Video.js falls back to whichever path can actually play the source. `<hlsjs-video>`, `<mux-video>`, and `<mux-audio>` accept it; DASH and Vimeo have no second playback path.

`maxAutoResolution` caps the highest rendition adaptive bitrate selection reaches for on its own:

```ts
video.source = { src: 'https://example.com/stream.m3u8', maxAutoResolution: '720p' };
```

The cap limits automatic selection without hiding anything. Renditions above it stay in `videoRenditions`, so a viewer can still choose 1080p by hand. Reach for it to hold down bandwidth on a stream that would otherwise climb to 4K, or to keep a background or thumbnail player cheap.

Renditions are matched on pixel area rather than height, which keeps unusual aspect ratios honest: an ultrawide 2560×1080 rendition carries more pixels than 16:9 1080p, so a `'1080p'` cap leaves it out. When every rendition sits above the cap, the smallest one plays.

Changing the cap applies to the running engine, so playback continues uninterrupted. It needs the hls.js engine — native HLS playback ignores it, and audio-only streams have no video renditions to cap.

### Capping to the player’s size

A 400px-wide player has no use for a 4K rendition, so `capRenditionToPlayerSize` holds automatic selection to the smallest rendition that still covers the element. It defaults to `true`, and the cap follows the element as it’s resized:

```ts
video.source = { src: 'https://example.com/stream.m3u8', capRenditionToPlayerSize: false };
```

The element is measured in device pixels, so a `2` device pixel ratio asks for twice the rendition a CSS measurement would — a 640px-wide player on a retina screen still gets 720p. Switch the cap off for a player whose layout size understates what it needs, such as one that goes fullscreen without a resize.

`minAutoResolution` bounds how far down that cap can reach, and defaults to `'720p'`:

```ts
video.source = { src: 'https://example.com/stream.m3u8', minAutoResolution: '480p' };
```

**It is not a quality floor.** It bounds the size-derived cap and nothing else. When bandwidth is poor, adaptive selection still drops below it — the cap is a ceiling, and selection stays free underneath. It also never raises an explicit `maxAutoResolution`: ask for at most `'360p'` alongside a `'720p'` floor and you get `'360p'`.

The default is there because the low rungs of a ladder exist for poor network conditions, and capping a small player down to them looks worse than its size suggests. Name a lower rung to weaken the floor, or `'270p'` to lift it for any real ladder.

### Four things that sound alike

Four separate mechanisms bound which rendition plays, and they’re easy to mistake for each other:

| Option | Side | What it does |
| --- | --- | --- |
| `source.playback.maxResolution` | Server | A Mux query param. Trims the manifest, so higher renditions never arrive — not even by hand. |
| `source.maxAutoResolution` | Client | A ceiling on automatic selection. Everything still arrives and stays selectable by hand. |
| `source.capRenditionToPlayerSize` | Client | The same kind of ceiling, derived from the element’s rendered size and moving with it. |
| `source.minAutoResolution` | Client | A bound on the one above. Not a quality minimum, and not related to `playback.minResolution`. |

`playback.maxResolution` and `playback.minResolution` are the server-side pair: they decide what the manifest carries. The other three only bound what adaptive selection picks from whatever does arrive.

## DRM protected sources

Protected content is licensed through `source.drm`, keyed by EME key system id — alongside the URL rather than inside an engine’s namespace, because which engine plays the manifest is decided later and both paths read it:

```ts
video.source = {
  src: 'https://example.com/protected.m3u8',
  drm: {
    'com.apple.fps': {
      licenseUrl: 'https://license.example.com/fairplay',
      serverCertificateUrl: 'https://license.example.com/fairplay-cert',
    },
    'com.widevine.alpha': { licenseUrl: 'https://license.example.com/widevine' },
    'com.microsoft.playready': { licenseUrl: 'https://license.example.com/playready' },
  },
};
```

Name every system you hold a license server for — which one gets used is the browser’s choice. `serverCertificateUrl` is the DRM server (application) certificate FairPlay requires; Widevine and PlayReady ignore it.

### What each path can license

The same `drm` reaches both playback paths, and how far it gets is the one thing worth knowing about each:

| Path | Key systems it negotiates | What Video.js does with `drm` |
| --- | --- | --- |
| hls.js (MSE) | FairPlay, Widevine, PlayReady | Hands it to hls.js as `drmSystems`, with `emeEnabled` switched on |
| The browser’s own HLS | FairPlay | Answers the element’s key requests itself: POSTs the CDM’s license request to `licenseUrl` and hands the response back |

So a source naming only Widevine plays through hls.js and cannot play on the native path — [`<native-hls-video>`](https://videojs.org/docs/framework/html/reference/components/native-hls-video), or any HLS element the browser ends up playing the manifest for. Video.js says so in development, and encrypted media that gets there with no `com.apple.fps` license server fails with a `MEDIA_ERR_ENCRYPTED` error rather than hanging.

For Widevine on hls.js, Video.js asks for a hardware-backed CDM first, falling back to whatever robustness the browser offers, so content restricted to L1 devices plays where it can. Supplying your own `requestMediaKeySystemAccessFunc` replaces that entirely.

### Licensing one engine differently

An engine’s own `drmSystems` **replaces** `source.drm`, for that engine alone. Reach for it when one path licenses differently, or when you need more of [hls.js’s DRM configuration](https://github.com/video-dev/hls.js/blob/master/docs/API.md#drmsystems) than `drm` covers:

```ts
video.source = {
  src: 'https://example.com/protected.m3u8',
  drm: { 'com.apple.fps': { licenseUrl, serverCertificateUrl } },
  engine: {
    // hls.js licenses from here instead, and the native path still from `drm`.
    hlsJs: { drmSystems: { 'com.widevine.alpha': { licenseUrl: widevineLicenseUrl } } },
  },
};
```

It replaces rather than merges, so the FairPlay server above is gone as far as hls.js is concerned. `engine.nativeHls.drmSystems` does the same for the native path.

## Mux sources name a playback ID

[`<mux-video>`](https://videojs.org/docs/framework/html/reference/components/mux-video) and [`<mux-audio>`](https://videojs.org/docs/framework/html/reference/components/mux-audio) identify a source by `playbackId` rather than a URL, and derive `src` from it. Everything else works the same, engine options included:

```ts
const video = document.querySelector('mux-video');

video.source = {
  playbackId: 'BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM',
  playback: { maxResolution: '1080p' },
  engine: { hlsJs: { maxBufferLength: 60 } },
};
```

`playback` params ride along in the URL, so `playback.maxResolution` decides which renditions the manifest carries in the first place. `maxAutoResolution` is the other half of that pair: it caps what adaptive selection picks from the renditions that do arrive. Use `playback.maxResolution` when a viewer should never get 4K, and `maxAutoResolution` when they should be able to ask for it.

The same split applies at the bottom of the ladder, and the names are close enough to be worth restating: `playback.minResolution` asks Mux to leave the lowest renditions out of the manifest, while `minAutoResolution` is a client-side bound on the player-size cap. They are unrelated.

Setting a Mux stream URL as `src` works too — the element parses the playback ID and query params back out into `source`. To play something Mux doesn’t host, name it with `src` inside `source`; engine options still apply.

Two more source params describe images rather than playback. `source.storyboard` and `source.poster` carry the modifiers for the storyboard VTT and the poster still — both belong to the source, since a signed token scopes them to one playback ID:

```ts
video.source = { playbackId, storyboard: { format: 'jpg' }, poster: { time: 12 } };
```

`<mux-video>` uses the storyboard itself, adding the thumbnail `<track>` for you so hover previews work without extra markup. Live streams have no storyboard, so the track is dropped once the stream type is known, and signed playback without a matching storyboard token adds none.

### Mux signs DRM with a token

Mux serves FairPlay, Widevine, and PlayReady from URLs derived from a single license token, so `source.drm` takes that token in place of the license servers it would otherwise name. Mux fills the rest in for you, so protected media plays whichever path the browser takes:

```ts
video.source = {
  playbackId,
  playback: { token: playbackToken },
  drm: { token: drmToken },
};
```

DRM playback is always signed, so a `playback.token` belongs alongside it — and `poster.token` / `storyboard.token` for the images. Each is scoped to a different audience, so they are four separate tokens rather than one reused four times. Sign them on your server; see [Mux’s DRM guide](https://www.mux.com/docs/guides/protect-videos-with-drm) for how.

A `drm.token` that isn’t scoped to DRM is ignored rather than sent, since the license request would be rejected. License servers named alongside the token win, key by key, for the systems Mux doesn’t license:

```ts
video.source = {
  playbackId,
  playback: { token: playbackToken },
  // FairPlay and PlayReady from Mux, Widevine from your own server.
  drm: { token: drmToken, 'com.widevine.alpha': { licenseUrl: widevineLicenseUrl } },
};
```

`source.poster` gets no such treatment — it’s only data, and nothing applies it to the media. Both URLs are readable from `contentData`, keyed by what each one describes:

```ts
video.contentData;
// {
//   poster: 'https://image.mux.com/PLAYBACK_ID/thumbnail.webp?time=12',
//   storyboard: 'https://image.mux.com/PLAYBACK_ID/storyboard.vtt?format=jpg',
// }

// Use the still as the poster, if that's what you want it for.
video.poster = video.contentData.poster ?? '';
```

It’s read-only and derived from `source`, so read it again after `sourcechange`. A key is missing when its URL can’t be built: no playback ID, or signed playback with no matching image token. Changing the URLs means changing the params they’re built from.

One more key arrives later. Mux publishes a small metadata document for each asset, and the media loads it alongside the stream, so `contentData.title` fills in with the asset’s title once it does — `contentdatachange` announces it. The player reads it as the fallback for its own title, so a Mux asset titled in the dashboard names itself without a `content-title` on the player. Any other entries the document carries ride along under their own keys. An untitled asset adds nothing.

`source.poster` takes the [full set of Mux image modifiers](https://www.mux.com/docs/guides/get-images-from-a-video), so a narrower still for a small viewport is a `width`:

```ts
video.source = { ...video.source, poster: { width: 320 } };
```

Because `source` has no attribute, the poster frame has one of its own. `poster-time` reflects to `source.poster.time`, letting you set it from markup:

```html
<mux-video src="https://stream.mux.com/PLAYBACK_ID.m3u8" poster-time="12"></mux-video>
```

It survives a `src` change, so swapping the source keeps the frame you asked for.

## Elements without engine options

[`<hls-video>`](https://videojs.org/docs/framework/html/reference/components/hls-video) and [`<hls-audio>`](https://videojs.org/docs/framework/html/reference/components/hls-audio) take `src` or `source`, and the usual media attributes.

Their `source` carries only `src`, `type`, and `drm`, with no `engine`, `preferPlayback`, or rendition caps. They run on our own playback engine, which does not accept configuration from the element yet.

On `<hls-video>`, `drm` licenses FairPlay, Widevine, and PlayReady, taking the same license servers as [DRM protected sources](#drm-protected-sources). `<hls-audio>` accepts `drm` but ignores it, so one source object serves both.

## See also

- [hlsjs-video element reference](https://videojs.org/docs/framework/html/reference/components/hlsjs-video)
- [mux-video element reference](https://videojs.org/docs/framework/html/reference/components/mux-video)
- [dash-video element reference](https://videojs.org/docs/framework/html/reference/components/dash-video)

---

HTML documentation: https://videojs.org/docs/framework/html/llms.txt
All documentation: https://videojs.org/llms.txt
