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 }) { + + + + + 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 ( + <> + router.navigate("/")}>Audio + + {audioTracks.map((v) => ( + change(v) : undefined} + disabled={!audioTracks.includes(v)} + > + {v.label} ({v.language}) + + ))} + + + ); +} 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")} + router.navigate("/audio")} + rightText={currentAudioTrack ? currentAudioTrack.label : ""} + > + {/* {t("player.menus.settings.qualityItem")} */} + Audio + 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 { 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 = (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 = (set, get) => ({ source: null, sourceId: null, qualities: [], + audioTracks: [], captionList: [], currentQuality: null, + currentAudioTrack: null, status: playerStatus.IDLE, meta: null, caption: {