Upload and play audio and subtitle tracks
A complete guide to uploading videos with multiple audio and subtitle tracks through the FastPix API, then playing them back with track switching on web, Android, and iOS.
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
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:
-
FastPix API credentials: a Token ID and Secret Key from your FastPix dashboard (Settings → API Keys). Combine them as
TokenID:SecretKeyand base64-encode the result for Basic Auth. -
Your video file: either a public URL to the video (for URL-based upload) or the file itself (for direct upload).
-
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:
| Type | Formats | Recommended |
|---|---|---|
| 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.io/v1/on-demand
curl --request POST \
--url https://api.fastpix.io/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:
| Parameter | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "subtitle" for subtitle tracks |
url | string | Yes | Public URL to the .srt or .vtt file |
languageCode | string | Yes | BCP 47 language tag (e.g., en, es, fr, de, hi) |
languageName | string | No | Display label shown in the player's subtitle menu (e.g., "English") |
Response:
{
"success": true,
"data": {
"id": "728edd65-5ab2-4e01-8848-948c5cf1950e",
"status": "created",
"playbackId": "ab1c2d3e4f5",
"tracks": [
{
"id": "aaa-111",
"type": "subtitle",
"languageCode": "en",
"languageName": "English"
},
{
"id": "bbb-222",
"type": "subtitle",
"languageCode": "es",
"languageName": "Spanish"
},
{
"id": "ccc-333",
"type": "subtitle",
"languageCode": "hi",
"languageName": "Hindi"
}
]
}
}NOTE:
Save each track'sidfrom the response. You'll need thesetrackIdvalues 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.io/v1/on-demand/{mediaId}/tracks
Add a subtitle track:
curl --request POST \
--url https://api.fastpix.io/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.io/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):
{
"success": true,
"data": {
"id": "ce926a2b-5448-4d39-88ab-d33641379a45",
"type": "audio",
"url": "https://your-cdn.com/audio/spanish-dub.mp3",
"languageCode": "es",
"languageName": "Spanish"
}
}
Savedata.idas thetrackId. Audio tracks are registered but not immediately ready. Wait for thevideo.media.track.readywebhook 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:
| Event | When it fires | Applies to |
|---|---|---|
video.media.track.created | Track is registered, processing has started | Audio tracks |
video.media.track.ready | Track is fully processed and available for playback | Subtitle and audio tracks |
video.media.track.updated | An updated track has been re-processed | Both |
Production pattern:
- Call the add-track API and store the returned
trackIdin your database with status"processing". - Listen for
video.media.track.readyon your webhook endpoint. - When the webhook fires, match the
trackIdand update the status to"ready". - 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.io/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.io/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 correcttrackIdbefore sending the request.
Quick reference (upload operations)
| Operation | Method | Endpoint | Notes |
|---|---|---|---|
| Upload video with subtitles | POST | /v1/on-demand | Include subtitle tracks in inputs array |
| Add subtitle to existing video | POST | /v1/on-demand/{mediaId}/tracks | type: "subtitle" |
| Add audio to existing video | POST | /v1/on-demand/{mediaId}/tracks | type: "audio" (only way to add audio) |
| Update a track | PATCH | /v1/on-demand/{mediaId}/tracks/{trackId} | Change URL, language, or name |
| Delete a track | DELETE | /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:
<script src="https://cdn.jsdelivr.net/npm/@fastpix/[email protected]" defer></script>NPM:
npm i @fastpix/fp-playerSee the installation guide for all options.
Embed the player
<fastpix-player
playback-id="YOUR_PLAYBACK_ID"
stream-type="on-demand">
</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:
<fastpix-player
playback-id="YOUR_PLAYBACK_ID"
stream-type="on-demand"
enable-cache-busting>
</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:
<fastpix-player
playback-id="YOUR_PLAYBACK_ID"
stream-type="on-demand"
token="YOUR_JWT_TOKEN">
</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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Audio Track Switching Demo</title>
<script defer src="https://cdn.jsdelivr.net/npm/@fastpix/[email protected]"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
}
fastpix-player { width: 100%; --aspect-ratio: 21/9; }
.player-container { position: relative; width: 100%; aspect-ratio: 16/9; }
.custom-subtitle {
position: absolute;
left: 50%; bottom: 10%;
transform: translateX(-50%);
max-width: 90%;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
display: none;
border-radius: 4px;
text-align: center;
font-size: 16px;
line-height: 1.4;
pointer-events: none;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
}
.track-controls { margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px; }
.track-controls h3 { margin: 0 0 10px 0; }
.track-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
.track-btn {
padding: 8px 16px;
border: 2px solid #ddd;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 14px;
}
.track-btn:hover { border-color: #007bff; }
.track-btn.active { background: #007bff; color: white; border-color: #007bff; }
.current-track { margin-top: 10px; font-size: 14px; color: #666; }
</style>
</head>
<body>
<h1>Audio Track Switching Demo</h1>
<!--
player-container: required for the custom subtitle overlay.
position: relative on the wrapper lets the overlay use absolute positioning.
default-audio-track / default-subtitle-track: set the initial active track
by label (case-insensitive) when the player loads.
hide-native-subtitles: suppresses the player's built-in subtitle rendering
so the custom overlay below is the only thing displaying subtitle text.
-->
<div class="player-container">
<fastpix-player
playback-id="your-playback-id-with-tracks"
loop
auto-play
muted
preload="auto"
default-audio-track="French"
default-subtitle-track="English"
hide-native-subtitles>
</fastpix-player>
<div class="custom-subtitle" data-role="custom-subtitle"></div>
</div>
<div class="player-time">
<button id="forwardButton">Forward</button>
<button id="backwardButton">Backward</button>
</div>
<div class="track-controls">
<h3>Audio Tracks</h3>
<div id="audioButtons" class="track-buttons"><em>Loading tracks...</em></div>
<div id="currentAudio" class="current-track"></div>
</div>
<div class="track-controls">
<h3>Subtitle Tracks</h3>
<div id="subtitleButtons" class="track-buttons"><em>Loading tracks...</em></div>
<div id="currentSubtitle" class="current-track"></div>
</div>
<script>
const player = document.querySelector('fastpix-player');
const audioButtonsContainer = document.getElementById('audioButtons');
const subtitleButtonsContainer = document.getElementById('subtitleButtons');
const currentAudioDisplay = document.getElementById('currentAudio');
const currentSubtitleDisplay = document.getElementById('currentSubtitle');
const forwardButton = document.getElementById('forwardButton');
const backwardButton = document.getElementById('backwardButton');
// Seek controls
forwardButton.addEventListener('click', () => player.seekForward(10));
backwardButton.addEventListener('click', () => player.seekBackward(10));
// Helper: find the custom subtitle overlay scoped to a given player's container.
// Using closest() + data-role makes this work correctly when multiple
// players are on the same page — each cue listener finds its own overlay.
function getCustomSubtitleDiv(forPlayer) {
const container = forPlayer.closest('.player-container');
return container ? container.querySelector('[data-role="custom-subtitle"]') : null;
}
let subtitlePollId = null;
// fastpixtracksready fires when the HLS manifest has been parsed and
// audio tracks are available. Subtitle textTracks attach slightly later,
// so we render audio immediately and poll for subtitles.
player.addEventListener('fastpixtracksready', () => {
renderAudioButtons(player.getAudioTracks());
// Clear any previous poll — fastpixtracksready can fire more than once.
if (subtitlePollId) clearInterval(subtitlePollId);
const startTime = Date.now();
subtitlePollId = setInterval(() => {
const subtitleTracks = player.getSubtitleTracks();
if (subtitleTracks && subtitleTracks.length > 0) {
clearInterval(subtitlePollId);
subtitlePollId = null;
renderSubtitleButtons(subtitleTracks);
} else if (Date.now() - startTime > 10_000) {
// Safety timeout: stop polling after 10 seconds if no tracks appear.
clearInterval(subtitlePollId);
subtitlePollId = null;
}
}, 500);
});
// Custom subtitle overlay — attached to every player on the page so each
// renders its own overlay independently via getCustomSubtitleDiv().
// hide-native-subtitles suppresses the player's own rendering, so this
// overlay is the only thing displaying subtitle text.
document.querySelectorAll('fastpix-player').forEach((p) => {
p.addEventListener('fastpixsubtitlecue', (e) => {
const { text } = e.detail || {};
const subtitleTracks = p.getSubtitleTracks();
const hasActive = Array.isArray(subtitleTracks) && subtitleTracks.some(t => t.isCurrent);
const overlay = getCustomSubtitleDiv(p);
if (!overlay) return;
if (hasActive && text) {
overlay.textContent = text;
overlay.style.display = 'block';
} else {
overlay.textContent = '';
overlay.style.display = 'none';
}
});
});
// Re-render buttons on every track change — whether triggered by your
// custom buttons, a programmatic call, or the built-in player menu.
player.addEventListener('fastpixaudiochange', (e) => {
const { tracks } = e.detail || {};
renderAudioButtons(tracks || player.getAudioTracks());
});
player.addEventListener('fastpixsubtitlechange', (e) => {
const { tracks } = e.detail || {};
renderSubtitleButtons(tracks || player.getSubtitleTracks());
});
// Render audio track buttons. setAudioTrack() takes a label string — not a
// numeric id. The id field is internal and must not be used for switching.
function renderAudioButtons(tracks) {
if (!tracks || tracks.length === 0) {
audioButtonsContainer.innerHTML = '<em>No audio tracks available</em>';
currentAudioDisplay.textContent = '';
return;
}
audioButtonsContainer.innerHTML = '';
tracks.forEach((track) => {
const btn = document.createElement('button');
btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
btn.textContent = `${track.label} (${track.language || 'unknown'})`;
btn.onclick = () => player.setAudioTrack(track.label);
audioButtonsContainer.appendChild(btn);
});
const current = tracks.find(t => t.isCurrent);
currentAudioDisplay.textContent = current
? `Current: ${current.label} (${current.language || 'unknown'})`
: '';
}
// Render subtitle track buttons. Always includes an Off button first.
// Off is active when no track has isCurrent: true.
function renderSubtitleButtons(tracks) {
subtitleButtonsContainer.innerHTML = '';
const offBtn = document.createElement('button');
offBtn.className = 'track-btn' + (!(tracks || []).some(t => t.isCurrent) ? ' active' : '');
offBtn.textContent = 'Off';
offBtn.onclick = () => {
// disableSubtitles() is preferred; fall back to setSubtitleTrack(null)
// for older player builds.
if (typeof player.disableSubtitles === 'function') {
player.disableSubtitles();
} else {
player.setSubtitleTrack(null);
}
// Clear the overlay immediately — don't wait for the next cue event,
// which may not fire for several seconds after subtitles are turned off.
const overlay = getCustomSubtitleDiv(player);
if (overlay) { overlay.textContent = ''; overlay.style.display = 'none'; }
currentSubtitleDisplay.textContent = 'Current: Off';
};
subtitleButtonsContainer.appendChild(offBtn);
if (!tracks || tracks.length === 0) {
currentSubtitleDisplay.textContent = 'No subtitle tracks available';
return;
}
tracks.forEach((track) => {
const btn = document.createElement('button');
btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
btn.textContent = `${track.label} (${track.language || 'unknown'})`;
btn.onclick = () => player.setSubtitleTrack(track.label);
subtitleButtonsContainer.appendChild(btn);
});
const current = tracks.find(t => t.isCurrent);
currentSubtitleDisplay.textContent = current
? `Current: ${current.label} (${current.language || 'unknown'})`
: 'Current: Off';
}
</script>
</body>
</html>Key points about the example above:
hide-native-subtitlessuppresses the player's built-in subtitle layer so thecustom-subtitleoverlay is the only thing rendering subtitle text. Without this attribute, cue text would appear twice.default-audio-track/default-subtitle-trackaccept 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.fastpixtracksreadyis the correct entry point for all track work. Never callgetAudioTracks()orgetSubtitleTracks()before this event fires.- Subtitle polling after
fastpixtracksreadyis intentional — subtitletextTracksregister slightly later than audio tracks. PollinggetSubtitleTracks()for a short window catches them reliably. setAudioTrack()andsetSubtitleTrack()take a label string, not a numericid. Theidfield in theTrackInfoobject is an internal index and must not be used for switching or persistence.- Off button clearing happens synchronously in
onclick.fastpixsubtitlecuemay 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 Subtitle switching and multi-track audio.
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):
dependencies {
// Check for the latest version
implementation("io.fastpix.player:android:1.0.5")
}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.propertiesto avoid hardcoding secrets in your source files.
repositories {
maven {
url = uri("https://maven.pkg.github.com/FastPix/fastpix-android-player")
credentials {
gpr.user=<your_github_username>
gpr.key=<your_github_pat>
}
}
}Click Sync Now after adding the repository.
2. Add the required imports
import io.fastpix.media3.PlayerView
import io.fastpix.media3.PlaybackListener
import io.fastpix.media3.core.FastPixPlayer
import io.fastpix.media3.core.PlaybackResolution
import io.fastpix.media3.tracks.AudioTrackListener
import io.fastpix.media3.tracks.SubtitleTrackListener
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException3. Set up playback
class VideoPlayerActivity : AppCompatActivity() {
private lateinit var binding: ActivityVideoPlayerBinding
private val playerView get() = binding.playerView
private var player: FastPixPlayer? = null
private lateinit var playbackListener: PlaybackListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityVideoPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
player = FastPixPlayer.Builder(this)
.setAutoplay(true)
.setLoop(false)
.build()
playerView.player = player
}
override fun onStart() {
super.onStart()
startPlayback()
}
private fun startPlayback() {
playbackListener = object : PlaybackListener {
override fun onPlay() {}
override fun onPause() {}
override fun onPlaybackStateChanged(isPlaying: Boolean) {}
override fun onError(error: PlaybackException) {}
}
player?.addPlaybackListener(playbackListener)
player?.setFastPixMediaItem {
playbackId = "YOUR_PLAYBACK_ID"
}
}
override fun onDestroy() {
super.onDestroy()
player?.removePlaybackListener(playbackListener)
if (isFinishing) {
playerView.release()
}
}
}4. Listen for and switch audio tracks
The SDK discovers audio tracks from the HLS manifest and fires callbacks when they're available:
import io.fastpix.media3.tracks.AudioTrackListener
import io.fastpix.media3.tracks.AudioTrackUpdateReason
player?.addAudioTrackListener(object : AudioTrackListener {
override fun onAudioTracksLoaded(tracks: List<AudioTrack>, reason: AudioTrackUpdateReason) {
// Populate your language selection UI
// Each AudioTrack has an id field; use it to switch tracks
}
override fun onAudioTracksChange(selectedTrack: AudioTrack) {
// Update the selected state in your UI
}
override fun onAudioTrackSwitching(isSwitching: Boolean) {
// Optionally show or hide a "switching" indicator
}
override fun onAudioTracksLoadedFailed(error: AudioTrackError) {
// Handle errors such as invalid ID or player not ready
}
})To switch to a specific audio track:
val tracks = player.getAudioTracks()
val target = tracks.firstOrNull() ?: return
player.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
val tracks = player.getAudioTracks()
val items = tracks.filter { it.isPlayable }.map { t ->
val title = t.languageName ?: t.label ?: t.languageCode ?: "Unknown"
title to t.id
}
// Present items in a dialog; on click: player.setAudioTrack(id)To set a preferred default audio track that applies automatically when tracks become available:
player?.setDefaultAudioTrack(languageName = "Spanish")5. Listen for and switch subtitle tracks
import io.fastpix.media3.tracks.SubtitleTrackListener
player?.addSubtitleTrackListener(object : SubtitleTrackListener {
override fun onSubtitlesLoaded(tracks: List<SubtitleTrack>) {
// Populate your subtitle selection UI
}
override fun onSubtitleChange(track: SubtitleTrack?) {
// track is null when subtitles are disabled
}
override fun onSubtitlesLoadedFailed(error: SubtitleTrackError) {
// Handle errors such as track not found or player not ready
}
override fun onSubtitleCueChange(info: SubtitleRenderInfo) {
// Observe cue text and timing if building a custom subtitle renderer
}
})To switch to a specific subtitle track or disable subtitles:
val subs = player?.getSubtitleTracks() ?: return
val target = subs.firstOrNull { it.languageName == "English" }
if (target != null) {
player?.setSubtitleTrack(target.id)
} else {
player?.disableSubtitles()
}To set a preferred default subtitle track:
player?.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:
player?.setFastPixMediaItem {
playbackId = "YOUR_PLAYBACK_ID"
playbackToken = "YOUR_JWT_TOKEN"
}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:
import UIKit
import FastPixPlayerSDK
import AVKit
class VideoPlayerViewController: UIViewController {
var playbackID = "YOUR_PLAYBACK_ID"
lazy var playerViewController = AVPlayerViewController()
override func viewDidLoad() {
super.viewDidLoad()
prepareAvPlayerController()
// Play on-demand video
playerViewController.prepare(
playbackID: playbackID,
playbackOptions: PlaybackOptions(streamType: "on-demand")
)
playerViewController.player?.play()
}
func prepareAvPlayerController() {
addChild(playerViewController)
view.addSubview(playerViewController.view)
playerViewController.didMove(toParent: self)
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
playerViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
playerViewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
playerViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
playerViewController.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
}
}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:
playerViewController.prepare(
playbackID: playbackID,
playbackOptions: PlaybackOptions(playbackToken: "YOUR_JWT_TOKEN")
)The iOS SDK also supports tvOS with the same API surface. See the tvOS installation section 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:
-
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. -
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.
-
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.
-
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
| Platform | SDK / Component | Install method | Track switching |
|---|---|---|---|
| Web | <fastpix-player> | npm or CDN | Automatic (CC button + audio icon) |
| Android | FastPix Android Player SDK | Gradle + GitHub Maven | Automatic (built-in UI controls) |
| iOS / tvOS | FastPix iOS Player SDK | Swift Package Manager | Automatic (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
- Manage subtitle tracks — Full API reference for subtitle track operations.
- Manage audio tracks — Full API reference for audio track operations.
- Subtitle switching and multi-track audio — 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.
- Set up webhooks — Register your endpoint for real-time track processing events.
- Secure playback with JWTs — Protect private media with signed tokens.
Updated about 2 hours ago