Skip to content
scroll to zoom · drag to pan

getHlsStreamingUrl

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:

Parameter Type
asset Pick<AssetDTO, "sha"> & VisibilityHint
opts? Omit<TransformOptions, "format">

string

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.