☰
eXosip+ffmpeg命令行搭建SIP客户端:信令与媒体协同实战
2026/10/9 4:19:20 网站建设 项目流程

简介:面向需要接入IP摄像头的SIP视频通话开发者,这份7z压缩包提供了一套轻量级实现思路:使用eXosip处理SIP信令,同时以ffmpeg和ffplay命令行完成RTP音视频流的接收与播放,从而避开pjsip功能冗余、需改源码的困境。整包共789个文件,约23.22MB,核心内容包括158个C源码、150个头文件、22个CC文件、构建脚本(configure、Makefile、am/in)、SIP协议报文样本(大量以sip命名的文本)以及少量3/m4/wav/mp4测试媒体,另外还带有DLL/LIB、Visual Studio工程文件和平台适配脚本,便于跨平台查阅和二次编译。目前已有589人学习。压缩包内含完整的eXosip库源码及其依赖工具,通过作者验证过的命令行配合方式,可以先快速打通摄像头取流、通话建立与媒体播放链路,再逐步替换为自研ffmpeg调用。其中大量SIP测试样例对理解REGISTER、INVITE、事务状态机等细节很有帮助,整体方案比改造pjsip更加聚焦和低成本。

1. 为什么用 eXosip+ffmpeg 命令行搭 SIP 客户端:先跑通再说

做 SIP 音视频通话落地时,大家第一反应都是 pjsip 全家桶,但真把 IP 摄像头塞进通话流程才发现,pjsip 功能太全,反而要为一两个点改源码,改完还不好调试。我这次直接用 eXosip 处理 SIP 信令,媒体部分先交给 ffmpeg 和 ffplay 命令行,用一套可以随时替换的脚本方案,躲开了修改 pjsip 源码的坑,一周内就把“摄像头画面进 SIP 通话”这条路跑通了。这套思路适合还在验证阶段、又不想一上来就啃协议栈源码的人,也适合给后续 C++ 代码实现留过渡方案。

2. 信令侧:eXosip 的注册与呼叫到底配什么

2.1 eXosip 初始化与 UDP 端口选择

eXosip 是基于 libosip 的协议栈封装,SIP 消息收发默认走 UDP 5060,但实际生产环境里经常和别的服务冲突,或者被防火墙拦。我一般直接指定本地端口,比如用 5062 作为 SIP 信令端口,这样不影响其他软电话。

#include <eXosip2/eXosip.h> int main() { osip_t *osip = NULL; eXosip_t *ctx = eXosip_malloc(); if (eXosip_init(ctx) != OSIP_SUCCESS) { eXosip_quit(ctx); return -1; } /* 设置信令监听端口,我习惯用 5062,避开 5060 的冲突 */ struct eXosip_cfg cfg; eXosip_get_cfg(ctx, &cfg); cfg.udp_port = 5062; eXosip_set_cfg(ctx, &cfg); eXosip_start(ctx); // ... }

这里有个容易忽略的点:eXosip_init会分配内部资源,eXosip_start才真正拉起事件循环。改 UDP 端口要在eXosip_start之前完成,否则不生效。另外 eXosip 内部会把 SIP 消息和事务状态机分开处理,INVITE 的重传机制不用你操心,但呼叫超时时间要自己通过eXosip_set_callbacks去控制,默认值偏长。

信令端口定下来后,还要关注 RTP 媒体端口。SIP 协议里 SDP 报文中的m=audio、m=video,以及c=IN IP4 x.x.x.x,决定了对方把媒体流发到哪里。eXosip 不管媒体流,它只负责把 SDP 文本塞进 SIP 消息里,所以你自己得先确定好本机用来接收 RTP 的 UDP 端口,比如 6000 和 6002,后面 ffplay 就监听这两个口。

2.2 注册流程的 SIP 消息处理

eXosip 的注册流程比 pjsip 直接得多,填好账号和服务器地址,调用eXosip_register_build_initial_REGISTER生成消息,再发送即可。但真正要花心思的是处理 401/407 挑战认证。

