mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 07:28:37 +08:00
1
This commit is contained in:
@@ -13,7 +13,10 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
interface Props {
|
||||
hlsUrl: string;
|
||||
isLive: boolean; // false → show offline placeholder
|
||||
isLive: boolean;
|
||||
qualities?: { qn: number; desc: string }[];
|
||||
currentQn?: number;
|
||||
onQualityChange?: (qn: number) => void;
|
||||
}
|
||||
|
||||
const HIDE_DELAY = 3000;
|
||||
@@ -24,7 +27,7 @@ const HEADERS = {
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
export function LivePlayer({ hlsUrl, isLive }: Props) {
|
||||
export function LivePlayer({ hlsUrl, isLive, qualities = [], currentQn = 0, onQualityChange }: Props) {
|
||||
const { width: SCREEN_W, height: SCREEN_H } = useWindowDimensions();
|
||||
const VIDEO_H = SCREEN_W * 0.5625;
|
||||
|
||||
@@ -45,7 +48,17 @@ export function LivePlayer({ hlsUrl, isLive }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
return <NativeLivePlayer hlsUrl={hlsUrl} screenW={SCREEN_W} screenH={SCREEN_H} videoH={VIDEO_H} />;
|
||||
return (
|
||||
<NativeLivePlayer
|
||||
hlsUrl={hlsUrl}
|
||||
screenW={SCREEN_W}
|
||||
screenH={SCREEN_H}
|
||||
videoH={VIDEO_H}
|
||||
qualities={qualities}
|
||||
currentQn={currentQn}
|
||||
onQualityChange={onQualityChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NativeLivePlayer({
|
||||
@@ -53,19 +66,25 @@ function NativeLivePlayer({
|
||||
screenW,
|
||||
screenH,
|
||||
videoH,
|
||||
qualities,
|
||||
currentQn,
|
||||
onQualityChange,
|
||||
}: {
|
||||
hlsUrl: string;
|
||||
screenW: number;
|
||||
screenH: number;
|
||||
videoH: number;
|
||||
qualities: { qn: number; desc: string }[];
|
||||
currentQn: number;
|
||||
onQualityChange?: (qn: number) => void;
|
||||
}) {
|
||||
// Lazy import to avoid web bundle issues
|
||||
const Video = require('react-native-video').default;
|
||||
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [buffering, setBuffering] = useState(true);
|
||||
const [showQualityPanel, setShowQualityPanel] = useState(false);
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const resetHideTimer = useCallback(() => {
|
||||
@@ -78,6 +97,38 @@ function NativeLivePlayer({
|
||||
return () => { if (hideTimer.current) clearTimeout(hideTimer.current); };
|
||||
}, []);
|
||||
|
||||
// Lock/unlock orientation on fullscreen toggle
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const ScreenOrientation = require('expo-screen-orientation');
|
||||
if (isFullscreen) {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
||||
);
|
||||
} else {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
);
|
||||
}
|
||||
} catch { /* graceful degradation in Expo Go */ }
|
||||
})();
|
||||
}, [isFullscreen]);
|
||||
|
||||
// Restore portrait on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
(async () => {
|
||||
try {
|
||||
const ScreenOrientation = require('expo-screen-orientation');
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
);
|
||||
} catch { /* ignore */ }
|
||||
})();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleTap = useCallback(() => {
|
||||
setShowControls(prev => {
|
||||
if (!prev) { resetHideTimer(); return true; }
|
||||
@@ -90,6 +141,8 @@ function NativeLivePlayer({
|
||||
? { width: screenH, height: screenW }
|
||||
: { width: screenW, height: videoH };
|
||||
|
||||
const currentQnDesc = qualities.find(q => q.qn === currentQn)?.desc ?? '';
|
||||
|
||||
const videoContent = (
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
<Video
|
||||
@@ -118,7 +171,7 @@ function NativeLivePlayer({
|
||||
{/* LIVE badge top-left */}
|
||||
<View style={styles.liveBadge} pointerEvents="none">
|
||||
<View style={styles.liveDot} />
|
||||
<Text style={styles.liveText}>LIVE</Text>
|
||||
<Text style={styles.liveText}>live</Text>
|
||||
</View>
|
||||
|
||||
{/* Center play/pause */}
|
||||
@@ -140,15 +193,53 @@ function NativeLivePlayer({
|
||||
<Ionicons name={paused ? 'play' : 'pause'} size={16} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1 }} />
|
||||
{qualities.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.qualityBtn}
|
||||
onPress={() => { setShowQualityPanel(true); resetHideTimer(); }}
|
||||
>
|
||||
<Text style={styles.qualityText}>{currentQnDesc || '清晰度'}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.ctrlBtn}
|
||||
onPress={() => setIsFullscreen(fs => !fs)}
|
||||
onPress={() => { setIsFullscreen(fs => !fs); resetHideTimer(); }}
|
||||
>
|
||||
<Ionicons name={isFullscreen ? 'contract' : 'expand'} size={16} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Quality selector panel */}
|
||||
{showQualityPanel && (
|
||||
<TouchableWithoutFeedback onPress={() => setShowQualityPanel(false)}>
|
||||
<View style={styles.qualityOverlay}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.qualityPanel}>
|
||||
<Text style={styles.qualityPanelTitle}>清晰度</Text>
|
||||
{qualities.map(q => (
|
||||
<TouchableOpacity
|
||||
key={q.qn}
|
||||
style={[styles.qualityItem, currentQn === q.qn && styles.qualityItemActive]}
|
||||
onPress={() => {
|
||||
onQualityChange?.(q.qn);
|
||||
setShowQualityPanel(false);
|
||||
}}
|
||||
>
|
||||
<Text style={[styles.qualityItemText, currentQn === q.qn && styles.qualityItemTextActive]}>
|
||||
{q.desc}
|
||||
</Text>
|
||||
{currentQn === q.qn && (
|
||||
<Ionicons name="checkmark" size={14} color="#00AEEC" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -223,6 +314,44 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
},
|
||||
ctrlBtn: { paddingHorizontal: 8, paddingVertical: 4 },
|
||||
qualityBtn: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
borderRadius: 4,
|
||||
marginRight: 4,
|
||||
},
|
||||
qualityText: { color: '#fff', fontSize: 11, fontWeight: '600' },
|
||||
qualityOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
qualityPanel: {
|
||||
backgroundColor: 'rgba(20,20,20,0.95)',
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
paddingBottom: 16,
|
||||
},
|
||||
qualityPanelTitle: {
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
qualityItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
qualityItemActive: { backgroundColor: 'rgba(0,174,236,0.15)' },
|
||||
qualityItemText: { color: '#ccc', fontSize: 14 },
|
||||
qualityItemTextActive: { color: '#00AEEC', fontWeight: '600' },
|
||||
fsModal: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
|
||||
Reference in New Issue
Block a user