From c08dea89d1231d85712612469b1181eca45ab6d1 Mon Sep 17 00:00:00 2001
From: Jorrin <jorrinkievit@hotmail.com>
Date: Mon, 18 Mar 2024 00:06:27 +0100
Subject: [PATCH 1/5] add audio track selector

---
 src/components/player/atoms/Settings.tsx      |  6 +++
 .../player/atoms/settings/AudioView.tsx       | 42 +++++++++++++++++++
 .../player/atoms/settings/SettingsMenu.tsx    |  8 ++++
 src/components/player/display/base.ts         | 32 ++++++++++++++
 src/components/player/display/chromecast.ts   |  3 ++
 .../player/display/displayInterface.ts        |  5 ++-
 src/stores/player/slices/display.ts           | 10 +++++
 src/stores/player/slices/source.ts            | 10 +++++
 8 files changed, 115 insertions(+), 1 deletion(-)
 create mode 100644 src/components/player/atoms/settings/AudioView.tsx

diff --git a/src/components/player/atoms/Settings.tsx b/src/components/player/atoms/Settings.tsx
index 68e36b83..5900df44 100644
--- a/src/components/player/atoms/Settings.tsx
+++ b/src/components/player/atoms/Settings.tsx
@@ -14,6 +14,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
 import { useOverlayRouter } from "@/hooks/useOverlayRouter";
 import { usePlayerStore } from "@/stores/player/store";
 
+import { AudioView } from "./settings/AudioView";
 import { CaptionSettingsView } from "./settings/CaptionSettingsView";
 import { CaptionsView } from "./settings/CaptionsView";
 import { DownloadRoutes } from "./settings/Downloads";