int register_with_auth(eXosip_t *ctx, const char *server, const char *user, const char *pass) { osip_message_t *reg = NULL; eXosip_register_build_initial_REGISTER(ctx, &reg, server, user, NULL, NULL); eXosip_lock(ctx); eXosip_register_send_initial_REGISTER(ctx, reg); eXosip_unlock(ctx); /* 等待 401 响应并处理认证 */ eXosip_event_t *ev = eXosip_event_wait(ctx, 0, 3000); if (ev && ev->type == EXOSIP_REGISTRATION_FAILURE) { /* 这里 eXosip 会携带 WWW-Authenticate 头,需要追加 Authorization */ osip_message_t *reg2 = NULL; eXosip_register_build_register(ctx, ev->rid, &reg2); int auth_type = eXosip_get_authentication_info(ctx, ev->rid, reg2); if (auth_type == 401) { osip_message_t *auth_reg = NULL; eXosip_register_build_register_with_auth(ctx, ev->rid, &auth_reg); eXosip_register_send_register(ctx, ev->rid, auth_reg); } } return 0; }

这段代码的关键是ev->rid,它是 eXosip 内部为这条注册事务分配的资源 ID。第二次 REGISTER 必须带上同一个 rid,否则认证信息对不上。很多初次接触 eXosip 的人会在这里翻车,写了半天 401 后不重新建消息,而是直接改第一个 reg 再发,结果服务器一直报 401。另外eXosip_get_authentication_info返回的 401/407 决定用WWW-Authenticate还是Proxy-Authenticate,PJSIP 里会自动处理,eXosip 得你手动走这一步。

注册成功后,eXosip 会抛一个EXOSIP_REGISTRATION_SUCCESS事件,一般用状态机记录一下。如果注册服务器要求 Keep-Alive,就在定时器里发eXosip_register_send_keepalive,默认是 30 秒一次。这个字段属于配置参数,直接传给eXosip_register_build_initial_REGISTER的第四个参数,具体看你要对接的平台。海康等平台的 SIP 接入国标里,注册间隔写得比较严,最好纯按服务器要求来。

2.3 呼叫与 SDP 协商:拿到对端媒体地址

SIP 呼叫的核心不是发 INVITE,而是把 SDP 生成对。eXosip 提供了eXosip_call_build_initial_invite,你需要自己往 SDP 里填音频和视频的行。命令行媒体方案里,ffplay 监听的是本机 UDP 端口,所以 SDP 里的m=video端口必须填 ffplay 实际绑定的端口,而不是随便填一个。

/* 构造 INVITE 的 SDP 部分,命令行方案中视频端口要与 ffplay 一致 */ char sdp[1024]; snprintf(sdp, sizeof(sdp), "v=0\r\n" "o=- 123456 2 IN IP4 %s\r\n" "s=SIP Call\r\n" "c=IN IP4 %s\r\n" "t=0 0\r\n" "m=video %d RTP/AVP 96\r\n" "a=rtpmap:96 H264/90000\r\n" "a=recvonly\r\n", local_ip, local_ip, rtp_video_port); osip_message_t *invite = NULL; eXosip_call_build_initial_invite(ctx, &invite, sdp, callee_uri, NULL, NULL); eXosip_call_send_initial_invite(ctx, invite);

这里我用a=recvonly,因为命令行测试阶段先只收画面,不做编码推流。对端回 200 OK 后,它的 SDP 里会给出m=video 端口和c=IP,这才是 ffmpeg 推流时要打过去的目标地址。解析 200 OK 时,不要自己去写 SDP 解析器,直接用sdp_message_parse,这个是 libosip 自带的,能扣出远端端口。

sdp_message_t *remote_sdp = NULL; if (ev->type == EXOSIP_CALL_ANSWERED) { osip_message_t *ans = ev->sip; const char *body = osip_message_get_body(ans); sdp_message_parse(&remote_sdp, body); /* 取远端视频端口和 IP,存到全局变量里,等 ffmpeg 那边用 */ const char *vconn = sdp_message_v_get_connection(remote_sdp); int vport = sdp_message_v_get_port(remote_sdp); }

SDP 解析这个动作必须放在单独线程或者状态机里,不能和信令发送混在一起。eXosip 的事件循环是单线程的,如果在event_wait里直接启动 ffmpeg,会把信令线程卡住,后续 BYE 消息都收不到。常见做法是EXOSIP_CALL_ANSWERED到来后,把对端地址写入一个共享变量,然后由媒体线程去读取并拉起 ffmpeg。

3. 媒体侧:用 ffplay 接入 RTP 音视频流

3.1 ffplay 监听 RTP 的方式与参数

拿到对端 SDP 之后,本机接收端要起 ffplay 监听 UDP 端口。最简单的命令是把 RTP 流直接交给 ffplay,但默认行为会去尝试解析依赖 SDP 的 payload,所以光有 UDP 端口还不行,要指定编解码格式。我用的是-protocol_whitelist和-fflags nobuffer,避免播放器因为缓冲延迟越堆越大。

ffplay -fflags nobuffer -protocol_whitelist "rtp,udp,file" -i rtp://0.0.0.0:6002 -analyzeduration 1000000 -probesize 1000000

这个命令里的rtp://0.0.0.0:6002表示 ffplay 在自己绑定的 UDP 6002 端口等 RTP 包。注意-protocol_whitelist必须带上file,否则后面如果还要读本地 SDP 文件会被拒。-analyzeduration和-probesize控制首帧时长,网络摄像头 H264 流如果 GOP 比较大,probesize 太小会解析不出来编码格式,显示器一直黑着。

命令行媒体方案的优点就是可以随时手动调参数,不需要重新编译。但有个前提:SIP 信令协商里必须让对端知道,你的接收端口是 6002,且只接收视频,不接收音频。否则对方以为你音频视频都收,往 6000 和 6002 同时发包,ffplay 只抓了其中一个流,另一个端口的数据就会在系统缓冲区里堆着,还可能导致内存增长。

3.2 手动构造 SDP 文件让 ffplay 识别编码

H264 over RTP 有个常见问题:ffplay 直接监听 UDP 时,无法确定 RTP 载荷的动态 payload 类型,比如 96 对应 H264,但对端可能用 98 对应 H264。这时候手动写一个本地 SDP 文件,把 payload 类型和编码对应关系写清楚,ffplay 就能正确解码。

v=0 o=- 0 0 IN IP4 0.0.0.0 s=No Name c=IN IP4 0.0.0.0 t=0 0 m=video 6002 RTP/AVP 96 a=rtpmap:96 H264/90000 a=fmtp:96 packetization-mode=1;profile-level-id=42C01E

写好后,启动命令换成:

ffplay -fflags nobuffer -protocol_whitelist "rtp,udp,file" -i local.sdp

a=fmtp里的packetization-mode=1是硬规定。IP 摄像头海康、大华普遍支持 H264 非交错模式,如果对端只发交错模式,ffmpeg 解码会报Invalid NAL unit。真遇到这种码流,可以把packetization-mode改成 0 试试,但多数情况下是摄像头侧设置问题,而不是 SDP 写错。

生成 SDP 文件时,c=行的 IP 要写成 0.0.0.0,表示只关心端口,不关心来源地址。这样 ffplay 不管对端从哪个 IP 发来 RTP,都能收。如果写成具体 IP,对端 NAT 环境下源地址变了就收不到。这是我踩过一次的坑,后面专门讲。

3.3 音视频与 SIP 的配合

命令行 ffplay 只能处理媒体,它不知道 SIP 信令什么时候开始。所以要用脚本或者一个小程序把信令和媒体串起来。最简单的做法是在 eXosip 收到 200 OK 后,用system()启动一个后台 ffplay,再把 PID 记下来,等收到 BYE 时,用kill杀掉对应进程。

#!/bin/bash # 收到呼叫后启动 ffplay 接收视频 ffplay -fflags nobuffer -protocol_whitelist "rtp,udp,file" \ -i rtp://0.0.0.0:6002 >/tmp/ffplay.log 2>&1 & echo $! > /tmp/ffplay.pid # 通话结束后杀掉 ffplay kill $(cat /tmp/ffplay.pid)

这个脚本的好处是 ffplay 异常退出后,日志在/tmp/ffplay.log里,直接看有没有解码错误。实际中控制不住的是 ffplay 的缓冲延迟,-fflags nobuffer也只能减少到一两百毫秒。如果对端是视频通话应用,双方唇音同步要求高,那命令行 ffplay 不是长久方案,但做验证足够了。

