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
  • Get started
    • Overview
    • Quickstart
  • Upload videos
    • Upload videos from device
    • Upload videos from a URL
    • Upload 4K videos
    • Speed up video processing
  • Playback and delivery
    • Play your videos
    • Embed a video in your app
    • Configure media quality levels
    • Enable MP4 Support for offline viewing
    • Create and manage playlists
  • Edit and transform videos
    • Add metadata to videos
    • Add a watermark to a video
    • Add an intro and outro to a video
    • Clip and trim videos
    • Merge and stitch videos
    • Remove unwanted video segments
  • Manage audio and subtitle tracks
    • Upload and play audio and subtitle tracks
    • Add subtitles to a video
    • Generate subtitles automatically
    • Add audio to a video
    • Replace a video's audio track
    • Normalize audio loudness
    • Overlay audio on a video timeline
  • Extract images from video
    • Create thumbnails from a video
    • Create GIFs from a video
    • Create timeline hovers from a video
  • Video security
    • Generate JWTs for secure media
    • Secure media access with JWTs
    • Restrict playback access
    • Set up DRM encryption
    • FairPlay DRM integration
  • VOD events
    • Media events
    • Transform media events
LogoLogo
StatusSupportDiscussionsLog inSign Up
On this page
  • Part 1: Upload a video with multiple audio and subtitle tracks
  • What you need before starting
  • Method 1: Include subtitle tracks at upload time
  • Method 2: Add tracks to an existing video
  • Wait for tracks to be ready (webhooks)
  • Update or remove tracks
  • Quick reference (upload operations)
  • Part 2: Playback videos with multiple audio and subtitle tracks
  • Web (FastPix Player)
  • Install the player
  • Embed the player
  • Secure playback (private videos)
  • Complete working example (custom track switcher UI)
  • Full API reference
  • Android (FastPix Android Player SDK)
  • 1. Add the dependency
  • 2. Add the required imports
  • 3. Set up playback
  • 4. Listen for and switch audio tracks
  • Example: Build a simple language menu
  • 5. Listen for and switch subtitle tracks
  • Secure playback
  • iOS (FastPix iOS Player SDK)
  • How track detection works (all platforms)
  • Playback quick reference
  • FAQ
  • Next steps
Manage audio and subtitle tracks

Upload and play audio and subtitle tracks

Was this page helpful?
Previous

Add subtitles to a video

Manage subtitle tracks with add, update, and delete operations supporting WebVTT and SRT formats.

Next
Built with

Table of contents

Part 1: Upload

  • What you need before starting
  • Method 1: Include subtitle tracks at upload time
  • Method 2: Add tracks to an existing video
  • Wait for tracks to be ready (webhooks)
  • Update or remove tracks
  • Quick reference (upload operations)

Part 2: Playback

  • Web (FastPix Player)
  • Android (FastPix Android Player SDK)
  • iOS (FastPix iOS Player SDK)
  • How track detection works (all platforms)
  • Playback quick reference

Reference

  • FAQ
  • Next steps

Part 1: Upload a video with multiple audio and subtitle tracks

FastPix lets you attach multiple subtitle files and audio files to a single video. Subtitle tracks can be included at the time of upload or added afterward. Audio tracks can only be added after the video has been created. This section covers the full workflow.

What you need before starting

Before you begin, make sure you have the following ready:

  1. FastPix API credentials: a Token ID and Secret Key from your FastPix dashboard (Settings → API Keys). Combine them as TokenID:SecretKey and base64-encode the result for Basic Auth.
  2. Your video file: either a public URL to the video (for URL-based upload) or the file itself (for direct upload).
  3. Your subtitle and audio files hosted publicly — FastPix fetches these by URL. Each file must be accessible without authentication. A CDN, S3 public bucket, or any public hosting works.

Supported formats:

TypeFormatsRecommended
Subtitles.vtt (WebVTT), .srt (SubRip).vtt
Audio.mp3, .aac, .wav, .ogg.mp3

If your file URL requires authentication (login, token, etc.), FastPix cannot fetch it and the track will fail. For S3, use a pre-signed URL with a generous expiry window.


Method 1: Include subtitle tracks at upload time

If your subtitle files are ready when you upload the video, you can attach them in the same API call by listing them in the inputs array alongside the video. Audio tracks cannot be included at upload time and must be added separately after the video is created (see Method 2).

Endpoint: POST https://api.fastpix.com/v1/on-demand

$curl --request POST \
> --url https://api.fastpix.com/v1/on-demand \
> --header 'authorization: Basic <YOUR_BASE64_CREDENTIALS>' \
> --header 'content-type: application/json' \
> --data '{
> "inputs": [
> {
> "type": "video",
> "url": "https://your-cdn.com/videos/my-course-video.mp4"
> },
> {
> "type": "subtitle",
> "url": "https://your-cdn.com/subtitles/english.vtt",
> "languageCode": "en",
> "languageName": "English"
> },
> {
> "type": "subtitle",
> "url": "https://your-cdn.com/subtitles/spanish.srt",
> "languageCode": "es",
> "languageName": "Spanish"
> },
> {
> "type": "subtitle",
> "url": "https://your-cdn.com/subtitles/hindi.vtt",
> "languageCode": "hi",
> "languageName": "Hindi"
> }
> ],
> "accessPolicy": "public",
> "maxResolution": "1080p"
>}'

What’s happening here: You’re creating a single media asset that contains one video and three subtitle tracks (English, Spanish, Hindi). FastPix processes all of them together and attaches the subtitle tracks to the video automatically.


Input parameters for each subtitle track:

ParameterTypeRequiredDescription
typestringYes"subtitle" for subtitle tracks
urlstringYesPublic URL to the .srt or .vtt file
languageCodestringYesBCP 47 language tag (e.g., en, es, fr, de, hi)
languageNamestringNoDisplay label shown in the player’s subtitle menu (e.g., “English”)

Response:

1{
2 "success": true,
3 "data": {
4 "id": "728edd65-5ab2-4e01-8848-948c5cf1950e",
5 "status": "created",
6 "playbackId": "ab1c2d3e4f5",
7 "tracks": [
8 {
9 "id": "aaa-111",
10 "type": "subtitle",
11 "languageCode": "en",
12 "languageName": "English"
13 },
14 {
15 "id": "bbb-222",
16 "type": "subtitle",
17 "languageCode": "es",
18 "languageName": "Spanish"
19 },
20 {
21 "id": "ccc-333",
22 "type": "subtitle",
23 "languageCode": "hi",
24 "languageName": "Hindi"
25 }
26 ]
27 }
28}

NOTE:

Save each track’s id from the response. You’ll need these trackId values if you want to update or remove individual tracks later.

This also works with the Direct Upload endpoint. Include your subtitle tracks inside the pushMediaSettings object. See the direct upload docs for the exact payload structure.


Method 2: Add tracks to an existing video

Once a video is uploaded, you can add both subtitle and audio tracks at any time using the tracks endpoint. This is also the only way to add audio tracks, since they cannot be included at upload time. Call the endpoint once per track.

Endpoint: POST https://api.fastpix.com/v1/on-demand/{mediaId}/tracks

Add a subtitle track:

$curl --request POST \
> --url https://api.fastpix.com/v1/on-demand/{mediaId}/tracks \
> --header 'authorization: Basic <YOUR_BASE64_CREDENTIALS>' \
> --header 'content-type: application/json' \
> --data '{
> "tracks": {
> "url": "https://your-cdn.com/subtitles/japanese.vtt",
> "type": "subtitle",
> "languageCode": "ja",
> "languageName": "Japanese"
> }
>}'

Add an audio track:

$curl --request POST \
> --url https://api.fastpix.com/v1/on-demand/{mediaId}/tracks \
> --header 'authorization: Basic <YOUR_BASE64_CREDENTIALS>' \
> --header 'content-type: application/json' \
> --data '{
> "tracks": {
> "url": "https://your-cdn.com/audio/spanish-dub.mp3",
> "type": "audio",
> "languageCode": "es",
> "languageName": "Spanish"
> }
>}'

There is no limit on the number of tracks per video. Call this endpoint once for each language or audio variant you want to add.


Response (audio track example):

1{
2 "success": true,
3 "data": {
4 "id": "ce926a2b-5448-4d39-88ab-d33641379a45",
5 "type": "audio",
6 "url": "https://your-cdn.com/audio/spanish-dub.mp3",
7 "languageCode": "es",
8 "languageName": "Spanish"
9 }
10}

SAVE THIS

Save data.id as the trackId. Audio tracks are registered but not immediately ready. Wait for the video.media.track.ready webhook before serving to viewers.


Wait for tracks to be ready (webhooks)

FastPix processes tracks asynchronously. After your API call returns, the track is queued for processing. You should listen for webhook events to know when tracks are ready for playback.

Key webhook events:

EventWhen it firesApplies to
video.media.track.createdTrack is registered, processing has startedAudio tracks
video.media.track.readyTrack is fully processed and available for playbackSubtitle and audio tracks
video.media.track.updatedAn updated track has been re-processedBoth

Production pattern:

  1. Call the add-track API and store the returned trackId in your database with status "processing".
  2. Listen for video.media.track.ready on your webhook endpoint.
  3. When the webhook fires, match the trackId and update the status to "ready".
  4. Only then expose the track to viewers in your player or UI.

Register your webhook endpoint in the FastPix dashboard under Settings → Webhooks. FastPix sends a POST request to your endpoint for each event.


Update or remove tracks

Update a track (fix a typo, swap the file, correct the language tag):

$curl --request PATCH \
> --url https://api.fastpix.com/v1/on-demand/{mediaId}/tracks/{trackId} \
> --header 'authorization: Basic <YOUR_BASE64_CREDENTIALS>' \
> --header 'content-type: application/json' \
> --data '{
> "url": "https://your-cdn.com/subtitles/spanish-v2.srt",
> "languageCode": "es",
> "languageName": "Spanish (Updated)"
>}'

Delete a track:

$curl --request DELETE \
> --url https://api.fastpix.com/v1/on-demand/{mediaId}/tracks/{trackId} \
> --header 'authorization: Basic <YOUR_BASE64_CREDENTIALS>'

NOTE:

Track deletion is permanent and cannot be undone. Double-check you have the correct trackId before sending the request.


Quick reference (upload operations)

OperationMethodEndpointNotes
Upload video with subtitlesPOST/v1/on-demandInclude subtitle tracks in inputs array
Add subtitle to existing videoPOST/v1/on-demand/{mediaId}/trackstype: "subtitle"
Add audio to existing videoPOST/v1/on-demand/{mediaId}/trackstype: "audio" (only way to add audio)
Update a trackPATCH/v1/on-demand/{mediaId}/tracks/{trackId}Change URL, language, or name
Delete a trackDELETE/v1/on-demand/{mediaId}/tracks/{trackId}Permanent

Part 2: Playback videos with multiple audio and subtitle tracks

Once your tracks are processed and ready, the FastPix Player handles track switching automatically. This section covers how playback works on web, Android, and iOS.

The core idea is the same across all platforms: the FastPix Player reads the HLS manifest, detects all attached subtitle and audio tracks, and presents them as selectable options in the player UI. You don’t need to write custom track-switching logic.


Web (FastPix Player)

The FastPix web player is a custom HTML element (<fastpix-player>) that handles everything out of the box.

Install the player

CDN:

1<script src="https://cdn.jsdelivr.net/npm/@fastpix/[email protected]" defer></script>

NPM:

$npm i @fastpix/fp-player

See the installation guide for all options.

Embed the player

1<fastpix-player
2 playback-id="YOUR_PLAYBACK_ID"
3 stream-type="on-demand">
4</fastpix-player>

All subtitle and audio tracks attached to that playbackId appear automatically in the player UI:

  • A CC button appears in the toolbar when subtitle tracks are detected. Clicking it shows a menu listing all available languages, plus an “Off” option.
  • An audio icon appears when multiple audio tracks are detected. Viewers can switch between language dubs or audio variants.

If you add tracks after the video is already being played:

When tracks are added dynamically to a video that viewers are currently watching, enable cache busting to ensure the player fetches the updated HLS manifest:

1<fastpix-player
2 playback-id="YOUR_PLAYBACK_ID"
3 stream-type="on-demand"
4 enable-cache-busting>
5</fastpix-player>

This appends a unique query parameter to the manifest URL, forcing the browser and CDN to serve the latest version instead of a cached copy.


Secure playback (private videos)

For videos with a private access policy, pass a signed JWT token:

1<fastpix-player
2 playback-id="YOUR_PLAYBACK_ID"
3 stream-type="on-demand"
4 token="YOUR_JWT_TOKEN">
5</fastpix-player>

Track switching works identically for private and public videos. The token authenticates access to the stream; once the player loads the manifest, all tracks are available.


Complete working example (custom track switcher UI)

