ffplay源码分析5-图像格式转换

本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10311376.html

ffplay 是 FFmpeg 工程自带的简单播放器,使用 FFmpeg 提供的解码器和 SDL 库进行视频播放。本文基于 FFmpeg 工程 8.0 版本进行分析,其中 ffplay 源码清单如下:
https://github.com/FFmpeg/FFmpeg/blob/n8.0/fftools/ffplay.c

在尝试分析源码前,可先阅读如下参考文章作为铺垫:
[1]. 雷霄骅,视音频编解码技术零基础学习方法
[2]. 视频编解码基础概念
[3]. 色彩空间与像素格式
[4]. 音频参数解析
[5]. FFmpeg基础概念

“ffplay源码分析”系列文章如下:
[1]. ffplay源码分析1-概述
[2]. ffplay源码分析2-数据结构
[3]. ffplay源码分析3-代码框架
[4]. ffplay源码分析4-音视频同步
[5]. ffplay源码分析5-图像格式转换
[6]. ffplay源码分析6-音频重采样
[7]. ffplay源码分析7-播放控制

5. 图像格式转换

图像格式转换实际是为视频播放做准备的。FFmpeg 负责解码,SDL 负责播放,FFmpeg 解码得到的视频帧的格式未必能被 SDL 支持,在这种情况下,需要进行图像格式转换,即将视频帧图像格式转换为 SDL 支持的图像格式,否则是无法正常显示的。

图像格式转换有两种实现方式,一种是通过视频滤镜 (filter) 实现,另一种是直接使用 sws_scale() 函数实现。实际上第一种方式中,视频滤镜的底层也是调用 sws_scale() 实现的图像格式转换。

旧版 ffplay 中滤镜是可选功能,代码通过 CONFIG_AVFILTER 宏决定是否使用滤镜。旧版 ffplay 中滤镜实现格式转换和 sws_scale() 实现格式转换两种代码都存在。新版 ffplay (也就是 c29e5ab5 这个提交之后),已删除了 CONFIG_AVFILTER 宏,滤镜功能成为必须功能,同时删除了 sws_scale() 实现格式转换的代码 。这个改动参考如下提交记录:

2023-03-10 c29e5ab5 fftools/ffplay: depend on avfilter

Making lavfi optional adds a lot of complexity for very questionable
gain.

本节将两种图像格式转换的方法都介绍一下。

5.1 FFmpeg 和 SDL 像素格式

像素格式表示一帧图像的像素点数据在内存中的排布格式。同一种像素格式在不同的系统中有不同的命名,这些系统如 FFmpeg 系统、SDL 系统,V4L2 系统等,都定义了各自系统内的像素格式。所谓“图像格式转换”中的“图像格式”,实际指的就是视频帧的像素格式。

关于像素格式的详细内容,可参考“色彩空间与像素格式

5.1.1 FFmpeg 和 SDL 像素格式映射表

在 ffplay.c 中定义了一个表 sdl_texture_format_map[],此表定义了 FFmpeg 像素格式与 SDL 像素格式的映射关系,如下:

static const struct TextureFormatEntry {
    enum AVPixelFormat format;
    int texture_fmt;
} sdl_texture_format_map[] = {
    { AV_PIX_FMT_RGB8,           SDL_PIXELFORMAT_RGB332 },
    { AV_PIX_FMT_RGB444,         SDL_PIXELFORMAT_RGB444 },
    { AV_PIX_FMT_RGB555,         SDL_PIXELFORMAT_RGB555 },
    { AV_PIX_FMT_BGR555,         SDL_PIXELFORMAT_BGR555 },
    { AV_PIX_FMT_RGB565,         SDL_PIXELFORMAT_RGB565 },
    { AV_PIX_FMT_BGR565,         SDL_PIXELFORMAT_BGR565 },
    { AV_PIX_FMT_RGB24,          SDL_PIXELFORMAT_RGB24 },
    { AV_PIX_FMT_BGR24,          SDL_PIXELFORMAT_BGR24 },
    { AV_PIX_FMT_0RGB32,         SDL_PIXELFORMAT_RGB888 },
    { AV_PIX_FMT_0BGR32,         SDL_PIXELFORMAT_BGR888 },
    { AV_PIX_FMT_NE(RGB0, 0BGR), SDL_PIXELFORMAT_RGBX8888 },
    { AV_PIX_FMT_NE(BGR0, 0RGB), SDL_PIXELFORMAT_BGRX8888 },
    { AV_PIX_FMT_RGB32,          SDL_PIXELFORMAT_ARGB8888 },
    { AV_PIX_FMT_RGB32_1,        SDL_PIXELFORMAT_RGBA8888 },
    { AV_PIX_FMT_BGR32,          SDL_PIXELFORMAT_ABGR8888 },
    { AV_PIX_FMT_BGR32_1,        SDL_PIXELFORMAT_BGRA8888 },
    { AV_PIX_FMT_YUV420P,        SDL_PIXELFORMAT_IYUV },
    { AV_PIX_FMT_YUYV422,        SDL_PIXELFORMAT_YUY2 },
    { AV_PIX_FMT_UYVY422,        SDL_PIXELFORMAT_UYVY },
};

此表中定义的格式是 SDL 能支持的格式,这些格式的图像帧送给 SDL 显示是不必进行格式转换的。其实 SDL 系统支持的格式,SDL renderer 未必能支持 (renderer 支持的格式与播放设备硬件相关),这种情况下 SDL 内部会做格式转换,以使 renderer 能正常渲染显示,细节不展开了。

5.1.2 get_sdl_pix_fmt_and_blendmode()

这个函数的作用是获取 FFmpeg 像素格式所对应的 SDL 像素格式,输入参数 format 指定 FFmpeg 像素格式,输出参数 sdl_pix_fmt 保存获取到的 SDL 像素格式:

static void get_sdl_pix_fmt_and_blendmode(int format, Uint32 *sdl_pix_fmt, SDL_BlendMode *sdl_blendmode)
{
    int i;
    *sdl_blendmode = SDL_BLENDMODE_NONE;
    *sdl_pix_fmt = SDL_PIXELFORMAT_UNKNOWN;
    if (format == AV_PIX_FMT_RGB32   ||
        format == AV_PIX_FMT_RGB32_1 ||
        format == AV_PIX_FMT_BGR32   ||
        format == AV_PIX_FMT_BGR32_1)
        *sdl_blendmode = SDL_BLENDMODE_BLEND;
    for (i = 0; i < FF_ARRAY_ELEMS(sdl_texture_format_map); i++) {
        if (format == sdl_texture_format_map[i].format) {
            *sdl_pix_fmt = sdl_texture_format_map[i].texture_fmt;
            return;
        }
    }
}

5.2 滤镜实现图像格式转换

使用滤镜进行图像格式转换是在视频解码线程中实现的。因为视频解码线程已经将视频帧做了格式转换,然后存入视频 frame 队列。这些帧的格式能被 SDL 支持,所以在视频播放线程中直接从队列取出视频帧送给 SDL 显示就可以了。

5.2.1 video_thread()

再次看一下 video_thread() 的实现:

static int video_thread(void *arg)
{
    ...
    AVFilterGraph *graph = NULL;
    AVFilterContext *filt_out = NULL, *filt_in = NULL;
    int last_w = 0;
    int last_h = 0;
    enum AVPixelFormat last_format = -2;
    int last_serial = -1;
    int last_vfilter_idx = 0;
    ...

    for (;;) {
        // 从packet队列中取出一个packet解码得到一个frame
        ret = get_video_frame(is, frame);
        ...

        // 配置视频滤镜
        if (   last_w != frame->width
            || last_h != frame->height
            || last_format != frame->format
            || last_serial != is->viddec.pkt_serial
            || last_vfilter_idx != is->vfilter_idx) {
            ...
            avfilter_graph_free(&graph);
            graph = avfilter_graph_alloc();
            ...
            graph->nb_threads = filter_nbthreads;
            // 如果滤镜未曾配置过则配置,vfilters_list存的是滤镜描述字符串,形如"transpose=cclock,pad=iw+20:ih"
            if ((ret = configure_video_filters(graph, is, vfilters_list ? vfilters_list[is->vfilter_idx] : NULL, frame)) < 0) {
                ...
                goto the_end;
            }
            filt_in  = is->in_video_filter;
            filt_out = is->out_video_filter;
            last_w = frame->width;
            last_h = frame->height;
            last_format = frame->format;
            last_serial = is->viddec.pkt_serial;
            last_vfilter_idx = is->vfilter_idx;
            frame_rate = av_buffersink_get_frame_rate(filt_out);
        }

        // 将frame送入滤镜输入端
        ret = av_buffersrc_add_frame(filt_in, frame);
        ...

        while (ret >= 0) {
            ...
            // 从滤镜输出端取frame
            ret = av_buffersink_get_frame_flags(filt_out, frame, 0);
            ...
            // 将当前帧压入frame_queue
            ret = queue_picture(is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);                
            ...
        }
        ...
    }
    ...
    return 0;
}

