-
视频编码
视频编码分为软编码和硬编码:软编码:
1.利用CPU进行大批量的编码计算处理。
2.兼容性好。
3.耗电量大,手机发烫(很烫,感觉要爆炸了O(∩_∩)O~)硬编码
1.利用GPU进行编码处理。
2.兼容性略差。
3.手机不会很烫。(硬编码需要iOS8及以上版本可以使用,之前并未开发,之前版本只能软编码。)
这里记录硬编码的实现,软编码后续会记录。
-
H264
视频编码需要了解的编码格式,H264/AVC为视频编码格式,需要将采集到的视频帧编码为H264格式的数据。
H264的特点:- 1.更高的编码效率:同H.263等标准的特率效率相比,能够平均节省大于50%的码率。
- 2.高质量的视频画面:H.264能够在低码率情况下提供高质量的视频图像,在较低带宽上提供高质量的图像传输是H.264的应用亮点。
- 3.提高网络适应能力:H.264可以工作在实时通信应用(如视频会议)低延时模式下,也可以工作在没有延时的视频存储或视频流服务器中。
H264的优势:
H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1。低码率(Low Bit Rate)对H.264的高的压缩比起到了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像,正因为如此,经过H.264压缩的视频数据,在网络传输过程中所需要的带宽更少,也更加经济。
PS:以上摘自百度百科。需要了解的可自行百度。
我们将采集到的视频数据编码为H264数据流,那采集到的原始视频数据是什么呢?实际上是YUV420格式的数据,视频采集那篇文章记录了设置输出设备的输出格式为:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
表示原始数据的格式为YUV420。那我们为什么要设置输出为YUV420数据呢,YUV420数据是什么呢?这篇文章介绍的很详细YUV和RGB。
简单来说有以下几点:
1.YUV420采样数据大小为RGB格式的一半(采样数据后续涉及到推流,所以数据越小越好)。
2.YUV格式所有编码器都支持,RGB格式却存在不兼容的情况。
3.YUV420格式适用于便携式设备。
代码如下:
#import
#import
@interface BBH264Encoder : NSObject
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)endEncode;
@end
#import "BBH264Encoder.h"
@interface BBH264Encoder()
/** 记录当前的帧数 */
@property (nonatomic, assign) NSInteger frameID;
/** 编码会话 */
@property (nonatomic, assign) VTCompressionSessionRef compressionSessionRef;
/** 文件写入对象 */
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end
@implementation BBH264Encoder
- (instancetype)init{
if (self = [super init]) {
// 1.初始化写入文件的对象(NSFileHandle用于写入二进制文件)
[self setupFileHandle];
// 2.初始化压缩编码的会话
[self setupCompressionSession];
}
return self;
}
- (void)setupFileHandle {
// 1.获取沙盒路径
NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.h264"];
// 2.如果原来有文件,则删除
[[NSFileManager defaultManager] removeItemAtPath:file error:nil];
[[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
// 3.创建对象
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}
- (void)setupCompressionSession{
//0.用于记录当前是第几帧数据(画面帧数非常多)
_frameID = 0;
//1.清空压缩上下文
if (_compressionSessionRef) {
VTCompressionSessionCompleteFrames(_compressionSessionRef, kCMTimeInvalid);
VTCompressionSessionInvalidate(_compressionSessionRef);
CFRelease(_compressionSessionRef);
_compressionSessionRef = NULL;
}
//2.录制视频的宽度&高度
int width = [UIScreen mainScreen].bounds.size.width;
int height = [UIScreen mainScreen].bounds.size.height;
//3.创建压缩会话
OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, bbCompressionSessionCallback, (__bridge void * _Nullable)(self), &_compressionSessionRef);
//4.判断状态
if (status != noErr) return;
//5.设置参数
//Profile_level,h264的协议等级,不同的清晰度使用不同的ProfileLevel
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
// 关键帧最大间隔
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(30)));
// 设置平均码率 单位是byte
int bitRate = [self getResolution];
CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
// 码率上限 接收数组类型CFArray[CFNumber] [bytes,seconds,bytes,seconds...] 单位是bps
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef _Nullable)@[@(bitRate*1.5/8), @1]);
// 设置期望帧率
int fps = 30;
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
// 设置实时编码
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
// 关闭重排Frame
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
// 设置比例16:9(分辨率宽高比)
VTSessionSetProperty(_compressionSessionRef, kVTCompressionPropertyKey_AspectRatio16x9, kCFBooleanTrue);
//6.准备编码
VTCompressionSessionPrepareToEncodeFrames(_compressionSessionRef);
}
/**
编码回调
*/
static void bbCompressionSessionCallback(
void * CM_NULLABLE outputCallbackRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CM_NULLABLE CMSampleBufferRef sampleBuffer ){
BBH264Encoder *encoder = (__bridge BBH264Encoder *)(outputCallbackRefCon);
//1.判断状态是否为没有错误
if (status != noErr) {
return;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, false);
BOOL isKeyframe = NO;
if (attachments != NULL) {
CFDictionaryRef attachment;
CFBooleanRef dependsOnOthers;
attachment = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
dependsOnOthers = CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
dependsOnOthers == kCFBooleanFalse ? (isKeyframe = YES) : (isKeyframe = NO);
}
//2.是否为关键帧
if (isKeyframe) {
//SPS and PPS.
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
size_t spsSize, ppsSize;
size_t parmCount;
const uint8_t* sps, *pps;
OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sps, &spsSize, &parmCount, NULL );
//获取SPS无错误则继续获取PPS
if (status == noErr) {
CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pps, &ppsSize, &parmCount, NULL );
NSData *spsData = [NSData dataWithBytes:sps length:spsSize];
NSData *ppsData = [NSData dataWithBytes:pps length:ppsSize];
//写入文件
[encoder gotSpsPps:spsData pps:ppsData];
}else{
return;
}
}
//3.前4个字节表示长度,后面的数据的长度
// 除了关键帧,其它帧只有一个数据
char *buffer;
size_t total;
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, NULL, &total, &buffer);
if (statusCodeRet == noErr) {
size_t offset = 0;
//返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
int const headerLenght = 4;
//循环获取NAL unit数据
while (offset < total - headerLenght) {
int NALUnitLength = 0;
// Read the NAL unit length
memcpy(&NALUnitLength, buffer + offset, headerLenght);
//从大端转系统端
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
NSData *data = [NSData dataWithBytes:buffer + headerLenght + offset length:NALUnitLength];
// Move to the next NAL unit in the block buffer
offset += headerLenght + NALUnitLength;
[encoder gotEncodedData:data isKeyFrame:isKeyframe];
}
}
}
/**
获取屏幕分辨率
*/
- (int)getResolution{
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGSize screenSize = screenRect.size;
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat screenX = screenSize.width * scale;
CGFloat screenY = screenSize.height * scale;
return screenX * screenY;
}
- (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
{
// 1.拼接NALU的header
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1;
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
// 2.将NALU的头&NALU的体写入文件
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:sps];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:pps];
}
- (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
{
NSLog(@"gotEncodedData %d", (int)[data length]);
if (self.fileHandle != NULL)
{
const char bytes[] = "\x00\x00\x00\x01";
size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
[self.fileHandle writeData:ByteHeader];
[self.fileHandle writeData:data];
}
}
- (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
// 1.将sampleBuffer转成imageBuffer
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
// 2.根据当前的帧数,创建CMTime的时间
CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
VTEncodeInfoFlags flags;
// 3.开始编码该帧数据
OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSessionRef,
imageBuffer,
presentationTimeStamp,
kCMTimeInvalid,
NULL, (__bridge void * _Nullable)(self), &flags);
if (statusCode == noErr) {
NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
}
}
- (void)endEncode {
VTCompressionSessionCompleteFrames(self.compressionSessionRef, kCMTimeInvalid);
VTCompressionSessionInvalidate(self.compressionSessionRef);
CFRelease(self.compressionSessionRef);
self.compressionSessionRef = NULL;
[self.fileHandle closeFile];
self.fileHandle = NULL;
}
@end
上述H264码流的NALU和SPS、PPS是什么呢?关于H264码流结构NALU、SPS\PPS。
此代码是将采集到的原始数据编码为H.264码流写入本地文件,此文件可以利用VLC播放器直接播放,测试结果,注意需要真机测试。
真机获取沙盒文件的方法请见:真机获取沙盒文件。
感谢coderWhy和iOSSinger两位的分享。