技术回顾

在本文开头,我们先一起回顾下GB/T28181联网系统通信协议结构:

联网系统在进行视音频传输及控制时应建立两个传输通道:会话通道媒体流通道

  • 会话通道用于在设备之间建立会话并传输系统控制命令;
  • 媒体流通道用于传输视音频数据,经过压缩编码的视音频流采用流媒体协议 RTP/RTCP传输。

具体如下图:

我们先来看看会话初始协议

  • 安全注册、实时视音频点播、历史视音频的回放等应用的会话控制采用IETF RFC 3261规定的 Register、Invite等请求和响应方法实现;
  • 历史视音频回放控制采用SIP扩展协议IETF RFC 2976规定的INFO方法实现;
  • 前端设备控制、信息查询、报警事件通知和分发等应用的会话控制采用 SIP扩展协议IETF RFC 3428规定的Message方法实现;
  • SIP消息应支持基于UDP和 TCP的传输;
  • 互联的系统平台及设备不应向对方的SIP端口发送应用无关消息,避免应用无关消息占用系统平台及设备的SIP消息处理资源。

接下来是会话描述协议

联网系统有关设备之间会话建立过程的会话协商和媒体协商应采用IETF RFC 4566协议描述,主要内容包括会话描述、媒体信息描述、时间信息描述。

会话协商和媒体协商信息应采用SIP消息的消息体携带传输。

控制描述协议

联网系统有关前端设备控制、报警信息、设备目录信息等控制命令应采用监控报警联网系统控制描述协议(MANSCDP)描述。

联网系统控制命令应采用SIP消息Message的消息体携带传输。

媒体回放控制协议

历史视音频的回放控制命令应采用监控报警联网系统实时流协议(MANSRTSP),实现设备在端到端之间对视音频流的正常播放、快速、暂停、停止、随机拖动播放等远程控制。

历史媒体的回放控制命令采用SIP消息Info的消息体携带传输。

由于我们主要侧重于GB/T 28181音视频实时数据接入,这块未做实现,有相关需求的开发者,参考对应的spec章节即可。

媒体传输和媒体编解码协议

  • 媒体流在联网系统IP网络上传输时应支持 RTP传输,媒体流发送源端应支持控制媒体流发送峰值功能;
  • RTP的负载应采用如下两种格式之一:基于 PS封装的视音频数据或视音频基本流数据;
  • 媒体流的传输应采用IETF RFC 3550规定的 RTP协议,提供实时数据传输中的时间戳信息及各数据流的同步;应采用IETFRFC3550规定的RTCP协议,为按序传输数据包提供可靠保证,提供流量控制和拥塞控制。

技术实现

下面以Android平台GB/T 28181设备接入实现为例,大概介绍下相关的参数配置和设计细节:

先说参数配置,除了常规的链接sip server基础配置外,我们根据规范要求,添加了注册有效期、心跳间隔、心跳间隔失败次数、信令传输协议等配置:

/*** GB28181 相关参数,可以修改相关参数后测试 ***/
GBSIPAgent gb28181_agent_ = null;
private int gb28181_sip_local_port_ = 12070;
private String gb28181_sip_server_id_ = "34020000002000000001";
private String gb28181_sip_domain_ = "3402000000";
private String gb28181_sip_server_addr_ = "192.168.0.105";
private int gb28181_sip_server_port_ = 15060; private String gb28181_sip_user_agent_filed_ = "NT GB28181 User Agent V1.2";
private String gb28181_sip_username_ = "31011500991320000069";
private String gb28181_sip_password_ = "12345678"; private int gb28181_reg_expired_ = 3600; // 注册有效期时间最小3600秒
private int gb28181_heartbeat_interval_ = 20; // 心跳间隔GB28181默认是60, 目前调整到20秒
private int gb28181_heartbeat_count_ = 3; // 心跳间隔3次失败,表示和服务器断开了
private int gb28181_sip_trans_protocol_ = 0; // 0表示信令用UDP传输, 1表示信令用TCP传输 private long gb28181_rtp_sender_handle_ = 0;
private int gb28181_rtp_payload_type_ = 96; private long player_handle_ = 0;
private long rtp_receiver_handle_ = 0;
private AtomicLong last_received_audio_data_time_ = new AtomicLong(0); /*** GB28181 相关参数,可以修改相关参数后测试 ***/

