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

1,193 阅读8分钟

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

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

读取到一帧原始视频流信息可能并不完整,它取决于不同编解码标准。比如使用很广泛的h264,非关键帧有可能需要参考前帧或者是前后帧。得益于ffmpeg优秀的设计与封装,使用ffmpeg解码视频帧还是很容易的。

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

该节 Demo 地址: github.com/czqasngit/f…
实例代码提供了Objective-CSwift两种实现,为了方便说明,文章引用的是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)进入,进过过滤变换,从出口(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实例的。
AVBuffer可以理解成定义,而AVFilterContext则是具体实现的实例,这和AVCodec与AVCodecContext关系无异。 初始化的变量定义在buffersrc.c中,如下: pixel_aspect: 一个像素的宽高比。在电脑上这个比例是1:1,像素是一个正方形。而在某些设备上这个像素单位不是正方形。简单的可以理解成,显示一个像素占用的屏幕宽与高的比例。
pix_fmt: 原始数据帖格式。
这里需要特别注意的是,为什么可以通过字符串以键值对的形式进行初始化呢?
这是因为ffmpeg里面实现了一套通过字符串查找对应属性的能力,这个实现是通过AVClass完成的。 AVFilterContext定义如下: 在ffmpeg里,所有支持通过键值查找或设置的结构体,它的第一个变量就是一个AVClass指针。
AVClass里保存了这个实例相关的AVOption指针,通过这个指针可以实现查找与设置功能。 所有对AVClass或者第一个变量是AVClass指针的对象进行操作函数定义在avutil/opt.h中。

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出口的数据格式是RGB24
enum 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名称的filter
ret = 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来渲染即可。代码如下:

- (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

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