mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 07:28:37 +08:00
277 lines
8.1 KiB
Markdown
277 lines
8.1 KiB
Markdown
|
|
# DASH 1080P Playback Implementation Plan
|
|||
|
|
|
|||
|
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|||
|
|
|
|||
|
|
**Goal:** 在 Android 上解锁 1080P+ 画质,通过切换至 DASH 格式并生成合法 MPD data URI 传给 ExoPlayer。
|
|||
|
|
|
|||
|
|
**Architecture:** `getPlayUrl` 按平台分叉——Android 使用 `fnval=16` 获取 DASH JSON(含分离的 video/audio 流及 segmentBase 范围);iOS/Web 保持 `fnval=0,platform=html5`(720P MP4)。DASH 数据在 `utils/dash.ts` 中组装成标准 MPD XML,经 `btoa` 编码为 `data:` URI,传给 `react-native-video` 的 ExoPlayer 播放。
|
|||
|
|
|
|||
|
|
**Tech Stack:** React Native 0.83, Expo SDK 55, react-native-video 6.x (ExoPlayer on Android), TypeScript
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## File Map
|
|||
|
|
|
|||
|
|
| 文件 | 操作 | 说明 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `services/types.ts` | Modify | 扩展 dash video/audio 条目类型,加 segmentBase/width/height/mimeType/frameRate |
|
|||
|
|
| `services/bilibili.ts` | Modify | `getPlayUrl` 按 Platform.OS 分叉 fnval |
|
|||
|
|
| `utils/dash.ts` | **Create** | `buildDashDataUri(playData, qn): string` |
|
|||
|
|
| `components/NativeVideoPlayer.tsx` | Modify | 有 dash 时用 data URI + `type="mpd"`,否则走原 durl 路径 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 1: 扩展 PlayUrlResponse 类型
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `services/types.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 更新 dash video/audio 条目类型,加入 B 站实际返回字段**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// services/types.ts — 替换现有 PlayUrlResponse
|
|||
|
|
|
|||
|
|
export interface DashSegmentBase {
|
|||
|
|
Initialization: string; // e.g. "0-938"
|
|||
|
|
indexRange: string; // e.g. "939-2694"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface DashVideoItem {
|
|||
|
|
id: number;
|
|||
|
|
baseUrl: string;
|
|||
|
|
bandwidth: number;
|
|||
|
|
mimeType: string; // "video/mp4"
|
|||
|
|
codecs: string; // "avc1.640028"
|
|||
|
|
width: number;
|
|||
|
|
height: number;
|
|||
|
|
frameRate: string; // "25" or "25000/1000"
|
|||
|
|
segmentBase: DashSegmentBase;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface DashAudioItem {
|
|||
|
|
id: number;
|
|||
|
|
baseUrl: string;
|
|||
|
|
bandwidth: number;
|
|||
|
|
mimeType: string; // "audio/mp4"
|
|||
|
|
codecs: string; // "mp4a.40.2"
|
|||
|
|
segmentBase: DashSegmentBase;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface PlayUrlResponse {
|
|||
|
|
durl?: Array<{
|
|||
|
|
url: string;
|
|||
|
|
length: number;
|
|||
|
|
size: number;
|
|||
|
|
}>;
|
|||
|
|
dash?: {
|
|||
|
|
duration: number;
|
|||
|
|
video: DashVideoItem[];
|
|||
|
|
audio: DashAudioItem[];
|
|||
|
|
};
|
|||
|
|
quality: number;
|
|||
|
|
accept_quality: number[];
|
|||
|
|
accept_description: string[];
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证 TypeScript 无报错**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd C:/claude-code-studly/reactBilibiliApp && npx tsc --noEmit 2>&1 | head -30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add services/types.ts
|
|||
|
|
git commit -m "feat: extend PlayUrlResponse types for DASH segmentBase fields"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 2: getPlayUrl 按平台分叉
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `services/bilibili.ts` (line 97–102)
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 修改 `getPlayUrl` 函数**
|
|||
|
|
|
|||
|
|
将现有函数替换为:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise<PlayUrlResponse> {
|
|||
|
|
const isAndroid = Platform.OS === 'android';
|
|||
|
|
const params = isAndroid
|
|||
|
|
? { bvid, cid, qn, fnval: 16, fourk: 1 }
|
|||
|
|
: { bvid, cid, qn, fnval: 0, platform: 'html5', fourk: 1 };
|
|||
|
|
const res = await api.get('/x/player/playurl', { params });
|
|||
|
|
return res.data.data as PlayUrlResponse;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
注:`Platform` 已在文件顶部从 `react-native` 导入(第 3 行),无需新增 import。
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证 TypeScript 无报错**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd C:/claude-code-studly/reactBilibiliApp && npx tsc --noEmit 2>&1 | head -30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add services/bilibili.ts
|
|||
|
|
git commit -m "feat: use DASH (fnval=16) on Android for getPlayUrl"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 3: 新建 utils/dash.ts
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `utils/dash.ts`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 创建文件,实现 `buildDashDataUri`**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// utils/dash.ts
|
|||
|
|
import type { PlayUrlResponse } from '../services/types';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从 Bilibili DASH 响应生成 MPD data URI。
|
|||
|
|
* 选取 id === qn 的视频流(找不到则取第一条),带宽最高的音频流。
|
|||
|
|
* 返回 "data:application/dash+xml;base64,..." 供 react-native-video 使用。
|
|||
|
|
*/
|
|||
|
|
export function buildDashDataUri(playData: PlayUrlResponse, qn: number): string {
|
|||
|
|
const dash = playData.dash!;
|
|||
|
|
|
|||
|
|
const video =
|
|||
|
|
dash.video.find(v => v.id === qn) ?? dash.video[0];
|
|||
|
|
const audio = dash.audio.reduce((best, a) =>
|
|||
|
|
a.bandwidth > best.bandwidth ? a : best
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const dur = dash.duration;
|
|||
|
|
|
|||
|
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|||
|
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
|
|||
|
|
profiles="urn:mpeg:dash:profile:isoff-on-demand:2011"
|
|||
|
|
type="static"
|
|||
|
|
mediaPresentationDuration="PT${dur}S">
|
|||
|
|
<Period duration="PT${dur}S">
|
|||
|
|
<AdaptationSet id="1" mimeType="${video.mimeType}" codecs="${video.codecs}" startWithSAP="1" subsegmentAlignment="true">
|
|||
|
|
<Representation id="v1" bandwidth="${video.bandwidth}" width="${video.width}" height="${video.height}" frameRate="${video.frameRate}">
|
|||
|
|
<BaseURL>${escapeXml(video.baseUrl)}</BaseURL>
|
|||
|
|
<SegmentBase indexRange="${video.segmentBase.indexRange}">
|
|||
|
|
<Initialization range="${video.segmentBase.Initialization}"/>
|
|||
|
|
</SegmentBase>
|
|||
|
|
</Representation>
|
|||
|
|
</AdaptationSet>
|
|||
|
|
<AdaptationSet id="2" mimeType="${audio.mimeType}" codecs="${audio.codecs}" startWithSAP="1" subsegmentAlignment="true">
|
|||
|
|
<Representation id="a1" bandwidth="${audio.bandwidth}">
|
|||
|
|
<BaseURL>${escapeXml(audio.baseUrl)}</BaseURL>
|
|||
|
|
<SegmentBase indexRange="${audio.segmentBase.indexRange}">
|
|||
|
|
<Initialization range="${audio.segmentBase.Initialization}"/>
|
|||
|
|
</SegmentBase>
|
|||
|
|
</Representation>
|
|||
|
|
</AdaptationSet>
|
|||
|
|
</Period>
|
|||
|
|
</MPD>`;
|
|||
|
|
|
|||
|
|
// btoa is available in React Native's Hermes JS engine
|
|||
|
|
return `data:application/dash+xml;base64,${btoa(xml)}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function escapeXml(s: string): string {
|
|||
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证 TypeScript 无报错**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd C:/claude-code-studly/reactBilibiliApp && npx tsc --noEmit 2>&1 | head -30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add utils/dash.ts
|
|||
|
|
git commit -m "feat: add buildDashDataUri utility for DASH MPD generation"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 4: 更新 NativeVideoPlayer 使用 DASH data URI
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `components/NativeVideoPlayer.tsx`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: 导入 `buildDashDataUri`,更新 url 选取逻辑**
|
|||
|
|
|
|||
|
|
在文件顶部 import 区域添加:
|
|||
|
|
```typescript
|
|||
|
|
import { buildDashDataUri } from '../utils/dash';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
将第 34 行的 url 计算替换为(在组件函数体内):
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 优先使用 DASH data URI(Android),降级使用 durl MP4(iOS)
|
|||
|
|
const url = playData?.dash
|
|||
|
|
? buildDashDataUri(playData, currentQn)
|
|||
|
|
: playData?.durl?.[0]?.url;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
将 `<Video>` 组件的 `source` prop 更新,DASH 时传入 `type` hint:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
<Video
|
|||
|
|
ref={videoRef}
|
|||
|
|
source={
|
|||
|
|
playData?.dash
|
|||
|
|
? { uri: url!, type: 'mpd', headers: BILIBILI_HEADERS }
|
|||
|
|
: { uri: url!, headers: BILIBILI_HEADERS }
|
|||
|
|
}
|
|||
|
|
style={styles.video}
|
|||
|
|
resizeMode="contain"
|
|||
|
|
controls
|
|||
|
|
paused={false}
|
|||
|
|
/>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: 验证 TypeScript 无报错**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd C:/claude-code-studly/reactBilibiliApp && npx tsc --noEmit 2>&1 | head -30
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: 手动测试清单**
|
|||
|
|
|
|||
|
|
在 Android 设备 / 模拟器上:
|
|||
|
|
1. `expo run:android`(需要 dev build,react-native-video 是 native 模块)
|
|||
|
|
2. 打开任意视频详情页
|
|||
|
|
3. 确认播放器加载且**有声音有画面**
|
|||
|
|
4. 点击右上角清晰度按钮,检查可选项是否包含 1080P(80)或更高
|
|||
|
|
5. 切换清晰度,确认切换后正常播放
|
|||
|
|
6. (可选)登录后重新打开视频,确认 `accept_quality` 中出现更高画质选项
|
|||
|
|
|
|||
|
|
在 iOS(如有设备):
|
|||
|
|
1. 确认退化到 720P MP4 durl 路径,视频正常播放无报错
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add components/NativeVideoPlayer.tsx
|
|||
|
|
git commit -m "feat: play DASH streams on Android for 1080P+ quality"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 完成标志
|
|||
|
|
|
|||
|
|
- [ ] Android 上视频清晰度列表出现 1080P(qn=80)及以上选项
|
|||
|
|
- [ ] 切换清晰度后视频带音频正常播放
|
|||
|
|
- [ ] iOS 仍可正常播放(720P durl 路径)
|
|||
|
|
- [ ] `npx tsc --noEmit` 无类型错误
|