Compare commits

...

1 Commits

Author SHA1 Message Date
herrrta
7ea2d81fb4 feat: Use VLC discovery API for chromecast and other devices
Currently, this only works for chromecast. In the future, this implementation will also work for other types like upnp & airplay.
2025-02-23 12:57:42 -05:00
17 changed files with 305 additions and 94 deletions

View File

@@ -7,10 +7,11 @@ import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybac
import { useWebSocket } from "@/hooks/useWebsockets"; import { useWebSocket } from "@/hooks/useWebsockets";
import { VlcPlayerView } from "@/modules/vlc-player"; import { VlcPlayerView } from "@/modules/vlc-player";
import { import {
OnDiscoveryStateChangedPayload,
PipStartedPayload, PipStartedPayload,
PlaybackStatePayload, PlaybackStatePayload,
ProgressUpdatePayload, ProgressUpdatePayload,
VlcPlayerViewRef, VlcPlayerViewRef, VLCRendererItem,
} from "@/modules/vlc-player/src/VlcPlayer.types"; } from "@/modules/vlc-player/src/VlcPlayer.types";
const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null; const downloadProvider = !Platform.isTV ? require("@/providers/DownloadProvider") : null;
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
@@ -30,6 +31,11 @@ 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";
import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client"; 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() { export default function page() {
const videoRef = useRef<VlcPlayerViewRef>(null); const videoRef = useRef<VlcPlayerViewRef>(null);
@@ -45,6 +51,8 @@ export default function page() {
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 [isPipStarted, setIsPipStarted] = useState(false);
const [rendererItems, setRendererItems] = useState<VLCRendererItem[]>([]);
const discoveryModal = useRef<BottomSheetModal>(null);
const progress = useSharedValue(0); const progress = useSharedValue(0);
const isSeeking = useSharedValue(false); const isSeeking = useSharedValue(false);
@@ -243,11 +251,6 @@ export default function page() {
[item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering] [item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering]
); );
const onPipStarted = useCallback((e: PipStartedPayload) => {
const { pipStarted } = e.nativeEvent;
setIsPipStarted(pipStarted);
}, []);
const changePlaybackState = useCallback( const changePlaybackState = useCallback(
async (isPlaying: boolean) => { async (isPlaying: boolean) => {
if (!api || offline || !stream) return; if (!api || offline || !stream) return;
@@ -298,9 +301,25 @@ export default function page() {
offline, offline,
}); });
const onPlaybackStateChanged = useCallback( const onPipStarted = useCallback((e: PipStartedPayload) => {
async (e: PlaybackStatePayload) => { const { pipStarted } = e.nativeEvent;
const { state, isBuffering, isPlaying } = 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") { if (state === "Playing") {
setIsPlaying(true); setIsPlaying(true);
@@ -409,6 +428,7 @@ export default function page() {
progressUpdateInterval={1000} progressUpdateInterval={1000}
onVideoStateChange={onPlaybackStateChanged} onVideoStateChange={onPlaybackStateChanged}
onPipStarted={onPipStarted} onPipStarted={onPipStarted}
onDiscoveryStateChanged={onDiscoveryStateChanged}
onVideoLoadEnd={() => { onVideoLoadEnd={() => {
setIsVideoLoaded(true); setIsVideoLoaded(true);
}} }}
@@ -420,34 +440,76 @@ export default function page() {
/> />
</View> </View>
{videoRef.current && !isPipStarted && isMounted === true ? ( {videoRef.current && !isPipStarted && isMounted === true ? (
<Controls <>
mediaSource={stream?.mediaSource} <Controls
item={item} mediaSource={stream?.mediaSource}
videoRef={videoRef} item={item}
togglePlay={togglePlay} videoRef={videoRef}
isPlaying={isPlaying} togglePlay={togglePlay}
isSeeking={isSeeking} isPlaying={isPlaying}
progress={progress} isSeeking={isSeeking}
cacheProgress={cacheProgress} progress={progress}
isBuffering={isBuffering} cacheProgress={cacheProgress}
showControls={showControls} isBuffering={isBuffering}
setShowControls={setShowControls} showControls={showControls}
setIgnoreSafeAreas={setIgnoreSafeAreas} setShowControls={setShowControls}
ignoreSafeAreas={ignoreSafeAreas} setIgnoreSafeAreas={setIgnoreSafeAreas}
isVideoLoaded={isVideoLoaded} ignoreSafeAreas={ignoreSafeAreas}
startPictureInPicture={videoRef?.current?.startPictureInPicture} isVideoLoaded={isVideoLoaded}
play={videoRef.current?.play} startPictureInPicture={videoRef?.current?.startPictureInPicture}
pause={videoRef.current?.pause} play={videoRef.current?.play}
seek={videoRef.current?.seekTo} pause={videoRef.current?.pause}
enableTrickplay={true} seek={videoRef.current?.seekTo}
getAudioTracks={videoRef.current?.getAudioTracks} enableTrickplay={true}
getSubtitleTracks={videoRef.current?.getSubtitleTracks} getAudioTracks={videoRef.current?.getAudioTracks}
offline={offline} getSubtitleTracks={videoRef.current?.getSubtitleTracks}
setSubtitleTrack={videoRef.current.setSubtitleTrack} offline={offline}
setSubtitleURL={videoRef.current.setSubtitleURL} setSubtitleTrack={videoRef.current.setSubtitleTrack}
setAudioTrack={videoRef.current.setAudioTrack} setSubtitleURL={videoRef.current.setSubtitleURL}
isVlc 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} ) : null}
</View> </View>
); );

View File

@@ -1,4 +1,4 @@
import { Ionicons } from "@expo/vector-icons"; import {Ionicons, MaterialCommunityIcons} from "@expo/vector-icons";
import { PropsWithChildren, ReactNode } from "react"; import { PropsWithChildren, ReactNode } from "react";
import { import {
TouchableOpacity, TouchableOpacity,
@@ -13,7 +13,7 @@ interface Props extends TouchableOpacityProps, ViewProps {
value?: string | null | undefined; value?: string | null | undefined;
children?: ReactNode; children?: ReactNode;
iconAfter?: ReactNode; iconAfter?: ReactNode;
icon?: keyof typeof Ionicons.glyphMap; icon?: keyof typeof Ionicons.glyphMap | keyof typeof MaterialCommunityIcons.glyphMap;
showArrow?: boolean; showArrow?: boolean;
textColor?: "default" | "blue" | "red"; textColor?: "default" | "blue" | "red";
onPress?: () => void; onPress?: () => void;
@@ -89,7 +89,19 @@ const ListItemContent = ({
<View className="flex flex-row items-center w-full"> <View className="flex flex-row items-center w-full">
{icon && ( {icon && (
<View className="border border-neutral-800 rounded-md h-8 w-8 flex items-center justify-center mr-2"> <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> </View>
)} )}
<Text <Text

View File

@@ -80,6 +80,7 @@ interface Props {
mediaSource?: MediaSourceInfo | null; mediaSource?: MediaSourceInfo | null;
seek: (ticks: number) => void; seek: (ticks: number) => void;
startPictureInPicture: () => Promise<void>; startPictureInPicture: () => Promise<void>;
startDiscovery: () => Promise<void>;
play: (() => Promise<void>) | (() => void); play: (() => Promise<void>) | (() => void);
pause: () => void; pause: () => void;
getAudioTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]); getAudioTracks?: (() => Promise<TrackInfo[] | null>) | (() => TrackInfo[]);
@@ -93,32 +94,33 @@ interface Props {
const CONTROLS_TIMEOUT = 4000; const CONTROLS_TIMEOUT = 4000;
export const Controls: React.FC<Props> = ({ export const Controls: React.FC<Props> = ({
item, item,
seek, seek,
startPictureInPicture, startDiscovery,
play, startPictureInPicture,
pause, play,
togglePlay, pause,
isPlaying, togglePlay,
isSeeking, isPlaying,
progress, isSeeking,
isBuffering, progress,
cacheProgress, isBuffering,
showControls, cacheProgress,
setShowControls, showControls,
ignoreSafeAreas, setShowControls,
setIgnoreSafeAreas, ignoreSafeAreas,
mediaSource, setIgnoreSafeAreas,
isVideoLoaded, mediaSource,
getAudioTracks, isVideoLoaded,
getSubtitleTracks, getAudioTracks,
setSubtitleURL, getSubtitleTracks,
setSubtitleTrack, setSubtitleURL,
setAudioTrack, setSubtitleTrack,
offline = false, setAudioTrack,
enableTrickplay = true, offline = false,
isVlc = false, enableTrickplay = true,
}) => { isVlc = false,
}) => {
const [settings] = useSettings(); const [settings] = useSettings();
const router = useRouter(); const router = useRouter();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -494,6 +496,17 @@ export const Controls: React.FC<Props> = ({
)} )}
<View className="flex flex-row items-center space-x-2 "> <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 && ( {!Platform.isTV && (
<TouchableOpacity <TouchableOpacity
onPress={startPictureInPicture} onPress={startPictureInPicture}

View File

@@ -23,7 +23,8 @@ public class VlcPlayerModule: Module {
"onVideoLoadEnd", "onVideoLoadEnd",
"onVideoProgress", "onVideoProgress",
"onVideoError", "onVideoError",
"onPipStarted" "onPipStarted",
"onDiscoveryStateChanged"
) )
AsyncFunction("startPictureInPicture") { (view: VlcPlayerView) in AsyncFunction("startPictureInPicture") { (view: VlcPlayerView) in
@@ -42,6 +43,14 @@ public class VlcPlayerModule: Module {
view.stop() view.stop()
} }
AsyncFunction("startDiscovery") { (view: VlcPlayerView) in
view.startDiscovery()
}
AsyncFunction("stopDiscovery") { (view: VlcPlayerView) in
view.stopDiscovery()
}
AsyncFunction("seekTo") { (view: VlcPlayerView, time: Int32) in AsyncFunction("seekTo") { (view: VlcPlayerView, time: Int32) in
view.seekTo(time) view.seekTo(time)
} }

View File

@@ -26,11 +26,15 @@ public class VLCPlayerView: UIView {
} }
class VLCPlayerWrapper: NSObject { class VLCPlayerWrapper: NSObject {
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "VLCPlayerWrapper")
private var lastProgressCall = Date().timeIntervalSince1970 private var lastProgressCall = Date().timeIntervalSince1970
public var player: VLCMediaPlayer = VLCMediaPlayer() public var player: VLCMediaPlayer = VLCMediaPlayer()
private var updatePlayerState: (() -> Void)? private var updatePlayerState: (() -> Void)?
private var updateVideoProgress: (() -> Void)? private var updateVideoProgress: (() -> Void)?
private var onDiscoveryStateChanged: ((_ renderers: [[String : Any]]) -> Void)?
private var playerView: VLCPlayerView = VLCPlayerView() private var playerView: VLCPlayerView = VLCPlayerView()
public var discoverer: VLCRendererDiscoverer?
public weak var pipController: VLCPictureInPictureWindowControlling? public weak var pipController: VLCPictureInPictureWindowControlling?
override public init() { override public init() {
@@ -38,15 +42,22 @@ class VLCPlayerWrapper: NSObject {
player.delegate = self player.delegate = self
player.drawable = self player.drawable = self
player.scaleFactor = 0 player.scaleFactor = 0
#if DEBUG
let consoleLogger = VLCConsoleLogger()
consoleLogger.level = VLCLogLevel.debug
player.libraryInstance.loggers = [consoleLogger]
#endif
} }
public func setup( public func setup(
parent: UIView, parent: UIView,
updatePlayerState: (() -> Void)?, updatePlayerState: (() -> Void)?,
updateVideoProgress: (() -> Void)? updateVideoProgress: (() -> Void)?,
onDiscoveryStateChanged: ((_ renderers: [[String : Any]]) -> Void)?
) { ) {
self.updatePlayerState = updatePlayerState self.updatePlayerState = updatePlayerState
self.updateVideoProgress = updateVideoProgress self.updateVideoProgress = updateVideoProgress
self.onDiscoveryStateChanged = onDiscoveryStateChanged
player.delegate = self player.delegate = self
parent.addSubview(playerView) parent.addSubview(playerView)
@@ -56,6 +67,50 @@ class VLCPlayerWrapper: NSObject {
public func getPlayerView() -> UIView { public func getPlayerView() -> UIView {
return playerView 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 // MARK: - VLCPictureInPictureDrawable
@@ -156,6 +211,16 @@ class VlcPlayerView: ExpoView {
private var externalSubtitles: [[String: String]]? private var externalSubtitles: [[String: String]]?
var hasSource = false 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 // MARK: - Initialization
required init(appContext: AppContext? = nil) { required init(appContext: AppContext? = nil) {
super.init(appContext: appContext) super.init(appContext: appContext)
@@ -169,10 +234,15 @@ class VlcPlayerView: ExpoView {
vlc.setup( vlc.setup(
parent: self, parent: self,
updatePlayerState: updatePlayerState, updatePlayerState: updatePlayerState,
updateVideoProgress: updateVideoProgress updateVideoProgress: updateVideoProgress,
onDiscoveryStateChanged: updateDiscoveryState
) )
} }
private func updateDiscoveryState(renderers: [[String: Any]]) {
self.onDiscoveryStateChanged?(["renderers": renderers])
}
private func setupNotifications() { private func setupNotifications() {
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, selector: #selector(applicationWillResignActive), self, selector: #selector(applicationWillResignActive),
@@ -190,6 +260,19 @@ class VlcPlayerView: ExpoView {
self.vlc.pipController?.startPictureInPicture() 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() { @objc func play() {
self.vlc.player.play() self.vlc.player.play()
self.isPaused = false self.isPaused = false
@@ -240,12 +323,6 @@ class VlcPlayerView: ExpoView {
self.startPosition = source["startPosition"] as? Int32 ?? 0 self.startPosition = source["startPosition"] as? Int32 ?? 0
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]] 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 { guard let uri = source["uri"] as? String, !uri.isEmpty else {
logger.error("Invalid or empty URI") logger.error("Invalid or empty URI")
self.onVideoError?(["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)") logger.debug("Media options: \(mediaOptions)")
media.addOptions(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 // MARK: - Deinitialization
deinit { deinit {

View File

@@ -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 VideoStateChangePayload = PlaybackStatePayload;
export type VideoProgressPayload = ProgressUpdatePayload; export type VideoProgressPayload = ProgressUpdatePayload;
@@ -71,9 +83,12 @@ export type VlcPlayerViewProps = {
onVideoLoadEnd?: (event: VideoLoadStartPayload) => void; onVideoLoadEnd?: (event: VideoLoadStartPayload) => void;
onVideoError?: (event: PlaybackStatePayload) => void; onVideoError?: (event: PlaybackStatePayload) => void;
onPipStarted?: (event: PipStartedPayload) => void; onPipStarted?: (event: PipStartedPayload) => void;
onDiscoveryStateChanged?: (event: OnDiscoveryStateChangedPayload) => void;
}; };
export interface VlcPlayerViewRef { export interface VlcPlayerViewRef {
startDiscovery: () => Promise<void>;
stopDiscovery: () => Promise<void>;
startPictureInPicture: () => Promise<void>; startPictureInPicture: () => Promise<void>;
play: () => Promise<void>; play: () => Promise<void>;
pause: () => Promise<void>; pause: () => Promise<void>;

View File

@@ -23,6 +23,12 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
const nativeRef = React.useRef<NativeViewRef>(null); const nativeRef = React.useRef<NativeViewRef>(null);
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
startDiscovery: async () => {
await nativeRef.current?.startDiscovery()
},
stopDiscovery: async () => {
await nativeRef.current?.stopDiscovery()
},
startPictureInPicture: async () => { startPictureInPicture: async () => {
await nativeRef.current?.startPictureInPicture() await nativeRef.current?.startPictureInPicture()
}, },
@@ -100,6 +106,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
onVideoLoadEnd, onVideoLoadEnd,
onVideoError, onVideoError,
onPipStarted, onPipStarted,
onDiscoveryStateChanged,
...otherProps ...otherProps
} = props; } = props;
@@ -127,6 +134,7 @@ const VlcPlayerView = React.forwardRef<VlcPlayerViewRef, VlcPlayerViewProps>(
onVideoProgress={onVideoProgress} onVideoProgress={onVideoProgress}
onVideoError={onVideoError} onVideoError={onVideoError}
onPipStarted={onPipStarted} onPipStarted={onPipStarted}
onDiscoveryStateChanged={onDiscoveryStateChanged}
/> />
); );
} }

View File

@@ -352,7 +352,8 @@
"audio_tracks": "Audiospuren:", "audio_tracks": "Audiospuren:",
"playback_state": "Wiedergabestatus:", "playback_state": "Wiedergabestatus:",
"no_data_available": "Keine Daten verfügbar", "no_data_available": "Keine Daten verfügbar",
"index": "Index:" "index": "Index:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "Als Nächstes", "next_up": "Als Nächstes",

View File

@@ -356,7 +356,8 @@
"audio_tracks": "Audio Tracks:", "audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:", "playback_state": "Playback State:",
"no_data_available": "No data available", "no_data_available": "No data available",
"index": "Index:" "index": "Index:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "Next up", "next_up": "Next up",

View File

@@ -352,7 +352,8 @@
"audio_tracks": "Pistas de audio:", "audio_tracks": "Pistas de audio:",
"playback_state": "Estado de la reproducción:", "playback_state": "Estado de la reproducción:",
"no_data_available": "No hay datos disponibles", "no_data_available": "No hay datos disponibles",
"index": "Índice:" "index": "Índice:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "A continuación", "next_up": "A continuación",

View File

@@ -353,7 +353,8 @@
"audio_tracks": "Pistes audio:", "audio_tracks": "Pistes audio:",
"playback_state": "État de lecture:", "playback_state": "État de lecture:",
"no_data_available": "Aucune donnée disponible", "no_data_available": "Aucune donnée disponible",
"index": "Index:" "index": "Index:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "À suivre", "next_up": "À suivre",

View File

@@ -352,7 +352,8 @@
"audio_tracks": "Tracce audio:", "audio_tracks": "Tracce audio:",
"playback_state": "Stato della riproduzione:", "playback_state": "Stato della riproduzione:",
"no_data_available": "Nessun dato disponibile", "no_data_available": "Nessun dato disponibile",
"index": "Indice:" "index": "Indice:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "Il prossimo", "next_up": "Il prossimo",

View File

@@ -351,7 +351,8 @@
"audio_tracks": "音声トラック:", "audio_tracks": "音声トラック:",
"playback_state": "再生状態:", "playback_state": "再生状態:",
"no_data_available": "データなし", "no_data_available": "データなし",
"index": "インデックス:" "index": "インデックス:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "次", "next_up": "次",

View File

@@ -352,7 +352,8 @@
"audio_tracks": "Audio Tracks:", "audio_tracks": "Audio Tracks:",
"playback_state": "Afspeelstatus:", "playback_state": "Afspeelstatus:",
"no_data_available": "Geen data beschikbaar", "no_data_available": "Geen data beschikbaar",
"index": "Index:" "index": "Index:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "Volgende", "next_up": "Volgende",

View File

@@ -351,7 +351,8 @@
"audio_tracks": "Ses Parçaları:", "audio_tracks": "Ses Parçaları:",
"playback_state": "Oynatma Durumu:", "playback_state": "Oynatma Durumu:",
"no_data_available": "Veri bulunamadı", "no_data_available": "Veri bulunamadı",
"index": "İndeks:" "index": "İndeks:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "Sıradaki", "next_up": "Sıradaki",

View File

@@ -351,7 +351,8 @@
"audio_tracks": "音频轨道:", "audio_tracks": "音频轨道:",
"playback_state": "播放状态:", "playback_state": "播放状态:",
"no_data_available": "无可用数据", "no_data_available": "无可用数据",
"index": "索引:" "index": "索引:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "下一个", "next_up": "下一个",

View File

@@ -351,7 +351,8 @@
"audio_tracks": "音頻軌道:", "audio_tracks": "音頻軌道:",
"playback_state": "播放狀態:", "playback_state": "播放狀態:",
"no_data_available": "無可用數據", "no_data_available": "無可用數據",
"index": "索引:" "index": "索引:",
"device_discovery": "Device discovery"
}, },
"item_card": { "item_card": {
"next_up": "下一個", "next_up": "下一個",