For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
StatusSupportDiscussionsLog inSign Up
Docs HomeAPI ReferenceVideo on DemandAI FeaturesLive StreamingVideo PlayerVideo DataCloud PlayoutRecipes
Docs HomeAPI ReferenceVideo on DemandAI FeaturesLive StreamingVideo PlayerVideo DataCloud PlayoutRecipes
  • Player SDKs
    • Introduction
  • Web player
    • Install the FastPix web player
    • Play uploaded videos
    • Handle playback errors
      • Build a custom track switcher UI
      • Build a custom quality selector
  • Android player
    • Install FastPix Android player
    • Set up the player
    • Play uploaded videos
    • Handle playback errors
  • iOS player
    • Install FastPix iOS player
    • Play uploaded videos
    • Handle playback errors
  • Flutter player
    • Install FastPix Flutter player
    • Play uploaded videos
    • Handle playback errors
LogoLogo
StatusSupportDiscussionsLog inSign Up
On this page
  • Table of contents
  • What you’ll build
  • Before you begin
  • Step 1 — Set up the player and container
  • Step 2 — Configure attributes (optional)
  • Step 3 — Add placeholder containers for your track buttons
  • Step 3b — Optional: Forward / Backward seek buttons
  • Step 4 — Wait for tracks, then render audio buttons
  • Step 5 — Poll for subtitle tracks
  • Step 6 — Render subtitle buttons with an Off option
  • Step 7 — Keep buttons in sync after track changes
  • Step 8 — Render a custom subtitle overlay (subtitle display logic)
  • Step 9 — Persist user preferences
  • Putting it all together
  • TrackInfo reference
  • Best practices
  • Full API reference
  • See also: React integration
Web playerExamples

Build a custom track switcher UI

Was this page helpful?
Previous

Build a custom quality selector

Next
Built with

Build a fully custom audio and subtitle track switcher UI using the FastPix Player JavaScript API, with cue rendering, preference persistence, and real-time sync.

The built-in subtitle and audio menus handle the default use case well. But if you’re building your own player controls — or need subtitle rendering you can fully style — FastPix Player exposes a complete JavaScript API for reading tracks, switching between them, and reacting to changes in real time.

This guide walks through the full demo implementation, step by step.


Table of contents

  • What you’ll build
  • Before you begin
  • Step 1 — Set up the player and container
  • Step 2 — Configure attributes (optional)
  • Step 3 — Add placeholder containers for your track buttons
  • Step 3b — Optional: Forward / Backward seek buttons
  • Step 4 — Wait for tracks, then render audio buttons
  • Step 5 — Poll for subtitle tracks
  • Step 6 — Render subtitle buttons with an Off option
  • Step 7 — Keep buttons in sync after track changes
  • Step 8 — Render a custom subtitle overlay
  • Step 9 — Persist user preferences
  • Putting it all together
  • TrackInfo reference
  • Best practices
  • Full API reference
  • See also: React integration

What you’ll build

By the end of this guide you’ll have:

  • A player that starts with a specific audio and subtitle track set by default
  • The player’s own subtitle rendering suppressed so your overlay is the only one visible
  • Custom audio track buttons that update to reflect the active track
  • Custom subtitle track buttons, including an Off option
  • Subtitle display logic — a styled subtitle overlay driven by the fastpixsubtitlecue event (you listen for the event and set the overlay’s text; when subtitles are Off or the cue ends, you clear or hide it)
  • UI that stays in sync as tracks change
  • User language preferences persisted across sessions

Before you begin

You’ll need a FastPix stream URL that already has multiple audio or subtitle tracks. If your stream doesn’t have them yet and you’d like to add some, see Add own audio tracks and Add own subtitle tracks.

The examples on this page use the CDN-hosted player script. If you’ve installed the player via npm, replace the <script> tag with your local build path.


Step 1 — Set up the player and container

Start with a <fastpix-player> wrapped in a positioned container. The container is required if you plan to overlay a custom subtitle div — position: relative on the wrapper is what makes absolute positioning of the subtitle layer work correctly.

