android 视频录制、添加滤镜以及编码

3,294 阅读8分钟

基本概念

码率: 码率就是数据传输时单位时间传送的数据位数,单位一般是kbps即千位每秒。码率影响体积,与体积成正比:码率越大,体积越大;码率越小,体积越小。码率还影响清晰度,码率越高清晰度也就越高。码率超过一定数值,对图像的质量没有多大影响。

帧率: 帧率或者称FPS(Frames Per Second,帧/秒),是指每秒显示的图片数,或者GPU处理时每秒能够更新的次数。越高的帧速率可以得到更流畅逼真的画面。

分辨率: 分辨率是用于度量图像内数据量多少的一个参数,通常表示成ppi,影响图像大小,与图像大小成正比:分辨率越高,图像越大;分辨率越低,图像越小。

一、视频录制

Android视频录制的方式有两种,一种是用MediaRecorder类来进行录制,这种方式录制处理出来的是封装好的mp4视频格式。另外一种就是通过Android.hardware包下的Camera采集摄像头数据,数据放回到java层,开发者在java在对数据进行处理存储。接下来我们就讲一下第二种录制方式的过程。

1. 打开摄像头

打开摄像头时,相机默认打开的是后置摄像头,通过调用以下方法打开摄像头

mCamera = Camera.open();

要是想使用前置摄像头的话就要用先找到找到前置摄像头,然后再调用Camera.open()方法

 Camera.CameraInfo info = new Camera.CameraInfo();
       int numCameras = Camera.getNumberOfCameras();//拿到摄像头的数量
       for (int i = 0; i < numCameras; i++) {
           Camera.getCameraInfo(i, info);
           //寻找前置摄像头
           if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
           //找到并打开摄像头
               mCamera = Camera.open(i);
               break;
           }
       }

在找到摄像头之后接下来的一步也是比较重要的一步,设置摄像头预览的大小,因为手机的屏幕尺寸有很多,摄像头支持的尺寸也很多,所以一定要找到合适的尺寸才能拿到最佳的预览效果。设置摄像头尺寸大小的代码如下

//找到摄像头的默认尺寸
Camera.Size ppsfv = parms.getPreferredPreviewSizeForVideo();
        if (ppsfv != null) {
            Log.d(TAG, "Camera preferred preview size for video is " +
                    ppsfv.width + "x" + ppsfv.height);
        }
        //  判断摄像头支持的尺寸是否包含手机屏幕尺寸
        for (Camera.Size size : parms.getSupportedPreviewSizes()) {
            if (size.width == width && size.height == height) {
            //设置屏幕尺寸
                parms.setPreviewSize(width, height);
                return;
            }
        }
        // 找不到就是用默认的尺寸
        Log.w(TAG, "Unable to set preview size to " + width + "x" + height);
        if (ppsfv != null) {
            parms.setPreviewSize(ppsfv.width, ppsfv.height);
        }
//设置录像提示,参数为true时,屏幕左下角会有录像提示,这个对影响帧率有一定影响   
    parms.setRecordingHint(true);
    mCamera.setParameters(parms);

在设置完尺寸之后要设置展示的角度

   //拿到系统展示时的信息
  Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
    // 这里这设置了竖屏和横屏时摄像头的展示角度 
        if(display.getRotation() == Surface.ROTATION_0) {
            mCamera.setDisplayOrientation(90);
            layout.setAspectRatio((double) mCameraPreviewHeight / mCameraPreviewWidth);
        } else if(display.getRotation() == Surface.ROTATION_270) {
            layout.setAspectRatio((double) mCameraPreviewHeight/ mCameraPreviewWidth);
            mCamera.setDisplayOrientation(180);
        } else {
            // Set the preview aspect ratio.
            layout.setAspectRatio((double) mCameraPreviewWidth / mCameraPreviewHeight);
        }

2. 设置预览

