一、视频采集输出数据格式YUV&RGB
RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
YUV的原理是把亮度与色度分离,研究证明,人眼对亮度的敏感超过色度。利用这个原理,可以把色度信息减少一点,人眼也无法查觉这一点。YUV三个字母中,其中”Y”表示明亮度(Lumina nce或Luma),也就是灰阶值;而”U”和”V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色
使用YUV的优点有两个:
1、彩色YUV图像转黑白YUV图像转换非常简单,这一特性用在于电视信号上。
2、YUV是数据总尺寸小于RGB格式
二、为什么要进行视频编码?
首先我们假设一种场景, 采集一分钟数据,需要多大的空间来存储。
1、视频分辨率是 1280 * 720.
2、一秒钟之内至少需要16帧画面(正常开发通常会采集30帧),为了不让用户感受明显卡顿现象.
3、采用NV12 (YUV420)输出格式计算。(YUV420为4个Y共用一个U和V,Y分量为全采样,即1字节,U分量和V分量只有Y分量的四分之一,即U分量和V分量的大小均为1/4字节,也就是说一张1像素的YUV420图像大小为:(3/2)=1.5字节。)
计算结果: 1280 * 720 * 1.5 * 16 * 60 = 1296 M
三、为什么采集到的视频数据可以编码?
我们采集到的视频源数据本身就存在一定的冗余信息。具体表现如下:
1、空间冗余:图片相邻像素之间有较强的相关性。
2、时间冗余:视频系列的相邻图像之间内容相似。
3、视觉冗余:人的视觉系统对某些细节不敏感。
空间冗余
空间冗余是指在同一张图像中,有很多像素点表示的信息是完全一样的,如果对每一个像素进行单独的存储,必然会非常浪费空间,也完全没有必要,此处部分存储的颜色色值相同,仅仅对应空间位置不同。
时间冗余
时间冗余是指多张图像之间,有非常多的相关性,由于一些小运动造成了细小差别。
我们可以看到两张图片相隔很近,彼此之间很多元素相同,如果对相同部分多次存储,则形成了冗余
视觉冗余
1.人类视觉系统HVS
2.对高频信息不敏感
3.对高对比度更敏感
4.对亮度信息比色度信息更敏感
5.对运动的信息更敏感
数字视频系统的设计应该考虑HVS的特点:
1.丢弃高频信息,只编码低频信息
2.提高边缘信息的主观质量
3.降低色度的解析度
四、音视频的编解码
音视频编解码, 说白了就是对音视频数据进行压缩, 减少数据对空间的占用, 便于网络传输, 存储和使用!
目前直播常用的音视频编解码方式是h.264/AVC, AAC/MP3
硬软编解码的区别:
软编码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
硬编码:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
五、了解几个和编码相关的概念。
- 帧率:FPS(每秒钟要多少帧画面,可以理解为一帧是一张图片)
- Gop(表示多少秒一个I帧)
- 码率:编码器每秒编出的数据大小,单位是kbps,比如800kbps代表编码器每秒产生800kb(或100KB)的数据。
- 分辨率:单位英寸中所包含的像素点数。
三者的对应直播质量的影响因素
:
帧率
:影响画面流畅度,与画面流畅度成正比:帧率越大,画面越流畅;帧率越小,画面越有跳动感。如果码率为变量,则帧率也会影响体积,帧率越高,每秒钟经过的画面越多,需要的码率也越高,体积也越大。帧率就是在1秒钟时间里传输的图片的帧数,也可以理解为图形处理器每秒钟能够刷新几次。
分辨率
:影响图像大小,与图像大小成正比:分辨率越高,图像越大;分辨率越低,图像越小。
清晰度
在码率一定的情况下,分辨率与清晰度成反比关系:分辨率越高,图像越不清晰,分辨率越低,图像越清晰。
在分辨率一定的情况下,码率与清晰度成正比关系,码率越高,图像越清晰;码率越低,图像越不清晰。
六、H264构成
h.264码流是由一系列的NAL单元组成。而NAL单元一般包含。
- 视频帧
-I帧:完整编码的帧,也叫关键帧
-P帧:参考之前的I帧生成的只包含差异部分编码的帧
-B帧:参考前后的帧编码的帧叫B帧 -
h.264参数集
-序列参数集(Sequence Parameter Set(SPS))
-图像参数集(Picture Parameter Set(PPS) )
七、h264硬编码
基本步骤
1、通过VTCompressionSessionCreate创建编码器
2、通过VTSessionSetProperty设置编码器属性
3、设置完属性调用VTCompressionSessionPrepareToEncodeFrames准备编码
4、输入采集到的视频数据,调用VTCompressionSessionEncodeFrame进行编码
5、获取到编码后的数据并进行处理
6、调用VTCompressionSessionCompleteFrames停止编码器
7、调用VTCompressionSessionInvalidate销毁编码器
1、创建编码器
VTCompressionSessionCreate用来创建视频编码会话,这个方法有10个参数,我们可以看一下苹果对这个API的注释
VTCompressionSessionCreate(
CM_NULLABLE CFAllocatorRef allocator,
int32_t width,
int32_t height,
CMVideoCodecType codecType,
CM_NULLABLE CFDictionaryRef encoderSpecification,
CM_NULLABLE CFDictionaryRef sourceImageBufferAttributes,
CM_NULLABLE CFAllocatorRef compressedDataAllocator,
CM_NULLABLE VTCompressionOutputCallback outputCallback,
void * CM_NULLABLE outputCallbackRefCon,
CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut)
allocator
:内存分配器,填NULL为默认分配器
width
、height
:视频帧像素的宽高,如果编码器不支持这个宽高的话可能会改变
codecType
:编码类型,枚举
encoderSpecification
:指定特定的编码器,填NULL的话由VideoToolBox自动选择
sourceImageBufferAttributes
:源像素缓冲区的属性,如果这个参数有值的话,VideoToolBox会创建一个缓冲池,不需要缓冲池可以设置为NULL
compressedDataAllocator
:压缩后数据的内存分配器,填NULL使用默认分配器
outputCallback
:视频编码后输出数据回调函数
outputCallbackRefCon
:回调函数中的自定义指针,我们通常传self,在回调函数中就可以拿到当前类的方法和属性了
compressionSessionOut
:编码器句柄,传入编码器的指针
OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault, size.width, size.height, kCMVideoCodecType_H264, NULL, NULL, NULL, VideoCompressonOutputCallback, (__bridge void *)(self), &_encodeSesion)
;
2、设置编码器属性 & 准备编码
编码器创建完了,所有给编码器设置属性都是调用VTSessionSetProperty方法来实现。
/** 设置编码属性 */
// 设置实时编码输出,降低编码延迟 (直播必然是实时输出,否则可会有延迟)
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// 设置关键帧间隔,即gop size 一组图片的间隔
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef)@(fps*2));
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, (__bridge CFTypeRef)@(fps*2));
// 置期望帧率(每秒多少帧,如果帧率过低,会造成画面卡顿)
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef)@(fps));
// 设置码率(码率: 编码效率, 码率越高,则画面越清晰, 如果码率较低会引起马赛克 --> 码率高有利于还原原始画面,但是也不利于传输)
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef)@(bt));
NSArray *limit = @[@(bt * 1.5/8),@(1)];
// 设置数据速率限制
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
// h264 profile, 直播一般使用baseline,可减少由于b帧带来的延时
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 防止编译B帧是被自动重新排序
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// 设置H264 熵编码模式 H264标准采用了两种熵编码模式
// 熵编码即编码过程中按熵原理不丢失任何信息的编码。信息熵为信源的平均信息量(不确定性的度量)
status = VTSessionSetProperty(_encodeSesion, kVTCompressionPropertyKey_H264EntropyMode, kVTH264EntropyMode_CABAC);
// 基本设置结束, 准备进行编码
status = VTCompressionSessionPrepareToEncodeFrames(_encodeSesion);
3、输入采集到的视频数据,调用VTCompressionSessionEncodeFrame进行编码
VTCompressionSessionEncodeFrame(
CM_NONNULL VTCompressionSessionRef session,
CM_NONNULL CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime duration, // may be kCMTimeInvalid
CM_NULLABLE CFDictionaryRef frameProperties,
void * CM_NULLABLE sourceFrameRefCon,
VTEncodeInfoFlags * CM_NULLABLE infoFlagsOut )
session
:创建编码器时的句柄
imageBuffer
:YUV数据,iOS通过摄像头采集出来的视频流数据类型是CMSampleBufferRef,我们要从里面拿到CVImageBufferRef来进行编码。通过CMSampleBufferGetImageBuffer方法可以从sampleBuffer中获得imageBuffer。
presentationTimeStamp
:这一帧的时间戳,单位是毫秒
duration
:这一帧的持续时间,如果没有持续时间,填kCMTimeInvalid
frameProperties
:指定这一帧的属性,这里我们可以用来指定产生I帧
encodeParams
:自定义指针
infoFlagsOut
:用于接收编码操作的信息,不需要就置为NULL
// 获取CVImageBufferRef
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 设置是否为I帧
NSDictionary *frameProperties = @{(__bridge NSString *)kVTEncodeFrameOptionKey_ForceKeyFrame: @(forceKeyFrame)};;
// 输入待编码数据
OSStatus status = VTCompressionSessionEncodeFrame(_compressionSessionRef, imageBuffer, kCMTimeInvalid, kCMTimeInvalid, (__bridge CFDictionaryRef)frameProperties, NULL, NULL);
4、获取到编码后的数据并进行处理
编码后的数据通过VTCompressionSessionCreate方法设置的回调函数返回。编码后的数据以及这一帧的基本信息都在CMSampleBufferRef中。如果这一帧是个关键帧,那么我们需要获取SPS和PPS数据,然后给这些数据加个开始码返回出去。
// 开始码
const char header[] = "\x00\x00\x00\x01";
size_t headerLen = (sizeof header) - 1;
NSData *headerData = [NSData dataWithBytes:header length:headerLen];
// 3.判断是否是关键帧
bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
//判断当前帧是否是关键帧
//获取 sps & pps 数据
if (isKeyframe) {
// 获取编码后的信息(存储于CMFormatDescriptionRef中)
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
///获取sps信息
size_t sparameterSetSize, sparameterSetCount;
const uint8_t *sparameterSet;
OSStatus spsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
///获取PPS信息
size_t pparameterSetSize, pparameterSetCount;
const uint8_t *pparameterSet;
OSStatus ppsStatus = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
if (spsStatus == noErr && ppsStatus == noErr) {
//sps数据加上开始码组成NALU
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSMutableData *spsData = [NSMutableData data];
[spsData appendData:headerData];
[spsData appendData:sps];
// pps数据加上开始码组成NALU
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
NSMutableData *ppsData = [NSMutableData data];
[ppsData appendData:headerData];
[ppsData appendData:pps];
}
}
5、停止编码
OSStatus status = VTCompressionSessionCompleteFrames(_compressionSessionRef, kCMTimeInvalid);
6、释放编码器
VTCompressionSessionInvalidate(_compressionSessionRef);
CFRelease(_compressionSessionRef);
_compressionSessionRef = NULL;