mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
直播弹幕与界面优化
- useLiveDanmaku: 重写协议解析,支持 zlib 压缩消息、token 认证、base64 帧处理 - services/bilibili: 新增 getLiveDanmakuInfo 获取弹幕服务器信息 - live/[roomId]: 使用真实 roomid 而非 URL 别名连接弹幕 - NativeVideoPlayer: 增加中文注释,移除调试日志 - video/[bvid]: UP主头像移至标题前,调整字号与样式 - index: 替换 logo 文字为下载按钮图标 - videoRows: 修复奇数视频配对,调整 BigRow 位置策略 - package.json: 移除未使用的 fast-xml-parser 和 xml2js 依赖
This commit is contained in:
@@ -39,10 +39,10 @@ const HEADERS = {
|
||||
function clamp(v: number, lo: number, hi: number) {
|
||||
return Math.max(lo, Math.min(hi, v));
|
||||
}
|
||||
|
||||
// Binary search in shots index to find frame number for given seek time
|
||||
//
|
||||
function findFrameByTime(index: number[], seekTime: number): number {
|
||||
let lo = 0, hi = index.length - 1;
|
||||
let lo = 0,
|
||||
hi = index.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1;
|
||||
if (index[mid] <= seekTime) lo = mid;
|
||||
@@ -51,7 +51,6 @@ function findFrameByTime(index: number[], seekTime: number): number {
|
||||
return lo;
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
playData: PlayUrlResponse | null;
|
||||
qualities: { qn: number; desc: string }[];
|
||||
@@ -117,7 +116,7 @@ export function NativeVideoPlayer({
|
||||
qualities.find((q) => q.qn === currentQn)?.desc ??
|
||||
String(currentQn || "HD");
|
||||
|
||||
// URL resolution
|
||||
// 解析播放链接,dash 需要构建 mpd uri,普通链接直接取第一个 durl。使用 useEffect 监听 playData 和 currentQn 变化,确保每次切换视频或清晰度时都能正确更新播放链接。错误处理逻辑保证即使 dash mpd 构建失败也能回退到普通链接,提升兼容性。
|
||||
useEffect(() => {
|
||||
if (!playData) {
|
||||
setResolvedUrl(undefined);
|
||||
@@ -131,8 +130,7 @@ export function NativeVideoPlayer({
|
||||
setResolvedUrl(playData.durl?.[0]?.url);
|
||||
}
|
||||
}, [playData, currentQn]);
|
||||
|
||||
// Video shots (thumbnails for seek preview)
|
||||
// 获取视频截图数据,供进度条预览使用。依赖 bvid 和 cid,确保在视频切换时重新获取截图。使用 cancelled 标志避免在组件卸载后更新状态,防止内存泄漏和潜在的错误。
|
||||
useEffect(() => {
|
||||
if (!bvid || !cid) return;
|
||||
let cancelled = false;
|
||||
@@ -140,7 +138,6 @@ export function NativeVideoPlayer({
|
||||
if (cancelled) return;
|
||||
if (shotData?.image?.length) {
|
||||
setShots(shotData);
|
||||
console.log(shotData.index,shotData.image,'缩略图长度')
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
@@ -152,18 +149,20 @@ export function NativeVideoPlayer({
|
||||
durationRef.current = duration;
|
||||
}, [duration]);
|
||||
|
||||
// 控制栏自动隐藏逻辑:每次用户交互后重置计时器,3秒无交互则隐藏。使用 useRef 存储计时器 ID 和拖动状态,避免闭包问题导致的计时器失效或误触发。
|
||||
const resetHideTimer = useCallback(() => {
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||
if (!isSeekingRef.current) {
|
||||
hideTimer.current = setTimeout(() => setShowControls(false), HIDE_DELAY);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 显示控制栏并重置隐藏计时器,确保用户每次交互后都有足够时间查看控制栏。依赖 resetHideTimer 保持稳定引用,避免不必要的重新渲染。
|
||||
const showAndReset = useCallback(() => {
|
||||
setShowControls(true);
|
||||
resetHideTimer();
|
||||
}, [resetHideTimer]);
|
||||
|
||||
// 点击视频区域切换控制栏显示状态,显示时重置隐藏计时器,隐藏时直接隐藏。使用 useCallback 优化性能,避免不必要的函数重新创建。
|
||||
const handleTap = useCallback(() => {
|
||||
setShowControls((prev) => {
|
||||
if (!prev) {
|
||||
@@ -175,7 +174,7 @@ export function NativeVideoPlayer({
|
||||
});
|
||||
}, [resetHideTimer]);
|
||||
|
||||
// Start hide timer on mount
|
||||
// 组件卸载时清理隐藏计时器,避免内存泄漏和潜在的状态更新错误。依赖项为空数组确保只在挂载和卸载时执行一次。
|
||||
useEffect(() => {
|
||||
resetHideTimer();
|
||||
return () => {
|
||||
@@ -191,7 +190,7 @@ export function NativeVideoPlayer({
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 使用 PanResponder 实现进度条拖动,支持在拖动过程中显示预览图。通过 touchXRef 和 rafRef 优化拖动性能,避免频繁更新状态导致的卡顿。用户松开拖动时,根据最终位置计算对应的时间点并跳转,同时清理状态和隐藏预览图。
|
||||
const panResponder = useRef(
|
||||
PanResponder.create({
|
||||
onStartShouldSetPanResponder: () => true,
|
||||
@@ -218,6 +217,7 @@ export function NativeVideoPlayer({
|
||||
});
|
||||
}
|
||||
},
|
||||
// 用户松开拖动,或拖动被中断(如来电),都视为结束拖动,需要清理状态和隐藏预览
|
||||
onPanResponderRelease: (_, gs) => {
|
||||
if (rafRef.current) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
@@ -253,7 +253,7 @@ export function NativeVideoPlayer({
|
||||
},
|
||||
}),
|
||||
).current;
|
||||
|
||||
// 进度条上触摸位置对应的时间点比例,0-1。非拖动状态为 null
|
||||
const touchRatio =
|
||||
touchX !== null ? clamp(touchX / barWidthRef.current, 0, 1) : null;
|
||||
const progressRatio = duration > 0 ? clamp(currentTime / duration, 0, 1) : 0;
|
||||
@@ -274,32 +274,37 @@ export function NativeVideoPlayer({
|
||||
const framesPerSheet = img_x_len * img_y_len;
|
||||
const totalFrames = framesPerSheet * image.length;
|
||||
const seekTime = touchRatio * duration;
|
||||
// index[i] = timestamp of frame i (seconds). Binary search returns position i (= frame number).
|
||||
// Cap to index.length-1 to avoid black padding frames beyond valid content.
|
||||
const frameIdx = index?.length && duration > 0
|
||||
? clamp(findFrameByTime(index, seekTime), 0, index.length - 1)
|
||||
: clamp(Math.floor(touchRatio * (totalFrames - 1)), 0, totalFrames - 1);
|
||||
// 通过时间戳索引找到最接近的帧,若无索引则均匀映射到总帧数上
|
||||
const frameIdx =
|
||||
index?.length && duration > 0
|
||||
? clamp(findFrameByTime(index, seekTime), 0, index.length - 1)
|
||||
: clamp(Math.floor(touchRatio * (totalFrames - 1)), 0, totalFrames - 1);
|
||||
|
||||
const sheetIdx = Math.floor(frameIdx / framesPerSheet);
|
||||
const local = frameIdx % framesPerSheet;
|
||||
const col = local % img_x_len;
|
||||
const row = Math.floor(local / img_x_len);
|
||||
|
||||
console.log('[thumb]', { seekTime, duration, indexLen: index?.length, frameIdx, totalFrames, sheetIdx, col, row });
|
||||
|
||||
// Scale sprite frame to display size
|
||||
console.log("[thumb]", {
|
||||
seekTime,
|
||||
duration,
|
||||
indexLen: index?.length,
|
||||
frameIdx,
|
||||
totalFrames,
|
||||
sheetIdx,
|
||||
col,
|
||||
row,
|
||||
});
|
||||
// 根据单帧图尺寸和预设的显示宽度计算缩放后的显示尺寸,保持宽高比
|
||||
const scale = THUMB_DISPLAY_W / TW;
|
||||
const DW = THUMB_DISPLAY_W;
|
||||
const DH = Math.round(TH * scale);
|
||||
|
||||
const trackLeft = barOffsetX.current;
|
||||
const absLeft = clamp(trackLeft + (touchX ?? 0) - DW / 2, 0, SCREEN_W - DW);
|
||||
|
||||
// Protocol-relative URLs from B站 API need explicit https:
|
||||
// 兼容处理图床地址,确保以 http(s) 协议开头
|
||||
const sheetUrl = image[sheetIdx].startsWith("//")
|
||||
? `https:${image[sheetIdx]}`
|
||||
: image[sheetIdx];
|
||||
console.log(sheetUrl,'sheetUrlsheetUrl')
|
||||
return (
|
||||
<View
|
||||
style={[styles.thumbPreview, { left: absLeft, width: DW }]}
|
||||
@@ -378,7 +383,6 @@ export function NativeVideoPlayer({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Permanent transparent tap layer — always above Video so taps always reach it */}
|
||||
<TouchableWithoutFeedback onPress={handleTap}>
|
||||
<View style={StyleSheet.absoluteFill} />
|
||||
</TouchableWithoutFeedback>
|
||||
@@ -401,7 +405,6 @@ export function NativeVideoPlayer({
|
||||
)}
|
||||
</LinearGradient>
|
||||
|
||||
{/* Center play/pause */}
|
||||
<TouchableOpacity
|
||||
style={styles.centerBtn}
|
||||
onPress={() => {
|
||||
@@ -418,13 +421,11 @@ export function NativeVideoPlayer({
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<LinearGradient
|
||||
colors={["transparent", "rgba(0,0,0,0.7)"]}
|
||||
style={styles.bottomBar}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
{/* Progress track */}
|
||||
<View
|
||||
ref={trackRef}
|
||||
style={styles.trackWrapper}
|
||||
@@ -432,7 +433,6 @@ export function NativeVideoPlayer({
|
||||
{...panResponder.panHandlers}
|
||||
>
|
||||
<View style={styles.track}>
|
||||
{/* Buffered: lighter white overlay on top of base */}
|
||||
<View
|
||||
style={[
|
||||
styles.trackLayer,
|
||||
@@ -442,7 +442,6 @@ export function NativeVideoPlayer({
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* Played: accent color on top */}
|
||||
<View
|
||||
style={[
|
||||
styles.trackLayer,
|
||||
@@ -470,8 +469,8 @@ export function NativeVideoPlayer({
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{/* Controls */}
|
||||
|
||||
{/* Controls row */}
|
||||
<View style={styles.ctrlRow}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
@@ -517,10 +516,8 @@ export function NativeVideoPlayer({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Thumbnail preview — absolute on container to avoid clipping */}
|
||||
{renderThumbnail()}
|
||||
|
||||
{/* Quality modal */}
|
||||
{/* 选清晰度 */}
|
||||
<Modal visible={showQuality} transparent animationType="fade">
|
||||
<TouchableOpacity
|
||||
style={styles.modalOverlay}
|
||||
|
||||
Reference in New Issue
Block a user