From eb6fcf639f25c13bb0a0e49a48b2278d7fe23c69 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 11 Jan 2026 00:46:30 +0530 Subject: [PATCH] ksp sub updates --- MPVKit | 1 - ios/KSPlayerManager.m | 69 -- ios/KSPlayerModule.swift | 71 -- ios/KSPlayerView.swift | 1003 ----------------- ios/KSPlayerViewManager.swift | 152 --- ios/Nuvio.xcodeproj/project.pbxproj | 128 +-- ios/Podfile | 5 +- ios/Podfile.lock | 18 +- src/components/player/KSPlayerComponent.tsx | 3 + src/components/player/KSPlayerCore.tsx | 19 +- .../player/ios/components/KSPlayerSurface.tsx | 5 +- .../player/modals/SubtitleModals.tsx | 112 +- src/i18n/locales/ar.json | 1 + src/i18n/locales/de.json | 1 + src/i18n/locales/en.json | 1 + src/i18n/locales/es.json | 1 + src/i18n/locales/fr.json | 1 + src/i18n/locales/it.json | 1 + src/i18n/locales/pt-BR.json | 1 + src/i18n/locales/pt-PT.json | 1 + 20 files changed, 171 insertions(+), 1423 deletions(-) delete mode 160000 MPVKit delete mode 100644 ios/KSPlayerManager.m delete mode 100644 ios/KSPlayerModule.swift delete mode 100644 ios/KSPlayerView.swift delete mode 100644 ios/KSPlayerViewManager.swift diff --git a/MPVKit b/MPVKit deleted file mode 160000 index 28de60c..0000000 --- a/MPVKit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 28de60c09651b8ca899d67456e02d285c92ceee2 diff --git a/ios/KSPlayerManager.m b/ios/KSPlayerManager.m deleted file mode 100644 index 0a9221a..0000000 --- a/ios/KSPlayerManager.m +++ /dev/null @@ -1,69 +0,0 @@ -// -// KSPlayerManager.m -// Nuvio -// -// Created by KSPlayer integration -// - -#import -#import -#import - -@interface RCT_EXTERN_MODULE (KSPlayerViewManager, RCTViewManager) - -RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary) -RCT_EXPORT_VIEW_PROPERTY(paused, BOOL) -RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(rate, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL) -RCT_EXPORT_VIEW_PROPERTY(usesExternalPlaybackWhileExternalScreenIsActive, BOOL) -RCT_EXPORT_VIEW_PROPERTY(subtitleBottomOffset, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(subtitleFontSize, NSNumber) -RCT_EXPORT_VIEW_PROPERTY(subtitleTextColor, NSString) -RCT_EXPORT_VIEW_PROPERTY(subtitleBackgroundColor, NSString) -RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString) - -// Event properties -RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onProgress, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onBuffering, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onEnd, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onError, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(onBufferingProgress, RCTDirectEventBlock) - -RCT_EXTERN_METHOD(seek : (nonnull NSNumber *)node toTime : (nonnull NSNumber *) - time) -RCT_EXTERN_METHOD(setSource : (nonnull NSNumber *) - node source : (nonnull NSDictionary *)source) -RCT_EXTERN_METHOD(setPaused : (nonnull NSNumber *)node paused : (BOOL)paused) -RCT_EXTERN_METHOD(setVolume : (nonnull NSNumber *) - node volume : (nonnull NSNumber *)volume) -RCT_EXTERN_METHOD(setPlaybackRate : (nonnull NSNumber *) - node rate : (nonnull NSNumber *)rate) -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) -RCT_EXTERN_METHOD(setAllowsExternalPlayback : (nonnull NSNumber *) - node allows : (BOOL)allows) -RCT_EXTERN_METHOD(setUsesExternalPlaybackWhileExternalScreenIsActive : ( - nonnull NSNumber *)node uses : (BOOL)uses) -RCT_EXTERN_METHOD(getAirPlayState : (nonnull NSNumber *)node resolve : ( - RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(showAirPlayPicker : (nonnull NSNumber *)node) - -@end - -@interface RCT_EXTERN_MODULE (KSPlayerModule, RCTEventEmitter) - -RCT_EXTERN_METHOD(getTracks : (NSNumber *)nodeTag resolve : ( - RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(getAirPlayState : (NSNumber *)nodeTag resolve : ( - RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) -RCT_EXTERN_METHOD(showAirPlayPicker : (NSNumber *)nodeTag) - -@end diff --git a/ios/KSPlayerModule.swift b/ios/KSPlayerModule.swift deleted file mode 100644 index 27dd614..0000000 --- a/ios/KSPlayerModule.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// KSPlayerModule.swift -// Nuvio -// -// Created by KSPlayer integration -// - -import Foundation -import KSPlayer -import React - -@objc(KSPlayerModule) -class KSPlayerModule: RCTEventEmitter { - override static func requiresMainQueueSetup() -> Bool { - return true - } - - override func supportedEvents() -> [String]! { - return [ - "KSPlayer-onLoad", - "KSPlayer-onProgress", - "KSPlayer-onBuffering", - "KSPlayer-onEnd", - "KSPlayer-onError" - ] - } - - @objc func getTracks(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - guard let nodeTag = nodeTag else { - reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil) - return - } - 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) - } - } - } - - @objc func getAirPlayState(_ nodeTag: NSNumber?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - guard let nodeTag = nodeTag else { - reject("INVALID_ARGUMENT", "nodeTag must not be nil", nil) - return - } - DispatchQueue.main.async { - if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager { - viewManager.getAirPlayState(nodeTag, resolve: resolve, reject: reject) - } else { - reject("NO_VIEW_MANAGER", "KSPlayerViewManager not found", nil) - } - } - } - - @objc func showAirPlayPicker(_ nodeTag: NSNumber?) { - guard let nodeTag = nodeTag else { - print("[KSPlayerModule] showAirPlayPicker called with nil nodeTag") - return - } - print("[KSPlayerModule] showAirPlayPicker called for nodeTag: \(nodeTag)") - DispatchQueue.main.async { - if let viewManager = self.bridge.module(for: KSPlayerViewManager.self) as? KSPlayerViewManager { - print("[KSPlayerModule] Found KSPlayerViewManager, calling showAirPlayPicker") - viewManager.showAirPlayPicker(nodeTag) - } else { - print("[KSPlayerModule] Could not find KSPlayerViewManager") - } - } - } -} diff --git a/ios/KSPlayerView.swift b/ios/KSPlayerView.swift deleted file mode 100644 index 9b8cd45..0000000 --- a/ios/KSPlayerView.swift +++ /dev/null @@ -1,1003 +0,0 @@ -// -// KSPlayerView.swift -// Nuvio -// -// Created by KSPlayer integration -// - -import Foundation -import KSPlayer -import React -import AVKit - -@objc(KSPlayerView) -class KSPlayerView: UIView { - private var playerView: IOSVideoPlayerView! - private var currentSource: NSDictionary? - private var isPaused = false - private var currentVolume: Float = 1.0 - weak var viewManager: KSPlayerViewManager? - - // Event blocks for Fabric - @objc var onLoad: RCTDirectEventBlock? - @objc var onProgress: RCTDirectEventBlock? - @objc var onBuffering: RCTDirectEventBlock? - @objc var onEnd: RCTDirectEventBlock? - @objc var onError: RCTDirectEventBlock? - @objc var onBufferingProgress: RCTDirectEventBlock? - @objc var onExitFullscreen: RCTDirectEventBlock? - - // Property setters that React Native will call - @objc var source: NSDictionary? { - didSet { - if let source = source { - setSource(source) - } - } - } - - @objc var paused: Bool = false { - didSet { - setPaused(paused) - } - } - - @objc var volume: NSNumber = 1.0 { - didSet { - setVolume(volume.floatValue) - } - } - - @objc var rate: NSNumber = 1.0 { - didSet { - setPlaybackRate(rate.floatValue) - } - } - - @objc var audioTrack: NSNumber = -1 { - didSet { - setAudioTrack(audioTrack.intValue) - } - } - - @objc var textTrack: NSNumber = -1 { - didSet { - setTextTrack(textTrack.intValue) - } - } - - // AirPlay properties - @objc var allowsExternalPlayback: Bool = true { - didSet { - setAllowsExternalPlayback(allowsExternalPlayback) - } - } - - @objc var usesExternalPlaybackWhileExternalScreenIsActive: Bool = true { - didSet { - setUsesExternalPlaybackWhileExternalScreenIsActive(usesExternalPlaybackWhileExternalScreenIsActive) - } - } - - // Subtitle customization props removed - using native KSPlayer styling - @objc var subtitleBottomOffset: NSNumber = 60 - @objc var subtitleFontSize: NSNumber = 16 - @objc var subtitleTextColor: NSString = "#FFFFFF" - @objc var subtitleBackgroundColor: NSString = "rgba(0,0,0,0.7)" - - @objc var resizeMode: NSString = "contain" { - didSet { - print("KSPlayerView: [PROP SETTER] resizeMode setter called with value: \(resizeMode)") - applyVideoGravity() - } - } - - override init(frame: CGRect) { - super.init(frame: frame) - setupPlayerView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupPlayerView() - } - - 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([ - playerView.topAnchor.constraint(equalTo: topAnchor), - playerView.leadingAnchor.constraint(equalTo: leadingAnchor), - playerView.trailingAnchor.constraint(equalTo: trailingAnchor), - playerView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - - // Let KSPlayer handle subtitles natively - no custom positioning - // Just set up player delegates and callbacks - setupPlayerCallbacks() - } - - private func applyVideoGravity() { - print("KSPlayerView: [VIDEO GRAVITY] Applying resizeMode: \(resizeMode)") - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - let contentMode: UIViewContentMode - switch self.resizeMode.lowercased { - case "cover": - contentMode = .scaleAspectFill - case "stretch": - contentMode = .scaleToFill - case "contain": - contentMode = .scaleAspectFit - default: - contentMode = .scaleAspectFit - } - - // Set contentMode on the player itself, not the view - self.playerView.playerLayer?.player.contentMode = contentMode - print("KSPlayerView: [VIDEO GRAVITY] Set player contentMode to: \(contentMode)") - } - } - - private func setupPlayerCallbacks() { - // Configure KSOptions - KSOptions.isAutoPlay = false - KSOptions.asynchronousDecompression = true - KSOptions.hardwareDecode = true - - // Set default subtitle font size - use smaller size for mobile - SubtitleModel.textFontSize = 16.0 - SubtitleModel.textBold = false - - print("KSPlayerView: [PERF] Global settings: asyncDecomp=\(KSOptions.asynchronousDecompression), hwDecode=\(KSOptions.hardwareDecode)") - } - - func setSource(_ source: NSDictionary) { - currentSource = source - - guard let uri = source["uri"] as? String else { - print("KSPlayerView: No URI provided") - sendEvent("onError", ["error": "No URI provided in source"]) - return - } - - // Validate URL before proceeding - guard let url = URL(string: uri), url.scheme != nil else { - print("KSPlayerView: Invalid URL format: \(uri)") - sendEvent("onError", ["error": "Invalid URL format: \(uri)"]) - return - } - - var headers: [String: String] = [:] - if let headersDict = source["headers"] as? [String: String] { - headers = headersDict - } - - // Choose player pipeline based on format - let isMKV = uri.lowercased().contains(".mkv") - if isMKV { - // Prefer MEPlayer (FFmpeg) for MKV - KSOptions.firstPlayerType = KSMEPlayer.self - KSOptions.secondPlayerType = nil - } else { - KSOptions.firstPlayerType = KSAVPlayer.self - KSOptions.secondPlayerType = KSMEPlayer.self - } - - // Create KSPlayerResource with validated URL - let resource = KSPlayerResource(url: url, options: createOptions(with: headers), name: "Video") - - print("KSPlayerView: Setting source: \(uri)") - print("KSPlayerView: URL scheme: \(url.scheme ?? "unknown"), host: \(url.host ?? "unknown")") - - playerView.set(resource: resource) - - // Set up delegate after setting the resource - if let playerLayer = playerView.playerLayer { - playerLayer.delegate = self - print("KSPlayerView: Delegate set successfully on playerLayer") - - // Apply video gravity after player is set up - applyVideoGravity() - } else { - print("KSPlayerView: ERROR - playerLayer is nil, cannot set delegate") - } - - // Apply current state - if isPaused { - playerView.pause() - } else { - playerView.play() - } - - setVolume(currentVolume) - - // Ensure AirPlay is properly configured after setting source - DispatchQueue.main.async { - self.setAllowsExternalPlayback(self.allowsExternalPlayback) - self.setUsesExternalPlaybackWhileExternalScreenIsActive(self.usesExternalPlaybackWhileExternalScreenIsActive) - } - } - - private func createOptions(with headers: [String: String]) -> KSOptions { - // Use custom HighPerformanceOptions subclass for frame buffer optimization - let options = HighPerformanceOptions() - // Disable native player remote control center integration; use RN controls - options.registerRemoteControll = false - - // PERFORMANCE OPTIMIZATION: Buffer durations for smooth high bitrate playback - // preferredForwardBufferDuration = 5.0s: Increased to prevent stalling on network hiccups - options.preferredForwardBufferDuration = 5.0 - // maxBufferDuration = 300.0s: Increased to allow 5 minutes of cache ahead - options.maxBufferDuration = 300.0 - - // Enable "second open" to relax startup/seek buffering thresholds (already enabled) - options.isSecondOpen = true - - // PERFORMANCE OPTIMIZATION: Fast stream analysis for high bitrate content - // Reduces startup latency significantly for large high-bitrate streams - options.probesize = 50_000_000 // 50MB for faster format detection - options.maxAnalyzeDuration = 5_000_000 // 5 seconds in microseconds for faster stream structure analysis - - // PERFORMANCE OPTIMIZATION: Decoder thread optimization - // Use all available CPU cores for parallel decoding - options.decoderOptions["threads"] = "0" // Use all CPU cores instead of "auto" - // refcounted_frames already set to "1" in KSOptions init for memory efficiency - - // PERFORMANCE OPTIMIZATION: Hardware decode explicitly enabled - // Ensure VideoToolbox hardware acceleration is always preferred for non-simulator - // Hardware decode and async decompression - options.hardwareDecode = true - options.asynchronousDecompression = true - - // 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 - // This prevents forcing HDR tone mapping on SDR content (which causes oversaturation) - // KSPlayer will automatically detect HDR10/Dolby Vision/HLG from the video format description - options.destinationDynamicRange = nil - - // 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 !headers.isEmpty { - // Clean and validate headers before adding - var cleanHeaders: [String: String] = [:] - for (key, value) in headers { - // Remove any null or empty values - if !value.isEmpty && value != "null" { - cleanHeaders[key] = value - } - } - - if !cleanHeaders.isEmpty { - options.appendHeader(cleanHeaders) - print("KSPlayerView: Added headers: \(cleanHeaders.keys.joined(separator: ", "))") - - if let referer = cleanHeaders["Referer"] ?? cleanHeaders["referer"] { - options.referer = referer - print("KSPlayerView: Set referer: \(referer)") - } - } - } - - print("KSPlayerView: [PERF] High-performance options configured: asyncDecomp=\(options.asynchronousDecompression), hwDecode=\(options.hardwareDecode), buffer=\(options.preferredForwardBufferDuration)s/\(options.maxBufferDuration)s, HDR=\(options.destinationDynamicRange?.description ?? "auto")") - - return options - } - - func setPaused(_ paused: Bool) { - isPaused = paused - if paused { - playerView.pause() - } else { - playerView.play() - } - } - - override var keyCommands: [UIKeyCommand]? { - return [ - UIKeyCommand( - input: UIKeyCommand.inputEscape, - modifierFlags: [], - action: #selector(handleEscapeKey), - discoverabilityTitle: "Exit Fullscreen" - ) - ] - } - - @objc func handleEscapeKey() { - print("KSPlayerView: ESC pressed") - sendEvent("onExitFullscreen", [:]) - } - - func setVolume(_ volume: Float) { - currentVolume = volume - playerView.playerLayer?.player.playbackVolume = volume - } - - func setPlaybackRate(_ rate: Float) { - playerView.playerLayer?.player.playbackRate = rate - print("KSPlayerView: Set playback rate to \(rate)x") - } - - func seek(to time: TimeInterval) { - guard let playerLayer = playerView.playerLayer, - playerLayer.player.isReadyToPlay, - playerLayer.player.seekable else { - print("KSPlayerView: Cannot seek - player not ready or not seekable") - return - } - - // Capture the current paused state before seeking - let wasPaused = isPaused - print("KSPlayerView: Seeking to \(time), paused state before seek: \(wasPaused)") - - playerView.seek(time: time) { [weak self] success in - guard let self = self else { return } - - if success { - print("KSPlayerView: Seek successful to \(time)") - - // Restore the paused state after seeking - // KSPlayer's seek may resume playback, so we need to re-apply the paused state - if wasPaused { - DispatchQueue.main.async { - self.playerView.pause() - print("KSPlayerView: Restored paused state after seek") - } - } - } else { - print("KSPlayerView: Seek failed to \(time)") - } - } - } - - func setAudioTrack(_ trackId: Int) { - if let player = playerView.playerLayer?.player { - let audioTracks = player.tracks(mediaType: .audio) - 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) { - 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) { - NSLog("KSPlayerView: [SET TEXT TRACK] Starting setTextTrack with trackId: %d", trackId) - - // Small delay to ensure player is ready - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - guard let self = self else { - NSLog("KSPlayerView: [SET TEXT TRACK] self is nil, aborting") - return - } - - NSLog("KSPlayerView: [SET TEXT TRACK] Executing track selection") - - if let player = self.playerView.playerLayer?.player { - let textTracks = player.tracks(mediaType: .subtitle) - 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 - 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 - 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 - NSLog("KSPlayerView: Found text track by array index %d (fallback)", trackId) - } - - if let track = selectedTrack { - NSLog("KSPlayerView: Selecting text track %d (index: %d): '%@' (ID: %d)", trackId, trackIndex, track.name, track.trackID) - - // Disable all tracks first - for t in textTracks { - t.isEnabled = false - } - - // Enable the selected track - track.isEnabled = true - - // Use KSPlayer's select method to update player state - player.select(track: track) - - // 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 { - NSLog("KSPlayerView: Warning - track could not be cast to SubtitleInfo") - } - - // Ensure subtitle views are visible - self.playerView.subtitleLabel.isHidden = false - self.playerView.subtitleBackView.isHidden = false - - NSLog("KSPlayerView: Successfully selected and enabled text track %d", trackId) - } else if trackId == -1 { - // Disable all subtitles - for track in textTracks { - track.isEnabled = false - } - self.playerView.srtControl.selectedSubtitleInfo = nil - self.playerView.subtitleLabel.isHidden = true - self.playerView.subtitleBackView.isHidden = true - NSLog("KSPlayerView: Disabled all text tracks") - } else { - NSLog("KSPlayerView: Text track %d not found. Available count: %d", trackId, textTracks.count) - } - } else { - NSLog("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 - // Create a better display name for subtitles - var displayName = track.name - if displayName.isEmpty || displayName == "Unknown" { - if let language = track.language, !language.isEmpty && language != "Unknown" { - displayName = language - } else if let languageCode = track.languageCode, !languageCode.isEmpty { - displayName = languageCode.uppercased() - } else { - displayName = "Subtitle \(index + 1)" - } - } - - // Add language info if not already in the name - if let language = track.language, !language.isEmpty && language != "Unknown" && !displayName.lowercased().contains(language.lowercased()) { - displayName += " (\(language))" - } - - return [ - "id": Int(track.trackID), // Use actual track ID, not array index - "index": index, // Keep index for backward compatibility - "name": displayName, - "language": track.language ?? "Unknown", - "languageCode": track.languageCode ?? "", - "isEnabled": track.isEnabled, - "isImageSubtitle": track.isImageSubtitle - ] - } - - return [ - "audioTracks": audioTracks, - "textTracks": textTracks - ] - } - - // AirPlay methods - func setAllowsExternalPlayback(_ allows: Bool) { - print("[KSPlayerView] Setting allowsExternalPlayback: \(allows)") - playerView.playerLayer?.player.allowsExternalPlayback = allows - } - - func setUsesExternalPlaybackWhileExternalScreenIsActive(_ uses: Bool) { - print("[KSPlayerView] Setting usesExternalPlaybackWhileExternalScreenIsActive: \(uses)") - playerView.playerLayer?.player.usesExternalPlaybackWhileExternalScreenIsActive = uses - } - - func showAirPlayPicker() { - print("[KSPlayerView] showAirPlayPicker called") - - DispatchQueue.main.async { - // Create a temporary route picker view for triggering AirPlay - let routePickerView = AVRoutePickerView() - routePickerView.tintColor = .white - routePickerView.alpha = 0.01 // Nearly invisible but still interactive - - // Find the current view controller - guard let viewController = self.findHostViewController() else { - print("[KSPlayerView] Could not find view controller for AirPlay picker") - return - } - - // Add to the view controller's view temporarily - viewController.view.addSubview(routePickerView) - - // Position it off-screen but still in the view hierarchy - routePickerView.frame = CGRect(x: -100, y: -100, width: 44, height: 44) - - // Force layout - viewController.view.setNeedsLayout() - viewController.view.layoutIfNeeded() - - // Wait a bit for the view to be ready, then trigger - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // Find and trigger the AirPlay button - self.triggerAirPlayButton(routePickerView) - - // Clean up after a delay - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - routePickerView.removeFromSuperview() - print("[KSPlayerView] Cleaned up temporary AirPlay picker") - } - } - } - } - - private func triggerAirPlayButton(_ routePickerView: AVRoutePickerView) { - // Recursively find the button in the route picker view - func findButton(in view: UIView) -> UIButton? { - if let button = view as? UIButton { - return button - } - for subview in view.subviews { - if let button = findButton(in: subview) { - return button - } - } - return nil - } - - if let button = findButton(in: routePickerView) { - print("[KSPlayerView] Found AirPlay button, triggering tap") - button.sendActions(for: .touchUpInside) - } else { - print("[KSPlayerView] Could not find AirPlay button in route picker") - } - } - - func getAirPlayState() -> [String: Any] { - guard let player = playerView.playerLayer?.player else { - return [ - "allowsExternalPlayback": false, - "usesExternalPlaybackWhileExternalScreenIsActive": false, - "isExternalPlaybackActive": false - ] - } - - return [ - "allowsExternalPlayback": player.allowsExternalPlayback, - "usesExternalPlaybackWhileExternalScreenIsActive": player.usesExternalPlaybackWhileExternalScreenIsActive, - "isExternalPlaybackActive": player.isExternalPlaybackActive - ] - } - - // Get current player state for React Native - func getCurrentState() -> [String: Any] { - guard let player = playerView.playerLayer?.player else { - return [:] - } - - return [ - "currentTime": player.currentPlaybackTime, - "duration": player.duration, - "buffered": player.playableTime, - "isPlaying": !isPaused, - "volume": currentVolume - ] - } - - // MARK: - Performance Optimization Helpers -} - -// MARK: - High Performance KSOptions Subclass - -/// Custom KSOptions subclass that overrides frame buffer capacity for high bitrate content -/// More buffered frames absorb decode spikes and network hiccups without quality loss -private class HighPerformanceOptions: KSOptions { - /// Override to increase frame buffer capacity for high bitrate content - /// - Parameters: - /// - fps: Video frame rate - /// - naturalSize: Video resolution - /// - isLive: Whether this is a live stream - /// - Returns: Number of frames to buffer - override func videoFrameMaxCount(fps: Float, naturalSize: CGSize, isLive: Bool) -> UInt8 { - if isLive { - // Increased from 4 to 8 for better live stream stability - return 8 - } - - // For high bitrate VOD: increase buffer based on resolution - if naturalSize.width >= 3840 || naturalSize.height >= 2160 { - // 4K needs more buffer frames to handle decode spikes - return 32 - } else if naturalSize.width >= 1920 || naturalSize.height >= 1080 { - // 1080p benefits from more frames - return 24 - } - - // Default for lower resolutions - return 16 - } -} - -extension KSPlayerView: KSPlayerLayerDelegate { - func player(layer: KSPlayerLayer, state: KSPlayerState) { - switch state { - case .readyToPlay: - // Ensure AirPlay is properly configured when player is ready - layer.player.allowsExternalPlayback = allowsExternalPlayback - layer.player.usesExternalPlaybackWhileExternalScreenIsActive = usesExternalPlaybackWhileExternalScreenIsActive - - // Debug: Check subtitle data source connection - let hasSubtitleDataSource = layer.player.subtitleDataSouce != nil - print("KSPlayerView: [READY TO PLAY] subtitle data source available: \(hasSubtitleDataSource)") - - // Keep subtitle views hidden until actual content is displayed - // They will be shown in the subtitle rendering callback when there's text to display - playerView.subtitleLabel.isHidden = true - playerView.subtitleBackView.isHidden = true - print("KSPlayerView: [READY TO PLAY] Subtitle views kept hidden until content available") - - // Manually connect subtitle data source to srtControl (this is the missing piece!) - if let subtitleDataSouce = layer.player.subtitleDataSouce { - print("KSPlayerView: [READY TO PLAY] Connecting subtitle data source to srtControl") - print("KSPlayerView: [READY TO PLAY] subtitleDataSouce type: \(type(of: subtitleDataSouce))") - - // Check if subtitle data source has any subtitle infos - print("KSPlayerView: [READY TO PLAY] subtitleDataSouce has \(subtitleDataSouce.infos.count) subtitle infos") - - for (index, info) in subtitleDataSouce.infos.enumerated() { - print("KSPlayerView: [READY TO PLAY] subtitleDataSouce info[\(index)]: ID=\(info.subtitleID), Name='\(info.name)', Enabled=\(info.isEnabled)") - } - // Wait 1 second like the original KSPlayer code does - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { [weak self] in - guard let self = self else { return } - print("KSPlayerView: [READY TO PLAY] About to add subtitle data source to srtControl") - self.playerView.srtControl.addSubtitle(dataSouce: subtitleDataSouce) - print("KSPlayerView: [READY TO PLAY] Subtitle data source connected to srtControl") - print("KSPlayerView: [READY TO PLAY] srtControl.subtitleInfos.count: \(self.playerView.srtControl.subtitleInfos.count)") - - // Log all subtitle infos - for (index, info) in self.playerView.srtControl.subtitleInfos.enumerated() { - print("KSPlayerView: [READY TO PLAY] SubtitleInfo[\(index)]: name=\(info.name), isEnabled=\(info.isEnabled), subtitleID=\(info.subtitleID)") - } - - // Try to manually trigger subtitle parsing for the current time - let currentTime = self.playerView.playerLayer?.player.currentPlaybackTime ?? 0 - print("KSPlayerView: [READY TO PLAY] Current playback time: \(currentTime)") - - // Force subtitle search for current time - let hasSubtitle = self.playerView.srtControl.subtitle(currentTime: currentTime) - print("KSPlayerView: [READY TO PLAY] Manual subtitle search result: \(hasSubtitle)") - print("KSPlayerView: [READY TO PLAY] Parts count after manual search: \(self.playerView.srtControl.parts.count)") - - if let firstPart = self.playerView.srtControl.parts.first { - print("KSPlayerView: [READY TO PLAY] Found subtitle part: start=\(firstPart.start), end=\(firstPart.end), text='\(firstPart.text?.string ?? "nil")'") - } - - // Only auto-select first enabled subtitle if textTrack prop is NOT set to -1 (disabled) - // If React Native explicitly set textTrack=-1, user wants subtitles off - if self.textTrack.intValue != -1 { - // Auto-select first enabled subtitle if none selected - if self.playerView.srtControl.selectedSubtitleInfo == nil { - self.playerView.srtControl.selectedSubtitleInfo = self.playerView.srtControl.subtitleInfos.first { $0.isEnabled } - if let selected = self.playerView.srtControl.selectedSubtitleInfo { - print("KSPlayerView: [READY TO PLAY] Auto-selected subtitle: \(selected.name)") - } else { - print("KSPlayerView: [READY TO PLAY] No enabled subtitle found for auto-selection") - } - } else { - print("KSPlayerView: [READY TO PLAY] Subtitle already selected: \(self.playerView.srtControl.selectedSubtitleInfo?.name ?? "unknown")") - } - } else { - print("KSPlayerView: [READY TO PLAY] textTrack=-1 (disabled), skipping auto-selection") - // Ensure subtitles are disabled - self.playerView.srtControl.selectedSubtitleInfo = nil - self.playerView.subtitleLabel.isHidden = true - self.playerView.subtitleBackView.isHidden = true - } - } - } else { - print("KSPlayerView: [READY TO PLAY] ERROR: No subtitle data source available") - } - - // Determine player backend type from actual player instance - let playerBackend: String - if let _ = layer.player as? KSMEPlayer { - playerBackend = "KSMEPlayer" - } else { - playerBackend = "KSAVPlayer" - } - - // 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"] ?? [], - "playerBackend": playerBackend - ]) - case .buffering: - sendEvent("onBuffering", ["isBuffering": true]) - case .bufferFinished: - sendEvent("onBuffering", ["isBuffering": false]) - case .playedToTheEnd: - sendEvent("onEnd", [:]) - case .error: - // Error will be handled by the finish delegate method - break - default: - break - } - } - - func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) { - // Debug: Confirm delegate method is being called - if currentTime.truncatingRemainder(dividingBy: 10.0) < 0.1 { - print("KSPlayerView: [DELEGATE CALLED] time=\(currentTime), total=\(totalTime)") - } - - // Manually implement subtitle rendering logic from VideoPlayerView - // This is the critical missing piece that was preventing subtitle rendering - - // Debug: Check srtControl state - let subtitleInfoCount = playerView.srtControl.subtitleInfos.count - let selectedSubtitle = playerView.srtControl.selectedSubtitleInfo - - // Always log subtitle state every 10 seconds to see when it gets populated - if currentTime.truncatingRemainder(dividingBy: 10.0) < 0.1 { - print("KSPlayerView: [SUBTITLE DEBUG] time=\(currentTime.truncatingRemainder(dividingBy: 10.0)), subtitleInfos=\(subtitleInfoCount), selected=\(selectedSubtitle?.name ?? "none")") - - // Also check if player has subtitle data source - let player = layer.player - let hasSubtitleDataSource = player.subtitleDataSouce != nil - print("KSPlayerView: [SUBTITLE DEBUG] player has subtitle data source: \(hasSubtitleDataSource)") - - // Log subtitle view states - print("KSPlayerView: [SUBTITLE DEBUG] subtitleLabel.isHidden: \(playerView.subtitleLabel.isHidden)") - print("KSPlayerView: [SUBTITLE DEBUG] subtitleBackView.isHidden: \(playerView.subtitleBackView.isHidden)") - print("KSPlayerView: [SUBTITLE DEBUG] subtitleLabel.text: '\(playerView.subtitleLabel.text ?? "nil")'") - print("KSPlayerView: [SUBTITLE DEBUG] subtitleLabel.attributedText: \(playerView.subtitleLabel.attributedText != nil ? "exists" : "nil")") - print("KSPlayerView: [SUBTITLE DEBUG] subtitleBackView.image: \(playerView.subtitleBackView.image != nil ? "exists" : "nil")") - - // Log all subtitle infos - for (index, info) in playerView.srtControl.subtitleInfos.enumerated() { - print("KSPlayerView: [SUBTITLE DEBUG] SubtitleInfo[\(index)]: name=\(info.name), isEnabled=\(info.isEnabled)") - } - } - - let hasSubtitleParts = playerView.srtControl.subtitle(currentTime: currentTime) - - // Debug: Check subtitle timing every 10 seconds - if currentTime.truncatingRemainder(dividingBy: 10.0) < 0.1 && subtitleInfoCount > 0 { - print("KSPlayerView: [SUBTITLE TIMING] time=\(currentTime), hasParts=\(hasSubtitleParts), partsCount=\(playerView.srtControl.parts.count)") - if let firstPart = playerView.srtControl.parts.first { - print("KSPlayerView: [SUBTITLE TIMING] firstPart start=\(firstPart.start), end=\(firstPart.end)") - print("KSPlayerView: [SUBTITLE TIMING] firstPart text='\(firstPart.text?.string ?? "nil")'") - print("KSPlayerView: [SUBTITLE TIMING] firstPart hasImage=\(firstPart.image != nil)") - } else { - print("KSPlayerView: [SUBTITLE TIMING] No parts available") - } - } - - if hasSubtitleParts { - 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 - - // 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") - print("KSPlayerView: [SUBTITLE RENDER] subtitleLabel.isHidden after: \(playerView.subtitleLabel.isHidden)") - print("KSPlayerView: [SUBTITLE RENDER] subtitleBackView.isHidden after: \(playerView.subtitleBackView.isHidden)") - } else { - print("KSPlayerView: [SUBTITLE RENDER] hasParts=true but no parts available - hiding views") - playerView.subtitleBackView.image = nil - playerView.subtitleLabel.attributedText = nil - playerView.subtitleBackView.isHidden = true - playerView.subtitleLabel.isHidden = true - } - } else { - // Only log this occasionally to avoid spam - if currentTime.truncatingRemainder(dividingBy: 30.0) < 0.1 { - print("KSPlayerView: [SUBTITLE RENDER] time=\(currentTime), hasParts=false - no subtitle at this time") - } - } - - let p = layer.player - // Ensure we have valid duration before sending progress updates - if totalTime > 0 { - sendEvent("onProgress", [ - "currentTime": currentTime, - "duration": totalTime, - "bufferTime": p.playableTime, - "airPlayState": getAirPlayState() - ]) - } - } - - func player(layer: KSPlayerLayer, finish error: Error?) { - if let error = error { - let errorMessage = error.localizedDescription - print("KSPlayerView: Player finished with error: \(errorMessage)") - - // Provide more specific error messages for common issues - var detailedError = errorMessage - if errorMessage.contains("avformat can't open input") { - detailedError = "Unable to open video stream. This could be due to:\n• Invalid or malformed URL\n• Network connectivity issues\n• Server blocking the request\n• Unsupported video format\n• Missing required headers" - } else if errorMessage.contains("timeout") { - detailedError = "Stream connection timed out. The server may be slow or unreachable." - } else if errorMessage.contains("404") || errorMessage.contains("Not Found") { - detailedError = "Video stream not found. The URL may be expired or incorrect." - } else if errorMessage.contains("403") || errorMessage.contains("Forbidden") { - detailedError = "Access denied. The server may be blocking requests or require authentication." - } - - sendEvent("onError", ["error": detailedError]) - } - } - - func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval) { - // Handle buffering progress if needed - sendEvent("onBufferingProgress", [ - "bufferedCount": bufferedCount, - "consumeTime": consumeTime - ]) - } -} - -extension KSPlayerView { - private func sendEvent(_ eventName: String, _ body: [String: Any]) { - DispatchQueue.main.async { - switch eventName { - case "onLoad": - self.onLoad?(body) - case "onProgress": - self.onProgress?(body) - case "onBuffering": - self.onBuffering?(body) - case "onEnd": - self.onEnd?([:]) - case "onError": - self.onError?(body) - case "onBufferingProgress": - self.onBufferingProgress?(body) - default: - break - } - } - } - // Renamed to avoid clashing with React's UIView category method - private func findHostViewController() -> UIViewController? { - var responder: UIResponder? = self - while let nextResponder = responder?.next { - if let viewController = nextResponder as? UIViewController { - return viewController - } - responder = nextResponder - } - return nil - } -} - -extension IOSVideoPlayerView { - @objc func handleEscapeKey() { - self.next?.perform(#selector(handleEscapeKey)) - } -} diff --git a/ios/KSPlayerViewManager.swift b/ios/KSPlayerViewManager.swift deleted file mode 100644 index 4017fb3..0000000 --- a/ios/KSPlayerViewManager.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// KSPlayerViewManager.swift -// Nuvio -// -// Created by KSPlayer integration -// - -import Foundation -import KSPlayer -import React - -@objc(KSPlayerViewManager) -class KSPlayerViewManager: RCTViewManager { - - // Not needed for RCTViewManager-based views; events are exported via Objective-C externs in KSPlayerManager.m - override func view() -> UIView! { - let view = KSPlayerView() - view.viewManager = self - return view - } - - override static func requiresMainQueueSetup() -> Bool { - return true - } - - override func constantsToExport() -> [AnyHashable : Any]! { - return [ - "EventTypes": [ - "onLoad": "onLoad", - "onProgress": "onProgress", - "onBuffering": "onBuffering", - "onEnd": "onEnd", - "onError": "onError", - "onBufferingProgress": "onBufferingProgress" - ] - ] - } - - // No-op: events are sent via direct event blocks on the view - - @objc func seek(_ node: NSNumber, toTime time: NSNumber) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.seek(to: TimeInterval(truncating: time)) - } - } - } - - @objc func setSource(_ node: NSNumber, source: NSDictionary) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setSource(source) - } - } - } - - @objc func setPaused(_ node: NSNumber, paused: Bool) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setPaused(paused) - } - } - } - - @objc func setVolume(_ node: NSNumber, volume: NSNumber) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setVolume(Float(truncating: volume)) - } - } - } - - @objc func setPlaybackRate(_ node: NSNumber, rate: NSNumber) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setPlaybackRate(Float(truncating: rate)) - } - } - } - - @objc func setAudioTrack(_ node: NSNumber, trackId: NSNumber) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setAudioTrack(Int(truncating: trackId)) - } - } - } - - @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) - } - } - } - - @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) - } - } - } - - // AirPlay methods - @objc func setAllowsExternalPlayback(_ node: NSNumber, allows: Bool) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setAllowsExternalPlayback(allows) - } - } - } - - @objc func setUsesExternalPlaybackWhileExternalScreenIsActive(_ node: NSNumber, uses: Bool) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - view.setUsesExternalPlaybackWhileExternalScreenIsActive(uses) - } - } - } - - @objc func getAirPlayState(_ node: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - let airPlayState = view.getAirPlayState() - resolve(airPlayState) - } else { - reject("NO_VIEW", "KSPlayerView not found", nil) - } - } - } - - @objc func showAirPlayPicker(_ node: NSNumber) { - print("[KSPlayerViewManager] showAirPlayPicker called for node: \(node)") - DispatchQueue.main.async { - if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView { - print("[KSPlayerViewManager] Found KSPlayerView, calling showAirPlayPicker") - view.showAirPlayPicker() - } else { - print("[KSPlayerViewManager] Could not find KSPlayerView for node: \(node)") - } - } - } -} \ No newline at end of file diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 96f27dd..f1f6d3c 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -11,12 +11,12 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 2AA769395C1242F225F875AF /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; - 72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */; }; 9FBA88F42E86ECD700892850 /* KSPlayerViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */; }; 9FBA88F52E86ECD700892850 /* KSPlayerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */; }; 9FBA88F62E86ECD700892850 /* KSPlayerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */; }; 9FBA88F72E86ECD700892850 /* KSPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + D66ACCC72CB69F1FF14A2585 /* libPods-Nuvio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 436A6FCA2C83F29076E121BA /* libPods-Nuvio.a */; }; F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -24,16 +24,16 @@ 13B07F961A680F5B00A75B9A /* Nuvio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Nuvio.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Nuvio/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Nuvio/Info.plist; sourceTree = ""; }; - 15864A7148A4384BAA9F0B37 /* 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 = ""; }; - 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 436A6FCA2C83F29076E121BA /* libPods-Nuvio.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Nuvio.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 49055D6E250FAFA21141FE49 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Nuvio/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 5346BAA9EF8C9C8182D4485C /* 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 = ""; }; 6E007C0BAC8C453623E81663 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift"; sourceTree = ""; }; 73BB213C2E9EEAC700EC03F8 /* NuvioRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NuvioRelease.entitlements; path = Nuvio/NuvioRelease.entitlements; sourceTree = ""; }; - 819F6DCD44DFE0C72440FDCF /* 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 = ""; }; - 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KSPlayerManager.m; sourceTree = ""; }; - 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerModule.swift; sourceTree = ""; }; - 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerView.swift; sourceTree = ""; }; - 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KSPlayerViewManager.swift; sourceTree = ""; }; + 904B4A0A0308D3727268BA5E /* 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 = ""; }; + 9FBA88F02E86ECD700892850 /* KSPlayerManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ../KSPlayer/RNBridge/KSPlayerManager.m; sourceTree = ""; }; + 9FBA88F12E86ECD700892850 /* KSPlayerModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerModule.swift; sourceTree = ""; }; + 9FBA88F22E86ECD700892850 /* KSPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerView.swift; sourceTree = ""; }; + 9FBA88F32E86ECD700892850 /* KSPlayerViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../KSPlayer/RNBridge/KSPlayerViewManager.swift; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Nuvio/SplashScreen.storyboard; sourceTree = ""; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -46,7 +46,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 72D4090694139E0DAD9B066E /* libPods-Nuvio.a in Frameworks */, + D66ACCC72CB69F1FF14A2585 /* libPods-Nuvio.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,7 +76,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - 406CD0CEF46F8EAEEAD92BF7 /* libPods-Nuvio.a */, + 436A6FCA2C83F29076E121BA /* libPods-Nuvio.a */, ); name = Frameworks; sourceTree = ""; @@ -131,8 +131,8 @@ D90A3959C97EE9926C513293 /* Pods */ = { isa = PBXGroup; children = ( - 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */, - 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */, + 904B4A0A0308D3727268BA5E /* Pods-Nuvio.debug.xcconfig */, + 5346BAA9EF8C9C8182D4485C /* Pods-Nuvio.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -152,15 +152,15 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Nuvio" */; buildPhases = ( - E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */, + 3B2D9C1D63379C2F30AC0F2B /* [CP] Check Pods Manifest.lock */, 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */, - B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */, - AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */, + 9F740EE07B5F97C85979C145 /* [CP] Embed Pods Frameworks */, + 550CD54859274FE505BA4957 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -234,45 +234,29 @@ shellPath = /bin/sh; shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n/bin/sh `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"` `\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; }; - 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = { + 3B2D9C1D63379C2F30AC0F2B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( - "$(SRCROOT)/.xcode.env", - "$(SRCROOT)/.xcode.env.local", - "$(SRCROOT)/Nuvio/Nuvio.entitlements", - "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[Expo] Configure project"; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( - "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift", + "$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Upload Debug Symbols to Sentry"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; - }; - AA47AE6072D35F0490B53926 /* [CP] Copy Pods Resources */ = { + 550CD54859274FE505BA4957 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -384,7 +368,45 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-resources.sh\"\n"; showEnvVarsInLog = 0; }; - B84E40F81DF927F34032D68B /* [CP] Embed Pods Frameworks */ = { + 99A79B70155E84EE1FB7F466 /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env", + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/Nuvio/Nuvio.entitlements", + "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/expo-configure-project.sh", + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Pods/Target Support Files/Pods-Nuvio/ExpoModulesProvider.swift", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Nuvio/expo-configure-project.sh\"\n"; + }; + 9B977D89FE30470F8C59964C /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + }; + 9F740EE07B5F97C85979C145 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -406,28 +428,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Nuvio/Pods-Nuvio-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - E060D0359630A7C0F4793812 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Nuvio-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -449,7 +449,7 @@ /* Begin XCBuildConfiguration section */ 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 819F6DCD44DFE0C72440FDCF /* Pods-Nuvio.debug.xcconfig */; + baseConfigurationReference = 904B4A0A0308D3727268BA5E /* Pods-Nuvio.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -487,7 +487,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 15864A7148A4384BAA9F0B37 /* Pods-Nuvio.release.xcconfig */; + baseConfigurationReference = 5346BAA9EF8C9C8182D4485C /* Pods-Nuvio.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/ios/Podfile b/ios/Podfile index 7c61ff4..e3032d0 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -50,8 +50,9 @@ target 'Nuvio' do ) # KSPlayer dependencies - pod 'KSPlayer', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main' - pod 'DisplayCriteria', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main', :modular_headers => true + # Use the local checkout so we can patch subtitle rendering (and other behaviors) without forking. + pod 'KSPlayer', :path => '../KSPlayer' + pod 'DisplayCriteria', :path => '../KSPlayer', :modular_headers => true pod 'FFmpegKit', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main', :modular_headers => true pod 'Libass', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index df4e4db..6b8ed68 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2760,7 +2760,7 @@ PODS: - Yoga (0.0.0) DEPENDENCIES: - - DisplayCriteria (from `https://github.com/kingslay/KSPlayer.git`, branch `main`) + - DisplayCriteria (from `../KSPlayer`) - EASClient (from `../node_modules/expo-eas-client/ios`) - EXApplication (from `../node_modules/expo-application/ios`) - EXConstants (from `../node_modules/expo-constants/ios`) @@ -2800,7 +2800,7 @@ DEPENDENCIES: - FFmpegKit (from `https://github.com/kingslay/FFmpegKit.git`, branch `main`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - ImageColors (from `../node_modules/react-native-image-colors/ios`) - - KSPlayer (from `https://github.com/kingslay/KSPlayer.git`, branch `main`) + - KSPlayer (from `../KSPlayer`) - Libass (from `https://github.com/kingslay/FFmpegKit.git`, branch `main`) - lottie-react-native (from `../node_modules/lottie-react-native`) - NitroMmkv (from `../node_modules/react-native-mmkv`) @@ -2910,8 +2910,7 @@ SPEC REPOS: EXTERNAL SOURCES: DisplayCriteria: - :branch: main - :git: https://github.com/kingslay/KSPlayer.git + :path: "../KSPlayer" EASClient: :path: "../node_modules/expo-eas-client/ios" EXApplication: @@ -2993,8 +2992,7 @@ EXTERNAL SOURCES: ImageColors: :path: "../node_modules/react-native-image-colors/ios" KSPlayer: - :branch: main - :git: https://github.com/kingslay/KSPlayer.git + :path: "../KSPlayer" Libass: :branch: main :git: https://github.com/kingslay/FFmpegKit.git @@ -3174,15 +3172,9 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" CHECKOUT OPTIONS: - DisplayCriteria: - :commit: a7cddd878f557afa6a1f2faad9d756949406adde - :git: https://github.com/kingslay/KSPlayer.git FFmpegKit: :commit: d7048037a2eb94a3b08113fbf43aa92bdcb332d9 :git: https://github.com/kingslay/FFmpegKit.git - KSPlayer: - :commit: a7cddd878f557afa6a1f2faad9d756949406adde - :git: https://github.com/kingslay/KSPlayer.git Libass: :commit: d7048037a2eb94a3b08113fbf43aa92bdcb332d9 :git: https://github.com/kingslay/FFmpegKit.git @@ -3332,6 +3324,6 @@ SPEC CHECKSUMS: SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d Yoga: 051f086b5ccf465ff2ed38a2cf5a558ae01aaaa1 -PODFILE CHECKSUM: 7c74c9cd2c7f3df7ab68b4284d9f324282e54542 +PODFILE CHECKSUM: b884d1ff07ac4a43323bce2e2e1342592513858c COCOAPODS: 1.16.2 diff --git a/src/components/player/KSPlayerComponent.tsx b/src/components/player/KSPlayerComponent.tsx index cecefda..d370f5e 100644 --- a/src/components/player/KSPlayerComponent.tsx +++ b/src/components/player/KSPlayerComponent.tsx @@ -19,6 +19,7 @@ interface KSPlayerViewProps { subtitleFontSize?: number; subtitleTextColor?: string; subtitleBackgroundColor?: string; + subtitleOutlineEnabled?: boolean; resizeMode?: 'contain' | 'cover' | 'stretch'; onLoad?: (data: any) => void; onProgress?: (data: any) => void; @@ -60,6 +61,7 @@ export interface KSPlayerProps { subtitleFontSize?: number; subtitleTextColor?: string; subtitleBackgroundColor?: string; + subtitleOutlineEnabled?: boolean; resizeMode?: 'contain' | 'cover' | 'stretch'; onLoad?: (data: any) => void; onProgress?: (data: any) => void; @@ -210,6 +212,7 @@ const KSPlayer = forwardRef((props, ref) => { subtitleFontSize={props.subtitleFontSize} subtitleTextColor={props.subtitleTextColor} subtitleBackgroundColor={props.subtitleBackgroundColor} + subtitleOutlineEnabled={props.subtitleOutlineEnabled} resizeMode={props.resizeMode} onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)} onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)} diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index d2f1f78..a5d96ce 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -616,6 +616,10 @@ const KSPlayerCore: React.FC = () => { /> {/* Video Surface & Pinch Zoom */} + {/* + For KSPlayer built-in subtitles (internal text tracks), we intentionally force background OFF. + Background styling is only supported/used for custom (external/addon) subtitles overlay. + */} { screenHeight={screenDimensions.height} customVideoStyles={{ width: '100%', height: '100%' }} subtitleTextColor={customSubs.subtitleTextColor} - subtitleBackgroundColor={customSubs.subtitleBackground ? `rgba(0,0,0,${customSubs.subtitleBgOpacity})` : 'transparent'} + subtitleBackgroundColor={ + tracks.selectedTextTrack !== null && + tracks.selectedTextTrack >= 0 && + !customSubs.useCustomSubtitles + ? 'rgba(0,0,0,0)' + : (customSubs.subtitleBackground ? `rgba(0,0,0,${customSubs.subtitleBgOpacity})` : 'transparent') + } + subtitleOutlineEnabled={ + tracks.selectedTextTrack !== null && + tracks.selectedTextTrack >= 0 && + !customSubs.useCustomSubtitles + ? customSubs.subtitleOutline + : false + } subtitleFontSize={customSubs.subtitleSize} subtitleBottomOffset={customSubs.subtitleBottomOffset} /> diff --git a/src/components/player/ios/components/KSPlayerSurface.tsx b/src/components/player/ios/components/KSPlayerSurface.tsx index 52bf7f9..ffe0d68 100644 --- a/src/components/player/ios/components/KSPlayerSurface.tsx +++ b/src/components/player/ios/components/KSPlayerSurface.tsx @@ -42,6 +42,7 @@ interface KSPlayerSurfaceProps { subtitleBackgroundColor?: string; subtitleFontSize?: number; subtitleBottomOffset?: number; + subtitleOutlineEnabled?: boolean; } export const KSPlayerSurface: React.FC = ({ @@ -74,7 +75,8 @@ export const KSPlayerSurface: React.FC = ({ subtitleTextColor, subtitleBackgroundColor, subtitleFontSize, - subtitleBottomOffset + subtitleBottomOffset, + subtitleOutlineEnabled }) => { const pinchRef = useRef(null); @@ -146,6 +148,7 @@ export const KSPlayerSurface: React.FC = ({ subtitleBackgroundColor={subtitleBackgroundColor} subtitleFontSize={subtitleFontSize} subtitleBottomOffset={subtitleBottomOffset} + subtitleOutlineEnabled={subtitleOutlineEnabled} onLoad={handleLoad} onProgress={onProgress} onBuffering={handleBuffering} diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index b3d1936..ef03a6d 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -239,7 +239,8 @@ export const SubtitleModals: React.FC = ({ = ({ - {/* Show Background */} - - - - {t('player_ui.show_background')} + {/* Background is only for CustomSubtitles (external/addon). Built-in subtitles force background OFF. */} + {!isUsingInternalSubtitle && ( + + + + {t('player_ui.show_background')} + + + + - - - - + )} {/* Advanced controls */} @@ -392,21 +395,23 @@ export const SubtitleModals: React.FC = ({ - {/* Background Opacity */} - - {t('player_ui.background_opacity')} - - setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - {subtitleBgOpacity.toFixed(1)} + {/* Background Opacity (CustomSubtitles only) */} + {!isUsingInternalSubtitle && ( + + {t('player_ui.background_opacity')} + + setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleBgOpacity.toFixed(1)} + + setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + - setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - + )} {!isUsingInternalSubtitle && ( {t('player_ui.text_shadow')} @@ -416,28 +421,43 @@ export const SubtitleModals: React.FC = ({ )} {/* Outline controls (now supported for ExoPlayer internal via native patch) */} - - {t('player_ui.outline_color')} - - {['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => ( - setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> - ))} - - - - {t('player_ui.outline_width')} - - setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - + {isUsingInternalSubtitle ? ( + // KSPlayer built-in subtitles: only expose an Outline on/off toggle (no width control). + + {t('player_ui.outline')} + setSubtitleOutline(!subtitleOutline)} + style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleOutline ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }} + > + {subtitleOutline ? t('player_ui.on') : t('player_ui.off')} - - {subtitleOutlineWidth} + + ) : ( + <> + + {t('player_ui.outline_color')} + + {['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => ( + setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> + ))} + - setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> - - - - + + {t('player_ui.outline_width')} + + setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + {subtitleOutlineWidth} + + setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> + + + + + + )} {!isUsingInternalSubtitle && ( diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index a42c202..8c9854f 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -410,6 +410,7 @@ "on": "تشغيل", "off": "إيقاف", "outline_color": "لون الإطار", + "outline": "الإطار", "outline_width": "عرض الإطار", "letter_spacing": "تباعد الأحرف", "line_height": "ارتفاع السطر", diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 374446e..3e093cb 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -410,6 +410,7 @@ "on": "Ein", "off": "Aus", "outline_color": "Umrandungsfarbe", + "outline": "Umrandung", "outline_width": "Umrandungsbreite", "letter_spacing": "Zeichenabstand", "line_height": "Zeilenhöhe", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 4bca439..391e15f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -410,6 +410,7 @@ "on": "On", "off": "Off", "outline_color": "Outline Color", + "outline": "Outline", "outline_width": "Outline Width", "letter_spacing": "Letter Spacing", "line_height": "Line Height", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 2325eb3..af91073 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -410,6 +410,7 @@ "on": "Sí", "off": "No", "outline_color": "Color de contorno", + "outline": "Contorno", "outline_width": "Ancho de contorno", "letter_spacing": "Espaciado de letras", "line_height": "Altura de línea", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 90177c8..93b9415 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -410,6 +410,7 @@ "on": "Activé", "off": "Désactivé", "outline_color": "Couleur du contour", + "outline": "Contour", "outline_width": "Largeur du contour", "letter_spacing": "Espacement des lettres", "line_height": "Hauteur de ligne", diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 42bc7b0..cc6e605 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -410,6 +410,7 @@ "on": "Attivo", "off": "Disattivo", "outline_color": "Colore contorno", + "outline": "Contorno", "outline_width": "Larghezza contorno", "letter_spacing": "Spaziatura lettere", "line_height": "Altezza riga", diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json index 2c3f5bd..24fac0b 100644 --- a/src/i18n/locales/pt-BR.json +++ b/src/i18n/locales/pt-BR.json @@ -410,6 +410,7 @@ "on": "Ligado", "off": "Desligado", "outline_color": "Cor do Contorno", + "outline": "Contorno", "outline_width": "Largura do Contorno", "letter_spacing": "Espaçamento de Letras", "line_height": "Altura da Linha", diff --git a/src/i18n/locales/pt-PT.json b/src/i18n/locales/pt-PT.json index a40280e..f226769 100644 --- a/src/i18n/locales/pt-PT.json +++ b/src/i18n/locales/pt-PT.json @@ -410,6 +410,7 @@ "on": "Ligado", "off": "Desligado", "outline_color": "Cor do Contorno", + "outline": "Contorno", "outline_width": "Largura do Contorno", "letter_spacing": "Espaçamento de Letras", "line_height": "Altura da Linha",