网站首页 > 开源技术 正文
需求
将编码的视频流解码为原始视频数据,编码视频流可以来自网络流或文件,解码后即可渲染到屏幕.
实现原理
正如我们所知,编码数据仅用于传输,无法直接渲染到屏幕上,所以这里利用FFmpeg解析文件中的编码的视频流,并将压缩视频数据(h264/h265)解码为指定格式(yuv,RGB)的视频原始数据,以渲染到屏幕上。 注意: 本例主要为解码,需要借助FFmpeg搭建模块,视频解析模块,渲染模块,这些模块在下面阅读前提皆有链接可直接访问。
总体架构 简易流程
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
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];
}
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
具体实现
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);
}
}
}
- AVERROR(EAGAIN): 当前状态下不接受输入,用户必须通过avcodec_receive_frame()读取输出的buffer. (一旦所有输出读取完毕,packet应该被重新发送,调用不会失败)
- AVERROR_EOF: 解码器已经被刷新,没有新的packet能发送给它.
- AVERROR(EINVAL): 解码器没有被打开
- AVERROR(ENOMEM): 将Packet添加到内部队列失败.
- AVERROR(EAGAIN): 输出不可用, 用户必须尝试发送一个新的输入数据
- AVERROR_EOF: 解码器被完全刷新,这儿没有更多的输出帧
- AVERROR(EINVAL): 解码器没有被打开.
- 其他负数: 解码错误.
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;
}
}
猜你喜欢
- 2024-10-20 今日头条在消息服务平台和容灾体系建设方面的实践与思考
- 2024-10-20 教你如何解决最常见的58种网络故障排除方法
- 2024-10-20 Linux 问题故障定位,看这一篇就够了,九招搞定所有问题
- 2024-10-20 iOS大解密:玄之又玄的KVO(解密电视剧全集在线观看免费完整版)
- 2024-10-20 在C语言中,如何优雅地实现全局错误日志记录?
- 2024-10-20 嵌入式大杂烩周记 第 7 期:zlog(嵌入式实战)
- 2024-10-20 纯C日志函数库 zlog(c语言日志模块)
- 2024-10-20 嵌入式老司机这样打log(嵌入式logo)
- 2024-10-20 log4c ,一个轻量级的C++日志库(log4j日志)
- 2024-10-20 iOS利用VideoToolbox实现视频硬解码
你 发表评论:
欢迎- 03-19基于layui+springcloud的企业级微服务框架
- 03-19开箱即用的前端开发模板,扩展Layui原生UI样式,集成第三方组件
- 03-19SpringMVC +Spring +Mybatis + Layui通用后台管理系统OneManageV2.1
- 03-19SpringBoot+LayUI后台管理系统开发脚手架
- 03-19layui下拉菜单form.render局部刷新方法亲测有效
- 03-19Layui 遇到的坑(记录贴)(layui chm)
- 03-19基于ASP.NET MVC + Layui的通用后台开发框架
- 03-19LayUi自定义模块的定义与使用(layui自定义表格)
- 最近发表
-
- 基于layui+springcloud的企业级微服务框架
- 开箱即用的前端开发模板,扩展Layui原生UI样式,集成第三方组件
- SpringMVC +Spring +Mybatis + Layui通用后台管理系统OneManageV2.1
- SpringBoot+LayUI后台管理系统开发脚手架
- layui下拉菜单form.render局部刷新方法亲测有效
- Layui 遇到的坑(记录贴)(layui chm)
- 基于ASP.NET MVC + Layui的通用后台开发框架
- LayUi自定义模块的定义与使用(layui自定义表格)
- Layui 2.9.11正式发布(layui2.6)
- Layui 2.9.13正式发布(layui2.6)
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)