mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
ios VLCKit 4.0 & All platform PiP support
This commit is contained in:
20
app.json
20
app.json
@@ -106,11 +106,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
["react-native-bottom-tabs"],
|
[
|
||||||
["./plugins/withChangeNativeAndroidTextToWhite.js"],
|
"react-native-bottom-tabs"
|
||||||
["./plugins/withGoogleCastActivity.js"],
|
],
|
||||||
["./plugins/withTrustLocalCerts.js"],
|
[
|
||||||
["./plugins/withGradleProperties.js"],
|
"./plugins/withChangeNativeAndroidTextToWhite.js"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"./plugins/withAndroidManifest.js"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"./plugins/withTrustLocalCerts.js"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"./plugins/withGradleProperties.js"
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import { Text } from "@/components/common/Text";
|
|||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { Controls } from "@/components/video-player/controls/Controls";
|
import { Controls } from "@/components/video-player/controls/Controls";
|
||||||
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
import { getDownloadedFileUrl } from "@/hooks/useDownloadedFileOpener";
|
||||||
import { useOrientation } from "@/hooks/useOrientation";
|
|
||||||
import { useOrientationSettings } from "@/hooks/useOrientationSettings";
|
|
||||||
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache";
|
||||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
import { VlcPlayerView } from "@/modules/vlc-player";
|
||||||
import {
|
import {
|
||||||
|
PipStartedPayload,
|
||||||
PlaybackStatePayload,
|
PlaybackStatePayload,
|
||||||
ProgressUpdatePayload,
|
ProgressUpdatePayload,
|
||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
@@ -18,13 +17,10 @@ const downloadProvider = !Platform.isTV
|
|||||||
? require("@/providers/DownloadProvider")
|
? require("@/providers/DownloadProvider")
|
||||||
: null;
|
: null;
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
|
||||||
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 native from "@/utils/profiles/native";
|
import native from "@/utils/profiles/native";
|
||||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||||
import { Api } from "@jellyfin/sdk";
|
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
|
||||||
import {
|
import {
|
||||||
getPlaystateApi,
|
getPlaystateApi,
|
||||||
getUserLibraryApi,
|
getUserLibraryApi,
|
||||||
@@ -42,14 +38,12 @@ import React, {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
BackHandler,
|
|
||||||
View,
|
View,
|
||||||
AppState,
|
AppState,
|
||||||
AppStateStatus,
|
AppStateStatus,
|
||||||
Platform,
|
Platform,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import settings from "../(tabs)/(home)/settings";
|
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -67,6 +61,7 @@ export default function page() {
|
|||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isBuffering, setIsBuffering] = useState(true);
|
const [isBuffering, setIsBuffering] = useState(true);
|
||||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||||
|
const [isPipStarted, setIsPipStarted] = useState(false);
|
||||||
|
|
||||||
const progress = useSharedValue(0);
|
const progress = useSharedValue(0);
|
||||||
const isSeeking = useSharedValue(false);
|
const isSeeking = useSharedValue(false);
|
||||||
@@ -190,6 +185,9 @@ export default function page() {
|
|||||||
lightHapticFeedback();
|
lightHapticFeedback();
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
|
} else {
|
||||||
|
videoRef.current?.play();
|
||||||
|
}
|
||||||
|
|
||||||
if (!offline && stream) {
|
if (!offline && stream) {
|
||||||
await getPlaystateApi(api).onPlaybackProgress({
|
await getPlaystateApi(api).onPlaybackProgress({
|
||||||
@@ -197,31 +195,14 @@ export default function page() {
|
|||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||||
mediaSourceId: mediaSourceId,
|
mediaSourceId: mediaSourceId,
|
||||||
positionTicks: msToTicks(progress.value),
|
positionTicks: msToTicks(progress.get()),
|
||||||
isPaused: true,
|
isPaused: !isPlaying,
|
||||||
playMethod: stream.url?.includes("m3u8")
|
|
||||||
? "Transcode"
|
|
||||||
: "DirectStream",
|
|
||||||
playSessionId: stream.sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
videoRef.current?.play();
|
|
||||||
if (!offline && stream) {
|
|
||||||
await getPlaystateApi(api).onPlaybackProgress({
|
|
||||||
itemId: item?.Id!,
|
|
||||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
|
||||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
|
||||||
mediaSourceId: mediaSourceId,
|
|
||||||
positionTicks: msToTicks(progress.value),
|
|
||||||
isPaused: false,
|
|
||||||
playMethod: stream?.url.includes("m3u8")
|
playMethod: stream?.url.includes("m3u8")
|
||||||
? "Transcode"
|
? "Transcode"
|
||||||
: "DirectStream",
|
: "DirectStream",
|
||||||
playSessionId: stream.sessionId,
|
playSessionId: stream.sessionId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
isPlaying,
|
isPlaying,
|
||||||
api,
|
api,
|
||||||
@@ -232,13 +213,13 @@ export default function page() {
|
|||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
offline,
|
offline,
|
||||||
progress.value,
|
progress,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const reportPlaybackStopped = useCallback(async () => {
|
const reportPlaybackStopped = useCallback(async () => {
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
const currentTimeInTicks = msToTicks(progress.value);
|
const currentTimeInTicks = msToTicks(progress.get());
|
||||||
|
|
||||||
await getPlaystateApi(api!).onPlaybackStopped({
|
await getPlaystateApi(api!).onPlaybackStopped({
|
||||||
itemId: item?.Id!,
|
itemId: item?.Id!,
|
||||||
@@ -273,8 +254,7 @@ export default function page() {
|
|||||||
|
|
||||||
const onProgress = useCallback(
|
const onProgress = useCallback(
|
||||||
async (data: ProgressUpdatePayload) => {
|
async (data: ProgressUpdatePayload) => {
|
||||||
if (isSeeking.value === true) return;
|
if (isSeeking.get() || isPlaybackStopped) return;
|
||||||
if (isPlaybackStopped === true) return;
|
|
||||||
|
|
||||||
const { currentTime } = data.nativeEvent;
|
const { currentTime } = data.nativeEvent;
|
||||||
|
|
||||||
@@ -282,7 +262,7 @@ export default function page() {
|
|||||||
setIsBuffering(false);
|
setIsBuffering(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.value = currentTime;
|
progress.set(currentTime);
|
||||||
|
|
||||||
if (offline) return;
|
if (offline) return;
|
||||||
|
|
||||||
@@ -301,7 +281,7 @@ export default function page() {
|
|||||||
playSessionId: stream.sessionId,
|
playSessionId: stream.sessionId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[item?.Id, isPlaying, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
[item?.Id, isSeeking, api, isPlaybackStopped, audioIndex, subtitleIndex]
|
||||||
);
|
);
|
||||||
|
|
||||||
useWebSocket({
|
useWebSocket({
|
||||||
@@ -311,6 +291,11 @@ export default function page() {
|
|||||||
offline,
|
offline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||||
|
const { pipStarted } = e.nativeEvent;
|
||||||
|
setIsPipStarted(pipStarted)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
|
const onPlaybackStateChanged = useCallback((e: PlaybackStatePayload) => {
|
||||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||||
|
|
||||||
@@ -340,25 +325,13 @@ export default function page() {
|
|||||||
: 0;
|
: 0;
|
||||||
}, [item]);
|
}, [item]);
|
||||||
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
return async () => {
|
|
||||||
stop();
|
|
||||||
};
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
const [appState, setAppState] = useState(AppState.currentState);
|
const [appState, setAppState] = useState(AppState.currentState);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
const handleAppStateChange = (nextAppState: AppStateStatus) => {
|
||||||
if (appState.match(/inactive|background/) && nextAppState === "active") {
|
|
||||||
// Handle app coming to the foreground
|
|
||||||
} else if (nextAppState.match(/inactive|background/)) {
|
|
||||||
// Handle app going to the background
|
// Handle app going to the background
|
||||||
if (videoRef.current && videoRef.current.pause) {
|
if (nextAppState.match(/inactive|background/)) {
|
||||||
videoRef.current.pause();
|
_setShowControls(false)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setAppState(nextAppState);
|
setAppState(nextAppState);
|
||||||
};
|
};
|
||||||
@@ -373,10 +346,9 @@ export default function page() {
|
|||||||
// Cleanup the event listener when the component is unmounted
|
// Cleanup the event listener when the component is unmounted
|
||||||
subscription.remove();
|
subscription.remove();
|
||||||
};
|
};
|
||||||
}, [appState]);
|
}, [appState, isPipStarted, isPlaying]);
|
||||||
|
|
||||||
// Preselection of audio and subtitle tracks.
|
// Preselection of audio and subtitle tracks.
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
let initOptions = [`--sub-text-scale=${settings.subtitleSize}`];
|
||||||
@@ -466,6 +438,7 @@ export default function page() {
|
|||||||
onVideoProgress={onProgress}
|
onVideoProgress={onProgress}
|
||||||
progressUpdateInterval={1000}
|
progressUpdateInterval={1000}
|
||||||
onVideoStateChange={onPlaybackStateChanged}
|
onVideoStateChange={onPlaybackStateChanged}
|
||||||
|
onPipStarted={onPipStarted}
|
||||||
onVideoLoadStart={() => {}}
|
onVideoLoadStart={() => {}}
|
||||||
onVideoLoadEnd={() => {
|
onVideoLoadEnd={() => {
|
||||||
setIsVideoLoaded(true);
|
setIsVideoLoaded(true);
|
||||||
@@ -496,6 +469,7 @@ export default function page() {
|
|||||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||||
ignoreSafeAreas={ignoreSafeAreas}
|
ignoreSafeAreas={ignoreSafeAreas}
|
||||||
isVideoLoaded={isVideoLoaded}
|
isVideoLoaded={isVideoLoaded}
|
||||||
|
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}
|
||||||
@@ -513,22 +487,3 @@ export default function page() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePoster(
|
|
||||||
item: BaseItemDto,
|
|
||||||
api: Api | null
|
|
||||||
): string | undefined {
|
|
||||||
const poster = useMemo(() => {
|
|
||||||
if (!item || !api) return undefined;
|
|
||||||
return item.Type === "Audio"
|
|
||||||
? `${api.basePath}/Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`
|
|
||||||
: getBackdropUrl({
|
|
||||||
api,
|
|
||||||
item: item,
|
|
||||||
quality: 70,
|
|
||||||
width: 200,
|
|
||||||
});
|
|
||||||
}, [item, api]);
|
|
||||||
|
|
||||||
return poster ?? undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
ticksToMs,
|
ticksToMs,
|
||||||
ticksToSeconds,
|
ticksToSeconds,
|
||||||
} from "@/utils/time";
|
} from "@/utils/time";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import {Ionicons, MaterialIcons} from "@expo/vector-icons";
|
||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
MediaSourceInfo,
|
MediaSourceInfo,
|
||||||
@@ -35,7 +35,7 @@ import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
|||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
import {Platform, TouchableOpacity, useWindowDimensions, View} from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import {
|
import {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
@@ -75,6 +75,7 @@ interface Props {
|
|||||||
isVideoLoaded?: boolean;
|
isVideoLoaded?: boolean;
|
||||||
mediaSource?: MediaSourceInfo | null;
|
mediaSource?: MediaSourceInfo | null;
|
||||||
seek: (ticks: number) => void;
|
seek: (ticks: number) => void;
|
||||||
|
startPictureInPicture: () => Promise<void>;
|
||||||
play: (() => Promise<void>) | (() => void);
|
play: (() => Promise<void>) | (() => void);
|
||||||
pause: () => void;
|
pause: () => void;
|
||||||
getAudioTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]);
|
getAudioTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]);
|
||||||
@@ -91,6 +92,7 @@ const CONTROLS_TIMEOUT = 4000;
|
|||||||
export const Controls: React.FC<Props> = ({
|
export const Controls: React.FC<Props> = ({
|
||||||
item,
|
item,
|
||||||
seek,
|
seek,
|
||||||
|
startPictureInPicture,
|
||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
@@ -212,6 +214,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
|
stop()
|
||||||
|
|
||||||
if (!bitrateValue) {
|
if (!bitrateValue) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
@@ -250,6 +254,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
|
stop()
|
||||||
|
|
||||||
if (!bitrateValue) {
|
if (!bitrateValue) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
@@ -413,6 +419,8 @@ export const Controls: React.FC<Props> = ({
|
|||||||
bitrateValue: bitrateValue.toString(),
|
bitrateValue: bitrateValue.toString(),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
|
stop()
|
||||||
|
|
||||||
if (!bitrateValue) {
|
if (!bitrateValue) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
router.replace(`player/direct-player?${queryParams}`);
|
router.replace(`player/direct-player?${queryParams}`);
|
||||||
@@ -499,6 +507,15 @@ export const Controls: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
}, [trickPlayUrl, trickplayInfo, time]);
|
}, [trickPlayUrl, trickplayInfo, time]);
|
||||||
|
|
||||||
|
const onClose = async () => {
|
||||||
|
stop()
|
||||||
|
lightHapticFeedback();
|
||||||
|
await ScreenOrientation.lockAsync(
|
||||||
|
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||||
|
);
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ControlProvider
|
<ControlProvider
|
||||||
item={item}
|
item={item}
|
||||||
@@ -551,6 +568,19 @@ export const Controls: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="flex flex-row items-center space-x-2 ">
|
<View className="flex flex-row items-center space-x-2 ">
|
||||||
|
{!Platform.isTV && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={startPictureInPicture}
|
||||||
|
>
|
||||||
|
<MaterialIcons
|
||||||
|
name="picture-in-picture"
|
||||||
|
size={24}
|
||||||
|
color="white"
|
||||||
|
style={{ opacity: showControls ? 1 : 0 }}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
{item?.Type === "Episode" && !offline && (
|
{item?.Type === "Episode" && !offline && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -592,13 +622,7 @@ export const Controls: React.FC<Props> = ({
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={async () => {
|
onPress={onClose}
|
||||||
lightHapticFeedback();
|
|
||||||
await ScreenOrientation.lockAsync(
|
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
|
||||||
);
|
|
||||||
router.back();
|
|
||||||
}}
|
|
||||||
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"
|
||||||
>
|
>
|
||||||
<Ionicons name="close" size={24} color="white" />
|
<Ionicons name="close" size={24} color="white" />
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ class VlcPlayerModule : Module() {
|
|||||||
"onVideoLoadStart",
|
"onVideoLoadStart",
|
||||||
"onVideoLoadEnd",
|
"onVideoLoadEnd",
|
||||||
"onVideoProgress",
|
"onVideoProgress",
|
||||||
"onVideoError"
|
"onVideoError",
|
||||||
|
"onPipStarted"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AsyncFunction("startPictureInPicture") { view: VlcPlayerView ->
|
||||||
|
view.startPictureInPicture()
|
||||||
|
}
|
||||||
|
|
||||||
AsyncFunction("play") { view: VlcPlayerView ->
|
AsyncFunction("play") { view: VlcPlayerView ->
|
||||||
view.play()
|
view.play()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,40 @@
|
|||||||
package expo.modules.vlcplayer
|
package expo.modules.vlcplayer
|
||||||
|
|
||||||
|
import android.R
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||||
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
import android.app.PictureInPictureParams
|
||||||
|
import android.app.RemoteAction
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.ViewGroup
|
import androidx.annotation.RequiresApi
|
||||||
import android.widget.FrameLayout
|
import androidx.core.app.ComponentActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import android.net.Uri
|
|
||||||
import expo.modules.kotlin.AppContext
|
import expo.modules.kotlin.AppContext
|
||||||
import expo.modules.kotlin.views.ExpoView
|
|
||||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||||
|
import expo.modules.kotlin.views.ExpoView
|
||||||
import org.videolan.libvlc.LibVLC
|
import org.videolan.libvlc.LibVLC
|
||||||
import org.videolan.libvlc.Media
|
import org.videolan.libvlc.Media
|
||||||
import org.videolan.libvlc.interfaces.IMedia
|
|
||||||
import org.videolan.libvlc.MediaPlayer
|
import org.videolan.libvlc.MediaPlayer
|
||||||
|
import org.videolan.libvlc.interfaces.IMedia
|
||||||
import org.videolan.libvlc.util.VLCVideoLayout
|
import org.videolan.libvlc.util.VLCVideoLayout
|
||||||
|
|
||||||
|
|
||||||
class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext), LifecycleObserver, MediaPlayer.EventListener {
|
class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext), LifecycleObserver, MediaPlayer.EventListener {
|
||||||
|
private val PIP_PLAY_PAUSE_ACTION = "PIP_PLAY_PAUSE_ACTION"
|
||||||
|
private val PIP_REWIND_ACTION = "PIP_REWIND_ACTION"
|
||||||
|
private val PIP_FORWARD_ACTION = "PIP_FORWARD_ACTION"
|
||||||
|
|
||||||
private var libVLC: LibVLC? = null
|
private var libVLC: LibVLC? = null
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
@@ -30,6 +47,7 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
private val onVideoProgress by EventDispatcher()
|
private val onVideoProgress by EventDispatcher()
|
||||||
private val onVideoStateChange by EventDispatcher()
|
private val onVideoStateChange by EventDispatcher()
|
||||||
private val onVideoLoadEnd by EventDispatcher()
|
private val onVideoLoadEnd by EventDispatcher()
|
||||||
|
private val onPipStarted by EventDispatcher()
|
||||||
|
|
||||||
private var startPosition: Int? = 0
|
private var startPosition: Int? = 0
|
||||||
private var isMediaReady: Boolean = false
|
private var isMediaReady: Boolean = false
|
||||||
@@ -44,9 +62,32 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
handler.postDelayed(this, updateInterval)
|
handler.postDelayed(this, updateInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val currentActivity get() = context.findActivity()
|
||||||
|
private val actions: MutableList<RemoteAction> = mutableListOf()
|
||||||
|
|
||||||
|
private val actionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
when (intent?.action) {
|
||||||
|
PIP_PLAY_PAUSE_ACTION -> if (isPaused) play() else pause()
|
||||||
|
PIP_FORWARD_ACTION -> seekTo((mediaPlayer?.time?.toInt() ?: 0) + 15_000)
|
||||||
|
PIP_REWIND_ACTION -> seekTo((mediaPlayer?.time?.toInt() ?: 0) - 15_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupView()
|
setupView()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
setupPipActions()
|
||||||
|
currentActivity.apply {
|
||||||
|
setPictureInPictureParams(getPipParams()!!)
|
||||||
|
addOnPictureInPictureModeChangedListener { info ->
|
||||||
|
onPipStarted(mapOf(
|
||||||
|
"pipStarted" to info.isInPictureInPictureMode
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
@@ -59,6 +100,76 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
Log.d("VlcPlayerView", "View setup complete")
|
Log.d("VlcPlayerView", "View setup complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun setupPipActions() {
|
||||||
|
val remoteActionFilter = IntentFilter()
|
||||||
|
val playPauseIntent: Intent = Intent(PIP_PLAY_PAUSE_ACTION).setPackage(context.packageName)
|
||||||
|
val forwardIntent: Intent = Intent(PIP_FORWARD_ACTION).setPackage(context.packageName)
|
||||||
|
val rewindIntent: Intent = Intent(PIP_REWIND_ACTION).setPackage(context.packageName)
|
||||||
|
|
||||||
|
remoteActionFilter.addAction(PIP_PLAY_PAUSE_ACTION)
|
||||||
|
remoteActionFilter.addAction(PIP_FORWARD_ACTION)
|
||||||
|
remoteActionFilter.addAction(PIP_REWIND_ACTION)
|
||||||
|
|
||||||
|
actions.addAll(
|
||||||
|
listOf(
|
||||||
|
RemoteAction(
|
||||||
|
Icon.createWithResource(context, R.drawable.ic_media_rew),
|
||||||
|
"Rewind",
|
||||||
|
"Rewind Video",
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
rewindIntent,
|
||||||
|
FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
),
|
||||||
|
RemoteAction(
|
||||||
|
Icon.createWithResource(context, R.drawable.ic_media_play),
|
||||||
|
"Play",
|
||||||
|
"Play Video",
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
playPauseIntent,
|
||||||
|
FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
),
|
||||||
|
RemoteAction(
|
||||||
|
Icon.createWithResource(context, R.drawable.ic_media_ff),
|
||||||
|
"Skip",
|
||||||
|
"Skip Forward",
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
forwardIntent,
|
||||||
|
FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context,
|
||||||
|
actionReceiver,
|
||||||
|
remoteActionFilter,
|
||||||
|
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPipParams(): PictureInPictureParams? {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
var builder = PictureInPictureParams.Builder()
|
||||||
|
.setActions(actions)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
builder = builder.setAutoEnterEnabled(true)
|
||||||
|
}
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun setSource(source: Map<String, Any>) {
|
fun setSource(source: Map<String, Any>) {
|
||||||
if (hasSource) {
|
if (hasSource) {
|
||||||
mediaPlayer?.attachViews(videoLayout, null, false, false)
|
mediaPlayer?.attachViews(videoLayout, null, false, false)
|
||||||
@@ -112,6 +223,12 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startPictureInPicture() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
currentActivity.enterPictureInPictureMode(getPipParams()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun play() {
|
fun play() {
|
||||||
mediaPlayer?.play()
|
mediaPlayer?.play()
|
||||||
isPaused = false
|
isPaused = false
|
||||||
@@ -284,3 +401,12 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun Context.findActivity(): androidx.activity.ComponentActivity {
|
||||||
|
var context = this
|
||||||
|
while (context is ContextWrapper) {
|
||||||
|
if (context is androidx.activity.ComponentActivity) return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
throw IllegalStateException("Failed to find ComponentActivity")
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
NativeModulesProxy,
|
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Subscription,
|
EventSubscription,
|
||||||
} from "expo-modules-core";
|
} from "expo-modules-core";
|
||||||
|
|
||||||
import VlcPlayerModule from "./src/VlcPlayerModule";
|
import VlcPlayerModule from "./src/VlcPlayerModule";
|
||||||
@@ -19,13 +18,11 @@ import {
|
|||||||
VlcPlayerViewRef,
|
VlcPlayerViewRef,
|
||||||
} from "./src/VlcPlayer.types";
|
} from "./src/VlcPlayer.types";
|
||||||
|
|
||||||
const emitter = new EventEmitter(
|
const emitter = new EventEmitter(VlcPlayerModule);
|
||||||
VlcPlayerModule ?? NativeModulesProxy.VlcPlayer
|
|
||||||
);
|
|
||||||
|
|
||||||
export function addPlaybackStateListener(
|
export function addPlaybackStateListener(
|
||||||
listener: (event: PlaybackStatePayload) => void
|
listener: (event: PlaybackStatePayload) => void
|
||||||
): Subscription {
|
): EventSubscription {
|
||||||
return emitter.addListener<PlaybackStatePayload>(
|
return emitter.addListener<PlaybackStatePayload>(
|
||||||
"onPlaybackStateChanged",
|
"onPlaybackStateChanged",
|
||||||
listener
|
listener
|
||||||
@@ -34,7 +31,7 @@ export function addPlaybackStateListener(
|
|||||||
|
|
||||||
export function addVideoLoadStartListener(
|
export function addVideoLoadStartListener(
|
||||||
listener: (event: VideoLoadStartPayload) => void
|
listener: (event: VideoLoadStartPayload) => void
|
||||||
): Subscription {
|
): EventSubscription {
|
||||||
return emitter.addListener<VideoLoadStartPayload>(
|
return emitter.addListener<VideoLoadStartPayload>(
|
||||||
"onVideoLoadStart",
|
"onVideoLoadStart",
|
||||||
listener
|
listener
|
||||||
@@ -43,7 +40,7 @@ export function addVideoLoadStartListener(
|
|||||||
|
|
||||||
export function addVideoStateChangeListener(
|
export function addVideoStateChangeListener(
|
||||||
listener: (event: VideoStateChangePayload) => void
|
listener: (event: VideoStateChangePayload) => void
|
||||||
): Subscription {
|
): EventSubscription {
|
||||||
return emitter.addListener<VideoStateChangePayload>(
|
return emitter.addListener<VideoStateChangePayload>(
|
||||||
"onVideoStateChange",
|
"onVideoStateChange",
|
||||||
listener
|
listener
|
||||||
@@ -52,7 +49,7 @@ export function addVideoStateChangeListener(
|
|||||||
|
|
||||||
export function addVideoProgressListener(
|
export function addVideoProgressListener(
|
||||||
listener: (event: VideoProgressPayload) => void
|
listener: (event: VideoProgressPayload) => void
|
||||||
): Subscription {
|
): EventSubscription {
|
||||||
return emitter.addListener<VideoProgressPayload>("onVideoProgress", listener);
|
return emitter.addListener<VideoProgressPayload>("onVideoProgress", listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Pod::Spec.new do |s|
|
Pod::Spec.new do |s|
|
||||||
s.name = 'VlcPlayer'
|
s.name = 'VlcPlayer'
|
||||||
s.version = '1.0.0'
|
s.version = '4.0.0a10'
|
||||||
s.summary = 'A sample project summary'
|
s.summary = 'A sample project summary'
|
||||||
s.description = 'A sample project description'
|
s.description = 'A sample project description'
|
||||||
s.author = ''
|
s.author = ''
|
||||||
@@ -10,8 +10,8 @@ Pod::Spec.new do |s|
|
|||||||
s.static_framework = true
|
s.static_framework = true
|
||||||
|
|
||||||
s.dependency 'ExpoModulesCore'
|
s.dependency 'ExpoModulesCore'
|
||||||
s.ios.dependency 'MobileVLCKit', '~> 3.6.1b1'
|
s.ios.dependency 'VLCKit', s.version
|
||||||
s.tvos.dependency 'TVVLCKit', '~> 3.6.1b1'
|
s.tvos.dependency 'VLCKit', s.version
|
||||||
|
|
||||||
# Swift/Objective-C compatibility
|
# Swift/Objective-C compatibility
|
||||||
s.pod_target_xcconfig = {
|
s.pod_target_xcconfig = {
|
||||||
|
|||||||
@@ -16,27 +16,20 @@ public class VlcPlayerModule: Module {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(
|
Events(
|
||||||
"onPlaybackStateChanged",
|
"onPlaybackStateChanged",
|
||||||
"onVideoStateChange",
|
"onVideoStateChange",
|
||||||
"onVideoLoadStart",
|
"onVideoLoadStart",
|
||||||
"onVideoLoadEnd",
|
"onVideoLoadEnd",
|
||||||
"onVideoProgress",
|
"onVideoProgress",
|
||||||
"onVideoError"
|
"onVideoError",
|
||||||
|
"onPipStarted"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AsyncFunction("startPictureInPicture") { (view: VlcPlayerView) in
|
||||||
|
view.startPictureInPicture()
|
||||||
|
}
|
||||||
|
|
||||||
AsyncFunction("play") { (view: VlcPlayerView) in
|
AsyncFunction("play") { (view: VlcPlayerView) in
|
||||||
view.play()
|
view.play()
|
||||||
}
|
}
|
||||||
@@ -69,14 +62,6 @@ public class VlcPlayerModule: Module {
|
|||||||
return view.getSubtitleTracks()
|
return view.getSubtitleTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in
|
|
||||||
// view.setVideoCropGeometry(geometry)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in
|
|
||||||
// return view.getVideoCropGeometry()
|
|
||||||
// }
|
|
||||||
|
|
||||||
AsyncFunction("setSubtitleURL") {
|
AsyncFunction("setSubtitleURL") {
|
||||||
(view: VlcPlayerView, url: String, name: String) in
|
(view: VlcPlayerView, url: String, name: String) in
|
||||||
view.setSubtitleURL(url, name: name)
|
view.setSubtitleURL(url, name: name)
|
||||||
|
|||||||
@@ -1,54 +1,168 @@
|
|||||||
import ExpoModulesCore
|
import ExpoModulesCore
|
||||||
#if os(tvOS)
|
import VLCKit
|
||||||
import TVVLCKit
|
|
||||||
#else
|
|
||||||
import MobileVLCKit
|
|
||||||
#endif
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
|
||||||
|
public class VLCPlayerView: UIView {
|
||||||
|
func setupView(parent: UIView) {
|
||||||
|
self.backgroundColor = .black
|
||||||
|
self.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
self.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
||||||
|
self.trailingAnchor.constraint(equalTo: parent.trailingAnchor),
|
||||||
|
self.topAnchor.constraint(equalTo: parent.topAnchor),
|
||||||
|
self.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
for subview in subviews {
|
||||||
|
subview.frame = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VLCPlayerWrapper: NSObject {
|
||||||
|
private var lastProgressCall = Date().timeIntervalSince1970
|
||||||
|
public var player: VLCMediaPlayer = VLCMediaPlayer()
|
||||||
|
private var updatePlayerState: (() -> ())?
|
||||||
|
private var updateVideoProgress: (() -> ())?
|
||||||
|
private var playerView: VLCPlayerView = VLCPlayerView()
|
||||||
|
public weak var pipController: VLCPictureInPictureWindowControlling?
|
||||||
|
|
||||||
|
override public init() {
|
||||||
|
super.init()
|
||||||
|
player.delegate = self
|
||||||
|
player.drawable = self
|
||||||
|
player.scaleFactor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setup(
|
||||||
|
parent: UIView,
|
||||||
|
updatePlayerState: (() -> ())?,
|
||||||
|
updateVideoProgress: (() -> ())?
|
||||||
|
) {
|
||||||
|
self.updatePlayerState = updatePlayerState
|
||||||
|
self.updateVideoProgress = updateVideoProgress
|
||||||
|
|
||||||
|
player.delegate = self
|
||||||
|
parent.addSubview(playerView)
|
||||||
|
playerView.setupView(parent: parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getPlayerView() -> UIView {
|
||||||
|
return playerView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VLCPictureInPictureDrawable
|
||||||
|
extension VLCPlayerWrapper: VLCPictureInPictureDrawable {
|
||||||
|
public func mediaController() -> (any VLCPictureInPictureMediaControlling)! {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
public func pictureInPictureReady() -> (((any VLCPictureInPictureWindowControlling)?) -> Void)! {
|
||||||
|
return { [weak self] controller in
|
||||||
|
self?.pipController = controller
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VLCPictureInPictureMediaControlling
|
||||||
|
extension VLCPlayerWrapper: VLCPictureInPictureMediaControlling {
|
||||||
|
func mediaTime() -> Int64 {
|
||||||
|
return player.time.value?.int64Value ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaLength() -> Int64 {
|
||||||
|
return player.media?.length.value?.int64Value ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func play() {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pause() {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
func seek(by offset: Int64, completion: @escaping () -> ()) {
|
||||||
|
player.jump(withOffset: Int32(offset), completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMediaSeekable() -> Bool {
|
||||||
|
return player.isSeekable
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMediaPlaying() -> Bool {
|
||||||
|
return player.isPlaying
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VLCDrawable
|
||||||
|
extension VLCPlayerWrapper: VLCDrawable {
|
||||||
|
public func addSubview(_ view: UIView) {
|
||||||
|
playerView.addSubview(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func bounds() -> CGRect {
|
||||||
|
return playerView.bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VLCMediaPlayerDelegate
|
||||||
|
extension VLCPlayerWrapper: VLCMediaPlayerDelegate {
|
||||||
|
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
||||||
|
let timeNow = Date().timeIntervalSince1970
|
||||||
|
if timeNow - lastProgressCall >= 1 {
|
||||||
|
lastProgressCall = timeNow
|
||||||
|
updateVideoProgress?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mediaPlayerStateChanged(_ state: VLCMediaPlayerState) {
|
||||||
|
self.updatePlayerState?()
|
||||||
|
|
||||||
|
guard let pipController = self.pipController else { return }
|
||||||
|
DispatchQueue.main.async(execute: {
|
||||||
|
pipController.invalidatePlaybackState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VLCMediaDelegate
|
||||||
|
extension VLCPlayerWrapper: VLCMediaDelegate {
|
||||||
|
// Implement VLCMediaDelegate methods if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class VlcPlayerView: ExpoView {
|
class VlcPlayerView: ExpoView {
|
||||||
private var mediaPlayer: VLCMediaPlayer?
|
private var vlc: VLCPlayerWrapper = VLCPlayerWrapper()
|
||||||
private var videoView: UIView?
|
|
||||||
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
private var progressUpdateInterval: TimeInterval = 1.0 // Update interval set to 1 second
|
||||||
private var isPaused: Bool = false
|
private var isPaused: Bool = false
|
||||||
private var currentGeometryCString: [CChar]?
|
|
||||||
private var lastReportedState: VLCMediaPlayerState?
|
|
||||||
private var lastReportedIsPlaying: Bool?
|
|
||||||
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
private var customSubtitles: [(internalName: String, originalName: String)] = []
|
||||||
private var startPosition: Int32 = 0
|
private var startPosition: Int32 = 0
|
||||||
private var isMediaReady: Bool = false
|
private var isMediaReady: Bool = false
|
||||||
private var externalTrack: [String: String]?
|
private var externalTrack: [String: String]?
|
||||||
private var progressTimer: DispatchSourceTimer?
|
|
||||||
private var isStopping: Bool = false // Define isStopping here
|
private var isStopping: Bool = false // Define isStopping here
|
||||||
private var lastProgressCall = Date().timeIntervalSince1970
|
|
||||||
var hasSource = false
|
var hasSource = false
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
required init(appContext: AppContext? = nil) {
|
required init(appContext: AppContext? = nil) {
|
||||||
super.init(appContext: appContext)
|
super.init(appContext: appContext)
|
||||||
setupView()
|
setupVLC()
|
||||||
setupNotifications()
|
setupNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
|
private func setupVLC() {
|
||||||
private func setupView() {
|
vlc.setup(
|
||||||
DispatchQueue.main.async {
|
parent: self,
|
||||||
self.backgroundColor = .black
|
updatePlayerState: updatePlayerState,
|
||||||
self.videoView = UIView()
|
updateVideoProgress: updateVideoProgress
|
||||||
self.videoView?.translatesAutoresizingMaskIntoConstraints = false
|
)
|
||||||
|
|
||||||
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),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupNotifications() {
|
private func setupNotifications() {
|
||||||
@@ -61,37 +175,44 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
|
func startPictureInPicture() {
|
||||||
|
self.vlc.pipController?.stateChangeEventHandler = { (isStarted: Bool) in
|
||||||
|
self.onPipStarted?(["pipStarted": isStarted])
|
||||||
|
}
|
||||||
|
self.vlc.pipController?.startPictureInPicture()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func play() {
|
@objc func play() {
|
||||||
self.mediaPlayer?.play()
|
self.vlc.player.play()
|
||||||
self.isPaused = false
|
self.isPaused = false
|
||||||
print("Play")
|
print("Play")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func pause() {
|
@objc func pause() {
|
||||||
self.mediaPlayer?.pause()
|
self.vlc.player.pause()
|
||||||
self.isPaused = true
|
self.isPaused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func seekTo(_ time: Int32) {
|
@objc func seekTo(_ time: Int32) {
|
||||||
guard let player = self.mediaPlayer else { return }
|
let wasPlaying = vlc.player.isPlaying
|
||||||
|
|
||||||
let wasPlaying = player.isPlaying
|
|
||||||
if wasPlaying {
|
if wasPlaying {
|
||||||
self.pause()
|
self.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let duration = player.media?.length.intValue {
|
if let duration = vlc.player.media?.length.intValue {
|
||||||
print("Seeking to time: \(time) Video Duration \(duration)")
|
print("Seeking to time: \(time) Video Duration \(duration)")
|
||||||
|
|
||||||
// If the specified time is greater than the duration, seek to the end
|
// If the specified time is greater than the duration, seek to the end
|
||||||
let seekTime = time > duration ? duration - 1000 : time
|
let seekTime = time > duration ? duration - 1000 : time
|
||||||
player.time = VLCTime(int: seekTime)
|
vlc.player.time = VLCTime(int: seekTime)
|
||||||
|
self.updatePlayerState()
|
||||||
|
|
||||||
|
// Let mediaPlayerStateChanged handle play state change
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
if wasPlaying {
|
if wasPlaying {
|
||||||
self.play()
|
self.play()
|
||||||
}
|
}
|
||||||
self.updatePlayerState()
|
}
|
||||||
} else {
|
} else {
|
||||||
print("Error: Unable to retrieve video duration")
|
print("Error: Unable to retrieve video duration")
|
||||||
}
|
}
|
||||||
@@ -104,11 +225,15 @@ class VlcPlayerView: ExpoView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:]
|
var mediaOptions = source["mediaOptions"] as? [String: Any] ?? [:]
|
||||||
self.externalTrack = source["externalTrack"] as? [String: String]
|
self.externalTrack = source["externalTrack"] as? [String: String]
|
||||||
var initOptions = source["initOptions"] as? [Any] ?? []
|
let initOptions: [String] = source["initOptions"] as? [String] ?? []
|
||||||
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||||
initOptions.append("--start-time=\(self.startPosition)")
|
|
||||||
|
for item in initOptions {
|
||||||
|
let option = item.components(separatedBy: "=")
|
||||||
|
mediaOptions.updateValue(option[1], forKey: option[0].replacingOccurrences(of: "--", with: ""))
|
||||||
|
}
|
||||||
|
|
||||||
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
guard let uri = source["uri"] as? String, !uri.isEmpty else {
|
||||||
print("Error: Invalid or empty URI")
|
print("Error: Invalid or empty URI")
|
||||||
@@ -120,12 +245,8 @@ class VlcPlayerView: ExpoView {
|
|||||||
let isNetwork = source["isNetwork"] as? Bool ?? false
|
let isNetwork = source["isNetwork"] as? Bool ?? false
|
||||||
|
|
||||||
self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()])
|
self.onVideoLoadStart?(["target": self.reactTag ?? NSNull()])
|
||||||
self.mediaPlayer = VLCMediaPlayer(options: initOptions)
|
|
||||||
self.mediaPlayer?.delegate = self
|
|
||||||
self.mediaPlayer?.drawable = self.videoView
|
|
||||||
self.mediaPlayer?.scaleFactor = 0
|
|
||||||
|
|
||||||
let media: VLCMedia
|
let media: VLCMedia!
|
||||||
if isNetwork {
|
if isNetwork {
|
||||||
print("Loading network file: \(uri)")
|
print("Loading network file: \(uri)")
|
||||||
media = VLCMedia(url: URL(string: uri)!)
|
media = VLCMedia(url: URL(string: uri)!)
|
||||||
@@ -141,38 +262,33 @@ class VlcPlayerView: ExpoView {
|
|||||||
print("Debug: Media options: \(mediaOptions)")
|
print("Debug: Media options: \(mediaOptions)")
|
||||||
media.addOptions(mediaOptions)
|
media.addOptions(mediaOptions)
|
||||||
|
|
||||||
self.mediaPlayer?.media = media
|
self.vlc.player.media = media
|
||||||
self.hasSource = true
|
self.hasSource = true
|
||||||
|
|
||||||
if autoplay {
|
if autoplay {
|
||||||
print("Playing...")
|
print("Playing...")
|
||||||
self.play()
|
self.play()
|
||||||
|
self.vlc.player.time = VLCTime(number: NSNumber(value: self.startPosition * 1000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setAudioTrack(_ trackIndex: Int) {
|
@objc func setAudioTrack(_ trackIndex: Int) {
|
||||||
self.mediaPlayer?.currentAudioTrackIndex = Int32(trackIndex)
|
let track = self.vlc.player.audioTracks[trackIndex]
|
||||||
|
track.isSelectedExclusively = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getAudioTracks() -> [[String: Any]]? {
|
@objc func getAudioTracks() -> [[String: Any]]? {
|
||||||
guard let trackNames = mediaPlayer?.audioTrackNames,
|
return vlc.player.audioTracks.enumerated().map {
|
||||||
let trackIndexes = mediaPlayer?.audioTrackIndexes
|
return ["name": $1.trackName, "index": $0 ]
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return zip(trackNames, trackIndexes).map { name, index in
|
|
||||||
return ["name": name, "index": index]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
@objc func setSubtitleTrack(_ trackIndex: Int) {
|
||||||
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
print("Debug: Attempting to set subtitle track to index: \(trackIndex)")
|
||||||
self.mediaPlayer?.currentVideoSubTitleIndex = Int32(trackIndex)
|
let track = self.vlc.player.textTracks[trackIndex]
|
||||||
print(
|
track.isSelectedExclusively = true;
|
||||||
"Debug: Current subtitle track index after setting: \(self.mediaPlayer?.currentVideoSubTitleIndex ?? -1)"
|
print("Debug: Current subtitle track index after setting: \(track.trackName)")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func setSubtitleURL(_ subtitleURL: String, name: String) {
|
@objc func setSubtitleURL(_ subtitleURL: String, name: String) {
|
||||||
@@ -180,9 +296,9 @@ class VlcPlayerView: ExpoView {
|
|||||||
print("Error: Invalid subtitle URL")
|
print("Error: Invalid subtitle URL")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let result = self.vlc.player.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||||
|
|
||||||
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
if result > 0 {
|
||||||
if let result = result {
|
|
||||||
let internalName = "Track \(self.customSubtitles.count + 1)"
|
let internalName = "Track \(self.customSubtitles.count + 1)"
|
||||||
print("Subtitle added with result: \(result) \(internalName)")
|
print("Subtitle added with result: \(result) \(internalName)")
|
||||||
self.customSubtitles.append((internalName: internalName, originalName: name))
|
self.customSubtitles.append((internalName: internalName, originalName: name))
|
||||||
@@ -192,28 +308,18 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
@objc func getSubtitleTracks() -> [[String: Any]]? {
|
||||||
guard let mediaPlayer = self.mediaPlayer else {
|
if self.vlc.player.textTracks.count == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = mediaPlayer.numberOfSubtitlesTracks
|
print("Debug: Number of subtitle tracks: \(self.vlc.player.textTracks.count)")
|
||||||
print("Debug: Number of subtitle tracks: \(count)")
|
|
||||||
|
|
||||||
guard count > 0 else {
|
let tracks = self.vlc.player.textTracks.enumerated().map { (index, track) in
|
||||||
return nil
|
if let customSubtitle = customSubtitles.first(where: { $0.internalName == track.trackName }) {
|
||||||
}
|
return ["name": customSubtitle.originalName, "index": index ]
|
||||||
|
|
||||||
var tracks: [[String: Any]] = []
|
|
||||||
|
|
||||||
if let names = mediaPlayer.videoSubTitlesNames as? [String],
|
|
||||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber]
|
|
||||||
{
|
|
||||||
for (index, name) in zip(indexes, names) {
|
|
||||||
if let customSubtitle = customSubtitles.first(where: { $0.internalName == name }) {
|
|
||||||
tracks.append(["name": customSubtitle.originalName, "index": index.intValue])
|
|
||||||
} else {
|
|
||||||
tracks.append(["name": name, "index": index.intValue])
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
return ["name": track.trackName, "index": index ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,21 +328,14 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setSubtitleTrackByName(_ trackName: String) {
|
private func setSubtitleTrackByName(_ trackName: String) {
|
||||||
guard let mediaPlayer = self.mediaPlayer else { return }
|
for track in self.vlc.player.textTracks {
|
||||||
|
if (track.trackName.starts(with: trackName)) {
|
||||||
// Get the subtitle tracks and their indexes
|
print("Track Index setting to: \(track.trackName)")
|
||||||
if let names = mediaPlayer.videoSubTitlesNames as? [String],
|
track.isSelectedExclusively = true
|
||||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [NSNumber]
|
|
||||||
{
|
|
||||||
for (index, name) in zip(indexes, names) {
|
|
||||||
if name.starts(with: trackName) {
|
|
||||||
let trackIndex = index.intValue
|
|
||||||
print("Track Index setting to: \(trackIndex)")
|
|
||||||
setSubtitleTrack(trackIndex)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
print("Track not found for name: \(trackName)")
|
print("Track not found for name: \(trackName)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,32 +368,27 @@ class VlcPlayerView: ExpoView {
|
|||||||
|
|
||||||
private func performStop(completion: (() -> Void)? = nil) {
|
private func performStop(completion: (() -> Void)? = nil) {
|
||||||
// Stop the media player
|
// Stop the media player
|
||||||
mediaPlayer?.stop()
|
vlc.player.stop()
|
||||||
|
|
||||||
// Remove observer
|
// Remove observer
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
|
||||||
// Clear the video view
|
// Clear the video view
|
||||||
videoView?.removeFromSuperview()
|
vlc.getPlayerView().removeFromSuperview()
|
||||||
videoView = nil
|
|
||||||
|
|
||||||
// Release the media player
|
|
||||||
mediaPlayer?.delegate = nil
|
|
||||||
mediaPlayer = nil
|
|
||||||
|
|
||||||
isStopping = false
|
isStopping = false
|
||||||
completion?()
|
completion?()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateVideoProgress() {
|
private func updateVideoProgress() {
|
||||||
guard let player = self.mediaPlayer else { return }
|
guard let media = self.vlc.player.media else { return }
|
||||||
|
|
||||||
let currentTimeMs = player.time.intValue
|
let currentTimeMs = self.vlc.player.time.intValue
|
||||||
let durationMs = player.media?.length.intValue ?? 0
|
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
||||||
|
|
||||||
print("Debug: Current time: \(currentTimeMs)")
|
print("Debug: Current time: \(currentTimeMs)")
|
||||||
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
if currentTimeMs >= 0 && currentTimeMs < durationMs {
|
||||||
if player.isPlaying && !self.isMediaReady {
|
if !self.isMediaReady {
|
||||||
self.isMediaReady = true
|
self.isMediaReady = true
|
||||||
// Set external track subtitle when starting.
|
// Set external track subtitle when starting.
|
||||||
if let externalTrack = self.externalTrack {
|
if let externalTrack = self.externalTrack {
|
||||||
@@ -304,21 +398,34 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.onVideoProgress?([
|
self.onVideoProgress?([
|
||||||
"currentTime": currentTimeMs,
|
"currentTime": currentTimeMs,
|
||||||
"duration": durationMs,
|
"duration": durationMs,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updatePlayerState() {
|
||||||
|
let player = self.vlc.player
|
||||||
|
self.onVideoStateChange?([
|
||||||
|
"target": self.reactTag ?? NSNull(),
|
||||||
|
"currentTime": player.time.intValue,
|
||||||
|
"duration": player.media?.length.intValue ?? 0,
|
||||||
|
"error": false,
|
||||||
|
"isPlaying": player.isPlaying,
|
||||||
|
"isBuffering": !player.isPlaying && player.state == VLCMediaPlayerState.buffering,
|
||||||
|
"state": player.state.description
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Expo Events
|
// MARK: - Expo Events
|
||||||
|
|
||||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||||
@objc var onVideoError: RCTDirectEventBlock?
|
@objc var onVideoError: RCTDirectEventBlock?
|
||||||
|
@objc var onPipStarted: RCTDirectEventBlock?
|
||||||
|
|
||||||
// MARK: - Deinitialization
|
// MARK: - Deinitialization
|
||||||
|
|
||||||
@@ -327,67 +434,6 @@ class VlcPlayerView: ExpoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VlcPlayerView: VLCMediaPlayerDelegate {
|
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification) {
|
|
||||||
// self?.updateVideoProgress()
|
|
||||||
let timeNow = Date().timeIntervalSince1970
|
|
||||||
if timeNow - lastProgressCall >= 1 {
|
|
||||||
lastProgressCall = timeNow
|
|
||||||
updateVideoProgress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
|
||||||
self.updatePlayerState()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updatePlayerState() {
|
|
||||||
guard let player = self.mediaPlayer else { return }
|
|
||||||
let currentState = player.state
|
|
||||||
|
|
||||||
var stateInfo: [String: Any] = [
|
|
||||||
"target": self.reactTag ?? NSNull(),
|
|
||||||
"currentTime": player.time.intValue,
|
|
||||||
"duration": player.media?.length.intValue ?? 0,
|
|
||||||
"error": false,
|
|
||||||
]
|
|
||||||
|
|
||||||
if player.isPlaying {
|
|
||||||
stateInfo["isPlaying"] = true
|
|
||||||
stateInfo["isBuffering"] = false
|
|
||||||
stateInfo["state"] = "Playing"
|
|
||||||
} else {
|
|
||||||
stateInfo["isPlaying"] = false
|
|
||||||
stateInfo["state"] = "Paused"
|
|
||||||
}
|
|
||||||
|
|
||||||
if player.state == VLCMediaPlayerState.buffering {
|
|
||||||
stateInfo["isBuffering"] = true
|
|
||||||
stateInfo["state"] = "Buffering"
|
|
||||||
} else if player.state == VLCMediaPlayerState.error {
|
|
||||||
print("player.state ~ error")
|
|
||||||
stateInfo["state"] = "Error"
|
|
||||||
self.onVideoLoadEnd?(stateInfo)
|
|
||||||
} else if player.state == VLCMediaPlayerState.opening {
|
|
||||||
print("player.state ~ opening")
|
|
||||||
stateInfo["state"] = "Opening"
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.lastReportedState != currentState
|
|
||||||
|| self.lastReportedIsPlaying != player.isPlaying
|
|
||||||
{
|
|
||||||
self.lastReportedState = currentState
|
|
||||||
self.lastReportedIsPlaying = player.isPlaying
|
|
||||||
self.onVideoStateChange?(stateInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VlcPlayerView: VLCMediaDelegate {
|
|
||||||
// Implement VLCMediaDelegate methods if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
extension VLCMediaPlayerState {
|
extension VLCMediaPlayerState {
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -396,9 +442,7 @@ extension VLCMediaPlayerState {
|
|||||||
case .playing: return "Playing"
|
case .playing: return "Playing"
|
||||||
case .paused: return "Paused"
|
case .paused: return "Paused"
|
||||||
case .stopped: return "Stopped"
|
case .stopped: return "Stopped"
|
||||||
case .ended: return "Ended"
|
|
||||||
case .error: return "Error"
|
case .error: return "Error"
|
||||||
case .esAdded: return "ESAdded"
|
|
||||||
@unknown default: return "Unknown"
|
@unknown default: return "Unknown"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export type VideoLoadStartPayload = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PipStartedPayload = {
|
||||||
|
nativeEvent: {
|
||||||
|
pipStarted: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type VideoStateChangePayload = PlaybackStatePayload;
|
export type VideoStateChangePayload = PlaybackStatePayload;
|
||||||
|
|
||||||
export type VideoProgressPayload = ProgressUpdatePayload;
|
export type VideoProgressPayload = ProgressUpdatePayload;
|
||||||
@@ -64,9 +70,11 @@ export type VlcPlayerViewProps = {
|
|||||||
onVideoLoadStart?: (event: VideoLoadStartPayload) => void;
|
onVideoLoadStart?: (event: VideoLoadStartPayload) => void;
|
||||||
onVideoLoadEnd?: (event: VideoLoadStartPayload) => void;
|
onVideoLoadEnd?: (event: VideoLoadStartPayload) => void;
|
||||||
onVideoError?: (event: PlaybackStatePayload) => void;
|
onVideoError?: (event: PlaybackStatePayload) => void;
|
||||||
|
onPipStarted?: (event: PipStartedPayload) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface VlcPlayerViewRef {
|
export interface VlcPlayerViewRef {
|
||||||
|
startPictureInPicture: () => Promise<void>;
|
||||||
play: () => Promise<void>;
|
play: () => Promise<void>;
|
||||||
pause: () => Promise<void>;
|
pause: () => Promise<void>;
|
||||||
stop: () => Promise<void>;
|
stop: () => Promise<void>;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
|||||||
const nativeRef = React.useRef<NativeViewRef>(null);
|
const nativeRef = React.useRef<NativeViewRef>(null);
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
startPictureInPicture: async () => {
|
||||||
|
await nativeRef.current?.startPictureInPicture()
|
||||||
|
},
|
||||||
play: async () => {
|
play: async () => {
|
||||||
await nativeRef.current?.play();
|
await nativeRef.current?.play();
|
||||||
},
|
},
|
||||||
@@ -96,6 +99,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
|||||||
onVideoProgress,
|
onVideoProgress,
|
||||||
onVideoLoadEnd,
|
onVideoLoadEnd,
|
||||||
onVideoError,
|
onVideoError,
|
||||||
|
onPipStarted,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -122,6 +126,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
|||||||
onVideoStateChange={onVideoStateChange}
|
onVideoStateChange={onVideoStateChange}
|
||||||
onVideoProgress={onVideoProgress}
|
onVideoProgress={onVideoProgress}
|
||||||
onVideoError={onVideoError}
|
onVideoError={onVideoError}
|
||||||
|
onPipStarted={onPipStarted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
38
plugins/withAndroidManifest.js
Normal file
38
plugins/withAndroidManifest.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const { withAndroidManifest: NativeAndroidManifest } = require("@expo/config-plugins");
|
||||||
|
|
||||||
|
const withAndroidManifest = (config) =>
|
||||||
|
NativeAndroidManifest(config, async (config) => {
|
||||||
|
const mainApplication = config.modResults.manifest.application[0];
|
||||||
|
|
||||||
|
// Initialize activity array if it doesn't exist
|
||||||
|
if (!mainApplication.activity) {
|
||||||
|
mainApplication.activity = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const googleCastActivityExists = mainApplication.activity.some(activity =>
|
||||||
|
activity.$?.["android:name"] === "com.reactnative.googlecast.RNGCExpandedControllerActivity"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only add the activity if it doesn't already exist
|
||||||
|
if (!googleCastActivityExists) {
|
||||||
|
mainApplication.activity.push({
|
||||||
|
$: {
|
||||||
|
"android:name": "com.reactnative.googlecast.RNGCExpandedControllerActivity",
|
||||||
|
"android:theme": "@style/Theme.MaterialComponents.NoActionBar",
|
||||||
|
"android:launchMode": "singleTask",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainActivity = mainApplication.activity.find(activity =>
|
||||||
|
activity.$?.["android:name"] === ".MainActivity"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mainActivity) {
|
||||||
|
mainActivity.$["android:supportsPictureInPicture"] = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = withAndroidManifest;
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
const { withAndroidManifest } = require("@expo/config-plugins");
|
|
||||||
|
|
||||||
const withGoogleCastActivity = (config) =>
|
|
||||||
withAndroidManifest(config, async (config) => {
|
|
||||||
const mainApplication = config.modResults.manifest.application[0];
|
|
||||||
|
|
||||||
// Initialize activity array if it doesn't exist
|
|
||||||
if (!mainApplication.activity) {
|
|
||||||
mainApplication.activity = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the activity already exists
|
|
||||||
const activityExists = mainApplication.activity.some(
|
|
||||||
(activity) =>
|
|
||||||
activity.$?.["android:name"] ===
|
|
||||||
"com.reactnative.googlecast.RNGCExpandedControllerActivity"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only add the activity if it doesn't already exist
|
|
||||||
if (!activityExists) {
|
|
||||||
mainApplication.activity.push({
|
|
||||||
$: {
|
|
||||||
"android:name":
|
|
||||||
"com.reactnative.googlecast.RNGCExpandedControllerActivity",
|
|
||||||
"android:theme": "@style/Theme.MaterialComponents.NoActionBar",
|
|
||||||
"android:launchMode": "singleTask",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = withGoogleCastActivity;
|
|
||||||
Reference in New Issue
Block a user