基本流程为:1) 解码得到一个视频帧,2) 将视频帧送给滤镜处理,做格式转换等,3) 将滤镜处理后的帧存入 frame 队列。

上述代码第一个 if 语句处,发现视频流格式有更新时,就会重新配置滤镜图,因为 last_format 等变量初始化为无效值,所以视频解码线程启动后,第一次执行 for 循环就会进入 if 分支执行 configure_video_filters() 配置好滤镜图。由这个配置好的滤镜图实现图像格式转换功能。

5.2.2 configure_video_filters()

configure_video_filters() 是用来建立滤镜图的,建立好的滤镜图实现一系列图像处理功能。

5.2.2.1 滤镜和滤镜图概念

滤镜 (filter) 处理的是原始帧数据,不同的视频滤镜具有不同的图像处理功能,如旋转、缩放等,多个滤镜可以串联起来,形成滤镜链 (filter_chain) 或滤镜图 (filter_graph),一个配置好的滤镜图如下:

滤镜图

滤镜图有两个特殊的端点:buffer 滤镜是滤镜图的输入端点,buffersink 滤镜是滤镜图的输出端点。

滤镜的操作涉及配置和使用两方面,先配置后使用。配置滤镜时,先配置好 buffer 滤镜和 buffersink 滤镜,再调用 configure_filtergraph() 把输入端点 buffer 滤镜、输出端点 buffersink 滤镜和中间各滤镜连接起来,代码可参考下一小节 。使用滤镜时,将待处理的视频帧被送入 buffer 滤镜 (调用 av_buffersrc_add_frame() 送入),经各滤镜处理后,从 buffersink 滤镜流出 (调用 av_buffersink_get_frame_flags() 取出 )。

滤镜的详细内容可参考“FFmpeg原始帧处理-滤镜API用法

5.2.2.2 configure_video_filters() 代码清单

configure_video_filters() 就是配置生成滤镜图 (filter_graph) 的,函数实现如下:

static int configure_video_filters(AVFilterGraph *graph, VideoState *is, const char *vfilters, AVFrame *frame)
{
    enum AVPixelFormat pix_fmts[FF_ARRAY_ELEMS(sdl_texture_format_map)];
    char sws_flags_str[512] = "";
    int ret;
    AVFilterContext *filt_src = NULL, *filt_out = NULL, *last_filter = NULL;
    AVCodecParameters *codecpar = is->video_st->codecpar;
    AVRational fr = av_guess_frame_rate(is->ic, is->video_st, NULL);
    const AVDictionaryEntry *e = NULL;
    int nb_pix_fmts = 0;
    int i, j;
    AVBufferSrcParameters *par = av_buffersrc_parameters_alloc();

    if (!par)
        return AVERROR(ENOMEM);

    // 获取目标像素格式列表(即当前renderer支持的目标像素格式列表):
    // FFmpeg中的像素格式与SDL中的像素格式具有对应关系,由映射表sdl_texture_format_map[]定义
    // renderer的texture_formats表示当前renderer支持的texture格式(SDL像素格式)
    // 此处查表得到renderer的texture_formats(SDL像素格式)对应FFmpeg中的像素格式
    for (i = 0; i < renderer_info.num_texture_formats; i++) {
        for (j = 0; j < FF_ARRAY_ELEMS(sdl_texture_format_map); j++) {
            if (renderer_info.texture_formats[i] == sdl_texture_format_map[j].texture_fmt) {
                pix_fmts[nb_pix_fmts++] = sdl_texture_format_map[j].format;
                break;
            }
        }
    }

    while ((e = av_dict_iterate(sws_dict, e))) {
        if (!strcmp(e->key, "sws_flags")) {
            av_strlcatf(sws_flags_str, sizeof(sws_flags_str), "%s=%s:", "flags", e->value);
        } else
            av_strlcatf(sws_flags_str, sizeof(sws_flags_str), "%s=%s:", e->key, e->value);
    }
    if (strlen(sws_flags_str))
        sws_flags_str[strlen(sws_flags_str)-1] = '\0';

    graph->scale_sws_opts = av_strdup(sws_flags_str);

    // 为buffer滤镜创建实例:filt_src(只分配"filter context",而未做初始化)
    filt_src = avfilter_graph_alloc_filter(graph, avfilter_get_by_name("buffer"),
                                           "ffplay_buffer");
    if (!filt_src) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    // 设置buffer滤镜:使用frame中的参数设置了像素格式、宽、高等关键信息
    par->format              = frame->format;
    par->time_base           = is->video_st->time_base;
    par->width               = frame->width;
    par->height              = frame->height;
    par->sample_aspect_ratio = codecpar->sample_aspect_ratio;
    par->color_space         = frame->colorspace;
    par->color_range         = frame->color_range;
    par->frame_rate          = fr;
    par->hw_frames_ctx = frame->hw_frames_ctx;
    ret = av_buffersrc_parameters_set(filt_src, par);
    if (ret < 0)
        goto fail;

    // 初始化buffer滤镜,此函数调用完毕后buffer滤镜初始化完毕
    // 应当在调用avfilter_init_dict()或avfilter_init_str()前设置好滤镜的参数
    // 另外一个函数avfilter_graph_create_filter()函数调用了avfilter_graph_alloc_filter()和
    // avfilter_init_str()两个函数,所以如果前面不使用avfilter_graph_alloc_filter()而使用
    // avfilter_graph_create_filter(),在avfilter_graph_create_filter()已经对滤镜完成了初
    // 始化之后又调用av_buffersrc_parameters_set()是错误的用法,这是旧版ffplay存在的问题,
    // 8.0版已修改此问题
    ret = avfilter_init_dict(filt_src, NULL);
    if (ret < 0)
        goto fail;

    // 为buffersink滤镜创建实例:filt_out(只分配"filter context",而未做初始化)
    filt_out = avfilter_graph_alloc_filter(graph, avfilter_get_by_name("buffersink"),
                                           "ffplay_buffersink");
    if (!filt_out) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    // 设置buffersink滤镜:设置pixel_formats和colorspaces参数
    if ((ret = av_opt_set_array(filt_out, "pixel_formats", AV_OPT_SEARCH_CHILDREN,
                                0, nb_pix_fmts, AV_OPT_TYPE_PIXEL_FMT, pix_fmts)) < 0)
        goto fail;
    if (!vk_renderer &&
        (ret = av_opt_set_array(filt_out, "colorspaces", AV_OPT_SEARCH_CHILDREN,
                                0, FF_ARRAY_ELEMS(sdl_supported_color_spaces),
                                AV_OPT_TYPE_INT, sdl_supported_color_spaces)) < 0)
        goto fail;

    // 初始化buffersink滤镜,此函数调用完毕后buffersink滤镜初始化完毕
    ret = avfilter_init_dict(filt_out, NULL);
    if (ret < 0)
        goto fail;

    last_filter = filt_out;

/* Note: this macro adds a filter before the lastly added filter, so the
 * processing order of the filters is in reverse */
// 将名为name值为arg的filter插入filtergraph中last_filter之后,并将新插入的filter与last_filter连接
#define INSERT_FILT(name, arg) do {                                          \
    AVFilterContext *filt_ctx;                                               \
                                                                             \
    ret = avfilter_graph_create_filter(&filt_ctx,                            \
                                       avfilter_get_by_name(name),           \
                                       "ffplay_" name, arg, NULL, graph);    \
    if (ret < 0)                                                             \
        goto fail;                                                           \
                                                                             \
    ret = avfilter_link(filt_ctx, 0, last_filter, 0);                        \
    if (ret < 0)                                                             \
        goto fail;                                                           \
                                                                             \
    last_filter = filt_ctx;                                                  \
} while (0)

    if (autorotate) {   // 自动旋转
        double theta = 0.0;
        int32_t *displaymatrix = NULL;
        AVFrameSideData *sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX);
        if (sd)
            displaymatrix = (int32_t *)sd->data;
        if (!displaymatrix) {
            const AVPacketSideData *psd = av_packet_side_data_get(is->video_st->codecpar->coded_side_data,
                                                                  is->video_st->codecpar->nb_coded_side_data,
                                                                  AV_PKT_DATA_DISPLAYMATRIX);
            if (psd)
                displaymatrix = (int32_t *)psd->data;
        }
        theta = get_rotation(displaymatrix);

        if (fabs(theta - 90) < 1.0) {
            INSERT_FILT("transpose", displaymatrix[3] > 0 ? "cclock_flip" : "clock");
        } else if (fabs(theta - 180) < 1.0) {
            if (displaymatrix[0] < 0)
                INSERT_FILT("hflip", NULL);
            if (displaymatrix[4] < 0)
                INSERT_FILT("vflip", NULL);
        } else if (fabs(theta - 270) < 1.0) {
            INSERT_FILT("transpose", displaymatrix[3] < 0 ? "clock_flip" : "cclock");
        } else if (fabs(theta) > 1.0) {
            char rotate_buf[64];
            snprintf(rotate_buf, sizeof(rotate_buf), "%f*PI/180", theta);
            INSERT_FILT("rotate", rotate_buf);
        } else {
            if (displaymatrix && displaymatrix[4] < 0)
                INSERT_FILT("vflip", NULL);
        }
    }

    // 配置并建立滤镜图,将所有滤镜(buffer滤镜,buffersink滤镜,vfilters参数中包含的其他滤镜)连接在一起
    if ((ret = configure_filtergraph(graph, vfilters, filt_src, last_filter)) < 0)
        goto fail;

    is->in_video_filter  = filt_src;
    is->out_video_filter = filt_out;

