mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
working
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -8,5 +8,8 @@
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"[swift]": {
|
||||
"editor.defaultFormatter": "sswg.swift-lang"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,394 +1,52 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { LargeMovieCarousel } from "@/components/home/LargeMovieCarousel";
|
||||
import { ScrollingCollectionList } from "@/components/home/ScrollingCollectionList";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { MediaListSection } from "@/components/medialists/MediaListSection";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { TAB_HEIGHT } from "@/constants/Values";
|
||||
import { hello, VlcPlayerView } from "@/modules/vlc-player";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { Api } from "@jellyfin/sdk";
|
||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import {
|
||||
getItemsApi,
|
||||
getSuggestionsApi,
|
||||
getTvShowsApi,
|
||||
getUserLibraryApi,
|
||||
getUserViewsApi,
|
||||
} from "@jellyfin/sdk/lib/utils/api";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { QueryFunction, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigation, useRouter } from "expo-router";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VlcPlayerViewRef,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
type ScrollingCollectionListSection = {
|
||||
type: "ScrollingCollectionList";
|
||||
title?: string;
|
||||
queryKey: (string | undefined | null)[];
|
||||
queryFn: QueryFunction<BaseItemDto[]>;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
};
|
||||
|
||||
type MediaListSection = {
|
||||
type: "MediaListSection";
|
||||
queryKey: (string | undefined)[];
|
||||
queryFn: QueryFunction<BaseItemDto>;
|
||||
};
|
||||
|
||||
type Section = ScrollingCollectionListSection | MediaListSection;
|
||||
|
||||
export default function index() {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [settings, _] = useSettings();
|
||||
|
||||
const [isConnected, setIsConnected] = useState<boolean | null>(null);
|
||||
const [loadingRetry, setLoadingRetry] = useState(false);
|
||||
|
||||
const { downloadedFiles } = useDownload();
|
||||
const navigation = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
router.push("/(auth)/downloads");
|
||||
}}
|
||||
className="p-2"
|
||||
>
|
||||
<Feather
|
||||
name="download"
|
||||
color={hasDownloads ? Colors.primary : "white"}
|
||||
size={22}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
});
|
||||
}, [downloadedFiles, navigation, router]);
|
||||
|
||||
const checkConnection = useCallback(async () => {
|
||||
setLoadingRetry(true);
|
||||
const state = await NetInfo.fetch();
|
||||
setIsConnected(state.isConnected);
|
||||
setLoadingRetry(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||
if (state.isConnected == false || state.isInternetReachable === false)
|
||||
setIsConnected(false);
|
||||
else setIsConnected(true);
|
||||
});
|
||||
|
||||
NetInfo.fetch().then((state) => {
|
||||
setIsConnected(state.isConnected);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const {
|
||||
data: userViews,
|
||||
isError: e1,
|
||||
isLoading: l1,
|
||||
} = useQuery({
|
||||
queryKey: ["home", "userViews", user?.Id],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await getUserViewsApi(api).getUserViews({
|
||||
userId: user.Id,
|
||||
});
|
||||
|
||||
return response.data.Items || null;
|
||||
},
|
||||
enabled: !!api && !!user?.Id,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const {
|
||||
data: mediaListCollections,
|
||||
isError: e2,
|
||||
isLoading: l2,
|
||||
} = useQuery({
|
||||
queryKey: ["home", "sf_promoted", user?.Id, settings?.usePopularPlugin],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) return [];
|
||||
|
||||
const response = await getItemsApi(api).getItems({
|
||||
userId: user.Id,
|
||||
tags: ["sf_promoted"],
|
||||
recursive: true,
|
||||
fields: ["Tags"],
|
||||
includeItemTypes: ["BoxSet"],
|
||||
});
|
||||
|
||||
return response.data.Items || [];
|
||||
},
|
||||
enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true,
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
const collections = useMemo(() => {
|
||||
const allow = ["movies", "tvshows"];
|
||||
return (
|
||||
userViews?.filter(
|
||||
(c) => c.CollectionType && allow.includes(c.CollectionType)
|
||||
) || []
|
||||
);
|
||||
}, [userViews]);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["home"],
|
||||
refetchType: "all",
|
||||
type: "all",
|
||||
exact: false,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["home"],
|
||||
refetchType: "all",
|
||||
type: "all",
|
||||
exact: false,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["item"],
|
||||
refetchType: "all",
|
||||
type: "all",
|
||||
exact: false,
|
||||
});
|
||||
setLoading(false);
|
||||
}, [queryClient]);
|
||||
|
||||
const createCollectionConfig = useCallback(
|
||||
(
|
||||
title: string,
|
||||
queryKey: string[],
|
||||
includeItemTypes: BaseItemKind[],
|
||||
parentId: string | undefined
|
||||
): ScrollingCollectionListSection => ({
|
||||
title,
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
if (!api) return [];
|
||||
return (
|
||||
(
|
||||
await getUserLibraryApi(api).getLatestMedia({
|
||||
userId: user?.Id,
|
||||
limit: 50,
|
||||
fields: ["PrimaryImageAspectRatio", "Path"],
|
||||
imageTypeLimit: 1,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
includeItemTypes,
|
||||
parentId,
|
||||
})
|
||||
).data || []
|
||||
);
|
||||
},
|
||||
type: "ScrollingCollectionList",
|
||||
}),
|
||||
[api, user?.Id]
|
||||
);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
if (!api || !user?.Id) return [];
|
||||
|
||||
const latestMediaViews = collections.map((c) => {
|
||||
const includeItemTypes: BaseItemKind[] =
|
||||
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
|
||||
const title = "Recently Added in " + c.Name;
|
||||
const queryKey = [
|
||||
"home",
|
||||
"recentlyAddedIn" + c.CollectionType,
|
||||
user?.Id!,
|
||||
c.Id!,
|
||||
];
|
||||
return createCollectionConfig(
|
||||
title || "",
|
||||
queryKey,
|
||||
includeItemTypes,
|
||||
c.Id
|
||||
);
|
||||
});
|
||||
|
||||
const ss: Section[] = [
|
||||
{
|
||||
title: "Continue Watching",
|
||||
queryKey: ["home", "resumeItems", user.Id],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getItemsApi(api).getResumeItems({
|
||||
userId: user.Id,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
includeItemTypes: ["Movie", "Series", "Episode"],
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
{
|
||||
title: "Next Up",
|
||||
queryKey: ["home", "nextUp-all", user?.Id],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getTvShowsApi(api).getNextUp({
|
||||
userId: user?.Id,
|
||||
fields: ["MediaSourceCount"],
|
||||
limit: 20,
|
||||
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||
enableResumable: false,
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
...latestMediaViews,
|
||||
...(mediaListCollections?.map(
|
||||
(ml) =>
|
||||
({
|
||||
title: ml.Name,
|
||||
queryKey: ["home", "mediaList", ml.Id!],
|
||||
queryFn: async () => ml,
|
||||
type: "MediaListSection",
|
||||
orientation: "vertical",
|
||||
} as Section)
|
||||
) || []),
|
||||
{
|
||||
title: "Suggested Movies",
|
||||
queryKey: ["home", "suggestedMovies", user?.Id],
|
||||
queryFn: async () =>
|
||||
(
|
||||
await getSuggestionsApi(api).getSuggestions({
|
||||
userId: user?.Id,
|
||||
limit: 10,
|
||||
mediaType: ["Video"],
|
||||
type: ["Movie"],
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "vertical",
|
||||
},
|
||||
{
|
||||
title: "Suggested Episodes",
|
||||
queryKey: ["home", "suggestedEpisodes", user?.Id],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const suggestions = await getSuggestions(api, user.Id);
|
||||
const nextUpPromises = suggestions.map((series) =>
|
||||
getNextUp(api, user.Id, series.Id)
|
||||
);
|
||||
const nextUpResults = await Promise.all(nextUpPromises);
|
||||
|
||||
return nextUpResults.filter((item) => item !== null) || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
];
|
||||
return ss;
|
||||
}, [api, user?.Id, collections, mediaListCollections]);
|
||||
|
||||
if (isConnected === false) {
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-full -mt-6 px-8">
|
||||
<Text className="text-3xl font-bold mb-2">No Internet</Text>
|
||||
<Text className="text-center opacity-70">
|
||||
No worries, you can still watch{"\n"}downloaded content.
|
||||
</Text>
|
||||
<View className="mt-4">
|
||||
<Button
|
||||
color="purple"
|
||||
onPress={() => router.push("/(auth)/downloads")}
|
||||
justify="center"
|
||||
iconRight={
|
||||
<Ionicons name="arrow-forward" size={20} color="white" />
|
||||
}
|
||||
>
|
||||
Go to downloads
|
||||
</Button>
|
||||
<Button
|
||||
color="black"
|
||||
onPress={() => {
|
||||
checkConnection();
|
||||
}}
|
||||
justify="center"
|
||||
className="mt-2"
|
||||
iconRight={
|
||||
loadingRetry ? null : (
|
||||
<Ionicons name="refresh" size={20} color="white" />
|
||||
)
|
||||
}
|
||||
>
|
||||
{loadingRetry ? (
|
||||
<ActivityIndicator size={"small"} color={"white"} />
|
||||
) : (
|
||||
"Retry"
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
const [playbackState, setPlaybackState] = useState<
|
||||
PlaybackStatePayload["nativeEvent"] | null
|
||||
>(null);
|
||||
const [progress, setProgress] = useState<
|
||||
ProgressUpdatePayload["nativeEvent"] | null
|
||||
>(null);
|
||||
|
||||
if (e1 || e2)
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-full -mt-6">
|
||||
<Text className="text-3xl font-bold mb-2">Oops!</Text>
|
||||
<Text className="text-center opacity-70">
|
||||
Something went wrong.{"\n"}Please log out and in again.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
useEffect(() => {
|
||||
videoRef.current?.play();
|
||||
}, []);
|
||||
|
||||
if (l1 || l2)
|
||||
return (
|
||||
<View className="justify-center items-center h-full">
|
||||
<Loader />
|
||||
</View>
|
||||
);
|
||||
const onProgress = (event: ProgressUpdatePayload) => {
|
||||
const { currentTime, duration } = event.nativeEvent;
|
||||
console.log(`Current Time: ${currentTime}, Duration: ${duration}`);
|
||||
setProgress(event.nativeEvent);
|
||||
};
|
||||
|
||||
const onPlaybackStateChanged = (event: PlaybackStatePayload) => {
|
||||
const { isBuffering, currentTime, duration, target, type } =
|
||||
event.nativeEvent;
|
||||
console.log("onVideoStateChange", {
|
||||
isBuffering,
|
||||
currentTime,
|
||||
duration,
|
||||
target,
|
||||
type,
|
||||
});
|
||||
setPlaybackState(event.nativeEvent);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={loading} onRefresh={refetch} />
|
||||
}
|
||||
key={"home"}
|
||||
contentContainerStyle={{
|
||||
paddingLeft: insets.left,
|
||||
@@ -400,36 +58,63 @@ export default function index() {
|
||||
}}
|
||||
>
|
||||
<View className="flex flex-col space-y-4">
|
||||
<Text>{hello()}</Text>
|
||||
<VlcPlayerView source="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" />
|
||||
<VlcPlayerView
|
||||
ref={videoRef}
|
||||
source={{
|
||||
uri: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||
autoplay: true,
|
||||
isNetwork: true,
|
||||
}}
|
||||
style={{ width: "100%", height: 300 }}
|
||||
onVideoProgress={onProgress}
|
||||
progressUpdateInterval={2000}
|
||||
onVideoStateChange={onPlaybackStateChanged}
|
||||
/>
|
||||
<VideoDebugInfo playbackState={playbackState} progress={progress} />
|
||||
</View>
|
||||
<Button
|
||||
title="pause"
|
||||
onPress={() => {
|
||||
videoRef.current?.pause();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="play"
|
||||
onPress={() => {
|
||||
videoRef.current?.play();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="seek to 10 seconds"
|
||||
onPress={() => {
|
||||
videoRef.current?.seekTo(10);
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
// Function to get suggestions
|
||||
async function getSuggestions(api: Api, userId: string | undefined) {
|
||||
if (!userId) return [];
|
||||
const response = await getSuggestionsApi(api).getSuggestions({
|
||||
userId,
|
||||
limit: 10,
|
||||
mediaType: ["Unknown"],
|
||||
type: ["Series"],
|
||||
});
|
||||
return response.data.Items ?? [];
|
||||
}
|
||||
|
||||
// Function to get the next up TV show for a series
|
||||
async function getNextUp(
|
||||
api: Api,
|
||||
userId: string | undefined,
|
||||
seriesId: string | undefined
|
||||
) {
|
||||
if (!userId || !seriesId) return null;
|
||||
const response = await getTvShowsApi(api).getNextUp({
|
||||
userId,
|
||||
seriesId,
|
||||
limit: 1,
|
||||
});
|
||||
return response.data.Items?.[0] ?? null;
|
||||
}
|
||||
const VideoDebugInfo: React.FC<{
|
||||
playbackState: PlaybackStatePayload["nativeEvent"] | null;
|
||||
progress: ProgressUpdatePayload["nativeEvent"] | null;
|
||||
}> = ({ playbackState, progress }) => (
|
||||
<View className="p-2.5 bg-black mt-2.5">
|
||||
<Text className="font-bold">Playback State:</Text>
|
||||
{playbackState && (
|
||||
<>
|
||||
<Text>Type: {playbackState.type}</Text>
|
||||
<Text>Current Time: {playbackState.currentTime}</Text>
|
||||
<Text>Duration: {playbackState.duration}</Text>
|
||||
<Text>Is Buffering: {playbackState.isBuffering ? "Yes" : "No"}</Text>
|
||||
<Text>Target: {playbackState.target}</Text>
|
||||
</>
|
||||
)}
|
||||
<Text className="font-bold mt-2.5">Progress:</Text>
|
||||
{progress && (
|
||||
<>
|
||||
<Text>Current Time: {progress.currentTime}</Text>
|
||||
<Text>Duration: {progress.duration.toFixed(2)}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,88 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class VlcPlayerModule: Module {
|
||||
// Each module class must implement the definition function. The definition consists of components
|
||||
// that describes the module's functionality and behavior.
|
||||
// See https://docs.expo.dev/modules/module-api for more details about available components.
|
||||
public func definition() -> ModuleDefinition {
|
||||
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
|
||||
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
|
||||
// The module will be accessible from `requireNativeModule('VlcPlayer')` in JavaScript.
|
||||
Name("VlcPlayer")
|
||||
View(VlcPlayerView.self) {
|
||||
Prop("source") { (view: VlcPlayerView, source: String) in
|
||||
view.setSource(source)
|
||||
}
|
||||
}
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("VlcPlayer")
|
||||
View(VlcPlayerView.self) {
|
||||
Prop("source") { (view: VlcPlayerView, source: [String: Any]) in
|
||||
view.setSource(source)
|
||||
}
|
||||
|
||||
Function("hello") {
|
||||
return "hello from native ios"
|
||||
Prop("progressUpdateInterval") { (view: VlcPlayerView, interval: Double) in
|
||||
view.setProgressUpdateInterval(interval)
|
||||
}
|
||||
|
||||
Prop("paused") { (view: VlcPlayerView, paused: Bool) in
|
||||
if paused {
|
||||
view.pause()
|
||||
} else {
|
||||
view.play()
|
||||
}
|
||||
}
|
||||
|
||||
Prop("muted") { (view: VlcPlayerView, muted: Bool) in
|
||||
view.setMuted(muted)
|
||||
}
|
||||
|
||||
Prop("volume") { (view: VlcPlayerView, volume: Int) in
|
||||
view.setVolume(volume)
|
||||
}
|
||||
|
||||
Prop("videoAspectRatio") { (view: VlcPlayerView, ratio: String) in
|
||||
view.setVideoAspectRatio(ratio)
|
||||
}
|
||||
|
||||
Events(
|
||||
"onProgress",
|
||||
"onPlaybackStateChanged",
|
||||
"onVideoLoadStart",
|
||||
"onVideoStateChange",
|
||||
"onVideoProgress"
|
||||
)
|
||||
|
||||
AsyncFunction("play") { (view: VlcPlayerView) in
|
||||
view.play()
|
||||
}
|
||||
|
||||
AsyncFunction("pause") { (view: VlcPlayerView) in
|
||||
view.pause()
|
||||
}
|
||||
|
||||
AsyncFunction("seekTo") { (view: VlcPlayerView, time: Double) in
|
||||
view.seekTo(time)
|
||||
}
|
||||
|
||||
AsyncFunction("jumpBackward") { (view: VlcPlayerView, interval: Int) in
|
||||
view.jumpBackward(interval)
|
||||
}
|
||||
|
||||
AsyncFunction("jumpForward") { (view: VlcPlayerView, interval: Int) in
|
||||
view.jumpForward(interval)
|
||||
}
|
||||
|
||||
AsyncFunction("setAudioTrack") { (view: VlcPlayerView, trackIndex: Int) in
|
||||
view.setAudioTrack(trackIndex)
|
||||
}
|
||||
|
||||
AsyncFunction("getAudioTracks") { (view: VlcPlayerView) -> [[String: Any]]? in
|
||||
return view.getAudioTracks()
|
||||
}
|
||||
|
||||
AsyncFunction("setSubtitleTrack") { (view: VlcPlayerView, trackIndex: Int) in
|
||||
view.setSubtitleTrack(trackIndex)
|
||||
}
|
||||
|
||||
AsyncFunction("getSubtitleTracks") { (view: VlcPlayerView) -> [[String: Any]]? in
|
||||
return view.getSubtitleTracks()
|
||||
}
|
||||
|
||||
AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in
|
||||
view.setVideoCropGeometry(geometry)
|
||||
}
|
||||
|
||||
AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in
|
||||
return view.getVideoCropGeometry()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,413 @@
|
||||
import ExpoModulesCore
|
||||
import UIKit
|
||||
import MobileVLCKit
|
||||
import UIKit
|
||||
|
||||
class VlcPlayerView: ExpoView, VLCMediaPlayerDelegate {
|
||||
private var mediaPlayer: VLCMediaPlayer?
|
||||
private var movieView: UIView?
|
||||
class VlcPlayerView: ExpoView {
|
||||
private var mediaPlayer: VLCMediaPlayer?
|
||||
private var videoView: UIView?
|
||||
private var progressUpdateTimer: Timer?
|
||||
private var progressUpdateInterval: TimeInterval = 0.5
|
||||
private var isPaused: Bool = false
|
||||
private var currentGeometryCString: [CChar]?
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
DispatchQueue.main.async {
|
||||
self.setupView()
|
||||
self.backgroundColor = UIColor.black // Set background color to black
|
||||
// MARK: - Initialization
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
setupView()
|
||||
setupNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
DispatchQueue.main.async {
|
||||
self.movieView = UIView()
|
||||
self.movieView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
// MARK: - Setup
|
||||
|
||||
if let movieView = self.movieView {
|
||||
self.addSubview(movieView)
|
||||
NSLayoutConstraint.activate([
|
||||
movieView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
movieView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
movieView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
movieView.bottomAnchor.constraint(equalTo: self.bottomAnchor)
|
||||
])
|
||||
}
|
||||
private func setupView() {
|
||||
DispatchQueue.main.async {
|
||||
self.backgroundColor = .black
|
||||
self.videoView = UIView()
|
||||
self.videoView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.setupMediaPlayer()
|
||||
if let videoView = self.videoView {
|
||||
self.addSubview(videoView)
|
||||
NSLayoutConstraint.activate([
|
||||
videoView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
||||
videoView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
||||
videoView.topAnchor.constraint(equalTo: self.topAnchor),
|
||||
videoView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
self.setupMediaPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupMediaPlayer() {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer = VLCMediaPlayer()
|
||||
self.mediaPlayer?.delegate = self
|
||||
self.mediaPlayer?.drawable = self.movieView
|
||||
print("Media player setup on main thread: \(Thread.isMainThread)")
|
||||
private func setupMediaPlayer() {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer = VLCMediaPlayer()
|
||||
self.mediaPlayer?.delegate = self
|
||||
self.mediaPlayer?.drawable = self.videoView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSource(_ source: String) {
|
||||
DispatchQueue.main.async {
|
||||
print("Setting media source on main thread: \(Thread.isMainThread)")
|
||||
if let url = URL(string: source) {
|
||||
self.mediaPlayer?.media = VLCMedia(url: url)
|
||||
print("Media set, now playing...")
|
||||
private func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(applicationWillResignActive),
|
||||
name: UIApplication.willResignActiveNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(applicationWillEnterForeground),
|
||||
name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
@objc func play() {
|
||||
self.mediaPlayer?.play()
|
||||
} else {
|
||||
print("Invalid URL.")
|
||||
}
|
||||
self.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handlePlayPause() {
|
||||
DispatchQueue.main.async {
|
||||
print("Handling play/pause on main thread: \(Thread.isMainThread)")
|
||||
if self.mediaPlayer?.isPlaying == true {
|
||||
@objc func pause() {
|
||||
self.mediaPlayer?.pause()
|
||||
} else {
|
||||
self.mediaPlayer?.play()
|
||||
}
|
||||
self.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||
DispatchQueue.main.async {
|
||||
print("Media player state changed on main thread: \(Thread.isMainThread)")
|
||||
if self.mediaPlayer?.state == .stopped {
|
||||
print("Media player stopped")
|
||||
}
|
||||
@objc func seekTo(_ time: Double) {
|
||||
self.mediaPlayer?.time = VLCTime(int: Int32(time * 1000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSource(_ source: [String: Any]) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.stop()
|
||||
self.mediaPlayer = nil
|
||||
|
||||
let mediaOptions = source["mediaOptions"] as? [String: Any]
|
||||
let initOptions = source["initOptions"] as? [Any]
|
||||
let uri = source["uri"] as? String
|
||||
let initType = source["initType"] as? Int ?? 0
|
||||
let autoplay = source["autoplay"] as? Bool ?? false
|
||||
let isNetwork = source["isNetwork"] as? Bool ?? false
|
||||
|
||||
guard let uri = uri, !uri.isEmpty else { return }
|
||||
|
||||
if initType == 2, let options = initOptions {
|
||||
self.mediaPlayer = VLCMediaPlayer(options: options)
|
||||
} else {
|
||||
self.mediaPlayer = VLCMediaPlayer()
|
||||
}
|
||||
|
||||
self.mediaPlayer?.delegate = self
|
||||
self.mediaPlayer?.drawable = self.videoView
|
||||
self.mediaPlayer?.scaleFactor = 0
|
||||
|
||||
let media: VLCMedia
|
||||
if isNetwork {
|
||||
media = VLCMedia(url: URL(string: uri)!)
|
||||
} else {
|
||||
media = VLCMedia(path: uri)
|
||||
}
|
||||
|
||||
media.delegate = self
|
||||
if let mediaOptions = mediaOptions {
|
||||
media.addOptions(mediaOptions)
|
||||
}
|
||||
|
||||
// Parse the media asynchronously
|
||||
media.parse()
|
||||
self.mediaPlayer?.media = media
|
||||
|
||||
if autoplay {
|
||||
self.play()
|
||||
}
|
||||
|
||||
self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()])
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setProgressUpdateInterval(_ interval: Double) {
|
||||
progressUpdateInterval = TimeInterval(interval / 1000.0)
|
||||
updateProgressTimer()
|
||||
}
|
||||
|
||||
@objc func jumpBackward(_ interval: Int) {
|
||||
mediaPlayer?.jumpBackward(Int32(interval))
|
||||
}
|
||||
|
||||
@objc func jumpForward(_ interval: Int) {
|
||||
mediaPlayer?.jumpForward(Int32(interval))
|
||||
}
|
||||
|
||||
@objc func setMuted(_ muted: Bool) {
|
||||
mediaPlayer?.audio?.isMuted = muted
|
||||
}
|
||||
|
||||
@objc func setVolume(_ volume: Int) {
|
||||
mediaPlayer?.audio?.volume = Int32(volume)
|
||||
}
|
||||
|
||||
@objc func setVideoAspectRatio(_ ratio: String) {
|
||||
DispatchQueue.main.async {
|
||||
ratio.withCString { cString in
|
||||
self.mediaPlayer?.videoAspectRatio = UnsafeMutablePointer(mutating: cString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setAudioTrack(_ trackIndex: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getAudioTracks() -> [[String: Any]]? {
|
||||
guard let trackNames = mediaPlayer?.audioTrackNames,
|
||||
let trackIndexes = mediaPlayer?.audioTrackIndexes
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return zip(trackNames, trackIndexes).map { name, index in
|
||||
return ["name": name, "index": index]
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
||||
guard let trackNames = mediaPlayer?.videoSubTitlesNames,
|
||||
let trackIndexes = mediaPlayer?.videoSubTitlesIndexes
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return zip(trackNames, trackIndexes).map { name, index in
|
||||
return ["name": name, "index": index]
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setSubtitleDelay(_ delay: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.currentVideoSubTitleDelay = NSInteger(delay)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setAudioDelay(_ delay: Int) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.currentAudioPlaybackDelay = NSInteger(delay)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func takeSnapshot(_ path: String, width: Int, height: Int) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.mediaPlayer?.saveVideoSnapshot(
|
||||
at: path, withWidth: Int32(width), andHeight: Int32(height))
|
||||
}
|
||||
}
|
||||
|
||||
@objc func setVideoCropGeometry(_ geometry: String?) {
|
||||
DispatchQueue.main.async {
|
||||
if let geometry = geometry, !geometry.isEmpty {
|
||||
self.currentGeometryCString = geometry.cString(using: .utf8)
|
||||
self.currentGeometryCString?.withUnsafeMutableBufferPointer { buffer in
|
||||
self.mediaPlayer?.videoCropGeometry = buffer.baseAddress
|
||||
}
|
||||
} else {
|
||||
self.currentGeometryCString = nil
|
||||
self.mediaPlayer?.videoCropGeometry = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getVideoCropGeometry() -> String? {
|
||||
guard let cString = mediaPlayer?.videoCropGeometry else {
|
||||
return nil
|
||||
}
|
||||
return String(cString: cString)
|
||||
}
|
||||
|
||||
@objc func setRate(_ rate: Float) {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.rate = rate
|
||||
}
|
||||
}
|
||||
|
||||
@objc func nextChapter() {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.nextChapter()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func previousChapter() {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.previousChapter()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getChapters() -> [[String: Any]]? {
|
||||
guard let currentTitleIndex = mediaPlayer?.currentTitleIndex,
|
||||
let chapters = mediaPlayer?.chapterDescriptions(ofTitle: currentTitleIndex)
|
||||
as? [[String: Any]]
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return chapters.compactMap { chapter in
|
||||
guard let name = chapter[VLCChapterDescriptionName] as? String,
|
||||
let timeOffset = chapter[VLCChapterDescriptionTimeOffset] as? NSNumber,
|
||||
let duration = chapter[VLCChapterDescriptionDuration] as? NSNumber
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return [
|
||||
"name": name,
|
||||
"timeOffset": timeOffset.doubleValue,
|
||||
"duration": duration.doubleValue,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func updateProgressTimer() {
|
||||
progressUpdateTimer?.invalidate()
|
||||
progressUpdateTimer = Timer.scheduledTimer(
|
||||
withTimeInterval: progressUpdateInterval, repeats: true
|
||||
) { [weak self] _ in
|
||||
self?.sendProgressUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
private func sendProgressUpdate() {
|
||||
DispatchQueue.main.async {
|
||||
guard let player = self.mediaPlayer else { return }
|
||||
let currentTime = player.time.intValue
|
||||
let duration = player.media?.length.intValue ?? 0
|
||||
let progress: [String: Any] = [
|
||||
"currentTime": currentTime,
|
||||
"duration": duration,
|
||||
]
|
||||
self.onVideoProgress?(progress)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func applicationWillResignActive() {
|
||||
if !isPaused {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func applicationWillEnterForeground() {
|
||||
if !isPaused {
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
private func release() {
|
||||
DispatchQueue.main.async {
|
||||
self.mediaPlayer?.stop()
|
||||
self.mediaPlayer = nil
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Expo Events
|
||||
|
||||
@objc var onProgress: RCTDirectEventBlock?
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
extension VlcPlayerView: VLCMediaPlayerDelegate {
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||
DispatchQueue.main.async {
|
||||
guard let player = self.mediaPlayer else { return }
|
||||
|
||||
let state = player.state
|
||||
var stateInfo: [String: Any] = [
|
||||
"target": self.reactTag ?? NSNull(),
|
||||
"currentTime": player.time.intValue,
|
||||
"duration": player.media?.length.intValue ?? 0,
|
||||
]
|
||||
|
||||
switch state {
|
||||
case .opening:
|
||||
stateInfo["type"] = "Opening"
|
||||
case .paused:
|
||||
self.isPaused = true
|
||||
stateInfo["type"] = "Paused"
|
||||
case .stopped:
|
||||
stateInfo["type"] = "Stopped"
|
||||
case .buffering:
|
||||
if player.isPlaying {
|
||||
// If the player is actually playing while in buffering state,
|
||||
// we'll report it as "Playing"
|
||||
self.isPaused = false
|
||||
stateInfo["type"] = "Playing"
|
||||
} else {
|
||||
stateInfo["type"] = "Buffering"
|
||||
stateInfo["isBuffering"] = true
|
||||
}
|
||||
case .playing:
|
||||
self.isPaused = false
|
||||
stateInfo["type"] = "Playing"
|
||||
case .esAdded:
|
||||
stateInfo["type"] = "ESAdded"
|
||||
case .ended:
|
||||
print("VLCMediaPlayerStateEnded")
|
||||
stateInfo["type"] = "Ended"
|
||||
case .error:
|
||||
stateInfo["type"] = "Error"
|
||||
self.release()
|
||||
@unknown default:
|
||||
stateInfo["type"] = "Unknown"
|
||||
}
|
||||
|
||||
self.onVideoStateChange?(stateInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||
updateVideoProgress()
|
||||
}
|
||||
|
||||
private func updateVideoProgress() {
|
||||
DispatchQueue.main.async {
|
||||
guard let player = self.mediaPlayer else { return }
|
||||
|
||||
let currentTime = player.time.intValue
|
||||
let duration = player.media?.length.intValue ?? 0
|
||||
|
||||
if currentTime >= 0 && currentTime < duration {
|
||||
self.onVideoProgress?([
|
||||
"target": self.reactTag ?? NSNull(),
|
||||
"currentTime": currentTime,
|
||||
"duration": duration,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VlcPlayerView: VLCMediaDelegate {
|
||||
func mediaMetaDataDidChange(_ aMedia: VLCMedia) {
|
||||
// Implement if needed
|
||||
}
|
||||
|
||||
func mediaDidFinishParsing(_ aMedia: VLCMedia) {
|
||||
DispatchQueue.main.async {
|
||||
let duration = aMedia.length.intValue
|
||||
self.onVideoStateChange?(["type": "MediaParsed", "duration": duration])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,89 @@
|
||||
export type ChangeEventPayload = {
|
||||
value: string;
|
||||
export type PlaybackStatePayload = {
|
||||
nativeEvent: {
|
||||
target: number;
|
||||
type:
|
||||
| "Opening"
|
||||
| "Paused"
|
||||
| "Stopped"
|
||||
| "Buffering"
|
||||
| "Playing"
|
||||
| "ESAdded"
|
||||
| "Ended"
|
||||
| "Error"
|
||||
| "Unknown";
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isBuffering?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProgressUpdatePayload = {
|
||||
nativeEvent: {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type VideoLoadStartPayload = {
|
||||
nativeEvent: {
|
||||
target: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type VideoStateChangePayload = PlaybackStatePayload;
|
||||
|
||||
export type VideoProgressPayload = ProgressUpdatePayload;
|
||||
|
||||
export type VlcPlayerSource = {
|
||||
uri: string;
|
||||
type?: string;
|
||||
isNetwork?: boolean;
|
||||
autoplay?: boolean;
|
||||
initOptions?: any[];
|
||||
mediaOptions?: { [key: string]: any };
|
||||
};
|
||||
|
||||
export type TrackInfo = {
|
||||
name: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type ChapterInfo = {
|
||||
name: string;
|
||||
timeOffset: number;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
export type VlcPlayerViewProps = {
|
||||
source: string;
|
||||
source: VlcPlayerSource;
|
||||
style?: Object;
|
||||
progressUpdateInterval?: number;
|
||||
paused?: boolean;
|
||||
muted?: boolean;
|
||||
volume?: number;
|
||||
videoAspectRatio?: string;
|
||||
onVideoProgress?: (event: ProgressUpdatePayload) => void;
|
||||
onVideoStateChange?: (event: PlaybackStatePayload) => void;
|
||||
onVideoLoadStart?: (event: VideoLoadStartPayload) => void;
|
||||
};
|
||||
|
||||
export interface VlcPlayerViewRef {
|
||||
play: () => Promise<void>;
|
||||
pause: () => Promise<void>;
|
||||
seekTo: (time: number) => Promise<void>;
|
||||
jumpBackward: (interval: number) => Promise<void>;
|
||||
jumpForward: (interval: number) => Promise<void>;
|
||||
setAudioTrack: (trackIndex: number) => Promise<void>;
|
||||
getAudioTracks: () => Promise<TrackInfo[] | null>;
|
||||
setSubtitleTrack: (trackIndex: number) => Promise<void>;
|
||||
getSubtitleTracks: () => Promise<TrackInfo[] | null>;
|
||||
setSubtitleDelay: (delay: number) => Promise<void>;
|
||||
setAudioDelay: (delay: number) => Promise<void>;
|
||||
takeSnapshot: (path: string, width: number, height: number) => Promise<void>;
|
||||
setRate: (rate: number) => Promise<void>;
|
||||
nextChapter: () => Promise<void>;
|
||||
previousChapter: () => Promise<void>;
|
||||
getChapters: () => Promise<ChapterInfo[] | null>;
|
||||
setVideoCropGeometry: (geometry: string | null) => Promise<void>;
|
||||
getVideoCropGeometry: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { EventEmitter } from 'expo-modules-core';
|
||||
|
||||
const emitter = new EventEmitter({} as any);
|
||||
|
||||
export default {
|
||||
PI: Math.PI,
|
||||
async setValueAsync(value: string): Promise<void> {
|
||||
emitter.emit('onChange', { value });
|
||||
},
|
||||
hello() {
|
||||
return 'Hello world! 👋';
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,124 @@
|
||||
import { requireNativeViewManager } from "expo-modules-core";
|
||||
import * as React from "react";
|
||||
|
||||
import { VlcPlayerViewProps } from "./VlcPlayer.types";
|
||||
import {
|
||||
VlcPlayerViewProps,
|
||||
VlcPlayerViewRef,
|
||||
VlcPlayerSource,
|
||||
TrackInfo,
|
||||
ChapterInfo,
|
||||
} from "./VlcPlayer.types";
|
||||
|
||||
const NativeView: React.ComponentType<VlcPlayerViewProps> =
|
||||
requireNativeViewManager("VlcPlayer");
|
||||
|
||||
export default function VlcPlayerView(props: VlcPlayerViewProps) {
|
||||
return <NativeView {...props} />;
|
||||
interface NativeViewRef extends VlcPlayerViewRef {
|
||||
setNativeProps?: (props: Partial<VlcPlayerViewProps>) => void;
|
||||
}
|
||||
|
||||
const NativeViewManager = requireNativeViewManager("VlcPlayer");
|
||||
|
||||
// Create a forwarded ref version of the native view
|
||||
const NativeView = React.forwardRef<NativeViewRef, VlcPlayerViewProps>(
|
||||
(props, ref) => <NativeViewManager {...props} ref={ref} />
|
||||
);
|
||||
|
||||
const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
(props, ref) => {
|
||||
const nativeRef = React.useRef<NativeViewRef>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
play: async () => {
|
||||
await nativeRef.current?.play();
|
||||
},
|
||||
pause: async () => {
|
||||
await nativeRef.current?.pause();
|
||||
},
|
||||
seekTo: async (time: number) => {
|
||||
await nativeRef.current?.seekTo(time);
|
||||
},
|
||||
jumpBackward: async (interval: number) => {
|
||||
await nativeRef.current?.jumpBackward(interval);
|
||||
},
|
||||
jumpForward: async (interval: number) => {
|
||||
await nativeRef.current?.jumpForward(interval);
|
||||
},
|
||||
setAudioTrack: async (trackIndex: number) => {
|
||||
await nativeRef.current?.setAudioTrack(trackIndex);
|
||||
},
|
||||
getAudioTracks: async () => {
|
||||
const tracks = await nativeRef.current?.getAudioTracks();
|
||||
return tracks ?? null;
|
||||
},
|
||||
setSubtitleTrack: async (trackIndex: number) => {
|
||||
await nativeRef.current?.setSubtitleTrack(trackIndex);
|
||||
},
|
||||
getSubtitleTracks: async () => {
|
||||
const tracks = await nativeRef.current?.getSubtitleTracks();
|
||||
return tracks ?? null;
|
||||
},
|
||||
setSubtitleDelay: async (delay: number) => {
|
||||
await nativeRef.current?.setSubtitleDelay(delay);
|
||||
},
|
||||
setAudioDelay: async (delay: number) => {
|
||||
await nativeRef.current?.setAudioDelay(delay);
|
||||
},
|
||||
takeSnapshot: async (path: string, width: number, height: number) => {
|
||||
await nativeRef.current?.takeSnapshot(path, width, height);
|
||||
},
|
||||
setRate: async (rate: number) => {
|
||||
await nativeRef.current?.setRate(rate);
|
||||
},
|
||||
nextChapter: async () => {
|
||||
await nativeRef.current?.nextChapter();
|
||||
},
|
||||
previousChapter: async () => {
|
||||
await nativeRef.current?.previousChapter();
|
||||
},
|
||||
getChapters: async () => {
|
||||
const chapters = await nativeRef.current?.getChapters();
|
||||
return chapters ?? null;
|
||||
},
|
||||
setVideoCropGeometry: async (geometry: string | null) => {
|
||||
await nativeRef.current?.setVideoCropGeometry(geometry);
|
||||
},
|
||||
getVideoCropGeometry: async () => {
|
||||
const geometry = await nativeRef.current?.getVideoCropGeometry();
|
||||
return geometry ?? null;
|
||||
},
|
||||
}));
|
||||
|
||||
const {
|
||||
source,
|
||||
style,
|
||||
progressUpdateInterval = 500,
|
||||
paused,
|
||||
muted,
|
||||
volume,
|
||||
videoAspectRatio,
|
||||
onVideoLoadStart,
|
||||
onVideoStateChange,
|
||||
onVideoProgress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const processedSource: VlcPlayerSource =
|
||||
typeof source === "string" ? { uri: source } : source;
|
||||
|
||||
return (
|
||||
<NativeView
|
||||
{...otherProps}
|
||||
ref={nativeRef}
|
||||
source={processedSource}
|
||||
style={[{ width: "100%", height: "100%" }, style]}
|
||||
progressUpdateInterval={progressUpdateInterval}
|
||||
paused={paused}
|
||||
muted={muted}
|
||||
volume={volume}
|
||||
videoAspectRatio={videoAspectRatio}
|
||||
onVideoLoadStart={onVideoLoadStart}
|
||||
onVideoStateChange={onVideoStateChange}
|
||||
onVideoProgress={onVideoProgress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default VlcPlayerView;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { VlcPlayerViewProps } from './VlcPlayer.types';
|
||||
|
||||
export default function VlcPlayerView(props: VlcPlayerViewProps) {
|
||||
return (
|
||||
<div>
|
||||
<span>{props.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user