added playback speed controls KSPlayer(iOS)

This commit is contained in:
tapframe 2025-10-24 21:01:19 +05:30
parent 1ba0a49778
commit a5feeb40a7
8 changed files with 95 additions and 26 deletions

View file

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

View file

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

View file

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

View file

@ -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<KSPlayerRef, KSPlayerProps>((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<KSPlayerRef, KSPlayerProps>((props, ref) => {
source={props.source}
paused={props.paused}
volume={props.volume}
rate={props.rate}
audioTrack={props.audioTrack}
textTrack={props.textTrack}
allowsExternalPlayback={props.allowsExternalPlayback}

View file

@ -2730,6 +2730,7 @@ const KSPlayerCore: React.FC = () => {
}}
paused={paused}
volume={volume / 100}
rate={playbackSpeed}
audioTrack={selectedAudioTrack ?? undefined}
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
allowsExternalPlayback={allowsAirPlay}

View file

@ -314,9 +314,24 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
</View>
)}
</View>
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<Ionicons name="close" size={closeIconSize} color="white" />
</TouchableOpacity>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{/* AirPlay Button - iOS only, KSAVPlayer only */}
{Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && (
<TouchableOpacity
style={{ padding: 8 }}
onPress={onAirPlayPress}
>
<Feather
name="airplay"
size={closeIconSize}
color={isAirPlayActive ? currentTheme.colors.primary : "white"}
/>
</TouchableOpacity>
)}
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<Ionicons name="close" size={closeIconSize} color="white" />
</TouchableOpacity>
</View>
</View>
</LinearGradient>
@ -520,7 +535,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
</Text>
</TouchableOpacity>
{/* Playback Speed Button - Hidden on iOS */}
{/* Playback Speed Button - Temporarily hidden on iOS */}
{Platform.OS !== 'ios' && (
<TouchableOpacity style={styles.bottomButton} onPress={cyclePlaybackSpeed}>
<Ionicons name="speedometer" size={20} color="white" />
@ -572,26 +587,6 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
</Text>
</TouchableOpacity>
)}
{/* AirPlay Button - iOS only, KSAVPlayer only */}
{Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && (
<TouchableOpacity
style={styles.bottomButton}
onPress={onAirPlayPress}
>
<Feather
name="airplay"
size={20}
color={isAirPlayActive ? currentTheme.colors.primary : "white"}
/>
<Text style={[
styles.bottomButtonText,
isAirPlayActive && { color: currentTheme.colors.primary }
]}>
{allowsAirPlay ? 'AirPlay' : 'AirPlay Off'}
</Text>
</TouchableOpacity>
)}
</View>
</View>
</LinearGradient>

View file

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

View file

@ -18,7 +18,22 @@ export interface BackupData {
watchProgress: Record<string, any>;
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<string, number>;
continueWatchingRemoved: Record<string, number>;
contentDuration: Record<string, number>;
@ -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);