---
title: Media sources
description: Set what a media element plays and how its engine plays it with the structured source property
---

import FrameworkCase from '@/components/docs/FrameworkCase.astro';
import DocsLink from '@/components/docs/DocsLink.astro';
import DocsLinkCard from '@/components/docs/DocsLinkCard.astro';
import Aside from '@/components/Aside.astro';

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:

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo
  source={{
    src: 'https://example.com/stream.m3u8',
    preferPlayback: 'mse',
    engine: { maxBufferLength: 60 },
  }}
/>
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
const video = document.querySelector('hlsjs-video');

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

## 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`, `MuxVideo`'s `playbackId` |
| How to interpret it | `source.type` | `'video/mp4'` |
| How Video.js plays it | `source`, alongside `engine` | `preferPlayback` |
| How the engine behaves | `source.engine` | hls.js's `maxBufferLength` |
| A side-car component's own settings | that component's props | <DocsLink slug="concepts/mux-data">Mux Data</DocsLink>, <DocsLink slug="concepts/cast">Google Cast</DocsLink> |

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

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo source={{ src: 'https://example.com/asset?id=42', type: 'application/vnd.apple.mpegurl' }} />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = { src: 'https://example.com/asset?id=42', type: 'application/vnd.apple.mpegurl' };
```
</FrameworkCase>

## `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 `engine`, intact**:

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo source={{ src: 'https://example.com/a.m3u8', preferPlayback: 'native' }} />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```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' }
```
</FrameworkCase>

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:

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo source={{ src, type: 'video/mp4', preferPlayback: 'native' }} />

// later: type and preferPlayback are gone, because the new object omits them
<HlsJsVideo source={{ src }} />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = { src, type: 'video/mp4', preferPlayback: 'native' };

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

## 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:

<FrameworkCase frameworks={["react"]}>
```tsx
// A fresh object literal on every render. Nothing reloads.
<HlsJsVideo source={{ src, engine: { maxBufferLength: 60 } }} />
```

This is why you can write `source` inline. React hands the element a brand new object every render, and the element recognizes it as the same source. There's no need to memoize it or spread the previous value to avoid clobbering anything.
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = { src, engine: { maxBufferLength: 60 } };
video.source = { src, engine: { maxBufferLength: 60 } }; // no-op
```
</FrameworkCase>

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

<Aside type="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.
</Aside>

## Engine options

`engine` is the playback engine's own configuration object, handed over untouched. There's no Video.js wrapper around it, so whatever the engine documents works:

| Element | `engine` is |
|---|---|
| <DocsLink slug="reference/hlsjs-video">HlsJsVideo</DocsLink>, <DocsLink slug="reference/mux-video">MuxVideo</DocsLink>, <DocsLink slug="reference/mux-audio">MuxAudio</DocsLink> | an [hls.js config](https://github.com/video-dev/hls.js/blob/master/docs/API.md#fine-tuning) |
| <DocsLink slug="reference/dash-video">DashVideo</DocsLink> | [dash.js settings](https://cdn.dashjs.org/latest/jsdoc/module-Settings.html) |
| `VimeoVideo` | [Vimeo embed parameters](https://developer.vimeo.com/player/sdk/embed) |

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

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo
  source={{
    src: 'https://example.com/stream.m3u8',
    engine: { maxBufferLength: 60, enableWorker: false },
  }}
/>
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = {
  src: 'https://example.com/stream.m3u8',
  engine: { maxBufferLength: 60, enableWorker: false },
};
```
</FrameworkCase>

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

<FrameworkCase frameworks={["react"]}>
```tsx
<DashVideo
  source={{
    src: 'https://example.com/manifest.mpd',
    engine: { streaming: { abr: { maxBitrate: { video: 2000 } } } },
  }}
/>
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
const video = document.querySelector('dash-video');

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

Because `engine` 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 `engine`.

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

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo source={{ src: 'https://example.com/stream.m3u8', preferPlayback: 'native' }} />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = { src: 'https://example.com/stream.m3u8', preferPlayback: 'native' };
```
</FrameworkCase>

It's a preference, not a demand — Video.js falls back to whichever path can actually play the source. `HlsJsVideo`, `MuxVideo`, and `MuxAudio` accept it; DASH and Vimeo have no second playback path.

## DRM protected sources

DRM is engine configuration like any other, so it goes under `engine` — for hls.js, that's `emeEnabled` and `drmSystems`, keyed by EME key system id:

<FrameworkCase frameworks={["react"]}>
```tsx
<HlsJsVideo
  source={{
    src: 'https://example.com/protected.m3u8',
    engine: {
      emeEnabled: true,
      drmSystems: {
        'com.widevine.alpha': { licenseUrl: 'https://license.example.com/widevine' },
        'com.apple.fps': {
          licenseUrl: 'https://license.example.com/fairplay',
          serverCertificateUrl: 'https://license.example.com/fairplay-cert',
        },
      },
    },
  }}
/>
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = {
  src: 'https://example.com/protected.m3u8',
  engine: {
    emeEnabled: true,
    drmSystems: {
      'com.widevine.alpha': { licenseUrl: 'https://license.example.com/widevine' },
      'com.apple.fps': {
        licenseUrl: 'https://license.example.com/fairplay',
        serverCertificateUrl: 'https://license.example.com/fairplay-cert',
      },
    },
  },
};
```
</FrameworkCase>

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

