需求
本文主要将含有编码的H.264,H.265视频流文件解码为原始视频数据,解码后即可渲染到屏幕或用作其他用途.
实现原理
正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用苹果原生框架VideoToolbox解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上.
注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问.
阅读前提
代码地址 : Video Decoder
掘金地址 : Video Decoder
简书地址 : Video Decoder
博客地址 : Video Decoder
1、总体架构
总体思想即将FFmpeg parse到的数据装到CMBlockBuffer
中,将extra data分离出的vps,sps,pps装到CMVideoFormatDesc
中,将计算好的时间戳装到CMTime
中,最后即可拼成完成的CMSampleBuffer
以用来提供给解码器.
CMSampleBufferCreate
1.1 简易流程
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
传出.
1.2 文件结构
image
1.3 快速使用
-
初始化preview
解码后的视频数据将渲染到该预览层
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
}
- (void)setupUI {
self.previewView = [[XDXPreviewView alloc] initWithFrame:self.view.frame];
[self.view addSubview:self.previewView];
[self.view bringSubviewToFront:self.startBtn];
}
-
解析并解码文件中视频数据
- (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类型文件.
- (void)getVideoDecodeDataCallback:(CMSampleBufferRef)sampleBuffer {
if (self.isH265File) {
// Note : the first frame not need to sort.
if (self.isDecodeFirstFrame) {
self.isDecodeFirstFrame = NO;
CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer);
[self.previewView displayPixelBuffer:pix];
}
XDXSortFrameHandler *sortHandler = [[XDXSortFrameHandler alloc] init];