GB/T28181规范
GB/T28181是中国国家标准,全称为《安全防范视频监控联网系统信息传输、交换、控制技术要求》,该标准规定了城市安全防范监控系统中视频监控联网系统的一般要求和架构,以及信息传输、交换、控制的技术要求。它主要应用于安防领域,为各种视频监控系统提供了一致的接口规范,使得不同厂商生产的视频监控设备可以相互兼容。规范规定了公共安全视频监控联网系统(以下简称“联网系统”)的互联结构,传输、交换、控制的基本要求和安全性要求,以及控制、传输流程和协议接口等技术要求。适用于公共安全视频监控联网系统的方案设计、系统检测、验收以及与之相关的设备研发生产。其他视频监控联网系统可参照执行。目前已更新至GB/T28181-2022版。
为什么要开发Android平台GB28181接入模块
实际上,Android平台GB28181接入模块,主要目标是可实现不具备国标音视频能力的 Android终端,通过平台注册接入到现有的GB/T28181—2016服务,可用于如执法记录仪、智能安全帽、智能监控、智慧零售、智慧教育、远程办公、明厨亮灶、智慧交通、智慧工地、雪亮工程、平安乡村、生产运输、车载终端等场景。
Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播和语音对讲,历史视音频文件查询和下载,支持对接数据类型如下:
- 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型),其中,Android平台前后摄像头数据,或者屏幕数据,或者Unity拿到的数据,均属编码前数据;
- 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
- 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。
功能设计
实际上,我们在做Android平台GB28181设备接入模块之前,已经有非常成熟的视音频采集(屏幕、摄像头、外部音视频数据)、软硬编码、录像、快照、实时动态水印等技术储备,所以,GB28181设备接入,主要考虑的是信令和媒体流传输这块,考虑到设备性能和实际场景,我们信令和媒体传输设计是分离的,Android端GB28181设备接入侧注册到国标平台后,如果国标平台不需要查看前端设备数据,我们仅维持心跳(KeepAlive),需要查看的时候,我们再开摄像头、麦克风编码打包投递数据给平台侧,尽可能的减少性能消耗,这块在执法记录仪、智能安全帽等场景下,非常实用。
- [视频格式]H.264/H.265(Android H.265硬编码);
- [音频格式]G.711 A律、AAC;
- [音量调节]Android平台采集端支持实时音量调节;
- [H.264硬编码]支持H.264特定机型硬编码;
- [H.265硬编码]支持H.265特定机型硬编码;
- [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
- [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
- 支持纯视频、音视频PS打包传输;
- 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
- 支持信令通道网络传输协议TCP/UDP设置;
- 支持注册、注销,支持注册刷新及注册有效期设置;
- 支持设备目录查询应答;
- 支持心跳机制,支持心跳间隔、心跳检测次数设置;
- 支持移动设备位置(MobilePosition)订阅和通知;
- 支持语音广播;
- 支持语音对讲;
- 支持历史视音频文件检索;
- 支持历史视音频文件下载;
- 支持云台控制和预置位查询;
- [实时水印]支持动态文字水印、png水印;
- [镜像]Android平台支持前置摄像头实时镜像功能;
- [实时静音]支持实时静音/取消静音;
- [实时快照]支持实时快照;
- [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
- [外部编码前视频数据对接]支持YUV数据对接;
- [外部编码前音频数据对接]支持PCM对接;
- [外部编码后视频数据对接]支持外部H.264数据对接;
- [外部编码后音频数据对接]外部AAC数据对接;
- [扩展录像功能]支持和录像模块组合使用,录像相关功能。
Android端如何实现后台视音频GB28181接入
后台采集摄像头和麦克风这块,不再赘述,基本做Andorid开发的,都能搞得定,需要注意的是,后台service推送,需要加入省电优化白名单,以免8.0及以上版本设备后台运行超过一分钟被自动停掉,6.0以上版本,需要动态获取权限:
if (Build.VERSION.SDK_INT>=26) { if(!isIgnoringBatteryOptimizations()) { gotoSettingIgnoringBatteryOptimizations(); } } //6.0及以上版本,动态获取Audio权限if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M) { RequestAudioPermission(); }
我们要做的就是选择分辨率、软硬编码等参数后,启动GB28181即可:
因为系后台服务,启动后,任务栏可以看到:
收到平台侧发来的Invite后,我们会调用try_preview_camera()来启动摄像头后台预览。
/** BackgroudService.java* Author: daniusdk.com*/publicvoidntsOnInvitePlay(StringdeviceId, SessionDescriptionsession_des) { handler_.postDelayed(newRunnable() { publicvoidrun() { // 先振铃响应下gb28181_agent_.respondPlayInvite(180, device_id_); if (!try_preview_camera()) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay try_preview_camera failed, response 488, device_id:"+device_id_); return; } MediaSessionDescriptionvideo_des=null; SDPRtpMapAttributeps_rtpmap_attr=null; // 28181 视频使用PS打包Vector<MediaSessionDescription>video_des_list=session_des_.getVideoPSDescriptions(); if (video_des_list!=null&&!video_des_list.isEmpty()) { for(MediaSessionDescriptionm : video_des_list) { if (m!=null&&m.isValidAddressType() &&m.isHasAddress() ) { video_des=m; ps_rtpmap_attr=video_des.getPSRtpMapAttribute(); break; } } } if (null==video_des) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay get video description is null, response 488, device_id:"+device_id_); return; } if (null==ps_rtpmap_attr ) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay get ps rtp map attribute is null, response 488, device_id:"+device_id_); return; } Log.i(TAG,"ntsOnInvitePlay, 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()); longrtp_sender_handle=lib_publisher_.CreateRTPSender(0); if ( rtp_sender_handle==0 ) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay 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(); lib_publisher_.SetRTPSenderTransportProtocol(rtp_sender_handle, video_des.isRTPOverUDP()?0:1); lib_publisher_.SetRTPSenderIPAddressType(rtp_sender_handle, video_des.isIPv4()?0:1); lib_publisher_.SetRTPSenderLocalPort(rtp_sender_handle, 0); lib_publisher_.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC()); lib_publisher_.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2Mlib_publisher_.SetRTPSenderClockRate(rtp_sender_handle, ps_rtpmap_attr.getClockRate()); lib_publisher_.SetRTPSenderDestination(rtp_sender_handle, video_des.getAddress(), video_des.getPort()); if ( lib_publisher_.InitRTPSender(rtp_sender_handle) !=0 ) { gb28181_agent_.respondPlayInvite(488, device_id_); lib_publisher_.DestoryRTPSender(rtp_sender_handle); return; } intlocal_port=lib_publisher_.GetRTPSenderLocalPort(rtp_sender_handle); if (local_port==0) { gb28181_agent_.respondPlayInvite(488, device_id_); lib_publisher_.DestoryRTPSender(rtp_sender_handle); return; } Log.i(TAG,"get local_port:"+local_port); Stringlocal_ip_addr=IPAddrUtils.getIpAddress(context_); MediaSessionDescriptionlocal_video_des=newMediaSessionDescription(video_des.getType()); local_video_des.addFormat(String.valueOf(ps_rtpmap_attr.getPayloadType())); local_video_des.addRtpMapAttribute(ps_rtpmap_attr); local_video_des.setAddressType(video_des.getAddressType()); local_video_des.setAddress(local_ip_addr); local_video_des.setPort(local_port); local_video_des.setTransportProtocol(video_des.getTransportProtocol()); local_video_des.setSSRC(video_des.getSSRC()); if (!gb28181_agent_.respondPlayInviteOK(device_id_,local_video_des) ) { lib_publisher_.DestoryRTPSender(rtp_sender_handle); Log.e(TAG, "ntsOnInvitePlay call respondPlayInviteOK failed."); return; } gb28181_rtp_sender_handle_=rtp_sender_handle; } privateStringdevice_id_; privateSessionDescriptionsession_des_; publicRunnableset(Stringdevice_id, SessionDescriptionsession_des) { this.device_id_=device_id; this.session_des_=session_des; returnthis; } }.set(deviceId, session_des),0); }
try_preview_camera()实现如下:
privatebooleantry_preview_camera() { if (camera_!=null) returntrue; SurfaceHoldersurface_holder=get_surface_holder(); if (null==surface_holder) { Log.e(TAG, "try_preview_camera surface_holder is null"); returnfalse; } camera_=open_camera(current_camera_type_); if (null==camera_) { Log.e(TAG, "try_preview_camera open_camera is null type:"+current_camera_type_); returnfalse; } if (!start_camera_preview(surface_holder)) { release_camera(); Log.i(TAG, "try_preview_camera start_camera_preview failed"); returnfalse; } returntrue; }
收到ack后,直接发送数据到国标平台侧即可
publicvoidntsOnAckPlay(StringdeviceId) { handler_.postDelayed(newRunnable() { publicvoidrun() { Log.i(TAG,"ntsOnACKPlay, device_id:"+device_id_); InitAndSetConfig(); lib_publisher_.SetGB28181RTPSender(publisher_handle_, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_); //libPublisher.SetGBTCPConnectTimeout(publisherHandle, 10*60*1000);//libPublisher.SetGBInitialTCPReconnectInterval(publisherHandle, 1000);//libPublisher.SetGBInitialTCPMaxReconnectAttempts(publisherHandle, 3);intstartRet=lib_publisher_.StartGB28181MediaStream(publisher_handle_); if (startRet!=0) { if (publisher_handle_!=0) { lib_publisher_.SmartPublisherClose(publisher_handle_); publisher_handle_=0; } destoryRTPSender(); Log.e(TAG, "Failed to start GB28181 service.."); return; } startAudioRecorder(); startLayerPostThread(); is_gb_stream_running_=true; } privateStringdevice_id_; publicRunnableset(Stringdevice_id) { this.device_id_=device_id; returnthis; } }.set(deviceId),0); }
关闭预览查看,处理bye信令:
publicvoidntsOnByePlay(StringdeviceId) { handler_.postDelayed(newRunnable() { publicvoidrun() { Log.i(TAG, "ntsOnByePlay, stop GB28181 media stream, deviceId="+device_id_); stopGB28181Stream(); destoryRTPSender(); } privateStringdevice_id_; publicRunnableset(Stringdevice_id) { this.device_id_=device_id; returnthis; } }.set(deviceId),0); }
其中stopGB28181Stream()需要关闭摄像头(对应release_camera())和麦克风(对应stopAudioRecorder()),确保只有国标平台测查看的时候,才开启摄像头,尽可能的减少性能损耗。
//停止GB28181 媒体流privatevoidstopGB28181Stream() { if (!is_gb_stream_running_) return; stopLayerPostThread(); stopAudioRecorder(); release_camera(); is_gb_stream_running_=false; lib_publisher_.StopGB28181MediaStream(publisher_handle_); if (publisher_handle_!=0) { lib_publisher_.SmartPublisherClose(publisher_handle_); publisher_handle_=0; } }
总结
以上是大概的流程,摄像头麦克风采集做到后台的话,可以在需要预览采集数据的时候才打开,不用的时候,直接关闭,只保留信令这块,打开视音频预览后,如果有语音广播过来,可以直接播放语音广播的数据,这样尽可能的减少设备的性能消耗,提高待机时间,特别是执法记录仪等户外设备,按需打开摄像头和麦克风,按需投递视音频数据到平台外侧,意义非常大。