This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
# 播放链路(Feed 播放页)实施计划
## Context
三端骨架已完成(server 32 后台接口 + 55 个 App 接口、admin Vue3 已验证、app Flutter 骨架 analyze 0 issue)。下一步是 App 的核心价值链路:**首页 Feed 沉浸式播放 + 观看计费**。
关键前提(已与用户确认):
- 服务端 `/hls` 未就绪(无转码产物、未挂载静态路由),本次 **只做 App 端**,播放器验证用 `--dart-define=TEST_VIDEO_URL` 注入公网测试 m3u8;真实 /hls 留到上传/转码迭代。
- 本机无 Xcode/Android SDK,验证以 `flutter analyze` + `flutter test` 为准。
契约依据:docs/03 §5.1-5.4(feed/watch/complete/like/favorite)、docs/01 §5.2.1/5.3(布局与交互规范)。
## 架构决策
1. **播放器 3 槽位池化**(`FeedPlayerPool`):media_kit 每个 Player 是一个 mpv 实例,PageView 无限列表每页持有会 OOM。窗口 `[i-1, i, i+1]` 映射到 `index % 3` 槽位:当前 play、相邻 open 预缓冲、出窗 stop。
2. **watch 去重持久化**(SharedPreferences):游客免费额度按设备计费,杀进程重进重复上报会白扣游客次数。key 按日分组 `watch_yyyyMMdd`,隔日惰性清理;complete 用 `complete_` 前缀同理。
3. **状态两层**:`feedProvider`(family 按策略 recommend/latest/hot)管列表/游标/当前索引/freeRemainingToday;`FeedPlayerPool` 管播放器生命周期。计费触发收在 Notifier.onShow,UI 不直接调 service。
4. **router 放开 /home 游客访问**:游客可进 Feed,超限(2002)才弹登录引导。
5. **骨架小改**:`ApiException` 增加 `data` 字段(2001 需读 points);`reportWatch` 返回强类型 `WatchResult`;VideoItem 补 `copyWith`(乐观更新)。
## 文件清单
### 新增(11 个)
| 文件 | 职责 |
|---|---|
| `app/lib/models/watch_result.dart` | watch 响应模型(costType/costPoints/remainingPoints/freeRemainingToday) |
| `app/lib/core/storage/watch_dedup_store.dart` | video_id+日期 去重集合(watch/complete 两组),SharedPreferences 持久化,隔日失效 |
| `app/lib/core/player/feed_player_pool.dart` | 3 槽位 Player 池:bindWindow/播放/暂停/预缓冲/释放;进度监听聚合(≥95% 或 completed 回调一次);构造接受 Player 工厂便于测试注入 fake |
| `app/lib/providers/feed_provider.dart` | FeedState + FeedNotifier:refresh/loadMore/onPageChanged(计费触发)/toggleLike/toggleFavorite |
| `app/lib/core/config/test_stream.dart` | dev 钩子 `resolvePlayUrl`:`String.fromEnvironment('TEST_VIDEO_URL')` 非空且 video_url 为空或非 http 时替换;做成可选参数纯函数便于单测 |
| `app/lib/screens/feed/feed_screen.dart` | Feed 页骨架:策略 Tab + 搜索占位、PageView.builder、提示条挂载;WidgetsBindingObserver 退后台暂停 |
| `app/lib/screens/feed/widgets/feed_video_page.dart` | 单页组装:播放器 + 手势层(单击暂停/继续、双击点赞)+ 互动栏 + 信息区 + 爱心动画层 |
| `app/lib/screens/feed/widgets/video_player_view.dart` | media_kit Video 包装:封面兜底(cached_network_image)、缓冲转圈、暂停态大播放图标 |
| `app/lib/screens/feed/widgets/interaction_bar.dart` | 右侧栏:头像、点赞/评论/收藏/分享带计数;未登录跳登录;评论/分享 Toast 占位(二期) |
| `app/lib/screens/feed/widgets/video_info_section.dart` | 左下:昵称、标题+#标签(本期仅样式不跳转)、描述展开/收起 |
| `app/lib/screens/feed/widgets/feed_overlays.dart` | 双击爱心动画、FreeQuotaBanner(≤3 显示)、2002 登录引导与 2001 积分不足弹窗 |
### 修改(5 个,均小改)
| 文件 | 改动 |
|---|---|
| `app/lib/services/video_service.dart` | reportWatch 返回 `WatchResult`;新增 `reportComplete(videoId, watchDuration) → bool` |
| `app/lib/core/network/api_exception.dart` | 增加 `data` 字段 + `isGuestLimit`(2002) / `isPointsInsufficient`(2001) getter |
| `app/lib/core/network/api_client.dart` | `_UnwrapInterceptor` 抛异常时透传 `body['data']` |
| `app/lib/models/video_item.dart` | 补 `copyWith` |
| `app/lib/screens/home/home_shell.dart` | 改 StatefulWidget,Tab1 = FeedScreen,IndexedStack 保活,其余 Tab 占位 |
| `app/lib/router.dart` | redirect 放开 /home 游客访问 |
## 关键实现要点
### 计费链路(FeedNotifier.onShow)
```
onShow(index):
pool.bindWindow(index, items) // 先播放
item = items[index]
if dedup.contains(item.videoId) return // 同日已上报
dedup.mark(item.videoId) // 先标记,失败不回滚(防重复扣费)
try:
r = VideoService.reportWatch(item.videoId)
if r.costType == 1: 顶部 Toast「本次消耗 1 积分」
state.freeRemainingToday = r.freeRemainingToday // 驱动提示条
on ApiException e:
2002 → pool.pauseCurrent(); 弹登录引导(确认跳 /login)
2001 → pool.pauseCurrent(); 弹「积分不足」(去充值/开通会员,本期 Toast 占位)
其他 → 静默,不影响播放
```
- complete:pool 进度回调 → `reportComplete`,走 `complete_` 去重集合;credited=false 无 UI。
- Toast 用简单 OverlayEntry 顶部条,不引第三方库。
### 互动乐观更新
未登录点击 → 跳 /login;已登录 → `copyWith` 立即更新 → 调接口 → 异常回滚 + Toast。双击:未点赞等同 toggleLike,已点赞只播动画。
### 免费剩余提示条
游客或(非会员且 remaining ≤ 3)时显示;初始 null 不显示,首次 watch 响应后落地。
## 验证方式
**自动化(本机可跑)**:
1. `WatchDedupStore` 单测:`SharedPreferences.setMockInitialValues` 模拟,验证 mark/contains、跨日 key 清理
2. `WatchResult.fromJson` 解析单测(cost_type 0/1/2/3)
3. `resolvePlayUrl` 纯函数三分支单测
4. Widget 测试:InteractionBar 计数与回调、FreeQuotaBanner 显隐(remaining=2/5/null)、FeedVideoPage 暂停图标(pool 工厂注入 fake)
5. `flutter analyze` 0 issues
**有设备后人工验证**(写进交接注释):注入测试流验证滑动播放/暂停/双击点赞;切策略 Tab;杀进程重进验证不重复扣费;mock 2002 弹登录引导;抓包确认 complete 只发一次。
## 不做(明确边界,防蔓延)
- 评论页、分享落地、关注、标签跳转(均二期,按钮 Toast 占位)
- 服务端 /hls 挂载与转码产物(上传/转码迭代)
- 去充值/开通会员页面(弹窗按钮 Toast 占位)