# getHlsStreamingUrl

```ts
function getHlsStreamingUrl(asset, opts?): string;
```

Defined in: packages/asset-client/dist/index.d.ts:507

Build an HLS streaming URL for a VIDEO asset (Phase 5). Returns the
master.m3u8 entry point — HLS-aware players (Video.js's
@videojs/http-streaming, hls.js, native iOS Safari) follow it to
fetch the variant playlist + segments at the appropriate bitrate
for the connection.

⚠️ A master.m3u8 is NOT a video file. Assigning it to `<video src>`
works only where the engine has native HLS; everywhere else it needs
an MSE player. And the classic feature test is now WRONG: Chrome 147
(April 2026) added native HLS, so `canPlayType("application/vnd.apple.mpegurl")`
answers "maybe" there and routes Chrome to the native branch, where it
opened a measured 17 s hero at 426x240 for ~8 s. Branch on the ENGINE:

## Parameters

| Parameter | Type |
| ------ | ------ |
| `asset` | [`Pick`](/api/nitida/sdk/react/-internal-/type-aliases/pick/)\<[`AssetDTO`](/api/nitida/sdk/type-aliases/assetdto/), `"sha"`\> & [`VisibilityHint`](/api/nitida/sdk/type-aliases/visibilityhint/) |
| `opts?` | [`Omit`](/api/nitida/asset-client/-internal-/type-aliases/omit/)\<[`TransformOptions`](/api/nitida/sdk/type-aliases/transformoptions/), `"format"`\> |

## Returns

`string`

## Example

```ts
function prefersNativeHls(video: HTMLVideoElement): boolean {
  if (video.canPlayType("application/vnd.apple.mpegurl") === "") return false;
  // Apple's engine, or an engine with no MSE to fall back on (iOS < 17.1).
  return "ManagedMediaSource" in globalThis || !("MediaSource" in globalThis);
}

const src = getHlsStreamingUrl(asset);
if (prefersNativeHls(video)) {
  video.src = src;
} else {
  const hls = new Hls({ capLevelToPlayerSize: false, abrEwmaDefaultEstimate: 5_000_000 });
  hls.loadSource(src);
  hls.attachMedia(video);
}
```

⚠️ **Do NOT pass `startLevel: -1` with `testBandwidth: true`.** That pair is
documented by hls.js as *"forces the player to download a fragment from the
lowest level to establish a bandwidth estimate"* — on a clip short enough to
be one segment, the probe IS the whole video, and it plays at the bottom
rung from first frame to last. (This doc-comment recommended exactly that
until 2026-08-18; a 5.042 s 4K asset was measured being delivered at
426x240 because of it.) Leave `startLevel` unset: hls.js then opens on the
FIRST level in the manifest, and the server puts the right one there —
a mid rung for long video, the top rung for a clip under 18 s, which is the
same rung native HLS opens on per RFC 8216 §6.3.4. The ladder decides; the
player should not second-guess it.

`capLevelToPlayerSize` is worth disabling explicitly: `@videojs/core`
defaults it to `true`, which caps quality to the player's rendered pixel box,
so a small inline player is pinned to 240p/360p on any connection.

On first request the server returns 202 Accepted while a background
job transcodes the ladder (typically 1-3 min for a 90 s source);
subsequent requests get 302 to the cached master.m3u8. Keep the
progressive MP4 as a fallback source for that window.

The ladder's ceiling is the source the job probes: built at ingest it
reads the RAW upload and a 4K master yields 1440p/2160p rungs; rebuilt
on demand after the raw is unavailable it reads the `-v.mp4`, which is
capped at 1920 wide. `getAssetUrl(sha, "video")` is always <= 1080p.