安卓采集摄像头画面生成MP4文件

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zxd_Android/article/details/81433531

主要用的安卓类有MediaCodec和MediaMuxer,MediaCodec负责视频数据编解码,MediaMuxer负责将编码后的数据封装成MP4文件,采集摄像头用的是camera,并且用surfaceview进行预览
1、初始化surfaceview与camera,预览摄像头的画面

  private void initSurfaceHolder() {
        surfaceHolder = surfaceView.getHolder();
        surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                initCamera();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                mCamera.startPreview();
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                destroyCamera();
            }
        });
    }

在surfaceview surfaceCreated中初始化摄像机配置。

  private void initCamera() {
        mCamera = Camera.open(1);
        mCamera.setPreviewCallback(this);
        mCamera.setDisplayOrientation(90);
        if (parameters == null) {
            parameters = mCamera.getParameters();
        }
        parameters.setPreviewFormat(ImageFormat.NV21);
        parameters.setPreviewSize(1280, 720);
        mCamera.setParameters(parameters);
        try {
            mCamera.setPreviewDisplay(surfaceHolder);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

注意由于摄像头默认是横着的,所以必须先调用setDisplayOrientation 翻转下,ImageFormat.NV21是摄像机默认的图像采样格式。
获取原始YUV数据:

 @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if (YUVQueue.size() > 10) {
            YUVQueue.poll();
        }
        YUVQueue.add(data);
    }

onPreviewFrame 回调中获取到的就是原始YUV数据,这里,我先将它放到队列中,以方便后面编码从中取出。

2、摄像头采集到的画面的帧数据拿到后,下一步当然是进行编码工作,编码用到的是MediaCodec,我们先初步了解下MediaCodec。
MediaCodec类可以获取底层媒体编码/解码库,是Android底层多媒体支持库的一部分(一般和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack搭配使用)。
这里写图片描述
简单来说,MediaCodec 工作时候有输入工作队列和输出工作队列,我们将摄像头原始画面数据放进空的buffer中,并且送给编码器,编码器对其进行编码,编码后的数据又通过output buffer队列输出来,我们取出编码后的buffer,并将其释放掉,再送给编码器。
下面开始代码初始化MediaCodec.

 //编码格式,AVC对应的是H264
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1280, 720);
        //YUV 420 对应的是图片颜色采样格式
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
        //比特率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 3000000);
        //帧率
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
        //I 帧间隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        try {
            mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
            //创建生成MP4初始化对象
            mediaMuxer = new MediaMuxer(MP4_PATH, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //进入配置状态
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //进行生命周期执行状态
        mediaCodec.start();

比特率,帧率根据需要自己设置。
另外 摄像头采集到的NV21数据,即YYYYYYYY VUVU,编码器需要的是NV12,即
YYYYYYY UVUV

 private void NV21ToNV12(byte[] nv21, byte[] nv12, int width, int height) {
        if (nv21 == null || nv12 == null) {
            return;
        }
        int framesize = width * height;
        int i, j;
        System.arraycopy(nv21, 0, nv12, 0, framesize);
        for (i = 0; i < framesize; i++) {
            nv12[i] = nv21[i];
        }
        for (j = 0; j < framesize / 2; j += 2) {
            nv12[framesize + j - 1] = nv21[j + framesize];
        }
        for (j = 0; j < framesize / 2; j += 2) {
            nv12[framesize + j] = nv21[j + framesize - 1];
        }
    }

好了,YUV数据也转换好了,下面我们就进行编码工作了,这里我先介绍同步方式,开启一个线程专门做编码任务。

    class EncoderThread extends Thread {
        @Override
        public void run() {
            long pts = 0;
            super.run();
            while (!isMuxFinish) {
                if (mediaCodec == null) {
                    break;
                }
                // 拿到有空闲的输入缓存区下标
                int inputBufferId = mediaCodec.dequeueInputBuffer(-1);
                if (inputBufferId >= 0) {
                    pts = computePresentationTime();
                    //有效的空的缓存区
                    ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
                    byte[] tempByte = getInputBuffer();
                    if(isMuxFinish){
                        break;
                    }
                    inputBuffer.put(tempByte);
                    //将数据放到编码队列
                    mediaCodec.queueInputBuffer(inputBufferId, 0, tempByte.length, pts, 0);
                }
                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                //得到成功编码后输出的out buffer Id
                int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                if (outputBufferId >= 0) {
                    ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
                    byte[] out = new byte[bufferInfo.size];
                    outputBuffer.get(out);
                    writeBytesToFile(out);
                    outputBuffer.position(bufferInfo.offset);
                    outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
                    // 将编码后的数据写入到MP4复用器
                    mediaMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);
                    //释放output buffer
                    mediaCodec.releaseOutputBuffer(outputBufferId, false);
                } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    MediaFormat mediaFormat = mediaCodec.getOutputFormat();
                    mTrackIndex = mediaMuxer.addTrack(mediaFormat);
                    mediaMuxer.start();
                }
            }

注意事项:
1、dequeueInputBuffer 得到input buffer ,如果返回-1表示没有可用的buffer,参数timeoutUs ,如果传的是0,将会立即返回,传的是-1,将会一直等待。
2、mediaCodec.queueInputBuffer(inputBufferId, 0, tempByte.length, pts, 0);
将装满帧数据的buffer放进输入队列,该方法第四个参数表示时间戳,这个参数最好给下值,否则output后面不会输出,这个是我实践中发现的。
3、MP4合成器mediaMuxer.addTrack(mediaFormat)一定要在INFO_OUTPUT_FORMAT_CHANGED条件满足后再调用,否则不起作用。
4、编码后调用mediaMuxer.writeSampleData(mTrackIndex, outputBuffer, info) 写入一帧数据或者一片数据
5、编码好一帧后记得releaseOutputBuffer

代码下载地址:https://download.csdn.net/download/zxd_android/10584840

GitHub地址
https://github.com/zxd1991/AndroidMedia

猜你喜欢

转载自blog.csdn.net/zxd_Android/article/details/81433531