1<script src="https://cdn.jsdelivr.net/npm/@fastpix/[email protected]" defer></script>
2
3<div class="player-container">
4 <fastpix-player
5 playback-id="your-playback-id-with-tracks"
6 loop
7 auto-play
8 muted
9 preload="auto"
10 ></fastpix-player>
11 <div class="custom-subtitle" data-role="custom-subtitle"></div>
12</div>
1body {
2 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3 max-width: 800px;
4 margin: 20px auto;
5 padding: 0 20px;
6}
7.player-container {
8 position: relative;
9 width: 100%;
10 aspect-ratio: 16/9;
11}
12
13fastpix-player {
14 width: 100%;
15 --aspect-ratio: 21/9;
16}
17
18/* Subtitle overlay: dark pill/bubble, centered over bottom of video */
19.custom-subtitle {
20 position: absolute;
21 left: 50%;
22 bottom: 10%;
23 transform: translateX(-50%);
24 max-width: 90%;
25 padding: 10px 20px;
26 background: rgba(40, 40, 40, 0.9);
27 color: #fff;
28 display: none;
29 border-radius: 9999px; /* pill shape */
30 text-align: center;
31 font-size: 15px;
32 line-height: 1.4;
33 pointer-events: none;
34 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
35}

The data-role="custom-subtitle" attribute is used later to locate this overlay relative to its specific player — this is what makes the pattern work cleanly when you have multiple players on the same page. In Step 8 you’ll add the subtitle display logic: listen for the fastpixsubtitlecue event and update this element’s textContent (and show/hide it) so the current cue text appears in your overlay.


Step 2 — Configure attributes (optional)

You can optionally set default tracks and subtitle rendering behavior with HTML attributes on <fastpix-player>:

  • default-audio-track — Start with a specific audio track by label (case-insensitive).
  • default-subtitle-track — Start with a specific subtitle track by label (case-insensitive).
  • disable-hidden-captions — Start with all subtitles/captions Off on load without firing fastpixsubtitlechange. Users (or your code) can still turn subtitles on later via the built-in menu or setSubtitleTrack(...).
  • hide-native-subtitles — Keep the player’s internal subtitle overlay visually empty while still emitting fastpixsubtitlecue and track events. Use this when you render subtitles yourself so you never see duplicate text.

Example — add them on the same <fastpix-player> as in Step 1 (replace labels with your manifest’s track names):

1<fastpix-player
2 playback-id="your-playback-id"
3 loop
4 auto-play
5 muted
6 preload="auto"
7 default-audio-track="French"
8 default-subtitle-track="English"
9 disable-hidden-captions
10 hide-native-subtitles
11></fastpix-player>

Full behavior and examples for these attributes are in the Full API reference and in the TrackInfo reference on this page. For the rest of this guide we’ll assume you add them to the player as needed.


Step 3 — Add placeholder containers for your track buttons

Below the player, add the HTML containers that will hold the rendered track buttons. Show a loading state until tracks are ready.

1<div class="track-controls">
2 <h3>Audio Tracks</h3>
3 <div id="audioButtons" class="track-buttons">
4 <em>Loading tracks...</em>
5 </div>
6 <div id="currentAudio" class="current-track"></div>
7</div>
8
9<div class="track-controls">
10 <h3>Subtitle Tracks</h3>
11 <div id="subtitleButtons" class="track-buttons">
12 <em>Loading tracks...</em>
13 </div>
14 <div id="currentSubtitle" class="current-track"></div>
15</div>
1.track-controls {
2 margin-top: 20px;
3 padding: 15px;
4 background: #f5f5f5;
5 border-radius: 8px;
6}
7
8.track-controls h3 {
9 margin: 0 0 10px 0;
10 font-weight: 700;
11 font-family: Georgia, 'Times New Roman', serif;
12 color: #000;
13}
14
15.track-buttons {
16 display: flex;
17 gap: 8px;
18 flex-wrap: wrap;
19}
20
21.track-btn {
22 padding: 8px 16px;
23 border: 1px solid #ddd;
24 border-radius: 6px;
25 background: white;
26 cursor: pointer;
27 font-size: 14px;
28}
29
30.track-btn:hover {
31 border-color: #007bff;
32}
33
34.track-btn.active {
35 background: #007bff;
36 color: white;
37 border-color: #007bff;
38}
39
40.current-track {
41 margin-top: 10px;
42 font-size: 13px;
43 color: #666;
44}
45
46.player-time {
47 margin-top: 12px;
48}

Step 3b — Optional: Forward / Backward seek buttons

The demo also includes simple skip-ahead / skip-back controls using the player’s seekForward(seconds) and seekBackward(seconds) methods. Add a small control group and wire it in script:

1<div class="player-time">
2 <button id="forwardButton">Forward 20s</button>
3 <button id="backwardButton">Backward 20s</button>
4</div>
1const forwardButton = document.getElementById('forwardButton');
2const backwardButton = document.getElementById('backwardButton');
3
4forwardButton.addEventListener('click', () => {
5 player.seekForward(10);
6});
7backwardButton.addEventListener('click', () => {
8 player.seekBackward(10);
9});

Seconds are clamped to the media range. You can use the same pattern with any skip value (e.g. 5 or 30).


Step 4 — Wait for tracks, then render audio buttons

Tracks are discovered asynchronously after the HLS manifest is parsed. The fastpixtracksready event is your signal that tracks are available. Always wait for this event before calling any track methods.

Get the player reference, wire up the event, and call getAudioTracks() to retrieve the current audio snapshot:

1const player = document.querySelector('fastpix-player');
2const audioButtonsContainer = document.getElementById('audioButtons');
3const currentAudioDisplay = document.getElementById('currentAudio');
4
5player.addEventListener('fastpixtracksready', () => {
6 const audioTracks = player.getAudioTracks();
7 renderAudioButtons(audioTracks);
8
9 // Subtitle tracks need separate handling — see Step 5
10});

getAudioTracks() returns an array of TrackInfo objects. The isCurrent flag tells you which track is active so you can set the initial selected state in your UI. See the TrackInfo reference for a full description of all fields.

1// Example return value from getAudioTracks()
2[
3 { id: 0, label: "English", language: "en", isDefault: false, isCurrent: false },
4 { id: 1, label: "French", language: "fr", isDefault: false, isCurrent: true },
5 { id: 2, label: "Hindi", language: "hi", isDefault: false, isCurrent: false }
6]

Now write renderAudioButtons to turn that array into buttons:

1function renderAudioButtons(tracks) {
2 if (!tracks || tracks.length === 0) {
3 audioButtonsContainer.innerHTML = '<em>No audio tracks available</em>';
4 currentAudioDisplay.textContent = '';
5 return;
6 }
7
8 audioButtonsContainer.innerHTML = '';
9
10 tracks.forEach((track) => {
11 const btn = document.createElement('button');
12 btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
13 btn.textContent = `${track.label} (${track.language || 'unknown'})`;
14 btn.onclick = () => {
15 // Public API is label-driven (no numeric ids / language codes)
16 player.setAudioTrack(track.label);
17 };
18 audioButtonsContainer.appendChild(btn);
19 });
20
21 const current = tracks.find((t) => t.isCurrent);
22 currentAudioDisplay.textContent = current
23 ? `Current: ${current.label} (${current.language || 'unknown'})`
24 : '';
25}

Note that setAudioTrack takes a label string — not a numeric id. The id field is an internal index and must never be used for switching or persistence.


Step 5 — Poll for subtitle tracks

Subtitle textTracks attach to the player slightly later than audio tracks — after MANIFEST_PARSED fires but before the player finishes registering them. This means fastpixtracksready can fire before subtitle tracks are ready, and calling getSubtitleTracks() immediately in the handler can return an empty array.

The reliable pattern is to poll getSubtitleTracks() for a short window after fastpixtracksready fires, and render as soon as the array becomes non-empty:

1const subtitleButtonsContainer = document.getElementById('subtitleButtons');
2const currentSubtitleDisplay = document.getElementById('currentSubtitle');
3
4let subtitlePollId = null;
5
6player.addEventListener('fastpixtracksready', (e) => {
7 const audioTracks = player.getAudioTracks();
8 renderAudioButtons(audioTracks);
9
10 if (subtitlePollId) clearInterval(subtitlePollId);
11 const startTime = Date.now();
12
13 subtitlePollId = setInterval(() => {
14 const subtitleTracks = player.getSubtitleTracks();
15 if (subtitleTracks && subtitleTracks.length > 0) {
16 clearInterval(subtitlePollId);
17 subtitlePollId = null;
18 renderSubtitleButtons(subtitleTracks);
19 } else if (Date.now() - startTime > 10000) { // safety timeout 10s
20 clearInterval(subtitlePollId);
21 subtitlePollId = null;
22 }
23 }, 500);
24});

Why not just use e.detail.subtitleTracks? The event detail does include subtitleTracks, but it reflects the state at the moment the event fired. If that was before subtitle tracks were registered, the array will be empty. Polling getSubtitleTracks() directly catches the second registration.


Step 6 — Render subtitle buttons with an Off option

