# 播放器进度拖动画面预览 ## 目标 首页 feed 与全屏播放器的进度条在拖动时显示当前时间点的视频画面缩略图、 当前时间与总时长;松手立即隐藏并提交 seek。缩略图未返回或失败时显示短剧 封面,不出现空白闪烁或错误文案。 ## 性能红线 1. 拖动期间主线程无卡顿;图片解码和原生取帧不得阻塞 UI 线程。 2. 原生取帧并发数不超过 1。 3. 预览图最大宽度 240px。 4. Dart 内存 LRU 缓存不超过 24 帧。 5. 不新增 pub 依赖。 6. 不改后端契约。 ## 实现 ### 缩略图服务 - 新增 `PlaybackThumbnailService`,公开入口按 `episodeId + position` 取 `ByteData`,底层通过 `MethodChannel('com.stormg.lumadrama/thumbnail')` 调用原生。 - 时间按 1 秒分桶;同一 bucket 的在途请求合流。 - 最多 24 帧 LRU;成功和失败结果都做过期控制,避免旧拖动位置覆盖当前预览。 - 全局单飞;拖动期间只保留最新 bucket,尚未开始的旧请求直接丢弃。 - 拖动停顿 120ms 后最多预取相邻一个 bucket,松手取消尚未开始的请求。 - 原生异常、缺失实现和超过 1.5 秒无结果统一返回 `null`。 ### 原生取帧 - Android 使用 `MediaMetadataRetriever`,优先本地下载路径,其次签名 URL; `OPTION_CLOSEST_SYNC` 取帧,最大宽 240px,JPEG 质量 70,单线程 Executor, 完成后立即 release。 - iOS 使用 `AVAssetImageGenerator`,应用画面方向,最大尺寸约 240x426, 合理时间容差,后台串行队列生成 JPEG。 - 两端均保证只回调一次;失败或 1.5 秒超时返回 `null`,不向 UI 抛异常。 ### UI - 新增 `ScrubPreviewCard`:宽 120dp、圆角 8、白色 1px 描边,显示 `当前时间 / 总时长`;沿拖动点水平跟随并做安全边距 clamp。 - 首页 `_FeedProgressBar` 与全屏 `SeekProgressBar` 共用该卡片。 - 封面始终铺在卡片底层;`Image.memory` 仅在帧到达后覆盖,并限制 `cacheWidth: 240`。 - 开始拖动请求当前 bucket,拖动更新只保留最新位置;松手立即隐藏卡片并取消 未开始请求。 - `PlayerPage` 持有页面级服务,dispose 时清空缓存,避免跨页面串集。 ## 测试 - 单测:bucket、LRU 淘汰、同 bucket 合流、过期请求丢弃、松手取消、 相邻 bucket 防抖、异常/超时回落。 - Widget:拖动显示预览卡片和时间、松手消失、无缩略图显示封面。 - 门禁:`dart analyze lib test` 与 `flutter test --no-pub`。 ## 非目标 - 不改服务端接口或持久化策略。 - 不新增第三方依赖。 - 不启停服务、不操作真机;原生取帧实际效果由 PM 真机验收。