The following example shows a fully functional page that goes beyond the default player UI. It wires up custom audio and subtitle track buttons, renders a styled subtitle overlay driven entirely by your own code, polls for subtitle tracks after the manifest is parsed, keeps buttons in sync as tracks change, and supports forward/backward seek controls. It’s a starting point you can drop into any HTML page and extend.

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 <script defer src="https://cdn.jsdelivr.net/npm/@fastpix/[email protected]"></script>
8 <style>
9 body {
10 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11 max-width: 800px;
12 margin: 20px auto;
13 padding: 0 20px;
14 }
15 fastpix-player { width: 100%; --aspect-ratio: 21/9; }
16 .player-container { position: relative; width: 100%; aspect-ratio: 16/9; }
17 .custom-subtitle {
18 position: absolute;
19 left: 50%; bottom: 10%;
20 transform: translateX(-50%);
21 max-width: 90%;
22 padding: 6px 12px;
23 background: rgba(0, 0, 0, 0.7);
24 color: #fff;
25 display: none;
26 border-radius: 4px;
27 text-align: center;
28 font-size: 16px;
29 line-height: 1.4;
30 pointer-events: none;
31 box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
32 }
33 .track-controls { margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px; }
34 .track-controls h3 { margin: 0 0 10px 0; }
35 .track-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
36 .track-btn {
37 padding: 8px 16px;
38 border: 2px solid #ddd;
39 border-radius: 6px;
40 background: white;
41 cursor: pointer;
42 font-size: 14px;
43 }
44 .track-btn:hover { border-color: #007bff; }
45 .track-btn.active { background: #007bff; color: white; border-color: #007bff; }
46 .current-track { margin-top: 10px; font-size: 14px; color: #666; }
47 </style>
48</head>
49<body>
50 <h1>Audio Track Switching Demo</h1>
51
52 <!--
53 player-container: required for the custom subtitle overlay.
54 position: relative on the wrapper lets the overlay use absolute positioning.
55 default-audio-track / default-subtitle-track: set the initial active track
56 by label (case-insensitive) when the player loads.
57 hide-native-subtitles: suppresses the player's built-in subtitle rendering
58 so the custom overlay below is the only thing displaying subtitle text.
59 -->
60 <div class="player-container">
61 <fastpix-player
62 playback-id="your-playback-id-with-tracks"
63 loop
64 auto-play
65 muted
66 preload="auto"
67 default-audio-track="French"
68 default-subtitle-track="English"
69 hide-native-subtitles>
70 </fastpix-player>
71 <div class="custom-subtitle" data-role="custom-subtitle"></div>
72 </div>
73
74 <div class="player-time">
75 <button id="forwardButton">Forward</button>
76 <button id="backwardButton">Backward</button>
77 </div>
78
79 <div class="track-controls">
80 <h3>Audio Tracks</h3>
81 <div id="audioButtons" class="track-buttons"><em>Loading tracks...</em></div>
82 <div id="currentAudio" class="current-track"></div>
83 </div>
84
85 <div class="track-controls">
86 <h3>Subtitle Tracks</h3>
87 <div id="subtitleButtons" class="track-buttons"><em>Loading tracks...</em></div>
88 <div id="currentSubtitle" class="current-track"></div>
89 </div>
90
91 <script>
92 const player = document.querySelector('fastpix-player');
93 const audioButtonsContainer = document.getElementById('audioButtons');
94 const subtitleButtonsContainer = document.getElementById('subtitleButtons');
95 const currentAudioDisplay = document.getElementById('currentAudio');
96 const currentSubtitleDisplay = document.getElementById('currentSubtitle');
97 const forwardButton = document.getElementById('forwardButton');
98 const backwardButton = document.getElementById('backwardButton');
99
100 // Seek controls
101 forwardButton.addEventListener('click', () => player.seekForward(10));
102 backwardButton.addEventListener('click', () => player.seekBackward(10));
103
104 // Helper: find the custom subtitle overlay scoped to a given player's container.
105 // Using closest() + data-role makes this work correctly when multiple
106 // players are on the same page — each cue listener finds its own overlay.
107 function getCustomSubtitleDiv(forPlayer) {
108 const container = forPlayer.closest('.player-container');
109 return container ? container.querySelector('[data-role="custom-subtitle"]') : null;
110 }
111
112 let subtitlePollId = null;
113
114 // fastpixtracksready fires when the HLS manifest has been parsed and
115 // audio tracks are available. Subtitle textTracks attach slightly later,
116 // so we render audio immediately and poll for subtitles.
117 player.addEventListener('fastpixtracksready', () => {
118 renderAudioButtons(player.getAudioTracks());
119
120 // Clear any previous poll — fastpixtracksready can fire more than once.
121 if (subtitlePollId) clearInterval(subtitlePollId);
122
123 const startTime = Date.now();
124 subtitlePollId = setInterval(() => {
125 const subtitleTracks = player.getSubtitleTracks();
126 if (subtitleTracks && subtitleTracks.length > 0) {
127 clearInterval(subtitlePollId);
128 subtitlePollId = null;
129 renderSubtitleButtons(subtitleTracks);
130 } else if (Date.now() - startTime > 10_000) {
131 // Safety timeout: stop polling after 10 seconds if no tracks appear.
132 clearInterval(subtitlePollId);
133 subtitlePollId = null;
134 }
135 }, 500);
136 });
137
138 // Custom subtitle overlay — attached to every player on the page so each
139 // renders its own overlay independently via getCustomSubtitleDiv().
140 // hide-native-subtitles suppresses the player's own rendering, so this
141 // overlay is the only thing displaying subtitle text.
142 document.querySelectorAll('fastpix-player').forEach((p) => {
143 p.addEventListener('fastpixsubtitlecue', (e) => {
144 const { text } = e.detail || {};
145 const subtitleTracks = p.getSubtitleTracks();
146 const hasActive = Array.isArray(subtitleTracks) && subtitleTracks.some(t => t.isCurrent);
147 const overlay = getCustomSubtitleDiv(p);
148 if (!overlay) return;
149 if (hasActive && text) {
150 overlay.textContent = text;
151 overlay.style.display = 'block';
152 } else {
153 overlay.textContent = '';
154 overlay.style.display = 'none';
155 }
156 });
157 });
158
159 // Re-render buttons on every track change — whether triggered by your
160 // custom buttons, a programmatic call, or the built-in player menu.
161 player.addEventListener('fastpixaudiochange', (e) => {
162 const { tracks } = e.detail || {};
163 renderAudioButtons(tracks || player.getAudioTracks());
164 });
165 player.addEventListener('fastpixsubtitlechange', (e) => {
166 const { tracks } = e.detail || {};
167 renderSubtitleButtons(tracks || player.getSubtitleTracks());
168 });
169
170 // Render audio track buttons. setAudioTrack() takes a label string — not a
171 // numeric id. The id field is internal and must not be used for switching.
172 function renderAudioButtons(tracks) {
173 if (!tracks || tracks.length === 0) {
174 audioButtonsContainer.innerHTML = '<em>No audio tracks available</em>';
175 currentAudioDisplay.textContent = '';
176 return;
177 }
178 audioButtonsContainer.innerHTML = '';
179 tracks.forEach((track) => {
180 const btn = document.createElement('button');
181 btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
182 btn.textContent = `${track.label} (${track.language || 'unknown'})`;
183 btn.onclick = () => player.setAudioTrack(track.label);
184 audioButtonsContainer.appendChild(btn);
185 });
186 const current = tracks.find(t => t.isCurrent);
187 currentAudioDisplay.textContent = current
188 ? `Current: ${current.label} (${current.language || 'unknown'})`
189 : '';
190 }
191
192 // Render subtitle track buttons. Always includes an Off button first.
193 // Off is active when no track has isCurrent: true.
194 function renderSubtitleButtons(tracks) {
195 subtitleButtonsContainer.innerHTML = '';
196
197 const offBtn = document.createElement('button');
198 offBtn.className = 'track-btn' + (!(tracks || []).some(t => t.isCurrent) ? ' active' : '');
199 offBtn.textContent = 'Off';
200 offBtn.onclick = () => {
201 // disableSubtitles() is preferred; fall back to setSubtitleTrack(null)
202 // for older player builds.
203 if (typeof player.disableSubtitles === 'function') {
204 player.disableSubtitles();
205 } else {
206 player.setSubtitleTrack(null);
207 }
208 // Clear the overlay immediately — don't wait for the next cue event,
209 // which may not fire for several seconds after subtitles are turned off.
210 const overlay = getCustomSubtitleDiv(player);
211 if (overlay) { overlay.textContent = ''; overlay.style.display = 'none'; }
212 currentSubtitleDisplay.textContent = 'Current: Off';
213 };
214 subtitleButtonsContainer.appendChild(offBtn);
215
216 if (!tracks || tracks.length === 0) {
217 currentSubtitleDisplay.textContent = 'No subtitle tracks available';
218 return;
219 }
220 tracks.forEach((track) => {
221 const btn = document.createElement('button');
222 btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
223 btn.textContent = `${track.label} (${track.language || 'unknown'})`;
224 btn.onclick = () => player.setSubtitleTrack(track.label);
225 subtitleButtonsContainer.appendChild(btn);
226 });
227 const current = tracks.find(t => t.isCurrent);
228 currentSubtitleDisplay.textContent = current
229 ? `Current: ${current.label} (${current.language || 'unknown'})`
230 : 'Current: Off';
231 }
232 </script>
233</body>
234</html>

Key points about the example above:

  • hide-native-subtitles suppresses the player’s built-in subtitle layer so the custom-subtitle overlay is the only thing rendering subtitle text. Without this attribute, cue text would appear twice.
  • default-audio-track / default-subtitle-track accept a track label (case-insensitive) and set the initially active track when the player loads. If the label doesn’t match any track in the manifest, the player falls back to the manifest default.
  • fastpixtracksready is the correct entry point for all track work. Never call getAudioTracks() or getSubtitleTracks() before this event fires.
  • Subtitle polling after fastpixtracksready is intentional — subtitle textTracks register slightly later than audio tracks. Polling getSubtitleTracks() for a short window catches them reliably.
  • setAudioTrack() and setSubtitleTrack() take a label string, not a numeric id. The id field in the TrackInfo object is an internal index and must not be used for switching or persistence.
  • Off button clearing happens synchronously in onclick. fastpixsubtitlecue may not fire again for several seconds after subtitles are turned off, so relying on the cue handler to clear the overlay causes a visible delay.

For a step-by-step breakdown of each part of this implementation — including how to persist user language preferences across sessions — see Build a custom track switcher UI.


Full API reference

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.

React integration

To see how this track switcher and custom subtitle overlay are integrated in a React app (for example, a vertical shorts feed with a track menu and subtitle pill), see FastPix React Shorts Demo. That repo uses the same APIs (getAudioTracks, setAudioTrack, getSubtitleTracks, setSubtitleTrack, fastpixtracksready, fastpixsubtitlecue, and others), 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.

For a full deep dive on how the player detects and renders subtitle and audio tracks, see Manage audio and subtitles.


Android (FastPix Android Player SDK)

The FastPix Android Player SDK is built on Media3/ExoPlayer. It automatically detects subtitle and audio tracks from the HLS manifest and supports on-the-fly switching with no additional track-switching code required.

1. Add the dependency

In your app-level build.gradle (or build.gradle.kts for Kotlin DSL):

1dependencies {
2 // Check for the latest version
3 implementation("io.fastpix.player:android:1.0.5")
4}

In your settings.gradle (or settings.gradle.kts), add the GitHub Maven repository. You’ll need a GitHub Personal Access Token (PAT) to access the private Maven package.

Note: Load credentials from local.properties to avoid hardcoding secrets in your source files.

1repositories {
2 maven {
3 url = uri("https://maven.pkg.github.com/FastPix/fastpix-android-player")
4 credentials {
5 gpr.user=<your_github_username>
6 gpr.key=<your_github_pat>
7 }
8 }
9}

Click Sync Now after adding the repository.


2. Add the required imports

1import io.fastpix.media3.PlayerView
2import io.fastpix.media3.PlaybackListener
3import io.fastpix.media3.core.FastPixPlayer
4import io.fastpix.media3.core.PlaybackResolution
5import io.fastpix.media3.tracks.AudioTrackListener
6import io.fastpix.media3.tracks.SubtitleTrackListener
7import androidx.media3.common.MediaItem
8import androidx.media3.common.PlaybackException

3. Set up playback

1class VideoPlayerActivity : AppCompatActivity() {
2
3 private lateinit var binding: ActivityVideoPlayerBinding
4 private val playerView get() = binding.playerView
5 private var player: FastPixPlayer? = null
6 private lateinit var playbackListener: PlaybackListener
7
8 override fun onCreate(savedInstanceState: Bundle?) {
9 super.onCreate(savedInstanceState)
10 binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
11 setContentView(binding.root)
12
13 player = FastPixPlayer.Builder(this)
14 .setAutoplay(true)
15 .setLoop(false)
16 .build()
17 playerView.player = player
18 }
19
20 override fun onStart() {
21 super.onStart()
22 startPlayback()
23 }
24
25 private fun startPlayback() {
26 playbackListener = object : PlaybackListener {
27 override fun onPlay() {}
28 override fun onPause() {}
29 override fun onPlaybackStateChanged(isPlaying: Boolean) {}
30 override fun onError(error: PlaybackException) {}
31 }
32 player?.addPlaybackListener(playbackListener)
33
34 player?.setFastPixMediaItem {
35 playbackId = "YOUR_PLAYBACK_ID"
36 }
37 }
38
39 override fun onDestroy() {
40 super.onDestroy()
41 player?.removePlaybackListener(playbackListener)
42 if (isFinishing) {
43 playerView.release()
44 }
45 }
46}

4. Listen for and switch audio tracks

The SDK discovers audio tracks from the HLS manifest and fires callbacks when they’re available:

1import io.fastpix.media3.tracks.AudioTrackListener
2import io.fastpix.media3.tracks.AudioTrackUpdateReason
3
4player?.addAudioTrackListener(object : AudioTrackListener {
5 override fun onAudioTracksLoaded(tracks: List<AudioTrack>, reason: AudioTrackUpdateReason) {
6 // Populate your language selection UI
7 // Each AudioTrack has an id field; use it to switch tracks
8 }
9
10 override fun onAudioTracksChange(selectedTrack: AudioTrack) {
11 // Update the selected state in your UI
12 }
13
14 override fun onAudioTrackSwitching(isSwitching: Boolean) {
15 // Optionally show or hide a "switching" indicator
16 }
17
18 override fun onAudioTracksLoadedFailed(error: AudioTrackError) {
19 // Handle errors such as invalid ID or player not ready
20 }
21})

To switch to a specific audio track:

1val tracks = player.getAudioTracks()
2val target = tracks.firstOrNull() ?: return
3player.setAudioTrack(target.id)

NOTE:

If a seek is in progress, the SDK defers the track switch until the seek completes.


Example: Build a simple language menu

1val tracks = player.getAudioTracks()
2val items = tracks.filter { it.isPlayable }.map { t ->
3 val title = t.languageName ?: t.label ?: t.languageCode ?: "Unknown"
4 title to t.id
5}
6// Present items in a dialog; on click: player.setAudioTrack(id)

To set a preferred default audio track that applies automatically when tracks become available:

1player?.setDefaultAudioTrack(languageName = "Spanish")

5. Listen for and switch subtitle tracks

1import io.fastpix.media3.tracks.SubtitleTrackListener
2
3player?.addSubtitleTrackListener(object : SubtitleTrackListener {
4 override fun onSubtitlesLoaded(tracks: List<SubtitleTrack>) {
5 // Populate your subtitle selection UI
6 }
7
8 override fun onSubtitleChange(track: SubtitleTrack?) {
9 // track is null when subtitles are disabled
10 }
11
12 override fun onSubtitlesLoadedFailed(error: SubtitleTrackError) {
13 // Handle errors such as track not found or player not ready
14 }
15
16 override fun onSubtitleCueChange(info: SubtitleRenderInfo) {
17 // Observe cue text and timing if building a custom subtitle renderer
18 }
19})

To switch to a specific subtitle track or disable subtitles:

1val subs = player?.getSubtitleTracks() ?: return
2val target = subs.firstOrNull { it.languageName == "English" }
3if (target != null) {
4 player?.setSubtitleTrack(target.id)
5} else {
6 player?.disableSubtitles()
7}

To set a preferred default subtitle track:

1player?.setDefaultSubtitleTrack(languageName = "English")

Note: Use BCP-47 or ISO language names such as "English", "Spanish", "Hindi", or "French". Default track preferences apply automatically when tracks become available and never override a manual selection. If the preferred language isn’t present in the stream, the player retains its current selection.


Secure playback

For videos with a private access policy, pass a signed JWT token via playbackToken:

1player?.setFastPixMediaItem {
2 playbackId = "YOUR_PLAYBACK_ID"
3 playbackToken = "YOUR_JWT_TOKEN"
4}

Track switching works identically for private and public videos.

For the full Android SDK reference, see FastPix player for Android.


iOS (FastPix iOS Player SDK)

The FastPix iOS Player SDK wraps AVPlayer and AVPlayerViewController. It auto-detects subtitle and audio tracks from the HLS manifest and supports dynamic audio track switching.

1. Install via Swift Package Manager:

In Xcode: File → Add Packages → enter the repository URL: https://github.com/FastPix/iOS-player


2. Import and set up playback:

1import UIKit
2import FastPixPlayerSDK
3import AVKit
4
5class VideoPlayerViewController: UIViewController {
6 var playbackID = "YOUR_PLAYBACK_ID"
7 lazy var playerViewController = AVPlayerViewController()
8
9 override func viewDidLoad() {
10 super.viewDidLoad()
11 prepareAvPlayerController()
12
13 // Play on-demand video
14 playerViewController.prepare(
15 playbackID: playbackID,
16 playbackOptions: PlaybackOptions(streamType: "on-demand")
17 )
18 playerViewController.player?.play()
19 }
20
21 func prepareAvPlayerController() {
22 addChild(playerViewController)
23 view.addSubview(playerViewController.view)
24 playerViewController.didMove(toParent: self)
25 playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
26
27 NSLayoutConstraint.activate([
28 playerViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
29 playerViewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
30 playerViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
31 playerViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
32 ])
33 }
34}

3. Audio and subtitle track switching:

The player dynamically detects all available audio tracks from the HLS manifest. Users can switch audio tracks through the player’s built-in interface without restarting the stream. Subtitle tracks detected from the manifest are displayed automatically during playback.

For secure playback:

1playerViewController.prepare(
2 playbackID: playbackID,
3 playbackOptions: PlaybackOptions(playbackToken: "YOUR_JWT_TOKEN")
4)

The iOS SDK also supports tvOS with the same API surface. See the tvOS setup guide for setup details.

Full SDK reference: FastPix player for iOS


How track detection works (all platforms)

Regardless of platform, the FastPix Player follows the same flow:

  1. Manifest parsing: When you provide a playbackId, the player constructs the HLS stream URL and fetches the manifest. The manifest contains metadata entries for each subtitle and audio track, including language, name, and URI.
  2. UI activation: If subtitle tracks are found, the subtitle/CC button is activated in the player interface. If multiple audio tracks are found, the audio selection control is activated.
  3. Default selection: The player selects the first subtitle track listed in the manifest and displays it by default. The default audio track is typically the original audio from the uploaded video.
  4. User switching: Viewers can switch languages or turn subtitles off through the player’s built-in menu. Audio track switching happens without restarting the stream.

Playback quick reference

PlatformSDK / ComponentInstall methodTrack switching
Web<fastpix-player>npm or CDNAutomatic (CC button + audio icon)
AndroidFastPix Android Player SDKGradle + GitHub MavenAutomatic (built-in UI controls)
iOS / tvOSFastPix iOS Player SDKSwift Package ManagerAutomatic (AVPlayer built-in + dynamic detection)

FAQ

Can I upload a video with subtitle tracks in a single API call?

Yes. Include subtitle files in the inputs array alongside your video when calling POST /v1/on-demand. Each subtitle item needs type: "subtitle", a public url, and a languageCode. Audio tracks cannot be included at upload time and must be added separately via the tracks endpoint after the video is created.

Is there a limit on the number of tracks per video?

There is no stated limit. You can add as many subtitle and audio tracks as you need.

What if I add tracks to a video that viewers are currently watching?

On web, enable enable-cache-busting on the player to force a manifest refresh. On mobile, the next time the player loads the stream, it will pick up the new tracks.

How do I know my tracks are ready for playback?

Listen for the video.media.track.ready webhook. For audio tracks, there’s also a video.media.track.created event that fires earlier (registered but still processing). Don’t serve the track until you receive ready.

What language code format should I use?

BCP 47. Simple codes like en, es, fr, de, ja, hi work for most cases. For regional variants, use en-US, pt-BR, zh-TW, etc.


Next steps

  • Add subtitles to a video — Full API reference for subtitle track operations.
  • Add audio to a video — Full API reference for audio track operations.
  • Manage audio and subtitles — Deep dive on how the web player handles track switching.
  • FastPix player for Android — Full Android SDK integration guide.
  • FastPix player for iOS — Full iOS SDK integration guide.
  • Webhook event reference — Register your endpoint for real-time track processing events.
  • Setup video using JWTs — Protect private media with signed tokens.