KSPlayre AUdio track selection fix

This commit is contained in:
tapframe 2025-09-18 01:56:05 +05:30
parent e9e16ed05a
commit 18815b8233
10 changed files with 534 additions and 303 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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