1. 为什么 WebRTC 呼叫需要 Colibri 这道“翻译官”

如果你做过浏览器里打电话的功能,一定清楚原生 WebRTC 有三样东西是绕不开的:getUserMedia 拿麦克风摄像头、RTCPeerConnection 做媒体协商、再加上一套 ICE 流程打通网络路径。这三样东西能让你在浏览器里和另一个浏览器直接通话,但一旦你想让网页里的用户拨打传统电话分机、走 SIP 中继、或者对接 FreeSWITCH 上的会议桥和 IVR,事情就没那么简单了。

电话网络侧的设备不认 RTCPeerConnection,认的是 SIP 协议。SIP 属于信令层,负责邀请、应答、挂断这些会话控制;而媒体层走的是 RTP/RTCP,加密走 SRTP。浏览器里没有内建 SIP 栈,你必须自己写 JS 去拼 SIP 消息、解析 SDP、维护 Dialog 状态,这活儿不是不能干,但干起來极其容易踩坑,尤其是 SIP 的事务定时器、鉴权摘要、re-INVITE 更新这些细节,随便一个没处理对,电话就静音或者直接断了。

Colibri 就是干这个的。它的定位是一个 WebRTC 媒体网关,跑在浏览器和 FreeSWITCH 之间,把浏览器这边熟悉的 WebSocket JSON 信令“翻译”成电话网络需要的 SIP 信令,同时把 WebRTC 的 DTLS-SRTP 媒体转换成 FreeSWITCH 媒体引擎能处理的格式。听起来像个中间翻译官,但实际上它承担了信令协议转换、媒体加解密、编码转码、NAT 穿透这一整套脏活累活。

我第一次接触 Colibri 的时候,项目正处于一个尴尬期:网页端已经用 WebRTC 实现了点对点会议,但用户强烈要求能拨打公司内部分机,也要能从网页端呼出到 PSTN。那时候团队里几个人分别调研过直接嵌一个 JS SIP 栈的方案,试下来发现浏览器兼容性、音频设备回音、NAT 穿透三座大山压得人喘不过气。后来换用 Colibri 做网关,架构立刻清爽很多:浏览器只负责发 JSON 信令和收流,所有电信级细节全部下沉到网关侧。

这里顺带说一下适合谁看。如果你是做 WebRTC 通信、网页呼叫中心、或者是 FreeSWITCH/Asterisk 的运维开发,需要让浏览器和传统电话网络打通,那 Colibri 的这套思路和部署细节很值得收藏。就算你最终没有选它(比如直接用 SignalWire 的云服务),搞清楚它的机制对你理解和排查 WebRTC 网关类问题也大有帮助。

2. Colibri 的架构设计与核心机制拆解

2.1 信令层:JSON over WebSocket 换掉 SIP

Colibri 的信令设计有一个很明显的产品化考虑:降低浏览器端的接入门槛。传统 SIP 信令走 UDP/WS,消息格式是文本行的 Request-Line、Header 字段、SDP Body,浏览器端解析起来虽然也有现成库,但出了问题很难定位。Colibri 直接定义了基于 WebSocket 的 JSON-RPC 风格接口,浏览器只做两件事:连上 WebSocket,然后发 JSON 对象。

一套最简的呼叫流程大概是这样的:浏览器先连接 wss://网关地址:端口,然后发送 login 消息进行认证,拿到会话标识后发 call 消息,携带被叫号码、主叫标识、媒体参数。网关收到 call 后立即返回一条 ack,表示已经开始处理,后面所有状态变化——振铃、应答、挂断、对端忙——都会以事件消息主动推送到浏览器端。

这个设计的好处在于,浏览器端写代码变成一件非常“前端”的事情:收到什么事件就更新什么 UI,要发起什么动作就发什么 JSON。不需要关心 SIP 的 100 Trying、180 Ringing、200 OK 之间那些状态机流转,因为网关已经帮你把状态都归一化成简洁的业务事件了。对于前端工程师来说,这个心智负担比直接操作 SIP 栈低了一个量级。

2.2 媒体层:DTLS-SRTP 加解密与转码

信令只是壳,真正吃性能的在媒体层。浏览器和 Colibri 之间是 WebRTC 媒体通道,走 DTLS-SRTP:先用 DTLS 握手协商出密钥,然后用 SRTP 加密 RTP 流。而 Colibri 和 FreeSWITCH 之间走的是传统 RTP/SRTP 媒体。这中间至少有四个环节必须打通:ICE 候选收集与连通性检测、DTLS 证书与指纹协商、SRTP 密钥派生、RTP 封装格式转换。

