ffmpeg编译gb28181_国标GB28181对接视频流

今天抽空写下以GB28181的方式获取摄像机视频流以备后用,同时也希望能帮助到正着手开发GB28181对接视频的同学,这块的资料实在不多。

今天讲的内容不涉及到平台对接,平台对接下次有时间再讲,平台对接相对更麻烦点。通过GB28181获取摄像机视频流,首先需要摄像机支持GB28181

,如何知道摄像机是否支持GB28181协议呢?请看下图:

图1.摄像机28181协议配置图

图1 展示了海康摄像机配置GB28181页面,其他厂家摄像机GB28181配置页面(我遇到的)基本跟海康配置的页面相同。

下面介绍下各配置项基本意义:

本地端口:默认为5060,SIP服务发送命令给摄像机时需要知道摄像机GB28181端口号,要不向哪发?

SIP服务器ID:说简单就是 服务器的标识,只不过这个标识有一定的要求,具体请参见28181-2001标准安全防范视频监控联网系统信息传输交换控制技术要求.pdf

当然也可以参考新点的文档,新旧文档这部分差异不大。文档在从群里下载。

SIP服务域:实际就是SIP服务器ID前10位。

SIP服务器地址:SIP服务所在机器的IP地址(如果存在多网卡建议将不用的网卡禁用掉)。

SIP服务器端口:SIP服务Port,其他SIP服务发送命令到此端口与之通信。

其他的配置默认即可。

GB28181配置好以后,需要启动摄像机GB28181服务。

启动摄像机GB28181的方法是勾选“启用”选项,启动成功后,摄像机会向SIP Server发送注册消息,通过抓包可以看到具体的注册消息内容:

图2 摄像机发送注册消息图

看下注册消息的具体内容:

图3 具体注册消息图

重要是Cantact信息,包含了摄像机GB28181 SIP ID 以及IP地址和端口号,这样与摄像机通信的SIP服务就知道往哪里回应答消息。

摄像机端基本介绍了完了(摄像机端相当于SIP Client),下面 介绍CG28181 服务端也即 SIP Server,这正是我们要实现的。

实现CG28181服务端可以借助于现有的开源库 PJSIP,自己实现开发量还是很大的,具体的实现步骤如下:

一. 将PJSIP运行起来,毕竟人家是一个服务。只有运行以后才能接收客户端发来的消息。

bool Init(std::string concat, int logLevel)

{

this->concat = concat;

pj_log_set_level(logLevel);

auto status = pj_init();

status = pjlib_util_init();

pj_caching_pool_init(&cachingPool, &pj_pool_factory_default_policy, 0);

status = pjsip_endpt_create(&cachingPool.factory, nullptr, &endPoint);

status = pjsip_tsx_layer_init_module(endPoint);

status = pjsip_ua_init_module(endPoint, nullptr);

pool = pj_pool_create(&cachingPool.factory, "proxyapp", 4000, 4000, nullptr);

auto pjStr =StrToPjstr(GetAddr());

pj_sockaddr_in pjAddr;

pjAddr.sin_family = pj_AF_INET();

pj_inet_aton(&pjStr, &pjAddr.sin_addr);

auto port = GetPort();

pjAddr.sin_port = pj_htons(static_cast(GetPort()));

status = pjsip_udp_transport_start(endPoint, &pjAddr, nullptr, 1, nullptr);

if (status != PJ_SUCCESS) return status;

auto realm = StrToPjstr(GetLocalDomain());

return pjsip_auth_srv_init(pool, &authentication, &realm, lookup, 0) == PJ_SUCCESS ? true : false;

}

以上是PJSip初始化的代码,需要将服务将要监听的端口传给PJSIP,这样服务就在监听的端口接收SIP 消息了。

二. 应答注册消息

摄像机端发送来Register消息后,如果服务端不应答,摄像机端会一直发送直到收到服务端应答为止。如果服务器端重新运行,需要手动再次

开启摄像机,如果等摄像机自己再次发送注册消息可能是一个小时以后,我们当然不希望那么久。

服务端应答注册消息代码

bool OnReceive(pjsip_rx_data* rdata) override

{

if(rdata->msg_info.cseq->method.id == PJSIP_REGISTER_METHOD)

{

auto expires = static_cast(pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_EXPIRES, nullptr));

auto authHdr = static_cast(pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_AUTHORIZATION, nullptr));

if(expires && expires->ivalue > 0 )

