本篇会有很多源代码,请注意阅读每行代码上面的注释。
本来是不准备写解码帧的,因为前面写了一篇关于解码一帧音频的博客。但是后面发现:虽然音视频(包括字幕)解码流程一样,但是它们解码后还会对帧有一些处理。而对于视频帧来说,这些处理还是很重要的。所以本篇主要目的介绍解码对视频帧的处理。
如果还不知道解码的流程请看: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,失败,返回 -1ret = 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) {// 编码总数加1convert_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;// 如果下载完成,则通知Androidif (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_RV24if (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_RV32switch (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: //yuv444if (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_framefor (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层源码之如何刷新视频的播放界面(七)