尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

PJSIP实现SIP通话:从初始化到转接的状态机实战解析

发布时间:2026/9/11 1:47:39

资讯中心
01
ARTICLE

PJSIP实现SIP通话:从初始化到转接的状态机实战解析

PJSIP实现SIP通话:从初始化到转接的状态机实战解析
简介针对PJSIP库的SIP语音通话DEMO项目面向VoIP应用开发者和通信协议学习者完整展示了基于PJSIP实现SIP呼叫的发起、接收以及呼叫转移和保持功能未包含会议与录音是一个便于入门和扩展的基础范例。压缩包大小约43.14MB总计383个文件其中Java源文件占绝大多数用于实现业务逻辑和界面交互XML配置管理服务器参数与界面布局SO动态库提供底层支持另有工程构建文件和示例数据方便在Android或Java环境中直接编译运行。核心代码涵盖PJSIP初始化、网络参数设置、SIP服务器注册、消息事件分发处理并实现了Hold/Unhold和REFER转接操作同时日志记录与错误处理机制也有参考价值。目前已有556人学习这份DEMO为开发者提供了可运行的SIP通话模型和清晰的代码路径尤其适合希望快速掌握SIP实战并基于PJSIP做二次开发的人群。1. 一个SIP通话DEMO为什么选PJSIP不选其他栈拿到sipdemo这个包的时候第一反应是它比我预想的小很多里面没有庞大的第三方依赖目录只有一堆.bin缓存文件和几个源码文件。这些.bin其实是构建过程中生成的中间产物真正有价值的是主程序源码、配置文件和一个Makefile。这个 DEMO 用 PJSIP 实现了 SIP 通话最核心的三个动作发起呼叫、接收呼叫、转接与保持。最让我意外的是它刻意砍掉了会议和录音——这反而让代码更值得读因为转接和保持涉及的 SIP 信令状态机比单纯打一通电话复杂得多。为什么选 PJSIP常见的选择还有 Sofia-SIP、eXosip但 PJSIP 在嵌入式和桌面端都保持了一致的 API 风格并且自带完整的媒体协商层。这个 DEMO 只需要语音通话PJSIP 的pjsua高层 API 几乎把信令和媒体封装成了“开箱即用”的状态机。对于想看明白 SIP 转接流程的开发者来说pjsua比手写 raw socket 解析 SIP 消息要省力得多但又没省到“啥也看不见”的抽象程度。下面直接从初始化讲起把每个关键参数的调试含义拆开。2. pjsua初始化与SIP注册账号配置的结构体和回调陷阱2.1 从pj_init到pjsua_start最小启动序列PJSIP 的pjsua层适合快速构建应用但初始化顺序非常敏感。DEMO 里通常按这个顺序调用#include pjsua-lib/pjsua.h static pjsua_acc_id acc_id; static void on_reg_state2(pjsua_acc_id aid, pjsua_reg_info *info) { if (info-cbparam-code 200 info-cbparam-code 300) { pjsua_call_make_call(aid, uri, NULL, NULL, NULL, NULL); } } int main() { pj_status_t status; status pjsua_create(); if (status ! PJ_SUCCESS) return -1; pjsua_config ua_cfg; pjsua_config_default(ua_cfg); ua_cfg.cb.on_reg_state2 on_reg_state2; ua_cfg.cb.on_incoming_call on_incoming_call; ua_cfg.cb.on_call_state on_call_state; ua_cfg.cb.on_call_media_state on_call_media_state; pjsua_logging_config log_cfg; pjsua_logging_config_default(log_cfg); log_cfg.console_level 4; // 打印 INFO 以上日志 pjsua_media_config media_cfg; pjsua_media_config_default(media_cfg); media_cfg.clock_rate 8000; // 与语音编码一致 status pjsua_init(ua_cfg, log_cfg, media_cfg); if (status ! PJ_SUCCESS) return -1; pjsua_transport_config tcfg; pjsua_transport_config_default(tcfg); tcfg.port 5060; status pjsua_transport_create(PJSIP_TRANSPORT_UDP, tcfg, NULL); if (status ! PJ_SUCCESS) return -1; status pjsua_start(); if (status ! PJ_SUCCESS) return -1; // 然后配置账号并注册 }pjsua_create()负责底层内存池和线程池的分配而pjsua_init()才会真正加载信令和媒体模块。这里有个常见误用有人把pjsua_acc_config的配置放在pjsua_init()之前但账号操作必须在pjsua_start()之后完成因为 SIP 栈此时才启动。pjsua_logging_config的console_level建议在调试期设为4生产环境降为2。DEMO 里的日志其实用的是PJ_LOG宏通过pjsua_logging_config全局控制不需要每个模块单独加 logger。2.2 注册账号pjsua_acc_config的关键字段与凭据注册到 SIP 服务器时pjsua_acc_config有几个字段会直接影响能否收到来电。我把 DEMO 里常用到的字段摘出来逐个说明含义。字段作用调试注意点id账号 AOR例如sip:1001voip.example.com必须与服务器配置一致否则注册会 403reg_uri注册服务器的地址形如sip:voip.example.com可携带端口:5060需为字符串格式cred_count与cred_info认证凭据包含用户名、密码、realmrealm 常被忽略服务器要求时必须填register设为PJ_TRUE才会发 REGISTER若只想收呼叫但不想注册可设为PJ_FALSEcontact_params附加到 Contact 头比如;transportudp多网卡时需正确选择 local IP示例配置pjsua_acc_config acc_cfg; pjsua_acc_config_default(acc_cfg); acc_cfg.id pj_str(sip:1001voip.example.com); acc_cfg.reg_uri pj_str(sip:voip.example.com:5060); acc_cfg.register PJ_TRUE; acc_cfg.cred_count 1; acc_cfg.cred_info[0].realm pj_str(voip.example.com); acc_cfg.cred_info[0].username pj_str(1001); acc_cfg.cred_info[0].data_type PJSIP_CRED_DATA_PLAIN_PASSWD; acc_cfg.cred_info[0].data pj_str(secret); acc_id pjsua_acc_add(acc_cfg, PJ_TRUE, NULL);realm这个字段最容易被忽略。本地测试用的 FreeSWITCH 或 Asterisk 通常会把 realm 设为服务器域名如果写成空白客户端会收到 401然后 pjsua 会根据data_type再次带凭据重试。DEMO 的配置文件sipserver.conf里如果只写了 IP没有 realm就会出现“能发 REGISTER 但永远注册不上”的现象。pjsua_acc_add的第二个参数PJ_TRUE表示立即注册。但注册是异步的回调on_reg_state2里才能拿到最终结果。DEMO 的源码里有一种偷懒做法在main里sleep(2)后再发起呼叫这在测试环境勉强可行生产环境不可取。正确做法是在on_reg_state2检查info-cbparam-code200 后再执行后续动作。2.3 回调事件把SIP状态机反射到应用层PJSIP 的pjsua_config里有多个回调DEMO 至少用到四个on_reg_state2、on_incoming_call、on_call_state、on_call_media_state。很多新手只处理来电忽略了on_call_state导致无法感知转接后的挂断事件。static void on_call_state(pjsua_call_id call_id, pjsip_event *e) { pjsua_call_info ci; pjsua_call_get_info(call_id, ci); printf(Call %d state: %s\n, call_id, ci.state_text.ptr); if (ci.state PJSIP_INV_STATE_DISCONNECTED) { // 清理会话相关的窗口句柄、音视频资源 } }on_call_state同样会收到PJSIP_INV_STATE_CONFIRMED表示媒体已连通。注意pjsua_call_info.state是pjsip_inv_state枚举state_text才是可读字符串。在回调里调用pjsua_call_get_info是线程安全的但不能在此回调中直接调用pjsua_call_hangup以外的阻塞操作否则容易造成信令线程死锁。常见的设计是把 UI 更新丢到独立消息队列里信令线程只改内部状态。3. 呼叫保持与转接UPDATE、REFER和会话计时器的配合3.1 保持通话send_reinvite与send_update两条路径呼叫保持的本质是把通话双方的媒体流暂时置为“ inactive ”但 SIP 会话本身不挂断。PJSIP 提供了pjsua_call_set_hold和pjsua_call_reinvite两个入口。pjsua_call_hold(call_id);内部实现是发送带ainactive或asendonly的 SDP 更新。对于持有方pjsua_call_set_hold会同时把本地媒体流暂停并发送 re-INVITE 给远端。对于被保持方远端会回复 200 OK 以及新的 SDP完成媒体方向协商。另一个路径是pjsua_call_update()它发送的是 UPDATE 请求而不是 re-INVITE。区别在于update 用于建立会话后且媒体尚未最终确认的状态而 re-INVITE 则可以在任何时刻修改会话。DEMO 里如果要快速展示保持用pjsua_call_set_hold最简单但你需要知道它默认走 re-INVITE。如果你的 SIP 服务器或远端设备不支持 re-INVITE少见但存在会卡在on_call_media_state的PJSUA_CALL_MEDIA_REMOTE_HOLD状态此时需要手动pjsua_call_update来兜底。方法SIP 消息适用场景风险pjsua_call_set_holdre-INVITE标准保持兼容性好部分防火墙会拦截 re-INVITEpjsua_call_updateUPDATE需要快速刷新 SDPRFC 3311 支持不完整时失效手动发送保持 SDPINVITE自定义媒体属性容易造成协商失败注意保持后本地用户可能听不到对方声音这是正常的。DEMO 的 UI 一般会显示“保持中”的状态但不会自动播放音乐。如果要放背景音乐需要在媒体流中内插一个 WAV 文件但这就属于录音/混音的范畴了DEMO 特地没有做。3.2 盲转与咨询转pjsua_call_xfer的完整调用链转接分两种盲转Blind Transfer和咨询转Attended Transfer。PJSIP 对两者的 API 不同但底层都依赖 REFER 请求。// 盲转直接把当前通话转给目标号码 pjsua_call_xfer(call_id, target_uri, NULL); // 咨询转先发起一个第二通电话给目标接通后再把第一通电话转过去 pjsua_call_id second_call; pjsua_call_make_call(acc_id, target_uri, NULL, NULL, NULL, second_call); // ... 等待 second_call 接通后 ... pjsua_call_xfer_replaces(call_id, PJSIP_REFER_SUBSCRIBE, second_call, NULL);pjsua_call_xfer的第一参数是当前通话第二个参数是目标 URI 字符串。它内部会构造一个REFER消息Refer-To头指向目标 URI并且自动订阅NOTIFY以获知转接状态。原通话的两端会收到 BYE然后on_call_state进入DISCONNECTED。这里有个踩坑点pjsua_call_xfer成功后原通话并不会立即断开。实际上 REFER 被目标接受后才发起新 INVITE。如果目标号码忙原通话还挂着只是状态变成“正在转接”。所以在 DEMO 的 UI 上必须给转接操作加上“等待回执”的中间状态不能直接关闭通话窗口。咨询转则使用pjsua_call_xfer_replaces它要求第二个通话已经建立而非振铃中。PJSIP_REFER_SUBSCRIBE表示希望在转接后收到事件通知。如果你不想订阅可以传PJSIP_REFER_NO_SUBSCRIBE但千万别乱传0这在旧版本 pjsua 里会导致 REFER 没有 From 标签而返回 400。3.3 不实现会议和录音降低了什么复杂度DEMO 最大的克制就是没有把会议和录音塞进来。会议需要pjsua_conf_connect桥接多个媒体流涉及混音器、采样率重采样、时钟同步。录音需要在媒体流转发的每一帧做旁路拷贝并写成 WAV 文件同时要处理挂断时的 flush。这两块会让核心信令代码占比缩小到一半以下不利于初学者快速抓住 SIP 状态机的骨架。省略这些功能后on_call_media_state的处理就非常轻只需要判断media_dir是否变成PJMEDIA_DIR_ENCODING或PJMEDIA_DIR_CAPTURE然后把默认的播放和捕获设备连接到底层媒体流。我一般会在on_call_media_state里加一行pjsua_conf_connect(ci.conf_slot, 0)和反方向的pjsua_conf_connect(0, ci.conf_slot)这是把本地麦克风和扬声器接到通话流的固定写法。static void on_call_media_state(pjsua_call_id call_id, pjsua_call_info *ci) { if (ci-media_status PJSUA_CALL_MEDIA_ACTIVE) { // 把本地声卡与通话桥接 pjsua_conf_connect(ci-conf_slot, 0); pjsua_conf_connect(0, ci-conf_slot); } }注意0号槽位是系统默认声卡设备ci-conf_slot是本次通话的媒体槽。如果你在保持状态下收到MEDIA_REMOTE_HOLD不要自动重连这两个连接否则对方会突然听到你的声音这属于媒体协商 bug。4. 用抓包验证REFER流程从Notify到Bye的边界检查4.1 用sngrep还原REFER会话写完了转接代码怎么确认它真的按 SIP 语义跑了我一般不用 pjsua 日志而是直接抓包看信令。sngrep是一个终端 UI 的 SIP 抓包工具能直接按 dialog 分组特别适合看 REFER 这种跨多个 call-id 的消息链。sudo apt install sngrep sudo sngrep -d eth0 -c -p 5060抓包后按下F7可以只筛选包含REFER的会话。一个正常的盲转流程应该看到时间源 - 目的消息T1手机A - 服务器INVITET2服务器 - 手机BINVITET3手机A - 服务器200 OKT4手机A - 服务器ACKT5手机A - 服务器REFER (Refer-To: 手机C)T6服务器 - 手机A202 AcceptedT7服务器 - 手机ANOTIFY (SIP/2.0 100 Trying)T8服务器 - 手机CINVITET9服务器 - 手机ANOTIFY (SIP/2.0 200 OK)T10手机A - 服务器BYET11服务器 - 手机BBYE重点关注T9中的 NOTIFY 消息体里的 SIP 状态码。如果转接成功状态码是200 OK如果目标忙你会看到486 Busy Here。此时原通话的 media 并没有立刻断开因为 PJSIP 会等待你的on_call_state处理DISCONNECTED事件然后才关闭媒体槽。4.2 转接失败的现场超时、486与REFER的NOTIFY状态调试转接时最常见的三种失败模式486 Busy Here目标在振铃时回复了忙。NOTIFY中携带486但原通话保持正常。你的应用需要提示“目标忙”并决定是否恢复通话。此时绝对不能再调用pjsua_call_hangup否则会断开原来的用户。408 Request Timeout目标无响应。原因是 REFER 的Expires头超时。PJSIP 默认用pjsua_acc_config的reg_timeout之类参数但 REFER 超时由pjsua_call_xfer的内部计时器控制。调大pjsua_config里的unstest参数没有用应该修改 SIP 事务层超时即pjsua_config的timer_interval和相关配置。NOTIFY 收不到如果目标服务器返回 200 OK 但神使鬼差地不生成 NOTIFY你的应用会一直卡在“转接中”。这是第三方 SBC 的常见问题PJSIP 的pjsua_call_xfer_replaces在订阅失败时会触发on_call_transfer_status回调需要在这个回调里设置一个应用层超时例如 10 秒内没收到最终状态就自动恢复通话。我一般会在on_call_transfer_status里加计数器统计e-body中携带的 SIP 状态码。static void on_call_transfer_status(pjsua_call_id call_id, int st_code, const pj_str_t *st_text, pjsip_generic_msg *resp, void *ret_data) { printf(Transfer status: %d %s\n, st_code, st_text-ptr); if (st_code 200) { // 最终状态更新 UI pjsua_call_setting_set_hold(pjsua_call_get_setting(call_id)); } }当st_code 200时REFER 的处理已经完成但此时BYE尚未必然发送。PJSIP 的文档里这个回调用于让你决定是否恢复通话或关闭窗口。如果st_code处于 100–199 之间表示还在进行中不要做任何销毁操作。最后提醒一个每次都会被问到的坑DEMO 包里的那些.bin文件不要试图用file命令去解析成某种结构它们只是 CMake 或自定义构建工具的增量缓存。真正常变的只有config文件和.c/.h源码改配置文件后必须重新编译否则 PJSIP 的pjsua_acc_config不会读取新的服务器地址。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。