1. 从一通电话说起:为什么要在两个平台上折腾PJSIP做VoIP或者实时音视频通信的开发者,大概率都绕不开PJSIP这个库。它是一套用C语言写的、完整实现了SIP、SDP、RTP、STUN、TURN等协议的通信框架,还自带了一套媒体处理栈。很多软电话、呼叫中心、对讲系统、会议终端,底层跑的都是它。但真正让人头疼的,往往不是写业务逻辑,而是第一步——把它在Linux和Windows上编译出来。我见过太多人在这一步卡住:Linux上configure报一堆缺库,Windows上打开工程文件发现一堆链接错误,或者编译出来的库跑起来一调用就崩。更麻烦的是,网上能找到的资料要么太老,要么只讲一个平台,要么默认你已经是个编译老手。所以这篇内容,我想把PJSIP在Linux和Windows两个平台上的编译安装过程,从头到尾、掰开揉碎地讲一遍,包括每一步为什么这么做、参数怎么选、踩过的坑怎么绕。这篇适合谁看?如果你是要在Linux服务器上部署SIP服务端,或者要在Windows上做客户端软电话,又或者需要交叉编译到嵌入式设备,那这篇内容基本能覆盖你的需求。我会尽量用大白话解释每一步的意图,让刚接触PJSIP的人也能跟着走下来,同时也会分享一些只有实际编译过多次才会知道的细节。先说一个整体认知:PJSIP的编译体系是"配置脚本+Makefile"的组合,Linux下靠configure生成Makefile,Windows下则提供了Visual Studio的工程文件。两个平台的依赖项、编译选项、产物结构都不一样,所以必须分开处理。下面我按平台拆开讲,最后再讲两个平台通用的配置裁剪和验证方法。2. Linux平台编译:从依赖检查到产物验证2.1 编译前的依赖清单与检查逻辑在Linux上编译PJSIP,第一步不是急着下载源码,而是先把依赖理清楚。PJSIP的核心库本身依赖不多,但如果你要启用音视频功能,依赖就会变多。我一般把依赖分成三类:基础编译工具、音频相关库、视频相关库。基础编译工具包括gcc、g++、make、autoconf、automake、libtool、pkg-config。这些在大多数发行版上默认就有,但pkg-config经常被忽略,而PJSIP的configure脚本会用它来查找第三方库。你可以用下面这条命令快速检查:which gcc g++ make autoconf automake libtool pkg-config如果哪个没输出路径,就说明缺了,需要先装上。音频方面,PJSIP默认使用自己的音频设备抽象层,但在Linux上通常会依赖ALSA或者PulseAudio。如果你要做软电话,建议至少装一个。视频方面,如果要用H.264编解码,需要libx264和libavcodec(FFmpeg的一部分)。不过视频这块我建议第一次编译先不启用,等基础库跑通了再加,否则依赖问题会混在一起,很难排查。这里有个经验:很多人编译失败是因为系统里装了多个版本的库,pkg-config找到的版本和实际链接的版本不一致。我习惯在configure之前先跑一遍pkg-config --list-all | grep -E "alsa|pulse|x264|libav",确认能找到的库版本,心里有数。2.2 configure参数的选择与背后逻辑PJSIP的configure脚本参数非常多,但常用的就那么几个。我一般会先跑一次./configure --help看看当前版本支持哪些选项,因为不同版本的PJSIP参数会有差异。下面是我常用的配置命令,以启用音频、禁用视频为例:./configure --prefix=/usr/local/pjsip \ --enable-shared \ --disable-video \ --disable-sdl \ --disable-ffmpeg \ --disable-v4l2 \ --disable-openh264 \ --with-alsa \ --disable-opencore-amr \ --disable-speex \ --disable-ilbc \ --disable-g7221 \ CFLAGS="-O2 -fPIC" \ CXXFLAGS="-O2 -fPIC"这里逐个解释一下为什么这么选。--prefix指定安装路径,我习惯装到/usr/local/pjsip,这样不会污染系统目录,卸载也方便。--enable-shared生成动态库,如果你的程序需要动态链接就用它;如果要做静态链接,就改成--disable-shared --enable-static。--disable-video这一串是先把视频相关全部关掉,因为视频依赖多、编译时间长,第一次编译先保证核心功能跑通。--with-alsa是启用ALSA音频支持,如果你用PulseAudio,可以换成--with-pulse。--disable-opencore-amr、--disable-speex这些是禁用一些可选的编解码器,它们需要额外的第三方库,第一次编译建议先关掉。CFLAGS和CXXFLAGS里的-fPIC很重要,如果你要生成动态库或者后续要链接到其他动态库,没有这个选项会报"relocation R_X86_64_PC32 against symbol"之类的错误。注意:如果你在64位系统上编译32位程序,需要额外指定-m32,并且确保装了32位的依赖库。这个场景在嵌入式开发中比较常见,但配置起来比较麻烦,建议直接用交叉编译工具链。configure跑完之后,它会输出一个总结,告诉你哪些功能启用了、哪些没启用。这个总结一定要仔细看,如果某个你需要的功能显示"no",就要回去检查对应的依赖是不是没装好。2.3 编译过程中的常见报错与排查链路configure通过之后,执行make dep && make开始编译。这一步最容易出问题,我按报错类型分几类讲。第一类是头文件找不到,报错类似fatal error: alsa/asoundlib.h: No such file or directory。这说明ALSA的开发包没装。在Debian/Ubuntu上是libasound2-dev,在CentOS/RHEL上是alsa-lib-devel。装完之后要重新跑configure,因为configure阶段会检测头文件是否存在。第二类是链接错误,报错类似undefined reference to 'snd_pcm_open'。这通常是库路径没配好,或者pkg-config没找到对应的库。你可以用pkg-config --libs alsa看看输出,如果为空,说明.pc文件不在默认搜索路径里。解决办法是设置PKG_CONFIG_PATH环境变量,或者直接在configure时用LDFLAGS指定库路径。第三类是编译到一半报内存不足,尤其是开多线程编译(make -j)的时候。PJSIP的某些源文件比较大,编译时占内存较多。如果机器内存小,建议用make -j2或者直接make单线程编译。我遇到过在1GB内存的虚拟机上用make -j4直接OOM的情况,换成单线程就过了。第四类是编译成功但运行时报error while loading shared libraries。这是因为动态库装到了/usr/local/pjsip/lib,但系统默认不搜索这个路径。解决办法有两个:一是把该路径加到/etc/ld.so.conf.d/下然后跑ldconfig,二是在运行程序时设置LD_LIBRARY_PATH。我推荐第一种,一劳永逸。排查的时候有个技巧:把make的输出重定向到文件,比如make 2>&1 | tee build.log,这样报错信息不会刷屏,可以慢慢看。另外,PJSIP的编译输出里,真正有用的报错通常在最后几行,前面的都是编译进度。2.4 安装与产物结构说明编译通过后,执行make install安装到--prefix指定的目录。安装完成后,目录结构大致如下:目录内容bin一些示例程序和工具include头文件,按模块分目录lib静态库和动态库文件share文档和示例配置头文件里,pjlib、pjlib-util、pjnath、pjmedia、pjsip、pjsip-simple、pjsip-ua、pjsua这些是核心模块。链接的时候,通常需要按依赖顺序链接,比如-lpjsua -lpjsip-ua -lpjsip-simple -lpjsip -lpjmedia -lpjnath -lpjlib-util -lpj。顺序错了会报未定义符号。我建议在安装完成后,写一个最小的测试程序验证一下。比如初始化PJSIP的库,创建一个SIP端点,然后销毁。这个测试程序能跑通,说明库本身没问题,后续再写业务逻辑就放心了。3. Windows平台编译:工具链选择与工程配置3.1 Visual Studio版本选择与工程文件对应关系Windows上编译PJSIP,官方推荐用Visual Studio。但PJSIP的工程文件是按VS版本分目录存放的,比如build/vs2022、build/vs2019等。你用的VS版本要和工程文件目录对应,否则打开工程会提示升级,升级过程中可能出问题。我一般建议用VS2019或VS2022,这两个版本对C99和C++11的支持比较好,PJSIP的新版本也主要针对这两个版本维护。如果你用的是更老的VS2015,可能会遇到一些语法不支持的问题,需要手动改代码。打开工程文件之前,先确认几件事:一是Windows SDK版本,PJSIP的工程默认可能指向某个特定版本的SDK,如果你没装,需要改工程属性;二是平台工具集,要和你的VS版本匹配;三是字符集,PJSIP默认用多字节字符集,不要改成Unicode,否则会有大量编译错误。工程文件在build/vs2022目录下,有一个samples.sln和pjsip.sln。pjsip.sln是核心库的解决方案,samples.sln是示例程序的解决方案。我建议先编译pjsip.sln,把库编出来,再编译示例验证。3.2 依赖库的预编译与路径配置Windows上编译PJSIP,最大的坑是第三方依赖。PJSIP在Windows上需要一些预编译的库,比如OpenSSL(用于TLS)、DirectX SDK(用于视频采集)、Windows Core Audio(用于音频)。这些库的路径需要在工程属性里配置。以OpenSSL为例,你需要先下载或编译OpenSSL的Windows版本,然后在一个公共的配置文件中指定路径。PJSIP的工程里有一个config_site.h文件,位于pjlib/include/pj/目录下。你可以在这个文件里定义一些宏,比如#define PJ_HAS_SSL_SOCK 1来启用SSL。但光定义宏还不够,还需要在工程属性里把OpenSSL的include路径和lib路径加进去。我通常的做法是:在解决方案目录下建一个third_party文件夹,把OpenSSL、FFmpeg等依赖按include和lib分目录放好,然后在工程属性里用相对路径引用。这样整个解决方案可以拷贝到其他机器上,只要路径结构一致就能编译。注意:PJSIP的Windows工程默认可能没有启用SSL,如果你需要TLS支持,除了改config_site.h,还要在链接器输入里加上libssl.lib和libcrypto.lib。这两个库的名字在不同OpenSSL版本里可能不一样,1.1.x是libssl.lib和libcrypto.lib,3.x也是类似的命名,但要注意版本匹配。3.3 编译配置的取舍:Debug还是ReleaseVisual Studio的工程默认有Debug和Release两个配置。Debug配置会带调试信息,方便排查问题,但运行速度慢、库体积大。Release配置做了优化,适合发布。我建议第一次编译用Debug,因为如果编译出错,Debug下的报错信息更详细,而且一些运行时检查能帮你发现潜在问题。但要注意,Debug和Release的库不能混用。如果你的程序是Release,链接的PJSIP库也必须是Release。混用会导致运行时崩溃,而且这种崩溃往往很难定位。我见过有人Debug的exe链接了Release的lib,结果一调用就崩,查了半天才发现是配置不匹配。编译的时候,先编译pjlib、pjlib-util、pjnath、pjmedia、pjsip、pjsip-simple、pjsip-ua、pjsua这些项目,顺序就是依赖顺序。VS的解决方案会处理依赖关系,但如果你手动编译单个项目,就要注意顺序。编译完成后,库文件在lib目录下,按配置分Debug和Release子目录。3.4 Windows下常见的链接错误与解决思路Windows下最常见的链接错误是LNK2019: unresolved external symbol。这个错误的原因通常是库没链接上,或者链接顺序不对。PJSIP的库之间有依赖关系,比如pjsua依赖pjsip-ua,pjsip-ua依赖pjsip-simple,以此类推。在链接器输入里,被依赖的库要放在后面。另一个常见错误是LNK2038: mismatch detected for 'RuntimeLibrary'。这是因为不同库用的运行时库版本不一致,比如有的用/MT,有的用/MD。解决办法是统一所有项目的运行时库设置。PJSIP的工程默认用的是/MT(静态运行时),如果你的程序用/MD,就需要改PJSIP的工程设置,或者改你自己的程序设置。还有一个坑是字符集问题。PJSIP的代码里大量使用了char*和ANSI字符串,如果你把工程字符集改成Unicode,会报一堆cannot convert from 'const char*' to 'LPCWSTR'的错误。所以千万不要改字符集,保持多字节字符集。如果遇到LNK1104: cannot open file 'xxx.lib',说明库路径没配好。检查工程属性里的"附加库目录"是否包含了所有依赖库的路径。我习惯把所有依赖库的路径都加到"附加库目录"里,然后在"附加依赖项"里只写库文件名,这样路径变了只需要改一个地方。4. 两个平台通用的配置裁剪与功能开关4.1 config_site.h的作用与常用配置项不管在Linux还是Windows上,config_site.h都是PJSIP编译配置的核心文件。它位于pjlib/include/pj/目录下,默认是空的,你需要根据自己的需求往里加宏定义。这个文件会在编译时被包含,影响整个库的功能开关。常用的配置项有这些:PJ_HAS_SSL_SOCK控制是否启用SSL/TLS,做安全通信必须开;PJMEDIA_HAS_VIDEO控制是否启用视频,不用视频就关掉,能省不少编译时间;PJMEDIA_HAS_SPEEX_CODEC、PJMEDIA_HAS_G7221_CODEC这些控制具体编解码器的启用;PJSUA_HAS_VIDEO控制pjsua高层API是否支持视频。我一般会建一个自己的config_site.h,内容类似这样:#define PJ_HAS_SSL_SOCK 1 #define PJMEDIA_HAS_VIDEO 0 #define PJMEDIA_HAS_SPEEX_CODEC 0 #define PJMEDIA_HAS_G7221_CODEC 0 #define PJMEDIA_HAS_ILBC_CODEC 0 #define PJSUA_HAS_VIDEO 0这样配置的好处是编译快、依赖少。等基础功能跑通了,再按需打开视频和编解码器。注意,改完config_site.h后,需要重新编译整个库,因为很多宏会影响头文件里的结构体定义。4.2 编解码器与媒体功能的按需启用PJSIP默认启用的编解码器包括G.711、G.722、GSM、iLBC等。其中G.711是必选的,因为它是SIP通话的默认编解码器。G.722是宽带编解码器,音质更好但占带宽更多。GSM和iLBC是低码率编解码器,适合网络条件差的情况。如果你要做的是语音通话,建议至少保留G.711和G.722。如果带宽有限,可以加上GSM或iLBC。但每加一个编解码器,编译时间和库体积都会增加。我一般先只保留G.711,跑通之后再按需加。视频方面,PJSIP支持H.264、VP8、VP9等编解码器,但需要额外的第三方库。H.264需要libx264,VP8/VP9需要libvpx。这些库在Windows上编译比较麻烦,建议直接用预编译版本。视频采集方面,Windows上用DirectShow,Linux上用V4L2,这些在configure或工程配置里都有对应选项。提示:如果你只是做语音通话,强烈建议把视频全部关掉。视频相关的代码量很大,编译一次可能要十几分钟,而且依赖问题多。等语音跑通了,再单独折腾视频。4.3 编译产物的验证:写一个最小可运行程序编译安装完成后,怎么确认库没问题?我习惯写一个最小程序,做三件事:初始化PJSIP库、创建一个SIP端点、销毁并退出。这个程序能跑通,说明库的编译和链接都没问题。在Linux上,编译命令大概是这样:gcc -o test_pjsip test_pjsip.c \ -I/usr/local/pjsip/include \ -L/usr/local/pjsip/lib \ -lpjsua -lpjsip-ua -lpjsip-simple -lpjsip \ -lpjmedia -lpjnath -lpjlib-util -lpj \ -lpthread -lm -lssl -lcrypto在Windows上,把库文件加到工程里,配置好include路径和lib路径,编译运行即可。如果程序能正常初始化并退出,说明库没问题。如果报错,根据报错信息回去检查对应的配置。这个最小程序还有一个作用:它可以作为你后续开发的起点。你可以在它的基础上逐步加功能,比如注册账号、拨打电话、发送消息等。每加一个功能就编译运行一次,这样出问题容易定位。5. 踩坑实录:那些文档里不会写的细节5.1 Linux下动态库路径的持久化配置前面提到过,动态库装到非标准路径后,运行时会找不到。除了设置LD_LIBRARY_PATH,更稳妥的做法是加到系统库配置里。具体操作是在/etc/ld.so.conf.d/目录下新建一个文件,比如pjsip.conf,内容就一行/usr/local/pjsip/lib,然后执行ldconfig。这样所有程序都能找到这个路径下的库,不用每次设置环境变量。但要注意,如果你是在容器里编译,容器重启后ldconfig的配置可能会丢失。所以最好把这一步写进Dockerfile或者启动脚本里。另外,如果你没有root权限,改不了/etc/ld.so.conf.d/,那就只能用LD_LIBRARY_PATH了。可以在~/.bashrc里加上export LD_LIBRARY_PATH=/usr/local/pjsip/lib:$LD_LIBRARY_PATH,这样每次登录自动生效。还有一个细节:ldconfig只对动态库生效,静态库不需要。如果你编译的是静态库,链接的时候直接把.a文件路径写全就行,运行时不需要额外配置。5.2 Windows下运行库与字符集的隐性冲突Windows下最隐蔽的坑是运行库冲突。PJSIP的工程默认用/MT,也就是静态链接C运行时。如果你的程序用/MD,链接的时候会报LNK2038。这个错误信息不太直观,很多人第一次遇到不知道怎么回事。解决办法有两种:一是把PJSIP的工程改成/MD,重新编译;二是把你的程序改成/MT。我一般推荐第一种,因为/MD是VS的默认设置,而且动态运行时库更省内存。改的方法是:在工程属性里,C/C++ -> 代码生成 -> 运行时库,改成"多线程DLL (/MD)"。注意Debug和Release要分别改,Debug对应/MDd,Release对应/MD。字符集的问题前面提过,这里再强调一下:PJSIP的代码里有很多char*和ANSI API调用,如果工程字符集设成Unicode,编译器会把字符串字面量当成宽字符,导致类型不匹配。所以工程属性里"字符集"一定要选"使用多字节字符集"。如果你在代码里需要用到宽字符,自己手动转换,不要改工程设置。5.3 交叉编译到嵌入式平台的注意事项虽然这篇主要讲Linux和Windows,但很多人编译PJSIP是为了交叉编译到嵌入式设备。这里简单提几个要点。交叉编译时,configure需要指定--host参数,比如--host=arm-linux-gnueabihf。同时要设置CC和CXX环境变量指向交叉编译器。依赖库也需要交叉编译版本,不能直接用宿主机的库。比如ALSA,你需要先交叉编译ALSA的库,然后在configure时用--with-alsa并指定路径。这个过程比较繁琐,建议用Buildroot或Yocto这类构建系统来管理依赖。还有一个坑是字节序和对齐。有些嵌入式平台是big-endian,或者对内存对齐要求严格,PJSIP的某些代码可能假设了小端序或宽松对齐。如果编译出来的库在设备上跑起来崩溃,可以检查一下config_site.h里有没有相关的字节序配置。5.4 编译时间优化与增量编译技巧PJSIP的完整编译比较耗时,尤其是开了视频和多个编解码器之后。我一般会做几件事来加速。第一,用make -j并行编译,核数越多越快。但前面说过,内存小的时候要控制并行数。第二,只编译需要的模块,比如不做视频就关掉视频相关模块。第三,用ccache缓存编译结果,第二次编译同样的代码会快很多。增量编译方面,如果你只改了config_site.h,理论上需要重新编译所有文件,因为很多文件都包含这个头文件。但如果你只改了某个模块的代码,可以进到那个模块的目录单独make。比如只改了pjsip模块,就cd pjsip && make,这样只编译这个模块,快很多。Windows下,VS的增量编译做得比较好,改一个文件只编译一个文件。但如果你改了config_site.h,VS可能不会自动重新编译所有依赖它的文件,需要手动"重新生成解决方案"。这一点要注意,否则可能出现改了配置但没生效的情况。6. 从编译产物到实际项目:链接与集成的经验6.1 链接顺序与依赖关系的处理PJSIP的库之间有严格的依赖关系,链接顺序错了就会报未定义符号。正确的顺序是从高层到低层:pjsua->pjsip-ua->pjsip-simple->pjsip->pjmedia->pjnath->pjlib-util->pj。这个顺序在Linux的gcc链接命令里很重要,因为gcc的链接器是从左到右解析符号的,如果被依赖的库放在前面,链接器处理到它的时候还不知道后面会用到它,就会报未定义。Windows的链接器对顺序的要求没那么严格,但为了保险,也建议按这个顺序写。在VS的工程属性里,"附加依赖项"里按顺序列出这些库就行。另外,如果启用了SSL,还要加上ssl和crypto库。如果用了音频,可能还要加asound(Linux)或winmm(Windows)。这些系统库一般放在最后。6.2 在自己的项目中集成PJSIP的目录结构建议我建议在项目里这样组织PJSIP相关的文件:project/ third_party/ pjsip/ include/ # PJSIP头文件 lib/ # PJSIP库文件 src/ main.c CMakeLists.txt # 或Makefile这样PJSIP作为第三方库独立存放,不和自己的代码混在一起。编译的时候,include路径指向third_party/pjsip/include,库路径指向third_party/pjsip/lib。如果以后要升级PJSIP版本,只需要替换这个目录,不用改自己的代码。用CMake的话,可以写一个FindPJSIP.cmake模块,自动查找PJSIP的头文件和库。这样跨平台的时候,Linux和Windows可以用同一套CMake脚本,只需要在Windows上指定库文件的后缀(.lib)和Linux上指定(.a或.so)。6.3 版本升级时的兼容性检查清单PJSIP的版本更新比较频繁,升级的时候要注意兼容性。我一般会检查这几项:一是API有没有变化,特别是一些核心结构体的字段有没有增删;二是config_site.h里的宏有没有改名或废弃;三是库的依赖关系有没有变化,比如某个模块被合并或拆分了。升级之前,先备份当前的config_site.h和编译脚本。然后下载新版本,对比config_site.h的默认内容,看看有没有新增的必选宏。再编译一个最小程序,跑通之后再替换到项目里。如果项目里用了PJSIP的高级功能,比如视频、会议、录音,要逐个测试,确保功能正常。还有一点:不同版本的PJSIP,库文件名可能不一样。比如某个版本把pjsip-ua合并到了pjsip里,链接的时候就不需要单独链接pjsip-ua了。升级后如果报找不到某个库,先检查一下新版本的库文件列表。7. 一些个人体会编译PJSIP这件事,说难不难,说简单也不简单。难点不在于技术本身,而在于信息太分散,每个平台的坑还不一样。我的经验是:第一次编译不要贪多,先把核心功能跑通,视频、SSL、额外编解码器这些都先关掉。等最小程序能跑了,再一个一个加功能,每加一个就验证一次。这样出问题的时候,范围很小,容易定位。另外,编译日志一定要保存。不管是Linux的make输出还是Windows的VS输出,都重定向到文件里。出问题的时候,搜索"error"关键字,通常最后几条就是根因。不要被前面一大堆warning吓到,warning一般不影响编译结果。最后,如果你在编译过程中遇到了这篇没覆盖的问题,我的建议是先去PJSIP的官方文档和邮件列表里搜一下,很多问题别人已经遇到过了。如果搜不到,再自己排查。排查的时候,从最简单的配置开始,逐步加功能,这样能最快找到是哪个配置项导致的。
1. 从一通电话说起:为什么要在两个平台上折腾PJSIP做VoIP或者实时音视频通信的开发者,大概率都绕不开PJSIP这个库。它是一套用C语言写的、完整实现了SIP、SDP、RTP、STUN、TURN等协议的通信框架,还自带了一套媒体处理栈。很多软电话、呼叫中心、对讲系统、会议终端,底层跑的都是它。但真正让人头疼的,往往不是写业务逻辑,而是第一步——把它在Linux和Windows上编译出来。我见过太多人在这一步卡住:Linux上configure报一堆缺库,Windows上打开工程文件发现一堆链接错误,或者编译出来的库跑起来一调用就崩。更麻烦的是,网上能找到的资料要么太老,要么只讲一个平台,要么默认你已经是个编译老手。所以这篇内容,我想把PJSIP在Linux和Windows两个平台上的编译安装过程,从头到尾、掰开揉碎地讲一遍,包括每一步为什么这么做、参数怎么选、踩过的坑怎么绕。这篇适合谁看?如果你是要在Linux服务器上部署SIP服务端,或者要在Windows上做客户端软电话,又或者需要交叉编译到嵌入式设备,那这篇内容基本能覆盖你的需求。我会尽量用大白话解释每一步的意图,让刚接触PJSIP的人也能跟着走下来,同时也会分享一些只有实际编译过多次才会知道的细节。先说一个整体认知:PJSIP的编译体系是"配置脚本+Makefile"的组合,Linux下靠configure生成Makefile,Windows下则提供了Visual Studio的工程文件。两个平台的依赖项、编译选项、产物结构都不一样,所以必须分开处理。下面我按平台拆开讲,最后再讲两个平台通用的配置裁剪和验证方法。2. Linux平台编译:从依赖检查到产物验证2.1 编译前的依赖清单与检查逻辑在Linux上编译PJSIP,第一步不是急着下载源码,而是先把依赖理清楚。PJSIP的核心库本身依赖不多,但如果你要启用音视频功能,依赖就会变多。我一般把依赖分成三类:基础编译工具、音频相关库、视频相关库。基础编译工具包括gcc、g++、make、autoconf、automake、libtool、pkg-config。这些在大多数发行版上默认就有,但pkg-config经常被忽略,而PJSIP的configure脚本会用它来查找第三方库。你可以用下面这条命令快速检查:which gcc g++ make autoconf automake libtool pkg-config如果哪个没输出路径,就说明缺了,需要先装上。音频方面,PJSIP默认使用自己的音频设备抽象层,但在Linux上通常会依赖ALSA或者PulseAudio。如果你要做软电话,建议至少装一个。视频方面,如果要用H.264编解码,需要libx264和libavcodec(FFmpeg的一部分)。不过视频这块我建议第一次编译先不启用,等基础库跑通了再加,否则依赖问题会混在一起,很难排查。这里有个经验:很多人编译失败是因为系统里装了多个版本的库,pkg-config找到的版本和实际链接的版本不一致。我习惯在configure之前先跑一遍pkg-config --list-all | grep -E "alsa|pulse|x264|libav",确认能找到的库版本,心里有数。2.2 configure参数的选择与背后逻辑PJSIP的configure脚本参数非常多,但常用的就那么几个。我一般会先跑一次./configure --help看看当前版本支持哪些选项,因为不同版本的PJSIP参数会有差异。下面是我常用的配置命令,以启用音频、禁用视频为例:./configure --prefix=/usr/local/pjsip \ --enable-shared \ --disable-video \ --disable-sdl \ --disable-ffmpeg \ --disable-v4l2 \ --disable-openh264 \ --with-alsa \ --disable-opencore-amr \ --disable-speex \ --disable-ilbc \ --disable-g7221 \ CFLAGS="-O2 -fPIC" \ CXXFLAGS="-O2 -fPIC"这里逐个解释一下为什么这么选。--prefix指定安装路径,我习惯装到/usr/local/pjsip,这样不会污染系统目录,卸载也方便。--enable-shared生成动态库,如果你的程序需要动态链接就用它;如果要做静态链接,就改成--disable-shared --enable-static。--disable-video这一串是先把视频相关全部关掉,因为视频依赖多、编译时间长,第一次编译先保证核心功能跑通。--with-alsa是启用ALSA音频支持,如果你用PulseAudio,可以换成--with-pulse。--disable-opencore-amr、--disable-speex这些是禁用一些可选的编解码器,它们需要额外的第三方库,第一次编译建议先关掉。CFLAGS和CXXFLAGS里的-fPIC很重要,如果你要生成动态库或者后续要链接到其他动态库,没有这个选项会报"relocation R_X86_64_PC32 against symbol"之类的错误。注意:如果你在64位系统上编译32位程序,需要额外指定-m32,并且确保装了32位的依赖库。这个场景在嵌入式开发中比较常见,但配置起来比较麻烦,建议直接用交叉编译工具链。configure跑完之后,它会输出一个总结,告诉你哪些功能启用了、哪些没启用。这个总结一定要仔细看,如果某个你需要的功能显示"no",就要回去检查对应的依赖是不是没装好。2.3 编译过程中的常见报错与排查链路configure通过之后,执行make dep && make开始编译。这一步最容易出问题,我按报错类型分几类讲。第一类是头文件找不到,报错类似fatal error: alsa/asoundlib.h: No such file or directory。这说明ALSA的开发包没装。在Debian/Ubuntu上是libasound2-dev,在CentOS/RHEL上是alsa-lib-devel。装完之后要重新跑configure,因为configure阶段会检测头文件是否存在。第二类是链接错误,报错类似undefined reference to 'snd_pcm_open'。这通常是库路径没配好,或者pkg-config没找到对应的库。你可以用pkg-config --libs alsa看看输出,如果为空,说明.pc文件不在默认搜索路径里。解决办法是设置PKG_CONFIG_PATH环境变量,或者直接在configure时用LDFLAGS指定库路径。第三类是编译到一半报内存不足,尤其是开多线程编译(make -j)的时候。PJSIP的某些源文件比较大,编译时占内存较多。如果机器内存小,建议用make -j2或者直接make单线程编译。我遇到过在1GB内存的虚拟机上用make -j4直接OOM的情况,换成单线程就过了。第四类是编译成功但运行时报error while loading shared libraries。这是因为动态库装到了/usr/local/pjsip/lib,但系统默认不搜索这个路径。解决办法有两个:一是把该路径加到/etc/ld.so.conf.d/下然后跑ldconfig,二是在运行程序时设置LD_LIBRARY_PATH。我推荐第一种,一劳永逸。排查的时候有个技巧:把make的输出重定向到文件,比如make 2>&1 | tee build.log,这样报错信息不会刷屏,可以慢慢看。另外,PJSIP的编译输出里,真正有用的报错通常在最后几行,前面的都是编译进度。2.4 安装与产物结构说明编译通过后,执行make install安装到--prefix指定的目录。安装完成后,目录结构大致如下:目录内容bin一些示例程序和工具include头文件,按模块分目录lib静态库和动态库文件share文档和示例配置头文件里,pjlib、pjlib-util、pjnath、pjmedia、pjsip、pjsip-simple、pjsip-ua、pjsua这些是核心模块。链接的时候,通常需要按依赖顺序链接,比如-lpjsua -lpjsip-ua -lpjsip-simple -lpjsip -lpjmedia -lpjnath -lpjlib-util -lpj。顺序错了会报未定义符号。我建议在安装完成后,写一个最小的测试程序验证一下。比如初始化PJSIP的库,创建一个SIP端点,然后销毁。这个测试程序能跑通,说明库本身没问题,后续再写业务逻辑就放心了。3. Windows平台编译:工具链选择与工程配置3.1 Visual Studio版本选择与工程文件对应关系Windows上编译PJSIP,官方推荐用Visual Studio。但PJSIP的工程文件是按VS版本分目录存放的,比如build/vs2022、build/vs2019等。你用的VS版本要和工程文件目录对应,否则打开工程会提示升级,升级过程中可能出问题。我一般建议用VS2019或VS2022,这两个版本对C99和C++11的支持比较好,PJSIP的新版本也主要针对这两个版本维护。如果你用的是更老的VS2015,可能会遇到一些语法不支持的问题,需要手动改代码。打开工程文件之前,先确认几件事:一是Windows SDK版本,PJSIP的工程默认可能指向某个特定版本的SDK,如果你没装,需要改工程属性;二是平台工具集,要和你的VS版本匹配;三是字符集,PJSIP默认用多字节字符集,不要改成Unicode,否则会有大量编译错误。工程文件在build/vs2022目录下,有一个samples.sln和pjsip.sln。pjsip.sln是核心库的解决方案,samples.sln是示例程序的解决方案。我建议先编译pjsip.sln,把库编出来,再编译示例验证。3.2 依赖库的预编译与路径配置Windows上编译PJSIP,最大的坑是第三方依赖。PJSIP在Windows上需要一些预编译的库,比如OpenSSL(用于TLS)、DirectX SDK(用于视频采集)、Windows Core Audio(用于音频)。这些库的路径需要在工程属性里配置。以OpenSSL为例,你需要先下载或编译OpenSSL的Windows版本,然后在一个公共的配置文件中指定路径。PJSIP的工程里有一个config_site.h文件,位于pjlib/include/pj/目录下。你可以在这个文件里定义一些宏,比如#define PJ_HAS_SSL_SOCK 1来启用SSL。但光定义宏还不够,还需要在工程属性里把OpenSSL的include路径和lib路径加进去。我通常的做法是:在解决方案目录下建一个third_party文件夹,把OpenSSL、FFmpeg等依赖按include和lib分目录放好,然后在工程属性里用相对路径引用。这样整个解决方案可以拷贝到其他机器上,只要路径结构一致就能编译。注意:PJSIP的Windows工程默认可能没有启用SSL,如果你需要TLS支持,除了改config_site.h,还要在链接器输入里加上libssl.lib和libcrypto.lib。这两个库的名字在不同OpenSSL版本里可能不一样,1.1.x是libssl.lib和libcrypto.lib,3.x也是类似的命名,但要注意版本匹配。3.3 编译配置的取舍:Debug还是ReleaseVisual Studio的工程默认有Debug和Release两个配置。Debug配置会带调试信息,方便排查问题,但运行速度慢、库体积大。Release配置做了优化,适合发布。我建议第一次编译用Debug,因为如果编译出错,Debug下的报错信息更详细,而且一些运行时检查能帮你发现潜在问题。但要注意,Debug和Release的库不能混用。如果你的程序是Release,链接的PJSIP库也必须是Release。混用会导致运行时崩溃,而且这种崩溃往往很难定位。我见过有人Debug的exe链接了Release的lib,结果一调用就崩,查了半天才发现是配置不匹配。编译的时候,先编译pjlib、pjlib-util、pjnath、pjmedia、pjsip、pjsip-simple、pjsip-ua、pjsua这些项目,顺序就是依赖顺序。VS的解决方案会处理依赖关系,但如果你手动编译单个项目,就要注意顺序。编译完成后,库文件在lib目录下,按配置分Debug和Release子目录。3.4 Windows下常见的链接错误与解决思路Windows下最常见的链接错误是LNK2019: unresolved external symbol。这个错误的原因通常是库没链接上,或者链接顺序不对。PJSIP的库之间有依赖关系,比如pjsua依赖pjsip-ua,pjsip-ua依赖pjsip-simple,以此类推。在链接器输入里,被依赖的库要放在后面。另一个常见错误是LNK2038: mismatch detected for 'RuntimeLibrary'。这是因为不同库用的运行时库版本不一致,比如有的用/MT,有的用/MD。解决办法是统一所有项目的运行时库设置。PJSIP的工程默认用的是/MT(静态运行时),如果你的程序用/MD,就需要改PJSIP的工程设置,或者改你自己的程序设置。还有一个坑是字符集问题。PJSIP的代码里大量使用了char*和ANSI字符串,如果你把工程字符集改成Unicode,会报一堆cannot convert from 'const char*' to 'LPCWSTR'的错误。所以千万不要改字符集,保持多字节字符集。如果遇到LNK1104: cannot open file 'xxx.lib',说明库路径没配好。检查工程属性里的"附加库目录"是否包含了所有依赖库的路径。我习惯把所有依赖库的路径都加到"附加库目录"里,然后在"附加依赖项"里只写库文件名,这样路径变了只需要改一个地方。4. 两个平台通用的配置裁剪与功能开关4.1 config_site.h的作用与常用配置项不管在Linux还是Windows上,config_site.h都是PJSIP编译配置的核心文件。它位于pjlib/include/pj/目录下,默认是空的,你需要根据自己的需求往里加宏定义。这个文件会在编译时被包含,影响整个库的功能开关。常用的配置项有这些:PJ_HAS_SSL_SOCK控制是否启用SSL/TLS,做安全通信必须开;PJMEDIA_HAS_VIDEO控制是否启用视频,不用视频就关掉,能省不少编译时间;PJMEDIA_HAS_SPEEX_CODEC、PJMEDIA_HAS_G7221_CODEC这些控制具体编解码器的启用;PJSUA_HAS_VIDEO控制pjsua高层API是否支持视频。我一般会建一个自己的config_site.h,内容类似这样:#define PJ_HAS_SSL_SOCK 1 #define PJMEDIA_HAS_VIDEO 0 #define PJMEDIA_HAS_SPEEX_CODEC 0 #define PJMEDIA_HAS_G7221_CODEC 0 #define PJMEDIA_HAS_ILBC_CODEC 0 #define PJSUA_HAS_VIDEO 0这样配置的好处是编译快、依赖少。等基础功能跑通了,再按需打开视频和编解码器。注意,改完config_site.h后,需要重新编译整个库,因为很多宏会影响头文件里的结构体定义。4.2 编解码器与媒体功能的按需启用PJSIP默认启用的编解码器包括G.711、G.722、GSM、iLBC等。其中G.711是必选的,因为它是SIP通话的默认编解码器。G.722是宽带编解码器,音质更好但占带宽更多。GSM和iLBC是低码率编解码器,适合网络条件差的情况。如果你要做的是语音通话,建议至少保留G.711和G.722。如果带宽有限,可以加上GSM或iLBC。但每加一个编解码器,编译时间和库体积都会增加。我一般先只保留G.711,跑通之后再按需加。视频方面,PJSIP支持H.264、VP8、VP9等编解码器,但需要额外的第三方库。H.264需要libx264,VP8/VP9需要libvpx。这些库在Windows上编译比较麻烦,建议直接用预编译版本。视频采集方面,Windows上用DirectShow,Linux上用V4L2,这些在configure或工程配置里都有对应选项。提示:如果你只是做语音通话,强烈建议把视频全部关掉。视频相关的代码量很大,编译一次可能要十几分钟,而且依赖问题多。等语音跑通了,再单独折腾视频。4.3 编译产物的验证:写一个最小可运行程序编译安装完成后,怎么确认库没问题?我习惯写一个最小程序,做三件事:初始化PJSIP库、创建一个SIP端点、销毁并退出。这个程序能跑通,说明库的编译和链接都没问题。在Linux上,编译命令大概是这样:gcc -o test_pjsip test_pjsip.c \ -I/usr/local/pjsip/include \ -L/usr/local/pjsip/lib \ -lpjsua -lpjsip-ua -lpjsip-simple -lpjsip \ -lpjmedia -lpjnath -lpjlib-util -lpj \ -lpthread -lm -lssl -lcrypto在Windows上,把库文件加到工程里,配置好include路径和lib路径,编译运行即可。如果程序能正常初始化并退出,说明库没问题。如果报错,根据报错信息回去检查对应的配置。这个最小程序还有一个作用:它可以作为你后续开发的起点。你可以在它的基础上逐步加功能,比如注册账号、拨打电话、发送消息等。每加一个功能就编译运行一次,这样出问题容易定位。5. 踩坑实录:那些文档里不会写的细节5.1 Linux下动态库路径的持久化配置前面提到过,动态库装到非标准路径后,运行时会找不到。除了设置LD_LIBRARY_PATH,更稳妥的做法是加到系统库配置里。具体操作是在/etc/ld.so.conf.d/目录下新建一个文件,比如pjsip.conf,内容就一行/usr/local/pjsip/lib,然后执行ldconfig。这样所有程序都能找到这个路径下的库,不用每次设置环境变量。但要注意,如果你是在容器里编译,容器重启后ldconfig的配置可能会丢失。所以最好把这一步写进Dockerfile或者启动脚本里。另外,如果你没有root权限,改不了/etc/ld.so.conf.d/,那就只能用LD_LIBRARY_PATH了。可以在~/.bashrc里加上export LD_LIBRARY_PATH=/usr/local/pjsip/lib:$LD_LIBRARY_PATH,这样每次登录自动生效。还有一个细节:ldconfig只对动态库生效,静态库不需要。如果你编译的是静态库,链接的时候直接把.a文件路径写全就行,运行时不需要额外配置。5.2 Windows下运行库与字符集的隐性冲突Windows下最隐蔽的坑是运行库冲突。PJSIP的工程默认用/MT,也就是静态链接C运行时。如果你的程序用/MD,链接的时候会报LNK2038。这个错误信息不太直观,很多人第一次遇到不知道怎么回事。解决办法有两种:一是把PJSIP的工程改成/MD,重新编译;二是把你的程序改成/MT。我一般推荐第一种,因为/MD是VS的默认设置,而且动态运行时库更省内存。改的方法是:在工程属性里,C/C++ -> 代码生成 -> 运行时库,改成"多线程DLL (/MD)"。注意Debug和Release要分别改,Debug对应/MDd,Release对应/MD。字符集的问题前面提过,这里再强调一下:PJSIP的代码里有很多char*和ANSI API调用,如果工程字符集设成Unicode,编译器会把字符串字面量当成宽字符,导致类型不匹配。所以工程属性里"字符集"一定要选"使用多字节字符集"。如果你在代码里需要用到宽字符,自己手动转换,不要改工程设置。5.3 交叉编译到嵌入式平台的注意事项虽然这篇主要讲Linux和Windows,但很多人编译PJSIP是为了交叉编译到嵌入式设备。这里简单提几个要点。交叉编译时,configure需要指定--host参数,比如--host=arm-linux-gnueabihf。同时要设置CC和CXX环境变量指向交叉编译器。依赖库也需要交叉编译版本,不能直接用宿主机的库。比如ALSA,你需要先交叉编译ALSA的库,然后在configure时用--with-alsa并指定路径。这个过程比较繁琐,建议用Buildroot或Yocto这类构建系统来管理依赖。还有一个坑是字节序和对齐。有些嵌入式平台是big-endian,或者对内存对齐要求严格,PJSIP的某些代码可能假设了小端序或宽松对齐。如果编译出来的库在设备上跑起来崩溃,可以检查一下config_site.h里有没有相关的字节序配置。5.4 编译时间优化与增量编译技巧PJSIP的完整编译比较耗时,尤其是开了视频和多个编解码器之后。我一般会做几件事来加速。第一,用make -j并行编译,核数越多越快。但前面说过,内存小的时候要控制并行数。第二,只编译需要的模块,比如不做视频就关掉视频相关模块。第三,用ccache缓存编译结果,第二次编译同样的代码会快很多。增量编译方面,如果你只改了config_site.h,理论上需要重新编译所有文件,因为很多文件都包含这个头文件。但如果你只改了某个模块的代码,可以进到那个模块的目录单独make。比如只改了pjsip模块,就cd pjsip && make,这样只编译这个模块,快很多。Windows下,VS的增量编译做得比较好,改一个文件只编译一个文件。但如果你改了config_site.h,VS可能不会自动重新编译所有依赖它的文件,需要手动"重新生成解决方案"。这一点要注意,否则可能出现改了配置但没生效的情况。6. 从编译产物到实际项目:链接与集成的经验6.1 链接顺序与依赖关系的处理PJSIP的库之间有严格的依赖关系,链接顺序错了就会报未定义符号。正确的顺序是从高层到低层:pjsua->pjsip-ua->pjsip-simple->pjsip->pjmedia->pjnath->pjlib-util->pj。这个顺序在Linux的gcc链接命令里很重要,因为gcc的链接器是从左到右解析符号的,如果被依赖的库放在前面,链接器处理到它的时候还不知道后面会用到它,就会报未定义。Windows的链接器对顺序的要求没那么严格,但为了保险,也建议按这个顺序写。在VS的工程属性里,"附加依赖项"里按顺序列出这些库就行。另外,如果启用了SSL,还要加上ssl和crypto库。如果用了音频,可能还要加asound(Linux)或winmm(Windows)。这些系统库一般放在最后。6.2 在自己的项目中集成PJSIP的目录结构建议我建议在项目里这样组织PJSIP相关的文件:project/ third_party/ pjsip/ include/ # PJSIP头文件 lib/ # PJSIP库文件 src/ main.c CMakeLists.txt # 或Makefile这样PJSIP作为第三方库独立存放,不和自己的代码混在一起。编译的时候,include路径指向third_party/pjsip/include,库路径指向third_party/pjsip/lib。如果以后要升级PJSIP版本,只需要替换这个目录,不用改自己的代码。用CMake的话,可以写一个FindPJSIP.cmake模块,自动查找PJSIP的头文件和库。这样跨平台的时候,Linux和Windows可以用同一套CMake脚本,只需要在Windows上指定库文件的后缀(.lib)和Linux上指定(.a或.so)。6.3 版本升级时的兼容性检查清单PJSIP的版本更新比较频繁,升级的时候要注意兼容性。我一般会检查这几项:一是API有没有变化,特别是一些核心结构体的字段有没有增删;二是config_site.h里的宏有没有改名或废弃;三是库的依赖关系有没有变化,比如某个模块被合并或拆分了。升级之前,先备份当前的config_site.h和编译脚本。然后下载新版本,对比config_site.h的默认内容,看看有没有新增的必选宏。再编译一个最小程序,跑通之后再替换到项目里。如果项目里用了PJSIP的高级功能,比如视频、会议、录音,要逐个测试,确保功能正常。还有一点:不同版本的PJSIP,库文件名可能不一样。比如某个版本把pjsip-ua合并到了pjsip里,链接的时候就不需要单独链接pjsip-ua了。升级后如果报找不到某个库,先检查一下新版本的库文件列表。7. 一些个人体会编译PJSIP这件事,说难不难,说简单也不简单。难点不在于技术本身,而在于信息太分散,每个平台的坑还不一样。我的经验是:第一次编译不要贪多,先把核心功能跑通,视频、SSL、额外编解码器这些都先关掉。等最小程序能跑了,再一个一个加功能,每加一个就验证一次。这样出问题的时候,范围很小,容易定位。另外,编译日志一定要保存。不管是Linux的make输出还是Windows的VS输出,都重定向到文件里。出问题的时候,搜索"error"关键字,通常最后几条就是根因。不要被前面一大堆warning吓到,warning一般不影响编译结果。最后,如果你在编译过程中遇到了这篇没覆盖的问题,我的建议是先去PJSIP的官方文档和邮件列表里搜一下,很多问题别人已经遇到过了。如果搜不到,再自己排查。排查的时候,从最简单的配置开始,逐步加功能,这样能最快找到是哪个配置项导致的。