使用VideoToolbox硬编&硬解
VideoToolbox简介
VideoToolbox 是一个低级的框架,可直接访问硬件的编解码器。能够为视频提供压缩和解压缩的服务,同时也提供存储在 CoreVideo 像素缓冲区的图像进行格式的转换。
优点
- 利用GPU或者专用处理器对视频流进行编解码,不用大量占用CPU资源。性能高,很好的实时性。
缺点
- 低码率下通常质量低于软编
VideoToolbox数据
-
CVPixelBuffer
// CVPixelBuffer 与 CVImageBuffer 类型相同 typealias CVPixelBuffer = CVImageBuffer
CVPixelBuffer 是存储在内存中的一个未压缩的光栅图像 Buffer,包括图像的宽度、高度等。
-
CMBlockBuffer
CMBlockBuffer 是一个任意的 Buffer,相当于 Buffer 中的 Any. 在管道中压缩视频的时候,会把它包装成 CMBlockBuffer。相当于 CMBlockBuffer 代表着一个压缩的数据。
-
CMSampleBuffer
CMSampleBuffer 可能是一个压缩的数据,也可能是一个未压缩的数据。取决于 CMSampleBuffer 里面是 CMBlockBuffer(压缩后) 还是 CVPixelBuffer(未压缩)
对于VideoToolbox,可以通过直接访问硬编解码器,将 H.264 文件或传输流转换为 iOS上的 CMSampleBuffer
并解码成 CVPixelBuffer
, 或将未压缩的 CVPixelBuffer
编码成 CMSampleBuffer
(将未编码的CMSampleBuffer(CVPixelBuffer)与已编码的CMSampleBuffer(CMBlockBuffer)的相互转换):
解码
- H.264 -> CMSampleBuffer -> CVPixelBuffer
编码:
- CVPixelBuffer -> CMSampleBuffer -> H.264
解码
把原始码流包装成 CMSampleBuffer
解码前的原始数据为H264码流,iOS可以使用 NSInputStream
读取H264文件。
H264 有两种封装格式,一种为 MP4 格式,一种是annexb格式。MP4格式是以NALU的长度分割;annexb格式是以 0x00000001 或 0x0000000001 分割。
VideoToolbox解码使用的 H264 为MP4格式,因此需要替换NALU的Header
-
使用 CMVideoFormatDescriptionCreateFromH264ParameterSets 将 SPS 和 PPS 封装成 CMVideoFormatDescription
typealias CMVideoFormatDescription = CMFormatDescription
-
修改 NALU 的 Header
NALU 只要有两种格式:Annex B 和 AVCC。Annex B 格式以 0x 00 00 01 或 0x 00 00 00 01 开头, AVCC 格式以所在 NALU 的长度开头。
替换掉NALU 的 StartCode
使用 CMBlockBufferCreateWithMemoryBlock 接口将 NALU unit 封装成 CMBlockBuffer
通过 CMSampleBufferCreate 将 CMBlockBuffer + CMVideoFormatDescription + CMTime 创建成 CMSampleBuffer
解码流程:
-
使用
VTDecompressionSessionCreate
创建解码会话VT_EXPORT OSStatus VTDecompressionSessionCreate( // 会话的分配器,默认使用kCFAllocatorDefault CM_NULLABLE CFAllocatorRef allocator, // 源视频帧的描述(包含SPS & PPS 信息) CM_NONNULL CMVideoFormatDescriptionRef videoFormatDescription, // 视频解码器(默认为空,由 VideoToolbox 选择) CM_NULLABLE CFDictionaryRef videoDecoderSpecification, // 包含解码配置信息的数组 CM_NULLABLE CFDictionaryRef destinationImageBufferAttributes, // 回调函数 const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback, // 解码会话对象的指针 CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut)
-
使用
VTSessionSetProperty
设置会话设置VTSessionSetProperty( // 解码会话 CM_NONNULL VTSessionRef session, // 属性 KEY CM_NONNULL CFStringRef propertyKey, // 设置的属性值 CM_NULLABLE CFTypeRef propertyValue )
-
使用
VTDecompressionSessionDecodeFrame
编码视频帧,在之前设置的回调函数中获取编码后的结果VT_EXPORT OSStatus VTDecompressionSessionDecodeFrame( // 解码会话 CM_NONNULL VTDecompressionSessionRef session, // 要解码的视频数据(包含一个或多个视频帧) CM_NONNULL CMSampleBufferRef sampleBuffer, // 解码器和解码会话的指令 VTDecodeFrameFlags decodeFlags, // 解码后的数据 void * CM_NULLABLE sourceFrameRefCon, VTDecodeInfoFlags * CM_NULLABLE infoFlagsOut)
回调函数返回数据
typedef void (*VTDecompressionOutputCallback)( // VTDecompressionOutputCallbackRecord 的 decompressionOutputRefCon字段值 void * CM_NULLABLE decompressionOutputRefCon, // 解码返回的数据 void * CM_NULLABLE sourceFrameRefCon, // 错误码 OSStatus status, // 解码操作的信息 VTDecodeInfoFlags infoFlags, // 包含解压缩的帧数据 CM_NULLABLE CVImageBufferRef imageBuffer, // 帧数据的时间戳 CMTime presentationTimeStamp, // 帧数据的表示时间 CMTime presentationDuration );
使用
VTCompressionSessionCompleteFrames
强制结束并完成编码编码完成后使用
VTCompressionSessionInvalidate
结束编码,并释放内存
编码
-
使用
VTDecompressionSessionCreate
创建 session(编码会话)VTCompressionSessionCreate( // 分配器,传NULL或KCFAllocatorDefault CM_NULLABLE CFAllocatorRef allocator, // 宽度 int32_t width, // 高度 int32_t height, // 编码类型 CMVideoCodecType codecType, // 编码规范 传NULL,videotoolbox自行选择 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)
-
VTSessionSetProperty
配置相关属性设置一些例如码率、帧率、分辨率等属性
- FPS(Frames PerSecond):每秒刷新的帧数。帧数越高,流畅度越高
- 分辨率
- 比特率/码率:表示经过编码(压缩)后的视频数据每秒钟需要用多少个比特来表示。比特率越高,视频的质量就越好;但编码后的文件也就越大。
-
VTCompressionSessionPrepareToEncodeFrames
准备编码VTCompressionSessionPrepareToEncodeFrames(self.session);
-
调用
VTCompressionSessionEncodeFrame
传入需要编码的视频帧VTCompressionSessionEncodeFrame( // 编码会话 CM_NONNULL VTCompressionSessionRef session, // 要编码的数据 CM_NONNULL CVImageBufferRef imageBuffer, // 时间戳 CMTime presentationTimeStamp, // 表示时间(may be kCMTimeInvalid) CMTime duration, // 数据的其他属性(key-value) CM_NULLABLE CFDictionaryRef frameProperties, // 帧数据的引用,将被传递给回调函数 void * CM_NULLABLE sourceFrameRefCon, VTEncodeInfoFlags * CM_NULLABLE infoFlagsOut )
-
执行编码回调函数
VTCompressionOutputCallback
如果是关键帧调用
CMSampleBufferGetFormatDescription
获取CMFormatDescriptionRef
,;然后用
CMVideoFormatDescriptionGetH264ParameterSetAtIndex
取得PPS和SPS;最后把每一帧的所有NALU数据前四个字节变成 0X00,00,00,01 之后再写入文件
void didCompressionOutputCallback(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) { //获取传入的参数 VideoEncode *encode = (__bridge VideoEncode *)outputCallbackRefCon; //判断是否是关键帧 CFArrayRef arrayRef = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true,); }
-
结束编码
调用编码完成函数,将编码会话销毁,释放资源
VTCompressionSessionCompleteFrames(session, KCMTimeInvalid); VTCompressionSessionInvalidate(session); CFRelease(session); session = NULL; frameID = 0;