技术背景
在写如何实现Android平台GB28181设备对接Camera2数据说明之前,我在前两年的blog就有针对camera2的RTMP直播推送模块做过技术分享:
在Google 推出Android 5.0的时候, Android Camera API 版本升级到了API2(android.hardware.camera2), 之前使用的API1(android.hardware.camera)就被标为 Deprecated 了。
Camera API2相较于API1有很大不同, 并且API2是为了配合HAL3进行使用的, API2有很多API1不支持的特性, 比如:
- 先进的API架构;
- 可以获取更多的帧(预览/拍照)信息以及手动控制每一帧的参数;
- 对Camera的控制更加完全(比如支持调整focus distance, 剪裁预览/拍照图片);
- 支持更多图片格式(yuv/raw)以及高速连拍等。
Camera2 API调用基础流程
- 通过context.getSystemService(Context.CAMERA_SERVICE) 获取CameraManager;
- 调用CameraManager .open()方法在回调中得到CameraDevice;
- 通过CameraDevice.createCaptureSession() 在回调中获取CameraCaptureSession;
- 构建CaptureRequest, 有三种模式可选 预览/拍照/录像.;
- 通过 CameraCaptureSession发送CaptureRequest, capture表示只发一次请求, setRepeatingRequest表示不断发送请求;
- 拍照数据可以在ImageReader.OnImageAvailableListener回调中获取, CaptureCallback中则可获取拍照实际的参数和Camera当前状态。
本次更新,系在Android平台camera2 RTMP推送的基础上,继续支持Android平台GB28181设备和语音广播接入,此外,添加了基于层结构设计的动态水印(动态水印的场景应用特别实在传统行业,重要性不言而喻。包含实时文字水印、图片水印),camera2的技术优越性不再赘述,无图无真相:
新的demo增加了动态水印设置、轻量级RTSP服务、实时录像、快照等。
技术实现
先说camera2的数据采集:
private class OnImageAvailableListenerImpl implements ImageReader.OnImageAvailableListener { @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); if ( image != null ) { if ( camera2Listener != null ) { camera2Listener.onCameraImageData(image); } image.close(); } } }
获取到的数据,投递到SmartPublisher Jni层:
@Override public void onCameraImageData(Image image) { Rect crop_rect = image.getCropRect(); if (isPushingRtmp || isRTSPPublisherRunning || isGB28181StreamRunning || isRecording) { if (libPublisher != null) { Image.Plane[] planes = image.getPlanes(); int w = image.getWidth(), h = image.getHeight(); int y_offset = 0, u_offset = 0, v_offset = 0; if (!crop_rect.isEmpty()) { w = crop_rect.width(); h = crop_rect.height(); y_offset += crop_rect.top * planes[0].getRowStride() + crop_rect.left * planes[0].getPixelStride(); u_offset += (crop_rect.top / 2) * planes[1].getRowStride() + (crop_rect.left / 2) * planes[1].getPixelStride(); v_offset += (crop_rect.top / 2) * planes[2].getRowStride() + (crop_rect.left / 2) * planes[2].getPixelStride(); ; // Log.i(TAG, "crop w:" + w + " h:" + h + " y_offset:"+ y_offset + " u_offset:" + u_offset + " v_offset:" + v_offset); } int scale_w = 0, scale_h = 0, scale_filter_mode = 0; scale_filter_mode = 3; libPublisher.PostLayerImageYUV420888ByteBuffer(publisherHandle, 0, 0, 0, planes[0].getBuffer(), y_offset, planes[0].getRowStride(), planes[1].getBuffer(), u_offset, planes[1].getRowStride(), planes[2].getBuffer(), v_offset, planes[2].getRowStride(), planes[1].getPixelStride(), w, h, 0, 0, scale_w, scale_h, scale_filter_mode, cameraImageRotationDegree_); } } }
PostLayerImageYUV420888ByteBuffer()接口设计如下:
/** * 投递层YUV420888图像, 专门为android.media.Image的android.graphics.ImageFormat.YUV_420_888格式提供的接口 * * @param index: 层索引, 必须大于等于0 * * @param left: 层叠加的左上角坐标, 对于第0层的话传0 * * @param top: 层叠加的左上角坐标, 对于第0层的话传0 * * @param y_plane: 对应android.media.Image.Plane[0].getBuffer() * * @param y_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0 * * @param y_row_stride: 对应android.media.Image.Plane[0].getRowStride() * * @param u_plane: android.media.Image.Plane[1].getBuffer() * * @param u_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0 * * @param u_row_stride: android.media.Image.Plane[1].getRowStride() * * @param v_plane: 对应android.media.Image.Plane[2].getBuffer() * * @param v_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0 * * @param v_row_stride: 对应android.media.Image.Plane[2].getRowStride() * * @param uv_pixel_stride: 对应android.media.Image.Plane[1].getPixelStride() * * @param width: width, 必须大于1, 且必须是偶数 * * @param height: height, 必须大于1, 且必须是偶数 * * @param is_vertical_flip: 是否垂直翻转, 0不翻转, 1翻转 * * @param is_horizontal_flip:是否水平翻转, 0不翻转, 1翻转 * * @param scale_width: 缩放宽,必须是偶数, 0或负数不缩放 * * @param scale_height: 缩放高, 必须是偶数, 0或负数不缩放 * * @param scale_filter_mode: 缩放质量, 传0使用默认速度,可选等级范围是:[1,3],值越大缩放质量越好, 但速度越慢 * * @param rotation_degree: 顺时针旋转, 必须是0, 90, 180, 270, 注意:旋转是在缩放, 垂直/水品反转之后再做, 请留意顺序 * * @return {0} if successful */ public native int PostLayerImageYUV420888ByteBuffer(long handle, int index, int left, int top, ByteBuffer y_plane, int y_offset, int y_row_stride, ByteBuffer u_plane, int u_offset, int u_row_stride, ByteBuffer v_plane, int v_offset, int v_row_stride, int uv_pixel_stride, int width, int height, int is_vertical_flip, int is_horizontal_flip, int scale_width, int scale_height, int scale_filter_mode, int rotation_degree);
动态水印-文字水印:
private int postText1Layer(int index, int left, int top, int video_w, int video_h) { if (video_w < 1 || video_h < 1) return 0; Bitmap text_bitmap = makeTextBitmap("文本水印一", getFontSize(video_w)+8, Color.argb(255, 200, 250, 0), false, 0,false); if (null == text_bitmap) return 0; ByteBuffer buffer = ByteBuffer.allocateDirect(text_bitmap.getByteCount()); text_bitmap.copyPixelsToBuffer(buffer); libPublisher.PostLayerImageRGBA8888ByteBuffer(handle_, index, left, top, buffer, 0, text_bitmap.getRowBytes(), text_bitmap.getWidth(), text_bitmap.getHeight(), 0, 0, 0, 0, 0,0); int ret = text_bitmap.getHeight(); text_bitmap.recycle(); return ret; }
动态水印-图片水印:
private Bitmap getAssetsBitmap() { Bitmap bitmap = null; try { InputStream s = getAssets().open("tca.png"); bitmap = BitmapFactory.decodeStream(s); s.close(); } catch (Exception e) { e.printStackTrace(); } return bitmap; } private int postPictureLayer(int index, int left, int top, int video_w, int video_h) { if (video_w < 1 || video_h < 1) return 0; Bitmap bitmap = getAssetsBitmap(); if (null == bitmap) { Log.e(TAG, "postPitcureLayer getAssetsBitmap is null"); return 0; } if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) { Log.e(TAG, "postPitcureLayer config is not ARGB_8888, config:" + Bitmap.Config.ARGB_8888); return 0; } ByteBuffer buffer = ByteBuffer.allocateDirect(bitmap.getByteCount()); bitmap.copyPixelsToBuffer(buffer); final int w = bitmap.getWidth(); final int h = bitmap.getHeight(); if ( w < 2 || h < 2 ) return 0; int scale_w = 0, scale_h = 0, scale_filter_mode = 0; final float r_w = video_w - left; // 有可能负数 final float r_h = video_h - top; // 有可能负数 if (w > r_w || h > r_h) { float s_w = w; float s_h = h; // 0.85的10次方是0.19687, 缩放到0.2倍差不多了 for ( int i = 0; i < 10; ++i) { s_w *= 0.85f; s_h *= 0.85f; if (s_w < r_w && s_h < r_h ) break; } if (s_w > r_w || s_h > r_h) return 0; // 如果小于16就算了,太小看也看不见 if (s_w < 16.0f || s_h < 16.0f) return 0; scale_w = align((int)(s_w + 0.5f), 2); scale_h = align( (int)(s_h + 0.5f), 2); scale_filter_mode = 3; } /* if ( scale_w > 0 && scale_h > 0) Log.i(TAG, "postTextLayer scale_w:" + scale_w + ", scale_h:" + scale_h + " w:" + w + ", h:" + h) ; */ libPublisher.PostLayerImageRGBA8888ByteBuffer(handle_, index, left, top, buffer, 0, bitmap.getRowBytes(), w, h, 0, 0, scale_w, scale_h, scale_filter_mode,0); int ret = scale_h > 0 ? scale_h : bitmap.getHeight(); bitmap.recycle(); return ret; }
动态水印控制:
public void startPost(long handle, int w, int h, boolean is_text, boolean is_pitcure) { this.is_exit_ = false; this.handle_ = handle; updateVideoSize(w, h); is_text_ = is_text; is_picture_ = is_pitcure; Log.i(TAG, "LayerPostThread.startPost w:" + w + ", h:" + h + ", is_text:" + is_text_ + ", is_pitcure:" + is_picture_); try { this.start(); } catch (Exception e) { e.printStackTrace(); } } public void enableText(boolean is_text) { is_text_ = is_text; clear_flag_ = true; if (handle_ != 0) { libPublisher.EnableLayer(handle_, timestamp_index_, is_text_?1:0); libPublisher.EnableLayer(handle_, text1_index_, is_text_?1:0); libPublisher.EnableLayer(handle_, text2_index_, is_text_?1:0); } } public void enablePicture(boolean is_picture) { is_picture_ = is_picture; clear_flag_ = true; if (handle_ != 0) { libPublisher.EnableLayer(handle_, picture_index_, is_picture_?1:0); } } public void stopPost() { this.is_exit_ = true; try { this.join(1000); } catch (Exception e) { e.printStackTrace(); } handle_ = 0; } private LayerPostThread layer_post_thread_ = null; private void startLayerPostThread() { if (null == layer_post_thread_) { layer_post_thread_ = new LayerPostThread(); int degree = cameraImageRotationDegree_; if (90 == degree || 270 == degree) layer_post_thread_.startPost(publisherHandle, video_height_, video_width_, isHasTextWatermark(), isHasPictureWatermark()); else layer_post_thread_.startPost(publisherHandle, video_width_, video_height_, isHasTextWatermark(), isHasPictureWatermark()); } } private void stopLayerPostThread() { if (layer_post_thread_ != null) { layer_post_thread_.stopPost(); layer_post_thread_ = null; } }
实时录像、快照之类不再赘述,gb28181的,其实和camera的部分一样:
private boolean initGB28181Agent() { if ( gb28181_agent_ != null ) return true; getLocation(context_); String local_ip_addr = IPAddrUtils.getIpAddress(context_); 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_.addPlayListener(this); gb28181_agent_.addAudioBroadcastListener(this); gb28181_agent_.addDeviceControlListener(this); // 必填信息 gb28181_agent_.setLocalAddress(local_ip_addr); gb28181_agent_.setServerParameter(gb28181_sip_server_addr_, gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_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.gb.ntsignalling.Device gb_device = new com.gb.ntsignalling.Device("34020000001380000001", "安卓测试设备", Build.MANUFACTURER, Build.MODEL, "宇宙","火星1","火星", true); if (mLongitude != null && mLatitude != null) { com.gb.ntsignalling.DevicePosition device_pos = new com.gb.ntsignalling.DevicePosition(); device_pos.setTime(mLocationTime); device_pos.setLongitude(mLongitude); device_pos.setLatitude(mLatitude); gb_device.setPosition(device_pos); gb_device.setSupportMobilePosition(true); // 设置支持移动位置上报 } gb28181_agent_.addDevice(gb_device); /* com.gb28181.ntsignalling.Device gb_device1 = new com.gb28181.ntsignalling.Device("34020000001380000002", "安卓测试设备2", Build.MANUFACTURER, Build.MODEL, "宇宙","火星1","火星", true); if (mLongitude != null && mLatitude != null) { com.gb28181.ntsignalling.DevicePosition device_pos = new com.gb28181.ntsignalling.DevicePosition(); device_pos.setTime(mLocationTime); device_pos.setLongitude(mLongitude); device_pos.setLatitude(mLatitude); gb_device1.setPosition(device_pos); gb_device1.setSupportMobilePosition(true); } gb28181_agent_.addDevice(gb_device1); */ if (!gb28181_agent_.createSipStack()) { gb28181_agent_ = null; Log.e(TAG, "initGB28181Agent gb28181_agent_.createSipStack failed."); return false; } boolean is_bind_local_port_ok = false; // 最多尝试5000个端口 int try_end_port = gb28181_sip_local_port_base_ + 5000; try_end_port = try_end_port > 65536 ?65536: try_end_port; for (int i = gb28181_sip_local_port_base_; i < try_end_port; ++i) { if (gb28181_agent_.bindLocalPort(i)) { is_bind_local_port_ok = true; break; } } if (!is_bind_local_port_ok) { gb28181_agent_.releaseSipStack(); gb28181_agent_ = null; Log.e(TAG, "initGB28181Agent gb28181_agent_.bindLocalPort failed."); return false; } if (!gb28181_agent_.initialize()) { gb28181_agent_.unBindLocalPort(); gb28181_agent_.releaseSipStack(); gb28181_agent_ = null; Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed."); return false; } return true; } @Override public void ntsRegisterOK(String dateString) { Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : "")); } @Override public void ntsRegisterTimeout() { Log.e(TAG, "ntsRegisterTimeout"); } @Override public void ntsRegisterTransportError(String errorInfo) { Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :"")); } @Override 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() { @Override public void run() { Log.i(TAG, "gb28281_heart_beart_timeout"); stopAudioPlayer(); destoryRTPReceiver(); if (gb_broadcast_source_id_ != null && gb_broadcast_target_id_ != null && gb28181_agent_ != null) gb28181_agent_.byeAudioBroadcast(gb_broadcast_source_id_, gb_broadcast_target_id_); gb_broadcast_source_id_ = null; gb_broadcast_target_id_ = null; btnGB28181AudioBroadcast.setText("GB28181语音广播"); btnGB28181AudioBroadcast.setEnabled(false); 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); } Log.i(TAG, "gb28281_heart_beart_timeout sip start"); gb28181_agent_.start(); } } },0); } @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); } @Override public void ntsOnAckPlay(String deviceId) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_); if (!isRTSPPublisherRunning && !isPushingRtmp && !isRecording) { InitAndSetConfig(); } libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_); int startRet = libPublisher.StartGB28181MediaStream(publisherHandle); if (startRet != 0) { if (!isRTSPPublisherRunning && !isPushingRtmp && !isRecording) { if (publisherHandle != 0) { libPublisher.SmartPublisherClose(publisherHandle); publisherHandle = 0; } } destoryRTPSender(); Log.e(TAG, "Failed to start GB28181 service.."); return; } if (!isRTSPPublisherRunning && !isPushingRtmp && !isRecording) { CheckInitAudioRecorder(); } startLayerPostThread(); isGB28181StreamRunning = true; } private String device_id_; public Runnable set(String device_id) { this.device_id_ = device_id; return this; } }.set(deviceId),0); } @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); } @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); } @Override public void ntsOnTerminatePlay(String deviceId) { handler_.postDelayed(new Runnable() { @Override 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); } @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); } @Override public void ntsOnDevicePositionRequest(String deviceId, int interval) { handler_.postDelayed(new Runnable() { @Override public void run() { getLocation(context_); Log.v(TAG, "ntsOnDevicePositionRequest, deviceId:" + this.device_id_ + ", Longitude:" + mLongitude + ", Latitude:" + mLatitude + ", Time:" + mLocationTime); if (mLongitude != null && mLatitude != null) { com.gb.ntsignalling.DevicePosition device_pos = new com.gb.ntsignalling.DevicePosition(); device_pos.setTime(mLocationTime); device_pos.setLongitude(mLongitude); device_pos.setLatitude(mLatitude); if (gb28181_agent_ != null ) { gb28181_agent_.updateDevicePosition(device_id_, device_pos); } } } private String device_id_; private int interval_; public Runnable set(String device_id, int interval) { this.device_id_ = device_id; this.interval_ = interval; return this; } }.set(deviceId, interval),0); } @Override public void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnNotifyBroadcastCommand, fromUserName:"+ from_user_name_ + ", fromUserNameAtDomain:"+ from_user_name_at_domain_ + ", SN:" + sn_ + ", sourceID:" + source_id_ + ", targetID:" + target_id_); if (gb28181_agent_ != null ) { gb28181_agent_.respondBroadcastCommand(from_user_name_, from_user_name_at_domain_,sn_,source_id_, target_id_, true); btnGB28181AudioBroadcast.setText("收到GB28181语音广播通知"); } } private String from_user_name_; private String from_user_name_at_domain_; private String sn_; private String source_id_; private String target_id_; public Runnable set(String from_user_name, String from_user_name_at_domain, String sn, String source_id, String target_id) { this.from_user_name_ = from_user_name; this.from_user_name_at_domain_ = from_user_name_at_domain; this.sn_ = sn; this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID),0); }
语音广播相关:
@Override public void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnAudioBroadcastPlay, fromFromUserName:" + command_from_user_name_ + " FromUserNameAtDomain:" + command_from_user_name_at_domain_ + " sourceID:" + source_id_ + ", targetID:" + target_id_); stopAudioPlayer(); destoryRTPReceiver(); if (gb28181_agent_ != null ) { String local_ip_addr = IPAddrUtils.getIpAddress(context_); boolean is_tcp = true; // 考虑到跨网段, 默认用TCP传输rtp包 rtp_receiver_handle_ = lib_player_.CreateRTPReceiver(0); if (rtp_receiver_handle_ != 0 ) { lib_player_.SetRTPReceiverTransportProtocol(rtp_receiver_handle_, is_tcp?1:0); lib_player_.SetRTPReceiverIPAddressType(rtp_receiver_handle_, 0); if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0) ) { int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_); boolean ret = gb28181_agent_.inviteAudioBroadcast(command_from_user_name_,command_from_user_name_at_domain_, source_id_, target_id_, "IP4", local_ip_addr, local_port, is_tcp?"TCP/RTP/AVP":"RTP/AVP"); if (!ret ) { destoryRTPReceiver(); btnGB28181AudioBroadcast.setText("GB28181语音广播"); } else { btnGB28181AudioBroadcast.setText("GB28181语音广播呼叫中"); } } else { destoryRTPReceiver(); btnGB28181AudioBroadcast.setText("GB28181语音广播"); } } } } private String command_from_user_name_; private String command_from_user_name_at_domain_; private String source_id_; private String target_id_; public Runnable set(String command_from_user_name, String command_from_user_name_at_domain, String source_id, String target_id) { this.command_from_user_name_ = command_from_user_name; this.command_from_user_name_at_domain_ = command_from_user_name_at_domain; this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID),0); } @Override public void ntsOnInviteAudioBroadcastException(String sourceID, String targetID, String errorInfo) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnInviteAudioBroadcastException, sourceID:" + source_id_ + ", targetID:" + target_id_); destoryRTPReceiver(); btnGB28181AudioBroadcast.setText("GB28181语音广播"); } private String source_id_; private String target_id_; public Runnable set(String source_id, String target_id) { this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(sourceID, targetID),0); } @Override public void ntsOnInviteAudioBroadcastTimeout(String sourceID, String targetID) { handler_.postDelayed(new Runnable() { @Override public void run() { Log.i(TAG, "ntsOnInviteAudioBroadcastTimeout, sourceID:" + source_id_ + ", targetID:" + target_id_); destoryRTPReceiver(); btnGB28181AudioBroadcast.setText("GB28181语音广播"); } private String source_id_; private String target_id_; public Runnable set(String source_id, String target_id) { this.source_id_ = source_id; this.target_id_ = target_id; return this; } }.set(sourceID, targetID),0); }
我们demo实现的横竖屏切换时,自动切换分辨率,有些国标平台,对分辨率切换支持的并不友好,这点可以根据实际情况调整,比如固定横竖屏。
总的来说,camera2对焦等各个方面,确实优于camera,替换升级也是大势所趋。