Files
JKVideo/docs/superpowers/plans/2026-03-10-dash-1080p.md
Developer 5bb6a3cd68 feat: unlock 1080P+ on Android via DASH streaming
- getPlayUrl uses fnval=16 (DASH) on Android, keeping fnval=0/html5 for iOS/web
- New utils/dash.ts builds a valid DASH MPD from Bilibili's segmentBase ranges
  and returns it as a data: URI for ExoPlayer consumption
- NativeVideoPlayer selects DASH path (type='mpd') or durl fallback automatically
- Extend PlayUrlResponse types with DashVideoItem/DashAudioItem/DashSegmentBase
2026-03-10 20:04:48 +08:00

277 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 97102)
- [ ] **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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
```
- [ ] **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 URIAndroid降级使用 durl MP4iOS
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 buildreact-native-video 是 native 模块)
2. 打开任意视频详情页
3. 确认播放器加载且**有声音有画面**
4. 点击右上角清晰度按钮,检查可选项是否包含 1080P80或更高
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 上视频清晰度列表出现 1080Pqn=80及以上选项
- [ ] 切换清晰度后视频带音频正常播放
- [ ] iOS 仍可正常播放720P durl 路径)
- [ ] `npx tsc --noEmit` 无类型错误