【通话】语音通话、视频通话、桌面共享
通话提供实时音视频能力:私聊一对一、群聊多人,支持语音、视频与桌面共享。通话模块由 yudao-module-im 后端模块的 rtc 包实现,用户侧前端在 @/views/im/home 的通话组件里。
本篇是「手册 + 原理」合篇:通话的「怎么用」很薄(发起 / 接听 / 挂断 + 通话记录),「怎么实现」(状态机、LiveKit 接入、信令与兜底)才是主体,所以一起讲。
信令走 IM,媒体走 LiveKit
芋道 IM 不自己传音视频流,而是外接开源 SFU LiveKit (opens new window):呼叫、接听、挂断等信令通过 IM 的 WebSocket 走,真正的音视频媒体流由客户端直连 LiveKit 房间。所以 RTC 是一个相对独立的子系统,不与文本消息的推送管线交织。
为什么选 LiveKit:自研 SFU(音视频选择性转发、级联、拥塞控制)成本极高,而 LiveKit 是开源、可自托管的 WebRTC SFU,提供成熟的多端 SDK(Web / 移动 / 桌面)、服务端 token 鉴权与房间事件 webhook,接入快、生态活跃;OpenAI 的实时语音(Realtime API / ChatGPT 语音)等方案也基于它,可靠性经过验证。芋道 IM 只负责「信令 + 业务状态」,把媒体面整体交给 LiveKit。
本文涉及通话记录表 im_rtc_call、通话参与者表 im_rtc_participant:一通通话一条主记录(im_rtc_call),通话里每个参与者一条明细(im_rtc_participant),通过 room(业务通话编号)关联。
- 通话记录:一通通话的主信息,含会话类型、媒体类型、发起人、状态、结束原因、各阶段时间。
- 通话参与者:通话中每个人的明细,含角色、参与状态、被邀请 / 接听 / 离开时间。
# 1. 表结构
省略 creator/create_time/updater/update_time/deleted/tenant_id 等通用字段
# 1.1 通话记录
通话记录,用户侧由 ImRtcCallController 提供接口(/im/rtc)。
CREATE TABLE `im_rtc_call` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`room` varchar(64) NOT NULL COMMENT '业务通话编号',
`conversation_type` tinyint NOT NULL COMMENT '会话类型(1=私聊,2=群聊)',
`media_type` tinyint NOT NULL COMMENT '媒体类型(1=语音,2=视频)',
`inviter_user_id` bigint NOT NULL COMMENT '发起人用户编号',
`group_id` bigint DEFAULT NULL COMMENT '群编号(群通话时)',
`status` tinyint NOT NULL COMMENT '通话状态',
`end_reason` tinyint DEFAULT NULL COMMENT '结束原因',
`start_time` datetime NOT NULL COMMENT '发起时间',
`accept_time` datetime DEFAULT NULL COMMENT '接通时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_room` (`room`)
) ENGINE=InnoDB COMMENT='IM 通话记录表';
① room 业务通话编号,全局唯一,是一通通话的稳定标识,也是 LiveKit 的房间名。
② conversation_type 私聊 / 群聊;media_type 语音 / 视频(枚举 ImRtcCallMediaTypeEnum);桌面共享是视频通话里的一路屏幕轨道,不单独占媒体类型。group_id 仅群通话有值。
③ status 通话状态、end_reason 结束原因,见 §2 状态机;start_time / accept_time / end_time 分别是发起 / 接通 / 结束时间,通话时长 = end_time - accept_time。
# 1.2 通话参与者
CREATE TABLE `im_rtc_participant` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`call_id` bigint NOT NULL COMMENT '通话编号',
`room` varchar(64) NOT NULL COMMENT '业务通话编号',
`user_id` bigint NOT NULL COMMENT '参与者用户编号',
`role` tinyint NOT NULL COMMENT '参与角色',
`status` tinyint NOT NULL COMMENT '参与状态',
`invite_time` datetime NOT NULL COMMENT '被邀请时间',
`accept_time` datetime DEFAULT NULL COMMENT '接听时间',
`leave_time` datetime DEFAULT NULL COMMENT '离开时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_room_user` (`room`, `user_id`)
) ENGINE=InnoDB COMMENT='IM 通话参与者表';
① call_id / room 关联通话,唯一键 (room, user_id) 保证一通通话里一个人一条明细。
② role 参与角色,枚举 ImRtcParticipantRoleEnum(1=发起人,2=被邀请者,3=主动加入者;主动加入仅群通话——旁观者点「N 人正在通话」胶囊条加入)。
③ status 参与状态,见 §2.2。
# 2. 通话状态机
通话有两套互相配合的状态机:主表 im_rtc_call.status(整通通话)与明细 im_rtc_participant.status(每个人)。
# 2.1 通话状态
枚举 ImRtcCallStatusEnum:
| 状态值 | 枚举 | 说明 | 可执行操作 |
|---|---|---|---|
| 10 | CREATED | 已创建(私聊等被叫接听;群聊发起人已进房) | 接听、拒绝、取消、加入 |
| 20 | RUNNING | 进行中(首个非发起人接通后) | 离开、追加邀请 |
| 30 | ENDED | 已结束 | — |
通话状态流转
createCall ──→ 创建(10) ──首个被叫接听──→ 进行中(20) ──任一方挂断/最后一人离开──→ 已结束(30)
│
└──无人接听 / 主叫取消 / 全部拒绝──→ 已结束(30)
- 发起(
createCall):落im_rtc_call(CREATED)+ 参与者明细(发起人 JOINED、被叫 INVITING),并推来电信令。 - 接听(
acceptCall):被叫接通,首个接通把通话推进到 RUNNING。 - 拒绝 / 取消(
rejectCall/cancelCall):接通前,被叫拒接或主叫取消。 - 离开(
leaveCall):接通后挂断;最后一人离开则通话 ENDED。 - 追加邀请 / 加入(
inviteCall/joinCall):仅群通话,通话中拉人或旁观者主动加入。
# 2.2 参与者状态
枚举 ImRtcParticipantStatusEnum:
| 状态值 | 枚举 | 说明 |
|---|---|---|
| 10 | INVITING | 邀请中(已发出 invite,等响应) |
| 20 | JOINED | 已加入(已进 LiveKit 房间) |
| 30 | REJECTED | 已拒绝(接通前点拒接) |
| 40 | NO_ANSWER | 未应答(通话结束仍未接) |
| 50 | LEFT | 已离开(接通后挂断 / 兜底) |
通话结束时,所有参与者明细必收敛到 LEFT / REJECTED / NO_ANSWER 三个终态之一。
# 2.3 结束原因
通话结束时按场景写 end_reason(枚举 ImRtcCallEndReasonEnum),并据此生成通话历史消息文案:
| 值 | 原因 | 场景 |
|---|---|---|
| 1 | 通话结束 | 接通后任一方主动挂断 |
| 2 | 已拒绝 | 被叫接通前点拒接 |
| 3 | 已取消 | 主叫接通前主动取消 |
| 4 | 无人接听 | 振铃超时未接通(参与者超时 Job 触发) |
| 5 | 对方正忙 | 私聊呼叫时对方在另一通话 |
| 9 | 通话异常 | 网络中断、设备失败等 |
# 3. 通话流程与 LiveKit
TODO @codex:"把本节私聊视频呼叫时序画成更直观的流程图:主叫 createCall → 服务端落库 + 推来电信令 → 被叫 acceptCall → 双方进 LiveKit 房间媒体互通 → 挂断 leaveCall 落『通话结束』消息;左右分栏标注『信令走 IM』『媒体走 LiveKit』。"
以私聊视频呼叫为例,端到端时序如下:
sequenceDiagram
participant 主叫
participant 服务端 as IM 服务端
participant LK as LiveKit
participant 被叫
主叫->>服务端: createCall(落 im_rtc_call CREATED + 参与者 INVITING)
服务端->>被叫: WebSocket 推送来电信令(RTC_CALL)
服务端-->>主叫: 返回 LiveKit token + url
主叫->>LK: 用 token 进房(等待)
被叫->>服务端: acceptCall(参与者 → JOINED)
服务端-->>被叫: 返回 token + url
被叫->>LK: 进房,媒体互通(status → RUNNING)
LK-->>服务端: webhook participant_joined / left(兜底状态)
被叫->>服务端: leaveCall(status → ENDED,落「通话结束」消息)
几个关键点:
- Token 签发:
buildCallRespVO在 invite / join / accept 时按当前用户签 LiveKit token(signCallToken)并下发livekitUrl;通话已结束(ENDED)则不签发,前端据此判定「通话已结束」。 - Webhook 兜底:LiveKit 的
participant_joined/participant_left回调(由 ImRtcLiveKitController 接收)会兜底纠正参与者状态,防止客户端异常退出导致状态卡住。 - 振铃超时:被叫长时间不接,由后端超时 Job 把参与者置
NO_ANSWER、通话置 ENDED(end_reason=无人接听);前端 RUNNING 端也会调noAnswerCallCheck兜底触发。 - 通话信令不入库:来电 / 参与者加入离开等信令(ImContentTypeEnum 的 1601 / 1602 / 1603)仅 WebSocket 推送、不入库;只有「通话开始」「通话结束」(1610 / 1611)入消息流,用于在聊天里渲染通话记录气泡 / 提示。信令推送机制详见 《WebSocket 实时推送与离线消息》。
# 4. 用户侧
# 4.1 私聊通话(一对一)
在好友资料卡或私聊会话里点「语音通话 / 视频通话」发起,对端收到来电后可接听 / 拒绝;接通后进入音视频窗口,可静音、开关摄像头、共享桌面(视频通话中开启屏幕共享轨道)、挂断。

# 4.2 群聊通话(多人)
在群里发起多人通话、勾选成员邀请,通话中还可继续追加邀请(inviteCall)。群里正在进行的通话会在群顶部显示「N 人正在通话」胶囊条(getActiveCall),旁观成员点击即可加入(joinCall)。

# 4.3 通话记录
无论私聊还是群聊,通话结束后都会在聊天里留下一条记录(如「通话时长 02:51」「已取消」「无人接听」),由 RTC_CALL_START / RTC_CALL_END 两条消息在聊天流里渲染。
# 5. 运营侧
运营侧在 [IM 即时通讯 -> 通话记录] 下,对应 @/views/im/manager/rtc 目录,由 ImRtcCallManagerController 提供只读分页查询:可按会话类型、媒体类型、通话状态、结束原因、时间等筛选,查看通话记录与参与人、通话时长。运营侧不发起 / 不干预通话。
