天天看點

ffplay源碼分析5-圖像格式轉換

本文為作者原創,轉載請注明出處:https://www.cnblogs.com/leisure_chn/p/10311376.html

ffplay是FFmpeg工程自帶的簡單點傳播放器,使用FFmpeg提供的解碼器和SDL庫進行視訊播放。本文基于FFmpeg工程4.1版本進行分析,其中ffplay源碼清單如下:

https://github.com/FFmpeg/FFmpeg/blob/n4.1/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支援,在這種情況下,需要進行圖像格式轉換,即将視訊幀圖像格式轉換為SDL支援的圖像格式,否則是無法正常顯示的。

圖像格式轉換是在視訊播放線程(主線程中)中的

upload_texture()

函數中實作的。調用鍊如下:

main() -- >
event_loop -->
refresh_loop_wait_event() -->
video_refresh() -->
video_display() -->
video_image_display() -->
upload_texture()
           

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支援的格式(諸如AV_PIX_FMT_RGB32),也不進行圖像格式轉換,使用

    SDL_UpdateTexture()

    &is->vid_texture

  3. 如果frame圖像格式不被SDL支援(即對應SDL_PIXELFORMAT_UNKNOWN),則需要進行圖像格式轉換
  4. 2)兩種類型不進行圖像格式轉換。我們考慮第3)種情況。

5.1 根據映射表擷取frame對應SDL中的像素格式

get_sdl_pix_fmt_and_blendmode()

這個函數的作用,擷取輸入參數

format

(FFmpeg像素格式)在SDL中的像素格式,取到的SDL像素格式存在輸出參數

sdl_pix_fmt

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) - 1; i++) {
        if (format == sdl_texture_format_map[i].format) {
            *sdl_pix_fmt = sdl_texture_format_map[i].texture_fmt;
            return;
        }
    }
}
           

在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 },
    { AV_PIX_FMT_NONE,           SDL_PIXELFORMAT_UNKNOWN },
};
           

可以看到,除了最後一項,其他格式的圖像送給SDL是可以直接顯示的,不必進行圖像轉換。

關于這些像素格式的含義,可參考“色彩空間與像素格式”

5.2 重新配置設定vid_texture

realloc_texture()

根據新得到的SDL像素格式,為

&is->vid_texture

重新配置設定空間,如下所示,先

SDL_DestroyTexture()

銷毀,再

SDL_CreateTexture()

建立

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;
}
           

5.3 複用或新配置設定一個SwsContext

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.4 圖像格式轉換

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(此處是鎖定整個texture),鎖定區具有隻寫屬性,用于更新圖像資料。

    pixels

    指向鎖定區。
  2. sws_scale()

    進行圖像格式轉換,轉換後的資料寫入

    pixels

    指定的區域。

    pixels

    包含4個指針,指向一組圖像plane。
  3. SDL_UnlockTexture()

    将鎖定的區域解鎖,将改變的資料更新到視訊緩沖區中。

    上述三步完成後,texture中已包含經過格式轉換後新的圖像資料。

    補充一下細節,

    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[]);
           

5.5 圖像顯示

texture對應一幀待顯示的圖像資料,得到texture後,執行如下步驟即可顯示:

SDL_RenderClear();                  // 使用特定顔色清空目前渲染目标
SDL_RenderCopy();                   // 使用部分圖像資料(texture)更新目前渲染目标
SDL_RenderPresent(sdl_renderer);    // 執行渲染,更新螢幕顯示
           

圖像顯示的流程細節可參考如下文章:

“FFmpeg簡易播放器的實作-視訊播放”

繼續閱讀