☰
pjsip 2.14视频编译与运行全链路指南
2026/10/15 13:19:02 网站建设 项目流程

简介:本资源是面向Android音视频开发者的PJSIP最新版视频通话实战Demo,适用于具备JNI基础和NDK编译经验的中高级开发者,用于快速集成SIP协议栈与H.264硬件加速视频能力。压缩包共270个文件,含233个Java源码(如pjsua2JNI.java、MainActivity.java等核心接口与主Activity)、16个XML布局与配置文件、8个已编译的.so动态库(提供ARM/x86多架构支持)、以及gradle构建脚本、.gitignore、README.md等工程必需文件,整体24.62MB,结构完整,开箱即用。已有1635人学习下载,可直接基于pjsua2_android_video_sample-master示例项目理解PJSIP栈初始化、SIP账号注册、音视频会话管理及OpenH264编解码链路集成;代码层级清晰,JNI桥接规范,配套SSL加密与媒体处理模块,便于二次开发与性能调优。

1. pjsip最新版本带视频demo:不是装上就能跑通的“开箱即用”,而是得亲手拧紧三颗螺丝的实时音视频工程

你搜到这个标题,大概率正卡在某个节点上:刚从官网下载了 pjsip 最新版源码,./configure && make && make install表面成功,一跑pjsua --video就报No video device found或Failed to initialize video subsystem;或者 demo 编译过了,但摄像头黑屏、远端画面撕裂、H.264 编码器死活不加载——这根本不是“带 demo”的问题,而是 pjsip 视频栈从编译期到运行时的完整链路里,有至少三个隐性依赖没被显式声明、三个关键配置项没被正确激活、三个平台级设备权限没被提前拿下。它不像 WebRTC 那样封装成 JS API,也不像 FFmpeg 那样靠命令行参数堆叠功能;pjsip 的视频能力是“可插拔但需手焊”的:你得明确告诉它用哪个后端(V4L2?AVFoundation?DirectShow?)、用哪个编码器(openh264?x264?libvpx?)、用哪个渲染器(SDL2?OpenGL?),而且每一步都得在 configure 阶段就钉死,运行时几乎不给你后悔药。本文面向的是已经跑过音频通话、现在想把视频加进现有 pjsip 集成方案的嵌入式/桌面端开发者,目标很实在:用 pjsip 2.14(截至 2024 年中最新稳定版)在 Linux x86_64 环境下,从零构建一个能真实采集本地摄像头、编码 H.264、推流给对端并解码显示的最小可验证 demo,不绕开任何底层细节,不假设你已装好“所有依赖”,只告诉你哪一行 configure 参数动不得、哪个头文件缺失会导致视频模块静默失效、为什么pjsua --list-codec里永远看不到 H264 —— 因为它压根没编进去。


2. 从源码到可执行:pjsip 2.14 视频子系统编译的硬性前置与最小化配置

pjsip 的视频支持不是默认开启的开关,而是一组必须显式启用、且相互强耦合的编译宏。很多翻车案例,根源在于开发者误以为--enable-video是个独立选项,实际上它只是冰山一角,底下连着三根主干依赖:视频设备抽象层(VidDev)、视频编解码框架(VidCodec)、视频传输协议栈(RTP/RTCP for video)。缺任一环,pjsua --video启动时就会跳过整个视频初始化流程,连错误日志都吝啬打印。

2.1 必装的系统级依赖:不是“建议安装”,而是 configure 会直接 abort 的硬门槛

