ffmpeg开发播放器学习笔记 - 软解视频流,渲染 RGB24

​该节是ffmpeg开发播放器学习笔记的第二节《软解视频流,渲染 RGB24》

如今显示器大都是采用了RGB颜色标准,在显示器上,是通过电子枪打在屏幕的红、绿、蓝三色发光极上来产生色彩的,电脑一般都能显示32位颜色,有一千万种以上的颜色。电脑屏幕上的所有颜色,都由这红色绿色蓝色三种色光按照不同的比例混合而成的。一组红色绿色蓝色就是一个最小的显示单位。屏幕上的任何一个颜色都可以由一组RGB值来记录和表达。因此这红色绿色蓝色又称为三原色光,用英文表示就是R(red)、G(green)、B(blue)。

image

✅ 第一节 - Hello FFmpeg
第二节 - 软解视频流,渲染 RGB24
第三节 - 认识YUV
第四节 - 硬解码,OpenGL渲染YUV
第五节 - Metal 渲染YUV
第六节 - 解码音频,使用AudioQueue 播放
第七节 - 音视频同步
第八节 - 完善播放控制
第九节 - 倍速播放
第十节 - 增加视频过滤效果
第十一节 - 音频变声

该节 Demo 地址: https://github.com/czqasngit/ffmpeg-player/releases/tag/RGB-Image-Render

实例代码提供了Objective-CSwift两种实现,为了方便说明,文章引用的是Objective-C代码,因为Swift代码指针看着不简洁。
该节最终效果如下图:

image
image

目录


  • 了解ffmpeg视频软解码流程
  • 从ffmpeg中读取一数据帧并解码
  • 了解ffmpeg filter工作流程
  • 使用ffmpeg filter输出RGB24格式的视频帧
  • 渲染RGB24格式的视频帧

了解ffmpeg视频软解码流程


上一小节展示了ffmpeg软解码初始化流程图,接下来看一下从初始化到解码渲染视频的完整流程图:
image

流程图的大致逻辑是这样的:

  • 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)流出的数据就是我们最终想要的数据了。它的大致结构如下:
image

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关系很像。

Buffer定义在buffersrc.c中,它的初始化参数如下:
image

pixel_aspect: 一个像素的宽高比。在电脑上这个比例是1:1,像素是一个正方形。而在某些设备上这个像素单位可能不是正方形。简单的可以理解成,显示一个像素占用的屏幕宽与高的比例。 pix_fmt: 原始数据帖格式。

这里需要特别注意的是,为什么可以通过字符串以键值对的形式进行初始化呢?
这是因为ffmpeg里面实现了一套通过字符串查找对应属性的能力,这个实现是通过AVClass完成的。以AVFilterContext为例,它的定义如下:

image

在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"呢?

看图:
image

每一个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

更多内容请关注微信公众号<<程序猿搬砖>>

你可能感兴趣的:(ffmpeg开发播放器学习笔记 - 软解视频流,渲染 RGB24)