技术文档

实时同步、位置与音视频通话

聊天变更通过 HTTP 长轮询通知,音视频使用独立的 LiveKit 媒体连接,iOS 来电可通过 APNs VoIP 唤醒。三者承担不同责任:事件告诉客户端刷新什么,通话 API 维护参与状态,媒体服务承载实际音视频。任何单一环节成功都不能代表整个通话已经可用。

普通事件游标

GET /events?after=0&wait=20 要求聊天 Bearer token。after 是当前用户已经处理到的普通事件 ID,必须为非负安全整数;wait 为 0–25 秒,默认 20。服务端先查已存在事件;有结果立即返回,没有结果才在进程内登记等待者。登记后再次查询以补上查询与注册之间的竞争窗口。

响应为 {events:[...],cursor}。每批最多 100 条、按 ID 升序;空批次的 cursor 保持请求 after。事件包含 id,conversationId,groupId,kind,messageId,createdAt,具体引用可能为 null。常见 kind 包括 message、message_read、group_message、group_message_read、friend_request、friend_accepted、friend_removed、block_changed、group_members_changed、call_changed 和 e2ee。

登录交换及 /me 会返回用户最新 eventCursor,供初始化增量监听。客户端先获取当前列表快照,再连续消费事件;断线后沿用已处理游标重连,并按事件刷新对应资源。不要把消息 ID 当事件游标,也不要直接用 e2ee sequence 调用普通 /events。

iOS 当前循环使用 wait=20,收到事件后刷新会话、群、好友申请及相关消息;call_changed 触发通话重查。401 清理该会话,其他错误约等待 3 秒再重试,且核对异步响应仍属于原 token。事件是刷新信号,消息正文仍从资源接口读取。等待器保存在单进程 Map,当前没有跨实例唤醒或 WebSocket 订阅路由。

MLS 的另一种游标

GET /e2ee/{conversations|groups}/{id}/events?deviceId=...&after=... 返回按设备收件的加密事件,每批最多 100 条,用 sequence 推进 cursor。它是立即查询接口,不使用普通事件的 wait 参数。普通 events 中的 e2ee 通知可以触发该查询,但两种游标必须分别保存。

客户端应在 MLS 验证及本地状态持久化成功后推进 sequence,Welcome 与 Commit 需要各自确认。iOS 独立传输器限制单次同步最多处理 100 批,超过后提示继续同步。若解密、发送者校验或状态保存失败,不应跳过密文并永久推进游标。详见消息与媒体。

实时位置是显式会话

单聊和群聊分别使用 /conversations/{id}/live-locations、/groups/{id}/live-locations。POST 显式开始,PUT 更新已经开启的共享,GET 查询当前参与者位置,DELETE 停止当前用户共享。GET /live-locations/mine 用于恢复自己的共享范围。

位置数据为 latitude、longitude、accuracy,必须是有限数字;纬度 -90–90、经度 -180–180、精度 0–100000 米。未先开始就 PUT 返回 409 NOT_SHARING。单聊开始和更新还要求仍是好友且未屏蔽,群操作要求当前成员。返回 startedAt、updatedAt,界面应展示更新时间,不能把静态最后坐标当实时移动。

当前服务端位置表没有自动到期字段,位置变更也未写入普通 events;客户端需单独刷新并在退出、停止共享和权限变化时处理生命周期。坐标是服务端可读数据。GET /location/city 仅提供基于请求公网地址的粗略城市与国家代码,不等于 GPS 定位,无法判断时返回 null。

通话创建与状态机

创建调用 POST /calls,发送 scopeKind(conversation/group)、scopeId、mode(audio/video)、UUID clientId。服务端要求当前成员;单聊额外要求好友及未屏蔽。同一个范围最多一场 ringing/active 通话,发起者不能同时占用另一场。相同发起者与 clientId 的相同请求返回同一通话;不同内容或冲突可能得到 CALL_BUSY。

状态 含义与转移
ringing 等待邀请者响应;发起者可 end 取消;接听后 active
active 至少已完成接听状态;媒体仍需单独连接
cancelled 发起者在响铃阶段取消,原因 caller_cancelled
missed 响铃达到约 60 秒无人接听,原因 unanswered
ended 挂断、全员忙、拒接、成员变化或空媒体房间等终态

邀请另有 ringing/accepted/declined/busy/missed/cancelled 状态。若其余所有成员都忙,创建请求可以返回 201 但 call 已是 ended、原因 busy,因此客户端必须读取响应 state。首位受邀者 accept 使通话 active,并登记发起者与接听者为参与者;后续群成员可 accept 或在 active 时 join。join 仅支持群通话。

GET /calls/active 查询当前用户正在响铃或实际参与的通话;/calls/current 按聊天范围查询;/calls/history 返回该范围最近 100 条终态记录。普通 call 响应不含内部房间名及请求 clientId。旧事件只能触发重查,不应把旧 state 覆盖新的终态。

媒体加入与退出

接听或加入成功后,参与者调用 /calls/{id}/media-token 获取 {url,token}。服务端只为 active 且尚未离开的当前参与者签发凭证,TTL 为 30 秒,权限限定当前房间、发布音视频与订阅,不允许发布 data。客户端必须及时连接,过期后重新获取;不能把聊天 Bearer token 当媒体 token。

active 单聊结束时删除媒体房间;群成员离开则移除该媒体参与者,只有群里无人继续参与时才结束整场并清理房间。删除好友、屏蔽、群解散或成员移除等路径会先处理相关媒体权限;媒体服务故障可能使动作返回 CALL_MEDIA_UNAVAILABLE,客户端需展示失败并可重试。

服务端约每秒检查响铃超时;默认约每 10 秒检查 active 房间人数,连续空房达到默认 90 秒宽限后结束通话并记录 connection_lost。检查媒体人数失败时不会据此认定空房。状态机的 active 仍不证明麦克风、摄像头、远端轨道或网络质量正常。

VoIP 推送与验收边界

iOS 通过 POST /voip/devices 注册设备 UUID 与十六进制 pushToken,记录绑定当前有效聊天 session。DELETE /voip/devices 注销设备。来电仅发给仍有有效 session 的待接听邀请者;APNs 返回 410、BadDeviceToken、Unregistered 或 DeviceTokenNotForTopic 时移除该 token。

推送载荷包含 aps 空对象,以及 callId、scopeKind、scopeId、mode、callerName,群通话另有 groupName。它没有聊天正文或媒体 token,但姓名、群名与通话关系仍属于元数据。推送类型为 voip、优先级 10、到期约 60 秒;缺少配置时不发送。APNs 的 HTTP 成功也不证明设备已经展示系统来电或成功接听。

仓库已有通话信令、媒体 SDK、系统通话桥接与推送发布器。真实双设备跨网络音视频、断线重连、后台与锁屏来电、生产 APNs、权限拒绝后的恢复仍需逐场景验收。请结合测试、运维、iOS和Android读取具体结果。

城市定位与精确共享、两端停止失败及最后坐标保留的具体区别见位置共享。

Markdown 原文完整文档文本