import React, { useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle, } from "react"; import { formatDuration } from "../utils/format"; import { View, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, Text, Modal, Image, PanResponder, ActivityIndicator, Animated, useWindowDimensions, } from "react-native"; import Video, { VideoRef } from "react-native-video"; import { LinearGradient } from "expo-linear-gradient"; import { Ionicons } from "@expo/vector-icons"; import type { PlayUrlResponse, VideoShotData, DanmakuItem, IVideoPlayer, } from "../services/types"; import { buildDashMpdUri } from "../utils/dash"; import { getVideoShot } from "../services/bilibili"; import DanmakuOverlay from "./DanmakuOverlay"; import { useTheme } from "../utils/theme"; import { usePlayProgressStore } from "../store/playProgressStore"; const BAR_H = 3; // 进度球尺寸 const BALL = 12; // 活跃状态下的拖动球增大尺寸,提升触控体验 const BALL_ACTIVE = 16; const HIDE_DELAY = 3000; const 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", }; function clamp(v: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, v)); } // function findFrameByTime(index: number[], seekTime: number): number { let lo = 0, hi = index.length - 1; while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (index[mid] <= seekTime) lo = mid; else hi = mid - 1; } return lo; } export interface NativeVideoPlayerRef extends IVideoPlayer { /** @deprecated 用 pause()/resume() 代替 */ setPaused: (v: boolean) => void; } interface Props { playData: PlayUrlResponse | null; qualities: { qn: number; desc: string }[]; currentQn: number; onQualityChange: (qn: number) => void; onFullscreen: () => void; style?: object; bvid?: string; cid?: number; danmakus?: DanmakuItem[]; isFullscreen?: boolean; onTimeUpdate?: (t: number) => void; initialTime?: number; forcePaused?: boolean; /** 点击播放器右上角"弹幕列表"图标,由外层打开 Sheet。仅在小窗口生效。 */ onDanmakuListPress?: () => void; /** 点击播放器左上角返回箭头,跟随播放器控制栏一起显隐。仅在小窗口生效。 */ onBack?: () => void; /** 视频封面 URL:作为 poster 覆盖在