欢迎光临
我们一直在努力

GB28181对接视频流

前言

        继上篇博文对GB28181流媒体服务方案的宏观解析之后,本篇将聚焦“对接视频流”这一具体实施环节,为开发者提供从设备接入到成功拉流播放的完整实操指南。我们将以LiveGBS国标流媒体平台与QuickGBLink测试工具为例,详述如何将GB28181设备的视频流转换为可在Web、App等终端播放的流。

一、对接视频流的本质:从信令到媒体的全链路

        “对接视频流”并非一个孤立的动作,而是一套标准化的信令交互与媒体传输过程。完整的视频流对接包含两大阶段:

(1). SIP信令通道建立:设备注册、心跳保活、目录查询、点播邀请(INVITE)。

(2). RTP媒体流传输与转换:设备将PS封装码流通过RTP推送到流媒体服务器,服务器解封装后重新分发。

        前者确保设备在线且可被控制,后者负责将原始监控流转化为互联网可消费的格式。只有这两个环节全部畅通,才算真正“对接成功”。

二、前置准备:搭建可用的国标环境

(1). 获取流媒体服务端

        按照上篇博文的架构,我们需要部署至少一个LiveSIPCMS(接入管理)和LiveSIPSMS(流媒体服务)。为便于测试,可直接使用官方提供的LiveGBS一体化包或Docker镜像,它将两个服务打包运行,默认SIP端口为5060(信令),HTTP API端口为10000。

(2). 准备一个GB28181模拟设备

三、GB28181设备对接视频流的流程

        搭建CG28181 服务端,也即 SIP Server,这正是我们要实现的。实现CG28181服务端可以借助于现有的开源库 PJSIP,具体的实现步骤如下:

3.1、启动GB28181服务端,接收客户端消息请求

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 消息了。

3.2、应答注册消息

        摄像机端发送来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,完成此项功能在初始化<br>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方法实现了发送响应数据到客户端(设备):

        接到PJSIP发送一些字符串给客户端:

void Response(pjsip_rc_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(poll, &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);

}

        从抓包来看,服务端给客户端(设备)回复了200OK

         SIP服务端响应注册命令后,发送Invite请求,请求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;

}

        以上服务端发给设备端请求视频的功能代码,在服务器发送视频请求以后,客户端开始推流到服务器,发送的信息可以通过抓包来看一下:

                                               图1-2 服务端发送invite视频消息

        设备端收到Invite请求后,会将视频数据以rtp的方式推送到指定的端口,端口在invite消息指定。这样在指定的地址(ip + port)就可以拿到数据了。

四、媒体流处理的关键技术细节

4.1、 RTP over TCP的打包格式

        TCP模式下,每个RTP包前会增加2字节的长度字段(网络字节序),代表RTP包长度。解析时需先读取2字节获得长度,再读取该长度的数据作为完整RTP包,这样可避免TCP粘包问题。

4.2、 PS流解封装

        从RTP payload中获取PS数据后,需按照MPEG-PS标准(ISO/IEC 13818-1)解析节目流,提取PES包。典型流程:

        搜索包起始码 00 00 01 BA(PS包头)。

        解析系统头部(可选)和节目流映射。

        根据stream_id提取视频PES(stream_id 0xE0)和音频PES(0xC0)。

        从PES包中剥离出H.264/H.265视频帧(需处理PTS/DTS)和G.711/AAC音频帧。

4.3、 转封装为FLV/HLS

        得到裸的AV数据后,流媒体服务会将其封装成FLV Tag(视频tag类型9,音频tag类型8),通过HTTP长连接推送FLV流;或切片生成TS文件,不断更新m3u8索引实现HLS。

五、总结

        GB28181对接视频流的过程,实质是让设备与流媒体服务之间完成“注册-邀请-推流-分发”的标准仪式。通过使用成熟的国标流媒体平台(如LiveGBS)和测试工具(如QuickGBLink),可以极大简化开发调试工作。作为开发者,重点需要掌握SIP信令的交互逻辑、RTP/PS的封装细节,以及TCP模式在公网中的必要性。在此基础上,灵活运用HTTP API进行二次集成,就能快速将传统安防监控视频流融入现代互联网应用,实现跨终端、无插件的流畅播放体验。

赞(0)
未经允许不得转载:171主机测评 » GB28181对接视频流
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址