ffplay原始碼分析5-影象格式轉換
ffplay是FFmpeg工程自帶的簡單播放器,使用FFmpeg提供的解碼器和SDL庫進行視訊播放。本文基於FFmpeg工程4.1版本進行分析,其中ffplay原始碼清單如下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.c在嘗試分析原始碼前,可先閱讀如下參考文章作為鋪墊:
[2].視訊編解碼基礎概念
[3].色彩空間與畫素格式
[4].音訊引數解析
[5].FFmpeg基礎概念
“ffplay原始碼分析”系列文章如下:
[1].ffplay原始碼分析1-概述
[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),則需要進行影象格式轉換
1) 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 cthe scaling context previously created with *sws_getContext() * @param srcSlicethe 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 dstthe 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 * @returnthe 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);// 執行渲染,更新螢幕顯示
影象顯示的流程細節可參考如下文章: