Files
streamyfin/components/video-player/controls/contexts/PlayerContext.tsx

116 lines
3.5 KiB
TypeScript

import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client";
import React, {
createContext,
type MutableRefObject,
type ReactNode,
useContext,
useMemo,
} from "react";
import type { SfPlayerViewRef, VlcPlayerViewRef } from "@/modules";
// Union type for both player refs
type PlayerRef = SfPlayerViewRef | VlcPlayerViewRef;
interface PlayerContextProps {
playerRef: MutableRefObject<PlayerRef | null>;
item: BaseItemDto;
mediaSource: MediaSourceInfo | null | undefined;
isVideoLoaded: boolean;
tracksReady: boolean;
}
const PlayerContext = createContext<PlayerContextProps | undefined>(undefined);
interface PlayerProviderProps {
children: ReactNode;
playerRef: MutableRefObject<PlayerRef | null>;
item: BaseItemDto;
mediaSource: MediaSourceInfo | null | undefined;
isVideoLoaded: boolean;
tracksReady: boolean;
}
export const PlayerProvider: React.FC<PlayerProviderProps> = ({
children,
playerRef,
item,
mediaSource,
isVideoLoaded,
tracksReady,
}) => {
const value = useMemo(
() => ({ playerRef, item, mediaSource, isVideoLoaded, tracksReady }),
[playerRef, item, mediaSource, isVideoLoaded, tracksReady],
);
return (
<PlayerContext.Provider value={value}>{children}</PlayerContext.Provider>
);
};
// Core context hook
export const usePlayerContext = () => {
const context = useContext(PlayerContext);
if (!context)
throw new Error("usePlayerContext must be used within PlayerProvider");
return context;
};
// Player controls hook - supports both SfPlayer (iOS) and VlcPlayer (Android)
export const usePlayerControls = () => {
const { playerRef } = usePlayerContext();
// Helper to get SfPlayer-specific ref (for iOS-only features)
const getSfRef = () => playerRef.current as SfPlayerViewRef | null;
return {
// Subtitle controls (both players support these, but with different interfaces)
getSubtitleTracks: async () => {
return playerRef.current?.getSubtitleTracks?.() ?? null;
},
setSubtitleTrack: (trackId: number) => {
playerRef.current?.setSubtitleTrack?.(trackId);
},
// iOS only (SfPlayer)
disableSubtitles: () => {
getSfRef()?.disableSubtitles?.();
},
addSubtitleFile: (url: string, select = true) => {
getSfRef()?.addSubtitleFile?.(url, select);
},
// Audio controls (both players)
getAudioTracks: async () => {
return playerRef.current?.getAudioTracks?.() ?? null;
},
setAudioTrack: (trackId: number) => {
playerRef.current?.setAudioTrack?.(trackId);
},
// Playback controls (both players)
play: () => playerRef.current?.play?.(),
pause: () => playerRef.current?.pause?.(),
seekTo: (position: number) => playerRef.current?.seekTo?.(position),
// iOS only (SfPlayer)
seekBy: (offset: number) => getSfRef()?.seekBy?.(offset),
setSpeed: (speed: number) => getSfRef()?.setSpeed?.(speed),
// Subtitle positioning - iOS only (SfPlayer)
setSubtitleScale: (scale: number) => getSfRef()?.setSubtitleScale?.(scale),
setSubtitlePosition: (position: number) =>
getSfRef()?.setSubtitlePosition?.(position),
setSubtitleMarginY: (margin: number) =>
getSfRef()?.setSubtitleMarginY?.(margin),
setSubtitleFontSize: (size: number) =>
getSfRef()?.setSubtitleFontSize?.(size),
// PiP (both players)
startPictureInPicture: () => playerRef.current?.startPictureInPicture?.(),
// iOS only (SfPlayer)
stopPictureInPicture: () => getSfRef()?.stopPictureInPicture?.(),
};
};