mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 08:41:57 +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;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 46;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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 */ = {
|
13B07FAE1A68108700A75B9A /* Nuvio */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
9F0599E52E7B2EF00090C551 /* NuvioDebug.entitlements */,
|
||||||
BB2F792B24A3F905000567C9 /* Supporting */,
|
BB2F792B24A3F905000567C9 /* Supporting */,
|
||||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
|
||||||
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
13B07FB01A68108700A75B9A /* AppDelegate.mm */,
|
||||||
|
|
@ -404,7 +406,7 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Nuvio/Nuvio.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Nuvio/NuvioDebug.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
|
@ -416,7 +418,10 @@
|
||||||
);
|
);
|
||||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
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;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
@ -447,7 +452,10 @@
|
||||||
DEVELOPMENT_TEAM = NLXTHANK2N;
|
DEVELOPMENT_TEAM = NLXTHANK2N;
|
||||||
INFOPLIST_FILE = Nuvio/Info.plist;
|
INFOPLIST_FILE = Nuvio/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
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;
|
MARKETING_VERSION = 1.0;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
@ -512,14 +520,14 @@
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
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)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = "$(inherited) ";
|
||||||
"$(inherited)",
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||||
|
|
@ -568,13 +576,13 @@
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
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)\"";
|
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\"";
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = "$(inherited) ";
|
||||||
"$(inherited)",
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
USE_HERMES = true;
|
USE_HERMES = true;
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = ""
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Debug"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
</ArchiveAction>
|
</ArchiveAction>
|
||||||
</Scheme>
|
</Scheme>
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,95 @@
|
||||||
<?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>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Nuvio</string>
|
<string>Nuvio</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.0</string>
|
<string>1.0.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>stremioexpo</string>
|
<string>stremioexpo</string>
|
||||||
<string>com.nuvio.app</string>
|
<string>com.nuvio.app</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>exp+nuvio</string>
|
<string>exp+nuvio</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>12</string>
|
<string>12</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
<array>
|
<array>
|
||||||
<string>_http._tcp</string>
|
<string>_http._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>App uses the local network to discover and connect to devices.</string>
|
<string>App uses the local network to discover and connect to devices.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>This app does not require microphone access.</string>
|
<string>This app does not require microphone access.</string>
|
||||||
<key>RCTRootViewBackgroundColor</key>
|
<key>RCTRootViewBackgroundColor</key>
|
||||||
<integer>4278322180</integer>
|
<integer>4278322180</integer>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIFileSharingEnabled</key>
|
||||||
<array>
|
<true/>
|
||||||
<string>audio</string>
|
<key>UILaunchStoryboardName</key>
|
||||||
</array>
|
<string>SplashScreen</string>
|
||||||
<key>UIFileSharingEnabled</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
<true/>
|
<array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<string>arm64</string>
|
||||||
<string>SplashScreen</string>
|
</array>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiresFullScreen</key>
|
||||||
<array>
|
<false/>
|
||||||
<string>arm64</string>
|
<key>UIStatusBarStyle</key>
|
||||||
</array>
|
<string>UIStatusBarStyleDefault</string>
|
||||||
<key>UIRequiresFullScreen</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<false/>
|
<array>
|
||||||
<key>UIStatusBarStyle</key>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIStatusBarStyleDefault</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<array>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<array>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>Dark</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
</array>
|
<false/>
|
||||||
<key>UIUserInterfaceStyle</key>
|
</dict>
|
||||||
<string>Dark</string>
|
</plist>
|
||||||
<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(setVolume:(nonnull NSNumber *)node volume:(nonnull NSNumber *)volume)
|
||||||
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)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface RCT_EXTERN_MODULE(KSPlayerModule, RCTEventEmitter)
|
@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
|
@end
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,13 @@ class KSPlayerModule: RCTEventEmitter {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func getTracks(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
@objc func getTracks(_ nodeTag: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||||
// This method can be expanded to get track information
|
DispatchQueue.main.async {
|
||||||
// For now, return empty tracks
|
if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager {
|
||||||
resolve([
|
viewManager.getTracks(nodeTag, resolve: resolve, reject: reject)
|
||||||
"audioTracks": [],
|
} else {
|
||||||
"textTracks": []
|
reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil)
|
||||||
])
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ class KSPlayerView: UIView {
|
||||||
private var isPaused = false
|
private var isPaused = false
|
||||||
private var currentVolume: Float = 1.0
|
private var currentVolume: Float = 1.0
|
||||||
weak var viewManager: KSPlayerViewManager?
|
weak var viewManager: KSPlayerViewManager?
|
||||||
|
private var loadTimeoutWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
// Event blocks for Fabric
|
// Event blocks for Fabric
|
||||||
@objc var onLoad: RCTDirectEventBlock?
|
@objc var onLoad: RCTDirectEventBlock?
|
||||||
|
|
@ -71,6 +72,14 @@ class KSPlayerView: UIView {
|
||||||
private func setupPlayerView() {
|
private func setupPlayerView() {
|
||||||
playerView = IOSVideoPlayerView()
|
playerView = IOSVideoPlayerView()
|
||||||
playerView.translatesAutoresizingMaskIntoConstraints = false
|
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)
|
addSubview(playerView)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
|
@ -85,11 +94,14 @@ class KSPlayerView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupPlayerCallbacks() {
|
private func setupPlayerCallbacks() {
|
||||||
// Set up the player layer delegate
|
|
||||||
playerView.playerLayer?.delegate = self
|
|
||||||
|
|
||||||
// Configure KSOptions (use static defaults where required)
|
// Configure KSOptions (use static defaults where required)
|
||||||
KSOptions.isAutoPlay = false
|
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) {
|
func setSource(_ source: NSDictionary) {
|
||||||
|
|
@ -105,12 +117,33 @@ class KSPlayerView: UIView {
|
||||||
headers = headersDict
|
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
|
// Create KSPlayerResource
|
||||||
let url = URL(string: uri)!
|
let url = URL(string: uri)!
|
||||||
let resource = KSPlayerResource(url: url, options: createOptions(with: headers), name: "Video")
|
let resource = KSPlayerResource(url: url, options: createOptions(with: headers), name: "Video")
|
||||||
|
|
||||||
print("KSPlayerView: Setting source: \(uri)")
|
print("KSPlayerView: Setting source: \(uri)")
|
||||||
playerView.set(resource: resource)
|
playerView.set(resource: resource)
|
||||||
|
|
||||||
|
// Set up delegate after setting the resource
|
||||||
|
playerView.playerLayer?.delegate = self
|
||||||
|
|
||||||
// Apply current state
|
// Apply current state
|
||||||
if isPaused {
|
if isPaused {
|
||||||
|
|
@ -120,11 +153,44 @@ class KSPlayerView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(currentVolume)
|
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 {
|
private func createOptions(with headers: [String: String]) -> KSOptions {
|
||||||
let options = 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
|
options.hardwareDecode = KSOptions.hardwareDecode
|
||||||
|
#endif
|
||||||
if !headers.isEmpty {
|
if !headers.isEmpty {
|
||||||
options.appendHeader(headers)
|
options.appendHeader(headers)
|
||||||
if let referer = headers["Referer"] ?? headers["referer"] {
|
if let referer = headers["Referer"] ?? headers["referer"] {
|
||||||
|
|
@ -149,34 +215,174 @@ class KSPlayerView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(to time: TimeInterval) {
|
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) {
|
func setAudioTrack(_ trackId: Int) {
|
||||||
if let player = playerView.playerLayer?.player {
|
if let player = playerView.playerLayer?.player {
|
||||||
let audioTracks = player.tracks(mediaType: .audio)
|
let audioTracks = player.tracks(mediaType: .audio)
|
||||||
if trackId >= 0 && trackId < audioTracks.count {
|
print("KSPlayerView: Available audio tracks count: \(audioTracks.count)")
|
||||||
// Enable only the selected track
|
print("KSPlayerView: Requested track ID: \(trackId)")
|
||||||
for (index, track) in audioTracks.enumerated() {
|
|
||||||
track.isEnabled = (index == 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) {
|
func setTextTrack(_ trackId: Int) {
|
||||||
if let player = playerView.playerLayer?.player {
|
if let player = playerView.playerLayer?.player {
|
||||||
let textTracks = player.tracks(mediaType: .subtitle)
|
let textTracks = player.tracks(mediaType: .subtitle)
|
||||||
if trackId >= 0 && trackId < textTracks.count {
|
print("KSPlayerView: Available text tracks count: \(textTracks.count)")
|
||||||
for (index, track) in textTracks.enumerated() {
|
print("KSPlayerView: Requested text track ID: \(trackId)")
|
||||||
track.isEnabled = (index == 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 {
|
} else if trackId == -1 {
|
||||||
// Disable all subtitles
|
// Disable all subtitles
|
||||||
for track in textTracks { track.isEnabled = false }
|
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
|
// Get current player state for React Native
|
||||||
func getCurrentState() -> [String: Any] {
|
func getCurrentState() -> [String: Any] {
|
||||||
|
|
@ -198,15 +404,20 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
||||||
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||||
switch state {
|
switch state {
|
||||||
case .readyToPlay:
|
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 p = layer.player
|
||||||
|
let tracks = getAvailableTracks()
|
||||||
sendEvent("onLoad", [
|
sendEvent("onLoad", [
|
||||||
"duration": p.duration,
|
"duration": p.duration,
|
||||||
"currentTime": p.currentPlaybackTime,
|
"currentTime": p.currentPlaybackTime,
|
||||||
"naturalSize": [
|
"naturalSize": [
|
||||||
"width": p.naturalSize.width,
|
"width": p.naturalSize.width,
|
||||||
"height": p.naturalSize.height
|
"height": p.naturalSize.height
|
||||||
]
|
],
|
||||||
|
"audioTracks": tracks["audioTracks"] ?? [],
|
||||||
|
"textTracks": tracks["textTracks"] ?? []
|
||||||
])
|
])
|
||||||
case .buffering:
|
case .buffering:
|
||||||
sendEvent("onBuffering", ["isBuffering": true])
|
sendEvent("onBuffering", ["isBuffering": true])
|
||||||
|
|
@ -224,11 +435,14 @@ extension KSPlayerView: KSPlayerLayerDelegate {
|
||||||
|
|
||||||
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
||||||
let p = layer.player
|
let p = layer.player
|
||||||
sendEvent("onProgress", [
|
// Ensure we have valid duration before sending progress updates
|
||||||
"currentTime": currentTime,
|
if totalTime > 0 {
|
||||||
"duration": totalTime,
|
sendEvent("onProgress", [
|
||||||
"bufferTime": p.playableTime
|
"currentTime": currentTime,
|
||||||
])
|
"duration": totalTime,
|
||||||
|
"bufferTime": p.playableTime
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func player(layer: KSPlayerLayer, finish error: Error?) {
|
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"?>
|
<?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>aps-environment</key>
|
|
||||||
<string>development</string>
|
|
||||||
<key>com.apple.developer.associated-domains</key>
|
|
||||||
<array/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export interface KSPlayerRef {
|
||||||
setVolume: (volume: number) => void;
|
setVolume: (volume: number) => void;
|
||||||
setAudioTrack: (trackId: number) => void;
|
setAudioTrack: (trackId: number) => void;
|
||||||
setTextTrack: (trackId: number) => void;
|
setTextTrack: (trackId: number) => void;
|
||||||
|
getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KSPlayerProps {
|
export interface KSPlayerProps {
|
||||||
|
|
@ -101,6 +102,13 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
UIManager.dispatchViewManagerCommand(node, commandId, [trackId]);
|
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
|
// No need for event listeners - events are handled through props
|
||||||
|
|
@ -121,12 +129,12 @@ 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}
|
||||||
onLoad={props.onLoad}
|
onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)}
|
||||||
onProgress={props.onProgress}
|
onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)}
|
||||||
onBuffering={props.onBuffering}
|
onBuffering={(e: any) => props.onBuffering?.(e?.nativeEvent ?? e)}
|
||||||
onEnd={props.onEnd}
|
onEnd={() => props.onEnd?.()}
|
||||||
onError={props.onError}
|
onError={(e: any) => props.onError?.(e?.nativeEvent ?? e)}
|
||||||
onBufferingProgress={props.onBufferingProgress}
|
onBufferingProgress={(e: any) => props.onBufferingProgress?.(e?.nativeEvent ?? e)}
|
||||||
style={props.style}
|
style={props.style}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ const VideoPlayer: React.FC = () => {
|
||||||
const isSourceSeekableRef = useRef<boolean | null>(null);
|
const isSourceSeekableRef = useRef<boolean | null>(null);
|
||||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||||
const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false);
|
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 openingFadeAnim = useRef(new Animated.Value(0)).current;
|
||||||
const openingScaleAnim = useRef(new Animated.Value(0.8)).current;
|
const openingScaleAnim = useRef(new Animated.Value(0.8)).current;
|
||||||
const backgroundFadeAnim = useRef(new Animated.Value(1)).current;
|
const backgroundFadeAnim = useRef(new Animated.Value(1)).current;
|
||||||
|
|
@ -512,7 +514,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [effectiveDimensions, videoAspectRatio]);
|
}, [effectiveDimensions, videoAspectRatio]);
|
||||||
|
|
||||||
// Force landscape orientation immediately when component mounts
|
// Force landscape orientation after opening animation completes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lockOrientation = async () => {
|
const lockOrientation = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -523,32 +525,39 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Lock orientation immediately
|
// Lock orientation after opening animation completes to prevent glitches
|
||||||
lockOrientation();
|
if (isOpeningAnimationComplete) {
|
||||||
|
lockOrientation();
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Do not unlock orientation here; we unlock explicitly on close to avoid mid-transition flips
|
// Do not unlock orientation here; we unlock explicitly on close to avoid mid-transition flips
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isOpeningAnimationComplete]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
const subscription = Dimensions.addEventListener('change', ({ screen }) => {
|
||||||
setScreenDimensions(screen);
|
setScreenDimensions(screen);
|
||||||
// Re-apply immersive mode on layout changes (Android)
|
// Re-apply immersive mode on layout changes (Android) - only after opening animation
|
||||||
enableImmersiveMode();
|
if (isOpeningAnimationComplete) {
|
||||||
|
enableImmersiveMode();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const initializePlayer = async () => {
|
const initializePlayer = async () => {
|
||||||
StatusBar.setHidden(true, 'none');
|
StatusBar.setHidden(true, 'none');
|
||||||
enableImmersiveMode();
|
// Enable immersive mode after opening animation to prevent glitches
|
||||||
|
if (isOpeningAnimationComplete) {
|
||||||
|
enableImmersiveMode();
|
||||||
|
}
|
||||||
startOpeningAnimation();
|
startOpeningAnimation();
|
||||||
|
|
||||||
// Initialize current volume and brightness levels
|
// Initialize current volume and brightness levels
|
||||||
// Volume starts at 100 (full volume) for VLC
|
// Volume starts at 100 (full volume) for VLC
|
||||||
setVolume(100);
|
setVolume(100);
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log(`[VideoPlayer] Initial volume: 100 (VLC native)`);
|
logger.log(`[VideoPlayer] Initial volume: 100 (VLC native)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentBrightness = await Brightness.getBrightnessAsync();
|
const currentBrightness = await Brightness.getBrightnessAsync();
|
||||||
setBrightness(currentBrightness);
|
setBrightness(currentBrightness);
|
||||||
|
|
@ -566,20 +575,22 @@ const VideoPlayer: React.FC = () => {
|
||||||
subscription?.remove();
|
subscription?.remove();
|
||||||
disableImmersiveMode();
|
disableImmersiveMode();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isOpeningAnimationComplete]);
|
||||||
|
|
||||||
// Re-apply immersive mode when screen gains focus (Android)
|
// Re-apply immersive mode when screen gains focus (Android)
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
enableImmersiveMode();
|
if (isOpeningAnimationComplete) {
|
||||||
|
enableImmersiveMode();
|
||||||
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [])
|
}, [isOpeningAnimationComplete])
|
||||||
);
|
);
|
||||||
|
|
||||||
// Re-apply immersive mode when app returns to foreground (Android)
|
// Re-apply immersive mode when app returns to foreground (Android)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onAppStateChange = (state: string) => {
|
const onAppStateChange = (state: string) => {
|
||||||
if (state === 'active') {
|
if (state === 'active' && isOpeningAnimationComplete) {
|
||||||
enableImmersiveMode();
|
enableImmersiveMode();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -587,7 +598,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
return () => {
|
return () => {
|
||||||
sub.remove();
|
sub.remove();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isOpeningAnimationComplete]);
|
||||||
|
|
||||||
const startOpeningAnimation = () => {
|
const startOpeningAnimation = () => {
|
||||||
// Logo entrance animation - optimized for faster appearance
|
// Logo entrance animation - optimized for faster appearance
|
||||||
|
|
@ -652,12 +663,13 @@ const VideoPlayer: React.FC = () => {
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]).start(() => {
|
]).start(() => {
|
||||||
openingScaleAnim.setValue(1);
|
|
||||||
openingFadeAnim.setValue(1);
|
|
||||||
setIsOpeningAnimationComplete(true);
|
setIsOpeningAnimationComplete(true);
|
||||||
|
// Delay hiding the overlay to allow background fade animation to complete
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
backgroundFadeAnim.setValue(0);
|
setShouldHideOpeningOverlay(true);
|
||||||
}, 100);
|
}, 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) => {
|
const seekToTime = (rawSeconds: number) => {
|
||||||
// Clamp to just before the end to avoid triggering onEnd.
|
// For KSPlayer, we need to wait for the player to be ready
|
||||||
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
|
if (!ksPlayerRef.current || isSeeking.current) {
|
||||||
if (ksPlayerRef.current && duration > 0 && !isSeeking.current) {
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
if (__DEV__) logger.log(`[VideoPlayer] Seeking to ${timeInSeconds.toFixed(2)}s out of ${duration.toFixed(2)}s`);
|
logger.error(`[VideoPlayer] Seek failed: ksPlayerRef=${!!ksPlayerRef.current}, seeking=${isSeeking.current}`);
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
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
|
// Slider callback functions for React Native Community Slider
|
||||||
|
|
@ -856,6 +873,12 @@ const VideoPlayer: React.FC = () => {
|
||||||
|
|
||||||
// KSPlayer returns times in seconds directly
|
// KSPlayer returns times in seconds directly
|
||||||
const currentTimeInSeconds = event.currentTime;
|
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
|
// Only update if there's a significant change to avoid unnecessary updates
|
||||||
if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) {
|
if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) {
|
||||||
|
|
@ -864,6 +887,13 @@ const VideoPlayer: React.FC = () => {
|
||||||
const bufferedTime = event.bufferTime || currentTimeInSeconds;
|
const bufferedTime = event.bufferTime || currentTimeInSeconds;
|
||||||
safeSetState(() => setBuffered(bufferedTime));
|
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)
|
// Periodic check for disabled audio track (every 3 seconds, max 3 attempts)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
@ -929,6 +959,9 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
// KSPlayer returns duration in seconds directly
|
// KSPlayer returns duration in seconds directly
|
||||||
const videoDuration = data.duration;
|
const videoDuration = data.duration;
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
logger.log(`[VideoPlayer] Setting duration to: ${videoDuration}`);
|
||||||
|
}
|
||||||
if (videoDuration > 0) {
|
if (videoDuration > 0) {
|
||||||
setDuration(videoDuration);
|
setDuration(videoDuration);
|
||||||
|
|
||||||
|
|
@ -959,16 +992,13 @@ const VideoPlayer: React.FC = () => {
|
||||||
logger.log(`[VideoPlayer] Raw audio tracks data:`, data.audioTracks);
|
logger.log(`[VideoPlayer] Raw audio tracks data:`, data.audioTracks);
|
||||||
data.audioTracks.forEach((track: any, idx: number) => {
|
data.audioTracks.forEach((track: any, idx: number) => {
|
||||||
logger.log(`[VideoPlayer] Track ${idx} raw data:`, {
|
logger.log(`[VideoPlayer] Track ${idx} raw data:`, {
|
||||||
index: track.index,
|
id: track.id,
|
||||||
title: track.title,
|
|
||||||
language: track.language,
|
|
||||||
type: track.type,
|
|
||||||
channels: track.channels,
|
|
||||||
bitrate: track.bitrate,
|
|
||||||
codec: track.codec,
|
|
||||||
sampleRate: track.sampleRate,
|
|
||||||
name: track.name,
|
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),
|
allKeys: Object.keys(track),
|
||||||
fullTrackObject: track
|
fullTrackObject: track
|
||||||
});
|
});
|
||||||
|
|
@ -976,63 +1006,33 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
|
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
|
// Build comprehensive track name from available fields
|
||||||
let trackName = '';
|
let trackName = '';
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
||||||
// Add language if available (try multiple possible fields)
|
// Add language if available
|
||||||
let language = track.language || track.lang || track.languageCode;
|
let language = track.language || 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (language && language !== 'Unknown' && language !== 'und' && language !== '') {
|
if (language && language !== 'Unknown' && language !== 'und' && language !== '') {
|
||||||
parts.push(language.toUpperCase());
|
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
|
// Add bitrate if available
|
||||||
const bitrate = track.bitrate || track.bitRate;
|
const bitrate = track.bitRate;
|
||||||
if (bitrate && bitrate > 0) {
|
if (bitrate && bitrate > 0) {
|
||||||
parts.push(`${Math.round(bitrate / 1000)}kbps`);
|
parts.push(`${Math.round(bitrate / 1000)}kbps`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sample rate if available
|
// Add bit depth if available
|
||||||
const sampleRate = track.sampleRate || track.sample_rate;
|
const bitDepth = track.bitDepth;
|
||||||
if (sampleRate && sampleRate > 0) {
|
if (bitDepth && bitDepth > 0) {
|
||||||
parts.push(`${Math.round(sampleRate / 1000)}kHz`);
|
parts.push(`${bitDepth}bit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add title if available and not generic
|
// Add track name if available and not generic
|
||||||
let title = track.title || track.name || track.label;
|
let title = track.name;
|
||||||
if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') {
|
if (title && !title.match(/^(Audio|Track)\s*\d*$/i) && title !== 'Unknown') {
|
||||||
// Clean up title by removing language brackets and trailing punctuation
|
// Clean up title by removing language brackets and trailing punctuation
|
||||||
title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim();
|
title = title.replace(/\s*\[[^\]]+\]\s*[-–—]*\s*$/, '').trim();
|
||||||
|
|
@ -1046,44 +1046,29 @@ const VideoPlayer: React.FC = () => {
|
||||||
trackName = parts.join(' • ');
|
trackName = parts.join(' • ');
|
||||||
} else {
|
} else {
|
||||||
// For simple track names like "Track 1", "Audio 1", etc., use them as-is
|
// 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)) {
|
if (simpleName && simpleName.match(/^(Track|Audio)\s*\d*$/i)) {
|
||||||
trackName = simpleName;
|
trackName = simpleName;
|
||||||
} else {
|
} else {
|
||||||
// Try to extract any meaningful info from the track object
|
trackName = `Audio ${index + 1}`;
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackLanguage = language || 'Unknown';
|
const trackLanguage = language || 'Unknown';
|
||||||
|
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
logger.log(`[VideoPlayer] Processed track ${index}:`, {
|
logger.log(`[VideoPlayer] Processed KSPlayer track ${index}:`, {
|
||||||
index: trackIndex,
|
id: trackIndex,
|
||||||
name: trackName,
|
name: trackName,
|
||||||
language: trackLanguage,
|
language: trackLanguage,
|
||||||
parts: parts,
|
parts: parts,
|
||||||
meaningfulFields: Object.keys(track).filter(key => {
|
bitRate: bitrate,
|
||||||
const value = track[key];
|
bitDepth: bitDepth
|
||||||
return value && typeof value === 'string' && value !== 'Unknown' && value !== 'und' && value.length > 1;
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: trackIndex, // Use the actual track index from VLC
|
id: trackIndex, // Use the actual track ID from KSPlayer
|
||||||
name: trackName,
|
name: trackName,
|
||||||
language: trackLanguage,
|
language: trackLanguage,
|
||||||
};
|
};
|
||||||
|
|
@ -1116,21 +1101,25 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.textTracks && data.textTracks.length > 0) {
|
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
|
// 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) {
|
if (DEBUG_MODE) {
|
||||||
logger.log(`[VideoPlayer] Available subtitle tracks:`, data.textTracks.map((track: any) => ({
|
logger.log(`[VideoPlayer] Available KSPlayer subtitle tracks:`, formattedTextTracks);
|
||||||
id: track.id,
|
|
||||||
index: track.index,
|
|
||||||
name: track.name,
|
|
||||||
language: track.language
|
|
||||||
})));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for English track first
|
// Look for English track first
|
||||||
const englishTrack = data.textTracks.find((track: any) => {
|
const englishTrack = formattedTextTracks.find((track: any) => {
|
||||||
const lang = (track.language || '').toLowerCase();
|
const lang = (track.language || '').toLowerCase();
|
||||||
const name = (track.name || '').toLowerCase();
|
const name = (track.name || '').toLowerCase();
|
||||||
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
||||||
|
|
@ -1138,12 +1127,9 @@ const VideoPlayer: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (englishTrack) {
|
if (englishTrack) {
|
||||||
// Try different ID fields that VLC might use
|
setSelectedTextTrack(englishTrack.id);
|
||||||
const trackId = englishTrack.id !== undefined ? englishTrack.id :
|
|
||||||
englishTrack.index !== undefined ? englishTrack.index : 0;
|
|
||||||
setSelectedTextTrack(trackId);
|
|
||||||
if (DEBUG_MODE) {
|
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) {
|
} else if (DEBUG_MODE) {
|
||||||
logger.log(`[VideoPlayer] No English subtitle track found, keeping subtitles disabled`);
|
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)`);
|
logger.log(`[VideoPlayer] Seeking to initial position: ${initialPosition}s (duration: ${videoDuration}s)`);
|
||||||
// Reduced timeout from 1000ms to 500ms
|
// Reduced timeout from 1000ms to 500ms
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (vlcRef.current && videoDuration > 0 && isMounted.current) {
|
if (videoDuration > 0 && isMounted.current) {
|
||||||
seekToTime(initialPosition);
|
seekToTime(initialPosition);
|
||||||
setIsInitialSeekComplete(true);
|
setIsInitialSeekComplete(true);
|
||||||
logger.log(`[VideoPlayer] Initial seek completed to: ${initialPosition}s`);
|
logger.log(`[VideoPlayer] Initial seek completed to: ${initialPosition}s`);
|
||||||
} else {
|
} 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);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
@ -1194,10 +1180,8 @@ const VideoPlayer: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const skip = (seconds: number) => {
|
const skip = (seconds: number) => {
|
||||||
if (vlcRef.current) {
|
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
||||||
const newTime = Math.max(0, Math.min(currentTime + seconds, duration - END_EPSILON));
|
seekToTime(newTime);
|
||||||
seekToTime(newTime);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => {
|
const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => {
|
||||||
|
|
@ -1504,6 +1488,23 @@ const VideoPlayer: React.FC = () => {
|
||||||
return;
|
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
|
// If changing tracks, briefly pause to allow smooth transition
|
||||||
const wasPlaying = !paused;
|
const wasPlaying = !paused;
|
||||||
if (wasPlaying) {
|
if (wasPlaying) {
|
||||||
|
|
@ -1544,12 +1545,11 @@ const VideoPlayer: React.FC = () => {
|
||||||
// and re-applied when switching back to built-in tracks. This prevents double-rendering.
|
// and re-applied when switching back to built-in tracks. This prevents double-rendering.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
if (!vlcRef.current) return;
|
|
||||||
if (useCustomSubtitles) {
|
if (useCustomSubtitles) {
|
||||||
// -1 disables native subtitle rendering in VLC
|
// -1 disables native subtitle rendering in VLC
|
||||||
vlcRef.current.setNativeProps && vlcRef.current.setNativeProps({ textTrack: -1 });
|
setSelectedTextTrack(-1);
|
||||||
} else if (typeof selectedTextTrack === 'number' && selectedTextTrack >= 0) {
|
} else if (typeof selectedTextTrack === 'number' && selectedTextTrack >= 0) {
|
||||||
vlcRef.current.setNativeProps && vlcRef.current.setNativeProps({ textTrack: selectedTextTrack });
|
// KSPlayer picks it up via prop
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// no-op: defensive guard in case ref methods are unavailable momentarily
|
// no-op: defensive guard in case ref methods are unavailable momentarily
|
||||||
|
|
@ -1731,9 +1731,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePlayback = () => {
|
const togglePlayback = () => {
|
||||||
if (vlcRef.current) {
|
setPaused(!paused);
|
||||||
setPaused(!paused);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle next episode button press
|
// Handle next episode button press
|
||||||
|
|
@ -2153,11 +2151,11 @@ const VideoPlayer: React.FC = () => {
|
||||||
if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) {
|
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`);
|
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;
|
const delayTime = Platform.OS === 'android' ? 1500 : 1000;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (vlcRef.current && duration > 0 && pendingSeek) {
|
if (duration > 0 && pendingSeek) {
|
||||||
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
|
logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`);
|
||||||
|
|
||||||
seekToTime(pendingSeek.position);
|
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}`);
|
logger.log(`[VideoPlayer] Available fields - quality: ${newStream.quality}, title: ${newStream.title}, addonName: ${newStream.addonName}, name: ${newStream.name}, addon: ${newStream.addon}`);
|
||||||
|
|
||||||
// Stop current playback
|
// Stop current playback
|
||||||
if (vlcRef.current) {
|
|
||||||
vlcRef.current.pause && vlcRef.current.pause();
|
|
||||||
}
|
|
||||||
setPaused(true);
|
setPaused(true);
|
||||||
|
|
||||||
// Set pending seek state
|
// Set pending seek state
|
||||||
|
|
@ -2297,17 +2292,18 @@ const VideoPlayer: React.FC = () => {
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
}]}>
|
}]}>
|
||||||
|
{!DISABLE_OPENING_OVERLAY && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.openingOverlay,
|
styles.openingOverlay,
|
||||||
{
|
{
|
||||||
opacity: backgroundFadeAnim,
|
opacity: backgroundFadeAnim,
|
||||||
zIndex: isOpeningAnimationComplete ? -1 : 3000,
|
zIndex: shouldHideOpeningOverlay ? -1 : 3000,
|
||||||
width: screenDimensions.width,
|
width: screenDimensions.width,
|
||||||
height: screenDimensions.height,
|
height: screenDimensions.height,
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
pointerEvents={shouldHideOpeningOverlay ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
{backdrop && (
|
{backdrop && (
|
||||||
<Animated.Image
|
<Animated.Image
|
||||||
|
|
@ -2388,6 +2384,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Source Change Loading Overlay */}
|
{/* Source Change Loading Overlay */}
|
||||||
{isChangingSource && (
|
{isChangingSource && (
|
||||||
|
|
@ -2410,12 +2407,12 @@ const VideoPlayer: React.FC = () => {
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.videoPlayerContainer,
|
styles.videoPlayerContainer,
|
||||||
{
|
{
|
||||||
opacity: openingFadeAnim,
|
opacity: DISABLE_OPENING_OVERLAY ? 1 : openingFadeAnim,
|
||||||
transform: isOpeningAnimationComplete ? [] : [{ scale: openingScaleAnim }],
|
transform: DISABLE_OPENING_OVERLAY ? [] : [{ scale: openingScaleAnim }],
|
||||||
width: screenDimensions.width,
|
width: screenDimensions.width,
|
||||||
height: screenDimensions.height,
|
height: screenDimensions.height,
|
||||||
}
|
}
|
||||||
|
|
@ -2535,7 +2532,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
paused={paused}
|
paused={paused}
|
||||||
volume={volume / 100}
|
volume={volume / 100}
|
||||||
audioTrack={selectedAudioTrack}
|
audioTrack={selectedAudioTrack ?? undefined}
|
||||||
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
||||||
onProgress={handleProgress}
|
onProgress={handleProgress}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
|
|
@ -2562,7 +2559,7 @@ const VideoPlayer: React.FC = () => {
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
duration={duration}
|
duration={duration}
|
||||||
zoomScale={zoomScale}
|
zoomScale={zoomScale}
|
||||||
vlcAudioTracks={[]} // TODO: Update with KSPlayer tracks
|
vlcAudioTracks={vlcAudioTracks}
|
||||||
selectedAudioTrack={selectedAudioTrack}
|
selectedAudioTrack={selectedAudioTrack}
|
||||||
availableStreams={availableStreams}
|
availableStreams={availableStreams}
|
||||||
togglePlayback={togglePlayback}
|
togglePlayback={togglePlayback}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue