mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-13 21:20:20 +00:00
KSPlayre AUdio track selection fix
This commit is contained in:
parent
e9e16ed05a
commit
18815b8233
10 changed files with 534 additions and 303 deletions
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
|
@ -37,8 +37,9 @@
|
|||
677190A93C7E1E59AC68D165 /* KSPlayerManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = KSPlayerManager.m; path = Nuvio/KSPlayerManager.m; sourceTree = "<group>"; };
|
||||
6C2E3173556A471DD304B334 /* Pods-Nuvio.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.debug.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-Nuvio.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Nuvio.release.xcconfig"; path = "Target Support Files/Pods-Nuvio/Pods-Nuvio.release.xcconfig"; sourceTree = "<group>"; };
|
||||
88706B115BE5800B1B31F65D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
88706B115BE5800B1B31F65D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
94E57CA110F3B584C9EB54FF /* KSPlayerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KSPlayerView.swift; path = Nuvio/KSPlayerView.swift; sourceTree = "<group>"; };
|
||||
9F0599E52E7B2EF00090C551 /* NuvioDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioDebug.entitlements; path = Nuvio/NuvioDebug.entitlements; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
13B07FAE1A68108700A75B9A /* Nuvio */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9F0599E52E7B2EF00090C551 /* NuvioDebug.entitlements */,
|
||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
||||
|
|
@ -404,7 +406,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
|
||||
CODE_SIGN_ENTITLEMENTS = Nuvio/NuvioDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
|
@ -416,7 +418,10 @@
|
|||
);
|
||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -447,7 +452,10 @@
|
|||
DEVELOPMENT_TEAM = NLXTHANK2N;
|
||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
|
@ -512,14 +520,14 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
|
|
@ -568,13 +576,13 @@
|
|||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@
|
|||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
|
@ -82,7 +82,7 @@
|
|||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
|
|||
|
|
@ -1,99 +1,95 @@
|
|||
<?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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>stremioexpo</string>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+nuvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>12</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>App uses the local network to discover and connect to devices.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require microphone access.</string>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
<integer>4278322180</integer>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nuvio</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>stremioexpo</string>
|
||||
<string>com.nuvio.app</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>exp+nuvio</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>12</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_http._tcp</string>
|
||||
</array>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>App uses the local network to discover and connect to devices.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app does not require microphone access.</string>
|
||||
<key>RCTRootViewBackgroundColor</key>
|
||||
<integer>4278322180</integer>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<false/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDefault</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Dark</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -31,11 +31,12 @@ RCT_EXTERN_METHOD(setPaused:(nonnull NSNumber *)node paused:(BOOL)paused)
|
|||
RCT_EXTERN_METHOD(setVolume:(nonnull NSNumber *)node volume:(nonnull NSNumber *)volume)
|
||||
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(getTracks:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
|
||||
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
||||
|
||||
RCT_EXTERN_METHOD(getTracks:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||
RCT_EXTERN_METHOD(getTracks:(nonnull NSNumber *)nodeTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -25,12 +25,13 @@ class KSPlayerModule: RCTEventEmitter {
|
|||
]
|
||||
}
|
||||
|
||||
@objc func getTracks(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
// This method can be expanded to get track information
|
||||
// For now, return empty tracks
|
||||
resolve([
|
||||
"audioTracks": [],
|
||||
"textTracks": []
|
||||
])
|
||||
@objc func getTracks(_ nodeTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.main.async {
|
||||
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||
viewManager.getTracks(nodeTag, resolve: resolve, reject: reject)
|
||||
} else {
|
||||
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class KSPlayerView: UIView {
|
|||
private var isPaused = false
|
||||
private var currentVolume: Float = 1.0
|
||||
weak var viewManager: KSPlayerViewManager?
|
||||
private var loadTimeoutWorkItem: DispatchWorkItem?
|
||||
|
||||
// Event blocks for Fabric
|
||||
@objc var onLoad: RCTDirectEventBlock?
|
||||
|
|
@ -71,6 +72,14 @@ class KSPlayerView: UIView {
|
|||
private func setupPlayerView() {
|
||||
playerView = IOSVideoPlayerView()
|
||||
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// Hide native controls - we use custom React Native controls
|
||||
playerView.isUserInteractionEnabled = false
|
||||
// Hide KSPlayer's built-in overlay/controls
|
||||
playerView.controllerView.isHidden = true
|
||||
playerView.contentOverlayView.isHidden = true
|
||||
playerView.controllerView.alpha = 0
|
||||
playerView.contentOverlayView.alpha = 0
|
||||
playerView.controllerView.gestureRecognizers?.forEach { $0.isEnabled = false }
|
||||
addSubview(playerView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
|
|
@ -85,11 +94,14 @@ class KSPlayerView: UIView {
|
|||
}
|
||||
|
||||
private func setupPlayerCallbacks() {
|
||||
// Set up the player layer delegate
|
||||
playerView.playerLayer?.delegate = self
|
||||
|
||||
// Configure KSOptions (use static defaults where required)
|
||||
KSOptions.isAutoPlay = false
|
||||
#if targetEnvironment(simulator)
|
||||
// Simulator: disable hardware decode and MEPlayer to avoid VT/Vulkan issues
|
||||
KSOptions.hardwareDecode = false
|
||||
KSOptions.asynchronousDecompression = false
|
||||
KSOptions.secondPlayerType = nil
|
||||
#endif
|
||||
}
|
||||
|
||||
func setSource(_ source: NSDictionary) {
|
||||
|
|
@ -105,12 +117,33 @@ class KSPlayerView: UIView {
|
|||
headers = headersDict
|
||||
}
|
||||
|
||||
// Choose player pipeline based on format
|
||||
let isMKV = uri.lowercased().contains(".mkv")
|
||||
#if targetEnvironment(simulator)
|
||||
if isMKV {
|
||||
// MKV not supported on AVPlayer in Simulator and MEPlayer is disabled
|
||||
sendEvent("onError", ["error": "MKV playback is not supported in the iOS Simulator. Test on a real device."])
|
||||
}
|
||||
#else
|
||||
if isMKV {
|
||||
// Prefer MEPlayer (FFmpeg) for MKV on device
|
||||
KSOptions.firstPlayerType = KSMEPlayer.self
|
||||
KSOptions.secondPlayerType = nil
|
||||
} else {
|
||||
KSOptions.firstPlayerType = KSAVPlayer.self
|
||||
KSOptions.secondPlayerType = KSMEPlayer.self
|
||||
}
|
||||
#endif
|
||||
|
||||
// Create KSPlayerResource
|
||||
let url = URL(string: uri)!
|
||||
let resource = KSPlayerResource(url: url, options: createOptions(with: headers), name: "Video")
|
||||
|
||||
print("KSPlayerView: Setting source: \(uri)")
|
||||
playerView.set(resource: resource)
|
||||
|
||||
// Set up delegate after setting the resource
|
||||
playerView.playerLayer?.delegate = self
|
||||
|
||||
// Apply current state
|
||||
if isPaused {
|
||||
|
|
@ -120,11 +153,44 @@ class KSPlayerView: UIView {
|
|||
}
|
||||
|
||||
setVolume(currentVolume)
|
||||
|
||||
// Start a safety timeout to surface errors if never ready
|
||||
loadTimeoutWorkItem?.cancel()
|
||||
let work = DispatchWorkItem { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let dur = self.playerView.playerLayer?.player.duration ?? 0
|
||||
if dur <= 0 {
|
||||
self.sendEvent("onError", ["error": "Playback timeout: stream did not become ready."])
|
||||
}
|
||||
}
|
||||
loadTimeoutWorkItem = work
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 8, execute: work)
|
||||
}
|
||||
|
||||
private func createOptions(with headers: [String: String]) -> KSOptions {
|
||||
let options = KSOptions()
|
||||
// Disable native player remote control center integration; use RN controls
|
||||
options.registerRemoteControll = false
|
||||
|
||||
// Configure audio for proper dialogue mixing using FFmpeg's pan filter
|
||||
// This approach uses standard audio engineering practices for multi-channel downmixing
|
||||
|
||||
// Use conservative center channel mixing that preserves spatial audio
|
||||
// c0 (Left) = 70% original left + 30% center (dialogue) + 20% rear left
|
||||
// c1 (Right) = 70% original right + 30% center (dialogue) + 20% rear right
|
||||
// This creates natural dialogue presence without the "playing on both ears" effect
|
||||
options.audioFilters.append("pan=stereo|c0=0.7*c0+0.3*c2+0.2*c4|c1=0.7*c1+0.3*c2+0.2*c5")
|
||||
|
||||
// Alternative: Use FFmpeg's surround filter for more sophisticated downmixing
|
||||
// This provides better spatial audio processing and natural dialogue mixing
|
||||
// options.audioFilters.append("surround=ang=45")
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
options.hardwareDecode = false
|
||||
options.asynchronousDecompression = false
|
||||
#else
|
||||
options.hardwareDecode = KSOptions.hardwareDecode
|
||||
#endif
|
||||
if !headers.isEmpty {
|
||||
options.appendHeader(headers)
|
||||
if let referer = headers["Referer"] ?? headers["referer"] {
|
||||
|
|
@ -149,34 +215,174 @@ class KSPlayerView: UIView {
|
|||
}
|
||||
|
||||
func seek(to time: TimeInterval) {
|
||||
playerView.seek(time: time) { _ in }
|
||||
guard let playerLayer = playerView.playerLayer,
|
||||
playerLayer.player.isReadyToPlay,
|
||||
playerLayer.player.seekable else {
|
||||
print("KSPlayerView: Cannot seek - player not ready or not seekable")
|
||||
return
|
||||
}
|
||||
|
||||
playerView.seek(time: time) { success in
|
||||
if success {
|
||||
print("KSPlayerView: Seek successful to \(time)")
|
||||
} else {
|
||||
print("KSPlayerView: Seek failed to \(time)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setAudioTrack(_ trackId: Int) {
|
||||
if let player = playerView.playerLayer?.player {
|
||||
let audioTracks = player.tracks(mediaType: .audio)
|
||||
if trackId >= 0 && trackId < audioTracks.count {
|
||||
// Enable only the selected track
|
||||
for (index, track) in audioTracks.enumerated() {
|
||||
track.isEnabled = (index == trackId)
|
||||
}
|
||||
print("KSPlayerView: Available audio tracks count: \(audioTracks.count)")
|
||||
print("KSPlayerView: Requested track ID: \(trackId)")
|
||||
|
||||
// Debug: Print all track information
|
||||
for (index, track) in audioTracks.enumerated() {
|
||||
print("KSPlayerView: Track \(index) - ID: \(track.trackID), Name: '\(track.name)', Language: '\(track.language ?? "nil")', isEnabled: \(track.isEnabled)")
|
||||
}
|
||||
|
||||
// First try to find track by trackID (proper way)
|
||||
var selectedTrack: MediaPlayerTrack? = nil
|
||||
var trackIndex: Int = -1
|
||||
|
||||
// Try to find by exact trackID match
|
||||
if let track = audioTracks.first(where: { Int($0.trackID) == trackId }) {
|
||||
selectedTrack = track
|
||||
trackIndex = audioTracks.firstIndex(where: { $0.trackID == track.trackID }) ?? -1
|
||||
print("KSPlayerView: Found track by trackID \(trackId) at index \(trackIndex)")
|
||||
}
|
||||
// Fallback: treat trackId as array index
|
||||
else if trackId >= 0 && trackId < audioTracks.count {
|
||||
selectedTrack = audioTracks[trackId]
|
||||
trackIndex = trackId
|
||||
print("KSPlayerView: Found track by array index \(trackId) (fallback)")
|
||||
}
|
||||
|
||||
if let track = selectedTrack {
|
||||
print("KSPlayerView: Selecting track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))")
|
||||
|
||||
// Use KSPlayer's select method which properly handles track selection
|
||||
player.select(track: track)
|
||||
|
||||
print("KSPlayerView: Successfully selected audio track \(trackId)")
|
||||
|
||||
// Verify the selection worked
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
let tracksAfter = player.tracks(mediaType: .audio)
|
||||
for (index, track) in tracksAfter.enumerated() {
|
||||
print("KSPlayerView: After selection - Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)")
|
||||
}
|
||||
}
|
||||
|
||||
// Configure audio downmixing for multi-channel tracks
|
||||
configureAudioDownmixing(for: track)
|
||||
} else if trackId == -1 {
|
||||
// Disable all audio tracks (mute)
|
||||
for track in audioTracks { track.isEnabled = false }
|
||||
print("KSPlayerView: Disabled all audio tracks")
|
||||
} else {
|
||||
print("KSPlayerView: Track \(trackId) not found. Available track IDs: \(audioTracks.map { Int($0.trackID) }), array indices: 0..\(audioTracks.count - 1)")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: No player available for audio track selection")
|
||||
}
|
||||
}
|
||||
|
||||
private func configureAudioDownmixing(for track: MediaPlayerTrack) {
|
||||
// Check if this is a multi-channel audio track that needs downmixing
|
||||
// This is a simplified check - in practice, you might want to check the actual channel layout
|
||||
let trackName = track.name.lowercased()
|
||||
let isMultiChannel = trackName.contains("5.1") || trackName.contains("7.1") ||
|
||||
trackName.contains("truehd") || trackName.contains("dts") ||
|
||||
trackName.contains("dolby") || trackName.contains("atmos")
|
||||
|
||||
if isMultiChannel {
|
||||
print("KSPlayerView: Detected multi-channel audio track '\(track.name)', ensuring proper dialogue mixing")
|
||||
print("KSPlayerView: Using FFmpeg pan filter for natural stereo downmixing")
|
||||
} else {
|
||||
print("KSPlayerView: Stereo or mono audio track '\(track.name)', no additional downmixing needed")
|
||||
}
|
||||
}
|
||||
|
||||
func setTextTrack(_ trackId: Int) {
|
||||
if let player = playerView.playerLayer?.player {
|
||||
let textTracks = player.tracks(mediaType: .subtitle)
|
||||
if trackId >= 0 && trackId < textTracks.count {
|
||||
for (index, track) in textTracks.enumerated() {
|
||||
track.isEnabled = (index == trackId)
|
||||
}
|
||||
print("KSPlayerView: Available text tracks count: \(textTracks.count)")
|
||||
print("KSPlayerView: Requested text track ID: \(trackId)")
|
||||
|
||||
// First try to find track by trackID (proper way)
|
||||
var selectedTrack: MediaPlayerTrack? = nil
|
||||
var trackIndex: Int = -1
|
||||
|
||||
// Try to find by exact trackID match
|
||||
if let track = textTracks.first(where: { Int($0.trackID) == trackId }) {
|
||||
selectedTrack = track
|
||||
trackIndex = textTracks.firstIndex(where: { $0.trackID == track.trackID }) ?? -1
|
||||
print("KSPlayerView: Found text track by trackID \(trackId) at index \(trackIndex)")
|
||||
}
|
||||
// Fallback: treat trackId as array index
|
||||
else if trackId >= 0 && trackId < textTracks.count {
|
||||
selectedTrack = textTracks[trackId]
|
||||
trackIndex = trackId
|
||||
print("KSPlayerView: Found text track by array index \(trackId) (fallback)")
|
||||
}
|
||||
|
||||
if let track = selectedTrack {
|
||||
print("KSPlayerView: Selecting text track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))")
|
||||
|
||||
// Use KSPlayer's select method which properly handles track selection
|
||||
player.select(track: track)
|
||||
|
||||
print("KSPlayerView: Successfully selected text track \(trackId)")
|
||||
} else if trackId == -1 {
|
||||
// Disable all subtitles
|
||||
for track in textTracks { track.isEnabled = false }
|
||||
print("KSPlayerView: Disabled all text tracks")
|
||||
} else {
|
||||
print("KSPlayerView: Text track \(trackId) not found. Available track IDs: \(textTracks.map { Int($0.trackID) }), array indices: 0..\(textTracks.count - 1)")
|
||||
}
|
||||
} else {
|
||||
print("KSPlayerView: No player available for text track selection")
|
||||
}
|
||||
}
|
||||
|
||||
// Get available tracks for React Native
|
||||
func getAvailableTracks() -> [String: Any] {
|
||||
guard let player = playerView.playerLayer?.player else {
|
||||
return ["audioTracks": [], "textTracks": []]
|
||||
}
|
||||
|
||||
let audioTracks = player.tracks(mediaType: .audio).enumerated().map { index, track in
|
||||
return [
|
||||
"id": Int(track.trackID), // Use actual track ID, not array index
|
||||
"index": index, // Keep index for backward compatibility
|
||||
"name": track.name,
|
||||
"language": track.language ?? "Unknown",
|
||||
"languageCode": track.languageCode ?? "",
|
||||
"isEnabled": track.isEnabled,
|
||||
"bitRate": track.bitRate,
|
||||
"bitDepth": track.bitDepth
|
||||
]
|
||||
}
|
||||
|
||||
let textTracks = player.tracks(mediaType: .subtitle).enumerated().map { index, track in
|
||||
return [
|
||||
"id": Int(track.trackID), // Use actual track ID, not array index
|
||||
"index": index, // Keep index for backward compatibility
|
||||
"name": track.name,
|
||||
"language": track.language ?? "Unknown",
|
||||
"languageCode": track.languageCode ?? "",
|
||||
"isEnabled": track.isEnabled,
|
||||
"isImageSubtitle": track.isImageSubtitle
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
"audioTracks": audioTracks,
|
||||
"textTracks": textTracks
|
||||
]
|
||||
}
|
||||
|
||||
// Get current player state for React Native
|
||||
func getCurrentState() -> [String: Any] {
|
||||
|
|
@ -198,15 +404,20 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
|||
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||
switch state {
|
||||
case .readyToPlay:
|
||||
// Send onLoad event to React Native
|
||||
// Cancel timeout when ready
|
||||
loadTimeoutWorkItem?.cancel()
|
||||
// Send onLoad event to React Native with track information
|
||||
let p = layer.player
|
||||
let tracks = getAvailableTracks()
|
||||
sendEvent("onLoad", [
|
||||
"duration": p.duration,
|
||||
"currentTime": p.currentPlaybackTime,
|
||||
"naturalSize": [
|
||||
"width": p.naturalSize.width,
|
||||
"height": p.naturalSize.height
|
||||
]
|
||||
],
|
||||
"audioTracks": tracks["audioTracks"] ?? [],
|
||||
"textTracks": tracks["textTracks"] ?? []
|
||||
])
|
||||
case .buffering:
|
||||
sendEvent("onBuffering", ["isBuffering": true])
|
||||
|
|
@ -224,11 +435,14 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
|||
|
||||
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
||||
let p = layer.player
|
||||
sendEvent("onProgress", [
|
||||
"currentTime": currentTime,
|
||||
"duration": totalTime,
|
||||
"bufferTime": p.playableTime
|
||||
])
|
||||
// Ensure we have valid duration before sending progress updates
|
||||
if totalTime > 0 {
|
||||
sendEvent("onProgress", [
|
||||
"currentTime": currentTime,
|
||||
"duration": totalTime,
|
||||
"bufferTime": p.playableTime
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
func player(layer: KSPlayerLayer, finish error: Error?) {
|
||||
|
|
|
|||
|
|
@ -85,4 +85,15 @@ class KSPlayerViewManager: RCTViewManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func getTracks(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.main.async {
|
||||
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||
let tracks = view.getAvailableTracks()
|
||||
resolve(tracks)
|
||||
} else {
|
||||
reject("NO_VIEW", "KSPlayerView not found", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<?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">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array/>
|
||||
</dict>
|
||||
<dict/>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface KSPlayerRef {
|
|||
setVolume: (volume: number) => void;
|
||||
setAudioTrack: (trackId: number) => void;
|
||||
setTextTrack: (trackId: number) => void;
|
||||
getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>;
|
||||
}
|
||||
|
||||
export interface KSPlayerProps {
|
||||
|
|
@ -101,6 +102,13 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
|||
UIManager.dispatchViewManagerCommand(node, commandId, [trackId]);
|
||||
}
|
||||
},
|
||||
getTracks: async () => {
|
||||
if (nativeRef.current) {
|
||||
const node = findNodeHandle(nativeRef.current);
|
||||
return await KSPlayerModule.getTracks(node);
|
||||
}
|
||||
return { audioTracks: [], textTracks: [] };
|
||||
},
|
||||
}));
|
||||
|
||||
// No need for event listeners - events are handled through props
|
||||
|
|
@ -121,12 +129,12 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
|||
volume={props.volume}
|
||||
audioTrack={props.audioTrack}
|
||||
textTrack={props.textTrack}
|
||||
onLoad={props.onLoad}
|
||||
onProgress={props.onProgress}
|
||||
onBuffering={props.onBuffering}
|
||||
onEnd={props.onEnd}
|
||||
onError={props.onError}
|
||||
onBufferingProgress={props.onBufferingProgress}
|
||||
onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)}
|
||||
onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)}
|
||||
onBuffering={(e: any) => props.onBuffering?.(e?.nativeEvent ?? e)}
|
||||
onEnd={() => props.onEnd?.()}
|
||||
onError={(e: any) => props.onError?.(e?.nativeEvent ?? e)}
|
||||
onBufferingProgress={(e: any) => props.onBufferingProgress?.(e?.nativeEvent ?? e)}
|
||||
style={props.style}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ const VideoPlayer: React.FC = () => {
|
|||
const isSourceSeekableRef = useRef<boolean | null>(null);
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false);
|
||||
const [shouldHideOpeningOverlay, setShouldHideOpeningOverlay] = useState(false);
|
||||
const DISABLE_OPENING_OVERLAY = false; // Enable opening overlay animation
|
||||
const openingFadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const openingScaleAnim = useRef(new Animated.Value(0.8)).current;
|
||||
const backgroundFadeAnim = useRef(new Animated.Value(1)).current;
|
||||
|
|
@ -512,7 +514,7 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
}, [effectiveDimensions, videoAspectRatio]);
|
||||
|
||||
// Force landscape orientation immediately when component mounts
|
||||
// Force landscape orientation after opening animation completes
|
||||
useEffect(() => {
|
||||
const lockOrientation = async () => {
|
||||
try {
|
||||
|
|
@ -523,32 +525,39 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Lock orientation immediately
|
||||
lockOrientation();
|
||||
// Lock orientation after opening animation completes to prevent glitches
|
||||
if (isOpeningAnimationComplete) {
|
||||
lockOrientation();
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Do not unlock orientation here; we unlock explicitly on close to avoid mid-transition flips
|
||||
};
|
||||
}, []);
|
||||
}, [isOpeningAnimationComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
||||
setScreenDimensions(screen);
|
||||
// Re-apply immersive mode on layout changes (Android)
|
||||
enableImmersiveMode();
|
||||
// Re-apply immersive mode on layout changes (Android) - only after opening animation
|
||||
if (isOpeningAnimationComplete) {
|
||||
enableImmersiveMode();
|
||||
}
|
||||
});
|
||||
const initializePlayer = async () => {
|
||||
StatusBar.setHidden(true, 'none');
|
||||
enableImmersiveMode();
|
||||
// Enable immersive mode after opening animation to prevent glitches
|
||||
if (isOpeningAnimationComplete) {
|
||||
enableImmersiveMode();
|
||||
}
|
||||
startOpeningAnimation();
|
||||
|
||||
|
||||
// Initialize current volume and brightness levels
|
||||
// Volume starts at 100 (full volume) for VLC
|
||||
setVolume(100);
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Initial volume: 100 (VLC native)`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const currentBrightness = await Brightness.getBrightnessAsync();
|
||||
setBrightness(currentBrightness);
|
||||
|
|
@ -566,20 +575,22 @@ const VideoPlayer: React.FC = () => {
|
|||
subscription?.remove();
|
||||
disableImmersiveMode();
|
||||
};
|
||||
}, []);
|
||||
}, [isOpeningAnimationComplete]);
|
||||
|
||||
// Re-apply immersive mode when screen gains focus (Android)
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
enableImmersiveMode();
|
||||
if (isOpeningAnimationComplete) {
|
||||
enableImmersiveMode();
|
||||
}
|
||||
return () => {};
|
||||
}, [])
|
||||
}, [isOpeningAnimationComplete])
|
||||
);
|
||||
|
||||
// Re-apply immersive mode when app returns to foreground (Android)
|
||||
useEffect(() => {
|
||||
const onAppStateChange = (state: string) => {
|
||||
if (state === 'active') {
|
||||
if (state === 'active' && isOpeningAnimationComplete) {
|
||||
enableImmersiveMode();
|
||||
}
|
||||
};
|
||||
|
|
@ -587,7 +598,7 @@ const VideoPlayer: React.FC = () => {
|
|||
return () => {
|
||||
sub.remove();
|
||||
};
|
||||
}, []);
|
||||
}, [isOpeningAnimationComplete]);
|
||||
|
||||
const startOpeningAnimation = () => {
|
||||
// Logo entrance animation - optimized for faster appearance
|
||||
|
|
@ -652,12 +663,13 @@ const VideoPlayer: React.FC = () => {
|
|||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
openingScaleAnim.setValue(1);
|
||||
openingFadeAnim.setValue(1);
|
||||
setIsOpeningAnimationComplete(true);
|
||||
// Delay hiding the overlay to allow background fade animation to complete
|
||||
setTimeout(() => {
|
||||
backgroundFadeAnim.setValue(0);
|
||||
}, 100);
|
||||
setShouldHideOpeningOverlay(true);
|
||||
}, 450); // Slightly longer than the background fade duration
|
||||
// Enable immersive mode and lock orientation now that animation is complete
|
||||
enableImmersiveMode();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -778,31 +790,36 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const seekToTime = (rawSeconds: number) => {
|
||||
// Clamp to just before the end to avoid triggering onEnd.
|
||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
||||
if (ksPlayerRef.current && duration > 0 && !isSeeking.current) {
|
||||
// For KSPlayer, we need to wait for the player to be ready
|
||||
if (!ksPlayerRef.current || isSeeking.current) {
|
||||
if (DEBUG_MODE) {
|
||||
if (__DEV__) logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||
}
|
||||
|
||||
isSeeking.current = true;
|
||||
|
||||
// KSPlayer uses direct time seeking
|
||||
ksPlayerRef.current.seek(timeInSeconds);
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMounted.current) {
|
||||
isSeeking.current = false;
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] KSPlayer seek completed to ${timeInSeconds.toFixed(2)}s`);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
if (DEBUG_MODE) {
|
||||
logger.error(`[VideoPlayer] Seek failed: ksPlayerRef=${!!ksPlayerRef.current}, duration=${duration}, seeking=${isSeeking.current}`);
|
||||
logger.error(`[VideoPlayer] Seek failed: ksPlayerRef=${!!ksPlayerRef.current}, seeking=${isSeeking.current}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp to just before the end to avoid triggering onEnd when duration is known.
|
||||
const timeInSeconds = duration > 0
|
||||
? Math.max(0, Math.min(rawSeconds, duration - END_EPSILON))
|
||||
: Math.max(0, rawSeconds);
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
if (__DEV__) logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
||||
}
|
||||
|
||||
isSeeking.current = true;
|
||||
|
||||
// KSPlayer uses direct time seeking
|
||||
ksPlayerRef.current.seek(timeInSeconds);
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMounted.current) {
|
||||
isSeeking.current = false;
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] KSPlayer seek completed to ${timeInSeconds.toFixed(2)}s`);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// Slider callback functions for React Native Community Slider
|
||||
|
|
@ -856,6 +873,12 @@ const VideoPlayer: React.FC = () => {
|
|||
|
||||
// KSPlayer returns times in seconds directly
|
||||
const currentTimeInSeconds = event.currentTime;
|
||||
const durationInSeconds = event.duration;
|
||||
|
||||
// Update duration if it's available and different
|
||||
if (durationInSeconds > 0 && durationInSeconds !== duration) {
|
||||
setDuration(durationInSeconds);
|
||||
}
|
||||
|
||||
// Only update if there's a significant change to avoid unnecessary updates
|
||||
if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) {
|
||||
|
|
@ -864,6 +887,13 @@ const VideoPlayer: React.FC = () => {
|
|||
const bufferedTime = event.bufferTime || currentTimeInSeconds;
|
||||
safeSetState(() => setBuffered(bufferedTime));
|
||||
}
|
||||
|
||||
// Safety: if audio is advancing but onLoad didn't fire, dismiss opening overlay
|
||||
if (!isOpeningAnimationComplete) {
|
||||
setIsVideoLoaded(true);
|
||||
setIsPlayerReady(true);
|
||||
completeOpeningAnimation();
|
||||
}
|
||||
|
||||
// Periodic check for disabled audio track (every 3 seconds, max 3 attempts)
|
||||
const now = Date.now();
|
||||
|
|
@ -929,6 +959,9 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
// KSPlayer returns duration in seconds directly
|
||||
const videoDuration = data.duration;
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Setting duration to: ${videoDuration}`);
|
||||
}
|
||||
if (videoDuration > 0) {
|
||||
setDuration(videoDuration);
|
||||
|
||||
|
|
@ -959,16 +992,13 @@ const VideoPlayer: React.FC = () => {
|
|||
logger.log(`[VideoPlayer] Raw audio tracks data:`, data.audioTracks);
|
||||
data.audioTracks.forEach((track: any, idx: number) => {
|
||||
logger.log(`[VideoPlayer] Track ${idx} raw data:`, {
|
||||
index: track.index,
|
||||
title: track.title,
|
||||
language: track.language,
|
||||
type: track.type,
|
||||
channels: track.channels,
|
||||
bitrate: track.bitrate,
|
||||
codec: track.codec,
|
||||
sampleRate: track.sampleRate,
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
languageCode: track.languageCode,
|
||||
isEnabled: track.isEnabled,
|
||||
bitRate: track.bitRate,
|
||||
bitDepth: track.bitDepth,
|
||||
allKeys: Object.keys(track),
|
||||
fullTrackObject: track
|
||||
});
|
||||
|
|
@ -976,63 +1006,33 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
|
||||
const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
|
||||
const trackIndex = track.index !== undefined ? track.index : index;
|
||||
const trackIndex = track.id !== undefined ? track.id : index;
|
||||
|
||||
// Build comprehensive track name from available fields
|
||||
let trackName = '';
|
||||
const parts = [];
|
||||
|
||||
// Add language if available (try multiple possible fields)
|
||||
let language = track.language || track.lang || track.languageCode;
|
||||
|
||||
// If no language field, try to extract from track name (e.g., "[Russian]", "[English]")
|
||||
if ((!language || language === 'Unknown' || language === 'und' || language === '') && track.name) {
|
||||
const languageMatch = track.name.match(/\[([^\]]+)\]/);
|
||||
if (languageMatch && languageMatch[1]) {
|
||||
language = languageMatch[1].trim();
|
||||
}
|
||||
}
|
||||
// Add language if available
|
||||
let language = track.language || track.languageCode;
|
||||
|
||||
if (language && language !== 'Unknown' && language !== 'und' && language !== '') {
|
||||
parts.push(language.toUpperCase());
|
||||
}
|
||||
|
||||
// Add codec information if available (try multiple possible fields)
|
||||
const codec = track.type || track.codec || track.format;
|
||||
if (codec && codec !== 'Unknown') {
|
||||
parts.push(codec.toUpperCase());
|
||||
}
|
||||
|
||||
// Add channel information if available
|
||||
const channels = track.channels || track.channelCount;
|
||||
if (channels && channels > 0) {
|
||||
if (channels === 1) {
|
||||
parts.push('MONO');
|
||||
} else if (channels === 2) {
|
||||
parts.push('STEREO');
|
||||
} else if (channels === 6) {
|
||||
parts.push('5.1CH');
|
||||
} else if (channels === 8) {
|
||||
parts.push('7.1CH');
|
||||
} else {
|
||||
parts.push(`${channels}CH`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add bitrate if available
|
||||
const bitrate = track.bitrate || track.bitRate;
|
||||
const bitrate = track.bitRate;
|
||||
if (bitrate && bitrate > 0) {
|
||||
parts.push(`${Math.round(bitrate / 1000)}kbps`);
|
||||
}
|
||||
|
||||
// Add sample rate if available
|
||||
const sampleRate = track.sampleRate || track.sample_rate;
|
||||
if (sampleRate && sampleRate > 0) {
|
||||
parts.push(`${Math.round(sampleRate / 1000)}kHz`);
|
||||
// Add bit depth if available
|
||||
const bitDepth = track.bitDepth;
|
||||
if (bitDepth && bitDepth > 0) {
|
||||
parts.push(`${bitDepth}bit`);
|
||||
}
|
||||
|
||||
// Add title if available and not generic
|
||||
let title = track.title || track.name || track.label;
|
||||
// Add track name if available and not generic
|
||||
let title = track.name;
|
||||
if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') {
|
||||
// Clean up title by removing language brackets and trailing punctuation
|
||||
title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim();
|
||||
|
|
@ -1046,44 +1046,29 @@ const VideoPlayer: React.FC = () => {
|
|||
trackName = parts.join(' • ');
|
||||
} else {
|
||||
// For simple track names like "Track 1", "Audio 1", etc., use them as-is
|
||||
const simpleName = track.name || track.title || track.label;
|
||||
const simpleName = track.name;
|
||||
if (simpleName && simpleName.match(/^(Track|Audio)\s*\d*$/i)) {
|
||||
trackName = simpleName;
|
||||
} else {
|
||||
// Try to extract any meaningful info from the track object
|
||||
const meaningfulFields: string[] = [];
|
||||
Object.keys(track).forEach(key => {
|
||||
const value = track[key];
|
||||
if (value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1) {
|
||||
meaningfulFields.push(`${key}: ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (meaningfulFields.length > 0) {
|
||||
trackName = `Audio ${index + 1} (${meaningfulFields.slice(0, 2).join(', ')})`;
|
||||
} else {
|
||||
trackName = `Audio ${index + 1}`;
|
||||
}
|
||||
trackName = `Audio ${index + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
const trackLanguage = language || 'Unknown';
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Processed track ${index}:`, {
|
||||
index: trackIndex,
|
||||
logger.log(`[VideoPlayer] Processed KSPlayer track ${index}:`, {
|
||||
id: trackIndex,
|
||||
name: trackName,
|
||||
language: trackLanguage,
|
||||
parts: parts,
|
||||
meaningfulFields: Object.keys(track).filter(key => {
|
||||
const value = track[key];
|
||||
return value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1;
|
||||
})
|
||||
bitRate: bitrate,
|
||||
bitDepth: bitDepth
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: trackIndex, // Use the actual track index from VLC
|
||||
id: trackIndex, // Use the actual track ID from KSPlayer
|
||||
name: trackName,
|
||||
language: trackLanguage,
|
||||
};
|
||||
|
|
@ -1116,21 +1101,25 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
}
|
||||
if (data.textTracks && data.textTracks.length > 0) {
|
||||
setVlcTextTracks(data.textTracks);
|
||||
// Process KSPlayer text tracks
|
||||
const formattedTextTracks = data.textTracks.map((track: any, index: number) => ({
|
||||
id: track.id !== undefined ? track.id : index,
|
||||
name: track.name || `Subtitle ${index + 1}`,
|
||||
language: track.language || track.languageCode || 'Unknown',
|
||||
isEnabled: track.isEnabled || false,
|
||||
isImageSubtitle: track.isImageSubtitle || false
|
||||
}));
|
||||
|
||||
setVlcTextTracks(formattedTextTracks);
|
||||
|
||||
// Auto-select English subtitle track if available
|
||||
if (selectedTextTrack === -1 && !useCustomSubtitles && data.textTracks.length > 0) {
|
||||
if (selectedTextTrack === -1 && !useCustomSubtitles && formattedTextTracks.length > 0) {
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Available subtitle tracks:`, data.textTracks.map((track: any) => ({
|
||||
id: track.id,
|
||||
index: track.index,
|
||||
name: track.name,
|
||||
language: track.language
|
||||
})));
|
||||
logger.log(`[VideoPlayer] Available KSPlayer subtitle tracks:`, formattedTextTracks);
|
||||
}
|
||||
|
||||
// Look for English track first
|
||||
const englishTrack = data.textTracks.find((track: any) => {
|
||||
const englishTrack = formattedTextTracks.find((track: any) => {
|
||||
const lang = (track.language || '').toLowerCase();
|
||||
const name = (track.name || '').toLowerCase();
|
||||
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
||||
|
|
@ -1138,12 +1127,9 @@ const VideoPlayer: React.FC = () => {
|
|||
});
|
||||
|
||||
if (englishTrack) {
|
||||
// Try different ID fields that VLC might use
|
||||
const trackId = englishTrack.id !== undefined ? englishTrack.id :
|
||||
englishTrack.index !== undefined ? englishTrack.index : 0;
|
||||
setSelectedTextTrack(trackId);
|
||||
setSelectedTextTrack(englishTrack.id);
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Auto-selected English subtitle track: ${englishTrack.name || 'Unknown'} (ID: ${trackId})`);
|
||||
logger.log(`[VideoPlayer] Auto-selected English subtitle track: ${englishTrack.name} (ID: ${englishTrack.id})`);
|
||||
}
|
||||
} else if (DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] No English subtitle track found, keeping subtitles disabled`);
|
||||
|
|
@ -1170,12 +1156,12 @@ const VideoPlayer: React.FC = () => {
|
|||
logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
|
||||
// Reduced timeout from 1000ms to 500ms
|
||||
setTimeout(() => {
|
||||
if (vlcRef.current && videoDuration > 0 && isMounted.current) {
|
||||
if (videoDuration > 0 && isMounted.current) {
|
||||
seekToTime(initialPosition);
|
||||
setIsInitialSeekComplete(true);
|
||||
logger.log(`[VideoPlayer] Initial seek completed to: ${initialPosition}s`);
|
||||
} else {
|
||||
logger.error(`[VideoPlayer] Initial seek failed: vlcRef=${!!vlcRef.current}, duration=${videoDuration}, mounted=${isMounted.current}`);
|
||||
logger.error(`[VideoPlayer] Initial seek failed: duration=${videoDuration}, mounted=${isMounted.current}`);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
|
@ -1194,10 +1180,8 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const skip = (seconds: number) => {
|
||||
if (vlcRef.current) {
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||
seekToTime(newTime);
|
||||
}
|
||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||
seekToTime(newTime);
|
||||
};
|
||||
|
||||
const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => {
|
||||
|
|
@ -1504,6 +1488,23 @@ const VideoPlayer: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Get the selected track info for logging
|
||||
const selectedTrack = vlcAudioTracks.find(track => track.id === trackId);
|
||||
if (selectedTrack && DEBUG_MODE) {
|
||||
logger.log(`[VideoPlayer] Switching to track: ${selectedTrack.name} (${selectedTrack.language})`);
|
||||
|
||||
// Check if this is a multi-channel track that might need downmixing
|
||||
const trackName = selectedTrack.name.toLowerCase();
|
||||
const isMultiChannel = trackName.includes('5.1') || trackName.includes('7.1') ||
|
||||
trackName.includes('truehd') || trackName.includes('dts') ||
|
||||
trackName.includes('dolby') || trackName.includes('atmos');
|
||||
|
||||
if (isMultiChannel) {
|
||||
logger.log(`[VideoPlayer] Multi-channel audio track detected: ${selectedTrack.name}`);
|
||||
logger.log(`[VideoPlayer] KSPlayer will apply downmixing to ensure dialogue is audible`);
|
||||
}
|
||||
}
|
||||
|
||||
// If changing tracks, briefly pause to allow smooth transition
|
||||
const wasPlaying = !paused;
|
||||
if (wasPlaying) {
|
||||
|
|
@ -1544,12 +1545,11 @@ const VideoPlayer: React.FC = () => {
|
|||
// and re-applied when switching back to built-in tracks. This prevents double-rendering.
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (!vlcRef.current) return;
|
||||
if (useCustomSubtitles) {
|
||||
// -1 disables native subtitle rendering in VLC
|
||||
vlcRef.current.setNativeProps && vlcRef.current.setNativeProps({ textTrack: -1 });
|
||||
setSelectedTextTrack(-1);
|
||||
} else if (typeof selectedTextTrack === 'number' && selectedTextTrack >= 0) {
|
||||
vlcRef.current.setNativeProps && vlcRef.current.setNativeProps({ textTrack: selectedTextTrack });
|
||||
// KSPlayer picks it up via prop
|
||||
}
|
||||
} catch (e) {
|
||||
// no-op: defensive guard in case ref methods are unavailable momentarily
|
||||
|
|
@ -1731,9 +1731,7 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
const togglePlayback = () => {
|
||||
if (vlcRef.current) {
|
||||
setPaused(!paused);
|
||||
}
|
||||
setPaused(!paused);
|
||||
};
|
||||
|
||||
// Handle next episode button press
|
||||
|
|
@ -2153,11 +2151,11 @@ const VideoPlayer: React.FC = () => {
|
|||
if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) {
|
||||
logger.log(`[VideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`);
|
||||
|
||||
if (pendingSeek.position > 0 && vlcRef.current) {
|
||||
if (pendingSeek.position > 0) {
|
||||
const delayTime = Platform.OS === 'android' ? 1500 : 1000;
|
||||
|
||||
setTimeout(() => {
|
||||
if (vlcRef.current && duration > 0 && pendingSeek) {
|
||||
if (duration > 0 && pendingSeek) {
|
||||
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
|
||||
|
||||
seekToTime(pendingSeek.position);
|
||||
|
|
@ -2228,9 +2226,6 @@ const VideoPlayer: React.FC = () => {
|
|||
logger.log(`[VideoPlayer] Available fields - quality: ${newStream.quality}, title: ${newStream.title}, addonName: ${newStream.addonName}, name: ${newStream.name}, addon: ${newStream.addon}`);
|
||||
|
||||
// Stop current playback
|
||||
if (vlcRef.current) {
|
||||
vlcRef.current.pause && vlcRef.current.pause();
|
||||
}
|
||||
setPaused(true);
|
||||
|
||||
// Set pending seek state
|
||||
|
|
@ -2297,17 +2292,18 @@ const VideoPlayer: React.FC = () => {
|
|||
top: 0,
|
||||
left: 0,
|
||||
}]}>
|
||||
{!DISABLE_OPENING_OVERLAY && (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.openingOverlay,
|
||||
{
|
||||
opacity: backgroundFadeAnim,
|
||||
zIndex: isOpeningAnimationComplete ? -1 : 3000,
|
||||
zIndex: shouldHideOpeningOverlay ? -1 : 3000,
|
||||
width: screenDimensions.width,
|
||||
height: screenDimensions.height,
|
||||
}
|
||||
]}
|
||||
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
||||
pointerEvents={shouldHideOpeningOverlay ? 'none' : 'auto'}
|
||||
>
|
||||
{backdrop && (
|
||||
<Animated.Image
|
||||
|
|
@ -2388,6 +2384,7 @@ const VideoPlayer: React.FC = () => {
|
|||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{/* Source Change Loading Overlay */}
|
||||
{isChangingSource && (
|
||||
|
|
@ -2410,12 +2407,12 @@ const VideoPlayer: React.FC = () => {
|
|||
</Animated.View>
|
||||
)}
|
||||
|
||||
<Animated.View
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.videoPlayerContainer,
|
||||
{
|
||||
opacity: openingFadeAnim,
|
||||
transform: isOpeningAnimationComplete ? [] : [{ scale: openingScaleAnim }],
|
||||
opacity: DISABLE_OPENING_OVERLAY ? 1 : openingFadeAnim,
|
||||
transform: DISABLE_OPENING_OVERLAY ? [] : [{ scale: openingScaleAnim }],
|
||||
width: screenDimensions.width,
|
||||
height: screenDimensions.height,
|
||||
}
|
||||
|
|
@ -2535,7 +2532,7 @@ const VideoPlayer: React.FC = () => {
|
|||
}}
|
||||
paused={paused}
|
||||
volume={volume / 100}
|
||||
audioTrack={selectedAudioTrack}
|
||||
audioTrack={selectedAudioTrack ?? undefined}
|
||||
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
||||
onProgress={handleProgress}
|
||||
onLoad={onLoad}
|
||||
|
|
@ -2562,7 +2559,7 @@ const VideoPlayer: React.FC = () => {
|
|||
currentTime={currentTime}
|
||||
duration={duration}
|
||||
zoomScale={zoomScale}
|
||||
vlcAudioTracks={[]} // TODO: Update with KSPlayer tracks
|
||||
vlcAudioTracks={vlcAudioTracks}
|
||||
selectedAudioTrack={selectedAudioTrack}
|
||||
availableStreams={availableStreams}
|
||||
togglePlayback={togglePlayback}
|
||||
|
|
|
|||
Loading…
Reference in a new issue