面向 AI 语音回复、实时翻译和 TTS 等场景的浏览器流式音频播放器。它接收持续到达的二进制音频块,通过浏览器原生 Media Source Extensions(MSE)边接收、边解析、边播放。
- 达到可配置的最小缓冲时长后尝试自动播放。
- 使用字节受限 FIFO 队列保存待写数据,不会静默覆盖旧音频。
- 限制播放头前方写入 MSE 的时长,降低长时间播放的内存增长风险。
- 提供
idle、buffering、playing、paused、ended和error状态。 - 支持暂停、恢复、显式播放、结束输入和永久销毁。
- 同时提供 ESM、CommonJS、UMD 和 TypeScript 类型声明。
播放器依赖 HTMLAudioElement 和 Media Source Extensions,只能在支持 MSE 的浏览器环境中播放。包可以被 Node.js 或 SSR 构建工具导入,但不能在没有浏览器媒体 API 的服务端执行 open()。
创建播放器前应检查实际使用的 MIME 类型:
import { AudioStreamPlayer } from '@agents-flex/audio-stream-player';
const mimeType = 'audio/mpeg';
if (!AudioStreamPlayer.isTypeSupported(mimeType)) {
throw new Error(`当前浏览器不支持 ${mimeType}`);
}传给 feed() 的所有 chunk 必须组成浏览器认可的同一条连续媒体流。chunk 边界不必与 MP3 帧或容器片段边界一致,但不要假设多个独立完整文件一定可以直接拼接;最终数据必须符合对应的 MSE byte stream 格式。
- MP3 常用
audio/mpeg。 - AAC/fMP4 通常使用
audio/mp4; codecs="mp4a.40.2",流开头必须包含正确的初始化片段。 - Opus/WebM 通常使用
audio/webm; codecs="opus",同样需要有效的容器初始化数据。 - WAV、裸 PCM 或任意二进制数据不一定能被 MSE 接受。
具体支持情况取决于浏览器,请始终以 AudioStreamPlayer.isTypeSupported() 的结果为准。
npm install @agents-flex/audio-stream-player也可以使用 pnpm 或 yarn:
pnpm add @agents-flex/audio-stream-player
yarn add @agents-flex/audio-stream-playerES Module 推荐写法:
import { AudioStreamPlayer } from '@agents-flex/audio-stream-player';CommonJS 打包工具也可以使用:
const { AudioStreamPlayer } = require('@agents-flex/audio-stream-player');无论采用哪种模块格式,播放器运行时仍然依赖浏览器 MSE API。
下面示例从 Fetch ReadableStream 读取 MP3,并在输入结束后继续播放已经缓冲的数据:
import { AudioStreamPlayer } from '@agents-flex/audio-stream-player';
const audio = document.querySelector<HTMLAudioElement>('#audio')!;
const player = new AudioStreamPlayer({
audioElement: audio,
mimeType: 'audio/mpeg',
autoplay: true,
minBufferMs: 300,
maxBufferMs: 5000,
maxQueueBytes: 16 * 1024 * 1024,
onStateChange: (state) => {
console.log('player state:', state);
if (state === 'ended') {
console.log('所有缓冲音频均已播放完毕');
}
},
onError: (error) => {
console.error('playback error:', error);
},
});
await player.open();
const response = await fetch('/speech.mp3');
if (!response.ok || !response.body) {
player.destroy();
throw new Error(`音频请求失败:${response.status}`);
}
const reader = response.body.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
if (!player.feed(value)) {
// 完整的等待与重试方式见下一节“背压处理”。
throw new Error('播放器暂时无法接收更多数据,请实施背压');
}
}
// 这里只代表输入结束;缓冲区中的音频仍会继续播放。
player.close();
} catch (error) {
player.destroy();
throw error;
}当组件卸载或页面不再需要播放器时释放资源:
player.destroy();feed() 返回 true 表示 chunk 已进入待写队列,不表示浏览器已经完成解析。以下情况会返回 false:
- 尚未完成
open()。 - 已调用
close()或destroy()。 - 播放器已经进入
error。 - 加入该 chunk 后会超过
maxQueueBytes。
队列满时不要丢弃音频,也不要立即结束播放。应暂停上游读取,等待播放推进并释放队列空间后重试同一个 chunk:
const MAX_QUEUE_BYTES = 16 * 1024 * 1024;
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function feedWithBackpressure(
player: AudioStreamPlayer,
chunk: Uint8Array,
signal: AbortSignal,
): Promise<void> {
if (chunk.byteLength > MAX_QUEUE_BYTES) {
throw new Error('单个 chunk 大于 maxQueueBytes');
}
while (!player.feed(chunk)) {
if (signal.aborted) throw signal.reason;
const state = player.getState();
if (state === 'error' || state === 'ended') {
throw new Error(`无法继续 feed,播放器状态为 ${state}`);
}
await delay(25);
}
}该辅助函数必须在 await player.open() 完成后使用。调用 destroy() 时应同时 abort 传入的 signal,确保等待循环能够立即退出。
如果上游本身支持 pause/resume,应优先使用上游的流量控制,而不是轮询。
调用方在 feed() 返回 true 后不应继续修改或复用同一个 Uint8Array 的内容;播放器可能稍后才将其复制给 SourceBuffer。
autoplay: true 表示播放器会在缓冲达到 minBufferMs 后尝试调用 audio.play(),但浏览器仍可能因为缺少用户手势而阻止播放。
自动播放被阻止时,播放器保留缓冲数据,不一定进入 error。可以在用户点击事件中显式播放:
playButton.addEventListener('click', async () => {
try {
await player.play();
} catch (error) {
console.error('无法开始播放', error);
}
});设置 autoplay: false 时,也应使用 play() 手动开始。resume() 只用于恢复此前由 pause() 产生的暂停。
典型生命周期如下:
idle -> buffering -> playing -> ended
^ |
|-----------| 网络波动或缓冲不足
|
paused 用户主动暂停
任何阶段发生不可恢复错误都可能进入 error。
| 状态 | 含义 |
|---|---|
idle |
实例刚创建,尚未成功打开。构造时不会主动触发该状态回调。 |
buffering |
MSE 已打开但数据不足,或播放期间等待更多数据。 |
playing |
audio.play() 已成功,音频正在播放。 |
paused |
用户通过 pause() 或原生 audio 控件暂停。仍可继续接收数据。 |
ended |
已结束输入,并且所有已缓冲音频都已经播放完毕。 |
error |
MSE、SourceBuffer 或 audio 元素发生不可恢复错误。 |
需要特别区分:
close():声明上游不会再产生数据,不会立即进入ended。ended:浏览器已经播放完全部缓冲内容。destroy():永久释放实例;调用后不能再次open()或play()。
每个实例只能成功调用一次 open()。如需播放新的独立媒体流,应销毁旧实例并创建新实例。
MSE 流在调用 endOfStream() 前通常没有可靠的总时长,audio.duration 可能是 Infinity 或 NaN。实时 TTS 尚未生成完成时,本身也不存在最终总时长。
建议 UI 分开显示以下指标:
| 指标 | 读取方式 | 含义 |
|---|---|---|
| 当前播放时间 | audio.currentTime |
当前播放头位置。 |
| 已解析时长 | 所有 audio.buffered.end(i) 的历史最大值 |
浏览器曾经成功解析到的最远时间点。 |
| 已缓冲时长 | 当前连续区间的 end - currentTime |
不发生新输入时还能连续播放多久。 |
| 总时长 | 有限的 audio.duration |
通常在所有数据写入并调用 endOfStream() 后才可靠。 |
因此在总时长未知时,建议显示“流式”,不要展示伪造的播放百分比。项目中的本地 demo 已实现这套展示方式。
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
mimeType |
string |
audio/mpeg |
传给 MediaSource.addSourceBuffer() 的 MIME 类型,不会自动检测。 |
autoplay |
boolean |
true |
达到起播阈值后是否尝试自动播放。 |
minBufferMs |
number |
300 |
自动起播所需的连续缓冲时长,必须大于等于 0。 |
maxBufferMs |
number |
5000 |
播放头前方允许写入 MSE 的最大时长,必须大于 0。 |
maxQueueBytes |
number |
16777216 |
尚未写入 MSE 的队列上限,必须大于 0。 |
audioElement |
HTMLAudioElement |
自动创建 | 可传入已挂载到页面的 <audio> 元素。 |
onStateChange |
(state) => void |
- | 状态实际发生变化时触发。 |
onError |
(error) => void |
- | 首次进入 error 状态时触发。 |
minBufferMs 不能大于 maxBufferMs。数值配置必须是有限数字。
maxBufferMs 限制的是已经写入 MSE 的前向时长,maxQueueBytes 限制的是尚未写入 MSE 的压缩数据。两者解决的是不同层面的内存增长问题。
| API | 返回值 | 说明 |
|---|---|---|
AudioStreamPlayer.isTypeSupported(mimeType) |
boolean |
检查当前环境的 MSE 格式支持情况。 |
new AudioStreamPlayer(options) |
AudioStreamPlayer |
创建实例并校验配置,但尚未打开 MSE。 |
open() |
Promise<void> |
初始化 MediaSource 和 SourceBuffer;必须在 feed 前等待完成。 |
feed(data) |
boolean |
接收 Uint8Array 或 ArrayBuffer;返回 false 时需要停止上游输入。 |
play() |
Promise<void> |
显式请求播放,也可用于自动播放被阻止后的重试。 |
pause() |
void |
暂停声音输出,不清空数据。 |
resume() |
Promise<void> |
恢复由 pause() 产生的暂停。 |
close() |
void |
结束输入,继续播放缓冲数据。 |
destroy() |
void |
永久释放播放器资源,可重复调用。 |
getState() |
PlayerState |
返回当前播放器状态。 |
getAudioElement() |
HTMLAudioElement |
返回实际使用的 audio 元素。 |
getCurrentMimeType() |
string |
返回创建 SourceBuffer 时使用的 MIME 类型。 |
getQueuedBytes() |
number |
返回尚未写入 SourceBuffer 的字节数。 |
npm install
npm run dev
npm test
npm run test:coverage
npm run buildnpm run dev:启动本地示例。npm test:执行类型检查和自动化测试。npm run test:coverage:执行测试并检查覆盖率门槛。npm run build:生成 ESM、CommonJS、UMD 和类型声明。
当前测试覆盖缓冲调度、自动播放、背压、暂停恢复、结束状态、错误处理和资源释放等关键路径。
src/
├── buffer/
│ ├── AudioBufferController.ts
│ └── AudioChunkQueue.ts
├── core/
│ └── AudioStreamPlayer.ts
├── demo/
│ └── main.ts
├── media/
│ ├── MediaSourceSession.ts
│ └── media-utils.ts
├── types/
│ └── index.ts
└── index.ts
tests/
├── AudioStreamPlayer.test.ts
└── internal-modules.test.ts