为了测试方便,我们在界面加了个启动/停止GB28181的按钮:

/*
* Github: https://github.com/daniulive/SmarterStreaming
*/
class ButtonGB28181AgentListener implements OnClickListener {
public void onClick(View v) {
stopAudioPlayer();
destoryRTPReceiver(); gb_broadcast_source_id_ = null;
gb_broadcast_target_id_ = null;
btnGB28181AudioBroadcast.setText("GB28181语音广播");
btnGB28181AudioBroadcast.setEnabled(false); stopGB28181Stream();
destoryRTPSender(); if (null == gb28181_agent_ ) {
if( !initGB28181Agent() )
return;
} if (gb28181_agent_.isRunning()) {
gb28181_agent_.terminateAllPlays(true);// 目前测试下来,发送BYE之后,有些服务器会立即发送INVITE,是否发送BYE根据实际情况看
gb28181_agent_.stop();
btnGB28181Agent.setText("启动GB28181");
}
else {
if ( gb28181_agent_.start() ) {
btnGB28181Agent.setText("停止GB28181");
}
}
}
}

其中,initGB2818Agent()主要是基础参数设置,对应的实现如下:

private boolean initGB28181Agent() {
if ( gb28181_agent_ != null )
return true; getLocation(context_); String local_ip_addr = IPAddrUtils.getIpAddress(context_);
Log.i(TAG, "initGB28181Agent local ip addr: " + local_ip_addr); if ( local_ip_addr == null || local_ip_addr.isEmpty() ) {
Log.e(TAG, "initGB28181Agent local ip is empty");
return false;
} gb28181_agent_ = GBSIPAgentFactory.getInstance().create();
if ( gb28181_agent_ == null ) {
Log.e(TAG, "initGB28181Agent create agent failed");
return false;
} gb28181_agent_.addListener(this); // 必填信息
gb28181_agent_.setLocalAddressInfo(local_ip_addr, gb28181_sip_local_port_);
gb28181_agent_.setServerParameter(gb28181_sip_server_addr_, gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_domain_);
gb28181_agent_.setUserInfo(gb28181_sip_username_, gb28181_sip_password_); // 可选参数
gb28181_agent_.setUserAgent(gb28181_sip_user_agent_filed_);
gb28181_agent_.setTransportProtocol(gb28181_sip_trans_protocol_==0?"UDP":"TCP"); // GB28181配置
gb28181_agent_.config(gb28181_reg_expired_, gb28181_heartbeat_interval_, gb28181_heartbeat_count_); com.gb28181.ntsignalling.Device gb_device = new com.gb28181.ntsignalling.Device("34020000001380000001", "安卓测试设备", Build.MANUFACTURER, Build.MODEL,
"宇宙","火星1","火星", true); if (mLongitude != null && mLatitude != null) {
com.gb28181.ntsignalling.DevicePosition device_pos = new com.gb28181.ntsignalling.DevicePosition(); device_pos.setTime(mLocationTime);
device_pos.setLongitude(mLongitude);
device_pos.setLatitude(mLatitude);
gb_device.setPosition(device_pos); gb_device.setSupportMobilePosition(true); // 设置支持移动位置上报
} gb28181_agent_.addDevice(gb_device); if (!gb28181_agent_.initialize()) {
gb28181_agent_ = null;
Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed.");
return false;
} return true;
}

参数设定后,开始发送Regiter到平台端,Android设备端,针对Register的处理如下:

@Override
public void ntsRegisterOK(String dateString) {
Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : ""));
} @Override
public void ntsRegisterTimeout() {
Log.e(TAG, "ntsRegisterTimeout");
} @Override
public void ntsRegisterTransportError(String errorInfo) {
Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :""));
}

Catalog不再赘述,我们看看Inite处理:

@Override
public void ntsOnInvitePlay(String deviceId, PlaySessionDescription session_des) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
MediaSessionDescription video_des = session_des_.getVideoDescription();
SDPRtpMapAttribute ps_rtpmap_attr = video_des.getPSRtpMapAttribute(); Log.i(TAG,"ntsInviteReceived, device_id:" +device_id_+", is_tcp:" + video_des.isRTPOverTCP()
+ " rtp_port:" + video_des.getPort() + " ssrc:" + video_des.getSSRC()
+ " address_type:" + video_des.getAddressType() + " address:" + video_des.getAddress()); // 可以先给信令服务器发送临时振铃响应
//sip_stack_android.respondPlayInvite(180, device_id_); long rtp_sender_handle = libPublisher.CreateRTPSender(0);
if ( rtp_sender_handle == 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
Log.i(TAG, "ntsInviteReceived CreateRTPSender failed, response 488, device_id:" + device_id_);
return;
} gb28181_rtp_payload_type_ = ps_rtpmap_attr.getPayloadType(); libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, video_des.isRTPOverUDP()?0:1);
libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, video_des.isIPv4()?0:1);
libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
libPublisher.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC());
libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2M
libPublisher.SetRTPSenderClockRate(rtp_sender_handle, ps_rtpmap_attr.getClockRate());
libPublisher.SetRTPSenderDestination(rtp_sender_handle, video_des.getAddress(), video_des.getPort()); if ( libPublisher.InitRTPSender(rtp_sender_handle) != 0 ) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
} int local_port = libPublisher.GetRTPSenderLocalPort(rtp_sender_handle);
if (local_port == 0) {
gb28181_agent_.respondPlayInvite(488, device_id_);
libPublisher.DestoryRTPSender(rtp_sender_handle);
return;
} Log.i(TAG,"get local_port:" + local_port); String local_ip_addr = IPAddrUtils.getIpAddress(context_);
gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port); gb28181_rtp_sender_handle_ = rtp_sender_handle;
} private String device_id_;
private PlaySessionDescription session_des_; public Runnable set(String device_id, PlaySessionDescription session_des) {
this.device_id_ = device_id;
this.session_des_ = session_des;
return this;
}
}.set(deviceId, session_des),0);
}

Ack后,开始发送打包后的ps数据:

@Override
public void ntsOnAckPlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_); if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp) {
InitAndSetConfig();
} libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_);
int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);
if (startRet != 0) { if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp ) {
if (publisherHandle != 0) {
libPublisher.SmartPublisherClose(publisherHandle);
publisherHandle = 0;
}
} destoryRTPSender(); Log.e(TAG, "Failed to start GB28181 service..");
return;
} if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp) {
if (pushType == 0 || pushType == 1) {
CheckInitAudioRecorder(); //enable pure video publisher..
}
} startLayerPostThread(); isGB28181StreamRunning = true;
} private String device_id_; public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
} }.set(deviceId),0);
}

再看看位置订阅处理:

@Override
public void ntsOnDevicePositionRequest(String deviceId, int interval) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
getLocation(context_); Log.v(TAG, "ntsOnDevicePositionRequest, deviceId:" + this.device_id_ + ", Longitude:" + mLongitude
+ ", Latitude:" + mLatitude + ", Time:" + mLocationTime); if (mLongitude != null && mLatitude != null) {
com.gb28181.ntsignalling.DevicePosition device_pos = new com.gb28181.ntsignalling.DevicePosition(); device_pos.setTime(mLocationTime);
device_pos.setLongitude(mLongitude);
device_pos.setLatitude(mLatitude); if (gb28181_agent_ != null ) {
gb28181_agent_.updateDevicePosition(device_id_, device_pos);
}
}
} private String device_id_;
private int interval_; public Runnable set(String device_id, int interval) {
this.device_id_ = device_id;
this.interval_ = interval;
return this;
} }.set(deviceId, interval),0);
}

语音广播和语音对讲处理:

@Override
public void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnNotifyBroadcastCommand, fromUserName:"+ from_user_name_ + ", fromUserNameAtDomain:"+ from_user_name_at_domain_
+ ", SN:" + sn_ + ", sourceID:" + source_id_ + ", targetID:" + target_id_); if (gb28181_agent_ != null ) {
gb28181_agent_.respondBroadcastCommand(from_user_name_, from_user_name_at_domain_,sn_,source_id_, target_id_, true);
btnGB28181AudioBroadcast.setText("收到GB28181语音广播通知");
}
} private String from_user_name_;
private String from_user_name_at_domain_;
private String sn_;
private String source_id_;
private String target_id_; public Runnable set(String from_user_name, String from_user_name_at_domain, String sn, String source_id, String target_id) {
this.from_user_name_ = from_user_name;
this.from_user_name_at_domain_ = from_user_name_at_domain;
this.sn_ = sn;
this.source_id_ = source_id;
this.target_id_ = target_id;
return this;
} }.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID),0);
} @Override
public void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnAudioBroadcastPlay, fromFromUserName:" + command_from_user_name_
+ " FromUserNameAtDomain:" + command_from_user_name_at_domain_
+ " sourceID:" + source_id_ + ", targetID:" + target_id_); stopAudioPlayer();
destoryRTPReceiver(); if (gb28181_agent_ != null ) {
String local_ip_addr = IPAddrUtils.getIpAddress(context_); boolean is_tcp = true; // 考虑到跨网段, 默认用TCP传输rtp包
rtp_receiver_handle_ = lib_player_.CreateRTPReceiver(0);
if (rtp_receiver_handle_ != 0 ) {
lib_player_.SetRTPReceiverTransportProtocol(rtp_receiver_handle_, is_tcp?1:0);
lib_player_.SetRTPReceiverIPAddressType(rtp_receiver_handle_, 0); if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0) ) {
int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_);
boolean ret = gb28181_agent_.inviteAudioBroadcast(command_from_user_name_,command_from_user_name_at_domain_,
source_id_, target_id_, "IP4", local_ip_addr, local_port, is_tcp?"TCP/RTP/AVP":"RTP/AVP"); if (!ret ) {
destoryRTPReceiver();
btnGB28181AudioBroadcast.setText("GB28181语音广播");
}
else {
btnGB28181AudioBroadcast.setText("GB28181语音广播呼叫中");
}
} else {
destoryRTPReceiver();
btnGB28181AudioBroadcast.setText("GB28181语音广播");
}
}
}
} private String command_from_user_name_;
private String command_from_user_name_at_domain_;
private String source_id_;
private String target_id_; public Runnable set(String command_from_user_name, String command_from_user_name_at_domain, String source_id, String target_id) {
this.command_from_user_name_ = command_from_user_name;
this.command_from_user_name_at_domain_ = command_from_user_name_at_domain;
this.source_id_ = source_id;
this.target_id_ = target_id;
return this;
} }.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID),0);
}

Bye处理如下:

@Override
public void ntsOnByePlay(String deviceId) {
handler_.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnByePlay, stop GB28181 media stream, deviceId=" + device_id_); stopGB28181Stream();
destoryRTPSender();
} private String device_id_; public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
} }.set(deviceId),0);
}

考虑到篇幅有限,上面仅展示基础的处理,总的来说,GB/T 28181接入,资料相对全面,但是好多都是基于demo的验证,经不住推敲,如果要产品化,开发者还需要很长的路要走。

