diff --git a/ios/KSPlayerManager.m b/ios/KSPlayerManager.m index d61dc406..5744e62d 100644 --- a/ios/KSPlayerManager.m +++ b/ios/KSPlayerManager.m @@ -14,6 +14,7 @@ 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) @@ -34,6 +35,7 @@ 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) diff --git a/ios/KSPlayerView.swift b/ios/KSPlayerView.swift index fd312693..98b29865 100644 --- a/ios/KSPlayerView.swift +++ b/ios/KSPlayerView.swift @@ -52,6 +52,12 @@ class KSPlayerView: UIView { } } + @objc var rate: NSNumber = 1.0 { + didSet { + setPlaybackRate(rate.floatValue) + } + } + @objc var audioTrack: NSNumber = -1 { didSet { setAudioTrack(audioTrack.intValue) @@ -402,6 +408,11 @@ class KSPlayerView: UIView { 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, diff --git a/ios/KSPlayerViewManager.swift b/ios/KSPlayerViewManager.swift index 733a8423..657b70aa 100644 --- a/ios/KSPlayerViewManager.swift +++ b/ios/KSPlayerViewManager.swift @@ -70,6 +70,14 @@ class KSPlayerViewManager: RCTViewManager { } } + @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 { diff --git a/src/components/player/KSPlayerComponent.tsx b/src/components/player/KSPlayerComponent.tsx index 5bf37106..55d3cf8f 100644 --- a/src/components/player/KSPlayerComponent.tsx +++ b/src/components/player/KSPlayerComponent.tsx @@ -10,6 +10,7 @@ interface KSPlayerViewProps { source?: KSPlayerSource; paused?: boolean; volume?: number; + rate?: number; audioTrack?: number; textTrack?: number; allowsExternalPlayback?: boolean; @@ -34,6 +35,7 @@ export interface KSPlayerRef { setSource: (source: KSPlayerSource) => void; setPaused: (paused: boolean) => void; setVolume: (volume: number) => void; + setPlaybackRate: (rate: number) => void; setAudioTrack: (trackId: number) => void; setTextTrack: (trackId: number) => void; getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>; @@ -47,6 +49,7 @@ export interface KSPlayerProps { source?: KSPlayerSource; paused?: boolean; volume?: number; + rate?: number; audioTrack?: number; textTrack?: number; allowsExternalPlayback?: boolean; @@ -100,6 +103,14 @@ const KSPlayer = forwardRef((props, ref) => { UIManager.dispatchViewManagerCommand(node, commandId, [volume]); } }, + setPlaybackRate: (rate: number) => { + if (nativeRef.current) { + const node = findNodeHandle(nativeRef.current); + // @ts-ignore legacy UIManager commands path for Paper + const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setPlaybackRate; + UIManager.dispatchViewManagerCommand(node, commandId, [rate]); + } + }, setAudioTrack: (trackId: number) => { if (nativeRef.current) { const node = findNodeHandle(nativeRef.current); @@ -173,6 +184,7 @@ const KSPlayer = forwardRef((props, ref) => { source={props.source} paused={props.paused} volume={props.volume} + rate={props.rate} audioTrack={props.audioTrack} textTrack={props.textTrack} allowsExternalPlayback={props.allowsExternalPlayback} diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 60cb9e46..3a1f556e 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -2730,6 +2730,7 @@ const KSPlayerCore: React.FC = () => { }} paused={paused} volume={volume / 100} + rate={playbackSpeed} audioTrack={selectedAudioTrack ?? undefined} textTrack={useCustomSubtitles ? -1 : selectedTextTrack} allowsExternalPlayback={allowsAirPlay} diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 0b7eb840..8cbfbdc0 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -314,9 +314,24 @@ export const PlayerControls: React.FC = ({ )} - - - + + {/* AirPlay Button - iOS only, KSAVPlayer only */} + {Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && ( + + + + )} + + + + @@ -520,7 +535,7 @@ export const PlayerControls: React.FC = ({ - {/* Playback Speed Button - Hidden on iOS */} + {/* Playback Speed Button - Temporarily hidden on iOS */} {Platform.OS !== 'ios' && ( @@ -572,26 +587,6 @@ export const PlayerControls: React.FC = ({ )} - - {/* AirPlay Button - iOS only, KSAVPlayer only */} - {Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && ( - - - - {allowsAirPlay ? 'AirPlay' : 'AirPlay Off'} - - - )} diff --git a/src/components/player/utils/playerStyles.ts b/src/components/player/utils/playerStyles.ts index 63c03931..7eed60f8 100644 --- a/src/components/player/utils/playerStyles.ts +++ b/src/components/player/utils/playerStyles.ts @@ -131,6 +131,14 @@ export const styles = StyleSheet.create({ closeButton: { padding: 8, }, + topRightButtons: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + topButton: { + padding: 8, + }, /* CloudStream Style - Center Controls */ diff --git a/src/services/backupService.ts b/src/services/backupService.ts index 770000ca..5a53e8b2 100644 --- a/src/services/backupService.ts +++ b/src/services/backupService.ts @@ -18,7 +18,22 @@ export interface BackupData { watchProgress: Record; addons: any[]; downloads: DownloadItem[]; - subtitles: any; + subtitles: { + subtitleSize?: number; + subtitleBackground?: boolean; + subtitleTextColor?: string; + subtitleBgOpacity?: number; + subtitleTextShadow?: boolean; + subtitleOutline?: boolean; + subtitleOutlineColor?: string; + subtitleOutlineWidth?: number; + subtitleAlign?: 'center' | 'left' | 'right'; + subtitleBottomOffset?: number; + subtitleLetterSpacing?: number; + subtitleLineHeightMultiplier?: number; + subtitleOffsetSec?: number; + [key: string]: any; // Allow for additional subtitle preferences + }; tombstones: Record; continueWatchingRemoved: Record; contentDuration: Record; @@ -444,7 +459,18 @@ export class BackupService { const scope = await this.getUserScope(); const scopedKey = `@user:${scope}:@subtitle_settings`; const subtitlesJson = await AsyncStorage.getItem(scopedKey); - return subtitlesJson ? JSON.parse(subtitlesJson) : {}; + let subtitleSettings = subtitlesJson ? JSON.parse(subtitlesJson) : {}; + + // Also check for legacy subtitle size preference + const legacySubtitleSize = await AsyncStorage.getItem('@subtitle_size_preference'); + if (legacySubtitleSize && !subtitleSettings.subtitleSize) { + const legacySize = parseInt(legacySubtitleSize, 10); + if (!Number.isNaN(legacySize) && legacySize > 0) { + subtitleSettings.subtitleSize = legacySize; + } + } + + return subtitleSettings; } catch (error) { logger.error('[BackupService] Failed to get subtitle settings:', error); return {}; @@ -738,6 +764,12 @@ export class BackupService { const scope = await this.getUserScope(); const scopedKey = `@user:${scope}:@subtitle_settings`; await AsyncStorage.setItem(scopedKey, JSON.stringify(subtitles)); + + // Also restore legacy subtitle size preference for backward compatibility + if (subtitles && typeof subtitles.subtitleSize === 'number') { + await AsyncStorage.setItem('@subtitle_size_preference', subtitles.subtitleSize.toString()); + } + logger.info('[BackupService] Subtitle settings restored'); } catch (error) { logger.error('[BackupService] Failed to restore subtitle settings:', error);