音视频通话
用户可以在网页中发起一对一和群内的语音、视频通话。Web SDK 负责通话的全部信令和媒体连接:发起、来电振铃、接听、拒绝、挂断,多台设备同时振铃,断线后的恢复,以及连接媒体服务、打开麦克风和摄像头。你的页面只需要显示通话界面,并把 SDK 交给你的音视频轨道挂到 <video>、<audio> 元素上。
音视频数据由 IM 服务提供的媒体服务承载,你不需要部署或配置任何媒体服务器,也不需要接入其他音视频 SDK:SDK 在第一次通话时自动加载所需的媒体组件。
// 发起一对一视频通话
const call = await im.calls.start({ to_user: 'bob', media: 'video' });
// 对方的画面和声音
call.on('track', ({ username, local }) => {
if (!local && username) call.attach(username, document.querySelector<HTMLVideoElement>('#remote')!);
});
// 挂断
await call.hangup();通话的完整规则(忙线、多台设备、群通话的人数、结束原因、计费)见服务端文档音视频通话。
准备工作
开启音视频
应用默认不开启音视频。在控制台的运行策略中开启“开启音视频通话”(rtc_enabled),并且应用的套餐包含音视频后,用户才能发起和接听通话。
客户端的运行配置 im.config.rtc_enabled 在策略和套餐都允许时为 true,请据此显示通话的入口;策略或套餐变化时运行配置随之更新,触发 config.changed:
function updateCallButtons() {
render('call-buttons', { visible: im.config?.rtc_enabled === true });
}
updateCallButtons();
im.on('config.changed', updateCallButtons);rtc_enabled 为 false 时,im.calls.start() 以 permission_denied(rtc_disabled)拒绝。
浏览器的要求
- 安全的页面:浏览器只允许 HTTPS 页面(以及本机调试用的
localhost)使用麦克风和摄像头。 - 麦克风和摄像头权限:接通后 SDK 自动打开麦克风,视频通话另外打开摄像头,浏览器会在第一次使用时弹出授权提示。用户拒绝授权或设备不可用时如何处理,见麦克风和摄像头。
- 在用户的点击中调用:浏览器要求页面有用户的一次点击才能播放声音、打开设备。请在按钮的点击事件中调用
start()、join()和resume()。
通过 script 标签引入时
使用全局构建(通过 <script> 标签引入 Web SDK)时,媒体组件不包含在全局构建中,需要在页面中另外引入,见初始化。通过 npm 安装时不需要任何额外步骤。
发起一对一通话
start() 指定对方的用户名和媒体类型:audio 语音,video 视频(通话中不能切换)。可以附带自定义字段 ext(如业务订单号),随来电送达对方。
import { DRError } from '@deeprespond/im-web';
try {
const call = await im.calls.start({ to_user: 'bob', media: 'video', ext: { order_id: '8812' } });
const { call: info } = call.getSnapshot();
if (info.status === 'ended' && info.end_reason === 'busy') {
showToast('对方忙线中');
} else {
render('call-screen', call);
}
} catch (err) {
if (err instanceof DRError && err.code === 'call_in_progress') {
showToast('你正在另一个通话中,请先挂断');
} else if (err instanceof DRError && err.code === 'user_blocked') {
showToast('对方拒绝了你的通话');
} else {
throw err;
}
}- 发起成功后你就进入了通话,SDK 立即连接媒体服务、打开麦克风(视频通话还有摄像头),等待对方接听;这时可以显示自己的预览画面。
- 对方正在另一个通话中时,
start()照常兑现,但返回的通话已经结束(end_reason为busy),不会振铃,也不连接媒体。 - 你自己已在一个通话中时,以
call_in_progress拒绝,details.call_id为你正在进行的通话。用户确认后,先挂断它再发起。 - 与发送单聊消息的规则相同:对方把你加入了黑名单时以
user_blocked拒绝,应用开启了只能给好友发消息而你们不是好友时以not_friend拒绝,你被全局禁言单聊时以user_muted拒绝。 - 发起太频繁时以
rate_limited拒绝,SDK 不自动重试;网络错误时 SDK 自动重试,不会重复发起。
接下来,对方接听后通话的 status 变为 active,见显示通话状态。
发起群通话
在一个群里发起群通话,usernames 为要振铃的群成员;也可以不邀请任何人,等其他成员从群通话横幅加入。
import { DRError } from '@deeprespond/im-web';
declare const groupId: string;
try {
const call = await im.calls.start({ to_group: groupId, usernames: ['lisi', 'wangwu'], media: 'audio' });
// 没有振铃的人(正在其他通话中、在群里被禁言等)
const notRinging = (call.getSnapshot().call.members ?? []).filter((m) => m.role === 'invitee' && m.state !== 'ringing');
if (notRinging.length > 0) showToast(`${notRinging.map((m) => m.username).join('、')} 暂时无法接听`);
} catch (err) {
if (err instanceof DRError && err.code === 'already_exists') {
// 群里已有进行中的群通话:改为加入它
const existing = await im.calls.get(String(err.details?.call_id));
await existing.join();
} else {
throw err;
}
}- 发起人必须能在群里发言(不被群禁言、全员禁言时是群主或管理员)。被邀请的人必须是群成员、能在群里发言,否则不振铃;正在其他通话中的人记为忙线(
busy),不振铃。 - 每个群同时只有一个群通话。群里已有进行中的群通话时以
already_exists拒绝,details.call_id为那个通话,请改为加入它。 - 同时在通话中的人数上限为运行配置的
max_group_call_participants(默认 16,含发起人),一次邀请的人数不能超过它减一,超出时 SDK 在本地以local_validation(too_many_invitees)拒绝。 - 群通话不检查个人黑名单。
接收来电
收到来电(一对一来电或群通话的邀请)时,SDK 触发 call.incoming 事件,开始振铃。同一浏览器的每个标签页都会收到这个事件,alert 为 true 的标签页是负责提醒的那一个,请只在它里面播放铃声,避免几个标签页同时响:
im.on('call.incoming', ({ call, alert }) => {
const { inviter, group, call: info } = call.getSnapshot();
render('incoming', {
from: inviter?.nickname || inviter?.username,
avatar: inviter?.avatar_url,
group: group?.name ?? null, // 群通话的群,一对一为 null
media: info.media, // 'audio' 或 'video'
});
if (alert) void ringtone.play();
// 停止振铃:对方取消、超时、在其他设备上接听或拒绝、本人接听或拒绝
const off = call.subscribe(() => {
if (!call.getSnapshot().ringing) {
ringtone.pause();
render('incoming', null);
off();
}
});
});- 正在振铃的来电:
im.calls.incoming是此刻正在振铃的全部来电。用户可能同时收到多个来电,由他选择接听哪一个。 - 离线时的来电:页面打开、网络恢复后,SDK 查询正在振铃的来电,没有通知过的同样触发
call.incoming;同一个来电不会重复触发。 - 振铃时间:由运行策略
rtc_ring_timeout_seconds决定(默认 60 秒)。到时间没有接听,SDK 停止振铃(快照的ringing变为false),一对一通话以no_answer结束,群通话中你的邀请记为未接听(missed)。 - 多台设备同时振铃:你在其他设备上(另一个浏览器、手机 App)接听或拒绝后,这里停止振铃。在其他设备上接听时,
call.changed事件的change.kind为answered_elsewhere,可以显示“已在其他设备接听”。 - 忙线:你正在通话中时,新的一对一来电由服务端直接判定为忙线,不会振铃;群通话的邀请记为忙线,不振铃。浏览器无法得知用户是否正在打电话,SDK 不会自动以忙线拒绝;你的页面可以按业务需要调用
reject({ reason: 'busy' })。
接听与拒绝
declare const call: import('@deeprespond/im-web').CallHandle;
// “接听”按钮的点击事件中
async function onAccept() {
await call.join();
render('call-screen', call);
}
// “拒绝”按钮
async function onDecline() {
await call.reject();
}join()接听来电或加入群通话,成功后 SDK 连接媒体服务、打开麦克风和摄像头,通话进入active(群通话中第一次有两个人在通话中时)。reject()拒绝。一对一通话随之结束(end_reason为rejected),群通话中只是你的邀请结束(declined),不影响其他人。你的其他设备随之停止振铃。- 你已在另一个通话中时,
join()以call_in_progress拒绝,details.call_id为那个通话,请先挂断它。 - 通话已经结束(对方已取消)时以
call_ended拒绝;你已在其他设备上接听时以call_in_progress(joined_on_other_device)拒绝。 - 群通话已满时以
limit_exceeded(call_full)拒绝。
显示通话状态
通话的全部状态在句柄的快照中,变化时订阅 subscribe() 即可更新界面:
call.subscribe(() => {
const s = call.getSnapshot();
render('call-screen', {
status: s.call.status, // 'ringing' 等待接听 / 'active' 通话中 / 'ended' 已结束
seconds: s.call.duration_seconds, // 接通后的时长
media: s.media, // 媒体连接:'connecting'、'connected'、'reconnecting' 等
microphone: s.local.microphone,
camera: s.local.camera,
others: s.remote, // 其他人是否开着麦克风、摄像头
});
});| 快照字段 | 说明 |
|---|---|
call | 通话,见 Call:状态 status、成员 members、结束原因 end_reason 等 |
self | 你在通话中的状态:state 为你的成员状态,joined_on_this_device 为你是否就在这台设备上通话 |
ringing | 这里是否正在振铃 |
media | 本标签页的媒体连接:none 没有 / connecting 正在连接 / connected 已连接 / reconnecting 网络波动,正在重新连接 / disconnected 已断开 |
local | 你的麦克风和摄像头是否打开 |
remote | 其他成员的媒体:每人一项,audio、video 为他的麦克风、摄像头是否打开 |
media_tab、resumable | 见多个标签页和页面刷新后恢复通话 |
需要知道“发生了什么”(谁加入、谁拒绝、谁离开)时,监听 call.changed 事件,change.kind 说明变化:
im.on('call.changed', ({ call, change }) => {
if (!change) return;
const who = change.usernames.filter(Boolean).join('、');
switch (change.kind) {
case 'joined': showToast(`${who} 加入了通话`); break;
case 'declined': showToast(`${who} 拒绝了邀请`); break;
case 'left': showToast(`${who} 离开了通话`); break;
case 'answered_elsewhere': showToast('已在其他设备接听'); break;
case 'ended': render('call-ended', call.getSnapshot().call.end_reason); break;
}
});change.kind | 说明 |
|---|---|
created | 你在其他设备上发起了通话,这里可以显示“正在通话”并提供挂断 |
invited | 群通话中有新的邀请 |
joined | 有人接听或加入 |
answered_elsewhere | 你在另一台设备上接听了 |
declined | 有人拒绝 |
missed | 群通话的邀请超时未接听 |
left | 有人离开或被移出,reason 为离开原因 |
canceled | 群通话中正在振铃的邀请被取消 |
ended | 通话结束,reason 为结束原因 |
你自己在这个页面上的操作(接听、拒绝、挂断)也会触发 call.changed,change 为 null。
把视频渲染到页面
SDK 以 track 事件交给你通话中的音视频轨道(标准的 MediaStreamTrack),包括你自己的(local 为 true)和其他成员的;有人关闭摄像头、离开时触发 trackRemoved。最简单的做法是在轨道变化时调用 attach(),把某个成员的全部轨道挂到一个元素上:
declare const call: import('@deeprespond/im-web').CallHandle;
const me = im.auth.currentUser?.username ?? '';
const localVideo = document.querySelector<HTMLVideoElement>('#local')!;
localVideo.muted = true; // 自己的预览不要播放自己的声音
const remoteVideo = document.querySelector<HTMLVideoElement>('#remote')!;
function refresh(username: string | null, local: boolean) {
if (local) call.attach(me, localVideo);
else if (username) call.attach(username, remoteVideo);
}
call.on('track', ({ username, local }) => refresh(username, local));
call.on('trackRemoved', ({ username, local }) => refresh(username, local));<video id="local" autoplay playsinline muted></video>
<video id="remote" autoplay playsinline></video>attach(username, element)把这个成员当前的全部轨道(声音和画面)设为元素的srcObject。也可以传成员的media_uid。语音通话用<audio autoplay>即可。- 群通话中为每个成员各准备一个元素,按
track事件中的username挂上去;成员的顺序和昵称取自快照的call.members。 - 自己的预览元素必须静音(
muted),否则会听到自己的回声。 - 显示“对方已静音”“对方关闭了摄像头”,用快照的
remote中每人的audio、video。
通话中的操作
静音与开关摄像头
// 静音 / 取消静音
await call.setMicrophoneEnabled(!call.getSnapshot().local.microphone);
// 关闭 / 打开摄像头(只用于视频通话)
await call.setCameraEnabled(!call.getSnapshot().local.camera);- 其他成员会看到你的状态变化(他们快照中的
remote)。 - 语音通话中打开摄像头以
invalid_state(audio_call)拒绝。视频通话关闭摄像头后,仍按视频通话记录。 - 媒体还没有连上时以
invalid_state(media_not_connected)拒绝。 - SDK 使用浏览器默认的麦克风和摄像头,目前不提供选择或切换设备的接口。
邀请更多人
群通话中,在通话中的成员可以继续邀请其他群成员。拒绝过、未接听、离开过的成员可以再次被邀请,重新振铃。返回每个人的结果,某个人失败不影响其他人:
const results = await call.invite(['zhaoliu', 'sunqi']);
for (const r of results) {
if ('code' in r) showToast(`${r.username} 无法邀请:${r.code}`);
else if (r.status === 'busy') showToast(`${r.username} 正在通话中`);
}成功的 status 为 invited(已振铃)、busy(正在其他通话中,没有振铃)或 already_in_call(已在本通话中);失败的带 code,如 not_group_member、group_muted、rate_limited(邀请太频繁)、limit_exceeded(通话已满)。一对一通话不能邀请他人。
移出成员
客户端不能把别人移出通话。需要移出时(如处理违规),由你的服务端调用通话管理接口;被移出的人快照中通话照常更新,他的媒体连接随之断开。
发送数据消息
sendData() 通过媒体连接向通话中的其他成员发送一段数据(字符串或 Uint8Array),适合同步“举手”、白板笔迹这类与通话同时进行的状态。数据只发给此刻在通话中的人,不保存。
await call.sendData(JSON.stringify({ action: 'raise_hand' }));
call.on('data', ({ username, data }) => {
const payload = JSON.parse(new TextDecoder().decode(data)) as { action: string };
if (payload.action === 'raise_hand') showToast(`${username} 举手了`);
});“对方已静音”等状态 SDK 已经提供(快照的 remote),不需要用数据消息同步。
挂断与结束
// 挂断:接通前为取消,接通后为离开,正在振铃时等同拒绝
await call.hangup();
// 结束群通话:全部成员离开(只有发起人、群主和群管理员可以)
await call.end();- 一对一通话中任一方挂断,通话结束。群通话中挂断只是你离开,其他人继续通话;离开后可以再次
join()加入。 - 群通话的发起人、群主和群管理员可以用
end()结束整个群通话,其他人调用以permission_denied(not_host)拒绝。对一对一通话,end()等同挂断。 - 群通话在通话中的人都离开后自动结束;只剩一人、且没有正在振铃的邀请,持续 60 秒后也自动结束。
- 你的任何一台设备都可以挂断。
- 挂断后 SDK 断开媒体、关闭麦克风和摄像头。
通话结束后,快照的 call.status 为 ended,call.end_reason 为结束原因,之后快照不再变化。界面上的提示由你决定,下表仅供参考,完整的说明见结束原因:
end_reason | 发起方的提示 | 接听方的提示 |
|---|---|---|
completed | 通话时长 | 通话时长 |
canceled | 已取消 | 对方已取消 |
rejected | 对方已拒绝 | 已拒绝 |
busy | 对方忙线中 | 未接来电 |
no_answer | 对方无应答 | 未接来电 |
connection_lost | 通话中断 | 通话中断 |
| 其他 | 通话已结束 | 通话已结束 |
以后可能增加新的结束原因,遇到不认识的值时请显示为“通话已结束”。
群通话横幅
群通话开始、在通话中的人数变化和结束时,群的全部在线成员收到通知,用于在群聊页面显示“3 人正在通话”的横幅和加入入口。SDK 把它记在本地,触发 groupCall.changed 事件,im.calls.groupCallBanner(groupId) 同步返回这个群当前的横幅(没有时为 null)。
页面刷新后、或者通知发出时你还不是群成员,本地可能还不知道群里的通话。打开一个群的聊天页面时,请调用 im.calls.groupCall(groupId) 查询一次:
declare const groupId: string;
function renderBanner() {
const banner = im.calls.groupCallBanner(groupId);
render('group-call-banner', banner && {
text: banner.joined_count > 1 ? `${banner.joined_count} 人正在通话` : '等待其他成员加入',
media: banner.media,
});
}
// 打开群聊页面时
await im.calls.groupCall(groupId).catch(() => null);
renderBanner();
const off = im.on('groupCall.changed', () => renderBanner());
// 点击横幅加入
async function onJoinFromBanner() {
const call = await im.calls.groupCall(groupId);
if (call) await call.join();
}横幅的人数可能有短暂的误差,准确的成员以通话快照为准。
多个标签页
同一浏览器的多个标签页是同一台设备,同一时刻只有一个标签页能在通话中:
- 来电在每个标签页都触发
call.incoming,在alert为true的标签页播放铃声;在哪个标签页接听,媒体(麦克风、摄像头、对方的画面和声音)就在哪个标签页。 - 其他标签页中,这个通话的快照
media_tab为other,可以显示“通话在其他标签页中进行”。在这些标签页中调用join()、setMicrophoneEnabled()、setCameraEnabled()以invalid_state(media_in_other_tab)拒绝;hangup()、end()、invite()可以在任何标签页调用。 - 正在通话的标签页被关闭后,通话不会立即中断:其他标签页中这个通话的
resumable变为true,用户可以在那里点击恢复,见下一节。
call.subscribe(() => {
const s = call.getSnapshot();
if (s.media_tab === 'other') render('call-hint', '通话在其他标签页中进行');
else if (s.resumable) render('call-hint', '点击恢复通话');
});断线与恢复
心跳和媒体凭据的续期由 SDK 自动进行,你不需要处理。
- 网络波动、切换网络:媒体连接自动重连,期间快照的
media为reconnecting,可以显示“网络不佳,正在重新连接”。只要在 45 秒内恢复,通话不受影响。 - 断网超过 45 秒:服务端判定你已掉线。一对一通话以
connection_lost结束;群通话中你离开(connection_lost),网络恢复后可以重新join()。 - 网络恢复后:SDK 查询你正在进行的通话和正在振铃的来电,与本地核对:已经结束的通话快照更新为
ended,断网期间的来电开始振铃。
页面刷新后恢复通话
用户在通话中刷新了页面(或关闭了正在通话的标签页)时,服务端仍认为他在通话中。页面重新打开、登录后,SDK 查询到这个通话,im.calls.current 为它,快照的 resumable 为 true。SDK 立即代为发送心跳,但浏览器要求用户点击后才能重新打开麦克风、播放声音,所以需要你提示用户“点击恢复通话”,在点击中调用 resume()。请尽快提示:45 秒内没有恢复,服务端会判定掉线。
function checkResumable() {
const call = im.calls.current;
if (call?.getSnapshot().resumable) {
render('resume-button', {
onClick: async () => {
await call.resume();
render('call-screen', call);
},
});
}
}
// 登录、重新连接后,SDK 同步完通话状态时
im.on('sync.completed', ({ modules }) => {
if (modules.includes('calls')) checkResumable();
});resumable 不为 true 时调用 resume() 以 invalid_state(not_resumable)拒绝。
麦克风和摄像头
接通、恢复通话以及 setMicrophoneEnabled(true)、setCameraEnabled(true) 时,SDK 打开麦克风和摄像头:
- 用户没有授权(或拒绝了浏览器的授权提示)时,以
permission_required拒绝,details.permissions列出缺少的权限(microphone、camera); - 设备不存在或被其他程序占用时,以
device_error拒绝,details.device为microphone或camera,details.reason为not_found或busy。
join() 和 resume() 遇到这两种错误时,通话已经接通,只是没有打开该设备(快照的 local 中为 false),对方能听到你的声音之前需要你引导用户授权,再调用 setMicrophoneEnabled(true):
import { DRError } from '@deeprespond/im-web';
declare const call: import('@deeprespond/im-web').CallHandle;
try {
await call.join();
} catch (err) {
if (err instanceof DRError && err.code === 'permission_required') {
render('permission-guide', err.details?.permissions); // 引导用户在浏览器中允许麦克风、摄像头
} else if (err instanceof DRError && err.code === 'device_error') {
showToast(err.details?.device === 'camera' ? '摄像头不可用' : '麦克风不可用');
} else {
throw err;
}
}
// 用户授权后
await call.setMicrophoneEnabled(true);发起通话时,SDK 在 start() 兑现之后才连接媒体、打开设备,权限问题不会让 start() 失败。媒体连上(快照的 media 为 connected)而 local.microphone 仍为 false 时,调用 setMicrophoneEnabled(true) 可以得到具体的错误。也可以在发起前先用浏览器的 navigator.mediaDevices.getUserMedia() 请求一次授权。
运行策略与套餐
| 设置 | 影响 |
|---|---|
开启音视频通话 rtc_enabled | 关闭时不能发起、接听、加入通话和邀请他人,以 permission_denied(rtc_disabled)拒绝;进行中的通话不受影响 |
来电振铃时间 rtc_ring_timeout_seconds | 默认 60 秒 |
群通话人数上限 max_group_call_participants | 默认 16,含发起人 |
通话记录 rtc_call_record_enabled | 开启时,通话结束后在会话中写入一条通话记录消息 |
以上见运行策略。此外:
- 应用的套餐不包含音视频时,
rtc_enabled为false; - 应用同时进行的通话数达到套餐的额度时,发起以
limit_exceeded(app_concurrent_calls)拒绝; - 应用处于只读状态(如欠费)时,发起和邀请以
app_unavailable拒绝,进行中的通话不受影响; - 本应用的音视频被平台暂停时,发起以
permission_denied(rtc_suspended)拒绝;音视频服务暂不可用时为permission_denied(rtc_not_configured)。这两种情况只在发起时才知道,请按原因提示用户; - 以
permission_denied(media_provider_unsupported)拒绝时,说明当前版本的 Web SDK 不支持平台为应用提供的媒体服务,请升级 Web SDK,并提示用户“当前版本不支持通话”。
通话记录
通话历史
im.calls.history() 分页返回你参与过的通话(包括未接听、拒绝的),按时间从新到旧,保留 180 天:
const page = await im.calls.history({ limit: 20 });
for (const c of page.items) {
render(c.type === 'single' ? c.peer : c.group_id, c.media, c.end_reason, c.duration_seconds, c.created_at);
}会话中的通话记录消息
通话结束后,服务端在会话中写入一条类型为 call 的消息(一对一写入双方的单聊会话,群通话写入群会话)。用显示辅助 renderCallRecord() 得到“通话时长 01:32”“对方已拒绝”这样的文字:
import type { MessageView } from '@deeprespond/im-web';
import { renderCallRecord } from '@deeprespond/im-web/render';
declare const message: MessageView;
if (message.type === 'call') {
const text = renderCallRecord(message, { self: im.auth.currentUser?.username ?? '' });
render('call-record', text);
}在 React 中使用
用 useDREvent 监听来电,用 useHandle 订阅通话的快照:
import { useState } from 'react';
import { useDREvent, useHandle } from '@deeprespond/im-web-react';
import type { CallHandle } from '@deeprespond/im-web';
function CallLayer() {
const [call, setCall] = useState<CallHandle | null>(null);
useDREvent('call.incoming', ({ call }) => setCall(call));
const snapshot = useHandle(call);
if (!call || !snapshot || snapshot.call.status === 'ended') return null;
if (snapshot.ringing) {
return (
<div>
<p>{snapshot.inviter?.nickname} 邀请你{snapshot.call.media === 'video' ? '视频' : '语音'}通话</p>
<button onClick={() => void call.join()}>接听</button>
<button onClick={() => void call.reject()}>拒绝</button>
</div>
);
}
return (
<div>
<p>{snapshot.call.status === 'active' ? `通话中 ${snapshot.call.duration_seconds} 秒` : '等待对方接听'}</p>
<button onClick={() => void call.setMicrophoneEnabled(!snapshot.local.microphone)}>
{snapshot.local.microphone ? '静音' : '取消静音'}
</button>
<button onClick={() => void call.hangup()}>挂断</button>
</div>
);
}把某个成员的画面挂到 <video> 上:
import { useEffect, useRef } from 'react';
import type { CallHandle } from '@deeprespond/im-web';
function MemberVideo({ call, username, self }: { call: CallHandle; username: string; self?: boolean }) {
const ref = useRef<HTMLVideoElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const update = () => call.attach(username, el);
update();
const offTrack = call.on('track', (p) => { if (p.username === username) update(); });
const offRemoved = call.on('trackRemoved', (p) => { if (p.username === username) update(); });
return () => { offTrack(); offRemoved(); };
}, [call, username]);
return <video ref={ref} autoPlay playsInline muted={self} />;
}Vue 绑定提供同名的 useDREvent、useHandle,用法相同。
接口参考
im.calls.start()
发起一对一通话或群通话。发起成功后你就在通话中,SDK 随即连接媒体服务。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.to_user | string | 二选一 | 一对一通话的对方用户名 |
p.to_group | string | 二选一 | 群通话所在的群 ID |
p.usernames | string[] | 否 | 群通话中要振铃的成员,不超过 max_group_call_participants 减一,不能包含自己 |
p.media | 'audio' | 'video' | 是 | 语音或视频,通话中不能切换 |
p.ext | Record<string, string> | 否 | 自定义字段,最多 16 项,JSON 编码后不超过 1 KB,随来电送达对方 |
返回:Promise<CallHandle>。一对一通话的对方正在通话中时,返回的通话已经结束(end_reason 为 busy)。
错误:permission_denied(rtc_disabled 应用没有开启音视频;rtc_suspended、rtc_not_configured 音视频暂不可用;media_provider_unsupported 当前版本不支持)、call_in_progress(你已在通话中,details.call_id 为那个通话)、already_exists(群里已有进行中的群通话,details.call_id 为那个通话)、user_blocked、not_friend、user_muted、not_group_member、group_muted、group_disabled、not_found(对方或群不存在)、limit_exceeded(app_concurrent_calls)、app_unavailable、rate_limited(call_rate、pair_call_rate、group_call_rate)、invalid_argument(如 self_call 呼叫自己、invalid_ext)、local_validation(缺少参数、too_many_invitees)。
im.calls.current
只读属性,CallHandle | null:你正在进行的通话(你的成员状态为 joined),包括在其他设备上进行的(self.joined_on_this_device 为 false);没有时为 null。
im.calls.incoming
只读属性,readonly CallHandle[]:此刻正在振铃的来电。
im.calls.get()
按通话 ID 取得通话的句柄,本地已有的直接返回,否则向服务端查询。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
call_id | string | 是 | 通话 ID |
返回:Promise<CallHandle>。
错误:not_found(通话不存在,或你看不到它:一对一通话只有双方能查到,群通话是群的当前成员能查到)。
im.calls.history()
分页返回你参与过的通话,按时间从新到旧。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
page.limit | number | 否 | 每页条数,默认 20,最多 50 |
page.cursor | string | 否 | 上一页返回的 next_cursor |
返回:Promise<Page<CallSummary>>。
错误:rate_limited(query_rate)。
im.calls.groupCall()
查询一个群当前进行中的群通话,同时更新本地的群通话横幅。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
group_id | string | 是 | 群 ID |
返回:Promise<CallHandle | null>,没有进行中的群通话时为 null。
错误:not_found(群不存在)、not_group_member、rate_limited。
im.calls.groupCallBanner()
同步返回本地记下的这个群的群通话横幅。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
group_id | string | 是 | 群 ID |
返回:GroupCallBanner | null。null 表示没有进行中的群通话,或本地还不知道(页面刷新后、没有查询过的群),见群通话横幅。
call.getSnapshot()
返回通话当前的快照(只读,变化时整体替换),见 CallSnapshot。
返回:CallSnapshot。
call.subscribe()
订阅快照的变化。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
listener | () => void | 是 | 快照变化时调用 |
返回:() => void,调用后取消订阅。
call.on()
监听媒体轨道和数据消息。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
event | 'track' | 'trackRemoved' | 'data' | 是 | 事件名 |
listener | (payload) => void | 是 | 处理函数 |
返回:() => void,调用后取消监听。
| 事件 | 载荷 | 说明 |
|---|---|---|
track | { username: string | null; media_uid: number; kind: 'audio' | 'video'; track: MediaStreamTrack; local: boolean } | 有新的轨道:有人加入、打开了摄像头,或你自己的麦克风、摄像头打开 |
trackRemoved | 同上 | 轨道不再可用:有人离开、关闭了摄像头 |
data | { username: string | null; media_uid: number; data: Uint8Array } | 收到其他成员的数据消息 |
轨道只在持有媒体连接的标签页中触发。
call.join()
接听来电、接听群通话的邀请、从横幅加入群通话,或离开群通话后重新加入。须在用户的点击中调用。
返回:Promise<void>,接听成功并连上媒体后兑现。
错误:call_in_progress(in_other_call 你在另一个通话中,details.call_id 为那个通话;joined_on_other_device 你已在其他设备上接听)、call_ended(通话已经结束)、limit_exceeded(call_full 群通话已满;call_member_limit 群通话累计涉及的成员已达上限)、version_conflict(你当前的状态不能接听,如已经拒绝过的一对一来电)、permission_denied(rtc_disabled 等)、not_group_member、group_muted、user_muted、invalid_state(media_in_other_tab)、permission_required、device_error(这两种情况已经接听,见麦克风和摄像头)、unsupported(媒体组件加载失败)、rate_limited(signal_rate,等待不超过 5 秒的 SDK 会自动重试一次)。
call.reject()
拒绝来电或群通话的邀请。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
p.reason | 'declined' | 'busy' | 否 | declined 拒绝(默认),busy 以忙线拒绝 |
返回:Promise<void>。重复拒绝视为成功。
错误:version_conflict(你已经接听,或没有被邀请)、rate_limited。
call.hangup()
挂断:接通前的发起方为取消,在通话中为离开,正在振铃时等同拒绝。不论成功与否,都会断开本标签页的媒体。
返回:Promise<void>。挂断已结束的通话视为成功。
错误:rate_limited、network_error 等网络错误。
call.end()
结束整个群通话,全部成员离开,正在振铃的邀请取消。只有发起人、群主和群管理员可以;对一对一通话等同挂断。
返回:Promise<void>。
错误:permission_denied(not_host)、rate_limited。
call.invite()
在群通话中邀请更多群成员。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
usernames | string[] | 是 | 要邀请的群成员,1 到 31 个,不能包含自己 |
返回:Promise<CallInviteResult[]>,每个人的结果,见邀请更多人。
错误:invalid_argument(group_call_only 一对一通话不能邀请;invalid_usernames;too_many_invitees)、permission_denied(not_in_call 你不在通话中;rtc_disabled)、call_ended、app_unavailable、local_validation(usernames 为空)。
call.resume()
页面刷新或通话所在的标签页关闭后,在本标签页恢复通话:重新连接媒体、打开麦克风和摄像头。只在快照的 resumable 为 true 时可用,须在用户的点击中调用。
返回:Promise<void>。
错误:invalid_state(not_resumable)、call_ended、version_conflict(你已不在通话中)、permission_required、device_error、unsupported。
call.setMicrophoneEnabled()
打开或关闭麦克风(静音)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
on | boolean | 是 | true 打开,false 关闭 |
返回:Promise<void>。
错误:invalid_state(media_not_connected 媒体还没有连上;media_in_other_tab 通话在其他标签页中)、permission_required、device_error。
call.setCameraEnabled()
打开或关闭摄像头,只用于视频通话。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
on | boolean | 是 | true 打开,false 关闭 |
返回:Promise<void>。
错误:invalid_state(audio_call 语音通话不能打开摄像头;media_not_connected;media_in_other_tab)、permission_required、device_error。
call.sendData()
通过媒体连接向通话中的其他成员发送数据。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
data | Uint8Array | string | 是 | 数据,字符串按 UTF-8 编码 |
返回:Promise<void>。
错误:invalid_state(media_not_connected)。
call.attach()
把一个成员当前的全部音视频轨道设为元素的 srcObject。成员的轨道变化后(track、trackRemoved 事件)需要再调用一次。找不到这个成员时什么也不做。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
username_or_uid | string | number | 是 | 成员的用户名,或他在通话中的 media_uid |
el | { srcObject: unknown } | 是 | 媒体元素,通常是 <video> 或 <audio> |
返回:无。
事件
以下事件用 im.on() 监听,见事件。
| 事件 | 载荷 | 说明 |
|---|---|---|
call.incoming | { call: CallHandle; alert: boolean } | 收到来电,开始振铃。alert 为 true 的标签页负责播放铃声 |
call.changed | { call: CallHandle; change: CallChange | null } | 通话有变化(其他人或其他设备的操作),见显示通话状态 |
groupCall.changed | GroupCallBanner | 某个群的群通话横幅有变化,请用 im.calls.groupCallBanner() 读取 |
数据结构
CallSnapshot
句柄的快照。
| 字段 | 类型 | 说明 |
|---|---|---|
call | Call | 通话 |
self | CallSelf | null | 你在通话中的状态:state 为你的成员状态(不是成员时为 null),joined_on_this_device 为你是否就在这台设备(这个浏览器)上通话 |
inviter | object | 来电的邀请人:username、nickname、avatar_url,只在来电中有 |
group | object | null | 群通话所在的群:group_id、name、avatar_url;一对一为 null,只在来电中有 |
media | string | 本标签页的媒体连接:none、connecting、connected、reconnecting、disconnected |
local | { microphone: boolean; camera: boolean } | 你的麦克风、摄像头是否打开 |
remote | Array<{ username: string | null; media_uid: number; audio: boolean; video: boolean }> | 其他成员的麦克风、摄像头是否打开 |
ringing | boolean | 这里是否正在振铃,到振铃截止时间自动变为 false |
media_tab | 'this' | 'other' | null | 媒体在本标签页、本浏览器的其他标签页,或都不在 |
resumable | boolean | 你在这台设备上的通话没有标签页持有媒体,可以调用 resume() 恢复 |
Call
通话。
| 字段 | 类型 | 说明 |
|---|---|---|
call_id | string | 通话 ID |
type | 'single' | 'group' | 一对一 / 群通话 |
group_id | string | null | 群通话所在的群,一对一为 null |
media | 'audio' | 'video' | 语音 / 视频 |
status | 'ringing' | 'active' | 'ended' | 等待接通 / 已接通 / 已结束,只按这个顺序前进 |
initiator | string | null | 发起人的用户名 |
max_participants | number | 同时在通话中的人数上限,发起时确定 |
joined_count | number | 此刻在通话中的人数 |
members | CallMember[] | 成员;通话历史中没有这个字段 |
created_at | string | 发起时间 |
answered_at | string | null | 接通时间,没有接通为 null |
ended_at | string | null | 结束时间 |
end_reason | string | null | 结束原因,见挂断与结束 |
ended_by | string | null | 结束通话的用户;服务端、平台和系统结束的为 null |
duration_seconds | number | 接通到结束(未结束时为到现在)的秒数,未接通为 0 |
version | number | 版本号,每次变化加一 |
ext | Record<string, string> | null | 发起时的自定义字段 |
时间都是 ISO 8601 格式的字符串。
CallMember
通话的成员。
| 字段 | 类型 | 说明 |
|---|---|---|
username | string | null | 用户名,已删除的用户为 null |
role | 'initiator' | 'invitee' | 'joiner' | 发起人 / 被邀请的人 / 自己加入群通话的人 |
state | string | ringing 正在振铃 / joined 在通话中 / left 已离开 / declined 已拒绝 / busy 忙线 / missed 未接听 / canceled 邀请被取消 |
media_uid | number | 成员在这个通话中的编号,attach() 可以用它 |
invited_by | string | null | 最近一次邀请他的人 |
ring_expires_at | string | null | 振铃截止时间 |
joined_at | string | null | 最近一次加入的时间 |
left_at | string | null | 最近一次离开的时间 |
leave_reason | string | null | 最近一次离开的原因,见离开原因 |
CallSummary
im.calls.history() 的一项:Call(不含 members),另有:
| 字段 | 类型 | 说明 |
|---|---|---|
self | CallSelf | 你在这个通话中的状态 |
peer | string | null | 一对一通话的对方用户名,群通话为 null |
CallChange
call.changed 事件中的变化。
| 字段 | 类型 | 说明 |
|---|---|---|
kind | string | 变化的类型,见显示通话状态 |
usernames | Array<string | null> | 状态变化的成员 |
actor | string | null | 操作人,你的服务端、平台和系统操作时为 null |
reason | string | null | 拒绝的原因、离开原因或结束原因 |
CallInviteResult
invite() 中一个人的结果,二者之一:
| 字段 | 类型 | 说明 |
|---|---|---|
username | string | 用户名 |
status | string | 成功时:invited、busy、already_in_call |
code | string | 失败时的错误码 |
message | string | 失败时的说明 |
details | Record<string, unknown> | 失败的详细原因 |
GroupCallBanner
群通话横幅。
| 字段 | 类型 | 说明 |
|---|---|---|
group_id | string | 群 ID |
call_id | string | 通话 ID |
media | 'audio' | 'video' | 语音 / 视频 |
status | 'ringing' | 'active' | 'ended' | 通话状态 |
joined_count | number | 在通话中的人数,可能有短暂的误差 |
initiator | string | null | 发起人 |
version | number | 版本号 |
独立频道
本页的呼叫、振铃和接听属于原通话。无需 IM 群的会议 / 语音房使用独立 RTC 频道,不自动使用来电 UI 或通知;媒体占用与原通话共享,计费口径分别定义。
