android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频

Posted 渐行渐远是否还有一种坚持留在心间

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频相关的知识,希望对你有一定的参考价值。

写在前面的

网上有很多博客都是讲使用opengl+camera2美颜相机,本人技术能力有限,觉得openGL android使用十分复杂,GLES20以后还需要理解顶点着色器片段着色器等晦涩的名词,离开操作手册还是什么也不会写。camera2 api接口的回调太多,导致代码逻辑混乱,难以组织逻辑。

偶然发现opencv处理后拍视频,一点也不用担心处理的耗时导致视频卡顿,MediaCodec自带Buffer缓冲,拍720p的视频也没问题,1080p还没试过。由此做下笔记。

而opencv的滤镜库也很成熟,美颜滤镜磨皮美白也一大把,如果加静态图文水印就更简单,用不到滤镜算法。为windows平台写的opencv滤镜算法都有参考价值。

MediaCodec编码后可以录制视频,也可以编码成h264,h265等推流直播,会议。

首先根据设置支持Profile获取支持录制的视频格式,这里的例子都没添加声道,因为我觉得添加声音一定不难。


public class CameraUtils 
 /**
     * 根据MediaRecord支持的录像size大小,MediaRecord支持的录像大小,也是MediaCodec支持的
     */
    public static Point initSupportProfile() 
        int mQuality = 0;

        if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_TIME_LAPSE_480P)) //无声音的480p
            mQuality = CamcorderProfile.QUALITY_TIME_LAPSE_480P;
         else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_TIME_LAPSE_QVGA)) 
            mQuality = CamcorderProfile.QUALITY_TIME_LAPSE_QVGA;
         else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_TIME_LAPSE_CIF)) 
            mQuality = CamcorderProfile.QUALITY_TIME_LAPSE_CIF;
         else if (CamcorderProfile.hasProfile(CamcorderProfile.QUALITY_TIME_LAPSE_720P)) 
            mQuality = CamcorderProfile.QUALITY_TIME_LAPSE_720P;
        
        Log.d("px", "finally mQuality resolution:" + mQuality);
        CamcorderProfile profile = CamcorderProfile.get(mQuality);
        Log.d("px", "video screen from CamcorderProfile resolution:" + profile.videoFrameWidth + "*" + profile.videoFrameHeight);

        return new Point(profile.videoFrameWidth, profile.videoFrameHeight);
    

然后根据这个视频尺寸去找预览尺寸,相同尺寸最好,设置帧率,预览颜色格式,帧率。值得一提的是,帧率设置在很多设备报错,可能不能设置,或设置的不对吧。

static Camera.Parameters showSupportPreviewSize(Camera camera, Point size) 
        //处理预览尺寸
        Camera.Parameters p = camera.getParameters();
        List<Camera.Size> previewSizes = p.getSupportedPreviewSizes();
        if (previewSizes != null) 
            StringBuilder sb = new StringBuilder("SupportedPreviewSizes:[");
            for (Camera.Size s : previewSizes) 
                sb.append(s.width).append("*").append(s.height).append(",");
                if (s.width * s.height == size.x * size.y) 
                    p.setPreviewSize(s.width, s.height);
                    Log.d("px", "resolution size:" + s.width + "*" + s.height);
                
            
            Log.d("px", sb.deleteCharAt(sb.length() - 1).toString());
         else 
            Log.d("px", "SupportedVideoSizes:null");
        

        //处理预览帧率
        int[] range = 0, 0;
        List<int[]> previewFpsRange = p.getSupportedPreviewFpsRange();
        StringBuilder sb = new StringBuilder("SupportedPreviewFpsRange:");
        for (int[] r : previewFpsRange) 
            sb.append("[");
            for (int i = 0; i < r.length; i++) 
                sb.append(r[i]).append(",");
            
            if (r[0] > 20 * 1000 && r[0] > range[0]) //取一个大于20的帧率
                range = r;
            
            sb.deleteCharAt(sb.length() - 1);
            sb.append("],");
        
        sb.deleteCharAt(sb.length() - 1);
        Log.d("px", sb.toString() + "->use:[" + range[0] + "," + range[1] + "]");
        //帧率
//        p.setPreviewFpsRange(24, 24);

        List<Integer> formats = p.getSupportedPreviewFormats();
        Log.d("px", "SupportedPreviewFormats:" + formats);
        if (formats.contains(ImageFormat.NV21))
            p.setPreviewFormat(ImageFormat.NV21);
        else if (formats.contains(ImageFormat.YV12))
            p.setPreviewFormat(ImageFormat.YV12);

        return p;
    

颜色格式默认就是NV21,告诉opencv你使用的颜色格式

