当前位置: 首页 > news >正文

音视频开发14 FFmpeg 视频 相关格式分析 -- H264 NALU格式分析

H264简介-也叫做 AVC

H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC

原始数据YUV,RGB为什么要压缩-知道就行

在⾳视频传输过程中,视频⽂件的传输是⼀个极⼤的问题;⼀段分辨率为1920*1080,每个像素点为RGB占⽤3个字节,帧率是25的视频,对于传输带宽的要求是:

1920x1080x3x25/1024/1024=148.315MB/s, 这个是每秒的 bytes 数

换成bps则意味着视频每秒带宽为 148.315MB/s x 8 = 1186.523Mbps

1186.523Mbps,这样的速率对于⽹络存储是不可接受的。因此视频压缩和编码技术应运⽽⽣。

H264编码原理

帧内压缩

对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在

相似性,因此视频帧图像可以进⾏图像压缩;H264采⽤了16*16的分块⼤⼩对,视频帧图像

进⾏相似⽐较和压缩编码。如下图所示:

帧间压缩

H264采⽤了独特的I帧、P帧和B帧策略 来实现,连续帧之间的压缩;

H264 编码结构解析
H264 除了实现了对视频的压缩处理之外,为了⽅便⽹络传输,提供了对应的视频编码和分⽚
策略;类似于⽹络数据封装成 IP 帧,在 H264 中将其称为组 ( GOP , group of pictures) 、⽚
slice )、宏块( Macroblock )这些⼀起组成了 H264 的码流分层结构; H264 将其组织成为
序列 (GOP) 、图⽚ (pictrue) 、⽚ (Slice) 、宏块 (Macroblock) 、⼦块 (subblock) 五个层次。
GOP (图像组)主要⽤作形容⼀个 IDR 帧 到下⼀个 IDR 帧之间的间隔了多少个帧。

H264将视频分为连续的帧进⾏传输,在连续的帧之间使⽤I帧、P帧和B帧。

同时对于帧内⽽⾔,将图像分块为⽚、宏块和字块进⾏分⽚传输;通过这个过程实现对视频⽂件的压缩包装。

IDR(Instantaneous Decoding Refresh,即时解码刷新)

⼀个序列的第⼀个图像叫做 IDR 图像(⽴即刷新图像),IDR 图像都是 I 帧图像。

I和IDR帧都使⽤帧内预测。I帧不⽤参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之

前的帧的。

但是在解码的时候,I 和 IDR 有区别。举例如下:在第一个解码的时候,解码到B8的时候,可以参考I10前面的P7.

在第二个解码的时候,B9 就只能参考 IDR8和 P11,不能参考IDR8之前的帧。

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

下⾯是⼀个H264码流的举例(从码流的帧分析可以看出来B帧不能被当做参考帧)

在假设条件下分析上图,假设GOP1 的是每秒25帧,也就是一帧画面需要1000/25 = 40ms.

I帧解码的时候时间点在0,那么读取下一帧B要依赖于 P,接着找下一帧是不是P,还不是,在找,直到找到P,也就是说:在 找P的时候已经过去了160ms了,大致如下:

I0 B40 B80 B120 P160

I0 B160

这意味着什么呢?在做实时性要求高的场景时,最好不要使用B帧

H264编码结构- NALU

NAL简介
  NAL层即网络抽象层(Network Abstraction Layer),是为了方便在网络上传输的一种抽象层。一般网络上传输的数据包有大小限制,而AVC(H264)的一帧大小远远大于网络传输的字节大小限制。因此要对AVC的数据流进行拆包,将一帧数据拆分为多个包传输。和NAL层相对是VAL层,即视频编码层(Video Coding Layer)

NALU就是经过分组后的一个一个数据包。
 

H.264 原始码流 ( 裸流 ) 是由⼀个接⼀个 NALU 组成。

发I帧之前,⾄少要发⼀次SPS和PPS。当分辨率变化的时候,要重新发送一次SPS和PPS(类似在视频网站上,我们将分辨率从720p变成1080p的时候)