fail:
    av_freep(&par);
    return ret;
}

代码基本流程就是滤镜的配置流程:

  1. 创建和初始化 buffer 滤镜 (滤镜图输入端点):分配“filter context”,设置参数,初始化
  2. 创建和初始化 buffersink 滤镜 (滤镜图输出端点):分配“filter context”",设置参数,初始化
  3. 配置生成滤镜图,将所有滤镜 (buffer 滤镜,buffersink 滤镜,vfilters 参数中包含的其他滤镜) 连接在一起

5.2.2.3 SDL renderer 支持的格式

configure_video_filters() 中的第一个 for 循环是为了获取“格式转换”滤镜的目标格式列表 (将 SDL renderer 支持的 SDL 格式列表转换为 FFmpeg 格式列表),存在局部变量 pix_fmts[] 中。

这里涉及到 SDL 中的几个概念:window 指播放视频时弹出的窗口;texture 指一帧图像数据,类似 FFmpeg 中的 frame;renderer 是渲染器,将 texture 渲染至 window,在 window 中把图像实际显示出来。configure_video_filters() 中用到的 renderer_info 变量是 ffplay 定义的静态变量,存储当前 renderer 的信息,renderer_info.texture_formats[] 数组就是当前 renderer 支持的像素格式列表,它是 SDL 系统像素格式的子集,因为 renderer 和底层软硬件相关,比如不同类型的显示器所支持的图像格式不同,一个具体的显示器所支持的图像格式显然是 SDL 系统像素格式的子集。大致弄清楚了这些概念,for 循环处的语句就比较好理解了。我们来看一个 ffplay 中定义的几个 SDL 系统的变量:

static SDL_Window *window;
static SDL_Renderer *renderer;
static SDL_RendererInfo renderer_info = {0};
static SDL_AudioDeviceID audio_dev;

int main(int argc, char **argv)
{
    ...
    flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
    ...
    if (SDL_Init (flags)) {
        ...
        exit(1);
    }
    ...

    if (!display_disable) {
        ...
        window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);
        SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
        if (window) {
            renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
            ...
            if (renderer) {
                if (!SDL_GetRendererInfo(renderer, &renderer_info))
                    av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n", renderer_info.name);
            }
        }
        ...
    }

    is = stream_open(input_filename, file_iformat);
    ...
}

从上面可以看到,主线程中创建了 SDL window 和 SDL renderer 后,调用 SDL_GetRendererInfo() 将 renderer 的信息存储到了 renderer_info 变量中,这些信息包括 renderer_info.texture_formats[],可以在视频解码线程的 configure_video_filters() 中使用。

5.2.2.4 格式转换的源格式和目标格式

在格式转换过程中,源格式就是源 frame 中的像素格式,目标格式是 SDL renderer 支持的格式列表 (存储在局部变量 pix_fmts[] 中)。

再看 configure_video_filters() 代码,可以看到,在配置 buffer 滤镜时,将 frame->format 设置为滤镜图的输入格式,在配置 buffersink 滤镜时,通过 av_opt_set_array(filt_out, "pixel_formats", ..., pix_fmts)) 指定了局部变量 pix_fmts[] 作为滤镜图的输出格式,最后调用 configure_filtergraph() 将各滤镜连接起来形成滤镜图,在建立滤镜图时,FFmpeg 内部会自动按需插入格式转换滤镜,这个格式转换滤镜实现图像格式转换功能。

