H264 NALU分析 + 提取H264码流实战

H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。

NALU(Network Abstract Layer Unit)全称为网络抽象层单元。

对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在相似性,因此视频帧图像可以进⾏图像压缩;H264采⽤了16*16的分块⼤⼩,对视频帧图像进⾏相似⽐较和压缩编码。

H264使⽤帧内压缩和帧间压缩的⽅式提⾼编码压缩率;H264采⽤了独特的I帧、P帧和B帧策略来实现,连续帧之间的压缩。

H264帧的分类:

帧的分类 全称 作用
I帧 帧内编码帧(intra picture) I 帧通常是每个 GOP(MPEG 所使⽤的⼀种视频压缩技术)的第⼀个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是⼀个图像经过压缩后的产物。⾃身可以通过视频解压算法解压成⼀张单独的完整的图⽚。
P帧 前向预测编码帧(predictive-frame) 通过充分将低于图像序列中前⾯已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧。需要参考其前⾯的⼀个I frame 或者P frame来⽣成⼀张完整的图⽚。
B帧 双向预测帧(bi-directional interpolated prediction frame) 既考虑与源图像序列前⾯已编码帧,也顾及源图像序列后⾯已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧。则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。
H264编码结构

H264每一帧的结构由图片组(GOP,group of pictures)、图⽚(pictrue)、⽚(Slice)、宏块(Macroblock)、⼦块(subblock)五个层次组成。


一个GOP视频序列图

GOP (图像组)主要⽤作形容⼀个IDR(Instantaneous Decoding Refresh,即时解码刷新)帧 到下⼀个IDR帧之间的间隔了多少个帧。

一个图像组的第一个图像叫做IDR图像,IDR图像都是I帧图像。

IDR的核⼼作⽤是,是为了解码的重同步,当解码器解码到 IDR 图像时,⽴即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始⼀个新的序列。这样,如果前⼀个序列出现重⼤错误,在这⾥可以获得重新同步的机会。IDR图像之后的图像永远不会使⽤IDR之前的图像的数据来解码。

NALU结构图

SPS:序列参数集(Sequence parameter set)SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。
PPS:图像参数集(Picture parameter set) ,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。

发I帧之前,⾄少要发⼀次SPS和PPS。

H.264原始码流(裸流)是由⼀个接⼀个NALU组成,它的功能分为两层,VCL(video codec layer,视频编码层)和NAL(Network Abstract Layer,网络抽象层)。

  • VCL:包括核⼼压缩引擎和块、宏块和⽚的语法级别定义,设计⽬标是尽可能地独⽴于⽹络进⾏⾼效的编码。
  • NAL:负责将VCL产⽣的⽐特字符串适配到各种各样的⽹络和多元环境中,覆盖了所有⽚级以上的语法级别。

在VCL进⾏数据传输或存储之前,这些编码的VCL数据,被映射或封装进NAL单元(NALU)。⼀个NALU = ⼀组对应于视频编码的NALU头部信息 + ⼀个原始字节序列负荷(RBSP,Raw Byte Sequence Payload).

NALU结构单元的主体结构如下所示;⼀个原始的H.264 NALU单元通常由[StartCode] [NALU Header] [NALU Payload]三部分组成,其中 Start Code ⽤于标示这是⼀个NALU 的开始,必须是"00 00 00 01" 或"00 00 01",除此之外基本相当于⼀个NAL header + RBSP;

FFmpeg解复⽤后,MP4、FLV、TS⽂件读取出来的packet是不带startcode,值得注意的是TS封装格式无start code、SPS、PPS也是可以正常播放的。

每个NALU是⼀个⼀定语法元素的可变⻓字节字符串,包括包含⼀个字节的头信息(⽤来表示数据类型),以及若⼲整数字节的负荷数据。

NALU头信息(1Byte):


NALU head
位域 位宽 说明
T 5 bit 负荷数据类型,表示NALU单元的类型,1~12由H.264使⽤,24~31由H.264以外的应⽤使⽤
R 2 重要性指示位,取值越⼤,表示当前NAL越重要,需要优先受到保护,如果当前NAL是属于参考帧的⽚、序列参数集或图像参数集这些重要的单位时,本句法元素必需⼤于0
F 1 禁⽌位,H.264 规范中规定了这⼀位必须为 0.

H.264标准指出,当数据流是储存在介质上时,在每个NALU 前添加起始码:0x000001 或0x00000001,⽤来指示⼀个NALU 的起始位置。

  • 在这样的机制下,在码流中检测起始码,作为⼀个NALU得起始标识,当检测到下⼀个起始码时,当前NALU结束
  • 3字节的0x000001只有⼀种场合下使⽤,就是⼀个完整的帧被编为多个slice(⽚)的时候,包含这些slice的NALU 使⽤3字节起始码。其余场合都是4字节0x00000001的。
H264 annexb模式

H264有两种封装格式:

  • annexb模式,传统模式,有startcode,SPS和PPS是在ES中
  • mp4模式,⼀般mp4 mkv都是mp4模式,container封装中没有startcode,SPS和PPS以及其他信息,每一个frame前面4个字节表示该frame的长度