{

if(authHdr)

{

cout <

response(rdata, PJSIP_SC_OK, DateHead);

QureryDeviceInfo(rdata);

}

else

{

response(rdata, PJSIP_SC_UNAUTHORIZED, AuthenHead);

}

return true;

}

}

return false;

}

OnReceive 是服务端接收注册消息以后的响应方法,也就是说要将OnReceive作为入参传给PJSIP,完成此项功能在初始化

PJSIP Moudle时。至于PJSIP moudle,这里不多解释,想要知道细节的话,可以查看PJSIP文档,文档群里有,代码如下:

bool Init(std::string concat, int loglevel)

{

bool ret = false;

if(!mainModule)

{

ret = context.Init(concat,loglevel);

if(!ret) return ret;

static struct pjsip_module moudle =

{

nullptr, nullptr,

{ "MainModule", 10 },

-1,

PJSIP_MOD_PRIORITY_APPLICATION,

nullptr,

nullptr,

nullptr,

nullptr,

nullptr,

&CGSipMedia::OnReceive,

nullptr,

nullptr,

nullptr,

};

mainModule = &moudle;

pjsip_inv_callback callback;

pj_bzero(&callback, sizeof(callback));

callback.on_state_changed = &onStateChanged;

callback.on_new_session = &onNewSession;

callback.on_tsx_state_changed = &onTsxStateChanged;

callback.on_rx_offer = &onRxOffer;

callback.on_rx_reinvite = &onRxReinvite;

callback.on_create_offer = &onCreateOffer;

callback.on_send_ack = &onSendAck;

ret = context.RegisterCallback(&callback);

if(!ret ) return ret;

context.InitModule();

ret = context.RegisterModule(mainModule);

if(!ret ) return ret;

CGSipModule::GetInstance().Init();

ret = context.CreateWorkThread(&proc,workthread,nullptr,"proxy");

}

return ret;

}

OnReceive方法内Resonse方法实现了发送响应数据到客户端(摄像机):

void Response(pjsip_rx_data* rdata, int st_code,intheadType)

{

std::lock_guard lk(lock);

pjsip_tx_data*tdata;

pjsip_endpt_create_response(endPoint, rdata, st_code, nullptr,&tdata);

auto date= DateTimeFormatter::format(LocalDateTime(), "%Y-%m-%dT%H:%M:%S");

pj_str_t c;

pj_str_t key;

pjsip_hdr*hdr;switch(headType)

{caseDateHead:

key= pj_str("Date");

hdr= reinterpret_cast(pjsip_date_hdr_create(pool, &key, pj_cstr(&c, date.c_str())));

pjsip_msg_add_hdr(tdata->msg, hdr);break;caseAuthenHead:

pjsip_auth_srv_challenge(&authentication, nullptr, nullptr, nullptr, PJ_FALSE, tdata);break;default:break;

}

pjsip_response_addr addr;

pjsip_get_response_addr(pool, rdata,&addr);

pjsip_endpt_send_response(endPoint,&addr, tdata, nullptr, nullptr);

}

实际也就是利用发PJSIP发送一些字符串给客户端。具体发送了些什么,可以抓个包看下。

图4 SIP服务应答注册消息

SIP 服务实际回了“200 OK” 给摄像机端。看下具体的消息内容:

图5  “200 OK” 具体内容

SIP服务端响应注册命令后,发送Invite请求,请求catalog信息,也就是设备基本信息,具体的方法上面已

给出,具体的内容是:

void QueryDeviveInfo(GBDevice *device, const string& scheme = "Catalog")

{

char szQuerInfo[200] = { 0 };

pj_ansi_snprintf(szQuerInfo, 200,

"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"

"\n"

"%s\n"

"17430\n"

"%s\n"

"\n", scheme.c_str(), device->GetUser()

);

pjsip_tx_data *tdata;

const pjsip_method method = { PJSIP_OTHER_METHOD,{ "MESSAGE", 7 } };

auto text = StrToPjstr(string(szQuerInfo));

pjsip_endpt_create_request(endPoint, &method, &StrToPjstr(device->GetSipIpUrl()), &StrToPjstr(concat), &StrToPjstr(device->GetSipCodecUrl()),&StrToPjstr(concat), nullptr, -1, &text, &tdata);

tdata->msg->body->content_type.type = pj_str("Application");

tdata->msg->body->content_type.subtype = pj_str("MANSCDP+xml");

pjsip_endpt_send_request(endPoint, tdata, -1, nullptr, nullptr);

}

SIP服务端 发送了请求catalog  消息,摄像机端收到消息发送其自身的catalog消息,SIP 服务端将在OnReceive中收到具体的catalog消息。取catalog消息的方法如下:

bool OnReceive(pjsip_rx_data* rdata) override

{

if (rdata->msg_info.cseq->method.id == PJSIP_OTHER_METHOD)

{

CGXmlParser xmlParser(context.GetMessageBody(rdata));

CGDynamicStruct dynamicStruct;

dynamicStruct.Set(xmlParser.GetXml());

auto cmd = xmlParser.GetXml()->firstChild()->nodeName();

auto cmdType = dynamicStruct.Get<:string>("CmdType");

if (cmdType != "Catalog") return false;

auto DeviceID = dynamicStruct.Get<:string>("DeviceID");

Vector deviceList = dynamicStruct.Get("DeviceList");

for (auto& x : deviceList)

{

CGCatalogInfo devinfo;

try

{

devinfo.PlatformAddr = rdata->pkt_info.src_name;

devinfo.PlatformPort = rdata->pkt_info.src_port;

devinfo.Address = x["Address"].convert();

devinfo.Name = WstringToString(x["Name"].convert());

devinfo.Manufacturer = x["Manufacturer"].convert();

devinfo.Model = x["Model"].convert();

devinfo.Owner = x["Owner"].convert();

devinfo.Civilcode = x["CivilCode"].convert();

devinfo.Registerway = x["RegisterWay"].convert();

devinfo.Secrecy = x["Secrecy"].convert();

//devinfo.IPAddress = x["IPAddress"].convert();

devinfo.DeviceID = x["DeviceID"].convert();

devinfo.Status= x["Status"].convert();

}

catch (...)

{

//continue;

}

if(callback)

{

callback(user, &devinfo);

}

//SipControlModule::GetInstance().CatalogCallBack(devinfo);

}

response(rdata, PJSIP_SC_OK,NoHead);

return true;

SIP服务取都摄像机的信息后就可以发送请求视频信息了,请求视频最为关键的是SDP,下面看下SDP信息如何填写:

static string createSDP(MediaContext& mediaContext)

{

char str[500] = { 0 };

pj_ansi_snprintf(str, 500,

"v=0\n"

"o=%s 0 0 IN IP4 %s\n"

"s=Play\n"

"c=IN IP4 %s\n"

"t=0 0\n"

"m=video %d RTP/AVP 96 98 97\n"

"a=recvonly\n"

"a=rtpmap:96 PS/90000\n"

"a=rtpmap:98 H264/90000\n"

"a=rtpmap:97 MPEG4/90000\n"

"y=0100000001\n",

mediaContext.GetDeviceId().c_str(),

mediaContext.GetRecvAddress().c_str(),

mediaContext.GetRecvAddress().c_str(),

mediaContext.GetRecvPort()

);

return str;

}

发送请求视频命令到摄像机端当然也是通过PJSIP API实现代码如下:

bool Invite(pjsip_dialog *dlg, MediaContext mediaContext, string sdp)

{

pjsip_inv_session *inv;

if (PJ_SUCCESS != pjsip_inv_create_uac(dlg, nullptr, 0, &inv)) return false;

pjsip_tx_data *tdata;

if (PJ_SUCCESS != pjsip_inv_invite(inv, &tdata)) return false;

pjsip_media_type type;

type.type = pj_str("application");

type.subtype = pj_str("sdp");

auto text = pj_str(const_cast(sdp.c_str()));

try

{

tdata->msg->body = pjsip_msg_body_create(pool, &type.type, &type.subtype, &text);

auto hName = pj_str("Subject");

auto subjectUrl = mediaContext.GetDeviceId() + ":" + SiralNum + "," + GetInstance().GetCode() + ":" + SiralNum;

auto hValue = pj_str(const_cast(subjectUrl.c_str()));

auto hdr = pjsip_generic_string_hdr_create(pool, &hName, &hValue);

pjsip_msg_add_hdr(tdata->msg, reinterpret_cast(hdr));

pjsip_inv_send_msg(inv, tdata);

}

catch (...)

{

}

return true;

}

代码就不解释了,要想知道到底发了什么还是抓个包看看,无论你用什么方法只要抓包的数据是正确定说明发送成功了。

图6 服务端发送invite视频消息

摄像机端收到Invite请求后,会将视频数据以rtp的方式推送到指定的端口,端口在invite消息指定。

这样在指定的地址(ip + port)就可以拿到数据了。

最后提供一个测试demo,demo的作用是可以让大家抓包,看看双方都发了些什么。

demo运行界面如下:

图6 demo运行初始界面

1.运行demo后,首先配置好配置,如果不知道可以默认,但IP地址需要修改,端口不能被占用。

2.完成配置各配置项以后点击获取视频源按钮 等待摄像机端注册。

3.摄像机端开启28181功能:具体的方法可以是:平台选择方式下拉框先选择一个非28181方式,点击保存,再选择28181方式并点击保存。

4.摄像机端成功开启28181功能以后,视频源下拉框中会显示摄像机的名称信息。

5.选中视频源下拉框中出现的选项并点击播放按钮,正常情况下会可以播放从摄像机端过来的视频流。

成功接入视频源并播放的运行界面如下。

图7 demo成功运行以后的界面

Demo 可以在群里下载。

如需交流,可以加QQ群1038388075,766718184,或者QQ:350197870

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/472288.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

表格布局等

1.设置黑色细边框技巧 表格的边框0;间距1;背景黑色; 单元格 背景 白色 2.特殊字符使用软键盘 3.使用图像占位符暂时代替未制作好的图片 4.小的背景.gif 平铺变成大的长条,占用资源少 5.当设置一个单元格的高度小于一定值时,无效;因为默认里面有一个空格字符,需要在代码里将空格…

LeetCode 1845. 座位预约管理系统(set)

文章目录1. 题目2. 解题1. 题目 请你设计一个管理 n 个座位预约的系统&#xff0c;座位编号从 1 到 n 。 请你实现 SeatManager 类&#xff1a; SeatManager(int n) 初始化一个 SeatManager 对象&#xff0c;它管理从 1 到 n 编号的 n 个座位。所有座位初始都是可预约的。in…

unity game和scene效果不一样_不同的真石漆装饰效果也是不一样的

外墙真石漆真的是一件很好的产品&#xff0c;具有防火性、防水性、安全且环保、粘力强、永不褪色等特点&#xff0c;无疑是人们较好的选择&#xff0c;在很早之前就已经逐渐的取代了瓷砖和其他石材在人们心中的位置。真石漆的品种不止一种&#xff0c;按照装饰效果我们可以分为…

噪声产生原因_空调噪声大?啄木鸟家庭维修,看看属于哪一个问题

夏天终于要结束了&#xff0c;我只想安静的睡一个好觉。这个夏天中&#xff0c;楼上的空调每夜不休不眠的工作着&#xff0c;可这个空调为什么运作时的声音这么“巨大”。“轰轰轰……”&#xff0c;楼层都似乎和它产生了共振。而每晚的我&#xff0c;反反复复努力入睡&#xf…

05-按钮的基本使用-开发步骤

从Xcode5开始&#xff0c;图片资源都放到Images.xcassets中进行管理先添加必须的图片到Images.xcassets中调整界面尺寸 由于模拟器的默认尺寸是3.5inch&#xff0c;为了避免出现不必要的麻烦&#xff0c;最好将storyboard中的UI界面尺寸也调整为3.5inch添加4个方向按钮和2个缩放…

【机器学习】sklearn数据特征预处理:归一化和标准化

归一化处理 特点&#xff1a;通过对原始数据进行变换把数据映射到(默认为[0,1])之间 from sklearn.preprocessing import MinMaxScaler def mm():"""归一化处理:return: NOne"""mm MinMaxScaler(feature_range(2,3))data mm.fit_transform(…

LeetCode 1848. 到目标元素的最小距离

文章目录1. 题目2. 解题1. 题目 给你一个整数数组 nums &#xff08;下标 从 0 开始 计数&#xff09;以及两个整数 target 和 start &#xff0c;请你找出一个下标 i &#xff0c;满足 nums[i] target 且 abs(i - start) 最小化 。注意&#xff1a;abs(x) 表示 x 的绝对值。…

python环境变量的运用_Windows下python环境变量配置

默认情况下&#xff0c;在windows下安装python之后&#xff0c;系统并不会自动添加相应的环境变量。此时不能在命令行直接使用python命令。1. 首先需要在系统中注册python环境变量&#xff1a;假设python的安装路径为c:\python2.6&#xff0c;则修改我的电脑->属性->高级…

python拆分合并文件_Python 视频文件的分割和合并

import os; import time; os.system(start "wmplayer.exe", "F:\\abc.mp4"); --分割代码 start---- import sys,os; kilobytes 1024; megabytes kilobytes*1024; chunksize int(10*megabytes); def split(fromfile,todir,chunksizechunksize): if not o…

[笔记]Go语言写文件几种方式性能对比

Go语言中写文件有多种方式&#xff0c;这里进行如下几种方式的速度对比&#xff1a; 打开文件&#xff0c;写入内容&#xff0c;关闭文件。如此重复多次打开文件&#xff0c;写入内容&#xff0c;defer 关闭文件。如此重复多次打开文件&#xff0c;重复多次写入内容&#xff0c…

【机器学习】sklearn数据集获取、分割、分类和回归

sklearn数据集1、数据集划分1.1 获取数据1.2 获取数据返回的类型举个栗子&#xff1a;1.3 对数据集进行分割举个栗子&#xff1a;2、 sklearn分类数据集3、 sklearn回归数据集1、数据集划分 机器学习一般的数据集会划分为两个部分&#xff1a; 训练数据&#xff1a;用于训练&a…

LeetCode 1846. 减小和重新排列数组后的最大元素

文章目录1. 题目2. 解题1. 题目 给你一个正整数数组 arr 。请你对 arr 执行一些操作&#xff08;也可以不进行任何操作&#xff09;&#xff0c;使得数组满足以下条件&#xff1a; arr 中 第一个 元素必须为 1 。任意相邻两个元素的差的绝对值 小于等于 1 &#xff0c;也就是…

ftp可以传输什么类型文件_FTP文件传输工具-ForkLift for Mac

orklift mac版是一款运行在Mac平台上的FTP文件传输工具。ForkLift拥有经典的两栏界面布局&#xff0c;简洁小巧。且支持FTP&#xff0c;SFTP&#xff0c;WebDAV&#xff0c;S3&#xff0c;iDisk&#xff0c;SMB&#xff0c;AFP和NIS协议&#xff0c;可以方便用户对本地以及远程…

1过程流程图 3 apqp_为什么过程开发的平面布置图要遵循精益原则?

今日话题为什么过程开发的平面布置图要遵循精益原则&#xff1f;问为什么过程开发的平面布置图要遵循精益原则&#xff1f;答工艺工程师根据过程流程图制定平面布置图&#xff0c;采用精益制造的原则&#xff0c;对加工与装配工位、物流路线、存储位置进行规划&#xff0c;以确…

把Scala代码当作脚本运行

1. 在类UNIX系统上作为脚本运行 在类Unix系统上&#xff0c;你可以设置一个shell前导词来执行脚本。如下例&#xff1a; Script.scala #!/usr/bin/env scala !# println("Hello" args(0)) 先输入chmod x Script.scala&#xff0c;确保对Script.scala文件有执行权限。…

LeetCode 1847. 最近的房间(排序离线计算 + 二分查找)

文章目录1. 题目2. 解题1. 题目 一个酒店里有 n 个房间&#xff0c;这些房间用二维整数数组 rooms 表示&#xff0c;其中 rooms[i] [roomIdi, sizei] 表示有一个房间号为 roomIdi 的房间且它的面积为 sizei 。每一个房间号 roomIdi 保证是 独一无二 的。 同时给你 k 个查询&…

用python写一个手机app签到脚本_利用Python实现App自动签到领取积分

要自动签到&#xff0c;最简单的是打开页面分析请求&#xff0c;然后我们用脚本实现请求的自动化。但是发现食行没有页面&#xff0c;只有 APP&#xff0c;这不是一个好消息&#xff0c;这意味着需要抓包处理了。有需要Python学习资料的小伙伴吗?小编整理【一套Python资料、源…

el表达式 循环_EL表达式和JSTL标签库(百战程序员047天)

1.EL表达式介绍&#xff1a;是一种非常简洁的表达式&#xff0c;语法简单&#xff0c;便于使用&#xff0c;灵感来源于ECMAScript和Xpath的表达式语言样式&#xff1a;${表达式}作用&#xff1a;简化jsp中获取作用域或者请求数据的写法。获取请求数据EL表达式1)获取请求数据(1)…

【机器学习】sklearn k-近邻算法

sklearn k-近邻算法1. sklearn k-近邻算法API2. k近邻算法实例-预测入住位置核心思想&#xff1a;你的“邻居”来推断出你的类别定义&#xff1a;如果一个样本在特征空间中的 k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别&#xff0c;则该样本也属于这个类别。…

openresty package.path require 报错

在文件中 package.path /usr/local/share/lua/5.1/?.lua;/usr/local/openresty/lualib/resty/?.lua; package.cpath /usr/local/lib/lua/5.1/?.so;执行local mysql require "mysql" --正确local mysql require "resty.mysql" --报错去掉 package.pa…