大家都知道,无人机-巡检系统,有效解决了传统巡查工作空间和时间局限问题,降低人力工作成本,有效替代人工巡检工作模式。智能巡检系统通过人工智能技术和机械智能技术完美结合,在工业等场景下,应用非常广泛。本文旨在讲如何实现无人机(如大疆无人机)数据到GB28181平台(如海康、大华、宇视等国标平台)。
本文以Android平台接入大疆无人机为例,首先,无人机可以通过厂商提供的接口,回调编码后的H.264/H.265数据,需要注意的是,由于GB/T28181-2016,官方规范,仅对H.264做过描述,考虑到系统通用性和尽可能的规避转码带来的性能或使用体验问题,一般建议H.264编码。
无人机的数据会上来后,可以通过编码后的数据接口,投递到JNI层,把视音频数据封装成PS包,让把PS包以负载的方式封装成RTP包,完成媒体数据的上传即可。
本文以转发的模块为例说明,无图无真相:
具体实现:APP启动后,我们先点击启动GB28181按钮,完成到国标平台的注册,并通过心跳机制,保持和国标平台端的通信。
当国标平台端,需要查看无人机的实时画面时,可以发送Invite,请求无人机画面,Android平台GB28181接入模块,这时启动拉取无人机回调数据,并完成数据投递,和H.264到PS到RTP的打包上传即可。
代码语言:javascript复制/*
* MainActivity.java
* GitHub: https://github.com/daniulive/SmarterStreaming
*/
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");
}
}
}
}
//停止GB28181 媒体流
private void stopGB28181Stream() {
if(!isGB28181StreamRunning)
return;
if (libPublisher != null) {
libPublisher.StopGB28181MediaStream(publisherHandle);
}
if (!isRecording && !isRTSPPublisherRunning && !isPushing) {
if (publisherHandle != 0) {
if (libPublisher != null) {
libPublisher.SmartPublisherClose(publisherHandle);
publisherHandle = 0;
}
}
}
isGB28181StreamRunning = false;
}
开放的video数据投递接口如下:
代码语言:javascript复制/**
* 设置编码后视频数据(H.264)
*
* @param codec_id, H.264对应 1
*
* @param data 编码后的video数据
*
* @param size data length
*
* @param is_key_frame 是否I帧, if with key frame, please set 1, otherwise, set 0.
*
* @param timestamp video timestamp
*
* @param pts Presentation Time Stamp, 显示时间戳
*
* @return {0} if successful
*/
public native int SmartPublisherPostVideoEncodedData(long handle, int codec_id, ByteBuffer data, int size, int is_key_frame, long timestamp, long pts);
如果还有audio的话,audio数据接口如下:
代码语言:javascript复制/**
* 设置音频数据(AAC/PCMA/PCMU/SPEEX)
*
* @param codec_id:
*
* NT_MEDIA_CODEC_ID_AUDIO_BASE = 0x10000,
* NT_MEDIA_CODEC_ID_PCMA = NT_MEDIA_CODEC_ID_AUDIO_BASE,
* NT_MEDIA_CODEC_ID_PCMU,
* NT_MEDIA_CODEC_ID_AAC,
* NT_MEDIA_CODEC_ID_SPEEX,
* NT_MEDIA_CODEC_ID_SPEEX_NB,
* NT_MEDIA_CODEC_ID_SPEEX_WB,
* NT_MEDIA_CODEC_ID_SPEEX_UWB,
*
* @param data audio数据
*
* @param size data length
*
* @param is_key_frame 是否I帧, if with key frame, please set 1, otherwise, set 0, audio忽略
*
* @param timestamp video timestamp
*
* @param parameter_info 用于AAC special config信息填充
*
* @param parameter_info_size parameter info size
*
* @return {0} if successful
*/
public native int SmartPublisherPostAudioEncodedData(long handle, int codec_id, ByteBuffer data, int size, int is_key_frame, long timestamp,ByteBuffer parameter_info, int parameter_info_size);
其他信令交互流程前面提到很多次了,本文不再赘述,这里主要看看Invite和Ack的处理:
先看Invite处理:
代码语言:javascript复制@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();
gb28181_rtp_encoding_name_ = ps_rtpmap_attr.getEncodingName();
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);
}
@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);
}
Ack后调用StartGB28181MediaStream(),开始发送大疆无人机编码后的数据到国标平台端。
代码语言: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 && !isPushing) {
OpenPushHandle();
}
libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_);
int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);
if (startRet != 0) {
if (!isRecording && !isRTSPPublisherRunning && !isPushing) {
if (publisherHandle != 0) {
libPublisher.SmartPublisherClose(publisherHandle);
publisherHandle = 0;
}
}
destoryRTPSender();
Log.e(TAG, "Failed to start GB28181 service..");
return;
}
isGB28181StreamRunning = true;
}
private String device_id_;
public Runnable set(String device_id) {
this.device_id_ = device_id;
return this;
}
}.set(deviceId),0);
}
需要注意的是,可以在国标平台端发起Invite请求,到Ack完成后,才开始调用大疆无人机的接口回调H.264数据,有些型号的无人机,也可以回调编码前的yuv/nv12等格式数据,这种我们也可以处理,自己编码即可。
由于无人机的特殊性,携带经纬度信息,也可以通过GB28181位置订阅(MobilePosition)实现无人机实时位置的更新。