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月前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
110 1
|
2月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
1月前
|
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开发知识可参考相关书籍。
83 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
开发工具 Android开发 iOS开发
安卓与iOS开发环境对比:选择适合你的平台
【9月更文挑战第26天】在移动应用开发的广阔天地中,安卓和iOS是两大巨头。它们各自拥有独特的优势和挑战,影响着开发者的选择和决策。本文将深入探讨这两个平台的开发环境,帮助你理解它们的核心差异,并指导你根据个人或项目需求做出明智的选择。无论你是初学者还是资深开发者,了解这些平台的异同都至关重要。让我们一起探索,找到最适合你的那片开发天地。
|
2月前
|
Android开发 开发者
Android平台无纸化同屏如何实现实时录像功能
Android平台无纸化同屏,如果需要本地录像的话,实现难度不大,只要复用之前开发的录像模块的就可以,对我们来说,同屏采集这块,只是数据源不同而已,如果是自采集的其他数据,我们一样可以编码录像。
|
2月前
|
安全 API 开发工具
Android平台RTMP推送|轻量级RTSP服务如何实现麦克风|扬声器声音采集切换
Android平台扬声器播放声音的采集,在无纸化同屏等场景下,意义很大,早期低版本的Android设备,是没法直接采集扬声器audio的(从Android 10开始支持),所以,如果需要采集扬声器audio,需要先做系统版本判断,添加相应的权限。
|
2月前
|
编解码 开发工具 Android开发
Android平台实现屏幕录制(屏幕投影)|音频播放采集|麦克风采集并推送RTMP或轻量级RTSP服务
Android平台屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务的相关技术实现,做成高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器
|
6天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
8天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
10天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。