天天看點

國标GB28181對接視訊流國标GB28181對接視訊流

國标GB28181對接視訊流

     今天抽空寫下以GB28181的方式擷取錄影機視訊流以備後用,同時也希望能幫助到正着手開發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發送注冊消息,通過抓包可以看到具體的注冊消息内容:

國标GB28181對接視訊流國标GB28181對接視訊流

                            圖2 錄影機發送注冊消息圖

看下注冊消息的具體内容:

國标GB28181對接視訊流國标GB28181對接視訊流

                                       圖3 具體注冊消息圖

重要是Cantact資訊,包含了錄影機GB28181 SIP ID 以及IP位址和端口号,這樣與錄影機通信的SIP服務就知道往哪裡回應答消息。

     錄影機端基本介紹了完了(錄影機端相當于SIP Client),下面 介紹CG28181 服務端也即 SIP Server,這正是我們要實作的。

實作CG28181服務端可以借助于現有的開源庫 PJSIP,自己實作開發量還是很大的,具體的實作步驟如下:

一. 将PJSIP運作起來,畢竟人家是一個服務。隻有運作以後才能接收用戶端發來的消息。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

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消息後,如果服務端不應答,錄影機端會一直發送直到收到服務端應答為止。如果伺服器端重新運作,需要手動再次

開啟錄影機,如果等錄影機自己再次發送注冊消息可能是一個小時以後,我們當然不希望那麼久。

服務端應答注冊消息代碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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

;

}

  

1

OnReceive 是服務端接收注冊消息以後的響應方法,也就是說要将OnReceive作為入參傳給PJSIP,完成此項功能在初始化<br>PJSIP Moudle時。至于PJSIP moudle,這裡不多解釋,想要知道細節的話,可以檢視PJSIP文檔,文檔群裡有,代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

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方法實作了發送響應資料到用戶端(錄影機):

國标GB28181對接視訊流國标GB28181對接視訊流
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);
   }
           
國标GB28181對接視訊流國标GB28181對接視訊流

   實際也就是利用發PJSIP發送一些字元串給用戶端。具體發送了些什麼,可以抓個包看下。

國标GB28181對接視訊流國标GB28181對接視訊流

                                                                                                                                               圖4 SIP服務應答注冊消息

SIP 服務實際回了“200 OK” 給錄影機端。看下具體的消息内容:

國标GB28181對接視訊流國标GB28181對接視訊流

                                  圖5  “200 OK” 具體内容

      SIP服務端響應注冊指令後,發送Invite請求,請求catalog資訊,也就是裝置基本資訊,具體的方法上面已

給出,具體的内容是:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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

);<br>}

 SIP服務端 發送了請求catalog  消息,錄影機端收到消息發送其自身的catalog消息,SIP 服務端将在OnReceive中收到具體的catalog消息。取catalog消息的方法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

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資訊如何填寫:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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實作代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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

;

}

  代碼就不解釋了,要想知道到底發了什麼還是抓個包看看,無論你用什麼方法隻要抓包的資料是正确定說明發送成功了。

國标GB28181對接視訊流國标GB28181對接視訊流

                                                圖6 服務端發送invite視訊消息

錄影機端收到Invite請求後,會将視訊資料以rtp的方式推送到指定的端口,端口在invite消息指定。

這樣在指定的位址(ip + port)就可以拿到資料了。

最後提供一個測試demo,demo的作用是可以讓大家抓包,看看雙方都發了些什麼。

demo運作界面如下:

國标GB28181對接視訊流國标GB28181對接視訊流

                                                                             圖6 demo運作初始界面

1.運作demo後,首先配置好配置,如果不知道可以預設,但IP位址需要修改,端口不能被占用。

2.完成配置各配置項以後點選擷取視訊源按鈕 等待錄影機端注冊。

3.錄影機端開啟28181功能:具體的方法可以是:平台選擇方式下拉框先選擇一個非28181方式,點選儲存,再選擇28181方式并點選儲存。

4.錄影機端成功開啟28181功能以後,視訊源下拉框中會顯示錄影機的名稱資訊。

5.選中視訊源下拉框中出現的選項并點選播放按鈕,正常情況下會可以播放從錄影機端過來的視訊流。

   成功接入視訊源并播放的運作界面如下。

國标GB28181對接視訊流國标GB28181對接視訊流

                                                                                                圖7 demo成功運作以後的界面

Demo 可以在群裡下載下傳。

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

 視訊教程 播放位址: https://space.bilibili.com/241181578/

  視訊下載下傳位址:http://www.chungen90.com/?news_33/

 Demo下載下傳位址: http://www.chungen90.com/?news_34