为什么浏览器要限制自动播放

移动端流量贵、电量有限、用户对"突然出声"的体验敏感。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 三件套即可定位"为什么不播"。

使用准则

  1. 任何自动播放的 video 都要 muted,否则几乎一定被拒。
  2. iOS 永远加 playsinline,否则强制全屏。
  3. play() 一定要 await 并 catch reject,提供 fallback UI。
  4. 把 GIF 改成 WebM/MP4;体积省 80%+,性能也好得多。
  5. 做帧处理时不要把 video 隐藏在 DOM 之外——否则需要用户手势。
参考资料