mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
feat: unlock 1080P+ on Android via DASH streaming
- getPlayUrl uses fnval=16 (DASH) on Android, keeping fnval=0/html5 for iOS/web - New utils/dash.ts builds a valid DASH MPD from Bilibili's segmentBase ranges and returns it as a data: URI for ExoPlayer consumption - NativeVideoPlayer selects DASH path (type='mpd') or durl fallback automatically - Extend PlayUrlResponse types with DashVideoItem/DashAudioItem/DashSegmentBase
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import {
|
||||
View, StyleSheet, Dimensions, TouchableOpacity,
|
||||
Text, Modal, FlatList,
|
||||
Text, Modal,
|
||||
} from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import Video, { VideoRef } from 'react-native-video';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { buildMpd } from '../utils/buildMpd';
|
||||
import type { PlayUrlResponse } from '../services/types';
|
||||
import { buildDashDataUri } from '../utils/dash';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const VIDEO_HEIGHT = width * 0.5625;
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
playData: PlayUrlResponse | null;
|
||||
qualities: { qn: number; desc: string }[];
|
||||
@@ -19,76 +24,46 @@ interface Props {
|
||||
onFullscreen: () => void;
|
||||
onMiniPlayer?: () => void;
|
||||
style?: object;
|
||||
}
|
||||
|
||||
function buildMp4Html(url: string): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>* { margin:0; padding:0; box-sizing:border-box; background:#000; } video { width:100vw; height:100vh; object-fit:contain; display:block; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="v" controls autoplay playsinline webkit-playsinline></video>
|
||||
<script>document.getElementById('v').src = ${JSON.stringify(url)};</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function buildDashHtml(mpdStr: string): string {
|
||||
const mpdBase64 = `data:application/dash+xml;base64,${btoa(unescape(encodeURIComponent(mpdStr)))}`;
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>* { margin:0; padding:0; box-sizing:border-box; background:#000; } video { width:100vw; height:100vh; object-fit:contain; display:block; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="v" autoplay controls playsinline webkit-playsinline></video>
|
||||
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
|
||||
<script>
|
||||
var player = dashjs.MediaPlayer().create();
|
||||
player.initialize(document.getElementById('v'), ${JSON.stringify(mpdBase64)}, true);
|
||||
player.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false } } } });
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function getHtml(playData: PlayUrlResponse | null): string {
|
||||
if (!playData) return '<html><body style="background:#000"></body></html>';
|
||||
if (playData.dash) {
|
||||
const v = playData.dash.video[0];
|
||||
const a = playData.dash.audio[0];
|
||||
if (v && a) {
|
||||
const mpd = buildMpd(v.baseUrl, v.codecs, v.bandwidth, a.baseUrl, a.codecs, a.bandwidth);
|
||||
return buildDashHtml(mpd);
|
||||
}
|
||||
}
|
||||
const url = playData.durl?.[0]?.url;
|
||||
if (url) return buildMp4Html(url);
|
||||
return '<html><body style="background:#000"></body></html>';
|
||||
onProgress?: (currentTime: number, duration: number) => void;
|
||||
seekTo?: { t: number; v: number };
|
||||
}
|
||||
|
||||
export function NativeVideoPlayer({
|
||||
playData, qualities, currentQn, onQualityChange, onFullscreen, onMiniPlayer, style,
|
||||
onProgress, seekTo,
|
||||
}: Props) {
|
||||
const [showQuality, setShowQuality] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
const currentDesc = qualities.find(q => q.qn === currentQn)?.desc ?? (currentQn ? String(currentQn) : 'HD');
|
||||
const html = getHtml(playData);
|
||||
const isDash = !!playData?.dash;
|
||||
const url = isDash
|
||||
? buildDashDataUri(playData!, currentQn)
|
||||
: playData?.durl?.[0]?.url;
|
||||
|
||||
useEffect(() => {
|
||||
if (seekTo !== undefined) videoRef.current?.seek(seekTo.t);
|
||||
}, [seekTo]);
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<WebView
|
||||
key={html}
|
||||
source={{ html }}
|
||||
style={styles.webview}
|
||||
allowsInlineMediaPlayback
|
||||
mediaPlaybackRequiresUserAction={false}
|
||||
javaScriptEnabled
|
||||
originWhitelist={['*']}
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
{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} />
|
||||
)}
|
||||
|
||||
<View style={styles.controls}>
|
||||
<TouchableOpacity style={styles.ctrlBtn} onPress={() => setShowQuality(true)}>
|
||||
@@ -112,10 +87,7 @@ export function NativeVideoPlayer({
|
||||
<TouchableOpacity
|
||||
key={q.qn}
|
||||
style={styles.qualityItem}
|
||||
onPress={() => {
|
||||
setShowQuality(false);
|
||||
onQualityChange(q.qn);
|
||||
}}
|
||||
onPress={() => { setShowQuality(false); onQualityChange(q.qn); }}
|
||||
>
|
||||
<Text style={[styles.qualityItemText, q.qn === currentQn && styles.qualityItemActive]}>
|
||||
{q.desc}
|
||||
@@ -132,45 +104,15 @@ export function NativeVideoPlayer({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
||||
webview: { flex: 1, backgroundColor: '#000' },
|
||||
controls: {
|
||||
position: 'absolute',
|
||||
bottom: 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',
|
||||
},
|
||||
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' },
|
||||
qualityText: { color: '#fff', fontSize: 12, fontWeight: '600' },
|
||||
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,
|
||||
},
|
||||
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 },
|
||||
qualityTitle: { fontSize: 15, fontWeight: '700', color: '#212121', paddingVertical: 10, textAlign: 'center' },
|
||||
qualityItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: '#eee',
|
||||
},
|
||||
qualityItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 12, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#eee' },
|
||||
qualityItemText: { fontSize: 14, color: '#333' },
|
||||
qualityItemActive: { color: '#00AEEC', fontWeight: '700' },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user