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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268