摄像头预览一般是放在GLSurfaceView或者TextureView中,什么是GLSurfaceView呢?官方文档解释是:GLSurfaceView是使用专用曲面显示OpenGL渲染的SurfaceView实现。
它的功能主要如下

  • 管理表面,这是可以组合到Android视图系统中的特殊内存。
  • 管理EGL显示,使OpenGL可以渲染到表面中。
  • 接受执行实际渲染的用户提供的Renderer对象。
  • 在专用线程上进行渲染,以将渲染性能与UI线程分离。
  • 支持按需渲染和连续渲染。
  • 可选地包装,跟踪和/或错误检查渲染器的OpenGL调用。

GLSurfaceView类有一个Renderer接口,这个接口可以看成是GLSurfaceView的生命周期一个,它主要有三个回调函数:

@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
       
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
 
    }
    @Override
    public void onDrawFrame(GL10 gl) {
      
    }

onSurfaceCreated() 这里面主要是一些初始化的操作,例如是 创建SurfaceTexture onSurfaceChanged() 这里就是党surface大小发生变化时会回调
onDrawFrame() 这里是返回当前帧的数据,在这里我们可以对帧数据进行处理。 GLSurfaceView的初始化流程大致如下:

        mGLView = (GLSurfaceView) findViewById(R.id.cameraPreview_surfaceView);
        mGLView.setEGLContextClientVersion(2);     // 选择EGL的版本
        //创建Renderer
        mRenderer = new CameraSurfaceRenderer(mCameraHandler, sVideoEncoder, outputFile);
        //设置Renderer
        mGLView.setRenderer(mRenderer);
        //设置渲染的模式
        mGLView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

Camera跟GLSurfaceView是怎么样关联起来的呢,重点是在Renderer里面,上面是一个自定义的Renderer,里面实现了刷新GLSurfaceView显示的画面,视频编码,以及将编码后 的视频输出到文件中。我们可以看一下CameraSurfaceRenderer的对应上面三个方法的代码

 @Override
    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        Log.d(TAG, "onSurfaceCreated");
        
        /
        //判断视频编码是否在进行
        mRecordingEnabled = mVideoEncoder.isRecording();
        if (mRecordingEnabled) {
            mRecordingStatus = RECORDING_RESUMED;
        } else {
            mRecordingStatus = RECORDING_OFF;
        }

       
        //创建一个全屏着色器和texture一起渲染
        mFullScreen = new FullFrameRect(
                new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
        mTextureId = mFullScreen.createTextureObject();

        
        //创建一个表面纹理来对图像流数据进行处理
        mSurfaceTexture = new SurfaceTexture(mTextureId);

        //通知UI线程,这里创建了SurfaceTexture
        mCameraHandler.sendMessage(mCameraHandler.obtainMessage(
                CameraCaptureActivity.CameraHandler.MSG_SET_SURFACE_TEXTURE, mSurfaceTexture));
    }