Only the hls.js (MSE) engine plays DRM-protected media, so native HLS playback ignores all of this and warns in development.

Video.js adds one thing on top: for Widevine it 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.

## Mux sources name a playback ID

<DocsLink slug="reference/mux-video">MuxVideo</DocsLink> and <DocsLink slug="reference/mux-audio">MuxAudio</DocsLink> identify a source by `playbackId` rather than a URL, and derive `src` from it. Everything else works the same, `engine` included:

<FrameworkCase frameworks={["react"]}>
```tsx
<MuxVideo
  source={{
    playbackId: 'BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM',
    playback: { maxResolution: '1080p' },
    engine: { maxBufferLength: 60 },
  }}
  playsInline
/>
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
const video = document.querySelector('mux-video');

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

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:

<FrameworkCase frameworks={["react"]}>
```tsx
<MuxVideo source={{ playbackId, storyboard: { format: 'jpg' }, poster: { time: 12 } }} playsInline />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = { playbackId, storyboard: { format: 'jpg' }, poster: { time: 12 } };
```
</FrameworkCase>

`MuxVideo` 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 and nothing else. It fills in the `engine.drmSystems` above for you, EME included:

<FrameworkCase frameworks={["react"]}>
```tsx
<MuxVideo
  source={{
    playbackId,
    playback: { token: playbackToken },
    drm: { token: drmToken },
  }}
  playsInline
/>
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = {
  playbackId,
  playback: { token: playbackToken },
  drm: { token: drmToken },
};
```
</FrameworkCase>

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. Naming `engine.drmSystems` yourself takes precedence, for content Mux doesn't license.

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

<FrameworkCase frameworks={["html"]}>
```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 ?? '';
```
</FrameworkCase>

<FrameworkCase frameworks={["react"]}>
```ts
media.contentData;
// {
//   poster: 'https://image.mux.com/PLAYBACK_ID/thumbnail.webp?time=12',
//   storyboard: 'https://image.mux.com/PLAYBACK_ID/storyboard.vtt?format=jpg',
// }
```
</FrameworkCase>

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.

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

<FrameworkCase frameworks={["react"]}>
```tsx
<MuxVideo source={{ playbackId, poster: { width: 320 } }} playsInline />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
video.source = { ...video.source, poster: { width: 320 } };
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
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.
</FrameworkCase>

## Elements without engine options

Two elements take `src` and the usual media attributes, but expose no `source`:

- <DocsLink slug="reference/native-hls-video">NativeHlsVideo</DocsLink> hands playback to the browser, so there is no engine to configure.
- <DocsLink slug="reference/simple-hls-video">SimpleHlsVideo</DocsLink> and <DocsLink slug="reference/simple-hls-audio-only">SimpleHlsAudioOnly</DocsLink> run on our own playback engine, which does not accept configuration from the element yet.

## Migrate from `config`

Media elements used to take a `config` object: one untyped bag holding engine options, source overrides, and component settings at once. It's gone, and each of its keys now has a specific home:

| Before | After |
|---|---|
| `config.preferPlayback` | `source.preferPlayback` |
| `config.contentType` | `source.type` |
| `config.hlsJs` | `source.engine` |
| `config.muxData` | the <DocsLink slug="concepts/mux-data">Mux Data</DocsLink> component's own props |
| `config.googleCast` | the <DocsLink slug="concepts/cast">Google Cast</DocsLink> component's own props |
| `config` on VimeoVideo | `source.engine` |

The `hlsJs` and `dashJs` nesting is gone: `engine` *is* the engine's config object now, so its options sit one level shallower than they did under `config`.

<FrameworkCase frameworks={["react"]}>
```tsx
// Before
<HlsJsVideo src={src} config={{ preferPlayback: 'native', hlsJs: { maxBufferLength: 60 } }} />

// After
<HlsJsVideo source={{ src, preferPlayback: 'native', engine: { maxBufferLength: 60 } }} />
```
</FrameworkCase>

<FrameworkCase frameworks={["html"]}>
```ts
// Before
video.config = { preferPlayback: 'native', hlsJs: { maxBufferLength: 60 } };

// After
video.source = { src, preferPlayback: 'native', engine: { maxBufferLength: 60 } };
```
</FrameworkCase>

`<native-hls-video>`, `<simple-hls-video>`, and `<simple-hls-audio-only>` accepted a `config` that nothing read. Remove it — those elements never applied it.

## See also

<DocsLinkCard slug="reference/hlsjs-video">HlsJsVideo element reference</DocsLinkCard>
<DocsLinkCard slug="reference/mux-video">MuxVideo element reference</DocsLinkCard>
<DocsLinkCard slug="reference/dash-video">DashVideo element reference</DocsLinkCard>
