sub/audio init

This commit is contained in:
tapframe 2026-01-10 01:36:45 +05:30
parent 4cc77b96cd
commit 3ef4f20781
9 changed files with 88 additions and 301 deletions

View file

@ -3173,7 +3173,7 @@ SPEC CHECKSUMS:
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
lottie-react-native: cbe3d931a7c24f7891a8e8032c2bb9b2373c4b9c
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
MPVKit: 25ac17c12586bbd3aab014c6a281f6981c3fb6cf
MPVKit: 268868ef845bb3130e70360b7764b0be45f6a196
NitroMmkv: 4af10c70043b4c3cded3f16547627c7d9d8e3b8b
NitroModules: a71a5ab2911caf79e45170e6e12475b5260a12d0
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47

View file

@ -1,227 +0,0 @@
import React, { useRef, useImperativeHandle, forwardRef, useEffect, useState } from 'react';
import { View, requireNativeComponent, UIManager, findNodeHandle, NativeModules } from 'react-native';
export interface KSPlayerSource {
uri: string;
headers?: Record<string, string>;
}
interface KSPlayerViewProps {
source?: KSPlayerSource;
paused?: boolean;
volume?: number;
rate?: number;
audioTrack?: number;
textTrack?: number;
allowsExternalPlayback?: boolean;
usesExternalPlaybackWhileExternalScreenIsActive?: boolean;
subtitleBottomOffset?: number;
subtitleFontSize?: number;
subtitleTextColor?: string;
subtitleBackgroundColor?: string;
resizeMode?: 'contain' | 'cover' | 'stretch';
onLoad?: (data: any) => void;
onProgress?: (data: any) => void;
onBuffering?: (data: any) => void;
onEnd?: () => void;
onError?: (error: any) => void;
onBufferingProgress?: (data: any) => void;
style?: any;
}
const KSPlayerViewManager = requireNativeComponent<KSPlayerViewProps>('KSPlayerView');
const KSPlayerModule = NativeModules.KSPlayerModule;
export interface KSPlayerRef {
seek: (time: number) => void;
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[] }>;
setAllowsExternalPlayback: (allows: boolean) => void;
setUsesExternalPlaybackWhileExternalScreenIsActive: (uses: boolean) => void;
getAirPlayState: () => Promise<{ allowsExternalPlayback: boolean; usesExternalPlaybackWhileExternalScreenIsActive: boolean; isExternalPlaybackActive: boolean }>;
showAirPlayPicker: () => void;
}
export interface KSPlayerProps {
source?: KSPlayerSource;
paused?: boolean;
volume?: number;
rate?: number;
audioTrack?: number;
textTrack?: number;
allowsExternalPlayback?: boolean;
usesExternalPlaybackWhileExternalScreenIsActive?: boolean;
subtitleBottomOffset?: number;
subtitleFontSize?: number;
subtitleTextColor?: string;
subtitleBackgroundColor?: string;
resizeMode?: 'contain' | 'cover' | 'stretch';
onLoad?: (data: any) => void;
onProgress?: (data: any) => void;
onBuffering?: (data: any) => void;
onEnd?: () => void;
onError?: (error: any) => void;
onBufferingProgress?: (data: any) => void;
style?: any;
}
const KSPlayer = forwardRef<KSPlayerRef, KSPlayerProps>((props, ref) => {
const nativeRef = useRef<any>(null);
const [key, setKey] = useState(0); // Force re-render when source changes
useImperativeHandle(ref, () => ({
seek: (time: number) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.seek;
UIManager.dispatchViewManagerCommand(node, commandId, [time]);
}
},
setSource: (source: KSPlayerSource) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setSource;
UIManager.dispatchViewManagerCommand(node, commandId, [source]);
}
},
setPaused: (paused: boolean) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setPaused;
UIManager.dispatchViewManagerCommand(node, commandId, [paused]);
}
},
setVolume: (volume: number) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setVolume;
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);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setAudioTrack;
UIManager.dispatchViewManagerCommand(node, commandId, [trackId]);
}
},
setTextTrack: (trackId: number) => {
console.log('[KSPlayerComponent] setTextTrack called with trackId:', trackId);
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
console.log('[KSPlayerComponent] setTextTrack dispatching command to node:', node);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setTextTrack;
console.log('[KSPlayerComponent] setTextTrack commandId:', commandId);
UIManager.dispatchViewManagerCommand(node, commandId, [trackId]);
} else {
console.warn('[KSPlayerComponent] setTextTrack: nativeRef.current is null');
}
},
getTracks: async () => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
if (node) {
return await KSPlayerModule.getTracks(node);
}
}
return { audioTracks: [], textTracks: [] };
},
setAllowsExternalPlayback: (allows: boolean) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setAllowsExternalPlayback;
UIManager.dispatchViewManagerCommand(node, commandId, [allows]);
}
},
setUsesExternalPlaybackWhileExternalScreenIsActive: (uses: boolean) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
// @ts-ignore legacy UIManager commands path for Paper
const commandId = UIManager.getViewManagerConfig('KSPlayerView').Commands.setUsesExternalPlaybackWhileExternalScreenIsActive;
UIManager.dispatchViewManagerCommand(node, commandId, [uses]);
}
},
getAirPlayState: async () => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
if (node) {
return await KSPlayerModule.getAirPlayState(node);
}
}
return { allowsExternalPlayback: false, usesExternalPlaybackWhileExternalScreenIsActive: false, isExternalPlaybackActive: false };
},
showAirPlayPicker: () => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
if (node) {
console.log('[KSPlayerComponent] Calling showAirPlayPicker with node:', node);
KSPlayerModule.showAirPlayPicker(node);
} else {
console.warn('[KSPlayerComponent] Cannot call showAirPlayPicker: node is null');
}
} else {
console.log('[KSPlayerComponent] nativeRef.current is null');
}
},
}));
// No need for event listeners - events are handled through props
// Force re-render when source changes to ensure proper reloading
useEffect(() => {
if (props.source) {
setKey(prev => prev + 1);
}
}, [props.source?.uri]);
return (
<KSPlayerViewManager
key={key}
ref={nativeRef}
source={props.source}
paused={props.paused}
volume={props.volume}
rate={props.rate}
audioTrack={props.audioTrack}
textTrack={props.textTrack}
allowsExternalPlayback={props.allowsExternalPlayback}
usesExternalPlaybackWhileExternalScreenIsActive={props.usesExternalPlaybackWhileExternalScreenIsActive}
subtitleBottomOffset={props.subtitleBottomOffset}
subtitleFontSize={props.subtitleFontSize}
subtitleTextColor={props.subtitleTextColor}
subtitleBackgroundColor={props.subtitleBackgroundColor}
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)}
onEnd={() => props.onEnd?.()}
onError={(e: any) => props.onError?.(e?.nativeEvent ?? e)}
onBufferingProgress={(e: any) => props.onBufferingProgress?.(e?.nativeEvent ?? e)}
style={props.style}
/>
);
});
KSPlayer.displayName = 'KSPlayer';
export default KSPlayer;