pjsip 视频模块在 configure 阶段会主动探测系统库是否存在,并校验头文件符号。以下五项是 Linux 下不可绕过的硬依赖,少一个,./configure就会终止并提示*** Error: xxx not found:

  • SDL2 开发库:用于跨平台视频渲染(Linux/X11、macOS/Quartz、Windows/GDI)。注意不是运行时的libsdl2-2.0-0,而是开发包libsdl2-dev(Debian/Ubuntu)或sdl2-devel(CentOS/RHEL)。pjsip 用它创建窗口、绑定 OpenGL 上下文、提交 YUV 帧到屏幕。
  • OpenH264 头文件与静态库:Cisco 开源的 H.264 编解码器,pjsip 官方推荐首选。必须提供openh264.h和libopenh264.a(非.so),因为 pjsip 默认静态链接以避免运行时 ABI 冲突。若用动态链接,需额外加--with-openh264=shared,但稳定性风险陡增。
  • Video4Linux2(V4L2)头文件:linux/videodev2.h,这是 Linux 摄像头设备的标准内核接口。没有它,pjsip 根本无法识别/dev/video*设备,pjsua --list-video-devices输出为空。
  • FFmpeg 的 libswscale 头文件:libswscale/swscale.h。pjsip 视频帧在采集(V4L2 输出 YUYV)、编码(H.264 输入 NV12)、渲染(SDL2 输入 RGB)之间频繁转换色彩空间和分辨率,全靠 swscale 做高效缩放与格式转换。仅装ffmpeg命令行工具不够,必须装libswscale-dev。
  • OpenGL 开发库:gl.h和glext.h。即使你不用 OpenGL 渲染,pjsip 的 SDL2 后端在 X11 下默认启用 OpenGL 上下文加速 YUV→RGB 转换。mesa-common-dev+libgl1-mesa-dev是 Debian 系标配。

提示:别信某些博客说“装了 ffmpeg 就够了”。pjsip configure 脚本会分别调用pkg-config --exists sdl2、pkg-config --exists libswscale、pkg-config --exists openh264,任何一个失败,--enable-video就自动失效,且不会警告——它会默默切回纯音频模式。

2.2 configure 的最小必要参数集:三颗螺丝,拧错一颗就废

pjsip 的 configure 脚本有超过 50 个视频相关选项,但真正决定视频能否编译进二进制的,只有以下三个参数组合。其他如--with-ffmpeg、--with-g7221等,属于锦上添花,此处先砍掉:

./configure \ --enable-video \ --with-opengl=/usr \ --with-openh264=/path/to/openh264/build \ CFLAGS="-I/path/to/openh264/build/include" \ LDFLAGS="-L/path/to/openh264/build/lib"

逐项拆解其不可替代性:

  • --enable-video:全局开关,激活pjmedia-videodev、pjmedia-videocodec、pjmedia-video三个子模块的编译。不加此参数,后续所有视频代码都不会进入 Makefile。
  • --with-opengl=/usr:显式指定 OpenGL 安装路径。虽然多数系统在/usr,但 configure 不会自动搜索/usr/local或/opt。若你的 Mesa 在/opt/mesa,此处必须写--with-opengl=/opt/mesa,否则#include <GL/gl.h>报错。
  • --with-openh264=/path/to/openh264/build:这是最易踩坑点。路径必须指向openh264 源码编译后的 build 目录(即执行过make && sudo make install后的install_prefix),而非源码目录。该路径下必须存在include/wels/codec_api.h和lib/libopenh264.a。pjsip 会在此路径下查找include和lib子目录,若结构不符(比如你把.a文件手动拷到/usr/lib却没放头文件),configure 会报OpenH264 header not found。

注意:CFLAGS和LDFLAGS是补救措施,不是替代方案。当--with-openh264指定的路径下头文件不在include/子目录,或库不在lib/子目录时,才用这两个环境变量强行注入。理想情况是让 openh264 的make install输出结构符合 pjsip 的预期,而不是靠环境变量打补丁。

2.3 make 过程中的关键验证点:别等跑 demo 才发现视频模块没编进去

