静图与视频之间
静图与视频都能在浏览器里解开之后(见解得开哪一半),剩下的是观感问题:让两者看起来是同一张照片的两种状态,而不是一张图旁边多了个播放器。
全部难点集中在切换的那一帧。
1 · 两种不同的失效
朴素做法是把 <img> 换成 <video>。切换瞬间视频还没有解出任何一帧,此时元素画什么,决定了会看到什么。
一个尚无解码帧的 <video> 不画黑色,它什么都不画——是透明的。于是失效表现取决于它背后有什么:
| 切换时下层有什么 | 首帧到达前看到 | 失效表现 |
|---|---|---|
| 什么都没有 | 容器底色(常为黑或页面背景) | 闪一下 |
| 同一张静图 | 那张静图 | 不闪,但首帧落地时画面跳 |
第二行是把静图留在下层的结果:视觉上没有断裂,问题转移到了内容上。视频从 mediaTime = 0 开始播,而静图对应的是锚点帧(Live Photo 里落在片段中间)。两者内容不同,首帧落地那一刻画面会跳一下。
这两个失效需要分开处理:垫底解决闪,对齐锚点解决跳。它们互不替代。
2 · 确认「已有一帧在屏上」
要在正确时刻切换,得先有一个可靠信号。候选事件里多数都不合用:
| 信号 | 含义 | 能否确认已有一帧在屏上 |
|---|---|---|
loadedmetadata |
时长与尺寸已知 | ❌ 不能 |
loadeddata |
当前播放位置的数据已到 | ❌ 不保证已呈现 |
canplay |
可以开始播 | ❌ 不能 |
requestVideoFrameCallback |
一帧已提交合成 | ✅ 能 |
requestVideoFrameCallback 的回调在一帧被送去合成后触发,这正是「屏上确实有画面了」的时刻。它的 metadata 参数里带 mediaTime,即该帧对应的媒体时间戳。
mediaTime。mediaTime 的第二个用途是对齐。把视频的 currentTime 预先设到锚点,再等第一个回调,此时屏上那一帧与静图内容一致,切换在两个维度上都无缝:
video.currentTime = anchorSeconds; // Motion Photo 取 MotionPhotoPresentationTimestampUs / 1e6(值为 -1 表示未指定,需兜底)
video.addEventListener('loadeddata', () => {
video.play();
video.requestVideoFrameCallback((now, meta) => {
// meta.mediaTime 已在锚点附近, 此刻交叉淡入不会跳
crossFadeToVideo();
});
}, { once: true });
警示 · 关键帧密度影响的是 seek 的耗时(要从前一个关键帧解到目标),而不是落点:HTML 规范里「就近取可快速恢复播放的位置」由 approximate-for-speed 标志控制,而该标志只有 fastSeek() 会置位,给 currentTime 赋值要求精确落点。实测 Chrome 151:对一段 10
fps、关键帧稀疏的视频请求 0.35 / 1.15 / 2.05 / 3.25 秒,currentTime 读回完全相同、四次取到的画面各不相同;而 fastSeek 在 Chrome 里根本没有实现('fastSeek' in video 为 false,核对于 2026-08)。
3 · 自动播放的门槛
无用户手势触发的播放受 autoplay policy 约束,两个属性是硬要求:
muted:静音是无条件可用的那条路径;有声自动播放要额外满足各浏览器的 autoplay policy(Chrome 会放行已交互过的域名、越过媒体参与度阈值的站点、已安装的 PWA 等),不能当作默认可得,被拒时play()的 promise 以NotAllowedError拒绝(核对于 2026-08)playsinline:iOS 上缺这条会被拉去全屏播放
Live Photo 在系统相册里长按是有声的。web 上要复现这一点,得看浏览器的 autoplay policy 放不放行——Chrome 在用户与本域交互过之后即放行有声播放,Safari 另有站点级设置。跨浏览器不可依赖,因此 hover 触发的播放实践中一律静音处理(核对于 2026-08)。
play() 返回 promise 这件事容易被忽略——它被拒绝时不会抛到全局,静默失败,界面就停在静图上没有任何提示。
video.play().catch((err) => {
// NotAllowedError: 缺手势或未静音; AbortError: 期间 src 被换掉
showStillWithPlayButton();
});
4 · 回到静图
播完要退回静图,ended 事件是切换点。反向切换没有首帧问题:静图是一张已解码的位图,显示它不需要等待。
一个细节是视频的最后一帧与静图内容通常不同(锚点在中间,播放停在末尾),所以退回时同样是一次内容跳变。做法上有两种取舍:淡出视频层,或把 currentTime 拨回锚点后再切。后者更贴近相册里的表现——松手后画面回到那张照片本身,而不是停在最后一格。
建议 · 静图始终留在下层、只对视频层做透明度切换,比在两个元素之间来回换 display 更省事:静图不会被卸载,反向切换零成本,且任何一次视频加载失败都自动退回一张能看的图。图 2-1 的 B、C 两格用的都是这个结构。
触发这套切换的动作本身还没有讨论:桌面上是 hover,移动端是长按,两者的判定与降级各有讲究,见 触发与降级。