iOS|iOS 利用VideoToolBox对视频进行编解码

iOS在8.0后提供了VideoToolBox, 使用户可以自行对视频进行硬编解码操作. 在这边简单先介绍一下硬编码和软编码.

一、软编码和硬编码如何区分 软编码:使用CPU进行编码
硬编码:使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等
二、软编码和硬编码比较 软编码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
硬编码:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码.
三、目前的主流GPU加速平台 Intel、AMD、NVIDIA
四、目前主流的GPU平台开发框架 CUDA:NVIDIA的封闭编程框架,通过框架可以调用GPU计算资源
AMD APP:AMD为自己的GPU提出的一套通用并行编程框架,标准开放,通过在CPU、GPU同时支持OpenCL框架,进行计算力融合。
OpenCL:开放计算语言,为异构平台编写程序的该框架,异构平台可包含CPU、GPU以及其他计算处理器,目标是使相同的运算能支持不同平台硬件加速。
Inel QuickSync:集成于Intel显卡中的专用视频编解码模块。
下面我们来完整的实现以下从视频采集开始, 到视频编码为h264格式, 然后再硬解码后播放的流程.
1. 视频采集 1.1创建Session
AVCaptureSession *session = [[AVCaptureSession alloc] init]; session.sessionPreset = AVCaptureSessionPreset1280x720;

1.2 设置视频的输入
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo]; NSError *error; AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error]; [session addInput:input];

1.3 设置视频的输出
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init]; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); [output setSampleBufferDelegate:self queue:queue]; [output setAlwaysDiscardsLateVideoFrames:YES]; [session addOutput:output]; // 视频输出的方向 // 注意: 设置方向, 必须在将output添加到session之后 AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo]; if (connection.isVideoOrientationSupported) { connection.videoOrientation = AVCaptureVideoOrientationPortrait; } else { NSLog(@"不支持设置方向"); }

1.4 添加预览层
AVCaptureVideoPreviewLayer *layer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session]; layer.frame = preView.bounds; [preView.layer insertSublayer:layer atIndex:0];

1.5 开始采集
[session startRunning];

通过[output setSampleBufferDelegate:self queue:queue]; 方法为视频采集添加了代理, 之后采集到的数据会被打包成CMSampleBufferResf在代理函数中返回.
下面是CMSampleBufferRef的定义
/*! @typedefCMSampleBufferRef @abstractA reference to a CMSampleBuffer, a CF object containing zero or more compressed (or uncompressed) samples of a particular media type (audio, video, muxed, etc).*/ typedef struct CM_BRIDGED_TYPE(id) opaqueCMSampleBuffer *CMSampleBufferRef;

CMSampleBuffer是iOS对视频采集内容的一个打包, 里面会包含CMTime表示频率, FormatDescription来描述视频的一些信息, 包括SPS/PPS, 视频的尺寸等信息, 以及视频数据块, 数据块中存储了编码好的数据或解码后的数据.

iOS|iOS 利用VideoToolBox对视频进行编解码
文章图片
CMSampleBuffer的组成 1.6 在采集代理中处理视频采集数据
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { [self.encoder encodeFrame:sampleBuffer]; }

2. 视频编码 视频编码需要使用到VTCompressionSession
/*! @typedefVTCompressionSessionRef @abstractA reference to a Video Toolbox Compression Session. @discussion A compression session supports the compression of a sequence of video frames. The session reference is a reference-counted CF object. To create a compression session, call VTCompressionSessionCreate; then you can optionally configure the session using VTSessionSetProperty; then to encode frames, call VTCompressionSessionEncodeFrame. To force completion of some or all pending frames, call VTCompressionSessionCompleteFrames. When you are done with the session, you should call VTCompressionSessionInvalidate to tear it down and CFRelease to release your object reference. */typedef struct CM_BRIDGED_TYPE(id) OpaqueVTCompressionSession*VTCompressionSessionRef;

VTCompressionSession用于压缩视频帧序列, 这是一个支持引用技术的CF对象. 系统提供了一些函数来使用它.
  1. 创建会话:VTCompressionSessionCreate
  2. 配置会话:VTSessionSetProperty
  3. 编码帧: VTCompressionSessionEncodeFrame
  4. 强制完成所有待处理的帧: VTCompressionSessionCompleteFrames
  5. 销毁会话: VTCompressionSessionInvalidate
注意: 使用完成后要调用CFRelease销毁对象
2.1 创建会话
VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressionCallback, (__bridge void * _Nullable)(self), &_compressionSession);

