音视频通话
用户可以在 App 中发起一对一和群内的语音、视频通话。本服务负责通话的信令和状态:发起、振铃、接听、拒绝、挂断,多台设备同时振铃,断线后的恢复,以及通话记录和统计。音视频数据由本服务提供的媒体服务承载,你不需要部署或配置任何媒体服务器。
- 你的客户端:用户发起、接听和挂断通话,都由客户端通过长连接或客户端接口完成,客户端 SDK 发布后由 SDK 封装,见客户端需要做什么;
- 你的服务端:查询通话、强制结束通话、把成员移出通话、查看每天的通话统计,见通话管理;通过事件回调接收通话的发起、接通和结束。
开启音视频
应用默认不开启音视频。在控制台应用详情的“应用策略”中开启“开启音视频通话”(rtc_enabled),并且应用的套餐包含音视频后,用户才能发起和接听通话,见运行策略。控制台应用详情的“音视频”页面显示音视频在本应用能否使用。
基本规则
- 两种通话:一对一通话(
single)在两个用户之间进行;群通话(group)在一个群内进行,参与者必须是这个群的成员。这套呼叫接口的多人通话需要 IM 群;不关联群的会议或语音房使用独立 RTC 频道。 - 两种媒体:语音通话(
audio)和视频通话(video),发起时确定,通话中不能切换。视频通话中客户端可以自行关闭摄像头,仍按视频通话记录和计费。 - 一人同时一个通话:一个用户同时只能在一个通话中,发起一对一通话后等待对方接听也算在通话中;他的多台设备中只有一台在通话中。正在通话的人不会再收到来电:一对一来电判定为忙线,群通话的邀请记为忙线、不振铃。只是正在振铃、还没有接听的人可以同时收到多个来电,接听其中一个后,要先挂断它才能接听另一个。
- 每个群同时一个群通话:群里已有进行中的群通话时,再发起会得到这个通话的 ID,客户端改为加入它。
- 以服务端为准:通话的状态、谁在通话中、何时接通和结束,都以服务端的记录为准。每次状态变化,通话的版本号
version加一,客户端收到的通知和响应都带完整的通话,只应用比本地版本号大的即可。 - 通话 ID:每个通话有一个
call_id,查询、结束通话和事件回调都用它标识通话。
一对一通话
发起:主叫指定被叫的用户名和媒体类型,可以附带自定义字段
ext(如业务订单号),随来电送达被叫,也会出现在通话对象和事件回调中。发起时检查:- 应用开启了音视频、可以提供服务;
- 主叫没有被全局禁言单聊(
chat),否则返回user_muted。被禁言的用户可以接听; - 与发送单聊消息的规则相同:被叫把主叫加入了黑名单时返回
user_blocked;应用开启了只能给好友发消息,而双方不是好友时返回not_friend; - 主叫此刻不在其他通话中,否则返回
call_in_progress; - 主叫的发起频率,以及应用的并发通话额度。
被叫不存在或已删除返回
not_found。被叫被封禁不影响发起:他收不到来电,振铃超时后以“无人接听”结束,不会因此暴露他被封禁。振铃:被叫的全部在线设备收到来电通知,不在线或 App 在后台的设备收到来电推送。主叫发起后就进入通话,连接媒体服务等待对方。振铃时间由运行策略
rtc_ring_timeout_seconds决定,默认 60 秒。接听:被叫在任意一台设备上接听,这台设备进入通话,通话接通(
status变为active),他的其他设备停止振铃,显示“已在其他设备接听”。
通话以下列方式结束,结束原因 end_reason 见结束原因:
| 情况 | 结束原因 |
|---|---|
| 接通后任一方挂断,时长从接通算到挂断 | completed |
| 接通前主叫挂断(取消) | canceled |
| 被叫拒绝 | rejected |
发起时被叫正在另一个通话中:不振铃,通话立即结束,发起的响应中通话已是 ended;或者被叫以“忙线”拒绝(SDK 发现设备正在进行运营商电话等其他通话时自动发出) | busy |
| 振铃超时,被叫没有接听 | no_answer |
| 任一方的设备掉线超过 45 秒,见断线与恢复 | connection_lost |
被叫只是在振铃(正在收到别人的来电)不算忙线,新的来电同样振铃,由他选择接听哪一个。两人几乎同时呼叫对方时,先到达的呼叫照常振铃,后到达的一方判定为对方忙线。被叫在接听前拉黑主叫、解除好友关系,不影响已经发出的来电;通话中拉黑对方也不中断通话。
群通话
- 发起:群成员在群里发起群通话,指定语音或视频,并选择要邀请的成员,最多为人数上限减一;也可以不邀请任何人,等其他成员自行加入。
- 发起人必须能在群里发言:群没有被封禁,他是群成员、没有在群里被禁言,群开启了全员禁言时他是群主或管理员;他也不能被全局禁言群聊(
group)。 - 被邀请的人必须能加入:是群成员、没有在群里被禁言、群开启全员禁言时是群主或管理员。不满足的不邀请,在发起的结果中逐个说明(
not_found、not_group_member、group_muted),以免响铃之后却接听不了。正在其他通话中的人记为忙线,不振铃。 - 群通话不检查个人黑名单,与群消息的规则相同。
- 群里已有进行中的群通话时,返回
409 already_exists,details.call_id为那个通话,客户端改为加入它。
- 发起人必须能在群里发言:群没有被封禁,他是群成员、没有在群里被禁言,群开启了全员禁言时他是群主或管理员;他也不能被全局禁言群聊(
- 群内提示:群通话开始、在通话中的人数变化和结束时,群的全部在线成员收到一条群通话状态的通知,用于在群会话中显示“3 人正在通话”的横幅和加入入口。这条通知只有人数,可能有误差;成员打开群会话时可以查询群当前的通话。
- 加入:被邀请的人接听邀请,或其他群成员从横幅加入,都是加入。加入的条件与发起相同,另外通话不能已满:在通话中的人数加上正在振铃的人数不超过人数上限(本人正在振铃的不重复计算),正在振铃的邀请为被邀请人预留了名额。拒绝过、未接听、离开过的成员都可以再加入。
- 邀请:在通话中的成员可以继续邀请其他群成员,被邀请的人同样要能加入;拒绝过、未接听、离开过的成员可以再次被邀请,重新振铃。
- 拒绝与未接听:被邀请的人可以拒绝,振铃超时记为未接听(时长与一对一相同),都不影响其他人继续通话。
- 离开:在通话中的成员挂断即离开,之后可以再加入。
- 结束:发起人、群主和群管理员可以结束群通话,其他人结束时返回
permission_denied(not_host)。结束时全部成员离开,正在振铃的邀请取消。此外以下情况自动结束:- 在通话中的人数降为 0:接通过的为
completed,没有接通过的为canceled;最后离开的人是掉线或设备被踢下线的,为connection_lost; - 只剩一人在通话中、且没有正在振铃的邀请,持续 60 秒:接通过的为
completed,没有接通过的为no_answer。这 60 秒留给其他成员从横幅加入,期间有人加入或有新的邀请就取消计时; - 群被解散或封禁:
group_unavailable。
- 在通话中的人数降为 0:接通过的为
- 群的变化:
- 成员退出群、被移出群(包括被拉黑、账号被删除):在通话中的立即离开(
removed_from_group),也被移出媒体房间;正在振铃的邀请取消; - 通话中被禁言、群开启全员禁言:已在通话中的人不受影响,之后不能再发起、加入(包括离开后重新加入)和邀请,也不能再被邀请;已经在振铃的邀请照常振铃,接听时被拒绝。
- 成员退出群、被移出群(包括被拉黑、账号被删除):在通话中的立即离开(
- 人数上限:同时在通话中的人数上限由运行策略
max_group_call_participants决定(默认 16,含发起人),发起时确定,之后修改策略不影响进行中的通话。一个群通话累计涉及的成员(包括已离开、拒绝、未接听的)最多 100 人,达到后不能再邀请或加入新的人,已经参与过的人照常重新加入。
多台设备
- 来电送到全部设备:来电通知推给被叫的全部在线设备;不在线或 App 在后台的设备收到来电推送,有效期为剩余的振铃时间,超时没有送达的不再送达。设备在振铃期间连上长连接时,会单独补发一次来电通知。来电推送的通道、免打扰规则见离线推送。
- 在一台设备上接听:这台设备进入通话,本人的其他设备收到“已在其他设备接听”,停止振铃;收到过来电推送的设备上的来电通知被静默清除(个别推送通道不支持清除,由 App 下次打开时清除)。
- 在一台设备上拒绝:一对一通话随之结束,群通话中本人的邀请结束,本人的其他设备停止振铃。
- 只有一台设备在通话中:媒体凭据只签发给在通话中的那台设备。本人的其他设备可以查看通话状态和挂断,但不能同时进入同一个通话,接听时返回
call_in_progress(joined_on_other_device)。暂不支持把通话转移到本人的另一台设备。 - 主叫的其他设备:主叫发起后,他的其他在线设备收到通话状态,可以显示“正在通话”并提供挂断。
- 同一台设备重复接听(如网络重试)结果相同,不会被当成另一台设备。
断线与恢复
- 心跳:在通话中的设备每 15 秒发送一次通话心跳。服务端 45 秒没有收到心跳,就认为这个成员已经掉线:一对一通话以
connection_lost结束;群通话中他离开(connection_lost),可以重新加入。App 被强制关闭、设备断网后,对方最迟约 45 秒后得知。 - 网络切换:在 Wi-Fi 与移动网络之间切换时,只要 45 秒内恢复心跳,通话不受影响,双方最多听到几秒的中断。
- App 重启后恢复:App 被系统关闭后 45 秒内重新启动的,查询本人正在进行的通话,本人仍在通话中且就是这台设备的(
self.joined_on_this_device为true),续期媒体凭据、重新连接媒体服务即可继续通话;已经超时的,一对一通话已结束,群通话可以重新加入。 - 重新连接后的同步:客户端登录、重新连接长连接后,查询本人正在进行的通话和正在振铃的来电,与本地核对。通知可能丢失、重复或乱序,以查询结果和版本号为准。
- 账号与设备的变化:在通话中的设备被踢下线或登录失效:一对一以
connection_lost结束,群通话中他离开(session_revoked)。账号被封禁或删除:他正在进行的一对一通话结束(user_unavailable),群通话中他离开(user_unavailable);账号被删除时,他正在振铃的一对一来电随之结束(user_unavailable),群通话的邀请随之取消。被封禁的人正在振铃的来电照常振铃到超时。 - 最长时长:通话最长 12 小时,超过后自动结束(
max_duration)。
媒体服务与媒体凭据
- 音视频数据由本服务提供的媒体服务承载。每个通话一个媒体房间,房间在第一个人连入时创建,通话结束后服务端关闭房间,仍连着的设备随之断开。
- 发起、接听和加入成功后,响应中带媒体凭据(
media),客户端用它连接媒体服务。媒体凭据由 SDK 使用,App 的业务代码不需要解析其中的字段。- 只签发给在通话中的那台设备,正在振铃的被叫拿不到,接听之前不能提前收听;
- 有效期 15 分钟(
media.expires_at),快到期时由这台设备续期; - 只能进入本通话的房间。语音通话只能发送麦克风的声音,视频通话可以发送麦克风和摄像头,都不能共享屏幕;可以收发数据消息,客户端可以用它同步“对方已静音”等状态。
- 成员离开群通话(挂断、被移出、掉线)时,服务端同时把他移出媒体房间,他不能凭手中的凭据继续收听。
- 客户端的 SDK 版本过旧、不支持当前的媒体服务时,发起、接听和加入返回
permission_denied(media_provider_unsupported),请提示用户升级 App。
客户端需要做什么
SDK 发布后由 SDK 封装
以下是客户端与服务端之间的交互,客户端 SDK 发布后由 SDK 封装,App 只需要处理界面。在 SDK 发布前需要直接对接时,请联系我们获取长连接协议。
客户端的通话操作推荐通过长连接发送,App 刚被来电推送唤醒、长连接还没连上时使用客户端接口。两者的请求和响应相同,长连接上请求的 data 为客户端接口的请求体加上 call_id:
| 操作 | 长连接 op | 客户端接口 | 说明 |
|---|---|---|---|
| 发起通话 | rtc.start | POST /client/v1/calls | to_user(一对一的被叫)或 to_group(群 ID)二选一;media 为 audio 或 video;群通话的 usernames 为要邀请的成员,最多 31 个;可选 ext、client_call_id,见下文 |
| 接听、加入 | rtc.join | POST /client/v1/calls/{call_id}/join | 接听一对一来电、接听群通话邀请、从横幅加入群通话、离开后重新加入 |
| 拒绝 | rtc.reject | POST /client/v1/calls/{call_id}/reject | 可选 reason:declined(默认)或 busy |
| 挂断 | rtc.hangup | POST /client/v1/calls/{call_id}/hangup | 接通前的主叫为取消,在通话中为离开,正在振铃的人等同拒绝。本人的任何一台设备都可以挂断 |
| 结束群通话 | rtc.end | POST /client/v1/calls/{call_id}/end | 只有发起人、群主和群管理员可以;对一对一通话等同挂断 |
| 邀请 | rtc.invite | POST /client/v1/calls/{call_id}/invite | 只用于群通话,usernames 为 1 到 31 个其他成员 |
| 心跳 | rtc.heartbeat | POST /client/v1/calls/{call_id}/heartbeat | 在通话中每 15 秒一次;连上媒体服务后的第一次心跳带 media_connected: true |
| 续期媒体凭据 | rtc.renew_token | POST /client/v1/calls/{call_id}/token | 只接受在通话中的那台设备 |
| 查询通话详情 | rtc.get | GET /client/v1/calls/{call_id} | 一对一通话只有双方能查到;群通话是群的当前成员能查到,已离开群的人只能查到自己参与过、已结束的通话 |
| 查询正在进行的通话和来电 | — | GET /client/v1/calls/active | current 为本人正在进行的通话,incoming 为正在振铃的来电(最多 20 个)。登录和重新连接后调用 |
| 查询通话历史 | — | GET /client/v1/calls | 本人参与过的通话,包括未接听、拒绝的,按时间倒序,limit 默认 20、最大 50 |
| 查询群的当前通话 | — | GET /client/v1/groups/{group_id}/call | 打开群会话时调用,用于显示横幅;没有进行中的群通话时 call 为 null |
例如在长连接上发起一对一视频通话:
{
"op": "rtc.start",
"id": "7",
"data": {
"client_call_id": "c-0001",
"to_user": "bob",
"media": "video",
"ext": { "order_id": "8812" }
}
}ext:字符串键值对,最多 16 项,键为 1 到 64 个字符、不能包含控制字符,JSON 编码后不超过 1 KB。不会放进来电推送。client_call_id:发起的去重 ID,1 到 64 个可见 ASCII 字符。网络错误、超时后用同一个 ID 重试,同一个发起人相同的 ID 只创建一个通话,重复提交返回第一次创建的通话(客户端接口的状态码为200,第一次为201)。
发起、接听和加入的响应带 call(通话,另有 self)、media(媒体凭据)和 heartbeat_interval_seconds(15),发起时被叫忙线、通话立即结束的没有 media;接听、加入另有 changed。拒绝、挂断和结束带 call 和 changed。changed 为 false 表示没有改变任何状态(如同一台设备重复接听、重复拒绝、挂断已结束的通话),可以视为成功;群通话的发起和邀请另有每个被邀请人的结果 results。self 为本人在通话中的状态:state 为本人的成员状态(不是成员为 null),joined_on_this_device 表示本人是否就在这台设备上通话。客户端收到的通话和成员没有 ended_via、member_count、peak_joined、billable_minutes、invited_at、joined_platform、join_count、joined_ms、first_joined_at、media_joined_at、media_left_at 这几个服务端字段。
服务端通过长连接推送以下通知:
| 通知 | 推送给 | 内容 |
|---|---|---|
rtc.call_invite | 新振铃的人的全部设备 | call 为通话;inviter 为邀请人的 username、nickname、avatar_url;group 为群通话所在群的 group_id、name、avatar_url,一对一为 null |
rtc.call_changed | 正在振铃和在通话中的成员,以及本次状态有变化的成员的全部设备 | call 为变化后的通话;change 说明变化:kind 为变化的类型,usernames 为状态变化的成员,actor 为操作人(你的服务端、平台和系统为 null),reason 为拒绝的原因、离开原因、取消原因或结束原因 |
rtc.group_call_changed | 群的全部在线成员 | 群通话开始、在通话中的人数变化和结束时推送,只有 group_id、call_id、media、status、joined_count、initiator、version,不含成员名单 |
change.kind 的取值:created(发起,只推给发起人的其他设备)、invited(群通话中有新的邀请)、joined(有人接听或加入)、answered_elsewhere(本人在另一台设备上接听了,停止振铃)、declined(有人拒绝)、missed(群通话的邀请超时未接听)、left(有人离开或被移出)、canceled(群通话中正在振铃的邀请被取消,如被移出群)、ended(通话结束)。一次操作引起的多个变化只发一条,kind 取最终的结果,例如拒绝导致群通话结束时为 ended。
{
"type": "event",
"event": "rtc.group_call_changed",
"at": "2026-10-04T19:22:32.957Z",
"data": {
"group_id": "100311544667045888",
"call_id": "100311615936659456",
"media": "audio",
"status": "ringing",
"joined_count": 1,
"initiator": "alice",
"version": 1
}
}客户端需要:
- 按运行配置中的
rtc_enabled显示通话的入口,它在应用策略和套餐都允许音视频时为true; - 为每次发起生成新的
client_call_id,失败重试时沿用; - 登录、重新连接长连接后调用
GET /client/v1/calls/active,恢复本设备正在进行的通话、为没有收到通知的来电振铃;本地有、查询结果中没有的通话按已结束处理; - 来电通知、来电推送和查询结果可能重复到达,按
call_id去重;在ring_expires_at停止振铃,等待服务端的结束通知; - 在通话中每 15 秒发送心跳,长连接不可用时改用客户端接口;心跳返回
call_ended时结束本地通话,返回version_conflict时说明本人已不在通话中(如被移出、掉线后已离开),断开媒体; - 媒体凭据到期前续期;
- 设备正在进行运营商电话等其他通话时,收到来电用
busy拒绝; - 渲染会话中的通话记录消息。
客户端的通话操作按用户限制频率,超出时返回 429 rate_limited,details.reason 说明是哪一项,按 Retry-After 等待。这些请求同时计入应用的客户端每秒请求数:
| 操作 | 上限 | details.reason |
|---|---|---|
| 发起通话(一对一和群通话合计) | 每个用户每分钟 10 次、每天 200 次 | call_rate |
| 同一主叫呼叫同一被叫 | 每小时 20 次 | pair_call_rate |
| 在同一个群里发起群通话 | 每个群每分钟 10 次 | group_call_rate |
| 在群通话中邀请 | 每个用户每分钟 30 人,超出的人在结果中单独返回 rate_limited,其余照常邀请 | invite_rate |
| 接听、加入、拒绝、挂断、结束 | 每个用户合计每分钟 60 次 | signal_rate |
| 续期媒体凭据 | 每个用户每分钟 10 次 | token_rate |
| 查询(通话详情、正在进行的通话、通话历史、群的当前通话)和发起的重复提交 | 每个用户合计每分钟 120 次 | query_rate |
心跳只计入应用的客户端请求数,同一个成员 5 秒内的第二次心跳直接忽略、仍返回成功。限制的计算方式见限流的计算方式。
客户端的通话操作可能返回以下错误,SDK 按 code 和 details 提示用户:
| HTTP 状态码 | code | 说明 |
|---|---|---|
| 400 | invalid_argument | details.reason 为 invalid_target(to_user、to_group 不是恰好一个)、invalid_media、self_call(呼叫自己)、invalid_ext、invalid_client_call_id、invalid_usernames(usernames 超过 31 个、包含本人,或邀请时为空)、too_many_invitees(邀请的人数超过人数上限减一,details.max 为可以邀请的人数)、group_call_only(对一对一通话邀请他人)、invalid_reject_reason |
| 403 | permission_denied | details.reason 为 rtc_disabled(应用没有开启音视频,或套餐不包含音视频)、rtc_suspended(本应用的音视频已被平台停用,details.suspend_reason 为原因)、rtc_not_configured(音视频服务暂不可用)、not_host(不是发起人、群主或群管理员却结束群通话)、not_in_call(不在通话中却邀请他人)、not_joined_device(不是在通话中的那台设备却续期媒体凭据)、media_provider_unsupported(App 版本过旧) |
| 403 | user_muted、user_blocked、not_friend、not_group_member、group_muted、group_disabled | 发起、加入或邀请时,与发送消息相同的禁言、黑名单、好友和群成员检查不通过 |
| 403 | app_unavailable | 应用处于只读状态(如欠费)时不能发起通话和邀请,其他操作照常 |
| 404 | not_found | 被叫不存在或已删除;通话不存在或本人看不到;群不存在 |
| 409 | call_in_progress | 本人已在通话中,不能再发起、接听或加入:details.reason 为 in_other_call(本人在另一个通话中,details.call_id 为那个通话)或 joined_on_other_device(本人已在另一台设备上接听或加入了这个通话) |
| 409 | call_ended | 通话已经结束,不能再接听、加入、邀请、续期或心跳;details.end_reason、details.ended_at 为结束的原因和时间 |
| 409 | already_exists | 群里已有进行中的群通话,details.call_id 为那个通话,改为加入它 |
| 409 | version_conflict | 本人在通话中的状态不允许这个操作,如接听之后又拒绝、从未被邀请却拒绝:details.reason 为 member_state,details.member_state 为本人当前的成员状态(从未被邀请为 null) |
| 409 | limit_exceeded | details.reason 为 call_full(群通话人数已满)、call_member_limit(群通话累计涉及的成员已达 100 人)或 app_concurrent_calls(应用进行中的通话数已达额度,details.limit 为额度) |
通话的状态
通话状态
通话的 status 只按以下顺序前进:
status | 说明 |
|---|---|
ringing | 已发起,还没有接通 |
active | 已接通:第一次有两个人同时在通话中 |
ended | 已结束,end_reason 为结束原因 |
成员的状态
通话的每个成员有一个角色 role 和状态 state。角色:initiator 发起人、invitee 被邀请的人、joiner 没有被邀请、自己加入群通话的人。
state | 说明 |
|---|---|
ringing | 正在振铃 |
joined | 在通话中 |
left | 已离开,leave_reason 为离开原因。群通话中可以重新加入 |
declined | 拒绝了来电或邀请 |
busy | 忙线:被邀请时正在另一个通话中,没有振铃;或以忙线拒绝 |
missed | 振铃超时,没有接听 |
canceled | 正在振铃时邀请被取消,如通话结束、被移出 |
离开原因
成员离开通话时的 leave_reason:
leave_reason | 说明 |
|---|---|
hangup | 本人挂断 |
ended | 通话结束时他仍在通话中 |
connection_lost | 心跳超时,设备掉线 |
removed | 被你的服务端、控制台或平台移出通话 |
removed_from_group | 退出了群或被移出群 |
session_revoked | 在通话中的设备被踢下线或登录失效 |
user_unavailable | 账号被封禁或删除 |
app_unavailable | 应用或租户不能提供服务,或平台停用了本应用的音视频 |
结束原因
通话结束时的 end_reason,一对一和群通话共用。界面上显示的文字由 App 决定,下表的提示仅供参考:
end_reason | 场景 | 发起人一方的提示 | 被叫一方的提示 |
|---|---|---|---|
completed | 接通后挂断;群通话最后的人离开,发起人、群主或管理员结束,或只剩一人超过 60 秒 | 通话时长 | 通话时长 |
canceled | 接通前发起人挂断;群通话接通前在通话中的人全部离开,或接通前被结束 | 已取消 | 对方已取消 |
rejected | 一对一被叫拒绝 | 对方已拒绝 | 已拒绝 |
busy | 一对一被叫忙线,或被叫以忙线拒绝 | 对方忙线中 | 未接来电 |
no_answer | 一对一振铃超时;群通话没有接通,只剩发起人超过 60 秒 | 对方无应答 | 未接来电 |
connection_lost | 一对一任一方掉线或设备被踢下线;群通话最后在通话中的人掉线或设备被踢下线 | 通话中断 | 通话中断 |
server_ended | 你的服务端或控制台结束了通话,或把一对一通话的一方移出 | 通话已结束 | 通话已结束 |
platform_ended | 平台结束了通话 | 通话已结束 | 通话已结束 |
user_unavailable | 一对一通话中一方的账号被封禁或删除,或振铃中的被叫账号被删除 | 通话已结束 | 通话已结束 |
group_unavailable | 群被解散或封禁 | 通话已结束 | 通话已结束 |
app_unavailable | 应用或租户不能提供服务,或平台停用了本应用的音视频 | 通话已结束 | 通话已结束 |
service_disabled | 平台停用了承载这个通话的媒体服务,并要求立即结束进行中的通话 | 通话已结束 | 通话已结束 |
max_duration | 通话超过 12 小时 | 通话已结束 | 通话已结束 |
以后可能增加新的结束原因,遇到不认识的值时显示为“通话已结束”。
通话的 ended_via 说明由谁结束:client 用户在客户端(ended_by 为结束通话的用户)、openapi 你的服务端、console 控制台、platform 平台、system 系统(如振铃超时、掉线、服务端判定的忙线)。
通话记录
通话结束后,会话中自动写入一条通话记录消息,每个通话恰好一条:
- 一对一:写入两人的单聊会话,发送者为主叫。没有接通的通话(如未接听、忙线、取消)计入被叫的未读数,让他看到未接来电;接通过的通话和被叫主动拒绝(
rejected)的不计入未读数。写入时任一方的账号已被删除的不写入; - 群通话:写入群会话,发送者为发起人(他已不在群里时为系统),不计入未读数。群已解散的不写入;
- 通话记录不发离线推送:来电、未接来电和取消已经推送过;
- 应用可以在运行策略中关闭通话记录(
rtc_call_record_enabled),例如由业务系统自己展示通话。
通话记录的消息类型 type 为 call,客户端和你的服务端都不能发送这种类型的消息。body 的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
call_id | String | 通话 ID,可以用它查询通话详情 |
call_type | String | single 一对一 / group 群通话 |
media | String | audio 语音 / video 视频 |
result | String | 结束原因,取值同结束原因 |
duration_seconds | Number | 通话时长,秒,未接通为 0 |
started_at | String | 发起时间 |
群会话中的一条通话记录(查询历史消息时返回):
{
"conversation_id": "100311544667045888",
"conversation_type": "group",
"seq": 5,
"message_id": "100313815656169472",
"client_msg_id": "sys_rtc_100313666666102784",
"sender": "alice",
"sender_type": "user",
"recipient": null,
"type": "call",
"body": {
"call_id": "100313666666102784",
"call_type": "group",
"media": "video",
"result": "completed",
"duration_seconds": 32,
"started_at": "2026-10-04T19:30:41.871Z"
},
"ext": null,
"mentions": null,
"reply_to": null,
"need_receipt": false,
"exclude_from_unread": true,
"reactions": null,
"pinned": null,
"edited": null,
"recalled": null,
"erased": false,
"via": "system",
"created_at": "2026-10-04T19:31:17.393Z",
"message_version": 0
}通话记录按普通消息保存,保留期由运行策略 message_retention_days 决定,与通话数据的保留期无关。
计费分钟数与统计
音视频按通话分钟数计费,分语音和视频:
- 每个成员从接通(或接通后加入)到离开的时长,每人分别向上取整到分钟后求和,就是这个通话的计费分钟数
billable_minutes。例如两人通话 28 秒,计 2 分钟;三人的群通话中两人各 32 秒、一人 15 秒,计 3 分钟; - 接通之前的振铃和等待不计;
- 通话结束时计算,计入结束那一天(北京时间)的用量。
计费分钟数在通话结束后可以从通话对象中查到,每天的合计见查询通话统计,计费方式见套餐与账单。
并发通话额度
应用同时进行(发起后还没有结束)的通话数受套餐的并发通话额度 max_concurrent_calls 限制,为 0 表示不限,在控制台应用的“套餐与用量”中查看,见套餐与账单。达到额度后再发起通话,返回 409 limit_exceeded,details.reason 为 app_concurrent_calls,details.limit 为额度:
{
"code": "limit_exceeded",
"message": "应用进行中的通话数已达上限",
"details": {
"limit": 1,
"reason": "app_concurrent_calls"
},
"request_id": "FW43EI3USADPEKRAFTOZWODLDX"
}额度只在发起时检查,进行中的通话不受影响;同时发起的多个通话可能让实际的通话数略超额度。每天同时进行的通话数峰值见查询通话统计。
运行策略
音视频相关的运行策略:
| 字段 | 说明 | 默认值 | 取值范围 |
|---|---|---|---|
rtc_enabled | 是否开启音视频通话 | false | true、false |
rtc_ring_timeout_seconds | 来电的振铃时间,单位为秒,一对一和群通话相同 | 60 | 20 到 120 |
max_group_call_participants | 群通话同时在通话中的人数上限,含发起人 | 16 | 3 到 32 |
rtc_call_record_enabled | 通话结束后是否在会话中写入通话记录 | true | true、false |
- 关闭
rtc_enabled,或应用的套餐不包含音视频时,用户不能发起、接听、加入通话,也不能在群通话中邀请,返回permission_denied(rtc_disabled);进行中的通话不受影响,可以正常完成。你的服务端查询和结束通话不受影响。 - 振铃时间和人数上限在发起或邀请时确定,修改后只影响之后的来电和通话。
应用状态的影响
| 情况 | 影响 |
|---|---|
| 平台停用了本应用的音视频 | 进行中的通话立即结束(app_unavailable);不能发起、接听、加入和邀请,返回 permission_denied(rtc_suspended),details.suspend_reason 为平台给出的原因。控制台的“音视频”页面显示停用的原因和时间,如有异议请联系我们 |
| 音视频服务暂不可用 | 不能发起通话,返回 permission_denied(rtc_not_configured) |
| 应用停用、被暂停服务,租户被暂停或处于注销冷静期 | 进行中的通话结束(app_unavailable),用户的请求返回 app_unavailable 或 tenant_unavailable,见应用和租户的状态 |
| 应用处于只读状态(如欠费) | 不能发起通话和在群通话中邀请,返回 app_unavailable(欠费时 details.reason 为 arrears);接听、加入、拒绝、挂断、心跳和续期照常,已经发起的通话可以正常完成。你的服务端查询、结束通话和移出成员照常 |
| 应用删除、租户注销 | 进行中的通话结束(app_unavailable),之后通话数据被删除 |
事件回调
在控制台配置事件回调后,你的服务端可以收到以下音视频事件。事件中用用户名表示用户,带发起时的 ext,没有承载通话的媒体服务的信息:
| 事件 | 触发时机 |
|---|---|
rtc.call_created | 发起通话,包括发起时被叫忙线、立即结束的通话 |
rtc.call_answered | 通话接通 |
rtc.member_joined | 群通话中有人加入(发起人在发起时加入不另发) |
rtc.member_left | 群通话中有人离开(通话结束时离开的不另发) |
rtc.call_ended | 通话结束,带结束原因、时长、计费分钟数和每个成员的通话时长 |
每个通话的 rtc.call_created 和 rtc.call_ended 各一条。事件可能重复投递、不保证顺序,请以 rtc.call_ended 为准记录通话的结果,例如按通话的计费分钟数向你的用户收费、把订单号放在 ext 中对应业务。
数据保留
- 通话和成员在结束 180 天后删除,之后查询通话不再返回;
- 通话统计保留到应用删除;
- 会话中的通话记录消息按
message_retention_days保留。
数据结构
通话对象
查询通话详情、结束通话等接口返回的通话。查询通话列表返回的每一项没有 members。
| 字段 | 类型 | 说明 |
|---|---|---|
call_id | String | 通话 ID |
type | String | single 一对一 / group 群通话 |
group_id | String | 群通话所在的群 ID,一对一为 null |
media | String | audio 语音 / video 视频 |
status | String | 通话状态:ringing / active / ended,见通话状态 |
initiator | String | 发起人的用户名,已删除的用户为 null |
max_participants | Number | 同时在通话中的人数上限:一对一为 2,群通话为发起时的 max_group_call_participants |
joined_count | Number | 此刻在通话中的人数,结束后为 0 |
ext | Object | 发起时附带的自定义字段,字符串键值对,没有为 null |
created_at | String | 发起时间 |
answered_at | String | 接通时间,未接通为 null |
ended_at | String | 结束时间,未结束为 null |
end_reason | String | 结束原因,见结束原因,未结束为 null |
ended_by | String | 在客户端结束通话的用户,如挂断、拒绝、结束群通话的人;你的服务端、平台和系统结束的,以及已删除的用户为 null |
duration_seconds | Number | 通话时长:从接通到结束(未结束时到现在)的秒数,向下取整;未接通为 0 |
version | Number | 版本号,通话或成员的状态每变化一次加一 |
members | Array<Object> | 全部成员,包括已离开、拒绝、未接听的,见通话成员对象;按 media_uid 升序,最多 100 个 |
ended_via | String | 结束方:client、openapi、console、platform、system,见结束原因;未结束为 null |
member_count | Number | 通话涉及的成员数,包括已离开、拒绝、未接听的 |
peak_joined | Number | 同时在通话中的人数的最大值 |
billable_minutes | Number | 计费分钟数,见计费分钟数与统计。通话结束时计算,未结束为 0 |
一个已结束的群视频通话:
{
"call_id": "100313666666102784",
"type": "group",
"group_id": "100311544667045888",
"media": "video",
"status": "ended",
"initiator": "alice",
"max_participants": 16,
"joined_count": 0,
"ext": {
"topic": "周会"
},
"created_at": "2026-10-04T19:30:41.871Z",
"answered_at": "2026-10-04T19:30:44.923Z",
"ended_at": "2026-10-04T19:31:17.049Z",
"end_reason": "completed",
"ended_by": "alice",
"duration_seconds": 32,
"version": 5,
"members": [
{
"username": "alice",
"role": "initiator",
"state": "left",
"media_uid": 1,
"invited_by": null,
"ring_expires_at": null,
"joined_at": "2026-10-04T19:30:41.871Z",
"left_at": "2026-10-04T19:31:17.049Z",
"leave_reason": "ended",
"invited_at": null,
"joined_platform": "ios",
"join_count": 1,
"joined_ms": 32126,
"first_joined_at": "2026-10-04T19:30:41.871Z",
"media_joined_at": "2026-10-04T19:30:41.898Z",
"media_left_at": null
},
{
"username": "bob",
"role": "invitee",
"state": "left",
"media_uid": 2,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:31:41.871Z",
"joined_at": "2026-10-04T19:30:44.923Z",
"left_at": "2026-10-04T19:31:17.049Z",
"leave_reason": "ended",
"invited_at": "2026-10-04T19:30:41.871Z",
"joined_platform": "android",
"join_count": 1,
"joined_ms": 32126,
"first_joined_at": "2026-10-04T19:30:44.923Z",
"media_joined_at": "2026-10-04T19:30:44.947Z",
"media_left_at": null
},
{
"username": "carol",
"role": "invitee",
"state": "left",
"media_uid": 3,
"invited_by": "alice",
"ring_expires_at": "2026-10-04T19:31:41.871Z",
"joined_at": "2026-10-04T19:30:46.967Z",
"left_at": "2026-10-04T19:31:02.014Z",
"leave_reason": "hangup",
"invited_at": "2026-10-04T19:30:41.871Z",
"joined_platform": "web",
"join_count": 1,
"joined_ms": 15047,
"first_joined_at": "2026-10-04T19:30:46.967Z",
"media_joined_at": "2026-10-04T19:30:46.983Z",
"media_left_at": null
}
],
"ended_via": "client",
"member_count": 3,
"peak_joined": 3,
"billable_minutes": 3
}通话成员对象
| 字段 | 类型 | 说明 |
|---|---|---|
username | String | 用户名,已删除的用户为 null |
role | String | 角色:initiator 发起人 / invitee 被邀请的人 / joiner 自己加入群通话的人 |
state | String | 成员状态,见成员的状态 |
media_uid | Number | 成员在这个通话中的编号,从 1 开始,按成为成员的先后分配 |
invited_by | String | 最近一次邀请他的用户名;发起人、自己加入的人为 null |
ring_expires_at | String | 最近一次邀请的振铃截止时间,没有被邀请过为 null |
joined_at | String | 最近一次加入的时间,没有加入过为 null |
left_at | String | 最近一次离开的时间,没有离开过为 null |
leave_reason | String | 最近一次离开的原因,见离开原因,没有离开过为 null |
invited_at | String | 最近一次被邀请的时间,没有被邀请过为 null |
joined_platform | String | 最近一次加入时所用设备的平台,如 ios、android、web,取值同登录时的 platform;没有加入过为 null |
join_count | Number | 加入的次数,群通话中离开后可以重新加入 |
joined_ms | Number | 在通话中的累计时长,毫秒,只计接通之后的部分。仍在通话中的,本次加入的时长在离开时才计入 |
first_joined_at | String | 第一次加入的时间,没有加入过为 null |
media_joined_at | String | 第一次连上媒体服务的时间,没有连上为 null。接听了却一直为 null 的,通常是设备的网络无法连接媒体服务 |
media_left_at | String | 最近一次断开媒体服务的时间,重新连上后清空,没有为 null |
与独立频道共享额度
原通话和独立 RTC 频道共享同用户媒体占用及应用并发额度。用户已经占用频道或其媒体仍在清理时,不能再占用通话。原通话的计费取整方式不变;频道使用单独计量项和日汇总取整,见频道用量。
