Skip to content

Repository files navigation

AudioStreamPlayer

面向 AI 语音回复、实时翻译和 TTS 等场景的浏览器流式音频播放器。它接收持续到达的二进制音频块,通过浏览器原生 Media Source Extensions(MSE)边接收、边解析、边播放。

在线文档

功能特点

  • 达到可配置的最小缓冲时长后尝试自动播放。
  • 使用字节受限 FIFO 队列保存待写数据,不会静默覆盖旧音频。
  • 限制播放头前方写入 MSE 的时长,降低长时间播放的内存增长风险。
  • 提供 idlebufferingplayingpausedendederror 状态。
  • 支持暂停、恢复、显式播放、结束输入和永久销毁。
  • 同时提供 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-player

ES 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 可能是 InfinityNaN。实时 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

API 返回值 说明
AudioStreamPlayer.isTypeSupported(mimeType) boolean 检查当前环境的 MSE 格式支持情况。
new AudioStreamPlayer(options) AudioStreamPlayer 创建实例并校验配置,但尚未打开 MSE。
open() Promise<void> 初始化 MediaSource 和 SourceBuffer;必须在 feed 前等待完成。
feed(data) boolean 接收 Uint8ArrayArrayBuffer;返回 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 build
  • npm 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

License

MIT

About

一个专为 AI 实时交互场景 打造的高性能音频流播放器。

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages