國標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<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/

 

 

 

 

                     

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM