简介:面向FreeSWITCH开发者与国标视频接入场景的GB28181协议支持模块源码包,可直接编译集成到FreeSWITCH,用于对接符合GB/T 28181-2016标准的网络摄像机、NVR及视频平台,解决异构设备统一接入问题。包内共9个文件,以核心C实现(mod_gb28181.c)、跨平台构建脚本(Makefile.am/Makefile.in)、Windows工程配置(.vcxproj)为主,另有Python示例脚本、配置模板、自动加载配置说明和README文档,压缩包整体仅17KB,结构紧凑且各文件职责明确,适合快速阅读和二次修改。已有57人学习参考。功能覆盖SIP信令注册、心跳保活、RTP over UDP实时拉流、设备目录查询、云台控制、录像回放等关键能力,适配主流FreeSWITCH版本。conf目录提供基础配置模板,autoload_configs说明自动加载方式,省去自行摸索配置的时间;配合README中的编译步骤和部署场景,无需额外中间件即可作为国标视频统一接入网关,适合安防集成或统一通信项目中需要国标接入能力的中高级开发者。
1. GB28181 国标接入不是加个模块那么简单:FreeSWITCH 在这里到底管哪一段
凌晨两点的值班室,监控平台上的一路摄像机离线了,故障单转到我这。这不是普通网络问题,是国标 28181 注册掉线了。我接过的项目里,把 GB28181 视频接入模块跑在 FreeSWITCH 下,是最容易让融合通信团队集体翻车的一件事:听起来是“加一个模块”的事,做起来要同时懂 SIP、懂 RTP、懂 PS 封装,还得摸清各厂商设备那些不按协议走的行为。这个源码包解决的就是“让 FreeSWITCH 能看懂国标设备的信令和视频流”这最后一公里——网上搜“海康威视gb28181接入平台开源”或“gb28181客户端”的人,多半是想找一个现成方案,但源码包到手不等于能跑起来。本文从协议拆解讲到编译落地,再到四个高频坑,适合正在做应急调度、视频联动、语音对讲,或者要把监控摄像头接到已有呼叫中心里的人。
2. 拆开国标协议栈:信令、媒体、对讲三条线在模块里各走什么路
2.1 国标信令的 SIP 外壳与 FreeSWITCH 原生协议栈的差异
先说结论:FreeSWITCH 自带的 sofia 协议栈不认识 GB28181 的私有扩展,你不能直接把摄像头注册到 sofia profile 上。
GB28181 规范全称《公共安全视频监控联网系统信息传输、交换、控制技术要求》,信令部分基于 SIP 2.0,但做了一堆私有扩展。比如 REGISTER 请求里携带的 User-Agent 是设备厂商自己的标识,INVITE 请求的 SDP 里媒体描述是PS/90000,而不是 FreeSWITCH 认识的H264/90000。更麻烦的是国标设备的注册周期短、重传快,不少设备收到 401 之后不会像 RFC3261 那样严格重发 REGISTER,而是直接发一个 CSeq 不连续的 REGISTER。sofia 处理这种包时因为 CSeq 不连续直接回 403,设备侧就报注册失败。
所以市面上的 GB28181 接入模块有个通用架构取向:模块自身维护一套独立的 SIP 协议栈(常见基于 eXosip2),在一个独立 UDP 端口上监听国标设备信令,而不是让设备直接注册到 sofia。模块收到 REGISTER 之后自己完成鉴权和心跳维护;收到 INVITE 之后,通过 ESL 接口向 FreeSWITCH 发起 originate 呼叫,把国标会话“翻译”成一个 FreeSWITCH 能处理的普通呼叫。这个设计在源码包的目录结构里一眼就能看出来——凡是拆成sip、media、esl三个子目录的,走的就是这条路。
用独立协议栈还有个现实好处:国标设备注册周期名义上是 3600 秒,但实际很多设备 600 秒就重发一次 REGISTER,而且凌晨批量上线时会瞬间涌入几百个 REGISTER。这些流量如果全进 sofia,会被当作异常 REGISTER 处理;放在独立栈里,模块可以做限流和会话收敛,把同一个域下多台设备的注册统一管理。
2.2 媒体通道:PS 流解封装与 RTP 负载类型映射
GB28181 的媒体封装是 RTP over UDP,但 RTP 的 payload 通常不是裸 H.264,而是 PS 流(Program Stream)。PS 流是 MPEG-2 系统层封装格式,里面以 PES 包形式同时携带视频(H.264/H.265/MPEG4)和音频(G.711/AAC)。FreeSWITCH 的媒体调度器只认标准 RTP 封装,比如 RFC3984 的单一 NAL 单元模式,它不知道 PS 流里怎么区分帧边界。
所以模块的媒体通道要做两件事。第一是 RTP 负载类型映射:国标设备发的 RTP 包 PT 可能是 96、98、112,而 FreeSWITCH 会话期望的是一个固定 PT。模块需要在收到第一个 RTP 包时解析实际 PT,并为每个会话维护一张从设备 PT 到 FreeSWITCH 侧 PT 的映射表。第二是 PS 解封装:把 PS 流的系统头、节目流映射表跳过,定位到 PES 包,再从 PES 里剥离出 H.264 的 NAL 单元,按 RFC3984 重新打成 FreeSWITCH 认识的 RTP 包。
最容易忽略的是时间戳。PS 流里 PES 头的 PTS 是 90kHz 时钟,但设备实现差异很大,有的 PTS 从零开始,有的从 Unix 时间戳换算。模块重打包时如果直接转发原始时间戳,FreeSWITCH 侧 jitter buffer 会乱跳,画面一顿一顿。我的做法是:模块维护会话级 RTP 时间戳基准,第一个包到达时记下base_pts,后续包用pts - base_pts + 固定偏移,让 FreeSWITCH 看到连续递增的时间戳序列。
2.3 语音对讲为什么是另一个世界
GB28181 的语音对讲是双向的,但上行和下行走的不是同一条路。上行方向,设备采集麦克风音频,通常以 G.711A 或 G.711U 编码进 PS 流,和视频一起封装;下行方向,平台要对设备喊话,需要把 FreeSWITCH 侧的音频以 RTP 单流发给设备,设备回 200 OK 后开始播放。
麻烦在容易翻车的是编码协商:FreeSWITCH 默认用 PCMU(G.711U),而国内监控设备 90% 默认只收 PCMA(G.711A),还有一批只认 G.722。如果模块不干预协商,直接转发 FreeSWITCH 的 200 OK 给设备,设备发现 SDP 里是 PCMU 直接回 488,或者干脆不响应。网上搜“gb28181语音对讲”常见到的“设备已呼通但听不到声音”,基本就是这个原因。所以模块处理对讲下行时,必须在 FreeSWITCH 的 dialplan 或 originate 拨号串里强制音频编码为设备期望的那一种。这一点在后面第 4 章和第 5 章会再展开。
3. 把源码包变成能跑的模块:最小依赖、编译要点与一台设备注册进来
3.1 先看源码包的目录设计,判断它是不是“正经包”
拿到源码包,先别急着编译,花十分钟把目录过一遍。一个设计合理的 GB28181 接入模块,顶层目录至少能看出三层职责:
mod_gb28181/ ├── src/ │ ├── gb28181_sip.c # eXosip 信令处理:REGISTER/INVITE/BYE/MESSAGE │ ├── gb28181_media.c # RTP 收发、PS 解封装、PT 映射 │ ├── gb28181_session.c # 会话状态机与超时回收 │ ├── gb28181_esl.c # ESL 客户端:向 FreeSWITCH 发 originate 命令 │ └── mod_gb28181.c # FreeSWITCH 模块入口:load/unload ├── conf/ │ ├── gb28181.conf.xml # 模块运行参数 │ └── acl.conf.xml ├── scripts/ │ └── install.sh └── README.md这个结构里最重要的判断标准:mod_gb28181.c是否和业务逻辑分离。如果整个包就一个.c文件,把信令和媒体逻辑全塞在模块回调里,那线上跑的时候很容易拖垮 FreeSWITCH 主进程——协议栈的 IO 线程和 FS 的 session 线程互相抢锁。我见过不止一次这种单文件模块,设备一批量上线,FS 进程 CPU 冲满,同机的其他 VoIP 呼叫全部受影响。
信令层和媒体层分离还有个实际好处:信令掉线时媒体流不一定马上断。模块可以在gb28181_session.c里独立维护会话超时,信令 60 秒没心跳但媒体 5 秒内还有包,就先不拆会话,等媒体也超时再回收端口。这个“先媒体后信令”的回收顺序,能减少大屏上“视频还在播放,设备状态却显示离线”的割裂问题。
3.2 编译最小模块:依赖安装与手动编译的三个关键点
编译前先确认 FreeSWITCH 版本。源码包如果明确写了兼容 FreeSWITCH 1.10 或 2.0,就对好版本再编,不要拿 1.10 的头文件编 2.0 的模块,mod 目录下的 ABI 不兼容会导致模块加载失败。Debian/Ubuntu 系统上一套最小依赖安装步骤如下:
# 安装编译依赖 apt-get install -y build-essential libexosip2-dev libosip2-dev \ libssl-dev libavformat-dev libavcodec-dev libavutil-dev # 确认 FreeSWITCH 头文件存在 ls /usr/local/freeswitch/include/freeswitch/switch.h # 进入源码包主目录编译安装 cd /usr/local/src/mod_gb28181 make make install如果源码包没有提供 Makefile,只给了源码,就需要手动编译。手动编译时最关键的三个点是头文件路径、链接库顺序、以及 eXosip 的 API 版本:
export FS_INC=/usr/local/freeswitch/include/freeswitch export FS_LIB=/usr/local/freeswitch/lib gcc -shared -fPIC -o mod_gb28181.so \ src/mod_gb28181.c src/gb28181_sip.c src/gb28181_media.c \ src/gb28181_session.c src/gb28181_esl.c \ -I$FS_INC -I/usr/include/eXosip2 \ -L$FS_LIB -lfreeswitch \ -leXosip2 -losip2 -lssl -lavformat -lavcodec -lavutil \ -Wl,-rpath,$FS_LIB cp mod_gb28181.so $FS_LIB/mod/说明一下参数:-I$FS_INC指向 FreeSWITCH 头文件,编译主要用到switch.h、switch_types.h、switch_module_interface.h,这几个头文件定义了模块加载接口和会话 API;-leXosip2链接的是国标信令栈库。注意 eXosip 4.x 里库名是eXosip2,2.x 里叫eXosip,链接前用dpkg -L libexosip2-dev确认实际库名,避免“undefined reference to eXosip_init”这类报错;-lavformat用于 PS 解封装,如果源码包是自己实现的 PS 解析器,可以不链接 ffmpeg 相关库。
还有一个容易踩的坑:有人在 Windows 上编译好后把 .dll 或 .so 拷到 Linux 服务器。GB28181 接入模块牵扯到 UDP 端口绑定和网卡多队列,这种代码最好在目标服务器上用相同内核版本编译。内核的 epoll 行为、UDP 收包缓冲区大小不一致,搬过去经常出现“编译没报错、跑起来丢包”这种玄学问题。
注意:编译报错时先看第一条错误,不要翻最后一条。绝大多数是头文件路径不对或库版本不匹配,第一条错误指向的缺失头文件或符号就是根因。
3.3 最小配置:让第一台海康/大华设备注册进来
编译安装完成,在 FreeSWITCH 的autoload_configs目录下放一个最小配置:
<configuration name="gb28181.conf" description="GB28181 Module"> <settings> <param name="sip-port" value="15060"/> <param name="sip-id" value="34020000002000000001"/> <param name="sip-domain" value="3402000000"/> <param name="sip-password" value="12345678"/> <param name="esl-host" value="127.0.0.1"/> <param name="esl-port" value="8021"/> <param name="esl-password" value="ClueCon"/> <param name="media-port-range" value="40000-41000"/> <param name="rtp-pt-video" value="96"/> <param name="rtp-pt-audio" value="98"/> </settings> </configuration>sip-port是模块自己监听的国标信令端口,不能和 FreeSWITCH 的 sofia 端口冲突,建议避开 5060、5080,用 15060 这种高位端口;sip-id是平台侧在国标系统里的 20 位编码,前 10 位是行政区划代码,中间 2 位是类型(200 代表平台),后 8 位是序号;sip-domain填设备所属的域编码。esl-password默认的ClueCon是 FreeSWITCH 出厂密码,生产环境必须改掉。
media-port-range是模块给国标设备发流的 RTP 端口段,这个段要同时加到防火墙白名单里,第 5 章会具体讲。配置好后在 FreeSWITCH 控制台执行:
module load mod_gb28181然后把海康威视摄像机或大华相机的国标配置页打开,填入平台 SIP 服务器地址、端口 15060、设备自己的 20 位 SIP 用户 ID、域编码和密码。设备保存配置后,模块日志会打出一行类似REGISTER from 34020000001320000001的记录,到这里第一台设备就算接入进来了。
4. 打通 FreeSWITCH 与国标设备:dialplan 路由和媒体协商的几个坑
4.1 用 ESL 把国标 INVITE 变成 FreeSWITCH 的 originate 呼叫
模块收到国标设备的 INVITE(通常是上级平台或客户端要拉流),要把这次请求变成 FreeSWITCH 能处理的一次呼叫。最常见做法是模块通过 ESL 连接 Event Socket,发一条 originate 命令。gb28181_esl.c里的核心逻辑大致是这样:
import socket import time def send_originate(session_id, rtp_host, rtp_port): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(("127.0.0.1", 8021)) sock.send(b"auth ClueCon\r\n\r\n") time.sleep(0.3) cmd = ( 'bgapi originate {gb28181_session_id=%s,hangup_after_bridge=true}' 'user/1000 &bridge(rtp://%s:%d)\r\n\r\n' % (session_id, rtp_host, rtp_port) ) sock.send(cmd.encode())这段代码的逻辑是:把国标会话 ID 作为自定义 channel 变量传给 FreeSWITCH,让 FreeSWITCH 呼一个本地分机user/1000,分机应答后通过&bridge桥接到模块的 RTP 端口上。这样 FreeSWITCH 侧的呼叫状态就是一个普通呼叫,挂断、保持、转接都能用现成 API。
参数说明:bgapi是异步执行,不会阻塞 ESL 连接,多路并发拉流时其中一路卡住不会拖累其他请求;hangup_after_bridge=true表示桥接结束后自动挂断,避免残留会话;gb28181_session_id变量必须传给 dialplan,后续路由靠它反查会话。
有个细节值得说:不要直接在 originate 命令里bridge(rtp://...),应该让呼叫先进入 dialplan。国标设备的 SDP 里音频视频参数组合复杂,dialplan 里可以用变量和条件做更灵活的路由。
4.2 dialplan 怎么写:按国标会话 ID 路由并保持呼叫存活
常见做法是约定一个特殊被叫号码前缀,比如gb_后面跟国标会话 ID:
<extension name="gb28181-router"> <condition field="destination_number" expression="^gb_(\d+)$"> <action application="answer"/> <action application="set" data="gb28181_session_id=$1"/> <action application="set" data="rtp_autoadjust_rate=true"/> <action application="set" data="rtp_append_audio_codec=PCMA"/> <action application="set" data="rtp_append_video_codec=H264"/> <action application="playback" data="silence_stream://1"/> </condition> </extension>这个 dialplan 的核心是保持信道存活,把媒体控制权交给模块。answer让 FreeSWITCH 进入应答状态,否则媒体通道不会建立;rtp_autoadjust_rate=true允许 FreeSWITCH 自动调整 RTP 发送速率,国标设备码率波动大时不容易溢出;rtp_append_audio_codec=PCMA和rtp_append_video_codec=H264是给媒体协商强制指定编码,避免 FreeSWITCH 默认的 PCMU 把设备带偏。playback silence_stream://1放一秒静音流,让 FreeSWITCH 的媒体调度器开始跑起来,音频视频通道才真正生效。
注意这里没有做真正的桥接,因为国标模块自己在 RTP 层面负责媒体转换,dialplan 只负责把呼叫状态保持在“通话中”。如果信道状态掉到hangup,模块会收到 BYE,设备端就会断流。大屏上出现“呼叫还在但画面卡住”时,先查 FreeSWITCH 里这个 channel 状态是不是还是ACTIVE。
4.3 媒体协商参数:PT 映射、SSRC 和编码优先级
GB28181 设备发来的 SDP 长这样:
m=video 30000 RTP/AVP 96 98 112 a=rtpmap:96 PS/90000 a=rtpmap:98 H264/90000 a=rtpmap:112 MPEG4/90000 a=ssrc:12345678FreeSWITCH 不认 PS 这种媒体描述,模块要在 INVITE 转发给 FreeSWITCH 之前做 SDP 重写。这里两个参数值得仔细调:rtp-pt-video,模块把 PS 和 H264 统一映射到这个 PT 上,再告诉 FreeSWITCH 这是H264/90000,FS 侧不需要知道底层是 PS;另一个是 SSRC 处理,FreeSWITCH 默认用自己的 SSRC,如果国标平台侧强制校验 SSRC,模块重写 SDP 时要把设备 SSRC 透传过去。
我一般建议在模块配置里加一组优先级参数:
<param name="video-priority" value="H264,PS,MPEG4"/> <param name="audio-priority" value="PCMA,PCMU,G722"/>这组参数的意思是:设备 SDP 里同时列出多种编码时,模块优先选 H264 处理,音频优先选 PCMA。原因很现实:PS 解封装在 ffmpeg 里走的是 mpegts 那套逻辑,对厂商自封装的 PS 流兼容性参差不齐,很多设备 PS 头打得不规范,解出来花屏;而 H264 直封包相对规整。音频选 PCMA 是因为国内监控设备几乎默认支持,FreeSWITCH 侧默认 PCMU,协商到 PCMA 后模块只做很小的负载类型转换,不用转码。
4.4 多域接入和重复注册:一个 FreeSWITCH 接两个国标平台
实际项目里经常出现一个 FreeSWITCH 既要接运营商级国标平台,又要接本地监控平台,两边设备域编码不同。模块配置里通常要开domain-checking开关,按域隔离会话:
<param name="domain-checking" value="on"/> <param name="max-registers-per-domain" value="1024"/>domain-checking=on的含义是:同一个设备 ID 在不同域下算不同实体,允许同时注册;同一个域下相同设备 ID 重复注册,才踢掉旧会话。这个开关不开的话,本地平台和设备直连同时注册时,后注册的会把先注册的顶掉。排查时非常隐蔽:两边看设备状态都在线,只有拉流时发现其中一路平台总是超时。
5. GB28181 接入排查手记:注册不上、黑屏、对讲没声的 4 个真实原因
5.1 设备一直显示“注册中”,UDP 信令被防火墙吃掉
现象:海康设备国标配置页一直显示“注册中”,模块日志里一条 REGISTER 都看不到。这不是协议问题,是网络问题。
原因:模块监听的 UDP 15060 端口没放通。很多运维习惯只放 TCP,不知道国标信令是 UDP。另一种是双网卡服务器,模块绑定了 0.0.0.0,设备请求从网卡 A 进来,回包走了网卡 B 的默认路由,设备侧等不到响应。
解决:先在服务器上用 tcpdump 确认信令是否到达:
tcpdump -i any udp port 15060 -nn -v看不到设备 IP 的包,检查设备到服务器的路由和防火墙;能看到请求但模块没回包,检查模块日志有没有绑定失败记录,再用ss -ulnp | grep 15060确认模块确实在监听。
5.2 设备已上线但拉流黑屏:RTP 端口段被防火墙挡住
现象:设备状态在线,用平台或 VLC 拉流画面黑屏,抓包能看到设备在发 RTP,但 FreeSWITCH 侧没收到。
原因:国标设备发流是设备主动向平台侧 RTP 端口推流,不是平台去设备那拉。设备在 NAT 后面或跨公网时,向模块的media-port-range推 RTP,端口未映射或运营商 UDP 限速,流量根本到不了模块。
解决:确认模块的media-port-range对应 UDP 端口段在防火墙全部放通:
iptables -A INPUT -p udp --dport 40000:41000 -j ACCEPT同时抓包确认模块侧有没有收到设备发来的 RTP:
tcpdump -i any udp portrange 40000-41000 -nn这个场景最容易踩坑的点:SIP 注册走 15060 端口通了,但媒体流走的是配置里完全不同的端口段,没通。两个端口段都通,设备才算真正“可用”。
注意:改完防火墙后不用重启模块,但需要重新拉流才会建立新的 RTP 会话。已经在黑屏状态的旧会话要先挂断。
5.3 有画面没声音:PCMA 与 PCMU 的编码偏好导致协商失败
现象:视频出来了,对讲或上行音频没声音,FreeSWITCH 侧呼叫日志里音频read_codec是 PCMU。
原因:FreeSWITCH 默认音频编码是 PCMU(G.711U),国内摄像头 90% 默认只发 PCMA(G.711A)。模块没在 SDP 重写时强制音频编码,FreeSWITCH 协商结果是 PCMU,设备发现这个编码不在能力集里,直接不发音频 RTP。
解决:在模块配置里把音频 PT 映射写死,dialplan 里显式指定:
<param name="audio-pt" value="98"/> <param name="force-audio-codec" value="PCMA"/>dialplan 里 originate 拨号串附加编码变量:
bgapi originate {gb28181_session_id=%s,absolute_codec_string=PCMA}user/1000 &bridge(...)absolute_codec_string=PCMA让 FreeSWITCH 侧只支持 PCMA,设备发现协商结果里有 PCMA 才发音频。注意改完要module reload mod_gb28181并重新拉流,新参数才生效,不用重启 FreeSWITCH。
5.4 设备离线后媒体端口一直占用,UDP 端口池耗尽
现象:跑了一周之后模块报no available RTP port,新设备拉流全部失败。
原因:国标设备异常掉线时只发 BYE 或者干脆不发,模块会话超时回收太慢,媒体端口没及时释放。尤其设备断电、断网时,TCP 的 FIN/RST 根本不存在,UDP 层面只能靠超时判断。
解决:把会话空闲超时调短,打开“媒体先于信令回收”开关:
<param name="session-timeout" value="180"/> <param name="media-timeout" value="60"/> <param name="recycle-media-first" value="true"/>这里的逻辑是:信令 180 秒无心跳判定会话死亡,但 RTP 媒体 60 秒无包就先释放端口。recycle-media-first=true后,媒体端口释放和会话销毁是两个独立动作,端口池不会被死会话占满。调完重启模块,日志里会出现media port recycled,端口池使用率应稳定在 60% 以下。
6. 验证与进阶:用抓包和日志把一次国标呼叫从头看到尾
6.1 一条 tcpdump 命令覆盖信令和媒体全程
验证国标接入最有效的做法不是看平台界面,而是抓包。信令端口 15060,媒体端口段 40000-41000,一次抓全:
tcpdump -i eth0 -s 0 -w /tmp/gb28181.pcap \ 'udp port 15060 or udp portrange 40000-41000'抓完用 Wireshark 打开,重点看三个时间点的时序:REGISTER 有没有回 200 OK、INVITE 后有没有 200 OK、以及 200 OK 后 1 秒内有没有 RTP 包到达。RTP 包在 Wireshark 里手动设置 payload type 为 96 并选 MP2T 解复用,否则 Wireshark 不认 PS 流。
6.2 日志调试开关:分级打开再收敛
模块日志通常从 error 到 debug 分级。我的做法是先开 info 跑一天确认无异常;出了问题再临时开 debug 抓 10 分钟,问题复现完立刻降回 info。FreeSWITCH 控制台执行:
console log gb28181,debugdebug 级别下会打印每个 RTP 包的 PT、SSRC 和序列号,最容易定位“协商是 96 实际发 98”这类设备不按协议走的毛刺。
做国标接入这几年,我的习惯是维护一张“设备 ID、信令端口、媒体端口、当前会话状态”四列表格,排查时先看表格再抓包。这个习惯帮我省掉了很多无头苍蝇式的翻日志时间。把这个思路用到你的项目里,上手速度会比直接翻代码快很多,希望帮到你。
本文还有配套的精品资源,点击获取