本篇会有很多源代码,请注意阅读每行代码上面的注释。
本来是不准备写解码帧的,因为前面写了一篇关于解码一帧音频的博客。但是后面发现:虽然音视频(包括字幕)解码流程一样,但是它们解码后还会对帧有一些处理。而对于视频帧来说,这些处理还是很重要的。所以本篇主要目的介绍解码对视频帧的处理。
如果还不知道解码的流程请看:Android --- IjkPlayer 阅读native层源码之如何将AvPacket数据解码出一帧数据(六)
将视频的AvPacket数据解码为AvFrame的线程为ff_ffplay.c中的video_thread,下面将由这里开始:
video_thread:
最终调用ff_ffplay.ffp_video_thread:
ffplay_video_thread:
static int ffplay_video_thread(void *arg)
{
省略。。。。
// 死循环
for (;;) {
// 解码成功且没被丢弃,返回为1,解码成功但被丢弃,返回 0,失败,返回 -1
ret = get_video_frame(ffp, frame);
// 默认关闭,是否下载某段时间内的一些视频帧图片
if (ffp->get_frame_mode) {
// 用户可以设置将帧重新编码后,下载到本地,而count=剩于的下载的帧数
if (!ffp->get_img_info || ffp->get_img_info->count <= 0) {
av_frame_unref(frame);
continue;
}
// 设置上一次的下载时间
last_dst_pts = dst_pts;
if (dst_pts < 0) {
// 设置开始下载时间
dst_pts = ffp->get_img_info->start_time;
} else {
// 更新每次的下载时间
dst_pts += (ffp->get_img_info->end_time - ffp->get_img_info->start_time) / (ffp->get_img_info->num - 1);
}
// 计算该帧的显示时间
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
pts = pts * 1000;
// 如果该帧离下载时间最近,则去下载
if (pts >= dst_pts) {
while (retry_convert_image <= MAX_RETRY_CONVERT_IMAGE) {
// 重新编码一帧需要格式、大小的图片,并下载到本地
ret = convert_image(ffp, frame, (int64_t)pts, frame->width, frame->height);
if (!ret) {
// 编码总数加1
convert_frame_count++;
// 退出循环
break;
}
// 记录重新编码一帧失败的次数
retry_convert_image++;
av_log(NULL, AV_LOG_ERROR, "convert image error retry_convert_image = %d\n", retry_convert_image);
}
// 清零重新编码一帧失败的次数
retry_convert_image = 0;
// 如果下载完成,则通知Android
if (ret || ffp->get_img_info->count <= 0) {
if (ret) {
av_log(NULL, AV_LOG_ERROR, "convert image abort ret = %d\n", ret);
ffp_notify_msg3(ffp, FFP_MSG_GET_IMG_STATE, 0, ret);
} else {
av_log(NULL, AV_LOG_INFO, "convert image complete convert_frame_count = %d\n", convert_frame_count);
}
goto the_end;
}
} else {
dst_pts = last_dst_pts;
}
// 删除该帧,不会用于播放
av_frame_unref(frame);
continue;
}
// 基于容器和编解码器信息猜测的时间基
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
// 该帧的显示时间
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 将一帧图片存入缓存
ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);
}
}
上面代码做了三个事情:
- 将AvPacket解码为AvFrame,和音频差不多,具体可以去看音频解码
- 如果在某段时间开启下载功能,会重新编码刚解码的帧为png,并保存到本地,且该帧不会用于播放。详细看上面的注释
- 如果没有开启下载,就会调用queue_picture函数去处理该帧数据,最后存入缓存中
queue_picture:
static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
省略。。。 上面代码基本和音频一样
// 如果解码后帧的格式、大小与缓存容器Frame要求的存储参数不同,重新设置容器参数(大小、格式)和构建新的播放参数
/* alloc or resize hardware picture buffer */
if (!vp->bmp || !vp->allocated ||
vp->width != src_frame->width ||
vp->height != src_frame->height ||
vp->format != src_frame->format) {
// 通知Java层视频大小改变,
if (vp->width != src_frame->width || vp->height != src_frame->height)
ffp_notify_msg3(ffp, FFP_MSG_VIDEO_SIZE_CHANGED, src_frame->width, src_frame->height);
vp->allocated = 0;
// 重新设置缓存容器Frame要求大小和格式
vp->width = src_frame->width;
vp->height = src_frame->height;
vp->format = src_frame->format;
// 重新构建新的播放参数
alloc_picture(ffp, src_frame->format);
}
// 如果设置了播放参数
if (vp->bmp) {
// 将帧格式转换为播放格式,默认播放格式为SDL_FCC_RV24
if (SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame) < 0) {
// 设置失败,退出
exit(1);
}
// 设置缓存的参数
vp->pts = pts;
vp->duration = duration;
vp->pos = pos;
vp->serial = serial;
vp->sar = src_frame->sample_aspect_ratio;
vp->bmp->sar_num = vp->sar.num;
vp->bmp->sar_den = vp->sar.den;
// 存入缓存队列中
frame_queue_push(&is->pictq);
}
return 0;
}
在存储前,又做了四件事:
- 首先判断是否需要精准的同步校验,该段代码和音频类似,已被省略。
- 然后会判断解码的视频帧AvFrame与缓存容器Frame要求格式、大小等参数是否相同,如果不同:重新设置容器参数并调用alloc_picture构建一个新的播放参数。
- 调用SDL_VoutFillFrameYUVOverlay函数:判断视频帧是否需要转码???如果需要,转码(注意:播放格式与帧格式不同,也可能不会转码)
- 设置缓存的信息,并将其存入缓存队列中
1,2,4没啥好说的,有注释,下面说3:
SDL_VoutFillFrameYUVOverlay:
最终调用:ijksdl_vout_overlay_ffmpeg.c中的func_fill_frame:
static int func_fill_frame(SDL_VoutOverlay *overlay, const AVFrame *frame)
{
// 根据播放格式,判断是否需要转码。 默认播放格式=SDL_FCC_RV32
switch (overlay->format) {
case SDL_FCC_YV12: //yv12
// 由于YV12 在帧中的存储的数组顺序为Y、V、U,而播放需要的顺序为Y、U、V。所以需要V、U两个数组的存储顺序调换
need_swap_uv = 1;
// no break;
case SDL_FCC_I420:
if (frame->format == AV_PIX_FMT_YUV420P || frame->format == AV_PIX_FMT_YUVJ420P) {
use_linked_frame = 1;
dst_format = frame->format;
} else {
dst_format = AV_PIX_FMT_YUV420P;
}
break;
case SDL_FCC_I444P10LE: //yuv444
if (frame->format == AV_PIX_FMT_YUV444P10LE) {
use_linked_frame = 1;
dst_format = frame->format;
} else {
dst_format = AV_PIX_FMT_YUV444P10LE;
}
break;
case SDL_FCC_RV32:
dst_format = AV_PIX_FMT_0BGR32;
break;
case SDL_FCC_RV24:
dst_format = AV_PIX_FMT_RGB24;
break;
case SDL_FCC_RV16:
dst_format = AV_PIX_FMT_RGB565;
break;
default:
return -1;
}
// 如果不需要转换帧的格式,进入
if (use_linked_frame) {
// 将解码后的帧数据拷贝到linked_frame中。最后会用于播放
av_frame_ref(opaque->linked_frame, frame);
// 让播放指针指向linked_frame变量,即播放时会使用播放指针指向的数据渲染界面
overlay_fill(overlay, opaque->linked_frame, opaque->planes);
// 是否交换VU两个像素数组的顺序,即按YVU顺序存储.
if (need_swap_uv)
FFSWAP(Uint8*, overlay->pixels[1], overlay->pixels[2]);
} else {
// 需要转换帧的格式
// 创建一个缓存帧,该帧用于存放转码后的数据,最后会用于播放
AVFrame* managed_frame = opaque_obtain_managed_frame_buffer(opaque);
// 让播放指针指向managed_frame变量,即播放时会使用播放指针指向的数据渲染界面
overlay_fill(overlay, opaque->managed_frame, opaque->planes);
// 将播放指针的地址放入swscale_dst_pic中,而播放指针指向的地址为opaque->managed_frame,
// 所以向swscale_dst_pic中存放值,相当于存放在opaque->managed_frame
for (int i = 0; i < overlay->planes; ++i) {
swscale_dst_pic.data[i] = overlay->pixels[i];
swscale_dst_pic.linesize[i] = overlay->pitches[i];
}
// 是否交换VU两个像素数组的顺序,即按YVU顺序存储.
if (need_swap_uv)
FFSWAP(Uint8*, swscale_dst_pic.data[1], swscale_dst_pic.data[2]);
}
// 如果不需要转换帧的格式,进入
if (use_linked_frame) {
// do nothing
}
// 注意:C语言中 if(0)=false; if(非0)=true。
// 需要转换帧的格式,如果ijk_image_convert转码成功返回0,不进入;否则返回-1,进入转码;
else if (ijk_image_convert(frame->width, frame->height,
dst_format, swscale_dst_pic.data, swscale_dst_pic.linesize,
frame->format, (const uint8_t**) frame->data, frame->linesize)) {
// 如果ijk_image_convert转码失败或者没转码数据,那么就会执行下面:使用sws_scale转码
// 区别:sws_getContext可以用于多路码流转换,为每个不同的码流都指定一个不同的转换上下文
// 获取一个转码的上下文,只能用于一路码流转换。
opaque->img_convert_ctx = sws_getCachedContext(opaque->img_convert_ctx,
frame->width, frame->height, frame->format, frame->width, frame->height,
dst_format, opaque->sws_flags, NULL, NULL, NULL);
// 开始转换格式
sws_scale(opaque->img_convert_ctx, (const uint8_t**) frame->data, frame->linesize,
0, frame->height, swscale_dst_pic.data, swscale_dst_pic.linesize);
}
return 0;
}
这段代码需要介绍的有点多:
- YUV (下面是自己参考其他博客的叙述,可能有问题)
对于Android:YUV420sp格式一般用在手机摄像
- 一帧的原始数据存放在AvFrame的data二维数组中:
RGB格式:存储在AvFrame->data[0];
YU12格式:Y存储在AvFrame->data[0];U存储在AvFrame->data[1];V存储在AvFrame->data[2];
YV12格式:Y存储在AvFrame->data[0];V存储在AvFrame->data[1];U存储在AvFrame->data[2];
YV444格式(SDL_FCC_I444P10LE):Y存储在AvFrame->data[0];U存储在AvFrame->data[1];V存储在AvFrame->data[2];
- need_swap_uv
如果播放格式为SDL_FCC_YV12,即YV12时,need_swap_uv=1,会可以将YU12的UV交换存储顺序,变为YV12
- use_linked_frame
表示是否需要转码。
如果use_linked_frame=1;表示不用转码,直接将视频帧数据拷贝到opaque->linked_frame 变量中。
如果use_linked_frame=0;表示需要转码,创建一个转码后的存储地方: opaque->managed_frame,转码后的数据存入该变量
由于FFmpeg解码后的视频帧只会是YUV格式的,所以如果播放格式为RGB格式,就必须转码。
- opaque->linked_frame 存放不需要转码的一帧数据。可能用于播放
- opaque->managed_frame 存放转码后的一帧数据。可能用于播放
- overlay
该变量的结构体为:SDL_VoutOverlay----记录播放参数,而SDL_VoutOverlay->pixels指针指向的就是最终的播放数据。
上面代码中使用overlay_fill函数将pixels指针指向opaque->linked_frame或者opaque->managed_frame
- ijk_image_convert与sws_scale
这两个函数都是负责转码的,ijk_image_convert的效率会比sws_scale高,先使用ijk_image_convert去转码,如果转码成功返回0,就不会用sws_scale。否则返回-1,调用sws_scale去转码。
下面细说什么情况调用ijk_image_convert或者sws_scale:
ijk_image_convert:
其中I420ToRGB565的实现在这里;I420ToABGR的实现在这里 都是转码成功返回0,否则-1.
ijk_image_convert函数最终调用LibYuv库中相应的转码函数,该库的转码函数效率会比sws_scale高。
总结下:
大前提:由于FFmpeg解码后的视频帧只会是YUV格式
- 播放格式为RGB565或者BGR32,且帧格式为YUV420P或者YUVJ420P,才使用ijk_image_convert函数转码;
- 其它情况需要转码,使用sws_scale转码。
最后在说一句:转码都比较消耗性能,最好还是人为的将播放格式设置的与帧格式相同,从而不需要转码。如果不知道如何设置,请看:Android --- IjkPlayer 阅读native层源码之如何刷新视频的播放界面(七)