需求
本文主要将含有编码的H.264,H.265视频流文件解码为原始视频数据,解码后即可渲染到屏幕或用作其他用途.
实现原理
正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用苹果原生框架VideoToolbox解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上.
注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问.
阅读前提
代码地址 : Video Decoder
掘金地址 : Video Decoder
简书地址 : Video Decoder
博客地址 : Video Decoder
总体架构
总体思想即将FFmpeg parse到的数据装到CMBlockBuffer中,将extra data分离出的vps,sps,pps装到CMVideoFormatDesc中,将计算好的时间戳装到CMTime中,最后即可拼成完成的CMSampleBuffer以用来提供给解码器.
简易流程
FFmpeg parse流程
- 创建format context: avformat_alloc_context
- 打开文件流: avformat_open_input
- 寻找流信息: avformat_find_stream_info
- 获取音视频流的索引值: formatContext->streams[i]->codecpar->codec_type == (isVideoStream ? AVMEDIA_TYPE_VIDEO : AVMEDIA_TYPE_AUDIO)
- 获取音视频流: m_formatContext->streams[m_audioStreamIndex]
- 解析音视频数据帧: av_read_frame
- 获取extra data: av_bitstream_filter_filter
VideoToolbox decode流程
- 比较上一次的extra data,如果数据更新需要重新创建解码器
- 分离并保存FFmpeg parse到的extra data中分离vps, sps, pps等关键信息 (比较NALU头)
- 通过CMVideoFormatDescriptionCreateFromH264ParameterSets,CMVideoFormatDescriptionCreateFromHEVCParameterSets装载vps,sps,pps等NALU header信息.
- 指定解码器回调函数与解码后视频数据类型(yuv,RGB…)
- 创建解码器VTDecompressionSessionCreate
- 生成CMBlockBufferRef装载解码前数据,再将其转为CMSampleBufferRef以提供给解码器.
- 开始解码VTDecompressionSessionDecodeFrame
- 在回调函数中CVImageBufferRef即为解码后的数据,可转为CMSampleBufferRef传出.
文件结构
快速使用
- 初始化preview - 解码后的视频数据将渲染到该预览层 
| 1 | - (void)viewDidLoad { | 
- 解析并解码文件中视频数据 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16- - (void)startDecodeByVTSessionWithIsH265Data:(BOOL)isH265 { 
 NSString *path = [[NSBundle mainBundle] pathForResource:isH265 ? @"testh265" : @"testh264" ofType:@"MOV"];
 XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path];
 XDXVideoDecoder *decoder = [[XDXVideoDecoder alloc] init];
 decoder.delegate = self;
 [parseHandler startParseWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, struct XDXParseVideoDataInfo *videoInfo, struct XDXParseAudioDataInfo *audioInfo) {
 if (isFinish) {
 [decoder stopDecoder];
 return;
 }
 
 if (isVideoFrame) {
 [decoder startDecodeVideoData:videoInfo];
 }
 }];
 }
- 将解码后数据渲染到屏幕上 () 
注意: 如果数据中含有B帧则需要做一个重排序才能渲染,本例提供两个文件,一个不含B帧的h264类型文件,一个含B帧的h265类型文件.
| 1 | - (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer { | 
具体实现
1. 从Parse到的数据中检测是否需要更新extra data.
使用FFmpeg parse的数据装在XDXParseVideoDataInfo结构体中,结构体定义如下,parse模块可在上文链接中学习,本节只将解码模块.1
2
3
4
5
6
7
8
9
10
11
12struct XDXParseVideoDataInfo {
    uint8_t                 *data;
    int                     dataSize;
    uint8_t                 *extraData;
    int                     extraDataSize;
    Float64                 pts;
    Float64                 time_base;
    int                     videoRotate;
    int                     fps;
    CMSampleTimingInfo      timingInfo;
    XDXVideoEncodeFormat    videoFormat;
};
通过缓存当前extra data可以将当前获取的extra data与上一次的进行对比,如果改变需要重新创建解码器,如果没有改变则解码器可复用.(此代码尤其适用于网络流中的视频流,因为视频流可能会改变)
| 1 | uint8_t *extraData = videoInfo->extraData; | 
2. 从extra data中分离关键信息(h265:vps),sps,pps.
创建解码器必须要有NALU Header中的一些关键信息,如vps,sps,pps,以用来组成一个CMVideoFormatDesc描述视频信息的数据结构,如上图
注意: h264码流需要sps,pps, h265码流则需要vps,sps,pps
- 分离NALU Header
首先确定start code的位置,通过比较前四个字节是否为00 00 00 01即可. 对于h264的数据,start code之后紧接着的是sps,pps, 对于h265的数据则是vps,sps,pps
- 确定NALU Header长度
通过sps索引与pps索引值可以确定sps长度,其他类似,注意,码流结构中均以4个字节的start code作为分界符,所以需要减去对应长度.
- 分离NALU Header数据
对于h264类型数据将数据&上0x1F可以确定NALU header的类型,对于h265类型数据,将数据&上0x4F可以确定NALU header的类型,这源于h264,h265的码流结构,如果不懂请参考文章最上方阅读前提中码流结构相关文章.
得到对应类型的数据与大小后,将其赋给全局变量,即可供后面使用.
| 1 | if (isNeedUpdate) { | 
3. 创建解码器
根据编码数据类型确定使用h264解码器还是h265解码器,如上图我们可得知,我们需要将数据拼成一个CMSampleBuffer类型以传给解码器解码.
- 生成 CMVideoFormatDescriptionRef
通过(vps)sps,pps信息组成CMVideoFormatDescriptionRef. 这里需要注意的是, h265编码数据有的码流数据中含有两个pps, 所以在拼装时需要判断以确定参数数量.
- 确定视频数据类型
通过指定kCVPixelFormatType_420YpCbCr8BiPlanarFullRange将视频数据类型设置为yuv 420sp, 如需其他格式可自行更改适配.
- 指定回调函数
- 创建编码器
通过上面提供的所有信息,即可调用VTDecompressionSessionCreate生成解码器上下文对象.
| 1 | // create decoder | 
4. 开始解码
- 将parse出来的原始数据装在XDXDecodeVideoInfo结构体中,以便后续扩展使用.
| 1 | typedef struct { | 
- 将编码数据装在CMBlockBufferRef中.
- 通过CMBlockBufferRef生成CMSampleBufferRef
- 解码数据
通过VTDecompressionSessionDecodeFrame函数即可完成解码一帧视频数据.第三个参数可以指定解码采用同步或异步方式.
| 1 | 
 | 
5. 解码后的数据
解码后的数据可在回调函数中获取.这里需要将解码后的数据CVImageBufferRef转为CMSampleBufferRef.然后通过代理传出.
| 1 | #pragma mark - Callback | 
6.销毁解码器
用完后记得销毁,以便下次使用.1
2
3
4
5
6
7
8
9
10
11if (_decoderSession) {
    VTDecompressionSessionWaitForAsynchronousFrames(_decoderSession);
    VTDecompressionSessionInvalidate(_decoderSession);
    CFRelease(_decoderSession);
    _decoderSession = NULL;
}
if (_decoderFormatDescription) {
    CFRelease(_decoderFormatDescription);
    _decoderFormatDescription = NULL;
}
7. 补充:关于带B帧数据重排序问题
注意,如果视频文件或视频流中含有B帧,则渲染时需要对视频帧做一个重排序,本文重点讲解码,排序将在后面文章中更新,代码中以实现,如需了解请下载Demo.
 
        