diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index efe6ac45..763166a9 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -539,11 +539,6 @@ export default function page() { [playbackManager, item?.Id, progress], ); - const _allSubs = - stream?.mediaSource.MediaStreams?.filter( - (sub) => sub.Type === "Subtitle", - ).sort((a, b) => Number(a.IsExternal) - Number(b.IsExternal)) || []; - const [isMounted, setIsMounted] = useState(false); // Add useEffect to handle mounting @@ -582,6 +577,14 @@ export default function page() { videoRef.current?.addSubtitleFile?.(url); }, []); + const getAudioTracks = useCallback(async () => { + return videoRef.current?.getAudioTracks?.() || null; + }, []); + + const setAudioTrack = useCallback((index: number) => { + videoRef.current?.setAudioTrack?.(index); + }, []); + // Apply MPV subtitle settings when video loads useEffect(() => { if (!isVideoLoaded || !videoRef.current) return; @@ -702,8 +705,10 @@ export default function page() { seek={seek} enableTrickplay={true} getSubtitleTracks={getSubtitleTracks} + getAudioTracks={getAudioTracks} offline={offline} setSubtitleTrack={setSubtitleTrack} + setAudioTrack={setAudioTrack} setSubtitleURL={setSubtitleURL} aspectRatio={aspectRatio} scaleFactor={scaleFactor} diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index 7037fb11..2310c468 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -28,7 +28,7 @@ import { useHaptic } from "@/hooks/useHaptic"; import { useIntroSkipper } from "@/hooks/useIntroSkipper"; import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { useTrickplay } from "@/hooks/useTrickplay"; -import type { MpvPlayerViewRef, SubtitleTrack } from "@/modules"; +import type { AudioTrack, MpvPlayerViewRef, SubtitleTrack } from "@/modules"; import { DownloadedItem } from "@/providers/Downloads/types"; import { useSettings } from "@/utils/atoms/settings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; @@ -71,8 +71,10 @@ interface Props { getSubtitleTracks?: | (() => Promise) | (() => SubtitleTrack[]); + getAudioTracks?: (() => Promise) | (() => AudioTrack[]); setSubtitleURL?: (url: string, customName: string) => void; setSubtitleTrack?: (index: number) => void; + setAudioTrack?: (index: number) => void; setVideoAspectRatio?: (aspectRatio: string | null) => Promise; setVideoScaleFactor?: (scaleFactor: number) => Promise; aspectRatio?: AspectRatio; @@ -100,8 +102,10 @@ export const Controls: FC = ({ mediaSource, isVideoLoaded, getSubtitleTracks, + getAudioTracks, setSubtitleURL, setSubtitleTrack, + setAudioTrack, setVideoAspectRatio, setVideoScaleFactor, aspectRatio = "default", @@ -501,7 +505,9 @@ export const Controls: FC = ({ previousItem={previousItem} nextItem={nextItem} getSubtitleTracks={getSubtitleTracks} + getAudioTracks={getAudioTracks} setSubtitleTrack={setSubtitleTrack} + setAudioTrack={setAudioTrack} setSubtitleURL={setSubtitleURL} aspectRatio={aspectRatio} scaleFactor={scaleFactor} diff --git a/components/video-player/controls/HeaderControls.tsx b/components/video-player/controls/HeaderControls.tsx index bb0296f4..5c04baf5 100644 --- a/components/video-player/controls/HeaderControls.tsx +++ b/components/video-player/controls/HeaderControls.tsx @@ -35,7 +35,9 @@ interface HeaderControlsProps { previousItem?: BaseItemDto | null; nextItem?: BaseItemDto | null; getSubtitleTracks?: (() => Promise) | (() => any[]); + getAudioTracks?: (() => Promise) | (() => any[]); setSubtitleTrack?: (index: number) => void; + setAudioTrack?: (index: number) => void; setSubtitleURL?: (url: string, customName: string) => void; aspectRatio?: AspectRatio; scaleFactor?: ScaleFactor; @@ -57,7 +59,9 @@ export const HeaderControls: FC = ({ previousItem, nextItem, getSubtitleTracks, + getAudioTracks, setSubtitleTrack, + setAudioTrack, setSubtitleURL, aspectRatio = "default", scaleFactor = 1.0, @@ -111,7 +115,9 @@ export const HeaderControls: FC = ({ {!Platform.isTV && (!offline || !mediaSource?.TranscodingUrl) && ( diff --git a/components/video-player/controls/contexts/VideoContext.tsx b/components/video-player/controls/contexts/VideoContext.tsx index c142f22d..d5d77680 100644 --- a/components/video-player/controls/contexts/VideoContext.tsx +++ b/components/video-player/controls/contexts/VideoContext.tsx @@ -9,12 +9,13 @@ import { useMemo, useState, } from "react"; -import type { SubtitleTrack } from "@/modules"; +import type { AudioTrack, SubtitleTrack } from "@/modules"; import type { Track } from "../types"; import { useControlContext } from "./ControlContext"; interface VideoContextProps { subtitleTracks: Track[] | null; + audioTracks: Track[] | null; setSubtitleTrack: ((index: number) => void) | undefined; setSubtitleURL: ((url: string, customName: string) => void) | undefined; } @@ -27,21 +28,29 @@ interface VideoProviderProps { | (() => Promise) | (() => SubtitleTrack[]) | undefined; + getAudioTracks: + | (() => Promise) + | (() => AudioTrack[]) + | undefined; setSubtitleTrack: ((index: number) => void) | undefined; + setAudioTrack: ((index: number) => void) | undefined; setSubtitleURL: ((url: string, customName: string) => void) | undefined; } /** - * Video context provider for managing subtitle tracks. +s * Video context provider for managing subtitle and audio tracks. * MPV player is used for all playback. */ export const VideoProvider: React.FC = ({ children, getSubtitleTracks, + getAudioTracks, setSubtitleTrack, + setAudioTrack, setSubtitleURL, }) => { const [subtitleTracks, setSubtitleTracks] = useState(null); + const [audioTracks, setAudioTracks] = useState(null); const ControlContext = useControlContext(); const isVideoLoaded = ControlContext?.isVideoLoaded; @@ -122,6 +131,7 @@ export const VideoProvider: React.FC = ({ let subtitleData: SubtitleTrack[] | null = null; try { subtitleData = await getSubtitleTracks(); + console.log("subtitleData", subtitleData); } catch (error) { console.log("[VideoContext] Failed to get subtitle tracks:", error); return; @@ -169,10 +179,49 @@ export const VideoProvider: React.FC = ({ fetchTracks(); }, [isVideoLoaded, getSubtitleTracks]); + // Fetch audio tracks + useEffect(() => { + const fetchAudioTracks = async () => { + if (getAudioTracks) { + let audioData: AudioTrack[] | null = null; + try { + audioData = await getAudioTracks(); + console.log("audioData", audioData); + } catch (error) { + console.log("[VideoContext] Failed to get audio tracks:", error); + return; + } + + const allAudio = + mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") || []; + + let embedAudioIndex = 0; + const processedAudio: Track[] = allAudio?.map((audio) => { + const mpvIndex = audioData?.at(embedAudioIndex)?.id ?? 1; + embedAudioIndex++; + return { + name: audio.DisplayTitle || "Undefined Audio", + index: audio.Index ?? -1, + setTrack: () => { + setAudioTrack?.(mpvIndex); + router.setParams({ + audioIndex: audio.Index?.toString() ?? "0", + }); + }, + }; + }); + + setAudioTracks(processedAudio); + } + }; + fetchAudioTracks(); + }, [isVideoLoaded, getAudioTracks]); + return ( = 0 else { @@ -979,11 +971,15 @@ final class MPVSoftwareRenderer { // MARK: - Subtitle Controls func getSubtitleTracks() -> [[String: Any]] { - guard let handle = mpv else { return [] } + guard let handle = mpv else { + Logger.shared.log("getSubtitleTracks: mpv handle is nil", type: "Warn") + return [] + } var tracks: [[String: Any]] = [] var trackCount: Int64 = 0 getProperty(handle: handle, name: "track-list/count", format: MPV_FORMAT_INT64, value: &trackCount) + Logger.shared.log("getSubtitleTracks: total track count = \(trackCount)", type: "Info") for i in 0.. [[String: Any]] { + guard let handle = mpv else { + Logger.shared.log("getAudioTracks: mpv handle is nil", type: "Warn") + return [] + } + var tracks: [[String: Any]] = [] + + var trackCount: Int64 = 0 + getProperty(handle: handle, name: "track-list/count", format: MPV_FORMAT_INT64, value: &trackCount) + + for i in 0.. 0 { + track["channels"] = Int(channels) + } + + var selected: Int32 = 0 + getProperty(handle: handle, name: "track-list/\(i)/selected", format: MPV_FORMAT_FLAG, value: &selected) + track["selected"] = selected != 0 + + Logger.shared.log("getAudioTracks: found audio track id=\(trackId), title=\(track["title"] ?? "none"), lang=\(track["lang"] ?? "none")", type: "Info") + tracks.append(track) + } + + Logger.shared.log("getAudioTracks: returning \(tracks.count) audio tracks", type: "Info") + return tracks + } + + func setAudioTrack(_ trackId: Int) { + Logger.shared.log("setAudioTrack: setting aid to \(trackId)", type: "Info") + setProperty(name: "aid", value: String(trackId)) + } + + func getCurrentAudioTrack() -> Int { + guard let handle = mpv else { return 0 } + var aid: Int64 = 0 + getProperty(handle: handle, name: "aid", format: MPV_FORMAT_INT64, value: &aid) + return Int(aid) + } } diff --git a/modules/mpv-player/ios/MpvPlayerModule.swift b/modules/mpv-player/ios/MpvPlayerModule.swift index 96e074bf..e522849d 100644 --- a/modules/mpv-player/ios/MpvPlayerModule.swift +++ b/modules/mpv-player/ios/MpvPlayerModule.swift @@ -150,6 +150,19 @@ public class MpvPlayerModule: Module { AsyncFunction("setSubtitleFontSize") { (view: MpvPlayerView, size: Int) in view.setSubtitleFontSize(size) } + + // Audio track functions + AsyncFunction("getAudioTracks") { (view: MpvPlayerView) -> [[String: Any]] in + return view.getAudioTracks() + } + + AsyncFunction("setAudioTrack") { (view: MpvPlayerView, trackId: Int) in + view.setAudioTrack(trackId) + } + + AsyncFunction("getCurrentAudioTrack") { (view: MpvPlayerView) -> Int in + return view.getCurrentAudioTrack() + } // Defines events that the view can send to JavaScript Events("onLoad", "onPlaybackStateChange", "onProgress", "onError") diff --git a/modules/mpv-player/ios/MpvPlayerView.swift b/modules/mpv-player/ios/MpvPlayerView.swift index 3e2407f4..3ac0315b 100644 --- a/modules/mpv-player/ios/MpvPlayerView.swift +++ b/modules/mpv-player/ios/MpvPlayerView.swift @@ -168,6 +168,20 @@ class MpvPlayerView: ExpoView { renderer?.addSubtitleFile(url: url) } + // MARK: - Audio Track Controls + + func getAudioTracks() -> [[String: Any]] { + return renderer?.getAudioTracks() ?? [] + } + + func setAudioTrack(_ trackId: Int) { + renderer?.setAudioTrack(trackId) + } + + func getCurrentAudioTrack() -> Int { + return renderer?.getCurrentAudioTrack() ?? 0 + } + // MARK: - Subtitle Positioning func setSubtitlePosition(_ position: Int) { diff --git a/modules/mpv-player/src/MpvPlayer.types.ts b/modules/mpv-player/src/MpvPlayer.types.ts index 70ff4896..edaabdac 100644 --- a/modules/mpv-player/src/MpvPlayer.types.ts +++ b/modules/mpv-player/src/MpvPlayer.types.ts @@ -56,6 +56,7 @@ export interface MpvPlayerViewRef { stopPictureInPicture: () => Promise; isPictureInPictureSupported: () => Promise; isPictureInPictureActive: () => Promise; + // Subtitle controls getSubtitleTracks: () => Promise; setSubtitleTrack: (trackId: number) => Promise; disableSubtitles: () => Promise; @@ -68,10 +69,24 @@ export interface MpvPlayerViewRef { setSubtitleAlignX: (alignment: "left" | "center" | "right") => Promise; setSubtitleAlignY: (alignment: "top" | "center" | "bottom") => Promise; setSubtitleFontSize: (size: number) => Promise; + // Audio controls + getAudioTracks: () => Promise; + setAudioTrack: (trackId: number) => Promise; + getCurrentAudioTrack: () => Promise; } + export type SubtitleTrack = { id: number; title?: string; lang?: string; selected?: boolean; }; + +export type AudioTrack = { + id: number; + title?: string; + lang?: string; + codec?: string; + channels?: number; + selected?: boolean; +}; diff --git a/modules/mpv-player/src/MpvPlayerView.tsx b/modules/mpv-player/src/MpvPlayerView.tsx index e99b77d3..d4afe2ee 100644 --- a/modules/mpv-player/src/MpvPlayerView.tsx +++ b/modules/mpv-player/src/MpvPlayerView.tsx @@ -84,6 +84,16 @@ export default React.forwardRef( setSubtitleFontSize: async (size: number) => { await nativeRef.current?.setSubtitleFontSize(size); }, + // Audio controls + getAudioTracks: async () => { + return await nativeRef.current?.getAudioTracks(); + }, + setAudioTrack: async (trackId: number) => { + await nativeRef.current?.setAudioTrack(trackId); + }, + getCurrentAudioTrack: async () => { + return await nativeRef.current?.getCurrentAudioTrack(); + }, })); return ;