Subtitle buttons work the same as audio buttons, with one addition: always render an Off button first. Off is the state where no track has isCurrent: true — which happens after setSubtitleTrack(null) or disableSubtitles() is called.

1function renderSubtitleButtons(tracks) {
2 subtitleButtonsContainer.innerHTML = '';
3
4 // Add "Off" button — active when no subtitle track has isCurrent: true
5 const offBtn = document.createElement('button');
6 offBtn.className = 'track-btn' + (!tracks.some((t) => t.isCurrent) ? ' active' : '');
7 offBtn.textContent = 'Off';
8 offBtn.onclick = () => {
9 if (typeof player.disableSubtitles === 'function') {
10 player.disableSubtitles();
11 } else {
12 player.setSubtitleTrack(null);
13 }
14 // Also immediately clear any currently displayed custom subtitle text
15 const customSubtitleDiv = getCustomSubtitleDiv(player);
16 if (customSubtitleDiv) {
17 customSubtitleDiv.textContent = '';
18 customSubtitleDiv.style.display = 'none';
19 }
20 currentSubtitleDisplay.textContent = 'Current: Off';
21 };
22 subtitleButtonsContainer.appendChild(offBtn);
23
24 if (!tracks || tracks.length === 0) {
25 currentSubtitleDisplay.textContent = 'No subtitle tracks available';
26 return;
27 }
28
29 tracks.forEach((track) => {
30 const btn = document.createElement('button');
31 btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
32 btn.textContent = `${track.label} (${track.language || 'unknown'})`;
33 btn.onclick = () => {
34 // Public API is label-driven (no numeric ids / language codes)
35 player.setSubtitleTrack(track.label);
36 };
37 subtitleButtonsContainer.appendChild(btn);
38 });
39
40 const current = tracks.find((t) => t.isCurrent);
41 currentSubtitleDisplay.textContent = current
42 ? `Current: ${current.label} (${current.language || 'unknown'})`
43 : 'Current: Off';
44}

disableSubtitles() vs setSubtitleTrack(null): Both turn subtitles off and produce the same result. disableSubtitles() is a named convenience method added in newer player builds. The feature-detect guard (typeof player.disableSubtitles === 'function') ensures the code works across all build versions — setSubtitleTrack(null) is the stable fallback.

Clear the overlay immediately on Off. When the user turns subtitles off, fastpixsubtitlecue may not fire again for several seconds — not until the next cue boundary. If you only clear the overlay in the cue handler, the last cue text stays visible on screen after the user has already turned subtitles off. Clearing it directly in the Off button’s onclick prevents this.


Step 7 — Keep buttons in sync after track changes

fastpixaudiochange and fastpixsubtitlechange fire whenever a track switch happens — whether the user clicked one of your buttons, a programmatic call was made, or the built-in player menu was used. Re-render buttons in these handlers so the active state always reflects reality.

1player.addEventListener('fastpixaudiochange', (e) => {
2 const { tracks, currentTrack } = e.detail || {};
3 const resolvedCurrent = currentTrack || (Array.isArray(tracks) ? tracks.find(t => t.isCurrent) : null);
4 renderAudioButtons(tracks || player.getAudioTracks());
5});
6
7player.addEventListener('fastpixsubtitlechange', (e) => {
8 const { tracks, currentTrack } = e.detail || {};
9 const resolvedCurrent = currentTrack || (Array.isArray(tracks) ? tracks.find(t => t.isCurrent) : null);
10 renderSubtitleButtons(tracks || player.getSubtitleTracks());
11});

Both events also carry currentTrack (a full TrackInfo object, or null for Off) and currentId if you need them for logging or analytics.


Step 8 — Render a custom subtitle overlay (subtitle display logic)

This step is the subtitle display logic: you listen for the fastpixsubtitlecue event and use it to show the current cue text in your custom overlay div (from Step 1), or clear/hide the overlay when subtitles are Off or the cue ends.

With hide-native-subtitles on the player, the player’s internal subtitle container never paints text (so the built-in overlay stays hidden). Cue data is still available: fastpixsubtitlecue still fires and you’re responsible for displaying that text in your own overlay.

The event fires each time the active cue changes. Its detail object carries:

FieldTypeDescription
textstringThe cue text to display. Empty string when the cue ends.
languagestringLanguage code of the active subtitle track (e.g. "en")
startTimenumberCue start time in seconds
endTimenumberCue end time in seconds

