音视频积累

MediaCodec 硬编码

2021-06-23  本文已影响0人  一枚懒人

一:基本流程

MediaCodec编码视屏主要分三步:

1:打开相机,接收相机拿到的数据

2:初始化MediaCodec,将基本配置配给meidiacodec

3:开始编解码,建编解码好的数据进行保存(保存成文件/推送到网络)

本节主要介绍下第二步和第三步

二:详细步骤

2.1 相机操作

打开相机拿到数据,目前Android 提供的相机API主要有 Camera1 Camera2和CameraX,本Demo主要使用使用简单的Camera1取数据,主要逻辑即初始化camera和开始进入预览录制状态,如下:

private void initCamera() {
        mCamera = Camera.open(mCameraId);
        Camera.Parameters parameters = mCamera.getParameters();
        int[] max = determineMaximumSupportedFramerate(parameters);
        Camera.CameraInfo camInfo = new Camera.CameraInfo();
        Camera.getCameraInfo(mCameraId, camInfo);
        int cameraRotationOffset = camInfo.orientation;
        int rotate = (360 + cameraRotationOffset - getDgree()) % 360;
        parameters.setRotation(rotate);
        parameters.setPreviewFormat(ImageFormat.NV21);
        List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
        parameters.setPreviewSize(width, height);
        parameters.setPreviewFpsRange(max[0], max[1]);
        mCamera.setParameters(parameters);
        int displayRotation;
        displayRotation = (cameraRotationOffset - getDgree() + 360) % 360;
        mCamera.setDisplayOrientation(displayRotation);
        try {
            mCamera.setPreviewDisplay(mSurfaceHodler);
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, e.toString());
        }
    }

private void startRecord() {
        int colorFormat = mCamera.getParameters().getPreviewFormat();
        Camera.Size previewSize = mCamera.getParameters().getPreviewSize();
        int size = previewSize.width * previewSize.height *
                ImageFormat.getBitsPerPixel(colorFormat) / 8;
        byte[] previewData = new byte[size];
        mCamera.addCallbackBuffer(previewData);
        mCamera.setPreviewCallback(callback);
    }
2.2 mediaCodec 初始化
private void initMediaCodec() {
        framerate = 15;
        bitrate = 2 * width * height * framerate / 20;
        try {
//           创建mediaCodec编码器
            mMediaCodec = MediaCodec.createEncoderByType("video/avc");
            // 创建 MediaFormat,主要配置的就是生成的视频的配置
            MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);    //码率
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
           //帧率
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
            //颜色空间 420_888
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatYUV420Planar);
            //I帧间隔
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
            //将mediacodec 设置进入configure状态
            mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mMediaCodec.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
2.3 配置编码
//同步方式获取编码数据
      private void h264EncodeSysn(byte[] data, Camera.Size previewSize, byte[] nv12Data) {
            try {
                //首先获取codec的入队的空buffer,获取到的是ID
                int index = mMediaCodec.dequeueInputBuffer(5000000l);
                if (index < 0) {
                    Log.e(TAG, "mMediaCodec no empty buffer");
                    return;
                }
                //将camera1出来的nv21数据转换成nv12数据
                nv12Data = NV21ToNV12(data, previewSize.width, previewSize.height);
                Log.e(TAG, "onPreviewFrame index is :" + index);
                //根据获取的index获取ByteBuffer
                ByteBuffer inputBuffer = mMediaCodec.getInputBuffer(index);
                //将nv12数据送到空ByteBuffer中
                inputBuffer.clear();
                inputBuffer.put(nv12Data);
                //将填满nv12数据的ButeBUffer送到编码器
                mMediaCodec.queueInputBuffer(index, 0, inputBuffer.position(),
                        System.nanoTime() / 1000, 0);
                Log.e(TAG, "onPreviewFrame queueInputBuffer end");
                //获取承载编码好数据的BufferInfo
                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                //获取承载编码好数据的BufferInfo的index
                int outIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                Log.e(TAG, "onPreviewFrame dequeueOutputBuffer end ,outIndex is " + outIndex);
                
                while (outIndex >= 0) {
                    Log.e(TAG, "onPreviewFrame begin outIndex is " + outIndex);
                    //根据index获取承载编码好数据的ByteBuffer
                    ByteBuffer outputByteBuffer = mMediaCodec.getOutputBuffer(outIndex);
                    byte[] outByte = new byte[bufferInfo.size];
                    outputByteBuffer.get(outByte);
                    //保存到文件264码流
                    if (outByte[0] == 0 && outByte[1] == 0 && outByte[2] == 0 && outByte[3] == 1 && outByte[4] == 103) {
                        //保存pps和sps
                        mPpsSps = outByte;
                    } else if (outByte[0] == 0 && outByte[1] == 0 && outByte[2] == 0 && outByte[3] == 1 && outByte[4] == 101) {
                        //I帧前面加上sps 和pps
                        byte[] iframe = new byte[mPpsSps.length + bufferInfo.size];
                        System.arraycopy(mPpsSps, 0, iframe, 0, mPpsSps.length);
                        System.arraycopy(outByte, 0, iframe, mPpsSps.length, outByte.length);
                        outByte = iframe;
                    }
                    //将数据保存到文件中
                    Util.save(outByte, 0, outByte.length, path, true);
                    //将用完的编码好的Buffer还给编码器
                    mMediaCodec.releaseOutputBuffer(index, false);
                    outIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                    Log.e(TAG, "onPreviewFrame end outIndex is " + outIndex);
                }
            } catch (Exception e) {
                Log.e(TAG, "exception:" + e.toString());
            } finally {
                //归还相机承载数据的buffer
                mCamera.addCallbackBuffer(nv12Data);
            }
        }
2.4 保存文件
 /**
     * 保存数据到本地
     *
     * @param buffer 要保存的数据
     * @param offset 要保存数据的起始位置
     * @param length 要保存数据长度
     * @param path   保存路径
     * @param append 是否追加
     */ 
public static void save(byte[] buffer, int offset, int length, String path, boolean append) {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(path, append);
            fos.write(buffer, offset, length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();[图片上传失败...(image-5f3222-1624425303405)]

![h264.png](https://img.haomeiwen.com/i11701169/1b00fae73db757cf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

![h264裸流信息.png](https://img.haomeiwen.com/i11701169/9627faa252fbaa78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.flush();
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
2.5 H264码流分析

经过上面mediacodec编码之后,保存的文件就可以用来保存成文件了,此时距离保存成MP4/AVI等其他格式的文件只剩下一步了,但是暂时先不管,我们先单纯的分析下H264 码流。首先使用16进制阅读器查看H264文件,如下

简单介绍下,H264 裸流由一个接一个NALU单元组成,一个接一个的NALU单元由startcode分割,startcode一般是:

0x000001(3Byte)或者0x00000001(4Byte),如果NALU对应的Slice为一帧的开始就用0x00000001,否则就用0x000001,如上图所示的startCode就是0x00 00 00 01,一般进行解码时就是从MP4或者其他视频封装格式中分解出来NALU单元,在从单元中分解出来各个信息进行解读,最后解析出来一帧一帧的数据,其中关键配置信息包括SPS PPS

I帧等,均由 startcode后面前2Byte的数据保存,如00 00 00 01第一个之后是67,67的二进制是 0 11 00111,对应包含的信息即:

image.png

知道裸流之后就知道 为了视频从哪一帧开始播放都可以播放增加了 一个操作就是在每一个I帧编码前就编码一组SPS和PPS,这样保证编码器至少隔一段时间差就可以读取到有效配置信息进行配置

image.png
image.png
上一篇下一篇

猜你喜欢

热点阅读