本文主要介紹GB28181 下級平台(設備)實現的基本內容,適合初入門同學,老司機可略過。
首先需要知道GB28181 上、下級關系,比如兩個平台A和B,如B需要從A獲取視頻流,則B是上級平台,A是下級平台。
另外需要清楚國標下級平台是廣義的,復雜的視頻平台可以是國標下級平台,支持國標NVR可以稱為國標平台,支持國
標的攝像機也可以稱為國標平台。
國標下級平台概率清楚后,接下來需要了解的是國標下級平台實現的功能。
1. 注冊
注冊功能是國標下級平台向國標上級平台發送Register消息(基於SIP)上下級平台互聯,第一個信令是下級平台發起的
具體的抓包內容是下圖所示:
圖1. 上級平台注冊消息截圖
如圖1所示,Request-Line 中 34020000002000000001為上級平台id,192.168.1.102為上級平台ip,5060 為上級平台信令
端口。From及To中的34020000001320000101是下級平台自身的id,192.168.1.106為下級平台ip,5060為下級平台信令端口。
下級平台上上級平台發送Register 消息如上級平台不回復 下級平台會一直發送Register消息,知道收到200 OK或者未鑒權消息
,如收到未鑒權消息,下級平台需要根據接收的參數(例如nonce)以及已知的上級平台密碼 通過MD5加密后生成response 等
信息再次發起注冊請求(具體的實現細節可參考GB28181-2016文檔)
基本代碼實現(基於PJSip):
首先初始化庫
bool Init(std::string contact, int logLevel,bool getCatalog,bool publicNet) { this->getCatalog = getCatalog; this->contact = contact; this->publicNet = publicNet; getPlatformIdFromContact(); 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.get(), &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 == PJ_SUCCESS; auto realm = StrToPjstr(GetLocalDomain()); return pjsip_auth_srv_init(pool, &authentication, realm.get(), lookup, 0) == PJ_SUCCESS ? true : false; }
發送Register消息
int startRegister() { pj_status_t status; pjsip_regc *regc; pj_thread_t *uithread; pj_thread_desc rtpdesc; if (!pj_thread_is_registered()) { pj_thread_register(nullptr, rtpdesc, &uithread); } //pj_thread_create(context.pool, "register", ®isterThreadHandler, nullptr, 0, 0, ®isterThread); status = pjsip_regc_create(context.endPoint, this, &clientCb, ®c); if (status != PJ_SUCCESS) { return status; } GBPlatform platform; platform.Init(registerInfo.platformId, registerInfo.platformAddr, registerInfo.platformPort); MediaContext mediaContxt(context.contact); GBPlatform localPlatform; localPlatform.Init(mediaContxt.GetDeviceId(), mediaContxt.GetplatformIP(), mediaContxt.GetPlatformPort()); auto pjContact = StrToPjstr(platform.GetContact()); auto pjSipCodecUrl = StrToPjstr(localPlatform.GetSipCodecUrl()); auto pjContextContact = StrToPjstr(context.contact); status = pjsip_regc_init(regc, pjContact.get(), pjSipCodecUrl.get(), pjSipCodecUrl.get(), 1, pjContextContact.get(), registerInfo.expires ? registerInfo.expires : 60); if (status != PJ_SUCCESS) { pjsip_regc_destroy(regc); return status; } pjsip_tx_data *tdata; pjsip_regc_register(regc, PJ_TRUE, &tdata); status = pjsip_regc_send(regc, tdata); }
2. 保活
下級注冊成功后(收到上級平台的200 OK消息)便開始發送保活消息(keepalive)保活間隔可以是3-5s 也可以是1分鍾,這
個時間點國標協議沒有規定,上下級協商(主要不是通過信令協商,是口頭協商)如上級平台一段時間沒有收到下級平台保活消
息 上級平台會認為下級平台已離線。 保活消息如下圖所示,其中Device字段中34020000001320000003是下級平台id。正常情況
下上級平台收到下級平台保活消息后發送200 OK消息。
圖2 保活消息截圖
基本代碼實現:
void keepAlive() { pjsip_tx_data *tdata; GBPlatform *platform = new GBPlatform(); platform->Init(registerInfo.platformId, registerInfo.platformAddr, registerInfo.platformPort); auto message = format( "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" "<Notify>\n" "<CmdType>Keepalive</CmdType>\n" "<SN>12</SN>\n" "<DeviceID>%s</DeviceID>\n" "<Status>OK</Status>\n" "</Notify>\n", context.platformId ); char msg[500] = { 0 }; message.copy(msg, message.size(), 0); const pjsip_method method = { PJSIP_OTHER_METHOD, {(char *)"MESSAGE", 7} }; auto text = StrToPjstr(msg); auto pjContact = StrToPjstr(context.contact); auto pjSipIpUrl = StrToPjstr(platform->GetSipIpUrl()); auto pjSipCodecUrl = StrToPjstr(platform->GetSipCodecUrl()); pj_status_t status = pjsip_endpt_create_request(context.endPoint, &method, pjSipIpUrl.get(), pjContact.get(), pjSipCodecUrl.get(), pjContact.get(), nullptr, -1, text.get(), &tdata); delete platform; tdata->msg->body->content_type.type = pj_str("Application"); tdata->msg->body->content_type.subtype = pj_str("MANSCDP+xml"); pjsip_endpt_send_request(context.endPoint, tdata, -1, this, on_keepalive_callback); }
3. 發送Catalog
下級平台第3個實現的功能是向上級平台推送自身的設備消息,當然前提是上級平台發送了請求設備消息。國標攝像機
只有一個設備就是它自身,國標NVR可能有多個(幾個通過連接了攝像機就幾個),國標平台則是該平台管理的攝像機。
響應Catalog消息內容如下圖所示:
圖3. 發送Catalog消息截圖
如圖3所示,Message Body 采用xml格式,消息體中第一個DeviceID 是下級平台自身的Id,DeviceList 中的Num值表示本
次Catalog消息中含有的設備數目。如果有100個設備,可以每次發一條,發100次,也可以每次發2條,發50次。這個國標
協議也沒有規定。Item節點中DeviceID是真正設備id好,Name是設備的名稱Status是設備的狀態(是否在線)。
基本代碼實現:
bool OnReceive(pjsip_rx_data* rdata) override { if (rdata->msg_info.cseq->method.id != PJSIP_OTHER_METHOD) return false; CGXmlParser xmlParser(context.GetMessageBody(rdata)); if (!xmlParser.GetXml()) { return true; } CGDynamicStruct dynamicStruct; dynamicStruct.Set(xmlParser.GetXml()); auto cmd = xmlParser.GetXml()->firstChild()->nodeName(); auto cmdType = dynamicStruct.Get<std::string>("CmdType"); if (cmdType != "Catalog") return false; std::string SN = ""; std::string PlatformAddr; int PlatformPort = 0; try { SN = dynamicStruct.Get<std::string>("SN"); } catch (Poco::Exception e) { std::cout << e.displayText() << std::endl; } bool registered = false; if (!SN.empty()) { auto formtoHdr = (pjsip_fromto_hdr*)pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_FROM, NULL); pjsip_sip_uri* frometoUri = (pjsip_sip_uri*)pjsip_uri_get_uri(formtoHdr->uri); std::string platFormId = context.PjstrTostr(frometoUri->user); PlatformAddr = rdata->pkt_info.src_name; PlatformPort = rdata->pkt_info.src_port; GBPlatform *platform = new GBPlatform; platform->Init(platFormId, PlatformAddr, PlatformPort); RegisterStatus status = CGSuperiorServerSessionGroup::GetInstance().IsServerRegistered(platform->GetPlatformUUID()); if (status == RegisterStatus::Registered) { registered = true; response(rdata, PJSIP_SC_OK, NoHead); std::vector<CGCatalogInfo> catalogs = CG28181MediaInfo::GetCatalogs(context.GetMediaAddrIp(), context.GetMediaAddrPort(), platform->GetPlatformUUID()); if (catalogs.empty()) { CGCatalogInfo catalog; context.ResponseCatalogInfo(platform, catalog, 0); } else { for (auto it = catalogs.begin(); it != catalogs.end(); it++) { (*it).SerialNumber = SN; context.ResponseCatalogInfo(platform, (*it), 1); } } } delete platform; } if (!registered) { response(rdata, PJSIP_SC_BAD_REQUEST, NoHead); } return true; }
4. 發送視頻
下級平台發送視頻跟發送Catalog消息一樣,都是被動的,只有在上級平台發送請求視頻命令Invite消息后將上級平台指定
的端口推送視頻(這里僅討論Udp的方式,實際GB28181-2016支持Tcp的方式)這里有3個問題需要清楚第1個是向哪個Ip
哪個端口推送,這個問題可以從上級平台發送Invite消息中得到答案。Invite消息攜帶了上級平台接收視頻的Ip及Port,如下圖4
所示。第2個需要搞清楚的問題是推什么格式的視頻流,答案是rtp +MpegPS流。視頻的編碼格式一般為H264,也有的是H265,
封裝的流程是相同的:先將裸流(H264 or H265)打包成MpegPS流 再加上12個字節的rtp包就可以了。第3個問題是 發送哪個
設備的流給上級平台,這個問題相對簡單點,上級平台發送過來的Invite消息中的SDP信息已經指定他想要的設備Id。
圖4. Invite消息截圖
基本代碼實現:
bool OnReceive(pjsip_rx_data* rdata) override { if (rdata->msg_info.cseq->method.id != PJSIP_INVITE_METHOD) return false; auto dlg = pjsip_rdata_get_dlg(rdata); pjsip_rdata_sdp_info *sdp = pjsip_rdata_get_sdp_info(rdata); if (!sdp || !sdp->sdp) return false; pj_uint32_t startTime = sdp->sdp->time.start; pj_uint32_t endTime = sdp->sdp->time.stop; pjsip_cid_hdr *callIdHdr = (pjsip_cid_hdr*)pjsip_msg_find_hdr(rdata->msg_info.msg, PJSIP_H_CALL_ID, NULL); std::string PlatformAddr = rdata->pkt_info.src_name; int PlatformPort = rdata->pkt_info.src_port; std::string platFormId = getPlatformIdFormHdr(rdata); string platformUUID = platFormId + "@" + PlatformAddr + ":" + std::to_string(PlatformPort); std::string ssrc = GetSSRCFromeSdp(sdp); bool isRegistered = CGSuperiorServerSessionGroup::GetInstance().IsServerRegistered(platformUUID); if (!isRegistered) { response(rdata, PJSIP_SC_BAD_REQUEST, NoHead); std::cout << platformUUID <<" not registered"<<std::endl; return true; } pjmedia_sdp_media * media = sdp->sdp->media[0]; CGTransportType transport = getTransportType(media->desc.transport); if (transport == CGTransportType::UnknownType) { response(rdata, PJSIP_SC_BAD_REQUEST, NoHead); return true; } //response(rdata, PJSIP_SC_TRYING, NoHead); pjsip_inv_session *inv = context.InviteAnswerTring(rdata); pjsip_sip_uri* requestLineUri = (pjsip_sip_uri*)pjsip_uri_get_uri(rdata->msg_info.msg->line.req.uri); string deviceId = context.PjstrTostr(requestLineUri->user) + "@" + PlatformAddr + ":" + std::to_string(PlatformPort);; CGCatalogInfo catalog = CG28181MediaInfo::GetCatalogByDeviceId(context.GetMediaAddrIp(), context.GetMediaAddrPort(), deviceId); if (catalog.TransportTypeSend != CGTransportType::BothUdpTcp) { if (catalog.TransportTypeSend != transport) { response(rdata, PJSIP_SC_BAD_REQUEST, NoHead); std::cout << catalog.DeviceID<< " TransportTypeSend incorrect" << std::endl; return true; } } if (catalog.Status != DeviceOnLine) { response(rdata, PJSIP_SC_BAD_REQUEST, NoHead); std:cout << catalog.DeviceID << " Device not online" << std::endl; } if (inv) { int port = CG28181MediaInfo::GetTransportBindingPort(context.GetMediaAddrIp(), context.GetMediaAddrPort(), catalog.ProtocolTypeRecv); //pjmedia_sdp_session *newSdp = createRealStreamResponseSDP(sdp->sdp,false,ssrc); MediaContext mediaContextLocal; mediaContextLocal.SetSSRC(ssrc); auto &context = CGSipContext::GetInstance(); mediaContextLocal.SetDeviceId(context.platformId); mediaContextLocal.SetTransportSrcPort(port); mediaContextLocal.SetTransportMediaServerAddr(context.GetAddr()); std::string sdpInfo= createRealStreamUdpSDP(mediaContextLocal); context.InviteAnswerOk(inv, sdpInfo); MediaContext mediaContext; mediaContext.SetDeviceId(context.PjstrTostr(requestLineUri->user)); mediaContext.SetRecvProtocolType(catalog.ProtocolTypeRecv); mediaContext.SetSendProtocolType(catalog.ProtocolTypeSend); if (catalog.ProtocolTypeRecv == CGProtocolType::PT_GB28181) { mediaContext.SetPlatformIP(catalog.PlatformAddr); mediaContext.SetPlatformPort(catalog.PlatformPort); } else { mediaContext.SetPlatformIP(PlatformAddr); mediaContext.SetPlatformPort(PlatformPort); } string receiveAddr = context.PjstrTostr(sdp->sdp->conn->addr); int receivePort = media->desc.port; mediaContext.SetRecvAddress(receiveAddr); mediaContext.SetRecvPort(receivePort); mediaContext.SetRequesterId(platformUUID); mediaContext.SetTransportSrcPort(port); mediaContext.SetSendRtpTransportType(static_cast<RtpTransportType>(transport)); mediaContext.SetRecvRtpTransportType(static_cast<RtpTransportType>(catalog.TransportTypeRecv)); mediaContext.SetStreamUrl(catalog.Url); mediaContext.SetSSRC(ssrc); std::shared_ptr<CGInviteSession> inviteSession = make_shared<CGInviteSession>(); inviteSession->Init(context.PjstrTostr(callIdHdr->id), inv, mediaContext); CGInviteSessionGroup::GetInstance().Add(inviteSession); mediaContext.SetTime(); } return true; }
整個下級平台與上級平台交互的流程如下圖所示:
圖5. 上、下級平台交互流程
作為國標下級平台還有很多其他的功能,比如雲台控制、報警、預置位設置等待,基本的流程跟發送Catalog
相似這里不再贅述。后面找時間會整理下級平台demo放到公司網站。最后提供一個Android端國標app(本質也是國標下級平台)供測試使用 ,該app實現了上述的
基本功能。下載地址http://www.chungen90.com/?news_32/
如需交流可加QQ群 1038388075