今天抽空写下以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<pj_uint16_t>(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_expires_hdr*>(pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_EXPIRES, nullptr));
  auto authHdr = static_cast<pjsip_authorization_hdr*>(pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_AUTHORIZATION, nullptr));
  if(expires && expires->ivalue > 0 )
  {
if(authHdr)
{
  cout <<"receive register info"<<endl;
  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,int headType)
{
  std::lock_guard<mutex> 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)
{
case DateHead:
  key = pj_str("Date");
  hdr = reinterpret_cast<pjsip_hdr*>(pjsip_date_hdr_create(pool, &key, pj_cstr(&c, date.c_str())));
   pjsip_msg_add_hdr(tdata->msg, hdr);
   break;
case AuthenHead:
  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"
"<Query>\n"
"<CmdType>%s</CmdType>\n"
"<SN>17430</SN>\n"
"<DeviceID>%s</DeviceID>\n"
"</Query>\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<std::string>("CmdType");
if (cmdType != "Catalog") return false; auto DeviceID = dynamicStruct.Get<std::string>("DeviceID"); Vector deviceList = dynamicStruct.Get<Vector>("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<string>();
  devinfo.Name = WstringToString(x["Name"].convert<wstring>());
  devinfo.Manufacturer = x["Manufacturer"].convert<string>();
  devinfo.Model = x["Model"].convert<string>();
  devinfo.Owner = x["Owner"].convert<string>();
  devinfo.Civilcode = x["CivilCode"].convert<string>();
  devinfo.Registerway = x["RegisterWay"].convert<int>();
  devinfo.Secrecy = x["Secrecy"].convert<int>();
  //devinfo.IPAddress = x["IPAddress"].convert<string>();
  devinfo.DeviceID = x["DeviceID"].convert<string>();
  devinfo.Status= x["Status"].convert<string>();
}
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<char *>(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<char*>(subjectUrl.c_str()));
auto hdr = pjsip_generic_string_hdr_create(pool, &hName, &hValue);
pjsip_msg_add_hdr(tdata->msg, reinterpret_cast<pjsip_hdr*>(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

视频教程 播放地址: https://space.bilibili.com/241181578/

视频下载地址:http://www.chungen90.com/?news_3/

Demo下载地址: http://www.chungen90.com/?news_2/

GB28181对接视频流的更多相关文章

  1. Onvif开发之服务端成功对接Rtsp视频流篇

    前面篇介绍onvif服务端的发现功能,继续在之前的代码基础上完成一个RTSP流的工作,也就是客户端通过ONVIF协议来预览设备端在这个之前必须确定几个简单的条件1 设备端能被发现2 设备端支持RTSP ...

  2. 3款知名RTMP推流模块比较:OBS VS SmartPublisher VS Flash Media Live Encoder

    OBS 功能强大,几乎所有你想要的场景它都有,用起来很顺手.可以将桌面.摄像头.程序窗口通过rtmp推送到流媒体服务器上. 当然如果你是开发者,想基于OBS做二次开发,实现二次产品化的化,难度比较大, ...

  3. Windows平台摄像头或屏幕RTMP推送介绍:OBS VS SmartPublisher

    好多开发者问道,既然有了OBS,你们为什么还要开发SmartPublisher? 的确,在我们进行Windows平台RTMP推送模块开发之前,市面上为数不多的Windows平台RTMP推流工具当属OB ...

  4. 轻便的gb28181协议中的rtp+ps格式视频流的封装和解析

    streams 轻便的gb28181协议中的rtp+ps格式视频流的封装和解析 packet packet实现ps的相关封装和解析, example/enc 通过joy4来读本地视频文件,然后调用Rt ...

  5. EasyNVR和EasyDSS云平台联手都不能解决的事情,只有国标GB28181能解决了

    需求痛点 我们经常收到这样一种需求,就是将客户手里的各种类型的网络摄像机IPC和网络硬盘录像机NVR进行统一的整合接入和管理,并进行常规的直播.存储.录像检索和回放等操作,而这个时候我们通常会选择用E ...

  6. GB28181国检推流

    GB28181国检有一向内容是实时播放摄像机流,经过一番努力,搞定这个功能,现分享心得: 首先需要了解流程,说简答点就是视频流从哪里来到什么地方去,下图描述了视频流推流,转发的 基本过程:信令交互成功 ...

  7. onvif规范的实现:成功实现ONVIF协议RTSP-Video-Stream与OnvifDeviceManager的视频对接

    有了前几篇的基础,现在可以正式开始onvif的实现工作,其中一项非常重要的部分就是视频流的对接,即能够在符合onvif标准的监控客户端软件里接收到设备端NVT发来的RTSP视频流.这里,我所用的客户端 ...

  8. 视频流GPU解码在ffempg的实现(二)-GPU解码器

    1.gpu解码器的基本调用流程 要做视频流解码,必须要了解cuda自身的解码流,因为二者是一样的底层实现,不一样的上层调用 那cuda的解码流程是如何的呢 在https://developer.nvi ...

  9. sip (gb28181)信令交互-视频点播与回播

    客户端发起的实时点播消息示范:(请求视频信令与断开视频信息 和 回播基本无差别) .请求视频流 INVITE sip:@ SIP/2.0 Via: SIP/;rport;branch=z9hG4bK2 ...

随机推荐

  1. Thanks for your encourage!

    将近三个月的学习,我的努力换回了代表荣誉的小黄衫,这令我很开心啊...我想是不是要写点什么来表达自己的心情呢=,=  于是就有了以下文字ahhhhhh... 学习心得: (1)学习中总会有失败和成功, ...

  2. [转]how to inserting multiple rows in one step

    To insert multiple rows in the table use executemany() method of cursor object. Syntax: cursor_objec ...

  3. java EE技术体系——CLF平台API开发注意事项(4)——API生命周期治理简单说明

    文档说明 截止日期:20170905,作者:何红霞,联系方式:QQ1028335395.邮箱:hehongxia626@163.com 综述 有幸加入到javaEE技术体系的研究与开发,也得益于大家的 ...

  4. 【Luogu】P3521ROT-Tree Rotations(线段树合并)

    题目链接 神奇的线段树合并qwq   不过就思路而言很好想…… 观察到一棵树无论怎么交换两棵左右子树,子树内部的最优逆序对并没影响……决策只影响左右子树之间的逆序对…… 于是线段树合并直接乱搞就好啦 ...

  5. 【Luogu】P3396哈希冲突(根号算法)

    题目链接 根号算法真的是博大精深啊……明明是暴力但复杂度就是能过 这也太强了吧!!! 预处理出p<=sqrt(n)的所有情况,耗时n根n 查询: 如果p<=根n,O1查表 如果p>= ...

  6. 周赛Problem 1025: Hkhv love spent money(RMQ)

    Problem 1025: Hkhv love spent money Time Limits:  1000 MS   Memory Limits:  65536 KB 64-bit interger ...

  7. P2420 让我们异或吧 (树链剖分,异或前缀和)

    题目描述 异或是一种神奇的运算,大部分人把它总结成不进位加法. 在生活中-xor运算也很常见.比如,对于一个问题的回答,是为1,否为0.那么: (A是否是男生 )xor( B是否是男生)=A和B是否能 ...

  8. 关于postman使用上发现的一点问题

    之前后台用的java,一直用的postman测试接口数据,之前不管是get.post.delete.put请求都是在param传递的数据,java下面是没问题可以测试的.但是今天自己写Node发现po ...

  9. javaweb学习总结(十)——HttpServletRequest对象(一)(转)

    (每天都会更新至少一篇以上,有兴趣的可以关注)转载自孤傲苍狼 一.HttpServletRequest介绍 HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器 ...

  10. 【CF1017A】The Rank(签到)

    题意:给定n个人的4门课成绩,排名按总分,保证总分互不相同,求1号名次 n<=1e3,a[i],b[i],c[i],d[i]<=1e2 思路: #include<cstdio> ...