技术背景
我们在做Android平台GB28181设备对接模块的时候,遇到这样的技术需求,开发者希望能以后台服务的形式运行程序,国标平台侧没有视频回传请求的时候,仅保持信令链接,有发起视频回传请求或语音广播时,打开摄像头,并实时回传音视频数据或接收处理国标平台侧发过来的语音广播数据。
技术实现
实际上,在做GB28181设备接入模块前几年,我们已经有后台采集摄像头推送RTMP的模块,这次只是把国标相关的代码加进去即可,废话不多说,上代码。
界面很简单,进入后,可以选择视频分辨率、前后摄像头,软硬编码类型,然后启动GB28181即可。
public int onStartCommand(Intent intent, int flags, int startId) { onStartPusher(intent, startId); return super.onStartCommand(intent, flags, startId); } private void onStartPusher(Intent intent, int startId) { Log.i(TAG, "onStartPusher..."); video_width_ = intent.getExtras().getInt("CAMERAWIDTH"); video_height_ = intent.getExtras().getInt("CAMERAHEIGHT"); boolean isCameraFaceFront = intent.getExtras().getBoolean("SWITCHCAMERA"); Log.i(TAG, "videoWidth: " + video_width_ + "videoHeight: " + video_height_ + " isCameraFaceFront: " + isCameraFaceFront); mCameraId = isCameraFaceFront?CAMERA_ID_FRONT:CAMERA_ID_BACK; is_hardware_encoder = intent.getExtras().getBoolean("HWENCODER"); mWindowManager = (WindowManager) getSystemService(Service.WINDOW_SERVICE); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, intent, 0); bgSurfaceView = new SurfaceView(this); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( 1, 1, WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, PixelFormat.TRANSLUCENT); layoutParams.gravity = Gravity.LEFT | Gravity.TOP; mWindowManager.addView(bgSurfaceView, layoutParams); bgSurfaceView.getHolder().addCallback(this); if (null == gb28181_agent_ ) { if(!initGB28181Agent() ) return; } if (!gb28181_agent_.isRunning()) { if ( gb28181_agent_.start() ) { } } }
onStartPusher()里面,我们调用initGB28181Agent()完成国标设备侧到平台侧的register、catalog和keepalive交互,如果平台侧需要订阅位置,国标设备端可以按照平台侧的订阅要求,实时上报位置信息。
GB28181信令交互处理如下:
// BackgroudService.java // Author: daniusdk.com public void ntsRegisterOK(String dateString) { Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : "")); } public void ntsRegisterTimeout() { Log.e(TAG, "ntsRegisterTimeout"); } public void ntsRegisterTransportError(String errorInfo) { Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :"")); } public void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo) { Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:" + exceptionCount+ ", exception info:" + (lastExceptionInfo!=null?lastExceptionInfo:"")); // 停止信令, 然后重启 handler_.postDelayed(new Runnable() { public void run() { Log.i(TAG, "gb28281_heart_beart_timeout"); record_executor_.cancel_tasks(); stopGB28181Stream(); destoryRTPSender(); if (gb28181_agent_ != null) { gb28181_agent_.terminateAllPlays(true); Log.i(TAG, "gb28281_heart_beart_timeout sip stop"); gb28181_agent_.stop(); String local_ip_addr = IPAddrUtils.getIpAddress(context_); if (local_ip_addr != null && !local_ip_addr.isEmpty() ) { Log.i(TAG, "gb28281_heart_beart_timeout get local ip addr: " + local_ip_addr); gb28181_agent_.setLocalAddress(local_ip_addr); } record_executor_.cancel_tasks(); Log.i(TAG, "gb28281_heart_beart_timeout sip start"); gb28181_agent_.start(); } } },0); } public void ntsOnInvitePlay(String deviceId, SessionDescription session_des) { handler_.postDelayed(new Runnable() { public void run() { // 先振铃响应下 gb28181_agent_.respondPlayInvite(180, device_id_); MediaSessionDescription video_des = null; SDPRtpMapAttribute ps_rtpmap_attr = null; // 28181 视频使用PS打包 Vector<MediaSessionDescription> video_des_list = session_des_.getVideoPSDescriptions(); if (video_des_list != null && !video_des_list.isEmpty()) { for(MediaSessionDescription m : 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()); long rtp_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); // 设置到2M lib_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; } int local_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); String local_ip_addr = IPAddrUtils.getIpAddress(context_); MediaSessionDescription local_video_des = new MediaSessionDescription(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; } private String device_id_; private SessionDescription session_des_; public Runnable set(String device_id, SessionDescription session_des) { this.device_id_ = device_id; this.session_des_ = session_des; return this; } }.set(deviceId, session_des),0); } public void ntsOnCancelPlay(String deviceId) { // 这里取消Play会话 handler_.postDelayed(new Runnable() { 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); } public void ntsOnAckPlay(String deviceId) { handler_.postDelayed(new Runnable() { public void run() { Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_); InitAndSetConfig(); stream_publisher_.SetGB28181RTPSender(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); boolean start_ret = stream_publisher_.StartGB28181MediaStream(); if (!start_ret) { stream_publisher_.try_release(); destoryRTPSender(); Log.e(TAG, "Failed to start GB28181 service.."); return; } startAudioRecorder(); startLayerPostThread(); } private String device_id_; public Runnable set(String device_id) { this.device_id_ = device_id; return this; } }.set(deviceId),0); } public void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo) { // 这里要释放掉响应的资源 Log.i(TAG, "ntsOnPlayInviteResponseException, deviceId=" + deviceId + " statusCode=" +statusCode + " errorInfo:" + errorInfo); handler_.postDelayed(new Runnable() { 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); } public void ntsOnByePlay(String deviceId) { handler_.postDelayed(new Runnable() { 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); } public void ntsOnTerminatePlay(String deviceId) { handler_.postDelayed(new Runnable() { public void run() { Log.i(TAG, "ntsOnTerminatePlay, 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); } public void ntsOnPlayDialogTerminated(String deviceId) { /* Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发 收到这个请做相关清理处理 */ handler_.postDelayed(new Runnable() { 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); } public void ntsOnDevicePositionRequest(String deviceId, int interval) { }
投递数据的代码如下:
public void onPreviewFrame(byte[] data, Camera camera) { frameCount++; if (data == null) { Parameters params = camera.getParameters(); Size size = params.getPreviewSize(); int bufferSize = (((size.width | 0x1f) + 1) * size.height * ImageFormat.getBitsPerPixel(params.getPreviewFormat())) / 8; camera.addCallbackBuffer(new byte[bufferSize]); } else { if (stream_publisher_.is_gb_stream_publishing()) { if (3 == video_opt_) { int w = video_width_, h = video_height_; int y_stride = video_width_, uv_stride = video_width_; int y_offset = 0, uv_offset = video_width_ * video_height_; int is_vertical_flip = 0, is_horizontal_flip = 0; int rotation_degree = 0; // 镜像只用在前置摄像头场景下 if (CAMERA_ID_FRONT.equals(mCameraId)) { // 竖屏, (垂直翻转->顺时旋转270度)等价于(顺时旋转旋转270度->水平翻转) if (PORTRAIT == currentOrigentation) is_vertical_flip = 1; else is_horizontal_flip = 1; } if (PORTRAIT == currentOrigentation) { if (CAMERA_ID_BACK.equals(mCameraId)) rotation_degree = 90; else rotation_degree = 270; } else if (LANDSCAPE_LEFT_HOME_KEY == currentOrigentation) { rotation_degree = 180; } int scale_w = 0, scale_h = 0, scale_filter_mode = 0; stream_publisher_.PostLayerImageNV21ByteArray(0, 0, 0, data, y_offset, y_stride, data, uv_offset, uv_stride, w, h, is_vertical_flip, is_horizontal_flip, scale_w, scale_h, scale_filter_mode, rotation_degree); } } camera.addCallbackBuffer(data); } }
onDestroy()处理如下:
public void onDestroy() { // TODO Auto-generated method stub Log.i(TAG, "activity destory!"); record_executor_.cancel_tasks(); if (gb28181_agent_ != null ) { gb28181_agent_.terminateAllPlays(true); gb28181_agent_.stop(); } stopAudioRecorder(); stopGB28181Stream(); destoryRTPSender(); stream_publisher_.release(); stopLayerPostThread(); if (gb28181_agent_ != null ) { Log.i(TAG, " gb28181_agent_.unInitialize++"); gb28181_agent_.unInitialize(); gb28181_agent_.unBindLocalPort(); gb28181_agent_.releaseSipStack(); Log.i(TAG, " gb28181_agent_.unInitialize--"); gb28181_agent_ = null; } if (!record_executor_.shutdown(60, TimeUnit.SECONDS)) Log.w(TAG, "call record_executor_.shutdown failed"); super.onDestroy(); }
总结
以上是Android平台GB28181设备接入侧后台摄像头采集并按需回传到GB28181平台大概流程,目前,Android平台GB28181设备接入侧模块,覆盖以下功能:
- [视频格式]H.264/H.265(Android H.265硬编码);
- [音频格式]G.711 A律、AAC;
- [音量调节]Android平台采集端支持实时音量调节;
- [H.264硬编码]支持H.264特定机型硬编码;
- [H.265硬编码]支持H.265特定机型硬编码;
- [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
- [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
- 支持横屏、竖屏推流;
- Android平台支持后台service推送屏幕(推送屏幕需要5.0+版本);
- 支持纯视频、音视频PS打包传输;
- 支持RTP OVER UDP和RTP OVER TCP被动模式(TCP媒体流传输客户端);
- 支持信令通道网络传输协议TCP/UDP设置;
- 支持注册、注销,支持注册刷新及注册有效期设置;
- 支持设备目录查询应答;
- 支持心跳机制,支持心跳间隔、心跳检测次数设置;
- 支持移动设备位置(MobilePosition)订阅和通知;
- 适用国家标准:GB/T 28181—2016;
- 支持语音广播;
- 支持语音对讲;
- 支持图像抓拍;
- 支持历史视音频文件检索;
- 支持历史视音频文件下载;
- 支持历史视音频文件回放;
- 支持云台控制和预置位查询;
- [实时水印]支持动态文字水印、png水印;
- [镜像]Android平台支持前置摄像头实时镜像功能;
- [实时静音]支持实时静音/取消静音;
- [实时快照]支持实时快照;
- [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
- [外部编码前视频数据对接]支持YUV数据对接;
- [外部编码前音频数据对接]支持PCM对接;
- [外部编码后视频数据对接]支持外部H.264数据对接;
- [外部编码后音频数据对接]外部AAC数据对接;
- [扩展录像功能]支持和录像SDK组合使用,录像相关功能。
后台采集摄像头回传到GB28181平台侧,主要还是启动个service,其他和前台采集流程类似,感兴趣的开发者,也可跟我单独沟通探讨。