编码转码这里要单独说。浏览器侧音频一般协商成 Opus,因为 WebRTC 规范要求所有浏览器必须支持 Opus;但 FreeSWITCH 对接传统网关或老式终端时,常用编码是 G.711(PCMU/PCMA)、G.722,甚至 iLBC。如果网关不做转码,两边 SDK 协商不到共同编码,呼叫就建立不起来。Colibri 具备媒体重协商能力,可以在网关侧把 Opus 转成 G.711,再把 G.711 的 RTP 包转发给 FreeSWITCH。音频转码是有 CPU 消耗的,好在新版 Colibri 对常用的 Opus<->G.711 路径做了大量优化,单台服务器跑几百路并发音频转码是有可能的。

视频路径类似,浏览器侧通常是 VP8/VP9/H.264 里协商一个,传统视频终端以 H.264 居多。Colibri 对视频默认走转封装或透传,如果编码一致就直接把 VP8 或 H.264 流搬过去,不做帧级重编码,这样性能压力会小很多。真正的视频转码(比如 VP8 转 H.264)需要额外的媒体引擎,Colibri 本身不主打这个,更推荐用户在服务端单独部署转码集群。

2.3 协议边界:Colibri 与 FreeSWITCH 如何协作

Colibri 并不是要替代 FreeSWITCH,它是站在 FreeSWITCH 前面的“接客层”。它在启动时会注册到 FreeSWITCH 的 ESL(Event Socket Library)上,通过 ESL 控制 FreeSWITCH 创建拨号方案、桥接呼叫、读取事件。换句话说,实际业务逻辑(IVR 流程、排队策略、计费话单)还是写在 FreeSWITCH 的 dialplan 里,Colibri 只负责把浏览器侧的爱恨情仇翻译成 FreeSWITCH 能听懂的命令。

这个分层很关键,它意味着你已有的大部分 FreeSWITCH 能力可以直接复用。比如语音邮箱、会议桥、队列坐席转接,都不需要重新开发,在 dialplan 里配好路由,Colibri 把呼叫送进 FreeSWITCH 后,一切照旧。我实际的感受是:有 FreeSWITCH 经验的人上手 Colibri 会非常快,因为排障路径是两段式的——先判断浏览器到 Colibri 的信令和媒体是否正常,再判断 Colibri 到 FreeSWITCH 的桥接是否收到。

部署形态上,Colibri 是独立进程,通过 wss 端口对外提供信令,通过媒体端口与浏览器交互 RTP。如果并发量高,可以独立部署在一台专用媒体服务器上,FreeSWITCH 在另一台,两端用内网高速链路桥接媒体。我早期测试时图省事全装在一台机器,当并发到 80 路以上时,CPU 中软中断明显升高,媒体路径和 FreeSWITCH 的音频处理互相抢资源。后来把 Colibri 单独拆出去,同配置下并发容量直接翻倍,这个教训很直接。

3. 实操落地:环境准备、编译安装与最小配置

3.1 编译 Colibri 的依赖清单与版本选型

Colibri 的官方仓库在 GitHub 上能找到,源码编译最大的坑是依赖版本不匹配。我建议在干净的 Debian/Ubuntu 服务器上操作,CentOS 7 上踩过 glib 版本过老导致编译失败的坑,不太推荐。

需要用到的依赖大致如下:

  • libuv:事件循环库,Colibri 的异步 IO 全靠它,版本建议 1.x 以上。
  • glib-2.0:GObject 基础库,很多核心对象基于它构建。
  • OpenSSL:DTLS 握手和 SRTP 密钥协商依赖,建议 1.1.1 以上,注意 LibreSSL 不一定兼容。
  • libsrtp:SRTP/RTP 加解密,建议使用 Cisco 维护的 libsrtp2。
  • opus:音频编解码,网关转发浏览器侧的 Opus 流必须有它。
  • sofia-sip:SIP 协议栈,用于 Colibri 与 FreeSWITCH 之间的 SIP 信令交互。
  • json-glib:JSON 解析,处理 WebSocket 信令消息时要用的。
apt-get install -y build-essential git automake autoconf libtool \
    pkg-config libuv1-dev libglib2.0-dev libssl-dev \
    libsrtp2-dev libopus-dev libsofia-sip-ua-dev libjson-glib-dev

