Skip to content

Multi-Track Support ​

Movi-Player supports multiple audio, video, and subtitle tracks without any server-side processing.

Overview ​

Many video files contain multiple tracks:

  • Audio: Different languages, commentary, audio descriptions
  • Subtitles: Various languages, captions, forced subs
  • Video: Different quality levels, camera angles

Movi-Player can switch between tracks seamlessly during playback.

Audio Tracks ​

Get Available Audio Tracks ​

typescript
import { MoviPlayer } from "movi-player/player";

const player = new MoviPlayer({
  source: { url: "anime.mkv" },
  canvas: document.getElementById("canvas") as HTMLCanvasElement,
});

await player.load({ url: "anime.mkv" });

const audioTracks = player.getAudioTracks();
console.log("Available audio tracks:", audioTracks);

// Example output:
// [
//   { id: 1, language: 'eng', title: 'English 5.1', codec: 'eac3', channels: 6 },
//   { id: 2, language: 'jpn', title: 'Japanese', codec: 'aac', channels: 2 },
//   { id: 3, language: 'eng', title: 'English Commentary', codec: 'aac', channels: 2 }
// ]

Switch Audio Track ​

typescript
// Switch to Japanese audio
const japaneseTrack = audioTracks.find((t) => t.language === "jpn");
if (japaneseTrack) {
  player.selectAudioTrack(japaneseTrack.id);
}

// Or by index
player.selectAudioTrack(audioTracks[1].id);

Audio Track UI ​

typescript
function setupAudioSelector(player: MoviPlayer) {
  const audioTracks = player.getAudioTracks();
  const selector = document.getElementById("audioSelect") as HTMLSelectElement;

  // Clear existing options
  selector.innerHTML = "";

  // Add options
  audioTracks.forEach((track, index) => {
    const option = document.createElement("option");
    option.value = String(track.id);

    let label = track.language?.toUpperCase() || `Track ${index + 1}`;
    if (track.title) {
      label += ` - ${track.title}`;
    }
    if (track.channels) {
      label += ` (${track.channels === 6 ? "5.1" : track.channels === 8 ? "7.1" : "Stereo"})`;
    }

    option.textContent = label;
    selector.appendChild(option);
  });

  // Handle selection
  selector.onchange = () => {
    const trackId = parseInt(selector.value);
    player.selectAudioTrack(trackId);
  };
}

Subtitle Tracks ​

Get Available Subtitles ​

typescript
const subtitleTracks = player.getSubtitleTracks();
console.log("Available subtitles:", subtitleTracks);

// Example output:
// [
//   { id: 3, language: 'eng', title: 'English', codec: 'subrip', forced: false },
//   { id: 4, language: 'eng', title: 'English (SDH)', codec: 'subrip', forced: false },
//   { id: 5, language: 'jpn', title: 'Japanese', codec: 'ass', forced: false },
//   { id: 6, language: 'eng', title: 'Signs/Songs', codec: 'ass', forced: true }
// ]

Enable/Disable Subtitles ​

typescript
// Enable English subtitles
const englishSub = subtitleTracks.find(
  (t) => t.language === "eng" && !t.forced,
);
if (englishSub) {
  player.selectSubtitleTrack(englishSub.id);
}

// Disable subtitles
player.selectSubtitleTrack(null);

Subtitle Selector UI ​

typescript
function setupSubtitleSelector(player: MoviPlayer) {
  const subtitleTracks = player.getSubtitleTracks();
  const selector = document.getElementById(
    "subtitleSelect",
  ) as HTMLSelectElement;

  // Clear and add "Off" option
  selector.innerHTML = '<option value="">Subtitles Off</option>';

  subtitleTracks.forEach((track, index) => {
    const option = document.createElement("option");
    option.value = String(track.id);

    let label = track.language?.toUpperCase() || `Subtitle ${index + 1}`;
    if (track.title) {
      label += ` - ${track.title}`;
    }
    if (track.forced) {
      label += " (Forced)";
    }

    option.textContent = label;
    selector.appendChild(option);
  });

  selector.onchange = () => {
    const value = selector.value;
    if (value === "") {
      player.selectSubtitleTrack(null);
    } else {
      player.selectSubtitleTrack(parseInt(value));
    }
  };
}

Video Tracks ​

Multiple Video Qualities ​

typescript
const videoTracks = player.getVideoTracks();
console.log("Available video tracks:", videoTracks);

// Example output:
// [
//   { id: 0, width: 1920, height: 1080, codec: 'hevc', bitrate: 8000000 },
//   { id: 1, width: 1280, height: 720, codec: 'hevc', bitrate: 4000000 },
//   { id: 2, width: 854, height: 480, codec: 'hevc', bitrate: 2000000 }
// ]

Switch Video Quality ​

typescript
// Switch to 4K
const track4K = videoTracks.find((t) => t.height >= 2160);
if (track4K) {
  player.selectVideoTrack(track4K.id);
}

// Switch to 720p
const track720p = videoTracks.find((t) => t.height === 720);
if (track720p) {
  player.selectVideoTrack(track720p.id);
}

Quality Selector UI ​

