airplay ios
This commit is contained in:
parent
fd4efe6c7f
commit
a8b4dc5a01
13 changed files with 471 additions and 108 deletions
|
|
@ -16,6 +16,8 @@ RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber)
|
RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber)
|
RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
|
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(usesExternalPlaybackWhileExternalScreenIsActive, BOOL)
|
||||||
|
|
||||||
// Event properties
|
// Event properties
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock)
|
||||||
|
|
@ -32,11 +34,17 @@ RCT_EXTERN_METHOD(setVolume:(nonnull NSNumber *)node volume:(nonnull NSNumber *)
|
||||||
RCT_EXTERN_METHOD(setAudioTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
|
RCT_EXTERN_METHOD(setAudioTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
|
||||||
RCT_EXTERN_METHOD(setTextTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
|
RCT_EXTERN_METHOD(setTextTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
|
||||||
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
RCT_EXTERN_METHOD(setAllowsExternalPlayback:(nonnull NSNumber *)node allows:(BOOL)allows)
|
||||||
|
RCT_EXTERN_METHOD(setUsesExternalPlaybackWhileExternalScreenIsActive:(nonnull NSNumber *)node uses:(BOOL)uses)
|
||||||
|
RCT_EXTERN_METHOD(getAirPlayState:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
RCT_EXTERN_METHOD(showAirPlayPicker:(nonnull NSNumber *)node)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
||||||
|
|
||||||
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
RCT_EXTERN_METHOD(getAirPlayState:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
RCT_EXTERN_METHOD(showAirPlayPicker:(nonnull NSNumber *)nodeTag)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
||||||
|
|
@ -34,4 +34,26 @@ class KSPlayerModule: RCTEventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func getAirPlayState(_ nodeTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||||
|
viewManager.getAirPlayState(nodeTag, resolve: resolve, reject: reject)
|
||||||
|
} else {
|
||||||
|
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func showAirPlayPicker(_ nodeTag: NSNumber) {
|
||||||
|
print("[KSPlayerModule] showAirPlayPicker called for nodeTag: \(nodeTag)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||||
|
print("[KSPlayerModule] Found KSPlayerViewManager, calling showAirPlayPicker")
|
||||||
|
viewManager.showAirPlayPicker(nodeTag)
|
||||||
|
} else {
|
||||||
|
print("[KSPlayerModule] Could not find KSPlayerViewManager")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import KSPlayer
|
import KSPlayer
|
||||||
import React
|
import React
|
||||||
|
import AVKit
|
||||||
|
|
||||||
@objc(KSPlayerView)
|
@objc(KSPlayerView)
|
||||||
class KSPlayerView: UIView {
|
class KSPlayerView: UIView {
|
||||||
|
|
@ -17,6 +18,8 @@ class KSPlayerView: UIView {
|
||||||
private var currentVolume: Float = 1.0
|
private var currentVolume: Float = 1.0
|
||||||
weak var viewManager: KSPlayerViewManager?
|
weak var viewManager: KSPlayerViewManager?
|
||||||
|
|
||||||
|
// AirPlay properties (removed duplicate declarations)
|
||||||
|
|
||||||
// Event blocks for Fabric
|
// Event blocks for Fabric
|
||||||
@objc var onLoad: RCTDirectEventBlock?
|
@objc var onLoad: RCTDirectEventBlock?
|
||||||
@objc var onProgress: RCTDirectEventBlock?
|
@objc var onProgress: RCTDirectEventBlock?
|
||||||
|
|
@ -58,6 +61,19 @@ class KSPlayerView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AirPlay properties
|
||||||
|
@objc var allowsExternalPlayback: Bool = true {
|
||||||
|
didSet {
|
||||||
|
setAllowsExternalPlayback(allowsExternalPlayback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc var usesExternalPlaybackWhileExternalScreenIsActive: Bool = true {
|
||||||
|
didSet {
|
||||||
|
setUsesExternalPlaybackWhileExternalScreenIsActive(usesExternalPlaybackWhileExternalScreenIsActive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
setupPlayerView()
|
setupPlayerView()
|
||||||
|
|
@ -161,6 +177,12 @@ class KSPlayerView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(currentVolume)
|
setVolume(currentVolume)
|
||||||
|
|
||||||
|
// Ensure AirPlay is properly configured after setting source
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setAllowsExternalPlayback(self.allowsExternalPlayback)
|
||||||
|
self.setUsesExternalPlaybackWhileExternalScreenIsActive(self.usesExternalPlaybackWhileExternalScreenIsActive)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createOptions(with headers: [String: String]) -> KSOptions {
|
private func createOptions(with headers: [String: String]) -> KSOptions {
|
||||||
|
|
@ -283,7 +305,7 @@ class KSPlayerView: UIView {
|
||||||
print("KSPlayerView: Successfully selected audio track \(trackId)")
|
print("KSPlayerView: Successfully selected audio track \(trackId)")
|
||||||
|
|
||||||
// Verify the selection worked
|
// Verify the selection worked
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
let tracksAfter = player.tracks(mediaType: .audio)
|
let tracksAfter = player.tracks(mediaType: .audio)
|
||||||
for (index, track) in tracksAfter.enumerated() {
|
for (index, track) in tracksAfter.enumerated() {
|
||||||
print("KSPlayerView: After selection - Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)")
|
print("KSPlayerView: After selection - Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)")
|
||||||
|
|
@ -399,6 +421,94 @@ class KSPlayerView: UIView {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AirPlay methods
|
||||||
|
func setAllowsExternalPlayback(_ allows: Bool) {
|
||||||
|
print("[KSPlayerView] Setting allowsExternalPlayback: \(allows)")
|
||||||
|
playerView.playerLayer?.player.allowsExternalPlayback = allows
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUsesExternalPlaybackWhileExternalScreenIsActive(_ uses: Bool) {
|
||||||
|
print("[KSPlayerView] Setting usesExternalPlaybackWhileExternalScreenIsActive: \(uses)")
|
||||||
|
playerView.playerLayer?.player.usesExternalPlaybackWhileExternalScreenIsActive = uses
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAirPlayPicker() {
|
||||||
|
print("[KSPlayerView] showAirPlayPicker called")
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// Create a temporary route picker view for triggering AirPlay
|
||||||
|
let routePickerView = AVRoutePickerView()
|
||||||
|
routePickerView.tintColor = .white
|
||||||
|
routePickerView.alpha = 0.01 // Nearly invisible but still interactive
|
||||||
|
|
||||||
|
// Find the current view controller
|
||||||
|
guard let viewController = self.findHostViewController() else {
|
||||||
|
print("[KSPlayerView] Could not find view controller for AirPlay picker")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the view controller's view temporarily
|
||||||
|
viewController.view.addSubview(routePickerView)
|
||||||
|
|
||||||
|
// Position it off-screen but still in the view hierarchy
|
||||||
|
routePickerView.frame = CGRect(x: -100, y: -100, width: 44, height: 44)
|
||||||
|
|
||||||
|
// Force layout
|
||||||
|
viewController.view.setNeedsLayout()
|
||||||
|
viewController.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
// Wait a bit for the view to be ready, then trigger
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
// Find and trigger the AirPlay button
|
||||||
|
self.triggerAirPlayButton(routePickerView)
|
||||||
|
|
||||||
|
// Clean up after a delay
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
|
routePickerView.removeFromSuperview()
|
||||||
|
print("[KSPlayerView] Cleaned up temporary AirPlay picker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func triggerAirPlayButton(_ routePickerView: AVRoutePickerView) {
|
||||||
|
// Recursively find the button in the route picker view
|
||||||
|
func findButton(in view: UIView) -> UIButton? {
|
||||||
|
if let button = view as? UIButton {
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
for subview in view.subviews {
|
||||||
|
if let button = findButton(in: subview) {
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let button = findButton(in: routePickerView) {
|
||||||
|
print("[KSPlayerView] Found AirPlay button, triggering tap")
|
||||||
|
button.sendActions(for: .touchUpInside)
|
||||||
|
} else {
|
||||||
|
print("[KSPlayerView] Could not find AirPlay button in route picker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAirPlayState() -> [String: Any] {
|
||||||
|
guard let player = playerView.playerLayer?.player else {
|
||||||
|
return [
|
||||||
|
"allowsExternalPlayback": false,
|
||||||
|
"usesExternalPlaybackWhileExternalScreenIsActive": false,
|
||||||
|
"isExternalPlaybackActive": false
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"allowsExternalPlayback": player.allowsExternalPlayback,
|
||||||
|
"usesExternalPlaybackWhileExternalScreenIsActive": player.usesExternalPlaybackWhileExternalScreenIsActive,
|
||||||
|
"isExternalPlaybackActive": player.isExternalPlaybackActive
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// Get current player state for React Native
|
// Get current player state for React Native
|
||||||
func getCurrentState() -> [String: Any] {
|
func getCurrentState() -> [String: Any] {
|
||||||
guard let player = playerView.playerLayer?.player else {
|
guard let player = playerView.playerLayer?.player else {
|
||||||
|
|
@ -419,6 +529,15 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
||||||
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||||
switch state {
|
switch state {
|
||||||
case .readyToPlay:
|
case .readyToPlay:
|
||||||
|
// Ensure AirPlay is properly configured when player is ready
|
||||||
|
layer.player.allowsExternalPlayback = allowsExternalPlayback
|
||||||
|
layer.player.usesExternalPlaybackWhileExternalScreenIsActive = usesExternalPlaybackWhileExternalScreenIsActive
|
||||||
|
|
||||||
|
// Determine player backend type
|
||||||
|
let uriString = currentSource?["uri"] as? String
|
||||||
|
let isMKV = uriString?.lowercased().contains(".mkv") ?? false
|
||||||
|
let playerBackend = isMKV ? "KSMEPlayer" : "KSAVPlayer"
|
||||||
|
|
||||||
// Send onLoad event to React Native with track information
|
// Send onLoad event to React Native with track information
|
||||||
let p = layer.player
|
let p = layer.player
|
||||||
let tracks = getAvailableTracks()
|
let tracks = getAvailableTracks()
|
||||||
|
|
@ -430,7 +549,8 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
||||||
"height": p.naturalSize.height
|
"height": p.naturalSize.height
|
||||||
],
|
],
|
||||||
"audioTracks": tracks["audioTracks"] ?? [],
|
"audioTracks": tracks["audioTracks"] ?? [],
|
||||||
"textTracks": tracks["textTracks"] ?? []
|
"textTracks": tracks["textTracks"] ?? [],
|
||||||
|
"playerBackend": playerBackend
|
||||||
])
|
])
|
||||||
case .buffering:
|
case .buffering:
|
||||||
sendEvent("onBuffering", ["isBuffering": true])
|
sendEvent("onBuffering", ["isBuffering": true])
|
||||||
|
|
@ -453,7 +573,8 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
||||||
sendEvent("onProgress", [
|
sendEvent("onProgress", [
|
||||||
"currentTime": currentTime,
|
"currentTime": currentTime,
|
||||||
"duration": totalTime,
|
"duration": totalTime,
|
||||||
"bufferTime": p.playableTime
|
"bufferTime": p.playableTime,
|
||||||
|
"airPlayState": getAirPlayState()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,4 +96,44 @@ class KSPlayerViewManager: RCTViewManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AirPlay methods
|
||||||
|
@objc func setAllowsExternalPlayback(_ node: NSNumber, allows: Bool) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||||
|
view.setAllowsExternalPlayback(allows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setUsesExternalPlaybackWhileExternalScreenIsActive(_ node: NSNumber, uses: Bool) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||||
|
view.setUsesExternalPlaybackWhileExternalScreenIsActive(uses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getAirPlayState(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||||
|
let airPlayState = view.getAirPlayState()
|
||||||
|
resolve(airPlayState)
|
||||||
|
} else {
|
||||||
|
reject("NO_VIEW", "KSPlayerView not found", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func showAirPlayPicker(_ node: NSNumber) {
|
||||||
|
print("[KSPlayerViewManager] showAirPlayPicker called for node: \(node)")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||||
|
print("[KSPlayerViewManager] Found KSPlayerView, calling showAirPlayPicker")
|
||||||
|
view.showAirPlayPicker()
|
||||||
|
} else {
|
||||||
|
print("[KSPlayerViewManager] Could not find KSPlayerView for node: \(node)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -460,8 +460,8 @@
|
||||||
"-lc++",
|
"-lc++",
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nuviohub.app;
|
||||||
PRODUCT_NAME = "Nuvio";
|
PRODUCT_NAME = Nuvio;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
|
|
@ -493,7 +493,7 @@
|
||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app;
|
||||||
PRODUCT_NAME = "Nuvio";
|
PRODUCT_NAME = Nuvio;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
|
@ -97,5 +97,5 @@
|
||||||
<string>Dark</string>
|
<string>Dark</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
22
package-lock.json
generated
22
package-lock.json
generated
|
|
@ -91,6 +91,7 @@
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-native": "^0.72.8",
|
"@types/react-native": "^0.72.8",
|
||||||
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"patch-package": "^8.0.1",
|
"patch-package": "^8.0.1",
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
"react-native-svg-transformer": "^1.5.0",
|
||||||
|
|
@ -4208,6 +4209,27 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-native-vector-icons": {
|
||||||
|
"version": "6.4.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz",
|
||||||
|
"integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-native": "^0.70"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-native-vector-icons/node_modules/@types/react-native": {
|
||||||
|
"version": "0.70.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz",
|
||||||
|
"integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-native-video": {
|
"node_modules/@types/react-native-video": {
|
||||||
"version": "5.0.20",
|
"version": "5.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-native-video/-/react-native-video-5.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-native-video/-/react-native-video-5.0.20.tgz",
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-native": "^0.72.8",
|
"@types/react-native": "^0.72.8",
|
||||||
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"patch-package": "^8.0.1",
|
"patch-package": "^8.0.1",
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
"react-native-svg-transformer": "^1.5.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useMemo, useRef } from 'react';
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
|
||||||
import { LegendList } from '@legendapp/list';
|
import { FlashList } from '@shopify/flash-list';
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
@ -97,6 +97,12 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
||||||
// Memoize the keyExtractor to prevent re-creation
|
// Memoize the keyExtractor to prevent re-creation
|
||||||
const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []);
|
const keyExtractor = useCallback((item: StreamingContent) => `${item.id}-${item.type}`, []);
|
||||||
|
|
||||||
|
// FlashList v2 optimization: getItemType for better performance
|
||||||
|
const getItemType = useCallback((item: StreamingContent) => {
|
||||||
|
// Return different types based on content for better recycling
|
||||||
|
return item.type === 'movie' ? 'movie' : 'series';
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={styles.catalogContainer}
|
style={styles.catalogContainer}
|
||||||
|
|
@ -163,10 +169,11 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<LegendList
|
<FlashList
|
||||||
data={catalog.items}
|
data={catalog.items}
|
||||||
renderItem={renderContentItem}
|
renderItem={renderContentItem}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
|
getItemType={getItemType}
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
contentContainerStyle={StyleSheet.flatten([
|
contentContainerStyle={StyleSheet.flatten([
|
||||||
|
|
@ -179,8 +186,8 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
||||||
ItemSeparatorComponent={ItemSeparator}
|
ItemSeparatorComponent={ItemSeparator}
|
||||||
onEndReachedThreshold={0.7}
|
onEndReachedThreshold={0.7}
|
||||||
onEndReached={() => {}}
|
onEndReached={() => {}}
|
||||||
recycleItems={true}
|
// FlashList v2 optimizations
|
||||||
maintainVisibleContentPosition
|
drawDistance={500}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1588,6 +1588,11 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
controlsTimeout.current = setTimeout(hideControls, 5000);
|
controlsTimeout.current = setTimeout(hideControls, 5000);
|
||||||
|
|
||||||
|
// Auto-fetch and load English external subtitles if available
|
||||||
|
if (imdbId) {
|
||||||
|
fetchAvailableSubtitles(undefined, true);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[AndroidVideoPlayer] Error in onLoad:', error);
|
logger.error('[AndroidVideoPlayer] Error in onLoad:', error);
|
||||||
// Set fallback values to prevent crashes
|
// Set fallback values to prevent crashes
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ interface KSPlayerViewProps {
|
||||||
volume?: number;
|
volume?: number;
|
||||||
audioTrack?: number;
|
audioTrack?: number;
|
||||||
textTrack?: number;
|
textTrack?: number;
|
||||||
|
allowsExternalPlayback?: boolean;
|
||||||
|
usesExternalPlaybackWhileExternalScreenIsActive?: boolean;
|
||||||
onLoad?: (data: any) => void;
|
onLoad?: (data: any) => void;
|
||||||
onProgress?: (data: any) => void;
|
onProgress?: (data: any) => void;
|
||||||
onBuffering?: (data: any) => void;
|
onBuffering?: (data: any) => void;
|
||||||
|
|
@ -32,6 +34,10 @@ export interface KSPlayerRef {
|
||||||
setAudioTrack: (trackId: number) => void;
|
setAudioTrack: (trackId: number) => void;
|
||||||
setTextTrack: (trackId: number) => void;
|
setTextTrack: (trackId: number) => void;
|
||||||
getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>;
|
getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>;
|
||||||
|
setAllowsExternalPlayback: (allows: boolean) => void;
|
||||||
|
setUsesExternalPlaybackWhileExternalScreenIsActive: (uses: boolean) => void;
|
||||||
|
getAirPlayState: () => Promise<{ allowsExternalPlayback: boolean; usesExternalPlaybackWhileExternalScreenIsActive: boolean; isExternalPlaybackActive: boolean }>;
|
||||||
|
showAirPlayPicker: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KSPlayerProps {
|
export interface KSPlayerProps {
|
||||||
|
|
@ -40,6 +46,8 @@ export interface KSPlayerProps {
|
||||||
volume?: number;
|
volume?: number;
|
||||||
audioTrack?: number;
|
audioTrack?: number;
|
||||||
textTrack?: number;
|
textTrack?: number;
|
||||||
|
allowsExternalPlayback?: boolean;
|
||||||
|
usesExternalPlaybackWhileExternalScreenIsActive?: boolean;
|
||||||
onLoad?: (data: any) => void;
|
onLoad?: (data: any) => void;
|
||||||
onProgress?: (data: any) => void;
|
onProgress?: (data: any) => void;
|
||||||
onBuffering?: (data: any) => void;
|
onBuffering?: (data: any) => void;
|
||||||
|
|
@ -109,6 +117,38 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
}
|
}
|
||||||
return { audioTracks: [], textTracks: [] };
|
return { audioTracks: [], textTracks: [] };
|
||||||
},
|
},
|
||||||
|
setAllowsExternalPlayback: (allows: boolean) => {
|
||||||
|
if (nativeRef.current) {
|
||||||
|
const node = findNodeHandle(nativeRef.current);
|
||||||
|
// @ts-ignore legacy UIManager commands path for Paper
|
||||||
|
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setAllowsExternalPlayback;
|
||||||
|
UIManager.dispatchViewManagerCommand(node, commandId, [allows]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setUsesExternalPlaybackWhileExternalScreenIsActive: (uses: boolean) => {
|
||||||
|
if (nativeRef.current) {
|
||||||
|
const node = findNodeHandle(nativeRef.current);
|
||||||
|
// @ts-ignore legacy UIManager commands path for Paper
|
||||||
|
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setUsesExternalPlaybackWhileExternalScreenIsActive;
|
||||||
|
UIManager.dispatchViewManagerCommand(node, commandId, [uses]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getAirPlayState: async () => {
|
||||||
|
if (nativeRef.current) {
|
||||||
|
const node = findNodeHandle(nativeRef.current);
|
||||||
|
return await KSPlayerModule.getAirPlayState(node);
|
||||||
|
}
|
||||||
|
return { allowsExternalPlayback: false, usesExternalPlaybackWhileExternalScreenIsActive: false, isExternalPlaybackActive: false };
|
||||||
|
},
|
||||||
|
showAirPlayPicker: () => {
|
||||||
|
if (nativeRef.current) {
|
||||||
|
const node = findNodeHandle(nativeRef.current);
|
||||||
|
console.log('[KSPlayerComponent] Calling showAirPlayPicker with node:', node);
|
||||||
|
KSPlayerModule.showAirPlayPicker(node);
|
||||||
|
} else {
|
||||||
|
console.log('[KSPlayerComponent] nativeRef.current is null');
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// No need for event listeners - events are handled through props
|
// No need for event listeners - events are handled through props
|
||||||
|
|
@ -129,6 +169,8 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
volume={props.volume}
|
volume={props.volume}
|
||||||
audioTrack={props.audioTrack}
|
audioTrack={props.audioTrack}
|
||||||
textTrack={props.textTrack}
|
textTrack={props.textTrack}
|
||||||
|
allowsExternalPlayback={props.allowsExternalPlayback}
|
||||||
|
usesExternalPlaybackWhileExternalScreenIsActive={props.usesExternalPlaybackWhileExternalScreenIsActive}
|
||||||
onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)}
|
onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)}
|
||||||
onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)}
|
onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)}
|
||||||
onBuffering={(e: any) => props.onBuffering?.(e?.nativeEvent ?? e)}
|
onBuffering={(e: any) => props.onBuffering?.(e?.nativeEvent ?? e)}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
const [textTracks, setTextTracks] = useState<TextTrack[]>([]);
|
||||||
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
const [selectedTextTrack, setSelectedTextTrack] = useState<number>(-1);
|
||||||
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain');
|
const [resizeMode, setResizeMode] = useState<ResizeModeType>('contain');
|
||||||
|
const [playerBackend, setPlayerBackend] = useState<string>('');
|
||||||
const [buffered, setBuffered] = useState(0);
|
const [buffered, setBuffered] = useState(0);
|
||||||
const [seekPosition, setSeekPosition] = useState<number | null>(null);
|
const [seekPosition, setSeekPosition] = useState<number | null>(null);
|
||||||
const ksPlayerRef = useRef<KSPlayerRef>(null);
|
const ksPlayerRef = useRef<KSPlayerRef>(null);
|
||||||
|
|
@ -260,6 +261,10 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
||||||
|
|
||||||
|
// AirPlay state
|
||||||
|
const [isAirPlayActive, setIsAirPlayActive] = useState<boolean>(false);
|
||||||
|
const [allowsAirPlay, setAllowsAirPlay] = useState<boolean>(true);
|
||||||
|
|
||||||
// Silent startup-timeout retry state
|
// Silent startup-timeout retry state
|
||||||
const startupRetryCountRef = useRef(0);
|
const startupRetryCountRef = useRef(0);
|
||||||
const startupRetryTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const startupRetryTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -956,6 +961,18 @@ const KSPlayerCore: React.FC = () => {
|
||||||
safeSetState(() => setBuffered(bufferedTime));
|
safeSetState(() => setBuffered(bufferedTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update AirPlay state if available
|
||||||
|
if (event.airPlayState) {
|
||||||
|
const wasAirPlayActive = isAirPlayActive;
|
||||||
|
setIsAirPlayActive(event.airPlayState.isExternalPlaybackActive);
|
||||||
|
setAllowsAirPlay(event.airPlayState.allowsExternalPlayback);
|
||||||
|
|
||||||
|
// Log AirPlay state changes for debugging
|
||||||
|
if (wasAirPlayActive !== event.airPlayState.isExternalPlaybackActive) {
|
||||||
|
if (__DEV__) logger.log(`[VideoPlayer] AirPlay state changed: ${event.airPlayState.isExternalPlaybackActive ? 'ACTIVE' : 'INACTIVE'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Safety: if audio is advancing but onLoad didn't fire, dismiss opening overlay
|
// Safety: if audio is advancing but onLoad didn't fire, dismiss opening overlay
|
||||||
if (!isOpeningAnimationComplete) {
|
if (!isOpeningAnimationComplete) {
|
||||||
setIsVideoLoaded(true);
|
setIsVideoLoaded(true);
|
||||||
|
|
@ -1039,6 +1056,24 @@ const KSPlayerCore: React.FC = () => {
|
||||||
logger.error('[VideoPlayer] onLoad called with null/undefined data');
|
logger.error('[VideoPlayer] onLoad called with null/undefined data');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Extract player backend information
|
||||||
|
if (data.playerBackend) {
|
||||||
|
const newPlayerBackend = data.playerBackend;
|
||||||
|
setPlayerBackend(newPlayerBackend);
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VideoPlayer] Player backend: ${newPlayerBackend}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset AirPlay state if switching to KSMEPlayer (which doesn't support AirPlay)
|
||||||
|
if (newPlayerBackend === 'KSMEPlayer' && (isAirPlayActive || allowsAirPlay)) {
|
||||||
|
setIsAirPlayActive(false);
|
||||||
|
setAllowsAirPlay(false);
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log('[VideoPlayer] Reset AirPlay state for KSMEPlayer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// KSPlayer returns duration in seconds directly
|
// KSPlayer returns duration in seconds directly
|
||||||
const videoDuration = data.duration;
|
const videoDuration = data.duration;
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
|
|
@ -1249,6 +1284,11 @@ const KSPlayerCore: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
controlsTimeout.current = setTimeout(hideControls, 5000);
|
controlsTimeout.current = setTimeout(hideControls, 5000);
|
||||||
|
|
||||||
|
// Auto-fetch and load English external subtitles if available
|
||||||
|
if (imdbId) {
|
||||||
|
fetchAvailableSubtitles(undefined, true);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[VideoPlayer] Error in onLoad:', error);
|
logger.error('[VideoPlayer] Error in onLoad:', error);
|
||||||
// Set fallback values to prevent crashes
|
// Set fallback values to prevent crashes
|
||||||
|
|
@ -2333,6 +2373,27 @@ const KSPlayerCore: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
|
}, [pendingSeek, isPlayerReady, isVideoLoaded, duration]);
|
||||||
|
|
||||||
|
// AirPlay handler
|
||||||
|
const handleAirPlayPress = async () => {
|
||||||
|
if (!ksPlayerRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First ensure AirPlay is enabled
|
||||||
|
if (!allowsAirPlay) {
|
||||||
|
ksPlayerRef.current.setAllowsExternalPlayback(true);
|
||||||
|
setAllowsAirPlay(true);
|
||||||
|
logger.log(`[VideoPlayer] AirPlay enabled before showing picker`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the AirPlay picker
|
||||||
|
ksPlayerRef.current.showAirPlayPicker();
|
||||||
|
|
||||||
|
logger.log(`[VideoPlayer] AirPlay picker triggered - check console for native logs`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[VideoPlayer] Error showing AirPlay picker:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectStream = async (newStream: any) => {
|
const handleSelectStream = async (newStream: any) => {
|
||||||
if (newStream.url === currentStreamUrl) {
|
if (newStream.url === currentStreamUrl) {
|
||||||
setShowSourcesModal(false);
|
setShowSourcesModal(false);
|
||||||
|
|
@ -2665,6 +2726,8 @@ const KSPlayerCore: React.FC = () => {
|
||||||
volume={volume / 100}
|
volume={volume / 100}
|
||||||
audioTrack={selectedAudioTrack ?? undefined}
|
audioTrack={selectedAudioTrack ?? undefined}
|
||||||
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
||||||
|
allowsExternalPlayback={allowsAirPlay}
|
||||||
|
usesExternalPlaybackWhileExternalScreenIsActive={true}
|
||||||
onProgress={handleProgress}
|
onProgress={handleProgress}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
onEnd={onEnd}
|
onEnd={onEnd}
|
||||||
|
|
@ -2706,8 +2769,12 @@ const KSPlayerCore: React.FC = () => {
|
||||||
onSlidingComplete={handleSlidingComplete}
|
onSlidingComplete={handleSlidingComplete}
|
||||||
buffered={buffered}
|
buffered={buffered}
|
||||||
formatTime={formatTime}
|
formatTime={formatTime}
|
||||||
|
playerBackend={playerBackend}
|
||||||
cyclePlaybackSpeed={cyclePlaybackSpeed}
|
cyclePlaybackSpeed={cyclePlaybackSpeed}
|
||||||
currentPlaybackSpeed={playbackSpeed}
|
currentPlaybackSpeed={playbackSpeed}
|
||||||
|
isAirPlayActive={isAirPlayActive}
|
||||||
|
allowsAirPlay={allowsAirPlay}
|
||||||
|
onAirPlayPress={handleAirPlayPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showPauseOverlay && (
|
{showPauseOverlay && (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform, Dimensions } from 'react-native';
|
import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform, Dimensions } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import Feather from 'react-native-vector-icons/Feather';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
|
|
@ -43,6 +44,10 @@ interface PlayerControlsProps {
|
||||||
buffered: number;
|
buffered: number;
|
||||||
formatTime: (seconds: number) => string;
|
formatTime: (seconds: number) => string;
|
||||||
playerBackend?: string;
|
playerBackend?: string;
|
||||||
|
// AirPlay props
|
||||||
|
isAirPlayActive?: boolean;
|
||||||
|
allowsAirPlay?: boolean;
|
||||||
|
onAirPlayPress?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
|
|
@ -80,6 +85,9 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
buffered,
|
buffered,
|
||||||
formatTime,
|
formatTime,
|
||||||
playerBackend,
|
playerBackend,
|
||||||
|
isAirPlayActive,
|
||||||
|
allowsAirPlay,
|
||||||
|
onAirPlayPress,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
const deviceWidth = Dimensions.get('window').width;
|
const deviceWidth = Dimensions.get('window').width;
|
||||||
|
|
@ -249,6 +257,26 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* AirPlay Button - iOS only, KSAVPlayer only */}
|
||||||
|
{Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.bottomButton}
|
||||||
|
onPress={onAirPlayPress}
|
||||||
|
>
|
||||||
|
<Feather
|
||||||
|
name="airplay"
|
||||||
|
size={20}
|
||||||
|
color={isAirPlayActive ? currentTheme.colors.primary : "white"}
|
||||||
|
/>
|
||||||
|
<Text style={[
|
||||||
|
styles.bottomButtonText,
|
||||||
|
isAirPlayActive && { color: currentTheme.colors.primary }
|
||||||
|
]}>
|
||||||
|
{allowsAirPlay ? 'AirPlay' : 'AirPlay Off'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue