From 985d01d5a96e137c9e1bca28f5d17e637de1c541 Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 26 Dec 2025 12:32:33 +0530 Subject: [PATCH] ksplayer sub rendering fix --- .../main/java/com/nuvio/app/mpv/MPVView.kt | 12 +- ios/KSPlayerView.swift | 135 +++++++----------- ios/KSPlayerViewManager.swift | 5 + ios/Nuvio.xcodeproj/project.pbxproj | 2 + ios/Nuvio/Info.plist | 2 +- package-lock.json | 2 +- package.json | 2 +- src/components/player/KSPlayerComponent.tsx | 5 + src/components/player/KSPlayerCore.tsx | 31 +++- .../player/ios/components/KSPlayerSurface.tsx | 5 + 10 files changed, 102 insertions(+), 99 deletions(-) diff --git a/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt b/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt index 96d6718..00149c4 100644 --- a/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt +++ b/android/app/src/main/java/com/nuvio/app/mpv/MPVView.kt @@ -132,7 +132,7 @@ class MPVView @JvmOverloads constructor( MPVLib.setOptionString("sub-auto", "fuzzy") MPVLib.setOptionString("sub-visibility", "yes") MPVLib.setOptionString("sub-font-size", "48") - MPVLib.setOptionString("sub-pos", "95") + MPVLib.setOptionString("sub-pos", "100") MPVLib.setOptionString("sub-color", "#FFFFFFFF") MPVLib.setOptionString("sub-border-size", "3") MPVLib.setOptionString("sub-border-color", "#FF000000") @@ -146,15 +146,15 @@ class MPVView @JvmOverloads constructor( MPVLib.setOptionString("sub-codepage", "auto") - MPVLib.setOptionString("osc", "no") - MPVLib.setOptionString("osd-level", "1") - MPVLib.setOptionString("blend-subtitles", "no") - MPVLib.setOptionString("sub-use-margins", "no") - MPVLib.setOptionString("sub-ass-override", "scale") + MPVLib.setOptionString("sub-use-margins", "yes") + MPVLib.setOptionString("sub-ass-override", "force") MPVLib.setOptionString("sub-scale", "1.0") MPVLib.setOptionString("sub-fix-timing", "yes") + MPVLib.setOptionString("osc", "no") + MPVLib.setOptionString("osd-level", "1") + MPVLib.setOptionString("sid", "auto") MPVLib.setOptionString("terminal", "no") diff --git a/ios/KSPlayerView.swift b/ios/KSPlayerView.swift index d81b278..7fe8b98 100644 --- a/ios/KSPlayerView.swift +++ b/ios/KSPlayerView.swift @@ -246,20 +246,15 @@ class KSPlayerView: UIView { } private func setupPlayerCallbacks() { - // Configure KSOptions (use static defaults where required) + // Configure KSOptions 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 - #else - // PERFORMANCE OPTIMIZATION: Enable asynchronous decompression globally - // This ensures the global default is correct for all player instances KSOptions.asynchronousDecompression = true - // Ensure hardware decode is enabled globally KSOptions.hardwareDecode = true - #endif + + // Set default subtitle font size - use standard size for readability + SubtitleModel.textFontSize = 20.0 // Moderate size that works on most devices + SubtitleModel.textBold = false + print("KSPlayerView: [PERF] Global settings: asyncDecomp=\(KSOptions.asynchronousDecompression), hwDecode=\(KSOptions.hardwareDecode)") } @@ -298,21 +293,14 @@ class KSPlayerView: UIView { // 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 + // Prefer MEPlayer (FFmpeg) for MKV KSOptions.firstPlayerType = KSMEPlayer.self KSOptions.secondPlayerType = nil } else { KSOptions.firstPlayerType = KSAVPlayer.self KSOptions.secondPlayerType = KSMEPlayer.self } - #endif // Create KSPlayerResource with validated URL let resource = KSPlayerResource(url: url, options: createOptions(with: headers), name: "Video") @@ -376,15 +364,9 @@ class KSPlayerView: UIView { // PERFORMANCE OPTIMIZATION: Hardware decode explicitly enabled // Ensure VideoToolbox hardware acceleration is always preferred for non-simulator - #if targetEnvironment(simulator) - options.hardwareDecode = false - options.asynchronousDecompression = false - #else - options.hardwareDecode = true // Explicitly enable hardware decode - // PERFORMANCE OPTIMIZATION: Asynchronous decompression (CRITICAL) - // Offloads VideoToolbox decompression to background threads, preventing main thread stalls + // Hardware decode and async decompression + options.hardwareDecode = true options.asynchronousDecompression = true - #endif // HDR handling: Let KSPlayer automatically detect content's native dynamic range // Setting destinationDynamicRange to nil allows KSPlayer to use the content's actual HDR/SDR mode @@ -557,22 +539,21 @@ class KSPlayerView: UIView { } func setTextTrack(_ trackId: Int) { - print("KSPlayerView: [SET TEXT TRACK] Starting setTextTrack with trackId: \(trackId)") + NSLog("KSPlayerView: [SET TEXT TRACK] Starting setTextTrack with trackId: %d", trackId) - // Wait slightly longer than the 1-second delay for subtitle data source connection - // This ensures srtControl.addSubtitle(dataSouce:) has been called in VideoPlayerView - DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { [weak self] in + // Small delay to ensure player is ready + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self = self else { - print("KSPlayerView: [SET TEXT TRACK] self is nil, aborting") + NSLog("KSPlayerView: [SET TEXT TRACK] self is nil, aborting") return } - print("KSPlayerView: [SET TEXT TRACK] Executing delayed track selection") + NSLog("KSPlayerView: [SET TEXT TRACK] Executing track selection") if let player = self.playerView.playerLayer?.player { let textTracks = player.tracks(mediaType: .subtitle) - print("KSPlayerView: Available text tracks count: \(textTracks.count)") - print("KSPlayerView: Requested text track ID: \(trackId)") + NSLog("KSPlayerView: Available text tracks count: %d", textTracks.count) + NSLog("KSPlayerView: Requested text track ID: %d", trackId) // First try to find track by trackID (proper way) var selectedTrack: MediaPlayerTrack? = nil @@ -582,84 +563,57 @@ class KSPlayerView: UIView { 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)") + NSLog("KSPlayerView: Found text track by trackID %d at index %d", trackId, 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)") + NSLog("KSPlayerView: Found text track by array index %d (fallback)", trackId) } if let track = selectedTrack { - print("KSPlayerView: Selecting text track \(trackId) (index: \(trackIndex)): '\(track.name)' (ID: \(track.trackID))") + NSLog("KSPlayerView: Selecting text track %d (index: %d): '%@' (ID: %d)", trackId, trackIndex, track.name, track.trackID) - // First disable all tracks to ensure only one is active + // Disable all tracks first for t in textTracks { t.isEnabled = false } - // Use KSPlayer's select method which properly handles track selection + // Enable the selected track + track.isEnabled = true + + // Use KSPlayer's select method to update player state player.select(track: track) - // Sync srtControl with player track selection - // Find the corresponding SubtitleInfo in srtControl and select it - if let matchingSubtitleInfo = self.playerView.srtControl.subtitleInfos.first(where: { subtitleInfo in - // Try to match by name or track ID - subtitleInfo.name.lowercased() == track.name.lowercased() || - subtitleInfo.subtitleID == String(track.trackID) - }) { - print("KSPlayerView: Found matching SubtitleInfo: \(matchingSubtitleInfo.name) (ID: \(matchingSubtitleInfo.subtitleID))") - self.playerView.srtControl.selectedSubtitleInfo = matchingSubtitleInfo - print("KSPlayerView: Set srtControl.selectedSubtitleInfo to: \(matchingSubtitleInfo.name)") + // CRITICAL: Cast MediaPlayerTrack to SubtitleInfo and set on srtControl + // FFmpegAssetTrack conforms to SubtitleInfo via extension + if let subtitleInfo = track as? SubtitleInfo { + self.playerView.srtControl.selectedSubtitleInfo = subtitleInfo + NSLog("KSPlayerView: Set srtControl.selectedSubtitleInfo to track '%@'", track.name) } else { - print("KSPlayerView: No matching SubtitleInfo found for track '\(track.name)' (ID: \(track.trackID))") - print("KSPlayerView: Available SubtitleInfos:") - for (index, info) in self.playerView.srtControl.subtitleInfos.enumerated() { - print("KSPlayerView: [\(index)] name='\(info.name)', subtitleID='\(info.subtitleID)'") - } + NSLog("KSPlayerView: Warning - track could not be cast to SubtitleInfo") } - // Ensure subtitle views are visible after selection + // Ensure subtitle views are visible self.playerView.subtitleLabel.isHidden = false self.playerView.subtitleBackView.isHidden = false - // Debug: Check the enabled state of all tracks after selection - print("KSPlayerView: Track states after selection:") - for (index, t) in textTracks.enumerated() { - print("KSPlayerView: Track \(index): ID=\(t.trackID), Name='\(t.name)', Enabled=\(t.isEnabled)") - } - - // Verify the selection worked after a delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - let tracksAfter = player.tracks(mediaType: .subtitle) - print("KSPlayerView: Verification after subtitle selection:") - for (index, track) in tracksAfter.enumerated() { - print("KSPlayerView: Track \(index) (ID: \(track.trackID)) isEnabled: \(track.isEnabled)") - } - - // Also verify srtControl selection - if let selectedInfo = self.playerView.srtControl.selectedSubtitleInfo { - print("KSPlayerView: srtControl.selectedSubtitleInfo: \(selectedInfo.name) (ID: \(selectedInfo.subtitleID))") - } else { - print("KSPlayerView: srtControl.selectedSubtitleInfo is nil") - } - } - - print("KSPlayerView: Successfully selected text track \(trackId)") + NSLog("KSPlayerView: Successfully selected and enabled text track %d", trackId) } else if trackId == -1 { // Disable all subtitles - for track in textTracks { track.isEnabled = false } - // Clear srtControl selection and hide subtitle views + for track in textTracks { + track.isEnabled = false + } self.playerView.srtControl.selectedSubtitleInfo = nil self.playerView.subtitleLabel.isHidden = true self.playerView.subtitleBackView.isHidden = true - print("KSPlayerView: Disabled all text tracks and cleared srtControl selection") + NSLog("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)") + NSLog("KSPlayerView: Text track %d not found. Available count: %d", trackId, textTracks.count) } } else { - print("KSPlayerView: No player available for text track selection") + NSLog("KSPlayerView: No player available for text track selection") } } } @@ -1015,7 +969,18 @@ extension KSPlayerView: KSPlayerLayerDelegate { if let part = playerView.srtControl.parts.first { print("KSPlayerView: [SUBTITLE RENDER] time=\(currentTime), text='\(part.text?.string ?? "nil")', hasImage=\(part.image != nil)") playerView.subtitleBackView.image = part.image - playerView.subtitleLabel.attributedText = part.text + + // Normalize font size for all subtitles to ensure consistent display + if let originalText = part.text { + let mutableText = NSMutableAttributedString(attributedString: originalText) + // Apply consistent font across the entire text + let font = UIFont.systemFont(ofSize: 20.0) + mutableText.addAttributes([.font: font], range: NSRange(location: 0, length: mutableText.length)) + playerView.subtitleLabel.attributedText = mutableText + } else { + playerView.subtitleLabel.attributedText = nil + } + playerView.subtitleBackView.isHidden = false playerView.subtitleLabel.isHidden = false print("KSPlayerView: [SUBTITLE RENDER] Set subtitle text and made views visible") diff --git a/ios/KSPlayerViewManager.swift b/ios/KSPlayerViewManager.swift index 657b70a..4017fb3 100644 --- a/ios/KSPlayerViewManager.swift +++ b/ios/KSPlayerViewManager.swift @@ -87,9 +87,14 @@ class KSPlayerViewManager: RCTViewManager { } @objc func setTextTrack(_ node: NSNumber, trackId: NSNumber) { + NSLog("[KSPlayerViewManager] setTextTrack called - node: %@, trackId: %@", node, trackId) DispatchQueue.main.async { + NSLog("[KSPlayerViewManager] setTextTrack on main queue - looking for view with tag: %@", node) if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { + NSLog("[KSPlayerViewManager] Found view, calling setTextTrack(%d)", Int(truncating: trackId)) view.setTextTrack(Int(truncating: trackId)) + } else { + NSLog("[KSPlayerViewManager] ERROR - Could not find KSPlayerView for tag: %@", node) } } } diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 28d1c83..69c16f2 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -476,6 +476,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; PRODUCT_NAME = Nuvio; + SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -508,6 +509,7 @@ OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.hub; PRODUCT_NAME = Nuvio; + SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/ios/Nuvio/Info.plist b/ios/Nuvio/Info.plist index dd57a5d..af0f297 100644 --- a/ios/Nuvio/Info.plist +++ b/ios/Nuvio/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.3.1 + 1.2.10 CFBundleSignature ???? CFBundleURLTypes diff --git a/package-lock.json b/package-lock.json index b085182..e690a85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "expo-brightness": "~14.0.7", "expo-clipboard": "~8.0.8", "expo-crypto": "~15.0.7", - "expo-dev-client": "~6.0.15", + "expo-dev-client": "~6.0.20", "expo-device": "~8.0.9", "expo-document-picker": "~14.0.7", "expo-file-system": "~19.0.17", diff --git a/package.json b/package.json index 084952f..f6c33bd 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "expo-brightness": "~14.0.7", "expo-clipboard": "~8.0.8", "expo-crypto": "~15.0.7", - "expo-dev-client": "~6.0.15", + "expo-dev-client": "~6.0.20", "expo-device": "~8.0.9", "expo-document-picker": "~14.0.7", "expo-file-system": "~19.0.17", diff --git a/src/components/player/KSPlayerComponent.tsx b/src/components/player/KSPlayerComponent.tsx index 569ca06..06fc2e3 100644 --- a/src/components/player/KSPlayerComponent.tsx +++ b/src/components/player/KSPlayerComponent.tsx @@ -120,11 +120,16 @@ const KSPlayer = forwardRef((props, ref) => { } }, setTextTrack: (trackId: number) => { + console.log('[KSPlayerComponent] setTextTrack called with trackId:', trackId); if (nativeRef.current) { const node = findNodeHandle(nativeRef.current); + console.log('[KSPlayerComponent] setTextTrack dispatching command to node:', node); // @ts-ignore legacy UIManager commands path for Paper const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setTextTrack; + console.log('[KSPlayerComponent] setTextTrack commandId:', commandId); UIManager.dispatchViewManagerCommand(node, commandId, [trackId]); + } else { + console.warn('[KSPlayerComponent] setTextTrack: nativeRef.current is null'); } }, getTracks: async () => { diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 6e3c1f5..af8a017 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -389,6 +389,27 @@ const KSPlayerCore: React.FC = () => { navigation.goBack(); }, [navigation, currentTime, duration, traktAutosync]); + // Track selection handlers - update state, prop change triggers native update + const handleSelectTextTrack = useCallback((trackId: number) => { + console.log('[KSPlayerCore] handleSelectTextTrack called with trackId:', trackId); + + // Disable custom subtitles when selecting a built-in track + // This ensures the textTrack prop is actually passed to the native player + if (trackId !== -1) { + customSubs.setUseCustomSubtitles(false); + } + + // Just update state - the textTrack prop change will trigger native update + tracks.selectTextTrack(trackId); + }, [tracks, customSubs]); + + const handleSelectAudioTrack = useCallback((trackId: number) => { + tracks.selectAudioTrack(trackId); + if (ksPlayerRef.current) { + ksPlayerRef.current.setAudioTrack(trackId); + } + }, [tracks, ksPlayerRef]); + // Stream selection handler const handleSelectStream = async (newStream: any) => { if (newStream.url === uri) { @@ -498,8 +519,8 @@ const KSPlayerCore: React.FC = () => { setZoomScale={setZoomScale} lastZoomScale={lastZoomScale} setLastZoomScale={setLastZoomScale} - audioTrack={tracks.selectedAudioTrack !== null ? tracks.selectedAudioTrack : undefined} - textTrack={customSubs.useCustomSubtitles ? undefined : (tracks.selectedTextTrack !== -1 ? tracks.selectedTextTrack : undefined)} + audioTrack={tracks.selectedAudioTrack ?? undefined} + textTrack={customSubs.useCustomSubtitles ? -1 : tracks.selectedTextTrack} onAudioTracks={(d) => tracks.setKsAudioTracks(d.audioTracks || [])} onTextTracks={(d) => tracks.setKsTextTracks(d.textTracks || [])} onLoad={onLoad} @@ -682,7 +703,7 @@ const KSPlayerCore: React.FC = () => { setShowAudioModal={modals.setShowAudioModal} ksAudioTracks={tracks.ksAudioTracks} selectedAudioTrack={tracks.selectedAudioTrack} - selectAudioTrack={tracks.selectAudioTrack} + selectAudioTrack={handleSelectAudioTrack} /> { ksTextTracks={tracks.ksTextTracks} selectedTextTrack={tracks.selectedTextTrack !== null ? tracks.selectedTextTrack : -1} useCustomSubtitles={customSubs.useCustomSubtitles} - selectTextTrack={tracks.selectTextTrack} + selectTextTrack={handleSelectTextTrack} disableCustomSubtitles={() => { customSubs.setUseCustomSubtitles(false); - tracks.selectTextTrack(-1); + handleSelectTextTrack(-1); }} /> diff --git a/src/components/player/ios/components/KSPlayerSurface.tsx b/src/components/player/ios/components/KSPlayerSurface.tsx index 715e6b2..4af5ab5 100644 --- a/src/components/player/ios/components/KSPlayerSurface.tsx +++ b/src/components/player/ios/components/KSPlayerSurface.tsx @@ -87,6 +87,11 @@ export const KSPlayerSurface: React.FC = ({ headers }; + // Debug: log textTrack prop changes + React.useEffect(() => { + console.log('[KSPlayerSurface] textTrack prop changed to:', textTrack); + }, [textTrack]); + // Handle buffering - KSPlayerComponent uses onBuffering callback const handleBuffering = (data: any) => { onBuffer(data?.isBuffering ?? false);