make完成后,不要急着make install,先做三件事验证视频模块是否真被编译:

  1. 检查生成的静态库是否含视频符号:

    nm pjsip/lib/libpjsua2.a | grep -i "vid\|h264" | head -5

    正常输出应包含类似T _pjmedia_vid_dev_create_factory、T _pjmedia_vid_codec_h264_init的符号。若全无vid或h264字样,说明 configure 阶段视频模块已被跳过。

  2. 确认 pjsua 可执行文件链接了 SDL2 和 OpenGL:

    ldd pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu | grep -E "(SDL|GL|swscale)"

    必须看到libSDL2.so、libGL.so、libswscale.so(或libswscale.so.6)三者。缺任一,运行时必崩。

  3. 运行pjsua --help,观察 help 文本是否出现 video 相关选项:

    ./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu --help | grep -A5 "Video options"

    若输出为空,证明pjsua二进制里压根没链接pjmedia-videodev模块,--video参数自然无效。

这三步验证耗时不到 30 秒,却能帮你省掉后续 2 小时的 demo 调试——因为一旦pjsua二进制本身不含视频逻辑,后面所有--video、--capture-dev参数都是对空气说话。


3. 视频设备与编解码器的运行时激活:为什么pjsua --list-video-devices返回空列表?

就算编译成功,pjsua启动时仍可能报告No video device found。这不是 bug,而是 pjsip 的视频设备工厂(VidDev Factory)在运行时按需加载,且加载失败时默认静默。我们必须手动触发设备枚举,并捕获底层错误。

3.1 设备枚举的隐藏开关:--log-level=5是唯一真相之眼

pjsip 的日志系统分 6 级(0~5),视频设备初始化的关键错误(如v4l2_open() failed: No such file or directory)只在 level 5 打印。不加此参数,你永远不知道为什么设备列表为空:

./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --log-level=5 \ --list-video-devices

典型有效输出节选:

10:22:34.123 os_core_unix.c !pjlib 2.14 for POSIX initialized 10:22:34.125 vid_dev.c ..Creating V4L2 video capture factory 10:22:34.126 vid_dev.c ..V4L2 factory created, scanning /dev/video* 10:22:34.127 vid_dev.c ..Found /dev/video0 (Logitech C920) 10:22:34.128 vid_dev.c ..Opening /dev/video0 with format UYVY 10:22:34.129 vid_dev.c ..V4L2 device /dev/video0 opened successfully 10:22:34.130 vid_dev.c ..Video device list: 10:22:34.131 vid_dev.c .. 0: Logitech C920 (V4L2 capture)

若看到..Creating V4L2 video capture factory但后续无Found /dev/video*,说明内核未识别摄像头;若看到Opening /dev/video0但报Permission denied,则是用户权限问题。

3.2 权限与 udev 规则:让普通用户直通/dev/video*

Linux 下访问摄像头需video用户组权限。临时方案是:

sudo usermod -a -G video $USER newgrp video # 刷新当前 shell 的组权限

但更可靠的是写 udev 规则,避免每次插拔都重置权限。创建/etc/udev/rules.d/99-video.rules:

SUBSYSTEM=="video4linux", GROUP="video", MODE="0664" SUBSYSTEM=="usb", ATTR{idVendor}=="046d", ATTR{idProduct}=="082d", GROUP="video", MODE="0664"

第二行针对罗技 C920(Vendor=046d, Product=082d),请根据lsusb输出替换为你设备的实际 ID。然后重载规则:

sudo udevadm control --reload-rules sudo udevadm trigger

3.3 编解码器注册检查:pjsua --list-codec为何不显示 H264?

即使 openh264 编译进了 pjsip,pjsua --list-codec仍可能不显示H264,原因只有一个:openh264 库在运行时未被成功 dlopen。pjsip 的视频编解码器是运行时动态加载的,路径由PJMEDIA_VID_CODEC_OPENH264_PATH环境变量控制,默认值为libopenh264.so,但你编译时用的是静态库libopenh264.a。

解决方案:强制使用静态链接,并在 configure 时声明:

./configure \ --enable-video \ --with-opengl=/usr \ --with-openh264=/path/to/openh264/build \ --with-openh264=static \ # 关键!告诉 pjsip 用静态库 CFLAGS="-I/path/to/openh264/build/include" \ LDFLAGS="-L/path/to/openh264/build/lib"

重新 configure && make 后,pjsua --list-codec必现:

Codec: H264/90000 PT=126 Fmtp: level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f

血泪经验:曾有个项目因疏忽没加--with-openh264=static,导致pjsua运行时尝试dlopen("libopenh264.so")失败,但日志 level 4 下完全不报错,--list-codec干净得像没这回事。直到加了--log-level=5,才看到dlopen(libopenh264.so): No such file or directory这行被埋没的日志。


4. 视频 demo 的最小可运行命令:从单机自环到双端互通的三步验证法

pjsip 自带的pjsua是终极 demo,但它不是“一键启动”,而是需要按视频通信的真实流程分步激活。我们采用三步渐进法,每步验证一个关键链路,避免一上来就配 SIP 服务器导致问题交织。

4.1 第一步:本地自环(Loopback)——验证采集+编码+渲染闭环

目标:不走网络,只用本机摄像头和屏幕,看能否实现“自己拍自己”。

./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --video \ --capture-dev=0 \ # 使用第一个视频设备(Logitech C920) --render-dev=0 \ # 使用第一个渲染设备(通常是 SDL2 窗口) --auto-play \ # 自动播放收到的视频(这里是自环流) --auto-rec \ # 自动录制发送的视频(可选,用于事后分析) --quality=10 \ # 编码质量,1~10,10 最高(但 CPU 消耗大) --ec-tail=250 \ # 回声消除尾长,视频通话必备 --use-srtp=0 \ # 关闭 SRTP,简化调试 --null-audio \ # 关闭音频,聚焦视频 --log-file=pjsua-video-loop.log

成功标志:

  • 终端输出Video stream is active;
  • 弹出 SDL2 窗口,显示本地摄像头画面(非黑屏);
  • 日志中出现H264 encoder started、H264 decoder started。

若窗口黑屏,检查--capture-dev编号是否正确(--list-video-devices输出的索引);若窗口卡顿,降低--quality至 5。

4.2 第二步:UDP 自环(UDP Loopback)——验证 RTP 封包与解包

目标:数据不出网卡,但在 UDP 层模拟网络传输,验证 pjsip 的视频 RTP 栈是否正常工作。

# 终端1:作为 sender,发流到本机 4000 端口 ./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --video \ --capture-dev=0 \ --add-codec=H264 \ --rtp-port=4000 \ --remote-uri=sip:test@127.0.0.1:4001 \ --use-srtp=0 \ --null-audio # 终端2:作为 receiver,收 4001 端口的流 ./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --video \ --render-dev=0 \ --auto-play \ --rtp-port=4001 \ --local-port=4001 \ --use-srtp=0 \ --null-audio

关键点:

  • --rtp-port指定 RTP 流绑定的本地端口;
  • --remote-uri中的 IP 和端口是对方 RTP 接收地址,此处设为127.0.0.1:4001;
  • 两个实例必须用不同--rtp-port,否则端口冲突。

成功标志:终端2 的 SDL2 窗口显示终端1 的摄像头画面,且pjsua日志中出现RTP packet received、Decoding H264 frame。

4.3 第三步:真实双端互通——接入 SIP 服务器完成端到端

当自环和 UDP 环都通过,再接入真实 SIP 环境。以免费开源 SIP 服务器 Kamailio 为例(非必须,任何标准 SIP 服务器均可):

  1. 在 Kamailio 上注册两个账号:alice@yourdomain.com、bob@yourdomain.com;
  2. 启动 alice 端(发送端):
    ./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --video \ --capture-dev=0 \ --add-codec=H264 \ --id="sip:alice@yourdomain.com" \ --registrar="sip:your-kamailio-ip:5060" \ --realm="yourdomain.com" \ --username="alice" \ --password="alice123" \ --null-audio
  3. 启动 bob 端(接收端):
    ./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --video \ --render-dev=0 \ --auto-play \ --id="sip:bob@yourdomain.com" \ --registrar="sip:your-kamailio-ip:5060" \ --realm="yourdomain.com" \ --username="bob" \ --password="bob123" \ --null-audio
  4. 在 alice 终端输入call sip:bob@yourdomain.com。

