音視訊通話
使用者可以在網頁中發起一對一和群內的語音、視訊通話。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 決定是否顯示通話入口,並監聽配置的變化。餘額和服務狀態可能在顯示按鈕之後變化,能否發起、接聽以服務端的結果為準:
function updateCallButtons() {
render('call-buttons', { visible: im.config?.rtc_enabled === true });
}
updateCallButtons();
im.on('config.changed', updateCallButtons);不允許時以 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(含發起人,由套餐決定,0 表示不限),一次邀請的人數不能超過它減一,超出時 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 | 含發起人,由套餐決定,0 表示不限 |
通話記錄 rtc_call_record_enabled | 開啟時,通話結束後在會話中寫入一條通話記錄訊息 |
以上見執行策略。此外:
- 切換到新計費之前,套餐不包含音影片時同樣以
rtc_disabled拒絕;新計費啟用後餘額不足時也拒絕發起和接聽,見音影片餘額; - 應用同時進行的通話數達到套餐的額度時,發起以
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 到 1000 個,不能包含自己 |
返回: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 頻道:頻道沒有來電和振鈴。一個使用者同時只能在一個通話或頻道中,兩者的計費分開計算。
