diff --git a/ios/KSPlayerManager.m b/ios/KSPlayerManager.m index 860dc360..d61dc406 100644 --- a/ios/KSPlayerManager.m +++ b/ios/KSPlayerManager.m @@ -20,6 +20,7 @@ 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(resizeMode, NSString) // Event properties RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) diff --git a/ios/KSPlayerView.swift b/ios/KSPlayerView.swift index 754ca03d..fd312693 100644 --- a/ios/KSPlayerView.swift +++ b/ios/KSPlayerView.swift @@ -91,6 +91,13 @@ class KSPlayerView: UIView { updateSubtitleFont(size: size) } } + + @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) @@ -128,7 +135,10 @@ class KSPlayerView: UIView { // KSPlayer's subtitleLabel renders internal subtitles playerView.subtitleLabel.isHidden = false playerView.subtitleBackView.isHidden = false - playerView.bringSubviewToFront(playerView.subtitleBackView) + // Move subtitle view to main container for independence from video transformations + playerView.subtitleBackView.removeFromSuperview() + self.addSubview(playerView.subtitleBackView) + self.bringSubviewToFront(playerView.subtitleBackView) print("KSPlayerView: [SETUP] Subtitle views made visible") print("KSPlayerView: [SETUP] subtitleLabel.isHidden: \(playerView.subtitleLabel.isHidden)") print("KSPlayerView: [SETUP] subtitleBackView.isHidden: \(playerView.subtitleBackView.isHidden)") @@ -149,7 +159,10 @@ class KSPlayerView: UIView { private func adjustSubtitlePositioning() { // Remove existing constraints for subtitle positioning playerView.subtitleBackView.removeFromSuperview() - playerView.addSubview(playerView.subtitleBackView) + // Add subtitle view to main container (self) instead of playerView to make it independent of video transformations + self.addSubview(playerView.subtitleBackView) + // Ensure subtitles are always on top of video + self.bringSubviewToFront(playerView.subtitleBackView) // Re-add subtitle label to subtitle back view playerView.subtitleBackView.addSubview(playerView.subtitleLabel) @@ -159,13 +172,14 @@ class KSPlayerView: UIView { playerView.subtitleLabel.translatesAutoresizingMaskIntoConstraints = false // Store the bottom constraint reference for dynamic updates - subtitleBottomConstraint = playerView.subtitleBackView.bottomAnchor.constraint(equalTo: playerView.bottomAnchor, constant: -CGFloat(subtitleBottomOffset.floatValue)) + // Constrain to main container (self) instead of playerView to make subtitles independent of video transformations + subtitleBottomConstraint = playerView.subtitleBackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -CGFloat(subtitleBottomOffset.floatValue)) NSLayoutConstraint.activate([ // Position subtitles using dynamic offset from React Native subtitleBottomConstraint!, - playerView.subtitleBackView.centerXAnchor.constraint(equalTo: playerView.centerXAnchor), - playerView.subtitleBackView.widthAnchor.constraint(lessThanOrEqualTo: playerView.widthAnchor, constant: -20), + playerView.subtitleBackView.centerXAnchor.constraint(equalTo: self.centerXAnchor), + playerView.subtitleBackView.widthAnchor.constraint(lessThanOrEqualTo: self.widthAnchor, constant: -20), playerView.subtitleBackView.heightAnchor.constraint(lessThanOrEqualToConstant: 100), // Subtitle label constraints within the back view @@ -200,6 +214,30 @@ class KSPlayerView: UIView { } } } + + 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 (use static defaults where required) @@ -275,6 +313,9 @@ class KSPlayerView: UIView { 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") } @@ -546,7 +587,11 @@ class KSPlayerView: UIView { } else if trackId == -1 { // Disable all subtitles for track in textTracks { track.isEnabled = false } - print("KSPlayerView: Disabled all text tracks") + // Clear srtControl selection and hide subtitle views + 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") } else { print("KSPlayerView: Text track \(trackId) not found. Available track IDs: \(textTracks.map { Int($0.trackID) }), array indices: 0..\(textTracks.count - 1)") } diff --git a/src/components/player/KSPlayerComponent.tsx b/src/components/player/KSPlayerComponent.tsx index a9a16ec5..5bf37106 100644 --- a/src/components/player/KSPlayerComponent.tsx +++ b/src/components/player/KSPlayerComponent.tsx @@ -16,6 +16,7 @@ interface KSPlayerViewProps { usesExternalPlaybackWhileExternalScreenIsActive?: boolean; subtitleBottomOffset?: number; subtitleFontSize?: number; + resizeMode?: 'contain' | 'cover' | 'stretch'; onLoad?: (data: any) => void; onProgress?: (data: any) => void; onBuffering?: (data: any) => void; @@ -52,6 +53,7 @@ export interface KSPlayerProps { usesExternalPlaybackWhileExternalScreenIsActive?: boolean; subtitleBottomOffset?: number; subtitleFontSize?: number; + resizeMode?: 'contain' | 'cover' | 'stretch'; onLoad?: (data: any) => void; onProgress?: (data: any) => void; onBuffering?: (data: any) => void; @@ -177,6 +179,7 @@ const KSPlayer = forwardRef((props, ref) => { usesExternalPlaybackWhileExternalScreenIsActive={props.usesExternalPlaybackWhileExternalScreenIsActive} subtitleBottomOffset={props.subtitleBottomOffset} subtitleFontSize={props.subtitleFontSize} + resizeMode={props.resizeMode} onLoad={(e: any) => props.onLoad?.(e?.nativeEvent ?? e)} onProgress={(e: any) => props.onProgress?.(e?.nativeEvent ?? e)} onBuffering={(e: any) => props.onBuffering?.(e?.nativeEvent ?? e)} diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 4e191f8d..60cb9e46 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -1315,6 +1315,12 @@ const KSPlayerCore: React.FC = () => { }; const cycleAspectRatio = () => { + // iOS KSPlayer: toggle native resize mode so subtitles remain independent + if (Platform.OS === 'ios') { + setResizeMode((prev) => (prev === 'cover' ? 'contain' : 'cover')); + return; + } + // Fallback (non‑iOS paths): keep legacy zoom behavior const newZoom = zoomScale === 1.1 ? 1 : 1.1; setZoomScale(newZoom); setZoomTranslateX(0); @@ -2717,7 +2723,7 @@ const KSPlayerCore: React.FC = () => { > 0 ? headers : undefined @@ -2730,6 +2736,7 @@ const KSPlayerCore: React.FC = () => { usesExternalPlaybackWhileExternalScreenIsActive={true} subtitleBottomOffset={subtitleBottomOffset} subtitleFontSize={subtitleSize} + resizeMode={resizeMode === 'none' ? 'contain' : resizeMode} onProgress={handleProgress} onLoad={onLoad} onEnd={onEnd} @@ -2762,6 +2769,7 @@ const KSPlayerCore: React.FC = () => { skip={skip} handleClose={handleClose} cycleAspectRatio={cycleAspectRatio} + currentResizeMode={resizeMode} setShowAudioModal={setShowAudioModal} setShowSubtitleModal={setShowSubtitleModal} isSubtitleModalOpen={showSubtitleModal}