FFmpeg 经常会自动插入格式转换滤镜,因为插入的滤镜不是在命令行或代码中显式指定的,所以这些自动插入的滤镜是隐式滤镜,这些滤镜在默默地起作用,用户一般也不会关注到它们。如果跟踪一下内部实现,可以发现,实际是 vf_scale 视频滤镜实现的像素格式转换功能,vf_scale 滤镜的底层又是调用 sws_scale() 函数来做格式转换的,细节不展开了。

5.2.3 视频解码和播放过程概览

使用滤镜实现图像格式转换的方式中,格式转换在解码线程中进行,视频解码线程的过程为:解码,转换,写队列;视频播放线程的过程为:读队列,音视频同步控制,SDL 显示。

视频解码线程函数调用过程如下:

video_thread()
|-> get_video_frame()                // 解码
|-> av_buffersrc_add_frame()         // 格式转换输入
|-> av_buffersink_get_frame_flags()  // 格式转换输出
|-> queue_picture()                  // 写队列

视频播放线程函数调用过程如下:

main()
|-> event_loop
    |-> refresh_loop_wait_event()
        |-> video_refresh()           // 从队列读出一帧显示
            |-> compute_target_delay()    // 音视频同步时间校正
            |-> video_display()           // 视频帧显示
                |-> SDL_RenderClear()         // 清空SDL渲染目标
                |-> video_image_display()     // 更新SDL渲染目标
                    |-> upload_texture()         // 更新SDL图像数据
                        |-> SDL_UpdateYUVTexture()/SDL_UpdateTexture()
                    |-> SDL_RenderCopyEx()       // 使用SDL图像数据更新SDL渲染目标
                |-> SDL_RenderPresent()       // 执行渲染,更新屏幕显示

5.3 sws_scale() 实现图像格式转换

新版本 ffplay 中已经删除了 sws_scale() 实现图像格式转换的代码。本节内容不必过多关注,可快速了解一下。本节代码主要取自 FFmpeg 4.4 版本 ffplay.c 源文件。

旧版本的 ffplay,如果打开了 CONFIG_AVFILTER 宏,就打开了视频滤镜,用视频滤镜做格式转换和 sws_scale() 做格式转换两种代码都存在,如果没打开 CONFIG_AVFILTER 宏,就只存在 sws_scale() 做格式转换的代码。为简便,本节只考虑未打开 CONFIG_AVFILTER 宏的情况,这种情况下,图像格式转换是在视频播放线程 (主线程) 中的 upload_texture() 函数中实现的, upload_texture() 内部是调用 sws_scale() 来做像素格式转换。

5.3.1 upload_texture()

upload_texture() 源码如下:

static int upload_texture(SDL_Texture **tex, AVFrame *frame, struct SwsContext **img_convert_ctx) {
    int ret = 0;
    Uint32 sdl_pix_fmt;
    SDL_BlendMode sdl_blendmode;
    // 根据frame中的图像格式(FFmpeg像素格式),获取对应的SDL像素格式
    get_sdl_pix_fmt_and_blendmode(frame->format, &sdl_pix_fmt, &sdl_blendmode);
    // 参数tex实际是&is->vid_texture,此处根据得到的SDL像素格式,为&is->vid_texture
    if (realloc_texture(tex, sdl_pix_fmt == SDL_PIXELFORMAT_UNKNOWN ? SDL_PIXELFORMAT_ARGB8888 : sdl_pix_fmt, frame->width, frame->height, sdl_blendmode, 0) < 0)
        return -1;
    switch (sdl_pix_fmt) {
        // frame格式是SDL不支持的格式,则需要进行图像格式转换,转换为目标格式AV_PIX_FMT_BGRA,对应SDL_PIXELFORMAT_BGRA32
        case SDL_PIXELFORMAT_UNKNOWN:
            /* This should only happen if we are not using avfilter... */
            *img_convert_ctx = sws_getCachedContext(*img_convert_ctx,
                frame->width, frame->height, frame->format, frame->width, frame->height,
                AV_PIX_FMT_BGRA, sws_flags, NULL, NULL, NULL);
            if (*img_convert_ctx != NULL) {
                uint8_t *pixels[4];
                int pitch[4];
                if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pitch)) {
                    sws_scale(*img_convert_ctx, (const uint8_t * const *)frame->data, frame->linesize,
                              0, frame->height, pixels, pitch);
                    SDL_UnlockTexture(*tex);
                }
            } else {
                av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
                ret = -1;
            }
            break;
        // frame格式对应SDL_PIXELFORMAT_IYUV,不用进行图像格式转换,调用SDL_UpdateYUVTexture()更新SDL texture
        case SDL_PIXELFORMAT_IYUV:
            if (frame->linesize[0] > 0 && frame->linesize[1] > 0 && frame->linesize[2] > 0) {
                ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0], frame->linesize[0],
                                                       frame->data[1], frame->linesize[1],
                                                       frame->data[2], frame->linesize[2]);
            } else if (frame->linesize[0] < 0 && frame->linesize[1] < 0 && frame->linesize[2] < 0) {
                ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height                    - 1), -frame->linesize[0],
                                                       frame->data[1] + frame->linesize[1] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -frame->linesize[1],
                                                       frame->data[2] + frame->linesize[2] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -frame->linesize[2]);
            } else {
                av_log(NULL, AV_LOG_ERROR, "Mixed negative and positive linesizes are not supported.\n");
                return -1;
            }
            break;
        // frame格式对应其他SDL像素格式,不用进行图像格式转换,调用SDL_UpdateTexture()更新SDL texture
        default:
            if (frame->linesize[0] < 0) {
                ret = SDL_UpdateTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height - 1), -frame->linesize[0]);
            } else {
                ret = SDL_UpdateTexture(*tex, NULL, frame->data[0], frame->linesize[0]);
            }
            break;
    }
    return ret;
}

