# B站直播详情页 实现计划
## Context
现有应用已有直播列表(LiveCard + useLiveList),但点击卡片只是用 `Linking.openURL` 跳转到外部浏览器,没有应用内直播详情页。本次目标是实现一个完整的 in-app 直播详情页,包括 HLS 播放器、主播信息、房间简介。
---
## 直播详情页功能需求(MVP)
| 功能 | 说明 |
|---|---|
| 直播播放器 | HLS (m3u8) 流,`react-native-video` 播放,支持全屏 |
| 房间信息 | 标题、分类(父/子分区)、在线人数 |
| 主播信息 | 头像、昵称 |
| 关注按钮 | 展示样式,不调 API(bili_jct 不在当前登录流程中) |
| 离线状态 | 若 `live_status !== 1` 或无流 URL,显示"暂未开播" |
**跳过(Post-MVP):** 实时弹幕WebSocket、质量切换、送礼、直播间聊天
---
## Bilibili 直播 API
| 函数 | 接口 | 说明 |
|---|---|---|
| `getLiveRoomDetail(roomId)` | `LIVE_BASE/room/v1/Room/get_info` | 房间详情(标题、分区、live_status、在线人数) |
| `getLiveAnchorInfo(roomId)` | `LIVE_BASE/live_user/v1/UserInfo/get_anchor_in_room` | 主播信息(uid、uname、face) |
| `getLiveStreamUrl(roomId)` | `LIVE_BASE/xlive/web-room/v2/index/getRoomPlayInfo` | 获取 HLS/FLV 流 URL |
**流 URL 提取路径:**
```
data.playurl_info.playurl.stream[]
→ protocol_name === 'http_hls'
→ format[].format_name === 'fmp4' or 'ts'
→ codec[].codec_name === 'avc'
→ url_info[0].host + codec.base_url
```
---
## 文件变更清单(按依赖顺序)
### 新增文件
1. `livePlan.md` — 本文件
2. `services/types.ts` — 添加 `LiveRoomDetail`、`LiveAnchorInfo`、`LiveStreamInfo` 类型
3. `services/bilibili.ts` — 添加 `getLiveRoomDetail`、`getLiveAnchorInfo`、`getLiveStreamUrl`
4. `hooks/useLiveDetail.ts` — 直播详情 hook(并行拉取房间信息+主播信息)
5. `components/LivePlayer.tsx` — 直播播放器(web 提示 / native 用 react-native-video HLS)
6. `app/live/_layout.tsx` — Stack layout(同 `app/video/_layout.tsx`)
7. `app/live/[roomId].tsx` — 直播详情页主文件
### 修改文件
8. `dev-proxy.js` — 添加 `/bilibili-live` 代理路由(修复 web 端直播 API)
9. `app/_layout.tsx` — 注册 `live` Stack.Screen(动画同 video)
10. `app/index.tsx` — 替换 `Linking.openURL` 为 `router.push('/live/...')`,给直播 tab 的卡片加 `onPress`
---
## 组件结构
```
app/live/[roomId].tsx
TopBar (返回 + 标题)
LivePlayer
├── web: "请在手机端观看直播"
└── native: + 简单控制栏 (LIVE 标牌 / 全屏)
ScrollView
房间标题
在线人数 + LivePulse
分区标签 (parent_area_name > area_name)
分割线
主播行 (头像 + uname + 关注按钮)
房间描述文本
```
---
## 数据流
```
LiveCard onPress → router.push('/live/[roomId]')
useLiveDetail(roomId)
Promise.all([getLiveRoomDetail, getLiveAnchorInfo])
if live_status===1 → getLiveStreamUrl → hlsUrl
→ LivePlayer
```
---
## 注意事项
1. **`roomId` 类型**:`useLocalSearchParams` 返回 string,需 `parseInt(roomId, 10)`
2. **router.push 类型**:使用 `as any` 规避 TypeScript 动态路由报错
3. **流 URL 提取**:用可选链 (`?.`) 兜底,失败时返回 `{ hlsUrl: '', flvUrl: '', qn: 0 }`,让 LivePlayer 显示离线状态
4. **live 无 onEnd**:直播流没有结束,不处理 `onEnd` 事件
5. **web 端 HLS**:`