11 KiB
语音通信(VoIP)
本模块描述了房间内两位用户如何建立语音通信(VoIP)通话。语音和视频通话均基于 WebRTC 1.0 标准构建。通话信令通过向房间发送消息事件来实现。在本规范版本中,仅支持双方通信(如两点对两点,或点对多点会议设备)。虽然通话可在包含多位成员的房间中发起,但同一时间仅允许两台设备参与通话。
所有 VoIP 事件均包含一个 version
字段。该字段用于判断设备是否支持此新版本的协议。例如,客户端可利用该字段判断是否应期待来自对方的 m.call.select_answer
事件。如果客户端接收到的事件 version
字段不是 0
或 "1"
(包括如数值型 1
),则应视同其 version
== "1"
处理。
需注意,这意味着未来任何版本的 VoIP 事件均应保持向后兼容性。如有必要引入非兼容性新规范,则将采用一套独立的事件类型。
通话方标识符
每当客户端首次参与新通话时,应为自身生成一个 party_id
,在通话期间一直使用。此标识符应足够长,确保即使多设备同时生成应答,其间发生冲突的概率极低:建议使用 8 个大小写字母+数字字符。通话方通过 (user_id, party_id)
元组进行识别。
客户端将包含该 party_id
字段,并放置于所有 VoIP 事件内容的顶层,包括 m.call.invite
。客户端用此字段识别自身事件的远端回显:由于用户可能与自己通话,无法简单地忽略来自自身用户的事件。此外,该字段还能区分不同客户端对同一邀请发出的不同应答,并将 m.call.candidates
事件与相应的应答/邀请进行匹配。
客户端实现可选择使用端到端加密中使用的设备 ID,用作此目的;或者可为每次通话生成不同 ID,以避免在未加密房间中泄露使用设备信息,或隐藏单一设备(即 Access Token)被用于多个通话方信令发送等信息。
party_id
的语法定义见下文。
礼让规则
根据 WebRTC 完美协商示例,在重新协商过程中存在礼让(politeness)规则。被叫方始终为礼让方。在碰撞(glare)情况下,通话方的礼让状态由是采纳呼入通话还是主动呼叫决定:如果客户端舍弃本地呼出而采用呼入通话,则其为礼让方。
通话事件存活性
m.call.invite
中包含 lifetime
字段,指示邀约有效的时长。收到邀请后,客户端应结合事件同步响应中的 age
字段与自接收到该事件以来的时间,判断邀约是否依然有效。使用 age
字段可确保客户端设备错误时钟不会导致通话异常。
若邀约有效且在用户可接受通话期间保持有效,应提示有来电。用户可接受通话的时长可由不同客户端自行决定,例如在锁定的移动设备上,可以比在解锁的桌面设备上更长。
在处理完整个同步响应(sync response)并(对于加密房间)尝试解密该房间全部加密事件后,客户端才应提示来电。这样可以避免同步响应中含有随后指示通话已挂断、被拒绝或已被他处接听的事件时,重复提示来电。
若客户端启动后,在处理本地已存储事件后,发现仍有有效的邀约,应在与 homeserver 完成一次同步后再予以提示。
建议的最小有效时长为 90 秒——这样可确保用户有充足时间接听来电。
ICE 候选(Candidate)批量发送
客户端应设法只发送少量候选事件,具体指引如下:
- 在邀请/应答事件本身中,应立即或几乎立即发现的 ICE 候选(例如 host 类型 candidate)。如能在短时间内收集到服务器反射或中继 candidate,应一并发送。建议初始延迟约 200ms。
- 之后,客户端应等待一段时间以收集更多候选,实现批量发送,而非每获一个立刻发送。建议在发送邀请后等待 2 秒,或发送应答后等待 500ms(因发送邀请后,客户端本就等待用户接听,可利用此延迟)。
结束候选通知(End-of-candidates)
值为空字符串的 ICE 候选表示不会再发送新的 ICE 候选。客户端必须在 m.call.candidates
消息中发送此类候选。虽然 WebRTC 规范要求浏览器生成此候选,但截止目前,并非所有浏览器都实现(Chrome 不生成,但会产生 icegatheringstatechange
事件)。候选生成结束时,客户端应立即发送全部候选,而不应再等待上文时间间隔。这可方便桥接到不支持逐步 ICE(trickle ICE)协议时对候选的批量处理。
DTMF
Matrix 客户端可按 WebRTC 规范发送 DTMF。截至 2020 年 8 月,WebRTC 标准尚不支持接收 DTMF,但 Matrix 客户端可接收并解析 RTP 负载中的 DTMF 信号。
VoIP 标识符的语法
call_id
和 party_id
必须遵循不透明标识符语法。
离开房间时的行为
若客户端检测到正在通话的用户离开房间,应将其视作所有正在进行中的通话的挂断事件。对于发送邀约而被邀请方离开房间的情形,规范未做硬性规定,但若房间内已无可接听用户,客户端可选择将其视作被拒绝(如仅剩发送方本人,或邀约的 invitee
字段被设置后未被接听)。
历史通话回溯时亦应如此处理。
支持的编解码器
Matrix 规范未强制指定特定音视频编解码器,完全遵循 WebRTC 规范。兼容的 Matrix VoIP 客户端将像被支持的“浏览器”一样,根据所支持的编解码器及其变体运作。需遵循最新的 WebRTC 规范版本,因此客户端应及时跟进 WebRTC 规范的新版本,无论 Matrix 规范是否变更。
事件
通用字段
{{% event-fields event_type="call_event" %}}
事件
{{% event-group group_name="m.call" %}}
客户端行为
通话建立流程如下,双方通过消息事件进行通信:
呼叫方 被叫方
[发起呼叫]
m.call.invite ----------->
m.call.candidate -------->
[..candidates..] -------->
[接听来电]
<--------------- m.call.answer
m.call.select_answer ----------->
[通话正在进行中]
<--------------- m.call.hangup
或通话被拒绝时:
呼叫方 被叫方
m.call.invite ------------>
m.call.candidate --------->
[..candidates..] --------->
[拒绝通话]
<-------------- m.call.hangup
通话协商遵循 WebRTC 规范进行。
面对来电,客户端可采取多种操作:
- 发送
m.call.answer
,尝试接听通话。 - 主动在所有设备上拒绝通话:如上图,发送
m.call.reject
,则所有用户设备均停止响铃,并通知呼叫方其来电被拒。 - 忽略通话:不发送任何事件,仅本地停止通话提示。用户的其他设备仍会继续响铃,呼叫方设备继续显示响铃,若无设备响应,通话将自动超时。
多流(Streams)
客户端可在一次 VoIP 通话中发送多路流。应通过在 m.call.invite
、m.call.answer
以及 m.call.negotiate
事件中加入 sdp_stream_metadata
属性来区分流。当元数据发生变更但无需重新协商时,可发送 m.call.sdp_stream_metadata_changed
事件。
推荐客户端对于带 audio_muted
字段且值设为 true 的来流,不要本地关闭 WebRTC 音轨。这是因为当对方取消静音后,客户端发送音频与 m.call.sdp_stream_metadata_changed
事件到达之间可能略有延迟,这段音频会被错过。对方静音后将停止发送音频,无需担心无意识的音频发送。
对于 video_muted
,建议仍应本地关闭视频流,避免接收端看到黑屏。
如 sdp_stream_metadata
存在,但来流未被列在其中,应直接忽略。若某流用途未知(purpose
类型未知)也应忽略。
为保证兼容性,若对方首次发来的 m.call.invite
或 m.call.answer
缺少 sdp_stream_metadata
属性,客户端应假定对方不支持此属性,即无法区分多流,客户端仅应使用第一条来流,并勿发送多于一条。
实现本规范的客户端应忽略无流的轨道(streamless tracks)。
被邀请方
若通话仅面向特定用户,invitee
字段应被加入,并设置为该用户的 Matrix ID。不含 invitee
字段的邀请,默认为面向房间内除发送者以外任何成员。
客户端在接收到未过期的邀请,invitee
字段缺失或等于本用户 Matrix ID 时,应视为有效来电,但是否响铃需根据与呼叫方关系和来电地点判定。建议客户端默认忽略来自公共房间的呼叫邀请。强烈建议即便未为来电响铃,客户端也应在房间中展示来电并标记其被忽略。
通话碰撞(Glare)
“通话碰撞”指两位用户几乎在同一时间互相呼叫对方,导致已有呼入/呼出通话而无法建立。可使用碰撞解决算法决定应挂断哪个通话,应接听哪个通话。如双方客户端实用相同算法,将会选择同一个通话,通话得以正常建立。
因通话目标为房间而非具体用户,以下碰撞解决算法仅适用于同一房间的通话:
- 若客户端在准备发送
m.call.invite
至某房间时,收到了同一房间的m.call.invite
:- 客户端应取消本地外呼,转而自动为用户接听来电。
- 若客户端已向某房间发送
m.call.invite
并在等待响应时收到同房间的m.call.invite
:- 客户端应对两个通话的 call_id 按字典序比较,保留较小者,挂断较大者;如来电为较小者,客户端应代表用户接听之。
对用户而言,通话建立过程应如同直接接通,无需察觉背后切换。任何初始化媒体流应当平滑迁移至被采纳的通话。
服务器行为
Homeserver 可以(可选)向客户端提供 TURN 服务器信息,便于客户端通过 TURN 实现点对点通信。客户端可通过下述 HTTP API 获取 TURN 服务器信息。
{{% http-api spec="client-server" api="voip" %}}
安全性考量
通话仅应在仅有两位用户的房间发起。若在多人聊天室发起,其他用户可能会拦截并接听该通话。