frame 中的像素格式是 FFmpeg 中定义的像素格式,FFmpeg 中定义的很多像素格式和 SDL 中定义的很多像素格式其实是同一种格式,只是名称不同而已。

根据 frame 中的像素格式与 SDL 支持的像素格式的匹配情况,upload_texture() 处理三种类型,对应 switch 语句的三个分支:

  1. 如果 frame 图像格式对应 SDL_PIXELFORMAT_IYUV 格式,不进行图像格式转换,使用 SDL_UpdateYUVTexture() 将图像数据更新到 is->vid_texture
  2. 如果 frame 图像格式对应其他被 SDL 支持的格式 (诸如 SDL_PIXELFORMAT_NV12, SDL_PIXELFORMAT_RGBA32 等),也不进行图像格式转换,使用 SDL_UpdateTexture() 将图像数据更新到 is->vid_texture
  3. 如果 frame 图像格式不被 SDL 支持 (即对应 SDL_PIXELFORMAT_UNKNOWN),则需要进行图像格式转换
    上述 1) 2) 两种类型不进行图像格式转换。我们考虑第 3) 种情况。

5.3.2 get_sdl_pix_fmt_and_blendmode()

get_sdl_pix_fmt_and_blendmode() 用于获得 SDL 中的像素格式,其实现参考 5.1.2 节。

5.3.3 realloc_texture()

realloc_texture() 用于分配 texture,函数实现如下:

static int realloc_texture(SDL_Texture **texture, Uint32 new_format, int new_width, int new_height, SDL_BlendMode blendmode, int init_texture)
{
    Uint32 format;
    int access, w, h;
    if (!*texture || SDL_QueryTexture(*texture, &format, &access, &w, &h) < 0 || new_width != w || new_height != h || new_format != format) {
        void *pixels;
        int pitch;
        if (*texture)
            SDL_DestroyTexture(*texture);
        if (!(*texture = SDL_CreateTexture(renderer, new_format, SDL_TEXTUREACCESS_STREAMING, new_width, new_height)))
            return -1;
        if (SDL_SetTextureBlendMode(*texture, blendmode) < 0)
            return -1;
        if (init_texture) {
            if (SDL_LockTexture(*texture, NULL, &pixels, &pitch) < 0)
                return -1;
            memset(pixels, 0, pitch * new_height);
            SDL_UnlockTexture(*texture);
        }
        av_log(NULL, AV_LOG_VERBOSE, "Created %dx%d texture with %s.\n", new_width, new_height, SDL_GetPixelFormatName(new_format));
    }
    return 0;
}

如果 is->vid_texture 未创建,或者图像长、宽或像素格式有改变 (与 is->vid_texture 的不同),则重新创建 is->vid_texture,先由 SDL_DestroyTexture() 销毁,再由 SDL_CreateTexture() 创建。

5.3.4 sws_getCachedContext()

sws_getCachedContext() 用于复用或新分配一个 SwsContext,函数原型为:

/**
 * Check if context can be reused, otherwise reallocate a new one.
 *
 * If context is NULL, just calls sws_getContext() to get a new
 * context. Otherwise, checks if the parameters are the ones already
 * saved in context. If that is the case, returns the current
 * context. Otherwise, frees context and gets a new context with
 * the new parameters.
 *
 * Be warned that srcFilter and dstFilter are not checked, they
 * are assumed to remain the same.
 */
