为什么浏览器要限制自动播放
移动端流量贵、电量有限、用户对"突然出声"的体验敏感。Apple 在 iOS 9 起强制要求 <video> 必须由用户手势触发;iOS 10 放宽了部分场景,但仍保留限制。Android Chrome 紧跟这一策略,又叠加了低功耗模式与流量节省模式。
WebKit 的官方说明(2016):动画 GIF 比同效果视频多消耗 12 倍带宽、多 2 倍解码能耗。这正是策略倾向于"用视频替换 GIF"的核心动机。
当前的自动播放策略
所有主流移动浏览器都遵守以下规则,可以自动播放:
- 视频无音轨(画面类、动效类)
- 视频被设置
muted - 视频在视口内可见(不是
display:none、不在视口外)
会被暂停或拒绝:
- 视频有声且非静音
- 在视口外 /
display:none/visibility:hidden - 程序
play()时上述条件不满足 → Promise 被 reject
2023 年起的额外约束:
- 低功耗模式(iOS Low Power / Android Battery Saver)下,即使满足条件也可能被浏览器延后或限速。
- 数据节省模式(Chrome Lite / Data Saver)下,
preload="auto"会被改写为metadata或none。 - 用户激活(user activation):iOS 16+ / Chrome 90+ 强化了"用户主动交互过才能自动播放"的判定,避免脚本绕过。
用 play() 时的现代写法
const video = document.querySelector('video');
video.muted = true; // 静音是自动播放的前提
video.playsInline = true; // iOS 行内播放
try {
await video.play();
} catch (err) {
// 自动播放被拒,引导用户点击播放
showFallbackButton();
}
永远不要只调用
play() 不处理 Promise reject。否则在限制严格的浏览器里视频"莫名其妙播不出来",没有可见反馈,调试也很费劲。playsinline 与全屏
iPhone 上没有 playsinline 时,<video> 一播放就强制全屏——这是历史问题。在 iOS 10 之前无解:
<video autoplay loop muted playsinline>
<source src="hero.webm" type="video/webm">
<source src="hero.mp4" type="video/mp4">
</video>
三个属性共同作用:
autoplay:允许自动播放(但仍需遵守上面规则)muted:静音,绕过有声限制playsinline:iOS 行内播放,不强制全屏(Android 通常默认行内)
iOS Safari 还提供媒体查询,可以判断当前浏览器是否支持行内播放:
@media (-webkit-video-playable-inline) {
/* 当前允许行内播放,给视频元素加样式 */
.hero video { display: block; }
}
@supports not (-webkit-video-playable-inline) {
/* 老 iOS 回退到 GIF */
.hero img.gif-fallback { display: block; }
}
用 video 替换 GIF
演示场景:列表卡片里有个循环动画,原来用 GIF,每个 5MB。换方案:
<video autoplay loop muted playsinline poster="preview.jpg">
<source src="card.webm" type="video/webm"
onerror="fallback(this)">
<source src="card.mp4" type="video/mp4">
<img src="card.gif" alt="">
</video>
<script>
function fallback(video) {
const img = video.querySelector('img');
if (img) video.parentNode.replaceChild(img, video);
}
</script>
回退顺序:WebM → MP4 → 原 GIF。WebM 通常体积最小,但 Safari < 14 不支持;MP4 作为兜底;GIF 是最后兜底。同效果下,视频通常只有 GIF 的 1/5 到 1/10 体积。
把视频帧画到 canvas
用 video 做视频处理或做 WebGL 纹理源很常见:
const video = document.createElement('video');
video.src = 'clip.webm';
video.loop = true;
video.muted = true;
video.playsInline = true;
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
document.body.appendChild(canvas);
video.addEventListener('playing', paint);
async function start() {
// 必须由用户手势触发(视频不在 DOM、不可见)
await video.play();
}
function paint() {
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
if (!video.paused) requestAnimationFrame(paint);
}
关键点:
- 如果
<video>没插入到 DOM、或处于display:none,必须由用户手势调用play()。 drawImage(video, ...)要求 video 已经在播——浏览器不允许"读取未播放的视频帧"。- 同样的方式可以接到 WebGL:把 canvas 作为纹理传给着色器即可。
首屏与封面图
移动端网络下,<video> 在数据到达前是黑屏。用 poster 指定封面:
<video autoplay loop muted playsinline poster="hero.jpg">
<source src="hero.mp4" type="video/mp4">
</video>
poster 在视频可播之前显示;首帧到达后会被替换。封面用 JPEG 而非 PNG,体积更小。
调试与诊断
- iOS Safari:Mac 端 Safari → Develop → iPhone → 选中页面,可看控制台。
- Android Chrome:
chrome://inspect/#devices,USB 调试开启后远程调试。 - 看
video.paused、video.error、video.networkState三件套即可定位"为什么不播"。
使用准则
- 任何自动播放的 video 都要
muted,否则几乎一定被拒。 - iOS 永远加
playsinline,否则强制全屏。 play()一定要await并 catch reject,提供 fallback UI。- 把 GIF 改成 WebM/MP4;体积省 80%+,性能也好得多。
- 做帧处理时不要把 video 隐藏在 DOM 之外——否则需要用户手势。