内容简介:将编码的视频流解码为原始视频数据,编码视频流可以来自网络流或文件,解码后即可渲染到屏幕.正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用FFmpeg解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上.注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问.
需求
将编码的视频流解码为原始视频数据,编码视频流可以来自网络流或文件,解码后即可渲染到屏幕.
实现原理
正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用FFmpeg解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上.
注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问.
阅读前提
代码地址 : Video Decoder
掘金地址 : Video Decoder
简书地址 : Video Decoder
博客地址 :Video Decoder
总体架构
简易流程
FFmpeg parse流程
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 av_bitstream_filter_filter
FFmpeg decode流程
- 确定解码器类型:
enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name)
- 创建视频流:
int av_find_best_stream(AVFormatContext *ic,enum FfmpegaVMediaType type,int wanted_stream_nb,int related_stream,AVCodec **decoder_ret,int flags);
- 初始化解码器:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec)
- 填充解码器上下文:
int avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *par);
- 打开指定类型的设备:
int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type, const char *device, AVDictionary *opts, int flags)
- 初始化编码器上下文对象:
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options)
- 初始化视频帧:
AVFrame *av_frame_alloc(void)
- 找到第一个I帧开始解码:
packet.flags == 1
- 将parse到的压缩数据送给解码器:
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt)
- 接收解码后的数据:
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame)
- 构造时间戳
- 将解码后的数据存到
CVPixelBufferRef
并将其转为CMSampleBufferRef
,解码完成
文件结构
快速使用
-
初始化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)startDecodeByFFmpegWithIsH265Data:(BOOL)isH265 { NSString *path = [[NSBundle mainBundle] pathForResource:isH265 ? @"testh265" : @"testh264" ofType:@"MOV"]; XDXAVParseHandler *parseHandler = [[XDXAVParseHandler alloc] initWithPath:path]; XDXFFmpegVideoDecoder *decoder = [[XDXFFmpegVideoDecoder alloc] initWithFormatContext:[parseHandler getFormatContext] videoStreamIndex:[parseHandler getVideoStreamIndex]]; decoder.delegate = self; [parseHandler startParseGetAVPackeWithCompletionHandler:^(BOOL isVideoFrame, BOOL isFinish, AVPacket packet) { if (isFinish) { [decoder stopDecoder]; return; } if (isVideoFrame) { [decoder startDecodeVideoDataWithAVPacket:packet]; } }]; }
-
将解码后数据渲染到屏幕上
-(void)getDecodeVideoDataByFFmpeg:(CMSampleBufferRef)sampleBuffer { CVPixelBufferRef pix = CMSampleBufferGetImageBuffer(sampleBuffer); [self.previewView displayPixelBuffer:pix]; }
具体实现
1. 初始化实例对象
因为本例中的视频数据源是文件,而format context上下文实在parse模块初始化的,所以这里仅仅需要将其传入解码器即可.
- (instancetype)initWithFormatContext:(AVFormatContext *)formatContext videoStreamIndex:(int)videoStreamIndex { if (self = [super init]) { m_formatContext = formatContext; m_videoStreamIndex = videoStreamIndex; m_isFindIDR = NO; m_base_time = 0; [self initDecoder]; } return self; }
2. 初始化解码器
- (void)initDecoder { // 获取视频流 AVStream *videoStream = m_formatContext->streams[m_videoStreamIndex]; // 创建解码器上下文对象 m_videoCodecContext = [self createVideoEncderWithFormatContext:m_formatContext stream:videoStream videoStreamIndex:m_videoStreamIndex]; if (!m_videoCodecContext) { log4cplus_error(kModuleName, "%s: create video codec failed",__func__); return; } // 创建视频帧 m_videoFrame = av_frame_alloc(); if (!m_videoFrame) { log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__); avcodec_close(m_videoCodecContext); } }
2.1. 创建解码器上下文对象
- (AVCodecContext *)createVideoEncderWithFormatContext:(AVFormatContext *)formatContext stream:(AVStream *)stream videoStreamIndex:(int)videoStreamIndex { AVCodecContext *codecContext = NULL; AVCodec *codec = NULL; // 指定解码器名称, 这里使用苹果VideoToolbox中的硬件解码器 const char *codecName = av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX); // 将解码器名称转为对应的枚举类型 enum AVHWDeviceType type = av_hwdevice_find_type_by_name(codecName); if (type != AV_HWDEVICE_TYPE_VIDEOTOOLBOX) { log4cplus_error(kModuleName, "%s: Not find hardware codec.",__func__); return NULL; } // 根据解码器枚举类型找到解码器 int ret = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0); if (ret < 0) { log4cplus_error(kModuleName, "av_find_best_stream faliture"); return NULL; } // 为解码器上下文对象分配内存 codecContext = avcodec_alloc_context3(codec); if (!codecContext){ log4cplus_error(kModuleName, "avcodec_alloc_context3 faliture"); return NULL; } // 将视频流中的参数填充到视频解码器中 ret = avcodec_parameters_to_context(codecContext, formatContext->streams[videoStreamIndex]->codecpar); if (ret < 0){ log4cplus_error(kModuleName, "avcodec_parameters_to_context faliture"); return NULL; } // 创建硬件解码器上下文 ret = InitHardwareDecoder(codecContext, type); if (ret < 0){ log4cplus_error(kModuleName, "hw_decoder_init faliture"); return NULL; } // 初始化解码器上下文对象 ret = avcodec_open2(codecContext, codec, NULL); if (ret < 0) { log4cplus_error(kModuleName, "avcodec_open2 faliture"); return NULL; } return codecContext; } #pragma mark - C Function AVBufferRef *hw_device_ctx = NULL; static int InitHardwareDecoder(AVCodecContext *ctx, const enum AVHWDeviceType type) { int err = av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0); if (err < 0) { log4cplus_error("XDXParseParse", "Failed to create specified HW device.\n"); return err; } ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx); return err; }
-
av_find_best_stream : 在文件中找到最佳流信息.
- ic: 媒体文件
- type: video, audio, subtitles…
- wanted_stream_nb: 用户请求的流编号,-1表示自动选择
- related_stream: 试着找到一个相关的流,如果没有可填-1
- decoder_ret: 非空返回解码器引用
- flags: 保留字段
-
avcodec_parameters_to_context: 根据提供的解码器参数中的值填充解码器上下文
仅仅将解码器中具有相应字段的任何已分配字段par被释放并替换为par中相应字段的副本。不涉及解码器中没有par中对应项的字段。
- av_hwdevice_ctx_create: 打开指定类型的设备并为其创建AVHWDeviceContext。
- avcodec_open2: 使用给定的AVCodec初始化AVCodecContext,在使用此函数之前,必须使用avcodec_alloc_context3()分配内存。
int av_find_best_stream(AVFormatContext *ic, enum FfmpegaVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags);
2.2. 创建视频帧
AVFrame
承载了原始的音视频数据.AVFrame通常被分配一次然后多次重复(例如,单个AVFrame以保持从解码器接收的帧)。在这种情况下,av_frame_unref()将释放框架所持有的任何引用,并在再次重用之前将其重置为其原始的清理状态。
// Get video frame m_videoFrame = av_frame_alloc(); if (!m_videoFrame) { log4cplus_error(kModuleName, "%s: alloc video frame failed",__func__); avcodec_close(m_videoCodecContext); }
3. 开始解码
首先找到编码数据流中第一个I帧, 然后调用 avcodec_send_packet
将压缩数据发送给解码器.最后利用循环接收 avcodec_receive_frame
解码后的视频数据.构造时间戳,并将解码后的数据填充到 CVPixelBufferRef
中并将其转为 CMSampleBufferRef
.
- (void)startDecodeVideoDataWithAVPacket:(AVPacket)packet { if (packet.flags == 1 && m_isFindIDR == NO) { m_isFindIDR = YES; m_base_time = m_videoFrame->pts; } if (m_isFindIDR == YES) { [self startDecodeVideoDataWithAVPacket:packet videoCodecContext:m_videoCodecContext videoFrame:m_videoFrame baseTime:m_base_time videoStreamIndex:m_videoStreamIndex]; } } - (void)startDecodeVideoDataWithAVPacket:(AVPacket)packet videoCodecContext:(AVCodecContext *)videoCodecContext videoFrame:(AVFrame *)videoFrame baseTime:(int64_t)baseTime videoStreamIndex:(int)videoStreamIndex { Float64 current_timestamp = [self getCurrentTimestamp]; AVStream *videoStream = m_formatContext->streams[videoStreamIndex]; int fps = DecodeGetAVStreamFPSTimeBase(videoStream); avcodec_send_packet(videoCodecContext, &packet); while (0 == avcodec_receive_frame(videoCodecContext, videoFrame)) { CVPixelBufferRef pixelBuffer = (CVPixelBufferRef)videoFrame->data[3]; CMTime presentationTimeStamp = kCMTimeInvalid; int64_t originPTS = videoFrame->pts; int64_t newPTS = originPTS - baseTime; presentationTimeStamp = CMTimeMakeWithSeconds(current_timestamp + newPTS * av_q2d(videoStream->time_base) , fps); CMSampleBufferRef sampleBufferRef = [self convertCVImageBufferRefToCMSampleBufferRef:(CVPixelBufferRef)pixelBuffer withPresentationTimeStamp:presentationTimeStamp]; if (sampleBufferRef) { if ([self.delegate respondsToSelector:@selector(getDecodeVideoDataByFFmpeg:)]) { [self.delegate getDecodeVideoDataByFFmpeg:sampleBufferRef]; } CFRelease(sampleBufferRef); } } }
- avcodec_send_packet: 将压缩视频帧数据送给解码器
- avcodec_receive_frame: 从解码器中获取解码后的数据
4. 停止解码
释放相关资源
- (void)stopDecoder { [self freeAllResources]; } - (void)freeAllResources { if (m_videoCodecContext) { avcodec_send_packet(m_videoCodecContext, NULL); avcodec_flush_buffers(m_videoCodecContext); if (m_videoCodecContext->hw_device_ctx) { av_buffer_unref(&m_videoCodecContext->hw_device_ctx); m_videoCodecContext->hw_device_ctx = NULL; } avcodec_close(m_videoCodecContext); m_videoCodecContext = NULL; } if (m_videoFrame) { av_free(m_videoFrame); m_videoFrame = NULL; } }
以上所述就是小编给大家介绍的《iOS利用FFmpeg实现视频硬解码》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- iOS利用VideoToolbox实现视频硬解码
- iOS利用FFmpeg解码音频数据并播放
- Netty-解码器架构与常用解码器
- Glide 缓存与解码复用
- 音频解码 Audio Converter
- golang之Json编码解码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
学习JavaScript数据结构与算法
[巴西] 格罗纳(Loiane Groner) / 孙晓博、邓钢、吴双、陈迪、袁源 / 人民邮电出版社 / 2015-10-1 / 39.00
本书首先介绍了JavaScript语言的基础知识,接下来讨论了数组、栈、队列、链表、集合、字典、散列表、树、图等数据结构,之后探讨了各种排序和搜索算法,包括冒泡排序、选择排序、插入排序、归并排序、快速排序、顺序搜索、二分搜索,还介绍了动态规划和贪心算法等常用的高级算法及相关知识。一起来看看 《学习JavaScript数据结构与算法》 这本书的介绍吧!