视频H264编码详解(上)

前言

本篇开始讲解大家最感兴趣的知识点 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 Header + NAL 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; //帧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的模式下,色彩的全部信息被保全下来,如图

    相邻的四个像素点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 * 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是左右互相借的,那可不可以上下左右借呢,答案当然是可以的

    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

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

    • 参数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创建编码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);
        });
    }

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

    八、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对象)转换成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:实现,先看图

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

    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,整个流数据的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
    举报
    评论 0