参数列表:
  1. 用于创建其内存分配模式, 传入NULL使用默认模式
  2. 帧的宽度,以像素为单位。如果视频编码器无法支持所提供的宽度和高度,则可能会更改它们。
  3. 帧的高度(以像素为单位)
  4. 编码类型(我们使用kCMVideoCodecType_H264)
  5. 指定必须使用的特定视频编码器。传递NULL以让视频工具箱选择编码器。
  6. 源像素缓冲区的必需属性,在为源帧创建像素缓冲池时使用。如果不希望Video Toolbox来创建,传递NULL.
  7. 压缩数据的分配器。传递NULL以使用默认分配器
  8. 编码的回调函数, 编码的后的数据可以通过这个函数回调给调用者. 这个回调可能是异步的, 函数可能会在其他线程中被执行. 后面会介绍这个函数.
  9. 可能会用到的一些参数, 会被传入到回调函数中
  10. compressionSession
一起来看一下我们需要传入的回调函数:
typedef void (*VTCompressionOutputCallback)( void * CM_NULLABLE outputCallbackRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CM_NULLABLE CMSampleBufferRef sampleBuffer );

函数中的参数:
  1. 在创建会话时我们传递进去的参数(参数9)
  2. 视频帧的相关参数, 调用VTCompressionSessionEncodeFrame时传入的名为sourceFrameRefCon参数
  3. 错误码, 当status为noErr时表示成功
  4. 编码操作中的一些信息, kVTEncodeInfo_Asynchronous表示编码在异步执行, kVTEncodeInfo_FrameDropped表示丢帧
  5. 编码后的CMSampleBuffer, 如果出现异常或者丢帧等状况会返回NULL
一会我们再来看编码函数的实现, 先接着会话的创建继续往下配置会话. 前面我们看到VTCompressionSession提供了VTSessionSetProperty函数来为其配置参数.
2.2 配置属性
// 2.2.设置帧率 VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef _Nonnull)(@24)); // 2.3.设置比特率(码率) 1500000/s VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef _Nonnull)(@1500000)); // bit VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFTypeRef _Nonnull)(@[@(1500000/8), @1])); // byte 除以8是将bit转化为byte// 2.4.设置GOP的大小 VTSessionSetProperty(_compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nonnull)(@20));

属性配置完后我们就可以准备编码了, 调用VTCompressionSessionPrepareToEncodeFrames让系统为会话分配资源, 如果没有调用这个函数, 那他会在第一次调用VTCompressionSessionEncodeFrame时被调用. 重复调用这个函数是不会有重复效果的.
2.3 准备编码
// 3.准备编码 VTCompressionSessionPrepareToEncodeFrames(_compressionSession);

2.4 开始编码
CMTime pts = CMTimeMake(self.frameIndex, 24); VTCompressionSessionEncodeFrame(self.compressionSession, imageBuffer, pts, kCMTimeInvalid, NULL, NULL, NULL);

来看一下VTCompressionSessionEncodeFrame函数
VTCompressionSessionEncodeFrame( CM_NONNULL VTCompressionSessionRefsession, CM_NONNULL CVImageBufferRefimageBuffer, CMTimepresentationTimeStamp, CMTimeduration, // may be kCMTimeInvalid CM_NULLABLE CFDictionaryRefframeProperties, void * CM_NULLABLEsourceFrameRefcon, VTEncodeInfoFlags * CM_NULLABLEinfoFlagsOut ) API_AVAILABLE(macosx(10.8), ios(8.0), tvos(10.2));

参数列表:
  1. 压缩会话
  2. 带压缩的视频帧, 这里需要传入的是一个CVImageBufferRef, 我们需要将得到的CMSampleBuffer转换成CVImageBuffer, 使用函数CMSampleBufferGetImageBuffer转化
  3. 当前帧的时间戳, 会被附加到sample中, 这里使用的是CMTime, CMTime表示的是每一帧使用的时间, 例如CMTime(2, 30), 这里将一秒分成了30份, 那么2就代表2/30秒, 使用CMTime可以更精确的表示时间.我们用frameIndex记录当前是第几帧, 同时默认视频是24帧的, 那么第一帧的时间用CMTime表示CMTime(1, 24) = 1 / 24 秒, 第二帧CMTime(2, 24) = 2 / 24秒, 以此来表示当前的时间戳.
  4. 当前帧所占用的时间, 也会被附加进samplebuffer中, 如果没有这方面的信息, 则传入kCmMTimeInvalid
  5. 一些会话属性, 可以为NULL
  6. 一些在回调函数中可能会用到的参数, 即sourceFrameRefCon参数
  7. 回调函数中的infoFlags参数
2.5 编码的回调
接下来, 系统会对视频进行编码, 并将编码的结果回调到我们预先准备好的回调函数中, 在这里我们可以对数据进行拼装, 制成h264文件, 写入到app的指定目录下
2.5.1 判断是否关键帧
对于关键帧, 他与其他P/B帧不同的是会含有一些关键信息, SPS/PPS信息, 所以我们需要先判断读取到的数据是否是关键帧数据. 文件的相关属性保存在samplebuffer中, 我们通过以下函数来获取:
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true); CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0); BOOL isKeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);

