news 2026/7/16 8:42:33

PJSIP C++开发实战:从核心类解析到可运行SIP客户端构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PJSIP C++开发实战:从核心类解析到可运行SIP客户端构建

1. 项目概述:PJSIP库的C++世界初探

如果你正在C++领域里折腾音视频通信,无论是想做一个自己的软电话、一个带通话功能的IoT设备,还是一个嵌入式的对讲系统,那么PJSIP这个名字你大概率绕不过去。它是一个开源的、功能强大的SIP(会话初始协议)和多媒体通信库,用C语言写成,但提供了对开发者更友好的PJSUA2这一层C++封装。网上资料虽然多,但往往要么是零散的API文档,要么是过于简单的“Hello World”,让人看了之后还是不知道从何下手去构建一个真正可用的项目。我自己在几年前第一次接触PJSIP时,就被它庞大的代码库和略显晦涩的文档给镇住了,踩了不少坑才摸清门道。

这篇文章,我就以一个过来人的身份,带你深入PJSIP的C++层(PJSUA2)。我不会只给你看一个注册到测试服务器就退出的示例代码,那只是冰山一角。我会拆解这个“Hello World”背后的每一个步骤,告诉你每个类(如Endpoint,Account,Call)到底是干什么的、怎么用,以及在实际项目中你会遇到哪些官方文档没细说的“坑”。我们的目标是,看完之后,你不仅能看懂示例,更能自己动手搭建起一个具备基本呼叫能力的C++客户端骨架,并理解其背后的设计哲学。无论你是音视频通信的初学者,还是有一定经验想系统了解PJSIP的开发者,这篇内容都能给你提供一条清晰的路径。

2. PJSUA2 C++核心类深度解析

PJSUA2的C++ API设计采用了面向对象的思想,将复杂的SIP协议栈和媒体处理逻辑封装成几个核心的类。理解这些类的职责和生命周期,是高效使用PJSIP的关键。很多人一开始就埋头写代码,结果发现对象创建和销毁的顺序不对,导致内存泄漏或程序崩溃。我们先来把这几个“主角”认识清楚。

2.1 Endpoint:通信栈的司令塔

Endpoint类是PJSUA2世界的单例(虽然从代码上看不是严格意义上的单例,但实践中我们通常只创建一个实例)。它是整个库的入口点和总控制器,负责管理库的生命周期、线程池、传输层(TCP/UDP/TLS)以及全局配置。

关键点与实操心得:

  • 唯一性与生命周期:虽然理论上可以创建多个Endpoint实例,但在99%的应用场景下,一个进程只需要一个。它的生命周期应该覆盖整个应用的通信周期。libCreate()libDestroy()必须成对调用,且通常发生在主线程。
  • 初始化的正确顺序libInit()必须在libCreate()之后,transportCreate()libStart()之前调用。libStart()会启动工作线程,之后库才能处理网络事件和媒体流。一个常见的错误是在libStart()之前就去创建账号或发起呼叫,这会导致操作失败。
  • 传输层(Transport):这是SIP信令的通道。示例中创建了一个UDP传输,监听5060端口。在实际项目中,你可能需要同时创建UDP和TCP传输(端口可相同或不同),以兼容不同的网络环境或服务器要求。TLS传输则用于加密的信令。
    // 创建UDP传输 TransportConfig udpCfg; udpCfg.port = 5060; ep.transportCreate(PJSIP_TRANSPORT_UDP, udpCfg); // 创建TCP传输,监听同一端口(系统会分配不同套接字) ep.transportCreate(PJSIP_TRANSPORT_TCP, udpCfg);

2.2 Account:你的SIP身份

Account类代表一个SIP账号(或称为用户代理)。你需要通过继承这个类并重写其虚函数(回调函数)来接收事件通知,比如注册状态变化、来电等。

关键点与实操心得:

  • 必须子类化:你不会直接使用Account类,而是必须创建它的子类。最重要的回调是onRegState(注册状态)和onIncomingCall(来电通知)。这是PJSIP事件驱动模型的核心。
  • 配置的复杂性AccountConfig对象包含了账号的所有配置:SIP URI(idUri)、注册服务器地址(registrarUri)、认证信息(authCreds)、媒体能力(mediaConfig)等。这里坑很多:
    • idUri格式必须是完整的sip:user@domain
    • AuthCredInfo的构造参数:认证类型(通常是“digest”)、域名(realm,如果服务器未指定可以用“*”通配)、用户名、数据类型(0表示纯文本密码)、密码。
    • 媒体配置(如是否启用视频、使用的编解码器优先级)也在这里设置,它决定了你的终端能支持什么类型的通话。
  • 对象所有权与内存管理:示例中使用new创建MyAccount对象,并在最后delete。在PJSUA2中,C++对象是原生对象,你需要自己管理生命周期。确保在Endpoint销毁之前,所有相关的AccountCall对象都已被妥善销毁,否则在libDestroy()时可能会访问已释放的内存导致崩溃。一种更现代的做法是使用std::unique_ptr来管理。

2.3 Call:一次通话的抽象

当你有来电或去电时,PJSIP会为你创建一个Call对象。和Account一样,你需要子类化Call来接收该次通话的特定事件,如媒体状态变化、呼叫状态变化(振铃、接通、挂断)、DTMF按键等。

关键点与实操心得:

  • 两种创建方式
    1. 接收来电:当Account::onIncomingCall回调被触发时,参数中会包含一个Call对象,这个对象已经由库创建好了。你只需要保存它(通常用智能指针),并调用answer()来接听,或hangup()来拒绝。
    2. 发起去电:你需要自己new一个你的MyCall对象,然后调用makeCall()方法,传入目标SIP URI。
  • 媒体流管理:通话建立后,Call对象提供了访问其音频、视频媒体的接口。你可以通过getAudioMedia()获取音频流对象,将其连接到会议桥进行混音或录音。这是实现通话录音、保持、转接等功能的基础。
  • 状态管理Call对象有复杂的生命周期状态(PJSIP_INV_STATE_NULL,CALLING,INCOMING,EARLY,CONNECTING,CONFIRMED,DISCONNECTED)。务必在onCallState回调中正确处理这些状态,并在DISCONNECTED状态后安全地清理Call对象。

2.4 AudioMedia 与 VideoMedia:操控音视频的把手

这两个类代表了抽象的音频和视频媒体流。它们本身不产生数据,而是作为“端口”,可以连接到会议桥或其他媒体端口,形成媒体处理链路。

关键点与实操心得:

  • 会议桥(Conference Bridge):这是一个核心概念。PJSIP内部有一个虚拟的混音器。每个AudioMedia都可以连接到会议桥。你的麦克风采集的音频作为一个AudioMedia连接到桥,扬声器播放的音频也从桥获取数据。通话对端的音频流也作为一个AudioMedia连接到同一个桥。这样,所有音频就在桥里自动混合了。对于点对点通话,你通常不需要手动操作桥。
  • 高级操作:通过AudioMedia对象,你可以实现:
    • 录音:创建一个AudioMediaRecorder并连接到通话的AudioMedia
    • 放音:创建一个AudioMediaPlayer播放WAV文件,并连接到会议桥或通话的AudioMedia(实现彩铃或语音提示)。
    • 音频转发/IVR:将多个AudioMedia相互连接,构建复杂的音频处理流程。
  • 线程安全:大部分AudioMediaVideoMedia的操作不是线程安全的。它们应该在PJSIP库的工作线程上下文(通常是在回调函数中)或在你通过Endpointutil工具类锁住内部锁之后进行。盲目地从GUI线程直接调用这些方法会导致随机崩溃。

3. 从“Hello World”到一个最小化可运行客户端

现在,我们超越那个简单的注册示例,构建一个具备基本呼叫功能的最小化C++客户端。这个客户端能注册到SIP服务器,能拨号,能接听,并处理基本的挂断。我会把每一步的意图和潜在陷阱讲清楚。

3.1 项目结构与编译环境搭建

首先,你需要成功编译PJSIP库。官方推荐使用它们的configuremake系统。对于桌面Linux或macOS,这相对直接。对于Windows,你需要Visual Studio和它的NMake。这里以Linux为例,但要点是通用的。

编译要点:

# 1. 获取代码 git clone https://github.com/pjsip/pjproject.git cd pjproject # 2. 配置,启用PJSUA2 ./configure --enable-shared --disable-sound --disable-video --prefix=/usr/local # 解释参数: # --enable-shared: 生成动态库,便于链接。 # --disable-sound: 首次编译可先禁用声音设备,避免依赖问题。 # --disable-video: 首次编译可先禁用视频,简化流程。 # --prefix: 安装路径。 # 3. 编译并安装 make dep && make sudo make install

