iOS中使用VideoToolBox进行H.264编解码

视频编解码简介

为什么要对视频进行编码

我们为什么要对视频进行编码呢? 要知道我们看到的视频,无论是大屏幕上的电影,还是手机上的短视频,其实都是由一张张图片组成的,为了让观看者不会感觉到卡顿,一秒钟至少需要有16张图片变换(这个一秒钟多少张图片的变换叫做帧率),一般情况下是30帧。那么按照这个方式算下来,一个1280x720分辨率的视频按照一秒钟的大小就是 1280x720x30 ≈ 420M,这仅仅是一秒钟的视频,这么大的数据量别说是在网络上传输了,就连你的硬盘恐怕也都吃不消。

所以我们需要对视频进行压缩,而压缩的过程中就需要对视频进行编码。

为什么编码后视频会变小

那为什么对视频的编码就是压缩呢?仔细想想我们平时看电影的时候,每个镜头或多或少都会有一些一段时间内不变的场景吧?这个时候每一帧都带上这个场景就造成了数据上的冗余,所以在编码过程中,就会去除这些冗余的数据。而在视频信息中一般有三种冗余信息,并且每种冗余信息都才用不同的方式处理:

  • 空间冗余:采用帧内预测编码压缩
  • 时间冗余:采用运动搜索和运动补偿压缩
  • 统计冗余:采用熵编码压缩

软编码和硬编码

编码又分为软编码和硬编码

  • 软编码:使用CPU进行编码
  • 硬编码:不使用CPU进行编码,使用比如显卡GPU

那他们有什么区别呢?之前用过一些视频直播平台App的同学有可能会感受到过,使用软编码的App在直播的时候手机会明显的发烫,这就是因为在使用CPU进行软编码的时候,会对CPU的负载影响很大,导致设备发热,但是软编码也有优点,就是低码率下的质量要比直接使用硬编码好一点。不过现在在GPU硬件平台也有移植了一些优秀的软编码算法,提升低码率下硬编码的质量。

H.264简介

国际上指定视频编解码技术的组织主要有两个,并且指定了两个主要系列的编解码规则,一个是国际电联(ITU)主导的H.26X系列,一个是国际标准组织机构(ISO)主导的MPEG系列。

而H.264是两个组织的联合视频组(JVT)共同制定的数字视频标准,所以他既是ITU的H.264标准,也是ISO的MPEG-4的第10部分或者叫AVC(Advanced Video Coding)。

所以在当下H.264还是很牛*的,另外H.265作为H.264的继任者,可以支持最高8K(8192x4320)分辨率的视频编解码,也是未来发展的趋势。

VCL与NAL分层

H.264的原始码流(裸流)是由一个接一个的NALU组成的,他的功能分为两部分:VCL:视频编码层,NAL:网络提取层。

VCL: 视像编码层(Video Coding Layer, 简称VCL),包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码,对视频原始数据进行压缩。
NAL: 网络抽象层(Network Abstraction Layer,简称NAL)。在以太网每个包大小是 1500 字节,而一帧往往会大于这个值,所以就需要用于按照一定格式,对 VCL 视像编码层输出的数据拆成多个包传输,并提供包头(header)等信息,以在不同速率的网络上传输或进行存储,所有的拆包和组包都是 NAL 层去处理的。覆盖了所有片级以上的语法级别。

logo

VCL数据传输或者存储之前,会被映射到NALU中,H.264数据包含这一个个NALU

logo

一个NALU = 一组对应于视频编码的NALU头部信息 + 一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)

H.264码流第一个NALU是SPS,第二个NALU是PPS,第三个NALU是IDR。

*SPS: 序列参数集 SPS(Sequence Parameter Sets),存储的是一个序列的信息,包括有多少帧等
PPS: 图像参数集 PPS(Picture Parameter Sets ),存储的一帧的信息。 解码的时候必须获取到 SPSPPS 的信息,才能对后面的数据进行解码。

H.264的三种帧

在H.264中定义了三种帧:

  • I帧:完整编码的帧叫I帧,也是关键帧,上边说到的电影中有不变场景的情况中,这个帧一般是一个全新完整的场景中的
  • P帧:参考之前的I帧生成的只包含差异部分编码的帧,也叫向前参考帧,P帧只会保存和之前帧不一样的数据,这样能够让数据大大减少
  • B帧:参考前后编码的帧,也叫双向参考帧,压缩时要参考前后两帧,压缩率会更高,但是因为对后一帧也有依赖,所以在直播中应用时需要考虑到网络传输的速度,这时候就不是很适合使用B帧

H.264的算法

H.264采用的核心算法是帧内压缩和帧间压缩。

  • 帧内压缩:生成I帧的算法
  • 帧间压缩:是生成B帧和P帧的算法

在编码的过程中,H.264会把几帧图像分为一组,称为GOP(Group of Picture)或者GOF(Group of Frame)。一般情况下,一个GOP会有一个I帧,并且以这个I帧开头,然后跟随多个P帧以及B帧。

logo

VideoToolBox

在iOS中,处理视频相关的框架有5个,从顶层开始分别是:

  • AVKit
  • AVFoundation
  • VideoToolBox
  • Core Media
  • Core Video

可以看到VideoToolBox是处于中间的位置,算是相对底层的框架,可以直接访问硬件进行硬编解码。

编码(VTCompressionSession)

VideoToolBox中使用VTCompressionSession进行编码,从
VTCompressionSession官方文档中可以看出来,使用VTCompressionSession进行编码主要有5个步骤。

  1. 使用 VTCompressionSessionCreate 创建一个session;
  2. 使用 VTSessionSetPropertyVTSessionSetProperties 对session设置一些属性;
  3. 使用 VTCompressionSessionEncodeFrame 进行编码,输出的内容会在callback中返回;
  4. 使用 VTCompressionSessionCompleteFrames 停止编码;
  5. 使用 VTCompressionSessionInvalidateCFRelease 来释放资源。

创建session

首先看一下创建session的函数

1
2
3
4
5
6
7
8
9
10
11
VTCompressionSessionCreate(
CFAllocatorRef allocator,
int32_t width,
int32_t height,
CMVideoCodecType codecType,
CFDictionaryRef encoderSpecification,
CFDictionaryRef sourceImageBufferAttributes,
CFAllocatorRef compressedDataAllocator,
VTCompressionOutputCallback outputCallback,
void * outputCallbackRefCon,
VTCompressionSessionRef * compressionSessionOut)

首先我们来看一下每个参数的作用:

  • CFAllocatorRef allocator:内存构造器,传递默认的kCFAllocatorDefault即可
  • int32_t width:编码器的宽度,不用多说
  • int32_t height:编码器的高度,也不用多说了
  • CMVideoCodecType codecType: 编码器的类型,如果使用H264的话,我们选择kCMVideoCodecType_H264就可以了
  • CFDictionaryRef encoderSpecification:编码规范,这边我们直接传NULL就可以了;
  • CFDictionaryRef sourceImageBufferAttributes:图像缓冲区属性,这里同样传递NULL即可;
  • CFAllocatorRef compressedDataAllocator:编码后数据的构造器,传NULL
  • VTCompressionOutputCallback outputCallback:编码结束后的回调函数
  • void outputCallbackRefCon:回调函数的参数,一般传`(__bridge void )(self)`或者指定的处理回调方法的类
  • VTCompressionSessionRef *compressionSessionOut;输出参数,传递一个session

配置参数

同样的,先看一下配置参数的方法:

1
2
3
4
VTSessionSetProperty(
VTSessionRef session,
CFStringRef propertyKey,
CFTypeRef propertyValue )

这个就好理解多了,

  • VTSessionRef session:编码器;
  • CFStringRef propertyKey:需要设置的property的key
  • CFTypeRef propertyValue:需要设置的property的value

可以配置的参数有很多,具体的内容都可以参考官方文档Compression Properties | Apple Developer Documentation

这里简单说几个(当然了,具体使用还是要看你的项目,这里说的是我的项目里用到的)

  • kVTCompressionPropertyKey_ProfileLevel 编码比特流的级别
  • kVTCompressionPropertyKey_RealTime 是否在运行时实时执行压缩
  • kVTCompressionPropertyKey_ExpectedFrameRate 期望的帧率,注意这里的期望并不一定能够达到