View file

@ -39,7 +39,7 @@ import {
} from './hooks';
// Platform-specific hooks
import { useKSPlayer } from './ios/hooks/useKSPlayer';
import { useMPVPlayer } from './ios/hooks/useMPVPlayer';
// App-level Hooks
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
@ -107,8 +107,6 @@ const KSPlayerCore: React.FC = () => {
screenDimensions, setScreenDimensions,
zoomScale, setZoomScale,
lastZoomScale, setLastZoomScale,
isAirPlayActive,
allowsAirPlay,
isSeeking,
isMounted,
} = playerState;
@ -132,7 +130,7 @@ const KSPlayerCore: React.FC = () => {
const openingAnim = useOpeningAnimation(backdrop, metadata);
const tracks = usePlayerTracks();
const { ksPlayerRef, seek } = useKSPlayer();
const { mpvPlayerRef, seek } = useMPVPlayer();
const customSubs = useCustomSubtitles();
const { settings } = useSettings();
const { currentTheme } = useTheme();
@ -172,7 +170,7 @@ const KSPlayerCore: React.FC = () => {
});
const controls = usePlayerControls({
playerRef: ksPlayerRef,
playerRef: mpvPlayerRef,
paused,
setPaused,
currentTime,
@ -514,10 +512,8 @@ const KSPlayerCore: React.FC = () => {
const handleSelectAudioTrack = useCallback((trackId: number) => {
tracks.selectAudioTrack(trackId);
if (ksPlayerRef.current) {
ksPlayerRef.current.setAudioTrack(trackId);
}
}, [tracks, ksPlayerRef]);
mpvPlayerRef.current?.setAudioTrack(trackId);
}, [tracks, mpvPlayerRef]);
// Stream selection handler
const handleSelectStream = async (newStream: any) => {
@ -617,7 +613,7 @@ const KSPlayerCore: React.FC = () => {
{/* Video Surface & Pinch Zoom */}
<KSPlayerSurface
ksPlayerRef={ksPlayerRef}
ksPlayerRef={mpvPlayerRef}
uri={uri}
headers={headers}
paused={paused}
@ -737,10 +733,7 @@ const KSPlayerCore: React.FC = () => {
onSlidingComplete={onSlidingComplete}
buffered={buffered}
formatTime={formatTime}
playerBackend="KSAVPlayer"
isAirPlayActive={isAirPlayActive}
allowsAirPlay={allowsAirPlay}
onAirPlayPress={() => ksPlayerRef.current?.showAirPlayPicker()}
playerBackend="MPV"
/>
</View>
)}

View file

@ -1,4 +1,4 @@
import React, { useRef, useImperativeHandle, forwardRef, useEffect } from 'react';
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
import { requireNativeComponent, UIManager, findNodeHandle, ViewStyle } from 'react-native';
interface MPVPlayerProps {
@ -9,15 +9,25 @@ interface MPVPlayerProps {
paused?: boolean;
volume?: number;
rate?: number;
audioTrack?: number;
textTrack?: number;
subtitleTextColor?: string;
subtitleBackgroundColor?: string;
subtitleFontSize?: number;
subtitleBottomOffset?: number;
style?: ViewStyle;
onLoad?: (event: any) => void;
onProgress?: (event: any) => void;
onEnd?: (event: any) => void;
onError?: (event: any) => void;
onAudioTracks?: (event: any) => void;
onTextTracks?: (event: any) => void;
}
export interface MPVPlayerRef {
seek: (time: number) => void;
setAudioTrack: (trackId: number) => void;
setTextTrack: (trackId: number) => void;
}
const ComponentName = 'MPVPlayerView';
@ -38,6 +48,28 @@ const MPVPlayerComponent = forwardRef<MPVPlayerRef, MPVPlayerProps>((props, ref)
);
}
},
setAudioTrack: (trackId: number) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
UIManager.dispatchViewManagerCommand(
node,
// @ts-ignore
UIManager.getViewManagerConfig(ComponentName).Commands.setAudioTrack,
[trackId]
);
}
},
setTextTrack: (trackId: number) => {
if (nativeRef.current) {
const node = findNodeHandle(nativeRef.current);
UIManager.dispatchViewManagerCommand(
node,
// @ts-ignore
UIManager.getViewManagerConfig(ComponentName).Commands.setTextTrack,
[trackId]
);
}
},
}));
return (
@ -48,6 +80,8 @@ const MPVPlayerComponent = forwardRef<MPVPlayerRef, MPVPlayerProps>((props, ref)
onProgress={(e: any) => props.onProgress?.(e.nativeEvent)}
onEnd={(e: any) => props.onEnd?.(e.nativeEvent)}
onError={(e: any) => props.onError?.(e.nativeEvent)}
onAudioTracks={(e: any) => props.onAudioTracks?.(e.nativeEvent)}
onTextTracks={(e: any) => props.onTextTracks?.(e.nativeEvent)}
/>
);
});

