2026-03-10 20:04:48 +08:00
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
2026-03-10 19:04:18 +08:00
|
|
|
import {
|
|
|
|
|
View, StyleSheet, Dimensions, TouchableOpacity,
|
2026-03-10 20:04:48 +08:00
|
|
|
Text, Modal,
|
2026-03-10 19:04:18 +08:00
|
|
|
} from 'react-native';
|
2026-03-10 20:04:48 +08:00
|
|
|
import Video, { VideoRef } from 'react-native-video';
|
2026-03-10 19:04:18 +08:00
|
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
|
|
|
import type { PlayUrlResponse } from '../services/types';
|
2026-03-10 20:04:48 +08:00
|
|
|
import { buildDashDataUri } from '../utils/dash';
|
2026-03-06 12:58:41 +08:00
|
|
|
|
|
|
|
|
const { width } = Dimensions.get('window');
|
|
|
|
|
const VIDEO_HEIGHT = width * 0.5625;
|
|
|
|
|
|
2026-03-10 20:04:48 +08:00
|
|
|
const BILIBILI_HEADERS = {
|
|
|
|
|
Referer: 'https://www.bilibili.com',
|
|
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-06 12:58:41 +08:00
|
|
|
interface Props {
|
2026-03-10 19:04:18 +08:00
|
|
|
playData: PlayUrlResponse | null;
|
|
|
|
|
qualities: { qn: number; desc: string }[];
|
|
|
|
|
currentQn: number;
|
|
|
|
|
onQualityChange: (qn: number) => void;
|
|
|
|
|
onFullscreen: () => void;
|
|
|
|
|
onMiniPlayer?: () => void;
|
|
|
|
|
style?: object;
|
2026-03-10 20:04:48 +08:00
|
|
|
onProgress?: (currentTime: number, duration: number) => void;
|
|
|
|
|
seekTo?: { t: number; v: number };
|
2026-03-10 19:04:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function NativeVideoPlayer({
|
|
|
|
|
playData, qualities, currentQn, onQualityChange, onFullscreen, onMiniPlayer, style,
|
2026-03-10 20:04:48 +08:00
|
|
|
onProgress, seekTo,
|
2026-03-10 19:04:18 +08:00
|
|
|
}: Props) {
|
|
|
|
|
const [showQuality, setShowQuality] = useState(false);
|
2026-03-10 20:04:48 +08:00
|
|
|
const videoRef = useRef<VideoRef>(null);
|
2026-03-10 19:04:18 +08:00
|
|
|
const currentDesc = qualities.find(q => q.qn === currentQn)?.desc ?? (currentQn ? String(currentQn) : 'HD');
|
2026-03-10 20:04:48 +08:00
|
|
|
const isDash = !!playData?.dash;
|
|
|
|
|
const url = isDash
|
|
|
|
|
? buildDashDataUri(playData!, currentQn)
|
|
|
|
|
: playData?.durl?.[0]?.url;
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (seekTo !== undefined) videoRef.current?.seek(seekTo.t);
|
|
|
|
|
}, [seekTo]);
|
2026-03-06 12:58:41 +08:00
|
|
|
|
|
|
|
|
return (
|
2026-03-10 19:04:18 +08:00
|
|
|
<View style={[styles.container, style]}>
|
2026-03-10 20:04:48 +08:00
|
|
|
{url ? (
|
|
|
|
|
<Video
|
|
|
|
|
ref={videoRef}
|
|
|
|
|
source={isDash
|
|
|
|
|
? { uri: url, type: 'mpd', headers: BILIBILI_HEADERS }
|
|
|
|
|
: { uri: url, headers: BILIBILI_HEADERS }
|
|
|
|
|
}
|
|
|
|
|
style={styles.video}
|
|
|
|
|
resizeMode="contain"
|
|
|
|
|
controls
|
|
|
|
|
paused={false}
|
|
|
|
|
onProgress={({ currentTime, seekableDuration }) =>
|
|
|
|
|
onProgress?.(currentTime, seekableDuration)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<View style={styles.placeholder} />
|
|
|
|
|
)}
|
2026-03-10 19:04:18 +08:00
|
|
|
|
|
|
|
|
<View style={styles.controls}>
|
|
|
|
|
<TouchableOpacity style={styles.ctrlBtn} onPress={() => setShowQuality(true)}>
|
|
|
|
|
<Text style={styles.qualityText}>{currentDesc}</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
|
|
|
|
<Ionicons name="expand" size={18} color="#fff" />
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
{onMiniPlayer && (
|
|
|
|
|
<TouchableOpacity style={styles.ctrlBtn} onPress={onMiniPlayer}>
|
|
|
|
|
<Ionicons name="tablet-portrait-outline" size={18} color="#fff" />
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
<Modal visible={showQuality} transparent animationType="fade">
|
|
|
|
|
<TouchableOpacity style={styles.modalOverlay} onPress={() => setShowQuality(false)}>
|
|
|
|
|
<View style={styles.qualityList}>
|
|
|
|
|
<Text style={styles.qualityTitle}>选择清晰度</Text>
|
|
|
|
|
{qualities.map(q => (
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
key={q.qn}
|
|
|
|
|
style={styles.qualityItem}
|
2026-03-10 20:04:48 +08:00
|
|
|
onPress={() => { setShowQuality(false); onQualityChange(q.qn); }}
|
2026-03-10 19:04:18 +08:00
|
|
|
>
|
|
|
|
|
<Text style={[styles.qualityItemText, q.qn === currentQn && styles.qualityItemActive]}>
|
|
|
|
|
{q.desc}
|
|
|
|
|
</Text>
|
|
|
|
|
{q.qn === currentQn && <Ionicons name="checkmark" size={16} color="#00AEEC" />}
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
))}
|
|
|
|
|
</View>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</Modal>
|
2026-03-06 12:58:41 +08:00
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
|
container: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
2026-03-10 20:04:48 +08:00
|
|
|
video: { flex: 1 },
|
|
|
|
|
placeholder: { flex: 1, backgroundColor: '#000' },
|
|
|
|
|
controls: { position: 'absolute', top: 8, right: 8, flexDirection: 'row', gap: 8 },
|
|
|
|
|
ctrlBtn: { backgroundColor: 'rgba(0,0,0,0.55)', borderRadius: 4, paddingHorizontal: 8, paddingVertical: 4, alignItems: 'center', justifyContent: 'center' },
|
2026-03-10 19:04:18 +08:00
|
|
|
qualityText: { color: '#fff', fontSize: 12, fontWeight: '600' },
|
2026-03-10 20:04:48 +08:00
|
|
|
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center' },
|
|
|
|
|
qualityList: { backgroundColor: '#fff', borderRadius: 12, paddingVertical: 8, paddingHorizontal: 16, minWidth: 180 },
|
2026-03-10 19:04:18 +08:00
|
|
|
qualityTitle: { fontSize: 15, fontWeight: '700', color: '#212121', paddingVertical: 10, textAlign: 'center' },
|
2026-03-10 20:04:48 +08:00
|
|
|
qualityItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#eee' },
|
2026-03-10 19:04:18 +08:00
|
|
|
qualityItemText: { fontSize: 14, color: '#333' },
|
|
|
|
|
qualityItemActive: { color: '#00AEEC', fontWeight: '700' },
|
2026-03-06 12:58:41 +08:00
|
|
|
});
|