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

Part 2: Playback

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:

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

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:

{
  "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'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.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"
  }
}
💾

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

<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

<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-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 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.properties to 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.PlaybackException

3. 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:

  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