typescript
function setupQualitySelector(player: MoviPlayer) {
  const videoTracks = player.getVideoTracks();
  const selector = document.getElementById(
    "qualitySelect",
  ) as HTMLSelectElement;

  selector.innerHTML = "";

  // Sort by resolution (highest first)
  const sorted = [...videoTracks].sort((a, b) => b.height - a.height);

  sorted.forEach((track) => {
    const option = document.createElement("option");
    option.value = String(track.id);

    let label = `${track.height}p`;
    if (track.height >= 2160) label = "4K";
    else if (track.height >= 1440) label = "1440p QHD";
    else if (track.height >= 1080) label = "1080p HD";
    else if (track.height >= 720) label = "720p HD";
    else if (track.height >= 480) label = "480p SD";
    else label = `${track.height}p`;

    if (track.isHDR) {
      label += " HDR";
    }

    option.textContent = label;
    selector.appendChild(option);
  });

  selector.onchange = () => {
    player.selectVideoTrack(parseInt(selector.value));
  };
}

Complete Track Manager ​

typescript
class TrackManager {
  private player: MoviPlayer;

  constructor(player: MoviPlayer) {
    this.player = player;
  }

  getTrackInfo() {
    return {
      audio: this.player.getAudioTracks(),
      video: this.player.getVideoTracks(),
      subtitle: this.player.getSubtitleTracks(),
    };
  }

  setAudioByLanguage(language: string) {
    const track = this.player
      .getAudioTracks()
      .find((t) => t.language === language);
    if (track) {
      this.player.selectAudioTrack(track.id);
      return true;
    }
    return false;
  }

  setSubtitleByLanguage(language: string | null) {
    if (language === null) {
      this.player.selectSubtitleTrack(null);
      return true;
    }

    const track = this.player
      .getSubtitleTracks()
      .find((t) => t.language === language && !t.forced);
    if (track) {
      this.player.selectSubtitleTrack(track.id);
      return true;
    }
    return false;
  }

  setVideoByQuality(minHeight: number) {
    const track = this.player
      .getVideoTracks()
      .filter((t) => t.height >= minHeight)
      .sort((a, b) => a.height - b.height)[0];

    if (track) {
      this.player.selectVideoTrack(track.id);
      return true;
    }
    return false;
  }

  autoSelectByPreferences(prefs: {
    audioLanguage?: string;
    subtitleLanguage?: string | null;
    maxVideoHeight?: number;
  }) {
    if (prefs.audioLanguage) {
      this.setAudioByLanguage(prefs.audioLanguage);
    }

    if (prefs.subtitleLanguage !== undefined) {
      this.setSubtitleByLanguage(prefs.subtitleLanguage);
    }

    if (prefs.maxVideoHeight) {
      this.setVideoByQuality(prefs.maxVideoHeight);
    }
  }
}

// Usage
const trackManager = new TrackManager(player);

trackManager.autoSelectByPreferences({
  audioLanguage: "jpn",
  subtitleLanguage: "eng",
  maxVideoHeight: 1080,
});

Using with Custom Element ​

html
<movi-player id="player" src="multi-track.mkv" controls></movi-player>

<div class="track-controls">
  <label>
    Audio:
    <select id="audioSelect"></select>
  </label>

  <label>
    Subtitles:
    <select id="subtitleSelect"></select>
  </label>

  <label>
    Quality:
    <select id="qualitySelect"></select>
  </label>
</div>

<script type="module">
  import "movi-player";

  const player = document.getElementById("player");

  player.addEventListener("loadedmetadata", () => {
    // Setup audio selector
    const audioTracks = player.getAudioTracks();
    const audioSelect = document.getElementById("audioSelect");

    audioTracks.forEach((track) => {
      const option = document.createElement("option");
      option.value = track.id;
      option.textContent = `${track.language} - ${track.title || track.codec}`;
      audioSelect.appendChild(option);
    });

    audioSelect.onchange = () => {
      player.selectAudioTrack(parseInt(audioSelect.value));
    };

    // Setup subtitle selector
    const subtitleTracks = player.getSubtitleTracks();
    const subtitleSelect = document.getElementById("subtitleSelect");

    subtitleSelect.innerHTML = '<option value="">Off</option>';

    subtitleTracks.forEach((track) => {
      const option = document.createElement("option");
      option.value = track.id;
      option.textContent = `${track.language} - ${track.title || "Subtitle"}`;
      subtitleSelect.appendChild(option);
    });

    subtitleSelect.onchange = () => {
      const value = subtitleSelect.value;
      player.selectSubtitleTrack(value ? parseInt(value) : null);
    };
  });
</script>

Supported Codecs ​

Audio Codecs ​

CodecFormatNotes
aacAACMost common
mp3MP3Legacy
opusOpusModern
flacFLACLossless
ac3Dolby AC-3Surround
eac3Dolby E-AC-3Enhanced Surround
dtsDTSSurround
vorbisVorbisWebM

Subtitle Formats ​

FormatExtensionFeatures
SRT.srtSimple text
ASS/SSA.ass, .ssaStyled, positioned
WebVTT.vttWeb standard
PGS.supBlu-ray image subs
VobSub.sub, .idxDVD image subs

No Conversion Needed

All tracks are processed in the browser. No need to extract or convert tracks on the server!

Released under the Apache-2.0 License.