音视频入门基础:H.264专题(16)——FFmpeg源码中,判断某文件是否为H.264裸流文件的实现

=================================================================

音视频入门基础:H.264专题系列文章:

音视频入门基础:H.264专题(1)------H.264官方文档下载

音视频入门基础:H.264专题(2)------使用FFmpeg命令生成H.264裸流文件

音视频入门基础:H.264专题(3)------EBSP, RBSP和SODB

音视频入门基础:H.264专题(4)------NALU Header:forbidden_zero_bit、nal_ref_idc、nal_unit_type简介

音视频入门基础:H.264专题(5)------FFmpeg源码中 解析NALU Header的函数分析

音视频入门基础:H.264专题(6)------FFmpeg源码:从H.264码流中提取NALU Header、EBSP、RBSP和SODB

音视频入门基础:H.264专题(7)------FFmpeg源码中 指数哥伦布编码的解码实现

音视频入门基础:H.264专题(8)------H.264官方文档的描述符

音视频入门基础:H.264专题(9)------SPS简介

音视频入门基础:H.264专题(10)------FFmpeg源码中,存放SPS属性的结构体和解码SPS的函数分析

音视频入门基础:H.264专题(11)------计算视频分辨率的公式

音视频入门基础:H.264专题(12)------FFmpeg源码中通过SPS属性计算视频分辨率的实现

音视频入门基础:H.264专题(13)------FFmpeg源码中通过SPS属性获取视频色彩格式的实现

音视频入门基础:H.264专题(14)------计算视频帧率的公式

音视频入门基础:H.264专题(15)------FFmpeg源码中通过SPS属性获取视频帧率的实现

音视频入门基础:H.264专题(16)------FFmpeg源码中,判断某文件是否为H.264裸流文件的实现

音视频入门基础:H.264专题(17)------FFmpeg源码获取H.264裸流文件信息(视频压缩编码格式、色彩格式、视频分辨率、帧率)的总流程

=================================================================

一、引言

通过FFmpeg命令可以判断出某个文件是否为AnnexB格式的H.264裸流:

所以FFmpeg是怎样判断出某个文件是否为AnnexB格式的H.264裸流呢?它内部其实是通过h264_probe函数来判断的。从文章《FFmpeg源码:av_probe_input_format3函数分析》中我们可以知道:

FFmpeg中实现容器格式检测的函数是av_probe_input_format3函数,其内部通过循环while ((fmt1 = av_demuxer_iterate(&i))) 拿到所有容器格式对应的AVInputFormat结构,然后通过score = fmt1->read_probe(&lpd)语句执行不同容器格式对应的解析函数,根据是否能被解析,以及匹配程度,来判断出这是哪种容器格式。而AnnexB格式的H.264裸流对应的解析函数就是h264_probe函数。

二、h264_probe函数的定义

h264_probe函数定义在FFmpeg源码(本文演示用的FFmpeg源码版本为5.0.3)的源文件libavformat/h264dec.c中:

cpp 复制代码
#define MAX_SPS_COUNT          32
#define MAX_PPS_COUNT         256

static int h264_probe(const AVProbeData *p)
{
    uint32_t code = -1;
    int sps = 0, pps = 0, idr = 0, res = 0, sli = 0;
    int i, ret;
    int pps_ids[MAX_PPS_COUNT+1] = {0};
    int sps_ids[MAX_SPS_COUNT+1] = {0};
    unsigned pps_id, sps_id;
    GetBitContext gb;

    for (i = 0; i + 2 < p->buf_size; i++) {
        code = (code << 8) + p->buf[i];
        if ((code & 0xffffff00) == 0x100) {
            int ref_idc = (code >> 5) & 3;
            int type    = code & 0x1F;
            static const int8_t ref_zero[] = {
                 2,  0,  0,  0,  0, -1,  1, -1,
                -1,  1,  1,  1,  1, -1,  2,  2,
                 2,  2,  2,  0,  2,  2,  2,  2,
                 2,  2,  2,  2,  2,  2,  2,  2
            };

            if (code & 0x80) // forbidden_bit
                return 0;

            if (ref_zero[type] == 1 && ref_idc)
                return 0;
            if (ref_zero[type] == -1 && !ref_idc)
                return 0;
            if (ref_zero[type] == 2) {
                if (!(code == 0x100 && !p->buf[i + 1] && !p->buf[i + 2]))
                    res++;
            }

            ret = init_get_bits8(&gb, p->buf + i + 1, p->buf_size - i - 1);
            if (ret < 0)
                return 0;

            switch (type) {
            case 1:
            case 5:
                get_ue_golomb_long(&gb);
                if (get_ue_golomb_long(&gb) > 9U)
                    return 0;
                pps_id = get_ue_golomb_long(&gb);
                if (pps_id > MAX_PPS_COUNT)
                    return 0;
                if (!pps_ids[pps_id])
                    break;

                if (type == 1)
                    sli++;
                else
                    idr++;
                break;
            case 7:
                skip_bits(&gb, 14);
                if (get_bits(&gb, 2))
                    return 0;
                skip_bits(&gb, 8);
                sps_id = get_ue_golomb_long(&gb);
                if (sps_id > MAX_SPS_COUNT)
                    return 0;
                sps_ids[sps_id] = 1;
                sps++;
                break;
            case 8:
                pps_id = get_ue_golomb_long(&gb);
                if (pps_id > MAX_PPS_COUNT)
                    return 0;
                sps_id = get_ue_golomb_long(&gb);
                if (sps_id > MAX_SPS_COUNT)
                    return 0;
                if (!sps_ids[sps_id])
                    break;
                pps_ids[pps_id] = 1;
                pps++;
                break;
            }
        }
    }
    ff_tlog(NULL, "sps:%d pps:%d idr:%d sli:%d res:%d\n", sps, pps, idr, sli, res);

    if (sps && pps && (idr || sli > 3) && res < (sps + pps + idr))
        return AVPROBE_SCORE_EXTENSION + 1;  // 1 more than .mpg

    return 0;
}

