mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
feat: scale factor and aspect ratio (#942)
This commit is contained in:
committed by
GitHub
parent
4fed25a3ab
commit
9410239c48
@@ -16,7 +16,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, Platform, View } from "react-native";
|
import { Alert, Platform, View } from "react-native";
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { BITRATES } from "@/components/BitrateSelector";
|
import { BITRATES } from "@/components/BitrateSelector";
|
||||||
import { Text } from "@/components/common/Text";
|
import { Text } from "@/components/common/Text";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
@@ -38,12 +38,9 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
|||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { storage } from "@/utils/mmkv";
|
|
||||||
import { generateDeviceProfile } from "@/utils/profiles/native";
|
import { generateDeviceProfile } from "@/utils/profiles/native";
|
||||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||||
|
|
||||||
const IGNORE_SAFE_AREAS_KEY = "video_player_ignore_safe_areas";
|
|
||||||
|
|
||||||
export default function page() {
|
export default function page() {
|
||||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
@@ -53,11 +50,12 @@ export default function page() {
|
|||||||
|
|
||||||
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
const [isPlaybackStopped, setIsPlaybackStopped] = useState(false);
|
||||||
const [showControls, _setShowControls] = useState(true);
|
const [showControls, _setShowControls] = useState(true);
|
||||||
const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(() => {
|
const [aspectRatio, setAspectRatio] = useState<
|
||||||
// Load persisted state from storage
|
"default" | "16:9" | "4:3" | "1:1" | "21:9"
|
||||||
const saved = storage.getBoolean(IGNORE_SAFE_AREAS_KEY);
|
>("default");
|
||||||
return saved ?? false;
|
const [scaleFactor, setScaleFactor] = useState<
|
||||||
});
|
1.0 | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 | 1.6 | 1.7 | 1.8 | 1.9 | 2.0
|
||||||
|
>(1.0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
const [isBuffering, setIsBuffering] = useState(true);
|
const [isBuffering, setIsBuffering] = useState(true);
|
||||||
@@ -82,11 +80,6 @@ export default function page() {
|
|||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Persist ignoreSafeAreas state whenever it changes
|
|
||||||
useEffect(() => {
|
|
||||||
storage.set(IGNORE_SAFE_AREAS_KEY, ignoreSafeAreas);
|
|
||||||
}, [ignoreSafeAreas]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
itemId,
|
itemId,
|
||||||
audioIndex: audioIndexStr,
|
audioIndex: audioIndexStr,
|
||||||
@@ -106,7 +99,7 @@ export default function page() {
|
|||||||
playbackPosition?: string;
|
playbackPosition?: string;
|
||||||
}>();
|
}>();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const offline = offlineStr === "true";
|
const offline = offlineStr === "true";
|
||||||
const playbackManager = usePlaybackManager();
|
const playbackManager = usePlaybackManager();
|
||||||
|
|
||||||
@@ -571,7 +564,14 @@ export default function page() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: "blue",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -580,8 +580,6 @@ export default function page() {
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
paddingLeft: ignoreSafeAreas ? 0 : insets.left,
|
|
||||||
paddingRight: ignoreSafeAreas ? 0 : insets.right,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VlcPlayerView
|
<VlcPlayerView
|
||||||
@@ -625,13 +623,11 @@ export default function page() {
|
|||||||
isBuffering={isBuffering}
|
isBuffering={isBuffering}
|
||||||
showControls={showControls}
|
showControls={showControls}
|
||||||
setShowControls={setShowControls}
|
setShowControls={setShowControls}
|
||||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
|
||||||
ignoreSafeAreas={ignoreSafeAreas}
|
|
||||||
isVideoLoaded={isVideoLoaded}
|
isVideoLoaded={isVideoLoaded}
|
||||||
startPictureInPicture={videoRef.current?.startPictureInPicture}
|
startPictureInPicture={videoRef.current?.startPictureInPicture}
|
||||||
play={videoRef.current?.play}
|
play={videoRef.current?.play || (() => {})}
|
||||||
pause={videoRef.current?.pause}
|
pause={videoRef.current?.pause || (() => {})}
|
||||||
seek={videoRef.current?.seekTo}
|
seek={videoRef.current?.seekTo || (() => {})}
|
||||||
enableTrickplay={true}
|
enableTrickplay={true}
|
||||||
getAudioTracks={videoRef.current?.getAudioTracks}
|
getAudioTracks={videoRef.current?.getAudioTracks}
|
||||||
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
||||||
@@ -639,6 +635,12 @@ export default function page() {
|
|||||||
setSubtitleTrack={videoRef.current?.setSubtitleTrack}
|
setSubtitleTrack={videoRef.current?.setSubtitleTrack}
|
||||||
setSubtitleURL={videoRef.current?.setSubtitleURL}
|
setSubtitleURL={videoRef.current?.setSubtitleURL}
|
||||||
setAudioTrack={videoRef.current?.setAudioTrack}
|
setAudioTrack={videoRef.current?.setAudioTrack}
|
||||||
|
setVideoAspectRatio={videoRef.current?.setVideoAspectRatio}
|
||||||
|
setVideoScaleFactor={videoRef.current?.setVideoScaleFactor}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
scaleFactor={scaleFactor}
|
||||||
|
setAspectRatio={setAspectRatio}
|
||||||
|
setScaleFactor={setScaleFactor}
|
||||||
isVlc
|
isVlc
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -59,8 +59,13 @@ import { VideoProvider } from "./contexts/VideoContext";
|
|||||||
import DropdownView from "./dropdown/DropdownView";
|
import DropdownView from "./dropdown/DropdownView";
|
||||||
import { EpisodeList } from "./EpisodeList";
|
import { EpisodeList } from "./EpisodeList";
|
||||||
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
import NextEpisodeCountDownButton from "./NextEpisodeCountDownButton";
|
||||||
|
import { type ScaleFactor, ScaleFactorSelector } from "./ScaleFactorSelector";
|
||||||
import SkipButton from "./SkipButton";
|
import SkipButton from "./SkipButton";
|
||||||
import { useControlsTimeout } from "./useControlsTimeout";
|
import { useControlsTimeout } from "./useControlsTimeout";
|
||||||
|
import {
|
||||||
|
type AspectRatio,
|
||||||
|
AspectRatioSelector,
|
||||||
|
} from "./VideoScalingModeSelector";
|
||||||
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
import { VideoTouchOverlay } from "./VideoTouchOverlay";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -72,8 +77,7 @@ interface Props {
|
|||||||
progress: SharedValue<number>;
|
progress: SharedValue<number>;
|
||||||
isBuffering: boolean;
|
isBuffering: boolean;
|
||||||
showControls: boolean;
|
showControls: boolean;
|
||||||
ignoreSafeAreas?: boolean;
|
|
||||||
setIgnoreSafeAreas: Dispatch<SetStateAction<boolean>>;
|
|
||||||
enableTrickplay?: boolean;
|
enableTrickplay?: boolean;
|
||||||
togglePlay: () => void;
|
togglePlay: () => void;
|
||||||
setShowControls: (shown: boolean) => void;
|
setShowControls: (shown: boolean) => void;
|
||||||
@@ -89,6 +93,12 @@ interface Props {
|
|||||||
setSubtitleURL?: (url: string, customName: string) => void;
|
setSubtitleURL?: (url: string, customName: string) => void;
|
||||||
setSubtitleTrack?: (index: number) => void;
|
setSubtitleTrack?: (index: number) => void;
|
||||||
setAudioTrack?: (index: number) => void;
|
setAudioTrack?: (index: number) => void;
|
||||||
|
setVideoAspectRatio?: (aspectRatio: string | null) => Promise<void>;
|
||||||
|
setVideoScaleFactor?: (scaleFactor: number) => Promise<void>;
|
||||||
|
aspectRatio?: AspectRatio;
|
||||||
|
scaleFactor?: ScaleFactor;
|
||||||
|
setAspectRatio?: Dispatch<SetStateAction<AspectRatio>>;
|
||||||
|
setScaleFactor?: Dispatch<SetStateAction<ScaleFactor>>;
|
||||||
isVlc?: boolean;
|
isVlc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +118,6 @@ export const Controls: FC<Props> = ({
|
|||||||
cacheProgress,
|
cacheProgress,
|
||||||
showControls,
|
showControls,
|
||||||
setShowControls,
|
setShowControls,
|
||||||
ignoreSafeAreas,
|
|
||||||
setIgnoreSafeAreas,
|
|
||||||
mediaSource,
|
mediaSource,
|
||||||
isVideoLoaded,
|
isVideoLoaded,
|
||||||
getAudioTracks,
|
getAudioTracks,
|
||||||
@@ -117,6 +125,12 @@ export const Controls: FC<Props> = ({
|
|||||||
setSubtitleURL,
|
setSubtitleURL,
|
||||||
setSubtitleTrack,
|
setSubtitleTrack,
|
||||||
setAudioTrack,
|
setAudioTrack,
|
||||||
|
setVideoAspectRatio,
|
||||||
|
setVideoScaleFactor,
|
||||||
|
aspectRatio = "default",
|
||||||
|
scaleFactor = 1.0,
|
||||||
|
setAspectRatio,
|
||||||
|
setScaleFactor,
|
||||||
offline = false,
|
offline = false,
|
||||||
isVlc = false,
|
isVlc = false,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -631,10 +645,26 @@ export const Controls: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [settings, isPlaying, isVlc, play, seek]);
|
}, [settings, isPlaying, isVlc, play, seek]);
|
||||||
|
|
||||||
const toggleIgnoreSafeAreas = useCallback(() => {
|
const handleAspectRatioChange = useCallback(
|
||||||
setIgnoreSafeAreas((prev) => !prev);
|
async (newRatio: AspectRatio) => {
|
||||||
lightHapticFeedback();
|
if (!setAspectRatio || !setVideoAspectRatio) return;
|
||||||
}, []);
|
|
||||||
|
setAspectRatio(newRatio);
|
||||||
|
const aspectRatioString = newRatio === "default" ? null : newRatio;
|
||||||
|
await setVideoAspectRatio(aspectRatioString);
|
||||||
|
},
|
||||||
|
[setAspectRatio, setVideoAspectRatio],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScaleFactorChange = useCallback(
|
||||||
|
async (newScale: ScaleFactor) => {
|
||||||
|
if (!setScaleFactor || !setVideoScaleFactor) return;
|
||||||
|
|
||||||
|
setScaleFactor(newScale);
|
||||||
|
await setVideoScaleFactor(newScale);
|
||||||
|
},
|
||||||
|
[setScaleFactor, setVideoScaleFactor],
|
||||||
|
);
|
||||||
|
|
||||||
const switchOnEpisodeMode = useCallback(() => {
|
const switchOnEpisodeMode = useCallback(() => {
|
||||||
setEpisodeView(true);
|
setEpisodeView(true);
|
||||||
@@ -801,17 +831,17 @@ export const Controls: FC<Props> = ({
|
|||||||
<Ionicons name='play-skip-forward' size={24} color='white' />
|
<Ionicons name='play-skip-forward' size={24} color='white' />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{/* {mediaSource?.TranscodingUrl && ( */}
|
{/* Video Controls */}
|
||||||
<TouchableOpacity
|
<AspectRatioSelector
|
||||||
onPress={toggleIgnoreSafeAreas}
|
currentRatio={aspectRatio}
|
||||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
onRatioChange={handleAspectRatioChange}
|
||||||
>
|
disabled={!setVideoAspectRatio}
|
||||||
<Ionicons
|
/>
|
||||||
name={ignoreSafeAreas ? "contract-outline" : "expand"}
|
<ScaleFactorSelector
|
||||||
size={24}
|
currentScale={scaleFactor}
|
||||||
color='white'
|
onScaleChange={handleScaleFactorChange}
|
||||||
/>
|
disabled={!setVideoScaleFactor}
|
||||||
</TouchableOpacity>
|
/>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||||
|
|||||||
135
components/video-player/controls/ScaleFactorSelector.tsx
Normal file
135
components/video-player/controls/ScaleFactorSelector.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import React from "react";
|
||||||
|
import { Platform, TouchableOpacity } from "react-native";
|
||||||
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
|
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||||
|
|
||||||
|
export type ScaleFactor =
|
||||||
|
| 1.0
|
||||||
|
| 1.1
|
||||||
|
| 1.2
|
||||||
|
| 1.3
|
||||||
|
| 1.4
|
||||||
|
| 1.5
|
||||||
|
| 1.6
|
||||||
|
| 1.7
|
||||||
|
| 1.8
|
||||||
|
| 1.9
|
||||||
|
| 2.0;
|
||||||
|
|
||||||
|
interface ScaleFactorSelectorProps {
|
||||||
|
currentScale: ScaleFactor;
|
||||||
|
onScaleChange: (scale: ScaleFactor) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScaleFactorOption {
|
||||||
|
id: ScaleFactor;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCALE_FACTOR_OPTIONS: ScaleFactorOption[] = [
|
||||||
|
{
|
||||||
|
id: 1.0,
|
||||||
|
label: "1.0x",
|
||||||
|
description: "Original size",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.1,
|
||||||
|
label: "1.1x",
|
||||||
|
description: "10% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.2,
|
||||||
|
label: "1.2x",
|
||||||
|
description: "20% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.3,
|
||||||
|
label: "1.3x",
|
||||||
|
description: "30% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.4,
|
||||||
|
label: "1.4x",
|
||||||
|
description: "40% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.5,
|
||||||
|
label: "1.5x",
|
||||||
|
description: "50% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.6,
|
||||||
|
label: "1.6x",
|
||||||
|
description: "60% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.7,
|
||||||
|
label: "1.7x",
|
||||||
|
description: "70% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.8,
|
||||||
|
label: "1.8x",
|
||||||
|
description: "80% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1.9,
|
||||||
|
label: "1.9x",
|
||||||
|
description: "90% larger",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2.0,
|
||||||
|
label: "2.0x",
|
||||||
|
description: "Double size",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ScaleFactorSelector: React.FC<ScaleFactorSelectorProps> = ({
|
||||||
|
currentScale,
|
||||||
|
onScaleChange,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
|
// Hide on TV platforms since zeego doesn't support TV
|
||||||
|
if (Platform.isTV || !DropdownMenu) return null;
|
||||||
|
|
||||||
|
const handleScaleSelect = (scale: ScaleFactor) => {
|
||||||
|
onScaleChange(scale);
|
||||||
|
lightHapticFeedback();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={disabled}
|
||||||
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||||
|
style={{ opacity: disabled ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
<Ionicons name='search-outline' size={24} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Label>Scale Factor</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
|
{SCALE_FACTOR_OPTIONS.map((option) => (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
key={option.id}
|
||||||
|
value={currentScale === option.id ? "on" : "off"}
|
||||||
|
onValueChange={() => handleScaleSelect(option.id)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>{option.label}</DropdownMenu.ItemTitle>
|
||||||
|
<DropdownMenu.ItemIndicator />
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import React from "react";
|
||||||
|
import { Platform, TouchableOpacity } from "react-native";
|
||||||
|
import { useHaptic } from "@/hooks/useHaptic";
|
||||||
|
|
||||||
|
const DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null;
|
||||||
|
|
||||||
|
export type AspectRatio = "default" | "16:9" | "4:3" | "1:1" | "21:9";
|
||||||
|
|
||||||
|
interface AspectRatioSelectorProps {
|
||||||
|
currentRatio: AspectRatio;
|
||||||
|
onRatioChange: (ratio: AspectRatio) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AspectRatioOption {
|
||||||
|
id: AspectRatio;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ASPECT_RATIO_OPTIONS: AspectRatioOption[] = [
|
||||||
|
{
|
||||||
|
id: "default",
|
||||||
|
label: "Original",
|
||||||
|
description: "Use video's original aspect ratio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "16:9",
|
||||||
|
label: "16:9",
|
||||||
|
description: "Widescreen (most common)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4:3",
|
||||||
|
label: "4:3",
|
||||||
|
description: "Traditional TV format",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "1:1",
|
||||||
|
label: "1:1",
|
||||||
|
description: "Square format",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "21:9",
|
||||||
|
label: "21:9",
|
||||||
|
description: "Ultra-wide cinematic",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AspectRatioSelector: React.FC<AspectRatioSelectorProps> = ({
|
||||||
|
currentRatio,
|
||||||
|
onRatioChange,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const lightHapticFeedback = useHaptic("light");
|
||||||
|
|
||||||
|
// Hide on TV platforms since zeego doesn't support TV
|
||||||
|
if (Platform.isTV || !DropdownMenu) return null;
|
||||||
|
|
||||||
|
const handleRatioSelect = (ratio: AspectRatio) => {
|
||||||
|
onRatioChange(ratio);
|
||||||
|
lightHapticFeedback();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<TouchableOpacity
|
||||||
|
disabled={disabled}
|
||||||
|
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
|
||||||
|
style={{ opacity: disabled ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
<Ionicons name='crop-outline' size={24} color='white' />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Content>
|
||||||
|
<DropdownMenu.Label>Aspect Ratio</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Separator />
|
||||||
|
|
||||||
|
{ASPECT_RATIO_OPTIONS.map((option) => (
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
key={option.id}
|
||||||
|
value={currentRatio === option.id ? "on" : "off"}
|
||||||
|
onValueChange={() => handleRatioSelect(option.id)}
|
||||||
|
>
|
||||||
|
<DropdownMenu.ItemTitle>{option.label}</DropdownMenu.ItemTitle>
|
||||||
|
<DropdownMenu.ItemSubtitle>
|
||||||
|
{option.description}
|
||||||
|
</DropdownMenu.ItemSubtitle>
|
||||||
|
<DropdownMenu.ItemIndicator />
|
||||||
|
</DropdownMenu.CheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -92,7 +92,9 @@ export interface VlcPlayerViewRef {
|
|||||||
nextChapter: () => Promise<void>;
|
nextChapter: () => Promise<void>;
|
||||||
previousChapter: () => Promise<void>;
|
previousChapter: () => Promise<void>;
|
||||||
getChapters: () => Promise<ChapterInfo[] | null>;
|
getChapters: () => Promise<ChapterInfo[] | null>;
|
||||||
setVideoCropGeometry: (geometry: string | null) => Promise<void>;
|
setVideoCropGeometry: (cropGeometry: string | null) => Promise<void>;
|
||||||
getVideoCropGeometry: () => Promise<string | null>;
|
getVideoCropGeometry: () => Promise<string | null>;
|
||||||
setSubtitleURL: (url: string) => Promise<void>;
|
setSubtitleURL: (url: string) => Promise<void>;
|
||||||
|
setVideoAspectRatio: (aspectRatio: string | null) => Promise<void>;
|
||||||
|
setVideoScaleFactor: (scaleFactor: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
|||||||
setSubtitleURL: async (url: string) => {
|
setSubtitleURL: async (url: string) => {
|
||||||
await nativeRef.current?.setSubtitleURL(url);
|
await nativeRef.current?.setSubtitleURL(url);
|
||||||
},
|
},
|
||||||
|
setVideoAspectRatio: async (aspectRatio: string | null) => {
|
||||||
|
await nativeRef.current?.setVideoAspectRatio(aspectRatio);
|
||||||
|
},
|
||||||
|
setVideoScaleFactor: async (scaleFactor: number) => {
|
||||||
|
await nativeRef.current?.setVideoScaleFactor(scaleFactor);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ class VlcPlayerModule : Module() {
|
|||||||
AsyncFunction("setSubtitleURL") { view: VlcPlayerView, url: String, name: String ->
|
AsyncFunction("setSubtitleURL") { view: VlcPlayerView, url: String, name: String ->
|
||||||
view.setSubtitleURL(url, name)
|
view.setSubtitleURL(url, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setVideoAspectRatio") { view: VlcPlayerView, aspectRatio: String? ->
|
||||||
|
view.setVideoAspectRatio(aspectRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setVideoScaleFactor") { view: VlcPlayerView, scaleFactor: Float ->
|
||||||
|
view.setVideoScaleFactor(scaleFactor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,6 +335,16 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
mediaPlayer?.addSlave(IMedia.Slave.Type.Subtitle, Uri.parse(subtitleURL), true)
|
mediaPlayer?.addSlave(IMedia.Slave.Type.Subtitle, Uri.parse(subtitleURL), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setVideoAspectRatio(aspectRatio: String?) {
|
||||||
|
log.debug("Setting video aspect ratio: $aspectRatio")
|
||||||
|
mediaPlayer?.aspectRatio = aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVideoScaleFactor(scaleFactor: Float) {
|
||||||
|
log.debug("Setting video scale factor: $scaleFactor")
|
||||||
|
mediaPlayer?.scale = scaleFactor
|
||||||
|
}
|
||||||
|
|
||||||
private fun setInitialExternalSubtitles() {
|
private fun setInitialExternalSubtitles() {
|
||||||
externalSubtitles?.let { subtitles ->
|
externalSubtitles?.let { subtitles ->
|
||||||
for (subtitle in subtitles) {
|
for (subtitle in subtitles) {
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ public class VlcPlayerModule: Module {
|
|||||||
view.setSubtitleTrack(trackIndex)
|
view.setSubtitleTrack(trackIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setVideoAspectRatio") { (view: VlcPlayerView, aspectRatio: String?) in
|
||||||
|
view.setVideoAspectRatio(aspectRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("setVideoScaleFactor") { (view: VlcPlayerView, scaleFactor: Float) in
|
||||||
|
view.setVideoScaleFactor(scaleFactor)
|
||||||
|
}
|
||||||
|
|
||||||
AsyncFunction("getSubtitleTracks") { (view: VlcPlayerView) -> [[String: Any]]? in
|
AsyncFunction("getSubtitleTracks") { (view: VlcPlayerView) -> [[String: Any]]? in
|
||||||
return view.getSubtitleTracks()
|
return view.getSubtitleTracks()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,26 @@ class VlcPlayerView: ExpoView {
|
|||||||
return tracks
|
return tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func setVideoAspectRatio(_ aspectRatio: String?) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let aspectRatio = aspectRatio {
|
||||||
|
// Convert String to C string for VLC
|
||||||
|
let cString = strdup(aspectRatio)
|
||||||
|
self.mediaPlayer?.videoAspectRatio = cString
|
||||||
|
} else {
|
||||||
|
// Reset to default (let VLC determine aspect ratio)
|
||||||
|
self.mediaPlayer?.videoAspectRatio = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setVideoScaleFactor(_ scaleFactor: Float) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.mediaPlayer?.scaleFactor = scaleFactor
|
||||||
|
print("Set video scale factor: \(scaleFactor)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc func stop(completion: (() -> Void)? = nil) {
|
@objc func stop(completion: (() -> Void)? = nil) {
|
||||||
guard !isStopping else {
|
guard !isStopping else {
|
||||||
completion?()
|
completion?()
|
||||||
|
|||||||
Reference in New Issue
Block a user