这个很重要,如果遇到我们显示不了图片或者视频的时候,应该第一个检查的就是 SPS 和PPS是否有正确的发送。

SPS:序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。

PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。

I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。

P: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。

B: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。

每个NALU = StartCode + 由一个1字节的NALU头部 + 一个包含控制信息编码视频数据的字节流组成

NALU 结构单元的主体结构如下所示;⼀个原始的 H.264 NALU 单元 通常由 [StartCode] [NALU
Header] [NALU Payload] 三部分组成。
Start Code ⽤于标示这是⼀个 NALU 单元的开始,
必须是"00 00 00 01" "00 00 01"
H.264 标准指出,当数据流是储存在介质上时,在每个 NALU 前添加起始码: 0x000001
0x00000001 ,⽤来指示⼀个 NALU 的起始和终⽌位置:
在这样的机制下,在码流中检测起始码,作为⼀个 NALU 得起始标识,当检测到下⼀个起始码时,当前NALU 结束。
3 字节的 0x000001 只有⼀种场合下使⽤,就是⼀个完整的帧被编为多个 slice (⽚)的时
候,包含这些 slice NALU 使⽤ 3 字节起始码。其余场合都是 4 字节 0x00000001 的。
NALU Header :
NALU Header占位8Bit,其中三个字段分别为
F 为禁⽌位,占 1bit  
forbidden_zero_bit: 在 H.264 规范中规定了这⼀位必须为 0。
R为重要性指示位,占 2bit 
nal_ref_idc :取 00~11, 似乎指示这个 NALU 的重要性 , 00 NALU 解码器可以丢弃它⽽不影响图像的回放,0 3 ,取值越⼤,表示当前 NAL 越重要,需要优先受到保护。如果当前 NAL是属于参考帧的⽚,或是序列参数集,或是图像参数集这些重要的单位时,本句法元 素必需⼤于0
T 为负荷 数据类型 ,占 5 bit
nal_unit_type:这个 NALU 单元的类型 ,1 12 H.264 使⽤, 24 31 H.264 以外的应⽤
其值如下:重点是 5,6,7,8
5      Coded slice of an IDR picture                                  VCL
        IDR图像的编码条带(⽚) slice_layer_without_partitioning_rbsp( )
6      Supplemental enhancement information (SEI)                     non-VCL
        辅助增强信息 (SEI)sei_rbsp( )
7      Sequence parameter set                                         non-VCL
        序列参数集 seq_parameter_set_rbsp( )
8      Picture parameter set                                          non-VCL
        图像参数集 pic_parameter_set_rbsp( )
0      Unspecified                                                    non-VCL未指定
1      Coded slice of a non-IDR picture                               VCL⼀个⾮IDR图像的编码条带slice_layer_without_partitioning_rbsp()
2      Coded slice data partition A                                   VCL编码条带数据分割块A slice_data_partition_a_layer_rbsp()
3      Coded slice data partition B                                   VCL编码条带数据分割块B slice_data_partition_b_layer_rbsp( )
4      Coded slice data partition C                                   VCL编码条带数据分割块C slice_data_partition_c_layer_rbsp( )
5      Coded slice of an IDR picture                                  VCLIDR图像的编码条带(⽚) slice_layer_without_partitioning_rbsp( )
6      Supplemental enhancement information (SEI)                     non-VCL辅助增强信息 (SEI)sei_rbsp( )
7      Sequence parameter set                                         non-VCL序列参数集 seq_parameter_set_rbsp( )
8      Picture parameter set                                          non-VCL图像参数集 pic_parameter_set_rbsp( )
9      Access unit delimiter                                          non-VCL访问单元分隔符 access_unit_delimiter_rbsp( )
10     End of sequence                                                non-VCL序列结尾 end_of_seq_rbsp( )
11     End of stream                                                  non-VCL流结尾end_of_stream_rbsp( )
12     Filler data                                                    non-VCL填充数据filler_data_rbsp( )
13     Sequence parameter set extension                               non-VCL序列参数集扩展seq_parameter_set_extension_rbsp( )
14     Prefix NAL unit                                                non-VCLNAL 单元前缀
15     Subset sequence parameter set                                  non-VCL子集序列参数集
16     Depth parameter set                                            non-VCL深度参数集
17..18 Reserved                                                       non-VCL保留
19     Coded slice of an auxiliary coded picture without partitioning non-VCL未分割的辅助编码图像的编码条带slice_layer_without_partitioning_rbsp( )
20     Coded slice extension                                          non-VCL编码切片扩展
21     Coded slice extension for depth view components                non-VCL深度视图组件的编码切片扩展
22..23 Reserved                                                       non-VCL保留
24..31 Unspecified                                                    non-VCL未定义

