使用Camera+MediaCodec录制编译音视频

之前写过一个开源项目仿微信视频拍摄UI基于 ffmpeg的视频录制编辑

使用的是VCamera库来录制,效果很差, 主要是因为ffmpeg的so库编译版本不支持targetSdkVersion26以上, 导致现在大部分项目都用不了, 而且优化不是很好

所以我用了基于Camera录制源+MediaCodec编码 替换了原本的录制方法, 并更新了ffmpeg库LanSoEditor 速度更快 兼容更好

1. 首先是打开camera预览画面

    //正常初始化Camera对象, 注意getCameraDisplayOrientation()这个方法, 
    //因为Camera得到的画面是歪的, 所以我们需要旋转一下
    public void openCamera(Activity activity, int cameraId, SurfaceHolder surfaceHolder){
        ...
        mCamera = Camera.open(cameraId);
        //视频旋转角度 因为Camera得到的画面是歪的, 所以我们需要旋转一下播放
        displayOrientation = getCameraDisplayOrientation(activity, cameraId);
        mCamera.setDisplayOrientation(displayOrientation);
        mCamera.setPreviewDisplay(surfaceHolder);
        mCamera.setPreviewCallback(previewCallback);

        Camera.Parameters parameters = mCamera.getParameters();
        previewSize = getPreviewSize();
        //视频录制尺寸
        parameters.setPreviewSize(previewSize[0], previewSize[1]);
        //录制焦点
        parameters.setFocusMode(getAutoFocus());
        parameters.setPictureFormat(ImageFormat.JPEG);
        //在后面会转成nv12在进行编码
        parameters.setPreviewFormat(ImageFormat.NV21);

        mCamera.setParameters(parameters);
        mCamera.startPreview();
        ...
    }
    
    //在这里cemara返回的数据是YUV420-NV21格式的数据
     mCameraHelp.setPreviewCallback(new Camera.PreviewCallback() {
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
             ...
             mYUVQueue.add(data);
             ...
        }
    });

2. 当开始录制的时候 初始化AvcEncoder(重点)

//首先是构造函数里面, 我们初始化MediaCodec编码器(视频宽高,码率,yuv420色彩,帧数,编码格式avc)
public AvcEncoder(int width, int height, ArrayBlockingQueue<byte[]> YUVQueue) {
        ...
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width*height*5);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mediaCodec.start();
        ...
    }
    
    //开始编码, 编译出来的数据是h264
    public void startEncoder(final String videoPath, final boolean isFrontCamera){
        //循环从YUVQueue里面拿取每帧数据
        while (isRunning.get()) {
            //把nv21转nv12
            NV21ToNV12(input,yuv420sp, width, height);
            //如果是前置摄像头的话 要上下颠倒一下画面
            rotateYUV420Degree180(yuv420sp, width, height);
            //此步就是mediaCodec转码 有兴趣去看详细代码, 在后面我会详细讲解
            mediaCodec.getInputBuffers();
        }
    }

3. 最后一步就是使用ffmpeg把h264转mp4

    //ffmpeg -i in -vcodec copy -f mp4 put
    //因为我的功能里有分段录制 所以我是先把h264转ts 然后多个ts合成mp4

4. 视频录制就完成了, 接下来是音频录制编码 初始化AudioRecordUtil

    public AudioRecordUtil(final String path){
        bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        //麦克风 采样率 单声道 音频格式, 缓存大小
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
        mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        ...
    }
    
    //和视频编码一样 也是循环
    public void startRecord(){
        while (isRecording.get()) {
            //拿到音频数据
            audioRecord.read(data, 0, bufferSize);
            //pcm转aac 有兴趣去看详细代码
            encodeData(data);
        }
    }

5. 最后录制完成 使用ffppeg把视频和音频文件合成在一起

    //ffmpeg -i video -1 audio -t vDuration -map 0:v -map 1:a -vcodec copy -acodec copy -absf aac_adtstoasc -y out

6. 音视频使用的MediaCodec除了编码的数据不一样 原理是一致的

    //得到编码器的输入和输出流, 输入流写入源数据 输出流读取编码后的数据
    ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
    ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
    //得到要使用的缓存序列角标
    int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
    if (inputBufferIndex >= 0) {
        pts = computePresentationTime(generateIndex);
        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
        inputBuffer.clear();
        //把要编码的数据添加进去
        inputBuffer.put(input);
        //塞到编码序列中, 等待MediaCodec编码
        mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
        generateIndex += 1;
    }

    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    //读取MediaCodec编码后的数据
    int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
    while (outputBufferIndex >= 0) {
        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
        byte[] outData = new byte[bufferInfo.size];
        //这步就是编码后的h264数据了
        outputBuffer.get(outData);
        if(bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG){//视频信息
            configByte = new byte[bufferInfo.size];
            configByte = outData;
        }else if(bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME){//关键帧
            byte[] keyframe = new byte[bufferInfo.size + configByte.length];
            System.arraycopy(configByte, 0, keyframe, 0, configByte.length);
            System.arraycopy(outData, 0, keyframe, configByte.length, outData.length);
            outputStream.write(keyframe, 0, keyframe.length);
        }else{//正常的媒体数据
            outputStream.write(outData, 0, outData.length);
        }
        //数据写入本地成功 通知MediaCodec释放data
        mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
        //读取下一次编码数据
        outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
    }

7. 还有拍照的逻辑 其实很简单 就是把一帧NV21数据转成jpeg

    //把NV21数据输出成jpeg
    YuvImage yuvimage = new YuvImage(data, ImageFormat.NV21, mCameraHelp.getWidth(), mCameraHelp.getHeight(), null);
    yuvimage.compressToJpeg(new Rect(0, 0, yuvimage.getWidth(), yuvimage.getHeight()), 100, fos);
    fos.close();
    //注意这里, 因为之前初始化camera就说过了, camera出来的画面是旋转的 所以我们这里也手动把图片旋转一下
    Matrix matrix = new Matrix();
    if(mCameraHelp.getCameraId() == Camera.CameraInfo.CAMERA_FACING_FRONT){
        matrix.setRotate(360-mCameraHelp.getDisplayOrientation());
        //如果是前置摄像头, 那么镜像一下
        matrix.postScale(-1, 1);
    }else{
        matrix.setRotate(mCameraHelp.getDisplayOrientation());
    }

到这里 其实主要逻辑就结束了 其余的视频编辑操作(裁剪, 截取, 添加水印, 播放速度等等)这些其实就是简单的ffmpeg语句, 大家自行看代码也能理解, 之前我也有文章讲过这些

仿微信视频拍摄UI, 基于ffmpeg的视频录制编辑(上)

仿微信视频拍摄UI, 基于ffmpeg的视频录制编辑(下)

希望能对你有所帮助