先说结论
Odoo 里的语音/视频通话,不是“前端浏览器一连上就算了”。
它在后端有正式的 RTC session 模型和 call history 模型,通话开始、成员加入、状态变化、无人后结束、僵尸会话清理,都有明确链路。
所以 Discuss 通话更像一种“实时协作对象”,而不是页面上的临时 UI 特效。
一、为什么每个频道成员只能有一个 RTC session
discuss.channel.rtc.session 对 channel_member_id 做了唯一约束。
这意味着在同一个频道语义下,成员的实时会话身份必须唯一。
它解决的是两个核心问题:
- 避免同一成员在同一频道被算成多个并发发言者
- 保证后续的邀请、广播、断开、清理都能定位到明确会话
所以 RTC session 不是“浏览器 tab 数量”,而是频道成员的实时通话占位。
二、为什么通话开始时会自动发一条消息
源码里,当某频道第一次出现 RTC session,系统会:
- 发一条
message_type="notification"的通话开始消息 - 创建一条
discuss.call.history - 写入开始时间和起始消息 ID
这非常像现实办公:
- 通话不是凭空发生
- 它应该在协作线程里留下一个“这里开始过一次通话”的痕迹
因此通话历史不是额外报表,而是频道协作历史的一部分。
三、为什么麦克风/摄像头/共享屏幕状态要落后端
RTC session 上存了:
is_screen_sharing_onis_camera_onis_mutedis_deaf
很多人会觉得这些都是前端本地状态,为什么还要广播到后端?
答案是:因为协作方需要看见彼此状态,而且这些状态会影响邀请、显示和实时交互。
如果全留在本地:
- 其他成员看不到一致状态
- 多端同步会混乱
- 会话结束后也无法进行统一清理
四、为什么要有失活会话垃圾回收
源码里 _inactive_rtc_session_domain() 把超过约 1 分 15 秒未更新的会话视作失活,_gc_inactive_sessions() 会自动清理。
这背后的场景很现实:
- 浏览器崩了
- 网络断了
- 用户直接关标签页
- Odoo session 失效
如果没有 GC,频道里会残留“明明人都不在了,还显示通话中”的僵尸状态。
五、为什么最后一个人离开时要做这么多事
unlink() 里,当频道最后一个 RTC session 离开,系统会:
- 取消邀请
- 清掉 SFU channel uuid 和 server url
- 补全 call history 的结束时间
- 广播会话结束
这说明“挂断电话”不是删一行数据,而是一组协作状态的收口动作。
六、最容易误解的地方
误区 1:RTC session 就是前端连接对象
不是,它还是频道协作状态对象。
误区 2:只要浏览器关了,后端自然知道
不一定,所以才需要 autovacuum 清理。
误区 3:通话历史只是统计功能
不是,它和频道消息历史是连着的。
七、排错顺序
当你遇到“频道一直显示有人在通话 / 通话没结束 / 状态不同步”时,建议按这个顺序查:
- 当前频道成员是否存在重复或僵尸 RTC session
- 最近是否触发过 inactive session GC
- 是否已经是最后一个成员离开但 call history 没写 end_dt
- SFU 地址和频道 UUID 是否已清空
- 最后再怀疑前端麦克风权限或浏览器兼容性
最后一句
Odoo 通话模块真正做的,不只是连音视频,而是把“谁在通话、何时开始、何时结束、现在是什么状态”纳入协作系统本身。
这就是它和纯前端插件最大的区别。
DISCUSSION
评论区