Skip to content
FrameworkStyle

Add a quality selector

Read available renditions, let the engine adapt automatically, and offer manual quality selection.

Read the renditions a stream offers, let the streaming engine adapt quality automatically, and give users manual control when they want it.

Quality selection applies to streaming sources. A progressive MP4 plays at one fixed quality and exposes no renditions; HLS and DASH streams offer several. See Media sources.

Let automatic quality selection do its job by default, and offer a quality menu for users who want to pin a rendition. Render the menu only when quality selection is available.

import { Container, createPlayer, Menu, QualityRadioGroup } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';

const { Player } = createPlayer({ features: videoFeatures });
const src = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8';

function QualityMenu(): ReactNode {
  return (
    <Menu.Root side="top" align="end">
      <Menu.Trigger className="settings-trigger" render={<button type="button" />}>
        Quality
      </Menu.Trigger>
      <Menu.Popup className="menu">
        <Menu.Content>
          <QualityRadioGroup
            className="menu-group"
            renderItem={(props, item) => (
              <Menu.RadioItem {...props} className="menu-item">
                <span>
                  {item.label}
                  {item.tier ? <sup className="menu-tier">{item.tier}</sup> : null}
                </span>
                {item.badge ? <span className="menu-badge">{item.badge}</span> : null}
                <Menu.ItemIndicator checked={item.checked} forceMount className="menu-indicator">

                </Menu.ItemIndicator>
              </Menu.RadioItem>
            )}
          />
        </Menu.Content>
      </Menu.Popup>
    </Menu.Root>
  );
}

export default function BasicUsage() {
  return (
    <Player>
      <Container className="media-container">
        <HlsJsVideo src={src} autoPlay crossOrigin="anonymous" muted playsInline loop />
        <div className="menu-bar">
          <QualityMenu />
        </div>
      </Container>
    </Player>
  );
}

How it works

The quality feature mirrors the media element’s video renditions into player state:

  • videoRenditionList holds each rendition’s id, width, height, bitrate, frameRate, codec, and whether it’s selected.
  • activeVideoRendition is the rendition currently playing. When the engine doesn’t report one directly, the player matches it from the video’s current dimensions.
  • selectVideoRendition(value) pins one rendition; pass 'auto' to return control to the engine’s adaptive selection.

useQualityOptions turns this state into ready-made menu options — labels like “1080p”, an “Auto” entry, and availability — as shown in the demo above.

Selecting “Auto” keeps adaptive bitrate switching active: the engine picks the best rendition for current bandwidth and viewport. Pinning a rendition disables adaptation until the user selects “Auto” again.

Availability and constraints

  • Quality availability is 'unavailable' for media that doesn’t offer a choice — progressive files, streams before the manifest loads, and single-rendition streams. Render quality UI conditionally; the demo’s availability check does this.
  • Renditions come from the streaming engine, so the list depends on what the manifest declares. A single-rendition stream offers no meaningful selection, so availability stays 'unavailable'.
  • Pinning a high rendition on a slow connection causes buffering: the engine can no longer step down. Keep “Auto” the default.
  • Rendition lists change on source change; selection resets with them.

Common variations

Auto quality only

If you don’t want to expose manual selection, do nothing: adaptive selection is on by default and needs no UI.

Cap or pin quality programmatically

import { Container } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { usePlayer, VideoPlayer } from '@videojs/react/video';

function PinLowestRendition() {
  const store = usePlayer();
  const renditions = usePlayer((s) => s.videoRenditionList);

  const lowest = [...renditions].sort((a, b) => (a.height ?? 0) - (b.height ?? 0))[0];

  return (
    <button type="button" onClick={() => lowest?.id && store.selectVideoRendition(lowest.id)}>
      Data saver
    </button>
  );
}

export default function App() {
  return (
    <VideoPlayer>
      <Container>
        <HlsJsVideo src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM.m3u8" muted playsInline />
        <PinLowestRendition />
      </Container>
    </VideoPlayer>
  );
}

Troubleshooting

The quality menu doesn’t render

Quality availability is 'unavailable'. The source is progressive (no renditions), the manifest hasn’t loaded yet, or the media element doesn’t support renditions. Use a streaming media element such as HlsJsVideo.

The menu doesn’t render for a single-rendition stream

The manifest declares a single rendition, so quality availability stays 'unavailable' — one entry offers no choice. Encode the stream as a multi-rendition ladder to give the engine and users something to choose between.

Playback buffers after selecting a quality

The pinned rendition exceeds available bandwidth. Selecting “Auto” lets the engine step down again.