{"_id":"@agents-flex/audio-stream-player","_rev":"2-3cb159f69d1373a8feb77c4035515a52","name":"@agents-flex/audio-stream-player","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@agents-flex/audio-stream-player","version":"1.0.0","keywords":["agents flex","audioStreamPlayer","audioPlayer","audio player","ai audio player","ai player","stream","streaming","stream player","streaming player"],"author":{"name":"yangfuhai"},"license":"MIT","_id":"@agents-flex/audio-stream-player@1.0.0","maintainers":[{"name":"yangfuhai","email":"fuhai999@gmail.com"}],"homepage":"https://github.com/agents-flex/audioStreamPlayer#readme","bugs":{"url":"https://github.com/agents-flex/audioStreamPlayer/issues"},"umd":"./dist/index.umd.js","dist":{"shasum":"f22262d570e0cf7cf1177cc647294ec3fb5d4ce2","tarball":"https://registry.npmjs.org/@agents-flex/audio-stream-player/-/audio-stream-player-1.0.0.tgz","fileCount":8,"integrity":"sha512-wnkyxDgGHlkmtKeNjJtSkVXBFQMWUC97B4O6gWNFD4wuveJT+BFa35BMt02xs3Esi0hVr7avG6JbwUPuVvNwGA==","signatures":[{"sig":"MEQCIACQD2jPTp7vTApCh9lIVFqnDF8l+aN3K13qeSXygBA/AiBReRavf1z23u4aXHrXXmmRwoq6YD4vyKDP01A1UXYW7g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":33091},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"umd":"./dist/index.umd.js","types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"518215be573d582f5cc3416d5da23063d3d5104d","scripts":{"dev":"vite","build":"tsc && vite build","preview":"vite preview"},"_npmUser":{"name":"yangfuhai","email":"fuhai999@gmail.com"},"repository":{"url":"git+https://github.com/agents-flex/audioStreamPlayer.git","type":"git"},"_npmVersion":"10.9.2","description":"一个专为 **AI 实时交互场景** 打造的高性能音频流播放器。","directories":{},"_nodeVersion":"22.14.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.1.0","typescript":"~6.0.2","@types/node":"^26.0.1","vite-plugin-dts":"^5.0.3"},"_npmOperationalInternal":{"tmp":"tmp/audio-stream-player_1.0.0_1782380506073_0.04743901847658316","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@agents-flex/audio-stream-player","author":{"name":"yangfuhai"},"version":"1.0.1","type":"module","keywords":["agents flex","audioStreamPlayer","audioPlayer","audio player","ai audio player","ai player","stream","streaming","stream player","streaming player"],"main":"./dist/index.cjs","module":"./dist/index.js","umd":"./dist/index.umd.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.cjs","import":"./dist/index.js","umd":"./dist/index.umd.js"}},"scripts":{"dev":"vite","build":"tsc && vite build","preview":"vite preview","test":"tsc -p tsconfig.test.json && vitest run","test:coverage":"tsc -p tsconfig.test.json && vitest run --coverage"},"devDependencies":{"@types/node":"^26.0.1","@vitest/coverage-v8":"^4.1.10","typescript":"~6.0.2","vite":"^8.1.0","vite-plugin-dts":"^5.0.3","vitest":"^4.1.10"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"repository":{"type":"git","url":"git+https://github.com/agents-flex/audioStreamPlayer.git"},"bugs":{"url":"https://github.com/agents-flex/audioStreamPlayer/issues"},"homepage":"https://github.com/agents-flex/audioStreamPlayer#readme","license":"MIT","_id":"@agents-flex/audio-stream-player@1.0.1","gitHead":"e4dcdb05dac8eff0c6d37589afb2238eed946510","description":"面向 AI 语音回复、实时翻译和 TTS 等场景的浏览器流式音频播放器。它接收持续到达的二进制音频块，通过浏览器原生 Media Source Extensions（MSE）边接收、边解析、边播放。","_nodeVersion":"22.14.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-do0iFv/r90PFwH4GTNnkpdXPw3OO6Ov1LejelKC1knS5g8M3er7/zlFZKp6vrYy9Ype2Ai/lLKlrmF1Mt177bA==","shasum":"55d0b013c6e861285a086a8dc14179b9c198cee5","tarball":"https://registry.npmjs.org/@agents-flex/audio-stream-player/-/audio-stream-player-1.0.1.tgz","fileCount":13,"unpackedSize":61950,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBkP123o3kfPDjM/XznGBILHNAXhiQlNa8u1Zx3pPCBkAiBj/XSj45866IfnLUqWbc0dvusA6uP6EC3vpfUPKRDrcA=="}]},"_npmUser":{"name":"yangfuhai","email":"fuhai999@gmail.com"},"directories":{},"maintainers":[{"name":"yangfuhai","email":"fuhai999@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/audio-stream-player_1.0.1_1786682167434_0.11082835662145074"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-25T09:41:45.837Z","modified":"2026-08-14T04:36:07.785Z","1.0.0":"2026-06-25T09:41:46.197Z","1.0.1":"2026-08-14T04:36:07.602Z"},"bugs":{"url":"https://github.com/agents-flex/audioStreamPlayer/issues"},"author":{"name":"yangfuhai"},"license":"MIT","homepage":"https://github.com/agents-flex/audioStreamPlayer#readme","keywords":["agents flex","audioStreamPlayer","audioPlayer","audio player","ai audio player","ai player","stream","streaming","stream player","streaming player"],"repository":{"type":"git","url":"git+https://github.com/agents-flex/audioStreamPlayer.git"},"description":"面向 AI 语音回复、实时翻译和 TTS 等场景的浏览器流式音频播放器。它接收持续到达的二进制音频块，通过浏览器原生 Media Source Extensions（MSE）边接收、边解析、边播放。","maintainers":[{"name":"yangfuhai","email":"fuhai999@gmail.com"}],"readme":"# AudioStreamPlayer\n\n面向 AI 语音回复、实时翻译和 TTS 等场景的浏览器流式音频播放器。它接收持续到达的二进制音频块，通过浏览器原生 Media Source Extensions（MSE）边接收、边解析、边播放。\n\n[在线文档](https://agentsflex.com/zh/audio/audio-stream-player.html)\n\n## 功能特点\n\n- 达到可配置的最小缓冲时长后尝试自动播放。\n- 使用字节受限 FIFO 队列保存待写数据，不会静默覆盖旧音频。\n- 限制播放头前方写入 MSE 的时长，降低长时间播放的内存增长风险。\n- 提供 `idle`、`buffering`、`playing`、`paused`、`ended` 和 `error` 状态。\n- 支持暂停、恢复、显式播放、结束输入和永久销毁。\n- 同时提供 ESM、CommonJS、UMD 和 TypeScript 类型声明。\n\n## 使用要求\n\n### 浏览器环境\n\n播放器依赖 `HTMLAudioElement` 和 Media Source Extensions，只能在支持 MSE 的浏览器环境中播放。包可以被 Node.js 或 SSR 构建工具导入，但不能在没有浏览器媒体 API 的服务端执行 `open()`。\n\n创建播放器前应检查实际使用的 MIME 类型：\n\n```typescript\nimport { AudioStreamPlayer } from '@agents-flex/audio-stream-player';\n\nconst mimeType = 'audio/mpeg';\n\nif (!AudioStreamPlayer.isTypeSupported(mimeType)) {\n  throw new Error(`当前浏览器不支持 ${mimeType}`);\n}\n```\n\n### 音频流格式\n\n传给 `feed()` 的所有 chunk 必须组成浏览器认可的同一条连续媒体流。chunk 边界不必与 MP3 帧或容器片段边界一致，但不要假设多个独立完整文件一定可以直接拼接；最终数据必须符合对应的 MSE byte stream 格式。\n\n- MP3 常用 `audio/mpeg`。\n- AAC/fMP4 通常使用 `audio/mp4; codecs=\"mp4a.40.2\"`，流开头必须包含正确的初始化片段。\n- Opus/WebM 通常使用 `audio/webm; codecs=\"opus\"`，同样需要有效的容器初始化数据。\n- WAV、裸 PCM 或任意二进制数据不一定能被 MSE 接受。\n\n具体支持情况取决于浏览器，请始终以 `AudioStreamPlayer.isTypeSupported()` 的结果为准。\n\n## 安装\n\n```bash\nnpm install @agents-flex/audio-stream-player\n```\n\n也可以使用 pnpm 或 yarn：\n\n```bash\npnpm add @agents-flex/audio-stream-player\nyarn add @agents-flex/audio-stream-player\n```\n\nES Module 推荐写法：\n\n```typescript\nimport { AudioStreamPlayer } from '@agents-flex/audio-stream-player';\n```\n\nCommonJS 打包工具也可以使用：\n\n```javascript\nconst { AudioStreamPlayer } = require('@agents-flex/audio-stream-player');\n```\n\n无论采用哪种模块格式，播放器运行时仍然依赖浏览器 MSE API。\n\n## 快速开始\n\n下面示例从 Fetch `ReadableStream` 读取 MP3，并在输入结束后继续播放已经缓冲的数据：\n\n```typescript\nimport { AudioStreamPlayer } from '@agents-flex/audio-stream-player';\n\nconst audio = document.querySelector<HTMLAudioElement>('#audio')!;\n\nconst player = new AudioStreamPlayer({\n  audioElement: audio,\n  mimeType: 'audio/mpeg',\n  autoplay: true,\n  minBufferMs: 300,\n  maxBufferMs: 5000,\n  maxQueueBytes: 16 * 1024 * 1024,\n  onStateChange: (state) => {\n    console.log('player state:', state);\n\n    if (state === 'ended') {\n      console.log('所有缓冲音频均已播放完毕');\n    }\n  },\n  onError: (error) => {\n    console.error('playback error:', error);\n  },\n});\n\nawait player.open();\n\nconst response = await fetch('/speech.mp3');\nif (!response.ok || !response.body) {\n  player.destroy();\n  throw new Error(`音频请求失败：${response.status}`);\n}\n\nconst reader = response.body.getReader();\n\ntry {\n  while (true) {\n    const { value, done } = await reader.read();\n    if (done) break;\n\n    if (!player.feed(value)) {\n      // 完整的等待与重试方式见下一节“背压处理”。\n      throw new Error('播放器暂时无法接收更多数据，请实施背压');\n    }\n  }\n\n  // 这里只代表输入结束；缓冲区中的音频仍会继续播放。\n  player.close();\n} catch (error) {\n  player.destroy();\n  throw error;\n}\n```\n\n当组件卸载或页面不再需要播放器时释放资源：\n\n```typescript\nplayer.destroy();\n```\n\n## 背压处理\n\n`feed()` 返回 `true` 表示 chunk 已进入待写队列，不表示浏览器已经完成解析。以下情况会返回 `false`：\n\n- 尚未完成 `open()`。\n- 已调用 `close()` 或 `destroy()`。\n- 播放器已经进入 `error`。\n- 加入该 chunk 后会超过 `maxQueueBytes`。\n\n队列满时不要丢弃音频，也不要立即结束播放。应暂停上游读取，等待播放推进并释放队列空间后重试同一个 chunk：\n\n```typescript\nconst MAX_QUEUE_BYTES = 16 * 1024 * 1024;\n\nfunction delay(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function feedWithBackpressure(\n  player: AudioStreamPlayer,\n  chunk: Uint8Array,\n  signal: AbortSignal,\n): Promise<void> {\n  if (chunk.byteLength > MAX_QUEUE_BYTES) {\n    throw new Error('单个 chunk 大于 maxQueueBytes');\n  }\n\n  while (!player.feed(chunk)) {\n    if (signal.aborted) throw signal.reason;\n\n    const state = player.getState();\n    if (state === 'error' || state === 'ended') {\n      throw new Error(`无法继续 feed，播放器状态为 ${state}`);\n    }\n\n    await delay(25);\n  }\n}\n```\n\n该辅助函数必须在 `await player.open()` 完成后使用。调用 `destroy()` 时应同时 abort 传入的 signal，确保等待循环能够立即退出。\n\n如果上游本身支持 `pause/resume`，应优先使用上游的流量控制，而不是轮询。\n\n调用方在 `feed()` 返回 `true` 后不应继续修改或复用同一个 `Uint8Array` 的内容；播放器可能稍后才将其复制给 `SourceBuffer`。\n\n## 自动播放\n\n`autoplay: true` 表示播放器会在缓冲达到 `minBufferMs` 后尝试调用 `audio.play()`，但浏览器仍可能因为缺少用户手势而阻止播放。\n\n自动播放被阻止时，播放器保留缓冲数据，不一定进入 `error`。可以在用户点击事件中显式播放：\n\n```typescript\nplayButton.addEventListener('click', async () => {\n  try {\n    await player.play();\n  } catch (error) {\n    console.error('无法开始播放', error);\n  }\n});\n```\n\n设置 `autoplay: false` 时，也应使用 `play()` 手动开始。`resume()` 只用于恢复此前由 `pause()` 产生的暂停。\n\n## 状态与生命周期\n\n典型生命周期如下：\n\n```text\nidle -> buffering -> playing -> ended\n           ^           |\n           |-----------|  网络波动或缓冲不足\n                       |\n                    paused  用户主动暂停\n```\n\n任何阶段发生不可恢复错误都可能进入 `error`。\n\n| 状态 | 含义 |\n| :--- | :--- |\n| `idle` | 实例刚创建，尚未成功打开。构造时不会主动触发该状态回调。 |\n| `buffering` | MSE 已打开但数据不足，或播放期间等待更多数据。 |\n| `playing` | `audio.play()` 已成功，音频正在播放。 |\n| `paused` | 用户通过 `pause()` 或原生 audio 控件暂停。仍可继续接收数据。 |\n| `ended` | 已结束输入，并且所有已缓冲音频都已经播放完毕。 |\n| `error` | MSE、SourceBuffer 或 audio 元素发生不可恢复错误。 |\n\n需要特别区分：\n\n- `close()`：声明上游不会再产生数据，不会立即进入 `ended`。\n- `ended`：浏览器已经播放完全部缓冲内容。\n- `destroy()`：永久释放实例；调用后不能再次 `open()` 或 `play()`。\n\n每个实例只能成功调用一次 `open()`。如需播放新的独立媒体流，应销毁旧实例并创建新实例。\n\n## 时间与缓冲语义\n\nMSE 流在调用 `endOfStream()` 前通常没有可靠的总时长，`audio.duration` 可能是 `Infinity` 或 `NaN`。实时 TTS 尚未生成完成时，本身也不存在最终总时长。\n\n建议 UI 分开显示以下指标：\n\n| 指标 | 读取方式 | 含义 |\n| :--- | :--- | :--- |\n| 当前播放时间 | `audio.currentTime` | 当前播放头位置。 |\n| 已解析时长 | 所有 `audio.buffered.end(i)` 的历史最大值 | 浏览器曾经成功解析到的最远时间点。 |\n| 已缓冲时长 | 当前连续区间的 `end - currentTime` | 不发生新输入时还能连续播放多久。 |\n| 总时长 | 有限的 `audio.duration` | 通常在所有数据写入并调用 `endOfStream()` 后才可靠。 |\n\n因此在总时长未知时，建议显示“流式”，不要展示伪造的播放百分比。项目中的本地 demo 已实现这套展示方式。\n\n## 配置选项\n\n| 参数 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| `mimeType` | `string` | `audio/mpeg` | 传给 `MediaSource.addSourceBuffer()` 的 MIME 类型，不会自动检测。 |\n| `autoplay` | `boolean` | `true` | 达到起播阈值后是否尝试自动播放。 |\n| `minBufferMs` | `number` | `300` | 自动起播所需的连续缓冲时长，必须大于等于 0。 |\n| `maxBufferMs` | `number` | `5000` | 播放头前方允许写入 MSE 的最大时长，必须大于 0。 |\n| `maxQueueBytes` | `number` | `16777216` | 尚未写入 MSE 的队列上限，必须大于 0。 |\n| `audioElement` | `HTMLAudioElement` | 自动创建 | 可传入已挂载到页面的 `<audio>` 元素。 |\n| `onStateChange` | `(state) => void` | - | 状态实际发生变化时触发。 |\n| `onError` | `(error) => void` | - | 首次进入 `error` 状态时触发。 |\n\n`minBufferMs` 不能大于 `maxBufferMs`。数值配置必须是有限数字。\n\n`maxBufferMs` 限制的是已经写入 MSE 的前向时长，`maxQueueBytes` 限制的是尚未写入 MSE 的压缩数据。两者解决的是不同层面的内存增长问题。\n\n## API\n\n| API | 返回值 | 说明 |\n| :--- | :--- | :--- |\n| `AudioStreamPlayer.isTypeSupported(mimeType)` | `boolean` | 检查当前环境的 MSE 格式支持情况。 |\n| `new AudioStreamPlayer(options)` | `AudioStreamPlayer` | 创建实例并校验配置，但尚未打开 MSE。 |\n| `open()` | `Promise<void>` | 初始化 MediaSource 和 SourceBuffer；必须在 feed 前等待完成。 |\n| `feed(data)` | `boolean` | 接收 `Uint8Array` 或 `ArrayBuffer`；返回 false 时需要停止上游输入。 |\n| `play()` | `Promise<void>` | 显式请求播放，也可用于自动播放被阻止后的重试。 |\n| `pause()` | `void` | 暂停声音输出，不清空数据。 |\n| `resume()` | `Promise<void>` | 恢复由 `pause()` 产生的暂停。 |\n| `close()` | `void` | 结束输入，继续播放缓冲数据。 |\n| `destroy()` | `void` | 永久释放播放器资源，可重复调用。 |\n| `getState()` | `PlayerState` | 返回当前播放器状态。 |\n| `getAudioElement()` | `HTMLAudioElement` | 返回实际使用的 audio 元素。 |\n| `getCurrentMimeType()` | `string` | 返回创建 SourceBuffer 时使用的 MIME 类型。 |\n| `getQueuedBytes()` | `number` | 返回尚未写入 SourceBuffer 的字节数。 |\n\n## 开发与测试\n\n```bash\nnpm install\nnpm run dev\nnpm test\nnpm run test:coverage\nnpm run build\n```\n\n- `npm run dev`：启动本地示例。\n- `npm test`：执行类型检查和自动化测试。\n- `npm run test:coverage`：执行测试并检查覆盖率门槛。\n- `npm run build`：生成 ESM、CommonJS、UMD 和类型声明。\n\n当前测试覆盖缓冲调度、自动播放、背压、暂停恢复、结束状态、错误处理和资源释放等关键路径。\n\n## 源码结构\n\n```text\nsrc/\n├── buffer/\n│   ├── AudioBufferController.ts\n│   └── AudioChunkQueue.ts\n├── core/\n│   └── AudioStreamPlayer.ts\n├── demo/\n│   └── main.ts\n├── media/\n│   ├── MediaSourceSession.ts\n│   └── media-utils.ts\n├── types/\n│   └── index.ts\n└── index.ts\n\ntests/\n├── AudioStreamPlayer.test.ts\n└── internal-modules.test.ts\n```\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"readme.md"}