开始编码

开始编码简单一些,就直接调用方法传递session即可:

1
VTCompressionSessionPrepareToEncodeFrames(VTCompressionSessionRef session )

回调

接下来通过回调获取到编码的参数

1
2
3
4
5
void compressionOutCallback(void *outputCallbackRefCon,
void *sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer)

还是先看一下各个参数:

  • void *outputCallbackRefCon 刚刚传递过来的参数,因为是C函数,所以不能直接使用本类中的变量,所以一般通过这个来过渡
  • void *sourceFrameRefCon
  • OSStatus status 编码的状态,如果为noErr(0)表示没有异常
  • VTEncodeInfoFlags infoFlags
  • CMSampleBufferRef sampleBuffer 回调的buffer参数

一般情况下,我们会在这里解析回调的数据,提取出sps和pps,然后循环nalu数据来获取编码后的数据,具体的代码我会写在下边。

结束

结束编码之后,记得回调关闭session和释放之前开辟的session

1
2
3
4
5
VTCompressionSessionCompleteFrames(encodingSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(encodingSession);
CFRelease(encodingSession);
encodingSession = NULL;

完整代码:

接下来贴下编码部分的完整代码,比较少就不另外写Demo了

.h:

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
//
// TLH264Encoder.h
// TeamLink
//
// Created by 张忠瑞 on 2020/4/22.
// Copyright © 2020 TrackView. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h>
NS_ASSUME_NONNULL_BEGIN
@protocol TLH264EncoderDelegate <NSObject>
- (void)gotSpsPps:(NSData *)sps pps:(NSData *)pps;
- (void)gotEncodedData:(NSData *)data isKeyFrame:(BOOL)isKeyFrame;
@end
@interface TLH264Encoder : NSObject
@property (nonatomic ,weak) id<TLH264EncoderDelegate> delegate;
- (instancetype)initWithWidth:(int)width andHeight:(int)height andFps:(int)fps;
- (void)encodeWithSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)stopEncode;
@end
NS_ASSUME_NONNULL_END