@@ -46,6 +47,11 @@ function SettingsOverlay({ id }: { id: string }) {
             <QualityView id={id} />
           </Menu.Card>
         </OverlayPage>
+        <OverlayPage id={id} path="/audio" width={343} height={431}>
+          <Menu.Card>
+            <AudioView id={id} />
+          </Menu.Card>
+        </OverlayPage>
         <OverlayPage id={id} path="/captions" width={343} height={431}>
           <Menu.CardWithScrollable>
             <CaptionsView id={id} />
diff --git a/src/components/player/atoms/settings/AudioView.tsx b/src/components/player/atoms/settings/AudioView.tsx
new file mode 100644
index 00000000..0158ea78
--- /dev/null
+++ b/src/components/player/atoms/settings/AudioView.tsx
@@ -0,0 +1,42 @@
+import { t } from "i18next";
+import { useCallback } from "react";
+
+import { Menu } from "@/components/player/internals/ContextMenu";
+import { useOverlayRouter } from "@/hooks/useOverlayRouter";
+import { AudioTrack } from "@/stores/player/slices/source";
+import { usePlayerStore } from "@/stores/player/store";
+
+import { SelectableLink } from "../../internals/ContextMenu/Links";
+
+export function AudioView({ id }: { id: string }) {
+  const router = useOverlayRouter(id);
+  const audioTracks = usePlayerStore((s) => s.audioTracks);
+  const currentAudioTrack = usePlayerStore((s) => s.currentAudioTrack);
+  const changeAudioTrack = usePlayerStore((s) => s.display?.changeAudioTrack);
+
+  const change = useCallback(
+    (track: AudioTrack) => {
+      changeAudioTrack?.(track);
+      router.close();
+    },
+    [router, changeAudioTrack],
+  );
+
+  return (
+    <>
+      <Menu.BackLink onClick={() => router.navigate("/")}>Audio</Menu.BackLink>
+      <Menu.Section className="flex flex-col pb-4">
+        {audioTracks.map((v) => (
+          <SelectableLink
+            key={v.id}
+            selected={v.id === currentAudioTrack?.id}
+            onClick={audioTracks.includes(v) ? () => change(v) : undefined}
+            disabled={!audioTracks.includes(v)}
+          >
+            {v.label} ({v.language})
+          </SelectableLink>
+        ))}
+      </Menu.Section>
+    </>
+  );
+}
diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx
index 8321c562..8225277b 100644
--- a/src/components/player/atoms/settings/SettingsMenu.tsx
+++ b/src/components/player/atoms/settings/SettingsMenu.tsx
@@ -16,6 +16,7 @@ export function SettingsMenu({ id }: { id: string }) {
   const { t } = useTranslation();
   const router = useOverlayRouter(id);
   const currentQuality = usePlayerStore((s) => s.currentQuality);
+  const currentAudioTrack = usePlayerStore((s) => s.currentAudioTrack);
   const selectedCaptionLanguage = usePlayerStore(
     (s) => s.caption.selected?.language,
   );
@@ -51,6 +52,13 @@ export function SettingsMenu({ id }: { id: string }) {
         >
           {t("player.menus.settings.qualityItem")}
         </Menu.ChevronLink>
+        <Menu.ChevronLink
+          onClick={() => router.navigate("/audio")}
+          rightText={currentAudioTrack ? currentAudioTrack.label : ""}
+        >
+          {/* {t("player.menus.settings.qualityItem")} */}
+          Audio
+        </Menu.ChevronLink>
         <Menu.ChevronLink
           onClick={() => router.navigate("/source")}
           rightText={sourceName}
diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts
index 51e6d7bb..8e155f69 100644
--- a/src/components/player/display/base.ts
+++ b/src/components/player/display/base.ts
@@ -81,6 +81,24 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
     emit("qualities", convertedLevels);
   }
 
+  function reportAudioTracks() {
+    if (!hls) return;
+    const currentTrack = hls.audioTracks[hls.audioTrack];
+    emit("changedaudiotrack", {
+      id: currentTrack.id.toString(),
+      label: currentTrack.name,
+      language: currentTrack.lang ?? "unknown",
+    });
+    emit(
+      "audiotracks",
+      hls.audioTracks.map((v) => ({
+        id: v.id.toString(),
+        label: v.name,
+        language: v.lang ?? "unknown",
+      })),
+    );
+  }
+
   function setupQualityForHls() {
     if (videoElement && canPlayHlsNatively(videoElement)) {
       return; // nothing to change
@@ -155,6 +173,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
           if (!hls) return;
           reportLevels();
           setupQualityForHls();
+          reportAudioTracks();
 
           if (isExtensionActiveCached()) {
             hls.on(Hls.Events.LEVEL_LOADED, async (_, data) => {
@@ -464,5 +483,18 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
       hls?.setSubtitleOption({ lang });
       return promise;
     },
+    changeAudioTrack(track) {
+      if (!hls) return;
+      const audioTrack = hls?.audioTracks.find(
+        (t) => t.id.toString() === track.id,
+      );
+      if (!audioTrack) return;
+      hls.audioTrack = hls.audioTracks.indexOf(audioTrack);
+      emit("changedaudiotrack", {
+        id: audioTrack.id.toString(),
+        label: audioTrack.name,
+        language: audioTrack.lang ?? "unknown",
+      });
+    },
   };
 }
diff --git a/src/components/player/display/chromecast.ts b/src/components/player/display/chromecast.ts
index 1a318f16..48f8b2ab 100644
--- a/src/components/player/display/chromecast.ts
+++ b/src/components/player/display/chromecast.ts
@@ -283,5 +283,8 @@ export function makeChromecastDisplayInterface(
     async setSubtitlePreference() {
       return Promise.resolve();
     },
+    changeAudioTrack() {
+      // cant change audio tracks
+    },
   };
 }
diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts
index 134bef44..2f17aaed 100644
--- a/src/components/player/display/displayInterface.ts
+++ b/src/components/player/display/displayInterface.ts
@@ -1,7 +1,7 @@
 import { MediaPlaylist } from "hls.js";
 
 import { MWMediaType } from "@/backend/metadata/types/mw";
-import { CaptionListItem } from "@/stores/player/slices/source";
+import { AudioTrack, CaptionListItem } from "@/stores/player/slices/source";
 import { LoadableSource, SourceQuality } from "@/stores/player/utils/qualities";
 import { Listener } from "@/utils/events";
 
@@ -25,6 +25,8 @@ export type DisplayInterfaceEvents = {
   loading: boolean;
   qualities: SourceQuality[];
   changedquality: SourceQuality | null;
+  audiotracks: AudioTrack[];
+  changedaudiotrack: AudioTrack | null;
   needstrack: boolean;
   canairplay: boolean;
   playbackrate: number;
@@ -60,6 +62,7 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
     automaticQuality: boolean,
     preferredQuality: SourceQuality | null,
   ): void;
+  changeAudioTrack(audioTrack: AudioTrack): void;
   processVideoElement(video: HTMLVideoElement): void;
   processContainerElement(container: HTMLElement): void;
   toggleFullscreen(): void;
diff --git a/src/stores/player/slices/display.ts b/src/stores/player/slices/display.ts
index 86743ccd..63403376 100644
--- a/src/stores/player/slices/display.ts
+++ b/src/stores/player/slices/display.ts
@@ -75,6 +75,16 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
         s.currentQuality = quality;
       });
     });