版本选型上我的建议是:依赖尽量选发行版默认源里的版本,不要自己源码编译装新版,除非你只是测试。因为 Colibri 编译时对 API 变化比较敏感,源里的稳定版本至少是经过社区验证的组合。如果你用的发行版较老,标准源里的 libsrtp 版本很低,那我建议从 Cisco 官方仓库单独编译一份 libsrtp2,这个库更新频繁,老版本和较新的 DTLS 协商流程有时候会出兼容问题。

3.2 从源码编译的完整过程与常见编译报错处理

依赖装完之后,克隆源码开始编译。我实际操作时一般先跑 autogen.sh 生成 configure 脚本,然后 configure、make、make install 三步走。

git clone https://github.com/signalwire/colibri.git
cd colibri
./autogen.sh
./configure --prefix=/usr/local/colibri
make -j$(nproc)
make install

这里提醒三个编译期的常见问题。第一个是 configure: error: Package requirements (glib-2.0 >= 2.32) were not met ,说明 glib 版本太老,Debian 9 和旧版 Ubuntu 容易出现,升级源或者手动编译新版 glib 都能解决,但我建议直接换新发行版。第二个是编译到 sofia-sip 相关文件时链接报错,多半是 libsofia-sip-ua-dev 没装或者头文件路径不对,确认这个包名在你的系统上存在即可。第三个是 make 过程中报 undefined reference to srtp_... 系列错误,这是链接顺序问题,可以把 libsrtp 的 -lsrtp2 放到链接参数最后,或者检查 pkg-config 是否能找到 srtp2.pc 文件。

我见过不少人卡在编译阶段就放弃了,其实编译打包只需要一次,后面跑起来稳定运行一年半载很常见。如果你实在不想编译,也可以看发行版有没有现成包,但 Colibri 一直不温不火,官方包很少,自编译基本是唯一靠谱的选择。

3.3 最小可运行的配置文件与启动验证

编译安装完之后,需要准备一个最小配置文件。Colibri 的配置文件通常是一个 JSON 格式的文件,包含监听端口、认证用户、SIP 注册信息、FreeSWITCH ESL 连接参数。我常用的最小配置大概长这样:

{
    "listen": {
        "port": 8080,
        "ssl": {
            "enabled": true,
            "certfile": "/etc/colibri/cert.pem",
            "keyfile": "/etc/colibri/key.pem"
        }
    },
    "auth": {
        "users": [
            {"username": "webuser", "password": "webpass"}
        ]
    },
    "freeswitch": {
        "esl_host": "127.0.0.1",
        "esl_port": 8021,
        "esl_password": "ClueCon",
        "dialplan": "XML"
    },
    "sip": {
        "interface": "0.0.0.0",
        "port": 5066,
        "transport": "udp"
    },
    "codecs": ["opus", "PCMU", "PCMA", "G722"]
}

注意 WebSocket 需要 TLS 证书,也就是 wss 协议。证书可以自己用 openssl 生成自签名,浏览器需要加信任才能连,生产环境建议上正规证书。certfile 和 keyfile 路径要写绝对路径,权限建议设置成 600,因为私钥泄露意味着任何人都能冒充你的网关。

启动服务后,先做几个基础验证。第一,监听端口有没有起来: ss -lntp | grep 8080 ;第二,进程日志有没有报错,比如证书加载失败、ESL 连接被拒绝;第三,用 curl 或浏览器试一下 wss 端口能不能通。我通常先做一步更基础的自检: netstat -luap | grep 5066 ,确保 SIP 端口也监听正常,不过有时候你并不需要 Colibri 对外暴露 SIP 端口,具体看业务需求。确认服务起来之后,再用 WebSocket 客户端连上去发一条 login,能收到成功响应就说明链路基本建立。

4. 浏览器端联调:WebSocket API 关键消息结构和事件流

4.1 连接、登录与会话保持

浏览器端核心代码很简单,直接用原生 WebSocket 就行,不需要额外依赖。连接成功后先发 login,认证通过后才能发呼叫类命令。我一般会封装一个简单的收发文件,方便后续扩展。

const ws = new WebSocket('wss://your-colibri-host:8080');

ws.onopen = () => {
    ws.send(JSON.stringify({
        jsonrpc: '2.0',
        method: 'login',
        params: { username: 'webuser', password: 'webpass' },
        id: 1
    }));
};

ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    console.log('收到网关消息:', msg);
    // 根据 msg.method / msg.result / msg.error 分发处理
};

