第一章:HTML <audio> 元素基础 —— 从零掌握 Web 音频的起点

这是「Web 音频开发技术知识库」系列的第一篇。在深入 Web Audio API、MSE、WebRTC 这些"重武器"之前,我们先把最基础、最常用的 <audio> 元素吃透。别小看这个标签,里面藏着不少坑和门道。


一、为什么从 <audio> 开始?

2008 年之前,网页想播放音频,必须借助 Flash 插件。HTML5 带来了原生 <audio> 标签,彻底改变了这一局面。时至今日,<audio> 仍然是 Web 音频开发的基石——无论是简单的背景音乐、播客播放器,还是复杂的在线 DAW(数字音频工作站),底层都离不开它。

理解 <audio> 元素,不仅是学会"放个音乐"那么简单,更是打开后续所有 Web 音频 API 大门的钥匙。HTMLMediaElement、音频事件系统、MSE……这些内容的操作对象,本质上都是 <audio> 元素本身。


二、最基础的用法:一行代码播放音频

<audio src="./music.mp3" controls></audio>

就这一行,浏览器会渲染出一个带有播放/暂停、进度条、音量控制的原生播放器。controls 属性是关键——没有它,标签存在但不可见,也没有任何 UI。

&lt;source&gt; 多格式兼容写法

不同浏览器对音频格式的支持不一样(这个问题在第四章会深入展开),所以生产环境中推荐用 &lt;source&gt; 标签提供多个格式备选:

<audio controls>
  <source src="music.ogg" type="audio/ogg" />
  <source src="music.mp3" type="audio/mpeg" />
  <source src="music.aac" type="audio/aac" />
  <p>你的浏览器不支持 HTML5 音频,请升级浏览器。</p>
</audio>

浏览器会从上到下依次尝试,找到第一个自己能播放的格式就停下来。最后的 &lt;p&gt; 是兜底的降级提示,针对极老的浏览器显示。

最佳实践:格式优先级建议 ogg/opus > mp3 > aac。Opus 编码在相同音质下体积最小,但 Safari 支持较晚;MP3 兼容性最广;AAC 是 Apple 生态的首选。


三、核心属性全解析

&lt;audio&gt; 标签的属性不多,但每一个都值得认真理解。

controls — 显示原生控制条

<audio src="music.mp3" controls></audio>

原生控制条的样式由浏览器决定,各浏览器差异较大,且很难通过 CSS 自定义。这就是为什么几乎所有正式项目都会隐藏原生控制条,自己用 JavaScript 写一套 UI。

autoplay — 自动播放(有大坑!)

<audio src="music.mp3" autoplay></audio>

这是新手最常踩的坑之一。现代浏览器(Chrome 66+、Firefox、Safari)默认阻止自动播放有声音频,除非满足以下条件之一:

  • 用户曾经与该页面有过交互(点击、键盘输入等)
  • 音频设置了 muted 属性(静音可以自动播放)
  • 该网站被用户加入了"允许自动播放"白名单
<!-- 静音自动播放是被允许的 -->
<audio src="music.mp3" autoplay muted></audio>

为什么有这个限制? 浏览器为了保护用户体验,防止页面一打开就突然响起音乐。要触发有声自动播放,必须在用户交互事件的回调中调用 audio.play()

loop — 循环播放

<audio src="music.mp3" controls loop></audio>

音频播放结束后自动从头开始。适合背景音乐、游戏音效等场景。

muted — 静音

<audio src="music.mp3" controls muted></audio>

初始化为静音状态,用户可以手动调节音量。常与 autoplay 配合使用。

preload — 预加载策略(性能关键!)

<audio src="music.mp3" preload="auto"></audio>

这个属性对性能影响很大,有三个值:

含义适用场景
none不预加载任何数据页面有大量音频、流量敏感场景
metadata只加载元数据(时长、格式等)大多数场景的推荐值
auto浏览器决定是否预加载完整音频用户大概率会播放的核心音频

注意:preload 只是给浏览器的建议,浏览器可以忽略它。移动端浏览器通常会忽略 auto,以节省流量。

crossorigin — 跨域资源处理

<audio src="https://cdn.example.com/music.mp3" crossorigin="anonymous"></audio>

