forked from Ninjalama/streamyfin_mirror
Compare commits
1 Commits
lint/remov
...
feat/vlc-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ea2d81fb4 |
@@ -7,10 +7,11 @@ import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybac
|
||||
import { useWebSocket } from "@/hooks/useWebsockets";
|
||||
import { VlcPlayerView } from "@/modules/vlc-player";
|
||||
import {
|
||||
OnDiscoveryStateChangedPayload,
|
||||
PipStartedPayload,
|
||||
PlaybackStatePayload,
|
||||
ProgressUpdatePayload,
|
||||
VlcPlayerViewRef,
|
||||
VlcPlayerViewRef, VLCRendererItem,
|
||||
} from "@/modules/vlc-player/src/VlcPlayer.types";
|
||||
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
@@ -30,6 +31,11 @@ import { useSettings } from "@/utils/atoms/settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModal, BottomSheetView} from "@gorhom/bottom-sheet";
|
||||
import {ListGroup} from "@/components/list/ListGroup";
|
||||
import {ListItem} from "@/components/list/ListItem";
|
||||
import {storage} from "@/utils/mmkv";
|
||||
import {t} from "i18next";
|
||||
|
||||
export default function page() {
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
@@ -45,6 +51,8 @@ export default function page() {
|
||||
const [isBuffering, setIsBuffering] = useState(true);
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||
const [isPipStarted, setIsPipStarted] = useState(false);
|
||||
const [rendererItems, setRendererItems] = useState<VLCRendererItem[]>([]);
|
||||
const discoveryModal = useRef<BottomSheetModal>(null);
|
||||
|
||||
const progress = useSharedValue(0);
|
||||
const isSeeking = useSharedValue(false);
|
||||
@@ -243,11 +251,6 @@ export default function page() {
|
||||
[item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering]
|
||||
);
|
||||
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
const { pipStarted } = e.nativeEvent;
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
|
||||
const changePlaybackState = useCallback(
|
||||
async (isPlaying: boolean) => {
|
||||
if (!api || offline || !stream) return;
|
||||
@@ -298,9 +301,25 @@ export default function page() {
|
||||
offline,
|
||||
});
|
||||
|
||||
const onPlaybackStateChanged = useCallback(
|
||||
async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
const onPipStarted = useCallback((e: PipStartedPayload) => {
|
||||
const { pipStarted } = e.nativeEvent;
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
|
||||
const onDiscoveryStateChanged = useCallback((e: OnDiscoveryStateChangedPayload) => {
|
||||
const {renderers} = e.nativeEvent;
|
||||
setRendererItems(renderers);
|
||||
}, []);
|
||||
|
||||
const startDiscovery = useCallback(async () => {
|
||||
videoRef?.current?.pause?.()
|
||||
videoRef?.current?.stopDiscovery?.()
|
||||
videoRef?.current?.startDiscovery?.()
|
||||
discoveryModal?.current?.present?.()
|
||||
}, [rendererItems, videoRef])
|
||||
|
||||
const onPlaybackStateChanged = useCallback(async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
|
||||
if (state === "Playing") {
|
||||
setIsPlaying(true);
|
||||
@@ -409,6 +428,7 @@ export default function page() {
|
||||
progressUpdateInterval={1000}
|
||||
onVideoStateChange={onPlaybackStateChanged}
|
||||
onPipStarted={onPipStarted}
|
||||
onDiscoveryStateChanged={onDiscoveryStateChanged}
|
||||
onVideoLoadEnd={() => {
|
||||
setIsVideoLoaded(true);
|
||||
}}
|
||||
@@ -420,34 +440,76 @@ export default function page() {
|
||||
/>
|
||||
</View>
|
||||
{videoRef.current && !isPipStarted && isMounted === true ? (
|
||||
<Controls
|
||||
mediaSource={stream?.mediaSource}
|
||||
item={item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
progress={progress}
|
||||
cacheProgress={cacheProgress}
|
||||
isBuffering={isBuffering}
|
||||
showControls={showControls}
|
||||
setShowControls={setShowControls}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
startPictureInPicture={videoRef?.current?.startPictureInPicture}
|
||||
play={videoRef.current?.play}
|
||||
pause={videoRef.current?.pause}
|
||||
seek={videoRef.current?.seekTo}
|
||||
enableTrickplay={true}
|
||||
getAudioTracks={videoRef.current?.getAudioTracks}
|
||||
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
||||
offline={offline}
|
||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||
setAudioTrack={videoRef.current.setAudioTrack}
|
||||
isVlc
|
||||
/>
|
||||
<>
|
||||
<Controls
|
||||
mediaSource={stream?.mediaSource}
|
||||
item={item}
|
||||
videoRef={videoRef}
|
||||
togglePlay={togglePlay}
|
||||
isPlaying={isPlaying}
|
||||
isSeeking={isSeeking}
|
||||
progress={progress}
|
||||
cacheProgress={cacheProgress}
|
||||
isBuffering={isBuffering}
|
||||
showControls={showControls}
|
||||
setShowControls={setShowControls}
|
||||
setIgnoreSafeAreas={setIgnoreSafeAreas}
|
||||
ignoreSafeAreas={ignoreSafeAreas}
|
||||
isVideoLoaded={isVideoLoaded}
|
||||
startPictureInPicture={videoRef?.current?.startPictureInPicture}
|
||||
play={videoRef.current?.play}
|
||||
pause={videoRef.current?.pause}
|
||||
seek={videoRef.current?.seekTo}
|
||||
enableTrickplay={true}
|
||||
getAudioTracks={videoRef.current?.getAudioTracks}
|
||||
getSubtitleTracks={videoRef.current?.getSubtitleTracks}
|
||||
offline={offline}
|
||||
setSubtitleTrack={videoRef.current.setSubtitleTrack}
|
||||
setSubtitleURL={videoRef.current.setSubtitleURL}
|
||||
setAudioTrack={videoRef.current.setAudioTrack}
|
||||
startDiscovery={startDiscovery}
|
||||
isVlc
|
||||
/>
|
||||
<BottomSheetModal
|
||||
ref={discoveryModal}
|
||||
enableDynamicSizing
|
||||
enableDismissOnClose
|
||||
snapPoints={["100%"]}
|
||||
onDismiss={() => {
|
||||
videoRef.current?.stopDiscovery?.()
|
||||
videoRef.current?.play?.()
|
||||
}}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
backgroundStyle={{
|
||||
backgroundColor: "#171717",
|
||||
}}
|
||||
backdropComponent={(sheetProps: BottomSheetBackdropProps) =>
|
||||
<BottomSheetBackdrop
|
||||
{...sheetProps}
|
||||
disappearsOnIndex={-1}
|
||||
appearsOnIndex={0}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BottomSheetView>
|
||||
<ListGroup title={t("player.device_discovery")} className="mt-4 h-1/3">
|
||||
{rendererItems.map((renderItem, index) => (
|
||||
<ListItem
|
||||
onPress={() => {
|
||||
// todo: set renderer item on player to change to device
|
||||
}}
|
||||
icon="cast"
|
||||
title={renderItem.name}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</ListGroup>
|
||||
|
||||
</BottomSheetView>
|
||||
</BottomSheetModal>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {Ionicons, MaterialCommunityIcons} from "@expo/vector-icons";
|
||||
import { PropsWithChildren, ReactNode } from "react";
|
||||
import {
|
||||
TouchableOpacity,
|
||||
@@ -13,7 +13,7 @@ interface Props extends TouchableOpacityProps, ViewProps {
|
||||
value?: string | null | undefined;
|
||||
children?: ReactNode;
|
||||
iconAfter?: ReactNode;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
icon?: keyof typeof Ionicons.glyphMap | keyof typeof MaterialCommunityIcons.glyphMap;
|
||||
showArrow?: boolean;
|
||||
textColor?: "default" | "blue" | "red";
|
||||
onPress?: () => void;
|
||||
@@ -89,7 +89,19 @@ const ListItemContent = ({
|
||||
<View className="flex flex-row items-center w-full">
|
||||
{icon && (
|
||||
<View className="border border-neutral-800 rounded-md h-8 w-8 flex items-center justify-center mr-2">
|
||||
<Ionicons name="person-circle-outline" size={18} color="white" />
|
||||
{icon in Ionicons.glyphMap ?
|
||||
<Ionicons
|
||||
name={icon as keyof typeof Ionicons.glyphMap}
|
||||
size={18}
|
||||
color="white"
|
||||
/>
|
||||
:
|
||||
<MaterialCommunityIcons
|
||||
name={icon as keyof typeof MaterialCommunityIcons.glyphMap}
|
||||
size={18}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
|
||||
@@ -80,6 +80,7 @@ interface Props {
|
||||
mediaSource?: MediaSourceInfo | null;
|
||||
seek: (ticks: number) => void;
|
||||
startPictureInPicture: () => Promise<void>;
|
||||
startDiscovery: () => Promise<void>;
|
||||
play: (() => Promise<void>) | (() => void);
|
||||
pause: () => void;
|
||||
getAudioTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]);
|
||||
@@ -93,32 +94,33 @@ interface Props {
|
||||
const CONTROLS_TIMEOUT = 4000;
|
||||
|
||||
export const Controls: React.FC<Props> = ({
|
||||
item,
|
||||
seek,
|
||||
startPictureInPicture,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
isPlaying,
|
||||
isSeeking,
|
||||
progress,
|
||||
isBuffering,
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
getSubtitleTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
offline = false,
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
item,
|
||||
seek,
|
||||
startDiscovery,
|
||||
startPictureInPicture,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
isPlaying,
|
||||
isSeeking,
|
||||
progress,
|
||||
isBuffering,
|
||||
cacheProgress,
|
||||
showControls,
|
||||
setShowControls,
|
||||
ignoreSafeAreas,
|
||||
setIgnoreSafeAreas,
|
||||
mediaSource,
|
||||
isVideoLoaded,
|
||||
getAudioTracks,
|
||||
getSubtitleTracks,
|
||||
setSubtitleURL,
|
||||
setSubtitleTrack,
|
||||
setAudioTrack,
|
||||
offline = false,
|
||||
enableTrickplay = true,
|
||||
isVlc = false,
|
||||
}) => {
|
||||
const [settings] = useSettings();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -494,6 +496,17 @@ export const Controls: React.FC<Props> = ({
|
||||
)}
|
||||
|
||||
<View className="flex flex-row items-center space-x-2 ">
|
||||
<TouchableOpacity
|
||||
onPress={startDiscovery}
|
||||
className="aspect-square flex flex-col rounded-xl items-center justify-center p-2"
|
||||
>
|
||||
<MaterialIcons
|
||||
name="cast"
|
||||
size={24}
|
||||
color="white"
|
||||
style={{ opacity: showControls ? 1 : 0 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{!Platform.isTV && (
|
||||
<TouchableOpacity
|
||||
onPress={startPictureInPicture}
|
||||
|
||||
@@ -23,7 +23,8 @@ public class VlcPlayerModule: Module {
|
||||
"onVideoLoadEnd",
|
||||
"onVideoProgress",
|
||||
"onVideoError",
|
||||
"onPipStarted"
|
||||
"onPipStarted",
|
||||
"onDiscoveryStateChanged"
|
||||
)
|
||||
|
||||
AsyncFunction("startPictureInPicture") { (view: VlcPlayerView) in
|
||||
@@ -42,6 +43,14 @@ public class VlcPlayerModule: Module {
|
||||
view.stop()
|
||||
}
|
||||
|
||||
AsyncFunction("startDiscovery") { (view: VlcPlayerView) in
|
||||
view.startDiscovery()
|
||||
}
|
||||
|
||||
AsyncFunction("stopDiscovery") { (view: VlcPlayerView) in
|
||||
view.stopDiscovery()
|
||||
}
|
||||
|
||||
AsyncFunction("seekTo") { (view: VlcPlayerView, time: Int32) in
|
||||
view.seekTo(time)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,15 @@ public class VLCPlayerView: UIView {
|
||||
}
|
||||
|
||||
class VLCPlayerWrapper: NSObject {
|
||||
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VLCPlayerWrapper")
|
||||
|
||||
private var lastProgressCall = Date().timeIntervalSince1970
|
||||
public var player: VLCMediaPlayer = VLCMediaPlayer()
|
||||
private var updatePlayerState: (() -> Void)?
|
||||
private var updateVideoProgress: (() -> Void)?
|
||||
private var onDiscoveryStateChanged: ((_ renderers: [[String : Any]]) -> Void)?
|
||||
private var playerView: VLCPlayerView = VLCPlayerView()
|
||||
public var discoverer: VLCRendererDiscoverer?
|
||||
public weak var pipController: VLCPictureInPictureWindowControlling?
|
||||
|
||||
override public init() {
|
||||
@@ -38,15 +42,22 @@ class VLCPlayerWrapper: NSObject {
|
||||
player.delegate = self
|
||||
player.drawable = self
|
||||
player.scaleFactor = 0
|
||||
#if DEBUG
|
||||
let consoleLogger = VLCConsoleLogger()
|
||||
consoleLogger.level = VLCLogLevel.debug
|
||||
player.libraryInstance.loggers = [consoleLogger]
|
||||
#endif
|
||||
}
|
||||
|
||||
public func setup(
|
||||
parent: UIView,
|
||||
updatePlayerState: (() -> Void)?,
|
||||
updateVideoProgress: (() -> Void)?
|
||||
updateVideoProgress: (() -> Void)?,
|
||||
onDiscoveryStateChanged: ((_ renderers: [[String : Any]]) -> Void)?
|
||||
) {
|
||||
self.updatePlayerState = updatePlayerState
|
||||
self.updateVideoProgress = updateVideoProgress
|
||||
self.onDiscoveryStateChanged = onDiscoveryStateChanged
|
||||
|
||||
player.delegate = self
|
||||
parent.addSubview(playerView)
|
||||
@@ -56,6 +67,50 @@ class VLCPlayerWrapper: NSObject {
|
||||
public func getPlayerView() -> UIView {
|
||||
return playerView
|
||||
}
|
||||
|
||||
public func startDiscovery() {
|
||||
if self.discoverer != nil {
|
||||
self.discoverer!.stop()
|
||||
self.discoverer!.start()
|
||||
return
|
||||
}
|
||||
let _discoverer = VLCRendererDiscoverer(name: "bonjour renderer")
|
||||
_discoverer!.delegate = self
|
||||
|
||||
self.discoverer = _discoverer
|
||||
self.discoverer?.start()
|
||||
}
|
||||
|
||||
public func stopDiscovery() {
|
||||
guard let discoverer = self.discoverer else { return }
|
||||
discoverer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
extension VLCPlayerWrapper: VLCRendererDiscovererDelegate {
|
||||
func rendererDiscovererItemAdded(_ rendererDiscoverer: VLCRendererDiscoverer?, item: VLCRendererItem?) {
|
||||
logger.debug("Renderer item added: \(item)")
|
||||
self.onDiscoveryStateChanged?(getRenderersMap(rendererDiscoverer: rendererDiscoverer))
|
||||
}
|
||||
|
||||
func rendererDiscovererItemDeleted(_ rendererDiscoverer: VLCRendererDiscoverer?, item: VLCRendererItem?) {
|
||||
logger.debug("Renderer item removed: \(item)")
|
||||
self.onDiscoveryStateChanged?(getRenderersMap(rendererDiscoverer: rendererDiscoverer))
|
||||
}
|
||||
|
||||
private func getRenderersMap(rendererDiscoverer: VLCRendererDiscoverer?) -> [[String : Any]] {
|
||||
let renderers = (rendererDiscoverer ?? discoverer)?.renderers.enumerated().map { (index, rendererItem) in
|
||||
return [
|
||||
"index": index,
|
||||
"name": rendererItem.name,
|
||||
"type": rendererItem.type,
|
||||
"iconURI": rendererItem.iconURI,
|
||||
"flags": rendererItem.flags
|
||||
]
|
||||
} ?? []
|
||||
logger.debug("Renderers mapped to: \(renderers)")
|
||||
return renderers
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - VLCPictureInPictureDrawable
|
||||
@@ -156,6 +211,16 @@ class VlcPlayerView: ExpoView {
|
||||
private var externalSubtitles: [[String: String]]?
|
||||
var hasSource = false
|
||||
|
||||
// MARK: - Expo Events
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@objc var onVideoError: RCTDirectEventBlock?
|
||||
@objc var onPipStarted: RCTDirectEventBlock?
|
||||
@objc var onDiscoveryStateChanged: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Initialization
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
@@ -169,10 +234,15 @@ class VlcPlayerView: ExpoView {
|
||||
vlc.setup(
|
||||
parent: self,
|
||||
updatePlayerState: updatePlayerState,
|
||||
updateVideoProgress: updateVideoProgress
|
||||
updateVideoProgress: updateVideoProgress,
|
||||
onDiscoveryStateChanged: updateDiscoveryState
|
||||
)
|
||||
}
|
||||
|
||||
private func updateDiscoveryState(renderers: [[String: Any]]) {
|
||||
self.onDiscoveryStateChanged?(["renderers": renderers])
|
||||
}
|
||||
|
||||
private func setupNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(applicationWillResignActive),
|
||||
@@ -190,6 +260,19 @@ class VlcPlayerView: ExpoView {
|
||||
self.vlc.pipController?.startPictureInPicture()
|
||||
}
|
||||
|
||||
func startDiscovery() {
|
||||
logger.debug("Starting Discovery")
|
||||
self.vlc.startDiscovery()
|
||||
if self.vlc.discoverer != nil {
|
||||
logger.debug("Discoverer description: \(self.vlc.discoverer!.description)")
|
||||
logger.debug("Discoverer renderer: \(self.vlc.discoverer!.renderers)")
|
||||
}
|
||||
}
|
||||
|
||||
func stopDiscovery() {
|
||||
self.vlc.stopDiscovery()
|
||||
}
|
||||
|
||||
@objc func play() {
|
||||
self.vlc.player.play()
|
||||
self.isPaused = false
|
||||
@@ -240,12 +323,6 @@ class VlcPlayerView: ExpoView {
|
||||
self.startPosition = source["startPosition"] as? Int32 ?? 0
|
||||
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
|
||||
|
||||
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 {
|
||||
logger.error("Invalid or empty URI")
|
||||
self.onVideoError?(["error": "Invalid or empty URI"])
|
||||
@@ -270,6 +347,20 @@ class VlcPlayerView: ExpoView {
|
||||
}
|
||||
}
|
||||
|
||||
for item in initOptions {
|
||||
let option = item.components(separatedBy: "=")
|
||||
var key = option[0].replacingOccurrences(of: "--", with: "")
|
||||
if option.count > 1 {
|
||||
mediaOptions.updateValue(
|
||||
option[1],
|
||||
forKey: key
|
||||
)
|
||||
}
|
||||
else {
|
||||
media.addOption(key)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Media options: \(mediaOptions)")
|
||||
media.addOptions(mediaOptions)
|
||||
|
||||
@@ -427,15 +518,6 @@ class VlcPlayerView: ExpoView {
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Expo Events
|
||||
@objc var onPlaybackStateChanged: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadStart: RCTDirectEventBlock?
|
||||
@objc var onVideoStateChange: RCTDirectEventBlock?
|
||||
@objc var onVideoProgress: RCTDirectEventBlock?
|
||||
@objc var onVideoLoadEnd: RCTDirectEventBlock?
|
||||
@objc var onVideoError: RCTDirectEventBlock?
|
||||
@objc var onPipStarted: RCTDirectEventBlock?
|
||||
|
||||
// MARK: - Deinitialization
|
||||
|
||||
deinit {
|
||||
|
||||
@@ -30,6 +30,18 @@ export type PipStartedPayload = {
|
||||
};
|
||||
};
|
||||
|
||||
export type VLCRendererItem = {
|
||||
index: number,
|
||||
name: string,
|
||||
type: string,
|
||||
iconURI: string,
|
||||
flags: number
|
||||
}
|
||||
|
||||
export type OnDiscoveryStateChangedPayload = {
|
||||
nativeEvent: { renderers: VLCRendererItem[] }
|
||||
}
|
||||
|
||||
export type VideoStateChangePayload = PlaybackStatePayload;
|
||||
|
||||
export type VideoProgressPayload = ProgressUpdatePayload;
|
||||
@@ -71,9 +83,12 @@ export type VlcPlayerViewProps = {
|
||||
onVideoLoadEnd?: (event: VideoLoadStartPayload) => void;
|
||||
onVideoError?: (event: PlaybackStatePayload) => void;
|
||||
onPipStarted?: (event: PipStartedPayload) => void;
|
||||
onDiscoveryStateChanged?: (event: OnDiscoveryStateChangedPayload) => void;
|
||||
};
|
||||
|
||||
export interface VlcPlayerViewRef {
|
||||
startDiscovery: () => Promise<void>;
|
||||
stopDiscovery: () => Promise<void>;
|
||||
startPictureInPicture: () => Promise<void>;
|
||||
play: () => Promise<void>;
|
||||
pause: () => Promise<void>;
|
||||
|
||||
@@ -23,6 +23,12 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
const nativeRef = React.useRef<NativeViewRef>(null);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
startDiscovery: async () => {
|
||||
await nativeRef.current?.startDiscovery()
|
||||
},
|
||||
stopDiscovery: async () => {
|
||||
await nativeRef.current?.stopDiscovery()
|
||||
},
|
||||
startPictureInPicture: async () => {
|
||||
await nativeRef.current?.startPictureInPicture()
|
||||
},
|
||||
@@ -100,6 +106,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
onVideoLoadEnd,
|
||||
onVideoError,
|
||||
onPipStarted,
|
||||
onDiscoveryStateChanged,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -127,6 +134,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
|
||||
onVideoProgress={onVideoProgress}
|
||||
onVideoError={onVideoError}
|
||||
onPipStarted={onPipStarted}
|
||||
onDiscoveryStateChanged={onDiscoveryStateChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -352,7 +352,8 @@
|
||||
"audio_tracks": "Audiospuren:",
|
||||
"playback_state": "Wiedergabestatus:",
|
||||
"no_data_available": "Keine Daten verfügbar",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Als Nächstes",
|
||||
|
||||
@@ -356,7 +356,8 @@
|
||||
"audio_tracks": "Audio Tracks:",
|
||||
"playback_state": "Playback State:",
|
||||
"no_data_available": "No data available",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Next up",
|
||||
|
||||
@@ -352,7 +352,8 @@
|
||||
"audio_tracks": "Pistas de audio:",
|
||||
"playback_state": "Estado de la reproducción:",
|
||||
"no_data_available": "No hay datos disponibles",
|
||||
"index": "Índice:"
|
||||
"index": "Índice:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "A continuación",
|
||||
|
||||
@@ -353,7 +353,8 @@
|
||||
"audio_tracks": "Pistes audio:",
|
||||
"playback_state": "État de lecture:",
|
||||
"no_data_available": "Aucune donnée disponible",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "À suivre",
|
||||
|
||||
@@ -352,7 +352,8 @@
|
||||
"audio_tracks": "Tracce audio:",
|
||||
"playback_state": "Stato della riproduzione:",
|
||||
"no_data_available": "Nessun dato disponibile",
|
||||
"index": "Indice:"
|
||||
"index": "Indice:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Il prossimo",
|
||||
|
||||
@@ -351,7 +351,8 @@
|
||||
"audio_tracks": "音声トラック:",
|
||||
"playback_state": "再生状態:",
|
||||
"no_data_available": "データなし",
|
||||
"index": "インデックス:"
|
||||
"index": "インデックス:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "次",
|
||||
|
||||
@@ -352,7 +352,8 @@
|
||||
"audio_tracks": "Audio Tracks:",
|
||||
"playback_state": "Afspeelstatus:",
|
||||
"no_data_available": "Geen data beschikbaar",
|
||||
"index": "Index:"
|
||||
"index": "Index:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Volgende",
|
||||
|
||||
@@ -351,7 +351,8 @@
|
||||
"audio_tracks": "Ses Parçaları:",
|
||||
"playback_state": "Oynatma Durumu:",
|
||||
"no_data_available": "Veri bulunamadı",
|
||||
"index": "İndeks:"
|
||||
"index": "İndeks:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "Sıradaki",
|
||||
|
||||
@@ -351,7 +351,8 @@
|
||||
"audio_tracks": "音频轨道:",
|
||||
"playback_state": "播放状态:",
|
||||
"no_data_available": "无可用数据",
|
||||
"index": "索引:"
|
||||
"index": "索引:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "下一个",
|
||||
|
||||
@@ -351,7 +351,8 @@
|
||||
"audio_tracks": "音頻軌道:",
|
||||
"playback_state": "播放狀態:",
|
||||
"no_data_available": "無可用數據",
|
||||
"index": "索引:"
|
||||
"index": "索引:",
|
||||
"device_discovery": "Device discovery"
|
||||
},
|
||||
"item_card": {
|
||||
"next_up": "下一個",
|
||||
|
||||
Reference in New Issue
Block a user