登录成功后会返回一个 session_id 或类似标识,后续所有请求都要带上这个 session_id,不然网关分不清你是在哪条会话里发起操作。这个设计很像 WebSocket 长连接上的多路复用,每条业务呼叫一个独立会话,但底层共享同一条连接。注意心跳保活,浏览器端的 WebSocket 闲置一段时间后可能被中间代理或网关侧的超时机制踢掉,建议每隔 30 秒左右发一个 ping 类的心跳消息。

4.2 发起呼叫:参数细节与媒体协商触发

登录成功之后,发一条 call 指令发起呼叫。网关收到后,会自行处理与 FreeSWITCH 之间的 SIP 流程,并给你返回一个 call_id,后续挂断、查询状态都靠这个 call_id。

// 在收到 login 成功的响应之后再执行
ws.send(JSON.stringify({
    jsonrpc: '2.0',
    method: 'call',
    params: {
        session_id: sessionId,
        destination: '1000',
        caller_id_name: '网页访客',
        caller_id_number: '8888',
        media: {
            audio: true,
            video: false
        }
    },
    id: 2
}));

这里有几个参数值得展开一下。destination 是一个分机号或 SIP 地址,最终由 FreeSWITCH 的 dialplan 解析;caller_id_name 和 caller_id_number 是主叫显示,注意 PSTN 对接时中文字符可能出现编码问题,稳妥起见用 ASCII;media 参数可以控制是否启用视频流,音频业务把 video 设为 false 可以显著降低协商难度和带宽占用。注意,Colibri 收到 call 请求后,不会立刻给你一个 200 表示对方已经接听,而是先把呼叫送进 FreeSWITCH,然后通过后续事件告诉你状态变化。

这里有个很重要的实操体会:拨号之前,浏览器要把 getUserMedia 的授权提前拿到,最好在用户点击“拨号”之前就完成。因为如果网关返回的 SDP offer 已经到了,浏览器却没有麦克风权限,媒体协商会因为缺少可用的音轨而失败,转为静音或直接失败。代码里这样做:

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 拿到 stream 后再发 call 指令

4.3 事件消息驱动:振铃、接听、挂断的完整状态流转

呼叫进入 FreeSWITCH 后,网关会推送一系列事件给浏览器。这里给出一个典型的事件序列:

阶段 事件类型 说明
呼叫已受理 call_accepted 网关收到 call 请求并开始处理
SIP 已发送 sip_invite_sent 网关已向 FreeSWITCH 发出 INVITE
对方振铃 call_ringing 被叫终端开始振铃
对方接听 call_answered 媒体通道即将双向打通
双方挂断 call_ended 呼叫结束,附带挂断原因码

前端收到 call_answered 之前,不要播放“通话中”的音效,也不要开启计时器,因为对方可能拒接。收到 call_ended 事件后,需要释放本地媒体流:

ws.send(JSON.stringify({
    jsonrpc: '2.0',
    method: 'hangup',
    params: {
        session_id: sessionId,
        call_id: callId,
        reason: 'normal_clearing'
    },
    id: 3
}));

同时把本地 stream.getTracks().forEach(track => track.stop()) 关掉麦克风。这个细节我早期经常忘记,导致通话结束后浏览器页面上麦克风指示灯还亮着,用户以为还在被监听,体验非常糟糕。

5. 常见问题与排查技巧实录

5.1 音频不通,视频正常

如果呼叫已经建立,但听不到对方声音,先判断方向:是听不到远端声音,还是远端听不到你。浏览器侧打开 chrome://webrtc-internals ,查看 outbound-rtp 和 inbound-rtp 的包计数。如果 outbound 正常但 inbound 为 0,多半是网关到 FreeSWITCH 的 RTP 回程断了;如果 outbound 也是 0,说明麦克风或本地编码链路出问题。

另一个隐藏很深的坑是回音消除。浏览器端的 Opus 默认开 AEC,但 Colibri 网关侧转码后再送给 FreeSWITCH,链路里可能引入额外时延,回音消除器反而误判。我遇到过一次案件:用户手机连蓝牙耳机,网页端通话总是听到自己的回声,后来在网关配置里调整了音频缓冲区大小和回声消除参数,问题才消失。这类问题没法一次定位,建议从浏览器、网关、FreeSWITCH 三个层面分别查看音频统计,逐段定位。

5.2 DTLS 握手失败

