Android --- IjkPlayer 阅读native层源码之将AvPacket解码为一帧视频(八)补充

本篇会有很多源代码,请注意阅读每行代码上面的注释。

本来是不准备写解码帧的,因为前面写了一篇关于解码一帧音频的博客。但是后面发现:虽然音视频(包括字幕)解码流程一样,但是它们解码后还会对帧有一些处理。而对于视频帧来说,这些处理还是很重要的。所以本篇主要目的介绍解码对视频帧的处理。

如果还不知道解码的流程请看: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);


    }
}

上面代码做了三个事情:

  1. 将AvPacket解码为AvFrame,和音频差不多,具体可以去看音频解码
  2. 如果在某段时间开启下载功能,会重新编码刚解码的帧为png,并保存到本地,且该帧不会用于播放。详细看上面的注释
  3. 如果没有开启下载,就会调用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;
}

在存储前,又做了四件事:

  1. 首先判断是否需要精准的同步校验,该段代码和音频类似,已被省略。
  2. 然后会判断解码的视频帧AvFrame与缓存容器Frame要求格式、大小等参数是否相同,如果不同:重新设置容器参数并调用alloc_picture构建一个新的播放参数。
  3. 调用SDL_VoutFillFrameYUVOverlay函数:判断视频帧是否需要转码???如果需要,转码(注意:播放格式与帧格式不同,也可能不会转码)
  4. 设置缓存的信息,并将其存入缓存队列中

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;
}

这段代码需要介绍的有点多:

  1. YUV (下面是自己参考其他博客的叙述,可能有问题)


    对于Android:YUV420sp格式一般用在手机摄像
     
  2. 一帧的原始数据存放在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];
     
  3. need_swap_uv
    如果播放格式为SDL_FCC_YV12,即YV12时,need_swap_uv=1,会可以将YU12的UV交换存储顺序,变为YV12
     
  4. use_linked_frame
    表示是否需要转码。
    如果use_linked_frame=1;表示不用转码,直接将视频帧数据拷贝到opaque->linked_frame 变量中。
    如果use_linked_frame=0;表示需要转码,创建一个转码后的存储地方: opaque->managed_frame转码后的数据存入该变量
    由于FFmpeg解码后的视频帧只会是YUV格式的,所以如果播放格式为RGB格式,就必须转码。
     
  5. opaque->linked_frame 存放不需要转码的一帧数据。可能用于播放
     
  6. opaque->managed_frame  存放转码后的一帧数据。可能用于播放
     
  7. overlay
    该变量的结构体为:SDL_VoutOverlay----记录播放参数,而SDL_VoutOverlay->pixels指针指向的就是最终的播放数据。
    上面代码中使用overlay_fill函数将pixels指针指向opaque->linked_frame或者opaque->managed_frame
     
  8. 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层源码之如何刷新视频的播放界面(七)

 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值