文章目录

  • 前言
  • 视频
  • 编解码
    • 硬件解码(高级)
      • 软解码(低级)
      • 软、硬解码对比
      • 视频解码有四个步骤
      • Android 系统中编解码器的命名方式
      • 查看当前设备支持的硬解码
  • 基础知识
    • RGB色彩空间
      • 常见的格式
      • 对比YUV
      • 索引格式
      • 分离RGB24像素数据中的R、G、B分量
    • BMP 文件格式
      • 格式组成
      • 像素排列顺序
      • RGB24格式像素数据封装为BMP图像
    • YUV色彩空间
      • 对比RGB
      • 采样模式
      • YUV格式
      • 文件大小
      • 文件格式
      • 像素排列顺序
    • 分离YUV
      • 分离YUVxxxP像素数据中的Y、U、V分量
      • YUV420P像素数据去掉颜色(灰度图)
      • YUV420P像素数据亮度减半
      • YUV420P像素数据的周围加边框
      • 生成YUV420P格式的灰阶测试图
      • 计算两个YUV420P像素数据的PSNR
    • RGB-YUV相互转换
      • 求Y
      • 求U
  • AVC视频码流解析
    • 类型帧
      • I帧
      • P帧
      • B帧
    • GOP
    • PTS、DTS
    • H264(AVC)
      • 背景
      • NALU
      • 结构
        • AnnexB
          • 防竞争字节序
        • avcC
          • avcC文件头部信息格式
          • NALULengthSizeMinusOne
      • 主体NALU
      • NALU的第一个字节
    • 读取SPS信息
    • slice
    • 宏块
  • HEVC视频码流解析
  • AV1视频码流解析
  • AV1、HEVC编解码格式对比
    • 未来
  • 音频
  • 常用知识
    • 音视频常见单词
    • 音视频常见名词
    • ffmpeg硬件加速
    • 常用命令
    • 注意的细节
    • H264画质级别
  • ffmpeg结构

前言

本文章是以学习笔记形式展开记录。

希望大家学成归来的时候多反馈国内的音视频社区吧

视频

编解码

#mermaid-svg-tGmBBVdDTpxJkghs {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#fff;}#mermaid-svg-tGmBBVdDTpxJkghs .error-icon{fill:hsl(180, 0%, 26.9607843137%);}#mermaid-svg-tGmBBVdDTpxJkghs .error-text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);stroke:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-tGmBBVdDTpxJkghs .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-tGmBBVdDTpxJkghs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tGmBBVdDTpxJkghs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tGmBBVdDTpxJkghs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tGmBBVdDTpxJkghs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tGmBBVdDTpxJkghs .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-tGmBBVdDTpxJkghs .marker.cross{stroke:#0b0b0b;}#mermaid-svg-tGmBBVdDTpxJkghs svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tGmBBVdDTpxJkghs .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#fff;}#mermaid-svg-tGmBBVdDTpxJkghs .cluster-label text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-tGmBBVdDTpxJkghs .cluster-label span{color:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-tGmBBVdDTpxJkghs .label text,#mermaid-svg-tGmBBVdDTpxJkghs span{fill:#fff;color:#fff;}#mermaid-svg-tGmBBVdDTpxJkghs .node rect,#mermaid-svg-tGmBBVdDTpxJkghs .node circle,#mermaid-svg-tGmBBVdDTpxJkghs .node ellipse,#mermaid-svg-tGmBBVdDTpxJkghs .node polygon,#mermaid-svg-tGmBBVdDTpxJkghs .node path{fill:#383838;stroke:#fff;stroke-width:1px;}#mermaid-svg-tGmBBVdDTpxJkghs .node .label{text-align:center;}#mermaid-svg-tGmBBVdDTpxJkghs .node.clickable{cursor:pointer;}#mermaid-svg-tGmBBVdDTpxJkghs .arrowheadPath{fill:undefined;}#mermaid-svg-tGmBBVdDTpxJkghs .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-tGmBBVdDTpxJkghs .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-tGmBBVdDTpxJkghs .edgeLabel{background-color:hsl(-120, 0%, 21.9607843137%);text-align:center;}#mermaid-svg-tGmBBVdDTpxJkghs .edgeLabel rect{opacity:0.5;background-color:hsl(-120, 0%, 21.9607843137%);fill:hsl(-120, 0%, 21.9607843137%);}#mermaid-svg-tGmBBVdDTpxJkghs .cluster rect{fill:hsl(180, 0%, 26.9607843137%);stroke:hsl(180, 0%, 16.9607843137%);stroke-width:1px;}#mermaid-svg-tGmBBVdDTpxJkghs .cluster text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-tGmBBVdDTpxJkghs .cluster span{color:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-tGmBBVdDTpxJkghs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(180, 0%, 26.9607843137%);border:1px solid undefined;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tGmBBVdDTpxJkghs :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

解封装
音视频压缩Data
音视频同步
解协议
得到封装格式-mp4
视频压缩Data
解码
原始数据-YUV
音频驱动/设备
音频压缩Data
音频采样数据-PCM
视频驱动/设备

硬件解码(高级)

通过显卡的视频加速功能对高清视频进行解码。因此硬解码能够将CPU从繁重的视频解码运算中释放出来,使播放设备具备流畅播放高清视频的能力。

显卡的GPU/VPU要比CPU更适合这类大数据量的、低难度的重复工作。视频解码工作从处理器那里分离出来,交给显卡去做

软解码(低级)

以前纯粹依靠CPU来解码的方式则是“软解码”。

软解码是在显卡本身不支持或者部分不支持硬件解码的前提下,将解压高清编码的任务交给CPU,这是基于硬件配置本身达不到硬解压要求的前提下,属于一个折中的无奈之举

软、硬解码对比

对于一个超级电视而言,观看高清电影无疑是用户最大的诉求,而硬解码的优势就在于可以流畅的支持1080p甚至4K清晰度的电影播放,而不需要占用CPU,CPU就可以如释重负,轻松上阵,承担更多的其他任务。

如果通过软解码的方式播放高清电影,CPU的负担较重,往往会出现卡顿、不流畅的现象。

视频解码有四个步骤

  1. VLD(流处理)
  2. iDCT(频率转换)
  3. MoComp(运动补偿)
  4. PostProc(显示后处理,解码去块滤波Deblocking)

通常我们所说的硬件加速或硬件解码,就是指视频解码的这几个步骤中,用显卡专用的解码引擎替代CPU的软件计算,降低CPU的计算负荷。

Android 系统中编解码器的命名方式

  • 软编解码器通常是以 OMX.google开头
  • 硬编解码器通常是以 OMX.[hardware_vendor] 开头的

但是还有一些不遵守这个命名规范的,不以 OMX. 开头的情况,它们也会被认为是软编解码器。

查看当前设备支持的硬解码

  • 通过/system/etc/media_codecs.xml可以确定当前设备支持哪些硬解码。
  • 通过/system/etc/media_profiles.xml可以知道设备支持的具体profile和level等详细信息。

基础知识

RGB色彩空间

所有的颜色可以通过三原色产生,这 三原色 就是 Red (红),Green(绿),Blue(蓝)

缺点是占用空间大,应用用RGB封装格式的的多,下文中的BMP就是使用的RGB做封装的

常见的格式

以下是基于小端的模式架构体系中

  • RGB24 : 每个像素占3个字节,R 占 8位,G 占 8位,B 占 8 位。即格式可以混合生成 25 6 3 256^3 2563 种颜色以下的格式也是依次类推。顺序为R-G-B

  • RGB16

    • RGB565 : 每个像素占2个字节,R 占 5位,G 占 6位,B 占 5 位。顺序为B-G-R
    • RGB555 :每个像素占2个字节(还剩一个最高位不使用),R 占 5位,G 占 5位,B 占 5 位。顺序为B-G-R
  • RGB32 :每个像素占4个字节,R 占 8位,G 占 8位,B 占 8 位,最后8位保留。顺序为B-G-R-0

  • ARGB32 :本质就是带alpha通道RGB32,保留的8个bit用来表示alpha的值。顺序为B-G-R-A

  • RGB1、RGB4、RGB8

opencv默认通道是BGR的。这是opencv的入门大坑之一,BGR是个历史遗留问题,为了兼容早年的某些硬件

对比YUV

与YUV420P三个分量分开存储不同,RGB24格式的每个像素的三个分量是连续存储的,对比如下:

  • YUV444p(称为Planar方式)
y y y y u u u u v v v v
y y y y u u u u v v v v
  • RGB24(称为Packed方式)
R G B R G B R G B R G B
R G B R G B R G B R G B

索引格式

  • RGB 的值是一个索引,不是真正的颜色值
  • 例如 RGB 的值占1位,那只有两个值 0 跟 1,通常用于黑白颜色
    • 0 跟 1 到底是什么颜色,是通过 索引表(也叫调色板)来定位的,不一定是 黑白,也可以是其他的颜色,叫索引格式
  • 只占 1 位的 RGB 称为 RGB1,还有 RGB4 占 4 位,索引表有 16 种颜色, RGB8 占 8 位,索引表有 256 种颜色。

分离RGB24像素数据中的R、G、B分量

因为RGB24格式的每个像素的三个分量是连续存储的,所以只需要遍历全部文件数据,然后一次取3个字节,将3个字节分为R、G、B分量保存就可以了

 unsigned char *pic=(unsigned char *)malloc(w*h*3);fread(pic,1,w*h*3,fp);for(int j=0;j<w*h*3;j=j+3){fwrite(pic+j,1,1,fp1);fwrite(pic+j+1,1,1,fp2);fwrite(pic+j+2,1,1,fp3);}
  • 根据文件大小推算文件宽高-(只能一些推算常见、有规律的宽高)
#原始文件大小 byte单位 十六进制
$ hexdump cie1931_500x500.rgb | tail -1                   [~/D/y音视频]
00b71b0
$ python3.9 -c "print(int('0x0b71b0',16))"                [~/D/y音视频]
750000
$ expr 750000 / 3                                         [~/D/y音视频]
250000
  • 根据视频宽高推算文件大小
$ expr 500 \* 500 \* 3                                    [~/D/y音视频]
750000
$ python3.9 -c "print(hex(750000))"                       [~/D/y音视频]
0xb71b0

这里有个奇怪的现象,暂时放置,#####################

$ du -k cie1931_500x500.rgb                                  [~/D/y音视频]
736K    cie1931_500x500.rgb
$ expr 736 \* 1024                                        [~/D/y音视频]
753664#这里的结果应该是750000才对

ffplay查看图片

ffplay -s 500x500 -pix_fmt rgb24  cie1931_500x500.rgb
#需要指定格式 不然会按照yuv420p格式打开
ffplay -s 500x500 -pix_fmt rgb24  -vf extractplanes='r' cie1931_500x500.rgb
#分量
ffplay -s 500x500 -pix_fmt rgb24  -vf extractplanes='g' cie1931_500x500.rgb
ffplay -s 500x500 -pix_fmt rgb24  -vf extractplanes='b' cie1931_500x500.rgb

BMP 文件格式

BMP 全称 Bitmap-File,是微软出的图像文件格式,目前只有 BMP 文件是用RGB 模式

格式组成

  • BMP文件头(14 bytes) ,存放一些文件相关的信息。

    • 1~2 - Magic number(除了数据文件外,基本都有,file命令就是通过它来判定文件类型的)
    • 3~6 - BMP文件大小
    • 7~10 - 扩展字节(目前是0)
    • 11~14 - 实际的像素数据的偏移值(没有调色板通常是 0x36)
  • 位图信息头,通常是 40 bytes 大小,也可以理解成 图像信息头,存放一些图像相关的信息,例如宽高之类的数据

    • 15~18 - 位图信息头大小
    • 19~22 - 图像的宽度(单位像素)
    • 23~26 - 图像的高度
    • 27~28 - color planes 的数量 ,通常是 0
    • 29~30 - 指每个像素需要多少位来存储(0x0018 -> REG24 -> 一个像素24位)
    • 31~34 - 压缩类型
    • 35~38 - 图像大小(不包含 BMP文件头跟位图信息头)
    • 39~42 - 水平分辨率
    • 43~45 - 垂直分辨率
    • 46~49 - 颜色表中的颜色索引数(没有索引表就是0 -> 真彩色)
    • 50~53 - 对图像显示有重要影响的颜色索引数码(00 -> 全都重要)
  • 调色板,大小由 颜色索引决定,本文的 juren.bmp 是 RGB24 模式,真彩色,没有用到 调色板,所以这个区域是0字节。

  • 位图数据,对于本文的 juren.bmp 来说,里面就是 RGB 数据,一个像素占 3 个字节。

像素排列顺序

默认从左下角到右上角,排列顺序是由 位图信息头 里面那个 高度是正数还是负数决定的

RGB24格式像素数据封装为BMP图像

这里我跟的时候发现代码运行后生成的bmp打不开,后来发现是结构体定义格式错了,按照上面bmp的头结构重新定义结构体后,不用进行R、B分量互换,主流电脑、笔记本中用C在写内存的时候会默认为小端模式,除非你在单片机上面写那就是大端模式

修订后的代码如下,因为这部分涉及到了结构体所有全部贴出来

#include <stdio.h>
#include <stdlib.h>struct Head {/*  char magic[2]; //一般是有4个字节长度的 Magic Number */int32_t file_size;int32_t extension;int32_t offset;
} head;struct Info {int32_t     info_size;int32_t       width;                  /* unit pixel */int32_t     height;unsigned short   num_color_planes;unsigned short bit;int32_t     compress_type;int32_t       kernel_data_size;int32_t        horizontal_resolution;short     vertical_resolution;    /* 2byte 下面需要再补充一个,如果置为0的话也可以不用写,编译器会通过字节对齐的方式跳过2个字节(下面是int_32) */char        _vertical_resolution;   /* 补充1byte */int32_t        tab_index;int32_t       important_idx;
} info;int main()
{printf( "head byte -> %ld\n", sizeof(head) );printf( "info byte -> %ld\n", sizeof(info) );int    w   = 256, h = 256;char   bfType[2]   = { 'B', 'M' };FILE    * fp_rgb24  = fopen( "lena_256x256_rgb24.rgb", "rb" );FILE * fp_bmp    = fopen( "out.bmp", "wb+" );if ( fp_rgb24 == NULL || fp_bmp == NULL ){printf( "NULL!" );return(-1);}unsigned char* rgb24_buf = (unsigned char *) malloc( w * h * 3 );unsigned char* re_rgb24_buf = (unsigned char *) malloc( w * h * 3 );fread( re_rgb24_buf, 1, w * h * 3, fp_rgb24 );//倒置字节for (int i = 0; i < w * h * 3; ++i) {rgb24_buf[i] = re_rgb24_buf[((w*h*3)-1)-i];}//不需要调整大小端
//  char tmp;
//  for ( int j = 0; j < w * h * 3; j = j + 3 ){//      tmp = rgb24_buf[j];
//      rgb24_buf[j] = rgb24_buf[j + 2];
//      rgb24_buf[j + 2] = tmp;
//  }int head_size = sizeof(bfType) + sizeof(head) + sizeof(info);printf( "%d\n", head_size );head.file_size   = head_size + w * h * 3;head.offset   = head_size;info.info_size = sizeof(info);info.width = w;info.height = h;info.num_color_planes = 1;info.bit= 3 * 8; /* rgb24 */info.compress_type = 0;info.kernel_data_size = w * h * 3;info.horizontal_resolution  = 0;info.vertical_resolution   = 0;info.tab_index = 0;info.important_idx = 0;fwrite( bfType, 1, sizeof(bfType), fp_bmp );fwrite( &head, 1, sizeof(head), fp_bmp );fwrite( &info, 1, sizeof(info), fp_bmp );fwrite( rgb24_buf, 1, w * h * 3, fp_bmp );free( re_rgb24_buf );re_rgb24_buf=NULL; //uaffree( rgb24_buf );rgb24_buf=NULL;fclose( fp_rgb24 );fclose( fp_bmp );return 0;
}

