页面离开了,麦克风为什么还亮着

工程与部署 · 2026-10-10 · 4 分钟

从停监听到释放采集资源,用引用计数管理共享音频上下文,并等待旧媒体连接完成断开。

Hanzora 的变更记录里有一条很直观的故障:语音运行时已经停止,浏览器的麦克风指示灯却没有熄灭。

AsrRuntime 原先保留采集实例,想让下一次启动更快。但单页应用切换路由时,完整销毁未必发生。只停止识别器,仍然可能持有麦克风流和音频节点。

停止就释放,不把责任留给最终销毁

当前 AsrRuntime.stop() 先停止识别器,再停止 audioCapture,最后清空引用。下次启动重新申请采集资源。

采集对象自身负责拆掉音频处理节点、释放音频上下文句柄,并对麦克风流的 tracks 调用 stop()。运行时负责决定何时停止,采集对象负责释放自己创建的资源,责任沿着创建关系传递。

这个选择放弃了跨停止阶段保留麦克风的热启动优化,换来更明确的生命周期。源码注释记录过重新启动的时间代价,本次没有做设备复测,因此这里只记录释放行为。

共享资源需要成对的所有权

KWS、采集和音频路由可能共享一个 AudioContext。任何模块都直接关闭全局实例,会让仍在使用它的其他模块失效;所有模块都不关闭,则会一直保留资源。

SharedAudioContext.acquire() 返回 { ctx, release } 句柄。每次获取增加引用,只有最后一个持有者释放时才调用 close()。句柄的 release() 是幂等的:同一个模块重复释放,引用只减少一次。

现有测试用两个消费者验证这条契约:先释放一个,关闭次数为 0;再释放最后一个,关闭次数为 1。重复释放第一个句柄也不能提前关闭第二个消费者正在使用的上下文。

异步断开还会影响下一次连接

媒体播放器遇到的是同一类生命周期问题。旧 LiveKit 房间的 disconnect() 尚未结束,新房间就已经开始连接;旧房间随后到达的 track 解绑回调,可能作用到新画面。

当前播放器让 stop() 缓存正在执行的 Promise,并让下一次 start() 等待它完成。多个停止调用共享同一任务,旧房间的解绑回调完成后才创建新房间。

回归测试记录 disconnect:begin → disconnect:end → createRoom 的顺序,还直接断言连续三次 stop() 返回同一 Promise。这里验证的是旧连接与新连接的先后关系,没有用固定延时猜测断开何时结束。

验证覆盖到哪里

本次在源码快照上重跑 15 项相关用例:共享音频上下文 8 项、ASR 采集释放 3 项、播放器停止与再启动 4 项,全部通过。

释放测试还覆盖「从未创建采集对象」以及「停止采集报错后仍清空引用」。播放器使用模拟房间与 jsdom,真实浏览器的麦克风指示灯、媒体播放和跨设备重连需要另行联调。

本文依据 Hanzora SDK 的 AsrRuntime.ts、AudioCapture.ts、SharedAudioContext.ts、LiveKitPlayer.ts 与对应测试整理,核对日期为 2026-10-10;项目见 Hanzora。