Build a custom quality selector
Build a custom video quality selector UI with the FastPix Player JavaScript API, slot-based overlays, and quality change events.
The FastPix Player exposes a JavaScript API for reading and switching video quality levels, which lets you build a branded quality selector that gives viewers explicit resolution control while preserving adaptive bitrate (ABR) fallback for unstable networks. Combine the quality methods with slot-based overlays to position your custom UI directly over the video without managing absolute positioning yourself.
Key terms
-
Rendition is one resolution/bitrate variant in the HLS manifest that FastPix generates during transcoding (for example, 360p at 800 kbps, 720p at 2.5 Mbps).
-
Adaptive Bitrate Streaming (ABR) is the default mode where the player switches renditions automatically based on the viewer's network speed.
-
playbackIdis the access-controlled identifier FastPix assigns for constructing the HLS playback URL:https://stream.fastpix.io/{PLAYBACK_ID}.m3u8.
Retrieve available quality levels
Call player.getQualityLevels() after the manifest loads to get the full rendition ladder.
const levels = player.getQualityLevels();
// Returns: [{ id, label, height, width, bitrate, frameRate }, ...]Each entry contains:
| Property | Type | Description |
|---|---|---|
id | number | Pass this to setQualityLevel() to lock playback to this rendition |
label | string | Human-readable label (for example, "720p") - display this in your UI |
height | number | Vertical resolution in pixels |
width | number | Horizontal resolution in pixels |
bitrate | number | Bits per second for this rendition |
frameRate | number | Frames per second |
NOTE
The array is empty for audio-only streams or single-rendition videos. Wait for the
fastpixqualitylevelsreadyevent before calling this method on a new video.
Lock playback to a specific resolution
Call player.setQualityLevel(id) to disable ABR and force the player to use one rendition.
player.setQualityLevel(2); // Lock to the rendition with id 2 (e.g. "720p")The id value comes from a getQualityLevels() entry. Do not hard-code IDs across videos, each manifest generates its own ID set.
Re-enable adaptive bitrate
Call player.setQualityAuto() to hand control back to the ABR engine. The player resumes switching renditions based on network conditions.
player.setQualityAuto();Use this when the viewer selects "Auto" in your menu, or as a fallback after a quality error.
Read the current quality state
Call player.getPlaybackQuality() to get a snapshot of the player's current quality mode and active rendition.
const q = player.getPlaybackQuality();
// Returns: { mode, lockedLevel, loadedLevel }| Property | Type | Description |
|---|---|---|
mode | "auto" or "manual" | "auto" = ABR active; "manual" = viewer locked a rendition |
lockedLevel | object or null | The rendition the viewer locked to, or null in auto mode |
loadedLevel | object | The rendition currently being decoded and played |
const q = player.getPlaybackQuality();
if (q.mode === "auto") {
// Highlight the "Auto" button
} else if (q.lockedLevel) {
// Highlight the button matching q.lockedLevel.id
}
console.log("Currently playing:", q.loadedLevel?.label); // e.g. "720p"NOTE
loadedLevelreflects what is actually on screen. Under ABR, it can differ fromlockedLevelbriefly during a rendition switch.
Listen for quality events
The player triggers three custom events on the <fastpix-player> element. Listen with addEventListener.
fastpixqualitylevelsready
fastpixqualitylevelsreadyTriggers when the HLS manifest is parsed and quality levels are available. Also triggers again if a new video loads.
player.addEventListener("fastpixqualitylevelsready", (e) => {
const levels = e.detail.levels;
// Build your quality menu buttons here
});event.detail contains:
{
"levels": [{ "id": 0, "label": "360p", "height": 360, "width": 640, "bitrate": 800000, "frameRate": 30 }]
}fastpixqualitychange
fastpixqualitychangeTriggers whenever the quality state changes, either from a viewer action (setQualityLevel / setQualityAuto) or from ABR switching renditions automatically.
player.addEventListener("fastpixqualitychange", (e) => {
console.log("Quality changed to:", e.detail.loadedLevel?.label);
console.log("Mode:", e.detail.mode);
// Re-highlight your menu buttons here
});event.detail contains:
| Property | Description |
|---|---|
mode | "auto" or "manual" |
lockedLevel | The locked rendition, or null |
loadedLevel | The rendition now playing |
previousLoadedLevel | The rendition before this change |
fastpixqualityfailed
fastpixqualityfailedTriggers when a quality change fails, either an invalid ID was passed to setQualityLevel() or a specific rendition failed to load from the network.
player.addEventListener("fastpixqualityfailed", (e) => {
console.warn("Quality failed:", e.detail.reason);
player.setQualityAuto(); // Fall back to ABR
});event.detail contains:
| Property | Description |
|---|---|
reason | Human-readable error string |
levelId | The ID that failed (if applicable) |
raw | Raw error from hls.js (for debugging) |
Build the full example
A complete, working quality selector combining every API and event described in this guide. The example uses slot overlays to position the menu in the top-right corner of the video. Copy the HTML below, replace your-playback-id with a real playbackId, and open it in a browser.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Quality Selector - FastPix Player</title>
<script defer src="https://cdn.jsdelivr.net/npm/@fastpix/fp-player@latest"></script>
<style>
/* ── Page layout ─────────────────────────────────────────── */
body {
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 32px auto;
padding: 0 16px;
background: #f9f9f9;
}
/* ── Player wrapper ──────────────────────────────────────── */
.player-shell {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 10px;
overflow: hidden;
background: #000;
}
fastpix-player {
width: 100%;
height: 100%;
display: block;
/* Hide the built-in resolution button so only the custom menu shows */
--resolution-selector: none;
/* Raise the slot layer above built-in player chrome */
--user-slot-z: 2001;
/* Keep bottom-row slots above the seek bar */
--user-slot-bottom-clearance: 48px;
}
/* ── Quality toggle button (inside slot="top-right") ──────── */
.quality-toggle {
padding: 7px 14px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: none;
border-radius: 6px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
backdrop-filter: blur(6px);
}
.quality-toggle:hover {
background: rgba(40, 40, 40, 0.85);
}
.quality-toggle[aria-expanded="true"] {
background: #2563eb;
}
/* ── Dropdown panel ──────────────────────────────────────── */
.quality-panel {
display: none;
flex-direction: column;
gap: 5px;
min-width: 150px;
max-height: 260px;
overflow-y: auto;
padding: 8px;
border-radius: 8px;
background: rgba(15, 15, 15, 0.92);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
margin-top: 6px;
}
.quality-panel.open {
display: flex;
}
/* ── Individual quality option button ─────────────────────── */
.q-option {
padding: 7px 11px;
font-size: 13px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 5px;
background: rgba(255, 255, 255, 0.05);
color: #ddd;
cursor: pointer;
}
.q-option:hover {
background: rgba(255, 255, 255, 0.12);
}
.q-option.active {
border-color: #3b82f6;
background: rgba(37, 99, 235, 0.3);
color: #fff;
}
/* ── Status bar below the player ─────────────────────────── */
.status-bar {
margin-top: 14px;
padding: 10px 14px;
font-size: 13px;
font-family: ui-monospace, monospace;
background: #f0f0f0;
border-radius: 7px;
color: #333;
line-height: 1.6;
}
</style>
</head>
<body>
<h2>Custom Quality Selector</h2>
<div class="player-shell">
<fastpix-player
id="player"
playback-id="your-playback-id"
preload="auto"
muted
>
<!-- slot="top-right" places this div in the top-right corner -->
<div slot="top-right" id="quality-slot" style="display:flex; flex-direction:column; align-items:flex-end;">
<!-- Toggle button -->
<button
type="button"
id="quality-toggle"
class="quality-toggle"
aria-expanded="false"
aria-controls="quality-panel"
>
Quality
</button>
<!-- Dropdown panel — populated by JS once levels are ready -->
<div
id="quality-panel"
class="quality-panel"
role="listbox"
aria-label="Video quality options"
></div>
</div>
</fastpix-player>
</div>
<!-- Live status: updates on every fastpixqualitychange event -->
<div class="status-bar" id="status">Mode: - · Playing: - · Locked: -</div>
<script>
customElements.whenDefined("fastpix-player").then(() => {
const player = document.getElementById("player");
const toggle = document.getElementById("quality-toggle");
const panel = document.getElementById("quality-panel");
const status = document.getElementById("status");
/* ── Dropdown open/close ─────────────────────────────── */
function openPanel() { panel.classList.add("open"); toggle.setAttribute("aria-expanded", "true"); }
function closePanel() { panel.classList.remove("open"); toggle.setAttribute("aria-expanded", "false"); }
toggle.addEventListener("click", () => {
panel.classList.contains("open") ? closePanel() : openPanel();
});
document.addEventListener("click", (e) => {
if (!panel.contains(e.target) && !toggle.contains(e.target)) closePanel();
});
/* ── Status bar ──────────────────────────────────────── */
function refreshStatus() {
const q = player.getPlaybackQuality();
const mode = q.mode === "auto" ? "Auto (ABR)" : "Manual";
const playing = q.loadedLevel ? q.loadedLevel.label : "-";
const locked = q.mode === "manual" && q.lockedLevel ? q.lockedLevel.label : "-";
status.textContent = `Mode: ${mode} · Playing: ${playing} · Locked: ${locked}`;
}
/* ── Highlight active button ─────────────────────────── */
function refreshActiveButton() {
const q = player.getPlaybackQuality();
panel.querySelectorAll(".q-option").forEach(btn => btn.classList.remove("active"));
if (q.mode === "auto") {
const autoBtn = panel.querySelector('[data-kind="auto"]');
if (autoBtn) autoBtn.classList.add("active");
} else if (q.lockedLevel) {
const lockedBtn = panel.querySelector(`[data-level-id="${q.lockedLevel.id}"]`);
if (lockedBtn) lockedBtn.classList.add("active");
}
}
/* ── Build the dropdown ──────────────────────────────── */
function buildMenu(levels) {
panel.textContent = "";
// Auto button — calls setQualityAuto()
const autoBtn = document.createElement("button");
autoBtn.type = "button";
autoBtn.className = "q-option";
autoBtn.dataset.kind = "auto";
autoBtn.textContent = "Auto (ABR)";
autoBtn.addEventListener("click", () => { player.setQualityAuto(); closePanel(); });
panel.appendChild(autoBtn);
// One button per rendition — calls setQualityLevel(id)
if (levels.length === 0) {
const note = document.createElement("span");
note.style.cssText = "font-size:12px;color:#888;padding:4px";
note.textContent = "No alternate renditions.";
panel.appendChild(note);
} else {
levels.forEach((lvl) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "q-option";
btn.dataset.levelId = String(lvl.id);
btn.textContent = lvl.label + (lvl.bitrate ? ` · ${Math.round(lvl.bitrate / 1000)} kbps` : "");
btn.addEventListener("click", () => { player.setQualityLevel(lvl.id); closePanel(); });
panel.appendChild(btn);
});
}
refreshActiveButton();
refreshStatus();
}
/* ── Event: levels ready ─────────────────────────────── */
player.addEventListener("fastpixqualitylevelsready", (e) => {
buildMenu(e.detail.levels);
});
/* ── Event: quality changed ──────────────────────────── */
player.addEventListener("fastpixqualitychange", () => {
refreshActiveButton();
refreshStatus();
});
/* ── Event: quality failed ───────────────────────────── */
player.addEventListener("fastpixqualityfailed", (e) => {
console.warn("[quality] failed:", e.detail.reason);
player.setQualityAuto();
refreshActiveButton();
refreshStatus();
});
/* ── Eager fallback if levels already loaded ─────────── */
queueMicrotask(() => {
const levels = player.getQualityLevels();
if (levels.length > 0) buildMenu(levels);
});
}); // end customElements.whenDefined
</script>
</body>
</html>What each section does
| Code section | API / event | Purpose |
|---|---|---|
--resolution-selector: none | CSS variable | Hides the built-in quality button |
slot="top-right" | Slot attribute | Places the toggle + panel in the top-right corner |
buildMenu(levels) | getQualityLevels() | Renders one button per rendition |
| Auto button click | setQualityAuto() | Re-enables adaptive bitrate |
| Rendition button click | setQualityLevel(id) | Locks to a specific resolution |
refreshStatus() | getPlaybackQuality() | Updates the status bar |
refreshActiveButton() | getPlaybackQuality() | Highlights the active button |
fastpixqualitylevelsready | Event | Triggers menu build after manifest loads |
fastpixqualitychange | Event | Keeps the UI in sync |
fastpixqualityfailed | Event | Falls back to Auto on error |
queueMicrotask fallback | getQualityLevels() | Handles pre-loaded manifests |
Understand the typical flow
- Page loads →
<fastpix-player>is created - Video manifest loads →
fastpixqualitylevelsreadytriggers - Your code calls
getQualityLevels()and builds quality buttons - Viewer clicks "720p" → your code calls
setQualityLevel(id) - Player switches rendition →
fastpixqualitychangetriggers - Your code calls
getPlaybackQuality()and highlights the active button - Viewer clicks "Auto" → your code calls
setQualityAuto() fastpixqualitychangetriggers again → UI updates
Quick reference
| Name | Type | Description |
|---|---|---|
getQualityLevels() | Method | Get all available renditions |
setQualityLevel(id) | Method | Lock to one rendition |
setQualityAuto() | Method | Re-enable ABR |
getPlaybackQuality() | Method | Read current quality state |
fastpixqualitylevelsready | Event | Manifest loaded - build your menu |
fastpixqualitychange | Event | Quality switched - update your UI |
fastpixqualityfailed | Event | Quality change failed - handle the error |
slot="top-right" etc. | HTML attribute | Place custom HTML over the video |
--user-slot-z | CSS variable | Control slot layer stacking order |
--user-slot-bottom-clearance | CSS variable | Space between bottom slots and seek bar |
--resolution-selector | CSS variable | Set to none to hide the built-in quality button |
Frequently asked questions
Do quality level IDs stay the same across different videos?
No. Each video's HLS manifest generates its own rendition ladder with unique IDs. Always call getQualityLevels() after fastpixqualitylevelsready triggers for the current video. Do not cache IDs from one video and reuse them on another.
What happens if the video has only one rendition?
getQualityLevels() returns an empty array. The player uses the single available rendition and ABR has no alternatives to switch between. Hide or disable your quality menu in this case.
Can I set a default quality lock before the video starts?
Listen for fastpixqualitylevelsready, find the rendition you want by label or height, and call setQualityLevel(id) immediately. The player applies the lock before the first segment plays at the ABR-selected level.
How do I show the currently active quality in the menu without polling?
Listen for fastpixqualitychange, it triggers on every ABR switch and every manual selection. Call getPlaybackQuality() inside the handler and update your UI. No polling or timers are needed.
What's next?
Updated 3 days ago