pjsip呼叫控制逻辑设计:拨号、接听、挂断完整示例

pjsip呼叫控制实战:从拨号到挂断的完整逻辑拆解

你有没有遇到过这样的场景?
在开发一款软电话应用时,点击“拨打”按钮后,对方没反应;或者来电了却无法正确弹出提示;最头疼的是通话中突然断开,日志里一堆SIP消息来回穿梭,却搞不清到底是哪一步出了问题。

如果你正在用pjsip做 VoIP 开发,那你一定不陌生这种“信令迷宫”的感觉。别急——今天我们不讲理论堆砌,也不罗列API文档,而是带你亲手走一遍真实的呼叫流程,把拨号、接听、挂断这三个核心动作,像搭积木一样一块块拼起来。

我们不跳步骤,不省略细节,目标只有一个:让你下次再看到INVITEBYE的时候,心里有底,代码有谱。


一个电话是怎么“打出去”的?

先来问个看似简单的问题:当你调用pjsua_call_make_call()的那一刻,到底发生了什么?

很多人以为这只是发了个网络请求。但其实,这背后是一整套状态机驱动的协议交互过程。我们从代码出发,一步步往下挖。

pj_status_t make_outgoing_call(const char *dst_uri) { pj_str_t uri = pj_str((char*)dst_uri); pjsua_call_setting cfg; pjsua_call_setting_default(&cfg); cfg.aud_cnt = 1; cfg.vid_cnt = 0; return pjsua_call_make_call( current_account_id, &uri, &cfg, NULL, NULL, NULL ); }

这段代码很短,但它触发的是整个 SIP 会话的起点。我们可以把它理解为按下拨号键后的“发令枪”。

第一步:准备SDP媒体协商内容

在发送 INVITE 之前,pjsip 会自动生成一份本地 SDP(Session Description Protocol),描述你能支持哪些音频编码(比如 G.711、OPUS)、RTP 端口是多少、是否启用RTCP等。这份SDP会被塞进 INVITE 消息体中。

💡 小知识:即使你不写一行SDP代码,pjsip也会默认帮你生成合理的初始媒体能力描述,这就是高级封装的好处。

第二步:构造并发送 INVITE 请求

INVITE 不是普通的 HTTP POST,它是一个完整的 SIP 请求,包含多个关键头域:

  • Via: 标记路由路径和传输协议(UDP/TCP)
  • From/To: 主叫与被叫身份
  • Call-ID: 全局唯一标识本次会话
  • CSeq: 命令序列号,防止重放
  • Contact: 回应地址,用于后续媒体建立
  • Authorization: 如果需要认证,这里带上凭证

这些字段 pjsip 都会自动填充,前提是你的账户已经成功注册(pjsua_acc_add()成功)。

第三步:进入事务等待状态

一旦 INVITE 发出,pjsip 内部就启动了一个客户端事务(Client Transaction),开始监听响应。此时,当前呼叫的状态变为:

PJSUA_CALL_STATE_CALLING

这个状态很重要——它意味着“我已经出手,正在等对方回应”。UI 上通常显示为“正在呼叫…”。

接下来会发生什么?取决于对方怎么回。


来电来了!如何处理一个 INVITE?

现在换个角度:你是被叫方。手机突然响了,屏幕上跳出“未知号码来电”,这是怎么实现的?

当你的设备收到一条 SIP INVITE 消息时,pjsip 协议栈首先做合法性检查:解析 URI、验证格式、查注册表看是否有对应账号。通过之后,立刻触发回调:

static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata) { PJ_LOG(3, (THIS_FILE, "Incoming call on account %d, call ID: %d", acc_id, call_id)); char caller[256]; pjsip_uri *uri = pjsip_uri_get_uri(rdata->msg_info.from->uri); if (PJSIP_URI_SCHEME_IS_SIP(uri)) { pjsip_sip_uri *sip_uri = (pjsip_sip_uri *)uri; pj_ansi_snprintf(caller, sizeof(caller), "%.*s@%.*s", (int)sip_uri->user.slen, sip_uri->user.ptr, (int)sip_uri->host.slen, sip_uri->host.ptr); } PJ_LOG(3, (THIS_FILE, "Caller: %s", caller)); pjsua_call_answer(call_id, 200, NULL, NULL); // 自动接听演示 }

关键点一:提取主叫信息

rdata->msg_info.from是原始 SIP 消息中的 From 头域。我们需要从中提取 SIP URI,并进一步解析出用户名和主机名。这才是真正的“来电号码”。

⚠️ 注意:不要直接信任 From 字段!恶意用户可以伪造主叫号码。生产环境中应结合鉴权机制或白名单校验。

关键点二:接听 or 拒绝?

你可能注意到,上面的例子用了pjsua_call_answer(call_id, 200, ...)直接接听。但在真实产品中,你应该:

  1. 弹出 UI 提示框(移动端用通知栏,桌面端弹窗)
  2. 用户点击“接听” → 调用answer(200)
  3. 用户点击“拒接” → 调用hangup(486)返回“Busy”

也可以先回一个180 Ringing表示“我收到了,正在振铃”,给主叫方更好的体验:

pjsua_call_answer(call_id, 180, NULL, NULL); // 先响铃 // ……几秒后用户操作…… pjsua_call_answer(call_id, 200, NULL, NULL); // 再正式接通

这样主叫方就能听到回铃音,而不是干等。


接通了!媒体流是怎么建立的?

无论是主动拨出还是被动接听,只要看到状态变成:

PJSUA_CALL_STATE_CONFIRMED

恭喜你,通话正式建立。但这只是信令层面的成功。真正能说话,还得靠 RTP 媒体流打通。

媒体建立的关键:SDP 协商

还记得前面提到的 SDP 吗?它的作用就是让双方知道:“我能听 OPUS 编码,RTP 端口是 4000”、“那我发 OPUS 到你 4000 端口”。

pjsip 在内部使用pjmedia_session管理媒体通道。当你进入CONFIRMED状态时,框架会自动完成以下工作:

  • 解析远端 SDP 中的媒体参数
  • 匹配共同支持的编解码器(如都支持 OPUS,则选用之)
  • 绑定本地 RTP/RTCP socket
  • 启动 ICE 连接候选交换(如果启用了 STUN/TURN)

这一切都不需要你手动干预,除非你要定制特殊行为(比如强制使用某编码)。

如何确认媒体已就绪?

除了监听on_call_state(),你还应该注册另一个重要回调:

void on_call_media_state(pjsua_call_id call_id) { pjsua_call_info ci; pjsua_call_get_info(call_id, &ci); if (ci.media_status == PJSUA_CALL_MEDIA_ACTIVE) { PJ_LOG(3, (THIS_FILE, "Media is UP! Start audio playback/recording.")); // 此时可安全连接声卡设备 pjsua_conf_connect(ci.conf_slot, 0); // 接入混音器 } }

这个函数会在媒体通道激活时被调用。你可以在这里打开麦克风、启动播放器,甚至开启录音功能。


挂断电话:不只是“结束通话”那么简单

最后一步,也是最容易出错的一步:挂断。

你以为调个hangup()就完事了?错。不同的时机,行为完全不同。

场景一:已接通后挂断 → 发送 BYE

pj_status_t hangup_call(pjsua_call_id call_id) { return pjsua_call_hangup(call_id, 0, NULL, NULL); }

此时 pjsip 会向对方发送BYE消息,对方回复200 OK,双方正常拆除会话。

对应的信令流程是:

YOU: BYE → THEM: 200 OK → (YOU enter DISCONNECTED)

场景二:未接通前取消 → 发送 CANCEL

如果你在对方还没回 200 OK 之前就点了挂断(比如等太久),这时实际发出的是CANCEL请求。

例如:

YOU: INVITE → (等待 10 秒无响应) → CANCEL → THEM: 487 Request Terminated → (YOU enter DISCONNECTED)

这也是为什么你在on_call_state()里看到last_status = 487时不要慌——这是正常的取消流程。

场景三:对方主动挂断 → 被动终止

对方发来BYE,你会收到一个事件,最终进入DISCONNECTED状态。

此时你不需要回复任何操作,pjsip 会自动回200 OK并清理资源。

但你可以在on_call_state(DISCONNECTED)中做一些收尾工作:

if (ci.state == PJSUA_CALL_STATE_DISCONNECTED) { PJ_LOG(3, (THIS_FILE, "Call ended. Reason: %.*s", (int)ci.disconnect_reason.slen, ci.disconnect_reason.ptr)); cleanup_call_resources(call_id); // 释放内存、关闭文件句柄等 }

实际开发中的那些“坑”,我都踩过了

别看流程清晰,真正在嵌入式或移动平台上跑起来,问题层出不穷。下面是我亲身经历的几个典型陷阱。

❌ 坑点一:重复接听导致崩溃

新手常犯的错误是在on_incoming_call里直接调answer(),但如果网络抖动导致 INVITE 重传,回调就会被触发多次!

后果:连续两次answer()→ 协议状态混乱 → 断言失败或 crash。

解决方案:记录每个 call_id 的处理状态:

static pj_hash_table_t *call_state_table; void on_incoming_call(...call_id...) { if (is_call_handled(call_id)) return; // 已处理则忽略 mark_call_as_handled(call_id); ... }

或者更简单粗暴:只允许一次接听操作。


❌ 坑点二:NAT穿透失败,媒体不通

明明信令通了,状态也到了CONFIRMED,但就是听不到声音?

大概率是 RTP 包卡在防火墙后面。这时候就得靠STUN/TURN来帮忙。

确保你在初始化时配置了辅助服务器:

pjsua_config cfg; pjsua_config_default(&cfg); cfg.stun_host = pj_str("stun.l.google.com:19302"); // 或者使用 TURN // cfg.turn_cfg.enabled = PJ_TRUE; // cfg.turn_cfg.server = pj_str("turn.example.com"); // cfg.turn_cfg.username = pj_str("user"); // cfg.turn_cfg.password = pj_str("pass");

没有 NAT 穿透支持,在复杂网络环境下成功率会大幅下降。


✅ 秘籍一:开启日志,看清每一步

调试 pjsip 最有效的手段是什么?不是加断点,而是看日志

pjsua_logging_config log_cfg; pjsua_logging_config_default(&log_cfg); log_cfg.log_level = 5; // 显示详细信息 log_cfg.console_level = 5;

设置 level=5 后,你会看到每一行 SIP 消息的明文输出,包括完整的 INVITE、BYE、ACK 报文。这对排查“为什么没收到 200 OK”这类问题极其有用。


✅ 秘籍二:合理管理资源,避免泄露

每次通话结束后,记得:

  • 关闭音频设备(pjsua_player_destroy,pjsua_recorder_destroy
  • 释放录音/播放缓冲区
  • 清除临时变量和上下文

尤其在嵌入式系统上,内存宝贵,一次忘记释放可能就会积累成 OOM。


总结一下:一张图看懂全过程

我们把整个流程串起来,画成一张简易的状态流转图:

[NULL] │ ├─拨号→ [CALLING] → 收到1xx → [EARLY] → 收到200 → 发ACK → [CONFIRMED] → 挂断 → [DISCONNECTED] │ ↑ ↑ └─来电← [INCOMING] ← INVITE └──── 接听答200 ─────┘ ↓ 拒绝→ 发486/603 → [DISCONNECTED] ↓ 忽略→ 超时 → [DISCONNECTED]

所有状态变化都由on_call_state()统一捕获,而具体动作(拨号、接听、挂断)则由 API 控制。


写在最后

掌握 pjsip 的呼叫控制逻辑,本质上是在掌握SIP 协议的生命线

你不需要成为 RFC 3261 专家,但必须清楚:

  • 每次 API 调用背后发生了什么
  • 每个状态变更意味着什么
  • 每条 SIP 消息的作用和时机

只有这样,当你面对“打不出去”、“接不了”、“无声”这些问题时,才能快速定位是信令问题、媒体问题,还是网络问题。

这篇文章覆盖了拨号、接听、挂断的全流程实现与常见陷阱,希望能帮你少走弯路。如果你正在做一个 VoIP 项目,不妨把这几个回调函数打印出来贴在工位上——它们是你和世界通话的桥梁。

如果你在集成过程中遇到了其他棘手问题,欢迎留言交流。我们一起拆解,直到跑通第一通电话为止。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1135888.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

燃料电池功率跟随cruise仿真模型!!!此模型基于Cruise2019版及Matlab201...

燃料电池功率跟随cruise仿真模型!!!此模型基于Cruise2019版及Matlab2018a搭建调试而成,跟随效果很好,任务仿真结束起始soc几乎相同。 控制模型主要包括燃料堆控制、DCDC控制、驱动力控制、再生制动控制、机械制动等模块…

医药信息管理|基于Python + Django医药信息管理系统(源码+数据库+文档)

医药信息管理 目录 基于PythonDjango医药信息管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于PythonDjango医药信息管理系统 一、前言 博主介绍&#xff1a…

加法器学习路径:掌握数字设计的第一步

加法器学习路径:掌握数字设计的第一步在数字电路的世界里,加法器远不止是“两个数相加”这么简单。它是一扇门——推开这扇门,你看到的不是单一功能模块,而是整个数字系统设计思维的缩影。从最基础的逻辑门组合,到影响…

招聘推荐|基于Python + Django招聘推荐系统(源码+数据库+文档)

招聘推荐 目录 基于PythonDjango招聘推荐系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于PythonDjango招聘推荐系统 一、前言 博主介绍:✌️大厂码农…

qthread实时性优化技巧实战分享

QThread实时性调优实战:从理论到工业级音频系统的精准控制你有没有遇到过这样的情况?明明代码逻辑清晰,硬件性能也够用,但系统就是“卡”在某个环节——音视频采集偶尔丢帧、控制指令响应延迟波动、高频数据处理出现抖动。尤其是在…

深度学习中文情感分析|基于Python + Django深度学习中文情感分析系统(源码+数据库+文档)

深度学习中文情感分析 目录 基于PythonDjango深度学习中文情感分析系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于PythonDjango深度学习中文情感分析系统 一、…

USB3.0接口定义引脚说明与电源管理设计完整示例

深入理解USB3.0接口设计:从引脚定义到电源管理的完整实战指南你有没有遇到过这样的情况?一个USB3.0设备插上去,系统识别成“USB2.0高速设备”,传输速度只有几百MB/s不说,还时不时断连、发热严重。调试几天下来&#xf…

P4145 上帝造题的七分钟 2 / 花神游历各国[线段树 区间开方(剪枝) + 区间求和]

P4145 上帝造题的七分钟 2 / 花神游历各国 时间限制: 1.00s 内存限制: 125.00MB 复制 Markdown 中文 退出 IDE 模式 题目背景 XLk 觉得《上帝造题的七分钟》不太过瘾,于是有了第二部。 题目描述 “第一分钟,X 说,要有数列&#xff0c…

虚拟串口软件权限配置:入门级安全设置指南

虚拟串口安全入门:从配置到防护的实战指南你有没有遇到过这样的场景?调试一个工业通信程序时,手头没有真实PLC设备,于是用虚拟串口软件搭了个仿真环境。一切正常运行——直到某天,另一个后台服务突然“抢走”了你的COM…

新手必看:QListView初学者常见问题汇总

QListView新手避坑指南:从“显示空白”到“流畅交互”的实战解析你有没有遇到过这种情况——代码写完,编译通过,运行起来却发现QListView一片空白?点也点不动,改也改不了。别急,这几乎是每个Qt初学者都会踩…

停车场管理|基于Python + Django停车场管理系统(源码+数据库+文档)

停车场管理 目录 基于PythonDjango停车场管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取: 基于PythonDjango停车场管理系统 一、前言 博主介绍:✌️大…

P1637 三元上升子序列[线段树维护 + 离散化]

P1637 三元上升子序列 时间限制: 1.00s 内存限制: 128.00MB 复制 Markdown 中文 退出 IDE 模式 题目描述 Erwin 最近对一种叫 thair 的东西巨感兴趣。。。 在含有 n 个整数的序列 a1​,a2​,…,an​ 中&#xff0c;三个数被称作thair当且仅当 i<j<k 且 ai​<aj…

医院信息管理|基于Python + Django医院信息管理系统(源码+数据库+文档)

医院信息管理 目录 基于PythonDjango医院信息管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于PythonDjango医院信息管理系统 一、前言 博主介绍&#xff1a…

低成本DSP变频器方案全解析:C语言源码、编译码、PCB图纸及物料清单详解

低成本dsp变频器方案&#xff0c;有C语言源码&#xff0c;编译码&#xff0c;PCB图纸&#xff0c;物料清单。最近在捣鼓个低成本DSP变频器方案&#xff0c;折腾了半个月总算有点眉目了。这次直接把PCB图纸甩进立创EDA就能打板&#xff0c;物料成本压到五十块以内&#xff0c;核…

让陪伴不缺席,让安心常在线——智慧康养服务APP功能一览

当忙碌让陪伴变得稀缺&#xff0c;当衰老让安全充满顾虑&#xff0c;这款专为老年群体量身打造的智慧康养服务APP&#xff0c;以AI技术精准匹配适老需求&#xff0c;将情感陪伴、记忆珍藏、安全守护三大核心价值融于一体——既为独居老人筑牢全天候温暖防线&#xff0c;也让异地…

RustFS主要有哪些竞争对手?一文讲透对象存储选型

当MinIO转身拥抱商业化的消息传开&#xff0c;技术圈一片哗然。寻找下一个靠谱的开源对象存储&#xff0c;突然成了许多开发团队的紧急任务。RustFS虽亮眼&#xff0c;但这条赛道上可不止它一位选手。 自从MinIO在2025年底宣布其开源版本进入“维护模式”&#xff0c;不再进行主…

基于USB3.0传输速度的工业U盘设计:从零实现

一块能扛住工厂震动、高温和24小时写入的U盘&#xff0c;是怎么做出来的&#xff1f;你有没有遇到过这种情况&#xff1a;产线上的检测设备每天生成几十GB的数据&#xff0c;导出一次要等半小时&#xff1f;或者车载记录仪在零下30C的东北冬天突然“罢工”&#xff0c;数据全丢…

牛批了,文字转语音神器

有时候在做一些短视频时&#xff0c;需要进行配音。有一些配音软件是收费的&#xff0c;今天给大家介绍一款免费的文字转语音的软件&#xff0c;有需要的小伙伴一定要下载收藏。 Read Aloud 免费的文字转语音软件 这款软件体积非常小巧&#xff0c;大小只有3兆。 软件无需安装…

实现多点触控支持:Synaptics驱动开发进阶指南

打造流畅多点触控体验&#xff1a;深入 Synaptics 驱动开发实战你有没有遇到过这种情况——在笔记本上用两个手指缩放图片时&#xff0c;光标突然跳走&#xff1f;或者三指滑动切换桌面时毫无反应&#xff1f;这些看似“玄学”的问题&#xff0c;背后往往藏着驱动层的细节玄机。…

【收藏】AI时代产品经理的生死劫:不懂架构师思维的PM将被淘汰

文章探讨了AI时代产品经理角色的根本转变。随着App和传统界面的消亡&#xff0c;AI产品经理必须从传统的需求分析者转变为系统架构师。未来的产品形态将是"用户→意图→数据→模型→Agent→工具→反馈→再生成"的智能链路&#xff0c;AI PM需要具备系统架构、意图理解…