ffplay 是 FFMPEG 工程自带的简单播放器, 使用 FFMPEG 提供的解码器和 SDL 库进行视频播放. 本文基于 FFMPEG 工程 4.1 版本进行分析, 其中 ffplay 源码清单如下:
在尝试分析源码前, 可先阅读如下参考文章作为铺垫:
[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), 则需要进行图像格式转换
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 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 简易播放器的实现 - 视频播放"
来源: https://www.cnblogs.com/leisure_chn/p/10311376.html