科百科
当前位置: 首页 范文大全

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

时间:2023-08-16 作者: 小编 阅读量: 2 栏目名: 范文大全

前言

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

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

上图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详解结构图如下:

  • NAL 单元是由一个NALU头部 一个切片.
  • 切片又可以细分成"切片头 切片数据".
  • 每个切片数据包括了很多宏块.
  • 每个宏块包括了宏块的类型,宏块的预测,残差数据.
H264码流分层结构图
  • 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 基本的数据结构CMSampleBuffer中有编码和解码2种情况,它们有区别
    • 编码后 数据存储在CMBlockBuffer中,其中流数据就是从这里获取的
    • 未编码 数据存储在CVPixelBuffer中
    2.4 编码的过程

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

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

    序列参数集SPS(Sequence Parameter Sets)

    图像参数集PPS(Picture Parameter Sets)

    这些仅了解即可。

    2.4 判断帧类型 I B P

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

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

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

    三、NALU单元数据详解

    NALU = NAL HeaderNAL Body

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

    3.1 NAL Header解析

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

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

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

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

    • 5:IDR图像的片(可以理解为I帧,I帧由多个I片组成)
    • 7:序列参数集(SPS)
    • 8:图像参数集(PPS)
    3.2 NAL类型介绍
    • 单一类型:一个RTP包只包含NALU,就是说H264帧里只包含了一个片,例如P帧或者B帧都是单一类型
    • 组合类型:一个RTP包含多个NALU,类型是24-27,像pps或者sps一般都放在一个包里,以为2个数据单元都非常小
    • 分片类型:一个NALU单元分成多个RTP包,类型28-29

    单一的NALU的RTP包

    组合NALU的RTP包

    分片NALU的RTP包

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

    FU Header
    • 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; //帧IDdispatch_queue_t cCaptureQueue; //捕获队列dispatch_queue_t cEncodeQueue; //编码队列VTCompressionSessionRef cEncodeingSession;//编码sessionCMFormatDescriptionRef 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的模式下,色彩的全部信息被保全下来,如图

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

    YUV4:2:2

    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的值是每间隔一个采样,而最终就变成

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

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

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

    YUV4:2:0

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

    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 * 81280 * 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

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

    • 参数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

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

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

    //初始化videoToolBox- (void)initVideoToolBox {dispatch_sync(cEncodeQueue, ^{frameID = 0;// 分辨率:与AVFoudation的分辨率保持一致int width = 480,height = 640;//1.调用VTCompressionSessionCreate创建编码sessionOSStatus 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);//码率的理解:码率大了话就会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊,但也勉强能看//码率计算公式,参考印象笔记//设置码率、上限、单位是bpsint bitRate = width * height * 3 * 4 * 8;CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateRef);//设置码率,均值,单位是byteint bigRateLimit = width * height * 3 * 4;CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit);VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateLimitRef);//开始编码VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);});}

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

    八、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,获取视频流所触发的方法是

    -(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函数

    其参数释义如下

    • 参数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文件格式吗?看下图

    在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函数
    • 参数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对象)转换成viewconntrollerViewController *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个索引关键帧获取spssize_t sparameterSetSize,sparameterSetCount;const uint8_t *sparameterSet;OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);if (statusCode == noErr) {//获取ppssize_t pparameterSetSize,pparameterSetCount;const uint8_t *pparameterSet;//从第1个索引关键帧获取ppsOSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);//sps和pps获取成功,准备写入文件if (statusCode == noErr) {// pps & sps -> NSDataNSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];if(encoder) {//写入文件[encoder gotSpsPps:sps pps:pps];}}}}// 还有其他操作...}

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

    所以就是添加起始位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 01const 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

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

    10.1 获取CMBlockBuffer

    当然是C函数

    很简单,就一句代码

    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);

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

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

    然后通过C函数获取

    CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);size_t length,totalLength; // 单个数据length,整个流数据的lengthchar *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;//读取 一单元长度的 nalumemcpy(&NALUnitLength, dataPointerbufferOffset, AVCCHeaderLength);//从大端模式转换为系统端模式(mac上就是小端模式)NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);//获取nalu数据NSData *data = [[NSData alloc]initWithBytes:(dataPointerbufferOffsetAVCCHeaderLength) length:NALUnitLength];//将nalu数据写入到文件[encoder gotEncodedData:data isKeyFrame:keyFrame];//读取下一个nalu 一次回调可能包含多个nalu数据bufferOffset= AVCCHeaderLengthNALUnitLength;}

    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个索引关键帧获取spssize_t sparameterSetSize,sparameterSetCount;const uint8_t *sparameterSet;OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);if (statusCode == noErr) {//获取ppssize_t pparameterSetSize,pparameterSetCount;const uint8_t *pparameterSet;//从第1个索引关键帧获取ppsOSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);//sps和pps获取成功,准备写入文件if (statusCode == noErr) {// pps & sps -> NSDataNSData *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,整个流数据的lengthchar *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;//读取 一单元长度的 nalumemcpy(&NALUnitLength, dataPointerbufferOffset, AVCCHeaderLength);//从大端模式转换为系统端模式(mac上就是小端模式)NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);//获取nalu数据NSData *data = [[NSData alloc]initWithBytes:(dataPointerbufferOffsetAVCCHeaderLength) length:NALUnitLength];//将nalu数据写入到文件[encoder gotEncodedData:data isKeyFrame:keyFrame];//读取下一个nalu 一次回调可能包含多个nalu数据bufferOffset= AVCCHeaderLengthNALUnitLength;}}}

    接着就是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流数据类型
    • 帧的顺序 SPSPPSI 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结束符
    • 写入顺序 HeaderspsDataHeaderppsData
    • 获取CMBlockBuffer CMSampleBufferGetDataBuffer
    • 遍历CMBlockBuffer 获取 nalu数据
    • 单个元素的length总体数据的length起始地址,指针偏移遍历
    • 大端模式转换成小端模式(mac系统默认小端模式)
    • 将nalu数据写入到文件
    • 和写入SPS/PPS一样,配置Header
    • 写入顺序 HeaderNALData
    ,
      推荐阅读
    • 1983年昆仑山到底发生了什么(1979年昆仑山上到底发生了什么)

      与其他军事文件不同的是,这批档案均为民国时期全国发生的各种古怪事件及处理情况的记录。为保密需要,对外宣称“中华人民共和国第091气象研究所”,军内统称“091”。这起惨祸发生不久后,在附近工作的地质队也遭到了死亡谷的袭击。专家说昆仑山是鬼神所在之地,但至今无从证实。

    • 工业机器人什么时候可以做(机器人跌落神坛)

      工业机器人什么时候可以做用3D看世界,让自动化更简单,请关注CAD2D3D,每日更新!国产工业机器人老大哥上半年又亏了1.5亿元,已经出现连续亏损了,不禁要问,是公司出问题了?德国KUKA只有机器人业务,6年前被卖了。种种迹象表明,四大巨头并不看好机器人。就像通信、互联网等新兴行业一样,喧嚣之后终归要恢复平静,工业机器人行业慢慢地也将成为传统行业,未来的市场竞争只会更加惨烈。

    • 婚内出轨女人的悲惨下场自述(老公出轨女邻居带给我的伤害)

      口述者:小谢小谢和老公经历了4年的恋爱长跑,最终两人步入婚姻殿堂。小谢老公出轨的对象是她们家楼下的邻居,一个外表平平,身材平庸的胖女人。小谢说,这事发生之前,两家唯一有关系的,就是因为女儿和邻居的女儿在同一家幼儿园上学,所以呢一来二去两家就熟悉了。自从小谢得病后,女儿也带不了了,只能送去了姥姥姥爷家。后来,小谢在清醒的时候非要和老公离婚。小谢的情绪也好多了,小谢说,这个婚看来真是离对了!

    • 大学生化妆技巧(简单介绍一下)

      遮瑕黑眼圈严重、痘印、红血丝等问题用遮瑕产品解决,利用好遮瑕刷。将修容膏涂在修容部位,用湿海绵蛋晕开。用蓬松刷蘸取散粉,轻敲刷柄抖去多余散粉后,用刷头部位轻“戳”皮肤定妆。额头中央,颧骨,鼻子凸起部位,下巴中央等部位,以高光打亮。唇妆打底唇部干燥的人润唇膏再次打底,唇色深的人使用唇部遮瑕液。唇刷唇刷用于勾画唇线,不至于之后涂口红时涂出来。

    • 耳朵痛是什么原因(右边耳朵痛是什么原因)

      原发性耳痛耳部疾病,包括外耳、中耳疾病,临床常由外耳损伤、炎症、异物刺伤、耵聍膨胀嵌顿等所引起。检查可见外耳道皮肤增厚、皲裂、脱屑,甚至分泌物积存。外耳道湿疹急性外耳道湿疹时,患者可有耳朵极痒,伴有烧灼疼痛感。外耳道真菌病合并感染一般有耳内发痒及胀闷感,有时奇痒,以夜间为甚。合并感染者可引起外耳道肿胀、疼痛和流脓。检查可见外耳道和鼓膜覆盖有黄黑色或白色粉末状或绒毛状真菌。

    • z300碳罐怎么开(Z03单踏板I-PEDAL模式解析与使用分享)

      至于真正意义上的刹车踏板,只有在紧急情况之下才会用到。控制好这百分之十之内的踏板踩抬并且做到丝滑,才是单踏板的精髓所在!单踏板出现的意义在于能够创造更舒适的驾乘感受、让动能回收变得可控、提高能源利用率、延长电车的续航里程、缓解里程焦虑带来的不安。合创作为新能源造车的新势力品牌!紧急情况一定要踩刹车!祝贺广大车友早日提车!

    • 2022暑假武汉大学生回重庆需要做核酸检测吗?

      2022暑假武汉大学生回重庆需要做核酸检测吗?目前重庆没有发布针对大学生暑假返渝的要求。按照重庆现行的管理措施:武汉返渝人员要提供48小时内核酸检测阴性证明,抵渝后24小时内做1次核酸检测。重庆核酸检测价格:单人单检降至不高于每人份16元;多人混检统一降至每人份不高于3.5元。检测项目价格含检验相关的试剂等耗材,为各级公立医疗机构最高指导价格,不得上浮,医疗机构可以下浮执行。

    • 什么主食热量最高(五种主食热量或相当于)

      如果经常吃在油锅里翻滚的食物,就相当于喝油,所摄入的脂肪越来越多,身材越来越胖,也会增加多种慢性疾病风险。对于这种食物多吃一口,就相当于在“喝油”,如果你不想身材越来越胖,这类食物还是赶紧拉入黑名单,或许更有助于达到减脂瘦身效果。制作凉皮都会用到面粉,尤其是炒凉皮,更容易导致各种调味品以及油脂摄入过多。常吃这类食物会导致身材越来越胖,如果您平时爱吃凉皮,偶尔吃一份普通凉皮即可,尽量不要吃炒凉皮。

    • 鸡肉焯水要多久(鸡肉焯水要多久时间)

      鸡肉焯水要十分钟左右主要看火候大小,如果火比较大可适当减少时间,一般不低于五分钟在炖鸡时,可放姜片、白醋、生抽等除去异味,我来为大家讲解一下关于鸡肉焯水要多久?跟着小编一起来看一看吧!在炖鸡时,可放姜片、白醋、生抽等除去异味。特别需要注意的是,一定要等水开了,再把鸡放进锅内,不要把鸡放进凉水内,这样鸡肉很容易老。