基于SIPDROID源码的小程序音视频通话实现与优化
简介:一份面向移动端实时通信开发者的Sipdroid语音及视频通话小程序源码包。项目以开源SIP客户端Sipdroid为核心,演示了如何使用SIP协议完成VoIP呼叫的注册、发起、接听与挂断,并集成WebRTC等音视频处理能力,覆盖Android层与小程序前端交互。压缩包共1614个文件,其中包含250个Java源码、183个C文件和149个H头文件,涉及Silk音频编解码、通话控制逻辑等底层实现;另有139个PNG图片用于界面绘制,63个XML配置描述页面布局与权限声明,以及少量CPP、Makefile等构建辅助文件,整体大小为3.84MB。目录中还保留约804个svn-base文件,便于追溯项目版本变更。资源目前已有163人学习下载。通过阅读这份源码,开发者可以理清Sipdroid的呼叫状态机、音视频流传输通道及小程序端通信封装方式,对于学习SIP协议、VoIP通话或搭建类似实时音视频应用具有直接参考价值。
1. 为什么一个 Android SIP 客户端源码会成为小程序音视频项目的核心参考
拿到这个叫做“小程序源码 sipdroid语音及视频通话.rar”的压缩包,第一眼全是 SKP_Silk_tables_NLSF_CB0_16.c、nb_celp.c 这类 Native 层文件,跟微信小程序看似不搭界。但实际拆过 VoIP 项目的人清楚,SIPDROID 最有价值的不是那套 Android UI,而是它把 SIP 注册、呼叫建立、媒体协商、编解码切换这些最容易做乱的环节,用一套被验证过的状态机串了起来。小程序端不能直接跑标准 SIP 协议,但信令流程完全可以照搬,媒体通道换成 WebRTC 就能在微信环境里实现通话。这个资源包恰好提供了最底层的语音编码参考,把这个思路搬进微信小程序源码,至少能省掉两周协议联调的时间。适合想自己实现音视频通话小程序、又不想从零摸索的开发者。
2. SIP协议与SIPDROID的呼叫状态机拆解
2.1 SIP消息流程:从REGISTER到BYE的状态转移
SIP 是应用层控制协议,传输层的 UDP 或 TCP 只负责搬运文本消息。一个最简单的注册加呼叫流程如下:客户端先发 REGISTER,服务器返回 401 质询,客户端带认证信息再发一次,得到 200 OK 后注册成功;呼叫时发 INVITE(携带 SDP),被叫振铃回 180,接听后回 200 OK,主叫再发 ACK,进入通话;结束后任一方发 BYE。
| 消息方向 | 方法 | 语义 | SDP所在 |
|---|---|---|---|
| 客户端→服务器 | REGISTER | 注册或刷新 | 无 |
| 服务器→客户端 | 401/200 | 认证质询/成功 | 无 |
| 主叫→被叫 | INVITE | 发起会话 | 是 |
| 被叫→主叫 | 180 Ringing | 振铃 | 无 |
| 被叫→主叫 | 200 OK | 接听 | 是 |
| 主叫→被叫 | ACK | 确认 | 无 |
| 任意方向 | BYE | 终止 | 无 |
注意 INVITE 和 200 OK 的 SDP 里携带了媒体格式、IP、端口,这是 SIP 与 HTTP 最大的不同。SIPDROID 的 SipStack 内部把每个事务拆成 ClientTransaction 和 ServerTransaction,分别管理状态码和定时器。例如 INVITE 事务的 Timer B 是 64 秒,超过就判定超时并回 408。小程序端这套 timer 同样需要实现,但建议把 INVITE 超时缩短到 10 秒,因为用户等不了那么久。
2.2 SIPDROID源码中的语音编码模块:SILK到底在干嘛
压缩包里的 SKP_Silk_tables_NLSF_CB0_16.c 和 SKP_Silk_NSQ_del_dec.c 属于 SILK 音频编码器。SILK 是 Skype 早期开源给 SIPDROID 使用的语音编码,后来与 CELT 合并成 Opus。它在 8kHz 采样、24kbps 码率下能够保持很高的可懂度,并且内部实现了丢包隐藏(PLC)和信道自适应。
具体文件分工:nb_celp.c 是窄带 CELP 核心的残差编码入口,SKP_Silk_NSQ_del_dec.c 是带延迟决策的噪声整形量化器,SKP_Silk_tables_NLSF_CB0_16.c 存放 NLSF 参数的码本表,用于高效压缩线谱频率。这一整套定点数实现是为了在嵌入式设备上不依赖浮点运算。小程序端没有条件编译这些 C 文件,但如果你在服务器侧保留了 SIPDROID 网关,这些编码器仍然是可用的。
SILK 的码率自适应算法值得借鉴:它不通过降低采样率来省流量,而是减少帧内比特分配,优先保住 300-3400Hz 的语音频段。这在小程序弱网语音优化中是很有效的思路。
2.3 将SIPDROID状态机映射到小程序逻辑层
SIPDROID 的 Java 层维护了一个 SipSession 状态机,状态包括 READY、CALLING、INCOMING、IN_CALL、END 等。每次收发 SIP 消息都会触发状态迁移。小程序里可以建一个同样的状态对象:
const CALL_STATE = {
IDLE: 0,
REGISTERING: 1,
REGISTERED: 2,
CALLING: 3,
RINGING: 4,
IN_CALL: 5,
ENDING: 6
};
class CallSession {
constructor() {
this.state = CALL_STATE.IDLE;
this.timers = {};
}
transition(nextState, reason) {
console.log(`[SIP] ${this.state} -> ${nextState} (${reason})`);
clearTimeout(this.timers[nextState]);
this.state = nextState;
this.onStateChange && this.onStateChange(this.state, reason);
}
}
这里 transition 是所有状态变更的唯一入口,日志格式统一,排查时可以直接看输出路径判断是哪一步出了问题。onStateChange 回调里做对应 UI 更新,比如 CALLING 状态显示拨号动画,RINGING 状态显示“等待接听”。timers 对象存放每个状态下的超时定时器,避免状态卡死不超时。
2.4 注册认证:Digest Auth在WebSocket里的处理
SIP 的注册认证是 HTTP Digest 的变体。服务器 401 响应的 WWW-Authenticate 头里带 realm 和 nonce,客户端需要计算 response。计算规则如下:
const md5 = require('crypto-js/md5');
function digestResponse(username, password, realm, nonce, uri, method) {
const ha1 = md5(`${username}:${realm}:${password}`).toString();
const ha2 = md5(`${method}:${uri}`).toString();
return md5(`${ha1}:${nonce}:${ha2}`).toString();
}
参数说明:uri 是请求行中的 Request-URI,比如 sip:1001@sip.example.com ;method 是 REGISTER 或 INVITE 。计算时注意 HA1 里的冒号是 ASCII 冒号,不要有多余空格。很多开发者在 Android 上用 DigestUtils 没问题,但移植到小程序后字符串编码不同,经常在非 ASCII 账号名上出错。
3. 在小程序端复刻SIPDROID的媒体协商与呼叫控制
3.1 信令通道设计:WebSocket承载SIP
小程序没有可用的标准 UDP Socket 来跑 SIP 协议,最直接的方式是把 SIP 消息封装进 WebSocket 帧,走 RFC 7118 的 SIP over WebSocket 标准。Asterisk 从 13 版本开始支持 chan_pjsip 的 WebSocket 传输,监听端口默认 8088,子协议为 sip 。如果用云函数做转发,常见做法是 Node.js 服务同时连接小程序端和 Asterisk:
const WebSocket = require('ws');
const sipWs = new WebSocket('ws://192.168.1.10:8088/ws', 'sip');
const appWs = require('./app-ws');
appWs.on('message', (msg) => sipWs.send(msg));
sipWs.on('message', (msg) => appWs.send(msg.toString()));
说明:sipWs 连接 Asterisk 时要带上子协议 sip ,否则 pjproject 会认为不是 SIP 连接从而直接断开。appWs 是小程序与云函数之间的 WebSocket 连接,这里省略了具体建立逻辑,但要注意两端的帧格式都是文本帧,不要发送二进制数据。
如果使用 FreeSWITCH,可以考虑 mod_verto 的 WebSocket 支持。开发阶段建议用 Asterisk,因为配置简单,日志清晰。
3.2 媒体流处理:WebRTC替代RTP/RTCP
SIPDROID 在 Android 上通过 AudioRecord 采集 PCM,SILK 编码后经 RTP 发送。小程序端无法直接使用 RTP/RTCP 栈,因为 ICE、DTLS、拥塞控制都需要底层系统支持,好在 WebRTC 已经封装了这些能力:采集、编码、FEC、NACK、拥塞控制全部由浏览器内核处理。实现视频通话时,使用微信的实时音视频组件或 WebView 里的 RTCPeerConnection。
需要注意 SDP 的差异。SILK 的 SDP 里使用 audio/SILK 和 a=fmtp 参数,而 WebRTC 生成的是 audio/opus 。因此服务器侧需要一个媒体网关做 SDP 转译,常见做法是用 Asterisk 的 res_pjsip 模块,把 WebRTC 的 MediaStream 映射到传统 RTP 流。或者选择支持 WebRTC 的 FreeSWITCH 版本,用 mod_verto 桥接。
3.3 代码示例:小程序端发起SIP INVITE请求
这里的拨打操作需要先建立 WebSocket,然后发送 INVITE 消息:
function sendInvite(ws, callee, sdp) {
const callId = `call-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const msg = [
`INVITE sip:${callee}@sip.example.com SIP/2.0`,
`Via: SIP/2.0/WSS ${myAddress};branch=z9hG4bK-${Date.now()}`,
'Max-Forwards: 70',
'From: <sip:1001@sip.example.com>;tag=tag-1001',
`To: <sip:${callee}@sip.example.com>`,
`Call-ID: ${callId}`,
'CSeq: 1 INVITE',
'Contact: <sip:1001@wx-client>',
'Content-Type: application/sdp',
`Content-Length: ${sdp.length}`, '',
sdp
].join('\r\n');
ws.send(msg);
return callId;
}
参数说明:Call-ID 必须全局唯一,同一会话的所有请求共享同一个 Call-ID,所以返回值要保存下来,后续的 CSeq 递增也依赖这个标识。Via 的 branch 参数用于事务匹配,每次新请求都要换分支,Asterisk 用它在响应时定位事务。Content-Length 是 SDP 文本的字节数,如果这里算错,SIP 解析器会粘包,常见错误是把 sdp.length 当作字符数而忽略中文字符,SDP 里最好保持 ASCII。
3.4 参数配置:SIP服务器地址、认证方式、编解码协商
小程序端连接参数建议按下表设置:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 信令协议 | WSS | WebSocket Secure,小程序要求 HTTPS/WSS |
| 服务器地址 | wss://sip.example.com:8088/ws | 需在微信公众平台配置合法域名 |
| 注册有效期 | 300 秒 | 短于服务器端 600 秒的过期时间,避免掉线 |
| 音频编码 | Opus 16000Hz | 带宽占用约 32kbps,丢包恢复能力比 SILK 好 |
| 视频编码 | H.264 | 小程序端硬件编码支持度最好 |
| STUN/TURN | 必须配置 TURN | 小程序网络受限严重,TURN 可兜底 |
这里重点说域名配置:小程序的 Socket 连接只能使用 HTTPS/WSS,且需要在微信公众平台后台配置 socket 合法域名。开发阶段可以勾选“不校验合法域名”,但体验版和正式版必须配置,否则连接直接失败。
4. 音频编码优化:从SILK到WebRTC Opus的参数对照
4.1 SILK编码器在SIPDROID中的角色
SIPDROID 的目录里出现 nb_celp.c、SKP_Silk_NSQ_del_dec.c 这些文件,意味着它使用了完整的 SILK 定点编码器。其中,SKP_Silk_NSQ_del_dec.c 负责将线性预测残差转成量化索引,这个模块的延迟决策特性决定了 SILK 在低码率下的语音自然度。SKP_Silk_tables_NLSF_CB0_16.c 和 SKP_Silk_tables_NLSF_CB0_10.c 分别是 16 维和 10 维的 NLSF 码本,SILK 根据当前采样率选择 10 维(窄带)还是 16 维(宽带)。
这些 C 文件对小程序开发最大的启发有两个:一是码本量化可以大幅压缩语音特征,二是编码器内部会保存前一帧的状态用来做丢包隐藏。在 WebRTC 的 Opus 里,这两个机制已经内置,你不需要自己实现,只需要正确配置参数。
4.2 Opus参数与SILK对照表
| 参数项 | SILK(SIPDROID默认) | Opus(WebRTC默认) | 影响 |
|---|---|---|---|
| 采样率 | 8kHz | 16kHz | 16kHz 语音频带更宽,自然度更好 |
| 码率 | 24kbps | 32kbps | 高码率抗丢包更强,但占用上行带宽 |
| 帧长 | 20ms | 20ms | 帧长越短延迟越低,但丢包敏感度越高 |
| 复杂度 | 3 | 5(可调) | 复杂度越高 CPU 占用越大,小程序端选 3 即可 |
| 丢包隐藏 | PLC | PLC+NACK | Opus 在 20% 丢包下仍可懂 |
SIPDROID 的默认复杂度较低,因为手机处理器资源有限。小程序端运行在微信宿主进程内,CPU 上限受限,建议在创建 AudioContext 时把 Opus 的 complexity 显式设置为 3,可以降低发热,减少通话中断概率。
4.3 小程序端音视频参数设置代码
以微信小程序 live-pusher 为例,以下是通话场景推荐配置:
{
"mode": "RTC",
"enable-camera": false,
"enable-mic": true,
"min-bitrate": 200,
"max-bitrate": 800,
"beauty": 0,
"aspect": "3:4",
"orientation": "portrait"
}
mode 字段必须为 RTC,如果误配置为 HD 或 SD,会走直播链路,延迟会达到 2-3 秒,无法用于通话。min-bitrate 和 max-bitrate 的单位是 kbps,语音场景下 200-400 就足够,视频场景需要 800-1500。beauty 设为 0 是关闭美颜,因为美颜会额外占用 GPU 和 CPU,在通话中反而导致画面掉帧。
如果使用 WebView 内嵌 WebRTC,还需要设置 RTCPeerConnection 的编码码率:
const pc = new RTCPeerConnection({ iceServers: [{ urls: 'turn:turn.example.com' }] });
const sender = pc.addTrack(stream.getAudioTracks()[0]);
const params = sender.getParameters();
params.encodings[0].maxBitrate = 64000;
sender.setParameters(params);
这里把音频编码码率限制在 64kbps,避免弱网下码率向上膨胀导致拥塞。注意,这个限制需要在音轨添加之后、协商之前设置,如果已经建立连接, setParameters 只影响后续编码,不会重新协商。
4.4 网络抖动与丢包处理
实测发现,小程序退到后台超过 5 秒,Wi-Fi 切 4G 时 WebSocket 连接往往已经死掉,但 WebRTC 的底层连接还可能维持几十秒。建议监听 wx.onNetworkStatusChange ,网络类型变化时主动重建 peer connection,否则会出现长时间 ICE 连接失败。
播放端的 jitter buffer 也要控制。使用 live-player 时,可以设置缓存范围:
this.ctx = wx.createLivePlayerContext('player');
this.ctx.play({
mode: 'RTC',
minCache: 200,
maxCache: 1000
});
参数说明:minCache 和 maxCache 单位是毫秒。minCache 设小会让缓冲快速收敛到 200ms,适合通话;maxCache 设大可以应对突发抖动,但会提高延迟。SIPDROID 的 jitter buffer 自适应算法也是类似的上下界设计,参考它的初始值可以少做几轮实测。
5. 实战验证:用Asterisk搭建服务器并调试小程序呼叫
5.1 Asterisk WebSocket配置要点
Asterisk 使用 chan_pjsip 时需要开启 WebSocket 传输。在 pjsip.conf 中添加:
[transport-ws]
type=transport
protocol=ws
bind=0.0.0.0:8088
然后设置 endpoint 的 transport 指向该传输:
[1001]
type=endpoint
context=public
allow=!all,ulaw,opus
transport=transport-ws
注意 allow 的顺序, !all 表示先禁止所有,再放行 ulaw 和 opus。如果上游是 WebRTC,建议只允许 opus,否则 Asterisk 可能优先选择 PCMU,触发不必要的转码导致音质下降。
5.2 信令抓包与注册验证
启动 Asterisk 后,执行:
asterisk -rx "pjsip show endpoints"
如果能看到 endpoint 有 Contact 地址,说明 WebSocket 注册已成功。若注册失败,使用 asterisk -rx "pjsip set logger on" 打开信令日志,日志中会打印每条 SIP 消息的原始内容,直接定位到 401、ACL 或 contact 绑定问题。
真机调式时,在微信开发者工具里打开 vConsole 的 network 面板查看 WebSocket 帧,确认 REGISTER 请求是否到达 Asterisk。注意开发者工具里 WebSocket 是通过浏览器机制实现的,和真机行为有差异,务必使用真机。
5.3 常见失败场景与参数调整
- 注册 401 循环:检查 digestResponse 里的 uri 是否和 REGISTER 请求行完全一致,realm 是否区分大小写。
- INVITE 后无回铃音:查看 Asterisk 的 dialplan 是否在拨号计划中配置了
Progress()应用,没有回铃音会导致小程序端一直停在 CALLING 状态。 - 媒体单向不通:小程序端必须配置 TURN,并且 TURN 服务器要开启 TCP 监听;Asterisk 的 rtp.conf 中要设置 rtpstart 和 rtpend 范围,保证媒体端口可用。
如果媒体只有上行没有下行,多半是 TURN 的 relay 地址没有正确上报。可以用 asterisk -rx "rtp set debug on" 查看 RTP 流方向,再对照 TURN 日志定位是哪一侧没有完成 relay。这个命令在调试完记得关掉,避免生产环境刷屏。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)