H264编码详解上(H264编码详解上)

前言

本篇开始讲解大家最感兴趣的知识点 H264视频编码,大致分上中下3篇,包括各个知识点的讲解和实际编码的部分。

一、H264结构与码流解析1.1 H264结构图

H264编码详解上(H264编码详解上)(1)

上图H264结构中,一个视频图像编码后的数据叫做一帧,一帧由一个片(slice)或多个片组成,一个片又由一个或多个宏块(MB)组成,一个宏块由多个子块组成,子块即16x16的yuv数据。宏块是作为H264编码的基本单位

  • 场和帧:视频的一场或一帧可用来产生一个编码图像。
  • 片:每个图象中,若干宏块被排列成片的形式。片分为I片、B片、P片和其他一些片。
    • I片只包含I宏块,P片可包含P和I宏块,而B片可包含B和I宏块。
      • I宏块利用从当前片中已解码的像素作为参考进行帧内预测。
      • P宏块利用前面已编码图象作为参考图象进行帧内预测。
      • B宏块则利用双向的参考图象(前一帧和后一帧)进行帧内预测。
    • 片的目的是为了限制误码的扩散和传输,使编码片相互间是独立的。某片的预测不能以其它片中的宏块为参考图像,这样某一片中的预测误差才不会传播到其它片中去。
  • 宏块:一个编码图像通常划分成若干宏块组成,一个宏块由一个16×16亮度像素和附加的一个8×8 Cb和一个8×8 Cr彩色像素块组成。

C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

1.2 H264编码分层

H264编码分层,分为了2层.

  • NAL层: (Network Abstraction Layer,视频数据网络抽象层)
    • 它的作用是H264只要在网络上传输,在传输的过程每个包以太网是1500字节. 而H264的帧往往会大于1500字节的.所以就要进行拆包. 将一个帧拆成多个包进行传输.所有的拆包或者组包都是通过NAL层去处理的.
  • VCL层:(Video Coding Layer,视频数据编码层) 它的作用就是对视频原始数据进行压缩.
1.3 码流的基本概念
  • SODB:(String of Data Bits,原始数据比特流) ,长度不一定是8的倍数.它是由VCL层产生的.因为非8的倍数所以处理比较麻烦.
  • RBSP:(Raw Byte Sequence Payload,SODB trailing bits) .算法是在SODB最后一位补1.不按字节对齐补0. 如果补齐0,不知道在哪里结束.所以补1.如果不够8位则按位补0.
  • EBSP:(Encapsulate Byte Sequence Payload) .就是生成压缩流之后,我们还要在每个帧之前加一个起始位.起始位一般是十六进制的0001.但是在整个编码后的数据里,可能会出来连续的2个0x00.那这样就与起始位产生了冲突.那怎么处理了? H264规范里说明如果处理2个连续的0x00,就额外增加一个0x03.这样就能预防压缩后的数据与起始位产生冲突.
  • NALU: NAL Header(1B) EBSP.NALU就是在EBSP的基础上加1B的网络头.

EBSP解码的要点

  • 每个NAL前有一个起始码 0x00 00 01(或者0x00 00 00 01),解码器检测每个起始码,作为一个NAL的起始标识,当检测到下一个起始码时,当前NAL结束。
  • 同时H.264规定,当检测到0x00 00 01时,也可以表征当前NAL的结束。那么NAL中数据出现0x000001或0x000000时怎么办?H.264引入了防止竞争机制,如果编码器检测到NAL数据存在0x000001或0x000000时,编码器会在最后个字节前插入一个新的字节0x03,这样解码器检测到0x000003时,把03抛弃,恢复原始数据(脱壳操作)。
  • 解码器在解码时,首先逐个字节读取NAL的数据,统计NAL的长度,然后再开始解码。
1.4 详解NAL Unit

NALU详解结构图如下:

H264编码详解上(H264编码详解上)(2)

  • NAL 单元是由一个NALU头部 一个切片.
  • 切片又可以细分成"切片头 切片数据".
  • 每个切片数据包括了很多宏块.
  • 每个宏块包括了宏块的类型,宏块的预测,残差数据.
H264码流分层结构图

H264编码详解上(H264编码详解上)(3)

  • A Annex格式数据: 就是起始码 Nal Unit 数据
  • NAL Unit: NALU 头 NALU数据
  • NALU 主体: 是由切片组成.切片包括切片头 切片数据
  • Slice数据: 宏块组成
  • PCM类: 宏块类型 pcm数据,或者宏块类型 宏块模式 残差数据
  • Residual: 残差块
  • ⚠️ 这个图比较重要.大家可以多看看。

    C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

    二、VideoToolBox简介

    VideoToolBox是苹果iOS8.0后推出的原生的硬编码框架,利用硬件加速器,基于Core Foundation库函数(它是C语言编写的)。

    2.1 使用步骤

    我们一般使用VideoToolBox框架,需要做的事情包括

    1. 创建session -> 设置编码相关参数 -> 开始编码 ->循环输入源数据(YUV 类型的数据,直接从摄像头获取)->获取编码后的H264数据 ->结束编码
    2. 构建H264文件,网络传输中其实也是H264文件
    2.2 基本的数据结构

    H264编码详解上(H264编码详解上)(4)

    CMSampleBuffer中有编码和解码2种情况,它们有区别
    • 编码后 数据存储在CMBlockBuffer中,其中流数据就是从这里获取的
    • 未编码 数据存储在CVPixelBuffer中
    2.4 编码的过程

    H264编码详解上(H264编码详解上)(5)

    上图中,通过视频编码,将原始数据编码生成H264流数据,但是,不是说拿到了h264数据就能直接交给解码器去处理,解码器只能处理的是h264文件数据。

    2.3 h264文件

    H264编码详解上(H264编码详解上)(6)

    上图中
    • 首先是SPS和PPS,解码时需优先解码SPS和PPS,才能接着对后面的数据进行解析。
    • 接着是I B P帧,可参考03-视频编码的## 七、H264相关概念。
    • 不管你使用那种框架编解码,如VideoToolBox、FFmpeg、硬编码等,不管你是哪种平台,如mac、windows或移动端,都需要遵循H264文件这种格式去进行。
    SPS 和 PPS

    序列参数集SPS(Sequence Parameter Sets)

    H264编码详解上(H264编码详解上)(7)

    图像参数集PPS(Picture Parameter Sets)

    H264编码详解上(H264编码详解上)(8)

    H264编码详解上(H264编码详解上)(9)

    H264编码详解上(H264编码详解上)(10)

    这些仅了解即可。

    2.4 判断帧类型 I B P

    我们知道,视频是由一帧一帧的画面组成,而帧又是一片或多片的数据组成,在网络传输的过程中,一片的数据可能很大,需要拆包发送,接收后再组包,那么问题来了:

    如何判断识别帧类型,区分 I B P帧呢?

    H264编码详解上(H264编码详解上)(11)

    H264编码详解上(H264编码详解上)(12)

    H264编码详解上(H264编码详解上)(13)

    C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

    三、NALU单元数据详解

    NALU = NAL Header NAL Body

    H264码流在网络中传输实际是以NALU的形式进行传输的,每个NALU由1个字节的Header和RBSP组成,如下图

    H264编码详解上(H264编码详解上)(14)

    3.1 NAL Header解析

    NAL Header为1个字节,占8位,那这8位里面到底包含了什么数据?

    • 第0位:F
    • 第1-2位:NRI
    • 第3-7位:TYPE,类型,就是通过它来判断帧类型 I帧 B帧 P帧的

    H264编码详解上(H264编码详解上)(15)

    H264编码详解上(H264编码详解上)(16)

    F: forbidden_zero_bit,在H264规范里面,规定了第一位必须是0,这个不详细解释了,记住即可。

  • NRI: 表示重要性,暂时无用处。000表示最无用,111最有用。用于表示当前NALU的重要性,值越大越重要。解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。
  • TYPE: 表示这个NAL的类型,以下表格有很多,不需要都记住,只需记住几个常用的即可

    • 5:IDR图像的片(可以理解为I帧,I帧由多个I片组成)
    • 7:序列参数集(SPS)
    • 8:图像参数集(PPS)

    H264编码详解上(H264编码详解上)(17)

    3.2 NAL类型介绍
    • 单一类型:一个RTP包只包含NALU,就是说H264帧里只包含了一个片,例如P帧或者B帧都是单一类型
    • 组合类型:一个RTP包含多个NALU,类型是24-27,像pps或者sps一般都放在一个包里,以为2个数据单元都非常小
    • 分片类型:一个NALU单元分成多个RTP包,类型28-29

    单一的NALU的RTP包

    H264编码详解上(H264编码详解上)(18)

    组合NALU的RTP包

    H264编码详解上(H264编码详解上)(19)

    分片NALU的RTP包

    H264编码详解上(H264编码详解上)(20)

    第1个字节:FU indicator分片单元指示符第2个字节:FU Header 分片单元头,有多个片,就有FU Header组合起来

    FU Header

    H264编码详解上(H264编码详解上)(21)

    • S: start bit用于指明分片的开始,在网络传输时,一个个包,我们知道他的分片的包,那么如何区分是开始还是末尾的包呢?如果为1就是分片的开始
    • E: end bit用于指明分片的结束
    • R: 未使用,设置为0
    • Type:指明分片NAL类型,网络传输完成后,还是需要将分片组合成NALU单元,这个NAL单元是关键帧还是非关键帧,是sps还是pps,就需要根据Type来判断

    思考:在传输过程中将一个帧切割成多个片,如果在传输过程中顺序打乱,或者丢失了其中某个片,我们怎么判断NALU单元传输完整呢?

    解决思路

    依据FU Header的S/E位,并借助于RTP包的包头,在RTP的包头包括了每个包的序列号,如果收到的包,收到了S包,也收到了E包,中间的包的序号是连续的,那就说明包是完整的,如果不是连续的就是丢包了,如果没有丢包就可以组合起来。

    C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

    四、AVFoundation采集视频数据实现(1)

    接下来,就是编码演示一下如何采集视频数据。大家可以回忆下之前的02-AVFoundation高级捕捉,我们之前实现的是一个基于系统相机的录制视频的功能,并没有涉及视频编码,所以这次编码演示不同

    1. 数据采集 基于AVFoudation框架(这个应该很熟悉了)
    2. 视频编码 基于VideoToolBox框架 整个过程大致就是

    数据采集 -> 编码完成 -> H264文件 -> 写入沙盒/网络传输

    4.1 数据采集

    相信大家现在都清楚数据采集的流程了,这里不多做说明,直接上代码(就在ViewController里处理)。

    1. 首先声明属性

    @interface ViewController ()<AVCaptureVideoDataOutputSampleBufferDelegate> @property(nonatomic,strong)UILabel *cLabel; @property(nonatomic,strong)AVCaptureSession *cCapturesession;//捕捉会话,用于输入输出设备之间的数据传递 @property(nonatomic,strong)AVCaptureDeviceInput *cCaptureDeviceInput;//捕捉输入 @property(nonatomic,strong)AVCaptureVideoDataOutput *cCaptureDataOutput;//捕捉输出 @property(nonatomic,strong)AVCaptureVideoPreviewLayer *cPreviewLayer;//预览图层 @end

    不同于相机的视频功能,这次输出使用的是AVCaptureVideoDataOutput,所以需遵循的delegate是AVCaptureVideoDataOutputSampleBufferDelegate。

    然后是需要创建队列完成2件事 捕获 和 编码

    @implementation ViewController { int frameID; //帧ID dispatch_queue_t cCaptureQueue; //捕获队列 dispatch_queue_t cEncodeQueue; //编码队列 VTCompressionSessionRef cEncodeingSession;//编码session CMFormatDescriptionRef format; //编码格式 NSFileHandle *fileHandele; //文件指针,存储沙盒时使用 }

    ViewDidLoad中的初始化

    - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. //基础UI实现 _cLabel = [[UILabel alloc]initWithFrame:CGRectMake(20, 20, 200, 100)]; _cLabel.text = @"cc课堂之H.264硬编码"; _cLabel.textColor = [UIColor redColor]; [self.view addSubview:_cLabel]; UIButton *cButton = [[UIButton alloc]initWithFrame:CGRectMake(200, 20, 100, 100)]; [cButton setTitle:@"play" forState:UIControlStateNormal]; [cButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [cButton setBackgroundColor:[UIColor orangeColor]]; [cButton addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:cButton]; }

    接下来就是按钮的点击事件

    - (void)buttonClick:(UIButton *)button { //判断_cCapturesession 和 _cCapturesession是否正在捕捉 if (!_cCapturesession || !_cCapturesession.isRunning ) { //修改按钮状态 [button setTitle:@"Stop" forState:UIControlStateNormal]; //开始捕捉 [self startCapture]; } else { [button setTitle:@"Play" forState:UIControlStateNormal]; //停止捕捉 [self stopCapture]; } }

    开始录制视频

    - (void)startCapture { self.cCapturesession = [[AVCaptureSession alloc]init]; //设置捕捉分辨率 self.cCapturesession.sessionPreset = AVCaptureSessionPreset640x480; //使用函数dispath_get_global_queue去得到队列 cCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); cEncodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); AVCaptureDevice *inputCamera = nil; //获取iPhone视频捕捉的设备,例如前置摄像头、后置摄像头...... NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *device in devices) { //拿到后置摄像头 if ([device position] == AVCaptureDevicePositionBack) { inputCamera = device; } } //将捕捉设备 封装成 AVCaptureDeviceInput 对象 self.cCaptureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil]; //判断是否能加入后置摄像头作为输入设备 if ([self.cCapturesession canAddInput:self.cCaptureDeviceInput]) { //将设备添加到会话中 [self.cCapturesession addInput:self.cCaptureDeviceInput]; } //配置输出 self.cCaptureDataOutput = [[AVCaptureVideoDataOutput alloc]init]; //设置丢弃最后的video frame 为NO [self.cCaptureDataOutput setAlwaysDiscardsLateVideoFrames:NO]; //设置video的视频捕捉的像素点压缩方式为 YUV4:2:0 [self.cCaptureDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; }

    关于 YUV4:2:0,这个之前没有接触过,接下来我们看看。

    C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

    五、YUV颜色详解

    我们比较熟悉的颜色系统 RGB,它每一个颜色通道占有1个字节。而YUV,是做音视频这块业务开发比较熟悉的,它的特点

    1. YUV(也称为YCbCr),是电视系统所采用的一种颜色编码方式
    2. Y: 表示亮度,也就是灰阶值,它是基础信号
    3. U和V表示的则是色度,UV的作用是描述影像的色彩及饱和度,它们用于指定像素的颜色。

    YUV和视频的关系:摄像机录制出来的视频就是YUV。

    5.1 YUV常见格式
    • YUV4:2:0(YCbCr 4:2:0) 比RGB少二分之一
    • YUV4:2:2(YCbCr 4:2:2) 比RGB少三分之一,节省了很多空间,有历史原因。
    • YUV4:4:4(YCbCr 4:4:4) 理解为1:1:1,就是4个Y对应4个U和4个V。
    YUV4:4:4

    在4:4:4的模式下,色彩的全部信息被保全下来,如图

    H264编码详解上(H264编码详解上)(22)

    相邻的四个像素点ABCD,每个像素点有自己的YUV,在色彩的二次采样的过程中,分别保留自己的YUV,称之为4:4:4。

    YUV4:2:2

    H264编码详解上(H264编码详解上)(23)

    ABCD四个相邻的像素点,A(Y0,U0,V0),B(Y1,U1,V1),C(Y2,U2,V2),D(Y3,U3,V3),当二次采样的时候,A采样的时候保留(Y0,U0),B保留(Y1,V1),C保留(Y2,U2),D保留(Y3,V3);也就是说,每个像素点的Y(明亮度)保留其本身的值,而U和V的值是每间隔一个采样,而最终就变成

    H264编码详解上(H264编码详解上)(24)

    也就是说A借B的V1,B借A的U0,C借D的V3,D借C的U2,这就是传说中的4:2:2,⼀张1280 * 720 ⼤⼩的图⽚,在YUV 4:2:2 采样时的⼤⼩为

    (1280 * 720 * 8 1280 * 720 * 0.5 * 8 * 2)/ 8 / 1024 / 1024 = 1.76 MB 。

    可以看到YUV 4:2:2 采样的图像⽐RGB 模型图像节省了三分之⼀的存储空间,在传输时占⽤的带宽也会随之减少。

    YUV4:2:0

    上面说到的4:2:2中我们可以看到相邻的两个像素点的UV是左右互相借的,那可不可以上下左右借呢,答案当然是可以的

    H264编码详解上(H264编码详解上)(25)

    YUV 4:2:0 采样,并不是指只采样U 分量⽽不采样V 分量。⽽是指,在每⼀⾏扫描时,只扫描⼀种⾊度分量(U 或者V),和Y 分量按照2 : 1 的⽅式采样。

    ⽐如,第⼀⾏扫描时,YU 按照2 : 1 的⽅式采样,那么第⼆⾏扫描时,YV 分量按照2:1 的⽅式采样。对于每个⾊度分量来说,它的⽔平⽅向和竖直⽅向的采样和Y 分量相⽐都是2:1 。假设第⼀⾏扫描了U 分量,第⼆⾏扫描了V 分量,那么需要扫描两⾏才能够组成完整的UV 分量。

    从映射出的像素点中可以看到,四个Y 分量是共⽤了⼀套UV 分量,⽽且是按照2*2 的⼩⽅格的形式分布的,相⽐YUV 4:2:2 采样中两个Y 分量共⽤⼀套UV 分量,这样更能够节省空间。⼀张1280 * 720 ⼤⼩的图⽚,在YUV 4:2:0 采样时的⼤⼩为:

    (1280 * 720 * 8 1280 * 720 * 0.25 * 8 * 2)/ 8 / 1024 / 1024 = 1.32 MB 相对于2.63M节省了一半的空间

    5.2 YUV存储格式
    • 平面格式(planar formats) :对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V,如 YYYY YYYY UU VV。
      • I420: YYYYYYYY UU VV --> YUV420P (PC专用的)
      • YV12: YYYYYYYY VV UU --> YUV420P
    • 紧缩格式(packed formats):对于packed的YUV格式,每个像素点的Y,U,V是连续交替存储的,如YUV YUV YUV YUV,这种排列方式跟 RGB 很类似。
      • NV12: YYYYYYYY UVUV --> YUV420SP
      • NV21: YYYYYYYY VUVU --> YUV420SP

    有可能在开发过程中,比如安卓和iOS,在解码视频后,发现视频图像出现倒置或者翻转,有可能就是因为他们的YUV的格式不一致导致的,PC端一般常用I420,安卓一般默认NV21,而iOS默认NV12,如果想行为统一,就需要保证一致的存储格式。

    六、AVFoundation采集视频数据实现(2)

    YUV颜色体系了解后,我们继续完成视频的采集流程

    - (void)startCapture { self.cCapturesession = [[AVCaptureSession alloc]init]; //设置捕捉分辨率 self.cCapturesession.sessionPreset = AVCaptureSessionPreset640x480; //使用函数dispath_get_global_queue去得到队列 cCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); cEncodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); AVCaptureDevice *inputCamera = nil; //获取iPhone视频捕捉的设备,例如前置摄像头、后置摄像头...... NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *device in devices) { //拿到后置摄像头 if ([device position] == AVCaptureDevicePositionBack) { inputCamera = device; } } //将捕捉设备 封装成 AVCaptureDeviceInput 对象 self.cCaptureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil]; //判断是否能加入后置摄像头作为输入设备 if ([self.cCapturesession canAddInput:self.cCaptureDeviceInput]) { //将设备添加到会话中 [self.cCapturesession addInput:self.cCaptureDeviceInput]; } //配置输出 self.cCaptureDataOutput = [[AVCaptureVideoDataOutput alloc]init]; //设置丢弃最后的video frame 为NO [self.cCaptureDataOutput setAlwaysDiscardsLateVideoFrames:NO]; //设置video的视频捕捉的像素点压缩方式为 420 [self.cCaptureDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; //设置捕捉代理 和 捕捉队列 [self.cCaptureDataOutput setSampleBufferDelegate:self queue:cCaptureQueue]; //判断是否能添加输出 if ([self.cCapturesession canAddOutput:self.cCaptureDataOutput]) { //添加输出 [self.cCapturesession addOutput:self.cCaptureDataOutput]; } //创建连接 AVCaptureConnection *connection = [self.cCaptureDataOutput connectionWithMediaType:AVMediaTypeVideo]; //设置连接的方向 [connection setVideoOrientation:AVCaptureVideoOrientationPortrait]; //初始化图层 self.cPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.cCapturesession]; //设置视频重力 [self.cPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspect]; //设置图层的frame [self.cPreviewLayer setFrame:self.view.bounds]; //添加图层 [self.view.layer addSublayer:self.cPreviewLayer]; //文件写入沙盒 NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) lastObject]stringByAppendingPathComponent:@"cc_video.h264"]; //先移除已存在的文件 [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; //新建文件 BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; if (!createFile) { NSLog(@"create file failed"); } else { NSLog(@"create file success"); } NSLog(@"filePaht = %@",filePath); fileHandele = [NSFileHandle fileHandleForWritingAtPath:filePath]; //初始化videoToolbBox [self initVideoToolBox]; //开始捕捉 [self.cCapturesession startRunning]; }

    C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

    七、VideoToolBox视频编码参数配置

    接下来就是videoToolbBox的初始化过程,包括视频编码的一些参数的配置。需要做的事情包括

    1. 创建编码session cEncodeingSession
    2. 配制编码的参数
    7.1 创建编码session

    创建编码session使用的C函数是VTCompressionSessionCreate

    H264编码详解上(H264编码详解上)(26)

    逐一解释下各个参数的含义

    • 参数1:分配器,设置NULL为默认分配
    • 参数2:分辨率width,单位是像素,如果此数据非法,系统会改为合理的值
    • 参数3:分辨率height,同上
    • 参数4:编码类型,如kCMVideoCodecType_H264
    • 参数5:编码规范。设置NULL由videoToolbox自己选择
    • 参数6:源像素缓冲区属性.设置NULL不让videToolbox创建,而自己创建
    • 参数7:压缩数据分配器.设置NULL,默认的分配
    • 参数8:回调函数。当VTCompressionSessionEncodeFrame被调用压缩一次后会被异步调用.

    ⚠️注:当你设置NULL的时候,你需要调用VTCompressionSessionEncodeFrameWithOutputHandler方法进行压缩帧处理,支持iOS9.0以上

    • 参数9:回调客户定义的参考值,即将self桥接,让C函数可以调用OC方法
    • 参数10:编码会话变量
    7.2 配制编码的参数

    配制编码的参数也需要使用C函数VTSessionSetProperty

    H264编码详解上(H264编码详解上)(27)

    这个函数很简单,参数释义如下

    • 参数1:配置参数的设置对象 cEncodeingSession
    • 参数2:属性名称
    • 参数3:属性的值
    7.3 完整初始化代码

    //初始化videoToolBox - (void)initVideoToolBox { dispatch_sync(cEncodeQueue, ^{ frameID = 0; // 分辨率:与AVFoudation的分辨率保持一致 int width = 480,height = 640; //1.调用VTCompressionSessionCreate创建编码session OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession); NSLog(@"H264:VTCompressionSessionCreate:%d",(int)status); if (status != 0) { NSLog(@"H264:Unable to create a H264 session"); return ; } //2.配制参数 //设置实时编码输出(避免延迟) VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue); //舍弃B帧 VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel,kVTProfileLevel_H264_Baseline_AutoLevel); //是否产生B帧(因为B帧在解码时并不是必要的,是可以抛弃B帧的) VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse); //设置关键帧(GOPsize)间隔,GOP太小的话图像会模糊 int frameInterval = 10; //需要类型转换 /** CFNumberCreate(CFAllocatorRef allocator, CFNumberType theType, const void *valuePtr) * allocator: 分配器 kCFAllocatorDefault默认 * theType: 数据类型 * *valuePtr: 指针,地址 */ CFNumberRef frameIntervalRaf = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval); VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRaf); //设置期望帧率,不是实际帧率 int fps = 10; CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps); VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef); //码率的理解:码率大了话就会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊,但也勉强能看 //码率计算公式,参考印象笔记 //设置码率、上限、单位是bps int bitRate = width * height * 3 * 4 * 8; CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate); VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateRef); //设置码率,均值,单位是byte int bigRateLimit = width * height * 3 * 4; CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit); VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateLimitRef); //开始编码 VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession); }); }

    其中,关于码率计算公式,可参考下图

    H264编码详解上(H264编码详解上)(28)

    八、AVFoundation采集视频数据实现(3)

    采集视频的流程还剩下停止捕捉和视频编码准备这2个节点了。

    8.1 停止捕捉

    在使用VideoToolBox视频编码之前,我们回到采集视频的流程,刚才我们实现了开始捕捉startCapture,还有停止捕捉未实现

    - (void)stopCapture { //停止捕捉 [self.cCapturesession stopRunning]; //移除预览图层 [self.cPreviewLayer removeFromSuperlayer]; //结束videoToolbBox [self endVideoToolBox]; //关闭文件 [fileHandele closeFile]; fileHandele = NULL; }

    其中,结束VideoToolBox代码如下

    -(void)endVideoToolBox { VTCompressionSessionCompleteFrames(cEncodeingSession, kCMTimeInvalid); VTCompressionSessionInvalidate(cEncodeingSession); CFRelease(cEncodeingSession); cEncodeingSession = NULL; }

    8.2 视频编码准备

    准备工作大家应该知道,肯定是在输出的delegate方法中去完成,我们此时使用的是输出是AVCaptureVideoDataOutput,它的delegate是AVCaptureVideoDataOutputSampleBufferDelegate,获取视频流所触发的方法是

    H264编码详解上(H264编码详解上)(29)

    -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { //开始视频录制,获取到摄像头的视频帧,传入encode方法中 dispatch_sync(cEncodeQueue, ^{ // 这是未编码/未压缩的视频流 [self encode:sampleBuffer]; }); }

    但是有个问题,视频和音频数据都是通过AVFoudation采集,然后交由这个代理方法!那么如何区分是视频还是音频数据呢?

    通过captureOutput对象,判断它是AVCaptureVideoDataOutput还是AVCaptureAudioDataOutput。

    九、VideoToolBox视频编码实现(1)9.1 编码函数

    和创建编码session一样,视频编码的函数也是C函数

    H264编码详解上(H264编码详解上)(30)

    其参数释义如下

    • 参数1:编码会话变量
    • 参数2:未编码数据
    • 参数3:获取到的这个sample buffer数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳.
    • 参数4:对于获取到sample buffer数据,这个帧的展示时间.如果没有时间信息,可设置kCMTimeInvalid.
    • 参数5:frameProperties: 包含这个帧的属性.帧的改变会影响后边的编码帧.
    • 参数6:ourceFrameRefCon: 回调函数会引用你设置的这个帧的参考值.
    • 参数7:infoFlagsOut: 指向一个VTEncodeInfoFlags来接受一个编码操作.如果使用异步运行,kVTEncodeInfo_Asynchronous被设置;同步运行,kVTEncodeInfo_FrameDropped被设置;设置NULL为不想接受这个信息.
    9.2 视频编码encode

    - (void)encode:(CMSampleBufferRef)sampleBuffer { //拿到每一帧未编码数据 CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer); //设置帧时间,如果不设置会导致时间轴过长。 CMTime presentationTimeStamp = CMTimeMake(frameID , 1000); VTEncodeInfoFlags flags; //编码函数 OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags); if (statusCode != noErr) { NSLog(@"H.264:VTCompressionSessionEncodeFrame faild with %d",(int)statusCode); //结束编码 VTCompressionSessionInvalidate(cEncodeingSession); CFRelease(cEncodeingSession); cEncodeingSession = NULL; return; } NSLog(@"H264:VTCompressionSessionEncodeFrame Success"); }

    此时编码已经完成,接下来有2个问题
    1. 去哪里获取编码成功的H264流数据?
    2. 拿到编码成功的数据后,接下来做什么?
    9.3 编码完成回调

    我们先来回答问题1,我们当初配置编码sessioncEncodeingSession时,指定了1个回调函数didCompressH264,这里就能拿到编码成功的H264流数据

    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)

    还记得我们之前讲解的H264文件格式吗?看下图

    H264编码详解上(H264编码详解上)(31)

    在NALU流数据中,第0个和第1个是SPS和PPS,这里面就包含了很多参数等关键信息,当然我们要先处理这个,而获取SPS和PPS,首先得拿到关键帧。这就是问题2:拿到编码成功的数据后,所需要做的事情。

    9.3.1 关键帧的判断

    大致分为3步

    1. 从sampleBuffer中获取数据流数组array

    CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);

    1. 从array中获取索引值为0的object

    CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);

    1. 判断是否关键帧

    bool isKeyFrame = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);

    9.3.2 获取SPS/PPS的C函数

    H264编码详解上(H264编码详解上)(32)

    • 参数1:图像存储方式
    • 参数2:0 索引值
    • 参数3、参数4、参数5:传值是地址,输出SPS/PPS的参数信息
    • 参数6:输出的信息,默认传0
    9.3.3 H264文件的生成

    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) { NSLog(@"didCompressH264 called with status %d infoFlags %d",(int)status,(int)infoFlags); //状态错误 if (status != 0) { return; } //没准备好 if (!CMSampleBufferDataIsReady(sampleBuffer)) { NSLog(@"didCompressH264 data is not ready"); return; } // 将ref(之前桥接的self对象)转换成viewconntroller ViewController *encoder = (__bridge ViewController *)outputCallbackRefCon; //判断当前帧是否为关键帧 bool keyFrame = !CFDictionaryContainsKey((CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync); //获取sps & pps 数据 只获取1次,保存在h264文件开头的第一帧中 //sps(sample per second 采样次数/s),是衡量模数转换(ADC)时采样速率的单位 //pps() if (keyFrame) { //图像存储方式,编码器等格式描述 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); //从第0个索引关键帧获取sps size_t sparameterSetSize,sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { //获取pps size_t pparameterSetSize,pparameterSetCount; const uint8_t *pparameterSet; //从第1个索引关键帧获取pps OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); //sps和pps获取成功,准备写入文件 if (statusCode == noErr) { // pps & sps -> NSData NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; if(encoder) { //写入文件 [encoder gotSpsPps:sps pps:pps]; } } } } // 还有其他操作... }

    接着就是写入 sps & pps的方法gotSpsPps:pps:实现,先看图

    H264编码详解上(H264编码详解上)(33)

    所以就是添加起始位00 00 00 01

    //第一帧写入 sps & pps - (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps { NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]); //添加起始位00 00 00 01 const char bytes[] = "\x00\x00\x00\x01"; //减1是去掉`\0`结束符 size_t length = (sizeof bytes) - 1; NSData *ByteHeader = [NSData dataWithBytes:bytes length:length]; [fileHandele writeData:ByteHeader]; [fileHandele writeData:sps]; [fileHandele writeData:ByteHeader]; [fileHandele writeData:pps]; }

    C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)

    十、VideoToolBox视频编码实现(2)

    上面已经处理完SPS/PPS了,接着就是之后的NALU流数据处理了,就是下图的CMBlockBuffer

    H264编码详解上(H264编码详解上)(34)

    CMBlockBuffer中汇总的就是编码后的数据流,我们需要获取它,然后转换成H264文件格式。

    10.1 获取CMBlockBuffer

    当然是C函数

    H264编码详解上(H264编码详解上)(35)

    很简单,就一句代码

    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);

    我们可以将dataBuffer理解为一个数组,我们需要遍历它,获取里面的数据。如何遍历呢?需要3个条件

    1. 单个元素的length
    2. 总体数据的length
    3. 起始地址

    然后通过C函数获取

    H264编码详解上(H264编码详解上)(36)

    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t length,totalLength; // 单个数据length,整个流数据的length char *dataPointer; //数据的首地址 // 根据单个数据length,整个NALU流数据的length,以及数据的首地址,就可以遍历整个数据流做处理了 -->可以理解为遍历数组 OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer); if (statusCodeRet == noErr) { //这里处理遍历,读取数据 }

    10.2 大端模式 & 小端模式

    在遍历处理数据之前,需要考虑一个问题 大端模式 & 小端模式。

    计算机硬件中,数据的存储方式有2种:大端字节序 和 小端字节序。

    • 大端字节序:高位字节在前面,低位字节在后面
    • 小端字节序:低位字节在前面,高位字节在后面

    比如,16进制数据0x01234567,大端字节序是01 23 45 67,而小端字节序则是67 45 23 01。

    为什么会有小端字节序呢? 因为计算机电路先处理低位字节,效率会比较高!所以,计算机内部处理都是从低位字节开始,而人类的读写习惯是大端字节序,因此,除了计算机内部,其他一般情况都是保持大端字节序。

    10.3 循环遍历处理NALU数据

    循环遍历有2种方式,一种是通过指针p 偏移来操作,一种是通过步长偏移操作,我们这里采用后者,代码如下

    size_t bufferOffset = 0; static const int AVCCHeaderLength = 4;//返回的nalu数据前4个字节不是001的startcode,而是大端模式的帧长度length //循环:通过偏移量来获取NALU数据 while (bufferOffset < totalLength - AVCCHeaderLength) { uint32_t NALUnitLength = 0; //读取 一单元长度的 nalu memcpy(&NALUnitLength, dataPointer bufferOffset, AVCCHeaderLength); //从大端模式转换为系统端模式(mac上就是小端模式) NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); //获取nalu数据 NSData *data = [[NSData alloc]initWithBytes:(dataPointer bufferOffset AVCCHeaderLength) length:NALUnitLength]; //将nalu数据写入到文件 [encoder gotEncodedData:data isKeyFrame:keyFrame]; //读取下一个nalu 一次回调可能包含多个nalu数据 bufferOffset = AVCCHeaderLength NALUnitLength; }

    10.4 完整版didCompressH264

    完整版代码

    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) { NSLog(@"didCompressH264 called with status %d infoFlags %d",(int)status,(int)infoFlags); //状态错误 if (status != 0) { return; } //没准备好 if (!CMSampleBufferDataIsReady(sampleBuffer)) { NSLog(@"didCompressH264 data is not ready"); return; } ViewController *encoder = (__bridge ViewController *)outputCallbackRefCon; //判断当前帧是否为关键帧 bool keyFrame = !CFDictionaryContainsKey((CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync); //获取sps & pps 数据 只获取1次,保存在h264文件开头的第一帧中 //sps(sample per second 采样次数/s),是衡量模数转换(ADC)时采样速率的单位 //pps() if (keyFrame) { //图像存储方式,编码器等格式描述 CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); //从第0个索引关键帧获取sps size_t sparameterSetSize,sparameterSetCount; const uint8_t *sparameterSet; OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0); if (statusCode == noErr) { //获取pps size_t pparameterSetSize,pparameterSetCount; const uint8_t *pparameterSet; //从第1个索引关键帧获取pps OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0); //sps和pps获取成功,准备写入文件 if (statusCode == noErr) { // pps & sps -> NSData NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize]; NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize]; if(encoder) { //写入文件 [encoder gotSpsPps:sps pps:pps]; } } } } CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); size_t length,totalLength; // 单个数据length,整个流数据的length char *dataPointer; //数据的首地址 // 根据单个数据length,整个NALU流数据的length,以及数据的首地址,就可以遍历整个数据流做处理了 -->可以理解为遍历数组 OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer); if (statusCodeRet == noErr) { size_t bufferOffset = 0; static const int AVCCHeaderLength = 4;//返回的nalu数据前4个字节不是001的startcode,而是大端模式的帧长度length //循环:通过偏移量来获取NALU数据 while (bufferOffset < totalLength - AVCCHeaderLength) { uint32_t NALUnitLength = 0; //读取 一单元长度的 nalu memcpy(&NALUnitLength, dataPointer bufferOffset, AVCCHeaderLength); //从大端模式转换为系统端模式(mac上就是小端模式) NALUnitLength = CFSwapInt32BigToHost(NALUnitLength); //获取nalu数据 NSData *data = [[NSData alloc]initWithBytes:(dataPointer bufferOffset AVCCHeaderLength) length:NALUnitLength]; //将nalu数据写入到文件 [encoder gotEncodedData:data isKeyFrame:keyFrame]; //读取下一个nalu 一次回调可能包含多个nalu数据 bufferOffset = AVCCHeaderLength NALUnitLength; } } }

    接着就是gotEncodedData:isKeyFrame:方法的实现

    - (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame { NSLog(@"gotEncodeData %d",(int)[data length]); if (fileHandele != NULL) { //添加4个字节的H264 协议 start code 分割符 //一般来说编码器编出的首帧数据为PPS & SPS //H264编码时,在每个NAL前添加起始码 0x000001,解码器在码流中检测起始码,当前NAL结束。 /* 为了防止NAL内部出现0x000001的数据,h.264又提出'防止竞争 emulation prevention"机制,在编码完一个NAL时,如果检测出有连续两个0x00字节,就在后面插入一个0x03。当解码器在NAL内部检测到0x000003的数据,就把0x03抛弃,恢复原始数据。 总的来说H264的码流的打包方式有两种,一种为annex-b byte stream format 的格式,这个是绝大部分编码器的默认输出格式,就是每个帧的开头的3~4个字节是H264的start_code,0x00000001或者0x000001。 另一种是原始的NAL打包格式,就是开始的若干字节(1,2,4字节)是NAL的长度,而不是start_code,此时必须借助某个全局的数据来获得编 码器的profile,level,PPS,SPS等信息才可以解码。 */ const char bytes[] ="\x00\x00\x00\x01"; //长度 size_t length = (sizeof bytes) - 1; //头字节 NSData *ByteHeader = [NSData dataWithBytes:bytes length:length]; //写入头字节 [fileHandele writeData:ByteHeader]; //写入H264数据 [fileHandele writeData:data]; } }

    总结
    • H264结构与码流解析
    • H264结构图
    • 视频图像编码后 帧
    • 片 一个片(slice)或多个片组成帧
    • 宏块 一个或多个宏块(MB)组成片
    • H264编码分层
    • NAL层: (Network Abstraction Layer,视频数据网络抽象层)
    • VCL层:(Video Coding Layer,视频数据编码层)
    • 码流
    • SODB:(String of Data Bits,原始数据比特流)
    • RBSP:(Raw Byte Sequence Payload,SODB trailing bits)
    • EBSP:(Encapsulate Byte Sequence Payload)
    • NALU: NAL Header(1B) EBSP 这个是重点
    • NAL Unit
    • NAL Unit = 一个NALU头部 一个切片
    • 切片 = 切片头 切片数据
    • 切片数据 = 宏块 ... 宏块
    • 宏块 = 类型 预测 残差数据
    • VideoToolBox
    • iOS8.0后推出的原生的硬编码框架,基于Core Foundation,C语言编写
    • 基本数据结构 CMSampleBuffer
    • 未编码 CVPixelBuffer
    • 编码后 CMBlockBuffer
    • 编码过程 CVPixelBuffer原始数据 -> video encoder -> CMBlockBuffer -> H264文件格式
    • H264文件
    • H264文件格式是NALU流数据类型
    • 帧的顺序 SPS PPS I B P帧
    • 识别I B P帧
    • 十六进制 换算成 二进制
    • 二进制取4-8位,再换算成成十进制
    • 十进制结果参照对照表
    • NALU单元数据详解
    • NALU = NAL Header(1 Byte) NAL Body
    • NAL Header解析
    • 1字节,即占8位
    • 第0位:F 值必须是0
    • 第1-2位:NRI 重要性 000最无用,111最有用
    • 第3-7位:TYPE,类型,就是通过它来判断帧类型 I帧 B帧 P帧的
    • 5表示I帧
    • 7表示SPS序列参数集
    • 8表示PPS图像参数集
    • NAL类型
    • 单一类型:一个RTP包只包含NALU,即H264帧里只包含了一个片
    • 组合类型:一个RTP包含多个NALU,例如像pps或者sps
    • 分片类型:一个NALU单元分成多个RTP包
    • 第1个字节:FU indicator分片单元指示符
    • 第2个字节:FU Header 分片单元头,有多个片
    • FU Header
    • S: start bit用于指明分片的开始
    • E: end bit用于指明分片的结束
    • R: 未使用,设置为0
    • Type:指明分片NAL类型,是关键帧还是非关键帧,是sps还是pps
    • NALU单元传输完整的识别
    • 收到S包 和 E包
    • 中间的包的序号是连续的
    • YUV颜色体系
    • 也称YCbCr,是电视系统所采用的一种颜色编码方式
    • Y: 表示亮度,也就是灰阶值,它是基础信号
    • U和V表示的则是色度,UV的作用是描述影像的色彩及饱和度,它们用于指定像素的颜色
    • YUV常见格式
    • YUV4:2:0(YCbCr 4:2:0) 比RGB少二分之一
    • YUV4:2:2(YCbCr 4:2:2) 比RGB少三分之一
    • YUV4:4:4(YCbCr 4:4:4) 理解为1:1:1
    • YUV存储格式
    • 平面格式(planar formats)
    • I420:YUV420P (PC专用的)
    • YV12:YUV420P
    • 紧缩格式(packed formats)
    • NV12:YUV420SP (iOS默认)
    • NV21:YUV420SP (安卓默认)
    • AVFoundation采集视频数据实现
    • 整体过程 数据采集 -> 编码完成 -> H264文件 -> 写入沙盒/网络传输
    • 数据采集 基于AVFoudation框架
    • 输出源AVCaptureVideoDataOutput,需遵循AVCaptureVideoDataOutputSampleBufferDelegate
    • 队列同步完成2件事 捕获 和 编码
    • video的视频捕捉的像素点压缩方式为 YUV4:2:0
    • kCVPixelBufferPixelFormatTypeKey : kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
    • 视频编码 基于VideoToolBox框架
    • 初始化videoToolbBox
    • 创建编码session VTCompressionSessionCreate
    • 配制编码的参数 VTSessionSetProperty
    • 实时编码kVTCompressionPropertyKey_RealTime
    • 舍弃B帧kVTCompressionPropertyKey_ProfileLevel
    • 产生B帧kVTCompressionPropertyKey_AllowFrameReordering
    • 关键帧(GOPsize)间隔kVTCompressionPropertyKey_MaxKeyFrameInterval
    • 期望帧率kVTCompressionPropertyKey_ExpectedFrameRate
    • 码率上限kVTCompressionPropertyKey_DataRateLimits
    • 码率均值kVTCompressionPropertyKey_AverageBitRate
    • VideoToolBox视频编码
    • 停止捕捉
    • 停止捕捉session
    • 移除预览图层
    • 结束videoToolbBox
    • 关闭文件
    • 编码前准备
    • 编码的时机点 AVCaptureVideoDataOutputSampleBufferDelegate方法-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
    • 编码实现
    • 获取未编码每一帧 CMSampleBufferGetImageBuffer
    • 编码函数 VTCompressionSessionEncodeFrame
    • 获取编码成功的H264流数据
    • 编码完成回调 VTCompressionSessionCreate时指定的回调函数
    • 从sampleBuffer中获取数据流数组CMSampleBufferGetSampleAttachmentsArray
    • 从array中获取索引值为0的CFDictionaryRefdic
    • 判断关键帧!CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync)
    • 生成H264文件格式
    • 获取SPS/PPS CMVideoFormatDescriptionGetH264ParameterSetAtIndex
    • 写入文件
    • 根据size和地址指针,读取NSData
    • 配置Header
    • 添加起始位"\x00\x00\x00\x01"
    • 去掉\0结束符
    • 写入顺序 Header spsData Header ppsData
    • 获取CMBlockBuffer CMSampleBufferGetDataBuffer
    • 遍历CMBlockBuffer 获取 nalu数据
    • 单个元素的length 总体数据的length 起始地址,指针偏移遍历
    • 大端模式转换成小端模式(mac系统默认小端模式)
    • 将nalu数据写入到文件
    • 和写入SPS/PPS一样,配置Header
    • 写入顺序 Header NALData
    ,

    免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

      分享
      投诉
      首页