本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-qswpmaez-ha.htmlhtml
ffplay是FFmpeg工程自帶的簡單播放器,使用FFmpeg提供的解碼器和SDL庫進行視頻播放。本文基於FFmpeg工程4.1版本進行分析,其中ffplay源碼清單以下:
https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.cgit
在嘗試分析源碼前,可先閱讀以下參考文章做爲鋪墊:
[1]. 雷霄驊,視音頻編解碼技術零基礎學習方法
[2]. 視頻編解碼基礎概念
[3]. 色彩空間與像素格式
[4]. 音頻參數解析
[5]. FFmpeg基礎概念github
「ffplay源碼分析」系列文章以下:
[1]. ffplay源碼分析1-概述
[2]. ffplay源碼分析2-數據結構
[3]. ffplay源碼分析3-代碼框架
[4]. ffplay源碼分析4-音視頻同步
[5]. ffplay源碼分析5-圖像格式轉換
[6]. ffplay源碼分析6-音頻重採樣
[7]. ffplay源碼分析7-播放控制數據結構
FFmpeg解碼獲得的視頻幀的格式未必能被SDL支持,在這種狀況下,須要進行圖像格式轉換,即將視頻幀圖像格式轉換爲SDL支持的圖像格式,不然是沒法正常顯示的。
圖像格式轉換是在視頻播放線程(主線程中)中的upload_texture()
函數中實現的。調用鏈以下:app
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)種狀況。ide
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像素格式的映射關係,以下:oop
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是能夠直接顯示的,沒必要進行圖像轉換。
關於這些像素格式的含義,可參考「色彩空間與像素格式」源碼分析
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; }
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。
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[]);
texture對應一幀待顯示的圖像數據,獲得texture後,執行以下步驟便可顯示:
SDL_RenderClear(); // 使用特定顏色清空當前渲染目標 SDL_RenderCopy(); // 使用部分圖像數據(texture)更新當前渲染目標 SDL_RenderPresent(sdl_renderer); // 執行渲染,更新屏幕顯示
圖像顯示的流程細節可參考以下文章:
「FFmpeg簡易播放器的實現-視頻播放」