1. 流式音频播放的实现
**问题:**如何实现TTS音频的流式播放,而不是等待所有音频数据接收完才播放?
解决方案:
-
使用 MediaSource API实现边接收边播放
-
创建 MediaSource 对象并生成 Blob URL 绑定到 audio 元素
-
在
sourceopen事件中创建 SourceBuffer,指定音频格式(audio/mpeg)
-
通过 ReadableStream 逐块读取后端流式返回的音频数据
-
使用
sourceBuffer.appendBuffer()追加数据,监听
updateend事件确保顺序追加
-
数据接收完成后调用
mediaSource.endOfStream()结束流
关键代码:
const mediaSource = new MediaSource();
avatarAudio.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', async () => {
const sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
const reader = stream.getReader();
while (true) {
const {value, done} = await reader.read();
if (done) break;
await new Promise(resolve => {
sourceBuffer.appendBuffer(value);
sourceBuffer.addEventListener('updateend', resolve, {once: true});
});
}
});
Copyjavascript
2. 音频播放时跳过开头的问题
**问题:**流式音频播放时跳过了开头部分,从中间某句话才开始播放。
解决方案:
-
**根本原因:**在音频数据还未解码完成时就调用了
play(),导致跳过未准备好的部分
-
**解决思路:**先追加第一个数据块,等待
canplay事件触发后再播放
-
检查
audio.readyState >= 3判断是否已经可以播放,避免事件监听器未触发
-
避免在
sourceopen回调开始就等待
canplay,会造成死锁(需要数据才能触发 canplay,但数据追加在回调里)
关键代码:
// 先追加第一个数据块
const {value: firstChunk} = await reader.read();
await new Promise(resolve => {
sourceBuffer.appendBuffer(firstChunk);
sourceBuffer.addEventListener('updateend', resolve, {once: true});
});
// 等待音频准备好
await new Promise(resolve => {
if (audio.readyState >= 3) resolve();
else audio.addEventListener('canplay', resolve, {once: true});
});
audio.play();
Copyjavascript
3. 请求取消与资源管理
**问题:**AI流式输出时频繁触发TTS请求,导致多个音频重叠播放或资源泄漏。
解决方案:
-
使用 AbortController管理 fetch 请求,新请求到来时取消上一个未完成的请求
-
使用全局变量保存当前的 MediaSource 和 AbortController 引用
-
在新请求开始前:
-
调用
abortController.abort()取消网络请求
-
调用
mediaSource.endOfStream()结束流
-
清空 audio 元素的 src 并暂停播放
-
停止视频动画
-
-
使用
URL.revokeObjectURL()释放 Blob URL,避免内存泄漏
关键代码:
let currentAbortController = null;
let currentMediaSource = null;
// 取消上一个请求
if (currentAbortController) {
currentAbortController.abort();
}
if (currentMediaSource?.readyState === 'open') {
currentMediaSource.endOfStream();
}
audio.pause();
audio.src = '';
// 创建新请求
currentAbortController = new AbortController();
fetch(url, { signal: currentAbortController.signal });
Copyjavascript
4. 音频与视频动画的同步控制
**问题:**如何确保视频动画与音频完全同步(同时开始、同时停止)?
解决方案:
-
**开始同步:**在音频开始播放时启动视频循环播放
-
**结束同步:**统一封装视频停止逻辑
stopVideoAnimation(),在所有音频结束场景调用:
-
音频正常播放结束(
onended)
-
音频播放错误(
onerror)
-
请求被取消(新请求到来时)
-
-
避免代码重复,确保所有分支都能正确停止视频
关键代码:
const stopVideoAnimation = () => {
if (video) {
video.pause();
video.currentTime = 0