编译后,关键的头文件通常在/usr/local/include,库文件在/usr/local/lib。你需要确保你的编译器能找到pjsua2.hpplibpjsua2.so(或.a文件)。

在CMakeLists.txt中链接(示例):

cmake_minimum_required(VERSION 3.10) project(MySipClient) set(CMAKE_CXX_STANDARD 11) # 寻找PJSIP库,这里假设你安装在了标准路径或通过环境变量指定 find_path(PJSUA2_INCLUDE_DIR pjsua2.hpp PATH_SUFFIXES pjlib-util pjsip) find_library(PJSUA2_LIBRARY NAMES pjsua2) include_directories(${PJSUA2_INCLUDE_DIR} ${PJSUA2_INCLUDE_DIR}/..) # 头文件路径可能有多层 add_executable(MySipClient main.cpp) target_link_libraries(MySipClient ${PJSUA2_LIBRARY} pthread dl) # 注意:PJSIP可能依赖其他库如ssl、crypto(如果启用TLS),需要一并链接。

3.2 实现核心事件处理类

我们创建两个类:MyAccountMyCall

// my_account.hpp #include <pjsua2.hpp> #include <iostream> #include <memory> #include <map> using namespace pj; class MyCall; class MyAccount : public Account { public: std::map<int, std::shared_ptr<MyCall>> activeCalls; // 管理当前活跃通话 virtual void onRegState(OnRegStateParam &prm) override { AccountInfo ai = getInfo(); std::cout << "[Account] Registration state changed. Code: " << prm.code << ", Reason: " << prm.reason << std::endl; std::cout << "[Account] Is registered? " << (ai.regIsActive ? "YES" : "NO") << std::endl; // 这里可以更新UI状态或触发重连逻辑 } virtual void onIncomingCall(OnIncomingCallParam &iprm) override { std::cout << "[Account] Incoming call from: " << iprm.callId << std::endl; // 创建我们的Call对象来包装这个新呼叫 auto call = std::make_shared<MyCall>(*this, iprm.callId); CallInfo ci; try { ci = call->getInfo(); } catch(Error& e) { std::cout << "[Account] Failed to get call info: " << e.info() << std::endl; return; } std::cout << "[Account] Remote URI: " << ci.remoteUri << std::endl; // 保存到映射表,用callId作为键 activeCalls[iprm.callId] = call; // 在实际应用中,这里应该弹出界面让用户选择接听或拒绝。 // 为了示例,我们自动接听。 std::cout << "[Account] Auto-answering call..." << std::endl; CallOpParam op; op.statusCode = PJSIP_SC_OK; call->answer(op); } };
// my_call.hpp #include <pjsua2.hpp> #include <iostream> #include “my_account.hpp” class MyCall : public Call { private: MyAccount &myAcc; // 持有所属账号的引用 public: MyCall(MyAccount &acc, int call_id) : Call(acc, call_id), myAcc(acc) {} virtual ~MyCall() { std::cout << "[Call] Call object destroyed, ID: " << getId() << std::endl; // 从账号的活跃通话列表中移除自己 myAcc.activeCalls.erase(getId()); } virtual void onCallState(OnCallStateParam &prm) override { CallInfo ci; try { ci = getInfo(); } catch(Error& e) { std::cout << "[Call] Error getting call info: " << e.info() << std::endl; return; } std::cout << "[Call] State changed to: " << ci.stateText << " (" << ci.lastStatusCode << " " << ci.lastReason << ")" << std::endl; if (ci.state == PJSIP_INV_STATE_DISCONNECTED) { // 通话结束,准备销毁对象。 // 注意:我们不能在这里直接delete this,因为可能还在库的回调栈中。 // 我们依靠shared_ptr的引用计数,当外部不再持有该对象时,它会自动被销毁。 // 更安全的做法是通知主线程或管理器来异步清理。 std::cout << "[Call] Call disconnected. Cleaning up." << std::endl; // 例如,可以发送一个事件到主循环 } else if (ci.state == PJSIP_INV_STATE_CONFIRMED) { std::cout << "[Call] Call is answered and active!" << std::endl; // 通话接通,可以开始处理媒体,比如开始录音(如果需要) } } virtual void onCallMediaState(OnCallMediaStateParam &prm) override { CallInfo ci = getInfo(); // 遍历通话中的所有媒体流 for (unsigned i = 0; i < ci.media.size(); ++i) { if (ci.media[i].type == PJMEDIA_TYPE_AUDIO) { AudioMedia *aud_med = (AudioMedia *)getMedia(i); if (aud_med) { // 获取通话的音频媒体,并连接到会议桥(这一步通常是自动的,但这里演示如何获取) try { // 实际上,对于简单的双向通话,库已经自动处理了连接。 // 这里我们可以获取到它,用于可能的扩展操作,如录音。 std::cout << "[Call] Audio media index: " << i << " is active." << std::endl; // 示例:如果你想手动操作,可以将它连接到另一个端口 // Endpoint::instance().audDevManager().getPlaybackDevMedia().startTransmit(*aud_med); } catch(Error& e) { std::cout << "[Call] Error handling audio media: " << e.info() << std::endl; } } } } } };