这里需要注意的问题 : (不知道是不是我的编译器问题 -> Apple clang version 13.1.6)

  1. 定义结构体的时候的字节对齐问题,我们知道BMP文件头是14 bytes,但是如果按照上面所述的结构来定义结构体的话,magic number会被扩展字节按int_32进行字节对齐,所以magic number需要单独进行写入

  2. 写完的bmp图片打开时,发生了图片颠倒的问题,所以需要倒置字节的操作,这里插个庄,后续再来处理

YUV色彩空间

YUV 是 指 YCbCr ,U 就是 Cb,V 就是 Cr

各分量表示含义 : Y- 亮度,U-色调,V-色饱和度(彩度)

在位深度为8bit的YUV 分量的值都是 0 ~ 255 (8位 = 2 8 2^8 28,即 [ 0 , ( 2 8 − 1 ) ] [0, (2^8 - 1)] [0,(28−1)]

这里的位深度就对应了PSNR的计算,挺好玩的~

简单的说就是:

  • Y亮度一张图片有了Y分量那么通过Y分量在不同局部初亮度潜深就可以描绘出这张图片的大致轮廓

  • U、V共同的决定了这张轮廓图的色调,U、V组成了一个(0~255)二维坐标,每个坐标点就是一个颜色值

  • 位深度就是只Y、U、V分量的取值范围,10bit就是0~1023的范围值,Y值的轮廓越来越明显,U、V色彩过滤就越来越缓和

对比RGB

  • RGB => 采集和显示比较好
  • YUV => 编码和存储比较好

在存储和编码之前,RGB 图像要转换为 YUV 图像,而 YUV 图像在 显示之前通常有必要转换回 RGB. 显示的时候 YUV 转成 RGB 通常是硬件或者软件内部做了,我们写代码开发的时候 这个 YUV 转 RGB 显示到屏幕这个过程通常是透明的

#mermaid-svg-TMJwgiGu2b5tSy7v {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#fff;}#mermaid-svg-TMJwgiGu2b5tSy7v .error-icon{fill:hsl(180, 0%, 26.9607843137%);}#mermaid-svg-TMJwgiGu2b5tSy7v .error-text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);stroke:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-TMJwgiGu2b5tSy7v .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-TMJwgiGu2b5tSy7v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TMJwgiGu2b5tSy7v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TMJwgiGu2b5tSy7v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TMJwgiGu2b5tSy7v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TMJwgiGu2b5tSy7v .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-TMJwgiGu2b5tSy7v .marker.cross{stroke:#0b0b0b;}#mermaid-svg-TMJwgiGu2b5tSy7v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TMJwgiGu2b5tSy7v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#fff;}#mermaid-svg-TMJwgiGu2b5tSy7v .cluster-label text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-TMJwgiGu2b5tSy7v .cluster-label span{color:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-TMJwgiGu2b5tSy7v .label text,#mermaid-svg-TMJwgiGu2b5tSy7v span{fill:#fff;color:#fff;}#mermaid-svg-TMJwgiGu2b5tSy7v .node rect,#mermaid-svg-TMJwgiGu2b5tSy7v .node circle,#mermaid-svg-TMJwgiGu2b5tSy7v .node ellipse,#mermaid-svg-TMJwgiGu2b5tSy7v .node polygon,#mermaid-svg-TMJwgiGu2b5tSy7v .node path{fill:#383838;stroke:#fff;stroke-width:1px;}#mermaid-svg-TMJwgiGu2b5tSy7v .node .label{text-align:center;}#mermaid-svg-TMJwgiGu2b5tSy7v .node.clickable{cursor:pointer;}#mermaid-svg-TMJwgiGu2b5tSy7v .arrowheadPath{fill:undefined;}#mermaid-svg-TMJwgiGu2b5tSy7v .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-TMJwgiGu2b5tSy7v .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-TMJwgiGu2b5tSy7v .edgeLabel{background-color:hsl(-120, 0%, 21.9607843137%);text-align:center;}#mermaid-svg-TMJwgiGu2b5tSy7v .edgeLabel rect{opacity:0.5;background-color:hsl(-120, 0%, 21.9607843137%);fill:hsl(-120, 0%, 21.9607843137%);}#mermaid-svg-TMJwgiGu2b5tSy7v .cluster rect{fill:hsl(180, 0%, 26.9607843137%);stroke:hsl(180, 0%, 16.9607843137%);stroke-width:1px;}#mermaid-svg-TMJwgiGu2b5tSy7v .cluster text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-TMJwgiGu2b5tSy7v .cluster span{color:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-TMJwgiGu2b5tSy7v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(180, 0%, 26.9607843137%);border:1px solid undefined;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TMJwgiGu2b5tSy7v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

RGB => YUV
YUV => RGB
编码
显示

采样模式

经过大量研究实验表明,视觉系统 对 色度 的敏感度 是远小于 亮度的。所以可以对 色度 采用更小的采样率来压缩数据,对亮度采用正常的采样率即可,这样压缩数据不会对视觉体验产生太大的影响,从而就由yuv444p产生了yuv422pyuv420p格式,它们的共同点都是都缩减了u、v分量的空间

  • 4-4-4 一个像素占 3 个字节
  • 4-2-2 平均一个像素占 2 个字节
  • 4-2-0 平均一个像素占 1.5 个字节

YUV格式

  • planner :平面格式, 先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V. (比如yuvj444pp就是planner)
  • semi-Planar:半平面的YUV格式,Y分量单独存储,但是UV分量交叉存储
  • packed :每个像素点的Y,U,V是连续交错存储的,和RGB的存放方式类似(对于YUV4:2:2格式而言,用紧缩格式很合适的,因此就有了UYVY、YUYV等)

这里的连续存储,不是一行像素里面连续存储,是整张图片的连续存储,例如xx444.yuv 图片是 6075kb 大小

  • 第 1~ 2025kb 都是 Y 数据
  • 第 2026 ~ 4050kb 都是 U 数据
  • 第 4051 ~ 6075kb 都是 V 数据

文件大小

Y U V 444 = S I Z E = 1920 ∗ 1080 ∗ 3 YUV444=SIZE=1920*1080*3 YUV444=SIZE=1920∗1080∗3

Y U V 422 = S I Z E = 1920 ∗ 1080 ∗ 2 YUV422=SIZE=1920*1080*2 YUV422=SIZE=1920∗1080∗2

Y U V 420 = S I Z E = 1920 ∗ 1080 ∗ 3 / 2 YUV420=SIZE=1920*1080*3/2 YUV420=SIZE=1920∗1080∗3/2

比如文件lena_256x256_yuv420p.yuv因为是420p那么文件大小就是就是w*h*3/2

$ du -k lena_256x256_yuv420p.yuv                               [~/demo]
96  lena_256x256_yuv420p.yuv
$ expr $(expr 256 \* 256 \* 3 / 2) / 1024                      [~/demo]
96

为了验证结果用vim打开文件,然后执行%!xxd转换成16进制,这里截取末尾

.....
00017fb0: b5b6 b4b4 b5b3 b4b5 b5b6 b4b6 b7b6 b6b7  ................
00017fc0: b6b5 b3b4 b3b2 b1b2 b1b1 afaf aeaf adad  ................
00017fd0: aaab a8a5 a5a2 a09b 9895 9493 9292 9199  ................
00017fe0: 9f9e a09f a0a5 acb0 b1b0 afb1 b0aa a7a2  ................
00017ff0: 9e9f a0a0 a2a2 9e9b 9c9d 9c9e 9e9f a6ae  ................
00018000: 0a

可以看到文件所有的数据长度为0x18000byte => 十进制98304byte => 十进制96kb

文件格式

yuv 图片文件除了 原始的像素数据,要正确打开一个YUV图片需要指定它的宽、高、采样格式

像素排列顺序

yuv 图片的像素存储顺序 是从 左上角 到 右下角的

分离YUV

分离YUVxxxP像素数据中的Y、U、V分量

存储顺序为 planner 文件中各分量所处位置,存在以下文件

$ du lena_256x256_yuv4*                                        [~/demo]
192 lena_256x256_yuv420p.yuv
256 lena_256x256_yuv422p.yuv
384 lena_256x256_yuv444p.yuv
  • yuv444p
y y y y u u u u v v v v
y y y y u u u u v v v v
 unsigned char *pic=(unsigned char *)malloc(w*h*3);fread(pic,1,w*h*3,fp);//Yfwrite(pic,1,w*h,fp1);//Ufwrite(pic+w*h,1,w*h,fp2);//Vfwrite(pic+w*h*2,1,w*h,fp3);
  • yuv422p
y y y y u u v v - - - -
y y y y u u v v - - - -

第一个像素跟第二个像素 共享一个UV。

422 格式里面的 U 值是新值,是由第一个像素跟第二个像素的 U 值加起来除以 2 得到的,所以 422 格式的 UV 值 其实是平均值。

所以,422 格式,是第一第二个像素共享 一组UV,第三第四像素共享 一组 UV

 unsigned char *pic=(unsigned char *)malloc(w*h*2);fread(pic,1,w*h*2,fp);//Yfwrite(pic,1,w*h,fp1);//Ufwrite(pic+w*h,1,w*h*1/2,fp2);//Vfwrite(pic+w*h*3/2,1,w*h*1/2,fp3);
  • yuv420p
y y y y u v - - - - - -
y y y y u v - - - - - -

第一行的第1,第2 像素,第二行的 第1,第2 像素共用一组 UV

不是第一行的 第 1~ 4 个像素共享 一组 UV,因为这样会造成空间距离太远,容易造成体验不太好

这些 UV 值都是新值,是以前的 4 个像素的 U 值加起来 除以4 的平均值

     unsigned char *pic=(unsigned char *)malloc(w*h*3/2);fread(pic,1,w*h*3/2,fp);//Y  这里Y分量正常取前面总文件大小的1/3fwrite(pic,1,w*h,fp1);//U  因为一个U是4个Y共享fwrite(pic+w*h,1,w*h/4,fp2);//V 同理一个V由4个Y共享fwrite(pic+w*h*5/4,1,w*h/4,fp3);

分离后

使用ffmpeg分离分量

ffmpeg -s 256x256 -pix_fmt yuv420p -i lena_256x256_yuv420p.yuv ff_420.y
#默认是yuv420p 这里也可不指定格式
ffmpeg -s 256x256 -pix_fmt yuv444p -i lena_256x256_yuv444p.yuv ff_444.y
#444p
ffmpeg -s 256x256 -pix_fmt yuv422p -i lena_256x256_yuv422p.yuv ff_422.y
#422p

测试结果是否准确

$ cmp output_420_y.y ff_420.y                                  [~/demo]
$ cmp output_420_u.y ff_420.U                                  [~/demo]
$ cmp output_420_v.y ff_420.V                                  [~/demo]$ cmp output_444_y.y ff_444.y                                  [~/demo]
$ cmp output_444_u.y ff_444.U                                  [~/demo]
$ cmp output_444_v.y ff_444.V                                  [~/demo]$ cmp output_422_y.y ff_422.y                                  [~/demo]
$ cmp output_422_u.y ff_422.u                                  [~/demo]
$ cmp output_422_v.y ff_422.v                                  [~/demo]#没有输出不一样的地方,说明提取正确
ffplay -s 256x256 -vf extractplanes='y' lena_256x256_yuv420p.yuv
#播放y分离,u,v同理

YUV420P像素数据去掉颜色(灰度图)

各分量 Y- 亮度,U-色调,V-色饱和度(彩度),U、V是图像中的经过偏置处理的色度分量。

因为要去掉图像的颜色所以需要将U、V分量置为无色,也就是Cr、Cb构成的二维坐标中的原点值即(128,128)

    unsigned char *pic=(unsigned char *)malloc(w*h*3/2);fread(pic,1,w*h*3/2,fp);//U V分量取值为128memset(pic+w*h,128,w*h/2);fwrite(pic,1,w*h*3/2,fp1);

YUV420P像素数据亮度减半

这个简单,将前面w*hY分量读出来然后取半值再进行写入(bb 一句 c语言可以直接操作内存和无缝嵌入汇编的特性不亏是写算法的瑞士军刀,现在其他语言限制太多了,这一点又想起了Darwin、linux、Windows不得不说 Linux + C + GNU-Coreutils的组合用起来那真是一个随心所欲)

 unsigned char *pic=(unsigned char *)malloc(w*h*3/2);int y = w*h;int u = w*h/4;int v = w*h/4;fread(pic,1,w*h*3/2,fp);unsigned char tmp = {0};for(int i=0;i<y;i++){tmp = pic[i]/2;pic[i] = tmp;}fwrite(pic,1,w*h*3/2,fp1);

YUV420P像素数据的周围加边框

这个也没什么好说的就是计算Y分量的起始值范围

 unsigned char *pic=(unsigned char *)malloc(w*h*3/2);int y = w*h;int u = w*h/4;int v = w*h/4;fread(pic,1,w*h*3/2,fp);unsigned char tmp = {0};int border = 5;for(int i=0;i<y;i++){if( i <= border*h||i >= (h-border)*h ||i%w <= border ||i%w >= (w-border)){pic[i] = tmp;}}fwrite(pic,1,w*h*3/2,fp1);

扩展:给bitmap图片加边框

//        int border = 50;
//        int h = (bitmapHeight * 4);
//        for (int i = 0; i < bytes.length; i++) {
//            if (i <= border * h ||
//                    i >= (bitmapHeight - border * 4) * h ||
//                    i % (bitmapWidth * 4) <= border * 4 ||
//                    i % (bitmapWidth * 4) >= (bitmapWidth * 4 - border * 4))
//                bytes[i] = 0;
//        }

生成YUV420P格式的灰阶测试图

 float lum_inc;int y = width*height;int u = width*height/4;int v = width*height/4;int barwidth=width/barnum;lum_inc=((float)(ymax))/((float)(barnum-1));unsigned char lum_temp;unsigned char* data_y=(unsigned char *)malloc(y);unsigned char* data_u=(unsigned char *)malloc(u);unsigned char* data_v=(unsigned char *)malloc(v);fp=fopen(url_out,"wb+");for(int j=0;j<height;j++){for(int i=0;i<width;i++){int index = i / barwidth;lum_temp = index*lum_inc;data_y[j*width+i]=(char)lum_temp;}}memset(data_u,128,u);memset(data_v,128,v);fwrite(data_y,y,1,fp);fwrite(data_u,u,1,fp);fwrite(data_v,v,1,fp);

计算两个YUV420P像素数据的PSNR

这里需要求一个MSE值PSNR值。这个算法打算在后面和SSIM、VMAF一起深入研究

  • M S E MSE MSE计算流程
#mermaid-svg-SaSfDqZg7EUt0YFp {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#fff;}#mermaid-svg-SaSfDqZg7EUt0YFp .error-icon{fill:hsl(180, 0%, 26.9607843137%);}#mermaid-svg-SaSfDqZg7EUt0YFp .error-text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);stroke:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-SaSfDqZg7EUt0YFp .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-SaSfDqZg7EUt0YFp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SaSfDqZg7EUt0YFp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SaSfDqZg7EUt0YFp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SaSfDqZg7EUt0YFp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SaSfDqZg7EUt0YFp .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-SaSfDqZg7EUt0YFp .marker.cross{stroke:#0b0b0b;}#mermaid-svg-SaSfDqZg7EUt0YFp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SaSfDqZg7EUt0YFp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#fff;}#mermaid-svg-SaSfDqZg7EUt0YFp .cluster-label text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-SaSfDqZg7EUt0YFp .cluster-label span{color:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-SaSfDqZg7EUt0YFp .label text,#mermaid-svg-SaSfDqZg7EUt0YFp span{fill:#fff;color:#fff;}#mermaid-svg-SaSfDqZg7EUt0YFp .node rect,#mermaid-svg-SaSfDqZg7EUt0YFp .node circle,#mermaid-svg-SaSfDqZg7EUt0YFp .node ellipse,#mermaid-svg-SaSfDqZg7EUt0YFp .node polygon,#mermaid-svg-SaSfDqZg7EUt0YFp .node path{fill:#383838;stroke:#fff;stroke-width:1px;}#mermaid-svg-SaSfDqZg7EUt0YFp .node .label{text-align:center;}#mermaid-svg-SaSfDqZg7EUt0YFp .node.clickable{cursor:pointer;}#mermaid-svg-SaSfDqZg7EUt0YFp .arrowheadPath{fill:undefined;}#mermaid-svg-SaSfDqZg7EUt0YFp .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-SaSfDqZg7EUt0YFp .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-SaSfDqZg7EUt0YFp .edgeLabel{background-color:hsl(-120, 0%, 21.9607843137%);text-align:center;}#mermaid-svg-SaSfDqZg7EUt0YFp .edgeLabel rect{opacity:0.5;background-color:hsl(-120, 0%, 21.9607843137%);fill:hsl(-120, 0%, 21.9607843137%);}#mermaid-svg-SaSfDqZg7EUt0YFp .cluster rect{fill:hsl(180, 0%, 26.9607843137%);stroke:hsl(180, 0%, 16.9607843137%);stroke-width:1px;}#mermaid-svg-SaSfDqZg7EUt0YFp .cluster text{fill:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-SaSfDqZg7EUt0YFp .cluster span{color:rgb(186.2500000001, 186.2500000001, 186.2500000001);}#mermaid-svg-SaSfDqZg7EUt0YFp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(180, 0%, 26.9607843137%);border:1px solid undefined;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SaSfDqZg7EUt0YFp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}

得到对比文件的间隔值
将间隔值进行平方求和
得到其值在各分量中的比重

M S E = 1 w ⋅ h ∑ i = 0 w − 1 ∑ j = 0 h − 1 [ i m g 1 ( i , j ) − i m g 2 ( i , j ) ] 2 MSE = \frac{1}{w \cdot h } \sum_{i=0}^{w-1} \sum_{j=0}^{h-1} \left[img_1(i,j) - img_2(i,j)\right]^2 MSE=w⋅h1​i=0∑w−1​j=0∑h−1​[img1​(i,j)−img2​(i,j)]2

  • P S N R PSNR PSNR就是基于这个值

P S N R = 10 ⋅ ln ⁡ [ ( 2 n − 1 ) 2 M S E ] PSNR = 10 \cdot \ln{\left[\frac{(2^n - 1)^2}{MSE}\right]} PSNR=10⋅ln[MSE(2n−1)2​]
其中 n 就是 b i t 位值,一般情况下是 8 b i t , 也就是 n = 8 \textcolor{red}{其中n就是bit位值,一般情况下是8bit,也就是n=8} 其中n就是bit位值,一般情况下是8bit,也就是n=8

程序引入数学库就很简单了

 unsigned char *pic1=(unsigned char *)malloc(w*h);unsigned char *pic2=(unsigned char *)malloc(w*h);fread(pic1,1,w*h,fp1);fread(pic2,1,w*h,fp2);double mse_sum=0,mse=0,psnr=0;for(int j=0;j<w*h;j++){//得到两个对比文件y值的-间隔值平方之和mse_sum+=pow((double)(pic1[j]-pic2[j]),2);}//得到这个-间隔值平方之和-在y分量中的比重mse=mse_sum/(w*h);unsigned int bit = pow(2,8);psnr=10*log10(pow((bit - 1),2)/mse);printf("%5.3f\n",psnr);

RGB-YUV相互转换

因为RGB中的每个分量都表示颜色值,所有不同的颜色值亮度也不同使RGB三个分量都与亮度相关,根据分量占有不同比重的亮度提取存在如下公式:
Y = K r ⋅ R + K g ⋅ G + K b ⋅ B Y = Kr \cdot R + Kg \cdot G + Kb \cdot B Y=Kr⋅R+Kg⋅G+Kb⋅B

  • K K K :权重因子

  • K r Kr Kr :Red通道的权重

  • K g Kg Kg :Green通道的权重

  • K b Kb Kb :Bule通道的权重

既然是权重则有 K r + K g + K b = 1 Kr + Kg + Kb = 1 Kr+Kg+Kb=1

K权重因子会影响压缩率,所以K的具体取值是根据不同的标准而定的,现有如下标准 :

BT Kr Kg Kb
601(SDTV标清) 0.299 0.587 0.114
709(HDTV高清) 0.2126 0.7152 0.0722
2020(UHDTV超高清) 0.2627 0.6780 0.0593

BT.601

因为市面上的文章都是针对BT.601进行讲解,为了方便资料查阅,这里也使用该标准进行实验

求Y

按照权重比可知.在 R 通道里面有 29.9% 的亮度信息,在 G通道 有 58.7% 的亮度信息,在 B通道 有 11.4% 的亮度信息,那么可得公式如下:

Y = 0.299 R + 0.587 G + 0.114 B Y = 0.299R + 0.587G + 0.114B Y=0.299R+0.587G+0.114B

求U

为了对标专业理论知识,这里用Cr来表示U。上面YUV讲解也有讲到

Cr 的定义是 R - Y,求解就很简单了:
∵ C r = R − Y ∴ C r = R − ( 0.299 R + 0.587 G + 0.114 B ) = R − 0.299 R − 0.587 G − 0.114 B = 0.701 R − 0.587 G − 0.114 B 当 R 值最大 G 、 B 值最小时 , 有最大值 , 在不同的格式中 R 、 G 、 B 的值不同,这里采取函数表达 ∵ R 、 G 、 B 的取值范围都是一样所以设 X = m a x , 最小值都为 0 ∴ C r m a x = 0.701 X − 0 − 0 = 0.701 X ∴ C r m i n = 0 − 0.587 X − 0.114 X = − 0.701 X \because Cr = R - Y \\ \therefore Cr = R - (0.299R + 0.587G + 0.114B) \\ = R - 0.299R - 0.587G - 0.114B \\ = 0.701R - 0.587G - 0.114B\\ 当R值最大G、B值最小时,有最大值,在不同的格式中R、G、B的值不同,这里采取函数表达\\ \because R、G、B的取值范围都是一样所以设X = max ,最小值都为0 \\ \therefore Cr_{max} = 0.701X - 0 - 0 = 0.701X \\ \therefore Cr_{min} = 0 - 0.587X - 0.114X = -0.701X ∵Cr=R−Y∴Cr=R−(0.299R+0.587G+0.114B)=R−0.299R−0.587G−0.114B=0.701R−0.587G−0.114B当R值最大G、B值最小时,有最大值,在不同的格式中R、G、B的值不同,这里采取函数表达∵R、G、B的取值范围都是一样所以设X=max,最小值都为0∴Crmax​=0.701X−0−0=0.701X∴Crmin​=0−0.587X−0.114X=−0.701X
所以上述可知Cr的值域为 [ − 0.701 X , 0.701 X ] [-0.701X,0.701X] [−0.701X,0.701X],那么它的增量可以为
2 ∗ 0.701 X ∗ 10 = 1.402 X 2*0.701X*10 = 1.402X 2∗0.701X∗10=1.402X

这里挖个坑,具体求值后面再来填,有点奇怪这里,需要参考一些其他资料

雷神这里的公式可以转换成功,那就先用着
Y = 0.299 R + 0.587 G + 0.114 B U = − 0.147 R + 0.289 G + 0.463 B V = 0.615 R − 0.515 G − 0.100 B Y = 0.299R+0.587G+0.114B \\ U = -0.147R +0.289G+ 0.463B\\ V = 0.615R-0.515G-0.100B Y=0.299R+0.587G+0.114BU=−0.147R+0.289G+0.463BV=0.615R−0.515G−0.100B

//R\G\B : 1byte : unsigned : 0xff
unsigned int64 tmp = 66*r + 129*g + 25*b
//转换一下
//max = 66*0xff + 129*0xff + 26*0xff = 0xdc23
//min = 66*0 + 129*0 + 26*0 = 0
//可以看到由R、G、B转过来的Y值范围是[0,0xdc23],可能是1-2字节。最大也就是2个字节unsigned int64 tmp2 = tmp + 128
//max = 0xdc23 + 0x80 = 0xdca3
//carry = 0xdb83 + 0x80 = 0xdc03
//min = 0 + 0x80 = 0x80
//解释一下为什么是+128,因为128是二进制的第8位,加了128就可以判断低位字节是否>=128,当>=128时则会产生进位,反之不影响高位字节unsigned int64 tmp3 = tmp2 >> 8
//这里就是去掉了低字节
//Y = 0xdc03 >> 8 = 0xdc//再回到公式
//Y = 0.299R + 0.587G + 0.114B ,C=1
//Y = 66R + 129G + 25B, C=220
//它们的系数相加是1,

AVC视频码流解析

类型帧

I帧

Intra-coded picture(帧内编码图像帧

关键帧,可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成

I帧“关键帧”压缩法 : 基于离散余弦变换DCT(Discrete Cosine Transform)的压缩技术,这种算法与JPEG压缩算法类似。采用I帧压缩可达到1/6的压缩比而无明显的压缩痕迹。

当不是直播的时候一般B帧为连续2帧,从而降低码率

  • 每次收到I帧就可以清除上一个I帧

  • 在视频画面播放过程中,若I帧丢失了,则后面的P帧也就随着解不出来,就会出现视频画面黑屏的现象;若P帧丢失了,则视频画面会出现花屏、马赛克等现象。

P帧

Predictive-coded Picture(前向预测编码图像帧

P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别(基于预测值和运动矢量),解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面

B帧

Bidirectionally predicted picture(双向预测编码图像帧)

B帧不管在哪个位置都需要等后面一帧解完才能解码B帧,所以会带来编码延迟,因为B帧要等P帧解码完成

I B P
解码DTS 1 3 2
显示PTS 1 2 3

实现这个功能是通过下面的PTS、DTS的

B帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确。所以比较节省内存

直播:

压缩和解码B帧时,由于要双向参考,所以它需要缓冲更多的数据,且使用的CPU也会更高,实时互动直播系统中,很少使用B帧。

GOP

编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures )

解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束

GOP可以理解为I帧间隔,一般直播GOP为1-2秒

GOP越长视频压缩效率越高,视频质量越差

直播时如果帧率为25,则一般GOP为25(1秒)、50(2秒)帧率的倍数

一帧图像可以分为多个slice通过系统线程并发编码从而加快编码速度

量化值和码率有关,量化越大图片损失越重,反之

网络好使用低码率编码进行传输,反之。所以可以使用动态修改码率,其实就是修改量化值

PTS、DTS

  • DTS(Decoding Time Stamp)解码时间戳

这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。

  • PTS(Presentation Time Stamp)显示时间戳

这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

在没有B帧的情况下,存放帧的顺序和显示帧的顺序就是一样的,PTS和DTS的值也是一样的。

H264(AVC)

背景

  • ITU-T称为H.264

    • 该组织制定了H.120H.261H.263等标准
  • ISO称为AVC(Advanced Video Coding)
    • 该组织制定了MPEG-1MPEG-2MPEG-4 Part2等标准
  • 2003年两家组合联合起来成立JVT 2001组织
    • 2003年制定了H264/AVC
    • 2013年制定了H265/HEVC

以下简称h264是一款收费编码器,设备生产商的设备

  • <10w 免费
  • >=10w 0.2美元/台
  • >=500w 0.1美元/台(上限970w美元)

H.264和x264的区别:

  • H.264是一种编码标准,x264是符合H.264标准的一个开源项目是免费的。

    可以理解为一个社区版,但是不支持某些H.264的高级特性

    H.264是商业版

  • X264不支持硬件加速

NALU

在 H.264/AVC 视频编码标准中,NAL 单元(Network Abstraction Layer Unit)是视频编码数据的基本单位,它是一个完整的数据包,包含了视频编码数据的一部分。

NAL 单元被分为两类:

  • 一类是 VCL NAL 单元(slice下面会说)
  • 一类是非 VCL NAL 单元

VCL NAL 单元包括了编码后的视频图像数据,而非 VCL NAL 单元包含了控制信息、参数设置等其他数据。

一个NALU有很多种类型,比如在FFmpeg的h264dec.c源码中:

     switch (nal->type) {case H264_NAL_SLICE://非 IDR 帧的 slice,也包含了完整的编码数据。case H264_NAL_DPA、H264_NAL_DPB、H264_NAL_DPC://用于多切片编码,分别表示当前的前一个、后一个和后两个 slice 的数据。case H264_NAL_SEI://包含了一些辅助信息,如时间戳等。case H264_NAL_PPS、H264_NAL_SPS_EXT://包含了图像参数集和序列参数集的数据。case H264_NAL_AUD://用于视频解码的同步,表示一个新的图像开始了。case H264_NAL_END_SEQUENCE、H264_NAL_END_STREAM、H264_NAL_FILLER_DATA://用于结束一个视频序列或流。case H264_NAL_AUXILIARY_SLICE://表示一个辅助的 slice,包含了一些额外的数据。}

结构

H264中没有音频、时间戳、只有图片信息,所以也是大家口中所说的码流、或者裸流,H264中分为两种不同的存储格式:1.AnnexB 2.avcC

注意:解码的环境下

  • ios中ViedeoToolbox只支持avcC格式
  • android中MediaCodec只支持AnnexB格式

AnnexB

它由一个个的NALU(Network Abstraction Layer Unit)组成,结构如下:

图中每个NALU之间通过startcode(起始码)进行分隔,起始码分为:

  • 00 00 01(3byte)
  • 00 00 00 01(4byte)

NALU对应的Slice为一帧数使用4byte的起始码,否则使用3byte的起始码分离出NALU

因为startcode可能会在码流中找到相同的字节,那么就会导致一个NALU被再次分割为消NALU,所以就出现了防竞争字节序

防竞争字节序

防竞争字节序就是在给NALU添加起始码之前,先对码流中的相同startcode进行字节替换

比如:码流中的000\001\002\003等先给它替换成为其他字节,最后再还原
00 ∣ 00 ∣ 00 → 00 ∣ 00 ∣ 03 ∣ 00 00 ∣ 00 ∣ 01 → 00 ∣ 00 ∣ 03 ∣ 01 00 ∣ 00 ∣ 02 → 00 ∣ 00 ∣ 03 ∣ 02 00 ∣ 00 ∣ 03 → 00 ∣ 00 ∣ 03 ∣ 03 00|00|00 \rightarrow 00|00|03|00\\ 00|00|01 \rightarrow 00|00|03|01\\ 00|00|02 \rightarrow 00|00|03|02\\ 00|00|03 \rightarrow 00|00|03|03\\ 00∣00∣00→00∣00∣03∣0000∣00∣01→00∣00∣03∣0100∣00∣02→00∣00∣03∣0200∣00∣03→00∣00∣03∣03

avcC

avcC主要用于mp4容器中。它采用的分割格式是在NALU前面的字节表示单个NALU的长度(大端模式)

在avcC格式中存在两个特殊的NALU即SPSPPS它们存放了在解码H264码流时必要的参数信息,在解码前必须获取到SPSPPS

avcC文件头部信息格式
bits name description
8 version 一般为1
8 avc profile 首个SPS的第1个字节
8 avc compatibility 首个SPS的第2个字节
8 avc level 首个SPS的第1个字节
6 reserved
2 NALULengthSizeMinusOne NULU长度-1(范围0~3)
3 reserved
5 number of SPS NALUs 有几个SPS,一般为1
16 SPS size SPS长度
SPS NALU data SPS NALU的数据
SPS_2 size
SPS_2 NALU data
SPS_n size
SPS_n NALU data
8 number of PPS NALUs 有几个PPS,一般为1
16 PPS size PPS长度
PPS NALU data PPS NALU的数据
NALULengthSizeMinusOne

NALULengthSizeMinusOne为3的时候,NALU的长度为4个字节,就是说NALU的前4个字节存放着它本身的数据长度值(大端模式、且该4个字节表示的值不包括该4个字节)

主体NALU

NALU中除去startCode的部分被称为EBSP数据比特串

NALU中除去startCode、防竞争字节的部分叫RBSP数据比特串

RBSP中除去补位bit的部分叫SODB数据比特串:

视频在编码是以bit为单位写入码流的(用户在输入时候是以字节为单位的),当写入的bit不满足8的倍数即剩余的bit不满一个byte。那么会在后续操作中不便于读取,所以在构建RBSP时会将最后不满一个byte的情况进行补齐,规则为:

先写入 1 bit 数据,数据内容是 1,然后开始补齐 0,直到补齐到一整个字节。

而当最后满了一个字节也会对它进行添加1一个字节操作00000001

最后的SODB就是裸码流数据

结构图如下:

NALU的第一个字节

nal_unit(NumButeslnNaLunit) Descriptor
forbidden_zero_bit 1 bit 静止位。当NALU丢失数据时为1,一般为0
nal_ref_idc 2 bit NALU重要性
nal_unit_type 5 bit NALU类型

nal_ref_idc存在以下值:

  • HIGHEST - 3
  • HIGH - 2
  • LOW - 1
  • DISPOSABLE - 0

当nal_ref_idc为0时则表示该NALU可有可无,即使丢失也不重要

nal_unit_type存在以下值:

nal_unit_type NALU 类型
0 未定义
1 非 IDR SLICE
2 非 IDR SLICE,采用 A 类数据划分片段
3 非 IDR SLICE,采用 B 类数据划分片段
4 非 IDR SLICE,采用 C 类数据划分片段
5 IDR SLICE
6 补充增强信息 SEI
7 序列参数集 SPS
8 图像参数集 PPS
9 分隔符
10 序列结束符
11 码流结束符
12 填充数据
13 序列参数扩展集
14~18 保留
19 未分割的辅助编码图像的编码条带
20~23 保留
24~31 未指定

读取SPS信息

seq_parameter_set_data() C Descriptor
profile_idc 0 u(8)
constraint_set0_flag 0 u(1)
constraint_set1_flag 0 u(1)
constraint_set2_flag 0 u(1)
constraint_set3_flag 0 u(1)
constraint_set4_flag 0 u(1)
constraint_set5_flag 0 u(1)
reserved_zero_2bits 0 u(2)
level_idc 0 u(8)
seq_parameter_set_id 0 ue(8)

slice

H.264 视频编码标准中,slice 是 VCL NAL 单元中的一个类型,包含了编码后的视频图像数据的一部分。每个 slice 包含了一个或多个宏块(macroblock),它们是编码后的视频图像数据的最小单位。

一个 NAL 单元和一个 slice 的区别在于:

  1. NAL 单元是视频编码数据的基本单位,而 slice 是包含了视频图像数据的一部分的 NAL 单元类型
  2. 每个 slice 包含了一个或多个宏块,而一个 NAL 单元可以包含一个或多个 slice。

宏块

H.264 中,不会逐个像素去处理图像,而是按照固定的长宽划分成小块称之为宏块

当像素为176x144时,规定一个宏块为16x16像素,那么就会划分为11x9的宏块

这里的格式是 宽x高,不是矩阵中的 行x列 ,刚好相反

HEVC视频码流解析

AV1视频码流解析

全称:AOMedia Video 1

文件后缀.avif或者.av1 .而.webm文件则是将AV1视频编码器和WebM音频编码器结合使用的文件格式

类型:一个开放、免专利的视频编码格式,专为通过网络进行[流传输,即流媒体]而设计

背景:2018年取代其前身Gooole的VP9,也就是基于VP9开发的一个分支,AV1所使用的编码技术主要来源于谷歌VP10

编码器:

  • rav1e:以牺牲编码效率为代价,成为最简单、最快符合AV1标准的视频编码器

起始由Mozilla(旗下Firefox浏览器)主导,现在主要由VideoLan接管的AV1开源编码器

  • SVT-AV1

由Intel、Netflix主导的AV1开源编解码器,目前已被AOM 确定为基本代码库,致力于开发可落地实用场景的AV1软件编解码器

  • libaom->aomenc

AV1标准制定过程中的主要开源编解码代码库,由AOM联合打造、维护,是目前AV1工具实现最完整的一款开源软件编解码器

  • Aurora(微帧)

解码器:

  • libaom->aomdec
  • SVT-AV1
  • libgav1(google,特别适合Android)
  • DAV1D(videolan\ffmpeg,AOM资助)

业内公认的性能优秀的AV1开源软件解码方案

支持:

  • 硬件(2020以来)

    • Intel、AMD、NVIDIA、MediaTek、SAMSUNG
    • Apple暂不支持
  • 软件
    • Firefox浏览器,Chromium浏览器内核,微软Windows10平台,Android Q系统。FFmpeg也早已支持AV1
  • 视频
    • YouTube、Netflix、爱奇艺,爱奇艺也于今年4月上线AV1,成为国内首家应用AV1视频网站

FFmpeg:

https://www.cnblogs.com/cjdty/p/16904134.html

#coufigure : --enable-libaomffmpeg -i test.webm  -c:v libx264 -c:a libfdk_aac -f mp4 test.mp4 -y

AV1、HEVC编解码格式对比

https://www.bilibili.com/read/cv20079800

AV1确实有着取代HEVC的实力:

  • 质量上超越HEVC的同时,编码耗时也比HEVC短很多,甚至比H264耗时还要短一点。
  • AV1在低码率下的压缩率非常高,这也让它非常适用于流媒体。
  • 相对应的HEVC,虽然在低码率下比H264质量高很多,但是一旦码率超过一万,其对比H264的提升就微乎其微了,并且导出耗时远高于H264

AV1与HEVC的结论

  • 对于相同的图像质量,AV1可以节省高达30%的文件大小(压缩率高
  • AV1编解码器提供比H.265/HEVC更好的图像质量(质量高
  • AV1编解码器需要HEVC/H.265编码时间的3倍(编解码时间慢)
  • HEVC由AMD、英伟达、英特尔、苹果、高通等的GPU/CPU支持。对AV1解码的支持已经在这里,但到目前为止,AV1编码仅由英伟达和英特尔支持。(兼容差

未来

当HEVC发布的H.265比流行的H.264高50%时,H.264并没有立即死亡并过时

音频

音频的常见格式:

  • 原始格式: 原始格式通常不经过压缩,保留了音频的所有信息,常见的原始格式包括:

    • PCM(脉冲编码调制):是一种最基本的音频格式,是数字音频的基础,通常没有压缩,采用的是无损编码。

    • WAV(Waveform Audio File Format):一种由Microsoft开发的无损音频文件格式,基于PCM编码,广泛应用于Windows平台。

    • AIFF(Audio Interchange File Format):一种由Apple公司开发的无损音频文件格式,也基于PCM编码,主要应用于Mac平台。

  • 压缩格式: 压缩格式通常是为了减小文件大小而采用的,其中有损压缩和无损压缩两种方式,常见的压缩格式包括:

    • MP3(MPEG-1 Audio Layer III):一种有损压缩音频格式,采用的是基于FFT的压缩算法,常用于音乐和语音的存储和传输。

    • AAC(Advanced Audio Coding):一种有损压缩音频格式,采用了更为先进的压缩算法,是当前最常用的音频压缩格式之一,广泛应用于音乐、电影、手机等领域。

    • WMA(Windows Media Audio):一种由Microsoft开发的音频压缩格式,支持有损和无损压缩,主要用于Windows平台。

  • 其他格式: 除了以上两种常见格式外,还有一些其他格式,如:

    • FLAC(Free Lossless Audio Codec):一种无损压缩音频格式,可以在不降低音频质量的前提下减小文件大小,适合用于高品质音频的存储和传输。

    • OGG(Ogg Vorbis):一种免费开源的音频压缩格式,采用无损压缩方式,常用于网络音乐和在线播放。

    • DSD(Direct Stream Digital):一种超高保真度的无损音频格式,采用1比1的编码方式,常用于SACD(超级音频CD)等高端音频存储媒介。

ffplay播放一个pcm需要指定采样率、采样格式、声道数,比如从下面mov文件中提取的pcm:

$ mediainfo input.mov
//...
Audio
ID                                       : 2
Format                                   : PCM
Format settings                          : Little / Signed
Codec ID                                 : lpcm
Duration                                 : 2 s 135 ms
Source duration                          : 2 s 138 ms
Bit rate mode                            : Constant
Bit rate                                 : 705.6 kb/s
Channel(s)                               : 1 channel //声道数
Sampling rate                            : 44.1 kHz  //采样率
Bit depth                                : 16 bits   //采样格式
Stream size                              : 184 KiB (7%)
Source stream size                       : 184 KiB (7%)
Title                                    : Core Media Audio
Encoded date                             : UTC 2023-03-09 13:47:05
Tagged date                              : UTC 2023-03-09 13:47:07

可以看出该PCM文件为单声道、44.1kHz采样率、16位采样格式

$ ffplay -f s16le -ar 44100 -ac 1 output.pcm
  • -f : 指定输入文件的格式为16位有符号整数(即s16le)

  • -ar : 指定采样率为44100Hz

  • -ac : 指定声道数为1

常用知识

音视频常见单词

super-resolution
compression
master
quality

音视频常见名词

码率 : kb/s 视频文件在单位时间内使用的数据流量

帧率 : fps一秒钟显示的帧数,帧率越高画面越流畅

分辨率 : 影响视频图像的大小

#修改帧率
ffmpeg -i test_video.mp4 -r 15 output.mp4 #导致画面卡顿,ppd
#修改码率
ffmpeg -i test_video.mp4 -b:v 0.5M output2.mp4 #会导致画面模糊
#修改分辨率
ffmpeg -i test_video.mp4 -s 640x480 output.mp4 #导致窗口变小

2k指的是2048x1080,2k和1080p分辨率差不多,大一点

当年的1920x1080 比这个标准略小,只能称为准2K,后来显示器出现了2560x1440这样的分辨率,它大于2048x1080,所以可以完整显示2K分辨率,而在当时,能完整显示2K电影分辨率的只有 2560x1440 这一个主流分辨率,后来以讹传讹,2560x1440就被称为了2K。

序号 图像分辨率 图像容器
240p 426x240
360p 640x360
480p 854x480
540p 960x540
720P 1280x720 HD(高清)
1080P 1920x1080 FHD(全高清)
2K 2048x1080 QHD(四倍高清)
4K 3840x2160 UHD(超高清)
8k 7680x4320
5k 5120x2880

慢速档位 : 每秒1帧
快速档位 : 每秒30帧

常见画质对比数值 :