音视频入门基础:H.264专题(20)——FFmpeg源码中,解码AVCDecoderConfigurationRecord的实现

一、引言

在《音视频入门基础:H.264专题(18)------AVCDecoderConfigurationRecord简介》中对H.264的avcC包装格式以及AVCDecoderConfigurationRecord的概念进行了简介。在FFmpeg源码中,通过ff_h264_decode_extradata函数来解码AVCDecoderConfigurationRecord(又称为extradata)。本文对该函数进行讲解。

二、ff_h264_decode_extradata函数的声明

ff_h264_decode_extradata函数声明在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的头文件libavcodec/h264_parse.h中:

cpp 复制代码
int ff_h264_decode_extradata(const uint8_t *data, int size, H264ParamSets *ps,
                             int *is_avc, int *nal_length_size,
                             int err_recognition, void *logctx);

该函数的作用是:解码avcC包装的H.264码流中的AVCDecoderConfigurationRecord(extradata)。

形参data:输入型参数,指向某个缓冲区。该缓冲区存放AVCDecoderConfigurationRecord,缓冲区的第一个字节必须存放AVCDecoderConfigurationRecord的configurationVersion属性**。**

形参size:输入型参数,形参data指向的缓冲区的大小。

形参ps:输出型参数。执行ff_h264_decode_extradata函数后,ps->sps_list[sps_id]->data指向的缓冲区会存放从SPS中提取出来的每个属性(sps_id是该SPS的seq_parameter_set_id);ps->pps_list[pps_id]->data指向的缓冲区会存放从PPS中提取出来的每个属性。(pps_id是该PPS的pic_parameter_set_id)这样在ff_h264_decode_extradata函数外部就可以通过"C语言的结构体指针强转"(const SPS*)ps->sps_list[sps_id]->data 获取结构体SPS中的成员,从而拿到SPS中的每个属性(可以通过类似方法获取到PPS中的成员)。

形参is_avc:输出型参数,表示是否为H.264。*is_avc的值为1表示是H.264,为0表示不是H.264。

形参nal_length_size:输出型参数,*nal_length_size的值为从AVCDecoderConfigurationRecord中解码出来的lengthSizeMinusOne属性的值加1,即表示存贮每个NALU前面的NALUnitLength所需的空间,单位为字节。

形参err_recognition:输入型参数。表示错误识别,值一般为0。

形参logctx:输入型参数。用来输出日志,可忽略。

返回值:解码AVCDecoderConfigurationRecord成功并且该码流为H.264,返回形参size的值;失败返回一个负数。

三、ff_h264_decode_extradata函数的定义

ff_h264_decode_extradata函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为7.0.1)的源文件libavcodec/h264_parse.c中:

cpp 复制代码
int ff_h264_decode_extradata(const uint8_t *data, int size, H264ParamSets *ps,
                             int *is_avc, int *nal_length_size,
                             int err_recognition, void *logctx)
{
    int ret;

    if (!data || size <= 0)
        return -1;

    if (data[0] == 1) {
        int i, cnt, nalsize;
        const uint8_t *p = data;

        *is_avc = 1;

        if (size < 7) {
            av_log(logctx, AV_LOG_ERROR, "avcC %d too short\n", size);
            return AVERROR_INVALIDDATA;
        }

        // Decode sps from avcC
        cnt = *(p + 5) & 0x1f; // Number of sps
        p  += 6;
        for (i = 0; i < cnt; i++) {
            nalsize = AV_RB16(p) + 2;
            if (nalsize > size - (p - data))
                return AVERROR_INVALIDDATA;
            ret = decode_extradata_ps_mp4(p, nalsize, ps, err_recognition, logctx);
            if (ret < 0) {
                av_log(logctx, AV_LOG_ERROR,
                       "Decoding sps %d from avcC failed\n", i);
                return ret;
            }
            p += nalsize;
        }
        // Decode pps from avcC
        cnt = *(p++); // Number of pps
        for (i = 0; i < cnt; i++) {
            nalsize = AV_RB16(p) + 2;
            if (nalsize > size - (p - data))
                return AVERROR_INVALIDDATA;
            ret = decode_extradata_ps_mp4(p, nalsize, ps, err_recognition, logctx);
            if (ret < 0) {
                av_log(logctx, AV_LOG_ERROR,
                       "Decoding pps %d from avcC failed\n", i);
                return ret;
            }
            p += nalsize;
        }
        // Store right nal length size that will be used to parse all other nals
        *nal_length_size = (data[4] & 0x03) + 1;
    } else {
        *is_avc = 0;
        ret = decode_extradata_ps(data, size, ps, 0, logctx);
        if (ret < 0)
            return ret;
    }
    return size;
}

四、ff_h264_decode_extradata函数的内部实现分析

