第二章:HTMLMediaElement 音频 API —— <audio> 背后真正的控制台
上一章我们掌握了
<audio>标签的基础用法。但你有没有想过,当你调用audio.play()时,背后究竟发生了什么?为什么<audio>和<video>能共用那么多 API?答案就在HTMLMediaElement里。这一章,我们把这个接口彻底拆开来看。
一、什么是 HTMLMediaElement?
HTMLMediaElement 是浏览器定义的一个抽象基类接口,它是 HTMLAudioElement 和 HTMLVideoElement 的共同父类。换句话说,<audio> 和 <video> 标签之所以能共用 play()、pause()、currentTime 这些 API,正是因为它们都继承自 HTMLMediaElement。
EventTarget
└── Node
└── Element
└── HTMLElement
└── HTMLMediaElement
├── HTMLAudioElement (<audio>)
└── HTMLVideoElement (<video>)
这个继承链意味着:你在本章学到的所有内容,对 <video> 同样适用。这是 Web 标准设计的精妙之处——音视频共享同一套底层控制逻辑。
二、媒体加载状态机:readyState
这是整个 HTMLMediaElement 中最容易被忽视、但又最重要的概念之一。readyState 描述了媒体资源当前的加载与可用程度,是一个从 0 到 4 的整数值。
console.log(audio.readyState); // 0 ~ 4| 常量名 | 值 | 含义 |
|---|---|---|
HAVE_NOTHING | 0 | 没有任何媒体数据 |
HAVE_METADATA | 1 | 已获取元数据(时长、尺寸等) |
HAVE_CURRENT_DATA | 2 | 当前播放位置有数据,但下一帧没有 |
HAVE_FUTURE_DATA | 3 | 当前和未来位置都有足够数据可以播放 |
HAVE_ENOUGH_DATA | 4 | 数据足够,可以持续播放到结束 |
理解这五个状态,能帮你解决大量"为什么我的 API 调用没有效果"的问题。例如,duration 属性只有在 readyState >= HAVE_METADATA(即 ≥ 1)时才有意义;play() 只有在 readyState >= HAVE_FUTURE_DATA(即 ≥ 3)时才能真正开始输出音频。
function safeSeek(audio, time) {
if (audio.readyState >= HTMLMediaElement.HAVE_METADATA) {
audio.currentTime = time;
} else {
// 等待 metadata 加载完成后再跳转
audio.addEventListener('loadedmetadata', () => {
audio.currentTime = time;
}, { once: true });
}
}三、网络状态:networkState
与 readyState 并列的另一个状态属性,描述的是网络层面的资源加载情况。
console.log(audio.networkState);| 常量名 | 值 | 含义 |
|---|---|---|
NETWORK_EMPTY | 0 | 元素未初始化,还未设置 src |
NETWORK_IDLE | 1 | 已选定资源,但当前未在加载 |
NETWORK_LOADING | 2 | 正在下载资源 |
NETWORK_NO_SOURCE | 3 | 找不到合适的资源(所有 source 都失败) |
networkState === 3 是排查"音频播放不出来"问题的重要线索——如果是这个值,说明 src 路径有问题或所有格式都不被支持。
四、完整属性手册
4.1 资源相关属性
src — 直接设置音频地址,赋值后浏览器立即开始资源选择流程:
audio.src = 'new-music.mp3';
audio.load(); // 重新加载新资源currentSrc — 只读,浏览器最终选定的实际播放地址。当你用了多个 <source> 标签时,currentSrc 告诉你浏览器选了哪个:
console.log(audio.currentSrc);
// "https://example.com/music.ogg"(如果 ogg 被选中)srcObject — 用于直接绑定 MediaStream 或 MediaSource 对象,这是第六章(MSE)和第十章(WebRTC)的核心入口:
// 将麦克风流直接绑定到 audio 元素播放
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
audio.srcObject = stream;
audio.play();
});4.2 播放控制属性
currentTime — 当前播放位置(秒),可读写。写入时触发跳转(seek)操作:
audio.currentTime = 60; // 跳到第 1 分钟duration — 只读,音频总时长(秒)。如果未知则返回 NaN,如果是直播流则返回 Infinity:
// 判断是否为直播流
const isLive = audio.duration === Infinity;playbackRate — 播放速率,默认 1.0。0.5 是半速,2.0 是两倍速。大多数浏览器支持 0.25 ~ 16 的范围,且音调会随速率变化(不像某些专业软件会保持音调):
audio.playbackRate = 1.5; // 1.5 倍速播放defaultPlaybackRate — 设置默认播放速率,在 load() 重置后生效,通常用于"记住用户的速率偏好":
audio.defaultPlaybackRate = 1.5;
audio.load(); // 重新加载后速率仍为 1.5paused — 只读布尔值,是否处于暂停状态。注意:音频结束后 paused 也为 true:
if (audio.paused) {
audio.play();
}ended — 只读布尔值,是否已播放到结尾:
audio.addEventListener('ended', () => {
console.log(audio.ended); // true
});seeking — 只读布尔值,是否正在执行跳转操作(用户拖动进度条时为 true):
audio.addEventListener('seeking', () => {
showLoadingSpinner(); // 跳转中显示 loading
});
audio.addEventListener('seeked', () => {
hideLoadingSpinner(); // 跳转完成隐藏 loading
});4.3 音量相关属性
volume — 音量,范围 0.0 ~ 1.0。设置超出范围会抛出 IndexSizeError:
audio.volume = 0.8;muted — 是否静音,布尔值。静音时 volume 值不变,只是不输出声音:
audio.muted = !audio.muted; // 切换静音defaultMuted — 对应 HTML 中的 muted 属性,表示初始静音状态。与 muted 的区别在于它会被 load() 重置:
audio.defaultMuted = true;
audio.load(); // 重置后仍为静音4.4 缓冲相关属性
buffered — 只读,返回一个 TimeRanges 对象,表示已缓冲的时间段。这在实现进度条缓冲显示时非常有用:
const buffered = audio.buffered;
for (let i = 0; i < buffered.length; i++) {
console.log(`缓冲段 ${i}: ${buffered.start(i)}s ~ ${buffered.end(i)}s`);
}seekable — 只读,TimeRanges 对象,表示可以 seek 的时间范围。直播流的 seekable 通常是空的或只有很小的范围:
if (audio.seekable.length > 0) {
console.log('可 seek 范围:', audio.seekable.start(0), '~', audio.seekable.end(0));
}played — 只读,TimeRanges 对象,记录所有已播放过的时间段(跳过的部分不计入)。
4.5 其他重要属性
autoplay — 对应 HTML 属性,JS 中可动态设置:
audio.autoplay = true;loop — 是否循环播放:
audio.loop = true;controls — 是否显示原生控制条:
audio.controls = false; // 隐藏原生控件,使用自定义 UIpreload — 预加载策略,值为 "none" / "metadata" / "auto":
audio.preload = 'metadata';crossOrigin — 跨域设置,注意 JS 中是驼峰命名 crossOrigin,HTML 中是 crossorigin:
audio.crossOrigin = 'anonymous';error — 只读,返回 MediaError 对象或 null。发生错误时通过它获取错误信息:
audio.addEventListener('error', () => {
const err = audio.error;
console.error(`错误码: ${err.code}, 信息: ${err.message}`);
});MediaError.code 的值:
| 值 | 常量 | 含义 |
|---|---|---|
| 1 | MEDIA_ERR_ABORTED | 用户中止了加载 |
| 2 | MEDIA_ERR_NETWORK | 网络错误 |
| 3 | MEDIA_ERR_DECODE | 解码失败 |
| 4 | MEDIA_ERR_SRC_NOT_SUPPORTED | 格式不支持或 src 无效 |
五、核心方法详解
play() — 开始播放
返回 Promise<void>,这是现代浏览器中最常见的异步陷阱:
async function playAudio(audio) {
try {
await audio.play();
console.log('播放中');
} catch (err) {
if (err.name === 'NotAllowedError') {
// 自动播放策略拦截,需要用户交互
showPlayButton();
} else if (err.name === 'NotSupportedError') {
// 格式不支持
showFormatError();
} else {
console.error('未知播放错误:', err);
}
}
}pause() — 暂停播放
同步方法,无返回值。但如果在 play() 的 Promise resolve 之前调用,会引发 AbortError:
// 安全的停止方法
async function stopAudio(audio) {
try {
await audio.play(); // 确保 play 已 resolve
} catch (e) {
// 忽略
} finally {
audio.pause();
audio.currentTime = 0;
}
}load() — 重置并重新加载
将元素重置到初始状态,重新开始资源选择和加载流程。切换歌曲时必须调用:
function changeSong(audio, newSrc) {
audio.src = newSrc;
audio.load(); // 重置状态,重新加载
audio.play();
}canPlayType(type) — 格式支持检测
返回 ""(不支持)、"maybe"、"probably" 三个字符串之一。注意没有 "yes",这是历史遗留设计:
function getBestFormat(audio) {
const formats = [
{ type: 'audio/ogg; codecs="opus"', src: 'music.opus' },
{ type: 'audio/ogg; codecs="vorbis"', src: 'music.ogg' },
{ type: 'audio/mpeg', src: 'music.mp3' },
{ type: 'audio/aac', src: 'music.aac' },
];
for (const fmt of formats) {
const support = audio.canPlayType(fmt.type);
if (support === 'probably' || support === 'maybe') {
return fmt.src;
}
}
return null; // 没有支持的格式
}
const audio = new Audio();
const bestSrc = getBestFormat(audio);
if (bestSrc) audio.src = bestSrc;fastSeek(time) — 快速跳转(非精确)
相比 currentTime = time 的精确跳转,fastSeek() 会跳到最近的关键帧,速度更快,适合用户拖动进度条时的实时预览:
// 拖动进度条时用 fastSeek,松手后用 currentTime 精确定位
progressBar.addEventListener('mousemove', (e) => {
if (isDragging) {
const ratio = e.offsetX / progressBar.offsetWidth;
audio.fastSeek(ratio * audio.duration);
}
});注意:
fastSeek()目前主要在 Firefox 和 Safari 中支持,Chrome 中等同于currentTime赋值。
六、TimeRanges 对象深入解析
buffered、seekable、played 都返回 TimeRanges 对象,理解它对实现专业播放器至关重要。
TimeRanges 代表一组不连续的时间区间(因为用户可能跳着播放,导致多段缓冲)。它有以下接口:
timeRanges.length // 区间数量
timeRanges.start(index) // 第 index 段的开始时间(秒)
timeRanges.end(index) // 第 index 段的结束时间(秒)实战:实现带缓冲显示的进度条
function renderBufferBar(audio, canvas) {
const ctx = canvas.getContext('2d');
const width = canvas.width;
const duration = audio.duration;
ctx.clearRect(0, 0, width, canvas.height);
// 绘制已缓冲区域(灰色)
ctx.fillStyle = '#555';
for (let i = 0; i < audio.buffered.length; i++) {
const startX = (audio.buffered.start(i) / duration) * width;
const endX = (audio.buffered.end(i) / duration) * width;
ctx.fillRect(startX, 0, endX - startX, canvas.height);
}
// 绘制已播放区域(主色)
ctx.fillStyle = '#667eea';
const playedX = (audio.currentTime / duration) * width;
ctx.fillRect(0, 0, playedX, canvas.height);
}
audio.addEventListener('progress', () => renderBufferBar(audio, progressCanvas));
audio.addEventListener('timeupdate', () => renderBufferBar(audio, progressCanvas));七、媒体资源选择算法(了解底层)
当浏览器遇到一个 <audio> 元素时,它会执行一套标准定义的资源选择算法,理解这个过程能帮你排查很多奇怪的问题:
第一步:确定资源来源
浏览器按以下优先级决定用哪个资源:
- 如果
srcObject不为null,直接使用(MSE / MediaStream 场景) - 如果
src属性有值,使用src - 如果有
<source>子元素,遍历它们
第二步:遍历 <source> 时的判断逻辑
对每个 <source> 元素,浏览器会检查:
type属性是否声明了浏览器支持的 MIME 类型(如果没写type,则跳过格式检查直接尝试)media属性(媒体查询)是否匹配当前环境
<!-- media 属性示例:移动端用低码率版本 -->
<audio controls>
<source src="music-hq.ogg" type="audio/ogg" media="(min-width: 768px)" />
<source src="music-lq.ogg" type="audio/ogg" />
</audio>第三步:触发加载
选定资源后,浏览器根据 preload 属性决定立即加载还是延迟加载,随后依次触发 loadstart → durationchange → loadedmetadata → loadeddata → canplay → canplaythrough 等事件(事件系统在第三章详细展开)。
八、实战:功能完整的播放器类封装
把 HTMLMediaElement 的核心 API 封装成一个可复用的类,是工程化实践的关键一步:
class AudioPlayer {
constructor(src, options = {}) {
this.audio = new Audio();
this.audio.preload = options.preload || 'metadata';
this.audio.crossOrigin = options.crossOrigin || null;
this._playPromise = null;
this._listeners = {};
if (src) this.load(src);
this._bindEvents();
}
// 加载新音频
load(src) {
this.audio.src = src;
this.audio.load();
return this;
}
// 安全播放
async play() {
try {
this._playPromise = this.audio.play();
await this._playPromise;
this._emit('play');
} catch (err) {
this._emit('error', err);
throw err;
}
return this;
}
// 安全暂停
async pause() {
if (this._playPromise) {
await this._playPromise.catch(() => {});
}
this.audio.pause();
this._emit('pause');
return this;
}
// 跳转
seek(time) {
if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {
this.audio.currentTime = Math.max(0, Math.min(time, this.audio.duration));
}
return this;
}
// 设置音量(带边界保护)
setVolume(vol) {
this.audio.volume = Math.max(0, Math.min(1, vol));
return this;
}
// 设置播放速率
setRate(rate) {
this.audio.playbackRate = rate;
return this;
}
// 获取播放进度(0 ~ 1)
get progress() {
if (!this.audio.duration) return 0;
return this.audio.currentTime / this.audio.duration;
}
// 获取缓冲进度(0 ~ 1)
get bufferedProgress() {
const b = this.audio.buffered;
if (!b.length || !this.audio.duration) return 0;
return b.end(b.length - 1) / this.audio.duration;
}
// 格式支持检测(静态方法)
static canPlay(mimeType) {
const a = new Audio();
return a.canPlayType(mimeType) !== '';
}
// 简易事件系统
on(event, fn) {
if (!this._listeners[event]) this._listeners[event] = [];
this._listeners[event].push(fn);
return this;
}
_emit(event, data) {
(this._listeners[event] || []).forEach(fn => fn(data));
}
_bindEvents() {
['timeupdate', 'ended', 'error', 'waiting', 'playing'].forEach(evt => {
this.audio.addEventListener(evt, (e) => this._emit(evt, e));
});
}
// 销毁,释放资源
destroy() {
this.audio.pause();
this.audio.src = '';
this.audio.load();
this._listeners = {};
}
}使用方式:
const player = new AudioPlayer('music.mp3', { preload: 'metadata' });
player
.on('timeupdate', () => {
progressBar.style.width = `%`;
})
.on('ended', () => {
console.log('播放结束');
});
// 用户点击播放按钮
playBtn.addEventListener('click', () => player.play());
// 格式检测
console.log(AudioPlayer.canPlay('audio/ogg; codecs="opus"')); // true/false九、本章知识图谱
小结
HTMLMediaElement 是整个 Web 媒体系统的核心接口。从状态机(readyState / networkState)到资源管理(src / srcObject / load()),从播放控制(play() / pause() / currentTime)到缓冲分析(TimeRanges),每一块都有其独特的设计逻辑和使用场景。
真正掌握这个接口,你会发现很多"玄学 bug"都变得有迹可循——因为你终于看到了 <audio> 背后真正运作的那台机器。
下一章我们将深入音频事件系统——HTMLMediaElement 能抛出十几种事件,它们的触发顺序、触发条件、以及如何在正确的时机做正确的事,是构建健壮播放器的关键所在。