成功标志:bob 端弹窗显示 alice 摄像头画面,alice 端日志出现Call is connected,Video stream is active。

提示:首次互通失败,90% 概率是防火墙阻断了 RTP 端口(默认 4000~4096)。务必在 Kamailio 服务器上配置rtp_proxy并在pjsua启动时加--rtp-proxy=your-kamailio-ip:7890,或直接关闭服务器防火墙测试。


5. 视频质量调优与避坑指南:那些让画面撕裂、卡顿、绿屏的玄学参数

pjsip 视频不是“开箱即用”,而是“开箱即调参”。以下五条是我在多个嵌入式设备(ARM Cortex-A72)、桌面端(Intel i5)、虚拟机(KVM/QEMU)上反复验证过的避坑清单,每一条都对应一个真实翻车现场。

5.1 现象:画面严重撕裂(Tearing),上下半屏错位

原因:V4L2 设备默认使用V4L2_BUF_TYPE_VIDEO_CAPTURE的单缓冲模式,pjsip 采集线程与渲染线程争抢同一帧内存。
解决:强制启用双缓冲(Double Buffering)并在 configure 时开启 V4L2 的VIDIOC_S_PARM支持:

./configure \ --enable-video \ --with-opengl=/usr \ --with-openh264=/path/to/openh264/build \ --with-openh264=static \ CFLAGS="-DPJMEDIA_VIDEO_DEV_HAS_V4L2_DOUBLE_BUFFER=1" \ LDFLAGS="-L/path/to/openh264/build/lib"

重新编译后,在pjsua启动时加--v4l2-double-buffer参数。

5.2 现象:CPU 占用率飙升至 100%,画面卡在 1fps

原因:H.264 编码器默认使用baselineprofile,但 pjsip 的 openh264 封装层未正确设置rc_mode(码率控制模式),导致编码器进入无约束的高质量模式。
解决:在pjsua启动时显式指定码率控制:

--h264-fps=15 \ --h264-bitrate=512000 \ --h264-profile=baseline \ --h264-level=3.1

其中--h264-bitrate是关键,单位 bps,512kbps 对 720p30 足够;--h264-level=3.1限制解码复杂度,避免低端设备解码失败。

5.3 现象:远端看到绿屏(Green Screen),本地预览正常

原因:色彩空间不匹配。V4L2 采集常为YUYV或MJPG,而 H.264 编码器要求NV12或I420。pjsip 的 swscale 转换若未启用,会直接 memcpy 错误格式帧。
解决:强制启用 swscale 转换,并指定输入/输出格式:

--v4l2-cap-format=YUYV \ --v4l2-render-format=I420 \ --sw-scale

--sw-scale参数会强制 pjsip 调用 libswscale 做格式转换,而非依赖硬件直传。

5.4 现象:pjsua --list-video-devices显示设备,但--capture-dev=0启动时报Invalid argument

原因:V4L2 设备不支持所请求的分辨率或帧率。pjsip 默认尝试640x480@30fps,但某些 USB 摄像头只支持640x480@15fps或1280x720@10fps。
解决:先用v4l2-ctl查询设备能力:

v4l2-ctl -d /dev/video0 --list-formats-ext

找到支持的Width/Height和Interval Discrete,然后在pjsua中指定:

--video-size=640x480 \ --video-fps=15

5.5 现象:视频通话建立后,10 秒内自动断开,日志报RTCP timeout

原因:RTCP 包未正确发送。pjsip 默认每 5 秒发一次 RTCP RR(Receiver Report),但若网络栈丢包或防火墙拦截 ICMP(RTCP 常用 ICMP 作保活),RR 丢失超 3 次即断连。
解决:缩短 RTCP 发送间隔,并禁用 ICMP 依赖:

--rtcp-mux \ --rtcp-interval=2000 \ --no-rtcp-nack

--rtcp-mux将 RTCP 复用到 RTP 端口,避免单独开防火墙端口;--rtcp-interval=2000设为 2 秒,提高保活频率。

注意:以上五条参数均需在pjsua启动时传入,无法在运行时用pjsua控制台命令修改。它们不是“可选优化”,而是特定场景下的刚需配置。我曾在某 ARM 设备上因漏掉--v4l2-double-buffer,连续三天排查撕裂问题,最后发现是 V4L2 驱动层的缓冲区竞态——这种底层黑匣子,只能靠 pjsip 提供的开关去绕过。


6. 实战技巧:用 GDB 实时观测视频帧流转,定位“画面卡住”的最后一公里

当所有配置都正确,pjsua也显示Video stream is active,但画面就是不动(卡在第一帧),常规日志已无法提供线索。此时,你需要一把手术刀:用 GDB 附着到pjsua进程,直接观测视频帧在内存中的生命周期。这不是炫技,而是定位“卡帧”类问题的终极手段。

6.1 设置 GDB 断点:锁住视频帧的三个关键节点

启动pjsua时加-n参数禁止前台交互,方便 GDB 附着:

./pjsip-apps/bin/pjsua-x86_64-unknown-linux-gnu \ --video \ --capture-dev=0 \ --render-dev=0 \ --auto-play \ -n \ --log-level=5

另起终端,用 GDB 附着:

gdb -p $(pgrep pjsua) (gdb) set follow-fork-mode child (gdb) break pjmedia_vid_port_put_frame (gdb) break pjmedia_vid_codec_encode (gdb) break pjmedia_vid_port_get_frame (gdb) continue

这三个函数是视频帧流转的核心钩子:

  • pjmedia_vid_port_put_frame:采集线程将 V4L2 帧送入视频端口(Port);
  • pjmedia_vid_codec_encode:编码线程从 Port 取帧,调用 openh264 编码;
  • pjmedia_vid_port_get_frame:渲染线程从 Port 取编码后帧,送 SDL2 显示。

6.2 观察帧计数器:确认是采集停摆还是渲染阻塞

在 GDB 中,当断点命中时,打印当前帧的序列号(seq)和时间戳(ts):

(gdb) p ((pjmedia_vid_frame*)frame)->seq (gdb) p ((pjmedia_vid_frame*)frame)->timestamp

正常情况:三个断点的seq值应严格递增(1,2,3,4…),且timestamp差值约等于1000000/fps(微秒)。若put_frame断点持续命中但encode不命中,说明编码线程卡死;若encode命中但get_frame不命中,说明渲染线程阻塞在 SDL2 锁上。

6.3 检查线程状态:揪出死锁元凶

在 GDB 中查看所有线程及调用栈:

(gdb) info threads (gdb) thread apply all bt

重点关注video_capture、video_encode、video_render三个线程。常见死锁模式:

  • video_capture线程卡在v4l2_ioctl(fd, VIDIOC_DQBUF, &buf):V4L2 驱动未释放缓冲区;
  • video_encode线程卡在WelsEnc::EncodeFrame():openh264 内部锁未释放;
  • video_render线程卡在SDL_LockTexture():SDL2 渲染上下文被其他线程占用。

此时,用pstack $(pgrep pjsua)辅助验证,比 GDB 更快看到线程阻塞点。

6.4 修改运行时参数:GDB 中热修复(慎用)

GDB 不仅能看,还能改。例如发现encode线程因bitrate过高卡死,可临时降低:

(gdb) p ((pjmedia_vid_codec_param*)param)->enc_bitrate = 256000

然后continue,观察是否恢复。这招不能替代配置,但能在紧急现场快速验证猜想。

我习惯在项目初期就把这套 GDB 流程固化为脚本:debug-video.sh,里面封装了pgrep、gdb -batch -ex一系列命令,一键启动、断点、打印关键变量。它不能替代正确的编译配置和参数设置,但当你站在“一切看起来都对,可画面就是不动”的悬崖边时,它是唯一能让你看清脚下岩石的探照灯。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询