跳转到主要内容

web客户端 - WebRTC 接入文档

本文面向 CRM、客服系统和业务前端开发人员,说明如何接入由服务提供方统一托管的 WebRTC 电话。

客户只需要加载在线脚本,不需要安装 npm 包、部署电话文件或取得项目源码。

1. 接入信息

在线脚本地址:

https://indo.infin8linx.com/v1/webrtc-phone-hosted-loader.global.js

客户业务后端需要向前端返回临时 SIP 凭据:

{
  "extension": "1001",
  "password": "temporary-sip-password",
  "displayName": "客服 1001"
}

displayName 可以省略。只有服务提供方明确给出了与分机号不同的鉴权用户名时,才额外传入 authorizationUser。

请勿把正式 SIP 密码写入 HTML、URL、公开 JavaScript 或浏览器存储。

2. 选择国家节点

国家/地区 sipNodeId 是否默认
墨西哥 mexico 否
印度尼西亚 indonesia 是
新加坡 singapore 否

客户选择国家后,将对应的 sipNodeId 传给电话即可,不需要填写域名和端口。

3. 接入内置电话条

推荐在业务页面加载后预先创建电话条,在用户输入账号时完成 iframe 和运行时初始化。此时不会连接 SIP,也不会注册坐席;点击登录后只需提交凭据完成注册,首次登录等待时间更短。

<script src="https://indo.infin8linx.com/v1/webrtc-phone-hosted-loader.global.js"></script>

<div id="phone-container"></div>

<script>
  let phonePromise;

  function preparePhone() {
    if (!phonePromise) {
      phonePromise = WebRTCPhoneHosted.HostedPhone.mount({
        container: '#phone-container',
        layout: 'bar',
        language: 'zh-CN',
        autoRegister: false,
        config: {
          sipNodeId: 'indonesia',
        },
      }).then((phone) => {
        phone.on('error', (error) => {
          console.error(error.code, error.message);
        });
        window.businessPhone = phone;
        return phone;
      }).catch((error) => {
        phonePromise = undefined;
        throw error;
      });
    }

    return phonePromise;
  }

  async function loginPhone(sipNodeId) {
    const response = await fetch('/api/sip-credentials', {
      credentials: 'same-origin',
      headers: { Accept: 'application/json' },
    });

    if (!response.ok) {
      throw new Error('无法取得电话凭据');
    }

    const phone = await preparePhone();
    await phone.configure({ sipNodeId });
    await phone.register(await response.json());
  }

  // 页面打开时预加载运行时,不会连接 WSS 或注册坐席。
  void preparePhone();

  // 在用户点击登录后调用:
  // await loginPhone('indonesia');
</script>

注册失败时可以保留并复用当前电话实例,修正凭据后再次调用 register(),不需要重复挂载 iframe。只有用户实际退出登录或页面永久卸载电话功能时才调用 destroy()。

直接在 mount() 中传入 credentials 仍然受支持,但 iframe 加载、Bridge 握手和 SIP 注册会连续发生,登录按钮的等待时间会更长。

切换悬浮拨号盘

将 layout 改成 floating:

const phone = await WebRTCPhoneHosted.HostedPhone.mount({
  container: '#phone-container',
  layout: 'floating',
  language: 'zh-CN',
  autoRegister: false,
  config: { sipNodeId: 'singapore' },
});

await phone.register(credentials);

布局支持:

  • bar:紧凑电话条,适合 CRM 顶部或工具栏;
  • floating:悬浮拨号盘,适合放在业务页面右下角。

语言支持 zh-CN 和 en。

已经挂载后可以直接切换布局,不会注销坐席或重建 SIP 连接:

await phone.setLayout('floating');
await phone.setLayout('bar');

4. 使用客户自己的界面

如果不需要内置电话条,可以创建无界面电话 Client:

const phone = await WebRTCPhoneHosted.HostedPhone.createClient({
  language: 'zh-CN',
  autoRegister: false,
  config: { sipNodeId: 'mexico' },
});

await phone.register(credentials);

phone.subscribe((snapshot) => {
  renderRegistration(snapshot.registration);
  renderCall(snapshot.call);
});

document.querySelector('#call').onclick = () => phone.call('1002');
document.querySelector('#hangup').onclick = () => phone.hangup();

5. 呼叫功能

内置电话条和无界面 Client 使用相同的业务 API。

呼出

await phone.call('81519140200');

也可以携带可选业务 SIP Header:

await phone.call('81519140200', {
  sipHeaders: {
    'X-Customer-Id': 'customer-10001',
    'X-Business-Id': 'order-20002',
    'X-Trace-Id': 'trace-30003',
  },
});

