cg28181对接视频流

王纲 王纲     2022-09-18     651

关键词:

     今天抽空写下以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 Mo

udle时。至于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成功运行以后的界面

 

摄像机通过GB28181输出的视频也可以在Web端无插件播放如下图所示:



                                                                                  图8. Web端无插件播放画面

Demo 可以在群里下载。

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

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

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

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

 

 

 

 

                     

 

移动视频类设备&平台国标gb28181输入输出,gb28181平台对接说明

VMS/smarteye支持28181输入、输出及安卓设备走28181接第三方国标平台说明文档发布时间:2021-07-0116:43:471. 前言国标服务指 GB/28181-2011 协议,该协议规定了城市监控报警联网系统中信息传输、交换、控制的互联结构、通信协议结... 查看详情

移动视频类设备&平台国标gb28181输入输出,gb28181平台对接说明

...标准,便于平台接入其他厂商的监控设备,获取视频流。 2、Smarteye平台支持28181设备接入说明VMS/smarteye平台支持第三方的国标协议的设备接入,该实现机制是一个前置的视频接入协议网关的软件插件的形式。其实现... 查看详情

android平台gb28181设备接入端对接编码前后音视频源类型浅析

前言今天主要对Android平台GB28181设备接入模块支持的接入数据类型,做个简单的汇总:编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数... 查看详情

gb28181之平台注册

在上一篇文章中,介绍了视频联网监控平台的国标对接。平台对接的第一步就是平台注册,能否成功对接使平台上线就看平台能否注册上。那么平台注册的流程是怎样的呢?一起来探讨下。<1>平台注册流程平台正常注册分为... 查看详情

gb28181协议的用途是啥

...那边可以远程调里面的录像还是支持gb28181协议是指支持对接公安网系统,但并不是公安平台能看得到你的设备,只有对接后才能实现。gb28181协议是指GB/T28181-2011《安全防范视频监控联网系统信息传输、交换、控制技术要求》是... 查看详情

gb28181平台对接接口详解(代码片段)

28181平台对接接口详解信令接口:1.1平台注册下级平台主动向上级平台注册; 注册鉴权信息(用户名和密码)由上级提供;举例说明:下级--->上级REGISTERsip:62010000002000000001@10.130.140.82:7100SIP/2.0Call-ID:c82... 查看详情

srs4对接海康威视gb28181协议推流rtmp、webrtc拉流

参考技术AUbuntu20.04.2LTS(GNU/Linux5.4.0-88-genericx86_64)编译SRS,需要切换到Develop分支,并开启gb28181功能:配置文件:push.gb28181.conf然后使用配置文件conf/push.gb28181.conf启动:摄像头IP地址配置:GB28181推流配置:选择GB28181;RTMP观看地址:... 查看详情

javacv音视频开发宝典:使用javacv读取gb28181海康大华平台和网络摄像头sdk回调视频码流并转码推流rtmp流媒体服务(代码片段)

...并推流到rtmp代码实现这里演示如何正确读取大华sdk回调ps视频流,然后解析预览视频图像,然后推流到rtmp。 查看详情

android平台gb28181接入模块技术接入说明

...背景今天,我们主要讲讲Android平台GB28181接入模块的技术对接,Android平台GB28181接入模块设计的目的,可实现不具备国标音视频能力的Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如智能监控、智慧零售、智慧... 查看详情

android平台gb28181接入端如何对接uvc摄像头?

我们在对接Android平台GB28181接入的时候,有公司提出这样的需求,除了采集执法记录仪摄像头自带的数据外,还想通过执法记录仪采集外接UVC摄像头。实际上,这块对我们来说有点炒冷饭了,不算新的诉求。​​大牛直播SDK​​... 查看详情

android平台gb28181接入端如何对接uvc摄像头?

我们在对接Android平台GB28181接入的时候,有公司提出这样的需求,除了采集执法记录仪摄像头自带的数据外,还想通过执法记录仪采集外接UVC摄像头。实际上,这块对我们来说有点炒冷饭了,不算新的诉求。大牛直播SDK在2016年对... 查看详情

如何实现android平台gb28181设备对接camera2数据

技术背景在写如何实现Android平台GB28181设备对接Camera2数据说明之前,我在前两年的blog就有针对camera2的RTMP直播推送模块做过技术分享:在Google推出Android5.0的时候,AndroidCameraAPI版本升级到了API2(android.hardware.camera2),之前使用的API1(and... 查看详情

srs4对接海康威视gb28181协议推流rtmpwebrtc拉流(代码片段)

一、系统环境Ubuntu20.04.2LTS(GNU/Linux5.4.0-88-genericx86_64)三、源代码、配置、编译、运行gitclonehttps://github.com/ossrs/srs.git编译SRS,需要切换到Develop分支,并开启gb28181功能:gitcheckoutfeature/gb28181&&./configure--with-gb28181&&a... 查看详情

国标28181:实时视频播放(代码片段)

...xff0c;主要功能是管理摄像头之类的设备以及控制摄像头将视频流转发给流媒体服务器的哪个端口流媒体服务器主要用于处理视频流的接收、转发和分发接口服务器和信令服务器可以整合为一个服务器。流媒体服务器最好单独部署... 查看详情

gb28181之目录设备推送

上下级平台对台对接,下级平台向上级平台注册成功后才能够推送目录和设备。在上一篇文章中(GB28181之平台国标注册),讲述了平台对接后的注册过程。那么这篇文件将在平台注册成功的基础上介绍目录和设备的推送。平台... 查看详情

gb28181视频服务器文档整理

...两年时间了,早期我们用纯C++开发了一个GB28181视频服务期,对外的接口是基于MQ协议的。这样开发出来的服务器主要有几个问题。1.SIP服务器和流媒体服务器是绑定在一个进程中的,因为没有分离,造成了视... 查看详情

如何让android平台像ipc一样实现gb28181前端设备接入

技术背景好多开发者在做国标对接的时候,首先想到的是IPC摄像头,通过参数化配置,接入到国标平台,实现媒体数据的按需查看等操作。像执法记录仪等智能终端,跑在Android平台,对接GB28181平台的需求也非常大,网上相关demo... 查看详情

国标gb28181视频服务器easygbs的集群方案

国标GB28181的集群目标国标GB28181视频平台单独运行时,可对外提供国标全功能的设备接入、语音服务、视频服务、录像服务等功能,根据硬件和带宽的配置,单台能支持10k级别的设备接入与流媒体输出服务;而当... 查看详情