For most overlays you’ll only need text. The startTime and endTime fields are useful if you want timed animations, karaoke-style highlighting, or precise cue scheduling.

1// Helper: locate the custom subtitle div scoped to a given player's container
2function getCustomSubtitleDiv(forPlayer) {
3 const container = forPlayer.closest('.player-container');
4 return container ? container.querySelector('[data-role="custom-subtitle"]') : null;
5}
6
7// Custom subtitle overlay: attach to ALL players so each renders separately.
8document.querySelectorAll('fastpix-player').forEach((p) => {
9 p.addEventListener('fastpixsubtitlecue', (e) => {
10 const { text, language, startTime, endTime } = /** @type {CustomEvent} */ (e).detail;
11
12 // Only show cues when a subtitle track is actually enabled (not Off).
13 const subtitleTracks = p.getSubtitleTracks();
14 const hasActiveSubtitle = Array.isArray(subtitleTracks) && subtitleTracks.some(t => t.isCurrent);
15
16 const customSubtitleDiv = getCustomSubtitleDiv(p);
17 if (customSubtitleDiv) {
18 if (hasActiveSubtitle && text) {
19 customSubtitleDiv.textContent = text;
20 customSubtitleDiv.style.display = 'block';
21 } else {
22 customSubtitleDiv.textContent = '';
23 customSubtitleDiv.style.display = 'none';
24 }
25 }
26 });
27});

Using document.querySelectorAll('fastpix-player') and p.closest('.player-container') rather than referencing a single player and overlay by id is intentional — this exact code works unchanged whether you have one player on the page or ten, because each cue listener finds its own scoped overlay through the DOM.

Because hide-native-subtitles keeps the player’s internal subtitle layer from painting, your overlay is the only thing rendering subtitles. Style it however you want — font, position, background, animation — without any risk of conflicting with the player’s own styles.


Step 9 — Persist user preferences

Store the user’s preferred language so it’s automatically applied the next time they load a video. Use the language code (e.g. "fr") for storage — it’s stable across videos. Use the label when calling setAudioTrack, since that’s what the selection API accepts.

Note: Restore audio preference in fastpixtracksready (audio tracks are ready then). Restore subtitle preference when subtitle tracks first become available — e.g. in the same place you first call renderSubtitleButtons(subtitleTracks) (e.g. inside the Step 5 poll callback when getSubtitleTracks() returns a non-empty array). If you restore subtitles in fastpixtracksready only, getSubtitleTracks() may still be empty and the saved preference won’t apply.

1// On load: restore audio preference (audio tracks are ready at fastpixtracksready)
2player.addEventListener('fastpixtracksready', () => {
3 const audioTracks = player.getAudioTracks();
4 const savedAudioLang = localStorage.getItem('fp_audioLang');
5
6 if (savedAudioLang) {
7 const match = audioTracks.find((t) => t.language === savedAudioLang);
8 if (match) player.setAudioTrack(match.label);
9 }
10});
11
12// Restore subtitle preference when subtitle tracks first become available (e.g. in your poll callback)
13function restoreSubtitlePreference() {
14 const subtitleTracks = player.getSubtitleTracks();
15 const savedSubtitleLang = localStorage.getItem('fp_subtitleLang');
16 if (savedSubtitleLang && subtitleTracks && subtitleTracks.length > 0) {
17 const match = subtitleTracks.find((t) => t.language === savedSubtitleLang);
18 if (match) player.setSubtitleTrack(match.label);
19 }
20}
21// Call restoreSubtitlePreference() when you first get subtitle tracks (e.g. in the Step 5 poll when subs appear).
22
23// On change: save updated preference
24player.addEventListener('fastpixaudiochange', (e) => {
25 const t = e.detail?.currentTrack;
26 if (t?.language) localStorage.setItem('fp_audioLang', t.language);
27});
28
29player.addEventListener('fastpixsubtitlechange', (e) => {
30 const t = e.detail?.currentTrack;
31 if (t?.language) {
32 localStorage.setItem('fp_subtitleLang', t.language);
33 } else {
34 // User turned subtitles off — clear the saved preference
35 localStorage.removeItem('fp_subtitleLang');
36 }
37});

Putting it all together

Here’s the complete implementation with every step assembled (matches demo/audio_subtitle_tracks.html; the “Current:” lines use label + language as in this guide, not the numeric id):