public static int preview2deocode(int previewFormat) 
        Log.d("px", "previewFormat=" + previewFormat);
        int decodeColor = 0;
        //获取相机预览的颜色格式,暂时只有这两种
        if (previewFormat == ImageFormat.NV21) 
            decodeColor = NativeUtils.NV21;
         else if (previewFormat == ImageFormat.YV12) 
            decodeColor = NativeUtils.YV12;
        
        return decodeColor;
    

然后再设置一些相机其他参数

 public static Camera.Parameters setupCameraParams(Camera camera, Point size) 
        //处理预览像素,帧率,颜色格式
        Camera.Parameters p = showSupportPreviewSize(camera, size);

        //设置简单的聚焦,白平衡,闪光灯等等
        p.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
        p.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
        p.setWhiteBalance(Camera.Parameters.WHITE_BALANCE_AUTO);
        p.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
        camera.setParameters(p);

        camera.setDisplayOrientation(90); // Portrait mode
        return p;
    

当然最好是,询问设备是否支持这些特性再设置,聚焦模式还可以自建线程聚焦。
怎么启动预览到surfaceview略过,主要是在surfaceChanged或surfaceCreate添加一个预览回调

 @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) 

        byte[] previewBuf = new byte[mPreviewSize.x * mPreviewSize.y * 3 / 2];
        camera.addCallbackBuffer(previewBuf);
        camera.setPreviewCallback(this);

    
 @Override
    public void onPreviewFrame(byte[] data, Camera camera) 
        if (state == State.Recorde) 
            recordTask.captureImg(data, mPreviewSize.x, mPreviewSize.y);
        
    

录像时VideoRecordTask中captureImg来持续捕获来自相机的预览数据

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class VideoRecordTask 
  public void captureImg(byte[] data, int w, int h) 
        Date date = new Date();
        NativeUtils.drawText(data, w, h, dateFormat.format(date));
        frameInfo.flags = MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
        frameInfo.presentationTimeUs = (date.getTime() - videoCreateTime) * 1000;

        putIn(data, frameInfo);
    

这个方法再涉及的方法通过名字应该可以看出来含义了,在源码中放出。
NativeUtils这是对接NDK的类了,这些都是根据业务需求来写的,并非通用的方法,根据我的需求,我要想绘制字体,得依次先使用fixFontFile,fixTextArea,fixColorFormat来初始化配置。

public class NativeUtils 
    static 
        System.loadLibrary("dxtx");//视频水印库
    

    public static int NV21 =21,NV12=22,YV12=19,I420=20;

    /**
     * 测试接口,ndk里面包含各种加水印的方法
     * @param imagePath
     * @param data
     * @param text
     * @return
     */
    public static native String testImg(String imagePath, int[] data, String text);

    /**
     * 用一段文字预算文字区域的大小,以方便生成背景
     * @param text
     */
    public static native void fixTextArea(String text);

    /**
     * 设置yuv的颜色输入格式和输出,@link ColorFormat
     * @param decodeColor
     * @param encodeColor
     */
    public static native void fixColorFormat(int decodeColor, int encodeColor);

    /**
     * 绘制文字
     * @param data yuv数据,来自视频解码.绘制后data数据被修改
     * @param w
     * @param h
     * @param text 需要绘制的文字
     */
    public static native void drawText(byte[] data, int w, int h, String text);

    /**
     * 释放内存
     */
    public static native void release();

    /**
     * 初始化字体文件,ndk内部会初始化字体大小,间距等属性
     * @param ttfPath
     */
    public static native void fixFontFile(String ttfPath);

我集成了typefree库,源码已在jni目录下,我也不关心这个库是什么原理了,只知道他是加载字体的就好了。抄的网上的集成成功的例子,通过编写android.mk,application.mk,命令ndk-build打包成libft2.a,再加入我自己的dxtx.so库。application.mk

APP_ABI := armeabi,arm64-v8a

讲道理应该要提供所有abi的,包括build.gradle里面,也应该生成all abi。
顺便科普一下android 手机架构abi.
armeabi兼容大部分手机
arm64-v8a是64位的,有这个架构的手机使用这里面的so,会加快计算速度。支持arm64-v8a的手机找不到arm64-v8a文件夹最终会兼容使用armeabi。

x86我只知道x86架构的模拟器会用,同理x86_64也是64位的,
mips系列还不知道什么手机在用。

    ndk 
            abiFilters "armeabi","arm64-v8a"
        

demo地址

以上是关于android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频的主要内容,如果未能解决你的问题,请参考以下文章

opencv for android 如何实现后台启动摄像头,不显示预览界面

利用Android Camera2 的照相机api 实现 实时的图像采集与预览

qt Android中使用opencv处理视频

OpenCV 网络摄像头帧率

使用OpenCV Android SDK从摄像头帧实时检测人脸

WebRTC中Android Demo中的摄像头从采集到预览流程