Libyuv之初体验

应用场景

最近在接入腾讯实时音视频,我们的应用场景是主叫向被叫推送外部采集的视频数据,主、被叫端的视频界面都得实现缩放、标注功能;音频传输使用SDK默认的就好了;
在实际开发中发现主叫还好做,被叫端就坑爹了,收到的数据是yuv i420格式的数据,根本无法直接转成bitmap显示到容器上。后面找到一个java方法去i420转nv21,然后再生成bitmap数据显示出来,但是这样每帧数据的转换时间再80ms左右,根本达不到要求,这下就尴尬了。
最后没办法了,只能在JNI上想办法,后面通过libyuv库达到了要求,平均每帧的转换时间再20ms的样子,在这种效率下就不会出现视频卡顿的情况了。

Java转换代码

若只转换单个图片,或对性能没有太大要求的话,使用java代码进行转换就可以满足要求了。

/**
 * 使用java代码方式将I420数据转换成NV21数据,转换后,颜色、格式都是OK的
 * 但是转换过程时间太长,平均时间再80ms左右,无法满足视频的要求
 *
 * @param data   i420类型的byte[]数据
 * @param width  图片宽度
 * @param height 图片高度
 * @return
 */
public Bitmap I420ToNv21(byte[] data, int width, int height) {
    long preTim = System.currentTimeMillis();
    long tagTime = System.currentTimeMillis();

    final int frameSize = width * height;   //bufferY
    final int qFrameSize = frameSize / 4;   //bufferV
    final int tempFrameSize = frameSize * 5 / 4;    //bufferU

    byte[] ret = new byte[data.length];
    System.arraycopy(data, 0, ret, 0, frameSize); // Y

    for (int i = 0; i < qFrameSize; i++) {
        ret[frameSize + i * 2] = data[tempFrameSize + i]; // Cb (U)
        ret[frameSize + i * 2 + 1] = data[frameSize + i]; // Cr (V)
    }

    long i420ToNV21 = System.currentTimeMillis() - tagTime;

    Bitmap bitmap = null;
    try {
        tagTime = System.currentTimeMillis();

        //调用这两个方法,生成bitmap的帧率都在10帧左右,无法满足要求
        bitmap = nv21ToBitmapByArgb(ret, width, height); //使用数组创建bitmap
		//bitmap = nv21ToBitmapByYuvImage(ret, width, height);//使用yuvImage方式

        long bitmapTime = System.currentTimeMillis() - tagTime;
        showLog("I420转Nv21时间:" + i420ToNV21 + " bitmap时间:" + bitmapTime);
    } catch (Exception e) {
        e.printStackTrace();
    }

    showLog("生成 bimtp时间:" + (System.currentTimeMillis() - preTim) + "  文件大小:" + bitmap.getRowBytes());
    return bitmap;
}

/**
 * 将nv21数据通过数组变换的方式转换成bitMap
 *
 * @param nv21
 * @param width
 * @param height
 * @return
 */
private Bitmap nv21ToBitmapByArgb(byte[] nv21, int width, int height) {
    if (yuvType == null) {
        yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length);
        in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);

        rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
        out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);
    }

    in.copyFrom(nv21);

    yuvToRgbIntrinsic.setInput(in);
    yuvToRgbIntrinsic.forEach(out);

    Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    out.copyTo(bmpout);

    return bmpout;
}

/**
 * 将nv21数据通过YuvImage的方式转换成bitmap
 *
 * @param nv21
 * @param width
 * @param height
 * @return
 */
