Quality Selection
Explicit quality control beyond ABR auto-selection: a manual quality menu, HDR-aware constraints, or locking a specific resolution. Built on the video player’s HLS layer.
Prerequisites: Video Quick Start. For HLS internals see HLS.
Reading available quality levels
Quality levels become available after the HLS manifest is parsed:
player.on('levels', ({ levels }) => {
// levels is QualityLevel[], ordered low to high by bitrate
levels.forEach((level, index) => {
console.log(`Level ${index}: ${level.width}x${level.height} @ ${level.bitrate} bps`);
});
});
// Or read synchronously after ready():
await player.ready();
const levels = player.qualityLevels();
QualityLevel shape:
interface QualityLevel {
bitrate: number; // stream bitrate in bits per second
height?: number; // encoded height in px, if known
width?: number; // encoded width in px, if known
label: string; // human-readable (e.g. '1080p')
index: number; // zero-based level index; pass to quality(idx)
supported?: boolean; // set when qualityLevels({ includeUnsupported: true })
dynamicRange?: 'sdr' | 'hdr'; // 'hdr' for PQ/HLG streams, 'sdr' otherwise
}
Manual quality lock
// Lock to a specific level by index (ABR is suspended):
player.quality(2);
// Return to automatic ABR:
player.quality('auto');
// Read the current selection: 'auto' when ABR is active,
// otherwise { index, track } for the locked level.
const current = player.quality();
const activeIndex = current === 'auto' ? -1 : current.index;
Listen for automatic level switches:
player.on('level-switched', ({ level }) => {
// level is the index hls.js switched to
updateQualityIndicator(level);
});
Building a quality menu
A complete, reactive quality menu:
let levels: QualityLevel[] = [];
// Populate menu when levels arrive:
player.on('levels', ({ levels: newLevels }) => {
levels = newLevels;
renderQualityMenu(levels, player.quality());
});
// Update active state when ABR switches automatically:
player.on('level-switched', ({ level }) => {
updateActiveLevel(level);
});
function renderQualityMenu(
levels: QualityLevel[],
current: CurrentQualitySelection | 'auto',
): void {
const menu = document.getElementById('quality-menu')!;
menu.innerHTML = '';
const activeIndex = current === 'auto' ? -1 : current.index;
// "Auto" option, re-enables ABR
const autoOption = document.createElement('button');
autoOption.textContent = 'Auto';
autoOption.classList.toggle('active', current === 'auto');
autoOption.addEventListener('click', () => player.quality('auto'));
menu.appendChild(autoOption);
// One button per level
levels.forEach((level, index) => {
const option = document.createElement('button');
option.textContent = level.label;
if (level.dynamicRange === 'hdr') option.textContent += ' HDR';
option.classList.toggle('active', index === activeIndex);
option.addEventListener('click', () => player.quality(index));
menu.appendChild(option);
});
}
HDR-aware constraints
HDR levels are automatically excluded from ABR selection on SDR displays.
Display-HDR detection uses matchMedia('(dynamic-range: high)').
On multi-monitor setups, the player re-evaluates when the window moves between displays.
You do not need to configure this; it is automatic. To surface HDR availability to your UI:
player.on('levels', ({ levels }) => {
const hasHdr = levels.some((level) => level.dynamicRange === 'hdr');
hdrBadge.hidden = !hasHdr;
});
To force-exclude HDR levels regardless of display capability (e.g. for battery-saving mode):
player.on('levels', ({ levels }) => {
const sdrLevels = levels.filter((level) => level.dynamicRange !== 'hdr');
if (sdrLevels.length > 0) {
// Lock to the highest SDR level
player.quality(levels.indexOf(sdrLevels[sdrLevels.length - 1]));
}
});
Pairing quality with audio language
When switching quality levels in a multi-audio-track stream, hls.js preserves the active audio rendition. No extra handling is needed.
To pair a quality change with a subtitle language switch in your UI:
player.on('level-switched', ({ level }) => {
const resolvedLevel = player.qualityLevels()[level];
if (resolvedLevel?.dynamicRange === 'hdr') {
// HDR content, disable subtitles that have poor readability on HDR
player.subtitle(null);
}
});
What to read next
- HLS: ABR internals, multi-monitor handling, custom backend
- Recipes: Subtitles: pairing quality with subtitle language
- Advanced: Custom Backend: replacing hls.js with Shaka Player or dash.js