背景
我们在做Android平台GB28181设备接入模块的时候,有开发者提到这样的诉求:他们的智能头盔、执法记录仪等设备,采集到的图像,是旋转了90、180甚至270°的,设备本身无法针对图像做翻转或者旋转操作,问我们这种情况下需要如何处理?
实际上,这块,我们前几年在做RTMP推送和轻量级RTSP服务模块的时候,老早处理了这类问题。
鉴于Android平台video数据采集分camera和camera2(Android 5.0+)接口,我们单独说明:
camera接口示例
//Github: https://github.com/daniulive/SmarterStreaming //author: 89030985@qq.com @Override public void onPreviewFrame(byte[] data, Camera camera) { frameCount++; if (frameCount % 3000 == 0) { Log.i("OnPre", "gc+"); System.gc(); Log.i("OnPre", "gc-"); } 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 (isRTSPPublisherRunning || isPushingRtmp || isRecording || isGB28181StreamRunning) { if (1 == video_opt_) { /* byte[] i420_data = new byte[videoWidth*videoHeight*3/2]; libPublisher.SmartPublisherNV21ToI420Rotate(publisherHandle, data, videoWidth, videoWidth,i420_data, videoHeight, videoHeight/2, videoHeight/2, videoWidth, videoHeight, 90); libPublisher.SmartPublisherOnCaptureVideoI420DataV2(publisherHandle, i420_data, videoHeight, videoWidth,videoHeight, videoHeight/2, videoHeight/2); */ libPublisher.SmartPublisherOnCaptureVideoData(publisherHandle, data, data.length, currentCameraType, currentOrigentation); } else if (3 == video_opt_) { int w = videoWidth, h = videoHeight; int y_stride = videoWidth, uv_stride = videoWidth; int y_offset = 0, uv_offset = videoWidth * videoHeight; int is_vertical_flip = 0, is_horizontal_flip = 0; int rotation_degree = 0; // 镜像只用在前置摄像头场景下 if (is_mirror && FRONT == currentCameraType) { // 竖屏, (垂直翻转->顺时旋转270度)等价于(顺时旋转旋转270度->水平翻转) if (PORTRAIT == currentOrigentation) is_vertical_flip = 1; else is_horizontal_flip = 1; } if (PORTRAIT == currentOrigentation) { if (BACK == currentCameraType) rotation_degree = 90; else rotation_degree = 270; } else if (LANDSCAPE_LEFT_HOME_KEY == currentOrigentation) { rotation_degree = 180; } if (640 == w && 480 == h && PORTRAIT == currentOrigentation) { // 480 * 640 竖屏情况下裁剪到 368 * 640, 均匀裁剪掉视频的上下两部分 h = 368; y_offset = 56 * y_stride; uv_offset += (56 >> 1) * uv_stride; } int scale_w = 0, scale_h = 0, scale_filter_mode = 0; // 缩放测试++ /* if (w >= 1280 && h >= 720) { scale_w = align((int)(w * 0.8 + 0.5), 2); scale_h = align((int)(h * 0.8 + 0.5), 2); } else { scale_w = align((int)(w * 1.5 + 0.5), 2); scale_h = align((int)(h * 1.5 + 0.5), 2); } if(scale_w >0 && scale_h >0) { scale_filter_mode = 3; Log.i(TAG, "onPreviewFrame w:" + w + ", h:" + h + " s_w:" + scale_w + ", s_h:" + scale_h); } */ // 缩放测试--- libPublisher.PostLayerImageNV21ByteArray(publisherHandle, 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); // i420接口测试++ /* byte[] i420_data = new byte[videoWidth*videoHeight*3/2]; int u_stride = videoWidth >> 1; int v_stride = u_stride; libPublisher.SmartPublisherNV21ToI420Rotate(publisherHandle, data, y_stride, uv_stride, i420_data, y_stride, u_stride, v_stride, videoWidth, videoHeight, 0); y_offset = 0; int u_offset = y_offset + videoWidth * videoHeight; int v_offset = u_offset + videoWidth*videoHeight/4; libPublisher.PostLayerImageI420ByteArray(publisherHandle, 0, 0, 0, i420_data, y_offset, y_stride, i420_data, u_offset, u_stride, i420_data, v_offset, v_stride, w, h, is_vertical_flip, is_horizontal_flip, scale_w, scale_h, scale_filter_mode, rotation_degree); */ // i420接口测试-- } } camera.addCallbackBuffer(data); } }
对应的接口设计如下:
/** * 投递层NV21图像 * * @param index: 层索引, 必须大于等于0 * * @param left: 层叠加的左上角坐标, 对于第0层的话传0 * * @param top: 层叠加的左上角坐标, 对于第0层的话传0 * * @param y_plane: y平面图像数据 * * @param y_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0 * * @param y_row_stride: stride information * * @param uv_plane: uv平面图像数据 * * @param uv_offset: 图像偏移, 这个主要目的是用来做clip的,一般传0 * * @param uv_row_stride: stride information * * @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 PostLayerImageNV21ByteArray(long handle, int index, int left, int top, byte[] y_plane, int y_offset, int y_row_stride, byte[] uv_plane, int uv_offset, int uv_row_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);
对应Camera2的接口示例
@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()) { // 裁剪测试++, 视频中心裁剪320*180一块区域 /*crop_rect.left = image.getWidth()/2 - 320/2; crop_rect.top = image.getHeight()/2 - 180/2; crop_rect.right = crop_rect.left + 320; crop_rect.bottom = crop_rect.top + 180; */ // 裁剪测试-- 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; int rotation_degree = cameraImageRotationDegree_; if (rotation_degree < 0) { Log.i(TAG, "onCameraImageData rotation_degree < 0, may need to set orientation_ to 0, 90, 180 or 270"); return; } 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, rotation_degree); } } }
对应的接口设计如下:
/** * 投递层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);
总结
无需赘述,看过以上两个接口后,是不是觉得,即使数据需要更客制化的处理,比如缩放、水平翻转、垂直翻转、旋转等,也都可以实现?
实际上,数据源这块,不止Android自带的采集设备,其他编码前数据类型(如YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565),均可实现更精细的处理。