MediaCodec视频编码H264流程及参考demo

发布于:2022-11-09 ⋅ 阅读:(10) ⋅ 点赞:(0) ⋅ 评论:(0)

        关于MediaCodec的简介和生命周期的讲解,在上一篇文章 MediaCodec视频解码流程详解及参考demo中已经阐述。

        本文则讲解使用MediaCodec进行视频编码的流程,主要通过一个例子,从摄像头采集数据,并将数据编码为h264文件。

一、MediaCodec简介及生命周期

        在上面提到的文章中已阐述。

二、MediaCodec编码流程

        下面将讲解使用mediacodec编码h264文件的API使用流程。本文主要是采用同步的编码方式。

        编码流程工作模型,与解码是一样:

1、创建mediacodec并初始化

        可通过createDecoderByType来创建mediacodec:

// 创建 MediaCodec,此时是 Uninitialized 状态
mediaCodec = MediaCodec.createEncoderByType("video/avc");

        上面表示创建了一个编码器,但是还需要对这个编码器进行一些配置,包括格式,码率帧率等等,如下:

MediaFormat mediaFormat = MediaFormat.createVideoFormat("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);                                                          //I 帧间隔

        设置好相关配置格式后,调用configure进行配置。

// 调用 configure 进入 Configured 状态
mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

        配置好后,可以调用start启动编码工作,进入Executing状态。不过一开始调用start后是先进入Executing的子状态Flushed状态,等后面编码线程启动后有数据了,才真正进入Running状态:

// 调用 start 进入 Executing 状态,开始准备编解码工作
mediaCodec.start();

2、编码线程

        编码线程是真正的编码过程,本例子是将数据编码为h264。

        在启动线程之前,先创建个文件,用来保存编码后的h264数据,如下:

private BufferedOutputStream outputStream;
FileOutputStream outStream;
private void createfile(String path){
    File file = new File(path);
    Log.d(TAG,"createfile path = "+path);
    if(file.exists()){
        file.delete();
    }
    try {
        outputStream = new BufferedOutputStream(new FileOutputStream(file));
    } catch (Exception e){
        e.printStackTrace();
    }
}

        获取可用的输入缓冲区的索引:

int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);

        获取输入缓冲区:

// 输入缓冲区
ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();

        从输入缓冲区队列中取出可用缓冲区,并填充数据:

// 从输入缓冲区队列中取出可用缓冲区,并填充数据
if (inputBufferIndex >= 0) {
    // 计算时间戳
    pts = computePresentationTime(generateIndex);
    ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
    inputBuffer.clear();
    inputBuffer.put(input);
    mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
    generateIndex += 1;
}

        创建输出缓冲区:

//输出缓冲区
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();

        获取已成功编解码的输出缓冲区的索引:

MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);

        从输出缓冲区队列中拿到编解码后的内容,配置相关内容,包括SPS,PPS等,然后进行相应操作(这里是写入output h264文件)后释放,供下一次使用:

// 从输出缓冲区队列中拿到编解码后的内容,进行相应操作(这里是写入output h264文件)后释放,供下一次使用
while (outputBufferIndex >= 0) {
    ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
    byte[] outData = new byte[bufferInfo.size];
    outputBuffer.get(outData);
    // flags 判断
    if(bufferInfo.flags == 2){   // 配置相关的内容,也就是 SPS,PPS
        configbyte = new byte[bufferInfo.size];
        configbyte = outData;
    }else if(bufferInfo.flags == 1){   //关键帧
        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{      // 非关键帧和SPS、PPS,直接写入文件,可能是B帧或者P帧
        outputStream.write(outData, 0, outData.length);
    }
    mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
    outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
}

        完整编码线程代码如下:

//编码子线程
public void StartEncoderThread(){
    Thread EncoderThread = new Thread(new Runnable() {

        @SuppressLint("NewApi")
        @Override
        public void run() {
            isRuning = true;
            byte[] input = null;
            long pts =  0;
            long generateIndex = 0;

            while (isRuning) {
                if (MainActivity.YUVQueue.size() >0){
                    input = MainActivity.YUVQueue.poll();
                    byte[] yuv420sp = new byte[m_width*m_height*3/2];
                    NV21ToNV12(input,yuv420sp,m_width,m_height);
                    input = yuv420sp;
                }
                if (input != null) {
                    try {
                        long startMs = System.currentTimeMillis();
                        // 输入缓冲区
                        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.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
                            generateIndex += 1;
                        }

                        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                        // 从输出缓冲区队列中拿到编解码后的内容,进行相应操作(这里是写入output h264文件)后释放,供下一次使用
                        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                        while (outputBufferIndex >= 0) {
                            //Log.d(TAG, "Get H264 Buffer Success! flag = "+bufferInfo.flags+",pts = "+bufferInfo.presentationTimeUs+"");
                            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                            byte[] outData = new byte[bufferInfo.size];
                            outputBuffer.get(outData);
                            // flags 判断
                            if(bufferInfo.flags == 2){   // 配置相关的内容,也就是 SPS,PPS
                                configbyte = new byte[bufferInfo.size];
                                configbyte = outData;
                            }else if(bufferInfo.flags == 1){   //关键帧
                                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{      // 非关键帧和SPS、PPS,直接写入文件,可能是B帧或者P帧
                                outputStream.write(outData, 0, outData.length);
                            }
                            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
                            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                        }

                    } catch (Throwable t) {
                        t.printStackTrace();
                    }
                } else {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    });
    EncoderThread.start();
}

3、编码结束关闭回收

        完成编码后,对相关内存回收和线程资源等关闭处理:

private void StopEncoder() {
    try {
        // 调用 stop 方法进入 Uninitialized 状态
        mediaCodec.stop();
        // 调用 release 方法释放,结束操作
        mediaCodec.release();
    } catch (Exception e){
        e.printStackTrace();
    }
}

public void StopThread(){
    isRuning = false;
    try {
        StopEncoder();
        outputStream.flush();
        outputStream.close();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

        至此,整个编码为h264文件的过程就结束了。

三、demo运行

        本人写了一个最简单的demo,使用mediacodec将摄像头采集到的数据编码为h264文件,采用同步编码的方式。

        关于编码后的h264文件路径,在代码中可以自己修改,如下位置:

        MainActivity.java下的:

        运行demo:

         点击START打开摄像头,进行录像采集摄像头数据:

         录制得差不多了,就点击STOP,完成编码,并提示编码后的h264文件的存放位置。

         完整例子已经放到github上,如下:

https://github.com/weekend-y/mediacodec_demo/tree/master/MediaCodec_EncodeH264