news 2026/3/8 3:59:27

Android平台下pjsip移植实战案例解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android平台下pjsip移植实战案例解析

从零开始:在Android上跑通PJSIP的实战之路

你有没有遇到过这样的场景?项目需要做一个VoIP软电话,要求支持SIP注册、来电弹窗、双向通话,还得稳定穿透各种复杂的NAT网络。市面上的SDK要么太贵,要么功能残缺,还可能夹带私货。于是你把目光投向了开源世界——PJSIP

它名声在外:协议完整、性能强悍、MIT授权免费商用。但真要把它塞进你的Android App里,你会发现,官方文档像一本没写完的操作手册,社区问答散落在十年之前的论坛帖中。编译报错一堆,运行起来不是崩溃就是静音。

别急。这篇文章不讲空话,只说实战。我会带你一步步走过我踩过的所有坑,从环境搭建到最终实现一个能打电话的APK。这不是理论推演,而是我在真实项目中验证过的完整路径。


为什么是 PJSIP?

先回答一个问题:明明有那么多现成的通信库,为什么要选 PJSIP?

因为它是少数能把 SIP 协议栈 + 音频引擎 + NAT穿透 + 编解码全包圆的C语言库。不像某些方案,信令用一个库,媒体处理又要引入FFmpeg和WebRTC子模块,最后包体积飙到20MB起步。

PJSIP 的设计哲学很“嵌入式”:
- 最小可运行配置仅需64KB内存;
- 支持G.711、Opus、iLBC等主流语音编码;
- 内建AEC(回声消除)、VAD(语音检测)、Jitter Buffer;
- 完整实现STUN/TURN/ICE,连DTLS-SRTP都给你备好了。

更重要的是,它允许你完全掌控底层逻辑。比如你想做一款可视门禁系统,只需要语音对讲+远程开门控制,不需要视频渲染那一套重包袱——PJSIP可以轻松裁剪掉视频相关模块,静态链接后整个libpjsip.a加起来不到1.5MB。

这正是我们在某智能楼宇项目中的选择依据。


准备工作:NDK 环境到底怎么配?

很多人第一步就卡住了:PJSIP 是用 Autotools 构建的,而 Android NDK 推荐用 CMake。两者怎么协同?

答案是:让 configure 脚本以为自己在交叉编译Linux程序,实际上指向NDK的工具链

先搞清楚几个关键点:

  1. NDK r21+ 已全面转向 LLVM/Clang,不再推荐使用GCC;
  2. 每个ABI(如arm64-v8a)对应不同的clang前端命令;
  3. --host参数必须与目标架构匹配;
  4. 必须显式指定 sysroot 和 API Level。

举个例子,你要为 arm64-v8a 编译,那编译器路径应该是:

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang

这里的21就是你设定的最低API等级。低于这个版本会链接失败。


自动化脚本才是王道

手动敲几十个参数太容易出错。我封装了一个通用构建脚本,放在项目的scripts/build_pjsip.sh下:

#!/bin/bash export ANDROID_NDK=/home/user/android-ndk-r25b export TARGET_ABI=arm64-v8a export API_LEVEL=21 case $TARGET_ABI in "armeabi-v7a") HOST="arm-linux-androideabi" TOOLCHAIN_SUFFIX="armv7a-linux-androideabi" ;; "arm64-v8a") HOST="aarch64-linux-android" TOOLCHAIN_SUFFIX="aarch64-linux-android" ;; *) echo "Unsupported ABI" exit 1 ;; esac export TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64 export CC="$TOOLCHAIN/bin/$TOOLCHAIN_SUFFIX$API_LEVEL-clang" export CXX="$TOOLCHAIN/bin/$TOOLCHAIN_SUFFIX$API_LEVEL-clang++" export AR="$TOOLCHAIN/bin/$HOST-ar" export LD="$TOOLCHAIN/bin/$HOST-ld" export STRIP="$TOOLCHAIN/bin/$HOST-strip" # 开始配置 ./configure \ --host=$HOST \ --prefix=$(pwd)/output/$TARGET_ABI \ --enable-shared=no \ --enable-static=yes \ --disable-video \ --disable-libwebrtc \ --disable-sound \ --disable-opencore-amr \ --with-android-ndk=$ANDROID_NDK \ --with-sdk-level=$API_LEVEL \ ac_cv_func_strnlen_working=yes \ CFLAGS="-DANDROID -D__ANDROID_API__=$API_LEVEL -fdata-sections -ffunction-sections -Os" \ LDFLAGS="-Wl,-gc-sections" make clean && make -j$(nproc) && make install