其作用就是检测某个文件是否为AnnexB格式的H.264裸流文件。

形参pd:输入型参数,为AVProbeData类型的指针。

AVProbeData结构体声明在libavformat/avformat.h中:

cpp 复制代码
/**
 * This structure contains the data a format has to probe a file.
 */
typedef struct AVProbeData {
    const char *filename;
    unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */
    int buf_size;       /**< Size of buf except extra allocated bytes */
    const char *mime_type; /**< mime_type, when known. */
} AVProbeData;

p->filename为:需要被推测格式的文件的路径。

p->buf:指向"存放从路径为p->filename的文件中读取出来的二进制数据"的缓冲区。

p->buf_size:缓冲区p->buf的大小,单位为字节。注:FFmpeg判断某个文件是否为H.264裸流时不会读取完整个H.264裸流文件,只会读取它前面的一部分,比如最开始的2048个字节。只要根据前面的这些字节就足够判断出它的格式了,所以p->buf_size的值一般就是2048。

p->mime_type:一般为NULL,可忽略。

返回值:返回一个类型为整形的分值。返回0表示该文件完全不符合AnnexB格式的H.264裸流文件的格式。返回AVPROBE_SCORE_EXTENSION + 1(也就是51)表示该文件比较符合AnnexB格式的H.264裸流文件的格式,但还需要在av_probe_input_format3函数中执行其它容器格式对应的解析函数来进行对比,最终通过最高分来确定到底是哪种容器格式。

三、h264_probe函数的内部实现原理

h264_probe函数中,首先通过下面语句,让变量code被赋值为十进制的4294967295,也就是十六进制的0xFFFFFFFF。(具体可以参考:《为什么有符号数0XFFFF FFFF代表-1?》):

cpp 复制代码
uint32_t code = -1;

然后通过下面语句初始化局部变量。其中变量sps表示该路H.264码流中sps(Sequence parameter set)的数量;pps表示该路H.264码流中pps(Picture parameter set)的数量;变量idr表示该路H.264码流中IDR SLICE(Coded slice of an IDR picture)的数量;变量sli表示该路H.264码流中非IDR SLICE(Coded slice of a non-IDR picture)的数量:

cpp 复制代码
int sps = 0, pps = 0, idr = 0, res = 0, sli = 0;

检测到0x000001或0x00000001的起始码时,意味读取到了某个NALU的开头,将其NALU Header中的nal_ref_idc和nal_unit_type读取出来,分别存贮到变量ref_idc和变量type中:

