美文网首页
RTMP摄像头直播-CameraX数据采集处理

RTMP摄像头直播-CameraX数据采集处理

作者: 辉涛 | 来源:发表于2022-08-17 00:12 被阅读0次

    距离上一次写东西,如果不翻记录,是真想不起来是什么时候了,在记忆中,应该是三月的时候,或者更早了,因为那时候还没有换工作。写到这里还是忍不住去翻了一下以往的记录,发现后来也有写过两篇,已经没什么印象了。从3月到8月五个多月的时间,回忆起来仿佛就在昨天。这半年来,对于我来说变化实在太大,首先是离开了自己工作了三年多的公司,很多种原因,在离开的时候并没有一丝丝不舍,此刻突然想到过往的一切,好多人好多事浮现在眼前,就在昨天看到前同事发的状态,好多熟悉的面孔,那一刻又映入眼帘。是的,毕竟是自己曾经为之努力奋斗的地方,尤其在19年的时候,那时候真的用过心并努力过,也包括20年初期的时候。当然,随之而来的就是摆烂了。刚出来那段时间有太多的不适应,首先是自己租了房子,吃饭都需要掏钱买,以至于觉得钱花的好快,在之前每收到工资我就直接转到天天基金账户了,而现在得考虑房租和饭钱和其它日常开销,在加上每天上班都需要挤地铁,以至于前期那段时间整个人被整的一团糟。

    我以为在我换了工作,激情会再一次被点燃,记得前段时间和一个玩的很好的初中同学聊天,谈到现在的状态,我谈到如果再让我选择一次,我也坚决不会做这个行业了,我想爱惜好自己的眼睛,去当兵,然而再也没有第二次选择的机会了。有时候挺想做一条咸鱼,但又无法做到最咸的那一条,既然当下选择了,我觉得还是努力做好当下的事情,既然做了一天和尚,就应该努力把这个钟敲好。当然经过岁月的洗礼,也有值得庆幸的地方,自己的心态也逐渐走向成熟,不再浮躁,而是能静下心来,思考一些事情。

    关于之前开源的项目,https://github.com/zhuhuitao/printer,前前后后一共迭代了10个版本左右,当时在做这件事情的时候,没有想到会有这么多同学去看和使用,说实在的,很开心。也看到好几个同学提了issues,和留言,由于我目前没有从事相关工作,身边也没有打印机,所以已接近大半年没有维护了,在此想跟大家说一声抱歉,后续如果有机会我还是会把问题整理出来,统一解决,深感抱歉。

    前言

    在很久以前一直想转音视频方向,一直没有机会,毕竟想跨入这个方向,确实有一些难度。虽然现在项目中也有音视频相关的东西,无奈都不是我负责。人生嘛总会遇到容易的事情和困难的事情,如果总是逃避困难的事情,想想也没有什么意义,当然适当强迫一下自己,或许会收到不一样的结果。在学习的过程中,学会总结和输出真的太重要了,如果别人看到后有收获,当然是值得开心的了,更多的是自己在总结和输出的时候,往往有更多的收获和对某个知识的理解。

    android设备直播流程

    在使用Android设备进行摄像头直播时,其过程应该是这样的:


    流程

    就图像而言,首先需要获得摄像头采集的数据,然后得到这个byte[]进行编码,再进行后续的封包与发送。我们通过CameraX图像分析接口得到的数据为ImageProxy(Image的代理类)。那么怎么从ImageProxy/Image中获取我们需要的数据呢,这个数据格式是什么?

    ImageProxy/Image

    Image是android SDK提供的一个完整的图像缓冲区,图像数据为:YUV或者RGB等格式。在编码时,一般编码器接收的待编码数据格式为I420。而ImageProxy则是CameraX中定义的一个接口,Image的所有方法,也都能够从ImageProxy调用。可以通过image的getPlanes方法得到PlaneProxy数组,关于CameraX的详细资料我们都可以在android官方文档查看到。https://developer.android.google.cn/training/camerax?hl=zh_cn。当然CameraX给到我们的数据格式在官网中有提到,为YUV_420_888格式的图片。

    YUV420

    YUV模型是根据一个亮度(Y分量)和两个色度(UV分量)来定义颜色空间,常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等,其中比较常见的YUV420分为两种:YUV420P和YUV420SP。其中Y表示亮度,U和V表示色度。( 如果UV数据都为0,那么我们将得到一个黑白的图像。)RGB中每个像素点都有独立的R、G和B三个颜色分量值,YUV根据U和V采样数目的不同,分为如YUV444、YUV422和YUV420等,而YUV420表示的就是每个像素点有一个独立的亮度表示,即Y分量;而色度,即U和V分量则由每4个像素点共享一个。举例来说,对于4x4的图片,在YUV420下,有16个Y值,4个U值和4个V值。YUV420根据颜色数据的存储顺序不同,又分为了多种不同的格式,这些格式实际存储的信息还是完全一致的。举例来说,对于4x4的图片,在YUV420下,任何格式都有16个Y值,4个U值和4个V值,不同格式只是Y、U和V的排列顺序变化。I420YYYYYYYYYYYYYYYYUUUUVVVV ,NV21 则为 YYYYYYYYYYYYYYYYUVUVUVUV 。也就是说,YUV420
    是一类格式的集合,YUV420并不能完全确定颜色数据的存储顺序。
    更详细的介绍可以参考这篇文章https://zhuanlan.zhihu.com/p/495400095

    PlaneProxy/Plane

    Y、U和V三个分量的数据分别保存在三个 Plane 类中,即通过 getPlanes() 得到的数组。 Plane 实际是对ByteBuffer 的封装。Image保证了planes[0]一定是Y,planes[1]一定是U,planes[2]一定是V。且对于plane [0],Y分量数据一定是连
    续存储的,中间不会有U或V数据穿插,也就是说我们一定能够一次性得到所有Y分量的值。
    但是对于UV数据,可能存在以下两种情况:

    1. planes[1] = {UUUU...},planes[2] = {VVVV...};
    2. planes[1] = {UVUV...},planes[2] = {VUVU...}。
      所以在我么取数据时需要在根据Plane中的另一个信息来确定如何取对应的U或者V数据。
    //行内数据值间隔
    //1,表示无间隔取值,即为上面的第一种情况
    //2,表示需要间隔一个数值取值,即为上面第二种情况
    int pixelStride = plan.getPixelStride();
    

    根据这个属性,我们将确定数据如何存储,因此如果需要取出代表I420格式的byte[],则为:

    YUV420中,y数据的长度为:width*height,而u,v都为width/2*height/2.
    
    int pixelStride = plans[0].getPixelStride();
    planes[0].getBuffer()
    byte [] = new byte[image.getWidth()/2*image.getHeight()/2];
    int pixelStride = planes[1].getPixelStride();
    if(pixelStride == 1){
      planes[1].getBuffer();//u数据
    }else if(pixelStride == 2){
        ByteBuffer uBuffer = planes[1].getBuffer();
        for(int i = 0;i<uBuffer.remaining;i++){
        u[i] = uBuffer.get();//丢弃一个数据,这里其实是v数据
        uBuffer.get():
    }
    }
    
    //v数据与u数据同样获取
    

    但是如果使用上面的代码去获取I420数据,可能会惊奇的发现,并不是在所有设置的Width与Height(分辨率)下都能够正常运行。我们忽略了什么,为什么会出现问题呢?在Plane中我们已经使用了 getBuffer 与 getPixelStride 两个方法,但是还有一个 getRowStride 是干嘛的呢?

    RowStride

    RowStride表示行步长,Y数据对应的行步长可能为:

    1. 等于Width;
    2. 大于Width;
      以4x4的I420为例,其数据可以看为:
          Y   Y   Y   Y
          Y   Y   Y   Y
          Y   Y   Y   Y
          Y   Y   Y   Y
          U   U
          U   U
          V   V
          V   V
    

    如果RowStride等于Width,那么我们直接通过 planes[0].getBuffer() 获得Y数据没有问题。
    但是如果RowStride大于Width,比如对于4x4的I420,如果每行需要以8字节对齐,那么可能得到的RowStride不
    等于4(Width),而是得到8。那么此时会在每行数据末尾补充占位的无效数据:

            Y   Y   Y   Y    0    0    0    0
            Y   Y   Y   Y    0    0    0    0
            Y   Y   Y   Y    0    0    0    0
            Y    Y  Y   Y    最后一行没有占位
    

    对于这种情况,我们获取Y数据,则为:

     /**
             * Y数据
             */
            //y数据的这个值只能是:1
            int pixelStride = planes[0].getPixelStride();
            ByteBuffer yBuffer = planes[0].getBuffer();
            int rowStride = planes[0].getRowStride();
    
            //1、rowStride 等于Width ,那么就是一个空数组
            //2、rowStride 大于Width ,那么就是每行多出来的数据大小个byte
            byte[] skipRow = new byte[rowStride - image.getWidth()];
            byte[] row = new byte[image.getWidth()];
            for (int i = 0; i < image.getHeight(); i++) {
                yBuffer.get(row);
                i420.put(row);
                // 不是最后一行才有无效占位数据,最后一行因为后面跟着U 数据,没有无效占位数据,不需要丢弃
                if (i < image.getHeight() - 1) {
                    yBuffer.get(skipRow);
                }
            }
    
    

    而对于U与V数据,对应的行步长可能为:

    1. 等于Width;
    2. 大于Width;
    3. 等于Width/2;
    4. 大于Width/2

    等于width

    这表示,我们获得planes[1]中不仅包含U数据,还会包含V的数据,此时pixelStride==2

        U    V    U    V
        U    V    U    V
    

    那么V数据:planes[2],则为:

        V    U    V    U
        V    U    V    U
    

    这种情况下,我们上面的代码也已经处理了。

    大于width

    与Y数据一样,可能由于字节对齐,出现RowStride大于Width的情况,与等于Width一样,planes[1]中不仅包含U数据,还会包含V的数据,此pixelStride==2。

          U    V    U    V    0    0    0    0
          U    V    U    V    最后一行没有占位
    

    planes[2],则为:

        V    U    V    U    0    0    0    0
        V    U    V    U    最后一行没有占位
    

    等于width/2

    当获取的U数据对应的RowStride等于Width/2,表示我们得到的planes[1]只包含U数据。此时pixelStride==1。那么planes[1]+planes[2]为:

        U    U
        U    U
        V    V
        V    V
    

    这种情况,所有的U数据是连在一起的,即 planes[1].getBuffer 可以直接获得完整的U数据。

    大于width/2

    同样我们得到的planes[1]只包含U数据,但是与Y数据一样,可能存在占位数据。此时pixelStride==1。planes[1]+planes[2]为:

        U    U    0    0    0    0    0    0
        U    U          最后一行没有占位
        V    V    0    0    0    0    0    0
        V    V           最后一行没有占位
    

    总结

    在获得了摄像头采集的数据之后,我们需要获取对应的YUV数据,需要根据pixelStride判断格式,同时还需要通过rowStride来确定是否存在无效数据,那么最终我们获取YUV数据的完整实现为:

        public static byte[] getBytes(ImageProxy image, int rotationDegrees, int width, int height) {
            //图像格式
            int format = image.getFormat();
            if (format != ImageFormat.YUV_420_888) {
                //抛出异常
            }
    
            ByteBuffer i420 = ByteBuffer.allocate(image.getWidth() * image.getHeight() * 3 / 2);
            // 3个元素 0:Y,1:U,2:V
            ImageProxy.PlaneProxy[] planes = image.getPlanes();
            // byte[]
    
            /**
             * Y数据
             */
            //y数据的这个值只能是:1
            int pixelStride = planes[0].getPixelStride();
            ByteBuffer yBuffer = planes[0].getBuffer();
            int rowStride = planes[0].getRowStride();
    
            //1、rowStride 等于Width ,那么就是一个空数组
            //2、rowStride 大于Width ,那么就是每行多出来的数据大小个byte
            byte[] skipRow = new byte[rowStride - image.getWidth()];
            byte[] row = new byte[image.getWidth()];
            for (int i = 0; i < image.getHeight(); i++) {
                yBuffer.get(row);
                i420.put(row);
                // 不是最后一行才有无效占位数据,最后一行因为后面跟着U 数据,没有无效占位数据,不需要丢弃
                if (i < image.getHeight() - 1) {
                    yBuffer.get(skipRow);
                }
            }
    
            /**
             * U、V
             */
            for (int i = 1; i < 3; i++) {
                ImageProxy.PlaneProxy plane = planes[i];
                pixelStride = plane.getPixelStride();
                rowStride = plane.getRowStride();
                ByteBuffer buffer = plane.getBuffer();
    
                //每次处理一行数据
                int uvWidth = image.getWidth() / 2;
                int uvHeight = image.getHeight() / 2;
    
                // 一次处理一个字节
                for (int j = 0; j < uvHeight; j++) {
                    for (int k = 0; k < rowStride; k++) {
                        //最后一行
                        if (j == uvHeight - 1) {
                            //uv没混合在一起
                            if (pixelStride == 1) {
                                //rowStride :大于等于Width/2
                                // 结合外面的if:
                                //  如果是最后一行,我们就不管结尾的占位数据了
                                if (k >= uvWidth) {
                                    break;
                                }
                            } else if (pixelStride == 2) {
                                //uv混在了一起
                                // rowStride:大于等于 Width
                                if (k >= image.getWidth()) {
                                    break;
                                }
                            }
                        }
    
    
                        byte b = buffer.get();
                        // uv没有混合在一起
                        if (pixelStride == 1) {
                            if (k < uvWidth) {
                                i420.put(b);
                            }
                        } else if (pixelStride == 2) {
                            // uv混合在一起了
                            //1、偶数位下标的数据是我们本次要获得的U/V数据
                            //2、占位无效数据要丢弃,不保存
                            if (k < image.getWidth() && k % 2 == 0) {
                                i420.put(b);
                            }
                        }
                    }
                }
            }
    
    
            //I420
            byte[] result = i420.array();
    
            if (rotationDegrees == 90 || rotationDegrees == 270) {
                //旋转之后 ,图像宽高交换
                result = rotation(result, image.getWidth(), image.getHeight(), rotationDegrees);
            }
    
            return result;
        }
    

    相关文章

      网友评论

          本文标题:RTMP摄像头直播-CameraX数据采集处理

          本文链接:https://www.haomeiwen.com/subject/pgpqgrtx.html