✅ 提示:如果你打算集成OpenH264或x264用于未来扩展视频能力,可以把--disable-video去掉,并添加--with-openh264=yes

运行这个脚本后,你会在output/arm64-v8a/lib目录下看到十几个.a文件,包括libpjsua2.a,libpjmedia.a,libpjnath.a等。头文件也会自动复制到include目录。


编译时报错了?这几个坑你肯定遇到过

❌ 错误一:undefined reference to dlopen

这是最常见的链接错误。虽然你在代码里没调用dlopen,但PJSIP内部用了动态加载机制(比如插件式日志模块),需要链接系统的动态库。

解决方法:在LDFLAGS中加入以下库:

-llog -landroid -ldl -latomic

其中:
--llog:打Log用;
--landroid:访问Android原生音频设备要用;
--ldl:解决dlopen问题;
--latomic:某些原子操作依赖。

可以在 configure 命令末尾加上:

LIBS="-llog -landroid -ldl -latomic"

❌ 错误二:error: use of undeclared identifier 'AF_PACKET'

这是因为 PJSIP 默认尝试启用数据链路层抓包功能(用于调试),但在Android上不允许。

解决方法:强制关闭网络嗅探相关特性,在 CFLAGS 中添加:

-DPJ_HAS_TCP=0 -DPJ_SOCK_HAS_PKTINFO=0

或者干脆在config_site.h中定义:

#define PJ_HAS_TCP 0 #define PJ_SOCK_HAS_PKTINFO 0

❌ 错误三:找不到android/log.h

说明SYSROOT没设置对。确保你的 NDK 路径正确,并且在 configure 时传入--with-android-ndk=参数。


JNI封装:如何安全地打通 Java 与 C 的边界?

现在我们有了.a静态库,下一步是要让Java层能调用这些C函数。

核心思路是:写一层JNI胶水代码,把 PJSUA-LIB 的事件模型桥接到 Android 的消息循环中

第一步:初始化 PJSIP 引擎

我们在 Java 层定义一个SipManager类:

public class SipManager { static { System.loadLibrary("pjsip"); } public native int initialize(String sipServer); public native int makeCall(String number); public native void hangUp(int callId); // 回调接口 public interface Listener { void onIncomingCall(int callId); void onCallStateChange(int callId, String state); } }

对应的 JNI 实现如下(简化版):

#include <jni.h> #include <pjlib.h> #include <pjsua-lib/pjsua.h> static JavaVM *g_jvm = NULL; static jobject g_listener_obj = NULL; static jmethodID mid_incoming_call = NULL; JNIEXPORT jint JNICALL Java_com_example_sip_SipManager_initialize(JNIEnv *env, jobject thiz, jstring sip_server) { // 保存 JVM 指针,供回调线程使用 (*env)->GetJavaVM(env, &g_jvm); // 创建全局引用,防止对象被GC回收 g_listener_obj = (*env)->NewGlobalRef(env, thiz); // 获取回调方法ID jclass cls = (*env)->GetObjectClass(env, thiz); mid_incoming_call = (*env)->GetMethodID(env, cls, "onIncomingCall", "(I)V"); // 初始化PJSIP pj_status_t status = pjsua_create(); if (status != PJ_SUCCESS) return -1; pjsua_config cfg; pjsua_logging_config log_cfg; pjsua_config_default(&cfg); pjsua_logging_config_default(&log_cfg); cfg.cb.on_incoming_call = &on_incoming_call; log_cfg.console_level = 3; status = pjsua_init(&cfg, &log_cfg, NULL); if (status != PJ_SUCCESS) return -2; status = pjsua_start(); if (status != PJ_SUCCESS) return -3; return 0; }

注意这里的关键细节:
-NewGlobalRef是必须的,否则Java对象可能在后台被回收;
-GetMethodID要提前缓存,避免每次回调都查找;
- 所有非主线程的PJSIP回调,必须先 AttachCurrentThread 到 JVM。


第二步:处理来电回调

当SIP服务器发来 INVITE 请求时,PJSIP会触发on_incoming_call回调:

void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata) { JNIEnv *env; int need_detach = 0; // 判断当前是否已附加到JVM int get_env_result = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (get_env_result == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) < 0) { return; } need_detach = 1; } if (mid_incoming_call) { (*env)->CallVoidMethod(env, g_listener_obj, mid_incoming_call, call_id); } if (need_detach) { (*g_jvm)->DetachCurrentThread(g_jvm); } }

这段代码处理了多线程环境下 JNI 调用的安全性问题。很多崩溃就是因为回调线程未Attach导致的。


实际运行中遇到的问题及应对策略

🎧 音频卡顿或单向通话?

这是Android平台上最典型的“表面正常、实则残废”的问题。

常见原因:
  • 后台进程被系统冻结,音频采集线程调度延迟;
  • AudioRecord缓冲区太小,导致丢帧;
  • 采样率不一致(App设为48kHz,PJSIP默认16kHz);
解决方案:
  1. 使用高优先级线程运行音频流
struct sched_param param; param.sched_priority = 10; pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);
  1. AndroidManifest.xml添加权限和前台服务声明
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <service android:name=".SipService" android:foregroundServiceType="microphone"/>

