Android平台GB28181设备接入模块摄像头采集方向不对怎么办?

简介: 我们在做Android平台GB28181设备接入模块的时候,有开发者提到这样的诉求:他们的智能头盔、执法记录仪等设备,采集到的图像,是旋转了90、180甚至270°的,设备本身无法针对图像做翻转或者旋转操作,问我们这种情况下需要如何处理?

背景

我们在做Android平台GB28181设备接入模块的时候,有开发者提到这样的诉求:他们的智能头盔、执法记录仪等设备,采集到的图像,是旋转了90、180甚至270°的,设备本身无法针对图像做翻转或者旋转操作,问我们这种情况下需要如何处理?


实际上,这块,我们前几年在做RTMP推送和轻量级RTSP服务模块的时候,老早处理了这类问题。


鉴于Android平台video数据采集分camera和camera2(Android 5.0+)接口,我们单独说明:

camera接口示例

16f9e19202e641c48839a9cca5992ba7.png

    //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的接口示例

c8669a68a61348548def791b05bc4ad7.jpg

    @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),均可实现更精细的处理。

相关文章
|
1月前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
160 4
|
2月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
122 1
|
3月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
20天前
|
IDE 开发工具 Android开发
移动应用开发之旅:探索Android和iOS平台
在这篇文章中,我们将深入探讨移动应用开发的两个主要平台——Android和iOS。我们将了解它们的操作系统、开发环境和工具,并通过代码示例展示如何在这两个平台上创建一个简单的“Hello World”应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧,帮助你更好地理解和掌握移动应用开发。
44 17
|
2月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
116 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
3月前
|
开发工具 Android开发 iOS开发
安卓与iOS开发环境对比:选择适合你的平台
【9月更文挑战第26天】在移动应用开发的广阔天地中,安卓和iOS是两大巨头。它们各自拥有独特的优势和挑战,影响着开发者的选择和决策。本文将深入探讨这两个平台的开发环境,帮助你理解它们的核心差异,并指导你根据个人或项目需求做出明智的选择。无论你是初学者还是资深开发者,了解这些平台的异同都至关重要。让我们一起探索,找到最适合你的那片开发天地。
|
3月前
|
Android开发 开发者
Android平台无纸化同屏如何实现实时录像功能
Android平台无纸化同屏,如果需要本地录像的话,实现难度不大,只要复用之前开发的录像模块的就可以,对我们来说,同屏采集这块,只是数据源不同而已,如果是自采集的其他数据,我们一样可以编码录像。
|
3月前
|
安全 API 开发工具
Android平台RTMP推送|轻量级RTSP服务如何实现麦克风|扬声器声音采集切换
Android平台扬声器播放声音的采集,在无纸化同屏等场景下,意义很大,早期低版本的Android设备,是没法直接采集扬声器audio的(从Android 10开始支持),所以,如果需要采集扬声器audio,需要先做系统版本判断,添加相应的权限。
|
3月前
|
编解码 开发工具 Android开发
Android平台实现屏幕录制(屏幕投影)|音频播放采集|麦克风采集并推送RTMP或轻量级RTSP服务
Android平台屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务的相关技术实现,做成高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器
|
20天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
41 19