在主线程中的handleMessage

 public void handleMessage(Message inputMessage) {
            int what = inputMessage.what;
            Log.d(TAG, "CameraHandler [" + this + "]: what=" + what);

            CameraCaptureActivity activity = mWeakActivity.get();
            if (activity == null) {
                Log.w(TAG, "CameraHandler.handleMessage: activity is null");
                return;
            }

            switch (what) {
                case MSG_SET_SURFACE_TEXTURE:
                    activity.handleSetSurfaceTexture((SurfaceTexture) inputMessage.obj);
                    break;
                default:
                    throw new RuntimeException("unknown msg " + what);
            }
        }
        
        //摄像机设置预览
 private void  handleSetSurfaceTexture(SurfaceTexture st) {
        st.setOnFrameAvailableListener(this);
        try {
            mCamera.setPreviewTexture(st);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
        mCamera.startPreview();
    }

在onSurfaceCreated()方法中,为什么要创建一个SurfaceTexture呢?它跟GLSurfaceView是什么关系呢?下面给大家讲一下

SurfaceView: 本质上是一个view,但是它有自己的Layer,它的显示也不受view的属性控制。如下图所示

SurfaceTexture: 它类似于中间件,不直接显示图像流数据,而是对图像流进行二次处理(类似于加滤镜,美颜)并转化成openGL的纹理,后可以交给GLSurfaceView显示,也可以交给TextureView显示

TextureView: 它不同于SurfaceView,不会在WMS中单独创建窗口,而是作为一个普通的view接受其管理,它可以将内容流直接投影到view中可以实现live preview功能,但要注意的是,TextureView必须在硬件加速的窗口中。

上面说完OnSurfaceCreated()方法,下面我们再看一下另外两个方法

 @Override
    public void onSurfaceChanged(GL10 unused, int width, int height) {
        Log.d(TAG, "onSurfaceChanged " + width + "x" + height);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void onDrawFrame(GL10 unused) {
        if (VERBOSE) Log.d(TAG, "onDrawFrame tex=" + mTextureId);
        boolean showBox = false;

        //拿到最新的帧
        mSurfaceTexture.updateTexImage();

        //根据录制状态做相应的操作
        if (mRecordingEnabled) {
            switch (mRecordingStatus) {
                case RECORDING_OFF:
                    Log.d(TAG, "START recording");
                    // start recording
                    mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
                            mOutputFile, 640, 480, 1000000, EGL14.eglGetCurrentContext()));
                    mRecordingStatus = RECORDING_ON;
                    break;
                case RECORDING_RESUMED:
                    Log.d(TAG, "RESUME recording");
                    mVideoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());
                    mRecordingStatus = RECORDING_ON;
                    break;
                case RECORDING_ON:
                    // yay
                    break;
                default:
                    throw new RuntimeException("unknown status " + mRecordingStatus);
            }
        } else {
            switch (mRecordingStatus) {
                case RECORDING_ON:
                case RECORDING_RESUMED:
                    // stop recording
                    Log.d(TAG, "STOP recording");
                    mVideoEncoder.stopRecording();
                    mRecordingStatus = RECORDING_OFF;
                    break;
                case RECORDING_OFF:
                    // yay
                    break;
                default:
                    throw new RuntimeException("unknown status " + mRecordingStatus);
            }
        }

       
        mVideoEncoder.setTextureId(mTextureId);

        //告诉编码器,当前帧可用
        mVideoEncoder.frameAvailable(mSurfaceTexture);

    
    }

二、视频处理

上面讲完视频录制,接下将一下视频处理。视频处理的方式有很多种,例如人脸美白,添加滤镜,添加背景音乐等等,甚至是抖音前段时间挺火的一个添加游戏都可以。下面讲一下添加滤镜的方法。 上面说了,处理帧数据的方法是在onDrawFrame()中的,所以添加滤镜也会在此方法中添加。下面我们看看代码

 @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
    @Override
    public void onDrawFrame(GL10 unused) {
       ...

        if (mIncomingWidth <= 0 || mIncomingHeight <= 0) {
           //如果长宽不对则返回
            Log.i(TAG, "Drawing before incoming texture size set; skipping");
            return;
        }
        //在此更新滤镜
        if (mCurrentFilter != mNewFilter) {
            updateFilter();
        }
        if (mIncomingSizeUpdated) {
            mFullScreen.getProgram().setTexSize(mIncomingWidth, mIncomingHeight);
            mIncomingSizeUpdated = false;
        }

        // 将从SurfaceTexture得到的数据添加到GLSurfaceView上
        mSurfaceTexture.getTransformMatrix(mSTMatrix);
        mFullScreen.drawFrame(mTextureId, mSTMatrix);

       ···
    }

