最后音频停了两小时,会议为什么还在进行中:一次僵尸录音会议复盘
问题现象
管理员后台显示一场会议仍在“进行中”,但最后音频已经是两个多小时前。它看起来像一条持续占用实时转写并发的异常任务,也可能被误认为测试人员故意不结束会议。
只读检查后确认,这场会议已经没有活动实时资源。真正没有结束的是业务主记录:
会议状态:RECORDING
录音管道:DISCONNECTED
健康状态:OFFLINE
实时资源:恢复窗口到期时已释放
这类记录可以称为“僵尸会议”:技术连接已经死亡,业务状态仍表示进行中。
环境与前置条件
问题发生在企业移动 App 内嵌的会议 H5 中,链路包括:
- WebView 麦克风与 Web Audio;
- H5 到 Spring Boot 的 WebSocket;
- 后端到第三方实时转写服务的长连接;
- Oracle 中的会议主记录、健康快照和事件时间线;
- 至少 5 分钟的断线恢复窗口。
系统此前有意采用“断线不等于结束”的设计。移动端切后台或短时断网时,服务端允许用户重新连接,而不是立刻替用户结束会议。
已观察到的时间线
将事件统一到同一时区后,可以还原出以下顺序:
15:35:35 会议开始
15:37:00 收到最后一段音频
15:37:00 页面进入 hidden,AudioContext 进入 interrupted
15:37:23 健康状态由 HEALTHY 变为 DEGRADED
15:37:31 WebSocket 以 1011 关闭
15:37:33 健康状态变为 OFFLINE
15:42:31 五分钟恢复窗口到期,释放实时资源
之后 会议主状态持续为 RECORDING
同时观察到:
- 没有任何重连,
reconnectCount=0; - 已产生少量有效转写;
- 恢复窗口到期事件明确写着“会议仍可恢复”;
- 管理后台的“进行中”来自会议主状态,不代表仍有第三方任务占用。
这些证据足以排除“服务端仍在持续接收静音音频”和“第三方实时任务一直没有释放”。
原因分析
直接原因:恢复窗口只回收资源,不改变业务状态
断线处理把三件事拆开了:
- WebSocket 是否连接;
- 第三方实时资源是否保留;
- 会议是否在业务上结束。
恢复窗口到期时,代码执行的是:
记录 RECOVERY_WINDOW_EXPIRED
释放第三方实时会话
释放本地会议所有权
保留 AM_MEETING.STATUS = RECORDING
这个设计避免一次锁屏或网络抖动自动生成纪要,但没有为“用户再也不回来”设置最终状态。
触发因素:页面进入后台后音频上下文被中断
最后音频停止前后出现了 visibility=hidden 和 AudioContext interrupted。这说明移动系统或宿主 WebView 暂停了音频采集。
随后 WebSocket 以 1011 关闭,但仅凭关闭码不能继续推断是客户端、代理还是服务端哪一层最先触发。已确认的事实是:连接关闭后客户端没有重新建立会话。
放大因素:管理员“进行中”只看主状态
如果运营总览只统计 AM_MEETING.STATUS = RECORDING,就会把以下两类会议放在一起:
- 正在稳定接收音频的活动会议;
- 已经
DISCONNECTED/OFFLINE且恢复窗口过期的僵尸会议。
这会让运营人员误判实时并发,也掩盖了需要人工处理的失联记录。
排查过程
1. 先确认会议主状态
需要查看开始、结束、持续时间和错误字段,确认会议是否真正进入过结束流程。
2. 再看健康快照
关键字段包括:
- 管道状态;
- 当前健康状态;
- 最后音频、最后第三方事件和最后转写时间;
- 断线时间;
- 恢复截止时间;
- 重连次数;
- 音频字节数和分片数。
RECORDING + DISCONNECTED/OFFLINE 已经说明“业务未结束”和“技术链路离线”同时存在。
3. 按时间查看健康事件
快照只能告诉我们现在怎样,事件才能解释如何走到这里。重点寻找:
CLIENT_PAGE_HIDDEN
CLIENT_AUDIO_CONTEXT_INTERRUPTED
AUDIO_STALLED
BROWSER_DISCONNECTED
RECOVERY_WINDOW_EXPIRED
BROWSER_RECONNECTED
RECORDING_FINISHING
RECORDING_COMPLETED
如果存在 RECOVERY_WINDOW_EXPIRED,但没有后续重连、结束或完成事件,就能确认恢复流程停在了“资源已释放、业务仍可恢复”。
4. 区分业务状态与运行时占用
数据库中的 RECORDING 不能单独证明第三方实时任务仍存在。还要结合:
- 当前实例的活动 Bridge 数;
- 待恢复任务数;
- 第三方实时会话数;
- 会议所有权是否仍被持有。
本次记录在恢复窗口到期后已经释放这些运行时资源,因此它影响运营数据,但不继续消耗实时并发。
为什么原方案不是简单的错误
“断线后不自动结束”解决了一个真实风险:移动 WebView 可能因为切后台、锁屏或短暂断网关闭连接。如果服务端立即结束会议,会在没有用户确认的情况下生成纪要,用户也无法继续录音。
原方案正确地区分了连接和业务语义,但只完成了状态机的前半段:
flowchart LR
ACTIVE[活动录音] --> OFFLINE[断线恢复窗口]
OFFLINE -->|窗口内重连| ACTIVE
OFFLINE -->|窗口到期| STALE[资源已释放但业务未结束]
STALE -->|当前缺失| FINAL[最终收口]
问题不在“可恢复”,而在 STALE 没有明确的后续政策。
改进方向
管理后台先准确表达状态
即使暂时不自动结束,也应把活动录音和失联会议分开:
RECORDING / HEALTHY:正在录音;RECORDING / DEGRADED:连接仍在,但近期无音频;RECORDING / OFFLINE:已断线,可恢复或已过期;RECORDING / STALE:恢复窗口已过期,需要恢复或人工收口。
“正在进行”数量最好按运行时活动状态统计,另设“失联待处理”数量,避免把僵尸会议算作实时占用。
服务端增加保守的最终收口策略
不能简单恢复“断线 90 秒自动结束”。更稳妥的策略应同时考虑:
- 恢复窗口已经到期;
- 当前没有活动 WebSocket、第三方任务和会议所有权;
- 最后音频已超过更长的可配置阈值;
- 用户没有在其他客户端恢复;
- 本地仍有录音分片等待补传时,不提前清理归档;
- 是否允许自动生成纪要由业务规则明确决定。
最终动作有三种候选:
- 自动结束并生成纪要;
- 标记为
INTERRUPTED,保留已有转写和录音,不自动生成; - 进入管理员待处理队列,由人工确认结束或恢复。
哪一种更合适取决于业务是否接受系统替用户结束会议。当前实践尚未确定最终政策,不能把建议写成已经实现。
最大会议时长必须由服务端兜底
如果三小时上限只由 H5 定时器执行,页面被系统杀掉后计时器也会消失。服务端应有独立的最大时长检查,至少能够将无限期 RECORDING 转为明确的超时状态。
服务端兜底不一定立即生成纪要,但必须让状态最终可解释。
验证方法
未来实现收口策略后,应覆盖:
- 断线后 5 分钟内重连,不被错误结束;
- 恢复窗口到期后实时资源立即释放;
- 长期离线达到最终阈值后进入预期终态;
- 最终阈值前用户重新进入,能够取消收口任务;
- 多个客户端竞争恢复时只有一个获得录音所有权;
- 仍有 IndexedDB 分片补传时,录音归档不会被误清理;
- 应用重启后,数据库中的失联会议仍能被扫描和处理;
- 管理后台分别统计活动会议、离线会议和待处理会议;
- 自动收口不会重复触发纪要和 OA 写入;
- 人工结束和自动收口都留下可审计事件。
失败思路与原因
只把后台卡片改成“已离线”
这能改善展示,却没有让业务状态最终收敛。列表、容量统计和后续流程仍会持续受到 RECORDING 影响。
看到最后音频过期就立即结束
AudioContext 暂停、静音发言和弱网都可能造成短期无音频。只看一个时间字段容易误杀正常会议。
恢复窗口到期时直接生成纪要
恢复窗口的目标只是复用实时连接,它通常远短于用户可能返回页面的时间。把资源窗口等同于业务结束阈值,会重新引入原先的误结束问题。
经验总结
实时系统中,“还有没有连接”“还有没有资源占用”和“业务是否结束”必须分别建模。但拆开之后,还要保证每条状态线最终能够收敛。
可恢复设计解决了移动端误结束,僵尸会议暴露了它的另一面:系统需要一个比实时恢复窗口更保守的最终收口层。排查这类问题时,不能只看会议主状态或最后音频时间,而应把主记录、健康快照、事件时间线和运行时资源放在同一条证据链中。
关于恢复窗口本身的设计,可继续阅读断线不等于结束:实时会议录音的可恢复会话设计。