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.
This commit is contained in:
herrrta
2025-02-22 13:28:40 -05:00
parent cf284eb3d8
commit 7ea2d81fb4
17 changed files with 305 additions and 94 deletions

View File

@@ -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)
}

View File

@@ -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 {

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 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>;

View File

@@ -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}
/>
);
}