Files
ReelShort/docs/plans/2026-10-22-scrub-frame-preview.md
T

2.7 KiB
Raw Blame History

播放器进度拖动画面预览

目标

首页 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 真机验收。