为什么视频可以压缩编码?
-
存在冗余信息
- 空间冗余:图像相邻像素之间有较强的相关性
- 时间冗余:视频序列的相邻图像之间内容相似
- 视觉冗余:人的视觉系统对某些细节不敏感
- 其他冗余信息
-
空间冗余
- 同一张图像中,有很多像素点表示的信息是完全一样的
- 如果对每一个像素进行单独的存储,必然会非常浪费空间,也完全没有必要
-
时间冗余
- 多张图像之间,有非常多的相关性,由于一些小运动造成了细小差别
- 如果对每张图像进行单独的像素存储,在下一张图片中又出现了相同的。那么相当于很多像素都存储了多份,必然会非常浪费空间,也是完全没有必要的
-
视觉冗余
- 人类视觉系统HVS(Human Visual System)
- 对高频信息不敏感
- 对高对比度更敏感
- 对亮度信息比色度信息更敏感
- 对运动的信息更敏感
- 数字视频系统的设计应该考虑HVS的特点:
- 丢弃高频信息,只编码低频信息
- 提高边缘信息的主观质量
- 降低色度的解析度
- 对感兴趣区域(Region of Interesting,ROI)进行特殊处理
- 人类视觉系统HVS(Human Visual System)
压缩编码的标准
- ITU:International Telecommunications Union VECG:Video Coding Experts Group(国际电传视讯联盟)
- ISO:International Standards Organization MPEG:Motion Picture Experts Group(国际标准组织机构)
ios8.0 之后 使用VideoToolBox框架
流程:
- 采集
- 获取到视频帧
- 对视频帧进行编码
- 获取到视频帧信息
- 将编码后的数据以NALU方式写入到文件
视频采集
视频硬件编码
-
初始化压缩编码会话(VTCompressionSessionRef)
- 在VideoToolbox框架的使用过程中,基本都是C语言函数
-
初始化后通过VTSessionSetProperty设置对象属性
- 编码方式:H.264编码
- 帧率:每秒钟多少帧画面
- 码率:单位时间内保存的数据量
- 关键帧(GOPsize)间隔:多少帧为一个GOP
准备编码
- (void)prepareEncodeWithWidth:(int)width height:(int)height{
//0 定义帧的下标值
frameIndex = 0;
//1.创建VTCompressionSessionRef 对象
//1.创建VTCompressionSessionRef 对象
// 参数一: CoreFoundation 创建对象的方式 ,NULL -> Default
// 参数二:编码的视频宽度
// 参数三: 编码的视频高度
// 参数四: 编码的标准 H.264/ H.265
// 参数五 ~ 参数七 NULL
// 参数八: 编码成功一帧数据后的函数回调
// 参数九: 回调函数的第一个参数
// VTCompressionSessionRef session;
VTCompressionSessionCreate(kCFAllocatorDefault,
width, height, kCMVideoCodecType_H264,
NULL, NULL, NULL,
compressionCallback, (__bridge void * _Nullable)(self),
&_session);
//2.设置VTCompressionSessionRef 属性
// 2.1 如果是直播,需要设置视频编码是实时输出
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef _Nullable)(@YES));
// 2.2 设置帧率 (16/24/30)
// 帧/s
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef _Nullable)(@30));
//2.3 设置比特率 (码率) bit/s 单位时间的数据量
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef _Nullable)(@(1500000))); // bit
CFArrayRef dataLimits = (__bridge CFArrayRef)(@[@(1500000/8),@1]); //byte
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_DataRateLimits, dataLimits);
// 2.4 设置GOP的大小
VTSessionSetProperty(self.session, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(20)));
//3.准备开始编码
VTCompressionSessionPrepareToEncodeFrames(self.session);
}
总结一下常用设置属性:
kVTCompressionPropertyKey_RealTime
编码是否实时输出kVTCompressionPropertyKey_ExpectedFrameRate
帧率,也就是一秒输出多少帧图像,16张就可以形成动画,一般默认30kVTCompressionPropertyKey_AverageBitRate
码率,一般是单位时间内的数据量 必须同时设置kVTCompressionPropertyKey_DataRateLimits
kVTCompressionPropertyKey_MaxKeyFrameInterval
GOP,默认设置20开始编码
- (void)encodeFrame:(CMSampleBufferRef)sampleBuffer{
//1.从CMSampleBufferRef 中获取 CVImageBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
//利用 VTCompressionSessionRef 编码 CMSampleBufferRef
//pts(presentationTimeStamp):展示时间戳,用来解码时,计算每一帧时间的
//dts(DecodeTimeStamp): 解码时间戳,决定该帧在什么时间展示
frameIndex ++;
// 第几帧 帧率
CMTime pts = CMTimeMake(frameIndex, 30);
VTCompressionSessionEncodeFrame(self.session,
imageBuffer, pts, kCMTimeInvalid, NULL, NULL, NULL);
}
CMSampleBuffer = CMTime(时间戳) +CMVideoFormatDesc(图片存储方式) + CMBlockBuffer(编码后的数据)
- 编码成功一帧数据后的函数回调
void compressionCallback(void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer){
// 0 获取到当前对象
H264Encoder *encoder = (__bridge H264Encoder *)(outputCallbackRefCon);
// 1.CMSampleBufferRef
// 2.判断该帧是否是关键帧
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
BOOL iskeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
// 3. 如果是关键帧,那么将关键帧写入文件之前,先写入 PPS / SPS数据
if (iskeyFrame) {
//3.1 获取参数信息
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
//3.2 从format 中获取sps信息
//
//参数二 : sps 0 pps 1
//参数三
const uint8_t *spsPointer;
size_t spsSize,spsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &spsPointer, &spsSize, &spsCount, NULL);
//3.3 从format 中获取pps信息
const uint8_t *ppsPointer;
size_t ppsSize,ppsCount;
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &ppsPointer, &ppsSize, &ppsCount, NULL);
// 3.4 将sps/pps 写入 NAL单元
NSData *spsData = [NSData dataWithBytes:spsPointer length:spsSize];
NSData *ppsData = [NSData dataWithBytes:ppsPointer length:ppsSize];
[encoder writeData:spsData];
[encoder writeData:ppsData];
}
// 4.将编码后的数据写入文件
// 4.1 获取CMSampleBufferRef
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
// 4.2 CMSampleBufferRef获取内存地址/长度
size_t totalLength;
char *dataPointer;
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer);
// 4.3 从dataPointer开始读取数据,并且写入NALU -> slice
static const int h264HeaderLength = 4;
size_t offsetLength = 0;
// 4.4 通过循环,不断的读取slice的切片数据,并且封装成NALU 写入文件
while (offsetLength < totalLength - h264HeaderLength) {
// 4.5 读取slice的长度
uint32_t naluLength;
memcpy(&naluLength, dataPointer+offsetLength, h264HeaderLength);
// 4.6 H264 大端字节序/ 小端字节序
naluLength = CFSwapInt32BigToHost(naluLength);
// 4.7 根据长度读取字节,并转成NSData
NSData *data = [NSData dataWithBytes:dataPointer+offsetLength+h264HeaderLength length:naluLength];
//4.8 写入文件
[encoder writeData:data];
//4.9 设置offsetLength
offsetLength += naluLength + h264HeaderLength;
}
}
需要注意的一点是,编码后的数据需要通过切片的方式读取数据,h264已经提供好了切片后的数据,并且默认使用4个字节提供每一个切片的数据的长度,在写入文件时候是不能包括这个4字节长度的。
- 写入数据
- (void)writeData:(NSData *)data{
// NALU 的形式写入
// NALU 头 0x 表示 16进制的某个数字 x 表示16进制的某个字节
const char bytes[] = "\x00\x00\x00\x01";
int headerLength = sizeof(bytes) - 1;
NSData *headerData = [NSData dataWithBytes:bytes length:headerLength];
// NALU 体
[self.fileHandle writeData:headerData];
[self.fileHandle writeData:data];
}
- 结束编码
- (void)endEncoding{
VTCompressionSessionInvalidate(self.session);
CFRelease(self.session);
}