需要特别注意的是,eXosip 的EXOSIP_CALL_CLOSED事件会在收到 BYE 后触发,也可能因为底层 socket 错误触发。不要只依赖这个事件杀进程,还要在回调里判断呼叫 ID,因为同一个 eXosip 实例可能同时挂多个呼叫。简单场景下就每次呼叫前清空/tmp/ffplay.pid,多呼叫场景还是用事务 ID 区分,否则会误杀上一通电话。

4. 推流侧:用 ffmpeg 把 IP 摄像头拉进 SIP 通话

4.1 从 RTSP 拉流并转成 RTP

通话中要把本地的 IP 摄像头画面推给对端,就轮到 ffmpeg 上场。摄像头一般提供 RTSP 流,ffmpeg 负责拉流、转封装、再推 RTP。这里有一个最常见的误区:以为 ffmpeg 的输出地址就是 SIP 对方的 IP,直接写rtp://对方IP:端口,结果对端 ffplay 解码花屏或完全没反应。原因在于 RTP 流必须配合 SDP 才能正确解码,或者至少保证 RTP 包里的SSRC、timestamp连续且自洽。

ffmpeg -re -i "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101" \ -c:v copy -an -f rtp "rtp://192.168.1.100:5004" \ -sdp_file video.sdp

-f rtp是 ffmpeg 的 RTP 封装器,它会自动把 H264 码流做成 RTP 包。-sdp_file video.sdp表示把对应的 SDP 写到一个临时文件,方便你检查 payload type 和端口。这里的地址192.168.1.100:5004是 SIP 对端在 SDP 里通告的媒体地址。你需要先解析 200 OK 里的 SDP,拿到对端 IP 和端口,再填充到这条命令里。

用-c:v copy是省去转码,前提是摄像头原始编码和目标端支持的编码一致。很多视频会议终端只认 H264 基线或主类,海康默认可能输出 High Profile,对端老解码器就不兼容。遇到不行就换-c:v libx264 -preset veryfast -tune zerolatency,这个参数对主流 SIP 终端兼容性都不错。

4.2 编码器参数与码率控制

命令行 ffmpeg 推流如果要转码,参数不能照抄转短视频的配置。SIP 通话对延迟要求高,码率要平滑,不然对端画面会卡。底层码率控制推荐-b:v 2M -maxrate 2M -bufsize 4M,把瞬时波动控制在缓冲区内。

ffmpeg -re -i "rtsp://admin:password@192.168.1.64:554/Streaming/Channels/101" \ -c:v libx264 -preset veryfast -tune zerolatency \ -b:v 2M -maxrate 2M -bufsize 4M -pix_fmt yuv420p \ -an -f rtp "rtp://192.168.1.100:5004" \ -sdp_file video.sdp

-tune zerolatency对 x264 很关键,它相当于告诉编码器把编码缓冲降到最低,否则画面出来会比你摄像头实际时间慢几百毫秒。-an表示不推音频,因为我们聊天场景里,音频单独走其他链路或者用对端自己的麦克风。如果摄像头本身带音频,而且你要把音频也推到对端,需要先-vn推音频,然后单独起一个 ffplay 进程用声卡采音,再混流输出。这是另外的复杂场景,命令行不适合做混音,后续写代码再处理。

一个容易忽略的细节:RTP 封装时,音频和视频必须分开推,不能在一台机器上同时推两个流到同一个端口。RTP 协议里音频和视频用的 SSRC 不同,端口也不同。ffmpeg 只处理一路流,如果你要做音视频混合,得先把它们封装成 TS 或用 muxer 合流,再-f rtp推出去。命令行方案里我倾向于视频走 RTP,音频直接 PCMA 或 G711 走另一路,反正 SIP 支持多路 m 行。

4.3 命令行与 eXosip 的联动脚本

当呼叫建立后,eXosip 拿到对端媒体地址,然后执行 ffmpeg 推流。这个动作建议放到独立的 shell 脚本里,把 IP 和端口作为参数传进去。因为 ffmpeg 推流是阻塞的,如果直接在 eXosip 回调里调用system(),整个程序会卡死,必须用system("script.sh &")或者 fork 线程。

#!/bin/bash # push_media.sh REMOTE_IP=$1 REMOTE_PORT=$2 RTSP_URL=$3 ffmpeg -re -i "$RTSP_URL" \ -c:v libx264 -preset veryfast -tune zerolatency \ -b:v 2M -maxrate 2M -bufsize 4M \ -an -f rtp "rtp://$REMOTE_IP:$REMOTE_PORT" \ -sdp_file /tmp/push_video.sdp > /tmp/ffmpeg_push.log 2>&1 & echo $! > /tmp/ffmpeg_push.pid

调用侧只需要把解析出来的 IP、端口、RTSP 地址拼接起来,把脚本投到后台。要注意 ffmpeg 的 RTSP 拉流地址里往往带用户名密码,这个字符串在命令行里直接暴露,测试可以,生产环境建议把凭据放到环境变量里,或者用-rtsp_transport tcp防止 UDP 拉流丢包。使用-rtsp_transport tcp也会让摄像头响应更稳定,特别是 WiFi 摄像头,UDP 拉流容易花屏。

ffmpeg -rtsp_transport tcp -i "rtsp://admin:pass@192.168.1.64/..." ...

加了这个参数后,RTSP 走 TCP,RTP 还在 UDP,相当于拉流链路可靠,推流链路还是实时 UDP。这避免了摄像头侧 NAT 映射不稳定导致拉流断连的问题。

5. eXosip+ffmpeg 联调避坑:现象、原因、解决

5.1 现象一:ffplay 黑屏无画面

现象:SIP 信令正常,呼叫建立成功,对端也显示正在通话,但 ffplay 窗口一直黑着,没有任何报错或者只有一行decode error。

原因:RTP 包到了,但 ffplay 没有正确识别编码格式。这种情况多数是动态 payload type 不匹配,比如对端发的 RTP 包 payload 是 96,而 ffplay 用默认映射规则认为 96 是 MPEG4,不是 H264。也可能对端发了 SDP,但 ffplay 没有读到本地 SDP,直接裸 UDP 监听时它没有能力猜出 H264 的封装。

解决:先看/tmp/ffplay.log里有没有Stream #0:0: Video: h264这行,如果有,说明解码器初始化成功,黑屏是信令里交错相关。如果看到Failed to parse video packet,那基本是 payload type 映射问题。手动写一份 SDP 文件,里面把a=rtpmap:96 H264/90000写明白,然后用ffplay -i video.sdp启动。每帧画面 GOP 太大时也黑屏,可以在 ffplay 命令后面加-vf "select='eq(n,0)+gt(t,prev_pts)"这种强制刷帧,但根本解法是让摄像头把 GOP 调小,一般设置 I 帧间隔 1 秒到 2 秒。

5.2 现象二:ffmpeg 推流后对端不显示

现象:ffmpeg 日志显示rtp://192.168.1.100:5004在推流,进程也不退出,但对端终端一直没画面。

原因:我还是经常看到有人把媒体地址错写成rtp://0.0.0.0:5004,这样就相当于发给本机发送端口,对端当然收不到。另一个常见原因是对端 RTP 端口不是固定的,而是 SIP 协商时动态开的,你需要解析 200 OK 的 SDP,而不是用 INVITE 请求里的端口。还可能是对端在 NAT 后面,RTP 是从内网发出来的,源地址和 INVITE 里的 SDP 不一致,但 ffmpeg 只往 SDP 里的 IP 打,当然打不通。

解决:先把 ffmpeg 推流地址改成对端 SDP 里的c=和m=行对应的 IP 和端口。如果对端 NAT,就要用-f rtp提供的rtpflags=skiprtcp,不要和 RTCP 消息纠缠。另外,可以先用本机 ffplay 验证 ffmpeg 推流是否正常:本地跑ffplay -i video.sdp,看画面能不能正常显示。如果能显示,说明 RTP 封装没问题,剩下的就是 UDP 路由和防火墙问题。

5.3 现象三:SIP 请求超时

现象:eXosip 发送 REGISTER 和 INVITE 后,长时间没有响应,或者服务器回了几次 404、408。

原因:eXosip_init之后没有调用eXosip_start,导致消息事件循环根本没跑,消息只在发送缓冲区出不去。或者 SIP 端口被防火墙拦了,比如只开放了 TCP 5060,而 eXosip 默认用 UDP。还有可能是 SIP 服务器域名解析到 IPv6 地址,而 eXosip 没有开启 IPv6,本地网络环境下 IPv6 通不了。