NALU Payload  就是 RBSP
payload 中文是有效载荷的意思
RBSP 是  Raw Byte Sequence Payloads的缩写, 翻译为:原始字节序列有效载荷。
这个有效载荷的含义是:包含控制信息或编码视频数据的字节流
前面看到有VCL 和 NON-VCL。可以简单的理解为VCL是真正的数据,NON-VCL也称为NAL,是辅助用的,实际的用处将这些真正的VCL数据,如果适配到网络环境中。
它的功能 分为两层,VCL( 视频编码层 )和NAL( ⽹络提取层 )
VCL :包括核⼼压缩引擎和块,宏块和⽚的语法级别定义,设计⽬标是尽可能地独⽴于⽹
络进⾏⾼效的编码;
NAL :负责将 VCL 产⽣的⽐特字符串适配到各种各样的⽹络和多元环境中,覆盖了所有⽚级
以上的语法级别

H264编码的组织

  一个完整的数据包包含多个NALU,不同的NALU该如何组织规范中并没有规定,因此实际实现比较广泛的有两种格式AnnexB和AVCC。

AnnexB

实际上我们前面学习就是以AnnexB 这种模式学习的。AnnexB是一种比较常见的H264码流格式,FFmpeg解封装的H264码流就是这种格式。

AnnexB的格式比较简单:每个NALU单元之前通过分隔符0x00 00 00 01或者0x00 00 01区分不同的NALU单元。

对于非VCL和VCL的单元是不区分的都是存储在NALU的Body中。
由于NALU的Body中的数据是压缩数据可能出现start code,因此规定RBSP中的0x000000、0x000001、0x000002和0x000003是非法。如果数据中包含类似的二进制序列需要插入一个“模拟预防”字节0x03来实现,使得0x000001变成0x00000301,解码时去除即可。

AVCC

另一种常见的存储H.264流的方法是AVCC格式。

也叫mp4 模式,⼀般 mp4 mkv 都是 mp4 模式,没有 startcode SPS PPS 以及其它信息
被封装在extradata 也叫做 container中,每⼀个 frame 前⾯ 4 个字节是这个 frame 的⻓度

AnnexB和 AVCC的转换

很多视频解码器只⽀持 annexb 这种模式,因此我们在解析的目标是AVCC的H264 的时候要 做转换:
ffmpeg 中⽤ h264_mp4toannexb_filter可以做转换。

这里开始的部分还是通过 av_read_frame方法 读取数据 到 AVPacket 的时候:

av_read_frame(avformatcontext, avpacket);

然后使用 ffmpeg提供的 av_bsf_send_packet方法,将 avpacket 数据塞入,注意的是:当我们将avpacket 数据塞入的时候,av_bsf_send_packet会自己管理内存,不管av_bsf_send_packet方法成功或者不成功,我们都要调用 av_packet_unref(pkt);将自己的refcount -1 。

int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
 

然后通过 ffmpeg 提供的 av_bsf_receive_packet方法, 将avpacket数据改动,当我们拿出的时候,也要记得调用 av_packet_unref(pkt);将自己的avpacket 的refcount -1。

int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);