+    newDisplay.on("audiotracks", (audioTracks) => {
+      set((s) => {
+        s.audioTracks = audioTracks;
+      });
+    });
+    newDisplay.on("changedaudiotrack", (audioTrack) => {
+      set((s) => {
+        s.currentAudioTrack = audioTrack;
+      });
+    });
     newDisplay.on("needstrack", (needsTrack) => {
       set((s) => {
         s.caption.asTrack = needsTrack;
diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts
index 5d04ef49..eb2ce9e1 100644
--- a/src/stores/player/slices/source.ts
+++ b/src/stores/player/slices/source.ts
@@ -56,12 +56,20 @@ export interface CaptionListItem {
   hls?: boolean;
 }
 
+export interface AudioTrack {
+  id: string;
+  label: string;
+  language: string;
+}
+
 export interface SourceSlice {
   status: PlayerStatus;
   source: SourceSliceSource | null;
   sourceId: string | null;
   qualities: SourceQuality[];
+  audioTracks: AudioTrack[];
   currentQuality: SourceQuality | null;
+  currentAudioTrack: AudioTrack | null;
   captionList: CaptionListItem[];
   caption: {
     selected: Caption | null;
@@ -109,8 +117,10 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
   source: null,
   sourceId: null,
   qualities: [],
+  audioTracks: [],
   captionList: [],
   currentQuality: null,
+  currentAudioTrack: null,
   status: playerStatus.IDLE,
   meta: null,
   caption: {

From 6ba53ec29a9e0f332173f9827b687505e9b1709d Mon Sep 17 00:00:00 2001
From: Jorrin <jorrinkievit@hotmail.com>
Date: Sun, 24 Mar 2024 01:04:11 +0100
Subject: [PATCH 2/5] only show audio when its available

---
 src/assets/locales/en.json                       |  1 +
 .../player/atoms/settings/SettingsMenu.tsx       | 16 +++++++++-------
 2 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index dc4d6ed1..60c08bfe 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -294,6 +294,7 @@
         "enableSubtitles": "Enable Subtitles",
         "experienceSection": "Viewing experience",
         "playbackItem": "Playback settings",
+        "audioItem": "Audio",
         "qualityItem": "Quality",
         "sourceItem": "Video sources",
         "subtitleItem": "Subtitle settings",
diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx
index 8225277b..e47dc6f5 100644
--- a/src/components/player/atoms/settings/SettingsMenu.tsx
+++ b/src/components/player/atoms/settings/SettingsMenu.tsx
@@ -52,13 +52,15 @@ export function SettingsMenu({ id }: { id: string }) {
         >
           {t("player.menus.settings.qualityItem")}
         </Menu.ChevronLink>
-        <Menu.ChevronLink
-          onClick={() => router.navigate("/audio")}
-          rightText={currentAudioTrack ? currentAudioTrack.label : ""}
-        >
-          {/* {t("player.menus.settings.qualityItem")} */}
-          Audio
-        </Menu.ChevronLink>
+        {currentAudioTrack && (
+          <Menu.ChevronLink
+            onClick={() => router.navigate("/audio")}
+            rightText={currentAudioTrack ? currentAudioTrack.label : ""}
+          >
+            {t("player.menus.settings.audioItem")}
+          </Menu.ChevronLink>
+        )}
+
         <Menu.ChevronLink
           onClick={() => router.navigate("/source")}
           rightText={sourceName}

From c2e587bf90b58358dc6ff04e5c07c2e3b4a71607 Mon Sep 17 00:00:00 2001
From: Jorrin <jorrinkievit@hotmail.com>
Date: Sun, 24 Mar 2024 01:52:16 +0100
Subject: [PATCH 3/5] unused import

---
 src/components/player/atoms/settings/AudioView.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/player/atoms/settings/AudioView.tsx b/src/components/player/atoms/settings/AudioView.tsx
index 0158ea78..0d18ada8 100644
--- a/src/components/player/atoms/settings/AudioView.tsx
+++ b/src/components/player/atoms/settings/AudioView.tsx
@@ -1,4 +1,3 @@
-import { t } from "i18next";
 import { useCallback } from "react";
 
 import { Menu } from "@/components/player/internals/ContextMenu";

From dbb1c1979664d1ae053f6f5e0bac68cd1344b378 Mon Sep 17 00:00:00 2001
From: Jorrin <jorrinkievit@hotmail.com>
Date: Sun, 24 Mar 2024 23:54:09 +0100
Subject: [PATCH 4/5] add flags to audio options

---
 .../player/atoms/settings/AudioView.tsx       | 32 ++++++++++++++++---
 1 file changed, 28 insertions(+), 4 deletions(-)

diff --git a/src/components/player/atoms/settings/AudioView.tsx b/src/components/player/atoms/settings/AudioView.tsx
index 0d18ada8..a2c9c7f6 100644
--- a/src/components/player/atoms/settings/AudioView.tsx
+++ b/src/components/player/atoms/settings/AudioView.tsx
@@ -1,13 +1,37 @@
 import { useCallback } from "react";
+import { useTranslation } from "react-i18next";
 
+import { FlagIcon } from "@/components/FlagIcon";
 import { Menu } from "@/components/player/internals/ContextMenu";
 import { useOverlayRouter } from "@/hooks/useOverlayRouter";
 import { AudioTrack } from "@/stores/player/slices/source";
 import { usePlayerStore } from "@/stores/player/store";
+import { getPrettyLanguageNameFromLocale } from "@/utils/language";
 
 import { SelectableLink } from "../../internals/ContextMenu/Links";
 
+export function AudioOption(props: {
+  langCode?: string;
+  children: React.ReactNode;
+  selected?: boolean;
+  onClick?: () => void;
+}) {
+  return (
+    <SelectableLink selected={props.selected} onClick={props.onClick}>
+      <span className="flex items-center">
+        <span data-code={props.langCode} className="mr-3 inline-flex">
+          <FlagIcon langCode={props.langCode} />
+        </span>
+        <span>{props.children}</span>
+      </span>
+    </SelectableLink>
+  );
+}
+
 export function AudioView({ id }: { id: string }) {
+  const { t } = useTranslation();
+  const unknownChoice = t("player.menus.subtitles.unknownLanguage");
+
   const router = useOverlayRouter(id);
   const audioTracks = usePlayerStore((s) => s.audioTracks);
   const currentAudioTrack = usePlayerStore((s) => s.currentAudioTrack);
@@ -26,14 +50,14 @@ export function AudioView({ id }: { id: string }) {
       <Menu.BackLink onClick={() => router.navigate("/")}>Audio</Menu.BackLink>
       <Menu.Section className="flex flex-col pb-4">
         {audioTracks.map((v) => (
-          <SelectableLink
+          <AudioOption
             key={v.id}
             selected={v.id === currentAudioTrack?.id}
+            langCode={v.language}
             onClick={audioTracks.includes(v) ? () => change(v) : undefined}
-            disabled={!audioTracks.includes(v)}
           >
-            {v.label} ({v.language})
-          </SelectableLink>
+            {getPrettyLanguageNameFromLocale(v.language) ?? unknownChoice}
+          </AudioOption>
         ))}
       </Menu.Section>
     </>

From e19ac55847d5d560e15f1e25a21482e17a324c1f Mon Sep 17 00:00:00 2001
From: Jorrin <jorrinkievit@hotmail.com>
Date: Sun, 24 Mar 2024 23:55:58 +0100
Subject: [PATCH 5/5] add pretty language to menu selection

---
 src/components/player/atoms/settings/SettingsMenu.tsx | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx
index e47dc6f5..8198d87b 100644
--- a/src/components/player/atoms/settings/SettingsMenu.tsx
+++ b/src/components/player/atoms/settings/SettingsMenu.tsx
@@ -36,6 +36,11 @@ export function SettingsMenu({ id }: { id: string }) {
       t("player.menus.subtitles.unknownLanguage")
     : undefined;
 
+  const selectedAudioLanguagePretty = currentAudioTrack
+    ? getPrettyLanguageNameFromLocale(currentAudioTrack.language) ??
+      t("player.menus.subtitles.unknownLanguage")
+    : undefined;
+
   const source = usePlayerStore((s) => s.source);
 
   const downloadable = source?.type === "file" || source?.type === "hls";
@@ -55,7 +60,7 @@ export function SettingsMenu({ id }: { id: string }) {
         {currentAudioTrack && (
           <Menu.ChevronLink
             onClick={() => router.navigate("/audio")}
-            rightText={currentAudioTrack ? currentAudioTrack.label : ""}
+            rightText={selectedAudioLanguagePretty ?? undefined}
           >
             {t("player.menus.settings.audioItem")}
           </Menu.ChevronLink>