浏览器与 Colibri 之间的媒体协商是 DTLS-SRTP,如果握手失败,典型表现是双方都显示已接听,但媒体完全不通。排查时先看 Colibri 日志里有没有 dtls handshake failed 相关记录,再检查证书链是否完整。自签名证书在部分浏览器上虽然 WebSocket 能连,但 DTLS 层的本地指纹核对仍然可能因为证书类型问题出错,建议在网关配置里指定一个 P-256 椭圆曲线密钥对的证书,兼容性最好。

我之前排查过一个诡异问题:同一套配置,Chrome 上通话正常,Safari 上 DTLS 一直握手失败。后来发现是 Safari 对 DTLS 1.0 的降级策略更严格,而网关侧 OpenSSL 版本较老,协商时只支持到 DTLS 1.0。升级 OpenSSL 到 1.1.1 以上,启用 DTLS 1.2 之后问题自然消失。如果你的 Colibri 环境也碰到类似浏览器兼容性怪问题,优先检查 TLS/DTLS 版本这条线。

5.3 NAT 穿透下的 ICE 失败

WebRTC 的 ICE 机制是为了穿透 NAT,但真实场景里总有运气不好的网络。浏览器侧先后发起 host 候选(内网地址)、srflx 候选(映射后的公网地址)、relay 候选(经过 TURN 服务器),Colibri 侧也有类似候选。如果双方只有一个 host 候选且处于不同内网,ICE 一定失败。

生产环境里,我会在 Colibri 前面部署 coturn 作为 TURN 中继,配置好 UDP/TCP 监听端口,并在浏览器端 pc 配置里填上 TURN 地址。注意 TURN 认证和防火墙端口要一起配好。另外,Colibri 所在服务器的防火墙要放行 UDP 大范围端口(比如 10000-20000),因为 RTP 媒体端口是动态分配的,只放行 SIP 端口是不够的。排查 ICE 失败时,去 chrome://webrtc-internals 看 ICE candidate pair 里有没有选到 relay 类型,如果只有 host 和 srflx,则说明 TURN 配置没生效。

5.4 信令连接频繁断开与重连风暴

WebSocket 连接断开有很多原因:后台代理超时、服务器端空闲超时、浏览器节能模式冻结计时器。处理思路是给心跳消息设计超时重连机制,并且要带指数退避,避免所有客户端同时重连造成网关雪崩。

我遇到过最典型的一次事故:前端代码里重连逻辑写得太激进,网络抖一下,几十个用户同时重连,网关瞬间收到大量 login 请求,部分连接被限流拒绝。后来我把重连间隔改成 1 秒、2 秒、4 秒、8 秒的退避序列,加上随机抖动,问题就再没出现过。另外,断线后本地要清掉正在通话中的 UI 状态,避免用户看到“通话还在进行”但实际媒体已经断了。

关于故障排查,我始终觉得先定边界再定细节最有效。画一条链路出来:麦克风 -> 浏览器 WebRTC -> Colibri -> FreeSWITCH -> 对端终端。哪一段有问题就先看哪段的日志和统计。WebRTC 类问题最忌讳直接在 UI 层找原因,你看着是“没有声音”,实际可能已经跨了五个设备,链路分析是唯一可靠的方法。

6. 我对 Colibri 这项技术选型的最终感受

做 WebRTC 网关这条路,Colibri 并不是唯一选择,但它给我最大的启发是“保持边界清晰”。浏览器端只做信令交互和媒体收发,所有电信级状态流转全部下沉到网关和服务端。这个思想在我后来的多个项目里一直沿用,包括后来一些自研网关的设计,核心分层都参考了这一套。

如果你要做轻量化的网页软电话、需要快速接入现有 FreeSWITCH 环境、或者想在邮件里加一个“点击拨号”的小功能,Colibri 的思路非常值得试。哪怕不上生产环境,仅作为内部调试工具,比如通过它分析 SDP 协商过程、研究 DTLS-SRTP 报文的交互细节,都是一个很不错的活体教材。

最后分享一个我在一次生产事故里得到的教训:任何网关类服务,上线前一定要压测 NAT 穿透和断线重连两个场景,不要只测局域网环境。因为 WebRTC 的大多数“线上异常”本质都是网络路径复杂度导致的,Colibri 再稳定,也架不住入口网络环境频繁变化。先在测试环境模拟三个不同运营商网络下的呼叫、挂断、重连,把所有边界情况都捋顺了,再上生产,你会省掉后面一整周的排障痛苦。

Logo

邀请您加入社区

更多推荐