3.3 主程序骨架与流程控制

主程序需要初始化库、创建账号、并进入一个事件循环(否则程序会直接退出)。

// main.cpp #include “my_account.hpp” #include “my_call.hpp” #include <pjsua2.hpp> #include <iostream> #include <atomic> #include <signal.h> #include <unistd.h> // for sleep using namespace pj; std::atomic<bool> g_quit{false}; void signal_handler(int sig) { g_quit = true; std::cout << "\n[Main] Signal received, shutting down..." << std::endl; } int main(int argc, char *argv[]) { // 注册信号处理,优雅退出 signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); Endpoint ep; try { // 1. 创建库实例 ep.libCreate(); // 2. 初始化端点配置 EpConfig ep_cfg; // 可以在这里配置日志级别、UA信息等 ep_cfg.logConfig.level = 4; // 设置日志级别为INFO ep_cfg.uaConfig.userAgent = "MySipClient/1.0"; ep.libInit(ep_cfg); // 3. 创建传输层 TransportConfig tcfg; tcfg.port = 5060; // 本地SIP端口 // 允许在同一个端口上绑定UDP和TCP ep.transportCreate(PJSIP_TRANSPORT_UDP, tcfg); // ep.transportCreate(PJSIP_TRANSPORT_TCP, tcfg); // 如需TCP则取消注释 // 4. 启动库 ep.libStart(); std::cout << "[Main] PJSUA2 started successfully." << std::endl; // 5. 创建并配置账号 MyAccount *acc = new MyAccount; // 使用智能指针更好,此处为演示 AccountConfig acfg; // !!! 重要:这里需要替换成你真实的SIP服务器信息 !!! acfg.idUri = "sip:1001@your.sip.server.com"; acfg.regConfig.registrarUri = "sip:your.sip.server.com"; acfg.sipConfig.authCreds.push_back( AuthCredInfo("digest", "*", "1001", 0, "your_password") ); // 媒体配置:启用哪些编解码器(优先级从高到低) acfg.mediaConfig.sndClockRate = 16000; // 音频时钟率,常用8000, 16000 acfg.mediaConfig.audioFramePtime = 20; // 每帧音频时长(ms),影响延迟 // 编解码器优先级:数字越小优先级越高 acfg.mediaConfig.codecCfgs.push_back(CodecConfig("pcma", 130)); // G.711 A-law acfg.mediaConfig.codecCfgs.push_back(CodecConfig("pcmu", 129)); // G.711 μ-law acfg.mediaConfig.codecCfgs.push_back(CodecConfig("opus/48000/2", 128)); // Opus acc->create(acfg); std::cout << "[Main] Account created, registering..." << std::endl; // 6. 主事件循环 std::cout << "[Main] Entering main loop. Press Ctrl+C to quit." << std::endl; while (!g_quit) { // Endpoint的handleEvents()方法会处理pending的事件,需要定期调用。 // 参数是最大等待时间(毫秒)。设为10ms是一个合理的值,平衡了响应速度和CPU占用。 ep.handleEvents(10); // 在这里,你可以添加自己的业务逻辑,比如检查用户输入(命令行)。 // 例如,一个简单的命令行拨号接口: static bool promptShown = false; if (!promptShown && acc->getInfo().regIsActive) { std::cout << "\n[Main] Registration successful! You can now make a call." << std::endl; std::cout << "Enter destination SIP URI (e.g., sip:1002@server.com) or 'q' to quit: "; promptShown = true; } // 注意:在实际GUI应用中,事件循环由GUI框架(Qt, GTK等)驱动, // 你需要在单独的线程或定时器中调用 handleEvents。 } // 7. 清理阶段 std::cout << "[Main] Shutting down..." << std::endl; // 先删除账号,触发注销 delete acc; // 等待一小段时间让注销请求发出(非必须,但更安全) ep.handleEvents(100); // 显式停止并销毁库 ep.libDestroy(); } catch (Error &err) { std::cerr << "[Main] Exception caught: " << err.info() << std::endl; return 1; } std::cout << "[Main] Exit." << std::endl; return 0; }

