本项目已收到哔哩哔哩(bilibili)律师函,要求停止对 B 站 API 的调用及相关仿制行为。

为尊重知识产权及相关法律法规,本仓库即日起停止后续维护与更新,不再接受新的 Issue 和 Pull Request。

现有代码仅作学习参考保留,请勿将本项目用于任何商业或违法用途。

感谢所有支持过本项目的朋友。
This commit is contained in:
Developer
2026-05-12 21:29:45 +08:00
parent 53c67079a1
commit 6e0dc2729c
16 changed files with 1304 additions and 453 deletions

View File

@@ -1,10 +1,12 @@
import React, { useState, useRef, useEffect } from 'react';
import { View, StyleSheet, Text, Platform, Modal, StatusBar, useWindowDimensions } from 'react-native';
import { Image } from 'expo-image';
// expo-screen-orientation requires a dev build; gracefully degrade in Expo Go
let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
try { ScreenOrientation = require('expo-screen-orientation'); } catch {}
import { NativeVideoPlayer, type NativeVideoPlayerRef } from './NativeVideoPlayer';
import type { PlayUrlResponse, DanmakuItem } from '../services/types';
import { useTheme } from '../utils/theme';
interface Props {
playData: PlayUrlResponse | null;
@@ -16,15 +18,22 @@ interface Props {
danmakus?: DanmakuItem[];
onTimeUpdate?: (t: number) => void;
initialTime?: number;
/** 详情页将其挂到弹幕 Sheet 的打开动作。仅小窗口生效,全屏暂不显示。 */
onDanmakuListPress?: () => void;
/** 播放器左上角返回按钮回调,跟随小窗口控制栏一起显隐。 */
onBack?: () => void;
/** 视频封面 URL。playData 未到达 / <Video> 首帧未到达时用作 poster避免黑闪 */
coverUrl?: string;
}
export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, bvid, cid, danmakus, onTimeUpdate, initialTime }: Props) {
export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, bvid, cid, danmakus, onTimeUpdate, initialTime, onDanmakuListPress, onBack, coverUrl }: Props) {
const [fullscreen, setFullscreen] = useState(false);
const { width, height } = useWindowDimensions();
const VIDEO_HEIGHT = width * 0.5625;
const needsRotation = !ScreenOrientation && fullscreen;
const lastTimeRef = useRef(0);
const seededRef = useRef(false);
const theme = useTheme();
// 续播:第一次拿到 initialTime 时塞进 lastTimeRef
if (!seededRef.current && typeof initialTime === 'number' && initialTime > 0) {
lastTimeRef.current = initialTime;
@@ -52,9 +61,13 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
}, []);
if (!playData) {
// 没拿到 playData 时背景用主题色(与页面同色,消除"页面→纯黑"撞色),有封面则铺封面
return (
<View style={[{ width, height: VIDEO_HEIGHT, backgroundColor: '#000' }, styles.placeholder]}>
<Text style={styles.placeholderText}>...</Text>
<View style={[{ width, height: VIDEO_HEIGHT, backgroundColor: theme.card }, styles.placeholder]}>
{!!coverUrl && (
<Image source={{ uri: coverUrl }} style={StyleSheet.absoluteFill} contentFit="cover" />
)}
<Text style={[styles.placeholderText, { color: coverUrl ? '#fff' : theme.textSub }]}>...</Text>
</View>
);
}
@@ -65,6 +78,7 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
<View style={{ width, height: VIDEO_HEIGHT, backgroundColor: '#000' }}>
<video
src={url}
poster={coverUrl}
style={{ width: '100%', height: '100%', backgroundColor: '#000' } as any}
controls
playsInline
@@ -89,6 +103,9 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
isFullscreen={false}
initialTime={lastTimeRef.current}
onTimeUpdate={(t) => { lastTimeRef.current = t; onTimeUpdate?.(t); }}
onDanmakuListPress={onDanmakuListPress}
onBack={onBack}
coverUrl={coverUrl}
/>
)}
@@ -112,6 +129,7 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
isFullscreen={true}
initialTime={lastTimeRef.current}
onTimeUpdate={(t) => { lastTimeRef.current = t; onTimeUpdate?.(t); }}
coverUrl={coverUrl}
style={needsRotation ? { width: height, height: width } : { flex: 1 }}
/>
</View>
@@ -124,5 +142,5 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
const styles = StyleSheet.create({
placeholder: { justifyContent: 'center', alignItems: 'center' },
placeholderText: { color: '#fff', fontSize: 14 },
placeholderText: { fontSize: 14 },
});