前端开发
在 React Native/Expo 中构建类…
适用于基于 React Native + Expo 开发第三方 Bilibili 客户端,需要实现 DASH 播放、弹幕、WBI 签名、扫码登录、直播弹幕和下载分享的场景。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 React Native/Expo 特定
解决思路
- 克隆并启动项目:git clone https://github.com/tiajinsha/JKVideo.git;cd JKVideo;npm install;npx expo start;用 Expo Go 扫码。注意 DASH 1080P+ 需要 Dev Build。
- 完整功能用 Dev Build:npm install;npx expo run:android 或 npx expo run:ios(iOS 需 macOS + Xcode)。
- Web 端运行:npm install;npx expo start --web;另开终端 node scripts/proxy.js,代理监听 3001 端口,绕过 Bilibili referer 限制。
- Android 直接安装 APK:从 Releases 下载,在 Android 设置中开启 Install from unknown sources。
- 按项目结构组织代码:app/index.tsx 首页 PagerView 热门/直播 tab;app/video/[bvid].tsx 视频详情(播放+评论+弹幕);app/live/[roomId].tsx 直播详情(HLS+实时弹幕);app/search.tsx 搜索;app/downloads.tsx 下载管理;app/settings.tsx 设置(画质、退出登录);components/ 放播放器、弹幕覆盖层、卡片;hooks/ 放视频列表、流、弹幕数据 hooks;services/ 放 Bilibili API(axios + cookie 拦截器);store/ 放 Zustand stores:auth、download、video、settings;utils/ 放 helpers:format、image proxy、MPD builder。
- 使用技术栈:React Native 0.83 + Expo SDK 55;expo-router v4 文件系统路由和 Stack 导航;Zustand 状态;Axios HTTP;@react-native-async-storage/async-storage 存储;react-native-video 播放 DASH MPD/HLS/MP4;react-native-webview 作为 HTML5 video 注入 fallback;react-native-pager-view 分页;@expo/vector-icons 的 Ionicons 图标。
- 实现 WBI 签名:在 utils/wbi.ts 使用纯 TypeScript MD5,不依赖外部 crypto;定义 MIXIN_KEY_ENC_TAB 数组;getMixinKey(rawKey) 按表映射字符、join、slice(0,32)。
- 实现 signWbi(params, imgKey, subKey):mixinKey = getMixinKey(imgKey + subKey);wts = Math.floor(Date.now() / 1000);signParams = { ...params, wts };按 key 字母排序;对值用 replace(/[!'()*]/g, '') 过滤特殊字符;用 encodeURIComponent 编码 key 和 value,拼成 query;w_rid = md5(query + mixinKey);返回 { ...signParams, w_rid }。
- 实现 getWbiKeys():从 AsyncStorage 读取 wbi_keys,若缓存未超过 12 小时则返回;否则请求 /x/web-interface/nav,从 res.data.data.wbi_img 的 img_url 和 sub_url 中取出文件名,去掉 .png 得到 imgKey 和 subKey;将 { keys, ts: Date.now() } 写入 AsyncStorage 并返回。
- 创建 Bilibili API 服务:services/api.ts 中用 axios.create({ baseURL: 'https://api.bilibili.com', timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36', 'Referer': 'https://www.bilibili.com' } });在请求拦截器中从 AsyncStorage 读取 SESSDATA,若存在则设置 config.headers['Cookie'] = `SESSDATA=${sessdata}`。
- 获取热门视频:getPopularVideos(pn = 1, ps = 20) 先 getWbiKeys(),再 signWbi({ pn, ps }, imgKey, subKey),GET /x/web-interface/popular,返回 res.data.data.list。
- 获取视频流:getVideoStream(bvid, cid, qn = 80) 先 getWbiKeys(),再 signWbi({ bvid, cid, qn, fnval: 4048, fnver: 0, fourk: 1 }, imgKey, subKey),GET /x/player/wbi/playurl,返回 res.data.data。
- 获取直播流 URL:getLiveStreamUrl(roomId) 请求 baseURL https://api.live.bilibili.com 的 /room/v1/Room/playUrl,params { cid: roomId, quality: 4, platform: 'h5' },返回 res.data.data.durl[0].url(HLS m3u8)。
- 构建 DASH MPD:在 utils/buildDashMpd.ts 实现 buildDashMpdUri(dashData):从 dashData 取 duration、video、audio;为每个 video 生成 AdaptationSet mimeType='video/mp4' segmentAlignment='true',内含 Representation id、bandwidth、codecs、width、height,BaseURL,SegmentBase indexRange,Initialization range;为每个 audio 生成 AdaptationSet mimeType='audio/mp4',同结构;拼成 MPD XML,type='static',mediaPresentationDuration='PT${duration}S',minBufferTime='PT1.5S',Period duration='PT${duration}S';写入 FileSystem.cacheDirectory 下 dash_${Date.now()}.mpd,返回 file:// URI 给 ExoPlayer。
- 实现 VideoPlayer 组件:props 为 bvid、cid、autoPlay;useEffect 调用 loadStream;调用 getVideoStream(bvid, cid);若 stream.dash 存在,await buildDashMpdUri(stream.dash) 得到 mpdUri;否则 setUseFallback(true);catch 也 setUseFallback(true);若 fallback,返回 WebView,source={{ uri: `https://www.bilibili.com/video/${bvid}` }},allowsInlineMediaPlayback,mediaPlaybackRequiresUserAction={false};否则返回 react-native-video 的 Video,source={{ uri: mpdUri }},style={{ width: '100%', aspectRatio: 16 / 9 }},controls,paused={!autoPlay},resizeMode='contain',onLoad 时 setCurrentVideo({ bvid, cid })。
- 实现视频弹幕 useDanmaku(cid):useEffect 调用 fetchDanmaku(cid);GET /x/v1/dm/list.so?oid=${cid},responseType='arraybuffer';用 new TextDecoder('utf-8').decode(res.data) 得到 XML;用 parseXmlDanmaku(xml) 解析 <d p='time,...'>text</d>;setDanmakuList(items);返回 danmakuList。
- 实现 DanmakuOverlay:LANE_COUNT = 5;过滤 danmakuList 中 d.time >= currentTime - 0.1 且 d.time < currentTime + 0.1;用 View style={StyleSheet.absoluteFillObject} pointerEvents='none' 包裹;每个弹幕渲染 DanmakuItem,传入 text、color、lane={i % LANE_COUNT}。
- 实现直播弹幕 useLiveDanmaku(roomId):连接 wss://broadcastlv.chat.bilibili.com/sub;onopen 时 ws.send(buildPacket(JSON.stringify({ roomid: roomId, uid: 0, protover: 2 }), 7)),并 startHeartbeat(ws);onmessage 时用 decompressPacket(event.data) 解压,遍历 packets,若 packet.op === 5,则 JSON.parse(packet.body) 后 handleCommand(msg);cleanup 时 ws.close()。
- 在直播弹幕 handleCommand 中处理消息:switch (msg.cmd);DANMU_MSG 时 setMessages(prev => [{ type: 'danmaku', text: msg.info[1], user: msg.info[2][1], isGuard: msg.info[7] > 0 }, ...prev].slice(0, 200));SEND_GIFT 时 setMessages(prev => [{ type: 'gift', user: msg.data.uname, gift: msg.data.giftName, count: msg.data.num }, ...prev].slice(0, 200))。
- 创建 Zustand videoStore:create<VideoState>;state 包含 currentVideo、isMiniplayer、quality(80=1080P,112=1080P+,120=4K);方法 setCurrentVideo、setMiniplayer、setQuality。
- 创建 Zustand authStore:create<AuthState>;state 包含 sessdata、isLoggedIn;login(sessdata) 先 AsyncStorage.setItem('SESSDATA', sessdata),再 set({ sessdata, isLoggedIn: true });logout() 先 AsyncStorage.removeItem('SESSDATA'),再 set({ sessdata: null, isLoggedIn: false })。
- 实现二维码登录 useQrLogin:generateQr() 请求 /x/passport-login/web/qrcode/generate,取 url 和 qrcode_key,setQrUrl、setQrKey,并 startPolling(qrcode_key);startPolling 每 2000ms 请求 /x/passport-login/web/qrcode/poll,params { qrcode_key: key };若 code === 0,从 res.headers['set-cookie'] 中找包含 SESSDATA= 的项,用 /SESSDATA=([^;]+)/ 提取 sessdata,await login(sessdata),clearInterval;组件卸载时 clearInterval。
- 实现下载管理 downloadStore:startDownload(bvid, quality) 先 getVideoStream(bvid, 0, quality);url = stream.durl?.[0]?.url ?? stream.dash?.video?.[0]?.baseUrl;path = `${FileSystem.documentDirectory}downloads/${bvid}_${quality}.mp4`;createDownloadResumable(url, path, { headers: { Referer: 'https://www.bilibili.com' } }, progress => 更新 progress = progress.totalBytesWritten / progress.totalBytesExpectedToWrite);把 { bvid, quality, path, progress: 0, task } 加入 downloads;await task.downloadAsync()。
- 实现局域网二维码分享:用 http.createServer 服务已下载文件;用 os.networkInterfaces() 遍历,返回第一个 family === 'IPv4' 且 !net.internal 的 net.address,否则返回 'localhost';用该 IP 生成二维码分享。
- 使用 expo-router 导航:app/video/[bvid].tsx 中用 useLocalSearchParams<{ bvid: string }>() 取 bvid;跳转用 router.push(`/video/${bvid}`)、router.push(`/live/${roomId}`);返回用 router.back()。
- Web 图片代理:运行 scripts/proxy.js(3001 端口);proxyImage(url) 中若 Platform.OS === 'web',返回 `http://localhost:3001/proxy?url=${encodeURIComponent(url)}`;原生直接返回 url。
- 画质代码参考:16=360P,32=480P,64=720P,80=1080P,112=1080P+(大会员),116=1080P60(大会员),120=4K(大会员)。
- 添加新 API endpoint:在 services/api.ts 写函数,例如 getVideoInfo(bvid):getWbiKeys();signWbi({ bvid }, imgKey, subKey);GET /x/web-interface/view,params 为 signed;返回 res.data.data。
- 添加新屏幕:在 app/history.tsx 创建默认导出组件,使用 <Stack.Screen options={{ title: '历史记录' }} />,该文件自动成为 /history 路由。
- 创建持久化 Zustand slice:store/settingsStore.ts 用 create(persist((set) => ({ defaultQuality: 80, danmakuEnabled: true, setDefaultQuality: (q) => set({ defaultQuality: q }), toggleDanmaku: () => set(s => ({ danmakuEnabled: !s.danmakuEnabled })) }), { name: 'jkvideo-settings', storage: createJSONStorage(() => AsyncStorage) }))。
- 排错:DASH 在 Expo Go 不播放用 Dev Build:npx expo run:android;Web 图片不加载运行 node scripts/proxy.js 并确保 web 使用代理 URL;API 412/风控确保 WBI 签名新鲜,清 AsyncStorage 缓存 keys;4K/1080P+ 需登录大会员;直播失败自动选 HLS,不支持 FLV;二维码过期关闭并重开登录模态;Cookie 不持久检查 AsyncStorage 权限,SESSDATA key 必须匹配拦截器;WebSocket 弹幕断连增加心跳频率,检查 packet 解压(zlib);iOS 构建失败运行 cd ios && pod install 后重建。
- 已知限制:动态流/发布/点赞需要 bili_jct CSRF token,尚未实现;不支持 FLV 直播,自动选 HLS fallback;Web 平台图片需本地代理服务绕过 Referer 限制;4K/1080P+ 需要登录 Bilibili 大会员;二维码 10 分钟后过期,重开模态刷新。
适用边界 · 注意事项
- 不适用于非 React Native/Expo 项目,或不想依赖 Bilibili 私有 API 与 Bilibili 服务的项目。
- 不支持 FLV 直播,且动态流/发布/点赞等需要 bili_jct CSRF token 的功能未实现。
- 4K/1080P+ 需要登录 Bilibili 大会员;Web 端图片加载需本地代理,二维码 10 分钟后会过期。