3.4 编译与初步测试

将以上三个文件放在同一目录,使用之前的CMake或直接使用g++编译:

g++ -std=c++11 -o mysipclient main.cpp my_account.cpp my_call.cpp \ -I/usr/local/include \ -L/usr/local/lib -lpjsua2 -lpjsua -lpjsip-ua -lpjsip-simple -lpjsip \ -lpjmedia-codec -lpjmedia -lpjmedia-videodev -lpjmedia-audiodev \ -lpjnath -lpjlib-util -lpj -lssl -lcrypto -lsrtp -lresample -lspeex -lopus \ -luuid -lm -lrt -lpthread -ldl

注意:链接库的顺序和名称可能因平台和编译选项而异。使用pkg-config(如果安装时生成了.pc文件)可以简化此过程:g++ ... $(pkg-config --cflags --libs libpjproject)

运行前,请确保修改main.cpp中的SIP服务器地址、用户名和密码。然后运行./mysipclient。如果看到注册成功的消息,说明基础链路已经打通。你可以用另一个SIP客户端(如MicroSIP, Linphone)呼叫你的账号,这个程序会自动接听。目前还无法主动拨号,我们将在下一部分扩展。

4. 功能扩展与实战问题排查

一个基本的客户端骨架有了,但要投入实用,还需要添加主动呼叫、挂断、DTMF发送、音量调节等功能。同时,实际部署中会遇到各种网络和设备问题。

4.1 实现主动呼叫与用户交互

我们需要修改主循环,使其能够接收用户输入并发起呼叫。同时,完善MyCall类,提供挂断等方法。

首先,在MyAccount中添加发起呼叫的方法:

// 在 MyAccount 类定义中添加 public: std::shared_ptr<MyCall> makeCall(const std::string &destUri) { std::cout << "[Account] Making call to: " << destUri << std::endl; auto call = std::make_shared<MyCall>(*this); CallOpParam prm(true); // true 表示默认使用音频 try { call->makeCall(destUri, prm); activeCalls[call->getId()] = call; return call; } catch (Error &err) { std::cerr << "[Account] Failed to make call: " << err.info() << std::endl; return nullptr; } }

然后,修改主程序的事件循环,加入简单的命令行解析:

// 在主循环中,替换掉之前的简单提示 while (!g_quit) { ep.handleEvents(10); // 简单的非阻塞标准输入检查(Linux/macOS示例) fd_set fds; struct timeval tv = {0, 0}; FD_ZERO(&fds); FD_SET(STDIN_FILENO, &fds); int rv = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv); if (rv > 0 && FD_ISSET(STDIN_FILENO, &fds)) { std::string input; std::getline(std::cin, input); if (input == "q" || input == "quit") { g_quit = true; } else if (input.substr(0, 4) == "call") { // 格式: call sip:1002@server.com std::string dest = input.substr(5); if (!dest.empty()) { auto call = acc->makeCall(dest); if (!call) { std::cout << "Call initiation failed." << std::endl; } } } else if (input.substr(0, 4) == "hang") { // 挂断当前活跃通话(简化处理,挂断第一个) if (!acc->activeCalls.empty()) { auto it = acc->activeCalls.begin(); CallOpParam op; it->second->hangup(op); std::cout << "Hanging up call ID: " << it->first << std::endl; } } else { std::cout << "Commands: call <uri>, hang, q" << std::endl; } } }

4.2 常见问题与调试技巧实录

在实际使用中,你会遇到各种各样的问题。下面是我总结的一些常见坑点和排查方法。

