音视频这块,看着就是一行 <video src="a.mp4" controls> 的事,真上了线才发现到处是坑:iPhone 上点播放直接跳全屏、静音能放有声放不了、微信里视频浮在弹窗上面盖不住、拖动进度条直接回到开头、监控画面延迟十几秒。可惜的不是属性的写的全了,而正是浏览器的自动播放的规则、编码的格式、服务端的响应头这三处都没能对齐上就出这样的问题了。

video、audio 的属性表摆正 🎬
很多人把 audio 当成没有画面的 video 来写,poster、width、height 全往上加,结果一个都不生效。audio 元素不认这些属性,它只有 src、preload、autoplay、loop、controls、muted、crossorigin 这些。
常用属性对照:
还有两条容易记混的:HTML 里写的是小写 playsinline,JS 里对应的是驼峰 video.playsInline,写错不报错但也不生效;muted 只在 HTML 属性阶段可靠,动态改 video.muted = true 后再 play(),在部分 iOS 版本上依然会被判为有声自动播放。
播不出来,先怀疑编码而不是属性 ⚙️
排查的第一步永远是编码。.mp4 只是容器,里面装的可能是 H.264,也可能是 H.265 或者 MPEG-4 Part 2,浏览器只认前者。
转码直接用这一条,覆盖了绝大多数兼容问题:
ffmpeg -i input.mov \
-c:v libx264 -profile:v main -level 4.0 -pix_fmt yuv420p \
-movflags +faststart \
-c:a aac -profile:a aac_low -ar 48000 -b:a 128k \
output.mp4
-movflags +faststart 一定要加,它把 moov box 挪到文件头部。放在尾部的话,浏览器必须把整个文件下载完才能起播,长视频就会一直转圈。音轨统一用 AAC-LC,采样率 44100 或 48000,HE-AAC 在部分安卓机型上会直接哑掉。
浏览器端可以先用 canPlayType 探一下:
const v = document.createElement('video');
console.log(v.canPlayType('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')); // probablyconsole.log(v.canPlayType('video/webm; codecs="vp9,opus"')); // maybe / ""
返回值只有 probably、maybe、空字符串三档,maybe 不代表能播,最终还是要用 error 事件兜底。
自动播放:规则早就改了,写法得跟着改 📢
现在各家浏览器的规则基本一致:静音 + 内联才允许自动播放,有声播放必须发生在用户手势里。Chrome 还有一套媒体参与度指数,用户在这个站点上频繁点播之后,有声自动播放也可能被放行,但这属于不可控因素,不能写进逻辑里。
async function tryPlay(video) {
video.muted = true; // 必须在 play() 之前
video.setAttribute('playsinline', '');
try {
await video.play();
} catch (e) {
if (e.name === 'NotAllowedError') {
// 展示「点击播放」浮层,在真实点击事件里再调一次 play()showPlayMask(() => { video.muted = false; video.play(); });
} else if (e.name === 'AbortError') {
// 上一次 play 还没结束又被打断,属于正常现象
}
}
}
三个细节:
play() 返回的是 Promise,不 catch 会在控制台抛未捕获异常,很多「偶发报错」就是这么来的
但令人不齿的是,即使开启了iOS的低电量模式,很多APP的静音自动播放的功能也都被拦了,唯一的出入就是在界面上必须留一个手动的入口了
视频切走时要主动 video.pause() 并把 src 清空后调 load(),否则后台继续解码,发热和耗电都压不住
移动端与微信 X5:全屏劫持和同层播放 📱
iOS 上不写 playsinline,一点播放就强制进原生全屏播放器,页面上的一切交互全部失效。微信安卓的 X5 内核更麻烦,它会用一个独立浮层接管 video,z-index 完全失效,弹窗被视频盖住是常态。
<videosrc="demo.mp4"mutedautoplayloopplaysinlinewebkit-playsinlinex5-playsinlinex5-video-player-type="h5-page"x5-video-player-fullscreen="true"x5-video-orientation="portrait"></video>
这几条都要写在 HTML 标签上。用 JS 动态 setAttribute 在部分版本不生效,因为内核在解析阶段就决定了用哪种播放器。需要感知全屏状态时监听 X5 的自定义事件:
document.addEventListener('x5videoenterfullscreen', () => { /* 页面进入全屏 */ });
document.addEventListener('x5videoexitfullscreen', () => { /* 退出全屏 */ });
同层播放下依然有两个坑:一是视频区域上方的 UI 层级仍然可能出问题,交互元素尽量别压在视频正上方;二是移动端多视频滑动切换时,X5 容易出现残留画面,切换前先 pause() 再换 src 更稳。
流媒体在微信里的选择也要分开:iOS 微信是 WKWebView,原生 HLS 和 MSE 都还行;安卓 X5 对 hls.js 支持不稳定,直接把 m3u8 交给内核原生播放更省事。
直播与长视频:HLS、HTTP-FLV、MSE 怎么选 📡
选型写法:
const video = document.querySelector('#player');
const m3u8 = 'https://example.com/live/stream.m3u8';
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = m3u8; // Safari、iOS 走原生
} else if ('MediaSource' in window &&
MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')) {
const hls = new Hls(); // 其余走 hls.js
hls.loadSource(m3u8);
hls.attachMedia(video);
} else {
fallbackWithPosterAndTip(); // 都不支持就给提示
}
iPhone 上的 MSE 直到较新版本才有限支持,直播别依赖它。另外微信、QQ 浏览器会缓存 m3u8 和分片,地址后面挂个时间戳参数可以避免更新不及时。
控制条、全屏、画中画与字幕 🎛️
产品要求 UI 统一,就别用原生 controls,自己画:播放暂停绑 play / pause,进度条用 timeupdate 读 currentTime,缓冲进度读 video.buffered.end(0)。
全屏:标准是 video.requestFullscreen();iPhone 上的 video 元素只有 webkitEnterFullscreen(),而且必须在用户手势里调用
画中画:判断 document.pictureInPictureEnabled 后调 video.requestPictureInPicture(),监听 enterpictureinpicture / leavepictureinpicture 同步 UI
字幕:<track kind="captions" src="sub.vtt" srclang="zh" label="中文" default">,跨域字幕必须同时配 crossorigin="anonymous" 和服务端 CORS 头,否则静默失败,控制台什么都不报
黑边:用 object-fit: cover 裁切,或 fill 拉伸,默认的 contain 会留边
投屏和下载:controlslist="nodownload noremoteplayback" 在 Chrome 系生效,投屏还可用 disableremoteplayback 属性
排查流程:error、事件顺序与服务端响应 🚨
播放失败先看 video.error.code:
正常加载的事件顺序是 loadstart → durationchange → loadedmetadata → loadeddata → canplay → canplaythrough → playing。卡在 waiting 或 stalled 上,多半是带宽或 Range 请求的问题;stalled 持续超过 3 秒基本可以判定网络侧异常。readyState 从 0 到 4 分别对应无数据、有元数据、有当前帧、可播放、可流畅播放,loading 动画按这个切换比按 waiting 更准。
服务端必须做到三件事:
支持 Range 请求,返回 206 Partial Content,否则进度条拖动会失效或直接跳回开头
Content-Type 正确:video/mp4、video/webm、application/vnd.apple.mpegurl,类型错了浏览器会直接判为不支持
跨域资源给 Access-Control-Allow-Origin,同时标签上加 crossorigin,否则字幕和 canvas 截图都拿不到
直接落地的播放组件写法 🔧
<videoid="player"class="player"poster="cover.jpg"preload="metadata"playsinlinewebkit-playsinlinex5-playsinlinex5-video-player-type="h5-page"crossorigin="anonymous"
><source src="demo.webm" type="video/webm"><source src="demo.mp4" type="video/mp4"><track kind="captions" src="sub.vtt" srclang="zh" label="中文" default>
你的浏览器不支持 video 标签</video>
const video = document.querySelector('#player');
video.addEventListener('loadedmetadata', () => {
video.muted = true;
video.play().catch(() => showPlayMask()); // 被拦就显示点击蒙层
});
video.addEventListener('waiting', () => showLoading(true));
video.addEventListener('playing', () => showLoading(false));
video.addEventListener('error', () => {
const code = video.error && video.error.code;
const tips = { 2: '网络异常,请检查连接', 3: '视频编码不支持', 4: '视频地址无效或格式不支持' };
showTip(tips[code] || '播放失败,请稍后重试');
});
// 页面隐藏时释放解码资源document.addEventListener('visibilitychange', () => {
if (document.hidden) { video.pause(); } else { video.play().catch(() => {})
});
多个 <source> 的写法要注意顺序:浏览器从上往下试,一旦选中某个就不会回头,所以把兼容面最广的 MP4 放最后兜底更稳。
音视频兼容这件事,本质上就是三张表对得上:编码格式浏览器认不认、自动播放规则浏览器放不放、服务端响应给得对不对。把这三处理顺了,剩下的属性问题基本都能在属性表和事件回调里找到答案。