#include 
#include 
#include 
#include 

static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
    av_strerror(errnum, err_buf, 128);
    return err_buf;
}

/*
AvCodecContext->extradata[]中为nalu长度
*   codec_extradata:
*   1, 64, 0, 1f, ff, e1, [0, 18], 67, 64, 0, 1f, ac, c8, 60, 78, 1b, 7e,
*   78, 40, 0, 0, fa, 40, 0, 3a, 98, 3, c6, c, 66, 80,
*   1, [0, 5],68, e9, 78, bc, b0, 0,
*/

//ffmpeg -i 2018.mp4 -codec copy -bsf:h264_mp4toannexb -f h264 tmp.h264
//ffmpeg 从mp4上提取H264的nalu h
int main(int argc, char **argv)
{
    AVFormatContext *ifmt_ctx = NULL;
    int             videoindex = -1;
    AVPacket        *pkt = NULL;
    int             ret = -1;
    int             file_end = 0; // 文件是否读取结束

    if(argc < 3)
    {
        printf("usage inputfile outfile\n");
        return -1;
    }
    FILE *outfp=fopen(argv[2],"wb");
    printf("in:%s out:%s\n", argv[1], argv[2]);

    // 分配解复用器的内存,使用avformat_close_input释放
    ifmt_ctx = avformat_alloc_context();
    if (!ifmt_ctx)
    {
        printf("[error] Could not allocate context.\n");
        return -1;
    }

    // 根据url打开码流,并选择匹配的解复用器
    ret = avformat_open_input(&ifmt_ctx,argv[1], NULL, NULL);
    if(ret != 0)
    {
        printf("[error]avformat_open_input: %s\n", av_get_err(ret));
        return -1;
    }

    // 读取媒体文件的部分数据包以获取码流信息
    ret = avformat_find_stream_info(ifmt_ctx, NULL);
    if(ret < 0)
    {
        printf("[error]avformat_find_stream_info: %s\n", av_get_err(ret));
        avformat_close_input(&ifmt_ctx);
        return -1;
    }

    // 查找出哪个码流是video/audio/subtitles
    videoindex = -1;
    // 推荐的方式
    videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if(videoindex == -1)
    {
        printf("Didn't find a video stream.\n");
        avformat_close_input(&ifmt_ctx);
        return -1;
    }

    // 分配数据包
    pkt = av_packet_alloc();
    av_init_packet(pkt);

    // 1 获取相应的比特流过滤器
    //FLV/MP4/MKV等结构中,h264需要h264_mp4toannexb处理。添加SPS/PPS等信息。
    // FLV封装时,可以把多个NALU放在一个VIDEO TAG中,结构为4B NALU长度+NALU1+4B NALU长度+NALU2+...,
    // 需要做的处理把4B长度换成00000001或者000001
    const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
    AVBSFContext *bsf_ctx = NULL;
    // 2 初始化过滤器上下文
    av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
    // 3 添加解码器属性
    avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
    av_bsf_init(bsf_ctx);

    file_end = 0;
    while (0 == file_end)
    {
        if((ret = av_read_frame(ifmt_ctx, pkt)) < 0)
        {
            // 没有更多包可读
            file_end = 1;
            printf("read file end: ret:%d\n", ret);
        }
        if(ret == 0 && pkt->stream_index == videoindex)
        {
#if 0
            int input_size = pkt->size;
            int out_pkt_count = 0;
            if (av_bsf_send_packet(bsf_ctx, pkt) != 0) // bitstreamfilter内部去维护内存空间
            {
                av_packet_unref(pkt);   // 你不用了就把资源释放掉
                continue;       // 继续送
            }
            av_packet_unref(pkt);   // 释放资源
            while(av_bsf_receive_packet(bsf_ctx, pkt) == 0)
            {
                out_pkt_count++;
                // printf("fwrite size:%d\n", pkt->size);
                size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
                if(size != pkt->size)
                {
                    printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
                }
                av_packet_unref(pkt);
            }
            if(out_pkt_count >= 2)
            {
                printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
                       input_size, out_pkt_count);
            }
#else       // TS流可以直接写入
            size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
            if(size != pkt->size)
            {
                printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
            }
            av_packet_unref(pkt);
#endif
        }
        else
        {
            if(ret == 0)
                av_packet_unref(pkt);        // 释放内存
        }
    }
    if(outfp)
        fclose(outfp);
    if(bsf_ctx)
        av_bsf_free(&bsf_ctx);
    if(pkt)
        av_packet_free(&pkt);
    if(ifmt_ctx)
        avformat_close_input(&ifmt_ctx);
    printf("finish\n");

    return 0;
}

命令

./do_extract_h264 believe.mp4 out.flv

结果:

in:believe.flv out:out.flv
read file end: ret:-541478725
finish
Press  to close this window...

你可能感兴趣的:(H264 NALU分析 + 提取H264码流实战)