View file

@ -47,10 +47,6 @@ interface PlayerControlsProps {
buffered: number;
formatTime: (seconds: number) => string;
playerBackend?: string;
// AirPlay props
isAirPlayActive?: boolean;
allowsAirPlay?: boolean;
onAirPlayPress?: () => void;
// MPV Switch (Android only)
onSwitchToMPV?: () => void;
useExoPlayer?: boolean;
@ -93,9 +89,6 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
buffered,
formatTime,
playerBackend,
isAirPlayActive,
allowsAirPlay,
onAirPlayPress,
onSwitchToMPV,
useExoPlayer,
}) => {
@ -349,19 +342,6 @@ export const PlayerControls: React.FC<PlayerControlsProps> = ({
)}
</View>
<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>
)}
{/* Switch to MPV Button - Android only, when using ExoPlayer */}
{Platform.OS === 'android' && onSwitchToMPV && useExoPlayer && (
<TouchableOpacity

View file

@ -2,7 +2,6 @@ import React, { useRef } from 'react';
import { Animated } from 'react-native';
import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import MPVPlayerComponent from '../../MPVPlayerComponent';
import { MPVPlayerRef } from '../../MPVPlayerComponent';
interface KSPlayerSurfaceProps {
@ -98,7 +97,7 @@ export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
console.log('[KSPlayerSurface] textTrack prop changed to:', textTrack);
}, [textTrack]);
// Handle buffering - KSPlayerComponent uses onBuffering callback
// Handle buffering - MPVPlayerComponent exposes buffering only via events we wire up
const handleBuffering = (data: any) => {
onBuffer(data?.isBuffering ?? false);
};
@ -135,12 +134,20 @@ export const KSPlayerSurface: React.FC<KSPlayerSurfaceProps> = ({
paused={paused}
volume={volume}
rate={playbackSpeed}
audioTrack={audioTrack}
textTrack={textTrack}
subtitleTextColor={subtitleTextColor}
subtitleBackgroundColor={subtitleBackgroundColor}
subtitleFontSize={subtitleFontSize}
subtitleBottomOffset={subtitleBottomOffset}
style={customVideoStyles.width ? customVideoStyles : { width: screenWidth, height: screenHeight }}
onLoad={handleLoad}
onProgress={onProgress}
onEnd={onEnd}
onError={onError}
onAudioTracks={onAudioTracks}
onTextTracks={onTextTracks}
/>
</Animated.View>

View file

@ -1,15 +0,0 @@
import { useRef } from 'react';
import { KSPlayerRef } from '../../KSPlayerComponent';
export const useKSPlayer = () => {
const ksPlayerRef = useRef<KSPlayerRef>(null);
const seek = (time: number) => {
ksPlayerRef.current?.seek(time);
};
return {
ksPlayerRef,
seek
};
};

View file

@ -0,0 +1,16 @@
import { useRef } from 'react';
import type { MPVPlayerRef } from '../../MPVPlayerComponent';
export const useMPVPlayer = () => {
const mpvPlayerRef = useRef<MPVPlayerRef>(null);
const seek = (time: number) => {
mpvPlayerRef.current?.seek(time);
};
return {
mpvPlayerRef,
seek,
};
};

View file

@ -110,7 +110,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
const isCompact = width < 360 || height < 640;
// Internal subtitle is active when a built-in track is selected AND not using custom/addon subtitles
const isUsingInternalSubtitle = selectedTextTrack >= 0 && !useCustomSubtitles;
// ExoPlayer has limited styling support - hide unsupported options when using ExoPlayer with internal subs
// ExoPlayer internal subtitles have limited styling support
const isExoPlayerInternal = useExoPlayer && isUsingInternalSubtitle;
const sectionPad = isCompact ? 12 : 16;
const chipPadH = isCompact ? 8 : 12;
@ -259,8 +259,8 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View>
</View>
{/* Quick Presets - Hidden for ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
{/* Quick Presets - only for CustomSubtitles overlay */}
{!isUsingInternalSubtitle && (
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad }}>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 10 }}>
<MaterialIcons name="star" size={16} color="rgba(255,255,255,0.7)" />
@ -329,7 +329,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</TouchableOpacity>
</View>
</View>
{/* Show Background - Not supported on ExoPlayer internal subtitles */}
{/* Show Background - not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
@ -346,28 +346,27 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
)}
</View>
{/* Advanced controls - Limited for ExoPlayer */}
{/* Advanced controls */}
<View style={{ backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 16, padding: sectionPad, gap: isCompact ? 10 : 14 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="build" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>{isExoPlayerInternal ? t('player_ui.position') : t('player_ui.advanced')}</Text>
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>{isUsingInternalSubtitle ? t('player_ui.position') : t('player_ui.advanced')}</Text>
</View>
{/* Text Color - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>{t('player_ui.text_color')}</Text>
</View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
))}
</View>
{/* Text Color - supported for MPV built-in, and for CustomSubtitles */}
<View style={{ marginTop: 8, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="palette" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: 'white', marginLeft: 8, fontWeight: '600' }}>{t('player_ui.text_color')}</Text>
</View>
)}
{/* Align - Not supported on ExoPlayer internal subtitles */}
{!isExoPlayerInternal && (
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
))}
</View>
</View>
{/* Align - only supported for CustomSubtitles overlay */}
{!isUsingInternalSubtitle && (
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>{t('player_ui.align')}</Text>
<View style={{ flexDirection: 'row', gap: 8 }}>