private Bitmap nv21ToBitmapByYuvImage(byte[] nv21, int width, int height) {
    Bitmap bitmap = null;
    try {
        YuvImage image = new YuvImage(nv21, ImageFormat.NV21, width, height, null);
        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        image.compressToJpeg(new Rect(0, 0, width, height), 80, stream);
        bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
        stream.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return bitmap;
}

Libyuv

libyuv是Google开源的实现各种YUV与RGB之间相互转换、旋转、缩放的库。它是跨平台的,可在Windows、Linux、Mac、Android等操作系统,x86、x64、arm架构上进行编译运行,支持SSE、AVX、NEON等SIMD指令加速。总而言之就是,在图像处理方面牛逼得不行。
官方源码,科学上网方可获取,我的网络太慢了,下载不下来。

JNI环境搭建

目前Android使用NDK有两种方式Android.mk和Cmake.txt两种方式,Cmake是官方比较推荐的方式,在配置方面会比Android.mk方式简单很多。
网上关于JNI环境搭建、这两种方式如何去配置已经有很多很详细的博文了,这里我列出我在开发过程中所搜集到的,比较详细全面的博文列举出来:

  • Android.mk方式:
    博文:Android Studio–NDK编译C代码为.so文件,JNI调用此博文在以下几点做了详细说明:

    1. JNI环境搭建
    2. 如何在一个已有项目中加入传统的JNI代码编译方式如何在一个已有项目中加入传统的JNI代码编译方式
    3. 手动编译SO包,项目每次运行的时候不用编译SO,只有在修改了源码,需要更新时候在手动去编译新的SO,有利于加快run的速度手动编译SO包,项目每次运行的时候不用编译SO,只有在修改了源码,需要更新时候在手动去编译新的SO,有利于加快run的速度
  • Cmake.txt方式:

    • 官方Cmake搭建教程
    • 博文一:AndroidStudio 进行 JNI / NDK 开发:初步配置及使用此博文在以下几点做了详细说明:
    1. NDK环境搭建
    2. 在原有旧项目中引入cmake方式运行JNI
    3. 此文章对cmake中的参数、native生成、第三方so、h文件引入等问题有详细说明,值得一看;
    • 博文二:Android Studio 2.2 CMAKE 高效NDK开发此博文在以下几点做了详细说明:
    1. cmke与传统JNI做了对比说明
    2. 普通项目加入传统、cmake两种方式做了说明普通项目加入传统、cmake两种方式做了说明

I420数据转换

在搜索Libyuv使用方法的时候,找到一位博主写的Demo跟我们的需求无限接近,于是我们就愉快的引用博主的Demo,在此基础上来做修改了,libyuv—libyuv测试使用ARGBToI420和ConvertToARGB接口。
其实从博文的描述上看,我几乎可以不用改代码,直接用这个里面的源码既可以,但是在实际开发过程中发现,demo里面将一张图片转成i420,然后将i420转argb显示为bitmap是没有问题;可是从腾讯实时音视频里面获取到的i420数据转argb的时候,发现转换之后颜色不对了,通过使用工具查看对比发现,貌似将其转换成了YV12的数据;
到现在还未找到原因,希望有同样需求的童鞋可以指点一下;
后面从这篇博文(使用libyuv对YUV数据进行缩放,旋转,镜像,裁剪等操作)中收到启发,可以将i420转成NV21的数据,然后再转成argb显示到容器中,经过这样的一顿操作,可算是将音视频的i420数据转成了我们需要的bitmap数据,虽然绕了一下,但是生成每帧画面的时间还是在20ms左右,并不会影响到我们的视觉体验。
到此,卡了N久的数据转换问题总算是基本搞定了。

Yuv原始数据查看

在i420数据转换过程中,只是通过效果看到颜色不对,但是对于数据转成了哪种格式,从结果中无法看出。这时我们可以把yuv原始数据保存为.yuv的文件,然后用YUV格式查看器RawViewer(并不是我上传的)这个工具去打开源文件,就可以分析出转换后的数据格式、宽高等信息。

/**
 *	保存yuv文件
 * @param name
 * @param buffer
 */
private void writeToFile(String name, ByteBuffer buffer) {
    try {
        String path = "/sdcard/" + name + ".yuv";
        FileOutputStream fos = new FileOutputStream(path, true);
        byte[] in = new byte[buffer.capacity()];
        buffer.clear();
        buffer.get(in);
        fos.write(in);
        fos.close();
    } catch (Exception ex) {
        ex.printStackTrace();
    }
}

SO引用

在项目中JNI开发好了之后,我们一般是不会变动的,若每次跑项目的时候,还需要去编译生成SO包,就太费时间了;
在此我给Demo中的项目改成了静态引用的libs里面的so的方式,我们只需要有修改的时候去更新一下SO即可。
SO包引用的两种方式:动态编译、静态引用。

动态编译

这种模式下面,每次跑项目的时候都会去重新生成新的SO包,在module的gradle中做如下配置:

android{
    externalNativeBuild {
        ndkBuild {
            path "jni/Android.mk"
        }
    }
    defaultConfig {
        externalNativeBuild {
            ndkBuild {
                arguments "NDK_APPLICATION_MK:=jni/Application.mk", "APP_PLATFORM:=android-14"
                abiFilters "armeabi-v7a"
            }
        }
    }
}

静态引用

每次更新代码之后,都需要手动执行ndk-build命令去生成SO包,然后引用,这样在没有更新到JNI相关内容的时候就无需跑JNI代码,节约时间。同样在module的gralde中配置。

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi', "armeabi-v7a"//, "x86", "mips"
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

ndk-build命令问题

刚开始的时候我一直在项目的根目录执行ndk-build命令,发现一直报错,无法生成SO包;在设置好ndk环境变量之后,进入到Android.mk所在的根目录,我这里是jni文件夹下面,然后再执行ndk-build就OK了,解决办法。
Libyuv之初体验_第1张图片

文档说明

Android.mk字段解释
Application.mk字段解释
ndk-build构建命令解释

最后附上Demo

你可能感兴趣的:(android)