.m:

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
//
// TLH264Encoder.m
// TeamLink
//
// Created by 张忠瑞 on 2020/4/22.
// Copyright © 2020 TrackView. All rights reserved.
//
/// Encode 流程:
/// 1.使用VTCompressionSessionCreate创建session
/// 2.使用VTSessionSetProperty配置编码器
/// 3.使用VTCompressionSessionEncodeFrame进行编码,编码好的数据会在compressionOutCallback中回调
/// 4.使用VTCompressionSessionCompleteFrames完成编码
/// 5.使用VTCompressionSessionInvalidate来释放资源。
/// https://developer.apple.com/documentation/videotoolbox/vtcompressionsession?language=objc
/// Property: https://developer.apple.com/documentation/videotoolbox/vtcompressionsession/compression_properties?language=objc
#import "TLH264Encoder.h"
@interface TLH264Encoder () {
dispatch_queue_t encodeQueue;
VTCompressionSessionRef encodingSession;
}
@end
@implementation TLH264Encoder
- (instancetype)initWithWidth:(int)width andHeight:(int)height andFps:(int)fps {
self = [super init];
if(self) {
[self createSessionWithWidth:width andHeight:height andFps:fps];
}
return self;
}
- (void)encodeWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
CMTime presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
VTEncodeInfoFlags flags;
OSStatus status = VTCompressionSessionEncodeFrame(encodingSession,
imageBuffer,
presentationTimeStamp,
kCMTimeInvalid,
NULL,
NULL,
&flags);
if(status != noErr) {
NSLog(@"Encode frame failure: status = %d",(int)status);
//停止编码
if (encodingSession) {
VTCompressionSessionInvalidate(encodingSession);
CFRelease(encodingSession);
encodingSession = NULL;
}
return;
}
NSLog(@"Encode frame success");
}
- (void)stopEncode {
if(encodingSession) {
VTCompressionSessionCompleteFrames(encodingSession, kCMTimeInvalid);
VTCompressionSessionInvalidate(encodingSession);
CFRelease(encodingSession);
encodingSession = NULL;
}
}
// MARK: - Private method
- (void)createSessionWithWidth:(int)width andHeight:(int)height andFps:(int)fps {
dispatch_queue_t encodeQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(encodeQueue, ^{
OSStatus status = VTCompressionSessionCreate(kCFAllocatorDefault,
width, height,
kCMVideoCodecType_H264,
NULL,
NULL,
NULL,
compressionOutCallback,
(__bridge void *)(self),
&self->encodingSession);
if(status != noErr) {
NSLog(@"Create session failure: status = %d",(int)status);
return;
}
NSLog(@"Create session success");
//配置session
//编码比特流的配置文件和级别
VTSessionSetProperty(self->encodingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
//运行时:建议视频编码实时执行压缩
VTSessionSetProperty(self->encodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
//设置预期帧率
CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
VTSessionSetProperty(self->encodingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
//开始编码
VTCompressionSessionPrepareToEncodeFrames(self->encodingSession);
});
}
// MARK: - Callback
void compressionOutCallback(void *outputCallbackRefCon,
void *sourceFrameRefCon,
OSStatus status,
VTEncodeInfoFlags infoFlags,
CMSampleBufferRef sampleBuffer) {
NSLog(@"compressionOutCallback status = %d, infoFlags = %d",(int)status,(int)infoFlags);
if(status != noErr) {
return;;
}
if(!CMSampleBufferDataIsReady(sampleBuffer)) {
NSLog(@"compressionOutCallback sampleBuffer is not ready");
return;
}
TLH264Encoder *encoder = (__bridge TLH264Encoder *)outputCallbackRefCon;
//判断是否为关键帧
bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
//关键帧获取pps和sps
if(keyframe) {
CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
size_t sparameterSetSize, sparameterSetCount;
const uint8_t *sparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
if (statusCode == noErr)
{
// 获得了sps,再获取pps
size_t pparameterSetSize, pparameterSetCount;
const uint8_t *pparameterSet;
OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
if (statusCode == noErr)
{
// 获取SPS和PPS data
NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
if (encoder.delegate)
{
//回调解码完成的sps和pps数据
[encoder.delegate gotSpsPps:sps pps:pps];
}
}
}
}
CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length, totalLength;
char *dataPointer;
//这里获取了数据指针,和NALU的帧总长度,前四个字节里面保存的
OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
if (statusCodeRet == noErr) {
size_t bufferOffset = 0;
static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
// 循环获取nalu数据
while (bufferOffset < totalLength - AVCCHeaderLength) {
uint32_t NALUnitLength = 0;
// 读取NALU长度的数据
memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
// 从大端转系统端
NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
if (encoder.delegate) {
[encoder.delegate gotEncodedData:data isKeyFrame:keyframe];
}
// 移动到下一个NALU单元
bufferOffset += AVCCHeaderLength + NALUnitLength;
}
}
}
@end

解码

解码和编码基本没有太大差别,VTDecompressionSession | Apple Developer Documentation官方文档中还是很清楚的写出了基本的几个步骤:

  1. 使用 VTDecompressionSessionCreate 创建一个解码器;
  2. 使用 VTSessionSetPropertyVTSessionSetProperties 来配置参数
  3. 调用 VTDecompressionSessionDecodeFrame 来开始解码
  4. 使用 VTDecompressionSessionInvalidateCFRelease 来释放参数

接下来还是一步一步来看一下:

创建解码器

首先还是来看一下调用的方法:

1
2
3
4
5
6
7
VTDecompressionSessionCreate(
CFAllocatorRef allocator,
CMVideoFormatDescriptionRef videoFormatDescription,
CFDictionaryRef videoDecoderSpecification,
CFDictionaryRef destinationImageBufferAttributes,
const VTDecompressionOutputCallbackRecord * outputCallback,
VTDecompressionSessionRef * decompressionSessionOut)

可以看到解码的参数和编码基本没太大区别:

  • CFAllocatorRef allocator:构造器,传默认的即可 kCFAllocatorDefault
  • CMVideoFormatDescriptionRef videoFormatDescription:数据的格式
  • CFDictionaryRef videoDecoderSpecification:解码器规范,同样传递NULL即可
  • CFDictionaryRef destinationImageBufferAttributes:NULL
  • const VTDecompressionOutputCallbackRecord * outputCallback:回调函数
  • VTDecompressionSessionRef * decompressionSessionOut:session

配置参数

解码器的参数配置和编码器的方法一样,只是可能传递的参数有所不同,这里就不多做介绍,可以直接看Decompression Properties | Apple Developer Documentation

开始解码

解码的时候有一点需要注意,我们需要根据传递过来的是否为关键帧或者sps及pps,来做不同的处理,这一块可以等下直接看代码

解码的回调

解码的回调中可以收到解码成功的CVPixelBufferRef

完整代码

接下来贴下完整的代码:

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
//
// TLH264Decoder.h
// TeamLink
//
// Created by 张忠瑞 on 2020/4/23.
// Copyright © 2020 TrackView. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h>
NS_ASSUME_NONNULL_BEGIN
@protocol TLH264DecoderDelegate <NSObject>
- (void)gotDecodeFrame:(CVImageBufferRef)pixelBuffer andPresentationTimeStamp:(CMTime)presentationTimeStamp;
@end
@interface TLH264Decoder : NSObject
@property (nonatomic ,weak) id<TLH264DecoderDelegate> delegate;
- (void)decodeNalu:(uint8_t *)frame size:(uint32_t)frameSize;
- (void)stopDecoder;
@end
NS_ASSUME_NONNULL_END

.m

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
//
// TLH264Decoder.m
// TeamLink
//
// Created by 张忠瑞 on 2020/4/23.
// Copyright © 2020 TrackView. All rights reserved.
//
/// 1.使用 VTDecompressionSessionCreate 方法来创一个解码器
/// 2.使用 VTSessionSetProperty 或者 VTSessionSetProperties 来对解码器进行配置
/// 3.使用 VTDecompressionSessionDecodeFrame 来进行数据的解码。
/// 4.使用 VTDecompressionSessionInvalidate 以及 CFRelease 来对资源的释放
/// https://developer.apple.com/documentation/videotoolbox/vtdecompressionsession?language=objc
#import "TLH264Decoder.h"
@interface TLH264Decoder () {
VTDecompressionSessionRef decodingSession;
CMVideoFormatDescriptionRef decodingFormatDescription; //解码format,封装sps和pps
uint8_t *sps;
NSInteger spsSize;
uint8_t *pps;
NSInteger ppsSize;
}
@end
@implementation TLH264Decoder
- (void)decodeNalu:(uint8_t *)frame size:(uint32_t)frameSize {
int nalu_type = (frame[4] & 0x1F);
CVPixelBufferRef pixelBuffer = NULL;
uint32_t nalSize = (uint32_t)(frameSize-4);
uint8_t *pNalSize = (uint8_t *)(&nalSize);
frame[0] = *(pNalSize + 3);
frame[1] = *(pNalSize + 2);
frame[2] = *(pNalSize + 1);
frame[3] = *(pNalSize);
switch (nalu_type) {
case 0x05: {
//关键帧
if([self createH264Decoder]) {
pixelBuffer = [self decode:frame size:frameSize];
}
break;
}
case 0x07: {
//sps(第七位)
spsSize = frameSize - 4;
sps = malloc(spsSize);
memcpy(sps, &frame[4], spsSize);
break;
}
case 0x08: {
//pps
ppsSize = frameSize - 4;
pps = malloc(ppsSize);
memcpy(pps, &frame[4], ppsSize);
break;
}
default: {
// B/P frame
if([self createH264Decoder]) {
pixelBuffer = [self decode:frame size:frameSize];
}
break;
}
}
}
- (void)stopDecoder {
if(decodingSession) {
VTDecompressionSessionInvalidate(decodingSession);
CFRelease(decodingSession);
decodingSession = NULL;
}
if(decodingFormatDescription) {
CFRelease(decodingFormatDescription);
decodingFormatDescription = NULL;
}
if (sps) {
free(sps);
}
if (pps) {
free(pps);
}
spsSize = 0;
ppsSize = 0;
}
// MARK: - Private method
//每次解析数据的时候都要检测一遍
- (BOOL)createH264Decoder {
if(decodingSession) {
return YES;
}
OSStatus formatDescStatus = [self createFormatDescription];
if(formatDescStatus != noErr) {
NSLog(@"create format description failure, status = %d",formatDescStatus);
return NO;
}
OSStatus sessionStatus = [self createSession];
if(sessionStatus != noErr) {
NSLog(@"create session failure, status = %d",sessionStatus);
return NO;
}
VTSessionSetProperty(decodingSession, kVTDecompressionPropertyKey_ThreadCount, (__bridge CFTypeRef)[NSNumber numberWithInt:1]);
VTSessionSetProperty(decodingSession, kVTDecompressionPropertyKey_RealTime, kCFBooleanTrue);
return YES;
}
- (OSStatus)createFormatDescription {
const uint8_t* const parameterSetPointers[2] = { sps, pps };
const size_t parameterSetSizes[2] = { spsSize, ppsSize };
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
2, //param count
parameterSetPointers,
parameterSetSizes,
4, //nal start code size
&decodingFormatDescription);
return status;
}
- (OSStatus)createSession {
//TODO: 传递width,height
NSDictionary *destinationPixelImageBufferAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange],
(id)kCVPixelBufferWidthKey: [NSNumber numberWithInt:720],
(id)kCVPixelBufferHeightKey: [NSNumber numberWithInt:960],
(id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:YES]};
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = didDecompress;
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
decodingFormatDescription,
NULL,
(__bridge CFDictionaryRef)destinationPixelImageBufferAttributes,
&callBackRecord,
&decodingSession);
return status;
}
- (CVPixelBufferRef)decode:(uint8_t *)frame size:(uint32_t)frameSize {
CVPixelBufferRef outputPixelBuffer = NULL;
CMBlockBufferRef blockBuffer = NULL;
OSStatus status = CMBlockBufferCreateWithMemoryBlock(NULL,
(void *)frame,
frameSize,
kCFAllocatorNull,
NULL,
0,
frameSize,
FALSE,
&blockBuffer);
if(status == kCMBlockBufferNoErr) {
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
blockBuffer,
decodingFormatDescription,
1,
0,
NULL,
1,
sampleSizeArray,
&sampleBuffer);
if(status == kCMBlockBufferNoErr && sampleBuffer) {
VTDecodeInfoFlags flagOut = 0;
VTDecodeFrameFlags flags = 0;
OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(decodingSession,
sampleBuffer,
flags,
&outputPixelBuffer,
&flagOut);
if(decodeStatus == kVTInvalidSessionErr) {
NSLog(@"Invalid session, reset decoder session");
} else if(decodeStatus == kVTVideoDecoderBadDataErr) {
NSLog(@"Decode failed status=%d(Bad data)", decodeStatus);
} else if(decodeStatus != noErr) {
NSLog(@"Decode failed status=%d", decodeStatus);
}
CFRelease(sampleBuffer);
}
CFRelease(blockBuffer);
}
return outputPixelBuffer;
}
// MARK: - Callback
static void didDecompress( void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ){
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
//持有pixelBuffer数据,否则会被释放,记得在delegate中使用完release
*outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
TLH264Decoder *decoder = (__bridge TLH264Decoder *)decompressionOutputRefCon;
if (decoder.delegate)
{
[decoder.delegate gotDecodeFrame:pixelBuffer andPresentationTimeStamp:presentationTimeStamp];
}
}
@end

最后

以上就是iOS中使用VideoToolBox来进行硬编解码的全部内容了,本来是想要在项目中为了避免内存使用过高才使用它(项目中需要在ReplayKit2的Extension中使用,有50M的内存限制),但是发现他不能在后台运行!这下就根本没法用了。

所以也就简单记录一下,没有太多的深入研究,仅供个人学习使用,如果有什么地方写的不对或者有更好的建议,欢迎各位大佬批评指正。

参考文档

VideoToolbox | Apple Developer Documentation
Apple 平台下的 VideoToolBox 框架 | Enki’s Notes
音视频学习(二)— H.264编码原理 - 掘金
直播一:H.264编码基础知识详解 - 紫忆 - 博客园
VideoToolbox使用说明 - 简书