三个 Header 都不是必传参数,没有值时不要传。

来电、接听与拒接

phone.on('incomingcall', ({ call }) => {
  console.log('来电号码', call.remoteIdentity);
});

await phone.answer();
await phone.reject(486, 'Busy Here');

挂断、静音、保持和 DTMF

await phone.hangup();

await phone.setMuted(true);
await phone.setMuted(false);
await phone.toggleMuted();

await phone.setHeld(true);
await phone.setHeld(false);
await phone.toggleHeld();

await phone.sendDTMF('1');
await phone.sendDTMF('#');

静音、保持和 DTMF 应在通话接通后调用。

6. 状态监听

监听完整状态:

const stop = phone.subscribe((snapshot) => {
  console.log('注册状态', snapshot.registration);
  console.log('通话状态', snapshot.call.state);
  console.log('对端号码', snapshot.call.remoteIdentity);
  console.log('接通时间', snapshot.call.connectedAt);
});

// 不再监听时
stop();

也可以按事件监听:

phone.on('registrationchange', ({ registration }) => {
  console.log(registration);
});

phone.on('callchange', ({ call }) => {
  console.log(call.state);
});

phone.on('error', (error) => {
  console.error(error.code, error.message, error.recoverable);
});

注册状态:

unregistered → connecting → registering → registered

通话常用状态:

idle、dialing、ringing、incoming、connecting、connected、ending、ended、failed

7. 麦克风和扬声器

await phone.requestMicrophonePermission();

const devices = await phone.listAudioDevices(true);

if (devices.outputs[0]) {
  await phone.setAudioOutputDevice(devices.outputs[0].deviceId);
}

如果浏览器阻止远端声音,请在用户点击按钮时调用:

await phone.resumeAudio();

部分浏览器不支持选择扬声器,此时会继续使用系统默认设备。

8. 切换节点或账号

没有通话且未注册时,可以复用实例切换节点后注册:

await phone.configure({ sipNodeId: 'singapore' });
await phone.register(newCredentials);

已经注册时,请先结束当前通话,再注销并切换节点:

await phone.unregister();
await phone.configure({ sipNodeId: 'singapore' });
await phone.register(newCredentials);

9. 注销和页面退出

await phone.unregister();
await phone.destroy();

客户退出登录或业务页面永久关闭电话功能时,应调用 destroy() 释放资源。 unregister() 会注销当前 Hosted 电话的 SIP Contact,并等待服务端确认;必须 await 完成后再跳转页面。destroy() 同样会先完成该流程,再移除 Hosted iframe,不会注销同一坐席在其他设备上的注册。

10. 常用错误码

错误码 含义 建议处理
MICROPHONE_DENIED 麦克风没有权限 引导用户授权浏览器麦克风
REGISTRATION_FAILED 电话注册失败 检查账号后联系服务提供方
UNREGISTRATION_FAILED 服务端未确认注销 联系服务提供方核查注册状态
CALL_FAILED 呼叫失败 展示提示并允许重新拨打
MEDIA_PLAY_FAILED 浏览器没有播放声音 用户点击后调用 resumeAudio()
INVALID_STATE 当前状态不能执行操作 根据电话状态禁用对应按钮

客户业务提示应根据 code 映射,不要依赖英文 message。

11. 接入要求

  • 客户生产页面使用 HTTPS;
  • 页面允许加载和嵌入 https://indo.infin8linx.com;
  • 页面允许托管电话使用麦克风和自动播放;
  • 建议电话容器宽度不小于 280px;
  • Chrome、Edge、Firefox 和 Safari 使用仍在厂商维护期内的版本。

如果客户页面设置了严格的 CSP、Permissions-Policy,或者客户系统本身也运行在 iframe 中,请把相关响应头和页面部署方式提供给服务提供方协助确认。

12. 验收项目

服务提供方如需协同验收,统一使用交付目录中的 hosted-demo.html。用户先选择节点并输入坐席凭据,注册成功后进入统一 CRM 页面,可以在右上角坐席菜单中切换“电话条 / 悬浮”样式,并从电话界面或客户列表发起呼叫。切换布局不会重新注册;展开的拨号盘覆盖在业务内容上方,不会撑开 CRM 布局。测试凭据仅保存在当前页面内存中。

  • 电话成功注册;
  • 三个国家节点按实际业务需要完成验证;
  • 呼出、呼入、接听、拒接和挂断正常;
  • 双向语音正常;
  • 静音、保持、恢复和 DTMF 正常;
  • 通话状态和时长正常更新;
  • 麦克风拒绝、注册失败和呼叫失败有明确提示。