简介:一份面向移动端实时通信开发者的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。这个命令在调试完记得关掉,避免生产环境刷屏。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