问题1:编译时链接错误,找不到pj_init等符号。

  • 原因:链接库的顺序不对。PJSIP的库之间有依赖关系,必须按照从高层到低层的顺序链接。
  • 解决:使用官方推荐的链接顺序,或者直接使用pkg-config。如果手动链接,可以参考以下大致顺序(从具体到抽象):-lpjsua2 -lpjsua -lpjsip-ua -lpjsip-simple -lpjsip -lpjmedia-codec -lpjmedia-videodev -lpjmedia-audiodev -lpjmedia -lpjnath -lpjlib-util -lpj

问题2:运行时崩溃,错误信息模糊。

  • 原因A:对象生命周期管理错误。在Endpoint销毁后,还尝试访问AccountCall对象。
  • 解决:确保销毁顺序:先销毁所有Call,再销毁所有Account,最后调用ep.libDestroy()。使用智能指针(std::shared_ptr)管理对象所有权,并注意避免循环引用(例如Call持有Account的引用,而非shared_ptr)。
  • 原因B:跨线程调用PJSUA2 API。大部分PJSUA2方法不是线程安全的。
  • 解决:所有对PJSUA2对象的操作(除了Endpoint::handleEvents()Endpoint::util里的工具方法),最好都在同一个线程(通常是主线程)执行,或者使用Endpoint::util()提供的锁机制。如果你在回调函数(如onIncomingCall)中,你已经在库的工作线程中,可以安全操作当前对象。

问题3:注册失败,返回408(超时)或403(禁止)。

  • 排查步骤
    1. 开启详细日志:在EpConfig中设置ep_cfg.logConfig.level = 5(DEBUG级别),ep_cfg.logConfig.consoleLevel = 4(INFO级别)。查看控制台输出的SIP消息。
    2. 检查网络连通性:确认客户端能ping通SIP服务器地址和端口(默认5060)。使用telnet server.com 5060测试端口是否开放。
    3. 检查账号信息idUriregistrarUri、用户名、密码、域名(realm)是否正确。抓包工具(如Wireshark)是终极武器,可以看到原始的SIP REGISTER请求和服务器响应,从中找到失败原因。
    4. 检查防火墙/NAT:如果客户端在NAT后,可能需要配置STUN服务器。在EpConfiguaConfig里设置stunServer
      ep_cfg.uaConfig.stunServer.push_back("stun.stunserver.org:3478");

问题4:能注册,但无法通话,没有声音。

  • 排查步骤
    1. 检查媒体协商:在日志中搜索“SDP offer/answer”。确认双方有共同的编解码器(如PCMA/PCMU/Opus)。你的AccountConfig.mediaConfig.codecCfgs优先级列表可能和对方不匹配。
    2. 检查音频设备:PJSIP是否能找到可用的音频设备?在初始化后,可以尝试打印音频设备列表:
      AudDevManager &adm = ep.audDevManager(); std::cout << "Capture devices: " << adm.getDevCount() << std::endl;
    3. 检查RTP流:通话建立后,是否在收发RTP包?日志中会有RTP统计信息。如果收不到对方的RTP,可能是NAT穿透失败。除了STUN,可能还需要启用ICE或TURN。
      // 在EpConfig中启用ICE ep_cfg.uaConfig.iceConfig.enableIce = true;
    4. 会议桥连接:虽然简单通话是自动的,但如果你做了自定义的媒体操作(如录音),可能意外断开了音频流与会议桥的连接。确保接通后,通话的音频流是连接到会议桥的。

问题5:在通话中,程序占用CPU过高。

  • 原因Endpoint::handleEvents()的调用间隔太短,或者在一个紧密循环中调用而没有延时。
  • 解决:给handleEvents()传递一个合理的等待时间,比如10-100毫秒。这个调用会阻塞等待事件,减少空转。在上面的主循环中,我们用了10ms。对于纯事件驱动的GUI程序,你可以在一个定时器(如100ms间隔)中调用它,而不是死循环。

4.3 进阶功能点简介

当基础通话稳定后,你可以考虑添加更多功能:

  • 通话保持(Hold):调用CallsetHold()方法。
  • 通话转移(Transfer):使用Callxfer()方法。
  • DTMF发送:使用CalldialDtmf()方法。
  • 音量调节:通过AudioMediaadjustRxLevel()adjustTxLevel()方法。
  • 通话录音:创建AudioMediaRecorder对象,并将其连接到通话的AudioMedia流。
  • 视频支持:在配置中启用视频(ep_cfg.uaConfig.vidInAutoTransmit = true),并处理VideoMediaVideoWindow对象。
  • 状态呈现(Presence):通过Buddy类订阅好友状态。