ff_h264_decode_extradata函数内部,首先判断形参data指向的缓冲区是否为空,或形参data指向的缓冲区的大小是否小于0。如果是,ff_h264_decode_extradata函数返回-1表示解码失败:

cpp 复制代码
    if (!data || size <= 0)
        return -1;

从 《音视频入门基础:H.264专题(18)------AVCDecoderConfigurationRecord简介》中可以知道,AVCDecoderConfigurationRecord的configurationVersion属性值必须为1。如果检测到configurationVersion的值为1(data[0] == 1),则让*is_avc为1,表示该码流为H.264的视频压缩编码格式;如果不为1,让*is_avc为0,表示该码流不是H.264:

cpp 复制代码
    if (data[0] == 1) {
//...
        *is_avc = 1;
//...
    } else {
        *is_avc = 0;
        ret = decode_extradata_ps(data, size, ps, 0, logctx);
        if (ret < 0)
            return ret;
    }

configurationVersion属性的值为1时,代码继续往下执行。从 《音视频入门基础:H.264专题(18)------AVCDecoderConfigurationRecord简介》中可以知道,AVCDecoderConfigurationRecord不包含任何SPS和PPS的极端情况下,AVCDecoderConfigurationRecord也占7个字节(configurationVersion + AVCProfileIndication + profile_compatibility + AVCLevelIndication + reserved + lengthSizeMinusOne + reserved + numOfSequenceParameterSets + numOfPictureParameterSets总共7个字节)。所以判断形参data指向的缓冲区的大小是否小于7字节,如果是,打印"avcC too short",并返回AVERROR_INVALIDDATA表示解码失败:

cpp 复制代码
        if (size < 7) {
            av_log(logctx, AV_LOG_ERROR, "avcC %d too short\n", size);
            return AVERROR_INVALIDDATA;
        }

读取出AVCDecoderConfigurationRecord的numOfSequenceParameterSets属性,得到SPS的数目,赋值给局部变量cnt:

cpp 复制代码
        // Decode sps from avcC
        cnt = *(p + 5) & 0x1f; // Number of sps

让指针p指向AVCDecoderConfigurationRecord的sequenceParameterSetLength属性:

cpp 复制代码
        p  += 6;

通过for循环,根据SPS的数目,读取每个SPS对应的sequenceParameterSetLength和sequenceParameterSetNALUnit:

cpp 复制代码
        for (i = 0; i < cnt; i++) {
            nalsize = AV_RB16(p) + 2;
            if (nalsize > size - (p - data))
                return AVERROR_INVALIDDATA;
            ret = decode_extradata_ps_mp4(p, nalsize, ps, err_recognition, logctx);
            if (ret < 0) {
                av_log(logctx, AV_LOG_ERROR,
                       "Decoding sps %d from avcC failed\n", i);
                return ret;
            }
            p += nalsize;
        }

上述代码块中,首先通过AV_RB16宏定义读取AVCDecoderConfigurationRecord中的sequenceParameterSetLength,把该值加2赋值给局部变量nalsize。所以nalsize的值为该SPS的长度加上2字节,也就是包含NALU Header的该SPS的实际NALU数据的长度 + 存贮sequenceParameterSetLength所需的空间,以字节为单位。关于AV_RB16宏定义的用法可以参考:《FFmpeg源码:AV_RB32、AV_RB16、AV_RB8宏定义分析》:

cpp 复制代码
            nalsize = AV_RB16(p) + 2;

decode_extradata_ps_mp4函数内部调用了decode_extradata_ps函数:

cpp 复制代码
static int decode_extradata_ps_mp4(const uint8_t *buf, int buf_size, H264ParamSets *ps,
                                   int err_recognition, void *logctx)
{
    int ret;

    ret = decode_extradata_ps(buf, buf_size, ps, 1, logctx);

//...
}

而decode_extradata_ps函数内部,首先通过ff_h2645_packet_split函数将一个个NALU的NALU Header、EBSP、RBSP和SODB从avcC包装的H.264码流中提取出来,然后通过ff_h264_decode_seq_parameter_set函数解码SPS,把SPS中的每个属性提取出来。关于ff_h2645_packet_split函数的用法可以参考:《音视频入门基础:H.264专题(6)------FFmpeg源码:从H.264码流中提取NALU Header、EBSP、RBSP和SODB》,关于ff_h264_decode_seq_parameter_set函数的用法可以参考:《音视频入门基础:H.264专题(10)------FFmpeg源码中,存放SPS属性的结构体和解码SPS的函数分析》:

cpp 复制代码
static int decode_extradata_ps(const uint8_t *data, int size, H264ParamSets *ps,
                               int is_avc, void *logctx)
{
    H2645Packet pkt = { 0 };
    int i, ret = 0;

    ret = ff_h2645_packet_split(&pkt, data, size, logctx, is_avc, 2, AV_CODEC_ID_H264, 1, 0);
    if (ret < 0) {
        ret = 0;
        goto fail;
    }

    for (i = 0; i < pkt.nb_nals; i++) {
        H2645NAL *nal = &pkt.nals[i];
        switch (nal->type) {
        case H264_NAL_SPS: {
            GetBitContext tmp_gb = nal->gb;
            ret = ff_h264_decode_seq_parameter_set(&tmp_gb, logctx, ps, 0);
            if (ret >= 0)
                break;
            av_log(logctx, AV_LOG_DEBUG,
                   "SPS decoding failure, trying again with the complete NAL\n");
            init_get_bits8(&tmp_gb, nal->raw_data + 1, nal->raw_size - 1);
            ret = ff_h264_decode_seq_parameter_set(&tmp_gb, logctx, ps, 0);
            if (ret >= 0)
                break;
            ret = ff_h264_decode_seq_parameter_set(&nal->gb, logctx, ps, 1);
            if (ret < 0)
                goto fail;
            break;
        }
        case H264_NAL_PPS:
            ret = ff_h264_decode_picture_parameter_set(&nal->gb, logctx, ps,
                                                       nal->size_bits);
            if (ret < 0)
                goto fail;
            break;
        default:
            av_log(logctx, AV_LOG_VERBOSE, "Ignoring NAL type %d in extradata\n",
                   nal->type);
            break;
        }
    }

fail:
    ff_h2645_packet_uninit(&pkt);
    return ret;
}

我们回到ff_h264_decode_extradata函数中,代码继续往下执行,通过for循环,根据PPS的数目,读取每个PPS对应的pictureParameterSetLength和pictureParameterSetNALUnit:

cpp 复制代码
        // Decode pps from avcC
        cnt = *(p++); // Number of pps
        for (i = 0; i < cnt; i++) {
            nalsize = AV_RB16(p) + 2;
            if (nalsize > size - (p - data))
                return AVERROR_INVALIDDATA;
            ret = decode_extradata_ps_mp4(p, nalsize, ps, err_recognition, logctx);
            if (ret < 0) {
                av_log(logctx, AV_LOG_ERROR,
                       "Decoding pps %d from avcC failed\n", i);
                return ret;
            }
            p += nalsize;
        }
        // Store right nal length size that will be used to parse all other nals
 

获取AVCDecoderConfigurationRecord中的lengthSizeMinusOne属性,把该值 + 1赋值给*nal_length_size。所以*nal_length_size的值为存贮NALUnitLength所需的空间,单位为字节:

cpp 复制代码
        // Store right nal length size that will be used to parse all other nals
        *nal_length_size = (data[4] & 0x03) + 1;

解码AVCDecoderConfigurationRecord成功并且该码流为H.264,返回形参size的值:

cpp 复制代码
    return size;

五、总结

1.FFmpeg源码中,通过ff_h264_decode_extradata函数来解码avcC包装的H.264码流中的AVCDecoderConfigurationRecord。

2.由于FFmpeg源码中,解码AVCDecoderConfigurationRecord的函数的名称为"ff_h264_decode_extradata"。所以很多人会把AVCDecoderConfigurationRecord称作"extradata"。AVCDecoderConfigurationRecord是ISO/IEC提供的H.264官方文档《H.264-AVC-ISO_IEC_14496-15》中的官方说法,而"extradata"是FFmpeg源码对AVCDecoderConfigurationRecord的称呼。

相关推荐
Hello.Reader7 小时前
FFmpeg 深度教程音视频处理的终极工具
ffmpeg·1024程序员节
runing_an_min7 小时前
ffmpeg视频滤镜:添加边框-drawbox
ffmpeg·音视频·边框·drawbox
wang_chao1189 小时前
FFMPEG+Qt 实时显示本机USB摄像头1080p画面以及同步录制mp4视频
qt·ffmpeg·音视频
EasyCVR12 小时前
宇视设备视频平台EasyCVR视频融合平台果园/鱼塘/养殖场/菜园有电没网视频监控方案
科技·音视频·视频监控·监控视频接入
ZPeng_csdn15 小时前
视频协议与封装格式
音视频
ai产品老杨15 小时前
深度学习模型量化原理
开发语言·人工智能·python·深度学习·安全·音视频
runing_an_min16 小时前
ffmpeg视频滤镜:网格-drawgrid
ffmpeg·音视频·网格·drawgrid
间彧1 天前
FFmpeg推流器
ffmpeg
北京同三维影音设备1 天前
同三维TK101控制键盘连接和使用视频说明书:控制键盘
计算机外设·音视频
生命几十年3万天1 天前
ffmpeg常用命令
ffmpeg