启动时创建通知,保持服务活跃:

Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("VoIP Service Running") .setSmallIcon(R.drawable.ic_call) .build(); startForeground(1, notification);
  1. 统一音频参数

在 PJSIP 初始化前设置:

pjsua_media_config media_cfg; pjsua_media_config_default(&media_cfg); media_cfg.clock_rate = 16000; // 统一使用16kHz media_cfg.snd_clock_rate = 16000; media_cfg.audio_frame_ptime = 20; // 20ms帧时长 media_cfg.channel_count = 1; // 单声道

这样既能降低功耗,又能提升兼容性。


🔒 如何穿透企业级NAT?

很多用户反映在家能打,在公司防火墙后就注册不上。根本原因是UDP端口被封锁。

方案一:启用 STUN
pj_stun_config stun_cfg; pj_stun_config_default(&stun_cfg); pj_stun_config_stun_server(&stun_cfg, "stun.l.google.com", 19302); pjsua_transport_config tp_cfg; pjsua_transport_config_default(&tp_cfg); pjsua_transport_config_set_stun_server(&tp_cfg, "stun.l.google.com");
方案二:部署自有 TURN 服务器(推荐)

使用 Coturn 搭建中继服务:

turnserver -a -f -r your-domain.com \ --lt-cred-mech \ --user=admin:password \ --realm=your-domain.com \ --listening-port=3478

然后在PJSIP中配置:

pjsua_var.turn_cfg.enable = PJ_TRUE; pjsua_var.turn_cfg.server = pj_str("turn.your-domain.com"); pjsua_var.turn_cfg.port = 3478; pjsua_var.turn_cfg.username = pj_str("admin"); pjsua_var.turn_cfg.password = pj_str("password");

这样一来,即使处于 symmetric NAT 之后,也能通过 relay mode 建立连接。


性能与体验优化建议

✅ 库体积控制

如果你只做语音通话,务必关闭不需要的模块:

--disable-video \ --disable-libwebrtc \ --disable-speex-aec \ --disable-gsm-codec \ --disable-ilbc-codec \ --without-libffi

最终静态库总大小可压缩至1.2~1.5MB,远小于任何基于WebRTC的方案。


✅ 功耗优化

  • 启用 VAD(Voice Activity Detection):
media_cfg.no_vad = 0; // 启用VAD

静默期间停止发送RTP包,节省流量和电量。

  • 空闲时暂停录音线程,收到呼叫再唤醒。

✅ 安全性增强

  • 启用 TLS 传输信令:
pjsua_transport_config_default(&tcp_cfg); tcp_cfg.tls_setting.method = PJSIP_TLSV1_METHOD; tcp_cfg.port = 5061; pjsua_transport_create(PJSIP_TRANSPORT_TLS, &tcp_cfg, NULL);
  • 启用 SRTP 加密媒体流:
cfg.use_srtp = PJMEDIA_SRTP_REQUIRED; cfg.srtp_secure_signaling = 1;

杜绝中间人攻击和窃听风险。


结语:这条路还能走多远?

完成上述步骤后,我们的App已经可以完成完整的SIP流程:注册 → 主叫 → 被叫 → 双向通话 → 挂断。

但这只是起点。PJSIP的强大之处在于它的可扩展性。接下来你可以轻松添加:

  • DTMF按键透传;
  • 多账号切换;
  • IM消息(MSRP协议);
  • 视频通话(启用Video模块 + OpenGL ES渲染);
  • 会议桥接(使用 pjsua_conf 连接多个通话);

而且这一切都不需要更换底层框架。

所以,当你下次面对“定制化VoIP需求”时,不妨试试亲手把PJSIP搬上Android。虽然前期门槛略高,但一旦跑通,你就拥有了一个真正属于自己的通信引擎。

如果你在集成过程中遇到了其他问题,欢迎在评论区留言交流。我可以分享更详细的 Makefile 配置、Coturn 部署脚本、以及音频调试工具链。

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

WechatRealFriends多账号切换实战:轻松管理多个微信好友关系

WechatRealFriends多账号切换实战&#xff1a;轻松管理多个微信好友关系 【免费下载链接】WechatRealFriends 微信好友关系一键检测&#xff0c;基于微信ipad协议&#xff0c;看看有没有朋友偷偷删掉或者拉黑你 项目地址: https://gitcode.com/gh_mirrors/we/WechatRealFrien…

作者头像 李华
网站建设 2026/3/8 2:35:35

基于Python+大数据+SSM南昌房价数据分析系统(源码+LW+调试文档+讲解等)/南昌房价分析/南昌房价数据/南昌房价系统/南昌房产数据分析/南昌房价研究系统/南昌楼市数据分析系统

博主介绍 &#x1f497;博主介绍&#xff1a;✌全栈领域优质创作者&#xff0c;专注于Java、小程序、Python技术领域和计算机毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2025-2026年最新1000个热门Java毕业设计选题…

作者头像 李华
网站建设 2026/3/6 1:34:07

华为光猫配置解密终极指南:这款开源神器让你轻松处理加密文件

你是否曾经因为无法查看华为光猫的完整配置而烦恼&#xff1f;当你想要优化网络设置、排查故障或者实现个性化配置时&#xff0c;却发现运营商提供的配置文件被层层加密&#xff0c;就像面对一个上了多重锁的保险箱&#xff0c;无从下手。 【免费下载链接】HuaWei-Optical-Netw…

作者头像 李华
网站建设 2026/2/19 3:28:41

38、PowerShell 使用指南:输出格式化、调试与自定义

PowerShell 使用指南:输出格式化、调试与自定义 1. 输出格式化 1.1 自定义格式化文件 在 PowerShell 中,当未指定格式化命令或属性时,所有格式化默认值由安装目录中的 *.Format.Ps1Xml 文件驱动。若要创建自定义格式化,可参考这些文件,但不要直接修改,而是创建新文件…

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

抖音批量下载神器:3分钟掌握个人主页视频一键保存技巧

在当前短视频风靡的时代&#xff0c;抖音平台汇聚了海量精彩内容。面对手动逐个保存视频的低效方式&#xff0c;一款智能化的批量下载工具应运而生。本教程将带领你从核心功能解析到实际应用操作&#xff0c;全方位掌握这款高效工具的运用技巧。 【免费下载链接】douyinhelper …

作者头像 李华
网站建设 2026/3/5 12:40:00

N_m3u8DL-RE完全手册:零基础掌握流媒体下载核心技术

N_m3u8DL-RE完全手册&#xff1a;零基础掌握流媒体下载核心技术 【免费下载链接】N_m3u8DL-RE 跨平台、现代且功能强大的流媒体下载器&#xff0c;支持MPD/M3U8/ISM格式。支持英语、简体中文和繁体中文。 项目地址: https://gitcode.com/GitHub_Trending/nm3/N_m3u8DL-RE …

作者头像 李华