如果是关键帧, 则需要进行SPS和PPS的拼接
if (isKeyFrame) { // 2.1.从CMSampleBufferRef获取CMFormatDescriptionRef CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); // 2.2.获取SPS信息 const uint8_t *spsOut; size_t spsSize, spsCount; CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &spsOut, &spsSize, &spsCount, NULL); // 2.3.获取PPS信息 const uint8_t *ppsOut; size_t ppsSize, ppsCount; CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &ppsOut, &ppsSize, &ppsCount, NULL); // 2.4.将SPS/PPS转成NSData, 并且写入文件 NSData *spsData = https://www.it610.com/article/[NSData dataWithBytes:spsOut length:spsSize]; NSData *ppsData = [NSData dataWithBytes:ppsOut length:ppsSize]; // 2.5.写入文件 [encoder gotSpsPps:spsData pps:ppsData]; }

关键帧信息保存在FormatDescription中.
这里有一个知识点, h264编码的最小数据单元为NALU单元, 每个NALU单元以固定的开头作为startCode, 且必须是: 00 00 00 01 或者00 00 01. 所以我们在文件中先写入"\x00\x00\x00\x01"四字节内容, 再将sps/pps追加上去.
2.5.2 写入blockBuffer文件
// 3.获取编码后的数据, 写入文件 // 3.1.获取CMBlockBufferRef CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); // 3.2.从blockBuffer中获取起始位置的内存地址 size_t totalLength = 0; char *dataPointer; CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer); // 3.3.一帧的图像可能需要写入多个NALU单元 --> Slice切换 static const int H264HeaderLength = 4; size_t bufferOffset = 0; while (bufferOffset < totalLength - H264HeaderLength) { // 3.4.从起始位置拷贝H264HeaderLength长度的地址, 计算NALULength int NALULength = 0; memcpy(&NALULength, dataPointer + bufferOffset, H264HeaderLength); // 大端模式/小端模式-->系统模式 // H264编码的数据是大端模式(字节序) NALULength = CFSwapInt32BigToHost(NALULength); // 3.5.从dataPointer开始, 根据长度创建NSData NSData *data = [NSData dataWithBytes:dataPointer + bufferOffset + H264HeaderLength length:NALULength]; // 3.6.写入文件 [encoder gotEncodedData:data isKeyFrame:YES]; // 3.7.重新设置bufferOffset bufferOffset += NALULength + H264HeaderLength; }

注意每个buffer要减去四字节, 而这里的四字节Header并不是0001的开始码, 而是大端模式的帧长度length
最后看一下有关文件写入的两个方法:
- (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]; } }

需要注意的有两点, 一是字符串是以'\0'为结尾的, 所以长度要减去'\0'这一字符的长度, 第二是每一个NALU单元都必须以'0001'作为开始码, 所以要手动附加上0001作为byteHeader在每个NALU单元, 包括SPS/PPS的NALU单元.
3. 视频解码 相比于视频编码, 视频解码的过程要相对复杂一些, 在开始解码前, 我们需要了解几个用到的对象:
  1. VTDecompressionSessionRef
    与编码时用到的VTCompressionSession相对应, 这是iOS中用于视频帧解码的会话, 系统也为我们提供了一些函数来处理视频解码的工作.
    1.1 VTDecompressionSessionCreate: 用于创建会话
    1.2 VTSessionSetProperty: 用于配置参数
    1.3 VTDecompressionSessionDecodeFrame: 解码调用的函数
    1.4 VTDecompressionSessionInvalidate: 销毁时调用的函数
  2. CMFormatDescriptionRef
    前面介绍了CMSampleBuffer的组成, 其中用于描述视频信息如:视频宽高,格式(kCMPixelFormat_32RGBA, kCMVideoCodecType_H264), 其他诸如颜色空间等信息的扩展被存放在CMFormatDescriptionRef中, 解码时, CMFormatDescriptionRef将会作为重要参数被传入到VTDecompressionSessionRef中.
  3. CVPixelBuffer
    typealias CVPixelBuffer = CVImageBuffer,CVImageBuffer是一种保存图像数据的抽象类型,表示未经编码或解码后的图像数据结构, 解码后我们得到的就是CVImageBuffer, 将他交给ImageView来播放.
