mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
added playback speed controls KSPlayer(iOS)
This commit is contained in:
parent
1ba0a49778
commit
a5feeb40a7
8 changed files with 95 additions and 26 deletions
|
|
@ -14,6 +14,7 @@
|
||||||
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
|
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber)
|
RCT_EXPORT_VIEW_PROPERTY(volume, NSNumber)
|
||||||
|
RCT_EXPORT_VIEW_PROPERTY(rate, NSNumber)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber)
|
RCT_EXPORT_VIEW_PROPERTY(audioTrack, NSNumber)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
|
RCT_EXPORT_VIEW_PROPERTY(textTrack, NSNumber)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(allowsExternalPlayback, BOOL)
|
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(setSource:(nonnull NSNumber *)node source:(nonnull NSDictionary *)source)
|
||||||
RCT_EXTERN_METHOD(setPaused:(nonnull NSNumber *)node paused:(BOOL)paused)
|
RCT_EXTERN_METHOD(setPaused:(nonnull NSNumber *)node paused:(BOOL)paused)
|
||||||
RCT_EXTERN_METHOD(setVolume:(nonnull NSNumber *)node volume:(nonnull NSNumber *)volume)
|
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(setAudioTrack:(nonnull NSNumber *)node trackId:(nonnull NSNumber *)trackId)
|
||||||
RCT_EXTERN_METHOD(setTextTrack:(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(getTracks:(nonnull NSNumber *)node resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,12 @@ class KSPlayerView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc var rate: NSNumber = 1.0 {
|
||||||
|
didSet {
|
||||||
|
setPlaybackRate(rate.floatValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc var audioTrack: NSNumber = -1 {
|
@objc var audioTrack: NSNumber = -1 {
|
||||||
didSet {
|
didSet {
|
||||||
setAudioTrack(audioTrack.intValue)
|
setAudioTrack(audioTrack.intValue)
|
||||||
|
|
@ -402,6 +408,11 @@ class KSPlayerView: UIView {
|
||||||
playerView.playerLayer?.player.playbackVolume = 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) {
|
func seek(to time: TimeInterval) {
|
||||||
guard let playerLayer = playerView.playerLayer,
|
guard let playerLayer = playerView.playerLayer,
|
||||||
playerLayer.player.isReadyToPlay,
|
playerLayer.player.isReadyToPlay,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
@objc func setAudioTrack(_ node: NSNumber, trackId: NSNumber) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
if let view = self.bridge.uiManager.view(forReactTag: node) as? KSPlayerView {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ interface KSPlayerViewProps {
|
||||||
source?: KSPlayerSource;
|
source?: KSPlayerSource;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
|
rate?: number;
|
||||||
audioTrack?: number;
|
audioTrack?: number;
|
||||||
textTrack?: number;
|
textTrack?: number;
|
||||||
allowsExternalPlayback?: boolean;
|
allowsExternalPlayback?: boolean;
|
||||||
|
|
@ -34,6 +35,7 @@ export interface KSPlayerRef {
|
||||||
setSource: (source: KSPlayerSource) => void;
|
setSource: (source: KSPlayerSource) => void;
|
||||||
setPaused: (paused: boolean) => void;
|
setPaused: (paused: boolean) => void;
|
||||||
setVolume: (volume: number) => void;
|
setVolume: (volume: number) => void;
|
||||||
|
setPlaybackRate: (rate: number) => void;
|
||||||
setAudioTrack: (trackId: number) => void;
|
setAudioTrack: (trackId: number) => void;
|
||||||
setTextTrack: (trackId: number) => void;
|
setTextTrack: (trackId: number) => void;
|
||||||
getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>;
|
getTracks: () => Promise<{ audioTracks: any[]; textTracks: any[] }>;
|
||||||
|
|
@ -47,6 +49,7 @@ export interface KSPlayerProps {
|
||||||
source?: KSPlayerSource;
|
source?: KSPlayerSource;
|
||||||
paused?: boolean;
|
paused?: boolean;
|
||||||
volume?: number;
|
volume?: number;
|
||||||
|
rate?: number;
|
||||||
audioTrack?: number;
|
audioTrack?: number;
|
||||||
textTrack?: number;
|
textTrack?: number;
|
||||||
allowsExternalPlayback?: boolean;
|
allowsExternalPlayback?: boolean;
|
||||||
|
|
@ -100,6 +103,14 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
UIManager.dispatchViewManagerCommand(node, commandId, [volume]);
|
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) => {
|
setAudioTrack: (trackId: number) => {
|
||||||
if (nativeRef.current) {
|
if (nativeRef.current) {
|
||||||
const node = findNodeHandle(nativeRef.current);
|
const node = findNodeHandle(nativeRef.current);
|
||||||
|
|
@ -173,6 +184,7 @@ const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
|
||||||
source={props.source}
|
source={props.source}
|
||||||
paused={props.paused}
|
paused={props.paused}
|
||||||
volume={props.volume}
|
volume={props.volume}
|
||||||
|
rate={props.rate}
|
||||||
audioTrack={props.audioTrack}
|
audioTrack={props.audioTrack}
|
||||||
textTrack={props.textTrack}
|
textTrack={props.textTrack}
|
||||||
allowsExternalPlayback={props.allowsExternalPlayback}
|
allowsExternalPlayback={props.allowsExternalPlayback}
|
||||||
|
|
|
||||||
|
|
@ -2730,6 +2730,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
paused={paused}
|
paused={paused}
|
||||||
volume={volume / 100}
|
volume={volume / 100}
|
||||||
|
rate={playbackSpeed}
|
||||||
audioTrack={selectedAudioTrack ?? undefined}
|
audioTrack={selectedAudioTrack ?? undefined}
|
||||||
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
textTrack={useCustomSubtitles ? -1 : selectedTextTrack}
|
||||||
allowsExternalPlayback={allowsAirPlay}
|
allowsExternalPlayback={allowsAirPlay}
|
||||||
|
|
|
||||||
|
|
@ -314,9 +314,24 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
<Ionicons name="close" size={closeIconSize} color="white" />
|
{/* AirPlay Button - iOS only, KSAVPlayer only */}
|
||||||
</TouchableOpacity>
|
{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>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
|
|
@ -520,7 +535,7 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Playback Speed Button - Hidden on iOS */}
|
{/* Playback Speed Button - Temporarily hidden on iOS */}
|
||||||
{Platform.OS !== 'ios' && (
|
{Platform.OS !== 'ios' && (
|
||||||
<TouchableOpacity style={styles.bottomButton} onPress={cyclePlaybackSpeed}>
|
<TouchableOpacity style={styles.bottomButton} onPress={cyclePlaybackSpeed}>
|
||||||
<Ionicons name="speedometer" size={20} color="white" />
|
<Ionicons name="speedometer" size={20} color="white" />
|
||||||
|
|
@ -572,26 +587,6 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</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>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,14 @@ export const styles = StyleSheet.create({
|
||||||
closeButton: {
|
closeButton: {
|
||||||
padding: 8,
|
padding: 8,
|
||||||
},
|
},
|
||||||
|
topRightButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
topButton: {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/* CloudStream Style - Center Controls */
|
/* CloudStream Style - Center Controls */
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,22 @@ export interface BackupData {
|
||||||
watchProgress: Record<string, any>;
|
watchProgress: Record<string, any>;
|
||||||
addons: any[];
|
addons: any[];
|
||||||
downloads: DownloadItem[];
|
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>;
|
tombstones: Record<string, number>;
|
||||||
continueWatchingRemoved: Record<string, number>;
|
continueWatchingRemoved: Record<string, number>;
|
||||||
contentDuration: Record<string, number>;
|
contentDuration: Record<string, number>;
|
||||||
|
|
@ -444,7 +459,18 @@ export class BackupService {
|
||||||
const scope = await this.getUserScope();
|
const scope = await this.getUserScope();
|
||||||
const scopedKey = `@user:${scope}:@subtitle_settings`;
|
const scopedKey = `@user:${scope}:@subtitle_settings`;
|
||||||
const subtitlesJson = await AsyncStorage.getItem(scopedKey);
|
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) {
|
} catch (error) {
|
||||||
logger.error('[BackupService] Failed to get subtitle settings:', error);
|
logger.error('[BackupService] Failed to get subtitle settings:', error);
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -738,6 +764,12 @@ export class BackupService {
|
||||||
const scope = await this.getUserScope();
|
const scope = await this.getUserScope();
|
||||||
const scopedKey = `@user:${scope}:@subtitle_settings`;
|
const scopedKey = `@user:${scope}:@subtitle_settings`;
|
||||||
await AsyncStorage.setItem(scopedKey, JSON.stringify(subtitles));
|
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');
|
logger.info('[BackupService] Subtitle settings restored');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[BackupService] Failed to restore subtitle settings:', error);
|
logger.error('[BackupService] Failed to restore subtitle settings:', error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue