视频H264编码详解(上) 视频编码为h.264
yuyutoo 2024-10-12 01:26 1 浏览 0 评论
前言
本篇开始讲解大家最感兴趣的知识点 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码流分层结构图
?? 这个图比较重要.大家可以多看看。
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
二、VideoToolBox简介
VideoToolBox是苹果iOS8.0后推出的原生的硬编码框架,利用硬件加速器,基于Core Foundation库函数(它是C语言编写的)。
2.1 使用步骤
我们一般使用VideoToolBox框架,需要做的事情包括
- 创建session -> 设置编码相关参数 -> 开始编码 ->循环输入源数据(YUV 类型的数据,直接从摄像头获取)->获取编码后的H264数据 ->结束编码
- 构建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,这个不详细解释了,记住即可。
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高级捕捉,我们之前实现的是一个基于系统相机的录制视频的功能,并没有涉及视频编码,所以这次编码演示不同
- 数据采集 基于AVFoudation框架(这个应该很熟悉了)
- 视频编码 基于VideoToolBox框架 整个过程大致就是
数据采集 -> 编码完成 -> H264文件 -> 写入沙盒/网络传输
4.1 数据采集
相信大家现在都清楚数据采集的流程了,这里不多做说明,直接上代码(就在ViewController里处理)。
- 首先声明属性
@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,是做音视频这块业务开发比较熟悉的,它的特点
- YUV(也称为YCbCr),是电视系统所采用的一种颜色编码方式
- Y: 表示亮度,也就是灰阶值,它是基础信号
- 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的初始化过程,包括视频编码的一些参数的配置。需要做的事情包括
- 创建编码session cEncodeingSession
- 配制编码的参数
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个问题
- 去哪里获取编码成功的H264流数据?
- 拿到编码成功的数据后,接下来做什么?
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步
- 从sampleBuffer中获取数据流数组array
CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
- 从array中获取索引值为0的object
CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);
- 判断是否关键帧
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个条件
- 单个元素的length
- 总体数据的length
- 起始地址
然后通过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
相关推荐
- jQuery VS AngularJS 你更钟爱哪个?
-
在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...
- Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0
-
在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...
- 如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例
-
要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...
- 编程技巧:Jquery实时验证,指定长度的「负小数」
-
为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...
- 一篇文章带你用jquery mobile设计颜色拾取器
-
【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...
- 编程技巧:Jquery实时验证,指定长度的「正小数」
-
为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...
- jquery.validate检查数组全部验证
-
问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...
- Vue进阶(幺叁肆):npm查看包版本信息
-
第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...
- layui中使用lay-verify进行条件校验
-
一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...
- jQuery是什么?如何使用? jquery是什么功能组件
-
jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...
- django框架的表单form的理解和用法-9
-
表单呈现...
- jquery对上传文件的检测判断 jquery实现文件上传
-
总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...
- Nodejs之MEAN栈开发(四)-- form验证及图片上传
-
这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...
- 大数据开发基础之JAVA jquery 大数据java实战
-
上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...
- 推荐四个开源的jQuery可视化表单设计器
-
jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)