diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 97cf7be8..af4dee46 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -75,9 +75,12 @@ export default function page() { const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); const [showControls, _setShowControls] = useState(true); const [isPipMode, setIsPipMode] = useState(false); - const [aspectRatio] = useState<"default" | "16:9" | "4:3" | "1:1" | "21:9">( - "default", - ); + const [aspectRatio, setAspectRatio] = useState< + "default" | "16:9" | "4:3" | "1:1" | "21:9" + >("default"); + const [scaleFactor, setScaleFactor] = useState< + 0 | 0.25 | 0.5 | 0.75 | 1.0 | 1.25 | 1.5 | 2.0 + >(0); const [isZoomedToFill, setIsZoomedToFill] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [isMuted, setIsMuted] = useState(false); @@ -892,6 +895,37 @@ export default function page() { ); }, [isZoomedToFill, useVlcPlayer]); + // VLC-specific handlers for aspect ratio and scale factor + const handleSetVideoAspectRatio = useCallback( + async (newAspectRatio: string | null) => { + if (!useVlcPlayer) return; + const ratio = (newAspectRatio ?? "default") as + | "default" + | "16:9" + | "4:3" + | "1:1" + | "21:9"; + setAspectRatio(ratio); + await (videoRef.current as VlcPlayerViewRef)?.setVideoAspectRatio?.( + newAspectRatio, + ); + }, + [useVlcPlayer], + ); + + const handleSetVideoScaleFactor = useCallback( + async (newScaleFactor: number) => { + if (!useVlcPlayer) return; + setScaleFactor( + newScaleFactor as 0 | 0.25 | 0.5 | 0.75 | 1.0 | 1.25 | 1.5 | 2.0, + ); + await (videoRef.current as VlcPlayerViewRef)?.setVideoScaleFactor?.( + newScaleFactor, + ); + }, + [useVlcPlayer], + ); + // Apply KSPlayer global settings before video loads (only when using KSPlayer) useEffect(() => { if (Platform.OS === "ios" && !useVlcPlayer) { @@ -1058,7 +1092,11 @@ export default function page() { seek={seek} enableTrickplay={true} offline={offline} + useVlcPlayer={useVlcPlayer} aspectRatio={aspectRatio} + setVideoAspectRatio={handleSetVideoAspectRatio} + scaleFactor={scaleFactor} + setVideoScaleFactor={handleSetVideoScaleFactor} isZoomedToFill={isZoomedToFill} onZoomToggle={handleZoomToggle} api={api} diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index a0ac938b..dd64f586 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -36,6 +36,7 @@ import { useVideoSlider } from "./hooks/useVideoSlider"; import { useVideoTime } from "./hooks/useVideoTime"; import { useControlsTimeout } from "./useControlsTimeout"; import { type AspectRatio } from "./VideoScalingModeSelector"; +import { type ScaleFactor } from "./VlcZoomControl"; interface Props { item: BaseItemDto; @@ -54,8 +55,13 @@ interface Props { startPictureInPicture?: () => Promise; play: () => void; pause: () => void; + useVlcPlayer?: boolean; + // VLC-specific props setVideoAspectRatio?: (aspectRatio: string | null) => Promise; aspectRatio?: AspectRatio; + scaleFactor?: ScaleFactor; + setVideoScaleFactor?: (scaleFactor: number) => Promise; + // KSPlayer-specific props isZoomedToFill?: boolean; onZoomToggle?: () => void; api?: Api | null; @@ -77,8 +83,11 @@ export const Controls: FC = ({ showControls, setShowControls, mediaSource, + useVlcPlayer = false, setVideoAspectRatio, aspectRatio = "default", + scaleFactor = 0, + setVideoScaleFactor, isZoomedToFill = false, onZoomToggle, offline = false, @@ -468,8 +477,11 @@ export const Controls: FC = ({ goToNextItem={goToNextItem} previousItem={previousItem} nextItem={nextItem} + useVlcPlayer={useVlcPlayer} aspectRatio={aspectRatio} setVideoAspectRatio={setVideoAspectRatio} + scaleFactor={scaleFactor} + setVideoScaleFactor={setVideoScaleFactor} isZoomedToFill={isZoomedToFill} onZoomToggle={onZoomToggle} /> diff --git a/components/video-player/controls/HeaderControls.tsx b/components/video-player/controls/HeaderControls.tsx index 47fde671..b1266348 100644 --- a/components/video-player/controls/HeaderControls.tsx +++ b/components/video-player/controls/HeaderControls.tsx @@ -5,12 +5,7 @@ import type { } from "@jellyfin/sdk/lib/generated-client"; import { useRouter } from "expo-router"; import { type FC, useCallback, useState } from "react"; -import { - Platform, - TouchableOpacity, - useWindowDimensions, - View, -} from "react-native"; +import { Platform, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useHaptic } from "@/hooks/useHaptic"; import { useOrientation } from "@/hooks/useOrientation"; @@ -18,7 +13,11 @@ import { OrientationLock } from "@/packages/expo-screen-orientation"; import { useSettings, VideoPlayerIOS } from "@/utils/atoms/settings"; import { ICON_SIZES } from "./constants"; import DropdownView from "./dropdown/DropdownView"; -import { type AspectRatio } from "./VideoScalingModeSelector"; +import { + type AspectRatio, + AspectRatioSelector, +} from "./VideoScalingModeSelector"; +import { type ScaleFactor, VlcZoomControl } from "./VlcZoomControl"; import { ZoomToggle } from "./ZoomToggle"; interface HeaderControlsProps { @@ -32,8 +31,13 @@ interface HeaderControlsProps { goToNextItem: (options: { isAutoPlay?: boolean }) => void; previousItem?: BaseItemDto | null; nextItem?: BaseItemDto | null; + useVlcPlayer?: boolean; + // VLC-specific props aspectRatio?: AspectRatio; setVideoAspectRatio?: (aspectRatio: string | null) => Promise; + scaleFactor?: ScaleFactor; + setVideoScaleFactor?: (scaleFactor: number) => Promise; + // KSPlayer-specific props isZoomedToFill?: boolean; onZoomToggle?: () => void; } @@ -49,15 +53,17 @@ export const HeaderControls: FC = ({ goToNextItem, previousItem, nextItem, - aspectRatio: _aspectRatio = "default", - setVideoAspectRatio: _setVideoAspectRatio, + useVlcPlayer = false, + aspectRatio = "default", + setVideoAspectRatio, + scaleFactor = 0, + setVideoScaleFactor, isZoomedToFill = false, onZoomToggle, }) => { const { settings } = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); - const { width: _screenWidth } = useWindowDimensions(); const lightHapticFeedback = useHaptic("light"); const { orientation, lockOrientation } = useOrientation(); const [isTogglingOrientation, setIsTogglingOrientation] = useState(false); @@ -175,21 +181,39 @@ export const HeaderControls: FC = ({ /> )} - {/* { - if (setVideoAspectRatio) { - const aspectRatioString = newRatio === "default" ? null : newRatio; - await setVideoAspectRatio(aspectRatioString); - } - }} - disabled={!setVideoAspectRatio} - />*/} - {})} - disabled={!onZoomToggle} - /> + {/* VLC-specific controls: Aspect Ratio and Scale/Zoom */} + {useVlcPlayer && ( + { + if (setVideoAspectRatio) { + const aspectRatioString = + newRatio === "default" ? null : newRatio; + await setVideoAspectRatio(aspectRatioString); + } + }} + disabled={!setVideoAspectRatio} + /> + )} + {useVlcPlayer && ( + { + if (setVideoScaleFactor) { + await setVideoScaleFactor(newScale); + } + }} + disabled={!setVideoScaleFactor} + /> + )} + {/* KSPlayer-specific control: Zoom to Fill */} + {!useVlcPlayer && ( + {})} + disabled={!onZoomToggle} + /> + )} void; + disabled?: boolean; +} + +interface ScaleOption { + id: ScaleFactor; + label: string; + description: string; +} + +const SCALE_OPTIONS: ScaleOption[] = [ + { + id: 0, + label: "Fit", + description: "Fit video to screen", + }, + { + id: 0.25, + label: "25%", + description: "Quarter size", + }, + { + id: 0.5, + label: "50%", + description: "Half size", + }, + { + id: 0.75, + label: "75%", + description: "Three quarters", + }, + { + id: 1.0, + label: "100%", + description: "Original video size", + }, + { + id: 1.25, + label: "125%", + description: "Slight zoom", + }, + { + id: 1.5, + label: "150%", + description: "Medium zoom", + }, + { + id: 2.0, + label: "200%", + description: "Maximum zoom", + }, +]; + +export const VlcZoomControl: React.FC = ({ + currentScale, + onScaleChange, + disabled = false, +}) => { + const lightHapticFeedback = useHaptic("light"); + + const handleScaleSelect = (scale: ScaleFactor) => { + onScaleChange(scale); + lightHapticFeedback(); + }; + + const optionGroups = useMemo(() => { + return [ + { + options: SCALE_OPTIONS.map((option) => ({ + type: "radio" as const, + label: option.label, + value: option.id, + selected: option.id === currentScale, + onPress: () => handleScaleSelect(option.id), + disabled, + })), + }, + ]; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentScale, disabled]); + + const trigger = useMemo( + () => ( + + + + ), + [disabled], + ); + + // Hide on TV platforms + if (Platform.isTV) return null; + + return ( + + ); +};