camera开发系列之三 相机数据采集硬编码h264

4,537 阅读8分钟

章节

Camera开发系列之一-显示摄像头实时画面

Camera开发系列之二-相机预览数据回调

Camera开发系列之三-相机数据硬编码为h264

Camera开发系列之四-使用MediaMuxer封装编码后的音视频到mp4容器

Camera开发系列之五-使用MediaExtractor制作一个简易播放器

Camera开发系列之六-使用mina框架实现视频推流

Camera开发系列之七-使用GLSurfaceviw绘制Camera预览画面

视频的播放过程可以简单理解为一帧一帧的画面按照时间顺序呈现出来的过程,就像在一个本子的每一页画上画,然后快速翻动的感觉。

notebook

有人就说了,这样不就简单了,直接把camera获取到的数据保存成文件,然后播放就行了。还需要编码不是多此一举么?你还别说,当初我就是这么想的,以至于被公司dalao鄙视了好久,终于知道了知识是多么重要。

为什么要对Camera获取到的数据编码

首先要讲一下为什么需要将camera获取到的yuv数据进行编码,就拿视频直播举例,视频直播非常注重实时性,实时性就是视频图像从产生到消费完成整个过程人感觉不到延迟,只要符合这个要求的视频业务都可以称为实时视频。要实时就要缩短延迟,要缩短延迟就要知道延迟是怎么产生的,视频从产生、编码、传输到最后播放消费,各个环节都会产生延迟,总体归纳为下图:

  1. 成像延迟,一般的技术是毫无为力的,涉及到 CCD 相关的硬件,现在市面上最好的 CCD,一秒钟 50 帧,成像延迟也在 20 毫秒左右,一般的 CCD 只有 20 ~ 25 帧左右,成像延迟 40 ~ 50 毫秒
  2. 编码延迟,和编码器有关系,本篇也主要围绕这个来讲。
  3. 实时互动视频一个关键的环节就是网络传输技术,不管是早期 VoIP,还是现阶段流行的视频直播,其主要手段是通过 TCP/IP 协议来进行通信。但是 IP 网络本来就是不可靠的传输网络,在这样的网络传输视频很容易造成卡顿现象和延迟。

我们知道从camera采集到的图像格式一般是YUV格式,这种格式的存储空间非常大,如果是 1080P 分辨率的图像空间:1920 *1080 * 3 /2= 3MB,就算转换为jpg也需要近200

k大小,如果是每秒12帧也需要近 2.4MB/S的带宽,这带宽在公网上传输是无法接受的。

视频编码器就是为了解决这个问题的,它会根据前后图像的变化做运动检测,通过各种压缩把变化的发送到对方,1080P 进行过 H.264 编码后带宽也就在 200KB/S ~ 300KB/S 左右。

结论:

在实际应用中,并不是每一帧都是完整的画面,因为如果每一帧画面都是完整的图片,那么一个视频的体积就会很大,这样对于网络传输或者视频数据存储来说成本太高,所以通常会对视频流中的一部分画面进行压缩(编码)处理。

硬编码又是什么

所谓的硬编码就是用GPU对视频帧进行编码,相对于软编码来说,硬编码的编码效率天差地别。更高的编码效率就意味着在相同帧率下能够获得更高的分辨率,更佳的画面质量。但是由于硬编码和手机硬件平台相关性较大,目前在部分机型上存在不兼容现象,所以并不能完全抛弃软编码方案而是作为硬编码的补充。

初识mediacodec

Android平台提供了mediacodec类对视频进行硬编码 ,MediaCodec类可用于访问低级媒体编解码器,即编码/解码组件。它是Android低级别多媒体支持基础设施的一部分(通常一起使用MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.)

#####1 初始化

mediacodec的初始化也非常简单,主要是下面三个方法:

static MediaCodec createByCodecName(String name);
static MediaCodec createEncoderByType(String type);
static MediaCodec createDecoderByType(String type);

如果要使用createByCodecName初始化,需要提前知道编解码器的具体名字,第二个方法和第三个方法分别对应编码和解码,根据type创建,开头以video/打头,比如h264就是"video/avc" 。

2 配置编解码参数

初始化之后调用如下方法进行配置:

void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)

先看看第一个参数MediaFormat是干什么的,官方解释:

Encapsulates the information describing the format of media data, be it audio or video.

它是音视频数据格式的简单描述

The format of the media data is specified as string/value pairs.

这个格式有点特殊,是string/value键值对

Keys common to all audio/video formats, all keys not marked optional are mandatory:

键一般是音频/视频这样的格式,所有键都是必须的

简而言之,就是配置一些编解码时的格式,比如帧率,码率,颜色空间等等。

第二个参数是指定要在其上呈现此解码器输出的view,编码可以传入null

第三个参数用于视频加密

第四个是指定CONFIGURE_FLAG_ENCODE将组件配置为编码器 ,如果是解码就传0

好吧,其实第三个是我随便说的,我也不知道具体能干什么,看官方文档上的解释也不详细,哪位知道的观众姥爷可以悄悄咪咪的告诉我,我悄悄的修改。

完整的代码如下:

MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", width, height);
        //颜色空间设置为yuv420sp
 mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
        //比特率,也就是码率 ,值越高视频画面更清晰画质更高
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
        //帧率,一般设置为30帧就够了
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
        //关键帧间隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        try {
            //初始化mediacodec
            mediaCodec = MediaCodec.createEncoderByType("video/avc");
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //设置为编码模式和编码格式
        mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
        mediaCodec.start();
3 开始编码

首先创建一个队列,将获取到的视频数据逐帧的放入:

private static int yuvqueuesize = 10;
private ArrayBlockingQueue<byte[]> YUVQueue = new ArrayBlockingQueue<>(yuvqueuesize);
public void putYUVData(byte[] buffer) {
        if (YUVQueue.size() >= 10) {
            YUVQueue.poll();
        }
        YUVQueue.add(buffer);
    }

在camera数据回调方法中调用:

Camera.setPreviewCallback(new Camera.PreviewCallback() {
                    @Override
                    public void onPreviewFrame(byte[] data, Camera camera) {
                         //给队列丢数据
                         putYUVData(data);
                    }
                });

其次初始化一个输出流,在构造函数中调用,往里面写编码后的数据:

private void createfile(){
        File file = new File(path);
        if(file.exists()){
            file.delete();
        }
        try {
            outputStream = new BufferedOutputStream(new FileOutputStream(file));
        } catch (Exception e){
            e.printStackTrace();
        }
    }

然后新建一个线程从队列里取出帧数据进行编码,需要注意的是我设置的是YUV420SP格式的颜色空间,所以这里要将NV21转换为NV12格式的:

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 (YUVQueue.size() > 0){
                        //从缓冲队列中取出一帧
                        input = YUVQueue.poll();
                        byte[] yuv420sp = new byte[m_width*m_height*3/2];
                        //把待编码的视频帧转换为YUV420格式
                        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();
                                //把转换后的YUV420格式的视频帧放到编码器输入缓冲区中
                                inputBuffer.put(input);
                                mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
                                generateIndex += 1;
                            }

                            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                            int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
                            while (outputBufferIndex >= 0) {
                                ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
                                byte[] outData = new byte[bufferInfo.size];
                                outputBuffer.get(outData);
                                if(bufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG){
                                    configbyte = new byte[bufferInfo.size];
                                    configbyte = outData;
                                }else if(bufferInfo.flags == 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.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();
    }

NV21转NV21:

private void NV21ToNV12(byte[] nv21, byte[] nv12, int width, int height) {
        if (nv21 == null || nv12 == null) return;
        int framesize = width * height;
        int i = 0, j = 0;
        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];
        }
    }

PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。虽然PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。 下面是计算pts的方法:

/**
     * 计算pts
     * @param frameIndex
     * @return
     */
    private long computePresentationTime(long frameIndex) {
        return 132 + frameIndex * 1000000 / framerate;
    }

    public boolean isEncodering(){
        return isRuning;
    }

最后写一个按钮,开启编码线程开始编码:

mBtnEncoder.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //启动线程编码
                StartEncoderThread();
            }
        });

参考链接;

Android摄像头采集的YUV数据旋转与镜像翻转

Android 硬解码MediaCodec配合SurfaceView的踏坑之旅

H.264编码原理以及I帧B帧P帧

理解音视频 PTS 和 DTS

项目地址:camera数据采集硬编码H.264 欢迎start和fork