GB/T 28181联网系统通信协议结构和技术实现的更多相关文章

  1. Linux常用系统文件目录结构

    Linux常用系统文件目录结构 bin:全称binary,含义是二进制.该目录中存储的都是一些二进制文件,文件都是可以被运行的. dev:该目录主要存放的是外接设备,例如硬盘.其他的光盘等.在其中的外 ...

  2. 《嵌入式系统原理与接口技术》——嵌入式系统接口应用基础

    本文为我负责编写的电子工业出版社出版的<嵌入式系统原理与接口技术>一书第七章部分,这里整理的仍然是修改稿,供需要的同学参考,本书为普通高等教育"十二五"规划教材,电子信 ...

  3. 互联网DSP广告系统架构及关键技术解析

    互联网DSP广告系统架构及关键技术解析 宿逆 关注 1.9 2017.10.09 17:05* 字数 8206 阅读 10271评论 2喜欢 60 广告和网络游戏是互联网企业主要的盈利模式 广告是广告 ...

  4. DSP广告系统架构及关键技术解析(转)

    广告和网络游戏是互联网企业主要的盈利模式 广告是广告主通过媒体以尽可能低成本的方式与用户达成接触的商业行为.也就是说按照某种市场意图接触相应人群,影响其中潜在用户,使其选择广告主产品的几率增加,或对广 ...

  5. 进阶!基于CentOS7系统使用cobbler实现单台服务器批量自动化安装不同版本系统(week3_day5_part2)-技术流ken

    前言 在上一篇博文<cobbler批量安装系统使用详解-技术流ken>中已经详细讲解了cobbler的使用以及安装,本篇博文将会使用单台cobbler实现自动化批量安装不同版本的操作系统. ...

  6. xp2p系统的10点技术创新和经验总结

    最近在开发完善九天鸟的xp2p系统,解决了很多技术问题,特此总结下. 第一个项目开发,非常重要,它对建立开发规范.团队协作.开发效率,有很重大的意义. 1.分页前台AJAX异步分页,用咱们自己的fup ...

  7. 从原理到场景 系统讲解 PHP 缓存技术

    第1章课程介绍 此为PHP相关缓存技术的课堂,有哪些主流的缓存技术可以被使用? 第1章 课程介绍 1-1课程介绍1-2布置缓存的目的1-3合理使用缓存1-4哪些环节适合用缓存 第2章 文件类缓存 2- ...

  8. TOF与结构光技术分析

    TOF与结构光技术分析 一.概述 结构光(Structuredlight),通常采用特定波长的不可见的激光作为光源,它发射出来的光带有编码信息,投射在物体上,通过一定算法来计算返回的编码图案的畸变来得 ...

  9. Hadoop系统通信协议介绍(转)

    转载自 ---- http://weilaiyxj.iteye.com/blog/913166 本文约定: DN: DataNode TT: TaskTracker NN: NameNode SNN: ...

随机推荐

  1. 《HelloGitHub》第 75 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  2. CF989C A Mist of Florescence 题解

    因为 \(1 \leq a,b,c,d \leq 100\) 所以每一个颜色都有属于自己的联通块. 考虑 \(a = b=c=d=1\) 的情况. AAAAAAAAAAAAAAAAAAAAAAAAAA ...

  3. 基于InsightFace的高精度人脸识别,可直接对标虹软

    一.InsightFace简介 InsightFace 是一个 2D/3D 人脸分析项目.InsightFace 的代码是在 MIT 许可下发布的. 对于 acadmic 和商业用途没有限制. 包含注 ...

  4. VS code 远程连接服务器步骤

    ①安装VS code,并连接远程服务器(本地也需要有ssh).参考 ②免密钥登录设置,参考 具体步骤:WIN+R -->cmd, 输入ssh-keygen,然后一直Enter,最终生成公钥和私钥 ...

  5. 经典的损失函数:交叉熵和MSE

    经典的损失函数: ①交叉熵(分类问题):判断一个输出向量和期望向量有多接近.交叉熵刻画了两个概率分布之间的距离,他是分类问题中使用比较广泛的一种损失函数.概率分布刻画了不同事件发生的概率. 熵的定义: ...

  6. JAVA编程练习01作业

    1.已知y与x的关系:,要求:从键盘上输入一个x的值,输出其对应的y的值. 2. 输入一个圆半径(r),计算并输出圆的面积和周长. 3.输入一个三位正整数n,输出其个位.十位和百位上的数字. 4.根据 ...

  7. 全网最新的nacos 2.1.0集群多节点部署教程

    原文链接:全网最新的nacos 2.1.0集群多节点部署教程-语雀 基本信息 进度整理中 版本 2.1.0 版本发布日期 2022-04-29 git revision number b5845313 ...

  8. 互联网界的IT巨变:从DOS的编辑器,到如今的无代码开发

    众所周知,Borland Pascal.Turbo Pascal.Turbo C等这类开发工具,都习惯自带IDE. 因此,我产生了一个大胆的想法. DOS时代下的Turbo C 如果说Anders这类 ...

  9. GCD Compression

    题目大意: 有一个数组 a 其中有 2n 个数,把它压缩进数组 b,b 的大小是 n-1. 所谓的"压缩"指的是两种操作: 首先舍弃 a 当中的两个数(你可以任意选择). 然后每次 ...

  10. md 中超链接的解析问题:解析`this.$set()`,`$`前要加空格或转义符 `\`

    在用 Editor.md 写文档时,插入超链接,发现一个奇怪的现象: 要想正确显示超链接,必须在 $ 前加空格或转义符 \