利用pjsip构建软电话(Softphone):零基础实战教程

从零开始用 pjsip 打造一个能打电话的软电话:实战全记录

你有没有想过,自己动手写一个可以拨打电话的“软电话”?不是模拟器,不是调用系统 API,而是真正通过 SIP 协议注册到服务器、拨打号码、听到对方声音的那种。

听起来很复杂?其实没那么玄。今天我们就用pjsip——这个被无数 VoIP 产品背书的开源通信库,手把手带你从零构建一个可运行的软电话原型。不需要任何前置知识,只要你懂一点 C 语言,就能跑通第一通电话。


为什么选 pjsip?

市面上做 VoIP 的方案不少:WebRTC、Linphone、Asterisk 客户端……但如果你想要的是对底层完全可控、性能高、资源占用小、还能跑在嵌入式设备上的解决方案,那 pjsip 几乎是唯一的选择。

它是纯 C 写的,跨平台支持极强(Windows/Linux/macOS/Android/iOS/裸机 RTOS 都行),代码结构清晰,文档虽然略硬核,但一旦摸清套路,开发效率非常高。

更重要的是:它不是一个玩具项目。像 Cisco、Polycom、Zoom 的某些模块,甚至一些工业级语音网关,背后都有 pjsip 的影子。

我们今天要做的,就是把它最核心的能力——注册、拨号、接电话、传音频——拆开讲透,让你不仅能跑起来,还能知道每一步到底发生了什么。


先搞清楚:软电话是怎么工作的?

别急着敲代码,先搞明白逻辑。

一个软电话的本质,其实是两个部分:

  1. SIP 信令控制:负责“打招呼”、“发起通话”、“挂断”这些动作。
  2. RTP 媒体传输:真正把你的声音打包发出去,把对方的声音收回来播放。

你可以把 SIP 想象成打电话前的“对话”:

“喂,我在吗?”
“在的。”
“我想和你通话。”
“好啊,我这边听筒准备好了,你发声音过来吧。”

等这番“握手”完成,双方就打开 RTP 通道,开始传语音数据了。

而 pjsip 的厉害之处在于:它把这两块全都包圆了。你只需要调几个 API,剩下的——编解码、回音消除、抖动缓冲、NAT 穿透——它都给你处理好了。


第一步:初始化 pjsip 运行环境

所有操作的前提是“启动引擎”。就像开车前要点火,我们必须先初始化 pjsip 的核心运行时。

#include <pjsua-lib/pjsua.h> static pjsua_config cfg; static pjsua_logging_config log_cfg; pj_status_t initialize_pjsip(void) { pj_status_t status; // 1. 创建 UA(User Agent)实例 status = pjsua_create(); if (status != PJ_SUCCESS) return status; // 2. 设置日志级别,方便调试 pjsua_logging_config_default(&log_cfg); log_cfg.level = 4; // 输出详细信息 // 3. 初始化默认配置 pjsua_config_default(&cfg); cfg.max_calls = 4; // 最多支持4路并发通话 cfg.stun_host = pj_str("stun.l.google.com:19302"); // 使用 Google 的 STUN 服务 // 4. 正式初始化 UA 核心 status = pjsua_init(&cfg, &log_cfg, NULL); if (status != PJ_SUCCESS) return status; // 5. 创建 UDP 传输层,监听 5060 端口 pjsua_transport_config tp_cfg; pjsua_transport_config_default(&tp_cfg); tp_cfg.port = 5060; status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &tp_cfg, NULL); if (status != PJ_SUCCESS) return status; // 6. 启动事件调度器 status = pjsua_start(); if (status != PJ_SUCCESS) return status; return PJ_SUCCESS; }

这段代码看着多,其实就干了六件事:

  • 创建用户代理(UA)
  • 打开日志开关
  • 设定最大通话数、启用 STUN(解决 NAT 问题的关键)
  • 初始化核心模块
  • 绑定 UDP 5060 端口接收 SIP 消息
  • 启动事件循环

其中pjsua_start()是关键,它会拉起后台线程处理网络 IO 和定时任务。之后所有的状态变化都会通过回调通知你。


第二步:添加账号并注册到服务器

光有引擎不行,你还得有个“身份”才能打电话。这就是 SIP 账号。

假设你有一个 VoIP 服务商提供的账户:
- URI:sip:alice@sip.provider.com
- 用户名:alice
- 密码:secret123
- 域名:sip.provider.com

我们要把这些信息告诉 pjsip,并让它自动去注册。

pjsua_acc_id add_sip_account(const char *sip_uri, const char *username, const char *password, const char *domain) { pjsua_acc_config acc_cfg; pjsua_acc_id acc_id; pjsua_acc_config_default(&acc_cfg); acc_cfg.id = pj_str((char*)sip_uri); // 完整标识 acc_cfg.reg_uri = pj_str((char*)domain); // 注册地址 acc_cfg.cred_count = 1; acc_cfg.cred_info[0].realm = pj_str("*"); acc_cfg.cred_info[0].scheme = pj_str("digest"); acc_cfg.cred_info[0].username = pj_str((char*)username); acc_cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN; acc_cfg.cred_info[0].data = pj_str((char*)password); acc_cfg.reg_timeout = 300; // 每5分钟重注册一次 pj_status_t status = pjsua_acc_add(&acc_cfg, PJ_TRUE, &acc_id); if (status != PJ_SUCCESS) { PJ_LOG(1, ("SOFTPHONE", "注册失败: %s", pjsip_strerror(status, NULL, 0))); return PJSUA_INVALID_ID; } return acc_id; }

重点看这一句:

pjsua_acc_add(&acc_cfg, PJ_TRUE, &acc_id);

第二个参数传PJ_TRUE表示:加完立即注册。否则你得手动调pjsua_acc_set_registration()才能触发。

注册成功后,你会看到日志里出现Registration successful,这时候你的软电话就算“上线”了。


第三步:拨打电话

注册好了,就可以打了。

比如你想打给bob@sip.provider.com,只需一行核心调用:

pjsua_call_id make_outgoing_call(pjsua_acc_id acc_id, const char *dst_uri) { pj_status_t status; pjsua_call_id call_id; pjsua_msg_data msg_data; pjsua_msg_data_init(&msg_data); pj_str_t uri = pj_str((char*)dst_uri); status = pjsua_call_make_call(acc_id, &uri, 0, NULL, &msg_data, &call_id); if (status != PJ_SUCCESS) { PJ_LOG(1, ("SOFTPHONE", "呼叫失败: %s", pjsip_strerror(status, NULL, 0))); return PJSUA_INVALID_ID; } return call_id; }

就这么简单?没错。

pjsip 会在背后自动生成 INVITE 请求,附带 SDP 描述本地支持的编码格式(如 PCMU、PCMA、OPUS 等),然后发给对方。

如果对方接受,就会回 200 OK + 它的 SDP,接着你再发 ACK 确认,RTP 通道建立,语音就开始传输了。

整个过程完全由库内部管理,你只需要关注“什么时候拨出”、“什么时候挂断”。


第四步:接听来电

别人打给你怎么办?靠回调。

你需要提前注册一系列事件处理器,其中最重要的就是on_incoming_call

void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata) { pjsua_call_info ci; pjsua_call_get_info(call_id, &ci); PJ_LOG(3, ("SOFTPHONE", "来电!来自 %.*s", (int)ci.remote_info.slen, ci.remote_info.ptr)); // 自动接听(实际项目中可弹窗提示) pjsua_call_answer(call_id, 200, NULL, NULL); }

当然,完整的应用还需要实现其他回调,比如:

  • on_call_state():监控通话状态(振铃、已连接、结束等)
  • on_call_media_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) { // 媒体已激活,连接到默认音频设备 pjsua_conf_connect(ci.conf_slot, 0); // 输入(麦克风)→ 播放(扬声器) pjsua_conf_connect(0, ci.conf_slot); // 反向也连上 } }

这两条conf_connect很关键,相当于把“麦克风”和“扬声器”接到这次通话的混音槽位上。不连这个,就算通了也听不到声音。


第五步:搞定音频设备

很多人第一次跑 demo 发现“能通但没声音”,八成是音频设备没配对。

pjsip 提供了简单的接口来设置录音和播放设备:

pj_status_t setup_audio_device(int capture_id, int playback_id) { pj_status_t status; status = pjsua_set_snd_dev(capture_id, playback_id); if (status != PJ_SUCCESS) return status; // 查看可用设备列表 pjsua_snd_dev_info dev_info[PJMEDIA_SOUND_MAX_DEVS]; unsigned count = PJ_ARRAY_SIZE(dev_info); pjsua_enum_snd_devs(dev_info, &count); for (unsigned i = 0; i < count; ++i) { PJ_LOG(4, ("AUDIO", "[%d] %s (%s%s)", i, dev_info[i].name, dev_info[i].input_count ? "in" : "", dev_info[i].output_count ? ",out" : "")); } return PJ_SUCCESS; }

建议首次运行时打印设备列表,确认哪个 ID 对应你的麦克风和扬声器。Windows 上常用的是 PortAudio 或 WASAPI,Linux 上是 ALSA。

另外,强烈建议开启 AEC(回音消除):

// 在配置中启用 AEC cfg.ec_options = PJMEDIA_ECHO_SIMPLE; // 或使用更高级的 SUPPRESSOR

不然你在免提状态下会听到自己的回声,体验极差。


实际工作流程拆解

让我们串一遍完整流程:

  1. 调用initialize_pjsip()→ 启动 pjsip 引擎
  2. 调用add_sip_account()→ 发送 REGISTER 到服务器
  3. 收到 200 OK → 显示“已上线”
  4. 用户点击“拨打” → 调make_outgoing_call()
  5. pjsip 发 INVITE + SDP → 对方回应 200 OK + SDP
  6. 发 ACK → 建立 RTP 流
  7. on_call_media_state触发 → 连接音频设备
  8. 麦克风采集 PCM → 编码 → 封装 RTP 包 → UDP 发送
  9. 接收远端 RTP 包 → 解码 → 送扬声器播放
  10. 用户点“挂断” → 发 BYE → 会话结束

全程异步驱动,无需轮询,CPU 占用很低。


常见坑点与应对策略

❌ 注册失败(Timeout / 403 Forbidden)

  • 检查用户名密码是否正确
  • 确认防火墙是否放行 UDP 5060 和随机 RTP 端口(通常 10000~20000)
  • 如果在公司内网,尝试启用 STUN:cfg.stun_host = pj_str("stun.l.google.com:19302");

🔊 来电无声或单通

  • 检查 SDP 是否协商出共同支持的编解码器(如 PCMU)
  • 确保on_call_media_state中正确连接了 conf_slot
  • 查看 NAT 类型,如果是对称型 NAT,需部署 TURN 服务器辅助穿透

🗣 回音严重

  • 必须启用 AEC:cfg.ec_options = PJMEDIA_ECHO_SUPPRESSOR;
  • 调低麦克风增益,避免啸叫
  • 使用耳机而非外放,物理隔离反馈路径

🐢 延迟高、卡顿

  • 检查网络延迟和丢包率
  • 增大 Jitter Buffer:pjmedia_jbuf_set_delay(target_ms)
  • 优先使用 OPUS 编码,适应弱网环境

工程最佳实践建议

  1. 不要阻塞事件线程
    所有回调函数应尽快返回,耗时操作扔到独立线程处理。

  2. 使用 FSM 管理呼叫状态
    把“空闲”、“拨号中”、“振铃”、“通话中”、“挂断中”做成状态机,防止非法操作。

  3. 预分配内存池
    pjsip 使用固定大小内存池,避免动态分配影响实时性。可在初始化时加大 pool size。

  4. 分级日志输出
    开发期设level=5,生产环境降为level=2,减少 I/O 开销。

  5. 优先选用 OPUS 编码
    相比 G.711(64kbps),OPUS 可在 20kbps 下保持高清语音,节省带宽。


结语:这只是个开始

我们现在已经有了一个能注册、能拨号、能接听、能传声音的软电话原型。虽然界面简陋,但它已经具备了 VoIP 终端的核心能力。

接下来你可以继续扩展:

  • 加入 GUI(Qt / Dear ImGui)
  • 支持视频通话(利用 pjsip 的视频模块)
  • 实现 IM 文本消息
  • 添加 DTMF 按键推送
  • 构建多方会议桥接器

而这一切的基础,都始于今天我们走过的这条路。

pjsip 不是最快的入门工具,也不是最容易上手的框架,但它足够强大、足够稳定、足够灵活。一旦掌握,你就拥有了构建专业级通信系统的钥匙。

所以,别再只停留在“听说”了。现在就去下载 pjsip 源码,编译它,运行它,改它,让它为你所用。

毕竟,真正的理解,永远来自亲手敲下的每一行代码。

如果你在搭建过程中遇到具体问题,欢迎留言交流——我们一起 debug。

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

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

相关文章

MediaPipe Pose技术揭秘:高精度骨骼检测背后的原理

MediaPipe Pose技术揭秘&#xff1a;高精度骨骼检测背后的原理 1. 引言&#xff1a;AI人体骨骼关键点检测的现实需求 在计算机视觉领域&#xff0c;人体姿态估计&#xff08;Human Pose Estimation&#xff09;是一项基础而关键的技术。它通过分析图像或视频中的人体结构&…

深入理解qthread中信号与槽的线程安全性

深入理解QThread中信号与槽的线程安全性&#xff1a;从机制到实战你有没有遇到过这样的场景&#xff1f;在子线程里处理完一堆数据&#xff0c;兴冲冲地调用label->setText("完成&#xff01;")&#xff0c;结果程序瞬间崩溃——没有明显报错&#xff0c;但调试器…

MediaPipe Pose完整部署:从零开始骨骼关键点检测

MediaPipe Pose完整部署&#xff1a;从零开始骨骼关键点检测 1. 引言&#xff1a;AI人体骨骼关键点检测的现实价值 随着计算机视觉技术的快速发展&#xff0c;人体姿态估计&#xff08;Human Pose Estimation&#xff09;已成为智能健身、动作捕捉、虚拟试衣、人机交互等场景…

手势识别避坑指南:用MediaPipe Hands镜像轻松实现21点定位

手势识别避坑指南&#xff1a;用MediaPipe Hands镜像轻松实现21点定位 在人机交互、智能控制和增强现实等前沿技术中&#xff0c;手势识别正逐渐成为下一代自然交互方式的核心。然而&#xff0c;许多开发者在尝试构建手势识别系统时&#xff0c;常常面临模型部署复杂、依赖环境…

React Native搭建环境新手必看常见错误汇总

React Native环境配置避坑指南&#xff1a;从零到运行&#xff0c;一次搞定 你是不是也经历过这样的场景&#xff1f;兴冲冲地打开终端&#xff0c;输入 npx react-native init MyAwesomeApp &#xff0c;结果等来的不是“Welcome to React Native”&#xff0c;而是一堆红色…

YOLOv8实战应用:智能安防监控系统快速搭建

YOLOv8实战应用&#xff1a;智能安防监控系统快速搭建 1. 引言&#xff1a;智能安防的视觉革命 随着城市化进程加快和公共安全需求提升&#xff0c;传统安防系统正面临从“看得见”向“看得懂”的转型压力。传统的视频监控依赖人工回看&#xff0c;效率低、响应慢&#xff0c…

毕业论文降AI神器推荐:从80%降到10%的秘密武器

毕业论文降AI神器推荐&#xff1a;从80%降到10%的秘密武器 “AI率80%&#xff0c;论文直接打回重写。” 这是我室友上周收到的噩耗。眼看答辩在即&#xff0c;毕业论文降AI成了宿舍里的热门话题。折腾了一周&#xff0c;终于帮他把**论文AI率从80%降到10%**以下&#xff0c;今…

MediaPipe Pose部署卡顿?极速CPU优化实战解决方案

MediaPipe Pose部署卡顿&#xff1f;极速CPU优化实战解决方案 1. 背景与痛点&#xff1a;AI人体骨骼关键点检测的落地挑战 随着AI视觉技术的发展&#xff0c;人体姿态估计&#xff08;Human Pose Estimation&#xff09;已成为智能健身、动作捕捉、虚拟试衣、安防监控等场景的…

libusb异步传输机制深度剖析与实践

libusb异步传输机制深度剖析与实践&#xff1a;从原理到工程落地在嵌入式系统、工业控制和高性能外设开发中&#xff0c;USB 已成为连接主机与设备的“标准语言”。无论是数据采集卡、图像传感器&#xff0c;还是音频接口&#xff0c;我们几乎都绕不开 USB 通信。而当面对高吞吐…

一键启动多语言翻译:HY-MT1.5-1.8B Docker部署全攻略

一键启动多语言翻译&#xff1a;HY-MT1.5-1.8B Docker部署全攻略 1. 引言 在全球化业务快速发展的背景下&#xff0c;实时、准确的多语言翻译能力已成为智能应用的核心基础设施。腾讯混元团队推出的 HY-MT1.5-1.8B 翻译模型&#xff0c;凭借其1.8亿参数量下卓越的翻译质量与极…

YOLOv8实战应用:智能仓储货物盘点系统快速搭建教程

YOLOv8实战应用&#xff1a;智能仓储货物盘点系统快速搭建教程 1. 引言&#xff1a;为什么需要智能仓储货物盘点&#xff1f; 在现代仓储物流体系中&#xff0c;人工盘点效率低、出错率高、成本高昂的问题长期存在。传统方式依赖员工逐件清点、手动记录&#xff0c;不仅耗时耗…

知网AI率降到0%可能吗?合理目标和实现方法

知网AI率降到0%可能吗&#xff1f;合理目标和实现方法 “**知网AI率降到0%**有可能吗&#xff1f;” 这个问题我被问过很多次。今天聊聊这个话题&#xff0c;帮大家建立合理的降AI目标。 先说结论&#xff1a;0%很难&#xff0c;也没必要 为什么很难降到0%&#xff1f; AIG…

人体动作分析系统:MediaPipe Pose实战开发教程

人体动作分析系统&#xff1a;MediaPipe Pose实战开发教程 1. 引言&#xff1a;AI 人体骨骼关键点检测的工程价值 随着计算机视觉技术的快速发展&#xff0c;人体姿态估计&#xff08;Human Pose Estimation&#xff09;已成为智能健身、虚拟试衣、动作捕捉、人机交互等领域的…

从0开始学目标检测:YOLOv8鹰眼极速CPU版入门

从0开始学目标检测&#xff1a;YOLOv8鹰眼极速CPU版入门 1. 引言&#xff1a;为什么你需要一个“鹰眼”级目标检测系统&#xff1f; 在智能监控、工业质检、无人零售等实际场景中&#xff0c;实时、准确、低资源消耗的目标检测能力已成为AI落地的核心需求。传统方案往往依赖G…

手把手教学:用YOLOv8鹰眼版实现智能交通流量统计

手把手教学&#xff1a;用YOLOv8鹰眼版实现智能交通流量统计 1. 引言&#xff1a;为什么需要智能交通流量统计&#xff1f; 随着城市化进程加速&#xff0c;交通拥堵、道路规划不合理、信号灯配时僵化等问题日益突出。传统的人工计数或基于传感器的统计方式成本高、覆盖有限、…

图解说明ES6模块的加载机制与执行顺序

深入理解 ES6 模块的加载机制&#xff1a;从依赖解析到执行顺序你有没有遇到过这样的情况&#xff1f;在项目中引入一个工具函数时&#xff0c;明明已经import了&#xff0c;却报出undefined&#xff1b;或者两个模块互相引用&#xff0c;结果一方拿到了undefined&#xff0c;而…

手势追踪极速体验:MediaPipe Hands镜像毫秒级响应实测

手势追踪极速体验&#xff1a;MediaPipe Hands镜像毫秒级响应实测 1. 引言&#xff1a;从人机交互到指尖感知 在智能硬件、虚拟现实和人机交互快速发展的今天&#xff0c;手势识别正逐步成为下一代自然交互方式的核心技术。相比传统的触控或语音输入&#xff0c;手势操作更直…

阿里大模型的并发限制.

https://bailian.console.aliyun.com/?tabdoc#/doc/?typemodel&url2840182

Zephyr快速理解:内核对象与线程管理要点

Zephyr 内核对象与线程管理&#xff1a;从机制到实战的深度剖析你有没有遇到过这样的嵌入式开发场景&#xff1f;系统功能越来越多&#xff0c;多个任务并行运行——一个负责采集传感器数据&#xff0c;一个处理蓝牙通信&#xff0c;还有一个要响应紧急按键事件。结果代码越写越…

freemodbus入门实战:实现寄存器读写操作示例

从零开始玩转 freemodbus&#xff1a;手把手教你实现寄存器读写在工业控制领域&#xff0c;设备之间要“说话”&#xff0c;靠的不是语言&#xff0c;而是通信协议。而说到串行通信里的“普通话”&#xff0c;Modbus绝对当仁不让。它简单、开放、稳定&#xff0c;几乎成了 PLC、…