大家好,我是椰子皮,一名音视频应用开发的小学生。
从这篇开始,我会在掘金更新一系列音视频应用的实战文章,所有内容都来自我从零开发带 ASR 实时字幕的开源视频会议系统的完整经历 ------ 不聊空泛的 API 文档,只讲从 0 到 1 落地、公网部署、线上踩坑、问题解决的全流程干货。
故事的起因是过年期间闲不住,想自己动手做一套完整的开源视频会议项目。现在的会议场景,ASR 实时字幕几乎是刚需,所以项目初期,我就先选了阿里云的免费 ASR 接口来试水。
本以为几行代码就能跑通的功能,结果和朋友一起实测的时候直接踩了大坑:识别结果特别飘,完全达不到可用标准。比如我明明只说一句「你好」,它能给我识别出「你好好啊好」这种离谱的结果,重复识别、乱加字、错译频发,断断续续完全没法用。
踩了整整 3 天坑,终于把这个问题彻底搞定了。这篇文章就跟大家完整拆解:免费 ASR 做会议实时字幕的核心坑点、问题排查全流程、根因分析,以及最终低成本搞定稳定可用的实时字幕的落地方案。
如果你也在做音视频会议、实时音视频、ASR 落地相关的项目,遇到类似的问题,欢迎一起交流。

问题现象
集成了阿里云实时语音识别(ASR)功能,用于实时将会议语音转成文字。但在实际使用中,出现了一系列概率性问题:
- 识别不准确:用户说"大家伙大家好",ASR 返回"大家好大家大家好"
- 重复字:识别结果出现 好好像、不不太行行、但是但是我我 等重复字符
- 添加多余内容:用户说"好了",ASR 返回"好了吗"
- 概率性出现:问题不是必现,但退出会议重新进入后,识别往往恢复正常
问题影响
- 会议记录质量下降
- 用户对语音识别功能的信任度降低
- 需要频繁退出重进才能正常使用
二、定位分析
技术原理
音频采集流程:
- 浏览器 getUserMedia() 获取麦克风流(通常 48kHz)
- AudioContext({ sampleRate: 16000 }) 创建音频处理上下文
- ScriptProcessor 每 4096 采样触发一次回调
- downsampleBuffer() 从实际采样率重采样到 16kHz
- encodePCMToBase64() 将 Float32 转为 16-bit PCM 并 Base64 编码
- 通过 WebSocket 发送到 Java 后端
- Java 后端解码 Base64,发送到阿里云 ASR
2.2 定位过程
第一步:排除采样率问题
首先怀疑音频采样率不匹配,因为浏览器可能忽略 AudioContext({ sampleRate: 16000 }) 的参数。
js
// 浏览器控制台验证
const ctx = new AudioContext({ sampleRate: 16000 });
console.log('实际采样率:', ctx.sampleRate); // 输出: 16000 ✅
结论:采样率正确,不是问题根源。 第二步:添加诊断日志
在前端和后端添加诊断日志:
js
// 前端
asrStreamer.js processor.onaudioprocess = (event) =>
{ const rms = Math.sqrt(pcmData.reduce((sum, v) => sum + v * v, 0) / pcmData.length);
console.log('[ASR诊断] RMS能量:', rms.toFixed(4), rms > 0.01 ? '✅有声音' : '❌静音'); };
java
// 后端 SignalingWebSocketHandler.java
logger.info("ASR音频诊断: userId={}, seq={}, 平均能量={}", userId, seq, avgEnergy);
logger.info("NLS WS 原始返回: {}", json);
第三步:发现关键线索
日志显示两个用户的 seq 序列同时存在:
- seq: 25883, 25884, 25885... (用户A)
- seq: 27330, 27331, 27332... (用户B)
- 且阿里云返回的识别结果有明显重复字:
js
- 好好像不太行,信号好像不不太行行,但是但是我我觉得这个这个事情把也也是可可以搞。
第四步:发现根本原因
检查 leaveCall() 函数时发现:
js
function leaveCall(options = {}) {
console.log('[App] 离开会议');
asrStreamer = null; // ❌ 直接设置为 null,没有调用 stop()!
cleanupDenoise();
cleanupRawMicStream();
// ...
}
发现问题原因,本来现象就是概现,很多时候重新进会议室也就概现成功了,问题根源就是
- 退出会议时,asrStreamer.stop() 没有被调用
- AudioContext 没有被正确关闭
- 阿里云 ASR 会话没有收到 stop 通知
- Java 端的 ASR 会话可能残留
- 重新进入时,新旧会话状态冲突,导致音频数据混乱 问题复现路径

三、前后台修改逻辑
修改文件:/src/App.vue
js
// ❌ 修改前
function leaveCall(options = {}) {
console.log('[App] 离开会议');
asrStreamer = null; // 直接设置为 null,资源泄漏!
cleanupDenoise();
// ...
}
// ✅ 修改后
function leaveCall(options = {}) {
console.log('[App] 离开会议');
// 先停止 ASR,再清空引用
if (asrStreamer) {
asrStreamer.stop(); // 正确关闭 AudioContext 和发送 stop 信号
asrStreamer = null;
}
cleanupDenoise();
// ...
}
同时修改logout()函数:
js
function logout() { // 先停止 ASR,再清空引用
if (asrStreamer) { asrStreamer.stop(); asrStreamer = null; }
if (session) { session.close();
} // ... }
后端修改 - 添加 ASR 参数优化 修改文件AliyunNlsAsrService.java
java
// ✅ 添加 ASR 参数优化识别准确率
Map<String, Object> payload = new java.util.HashMap<>();
payload.put("format", config.format());
payload.put("sample_rate", config.sampleRate());
payload.put("enable_intermediate_result", true);
payload.put("enable_punctuation_prediction", true);
payload.put("enable_inverse_text_normalization", true);
// 新增参数
payload.put("disfluency_removal", true); // 去除语气词、重复词
payload.put("max_sentence_silence", 1000); // 最长句子静音时间(ms)
payload.put("max_start_silence", 5000); // 最长开始静音时间(ms)
后端修改 - 添加日志确认 修改文件:/.../SignalingWebSocketHandler.java
js
// ✅ ASR 会话创建日志
private void handleAsrStart(SessionContext context, SignalingMessage payload) {
// ...
logger.info("ASR会话创建: userId={}, roomId={}, format={}, sampleRate={}",
context.userId, context.roomId, format, sampleRate);
// ...
}
// ✅ ASR 会话关闭日志(连接断开时)
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
// ...
if (context.asrSession != null) {
logger.info("ASR会话关闭(连接断开): userId={}, roomId={}", context.userId, context.roomId);
context.asrSession.stop();
context.asrSession = null;
}
// ...
}
完整的ASR生命周期为:

总结
问题根因
- 资源未正确释放:退出会议时直接设置 asrStreamer = null,没有调用 stop() 方法
- AudioContext 泄漏:浏览器 AudioContext 未关闭,占用系统资源
- ASR 会话残留:阿里云 ASR WebSocket 连接未正确关闭,导致会话叠加
- 状态冲突:新旧会话的音频数据混淆,导致识别结果混乱
修复要点
| 修改点 | 修改内容 | 效果 |
|---|---|---|
| leaveCall() | 先调用 stop() 再置空 | 确保资源释放 |
| logout() | 先调用 stop() 再置空 | 确保资源释放 |
| asrStreamer.stop() | 重置 seq、发送 stop 信号、关闭 AudioContext | 完整清理会话 |
| ASR 参数 | 添加 disfluency_removal 等 | 提高识别准确率 |

修复效果:问题完全解决,每次进入会议都能正常识别,不再需要"退出重进"。