天天看點

GB28181 下級平台(裝置)實作

        本文主要介紹GB28181 下級平台(裝置)實作的基本内容,适合初入門同學,老司機可略過。

 首先需要知道GB28181 上、下級關系,比如兩個平台A和B,如B需要從A擷取視訊流,則B是上級平台,A是下級平台。

另外需要清楚國标下級平台是廣義的,複雜的視訊平台可以是國标下級平台,支援國标NVR可以稱為國标平台,支援國

标的錄影機也可以稱為國标平台。

       國标下級平台機率清楚後,接下來需要了解的是國标下級平台實作的功能。

1. 注冊

       注冊功能是國标下級平台向國标上級平台發送Register消息(基于SIP)上下級平台互聯,第一個信令是下級平台發起的

具體的抓包内容是下圖所示:

GB28181 下級平台(裝置)實作

                                   圖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", &registerThreadHandler, nullptr, 0, 0, &registerThread);
   status = pjsip_regc_create(context.endPoint, this, &clientCb, &regc);
    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消息。

GB28181 下級平台(裝置)實作

              圖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消息内容如下圖所示:

GB28181 下級平台(裝置)實作

             圖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。

GB28181 下級平台(裝置)實作

             圖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;

}
      

    整個下級平台與上級平台互動的流程如下圖所示:

GB28181 下級平台(裝置)實作

                             圖5.  上、下級平台互動流程

         作為國标下級平台還有很多其他的功能,比如雲台控制、報警、預置位設定等待,基本的流程跟發送Catalog

相似這裡不再贅述。後面找時間會整理下級平台demo放到公司網站。最後提供一個Android端國标app(本質也是國标下級平台)供測試使用 ,該app實作了上述的

基本功能。下載下傳位址http://www.chungen90.com/?news_32/

如需交流可加QQ群 1038388075 

繼續閱讀