diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md
index d2be7789ad7..24aa184abd6 100644
--- a/api-extractor/report/hls.js.api.md
+++ b/api-extractor/report/hls.js.api.md
@@ -129,6 +129,7 @@ export type AudioSelectionOption = {
     name?: string;
     audioCodec?: string;
     groupId?: string;
+    default?: boolean;
 };
 
 // Warning: (ae-missing-release-tag) "AudioStreamController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -3164,6 +3165,8 @@ export type SubtitleSelectionOption = {
     characteristics?: string;
     name?: string;
     groupId?: string;
+    default?: boolean;
+    forced?: boolean;
 };
 
 // Warning: (ae-missing-release-tag) "SubtitleStreamController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
diff --git a/src/controller/audio-track-controller.ts b/src/controller/audio-track-controller.ts
index bfb876842e4..2cb4b65f416 100644
--- a/src/controller/audio-track-controller.ts
+++ b/src/controller/audio-track-controller.ts
@@ -157,17 +157,31 @@ class AudioTrackController extends BasePlaylistController {
         // Do not dispatch AUDIO_TRACKS_UPDATED when there were and are no tracks
         return;
       }
+      this.tracksInGroup = audioTracks;
 
-      if (!currentTrack) {
-        currentTrack = this.setAudioOption(this.hls.config.audioPreference);
+      // Find preferred track
+      const audioPreference = this.hls.config.audioPreference;
+      if (!currentTrack && audioPreference) {
+        const groupIndex = findMatchingOption(
+          audioPreference,
+          audioTracks,
+          audioMatchPredicate,
+        );
+        if (groupIndex > -1) {
+          currentTrack = audioTracks[groupIndex];
+        } else {
+          const allIndex = findMatchingOption(audioPreference, this.tracks);
+          currentTrack = this.tracks[allIndex];
+        }
       }
 
-      this.tracksInGroup = audioTracks;
+      // Select initial track
       let trackId = this.findTrackId(currentTrack);
       if (trackId === -1 && currentTrack) {
         trackId = this.findTrackId(null);
       }
 
+      // Dispatch events and load track if needed
       const audioTracksUpdated: AudioTracksUpdatedData = { audioTracks };
       this.log(
         `Updating audio tracks, ${
diff --git a/src/controller/subtitle-track-controller.ts b/src/controller/subtitle-track-controller.ts
index 24b8f03540d..75e28294a9b 100644
--- a/src/controller/subtitle-track-controller.ts
+++ b/src/controller/subtitle-track-controller.ts
@@ -35,7 +35,6 @@ class SubtitleTrackController extends BasePlaylistController {
   private currentTrack: MediaPlaylist | null = null;
   private selectDefaultTrack: boolean = true;
   private queuedDefaultTrack: number = -1;
-  private trackChangeListener: () => void = () => this.onTextTracksChanged();
   private asyncPollTrackChange: () => void = () => this.pollTrackChange(0);
   private useTextTrackPolling: boolean = false;
   private subtitlePollingInterval: number = -1;
@@ -51,7 +50,7 @@ class SubtitleTrackController extends BasePlaylistController {
     this.tracks.length = 0;
     this.tracksInGroup.length = 0;
     this.currentTrack = null;
-    this.trackChangeListener = this.asyncPollTrackChange = null as any;
+    this.onTextTracksChanged = this.asyncPollTrackChange = null as any;
     super.destroy();
   }
 
@@ -121,7 +120,7 @@ class SubtitleTrackController extends BasePlaylistController {
   private pollTrackChange(timeout: number) {
     self.clearInterval(this.subtitlePollingInterval);
     this.subtitlePollingInterval = self.setInterval(
-      this.trackChangeListener,
+      this.onTextTracksChanged,
       timeout,
     );
   }
@@ -231,12 +230,10 @@ class SubtitleTrackController extends BasePlaylistController {
           !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1,
       );
       if (subtitleTracks.length) {
-        // Disable selectDefaultTrack if there are no default or forced tracks
+        // Disable selectDefaultTrack if there are no default tracks
         if (
           this.selectDefaultTrack &&
-          !subtitleTracks.some(
-            (track) => track.default || track.forced || track.autoselect,
-          )
+          !subtitleTracks.some((track) => track.default)
         ) {
           this.selectDefaultTrack = false;
         }
@@ -248,19 +245,31 @@ class SubtitleTrackController extends BasePlaylistController {
         // Do not dispatch SUBTITLE_TRACKS_UPDATED when there were and are no tracks
         return;
       }
+      this.tracksInGroup = subtitleTracks;
 
-      if (!currentTrack) {
-        currentTrack = this.setSubtitleOption(
-          this.hls.config.subtitlePreference,
+      // Find preferred track
+      const subtitlePreference = this.hls.config.subtitlePreference;
+      if (!currentTrack && subtitlePreference) {
+        this.selectDefaultTrack = false;
+        const groupIndex = findMatchingOption(
+          subtitlePreference,
+          subtitleTracks,
         );
+        if (groupIndex > -1) {
+          currentTrack = subtitleTracks[groupIndex];
+        } else {
+          const allIndex = findMatchingOption(subtitlePreference, this.tracks);
+          currentTrack = this.tracks[allIndex];
+        }
       }
 
-      this.tracksInGroup = subtitleTracks;
+      // Select initial track
       let trackId = this.findTrackId(currentTrack);
       if (trackId === -1 && currentTrack) {
         trackId = this.findTrackId(null);
       }
 
+      // Dispatch events and load track if needed
       const subtitleTracksUpdated: SubtitleTracksUpdatedData = {
         subtitleTracks,
       };
@@ -286,10 +295,7 @@ class SubtitleTrackController extends BasePlaylistController {
     for (let i = 0; i < tracks.length; i++) {
       const track = tracks[i];
       if (
-        (selectDefault &&
-          !track.default &&
-          !track.forced &&
-          !track.autoselect) ||
+        (selectDefault && !track.default) ||
         (!selectDefault && !currentTrack)
       ) {
         continue;
@@ -492,7 +498,7 @@ class SubtitleTrackController extends BasePlaylistController {
     }
 
     // exit if track id as already set or invalid
-    if (newId < -1 || newId >= tracks.length) {
+    if (newId < -1 || newId >= tracks.length || !Number.isFinite(newId)) {
       this.warn(`Invalid subtitle track id: ${newId}`);
       return;
     }
@@ -511,7 +517,7 @@ class SubtitleTrackController extends BasePlaylistController {
       this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { id: newId });
       return;
     }
-    const trackLoaded = track.details && !track.details.live;
+    const trackLoaded = !!track.details && !track.details.live;
     if (newId === this.trackId && track === lastTrack && trackLoaded) {
       return;
     }
@@ -533,7 +539,7 @@ class SubtitleTrackController extends BasePlaylistController {
     this.loadPlaylist(hlsUrlParameters);
   }
 
-  private onTextTracksChanged(): void {
+  private onTextTracksChanged = () => {
     if (!this.useTextTrackPolling) {
       self.clearInterval(this.subtitlePollingInterval);
     }
@@ -559,7 +565,7 @@ class SubtitleTrackController extends BasePlaylistController {
     if (this.subtitleTrack !== trackId) {
       this.setSubtitleTrack(trackId);
     }
-  }
+  };
 }
 
 export default SubtitleTrackController;
diff --git a/src/types/media-playlist.ts b/src/types/media-playlist.ts
index 5bb510ac3c7..33e3e2ebe05 100644
--- a/src/types/media-playlist.ts
+++ b/src/types/media-playlist.ts
@@ -23,6 +23,7 @@ export type AudioSelectionOption = {
   name?: string;
   audioCodec?: string;
   groupId?: string;
+  default?: boolean;
 };
 
 export type SubtitleSelectionOption = {
@@ -31,6 +32,8 @@ export type SubtitleSelectionOption = {
   characteristics?: string;
   name?: string;
   groupId?: string;
+  default?: boolean;
+  forced?: boolean;
 };
 
 // audioTracks, captions and subtitles returned by `M3U8Parser.parseMasterPlaylistMedia`
diff --git a/src/utils/rendition-helper.ts b/src/utils/rendition-helper.ts
index 8f0fdbe213c..1e733489bb6 100644
--- a/src/utils/rendition-helper.ts
+++ b/src/utils/rendition-helper.ts
@@ -323,12 +323,22 @@ export function matchesOption(
     track: MediaPlaylist,
   ) => boolean,
 ): boolean {
-  const { groupId, name, lang, assocLang, characteristics } = option;
+  const {
+    groupId,
+    name,
+    lang,
+    assocLang,
+    characteristics,
+    default: isDefault,
+  } = option;
+  const forced = (option as SubtitleSelectionOption).forced;
   return (
     (groupId === undefined || track.groupId === groupId) &&
     (name === undefined || track.name === name) &&
     (lang === undefined || track.lang === lang) &&
     (lang === undefined || track.assocLang === assocLang) &&
+    (isDefault === undefined || track.default === isDefault) &&
+    (forced === undefined || track.forced === forced) &&
     (characteristics === undefined ||
       characteristicsMatch(characteristics, track.characteristics)) &&
     (matchPredicate === undefined || matchPredicate(option, track))
diff --git a/tests/unit/controller/subtitle-track-controller.js b/tests/unit/controller/subtitle-track-controller.js
deleted file mode 100644
index 92afdd3731d..00000000000
--- a/tests/unit/controller/subtitle-track-controller.js
+++ /dev/null
@@ -1,372 +0,0 @@
-import SubtitleTrackController from '../../../src/controller/subtitle-track-controller';
-import Hls from '../../../src/hls';
-import sinon from 'sinon';
-import { LoadStats } from '../../../src/loader/load-stats';
-import { LevelDetails } from '../../../src/loader/level-details';
-import { Events } from '../../../src/events';
-
-describe('SubtitleTrackController', function () {
-  let subtitleTrackController;
-  let videoElement;
-  let sandbox;
-
-  beforeEach(function () {
-    const hls = new Hls({
-      renderNatively: true,
-    });
-
-    videoElement = document.createElement('video');
-    subtitleTrackController = new SubtitleTrackController(hls);
-    subtitleTrackController.media = videoElement;
-    subtitleTrackController.tracks = subtitleTrackController.tracksInGroup = [
-      {
-        id: 0,
-        groupId: 'default-text-group',
-        lang: 'en',
-        name: 'English',
-        type: 'SUBTITLES',
-        url: 'baz',
-        details: { live: false },
-      },
-      {
-        id: 1,
-        groupId: 'default-text-group',
-        lang: 'sv',
-        name: 'Swedish',
-        type: 'SUBTITLES',
-        url: 'bar',
-      },
-      {
-        id: 2,
-        groupId: 'default-text-group',
-        lang: 'en',
-        name: 'Untitled CC',
-        type: 'SUBTITLES',
-        url: 'foo',
-        details: { live: true },
-      },
-    ];
-
-    const textTrack1 = videoElement.addTextTrack('subtitles', 'English', 'en');
-    const textTrack2 = videoElement.addTextTrack('subtitles', 'Swedish', 'sv');
-    const textTrack3 = videoElement.addTextTrack(
-      'captions',
-      'Untitled CC',
-      'en',
-    );
-
-    textTrack1.groupId = 'default-text-group';
-    textTrack2.groupId = 'default-text-group';
-    textTrack3.groupId = 'default-text-group';
-    subtitleTrackController.groupId = 'default-text-group';
-
-    textTrack1.mode = 'disabled';
-    textTrack2.mode = 'disabled';
-    textTrack3.mode = 'disabled';
-    sandbox = sinon.createSandbox();
-  });
-
-  afterEach(function () {
-    sandbox.restore();
-  });
-
-  describe('onTextTrackChanged', function () {
-    it('should set subtitleTrack to -1 if disabled', function () {
-      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
-
-      videoElement.textTracks[0].mode = 'disabled';
-      subtitleTrackController.onTextTracksChanged();
-
-      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
-    });
-
-    it('should set subtitleTrack to 0 if hidden', function () {
-      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
-
-      videoElement.textTracks[0].mode = 'hidden';
-      subtitleTrackController.onTextTracksChanged();
-
-      expect(subtitleTrackController.subtitleTrack).to.equal(0);
-    });
-
-    it('should set subtitleTrack to 0 if showing', function () {
-      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
-
-      videoElement.textTracks[0].mode = 'showing';
-      subtitleTrackController.onTextTracksChanged();
-
-      expect(subtitleTrackController.subtitleTrack).to.equal(0);
-    });
-
-    it('should set subtitleTrack id captions track is showing', function () {
-      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
-
-      videoElement.textTracks[2].mode = 'showing';
-      subtitleTrackController.onTextTracksChanged();
-
-      expect(videoElement.textTracks[2].kind).to.equal('captions');
-      expect(subtitleTrackController.subtitleTrack).to.equal(2);
-    });
-  });
-
-  describe('set subtitleTrack', function () {
-    it('should set active text track mode to showing', function () {
-      videoElement.textTracks[0].mode = 'disabled';
-
-      subtitleTrackController.subtitleDisplay = true;
-      subtitleTrackController.subtitleTrack = 0;
-
-      expect(videoElement.textTracks[0].mode).to.equal('showing');
-    });
-
-    it('should set active text track mode to hidden', function () {
-      videoElement.textTracks[0].mode = 'disabled';
-      subtitleTrackController.subtitleDisplay = false;
-      subtitleTrackController.subtitleTrack = 0;
-
-      expect(videoElement.textTracks[0].mode).to.equal('hidden');
-    });
-
-    it('should disable previous track', function () {
-      // Change active track without triggering setSubtitleTrackInternal
-      subtitleTrackController.trackId = 0;
-      // Change active track and trigger setSubtitleTrackInternal
-      subtitleTrackController.subtitleTrack = 1;
-
-      expect(videoElement.textTracks[0].mode).to.equal('disabled');
-    });
-
-    it('should trigger SUBTITLE_TRACK_SWITCH', function () {
-      const triggerSpy = sandbox.spy(subtitleTrackController.hls, 'trigger');
-      subtitleTrackController.canLoad = true;
-      subtitleTrackController.trackId = 0;
-      subtitleTrackController.subtitleTrack = 1;
-
-      expect(triggerSpy).to.have.been.calledTwice;
-      expect(triggerSpy.firstCall).to.have.been.calledWith(
-        'hlsSubtitleTrackSwitch',
-        {
-          id: 1,
-          groupId: 'default-text-group',
-          name: 'Swedish',
-          type: 'SUBTITLES',
-          url: 'bar',
-        },
-      );
-    });
-
-    it('should trigger SUBTITLE_TRACK_LOADING if the track has no details', function () {
-      const triggerSpy = sandbox.spy(subtitleTrackController.hls, 'trigger');
-      subtitleTrackController.canLoad = true;
-      subtitleTrackController.trackId = 0;
-      subtitleTrackController.subtitleTrack = 1;
-
-      expect(triggerSpy).to.have.been.calledTwice;
-      expect(triggerSpy.secondCall).to.have.been.calledWith(
-        'hlsSubtitleTrackLoading',
-        {
-          url: 'bar',
-          id: 1,
-          groupId: 'default-text-group',
-          deliveryDirectives: null,
-        },
-      );
-    });
-
-    it('should not trigger SUBTITLE_TRACK_LOADING if the track has details and is not live', function () {
-      const triggerSpy = sandbox.spy(subtitleTrackController.hls, 'trigger');
-      subtitleTrackController.trackId = 1;
-      subtitleTrackController.subtitleTrack = 0;
-
-      expect(triggerSpy).to.have.been.calledOnce;
-      expect(triggerSpy.firstCall).to.have.been.calledWith(
-        'hlsSubtitleTrackSwitch',
-        {
-          id: 0,
-          groupId: 'default-text-group',
-          name: 'English',
-          type: 'SUBTITLES',
-          url: 'baz',
-        },
-      );
-    });
-
-    it('should trigger SUBTITLE_TRACK_SWITCH if passed -1', function () {
-      const triggerSpy = sandbox.spy(subtitleTrackController.hls, 'trigger');
-      subtitleTrackController.trackId = 0;
-      subtitleTrackController.subtitleTrack = -1;
-
-      expect(triggerSpy.firstCall).to.have.been.calledWith(
-        'hlsSubtitleTrackSwitch',
-        { id: -1 },
-      );
-    });
-
-    it('should trigger SUBTITLE_TRACK_LOADING if the track is live, even if it has details', function () {
-      const triggerSpy = sandbox.spy(subtitleTrackController.hls, 'trigger');
-      subtitleTrackController.canLoad = true;
-      subtitleTrackController.trackId = 0;
-      subtitleTrackController.subtitleTrack = 2;
-
-      expect(triggerSpy).to.have.been.calledTwice;
-      expect(triggerSpy.secondCall).to.have.been.calledWith(
-        'hlsSubtitleTrackLoading',
-        {
-          url: 'foo',
-          id: 2,
-          groupId: 'default-text-group',
-          deliveryDirectives: null,
-        },
-      );
-    });
-
-    it('should do nothing if called with out of bound indices', function () {
-      const clearReloadSpy = sandbox.spy(subtitleTrackController, 'clearTimer');
-      subtitleTrackController.subtitleTrack = 5;
-      subtitleTrackController.subtitleTrack = -2;
-
-      expect(clearReloadSpy).to.have.not.been.called;
-    });
-
-    it('should do nothing if called with a non-number', function () {
-      subtitleTrackController.subtitleTrack = undefined;
-      subtitleTrackController.subtitleTrack = null;
-    });
-
-    describe('toggleTrackModes', function () {
-      // This can be the case when setting the subtitleTrack before Hls.js attaches to the mediaElement
-      it('should not throw an exception if trackId is out of the mediaElement text track bounds', function () {
-        subtitleTrackController.trackId = 3;
-        subtitleTrackController.toggleTrackModes(1);
-      });
-
-      it('should disable all textTracks if called with -1', function () {
-        [].slice.call(videoElement.textTracks).forEach((t) => {
-          t.mode = 'showing';
-        });
-        subtitleTrackController.toggleTrackModes(-1);
-        [].slice.call(videoElement.textTracks).forEach((t) => {
-          expect(t.mode).to.equal('disabled');
-        });
-      });
-
-      it('should not throw an exception if the mediaElement does not exist', function () {
-        subtitleTrackController.media = null;
-        subtitleTrackController.toggleTrackModes(1);
-      });
-    });
-
-    describe('onSubtitleTrackLoaded', function () {
-      it('exits early if the loaded track does not match the requested track', function () {
-        const playlistLoadedSpy = sandbox.spy(
-          subtitleTrackController,
-          'playlistLoaded',
-        );
-        subtitleTrackController.canLoad = true;
-        subtitleTrackController.trackId = 1;
-
-        const mockLoadedEvent = {
-          id: 999,
-          groupId: 'default-text-group',
-          details: { foo: 'bar' },
-          stats: new LoadStats(),
-        };
-        subtitleTrackController.onSubtitleTrackLoaded(
-          Events.SUBTITLE_TRACK_LOADED,
-          mockLoadedEvent,
-        );
-        expect(subtitleTrackController.timer).to.equal(-1);
-        expect(playlistLoadedSpy).to.have.not.been.called;
-
-        mockLoadedEvent.id = 0;
-        subtitleTrackController.onSubtitleTrackLoaded(
-          Events.SUBTITLE_TRACK_LOADED,
-          mockLoadedEvent,
-        );
-        expect(subtitleTrackController.timer).to.equal(-1);
-        expect(playlistLoadedSpy).to.have.not.been.called;
-
-        mockLoadedEvent.id = 1;
-        subtitleTrackController.onSubtitleTrackLoaded(
-          Events.SUBTITLE_TRACK_LOADED,
-          mockLoadedEvent,
-        );
-        expect(subtitleTrackController.timer).to.equal(-1);
-        expect(playlistLoadedSpy).to.have.been.calledOnce;
-      });
-
-      it('does not set the reload timer if the canLoad flag is set to false', function () {
-        const details = new LevelDetails('');
-        subtitleTrackController.canLoad = false;
-        subtitleTrackController.trackId = 1;
-        subtitleTrackController.onSubtitleTrackLoaded(
-          Events.SUBTITLE_TRACK_LOADED,
-          {
-            id: 1,
-            groupId: 'default-text-group',
-            details,
-            stats: new LoadStats(),
-          },
-        );
-        expect(subtitleTrackController.timer).to.equal(-1);
-      });
-
-      it('sets the live reload timer if the level is live', function () {
-        const details = new LevelDetails('');
-        subtitleTrackController.canLoad = true;
-        subtitleTrackController.trackId = 1;
-        subtitleTrackController.onSubtitleTrackLoaded(
-          Events.SUBTITLE_TRACK_LOADED,
-          {
-            id: 1,
-            groupId: 'default-text-group',
-            details,
-            stats: new LoadStats(),
-          },
-        );
-        expect(subtitleTrackController.timer).to.exist;
-      });
-
-      it('stops the live reload timer if the level is not live', function () {
-        const details = new LevelDetails('');
-        details.live = false;
-        subtitleTrackController.trackId = 1;
-        subtitleTrackController.timer = self.setTimeout(() => {}, 0);
-        subtitleTrackController.onSubtitleTrackLoaded(
-          Events.SUBTITLE_TRACK_LOADED,
-          {
-            id: 1,
-            groupId: 'default-text-group',
-            details,
-            stats: new LoadStats(),
-          },
-        );
-        expect(subtitleTrackController.timer).to.equal(-1);
-      });
-    });
-
-    describe('stopLoad', function () {
-      it('stops loading', function () {
-        const clearReloadSpy = sandbox.spy(
-          subtitleTrackController,
-          'clearTimer',
-        );
-        subtitleTrackController.stopLoad();
-        expect(subtitleTrackController.canLoad).to.be.false;
-        expect(clearReloadSpy).to.have.been.calledOnce;
-      });
-    });
-
-    describe('startLoad', function () {
-      it('starts loading', function () {
-        const loadCurrentTrackSpy = sandbox.spy(
-          subtitleTrackController,
-          'loadPlaylist',
-        );
-        subtitleTrackController.startLoad();
-        expect(subtitleTrackController.canLoad).to.be.true;
-        expect(loadCurrentTrackSpy).to.have.been.calledOnce;
-      });
-    });
-  });
-});
diff --git a/tests/unit/controller/subtitle-track-controller.ts b/tests/unit/controller/subtitle-track-controller.ts
new file mode 100644
index 00000000000..4dafd12832a
--- /dev/null
+++ b/tests/unit/controller/subtitle-track-controller.ts
@@ -0,0 +1,579 @@
+import SubtitleTrackController from '../../../src/controller/subtitle-track-controller';
+import Hls from '../../../src/hls';
+import { LoadStats } from '../../../src/loader/load-stats';
+import { LevelDetails } from '../../../src/loader/level-details';
+import { Events } from '../../../src/events';
+import { AttrList } from '../../../src/utils/attr-list';
+import type {
+  MediaAttributes,
+  MediaPlaylist,
+} from '../../../src/types/media-playlist';
+import type { Level } from '../../../src/types/level';
+import type {
+  ComponentAPI,
+  NetworkComponentAPI,
+} from '../../../src/types/component-api';
+
+import sinon from 'sinon';
+import chai from 'chai';
+import sinonChai from 'sinon-chai';
+
+chai.use(sinonChai);
+const expect = chai.expect;
+
+type HlsTestable = Omit<
+  Hls,
+  'levelController' | 'networkControllers' | 'coreComponents'
+> & {
+  levelController: {
+    levels: Pick<Level, 'subtitleGroups'>[];
+  };
+  coreComponents: ComponentAPI[];
+  networkControllers: NetworkComponentAPI[];
+};
+
+describe('SubtitleTrackController', function () {
+  let hls: HlsTestable;
+  let subtitleTrackController: SubtitleTrackController;
+  let subtitleTracks: MediaPlaylist[];
+  let switchLevel: () => void;
+  let videoElement;
+  let sandbox;
+
+  beforeEach(function () {
+    hls = new Hls() as unknown as HlsTestable;
+    hls.networkControllers.forEach((component) => component.destroy());
+    hls.networkControllers.length = 0;
+    hls.coreComponents.forEach((component) => component.destroy());
+    hls.coreComponents.length = 0;
+    subtitleTrackController = new SubtitleTrackController(
+      hls as unknown as Hls,
+    );
+    hls.networkControllers.push(subtitleTrackController);
+    hls.levelController = {
+      levels: [
+        {
+          subtitleGroups: ['default-text-group'],
+        },
+      ],
+    };
+
+    videoElement = document.createElement('video');
+    hls.trigger(Events.MEDIA_ATTACHED, { media: videoElement });
+
+    subtitleTracks = [
+      {
+        attrs: new AttrList({}) as MediaAttributes,
+        autoselect: true,
+        bitrate: 0,
+        default: false,
+        forced: false,
+        id: 0,
+        groupId: 'default-text-group',
+        lang: 'en-US',
+        name: 'English',
+        type: 'SUBTITLES',
+        url: 'baz',
+        // details: { live: false },
+      },
+      {
+        attrs: new AttrList({}) as MediaAttributes,
+        autoselect: true,
+        bitrate: 0,
+        default: false,
+        forced: false,
+        id: 1,
+        groupId: 'default-text-group',
+        lang: 'sv',
+        name: 'Swedish',
+        type: 'SUBTITLES',
+        url: 'bar',
+      },
+      {
+        attrs: new AttrList({}) as MediaAttributes,
+        autoselect: true,
+        bitrate: 0,
+        default: false,
+        forced: false,
+        id: 2,
+        groupId: 'default-text-group',
+        lang: 'en-US',
+        name: 'Untitled CC',
+        type: 'SUBTITLES',
+        url: 'foo',
+        // details: { live: true },
+      },
+    ];
+    const levels = [
+      {
+        subtitleGroups: ['default-text-group'],
+      },
+    ] as any;
+    hls.trigger(Events.MANIFEST_PARSED, {
+      subtitleTracks,
+      levels,
+      audioTracks: [],
+      sessionData: null,
+      sessionKeys: null,
+      firstLevel: 0,
+      stats: new LoadStats(),
+      audio: true,
+      video: true,
+      altAudio: true,
+    });
+
+    switchLevel = () => {
+      hls.trigger(Events.LEVEL_LOADING, {
+        id: 0,
+        level: 0,
+        pathwayId: undefined,
+        url: '',
+        deliveryDirectives: null,
+      });
+    };
+
+    const textTrack1 = videoElement.addTextTrack(
+      'subtitles',
+      'English',
+      'en-US',
+    );
+    const textTrack2 = videoElement.addTextTrack('subtitles', 'Swedish', 'sv');
+    const textTrack3 = videoElement.addTextTrack(
+      'captions',
+      'Untitled CC',
+      'en-US',
+    );
+
+    textTrack1.groupId = 'default-text-group';
+    textTrack2.groupId = 'default-text-group';
+    textTrack3.groupId = 'default-text-group';
+
+    textTrack1.mode = 'disabled';
+    textTrack2.mode = 'disabled';
+    textTrack3.mode = 'disabled';
+    sandbox = sinon.createSandbox();
+  });
+
+  afterEach(function () {
+    sandbox.restore();
+  });
+
+  describe('onTextTracksChanged', function () {
+    beforeEach(function () {
+      switchLevel();
+    });
+    it('should set subtitleTrack to -1 if disabled', function () {
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+
+      const onTextTracksChanged = sinon.spy(
+        subtitleTrackController,
+        'onTextTracksChanged' as any,
+      );
+
+      videoElement.textTracks[0].mode = 'showing';
+
+      return new Promise((resolve) => {
+        self.setTimeout(() => {
+          expect(subtitleTrackController.subtitleTrack).to.equal(0);
+          expect(onTextTracksChanged).to.have.been.calledOnce;
+          videoElement.textTracks[0].mode = 'disabled';
+          self.setTimeout(() => {
+            expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+            expect(onTextTracksChanged).to.have.been.calledTwice;
+            resolve(true);
+          }, 500);
+        }, 500);
+      });
+    });
+
+    it('should set subtitleTrack to 0 if hidden', function () {
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+
+      videoElement.textTracks[0].mode = 'hidden';
+
+      return new Promise((resolve) => {
+        hls.on(Events.SUBTITLE_TRACK_SWITCH, () => {
+          expect(subtitleTrackController.subtitleTrack).to.equal(0);
+          resolve(true);
+        });
+      });
+    });
+
+    it('should set subtitleTrack to 0 if showing', function () {
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+
+      videoElement.textTracks[0].mode = 'showing';
+
+      return new Promise((resolve) => {
+        hls.on(Events.SUBTITLE_TRACK_SWITCH, () => {
+          expect(subtitleTrackController.subtitleTrack).to.equal(0);
+          resolve(true);
+        });
+      });
+    });
+
+    it('should set subtitleTrack id captions track is showing', function () {
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+
+      videoElement.textTracks[2].mode = 'showing';
+
+      return new Promise((resolve) => {
+        hls.on(Events.SUBTITLE_TRACK_SWITCH, () => {
+          expect(videoElement.textTracks[2].kind).to.equal('captions');
+          expect(subtitleTrackController.subtitleTrack).to.equal(2);
+          resolve(true);
+        });
+      });
+    });
+  });
+
+  describe('initial track selection', function () {
+    it('should not select any tracks if there are no default of forces tracks (ignoring autoselect)', function () {
+      switchLevel();
+      expect(subtitleTracks[0].autoselect).to.equal(true);
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+    });
+
+    it('should not select forced tracks', function () {
+      subtitleTracks[1].forced = true;
+      switchLevel();
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+    });
+
+    it('should select the default track when there are no forced tracks', function () {
+      subtitleTracks[2].default = true;
+      switchLevel();
+      expect(subtitleTrackController.subtitleTrack).to.equal(2);
+    });
+
+    it('should select the first default track when there are no forced tracks', function () {
+      subtitleTracks[0].default = true;
+      subtitleTracks[1].default = true;
+      subtitleTracks[2].default = true;
+      switchLevel();
+      expect(subtitleTrackController.subtitleTrack).to.equal(0);
+    });
+
+    it('should not select forced tracks over the default tracks (one forced track)', function () {
+      subtitleTracks[1].default = true;
+      subtitleTracks[2].forced = true;
+      switchLevel();
+      expect(subtitleTrackController.subtitleTrack).to.equal(1);
+    });
+
+    it('should not select forced tracks over the default tracks (two forced track)', function () {
+      subtitleTracks[0].forced = true;
+      subtitleTracks[1].forced = true;
+      subtitleTracks[2].default = true;
+      switchLevel();
+      expect(subtitleTrackController.subtitleTrack).to.equal(2);
+    });
+
+    describe('with subtitlePreference', function () {
+      it('should select the first track with matching lang', function () {
+        hls.config.subtitlePreference = {
+          lang: 'en-US',
+        };
+        subtitleTracks[2].default = true;
+        switchLevel();
+        expect(subtitleTrackController.subtitleTrack).to.equal(0);
+      });
+      it('should select the first track with matching properties', function () {
+        hls.config.subtitlePreference = {
+          lang: 'en-US',
+          default: true,
+        };
+        subtitleTracks[2].default = true;
+        switchLevel();
+        expect(subtitleTrackController.subtitleTrack).to.equal(2);
+      });
+      it('should not select default track if an unmatched preference is present', function () {
+        hls.config.subtitlePreference = {
+          lang: 'none',
+        };
+        subtitleTracks[2].default = true;
+        switchLevel();
+        expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+      });
+    });
+  });
+
+  describe('set subtitleTrack', function () {
+    beforeEach(function () {
+      switchLevel();
+    });
+    it('should set active text track mode to showing', function () {
+      videoElement.textTracks[0].mode = 'disabled';
+
+      subtitleTrackController.subtitleDisplay = true;
+      subtitleTrackController.subtitleTrack = 0;
+
+      expect(videoElement.textTracks[0].mode).to.equal('showing');
+    });
+
+    it('should set active text track mode to hidden', function () {
+      videoElement.textTracks[0].mode = 'disabled';
+      subtitleTrackController.subtitleDisplay = false;
+      subtitleTrackController.subtitleTrack = 0;
+
+      expect(videoElement.textTracks[0].mode).to.equal('hidden');
+    });
+
+    it('should disable previous track', function () {
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+
+      const onTextTracksChanged = sinon.spy(
+        subtitleTrackController,
+        'onTextTracksChanged' as any,
+      );
+
+      videoElement.textTracks[0].mode = 'showing';
+
+      return new Promise((resolve) => {
+        self.setTimeout(() => {
+          expect(subtitleTrackController.subtitleTrack).to.equal(0);
+          expect(videoElement.textTracks[0].mode).to.equal('showing');
+          expect(onTextTracksChanged).to.have.been.calledOnce;
+          subtitleTrackController.subtitleTrack = 1;
+          self.setTimeout(() => {
+            expect(videoElement.textTracks[0].mode).to.equal('disabled');
+            expect(videoElement.textTracks[1].mode).to.equal('showing');
+            expect(onTextTracksChanged).to.have.been.calledTwice;
+            resolve(true);
+          }, 500);
+        }, 500);
+      });
+    });
+
+    it('should disable all textTracks when set to -1', function () {
+      [].slice.call(videoElement.textTracks).forEach((t) => {
+        t.mode = 'showing';
+      });
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+      subtitleTrackController.subtitleTrack = -1;
+      [].slice.call(videoElement.textTracks).forEach((t) => {
+        expect(t.mode).to.equal('disabled');
+      });
+    });
+
+    it('should trigger SUBTITLE_TRACK_SWITCH', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTrackController.startLoad();
+      subtitleTrackController.subtitleTrack = 1;
+
+      expect(triggerSpy).to.have.been.calledTwice;
+      expect(triggerSpy.firstCall).to.have.been.calledWith(
+        'hlsSubtitleTrackSwitch',
+        {
+          id: 1,
+          groupId: 'default-text-group',
+          name: 'Swedish',
+          type: 'SUBTITLES',
+          url: 'bar',
+        },
+      );
+    });
+
+    it('should trigger SUBTITLE_TRACK_LOADING if the track has no details', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTrackController.startLoad();
+      subtitleTrackController.subtitleTrack = 1;
+
+      expect(triggerSpy).to.have.been.calledTwice;
+      expect(triggerSpy.secondCall).to.have.been.calledWith(
+        'hlsSubtitleTrackLoading',
+        {
+          url: 'bar',
+          id: 1,
+          groupId: 'default-text-group',
+          deliveryDirectives: null,
+        },
+      );
+    });
+
+    it('should not trigger SUBTITLE_TRACK_LOADING if the track has details and is not live', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTracks[0].details = { live: false } as any;
+      subtitleTrackController.startLoad();
+      subtitleTrackController.subtitleTrack = 0;
+
+      expect(triggerSpy).to.have.been.calledOnce;
+      expect(triggerSpy.firstCall).to.have.been.calledWith(
+        'hlsSubtitleTrackSwitch',
+        {
+          id: 0,
+          groupId: 'default-text-group',
+          name: 'English',
+          type: 'SUBTITLES',
+          url: 'baz',
+        },
+      );
+    });
+
+    it('should trigger SUBTITLE_TRACK_SWITCH if passed -1', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTrackController.subtitleTrack = -1;
+      expect(triggerSpy.firstCall).to.have.been.calledWith(
+        'hlsSubtitleTrackSwitch',
+        { id: -1 },
+      );
+    });
+
+    it('should trigger SUBTITLE_TRACK_LOADING if the track is live, even if it has details', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTracks[2].details = { live: true } as any;
+      subtitleTrackController.startLoad();
+      subtitleTrackController.subtitleTrack = 2;
+
+      expect(triggerSpy).to.have.been.calledTwice;
+      expect(triggerSpy.secondCall).to.have.been.calledWith(
+        'hlsSubtitleTrackLoading',
+        {
+          url: 'foo',
+          id: 2,
+          groupId: 'default-text-group',
+          deliveryDirectives: null,
+        },
+      );
+    });
+
+    it('should do nothing if called with out of bound indices', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTrackController.subtitleTrack = 5;
+      subtitleTrackController.subtitleTrack = -2;
+      expect(triggerSpy).to.have.callCount(0);
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+    });
+
+    it('should do nothing if called with a invalid index', function () {
+      const triggerSpy = sandbox.spy(hls, 'trigger');
+      subtitleTrackController.subtitleTrack = undefined as any;
+      subtitleTrackController.subtitleTrack = null as any;
+      expect(triggerSpy).to.have.callCount(0);
+      expect(subtitleTrackController.subtitleTrack).to.equal(-1);
+    });
+  });
+
+  describe('toggleTrackModes', function () {
+    // This can be the case when setting the subtitleTrack before Hls.js attaches to the mediaElement
+    it('should not throw an exception if trackId is out of the mediaElement text track bounds', function () {
+      switchLevel();
+      hls.detachMedia();
+      const toggleTrackModesSpy = sandbox.spy(
+        subtitleTrackController,
+        'toggleTrackModes',
+      );
+      (subtitleTrackController as any).trackId = 3;
+      hls.trigger(Events.MEDIA_ATTACHED, { media: videoElement });
+      subtitleTrackController.subtitleDisplay = true; // setting subtitleDisplay invokes `toggleTrackModes`
+      expect(toggleTrackModesSpy).to.have.been.calledOnce;
+    });
+  });
+
+  describe('onSubtitleTrackLoaded', function () {
+    beforeEach(function () {
+      switchLevel();
+    });
+    it('exits early if the loaded track does not match the requested track', function () {
+      const playlistLoadedSpy = sandbox.spy(
+        subtitleTrackController,
+        'playlistLoaded',
+      );
+      subtitleTrackController.startLoad();
+      (subtitleTrackController as any).trackId = 1;
+      (subtitleTrackController as any).currentTrack = subtitleTracks[1];
+
+      const mockLoadedEvent = {
+        id: 999,
+        groupId: 'default-text-group',
+        details: { foo: 'bar' } as any,
+        stats: new LoadStats(),
+        networkDetails: {},
+        deliveryDirectives: null,
+      };
+      hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent);
+      expect((subtitleTrackController as any).timer).to.equal(-1);
+      expect(playlistLoadedSpy).to.have.not.been.called;
+
+      mockLoadedEvent.id = 0;
+      hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent);
+      expect((subtitleTrackController as any).timer).to.equal(-1);
+      expect(playlistLoadedSpy).to.have.not.been.called;
+
+      mockLoadedEvent.id = 1;
+      hls.trigger(Events.SUBTITLE_TRACK_LOADED, mockLoadedEvent);
+      expect((subtitleTrackController as any).timer).to.equal(-1);
+      expect(playlistLoadedSpy).to.have.been.calledOnce;
+    });
+
+    it('does not set the reload timer if loading has not started', function () {
+      const details = new LevelDetails('');
+      subtitleTrackController.stopLoad();
+      (subtitleTrackController as any).trackId = 1;
+      (subtitleTrackController as any).currentTrack = subtitleTracks[1];
+      hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
+        id: 1,
+        groupId: 'default-text-group',
+        details,
+        stats: new LoadStats(),
+        networkDetails: {},
+        deliveryDirectives: null,
+      });
+      expect((subtitleTrackController as any).timer).to.equal(-1);
+    });
+
+    it('sets the live reload timer if the level is live', function () {
+      const details = new LevelDetails('');
+      subtitleTrackController.startLoad();
+      (subtitleTrackController as any).trackId = 1;
+      (subtitleTrackController as any).currentTrack = subtitleTracks[1];
+      hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
+        id: 1,
+        groupId: 'default-text-group',
+        details,
+        stats: new LoadStats(),
+        networkDetails: {},
+        deliveryDirectives: null,
+      });
+      expect((subtitleTrackController as any).timer).to.exist;
+    });
+
+    it('stops the live reload timer if the level is not live', function () {
+      const details = new LevelDetails('');
+      details.live = false;
+      (subtitleTrackController as any).trackId = 1;
+      (subtitleTrackController as any).currentTrack = subtitleTracks[1];
+      (subtitleTrackController as any).timer = self.setTimeout(() => {}, 0);
+      hls.trigger(Events.SUBTITLE_TRACK_LOADED, {
+        id: 1,
+        groupId: 'default-text-group',
+        details,
+        stats: new LoadStats(),
+        networkDetails: {},
+        deliveryDirectives: null,
+      });
+      expect((subtitleTrackController as any).timer).to.equal(-1);
+    });
+  });
+
+  describe('stopLoad', function () {
+    it('stops loading', function () {
+      const clearReloadSpy = sandbox.spy(subtitleTrackController, 'clearTimer');
+      subtitleTrackController.stopLoad();
+      expect((subtitleTrackController as any).canLoad).to.be.false;
+      expect(clearReloadSpy).to.have.been.calledOnce;
+    });
+  });
+
+  describe('startLoad', function () {
+    it('starts loading', function () {
+      const loadCurrentTrackSpy = sandbox.spy(
+        subtitleTrackController,
+        'loadPlaylist',
+      );
+      subtitleTrackController.startLoad();
+      expect((subtitleTrackController as any).canLoad).to.be.true;
+      expect(loadCurrentTrackSpy).to.have.been.calledOnce;
+    });
+  });
+});