@interface ViewController () {uint8_t *inputBuffer; // 读出的数据流的缓存空间 long inputSize; // 读出读出的数据流的大小(即当前inputBuffer中数据流的大小) long maxInputSize; // 最大缓存的数据流大小(即inputBuffer的大小)uint8_t *packetBuffer; // 读出的一帧packet的空间 long packetSize; // 一帧packet的大小size_t mSPSSize; // SPS的大小 uint8_t *mSPS; // SPS的指针 size_t mPPSSize; // PPS的大小 uint8_t *mPPS; // PPS的指针NSInputStream *inputStream; // 读取数据的流文件CMFormatDescriptionRef mFormatDescription; // 解码器的描述对象, 封装了sps,pps文件 VTDecompressionSessionRef mDecodeSession; // 解码的对象 }

流程 定时读取数据 --> 解码读出的帧 --> 交给AAPLEAGLLayer播放
  1. 准备工作
// 1.获取mOpenGLView用于之后展示数据 self.playLayer = [[AAPLEAGLLayer alloc] initWithFrame:self.view.bounds]; self.playLayer.backgroundColor = [UIColor blackColor].CGColor; [self.view.layer insertSublayer:self.playLayer atIndex:0]; // 解码的队列 self.mDecodeQueue = dispatch_get_global_queue(0, 0); // 创建CADisplayLink, 用于定时获取信息 self.mDispalyLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateFrame)]; self.mDispalyLink.frameInterval = 2; // 默认是30FPS的帧率录制 [self.mDispalyLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [self.mDispalyLink setPaused:YES];

这里使用AAPLEAGLLayer作为播放器, 创建一个同步全局队列用于解码. 因为解码出来的文件是有序的, 所以这里我们需要用到同步队列. 创建一个CADisplayLink的定时器, 默认是60FPS的, 我们设置帧间隔为2, 让他以30FPS播放, 播放与编码时候的帧率要保持一致.
  1. 开始解码
  • 创建inputBuffer缓存空间, 创建文件流, 打开文件流, 打开定时器定时读取帧文件
maxInputSize = 640 * 480; inputBuffer = malloc(maxInputSize); inputStream = [NSInputStream inputStreamWithFileAtPath:[[NSBundle mainBundle] pathForResource:@"video" ofType:@"h264"]]; [inputStream open]; [self.mDispalyLink setPaused:NO];

  • 读取文件
// 读取时, 发现packet和packetBuffer都是有值的说明这里面保存着上一帧的内容, 则将这些内容清空 if (packetSize && packetBuffer) { packetSize = 0; free(packetBuffer); packetBuffer = NULL; }if (inputStream.hasBytesAvailable && inputSize < maxInputSize) {NSLog(@"current inputSize = %ld", inputSize); inputSize += [inputStream read:inputBuffer + inputSize maxLength:(maxInputSize - inputSize)]; NSLog(@"read inputSize = %ld", inputSize); } // 比较是否为开始码, 只有开头是开始码才表示这是一个完整的NALU文件的开始 if (memcmp(inputBuffer, lyStartCode, 4) == 0) {if (inputSize > 4) {uint8_t *startP = inputBuffer + 4; uint8_t *endP = inputBuffer + inputSize; while (startP != endP) {if (memcmp(startP - 3, lyStartCode, 4) == 0) {packetSize = startP - 3 - inputBuffer; packetBuffer = malloc(packetSize); memcpy(packetBuffer, inputBuffer, packetSize); // 将一个完整的packet连同开始码写进一个buffer中 memmove(inputBuffer, inputBuffer + packetSize, inputSize - packetSize); // inputBuffer 左移, 将写进packetBuffer中的内容覆盖掉 inputSize -= packetSize; // 减去packetSize的长度, 因为此时inputBuffer中已经没有packetBuffer中的数据了, inputBuffer中实际内容的长度减少了packetSize break; } else {startP++; } } } }

读取结束后, 如果packetBuffer和packetSize都为0, 表示已经没有更多内容被读取了, 这时候结束解码, 释放资源
if (packetBuffer == NULL || packetSize == 0) {[self onPutEnd]; return; }

