> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://fastpix.com/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://fastpix.com/docs/_mcp/server.

# 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.
* **`playbackId`** is the access-controlled identifier FastPix assigns for constructing the HLS playback URL: `https://stream.fastpix.com/{PLAYBACK_ID}.m3u8`.

\


## Retrieve available quality levels

Call `player.getQualityLevels()` after the manifest loads to get the full rendition ladder.

```javascript
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 `fastpixqualitylevelsready` event 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.

```javascript
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.

```javascript
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.

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

```javascript
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**
>
> `loadedLevel` reflects what is actually on screen. Under ABR, it can differ from `lockedLevel` briefly during a rendition switch.

\


## Listen for quality events

The player triggers three custom events on the `<fastpix-player>` element. Listen with `addEventListener`.

#### `fastpixqualitylevelsready`

Triggers when the HLS manifest is parsed and quality levels are available. Also triggers again if a new video loads.

```javascript
player.addEventListener("fastpixqualitylevelsready", (e) => {
  const levels = e.detail.levels;
  // Build your quality menu buttons here
});
```

`event.detail` contains:

```json
{
  "levels": [{ "id": 0, "label": "360p", "height": 360, "width": 640, "bitrate": 800000, "frameRate": 30 }]
}
```

\


#### `fastpixqualitychange`

Triggers whenever the quality state changes, either from a viewer action (`setQualityLevel` / `setQualityAuto`) or from ABR switching renditions automatically.

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

Triggers when a quality change fails — either when an invalid ID is passed to `setQualityLevel()`, or when a specific rendition fails to load from the network.

```javascript
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](/docs/web-player/adjust-player-layout-and-ui#position-your-ui-with-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.

```html
<!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

1. Page loads → `<fastpix-player>` is created
2. Video manifest loads → `fastpixqualitylevelsready` triggers
3. Your code calls `getQualityLevels()` and builds quality buttons
4. Viewer clicks "720p" → your code calls `setQualityLevel(id)`
5. Player switches rendition → `fastpixqualitychange` triggers
6. Your code calls `getPlaybackQuality()` and highlights the active button
7. Viewer clicks "Auto" → your code calls `setQualityAuto()`
8. `fastpixqualitychange` triggers 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

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.

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

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.

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?

* [Control playback resolution](/docs/web-player/control-playback-resolution)
* [Adjust player layout and UI](/docs/web-player/adjust-player-layout-and-ui)
* [Set the aspect ratio](/docs/web-player/set-the-aspect-ratio)