如果你是因为搜“HTML 音频/视频”点进来的,我猜你八成已经经历过这样一个场景:在某个网页上看到一段很漂亮的视频播放器,于是自己也写了一个 video 标签,结果双击打开本地页面,黑屏、没声音、控制条倒是杵在那里——就是不放。别急,这个“黑屏”问题我后面会专门拆。你先记住一个结论:HTML5 的 audio 和 video 标签是网页音视频播放的地基,B站、西瓜视频、各种在线音乐播放器的网页端,底层都在用这套原生 API,只是外面套了各自的业务逻辑和封装组件。
这篇文章我不打算给你复制一段 demo 然后收工。我会从底层原理讲到实际调优,从“为什么双击不播放”讲到 m3u8 流媒体、HEVC 编码、Web Audio 音频处理,再到蓝牙音频接收器、MAX98357A 这类硬件场景里 HTML 音频到底扮演什么角色。内容更适合刚入门的前端、经常和网页音视频打交道的运营,以及想在自己网页里嵌入音视频但又不想一上来就被各种播放器 SDK 绑架的人。
1. 先搞清楚核心需求:HTML 音频/视频到底能做什么
1.1 audio 与 video:一对孪生兄弟
HTML5 里负责音视频播放的有两个标签,一个是 audio,一个是 video。大部分人第一次接触会觉得它们是两个完全不同的东西,实际上它们共用同一套媒体播放接口 HTMLMediaElement,只不过 audio 没有画面,video 多了视频区域和跟画面相关的属性。可以这么理解:audio 是收音机,video 是电视机,背后那套“调台、音量、播放暂停”的逻辑基本一模一样。
因此你在网上搜教程时,会经常看到 audio 的代码拿到 video 上也能跑,反过来也一样。比如 video 可以直接播放一段只有音频的 MP3 文件,只是画面上啥都不显示;audio 也可以通过设置 src 指向视频文件来播放声音。搞清楚这一点,后面很多问题你的排查思路就没那么乱了。
1.2 浏览器到底能放哪些格式
说一个很多人忽略的事实:浏览器本身并不是“什么格式都能播”的万能播放器。它更像是一个壳子,最终能不能解码,取决于浏览器内置的解码器以及操作系统提供的解码能力。这也是为什么同一段视频在 Chrome 上能播,在某个老版本浏览器上就提示“格式不支持”。
| 类型 | 推荐格式 | 兼容性说明 |
|---|---|---|
| 视频 | MP4(H.264 + AAC) | 最稳,几乎所有浏览器都支持 |
| 视频 | WebM(VP8/VP9) | Chrome/Firefox 支持好,Safari 部分版本有兼容问题 |
| 视频 | Ogg | 基本已经退出主流舞台 |
| 音频 | MP3 | 兼容性最好 |
| 音频 | AAC | 常见于 MP4 容器内,播放器场景用得最多 |
| 音频 | WAV | 无损,体积大,适合短音频和录音场景 |
| 音频 | FLAC | 无损,Chrome/Android 支持尚可,Safari 支持差 |
| 音频 | Opus | 压缩率高,现代浏览器基本都支持 |
实际项目里我会这样选:视频优先出 MP4(H.264 编码),音频优先出 MP3 或者 AAC。这不代表其他格式不好,而是你面向 C 端用户时,永远要用兼容面最大的方案兜底。如果追求更高画质或更小体积,再在 MP4 之外额外提供 WebM 版本,让浏览器自己选。
1.3 一个最小可运行的页面
先给一个最基础的页面,你直接放到静态服务器目录下就能跑:
html复制<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML 音视频最小示例</title>
</head>
<body>
<video src="movie.mp4" controls muted autoplay loop playsinline></video>
<audio src="music.mp3" controls preload="metadata"></audio>
</body>
</html>
这里有两个细节要注意:一是 video 的 autoplay 在 Chrome 里单独用往往不生效,必须配合 muted 才能自动播放,这是因为浏览器不允许“有声自动播放”打扰用户;二是 playsinline 这个属性,在 iOS Safari 上如果不加,视频一点播放就会自动进入系统全屏播放,体验很突兀。后面我会展开讲移动端。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. audio/video 标签的核心属性与控制 API:从播放到自定义播放器
2.1 常用属性:每个属性值都可能是个坑
网上有大量属性列表,但我建议你先记住下面这几个高频的,知道它们各自是干嘛的,比死记硬背属性名更有用。
| 属性 | 作用 | 我的提醒 |
|---|---|---|
| src | 指定音视频地址 | 可以运行时动态改 |
| controls | 显示原生控制条 | 样式不可控,多端表现不一致 |
| autoplay | 自动播放 | 移动端和 Chrome 都要求静音或用户手势 |
| muted | 静音播放 | 常和 autoplay 搭配使用 |
| loop | 循环播放 | 适合背景音乐、Banner 视频 |
| preload | 预加载策略 | 取值 none/metadata/auto,影响流量 |
| poster | 视频封面图 | 视频加载前显示,可以先给个占位 |
| playsinline | 内联播放 | iOS 上非常重要 |
| volume | 音量,范围 0-1 | 只读属性,要用 JS 修改 |
| currentTime | 当前播放位置,单位秒 | 可以读写,实现进度跳转 |
| playbackRate | 播放倍速 | 支持 0.5、1、1.25、2 等 |
这里面最容易坑到人的是 preload。preload="auto" 表示浏览器会尽可能把整个资源加载完,听起来体验很好,但如果你的页面里塞了好几个视频,用户一进来就疯狂下载,流量和带宽直接崩。我建议普通场景用 preload="metadata",让浏览器只读取视频的基础信息(时长、分辨率、封面),等用户真正点播放再加载完整资源。
2.2 用 JavaScript 控制播放器的五个核心操作
原生 controls 虽然能用,但项目做到后面一定会遇到自定义播放器需求:统一样式、加广告、加弹幕、埋点统计。这时候你控制的核心 API 其实是这几个:
javascript复制const video = document.querySelector('video');
// 播放与暂停
video.play();
video.pause();
// 跳转进度,单位秒
video.currentTime = 30;
// 音量与静音
video.volume = 0.5;
video.muted = true;
// 倍速播放
video.playbackRate = 1.5;
// 全屏
video.requestFullscreen();
这段代码看起来简单,但有几个边界条件我踩过坑:
第一,play() 返回的是一个 Promise。如果浏览器不允许自动播放,这个 Promise 会 reject,你会看到控制台报 Uncaught (in promise) 的错。所以稳妥写法是:
javascript复制const promise = video.play();
if (promise !== undefined) {
promise.catch(() => {
// 自动播放被拦截,展示一个播放按钮让用户手动触发
});
}
第二,currentTime 赋值不一定立即生效。视频还在加载时,直接设置 currentTime 有可能会导致浏览器重新请求一段新的数据,最好等到 loadedmetadata 事件触发后再跳转。
第三,requestFullscreen 全屏时,默认显示的是浏览器的全屏界面,视频区域会被拉伸到屏幕尺寸,但里面的控制条未必显示。如果你做了自定义控制栏,还得在全屏之后控制它的显示/隐藏逻辑,这个不难,但很容易漏。
2.3 为什么我建议做自定义控制栏
原生 controls 最省事,但它有一个天然问题:不同浏览器渲染出来的样式完全不一样。Chrome 是一套,Firefox 是一套,Safari 又是另一套,你想在 iOS 和 Android 上做到完全一致的播放器外观,几乎不可能。
所以中大型项目基本都会自己做控制栏。核心思路是:保留 video 标签,但去掉 controls 属性,外面套一个自定义容器,用 HTML/CSS 画出播放按钮、进度条、时间、音量,再用上面的 JS API 去联动。这样控制的是你自己的 DOM,样式完全可控。
不过自定义控制栏的坑也很明显:进度条拖动到底要多精细,音量按钮点击后如何防误触,全屏时控制栏怎么定位。我的建议是,初期不要追求大而全,先把“播放/暂停、进度条、时间显示、音量、全屏”这五个做到,就足够覆盖 90% 的业务场景了。
2.4 加载策略:别让大文件拖垮页面首屏
很多人刚开始做音视频页面,最喜欢直接把 video 标签扔在页面里,src 指向一个几十 MB 的 MP4。结果页面打开的时候,浏览器会优先下载视频数据,首屏渲染被拖慢,用户滑动页面还会卡顿。
正确的做法是:第一个视频文件不要直接加载,先用一张 poster 占位,等用户点击播放后再动态给 video 赋 src 并调用 play()。如果页面里确实需要展示多个视频,优先考虑“点击哪个播哪个”,而不是一进来全部加载。等业务量上来后,还可以做“下一条预加载”的优化:当前视频快要播完时,再悄悄把下一个视频拉到内存里,减少切换等待时间。
3. 本地文件无法预览的真相:file 协议、MIME 类型与静态服务器
3.1 为什么双击 HTML 文件黑屏
这个问题的出现频率,在所有 HTML 音视频相关搜索里能排进前三。很多人写好了代码,双击桌面上的 HTML 文件,视频区域黑乎乎一片,控制台报错:Not allowed to load local resource。其实真相不是你的代码错了,而是“打开页面的方式”从一开始就不对。
双击 HTML 文件时,浏览器地址栏里显示的是 file:///C:/Users/xxx/index.html 这样的协议。file 协议下,浏览器对本地文件的读取限制极其严格。你用 img 标签引用一张本地图片可能还能凑合显示,但音视频文件、AJAX 请求、ES Module 这些功能,很多浏览器会直接禁止以 file 协议访问本地资源。简单说,浏览器不信任“一个本地文件去读取另一个本地文件”,这是它的安全边界。
3.2 file 协议、跨域与 MIME:三个坑其实是一件事
你可以先做一个实验:右键点击 HTML 文件,选择“打开方式”里的浏览器,这时候控制台大概率出现两类错误。一类是跨域相关,提示你 CORS 策略禁止加载本地 file 资源;另一类是 MIME 类型错误,提示 resource interpreted as Audio but transferred with MIME type application/octet-stream。
很多人把这两件事分开看,其实它们背后的核心都是:浏览器拿到的不是来自 HTTP 服务器的响应,而是一个本地文件的路径。没有服务器,就没有办法告诉你这个资源该以什么样的 Content-Type 返回,也没有办法告诉浏览器“允许这个页面访问这个资源”。只要你在本地起一个静态服务器,用 http://localhost 打开页面,很多莫名其妙的问题都会自动消失。
3.3 最省事的三种本地预览方案
我自己的固定操作是先用 Python 起个静态服务,因为最快:
bash复制cd 你的项目目录
python3 -m http.server 8080
启动后浏览器访问 http://localhost:8080/你的页面.html 就能看到效果。如果你不会 Python,也可以用 VS Code 的 Live Server 插件,右键页面选择 Open with Live Server 就能起来。同样是前端工具的 npx serve 也可以:
bash复制npx serve .
这三种方式本质都一样:让页面通过 http:// 协议访问,浏览器才会正常加载同目录下的 MP4、MP3、m3u8。
需要注意一个细节:如果你的页面是放在某个子目录里的,引用视频时最好用相对路径,例如 src="./video/movie.mp4",而不要用绝对路径 src="/video/movie.mp4"。绝对路径在本地服务器下会直接从根目录找资源,一旦文件夹结构变了就容易 404。
3.4 线上部署时常见的资源引用问题
本地预览搞定了,上线又是另一套逻辑。线上部署最常见的坑是资源路径写死成本地地址,比如 src="C:/Users/xxx/movie.mp4"。这个在别人电脑上肯定打不开,必须换成相对路径或者 CDN 地址。
还有一种是跨域资源没处理。如果你的视频放在 A 域名,页面在 B 域名,直接用 video.src 引用很多时候是可以播放的,因为 video 标签天然允许跨域加载资源,但如果你要做一些更高级的玩法——比如用 canvas 抓视频帧、用 Web Audio 分析音频数据、在视频上叠加特效——就必须给 video 标签加上 crossorigin="anonymous",同时服务器响应头里返回 Access-Control-Allow-Origin,不然浏览器会把你的 JS 拦截住。
4. 流媒体播放与编码兼容:m3u8、HEVC、推拉流的浏览器边界
4.1 为什么现在的视频平台不直接放 MP4
很多新手会想:既然 MP4 兼容性那么好,为什么 B站、西瓜视频网页端不用一个 video 标签指向 MP4 完事?这个问题的答案,决定了你对流媒体理解的深度。
直接放一个 MP4 文件,常见的问题有两个:第一,文件太大,用户想看 10 分钟后的内容,服务器得多难受才能把整个文件推过来;第二,直播场景根本没有完整的 MP4 文件,数据是不断产生的。所以实际项目几乎都会用流媒体协议,把视频切成一个个小分片,播放器边下边播,跳进度时只需要下载对应时间附近的几个分片。HLS 是这套思路里最成功的协议之一。
4.2 HLS 与 m3u8:Safari 原生支持,Chrome 要借助 hls.js
HLS 协议对外表现是一个 m3u8 文件,正文里是很多个 ts 分片的播放列表。浏览器拿到 m3u8 后,会按照列表里的地址继续请求对应的视频分片。Safari 从很早开始就原生支持 HLS,所以你可以在 Safari 里直接写 video.src = "xxx.m3u8" 播放。但 Chrome 和 Firefox 一直不原生支持 HLS,需要引入 hls.js 把 m3u8 解析并转成浏览器认识的格式。
html复制<script src="https://cdn.jsdelivr.net/npm/hls.js@1"></script>
<video id="video" controls></video>
<script>
const video = document.getElementById('video');
const src = 'https://example.com/playlist.m3u8';
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari 原生 HLS
video.src = src;
}
</script>
这里需要提醒两点:一是 m3u8 必须是可访问的完整地址,不能是相对路径;二是如果服务器开启了防盗链,你可能要带上 referer 或 token,hls.js 里可以通过配置自定义请求头处理。实际在做直播和点播时,服务端给出的 m3u8 往往还是带鉴权参数的,链接几小时就失效,这些都需要后端配合。
4.3 编码兼容与 HEVC 视频扩展
说完了容器和协议,再往深一层就是编码。MP4 只是容器,里面视频数据是 H.264 编码还是 H.265(HEVC)编码,完全是两回事。H.264 兼容性好,但压缩率不如 H.265;H.265 画质更优、体积更小,但在浏览器里的支持情况很微妙。
很多人问为什么某台 Windows 电脑播放 HEVC 视频黑屏,核心原因不是浏览器不支持,而是操作系统没装解码器。Windows 上需要安装“HEVC 视频扩展”才能让 Chrome 或者 Edge 硬解 H.265。macOS 这边 Safari 兼容性相对好一些。如果你不想跟用户解释这些,最稳妥的做法是:后端统一转出 H.264 编码的 MP4,清晰度选一个中间档,优先保证能播。
如果你确实想支持 HEVC,可以先用 canPlayType 检测一下当前的浏览器环境:
javascript复制const support = video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"');
if (support === 'probably') {
// 支持 HEVC,使用 HEVC 版本
} else {
// 不支持,退回 H.264 版本
}
这里我建议不要把判断逻辑写死,因为你无法预料用户是在 Windows、Mac 还是手机上访问。实践里更常见的做法是提供一个多码率切换菜单,用户自动选择或自动降级,而不是赌某一个编码浏览器一定能解。
4.4 视频推拉流是什么:RTMP、RTSP、WebRTC 和 video 标签的关系
“视频推拉流”是监控、直播、音视频 SDK 文档里经常出现的词。推流,是把采集到的画面和声音推到服务器;拉流,是从服务器把视频数据拉下来播放。很多人误以为 video 标签可以直接拉 RTSP 流,这是理解偏差。
浏览器里的 video 标签不支持 RTSP,也不支持 RTMP,因为它们不是 HTTP 协议。RTSP 常用于摄像头和流媒体服务器之间,RTMP 曾经是直播推流的主流,但浏览器端没法直接用。现在网页直播的主流做法是:服务端把 RTMP/RTSP 流转封装成 HLS 分片,前端用 video + hls.js 播放;或者用 WebRTC 做低延迟直播,前端通过 WebRTC API 拉流,再用 video 标签渲染画面的流,而不是直接把 src 指向 WebRTC 地址。
| 协议 | 延迟 | 浏览器支持 | 适用场景 |
|---|---|---|---|
| HLS | 延迟较高(几秒到十几秒) | Safari 原生,Chrome 用 hls.js | 点播、常规直播 |
| RTMP | 低 | 不支持 | 推流到服务器 |
| RTSP | 低 | 不支持 | 摄像头、监控 |
| WebRTC | 低 | 支持,但需要单独封装 | 视频通话、低延迟直播 |
如果你在做一个产品,同时要求“页面能看监控画面”和“尽量不额外付费买服务”,比较接地气的方案是:用 ffmpeg 把 RTSP 流转成 HLS 分片,丢到 Nginx 里,再由浏览器播放。代价是延迟会高几秒,但胜在方案成熟、实现成本低。
5. 音频处理进阶:裁剪、提取网页音频与 Web Audio API
5.1 浏览器里的音频处理:AudioContext 是一个水管系统
video 和 audio 标签负责“播放”,但如果你的需求是“给音频加音量表”“截取一段音频”“做实时变声”,这就不只是播放的范畴了,要用到 Web Audio API。很多前端第一次接触 Web Audio 会被一堆节点名吓到,比如 AudioContext、AnalyserNode、GainNode。你可以把 AudioContext 理解成一套水管系统,音频数据是水流,节点是不同功能的阀门:sourceNode 是源头,gainNode 控制音量大小,analyserNode 可以查看水流的波形,最后连接到 destination(也就是音箱)。
5.2 做音频裁剪的基本思路
网页里裁剪音频,核心操作分三步:解码原始音频、截取指定区间、重新编码导出。下面是一个提取音频片段并生成新 AudioBuffer 的关键思路:
javascript复制const arrayBuffer = await fetch('music.mp3').then(r => r.arrayBuffer());
const audioContext = new AudioContext();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const start = 10; // 从第 10 秒开始
const duration = 5; // 截 5 秒
const newBuffer = audioContext.createBuffer(
audioBuffer.numberOfChannels,
duration * audioBuffer.sampleRate,
audioBuffer.sampleRate
);
for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {
newBuffer.copyToChannel(
audioBuffer.getChannelData(ch).subarray(
start * audioBuffer.sampleRate,
(start + duration) * audioBuffer.sampleRate
),
ch
);
}
核心逻辑是拿到原始 PCM 数据后,按采样率换算成数组下标,截取中间那一小段,再放到新的 AudioBuffer 里。这个新 AudioBuffer 可以直接播放,也可以进一步编码成 WAV 文件下载。需要注意:浏览器默认不提供 MP3 编码器,如果你非要导出 MP3,得引入 lamejs 这类 JS 库;如果只是自己用,WAV 无损格式已经够用,体积不必太担心。
5.3 提取网页音频的通用思路:从 B 站音频场景说起
搜“B站音频下载”这类词的场景,本质是想把网页里正在播放的声音存下来。常规做法不是去抓包然后用专用下载器,而是:打开开发者工具,切到 Network 面板,播放视频,过滤媒体类型的请求,找到音频流地址,然后手动下载。但这里有两个前置障碍:第一,很多平台做了防盗链,请求头校验不过就直接拒绝;第二,即使你拿到了音频地址,如果服务器没有允许跨域,你用 JS 去 fetch 这段音频也会被浏览器拦下。
如果你是在合法授权的前提下,想做“下载网页音频”的功能,可以走 Blob URL 路线:
javascript复制const response = await fetch('https://example.com/audio.m4s', {
headers: {
'Referer': 'https://example.com'
}
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
// url 可以直接用于 audio.src,也可以触发下载
但我也要说一句:平台上的音频资源多受版权保护,自己做来研究技术、分析数据结构没问题,千万别拿去做二次传播或商业化,这会给你自己和平台都带来风险。这一章的真正价值是让你理解浏览器对跨域资源、媒体请求和 Blob URL 的完整链路。
5.4 做一个最简单的音频可视化
Web Audio 里面最直观、也最能提升页面逼格的就是“音频波形可视化”。原理是用 AnalyserNode 不断采样当前播放音频的频域数据,再把数据画到 canvas 上。基础代码如下:
javascript复制const audioContext = new AudioContext();
const source = audioContext.createMediaElementSource(audioElement);
const analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
analyser.connect(audioContext.destination);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
function draw() {
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
// 用 dataArray 的值画 canvas 柱状图或波形
}
这里有个容易被忽略的细节:audio 元素一旦通过 createMediaElementSource 接入了 AudioContext,它的声音输出就由 AudioContext 控制,原本标签里的音量、静音设置仍然生效,但你需要确保 source.connect 到 destination,否则会“播了但听不见”。很多新手在这里卡住,就是因为只连了 AnalyserNode 忘了连 destination。
6. 场景延伸:从网页到硬件,HTML 音频的“出圈”玩法
6.1 蓝牙音频接收器与网页音频的延时问题
搜“蓝牙音频接收器模块”“蓝牙音频接收器”很大一部分人是打算自己DIY一个蓝牙音箱,或者把老音箱变成无线音箱。这和 HTML 音频有什么关系?如果你用网页去控制一块树莓派/开发板上的播放器,音频最终通过蓝牙音频接收器输出,就会遇到一个典型问题:延迟偏大。
蓝牙音频链路走 A2DP 协议,传输编码(SBC、AAC、aptX)会引入几十毫秒到几百毫秒不等的延迟。网页端播放视频时,如果声音通过蓝牙音箱输出,画面和声音不同步的现象在低端蓝牙接收器上特别明显。我的处理经验是:做视频场景时优先选支持 aptX Low Latency 或接收端带低延迟模式的蓝牙模块;如果只是听歌、播报通知,普通蓝牙模块问题不大。HTML5 提供的 audio 标签没有直接的“降低蓝牙延迟”参数,这个物理链路损耗需要在硬件选型阶段控制好。
6.2 MAX98357A、TDA2030 这类功放模块与网页播放链路
MAX98357A 是数字功放模块,常用 I2S 接口,可以直接接树莓派/Pico;TDA2030 是经典模拟功放芯片,适合自己搭音箱功放电路。很多折腾硬件的人会问:能不能让网页来播放音频,然后通过这些东西发声。
能,但要拆清楚职责。浏览器负责解码和播放音频,输出的是数字音频流;树莓派之类的设备负责接收流并传给 MAX98357A,再由功放芯片推动喇叭。也就是说,网页只是“音源”,真正把电流放大到喇叭能推动的级别,是功放模块的活。如果你做的是局域网播放器,可以让树莓派跑一个 web 服务,页面里放几个预设的音频文件,点击播放,树莓派通过 I2S 输出到 MAX98357A,这样做比原生 App 省很多开发量。
涉及功放模块时有一个实际工程问题:避免爆音。在网页上点击播放的瞬间,如果音频电平突然从 0 跳到高音量,MAX98357A 这类数字功放会发出“啪”的一声。解决办法是在前端做音量淡入:播放开始时把 audio.volume 从 0 逐步增加到目标值,比如每 50 毫秒加 0.05,听起来会顺滑很多。
6.3 移动端的几个硬约束:autoplay、playsinline 与 AudioContext
移动端是 HTML 音频/视频真正的大考。我在测试 Android 和 iOS 时,几乎每次都能碰到新的限制。最核心的有三个:
- iOS Safari 不允许带声音的自动播放,必须由用户手势触发。视频想自动循环播放当背景,必须 muted + playsinline。
- Web Audio 在 iOS 上初始状态往往是 suspended,需要你在用户首次点击的触摸事件里调用 audioContext.resume(),否则整个音频管道不工作。
- 视频播放时如果想让它是“嵌在页面里”而不是“跳出全屏”,要同时加上 playsinline 和 webkit-playsinline。
html复制<video src="demo.mp4" muted playsinline webkit-playsinline autoplay loop></video>
javascript复制document.addEventListener('click', () => {
if (audioContext.state === 'suspended') {
audioContext.resume();
}
}, { once: true });
移动端的另一个坑是屏幕常亮。用户看视频时如果屏幕自动熄灭了,播放会继续但体验很差。浏览器有 Page Visibility API,你可以监听 visibilitychange,页面不可见时自动暂停,回来时再恢复,这也是很多成熟播放器的默认行为。
7. 我调试 HTML 音视频几个固定的动作
最后分享几个我自己的排查习惯,每次遇到“网页放不出音频/视频”的问题,我都会按固定顺序扫一遍。
第一看 Network 面板。先确认视频或音频文件有没有发请求,请求返回的状态码是不是 200,响应头里的 Content-Type 是不是 video/mp4 或 audio/mpeg。这一步能筛掉一半问题——很多时候只是地址 404 或者服务器 MIME 配错了。
第二看 MediaError。HTMLMediaElement 有一个 error 对象,能在事件里输出:
javascript复制video.addEventListener('error', () => {
console.log(video.error.code, video.error.message);
});
这里的 code 如果是 4,代表资源不可用;如果是 2,代表网络取资源失败。配合 message,基本能定位是网络问题还是解码问题。
第三看 chrome://media-internals。Chrome 自带的媒体调试面板,能看到每一个媒体元素的播放状态、解码器、错误信息。页面无法播放时,打开这个页面看具体的 error 字段,往往比瞎猜快得多。
第四,如果视频是“黑屏但有声音”,优先怀疑视频编码或帧尺寸问题;如果是“有画面但没有声音”,检查服务端是不是把音频轨道丢失了,或者容器里的音频编码是不是浏览器不支持的格式。
HTML 音频/视频这套体系,往浅了说就是一个 video 标签一个 src,往深了说涉及编码、协议、跨域、硬件解码、音频图,能挖的内容非常多。我这些年做播放器相关项目的最大感受是:不要在同一个坑里踩两次,提前把这些边界条件都过一遍,遇到问题就按上面的排查链路走,比靠运气碰答案要可靠得多。
