作者:叶余
来源: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语句的三个分支:
- 如果frame图像格式对应SDL_PIXELFORMAT_IYUV格式,不进行图像格式转换,使用
SDL_UpdateYUVTexture()
将图像数据更新到&is->vid_texture
- 如果frame图像格式对应其他被SDL支持的格式(诸如AV_PIX_FMT_RGB32),也不进行图像格式转换,使用
SDL_UpdateTexture()
将图像数据更新到&is->vid_texture
- 如果frame图像格式不被SDL支持(即对应SDL_PIXELFORMAT_UNKNOWN),则需要进行图像格式转换
- 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); } }
上述代码有三个步骤:
SDL_LockTexture()
锁定texture中的一个rect(此处是锁定整个texture),锁定区具有只写属性,用于更新图像数据。pixels
指向锁定区。sws_scale()
进行图像格式转换,转换后的数据写入pixels
指定的区域。pixels
包含4个指针,指向一组图像plane。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); // 执行渲染,更新屏幕显示
图像显示的流程细节可参考如下文章:
「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。