1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Audio Track Switching Demo</title>
7
8 <script src="https://cdn.jsdelivr.net/npm/@fastpix/[email protected]" defer></script>
9 <style>
10 body {
11 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12 max-width: 800px;
13 margin: 20px auto;
14 padding: 0 20px;
15 }
16 fastpix-player {
17 width: 100%;
18 --aspect-ratio: 21/9;
19 }
20 .player-container {
21 position: relative;
22 width: 100%;
23 aspect-ratio: 16/9;
24 }
25 /* Subtitle overlay: dark pill/bubble, centered over bottom of video */
26 .custom-subtitle {
27 position: absolute;
28 left: 50%;
29 bottom: 10%;
30 transform: translateX(-50%);
31 max-width: 90%;
32 padding: 10px 20px;
33 background: rgba(40, 40, 40, 0.9);
34 color: #fff;
35 display: none;
36 border-radius: 9999px;
37 text-align: center;
38 font-size: 15px;
39 line-height: 1.4;
40 pointer-events: none;
41 box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
42 }
43 .track-controls {
44 margin-top: 20px;
45 padding: 15px;
46 background: #f5f5f5;
47 border-radius: 8px;
48 }
49 .track-controls h3 {
50 margin: 0 0 10px 0;
51 font-weight: 700;
52 font-family: Georgia, 'Times New Roman', serif;
53 color: #000;
54 }
55 .track-buttons {
56 display: flex;
57 gap: 8px;
58 flex-wrap: wrap;
59 }
60 .track-btn {
61 padding: 8px 16px;
62 border: 1px solid #ddd;
63 border-radius: 6px;
64 background: white;
65 cursor: pointer;
66 font-size: 14px;
67 }
68 .track-btn:hover {
69 border-color: #007bff;
70 }
71 .track-btn.active {
72 background: #007bff;
73 color: white;
74 border-color: #007bff;
75 }
76 .current-track {
77 margin-top: 10px;
78 font-size: 13px;
79 color: #666;
80 }
81 .player-time {
82 margin-top: 12px;
83 }
84 </style>
85</head>
86<body>
87 <h1>Audio Track Switching Demo</h1>
88
89 <div class="player-container">
90 <fastpix-player
91 playback-id="your-playback-id-with-tracks"
92 loop
93 auto-play
94 muted
95 preload="auto"
96 default-audio-track="French"
97 default-subtitle-track="English"
98 hide-native-subtitles
99 >
100 </fastpix-player>
101 <div class="custom-subtitle" data-role="custom-subtitle"></div>
102 </div>
103
104 <div class="player-time">
105
106 <button id="forwardButton">Forward 20s</button>
107 <button id="backwardButton">Backward 20s</button>
108
109 </div>
110
111 <div class="track-controls">
112 <h3>Audio Tracks</h3>
113 <div id="audioButtons" class="track-buttons">
114 <em>Loading tracks...</em>
115 </div>
116 <div id="currentAudio" class="current-track"></div>
117 </div>
118
119 <div class="track-controls">
120 <h3>Subtitle Tracks</h3>
121 <div id="subtitleButtons" class="track-buttons">
122 <em>Loading tracks...</em>
123 </div>
124 <div id="currentSubtitle" class="current-track"></div>
125 </div>
126
127 <script>
128 // Controls below target the first player on the page,
129 // but custom subtitles are scoped per-player container.
130 const player = document.querySelector('fastpix-player');
131 const audioButtonsContainer = document.getElementById('audioButtons');
132 const subtitleButtonsContainer = document.getElementById('subtitleButtons');
133 const currentAudioDisplay = document.getElementById('currentAudio');
134 const currentSubtitleDisplay = document.getElementById('currentSubtitle');
135 const forwardButton = document.getElementById('forwardButton');
136 const backwardButton = document.getElementById('backwardButton');
137
138 forwardButton.addEventListener('click', () => {
139 player.seekForward(10);
140 });
141 backwardButton.addEventListener('click', () => {
142 player.seekBackward(10);
143 });
144
145 function getCustomSubtitleDiv(forPlayer) {
146 const container = forPlayer.closest('.player-container');
147 return container ? container.querySelector('[data-role="custom-subtitle"]') : null;
148 }
149 let subtitlePollId = null;
150
151 // Wait for tracks to be ready
152 player.addEventListener('fastpixtracksready', (e) => {
153 console.log("Tracks ready!", e.detail);
154
155 // Get audio tracks
156 const audioTracks = player.getAudioTracks();
157 console.log("Audio tracks:", audioTracks);
158
159 // Render audio track buttons
160 renderAudioButtons(audioTracks);
161
162 // Subtitle tracks often become available slightly after MANIFEST_PARSED.
163 // Poll getSubtitleTracks() for a short window so we render
164 // all detected subtitle tracks automatically, without needing to click Off.
165 if (subtitlePollId) {
166 clearInterval(subtitlePollId);
167 }
168 const startTime = Date.now();
169 subtitlePollId = setInterval(() => {
170 const subtitleTracks = player.getSubtitleTracks();
171 console.log("Polled subtitle tracks:", subtitleTracks);
172 if (subtitleTracks && subtitleTracks.length > 0) {
173 clearInterval(subtitlePollId);
174 subtitlePollId = null;
175 // Render and highlight whichever subtitle track is currently "showing"
176 // so that the actively playing subtitle is highlighted by default.
177 renderSubtitleButtons(subtitleTracks);
178 } else if (Date.now() - startTime > 10000) { // safety timeout 10s
179 clearInterval(subtitlePollId);
180 subtitlePollId = null;
181 }
182 }, 500);
183 });
184
185 // Custom subtitle overlay: attach to ALL players so each renders separately.
186 document.querySelectorAll('fastpix-player').forEach((p) => {
187 p.addEventListener('fastpixsubtitlecue', (e) => {
188 const { text, language, startTime, endTime } = /** @type {CustomEvent} */ (e).detail;
189 console.log('Current subtitle cue:', text, language, startTime, endTime);
190
191 // Only show cues when a subtitle track is actually enabled (not Off).
192 const subtitleTracks = p.getSubtitleTracks();
193 const hasActiveSubtitle = Array.isArray(subtitleTracks) && subtitleTracks.some(t => t.isCurrent);
194
195 const customSubtitleDiv = getCustomSubtitleDiv(p);
196 if (customSubtitleDiv) {
197 if (hasActiveSubtitle && text) {
198 customSubtitleDiv.textContent = text;
199 customSubtitleDiv.style.display = 'block';
200 } else {
201 customSubtitleDiv.textContent = '';
202 customSubtitleDiv.style.display = 'none';
203 }
204 }
205 });
206 });
207
208 // Listen for audio track changes
209 player.addEventListener('fastpixaudiochange', (e) => {
210 const { tracks, currentTrack } = /** @type {CustomEvent} */ (e).detail || {};
211 const resolvedCurrent = currentTrack || (Array.isArray(tracks) ? tracks.find(t => t.isCurrent) : null);
212 console.log("Audio changed. Current track:", resolvedCurrent);
213 renderAudioButtons(tracks || player.getAudioTracks());
214 });
215
216 // Listen for subtitle track changes
217 player.addEventListener('fastpixsubtitlechange', (e) => {
218 const { tracks, currentTrack } = /** @type {CustomEvent} */ (e).detail || {};
219 const resolvedCurrent = currentTrack || (Array.isArray(tracks) ? tracks.find(t => t.isCurrent) : null);
220 console.log("Subtitle changed. Current track:", resolvedCurrent);
221 renderSubtitleButtons(tracks || player.getSubtitleTracks());
222 });
223
224 function renderAudioButtons(tracks) {
225 if (!tracks || tracks.length === 0) {
226 audioButtonsContainer.innerHTML = '<em>No audio tracks available</em>';
227 currentAudioDisplay.textContent = '';
228 return;
229 }
230
231 audioButtonsContainer.innerHTML = '';
232
233 tracks.forEach(track => {
234 const btn = document.createElement('button');
235 btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
236 btn.textContent = `${track.label} (${track.language || 'unknown'})`;
237 btn.onclick = () => {
238 console.log(`Switching to audio track: ${track.label} (${track.language || 'unknown'})`);
239 // Public API is label-driven (no numeric ids / language codes)
240 player.setAudioTrack(track.label);
241 };
242 audioButtonsContainer.appendChild(btn);
243 });
244
245 const current = tracks.find(t => t.isCurrent);
246 currentAudioDisplay.textContent = current
247 ? `Current: ${current.label} (${current.language || 'unknown'})`
248 : '';
249 }
250
251 function renderSubtitleButtons(tracks) {
252 subtitleButtonsContainer.innerHTML = '';
253
254 // Add "Off" button
255 const offBtn = document.createElement('button');
256 offBtn.className = 'track-btn' + (!tracks.some(t => t.isCurrent) ? ' active' : '');
257 offBtn.textContent = 'Off';
258 offBtn.onclick = () => {
259 console.log('Turning subtitles off');
260 // Prefer the new public API if available
261 if (typeof player.disableSubtitles === 'function') {
262 player.disableSubtitles();
263 console.log('Subtitles disabled');
264 } else {
265 // Fallback for older builds
266 player.setSubtitleTrack(null);
267 console.log('Subtitles disabled (fallback)');
268 }
269 // Also immediately clear any currently displayed custom subtitle text
270 const customSubtitleDiv = getCustomSubtitleDiv(player);
271 if (customSubtitleDiv) {
272 customSubtitleDiv.textContent = '';
273 customSubtitleDiv.style.display = 'none';
274 }
275 // Update status text
276 currentSubtitleDisplay.textContent = 'Current: Off';
277 };
278 subtitleButtonsContainer.appendChild(offBtn);
279
280 if (!tracks || tracks.length === 0) {
281 currentSubtitleDisplay.textContent = 'No subtitle tracks available';
282 return;
283 }
284
285 tracks.forEach(track => {
286 const btn = document.createElement('button');
287 btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
288 btn.textContent = `${track.label} (${track.language || 'unknown'})`;
289 btn.onclick = () => {
290 console.log(`Switching to subtitle track: ${track.label} (${track.language || 'unknown'})`);
291 // Public API is label-driven (no numeric ids / language codes)
292 player.setSubtitleTrack(track.label);
293 };
294 subtitleButtonsContainer.appendChild(btn);
295 });
296
297 const current = tracks.find(t => t.isCurrent);
298 currentSubtitleDisplay.textContent = current
299 ? `Current: ${current.label} (${current.language || 'unknown'})`
300 : 'Current: Off';
301 }
302 </script>
303</body>
304</html>

