学完这篇,你能给任意页面写出一段「主流浏览器都能播、老设备也能优雅降级」的视频代码,并知道格式怎么转、失败怎么兜。
第一步:先把 video 骨架写对
<video> 支持多个 <source>,浏览器从上往下试,遇到不认识的 type 直接跳过,不会报错。这是最基础的一层降级。
<video controls preload="metadata" poster="/cover.jpg" playsinline width="640">
<source src="/movie.webm" type="video/webm">
<source src="/movie.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
你的浏览器不支持 HTML5 视频,<a href="/movie.mp4">点此下载观看</a>。
</video>
几个参数的作用:
preload="metadata":只拉头部信息拿时长,不预下载整片,省流量。想更快出画面用 preload="auto",但要考虑带宽。
poster:首帧图,视频没加载出来时先显示它,也兼作兜底画面。
playsinline:iOS Safari 必需,否则一播就强制全屏。
controls 一定要加,别自己造一套然后又忘了键盘可访问性。
顺序建议 WebM 在前、MP4 在后:现代浏览器走体积更小的 WebM,Safari 和不支持 WebM 的自动落到 MP4。
注意:<source> 的 type 里带 codecs 参数能让浏览器提前判断,省掉一次无效请求。只写 type="video/mp4" 而不写 codecs,遇到特殊编码(比如 HEVC)会先下载再失败。
第二步:用 ffmpeg 准备两套格式
不要指望用户的原始文件。统一转成 H.264 + AAC 的 MP4 作为保底,VP9 或 AV1 的 WebM 作为优化:
ffmpeg -i input.mov -c:v libx264 -profile:v main -level 4.0 \
-pix_fmt yuv420p -crf 23 -preset medium \
-c:a aac -b:a 128k -movflags +faststart output.mp4
# WebM:体积更小
ffmpeg -i input.mov -c:v libvpx-vp9 -b:v 0 -crf 32 -c:a libopus -b:a 96k output.webm
几个不能省的参数:
-pix_fmt yuv420p:不写的话 QuickTime 系播放器可能黑屏。
-movflags +faststart:把 moov 索引挪到文件头,视频能边下边播。忘了这个参数,用户要等整片下完才开始播。
-profile:v main:baseline 兼容最广但压缩率差,main 是平衡点。
注意:服务器必须支持 Range 请求(HTTP 206),否则 Safari 和 iOS 完全无法拖动进度条。Nginx 默认支持,但如果用了某些代理或对象存储,要单独确认。
第三步:JS 做能力检测,动态换源
<source> 的降级是「静默」的,你拿不到反馈。需要更细控制时,用 canPlayType 主动判断:
const video = document.querySelector('video');
const probe = document.createElement('video');
const canMp4 = probe.canPlayType('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
const canWebm = probe.canPlayType('video/webm; codecs="vp9"');
if (canWebm === 'probably') {
video.src = '/movie.webm';
} else if (canMp4) {
video.src = '/movie.mp4';
} else {
showFallback(); // 换成下载链接或提示图
}
canPlayType 返回值三档:"probably"(基本确定能播)、"maybe"(可能能播,通常因为编码信息不足)、""(不支持)。
注意:给 <video> 直接设 src 后,原有的 <source> 会被忽略。混用两种方式时要么全用 source,要么全用 JS 设 src。
第四步:监听失败,把兜底做实
最容易被忽略的一点:error 事件要绑在最后一个 <source> 上,或者绑在 video 上判断 networkState。
const sources = video.querySelectorAll('source');
const last = sources[sources.length - 1];
last.addEventListener('error', () => {
// 所有源都失败了
video.replaceWith(createDownloadCard('/movie.mp4'));
});
video.addEventListener('error', () => {
console.log('video error code:', video.error?.code);
});
判断依据:video.networkState === 3(NETWORK_NO_SOURCE)表示所有源都没找到可用资源。
常见 error.code 含义:1 = 加载被中止,2 = 网络错误,3 = 解码失败,4 = 格式不支持或 URL 不可用。
第五步:移动端和自动播放的坑
- 自动播放:所有现代浏览器都禁止「带声音的自动播放」。想要自动播,必须同时加
muted 和 autoplay,再配 playsinline。
- iOS 低电量模式:即使 muted + autoplay 也可能被拦,务必在
play() 的 Promise 上 catch,不要裸调。
- 微信内置浏览器:加
playsinline 和 webkit-playsinline,需要 X5 内核全屏控制时再补 x5-video-player-type="h5-page"。
- 省流量:移动端默认
preload="none",用户点了再加载。
video.play().catch(() => {
// 展示自定义播放按钮,等用户手势触发
showPlayButton();
});
第六步:别忘了字幕和无障碍
<track kind="subtitles" src="/sub.vtt" srclang="zh" label="中文" default>
跨域加载字幕或视频时给标签加 crossorigin="anonymous",同时服务端要返回对应的 CORS 头,否则字幕静默失败、canvas 截图也会污染画布。
注意:.vtt 文件编码必须是 UTF-8,用 GBK 保存会整段乱码,而且浏览器不会报错,只是什么都不显示。
小结
- 用多
<source> 做第一层降级,WebM 在前、MP4 在后,type 里带 codecs。
- 转码固定套路:H.264 main + yuv420p + AAC +
+faststart;服务器必须支持 Range 请求。
- 需要精细控制时用
canPlayType 主动检测,但不要和 <source> 混用。
- 兜底要绑在最后一个 source 的 error 上,或判断
networkState === 3。
- 自动播放 =
muted + autoplay + playsinline,并且 play() 必须 catch。
- 字幕
.vtt 用 UTF-8,跨域记得 crossorigin。