jkvideo-bilibili-react-native

GitHub 作者 LeoYeAI/openclaw-master-skills

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-native
cURL直接下载,无需登录
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]