解决:先确认本地有没有监听 UDP 5062:ss -ulnp | grep 5062。没有就检查eXosip_start是否调用。有端口但消息发不出去,就用tcpdump -i any udp port 5062看有没有 SIP 请求离开发送端。如果是 NAT 环境,还要检查注册服务器和本机是否在同一个子网,以及是否需要加rport参数。eXosip 默认会填充了自己的 Via 头,但没建路由前,它不会自动响应 401 挑战。

5.4 现象四:音视频不同步

现象:视频和音频如果同时用两个 ffmpeg 进程推,对端看到的声音和画面错得越来越厉害,开始的几秒能对上,十几秒后音画开始分离。

原因:视频流和音频流的 RTP 时间戳是各自独立的,ffmpeg 在两个进程里分别维护自己的时钟,没有参考同一个 wall-clock。ffplay 收到的视频流时间戳从 0 开始,音频流也从 0 开始,但两者实际录制时间不同,解码端用各自的时钟播放,不同步是必然的。

解决:命令行方案做不到音视频强制同步,因为同步需要 muxer 层级的时间基。要么在 SIP 通话里只走一路视频,音频用系统软电话或本地麦克风直通;要么在 ffmpeg 里用-use_wallclock_as_timestamps 1让 RTP 时间戳基于实际时钟,但前提是摄像头和音频采集设备都支持 PTP。对大多数场景,我的保守做法是不同时推音视频,等后续写 C++ 代码调 ffmpeg 时,再通过av_compare_ts对齐,或者直接用 libavformat 的 RTP muxer 统一时间基。

5.5 现象五:NAT 环境下 RTP 方向不通

现象:信令都通,但 ffplay 一直在等 RTP,tcpdump发现对端发来的包到了一个很奇怪的端口,和本地 ffplay 监听端口对不上。

原因:SIP 在 NAT 环境下,SDP 里的c=地址可能是内网地址,对端往这个地址发数据根本到不了。eXosip 不会帮你改 SDP 里的 IP,它只透明转发。另外对端的 RTP 源端口和 SDP 通告端口可能不同,因为 RTCP 端口或者端口映射导致。

解决:最有效的方式是在 eXosip 收到 INVITE 时,用eXosip_call_get_remote_sdp拿到的地址,然后对比实际收到的 RTP 包的源地址。ffplay 监听 0.0.0.0 可解决部分问题,但 NAT 外网环境还要配合 STUN 打洞。命令行阶段我建议先在同一内网里测试,等联调通过后再考虑暴露到公网。内网测试一切正常、公网失败时,先检查防火墙是否放行 UDP 端口范围,一般需要放行 5004、6000、6002 这些 RTP 端口和 5062 信令端口。

6. 从命令行到代码实现的过渡:验证方法与小技巧

验证这套方案是否真正跑通,不能只看屏幕。我先说我最常用的验证套路:用tcpdump -i any udp port 5062 -A -c 100抓 SIP 信令,看 INVITE、200 OK、ACK 的时序是否完整。再看 RTP 流,ffprobe -v trace -i video.sdp能打印出 RTP 包的详细信息,确认 payload type、时间戳、SSRC 三条关键字段。如果 ffprobe 能看到Stream #0:0: Video: h264,说明码流正确。

从命令行过渡到代码实现时,优先替换信令层,保留 ffplay 做解码。比如先用 C++ 调用 eXosip 把信令跑通,媒体侧仍然用fork/exec拉起 ffplay。确认信令逻辑完全稳定后,再把 ffplay 替换成 libavformat + libavcodec 的解码管线。这个顺序能让你把 SIP 状态机的问题和音视频解码的问题分开排查。

一个小技巧:在推流命令里故意加-fflags nobuffer和-flags low_delay虽然是一堆常见的低延迟参数,但真正决定延迟的是编码器和缓冲设置。我后来在写代码时,一直保留着-rtsp_transport tcp和-tune zerolatency这两个习惯,前者避免摄像头拉流断流,后者保证编码延迟。所有测试过的参数我都写进注释里,方便回滚。从那次踩坑之后,我每次做 SIP 媒体联调,都强制自己在信令层用同一套脚本验证一遍,再动代码,希望这套思路也能帮到你。

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

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

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

立即咨询