如何实现Android平台GB28181设备对接Camera2数据

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何实现Android平台GB28181设备对接Camera2数据相关的知识,希望对你有一定的参考价值。

技术背景

在写如何实现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不支持的特性, 比如:

  1. 更先进的API架构;
  2. 可以获取更多的帧(预览/拍照)信息以及手动控制每一帧的参数;
  3. 对Camera的控制更加完全(比如支持调整focus distance, 剪裁预览/拍照图片);
  4. 支持更多图片格式(yuv/raw)以及高速连拍等。

如何实现Android平台GB28181设备对接Camera2数据_gb28181推流

Camera2 API调用基础流程

  1. 通过context.getSystemService(Context.CAMERA_SERVICE) 获取CameraManager;
  2. 调用CameraManager .open()方法在回调中得到CameraDevice;
  3. 通过CameraDevice.createCaptureSession() 在回调中获取CameraCaptureSession;
  4. 构建CaptureRequest, 有三种模式可选 预览/拍照/录像.;
  5. 通过 CameraCaptureSession发送CaptureRequest, capture表示只发一次请求, setRepeatingRequest表示不断发送请求;
  6. 拍照数据可以在ImageReader.OnImageAvailableListener回调中获取, CaptureCallback中则可获取拍照实际的参数和Camera当前状态。

本次更新,系在Android平台camera2 RTMP推送的基础上,继续支持Android平台GB28181设备和语音广播接入,此外,添加了基于层结构设计的动态水印(动态水印的场景应用特别实在传统行业,重要性不言而喻。包含实时文字水印、图片水印),camera2的技术优越性不再赘述,无图无真相:

如何实现Android平台GB28181设备对接Camera2数据_android

新的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以上是关于如何实现Android平台GB28181设备对接Camera2数据的主要内容,如果未能解决你的问题,请参考以下文章

Android平台GB28181设备接入端对接编码前后音视频源类型浅析

Android平台GB28181设备接入端如何调节实时音量?

Android平台GB28181接入模块技术接入说明

Android平台GB28181接入端如何对接UVC摄像头?

Android平台GB28181接入端如何对接UVC摄像头?

移动视频类设备&平台国标GB28181输入输出,GB28181平台对接说明