ffmpeg解码H264裸流为YUV数据


视频画面的传输中,由于原始数据过大,实际传输的数据是已经编码好的数据,一般是H264, 当客户端收到后就需要解码并显示出来。

裸流解析成AVPacket

AVCodecParser

AVCodecParser用于解析输入的数据流并把它分成一帧一帧的压缩编码数据。就像是你把肉塞进火腿,再交给它负责帮你切片,够一个完整的帧就返回给你处理。

使用

int parser_len = 0;

while(parser_len < buffer.length()){
    parser_len += av_parser_parse2(m_pVideoParserContext, m_pVideoDecoder->GetCodecContext(),
                                   &pParsePacket->data, &pParsePacket->size,
                                   (uint8_t *)(buffer.data() + parser_len), buffer.length() - parser_len,
                                   AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);

    if(0 == pParsePacket->size){
        continue;
    }

    switch(m_pVideoParserContext->pict_type){
        case AV_PICTURE_TYPE_I: qDebug("Type:I\t");break;
        case AV_PICTURE_TYPE_P: qDebug("Type:P\t");break;
        case AV_PICTURE_TYPE_B: qDebug("Type:B\t");break;
    default: qDebug("Type:Other\t");break;
}
    //pParsePacket就是一帧的数据包AVPacket,这里可以保存作为录像
    NotifyReceiveVideoPacket(pParsePacket);

    //丢给解码器处理
    m_pVideoDecoder->Decode(pParsePacket);

    av_packet_unref(pParsePacket);
} // parse H264 packet while

每次塞给AVCodecParser的数据是buffer中的数据,其中可能包含多个帧,所以内部会有一个while循环,parser_len是已经解析了的数据的长度,直到解析完一次的buffer,再塞新的数据到buffer继续处理。

解码为AVFrame

if(0 != avcodec_send_packet(pCodecContext, pPacket)){
    qDebug("avcodec_send_packet failed");
    return false;
}

while(0 == avcodec_receive_frame(pCodecContext, pFrame)){
    //pFrame包含了解码后的YUV数据
    ...
}

由于AVFrame是ffmpeg的数据结构,要分别提取出Y、U、V三个通道的数据才能用于显示。

YUV数据格式

RGB来表示颜色大家都不陌生,R(红色)、G(绿色)、B(蓝色),通过这三基色就可以组合成其他需要的颜色。YUV也是一种表示颜色的方式,其中Y(亮度)、U(色度)、V(浓度)。YUV根据采样方式的不同又有YUV444、YUV422、YUV420等多种格式。这里主要介绍YUV420采样格式。

YUV格式

YUV420

YUV420格式是指,每个像素都保留一个Y分量 (亮度全抽样),而在水平方向上,不是每行都取U和V分量,而是一行只取U分量,接着一行就只取V分量,以此重复(即4:2:0, 4:0:2, 4:2:0, 4:0:2 …….)。所以420不是指没有V,而是指一行采样只取U,另一行采样只取V。从4x4矩阵列来看,每4个矩阵点Y区域中,只有一个U和V,所以它们的比值是4:1。

数据大小

对于1个像素的信息存储
  1. RGB格式:R、G、B各占8位,共24位,即3byte
  2. YUV420格式: Y占8位,U、V每4个点共有一个,共8 + 8 / 4 + 8 / 4 = 12位,即3 / 2byte
对于一张图像的数据大小
  1. RGB格式:width height 3byte
  2. YUV420格式:width height 3 / 2 byte

所以采取YUV420来存储图像数据比RGB格式节省了一半的空间。

YUV420P

采样好了数据,在存储YUV数据的时候,对于YUV不同的存储方式又有YUV420P(YV12)、YUV420SP(NV12)等分类。

yv12

nv12

从图上可以看出,YUV420P和NV12的区别就是一个是UV交替存储,一个是先存U再存V。这个在解码的时候就看你解码格式指定的是哪个了,默认解码是YUV420P。

从AVFrame中获取YUV数据

YUV420P在AVFrame中的存储,data[0]存Y分量,data[1]存U分量,data[2]存V分量。其中,图像每一行Y、U、V数据的大小分别是linesize[0]、linesize[1]、linesize[2],除了实际的图像数据,还有一些填充数据是不需要的。填充数据应该是为了内存对齐,保留的话可能会导致花屏,具体可以看这里的分析

所以,获取实际的图像YUV数据代码为:

for(int i = 0; i < pFrame->height; i++)
  memcpy(m_pBuffer + pFrame->width * i, pFrame->data[0]  + pFrame->linesize[0] * i, pFrame->width);

for(int j = 0; j < pFrame->height / 2; j++)
  memcpy(m_pBuffer + pFrame->width / 2 * j + ysize, pFrame->data[1]  + pFrame->linesize[1] * j, pFrame->width / 2);

for(int k = 0; k < pFrame->height / 2; k++)
  memcpy(m_pBuffer + pFrame->width / 2 * k + ysize * 5 / 4, pFrame->data[2]  + pFrame->linesize[2] * k, pFrame->width / 2);

我是直接用一个pBuffer来保存YUV数据了,记住偏移量就行,想分开存也没问题。这里的数据就可以直接交给渲染部分来显示图像了。

参考资料

文章目录
  1. 1. 裸流解析成AVPacket
    1. 1.1. AVCodecParser
    2. 1.2. 使用
  2. 2. 解码为AVFrame
  3. 3. YUV数据格式
    1. 3.1. YUV420
    2. 3.2. 数据大小
      1. 3.2.1. 对于1个像素的信息存储
      2. 3.2.2. 对于一张图像的数据大小
    3. 3.3. YUV420P
  4. 4. 从AVFrame中获取YUV数据
  5. 5. 参考资料
| | 81.7k