技术背景
早在2015年,我们发布了RTMP直播推送模块,那时候音视频直播这块场景需求,还不像现在这么普遍,我们做这块的初衷,主要是为了实现移动单兵应急指挥系统的低延迟音视频数据传输。好多开发者可能会疑惑,走RTMP怎么可能低延迟?网上看到的RTMP推拉流延迟,总归要2-3秒起,如果是自己实现框架,RTMP推拉流逻辑自己实现的话,延迟确实可以控制在毫秒级,这个已无需赘述。
随着无纸化会议、智慧教室、智能化硬件产品的普及,RTMP的技术方案发展一度非常好,有些无人机或智能机器人,都可以自带推送RTMP流数据,配合大牛直播SDK的RTMP低延迟播放器模块,可以实现毫秒级的技术体验。
那为什么后面要做GB28181设备接入模块呢?我们推出的Android平台GB28181接入模块的目的,可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景。
GB28181规范,信令和媒体数据分离,可以订阅实时位置信息、云台控制、对焦等,数据传输走TCP或UDP,实现按需查看和语音广播、语音对讲,更成体系化,也更适合有交互的场景。
我们实现demo的时候,RTMP推送和GB28181都放到一起了,也就是说,可以同时使用RTMP推送和GB28181设备接入,也可以单独使用。
技术对比
RTMP推送
RTMP采用的是TCP传输,采用全自研框架,易于扩展,自适应算法让延迟更低、采集编码传输效率更高。延迟配合我们的播放器,轻松实现毫秒级延迟。
功能设计如下:
- 音频编码:AAC/SPEEX;
- 视频编码:H.264、H.265(RTMP扩展H.265);
- 推流协议:RTMP;
- [音视频]支持纯音频/纯视频/音视频推送;
- [摄像头]支持采集过程中,前后摄像头实时切换;
- 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
- 支持RTMP推送 live|record模式设置;
- 支持前置摄像头镜像设置;
- 支持软编码、特定机型硬编码;
- 支持横屏、竖屏推送;
- 支持Android屏幕采集推送;
- 支持自建标准RTMP服务器或CDN;
- 支持断网自动重连、网络状态回调;
- 支持动态水印(文字、图片);
- 支持降噪处理、自动增益控制;
- 支持实时快照;
- 支持实时静音和实时音量调节;
- 支持录像功能扩展(录制MP4文件);
- 支持Android 5.1及以上版本。
接口调用如下:
class ButtonStartPushListener implements View.OnClickListener {
public void onClick(View v) {
if (isPushingRtmp) {
stopPush();
btnRTMPPusher.setText("推送RTMP");
return;
}
Log.i(TAG, "onClick start push rtmp..");
if (libPublisher == null)
return;
if (!isRTSPPublisherRunning && !isGB28181StreamRunning && !isRecording) {
InitAndSetConfig();
}
if (libPublisher.SmartPublisherSetURL(publisherHandle, rtmp_pusher_url) != 0) {
Log.e(TAG, "Failed to set publish stream URL..");
}
int startRet = libPublisher.SmartPublisherStartPublisher(publisherHandle);
if (startRet != 0) {
Log.e(TAG, "Failed to start push stream..");
return;
}
if (!isRTSPPublisherRunning && !isGB28181StreamRunning && !isRecording) {
CheckInitAudioRecorder();
}
startLayerPostThread();
btnRTMPPusher.setText("停止推送 ");
isPushingRtmp = true;
}
}
停止RTMP推送
//停止rtmp推送
private void stopPush() {
if(!isPushingRtmp)
return;
isPushingRtmp = false;
if (!isRTSPPublisherRunning && !isGB28181StreamRunning && !isRecording)
stopLayerPostThread();
if (!isRTSPPublisherRunning && !isGB28181StreamRunning && !isRecording) {
if (audioRecord_ != null) {
Log.i(TAG, "stopPush, call audioRecord_.StopRecording..");
audioRecord_.Stop();
if (audioRecordCallback_ != null) {
audioRecord_.RemoveCallback(audioRecordCallback_);
audioRecordCallback_ = null;
}
audioRecord_ = null;
}
}
if (null == libPublisher || 0 == publisherHandle)
return;
libPublisher.SmartPublisherStopPublisher(publisherHandle);
if (!isRTSPPublisherRunning && !isGB28181StreamRunning && !isRecording) {
releasePublisherHandle();
}
}
GB/T28181
国标GB/T28181协议全称《安全防范视频监控联网系统信息传输、交换、控制技术要求》,是一个定义视频联网传输和设备控制标准的白皮书,由公安部科技信息化局提出,该标准规定了城市监控报警联网系统中信息传输、交换、控制的互联结构、通信协议结构,传输、交换、控制的基本要求和安全性要求,以及控制、传输流程和协议接口等技术要求。解决了视频间互联互通,数据共享,以及设备控制的问题,这个问题从顶层解决了视频信息各自为战的问题,打通了视频联网的信息孤岛。
支持对接数据类型:
- 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
- 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
- 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。
功能设计如下:
- 音频编码:G.711 A律、AAC;
- 视频编码:H.264、H.265;
- 支持纯视频、音视频PS打包传输;
- [摄像头]支持采集过程中,前后摄像头实时切换;
- 支持帧率、关键帧间隔(GOP)、码率(bit-rate)设置;
- 支持前置摄像头镜像设置;
- 支持软编码、特定机型硬编码;
- 支持横屏、竖屏推送;
- 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
- 支持信令通道网络传输协议TCP/UDP设置;
- 支持注册、注销,支持注册刷新及注册有效期设置;
- 支持设备目录查询应答;
- 支持心跳机制,支持心跳间隔、心跳检测次数设置;
- 支持移动设备位置(MobilePosition)订阅和通知;
- 支持云台控制和预置位查询;
- 支持设备目录查询应答;
- 支持TeleBoot远程启动回调;
- 支持语音广播;
- 支持语音对讲;
- 支持动态水印(文字、图片);
- 支持降噪处理、自动增益控制;
- 支持实时快照;
- 支持实时静音和实时音量调节;
- 支持录像功能扩展(录制MP4文件);
- 适用国家标准:GB/T 28181—2016;
- 支持Android 5.1以上版本。
信令处理
GBSIPAgentListener主要系GB28181注册、心跳、DevicePosition等,如注册成功、注册超时、注册网络传输层错误、心跳异常、设备位置请求处理:
public interface GBSIPAgentListener
{
/*注册成功
* @param dateString: 服务器日期,用来校准设备端时间,用户自行决定是否校准设备时间
*/
void ntsRegisterOK(String dateString);
/*
*注册超时
*/
void ntsRegisterTimeout();
/*
*注册网络传输层异常
*/
void ntsRegisterTransportError(String errorInfo);
/*
*心跳达到异常次数
*/
void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo);
/*
* 设备位置请求, 这个主要用在移动设备位置订阅上
* @param interval 请求间隔, 单位是毫秒
*/
void ntsOnDevicePositionRequest(String deviceId, int interval);
}
GBSIPAgentPlayListener主要系GB28181的Invite、Ack、Bye等处理:
public interface GBSIPAgentPlayListener {
/*
*收到s=Play的实时视音频点播
*/
void ntsOnInvitePlay(String deviceId, SessionDescription sessionDescription);
/*
*发送play invite response 异常
*/
void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo);
/*
* 收到CANCEL play INVITE请求
*/
void ntsOnCancelPlay(String deviceId);
/*
* 收到Ack
*/
void ntsOnAckPlay(String deviceId);
/*
* 收到Bye
*/
void ntsOnByePlay(String deviceId);
/*
* 不是在收到BYE Message情况下, 终止Play
*/
void ntsOnTerminatePlay(String deviceId);
/*
* Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
收到这个, 请做相关清理处理
*/
void ntsOnPlayDialogTerminated(String deviceId);
}
GBSIPAgentAudioBroadcastListener主要系GB28181语音广播处理相关,如有语音广播相关需求,可参照demo实例实现:
public interface GBSIPAgentAudioBroadcastListener {
/*
*收到语音广播通知
*/
void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID);
/*
*需要准备接受语音广播的SDP内容
*/
void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID);
/*
*音频广播, 发送Invite请求异常
*/
void ntsOnInviteAudioBroadcastException(String sourceID, String targetID, String errorInfo);
/*
*音频广播, 等待Invite响应超时
*/
void ntsOnInviteAudioBroadcastTimeout(String sourceID, String targetID);
/*
*音频广播, 收到Invite消息最终响应
*/
void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID, int statusCode, SessionDescription sessionDescription);
/*
* 音频广播, 收到BYE Message
*/
void ntsOnByeAudioBroadcast(String sourceID, String targetID);
/*
* 不是在收到BYE Message情况下, 终止音频广播
*/
void ntsOnTerminateAudioBroadcast(String sourceID, String targetID);
}
GBSIPAgentDeviceControlListener主要系GB28181设备控制相关,比如远程启动、云台控制:
public interface GBSIPAgentDeviceControlListener {
/*
* 收到远程启动控制命令
*/
void ntsOnDeviceControlTeleBootCommand(String deviceId, String teleBootValue);
/*
* 云台控制
*/
void ntsOnDeviceControlPTZCmd(String deviceId, String typeValue);
}
GBSIPAgentQueryCommandListener主要系GB28181查询命令,如预置位查询:
public interface GBSIPAgentQueryCommandListener {
/*
* 设备预置位查询
*/
void ntsOnDevicePresetQueryCommand(String fromUserName, String fromUserNameAtDomain, String sn, String deviceId);
}
GBSIPAgentTalkListener主要系GB28181语音对讲相关处理:
public interface GBSIPAgentTalkListener {
/*
*收到s=Talk 语音对讲
*/
void ntsOnInviteTalk(String deviceId, SessionDescription sessionDescription);
/*
*发送talk invite response 异常
*/
void ntsOnTalkInviteResponseException(String deviceId, int statusCode, String errorInfo);
/*
* 收到CANCEL Talk INVITE请求
*/
void ntsOnCancelTalk(String deviceId);
/*
* 收到Ack
*/
void ntsOnAckTalk(String deviceId);
/*
* 收到Bye
*/
void ntsOnByeTalk(String deviceId);
/*
* 不是在收到BYE Message情况下, 终止Talk
*/
void ntsOnTerminateTalk(String deviceId);
/*
* Talk会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
收到这个, 请做相关清理处理
*/
void ntsOnTalkDialogTerminated(String deviceId);
}
媒体数据处理
RTP数据发送
RTP Sender(SmartPublisherJniV2.java)相关接口设计:
/*
* SmartPublisherJniV2.java
* Author: https://daniusdk.com
*/
/*
* 创建RTP Sender实例
*
* @param reserve:保留参数传0
*
* @return RTP Sender 句柄,0表示失败
*/
public native long CreateRTPSender(int reserve);
/**
*设置 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);
/**
*设置 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);
/**
*设置 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);
/**
*设置 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);
/**
*设置 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);
/**
*设置 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);
/**
*设置 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);
/**
* 设置是否开启 RTP Receiver
* @param rtp_sender_handle, CreateRTPSender返回值
* @param is_enable, 0表示不收RTP包, 1表示收RTP包, SDK默认值为0.
* @return
*/
public native int EnableRTPSenderReceive(long rtp_sender_handle, int is_enable);
/**
*设置RTP Receiver SSRC
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败
*
* @return {0} if successful
*/
public native int SetRTPSenderReceiveSSRC(long rtp_sender_handle, String ssrc);
/**
*设置RTP Receiver Payload 相关信息
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @param payload_type, 请参考 RFC 3551
*
* @param encoding_name, 编码名, 请参考 RFC 3551, 如果payload_type不是动态的, 可能传null就好
*
* @param media_type, 媒体类型, 请参考 RFC 3551, 1 是视频, 2是音频
*
* @param clock_rate, 请参考 RFC 3551
*
* @return {0} if successful
*/
public native int SetRTPSenderReceivePayloadType(long rtp_sender_handle, int payload_type, String encoding_name, int media_type, int clock_rate);
/**
*设置RTP Receiver PS的pts和dts clock frequency
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @param ps_clock_frequency, 默认是90000, 一些特殊场景需要设置
*
* @return {0} if successful
*/
public native int SetRTPSenderReceivePSClockFrequency(long rtp_sender_handle, int ps_clock_frequency);
/**
*设置 RTP Receiver 音频采样率
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param sampling_rate, 音频采样率
*
* @return {0} if successful
*/
public native int SetRTPSenderReceiveAudioSamplingRate(long rtp_sender_handle, int sampling_rate);
/**
*设置 RTP Receiver 音频通道数
*
* @param rtp_sender_handle, CreateRTPSender返回值
* @param channels, 音频通道数
*
* @return {0} if successful
*/
public native int SetRTPSenderReceiveAudioChannels(long rtp_sender_handle, int channels);
/**
*初始化RTP Sender, 初始化之前先调用上面的接口配置相关参数
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return {0} if successful
*/
public native int InitRTPSender(long rtp_sender_handle);
/**
*获取RTP Sender RTP Socket本地端口
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return 失败返回0, 成功的话返回响应的端口, 请在InitRTPSender返回成功之后调用
*/
public native int GetRTPSenderLocalPort(long rtp_sender_handle);
/**
* UnInit RTP Sender
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return {0} if successful
*/
public native int UnInitRTPSender(long rtp_sender_handle);
/**
* 释放RTP Sender, 释放之后rtp_sender_handle就无效了,请不要再使用
*
* @param rtp_sender_handle, CreateRTPSender返回值
*
* @return {0} if successful
*/
public native int DestoryRTPSender(long rtp_sender_handle);
RTP数据接收
对应RTP Receiver(SmartPlayerJniV2.java)相关接口设计,如无语音广播或语音对讲相关技术需求,这部分可忽略:
/*
* SmartPlayerJniV2.java
* Author: https://daniusdk.com
*/
/*
* 创建RTP Receiver
*
* @param reserve:保留参数传0
*
* @return RTP Receiver 句柄,0表示失败
*/
public native long CreateRTPReceiver(int reserve);
/**
*设置 RTP Receiver传输协议
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param transport_protocol, 0:UDP, 1:TCP, 默认是UDP
*
* @return {0} if successful
*/
public native int SetRTPReceiverTransportProtocol(long rtp_receiver_handle, int transport_protocol);
/**
*设置 RTP Receiver IP地址类型
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param ip_address_type, 0:IPV4, 1:IPV6, 默认是IPV4
*
* @return {0} if successful
*/
public native int SetRTPReceiverIPAddressType(long rtp_receiver_handle, int ip_address_type);
/**
*设置 RTP Receiver RTP Socket本地端口
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param port, 必须是偶数,设置0的话SDK会自动分配, 默认值是0
*
* @return {0} if successful
*/
public native int SetRTPReceiverLocalPort(long rtp_receiver_handle, int port);
/**
*设置 RTP Receiver SSRC
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param ssrc, 如果设置的话,这个字符串要能转换成uint32类型, 否则设置失败
*
* @return {0} if successful
*/
public native int SetRTPReceiverSSRC(long rtp_receiver_handle, String ssrc);
/**
*创建 RTP Receiver 会话
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param reserve, 保留值,目前传0
*
* @return {0} if successful
*/
public native int CreateRTPReceiverSession(long rtp_receiver_handle, int reserve);
/**
*获取 RTP Receiver RTP Socket本地端口
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return 失败返回0, 成功的话返回响应的端口, 请在CreateRTPReceiverSession返回成功之后调用
*/
public native int GetRTPReceiverLocalPort(long rtp_receiver_handle);
/**
*设置 RTP Receiver Payload 相关信息
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @param payload_type, 请参考 RFC 3551
*
* @param encoding_name, 编码名, 请参考 RFC 3551, 如果payload_type不是动态的, 可能传null就好
*
* @param media_type, 媒体类型, 请参考 RFC 3551, 1 是视频, 2是音频
*
* @param clock_rate, 请参考 RFC 3551
*
* @return {0} if successful
*/
public native int SetRTPReceiverPayloadType(long rtp_receiver_handle, int payload_type, String encoding_name, int media_type, int clock_rate);
/**
*设置 RTP Receiver 音频采样率
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param sampling_rate, 音频采样率
*
* @return {0} if successful
*/
public native int SetRTPReceiverAudioSamplingRate(long rtp_receiver_handle, int sampling_rate);
/**
*设置 RTP Receiver 音频通道数
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param channels, 音频通道数
*
* @return {0} if successful
*/
public native int SetRTPReceiverAudioChannels(long rtp_receiver_handle, int channels);
/**
*设置 RTP Receiver 远端地址
*
* @param rtp_receiver_handle, CreateRTPReceiver
* @param address, IP地址
* @param port, 端口
*
* @return {0} if successful
*/
public native int SetRTPReceiverRemoteAddress(long rtp_receiver_handle, String address, int port);
/**
*初始化 RTP Receiver
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return {0} if successful
*/
public native int InitRTPReceiver(long rtp_receiver_handle);
/**
*UnInit RTP Receiver
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return {0} if successful
*/
public native int UnInitRTPReceiver(long rtp_receiver_handle);
/**
*Destory RTP Receiver Session
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return {0} if successful
*/
public native int DestoryRTPReceiverSession(long rtp_receiver_handle);
/**
*Destory RTP Receiver
*
* @param rtp_receiver_handle, CreateRTPReceiver
*
* @return {0} if successful
*/
public native int DestoryRTPReceiver(long rtp_receiver_handle);
PostAudioPacket(SmartPlayerJniV2.java),投递音频包给外部Live source,目前仅于语音对讲使用:
/*
* SmartPlayerJniV2.java
* Author: https://daniusdk.com
*/
/**
* 投递音频包给外部Live source, 注意ByteBuffer对象必须是DirectBuffer
*
* @param handle: return value from SmartPlayerOpen()
*
* @return {0} if successful
*/
public native int PostAudioPacket(long handle, int codec_id,
java.nio.ByteBuffer packet, int offset, int size, long pts, boolean is_pts_discontinuity,
java.nio.ByteBuffer extra_data, int extra_data_offset, int extra_data_size, int sample_rate, int channels);
总结
Android平台音视频数据流通,主要还看技术方案选择怎样的场景,如果是对接执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景等,目前选择GB28181的更多一些,如果主要是上云或者无纸化同屏、智慧教室等,还是RTMP推送多一些。具体可以根据场景选择适合自己的技术方案。
大家比较担心延迟问题,如果GB28181平台侧走RTMP或者webrtc的话,延迟也不大,和RTMP方案一样,整体都可以做到毫秒级。