SwsContext *sws_getCachedContext(SwsContext *context, int srcW, int srcH,
                                 enum AVPixelFormat srcFormat, int dstW, int dstH,
                                 enum AVPixelFormat dstFormat, int flags,
                                 SwsFilter *srcFilter, SwsFilter *dstFilter,
                                 const double *param);

sws_getCachedContext() 被调用的代码如下:

*img_convert_ctx = sws_getCachedContext(*img_convert_ctx,
    frame->width, frame->height, frame->format, frame->width, frame->height,
    AV_PIX_FMT_BGRA, sws_flags, NULL, NULL, NULL);

检查输入参数,第一个输入参数 *img_convert_ctx 对应形参 struct SwsContext *context。如果 context 是 NULL,调用 sws_getContext() 重新获取一个 context。如果 context 不是 NULL,检查其他项输入参数是否和 context 中存储的各参数一样,若不一样,则先释放 context 再按照新的输入参数重新分配一个 context。若一样,直接使用现有的 context。

5.3.5 sws_scale()

sws_scale() 实现图像格式转换,函数原型如下:

/**
 * Scale the image slice in srcSlice and put the resulting scaled
 * slice in the image in dst. A slice is a sequence of consecutive
 * rows in an image.
 *
 * Slices have to be provided in sequential order, either in
 * top-bottom or bottom-top order. If slices are provided in
 * non-sequential order the behavior of the function is undefined.
 *
 * @param c         the scaling context previously created with
 *                  sws_getContext()
 * @param srcSlice  the array containing the pointers to the planes of
 *                  the source slice
 * @param srcStride the array containing the strides for each plane of
 *                  the source image
 * @param srcSliceY the position in the source image of the slice to
 *                  process, that is the number (counted starting from
 *                  zero) in the image of the first row of the slice
 * @param srcSliceH the height of the source slice, that is the number
 *                  of rows in the slice
 * @param dst       the array containing the pointers to the planes of
 *                  the destination image
 * @param dstStride the array containing the strides for each plane of
 *                  the destination image
 * @return          the height of the output slice
 */
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
              const int srcStride[], int srcSliceY, int srcSliceH,
              uint8_t *const dst[], const int dstStride[]);

sws_scale() 被调用的代码如下:

if (*img_convert_ctx != NULL) {
    uint8_t *pixels[4];
    int pitch[4];
    if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pitch)) {
        sws_scale(*img_convert_ctx, (const uint8_t * const *)frame->data, frame->linesize,
                  0, frame->height, pixels, pitch);
        SDL_UnlockTexture(*tex);
    }
}

上述代码有三个步骤:

  1. SDL_LockTexture() 锁定 texture 中的一个 rect (第二个参数为 NULL 表示锁定整个 texture),锁定区具有只写属性,用于更新图像数据。pixels 参数包含 4 个指针,指向一组图像 plane,位于只写锁定区中,用来写入格式转换后的新数据。
  2. sws_scale() 进行图像格式转换,转换后的数据写入 pixels 指定的区域。
  3. SDL_UnlockTexture() 将锁定的区域解锁,锁定区被取消只写属性。
    上述三步完成后,texture 中已包含经过格式转换后新的图像数据,可用于显示。

5.3.6 视频解码和播放过程概览

使用 sws_scale() 实现图像格式转换的方式中,格式转换在播放线程中进行,视频解码线程的过程为:解码,写队列;视频播放线程的过程为:读队列,音视频同步控制,格式转换,SDL 显示。

视频解码线程函数调用过程如下:

video_thread()
|-> get_video_frame()                // 解码
|-> queue_picture()                  // 写队列

视频播放线程函数调用过程如下:

main()
|-> event_loop
    |-> refresh_loop_wait_event()
        |-> video_refresh()           // 从队列读出一帧显示
            |-> compute_target_delay()    // 音视频同步时间校正
            |-> video_display()           // 视频帧显示
                |-> SDL_RenderClear()         // 清空SDL渲染目标
                |-> video_image_display()     // 更新SDL渲染目标
                    |-> upload_texture()         // 更新SDL图像数据
                        |-> sws_scale()          // 图像格式转换
                    |-> SDL_RenderCopyEx()       // 使用SDL图像数据更新SDL渲染目标
                |-> SDL_RenderPresent()       // 执行渲染,更新屏幕显示
posted @ 2019-01-23 20:51  叶余  阅读(3789)  评论(1)    收藏  举报