技术背景
在实现Android平台GB28181前端设备接入之前,我们几年前就有了非常成熟的RTMP推送、RTSP推送和轻量级RTSP服务等模块,特别是RTMP推送,行业内应用非常广泛,好多开发者可能会问,既然有了以上模块,干嘛还要实现GB28181的前端接入呢?
首先,我们了解下GB/T28181: 国标GB/T28181协议全称《安全防范视频监控联网系统信息传输、交换、控制技术要求》,是一个定义视频联网传输和设备控制标准的白皮书,由公安部科技信息化局提出,该标准规定了城市监控报警联网系统中信息传输、交换、控制的互联结构、通信协议结构,传输、交换、控制的基本要求和安全性要求,以及控制、传输流程和协议接口等技术要求。解决了视频间互联互通,数据共享,以及设备控制的问题,这个问题从顶层解决了视频信息各自为战的问题,打通了视频联网的信息孤岛。
技术特点
GB28181协议实现分两块,一块是信令部分,一块是流媒体数据传输。GB28181相对RTMP,支持TCP和UDP模式,信令流负责session交互,数据流负责数据传输,适合标准协议规范的平台级产品对接。
Android终端除支持常规的音视频数据接入外,还可以支持Subscribe订阅实时位置(MobilePosition)、实时目录查询等,支持标准28181服务对接。
此外,产品设计这块,媒体流支持最新GB28181-2016的UDP和TCP被动模式,参数配置,支持注册有效期、心跳间隔、心跳间隔次数、TCP/UDP信令设置,支持RTP Sender IP地址类型、RTP Socket本地端口、SSRC、RTP socket 发送Buffer大小、RTP时间戳时钟频率设置,支持注册成功、注册超时、INVIT、ACK、BYE状态回调。
功能设计
Android端GB28181前端设备模块,支持常规的视频采集、编码设定,功能设计如下:
- [本地预览]支持本地前后置摄像头预览;
- [视频格式]H.264/H.265(Android H.265硬编码);
- [音频格式]AAC;
- [音量调节]Android平台采集端支持实时音量调节;
- [H.264硬编码]支持H.264特定机型硬编码;
- [H.265硬编码]支持H.265特定机型硬编码;
- [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
- [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
- [横竖屏推流]Android平台支持支持横屏、竖屏推流;
- [多分辨率支持]支持摄像头或屏幕多种分辨率设置;
- [移动端推屏]Android平台支持后台service推送屏幕(推送屏幕需要5.0 版本);
- [模式支持]媒体流支持最新GB28181-2016的UDP和TCP被动模式;
- [参数设置]支持注册有效期、心跳间隔、心跳间隔次数、TCP/UDP信令设置;
- [参数设置]支持RTP Sender IP地址类型、RTP Socket本地端口、SSRC、RTP socket 发送Buffer大小、RTP时间戳时钟频率设置;
- [状态回调]支持注册成功、注册超时、INVIT、ACK、BYE状态回调;
- [水印]支持文字水印、png水印;
- [镜像]Android平台支持前置摄像头实时镜像功能;
- [前后摄像头实时切换]Android平台支持采集过程中,前后摄像头切换;
- [复杂网络处理]支持断网重连等各种网络环境自动适配;
- [动态码率]支持根据网络情况自动调整推流码率;
- [实时静音]支持推送过程中,实时静音/取消静音;
- [实时快照]支持推流过程中,实时快照;
- [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
- [外部编码前视频数据对接]支持YUV数据对接;
- [外部编码前音频数据对接]支持PCM对接;
- [外部编码后视频数据对接]支持外部H.264数据对接;
- [外部编码后音频数据对接]外部AAC数据对接;
- [扩展录像功能]支持和录像模块组合使用,录像相关功能;
- [服务器兼容]支持标准GB28181服务。
接口设计
接口设计,我们分两块:RTP Sender接口和GB28181接口,详细的例子,也可以关注github;
RTP Sender接口描述:
1. 创建RTP Sender实例,返回实例句柄:
代码语言:javascript复制 /*
* 创建RTP Sender实例
*
* @param reserve:保留参数传0
*
* @return RTP Sender 句柄,0表示失败
*/
public native long CreateRTPSender(int reserve);
2. 设置 RTP Sender传输协议,0:UDP, 1:TCP, 默认是UDP
代码语言:javascript复制 /**
*设置 RTP Sender传输协议
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP
*
* @return {0} if successful
*/
public native int SetRTPSenderTransportProtocol(long rtp_sender_handle, int transport_protocol);
3. 设置RTP Sender IP地址类型,如IPv4和IPv6,当前仅支持IPv4
代码语言:javascript复制 /**
*设置 RTP Sender IP地址类型
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param ip_address_type, 0:IPV4, 1:IPV6, 默认是IPV4, 当前仅支持IPV4
*
* @return {0} if successful
*/
public native int SetRTPSenderIPAddressType(long rtp_sender_handle, int ip_address_type);
4. 设置 RTP Sender RTP Socket本地端口,port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0
代码语言:javascript复制 /**
*设置 RTP Sender RTP Socket本地端口
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0
*
* @return {0} if successful
*/
public native int SetRTPSenderLocalPort(long rtp_sender_handle, int port);
5. 设置 RTP Sender SSRC
代码语言:javascript复制 /**
*设置 RTP Sender SSRC
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败
*
* @return {0} if successful
*/
public native int SetRTPSenderSSRC(long rtp_sender_handle, String ssrc);
6. 设置 RTP Sender RTP socket 发送Buffer大小
代码语言:javascript复制 /**
*设置 RTP Sender RTP socket 发送Buffer大小
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param buffer_size, 必须大于0, 默认是512*1024, 当前仅对UDP socket有效, 根据视频码率考虑设置合适的值
*
* @return {0} if successful
*/
public native int SetRTPSenderSocketSendBuffer(long rtp_sender_handle, int buffer_size);
7. 设置 RTP Sender RTP时间戳时钟频率
代码语言:javascript复制 /**
*设置 RTP Sender RTP时间戳时钟频率
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param clock_rate, 必须大于0, 对于GB28181 PS规定是90kHz, 也就是90000
*
* @return {0} if successful
*/
public native int SetRTPSenderClockRate(long rtp_sender_handle, int clock_rate);
8. 设置 RTP Sender 目的IP地址, 注意当前用在GB2818推送上,只设置一个地址,将来扩展如果用在其他地方,可能要设置多个目的地址,到时候接口可能会调整
代码语言:javascript复制 /**
*设置 RTP Sender 目的IP地址, 注意当前用在GB2818推送上,只设置一个地址,将来扩展如果用在其他地方,可能要设置多个目的地址,到时候接口可能会调整
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param address, IP地址
* @param port, 端口
*
* @return {0} if successful
*/
public native int SetRTPSenderDestination(long rtp_sender_handle, String address, int port);
9. 初始化RTP Sender, 初始化之前先调用上面的接口配置相关参数
代码语言:javascript复制 /**
*初始化RTP Sender, 初始化之前先调用上面的接口配置相关参数
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return {0} if successful
*/
public native int InitRTPSender(long rtp_sender_handle);
10. 获取RTP Sender RTP Socket本地端口
代码语言:javascript复制 /**
*获取RTP Sender RTP Socket本地端口
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return 失败返回0, 成功的话返回响应的端口, 请在InitRTPSender返回成功之后调用
*/
public native int GetRTPSenderLocalPort(long rtp_sender_handle);
11. UnInit RTP Sender
代码语言:javascript复制 /**
* UnInit RTP Sender
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return {0} if successful
*/
public native int UnInitRTPSender(long rtp_sender_handle);
12. 释放RTP Sender, 释放之后rtp_sender_handle就无效了,请不要再使用
代码语言:javascript复制 /**
* 释放RTP Sender, 释放之后rtp_sender_handle就无效了,请不要再使用
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return {0} if successful
*/
public native int DestoryRTPSender(long rtp_sender_handle);
GB28181相关接口
1. 设置GB28181 RTP Sender
代码语言:javascript复制 /**
* 设置GB28181 RTP Sender
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param rtp_payload_type, 对于GB28181 PS, 协议定义是96, 具体以SDP为准
*
* @return {0} if successful
*/
public native int SetGB28181RTPSender(long handle, long rtp_sender_handle, int rtp_payload_type);
2. 启动 GB28181 媒体流
代码语言:javascript复制 /**
* 启动 GB28181 媒体流
*
* @return {0} if successful
*/
public native int StartGB28181MediaStream(long handle);
3. 停止 GB28181 媒体流
代码语言:javascript复制 /**
* 停止 GB28181 媒体流
*
* @return {0} if successful
*/
public native int StopGB28181MediaStream(long handle);
接口调用实例
1. 相关参数初始化
代码语言:javascript复制 /*** GB28181 相关参数,可以修改相关参数后测试 ***/
GBSIPAgent gb28181_agent_ = null;
private int gb28181_sip_local_port_ = 12070;
private String gb28181_sip_server_id_ = "34020000002000000001";
private String gb28181_sip_server_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.0";
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;
/*** GB28181 相关参数,可以修改相关参数后测试 ***/
2. 启动或停止GB28181操作
代码语言:javascript复制 class ButtonGB28181AgentListener implements OnClickListener {
public void onClick(View v) {
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");
}
}
}
}
3. InitGB28181Agent实现
代码语言:javascript复制 private boolean initGB28181Agent()
{
if ( gb28181_agent_ != null )
return true;
String local_ip_addr = IPAddrUtils.getIpAddress(myContext);
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_server_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);
getLocation(this);
gb_device.setLongitude(mLongitude);
gb_device.setLatitude(mLatitude);
gb28181_agent_.addDevice(gb_device);
if (!gb28181_agent_.initialize()) {
gb28181_agent_ = null;
Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed.");
return false;
}
return true;
}
4. 注册成功后,返回注册时间
代码语言:javascript复制 @Override
public void ntsRegisterOK(String dateString) {
Log.i(TAG, "ntsRegisterOK Date: " (dateString!= null? dateString : ""));
}
5. 注册超时回调
代码语言:javascript复制 @Override
public void ntsRegisterTimeout() {
Log.e(TAG, "ntsRegisterTimeout");
}
6. 注册transport异常回调
代码语言:javascript复制 @Override
public void ntsRegisterTransportError(String errorInfo) {
Log.e(TAG, "ntsRegisterTransportError error:" (errorInfo != null?errorInfo :""));
}
7. 心跳异常回调
代码语言:javascript复制 @Override
public void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo) {
Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:" exceptionCount
", exception info:" (lastExceptionInfo!=null?lastExceptionInfo:""));
// 10毫秒后,停止信令, 然后重启
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "gb28281_heart_beart_timeout");
stopGB28181Stream();
destoryRTPSender();
if (gb28181_agent_ != null) {
Log.i(TAG, "gb28281_heart_beart_timeout sip stop");
gb28181_agent_.stop();
Log.i(TAG, "gb28281_heart_beart_timeout sip start");
gb28181_agent_.start();
}
}
},10);
}
8. Invite返回OK后,创建RTP Sender,根据返回的信息,设定相关参数
代码语言:javascript复制 @Override
public void ntsOnInvitePlay(String deviceId, InvitePlaySessionDescription session_des) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG,"ntsInviteReceived, device_id:" device_id_ ", is_tcp:" session_des_.isRTPOverTCP()
" rtp_port:" session_des_.getMediaPort() " ssrc:" session_des_.getSSRC()
" address_type:" session_des_.getAddressType() " address:" session_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_ = session_des_.getPSRtpMapAttribute().getPayloadType();
libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, session_des_.isRTPOverUDP()?0:1);
libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, session_des_.isIPv4()?0:1);
libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
libPublisher.SetRTPSenderSSRC(rtp_sender_handle, session_des_.getSSRC());
libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2M
libPublisher.SetRTPSenderClockRate(rtp_sender_handle, session_des_.getPSRtpMapAttribute().getClockRate());
libPublisher.SetRTPSenderDestination(rtp_sender_handle, session_des_.getAddress(), session_des_.getMediaPort());
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(myContext);
gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port);
gb28181_rtp_sender_handle_ = rtp_sender_handle;
}
private String device_id_;
private InvitePlaySessionDescription session_des_;
public Runnable set(String device_id, InvitePlaySessionDescription session_des) {
this.device_id_ = device_id;
this.session_des_ = session_des;
return this;
}
}.set(deviceId, session_des),0);
}
9. 取消播放
代码语言:javascript复制 @Override
public void ntsOnCancelPlay(String deviceId) {
// 这里取消Play会话
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnCancelPlay, deviceId=" device_id_);
destoryRTPSender();
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
10. Ack收到后,开始发送音视频数据
代码语言:javascript复制 @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 && !isPushingRtsp && !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 && !isPushingRtsp) {
if (publisherHandle != 0) {
libPublisher.SmartPublisherClose(publisherHandle);
publisherHandle = 0;
}
}
destoryRTPSender();
Log.e(TAG, "Failed to start GB28181 service..");
return;
}
if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {
if (pushType == 0 || pushType == 1) {
CheckInitAudioRecorder(); //enable pure video publisher..
}
}
isGB28181StreamRunning = true;
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
11. Invite异常处理
代码语言:javascript复制 @Override
public void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo) {
// 这里要释放掉响应的资源
Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" deviceId " statusCode=" statusCode
" errorInfo:" errorInfo);
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" device_id_);
destoryRTPSender();
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
12. 收到Bye停止发送数据
代码语言:javascript复制 @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);
}
13. Play Dialog终止处理
代码语言:javascript复制 @Override
public void ntsOnPlayDialogTerminated(String deviceId) {
/*
Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
收到这个请做相关清理处理
*/
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "ntsOnPlayDialogTerminated, 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);
}
总结
GB28181设计,除了支持TCP和UDP传输外,支持信令和数据传输分离,可实现其他终端针对前端设备的按需播放和处理,无需单独的信令支撑。缺点是外部支持GB28181的服务器不多,开源如SRS服务器针对GB28181的支持暂不够商用级,期待后续版本升级。