
简介面向需要接入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. 为什么用 eXosipffmpeg 命令行搭 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 报文中的maudio、mvideo以及cIN 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-AuthenticatePJSIP 里会自动处理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 里的mvideo端口必须填 ffplay 实际绑定的端口而不是随便填一个。/* 构造 INVITE 的 SDP 部分命令行方案中视频端口要与 ffplay 一致 */ char sdp[1024]; snprintf(sdp, sizeof(sdp), v0\r\n o- 123456 2 IN IP4 %s\r\n sSIP Call\r\n cIN IP4 %s\r\n t0 0\r\n mvideo %d RTP/AVP 96\r\n artpmap:96 H264/90000\r\n arecvonly\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);这里我用arecvonly因为命令行测试阶段先只收画面不做编码推流。对端回 200 OK 后它的 SDP 里会给出mvideo 端口和cIP这才是 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 就能正确解码。v0 o- 0 0 IN IP4 0.0.0.0 sNo Name cIN IP4 0.0.0.0 t0 0 mvideo 6002 RTP/AVP 96 artpmap:96 H264/90000 afmtp:96 packetization-mode1;profile-level-id42C01E写好后启动命令换成ffplay -fflags nobuffer -protocol_whitelist rtp,udp,file -i local.sdpafmtp里的packetization-mode1是硬规定。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 21 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:password192.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:password192.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 21 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:pass192.168.1.64/... ...加了这个参数后RTSP 走 TCPRTP 还在 UDP相当于拉流链路可靠推流链路还是实时 UDP。这避免了摄像头侧 NAT 映射不稳定导致拉流断连的问题。5. eXosipffmpeg 联调避坑现象、原因、解决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 文件里面把artpmap:96 H264/90000写明白然后用ffplay -i video.sdp启动。每帧画面 GOP 太大时也黑屏可以在 ffplay 命令后面加-vf selecteq(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提供的rtpflagsskiprtcp不要和 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 5062ss -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 一直在等 RTPtcpdump发现对端发来的包到了一个很奇怪的端口和本地 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 媒体联调都强制自己在信令层用同一套脚本验证一遍再动代码希望这套思路也能帮到你。本文还有配套的精品资源点击获取