TrackInfo reference

Every track method and event uses a TrackInfo object to represent a single track:

1type TrackInfo = {
2 id: number; // Internal numeric index — do NOT use for switching or persistence
3 label: string; // Display name used for switching, e.g. "English", "French"
4 language?: string; // BCP 47 code, e.g. "en", "fr", "hi" — use this for persistence
5 isDefault: boolean; // Whether this track is marked as default in the HLS manifest
6 isCurrent: boolean; // Whether this track is currently active
7};

Key rules:

  • Switch tracks using label — e.g. setAudioTrack('French')
  • Persist user preferences using language — e.g. localStorage.setItem('fp_audioLang', 'fr')
  • Never store or switch using the numeric id — it is internal and can differ between videos

Best practices

Track lists vary per asset. Don’t assume all videos expose the same set of audio or subtitle tracks. Check tracks.length before rendering and hide controls that don’t apply — for example, hide the audio selector entirely if only one audio track is available.

fastpixtracksready can fire more than once. Treat every emission as “tracks snapshot updated” and re-render idempotently. Clearing containers with innerHTML = '' before each render is enough.

Clear the custom subtitle overlay immediately on Off. fastpixsubtitlecue may not fire again for several seconds after subtitles are turned off. Don’t rely on the cue handler to clear it — clear the overlay directly in your Off button handler.

Use the playing event to clear the overlay after a source change. If the player source changes mid-session, any stale subtitle text in the overlay should be cleared. The player forwards the video element’s playing event, so listen for playing on the <fastpix-player> and reset the overlay when it fires.

Clean up event listeners when components unmount. In React, Vue, or any single-page app, ghost listeners accumulate across navigations if you don’t remove them. Use named handler functions and pair every addEventListener with a corresponding removeEventListener in your cleanup or beforeUnmount hook.


Full API reference

This guide covers the integration patterns and the demo implementation. For the complete reference — all methods, properties, events, attributes, and additional usage examples — see the Audio & Subtitle Tracks API reference in the FastPix web player repository.


See also: React integration

To see how this track switcher and custom subtitle overlay are integrated in a React app (e.g. a vertical shorts feed with track menu and subtitle pill), go through the FastPix React Shorts Demo. That repo uses the same APIs (getAudioTracks, setAudioTrack, getSubtitleTracks, setSubtitleTrack, fastpixtracksready, fastpixsubtitlecue, etc.), mounts the player with document.createElement('fastpix-player') in useEffect, and shows React patterns for refs, cleanup, and feed-level state. Clone it, run npm install and npm run dev, then replace the feed playback IDs with your own multi-track assets.