  • 解码
// 获取nalusize的大小 uint32_t naluSize = (uint32_t)(packetSize - 4); // 获取packetBuffer的指向 uint32_t *pNaluSize = (uint32_t *)packetBuffer; // 转成大端主机地址 *pNaluSize = CFSwapInt32HostToBig(naluSize); // 在buffer的前面填入代表长度的int CVPixelBufferRef pixelBuffer = NULL; int nalType = packetBuffer[4] & 0x1F; switch (nalType) { case 0x07: // 5.1.获取SPS信息, 并且保存 mSPSSize = packetSize - 4; mSPS = malloc(mSPSSize); memcpy(mSPS, packetBuffer + 4, mSPSSize); break; case 0x08: // 5.2.获取PPS信息, 并且保存起来 mPPSSize = packetSize - 4; mPPS = malloc(mPPSSize); memcpy(mPPS, packetBuffer + 4, mPPSSize); break;

SPS和PPS的nal单元以7/8开头, 如果得到这样的开头就可以判断出是否为SPS文件或PPS文件. 注意, naltype位于packet的第五个字节, 也就是packetBuffer[4], 前四个字节是0001开始码.
NAL单元以5开头则表示为关键帧, 在解码关键帧的时候需要先构建VTDecompressionSessionRef和CMFormatDescriptionRef
case 0x05: // 5.3.初始化硬解码需要的内容 [self initVideoToolBox]; // 5.4.编码I帧数据 pixelBuffer = [self decode]; break;

- (void)initVideoToolBox {const uint8_t *parameterSetPointers[2] = {mSPS, mPPS}; const size_t parameterSetSizes[2] = {mSPSSize, mPPSSize}; // 创建formatDescripiton OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(NULL, 2, parameterSetPointers, parameterSetSizes, 4, &mFormatDescription); NSLog(@"mFormatDescription creat %@", status == noErr ? @"success" : @"failed"); // 配置参数 NSDictionary *attr = @{(__bridge NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)}; VTDecompressionOutputCallbackRecord decompressionCallbackRecord; decompressionCallbackRecord.decompressionOutputCallback = didCompressed; // 创建VTDecompressionSession对象 status = VTDecompressionSessionCreate(NULL, mFormatDescription, NULL, (__bridge CFDictionaryRef)attr, &decompressionCallbackRecord, &mDecodeSession); }

这里用到的一个回调函数"didCompressed"
void didCompressed (void * CM_NULLABLE decompressionOutputRefCon, void * CM_NULLABLE sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CM_NULLABLE CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ) {CVPixelBufferRef *outBufferRef = (CVPixelBufferRef *)sourceFrameRefCon; *outBufferRef = CVBufferRetain(imageBuffer); }

使用VideoToolBox解码时, 就会通过这样的回调拿到解码的结果, 我们在这个函数里拿到sourceFrameRefCon就是最终需要的CVPixeBuffer
然后调用解码的方法:
- (CVPixelBufferRef)decode {// 通过之前的packetBuffer/packetSize给blockBuffer赋值 CMBlockBufferRef blockBuffer = NULL; CMBlockBufferCreateWithMemoryBlock(NULL, (void *)packetBuffer, packetSize, kCFAllocatorNull, NULL, 0, packetSize, 0, &blockBuffer); // 创建准备的对象 CMSampleBufferRef sampleBuffer = NULL; const size_t sampleSizeArray = {packetSize}; CMSampleBufferCreateReady(NULL, blockBuffer, mFormatDescription, 0, 0, NULL, 0, &sampleSizeArray, &sampleBuffer); // 开始解码 CVPixelBufferRef outPixeBufferSource = NULL; VTDecompressionSessionDecodeFrame(mDecodeSession, sampleBuffer, 0, &outPixeBufferSource, NULL); CFRelease(sampleBuffer); CFRelease(blockBuffer); return outPixeBufferSource; }

而对于非关键帧, 就可以直接调用解码了.
default: // 5.5.解码B/P帧数据 pixelBuffer = [self decode]; break;

最后使用AAPPLEAGLLayer播放解码后的内容
if(pixelBuffer) { dispatch_async(dispatch_get_main_queue(), ^{ self.playLayer.pixelBuffer = pixelBuffer; CVPixelBufferRelease(pixelBuffer); }); }

【iOS|iOS 利用VideoToolBox对视频进行编解码】以上就是这次从采集到编码到解码的全部过程啦.

    推荐阅读