当音频文件来自不同域时,如果你需要通过 Web Audio API 对其进行处理(如音频可视化),就必须设置这个属性,否则会遇到 CORS 错误。值为 anonymous(不带凭证)或 use-credentials(带 Cookie 等凭证)。


四、用 JavaScript 控制音频播放

实际项目中,我们几乎不用原生控件,而是通过 JavaScript 完全接管播放逻辑。

获取 audio 元素

// 方式一:获取 HTML 中已有的元素
const audio = document.querySelector('audio');
 
// 方式二:纯 JS 动态创建(不需要 HTML 标签)
const audio = new Audio('music.mp3');

new Audio()HTMLAudioElement 的构造函数,创建出的对象和 HTML 中的 &lt;audio&gt; 完全等价。

播放与暂停

// play() 返回一个 Promise!
const playPromise = audio.play();
 
if (playPromise !== undefined) {
  playPromise
    .then(() => {
      console.log('播放成功');
    })
    .catch((error) => {
      // 最常见的错误:NotAllowedError(自动播放被阻止)
      console.error('播放失败:', error.name, error.message);
    });
}
 
// 暂停
audio.pause();

重点audio.play() 返回的是 Promise,不是同步操作。很多 bug 都来自忽略这个 Promise,导致在播放还未开始时就调用了 pause(),触发 AbortError

控制进度

// 读取当前播放时间(秒)
console.log(audio.currentTime);
 
// 跳转到指定时间(秒)
audio.currentTime = 30; // 跳到第 30 秒
 
// 获取总时长(需要 metadata 加载完成后才有值)
console.log(audio.duration);

控制音量

// 音量范围 0.0 ~ 1.0
audio.volume = 0.5; // 50% 音量
 
// 静音切换
audio.muted = true;
audio.muted = false;

五、实战:手写一个自定义音频播放器

下面用原生 HTML + CSS + JavaScript 实现一个功能完整的自定义播放器,包含:播放/暂停、进度条拖拽、音量控制、时间显示。

HTML 结构

<div class="player">
  <audio id="audio" src="music.mp3" preload="metadata"></audio>
 
  <div class="player-info">
    <span class="song-title">未知歌曲</span>
  </div>
 
  <div class="progress-bar" id="progressBar">
    <div class="progress-fill" id="progressFill"></div>
  </div>
 
  <div class="controls">
    <span class="time" id="currentTime">0:00</span>
    <button id="playBtn">▶</button>
    <span class="time" id="duration">0:00</span>
    <input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1" />
  </div>
</div>

CSS 样式

.player {
  width: 360px;
  background: #1a1a2e;
  border-radius: 16px;
  padding: 24px;
  color: #fff;
  font-family: sans-serif;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
 
.player-info {
  text-align: center;
  margin-bottom: 16px;
  font-size: 14px;
  opacity: 0.8;
}
 
.progress-bar {
  width: 100%;
  height: 6px;
  background: #333;
  border-radius: 3px;
  cursor: pointer;
  margin-bottom: 16px;
  position: relative;
}
 
.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #667eea, #764ba2);
  border-radius: 3px;
  width: 0%;
  transition: width 0.1s linear;
}
 
.controls {
  display: flex;
  align-items: center;
  gap: 12px;
}
 
#playBtn {
  background: linear-gradient(135deg, #667eea, #764ba2);
  border: none;
  border-radius: 50%;
  width: 44px;
  height: 44px;
  font-size: 16px;
  color: #fff;
  cursor: pointer;
  flex-shrink: 0;
}
 
.time {
  font-size: 12px;
  opacity: 0.7;
  min-width: 36px;
}
 
#volumeSlider {
  flex: 1;
  accent-color: #667eea;
}

JavaScript 核心逻辑

const audio = document.getElementById('audio');
const playBtn = document.getElementById('playBtn');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
const currentTimeEl = document.getElementById('currentTime');
const durationEl = document.getElementById('duration');
const volumeSlider = document.getElementById('volumeSlider');
 
// 格式化时间:秒 -> "m:ss"
function formatTime(seconds) {
  if (isNaN(seconds)) return '0:00';
  const m = Math.floor(seconds / 60);
  const s = Math.floor(seconds % 60).toString().padStart(2, '0');
  return `${m}:${s}`;
}
 
// 播放/暂停切换
playBtn.addEventListener('click', () => {
  if (audio.paused) {
    audio.play().catch((err) => console.error(err));
    playBtn.textContent = '⏸';
  } else {
    audio.pause();
    playBtn.textContent = '▶';
  }
});
 
// 音频 metadata 加载完成后更新总时长
audio.addEventListener('loadedmetadata', () => {
  durationEl.textContent = formatTime(audio.duration);
});
 
// 播放进度更新(timeupdate 事件高频触发,约每 250ms 一次)
audio.addEventListener('timeupdate', () => {
  const percent = (audio.currentTime / audio.duration) * 100;
  progressFill.style.width = `${percent}%`;
  currentTimeEl.textContent = formatTime(audio.currentTime);
});
 
// 点击进度条跳转
progressBar.addEventListener('click', (e) => {
  const rect = progressBar.getBoundingClientRect();
  const clickX = e.clientX - rect.left;
  const ratio = clickX / rect.width;
  audio.currentTime = ratio * audio.duration;
});
 
// 音量控制
volumeSlider.addEventListener('input', () => {
  audio.volume = volumeSlider.value;
  audio.muted = volumeSlider.value == 0;
});
 
// 播放结束
audio.addEventListener('ended', () => {
  playBtn.textContent = '▶';
  progressFill.style.width = '0%';
  audio.currentTime = 0;
});

这个播放器已经具备了生产可用的基本功能。你可以在此基础上扩展:歌曲列表切换、歌词同步、播放模式(随机/顺序/单曲循环)等。


六、常见问题与避坑指南

问题一:audio.durationNaN

原因:你在 loadedmetadata 事件触发之前就读取了 duration。音频的元数据需要时间加载,必须等事件触发后才能拿到准确值。

// 错误做法
console.log(audio.duration); // NaN
 
// 正确做法
audio.addEventListener('loadedmetadata', () => {
  console.log(audio.duration); // 正确的时长
});

问题二:play()AbortError

原因:在上一次 play() 的 Promise 还未 resolve 时,就调用了 pause()

// 安全的播放/暂停切换写法
let playPromise;
 
function safePlay() {
  playPromise = audio.play();
}
 
function safePause() {
  if (playPromise !== undefined) {
    playPromise.then(() => audio.pause()).catch(() => {});
  }
}

问题三:iOS Safari 无法自动播放?

iOS 对自动播放的限制极其严格,即使是 muted 也不一定能播放。唯一可靠的方案是:在用户触摸事件(touchstartclick)的回调中调用 audio.play(),并且不能有任何异步间隔。

// 在用户交互中"解锁"音频
document.addEventListener('touchstart', () => {
  audio.play().then(() => audio.pause()); // 先播再停,解锁音频上下文
}, { once: true });

问题四:跨域音频无法用 Web Audio API 处理?

确保服务端设置了正确的 CORS 响应头,并在 &lt;audio&gt; 标签上添加 crossorigin="anonymous"

<audio src="https://cdn.example.com/music.mp3" crossorigin="anonymous"></audio>

服务端需返回:

Access-Control-Allow-Origin: *

七、&lt;audio&gt; 的能力边界

学到这里,我们也要清醒地认识 &lt;audio&gt; 的局限性,这也是后续章节存在的意义:

  • 无法实时处理音频数据(如添加混响、均衡器效果)→ 需要 Web Audio API(第七章)
  • 无法播放动态生成的音频流(如直播、自适应码率)→ 需要 MSE(第六章)
  • 无法精确控制音频缓冲区(如游戏音效的极低延迟)→ 需要 AudioBuffer(第七章)
  • 无法获取麦克风输入(如录音、实时通话)→ 需要 WebRTC / MediaStream(第十章)

&lt;audio&gt; 是入口,但不是终点。


八、本章知识图谱

流程图画布 · 115%
Mermaid 流程图加载中...

小结

&lt;audio&gt; 元素看似简单,实则细节满满。从属性配置到 JavaScript 控制,从自定义 UI 到各种边界问题,每一块都值得认真打磨。本章是整个系列的基石,后续所有章节的内容都建立在对 &lt;audio&gt; 元素的深刻理解之上。

下一章我们将深入 HTMLMediaElement API——那是 &lt;audio&gt; 背后真正的"控制台",包含更多属性、方法和状态机逻辑,带你从"会用"进阶到"精通"。