news 2026/4/7 12:31:36

使用CMake集成pjsip到Android项目图解说明

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用CMake集成pjsip到Android项目图解说明

手把手教你用 CMake 把 pjsip 接入 Android 项目

你有没有遇到过这样的场景:想在自己的 App 里加个语音通话功能,查了一圈发现pjsip是最靠谱的选择——开源、稳定、功能全。但一上手就卡住了:编译报错、链接失败、ABI 不兼容、日志出不来……更头疼的是,网上教程大多还停留在Android.mk时代,而你现在用的是 Google 官方主推的CMake

别急。这篇文章就是为了解决这个“最后一公里”问题而写的。

我们不堆术语,不讲空话,只聚焦一件事:如何干净利落地把 pjsip 通过 CMake 集成进你的 Android 项目。从交叉编译到库引用,从头文件配置到运行调试,一步步带你走通全流程。


为什么选 pjsip?它真的适合移动端吗?

先说结论:是的,而且非常合适

虽然 pjsip 是用纯 C 写的,看起来像是“上古时代”的技术栈,但它有几个关键优势让它至今仍是 VoIP 领域的香饽饽:

  • 轻量高效:内存占用低,启动快,特别适合资源受限的移动设备。
  • 协议完整:SIP + SDP + RTP/RTCP + STUN/TURN/ICE 全都有,一套搞定信令和媒体。
  • 音频处理强:内置回声消除(AEC)、抖动缓冲、多种编解码器支持(G.711, Opus 等)。
  • 跨平台能力拉满:Linux、Windows、iOS、Android 全能跑,代码复用率高。

更重要的是,它提供了PJSUA2这个高级封装 API,让你不用直接操作底层 SIP 消息就能实现注册、拨号、接听等核心功能,开发效率大大提升。

不过也得承认:它的构建系统对新手不太友好。尤其是要在 Android 上跑起来,得先做交叉编译,生成.a静态库,再导入项目。这一步就把很多人挡在门外了。

好消息是,一旦你把库编好,后续集成反而很简单——只要你会写CMakeLists.txt


编译 pjsip:先给它“瘦身”,再交叉编译

第一步:准备源码与环境

git clone https://github.com/pjsip/pjproject.git cd pjproject

推荐使用v2.13 或更新版本,对 Android NDK r21+ 支持更好。

接着设置几个关键环境变量(以 Linux/macOS 为例):

export ANDROID_NDK_ROOT=/path/to/your/android-ndk export TARGET_ABI=armeabi-v7a # 可改为 arm64-v8a, x86_64 等 export ANDROID_API=21

⚠️ 注意:NDK 路径不能有空格!否则 configure 脚本会挂掉。

第二步:创建 config_site.h —— 让 pjsip 适应 Android

pjlib/include/pj/目录下新建一个config_site.h文件,内容如下:

#define PJ_CONFIG_ANDROID 1 #include <pj/config_site_sample.h> // 关闭视频相关模块(省空间) #define PJMEDIA_HAS_VIDEO 0 // 启用 Opus 编解码器(可选) #define PJMEDIA_HAS_OPUS_CODEC 1 // 使用 OpenSL ES 作为音频后端 #define PJMEDIA_AUDIO_DEV_HAS_OPENSL 1 // 禁用浮点异常检测(Android 上不需要) #define PJ_HAS_FLOATING_POINT 1

这个文件的作用是告诉 pjsip:“我现在跑在 Android 上,请按这里的配置来裁剪功能。”比如关掉视频可以减少约 3MB 的体积。

第三步:运行 configure-android 脚本

pjsip 官方提供了一个专门用于 Android 的配置脚本:

./configure-android \ --use-ndk-cflags \ --target=armv7a-linux-androideabi \ --with-android-ndk=$ANDROID_NDK_ROOT \ --with-android-api=$ANDROID_API \ --disable-video \ --enable-g711-codec \ --enable-opus-codec

如果你要编译arm64-v8a,则换成:

--target=aarch64-linux-android

执行成功后会生成 Makefile。然后开始编译:

make dep && make clean && make -j8

等待几分钟,你会在pjlib/lib,pjsip/lib等目录看到一堆.a文件,例如:

  • libpjsua2.a
  • libpjmedia.a
  • libpjsip-core.a
  • libpjlib-util.a

这些就是你需要的静态库。

第四步:批量处理多 ABI

为了支持不同手机架构,你需要为每个 ABI 分别编译一次。建议写个脚本自动化完成:

#!/bin/bash ABIS=("armeabi-v7a" "arm64-v8a" "x86" "x86_64") for abi in "${ABIS[@]}"; do echo "Building for $abi" ./configure-android --target=... # 根据 abi 设置参数 make clean && make -j8 cp -r lib ../prebuilt/$abi/ done

最后整理出这样一个目录结构:

app/src/main/jniLibs/ ├── armeabi-v7a/ │ ├── libpjsua2.a │ ├── libpjmedia.a │ └── ... ├── arm64-v8a/ │ ├── libpjsua2.a │ └── ... └── ...

同时把所有头文件复制到cpp/include/下:

app/src/main/cpp/include/ ├── pjsip/ ├── pjmedia/ ├── pjnath/ └── pj/

现在,准备工作完成了。


CMake 怎么写?这才是重点!

接下来就是在项目中配置CMakeLists.txt,让 Gradle 能顺利找到并链接这些库。

先看顶层 CMakeLists.txt

cmake_minimum_required(VERSION 3.18) project("sip-native") # 使用 C99 和 C++17 set(CMAKE_C_STANDARD 99) set(CMAKE_CXX_STANDARD 17) # 启用异常和 RTTI(PJSUA2 需要用到) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions -frtti") # 导入预编译的 pjsip 静态库 macro(import_pjsip_lib NAME) add_library(${NAME} STATIC IMPORTED) set_target_properties(${NAME} PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/lib${NAME}.a ) endmacro() # 批量导入所有模块 import_pjsip_lib(pjlib) import_pjsip_lib(pjlib_util) import_pjsip_lib(pjsip_core) import_pjsip_lib(pjsip_simple) import_pjsip_lib(pjsip_ua) import_pjsip_lib(pjsua2) import_pjsip_lib(pjnath) import_pjsip_lib(pjmedia) # 创建本地 JNI 封装库 add_library(native-sip SHARED native_sip.cpp SipManager.cpp ) # 包含头文件路径 target_include_directories(native-sip PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/include/pj ${CMAKE_SOURCE_DIR}/include/pjmedia ${CMAKE_SOURCE_DIR}/include/pjsip ) # 链接所有依赖库 target_link_libraries(native-sip # pjsip 模块 pjlib pjlib_util pjsip_core pjsip_simple pjsip_ua pjsua2 pjnath pjmedia # 系统库 log android m atomic )

几点关键说明:

  • IMPORTED_LOCATION中的${ANDROID_ABI}是 CMake 自动传入的,对应当前构建的目标架构。
  • 必须启用-fexceptions-frtti,因为 PJSUA2 内部用了 C++ 异常和动态类型识别。
  • atomic库必须显式链接,否则某些 ARM 设备上会出现undefined reference to __atomic_load_8错误。
  • logandroid是 Android 原生系统库,分别用于日志输出和访问 Java 层对象。

在 build.gradle 中启用 CMake

确保app/build.gradle里有这段:

android { compileSdk 34 defaultConfig { applicationId "com.example.voipapp" minSdk 21 targetSdk 34 versionCode 1 versionName "1.0" externalNativeBuild { cmake { cppFlags "-std=c++17", "-fexceptions", "-frtti" abiFilters 'armeabi-v7a', 'arm64-v8a' } } } externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.18.1' } } }

这样 Gradle 就知道去哪找 CMake 脚本,并自动为每个 ABI 构建对应的.so


JNI 层怎么封?别让回调崩了 App

Java 层不可能直接调用 C 函数,必须通过 JNI 桥接。你可以创建一个SipManager类来做封装。

示例:native_sip.cpp 中的关键函数

#include <jni.h> #include <pjsua2.hpp> #include <android/log.h> #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "SIP-JNI", __VA_ARGS__) using namespace pj; static JavaVM *g_jvm = nullptr; static jobject g_listener = nullptr; class MyCall : public Call { public: MyCall(Account &acc, int call_id = PJSUA_INVALID_ID) : Call(acc, call_id) {} void onCallState(OnCallStateParam &param) override { if (g_listener && g_jvm) { JNIEnv *env; bool detach = false; if (g_jvm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) { g_jvm->AttachCurrentThread(&env, nullptr); detach = true; } jclass cls = env->GetObjectClass(g_listener); jmethodID mid = env->GetMethodID(cls, "onCallStateChanged", "(I)V"); env->CallVoidMethod(g_listener, mid, getState()); if (detach) g_jvm->DetachCurrentThread(); } } };

注意线程安全问题:pjsip 内部有自己的 I/O 线程,回调发生时可能不在 JVM 主线程,所以要用GetEnv判断是否需要AttachCurrentThread

注册 JVM 和事件监听器

extern "C" JNIEXPORT void JNICALL Java_com_example_SipService_nativeInit(JNIEnv *env, jobject thiz) { env->GetJavaVM(&g_jvm); // 保存全局引用,防止被回收 g_listener = env->NewGlobalRef(thiz); // 初始化 pjsip Endpoint::instance().libCreate(); EpConfig cfg; Endpoint::instance().libInit(cfg); } extern "C" JNIEXPORT void JNICALL Java_com_example_SipService_nativeStartAccount(JNIEnv *env, jobject thiz, jstring sipUri) { const char *uri = env->GetStringUTFChars(sipUri, nullptr); AccountConfig acfg; acfg.setIdUri(std::string(uri)); // ... 配置服务器地址、认证信息等 Account *acc = new Account(); acc->create(acfg); env->ReleaseStringUTFChars(sipUri, uri); }

Java 层就可以这样调用:

public class SipService extends Service { static { System.loadLibrary("native-sip"); } public native void nativeInit(); public native void nativeStartAccount(String sipUri); @Override public void onCreate() { super.onCreate(); nativeInit(); nativeStartAccount("sip:user@server.com"); } // 回调方法 public void onCallStateChanged(int state) { Log.d("SIP", "Call state: " + state); // 更新 UI 或发送广播 } }

常见坑点与解决方案

问题表现解法
编译时报__atomic未定义undefined reference to __atomic_load_8target_link_libraries中加上atomic
日志不出PJ_LOG_LEVEL=5也没输出调用LogWriter::setLevel(5);并确保 stdout 重定向
音频断续/延迟高对话听起来一卡一卡使用 OpenSL ES,采样率设为 16kHz,帧大小 20ms
JNI 崩溃JNI DETECTED ERROR IN APPLICATION所有非主线程回调前先AttachCurrentThread
库太大APK 增大十几 MBconfig_site.h中关闭视频、H.264、G.729 等非必要模块

还有一个实用技巧:如果只想保留核心语音功能,可以在编译时禁用大量模块:

--disable-video --disable-speex-codec --disable-gsm-codec --without-libffi

这样最终打包下来的.a文件总和可以控制在6~8MB左右。


最后一点思考:这条路还能走多远?

也许你会问:现在 WebRTC 都这么火了,为啥还要折腾 pjsip?

答案是:场景不同,需求不同

  • 如果你要做浏览器互通、高清视频会议、数据通道传输,那当然是 WebRTC 更合适。
  • 但如果你对接的是传统 PBX、IMS 网络、SIP 运营商线路,或者只是做一个简单的软电话客户端,pjsip 依然是最成熟、最稳定的方案之一

而且它足够轻量,学习成本相对可控,社区资料丰富,文档齐全。配合 CMake + JNI 的现代 Android 开发模式,完全可以打造出专业级的 VoIP 应用。

更重要的是,这套集成思路不仅适用于 pjsip,也适用于其他任何需要引入第三方 C/C++ 静态库的项目。掌握了这一套流程,你就等于打通了 native 开发的任督二脉。


如果你正在尝试将 SIP 协议集成到 App 中,不妨试试这条路。我已经把它跑通了,也希望你能少踩点坑。

有任何疑问或更好的优化建议,欢迎留言交流。

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

FreeMove:彻底解决C盘空间危机的智能文件迁移神器

FreeMove&#xff1a;彻底解决C盘空间危机的智能文件迁移神器 【免费下载链接】FreeMove Move directories without breaking shortcuts or installations 项目地址: https://gitcode.com/gh_mirrors/fr/FreeMove C盘爆满已经成为困扰无数Windows用户的头号难题。当系统…

作者头像 李华
网站建设 2026/4/5 9:52:12

Bypass Paywalls Chrome Clean:终极免费内容解锁方案详解

Bypass Paywalls Chrome Clean&#xff1a;终极免费内容解锁方案详解 【免费下载链接】bypass-paywalls-chrome-clean 项目地址: https://gitcode.com/GitHub_Trending/by/bypass-paywalls-chrome-clean 在信息爆炸的时代&#xff0c;付费墙已成为获取高质量内容的巨大…

作者头像 李华
网站建设 2026/4/3 9:26:17

Qwen3-Embedding-4B降本增效:中小企业部署实战指南

Qwen3-Embedding-4B降本增效&#xff1a;中小企业部署实战指南 随着大模型技术的普及&#xff0c;向量嵌入&#xff08;Embedding&#xff09;已成为信息检索、语义搜索、推荐系统等应用的核心组件。然而&#xff0c;对于资源有限的中小企业而言&#xff0c;如何在保证性能的同…

作者头像 李华
网站建设 2026/4/5 19:20:26

QQ音乐加密文件终极解码指南:一键实现跨平台播放

QQ音乐加密文件终极解码指南&#xff1a;一键实现跨平台播放 【免费下载链接】qmcdump 一个简单的QQ音乐解码&#xff08;qmcflac/qmc0/qmc3 转 flac/mp3&#xff09;&#xff0c;仅为个人学习参考用。 项目地址: https://gitcode.com/gh_mirrors/qm/qmcdump 还在为QQ音…

作者头像 李华
网站建设 2026/4/3 11:31:28

DeepSeek-Prover-V1.5:63.5%准确率的数学证明开源神器

DeepSeek-Prover-V1.5&#xff1a;63.5%准确率的数学证明开源神器 【免费下载链接】DeepSeek-Prover-V1.5-Base DeepSeek-Prover-V1.5-Base&#xff1a;提升数学证明效率的开源利器&#xff0c;融合强化学习与蒙特卡洛树搜索&#xff0c;助力Lean 4定理证明。在miniF2F测试集上…

作者头像 李华
网站建设 2026/3/31 5:38:02

小白也能懂的语音合成技术:IndexTTS-2-LLM从0开始

小白也能懂的语音合成技术&#xff1a;IndexTTS-2-LLM从0开始 在人工智能快速发展的今天&#xff0c;语音合成&#xff08;Text-to-Speech, TTS&#xff09;技术已经不再是实验室里的高深课题&#xff0c;而是逐渐走进日常应用的重要工具。无论是智能客服、有声读物&#xff0…

作者头像 李华