该节是ffmpeg开发播放器学习笔记
的第二节《软解视频流,渲染 RGB24》
如今显示器大都是采用了RGB颜色标准,在显示器上,是通过电子枪打在屏幕的红、绿、蓝三色发光极上来产生色彩的,电脑一般都能显示32位颜色,有一千万种以上的颜色。电脑屏幕上的所有颜色,都由这红色绿色蓝色三种色光按照不同的比例混合而成的。一组红色绿色蓝色就是一个最小的显示单位。屏幕上的任何一个颜色都可以由一组RGB值来记录和表达。因此这红色绿色蓝色又称为三原色光,用英文表示就是R(red)、G(green)、B(blue)。
✅ 第一节 - Hello FFmpeg
第二节 - 软解视频流,渲染 RGB24
第三节 - 认识YUV
第四节 - 硬解码,OpenGL渲染YUV
第五节 - Metal 渲染YUV
第六节 - 解码音频,使用AudioQueue 播放
第七节 - 音视频同步
第八节 - 完善播放控制
第九节 - 倍速播放
第十节 - 增加视频过滤效果
第十一节 - 音频变声
该节 Demo 地址: https://github.com/czqasngit/ffmpeg-player/releases/tag/RGB-Image-Render
实例代码提供了Objective-C
与Swift
两种实现,为了方便说明,文章引用的是Objective-C
代码,因为Swift
代码指针看着不简洁。
该节最终效果如下图:
目录
- 了解ffmpeg视频软解码流程
- 从ffmpeg中读取一数据帧并解码
- 了解ffmpeg filter工作流程
- 使用ffmpeg filter输出RGB24格式的视频帧
- 渲染RGB24格式的视频帧
了解ffmpeg视频软解码流程
上一小节展示了ffmpeg软解码初始化流程图,接下来看一下从初始化到解码渲染视频的完整流程图:
流程图的大致逻辑是这样的:
- 1.初始化ffmpeg
- 2.从ffmpeg中读取一帧数据
- 3.读取到视频数据,放到视频解码器上下文中进行解码,得到一帧原始格式的视频数据
- 4.数据放进ffmpeg filter,输出目标格式的数据
- 5.渲染目标格式数据帧
从ffmpeg中读取一数据帧并解码
目前只渲染视频,所有读取视频帧的触发器使用定时器(Timer),并将定时器的触发时间间隔设置成1.0/fps。
1.读取视频帧
AVPacket packet = av_packet_alloc();av_read_frame(formatContext, packet);
packet是重复使用的,在使用前需要清理上一帧数据的内容
av_packet_unref(packet);
读取完成之后,判断是视频帧还是音频帧
if(self->packet->stream_index == videoStreamIndex){ }
videoStreamIndex是在初始化AVCodecContext时从AVStream中读取的
2.解码视频帧
int ret = avcodec_send_packet(codecContext, packet);
调用函数将未解码的帧发送给视频解码器进行解码
AVFrame *frame = av_frame_alloc();/// 清理AVFrame中上一帧的数据av_frame_unref(frame);if(ret != 0) return NULL;ret = avcodec_receive_frame(codecContext, frame);
获取解码后的视频数据帧,数据保存在frame中
到此解码视频帧完成了,但此时得到的是原始的视频格式如: YUV420P。本节需要渲染的是RGB24,所以需要对视频帧进行转码。
了解ffmpeg filter工作流程
ffmpeg filter可以理解成过滤器、滤波器,将不同的数据变换定义成一个个的filter节点,让数据像流水一样流过这些由filter连接的管道,数据从入口(buffer)进入,经过filter变换,从出口(bufferSink)流出的数据就是我们最终想要的数据了。它的大致结构如下:
Buffer
: ffmpeg中提供的filter,它负责接收数据,作为整个filter graph的输入端,它只包含一个输出端。它有几个初始化参数,其它有一个是pix_fmt,指定了输入视频的格式。 BufferSink
: ffmpeg中提供的filter,它负责输出最终数据,作为整个filter graph的输出端,它只包含一个输入端。它有一个初始化参数pix_fmts指定了输出时的视频帧格式。 Filters
: 开发者可以自定义自由组件的部分,每个filter都有一个输入与输出,用于连接上下的Filter。开发者可以开发自定义filter实现想要的效果,ffmpeg也提供了一些现成 的filter可以使用。每个中间使用的Filter都包含了输入端与输出端用于承接上一个Filter的视频帧数据并输出处理后的视频帧数据 AVFilterGraph
: 整个过滤器的管理者。
使用ffmpeg filter输出RGB24格式的视频帧
1.创建AVFilterGrapha
AVFilterGraph *graph = avfilter_graph_alloc();
2.创建Buffer Filter
/// 获取时间基AVRational time_base = stream->time_base;/// 获取到buffer filter的定义const AVFilter *buffer = avfilter_get_by_name("buffer");char args[512];/// 在创建buffer filter的时候传入一个字符串作为初始化时的参数/// 这里需要注意的是对应的变量的参数不能是AV_OPT_TYPE_BINARY这种类型/// AV_OPT_TYPE_BINARY需要单独设置,它的数据是指向内存的地址,所以不能通过字符串初始化snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", codecContext->width, codecContext->height, codecContext->pix_fmt, time_base.num, time_base.den, codecContext->sample_aspect_ratio.num, codecContext->sample_aspect_ratio.den);AVFilterContext *bufferContext = NULL;/// 创建buffer filter的实例,实例指的就是AVFilterContext的指针,存在了这个filter的所有信息int ret = avfilter_graph_create_filter(&bufferContext, buffer, "in", args, NULL, graph);
avfilter_graph_create_filter中的第三个参数是给这个实例取了一个别名。中间部分的filter在后期连接的时候是通过字符来指定filter实例的。
AVFilter可以理解成定义,而AVFilterContext可以理解成运行时的AVFilter,这和AVCodec与AVCodecContext关系很像。
pixel_aspect
: 一个像素的宽高比。在电脑上这个比例是1:1,像素是一个正方形。而在某些设备上这个像素单位可能不是正方形。简单的可以理解成,显示一个像素占用的屏幕宽与高的比例。 pix_fmt
: 原始数据帖格式。
这里需要特别注意的是,为什么可以通过字符串以键值对的形式进行初始化呢?
这是因为ffmpeg里面实现了一套通过字符串查找对应属性的能力,这个实现是通过AVClass完成的。以AVFilterContext为例,它的定义如下:
在ffmpeg里,所有支持通过键值查找或设置的结构体,它的第一个变量就是一个AVClass指针。
AVClass里保存了这个实例相关的AVOption指针,通过这个指针可以实现查找与设置功能。所有对AVClass或者第一个变量是AVClass指针的对象进行操作函数定义在avutil/opt.h
中。
以常见的av_opt_find2函数为例:
const AVOption *av_opt_find2(void *obj, const char *name, const char *unit, int opt_flags, int search_flags, void **target_obj){ const AVClass *c; const AVOption *o = NULL; if(!obj) return NULL; c= *(AVClass**)obj; if (!c) return NULL; "省略了具体查找的代码" return NULL;}
函数的第一个参数是第一个变量为AVClass指针的结构体实例,(AVClass **)obj获取到的是指向obj首地址的指针,obj的第一个变量就是AVClass *,所以也是指向AVClass *的指针,取地址c = (AVClass **)obj; c 就是AVClass *了。如果不是太好理解,画个图自己看一下就明白了。
3.创建BufferSink
int ret = avfilter_graph_create_filter(&bufferSinkContext, bufferSink, "out", NULL, NULL, graph);av_print_obj_all_options(bufferSinkContext);/** pix_fmts在buffersink.c中定义了一个AVFilter名称为buffersink,添加了一个AVOption为pix_fmts static const AVOption buffersink_options[] = { { "pix_fmts", "set the supported pixel formats", OFFSET(pixel_fmts), AV_OPT_TYPE_BINARY, .flags = FLAGS }, { NULL }, }; *//// 这里的pix_fmts不能通过字符串的形式初始化,因为他的类型是一个AV_OPT_TYPE_BINARY/// pix_fmts定义如下: enum AVPixelFormat *pixel_fmts; 它是一个指针/// 设置buffersink出口的数据格式是RGB24enum AVPixelFormat format[] = {AV_PIX_FMT_RGB24}; //想要转换的格式ret = av_opt_set_bin(bufferSinkContext, "pix_fmts", (uint8_t *)&format, sizeof(self->fmt), AV_OPT_SEARCH_CHILDREN);
创建BufferSink的过程与创建Buffer是一样的,只是这里需要注意的是定义在liavfilter/buffersink.c
中的属性只有一个pix_fmts
(目标格式),它的类型是binary,所以不能通过字符串的形式将参数传到初始化方法中,需要通过额外的方法av_opt_set_bin
来设置,这也是前面提到的定义在opt.h
中一系列方法的其中一个。
4.初始化AVFilterInOut
AVFilterInOut *inputs = avfilter_inout_alloc();AVFilterInOut *outputs = avfilter_inout_alloc();inputs->name = av_strdup("out");inputs->filter_ctx = bufferSinkContext;inputs->pad_idx = 0;inputs->next = NULL;outputs->name = av_strdup("in");outputs->filter_ctx = bufferContext;outputs->pad_idx = 0;outputs->next = NULL;
一开始这个地方可能不太好理解,为什么outputs->name是"in"呢?
看图:每一个AVFilterGraph都有一个inputs与一个outputs,而这个outputs在设置的时候设置成了"in",filter_ctx是bufferContext。即可以理解成这个outputs是buffer的outputs,inputs是bufferSink的inputs。因为buffer只有输出,BufferSink只有输入。
5.解析filters并设置AVFilterGraph的inputs与outputs
/// filters: 参数传入一个null名称的filterret = avfilter_graph_parse_ptr(graph, "null", &inputs, &outputs, NULL);
使用字符串解析来添加filter到graph中,这里没有额外的filter在中间连接,所以传入"null",整个graph中有两个filter,buffer(解码数据的输入filter),buffersink(获取解码数据的filter)。"null"是一个特殊的filter,它表示没有其它filter了。
它的定义如下:
AVFilter ff_vf_null = { .name = "null", .description = NULL_IF_CONFIG_SMALL("Pass the source unchanged to the output."), .inputs = avfilter_vf_null_inputs, .outputs = avfilter_vf_null_outputs,};
如果使用了其它的filter,它的描述是像这样:
const char *filter_descr = "scale=78:24,transpose=cclock";
6.检查并链接
int ret = avfilter_graph_config(graph, NULL);
7.输出RGB24格式的视频帧
int ret = av_buffersrc_add_frame(bufferContext, frame);if(ret < 0) { NSLog(@"add frame to buffersrc failed."); return;}ret = av_buffersink_get_frame(bufferSinkContext, outputFrame);
av_buffersrc_add_frame将原始数据帧(待转换数据帧)添加到bufferContext,然后通过av_buffersink_get_frame从bufferSinkContext中获取转换之后的数据帧。
渲染RGB24格式的视频帧
AVFrame的格式是RGB24,它只有一个平面数据存放在data[0]中,linesize[0]存放了一行所需要的字节数。由于不同CPU平台可能有不同的对齐方式,所以这个数据与width的值可能不相等,最后使用熟悉CoreGraphics渲染RGB24即可。
代码如下:
- (void)displayWithAVFrame:(AVFrame *)rgbFrame { int linesize = rgbFrame->linesize[0]; int videoHeight = rgbFrame->height; int videoWidth = rgbFrame->width; int len = (linesize * videoHeight); UInt8 *bytes = (UInt8 *)malloc(len); memcpy(bytes, rgbFrame->data[0], len); dispatch_async(display_rgb_queue, ^{ CFDataRef data = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, bytes, len, kCFAllocatorNull); if(!data) { NSLog(@"create CFDataRef failed."); free(bytes); return; } if(CFDataGetLength(data) == 0) { CFRelease(data); free(bytes); return; } CGDataProviderRef provider = CGDataProviderCreateWithCFData(data); CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); CGImageRef imageRef = CGImageCreate(videoWidth, videoHeight, 8, 3 * 8, linesize, colorSpaceRef, bitmapInfo, provider, NULL, YES, kCGRenderingIntentDefault); NSSize size = NSSizeFromCGSize(CGSizeMake(videoWidth, videoHeight)); NSImage *image = [[NSImage alloc] initWithCGImage:imageRef size:size]; CGImageRelease(imageRef); CGColorSpaceRelease(colorSpaceRef); CGDataProviderRelease(provider); CFRelease(data); free(bytes); dispatch_async(dispatch_get_main_queue(), ^{ @autoreleasepool { self.imageView.image = image; } }); });}
到此,完整的解码视频帧,输出RGB24格式并渲染的大致流程就完成了。
值得注意的是,使用CoreGraphics渲染的效率并不高,CPU使用率达到了35%。
总结:
- 了解ffmpeg解码大致流程,它的过程不复杂
- 读取一帧原始数据,并判断是音频还是视频,交给不同的解码器进行解码
- 了解filter的使用流程,并使用filter完成目标格式的输出
- 利用CoreGraphics渲染RGB24
更多内容请关注微信公众号<<程序猿搬砖>>