iOS音视频开发-视频硬编码的实现

视频编码
视频编码分为软编码和硬编码:

软编码:
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格式适用于便携式设备。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#import <UIKit/UIKit.h>
#import <VideoToolbox/VideoToolbox.h>

@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码流结构NALUSPS\PPS
此代码是将采集到的原始数据编码为H.264码流写入本地文件,此文件可以利用VLC播放器直接播放,测试结果,注意需要真机测试。
真机获取沙盒文件的方法请见:真机获取沙盒文件
感谢coderWhy和iOSSinger两位的分享。