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运行起来,毕竟人家是一个服务。只有运行以后才能接收客户端发来的消息。

1bool Init(std::string concat, int logLevel) 2{ 3 this->concat = concat; 4 pj_log_set_level(logLevel); 5 auto status = pj_init(); 6 7 status = pjlib_util_init(); 8 9 pj_caching_pool_init(&cachingPool, &pj_pool_factory_default_policy, 0); 10 11 status = pjsip_endpt_create(&cachingPool.factory, nullptr, &endPoint); 12 13 status = pjsip_tsx_layer_init_module(endPoint); 14 15 status = pjsip_ua_init_module(endPoint, nullptr); 16 17 pool = pj_pool_create(&cachingPool.factory, "proxyapp", 4000, 4000, nullptr); 18 auto pjStr =StrToPjstr(GetAddr()); 19 20 pj_sockaddr_in pjAddr; 21 pjAddr.sin_family = pj_AF_INET(); 22 pj_inet_aton(&pjStr, &pjAddr.sin_addr); 23 24 auto port = GetPort(); 25 pjAddr.sin_port = pj_htons(static_cast<pj_uint16_t>(GetPort())); 26    status = pjsip_udp_transport_start(endPoint, &pjAddr, nullptr, 1, nullptr); 27 if (status != PJ_SUCCESS) return status; 28 29 auto realm = StrToPjstr(GetLocalDomain()); 30 return pjsip_auth_srv_init(pool, &authentication, &realm, lookup, 0) == PJ_SUCCESS ? true : false; 31 32}

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

二. 应答注册消息

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

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

服务端应答注册消息代码

1bool OnReceive(pjsip_rx_data* rdata) override 2{ 3 if(rdata->msg_info.cseq->method.id == PJSIP_REGISTER_METHOD) 4 { 5   auto expires = static_cast<pjsip_expires_hdr*>(pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_EXPIRES, nullptr)); 6   auto authHdr = static_cast<pjsip_authorization_hdr*>(pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_AUTHORIZATION, nullptr)); 7   if(expires && expires->ivalue > 0 ) 8   { 9 if(authHdr) 10 { 11   cout <<"receive register info"<<endl; 12   response(rdata, PJSIP_SC_OK, DateHead); 13   QureryDeviceInfo(rdata); 14 } 15 else 16 { 17   response(rdata, PJSIP_SC_UNAUTHORIZED, AuthenHead); 18 } 19 return true; 20   } 21 } 22 return false; 23} 24 25OnReceive 是服务端接收注册消息以后的响应方法,也就是说要将OnReceive作为入参传给PJSIP,完成此项功能在初始化PJSIP Moudle时。至于PJSIP moudle,这里不多解释,想要知道细节的话,可以查看PJSIP文档,文档群里有,代码如下: 26 27bool Init(std::string concat, int loglevel) 28{ 29  bool ret = false; 30 if(!mainModule) 31 { 32 ret = context.Init(concat,loglevel); 33 if(!ret) return ret; 34 35 static struct pjsip_module moudle = 36 { 37   nullptr, nullptr, 38 { "MainModule", 10 }, 39 -1, 40 PJSIP_MOD_PRIORITY_APPLICATION, 41 nullptr, 42 nullptr, 43 nullptr, 44 nullptr, 45 nullptr, 46 &CGSipMedia::OnReceive, 47 nullptr, 48 nullptr, 49 nullptr, 50 }; 51 mainModule = &moudle; 52 pjsip_inv_callback callback; 53 pj_bzero(&callback, sizeof(callback)); 54 callback.on_state_changed = &onStateChanged; 55 callback.on_new_session = &onNewSession; 56 callback.on_tsx_state_changed = &onTsxStateChanged; 57 callback.on_rx_offer = &onRxOffer; 58 callback.on_rx_reinvite = &onRxReinvite; 59 callback.on_create_offer = &onCreateOffer; 60 callback.on_send_ack = &onSendAck; 61 ret = context.RegisterCallback(&callback); 62 if(!ret ) return ret; 63 64 context.InitModule(); 65 ret = context.RegisterModule(mainModule); 66 if(!ret ) return ret; 67 68 CGSipModule::GetInstance().Init(); 69 ret = context.CreateWorkThread(&proc,workthread,nullptr,"proxy"); 70 } 71 return ret; 72 }

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

1void Response(pjsip_rx_data* rdata, int st_code,int headType) 2 { 3   std::lock_guard<mutex> lk(lock); 4 pjsip_tx_data* tdata; 5  pjsip_endpt_create_response(endPoint, rdata, st_code, nullptr, &tdata); 6 auto date = DateTimeFormatter::format(LocalDateTime(), "%Y-%m-%dT%H:%M:%S"); 7 pj_str_t c; 8 pj_str_t key; 9 pjsip_hdr *hdr; 10 switch(headType) 11 { 12 case DateHead: 13   key = pj_str("Date"); 14   hdr = reinterpret_cast<pjsip_hdr*>(pjsip_date_hdr_create(pool, &key, pj_cstr(&c, date.c_str()))); 15    pjsip_msg_add_hdr(tdata->msg, hdr); 16    break; 17 case AuthenHead: 18   pjsip_auth_srv_challenge(&authentication, nullptr, nullptr, nullptr, PJ_FALSE, tdata); 19   break; 20 default: 21 break; 22 } 23 pjsip_response_addr addr; 24 pjsip_get_response_addr(pool, rdata, &addr); 25 pjsip_endpt_send_response(endPoint, &addr, tdata, nullptr, nullptr); 26 }

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

                                                                                                                                               图4 SIP服务应答注册消息

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

                                  图5  “200 OK” 具体内容

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

给出具体的内容是:

1void QueryDeviveInfo(GBDevice *device, const string& scheme = "Catalog") 2{ 3  char szQuerInfo[200] = { 0 }; 4  pj_ansi_snprintf(szQuerInfo, 200, 5 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" 6 "<Query>\n" 7 "<CmdType>%s</CmdType>\n" 8 "<SN>17430</SN>\n" 9 "<DeviceID>%s</DeviceID>\n" 10 "</Query>\n", scheme.c_str(), device->GetUser() 11 ); 12  pjsip_tx_data *tdata; 13  const pjsip_method method = { PJSIP_OTHER_METHOD,{ "MESSAGE", 7 } }; 14  auto text = StrToPjstr(string(szQuerInfo)); 15  pjsip_endpt_create_request(endPoint, &method, &StrToPjstr(device->GetSipIpUrl()), &StrToPjstr(concat), &StrToPjstr(device->GetSipCodecUrl()),&StrToPjstr(concat), nullptr, -1, &text, &tdata); 16  tdata->msg->body->content_type.type = pj_str("Application"); 17  tdata->msg->body->content_type.subtype = pj_str("MANSCDP+xml"); 18  pjsip_endpt_send_request(endPoint, tdata, -1, nullptr, nullptr);}

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

1bool OnReceive(pjsip_rx_data* rdata) override 2{ 3  if (rdata->msg_info.cseq->method.id == PJSIP_OTHER_METHOD) 4  { 5 CGXmlParser xmlParser(context.GetMessageBody(rdata)); 6 CGDynamicStruct dynamicStruct; 7 dynamicStruct.Set(xmlParser.GetXml()); 8 9 auto cmd = xmlParser.GetXml()->firstChild()->nodeName(); 10 auto cmdType = dynamicStruct.Get<std::string>("CmdType"); 11 if (cmdType != "Catalog") return false; 12 13 auto DeviceID = dynamicStruct.Get<std::string>("DeviceID"); 14 15 Vector deviceList = dynamicStruct.Get<Vector>("DeviceList"); 16 17 for (auto& x : deviceList) 18 { 19   CGCatalogInfo devinfo; 20 try 21 { 22   devinfo.PlatformAddr = rdata->pkt_info.src_name; 23   devinfo.PlatformPort = rdata->pkt_info.src_port; 24 25   devinfo.Address = x["Address"].convert<string>(); 26   devinfo.Name = WstringToString(x["Name"].convert<wstring>()); 27   devinfo.Manufacturer = x["Manufacturer"].convert<string>(); 28   devinfo.Model = x["Model"].convert<string>(); 29   devinfo.Owner = x["Owner"].convert<string>(); 30   devinfo.Civilcode = x["CivilCode"].convert<string>(); 31   devinfo.Registerway = x["RegisterWay"].convert<int>(); 32   devinfo.Secrecy = x["Secrecy"].convert<int>(); 33   //devinfo.IPAddress = x["IPAddress"].convert<string>(); 34   devinfo.DeviceID = x["DeviceID"].convert<string>(); 35   devinfo.Status= x["Status"].convert<string>(); 36 } 37 catch (...) 38 { 39 //continue; 40 } 41 if(callback) 42 { 43 callback(user, &devinfo); 44 } 45 //SipControlModule::GetInstance().CatalogCallBack(devinfo); 46 } 47 48 response(rdata, PJSIP_SC_OK,NoHead); 49 return true;

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

1static string createSDP(MediaContext& mediaContext) 2{ 3 char str[500] = { 0 }; 4 pj_ansi_snprintf(str, 500, 5 "v=0\n" 6 "o=%s 0 0 IN IP4 %s\n" 7 "s=Play\n" 8 "c=IN IP4 %s\n" 9 "t=0 0\n" 10 "m=video %d RTP/AVP 96 98 97\n" 11 "a=recvonly\n" 12 "a=rtpmap:96 PS/90000\n" 13 "a=rtpmap:98 H264/90000\n" 14 "a=rtpmap:97 MPEG4/90000\n" 15 "y=0100000001\n", 16 mediaContext.GetDeviceId().c_str(), 17 mediaContext.GetRecvAddress().c_str(), 18 mediaContext.GetRecvAddress().c_str(), 19 mediaContext.GetRecvPort() 20 ); 21 return str; 22}

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

1bool Invite(pjsip_dialog *dlg, MediaContext mediaContext, string sdp) 2{ 3 pjsip_inv_session *inv; 4 if (PJ_SUCCESS != pjsip_inv_create_uac(dlg, nullptr, 0, &inv)) return false; 5 pjsip_tx_data *tdata; 6 if (PJ_SUCCESS != pjsip_inv_invite(inv, &tdata)) return false; 7 pjsip_media_type type; 8 type.type = pj_str("application"); 9 type.subtype = pj_str("sdp"); 10 auto text = pj_str(const_cast<char *>(sdp.c_str())); 11 try 12 { 13 tdata->msg->body = pjsip_msg_body_create(pool, &type.type, &type.subtype, &text); 14 15 auto hName = pj_str("Subject"); 16 auto subjectUrl = mediaContext.GetDeviceId() + ":" + SiralNum + "," + GetInstance().GetCode() + ":" + SiralNum; 17 auto hValue = pj_str(const_cast<char*>(subjectUrl.c_str())); 18 auto hdr = pjsip_generic_string_hdr_create(pool, &hName, &hValue); 19 pjsip_msg_add_hdr(tdata->msg, reinterpret_cast<pjsip_hdr*>(hdr)); 20 pjsip_inv_send_msg(inv, tdata); 21 } 22 catch (...) 23 { 24 } 25 return true; 26}

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

                                                图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群127903734 ,或者QQ:350197870。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

00:Java简单了解

浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。