这时候 avpacket 中的数据,就从 AVCC转换成 AnnexB的了。就可以直接写入到 自己像存储的.h264文件。

那么 AVBSFContext 是怎么来的呢?

参考如下的几步:

// 1 找到 h264_mp4toannexb 的过滤器
const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;
// 2 初始化过滤器上下⽂
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
该av_bsf_alloc方法的说明如下:* Allocate a context for a given bitstream filter. The caller must fill in the* context parameters as described in the documentation and then call* av_bsf_init() before sending any data to the filter.// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);av_bsf_init(bsf_ctx);

注意的是:如果文件是TS流,可以不使用该方法,如果使用,也不会有问题。

但是如果文件是mp4文件,或者flv文件,则要使用该方法,如果不使用,会有问题

整体code如下:

#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
#include<libavcodec/bsf.h>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/subtitlesvideoindex = -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或者000001const 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 0int 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;
}

这里我们要复习一下  av_packet_unref(pkt);和 av_packet_free(pkt)的区别:

相关文章:

  • 查找最小的K个元素
  • SpringBoot案例,通关版
  • 实时瞳孔分割算法-RITnet论文复现
  • 【Python】超时请求或计算的处理
  • Dify快速接入微信
  • C++之map
  • OVF(Open Virtualization Format)或OVA(Open Virtual Appliance)格式有什么区别
  • OpenGL 和 DirectX 矩阵 乘法 左乘和右乘,glm和DirectXMath。 OpenGL用列矩阵 在 glsl中反转矩阵
  • C语言(数据存储)
  • 计算机网络学习实践:模拟PPP协议验证虚拟局域网(VLAN)
  • 【人工智能Ⅱ】实验8:生成对抗网络
  • 做外贸,怎么选国外服务器?
  • “神经网络之父”和“深度学习鼻祖”Geoffrey Hinton
  • 字节裁员!开启裁员新模式。。
  • 鸿蒙开发接口资源调度:【@ohos.backgroundTaskManager (后台任务管理)】
  • Bootstrap JS插件Alert源码分析
  • Docker 笔记(2):Dockerfile
  • HTTP--网络协议分层,http历史(二)
  • Java教程_软件开发基础
  • js中forEach回调同异步问题
  • Linux学习笔记6-使用fdisk进行磁盘管理
  • Netty+SpringBoot+FastDFS+Html5实现聊天App(六)
  • Python - 闭包Closure
  • Python socket服务器端、客户端传送信息
  • spring cloud gateway 源码解析(4)跨域问题处理
  • Vue2.x学习三:事件处理生命周期钩子
  • 百度小程序遇到的问题
  • 持续集成与持续部署宝典Part 2:创建持续集成流水线
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 极限编程 (Extreme Programming) - 发布计划 (Release Planning)
  • 码农张的Bug人生 - 初来乍到
  • 前端技术周刊 2019-02-11 Serverless
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 数据科学 第 3 章 11 字符串处理
  • 推荐一个React的管理后台框架
  • 译自由幺半群
  • 智能合约开发环境搭建及Hello World合约
  • #13 yum、编译安装与sed命令的使用
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (20050108)又读《平凡的世界》
  • (2009.11版)《网络管理员考试 考前冲刺预测卷及考点解析》复习重点
  • (MATLAB)第五章-矩阵运算
  • (Matlab)使用竞争神经网络实现数据聚类
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (第27天)Oracle 数据泵转换分区表
  • (经验分享)作为一名普通本科计算机专业学生,我大学四年到底走了多少弯路
  • (七)Flink Watermark
  • (全注解开发)学习Spring-MVC的第三天
  • (四)模仿学习-完成后台管理页面查询
  • (已解决)报错:Could not load the Qt platform plugin “xcb“
  • (转)EXC_BREAKPOINT僵尸错误
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .NET Core 中的路径问题
  • .Net Core/.Net6/.Net8 ,启动配置/Program.cs 配置
  • .NET delegate 委托 、 Event 事件