# 播放链路(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 占位)