h264解码

359 阅读4分钟

h264编码

h264的压缩方法

  • 1、分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多

  • 2、定义帧:将每组内各帧图像定义为三种类型,即 I 帧、B帧和P帧

  • 3、预测帧:以帧作为基础帧,以帧预测P帧,再由 I 帧和P帧预测B帧

  • 4、数据传输:最后将 I 帧数据与预测的差值信息进行存储和传输

帧内(Intraframe)

压缩也称为空间压缩(Spatial compression)。当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,因此可以独立的解码、显示。帧内压缩一般达不到很高的压缩,跟编码jpeg差不多。

帧间(Interframe)

压缩的原理是:相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点,也即连续的视频及其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减少压缩比。帧间压缩也称为时间压缩,它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的。帧差值(Frame differencing)算法是一种典型的时间压缩发,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。

有损(Lossy)

压缩和无损(Lossy less)压缩。无损压缩也即压缩前和解压缩后的数据完全一致。多数的无损压缩都采用RLE行程编码算法。有损压缩意味着解压缩后的数据与压缩前的数据不一致。在压缩的过程中要丢失一些人眼和耳朵所不敏感的图像或音频信息,而且丢失的信息不可恢复。几乎所有高压缩的算法都采用有损压缩,这样才能达到低数据率的目标。丢失的数据率与压缩比有关,压缩比越小,丢失的数据越多,解压缩后的效果一般越差。此外,某些有损压缩算法采用多次重复压缩的方式,这样还会引起额外的数据丢失

硬解码流程

mediaCodec 是android系统提供的用于操作dps解码芯片的上层接口。它是流水线工作模式。

创建 - 配置 - 启动 - 解码 - 结束

mediaCodec创建

创建mediaCodec的解码器,解码器根据码流的格式可以有很多种,最常用的就是h264码流,在android定义为video/avc

mediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);

mediaCodec配置

配置主要为了告知解码器一些关于流的信息,比如宽高,帧率等。最重要的是如果要渲染到页面中,需要提供surface,这个surface一般是由surfaceView初始化获得。

MediaFormat mediaformat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,64, 368);
//设置关键帧间隔
mediaformat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
//加入的surface表示后续的内容输出到surface中,flag=0,如果是编码则需要设置flag为编码
mediaCodec.configure(mediaformat, surface, null, 0);

启动

mediaCodec.start();

解码流程

  • 从MediaCodec获取ByteBuffer
int bufferIndex =  mediaCodec.dequeueInputBuffer(10000);
if (bufferIndex >= 0){
    //获取这块空闲的buffer
    ByteBuffer byteBuffer = mediaCodec.getInputBuffer(bufferIndex);
}
  • 从视频中获取每帧的数据,填充ByteBuffer,提交给mediaCodec
byte[] bytes = getBytes(path);
int nextFrameStart = findByFrame(bytes , startIndex + 2, bytes.length);
int size = nextFrameStart -startIndex;
byteBuffer.put(bytes , startIndex , size);
mediaCodec.queueInputBuffer(bufferIndex ,0 , size , 0 , 0);
  • 获取输出信息,输出的yuv数据信息保存在BufferInfo中
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int outIndex = mediaCodec.dequeueOutputBuffer(info , 1100);
if (outIndex >= 0){
    //控制播放速度
    Thread.sleep(33);
    //将解码好的数据渲染到surface中
    mediaCodec.releaseOutputBuffer(outIndex , true);
}

整体过程

byte[] bytes = getBytes(path);
int startIndex = 0;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true){
    int nextFrameStart = findByFrame(bytes , startIndex + 2, bytes.length);
    int size = nextFrameStart -startIndex;
    if (size > 0 ){
        //从mediaCodec中找到一块空闲的buffer,mediaCodec是为所有的app服务的,因此不能通过回调的形式传回处理好的数据
        int bufferIndex =  mediaCodec.dequeueInputBuffer(10000);
        if (bufferIndex >= 0){
            //获取这块空闲的buffer
            ByteBuffer byteBuffer = mediaCodec.getInputBuffer(bufferIndex);
            Log.i("hucaihua", "decodeH264: 输入  "+size);
            //往buffer中放入要解码的数据
            byteBuffer.put(bytes , startIndex , size);
            //解码不需要设置pts,因为文件中的顺序,刚好就是解码顺序。
            //告知mediaCodec已经放置好数据,可以开始解码了
            mediaCodec.queueInputBuffer(bufferIndex ,0 , size , 0 , 0);
            startIndex = nextFrameStart;

        }else {
            Log.e("hucaihua" , "没有合适的Buffer");
        }
    }

    // info用来装decode出来的数据的信息,它是远远大于我们输入的信息的。
    // 从mediaCodec中获取输出数据,输出数据的信息与输入数据信息不一样,因此mediaCodec通过BufferInfo来告知我们
    int outIndex = mediaCodec.dequeueOutputBuffer(info , 1100);
    if (outIndex >= 0){
        //控制播放速度
        Thread.sleep(33);
        //将解码好的数据渲染到surface中
        mediaCodec.releaseOutputBuffer(outIndex , true);
    }
}

// 0x 00 00 00 01 或者 0x 00 00 01
private int findByFrame( byte[] bytes, int start, int totalSize) {
    for (int i = start; i <= totalSize-4; i++) {
        if (((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x00) && (bytes[i + 3] == 0x01))
                ||((bytes[i] == 0x00) && (bytes[i + 1] == 0x00) && (bytes[i + 2] == 0x01))) {
            return i;
        }
    }
    return -1;
}

结束

mediaCodec.stop();