updateFilter()的方法中做了什么呢,我们看一下

 public void updateFilter() {
        Texture2dProgram.ProgramType programType;
        float[] kernel = null;
        float colorAdj = 0.0f;

        Log.d(TAG, "Updating filter to " + mNewFilter);
        switch (mNewFilter) {
            case CameraCaptureActivity.FILTER_NONE:
                programType = Texture2dProgram.ProgramType.TEXTURE_EXT;
                break;
            case CameraCaptureActivity.FILTER_BLACK_WHITE:
                
                programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BW;
                break;
            case CameraCaptureActivity.FILTER_BLUR:
                programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
                kernel = new float[] {
                        1f/16f, 2f/16f, 1f/16f,
                        2f/16f, 4f/16f, 2f/16f,
                        1f/16f, 2f/16f, 1f/16f };
                break;
            case CameraCaptureActivity.FILTER_SHARPEN:
                programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
                kernel = new float[] {
                        0f, -1f, 0f,
                        -1f, 5f, -1f,
                        0f, -1f, 0f };
                break;
            case CameraCaptureActivity.FILTER_EDGE_DETECT:
                programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
                kernel = new float[] {
                        -1f, -1f, -1f,
                        -1f, 8f, -1f,
                        -1f, -1f, -1f };
                break;
            case CameraCaptureActivity.FILTER_EMBOSS:
                programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
                kernel = new float[] {
                        2f, 0f, 0f,
                        0f, -1f, 0f,
                        0f, 0f, -1f };
                colorAdj = 0.5f;
                break;
            default:
                throw new RuntimeException("Unknown filter mode " + mNewFilter);
        }

        
        if (programType != mFullScreen.getProgram().getProgramType()) {
            mFullScreen.changeProgram(new Texture2dProgram(programType));
           
            mIncomingSizeUpdated = true;
        }
 
        
        if (kernel != null) {
            mFullScreen.getProgram().setKernel(kernel, colorAdj);
        }

        mCurrentFilter = mNewFilter;
    }

代码中,设置了不同滤镜的kernel值,然后在setKernel()方法中,将对应值从到openGL对应方法中进行处理,这些方法如下

            GLES20.glUniform1fv(muKernelLoc, KERNEL_SIZE, mKernel, 0);
            GLES20.glUniform2fv(muTexOffsetLoc, KERNEL_SIZE, mTexOffset, 0);
            GLES20.glUniform1f(muColorAdjustLoc, mColorAdjust);

这些方法是更改OpenGL Uniform变量的值,从而显示不同的颜色效果。 (对于Uniform变量的定义及使用可以看博客:OpenGL 图形库使用(四) —— Uniform及更多属性

三、编码以及输出

常用的视频编码有

  • MPEG系列 :MPEG1,MPEG2,MPEG4等等
  • H.26X系列:现在常用H.264,因为在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。H.264的最大优势是具有很高的数据压缩率,可以有效减少用户带宽的占用。

常见的视频格式有mp4、3GP、avi、rmvb、apple quicktime的mov、以及各大播放器自己封装的格式

在这个demo中,采用的是h.264编码,封装格式是mp4格式 在关于录制的代码中,大家也看到以下的方法

 mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(  mOutputFile, 640, 480, 1000000, EGL14.eglGetCurrentContext()));

这个方法的最终调用是下面这个方法

 private static final String MIME_TYPE = "video/avc";    // H.264 Advanced Video Coding

    public VideoEncoderCore(int width, int height, int bitRate, File outputFile)
            throws IOException {
        mBufferInfo = new MediaCodec.BufferInfo();

        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);

        //设置一些码率,帧率,以及取I帧的间隔时间等
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        if (VERBOSE) Log.d(TAG, "format: " + format);

       
        //新建编码器以及创建一个Surface进行数据的输入
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mInputSurface = mEncoder.createInputSurface();
        mEncoder.start();

        //封装成mp4输出到文件中
        mMuxer = new MediaMuxer(outputFile.toString(),
                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        mTrackIndex = -1;
        mMuxerStarted = false;
    }

在上面的代码中编码器创建了一个surface来作为数据源的输入,因为surface保存了当前窗口的像素数据。同时作为输入的surface还要求带有openGL ES渲染接口。

总结

上面的demo是通过Camera + OpengGL + MediaCodec +进MediaMuxer行视频录制、处理、编码以及封装,这样做的好处就是能在录制视频的过程中进行处理,也可以将视频用自己想要的格式进行编码封装。除此之外项目中的代码均来自google工程师的grafika项目,其项目还有很多值得大家学习的点,大家可以多多阅读和实践。
OpenGL的学习网站
google 工程师的grafika项目

滤镜相关:
GPUImage
MagicCamera