5. 项目集成与部署思考

将PJSIP集成到你的实际C++项目中,远不止是编译和链接。你需要考虑架构设计。

线程模型:PJSIP内部有自己的工作线程。你的UI线程(或主逻辑线程)与PJSIP的交互必须通过线程安全的方式。推荐的做法是:

  1. 主线程负责创建、初始化、销毁Endpoint,并定期调用handleEvents()
  2. 所有从PJSIP回调(如onIncomingCall,onCallState)中触发的业务逻辑,如果需要更新UI,必须通过消息队列、事件总线或线程安全的方式通知到UI线程。
  3. 所有从UI线程发起的主动操作(如拨号、挂断),应直接调用PJSUA2 API(因为它们最终会向PJSIP的工作线程发送消息),但要注意前面提到的对象生命周期。

错误处理:PJSUA2大量使用C++异常(pj::Error)。务必用try...catch块包裹所有可能抛出异常的操作,并进行合理的错误恢复,而不是让整个程序崩溃。

日志与监控:将PJSIP的日志重定向到你自己的日志系统。你可以实现一个自定义的LogWriter并注册到Endpoint,这样就能将日志统一管理,并写入文件或网络。

资源清理:确保在程序退出前,所有资源都被有序释放。特别是当程序被强制终止(如Ctrl+C)时,应在信号处理函数中设置标志位,让主循环优雅退出,完成销毁流程。

最后,PJSIP是一个相当底层的库,功能强大但也复杂。从“Hello World”到生产可用的产品,中间有很长的路要走,需要仔细阅读文档,大量测试,尤其是针对异常网络情况(丢包、延迟、NAT类型变化)的测试。希望这篇深入解析能为你点亮第一盏灯,让你在音视频通信的C++开发之路上,少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 8:38:34

AI Agent环境构建:操作系统级运行时设计与实操

1. 项目概述&#xff1a;这不是搭个“环境”&#xff0c;而是给AI Agent装上操作系统 “Agent -Practice--build your agent environment”这个标题乍看像一句命令行提示&#xff0c;甚至有点像某个IDE报错后的临时笔记——但它背后藏着当前AI工程落地最核心的瓶颈&#xff1a;…

作者头像 李华
网站建设 2026/7/16 8:38:29

强化学习模型蒸馏:PowerOPD幂变换解决奖励无界问题

在强化学习模型蒸馏的实际应用中&#xff0c;OPD&#xff08;On-Policy Distillation&#xff09;的奖励函数设计一直是影响性能的关键因素。很多开发者在复现论文或项目落地时&#xff0c;都会遇到奖励值边界不确定、训练不稳定等问题。本文将从实际工程角度深入分析OPD奖励设…

作者头像 李华
网站建设 2026/7/16 8:37:27

《龍魂系统·民间出创意国家出维护永恒锁》

《龍魂系统民间出创意国家出维护永恒锁》 不是协议&#xff0c;是锁。 不是起草&#xff0c;是焊死。 不是闭环&#xff0c;是无限圈圈。P0 焊死 不可修订 民间出创意&#xff0c;国家出维护&#xff0c;此分永不改UID9622一票否决权&#xff0c;此权永不撤16人格对应16部门&am…

作者头像 李华
网站建设 2026/7/16 8:37:25

前端部署实战:Vercel上线全流程与环境变量配置详解

1. 这不是“点一下就上线”的魔法&#xff0c;而是网站落地前最后一道实操关卡 “从 0 到 1 做网站&#xff5c;第 5 步&#xff1a;把网站部署上线”——这个标题里藏着一个被严重低估的真相&#xff1a; 部署不是终点&#xff0c;而是网站真正开始呼吸的第一口空气。 我带过…

作者头像 李华
网站建设 2026/7/16 8:36:55

大语言模型指令遵循能力评测与改进策略分析

最近测试了一款新版ChatGPT对话学英语工具&#xff0c;发现虽然单词学习功能尚可&#xff0c;但指令遵循能力存在明显缺陷。比如明确要求不使用"很棒很棒"这类表达&#xff0c;模型却完全纠正不过来&#xff0c;还会输出一些莫名其妙的句子。这种情况其实反映了当前大…

作者头像 李华