jkvideo-bilibili-react-native
Expert skill for building and extending JKVideo, a React Native Bilibili-like client with DASH playback, danmaku, WBI signing, and live streaming
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install github:LeoYeAI~openclaw-master-skills~jkvideo-bilibili-react-nativecURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/github%3ALeoYeAI~openclaw-master-skills~jkvideo-bilibili-react-native/file -o jkvideo-bilibili-react-native.md# JKVideo Bilibili React Native Client
> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.
JKVideo is a full-featured third-party Bilibili client built with React Native 0.83 + Expo SDK 55. It supports DASH adaptive streaming, real-time danmaku (bullet comments), WBI API signing, QR code login, live streaming with WebSocket danmaku, and a download manager with LAN QR sharing.
---
## Installation & Setup
### Prerequisites
- Node.js 18+
- npm or yarn
- For Android: Android Studio + SDK
- For iOS: macOS + Xcode 15+
### Quick Start (Expo Go — no compilation)
```bash
git clone https://github.com/tiajinsha/JKVideo.git
cd JKVideo
npm install
npx expo start
```
Scan the QR with Expo Go app. Note: DASH 1080P+ requires Dev Build.
### Dev Build (Full Features — Recommended)
```bash
npm install
npx expo run:android # Android
npx expo run:ios # iOS (macOS + Xcode required)
```
### Web with Image Proxy
```bash
npm install
npx expo start --web
# In a separate terminal:
node scripts/proxy.js # Starts proxy on port 3001 to bypass Bilibili referer restrictions
```
### Install APK Directly (Android)
Download from [Releases](https://github.com/tiajinsha/JKVideo/releases/latest) — enable "Install from unknown sources" in Android settings.
---
## Project Structure
```
app/
index.tsx # Home (PagerView hot/live tabs)
video/[bvid].tsx # Video detail (playback + comments + danmaku)
live/[roomId].tsx # Live detail (HLS + real-time danmaku)
search.tsx # Search page
downloads.tsx # Download manager
settings.tsx # Settings (quality, logout)
components/ # UI: player, danmaku overlay, cards
hooks/ # Data hooks: video list, streams, danmaku
services/ # Bilibili API (axios + cookie interceptor)
store/ # Zustand stores: auth, download, video, settings
utils/ # Helpers: format, image proxy, MPD builder
```
---
## Key Technology Stack
| Layer | Technology |
|---|---|
| Framework | React Native 0.83 + Expo SDK 55 |
| Routing | expo-router v4 (file-system, Stack nav) |
| State | Zustand |
| HTTP | Axios |
| Storage | @react-native-async-storage/async-storage |
| Video | react-native-video (DASH MPD / HLS / MP4) |
| Fallback | react-native-webview (HTML5 video injection) |
| Paging | react-native-pager-view |
| Icons | @expo/vector-icons (Ionicons) |
---
## WBI Signature Implementation
Bilibili requires WBI signing for most API calls. JKVideo implements pure TypeScript MD5 with 12h nav cache.
```typescript
// utils/wbi.ts — pure TS MD5, no external crypto deps
const MIXIN_KEY_ENC_TAB = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35,
27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13
];
function getMixinKey(rawKey: string): string {
return MIXIN_KEY_ENC_TAB
.map(i => rawKey[i])
.join('')
.slice(0, 32);
}
export async function signWbi(
params: Record<string, string | number>,
imgKey: string,
subKey: string
): Promise<Record<string, string | number>> {
const mixinKey = getMixinKey(imgKey + subKey);
const wts = Math.floor(Date.now() / 1000);
const signParams = { ...params, wts };
// Sort params alphabetically, filter special chars
const query = Object.keys(signParams)
.sort()
.map(k => {
const val = String(signParams[k]).replace(/[!'()*]/g, '');
return `${encodeURIComponent(k)}=${encodeURIComponent(val)}`;
})
.join('&');
const wRid = md5(query + mixinKey); // pure TS md5
return { ...signParams, w_rid: wRid };
}
// Fetch and cache nav keys (12h TTL)
export async function getWbiKeys(): Promise<{ imgKey: string; subKey: string }> {
const cached = await AsyncStorage.getItem('wbi_keys');
if (cached) {
const { keys, ts } = JSON.parse(cached);
if (Date.now() - ts < 12 * 3600 * 1000) return keys;
}
const res = await api.get('/x/web-interface/nav');
const { img_url, sub_url } = res.data.data.wbi_img;
const imgKey = img_url.split('/').pop()!.replace('.png', '');
const subKey = sub_url.split('/').pop()!.replace('.png', '');
const keys = { imgKey, subKey };
await AsyncStorage.setItem('wbi_keys', JSON.stringify({ keys, ts: Date.now() }));
return keys;
}
```
---
## Bilibili API Service
```typescript
// services/api.ts
import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
export const api = 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',
},
});
// Inject SESSDATA cookie from store
api.interceptors.request.use(async (config) => {
const sessdata = await AsyncStorage.getItem('SESSDATA');
if (sessdata) {
config.headers['Cookie'] = `SESSDATA=${sessdata}`;
}
return config;
});
// Popular video list (WBI signed)
export async function getPopularVideos(pn = 1, ps = 20) {
const { imgKey, subKey } = await getWbiKeys();
const signed = await signWbi({ pn, ps }, imgKey, subKey);
const res = await api.get('/x/web-interface/popular', { params: signed });
return res.data.data.list;
}
// Video stream info (DASH)
export async function getVideoStream(bvid: string, cid: number, qn = 80) {
const { imgKey, subKey } = await getWbiKeys();
const signed = await signWbi(
{ bvid, cid, qn, fnval: 4048, fnver: 0, fourk: 1 },
imgKey, subKey
);
const res = await api.get('/x/player/wbi/playurl', { params: signed });
return res.data.data;
}
// Live stream URL
export async function getLiveStreamUrl(roomId: number) {
const res = await api.get('/room/v1/Room/playUrl', {
params: { cid: roomId, quality: 4, platform: 'h5' },
baseURL: 'https://api.live.bilibili.com',
});
return res.data.data.durl[0].url; // HLS m3u8
}
```
---
## DASH MPD Builder
ExoPlayer needs a local MPD file. JKVideo generates it from Bilibili's DASH response:
```typescript
// utils/buildDashMpd.ts
export function buildDashMpdUri(dashData: BiliDashData): string {
const { duration, video, audio } = dashData;
const videoAdaptations = video.map((v) => `
<AdaptationSet mimeType="video/mp4" segmentAlignment="true">
<Representation id="${v.id}" bandwidth="${v.bandwidth}"
codecs="${v.codecs}" width="${v.width}" height="${v.height}">
<BaseURL>${escapeXml(v.baseUrl)}</BaseURL>
<SegmentBase indexRange="${v.segmentBase.indexRange}">
<Initialization range="${v.segmentBase.initialization}"/>
</SegmentBase>
</Representation>
</AdaptationSet>`).join('');
const audioAdaptations = audio.map((a) => `
<AdaptationSet mimeType="audio/mp4" segmentAlignment="true">
<Representation id="${a.id}" bandwidth="${a.bandwidth}" codecs="${a.codecs}">
<BaseURL>${escapeXml(a.baseUrl)}</BaseURL>
<SegmentBase indexRange="${a.segmentBase.indexRange}">
<Initialization range="${a.segmentBase.initialization}"/>
</SegmentBase>
</Representation>
</AdaptationSet>`).join('');
const mpd = `<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" type="static"
mediaPresentationDuration="PT${duration}S" minBufferTime="PT1.5S">
<Period duration="PT${duration}S">
${videoAdaptations}
${audioAdaptations}
</Period>
</MPD>`;
// Write to temp file, return file:// URI for ExoPlayer
const path = `${FileSystem.cacheDirectory}dash_${Date.now()}.mpd`;
FileSystem.writeAsStringAsync(path, mpd);
return path;
}
```
---
## Video Player Component
```typescript
// components/VideoPlayer.tsx
import Video from 'react-native-video';
import { WebView } from 'react-native-webview';
import { useVideoStore } from '../store/videoStore';
interface VideoPlayerProps {
bvid: string;
cid: number;
autoPlay?: boolean;
}
export function VideoPlayer({ bvid, cid, autoPlay = false }: VideoPlayerProps) {
const [mpdUri, setMpdUri]