cpp 复制代码
    for (i = 0; i + 2 < p->buf_size; i++) {
        code = (code << 8) + p->buf[i];
        if ((code & 0xffffff00) == 0x100) {
            int ref_idc = (code >> 5) & 3;
            int type    = code & 0x1F;
       //...

从文章《音视频入门基础:H.264专题(4)------NALU Header:forbidden_zero_bit、nal_ref_idc、nal_unit_type简介》中,可以知道,NALU Header中的forbidden_zero_bit 的值应为0。所以如果检测到forbidden_zero_bit 的值为1,h264_probe函数返回0,表示该文件完全不符合H.264裸流文件的格式:

cpp 复制代码
            if (code & 0x80) // forbidden_bit
                return 0;

我们再来看看下面语句是什么意思:

cpp 复制代码
            static const int8_t ref_zero[] = {
                 2,  0,  0,  0,  0, -1,  1, -1,
                -1,  1,  1,  1,  1, -1,  2,  2,
                 2,  2,  2,  0,  2,  2,  2,  2,
                 2,  2,  2,  2,  2,  2,  2,  2
            };

            //...

            if (ref_zero[type] == 1 && ref_idc)
                return 0;
            if (ref_zero[type] == -1 && !ref_idc)
                return 0;
            if (ref_zero[type] == 2) {
                if (!(code == 0x100 && !p->buf[i + 1] && !p->buf[i + 2]))
                    res++;
            }

语句:

cpp 复制代码
if (ref_zero[type] == 1 && ref_idc)
                return 0;

的意思是:根据H.264官方文档《T-REC-H.264-202108-I!!PDF-E.pdf》第65页中的表格,下面表格中的红框里面的NALU重要性低,它们的nal_ref_idc值应为0。如果它们的值大于0,则h264_probe函数返回0,表示该文件完全不符合H.264裸流文件的格式:

语句:

cpp 复制代码
if (ref_zero[type] == -1 && !ref_idc)
                return 0;

的意思是:下面红框里面的NALU重要性高,它们的nal_ref_idc值应为1到3。如果它们的值为0,则h264_probe函数返回0,表示该文件完全不符合H.264裸流文件的格式:

初始化GetBitContext结构体,使得接下来可以按位读取这路H.264码流中的数据。如果初始化失败,h264_probe函数返回0,表示该文件完全不符合H.264裸流文件的格式(关于init_get_bits8函数可以参考:《FFmpeg中位操作相关的源码:GetBitContext结构体,init_get_bits函数、get_bits1函数和get_bits函数分析》):

cpp 复制代码
ret = init_get_bits8(&gb, p->buf + i + 1, p->buf_size - i - 1);
if (ret < 0)
    return 0;

然后如果上述读取到的NALU的NALU Header中的nal_unit_type为7,表示该NALU为sps,会执行下面语句:

cpp 复制代码
switch (type) {
//...
case 7:
    skip_bits(&gb, 14);
    if (get_bits(&gb, 2))
        return 0;
    skip_bits(&gb, 8);
    sps_id = get_ue_golomb_long(&gb);
    if (sps_id > MAX_SPS_COUNT)
        return 0;
    sps_ids[sps_id] = 1;
    sps++;
    break;
//...
}

上面代码块中,语句:

cpp 复制代码
if (get_bits(&gb, 2))
    return 0;

的意思是:读取sps中的reserved_zero_2bits属性。根据H.264官方文档第44页,reserved_zero_2bits的值应为0,如果它不为0,h264_probe函数返回0,表示该文件完全不符合H.264裸流文件的格式:

上面代码块中,语句:

cpp 复制代码
sps_id = get_ue_golomb_long(&gb);
if (sps_id > MAX_SPS_COUNT)
    return 0;

的意思是:读取sps中的seq_parameter_set_id属性。根据H.264官方文档第74页,seq_parameter_set_id属性的取值范围为0 ~ 31(包括0 ~ 31),所以如果读取出来的seq_parameter_set_id大于MAX_SPS_COUNT,也就是大于32,h264_probe函数返回0,表示该文件完全不符合H.264裸流文件的格式:(注:个人认为这部分的FFmpeg源码写得有bug,应该是 if(sps_id >= MAX_SPS_COUNT)才对吧?因为根据官方文档seq_parameter_set_id不能为32!!!):

h264_probe函数中nal_unit_type为其它值时的处理跟sps的大同小异,这里就不说了。

最后通过下面语句判断:该路H.264码流中,如果存在sps,存在pps,并且存在IDR SLICE或者非IDR SLICE的数量大于3个,则返回AVPROBE_SCORE_EXTENSION + 1(也就是返回51),意味着该文件比较符合AnnexB格式的H.264裸流文件格式:

cpp 复制代码
if (sps && pps && (idr || sli > 3) && res < (sps + pps + idr))
        return AVPROBE_SCORE_EXTENSION + 1;  // 1 more than .mpg

    return 0;
相关推荐
lxkj_20243 小时前
使用线程局部存储解决ffmpeg中多实例调用下自定义日志回调问题
ffmpeg
简鹿办公5 小时前
如何提取某站 MV 视频中的音乐为 MP3 音频
音视频·简鹿视频格式转换器·视频提取mp3音频
yufengxinpian5 小时前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
runing_an_min7 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
ruizhenggang7 小时前
ffmpeg本地编译不容易发现的问题 — Error:xxxxx not found!
ffmpeg
runing_an_min9 小时前
ffmpeg视频滤镜:提取缩略图-framestep
ffmpeg·音视频·framestep
小曲曲10 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
EasyNTS12 小时前
H.264/H.265播放器EasyPlayer.js网页全终端安防视频流媒体播放器关于iOS不能系统全屏
h.265·h.264
EasyNTS12 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
安静读书12 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频