ijkplayer 源码解析4(视频解码+渲染)

ijkplayer源码解析系列文章

本章主要解析视频解码 + 视频帧刷新逻辑 + 视频帧绘制 这3个部分;
第一部分是视频的解封装 AVPacket -> AVFrame 线程函数ffplay_video_thread
第二部分是视频的刷新逻辑 video_refresh_thread
第三个部分是视频的渲染层IJKSDLGLView

视频播放流程图可以参考如下


视频解码播放流程图.jpg

前情提要,ijkplayer的解码线程独立于数据读取线程,每个类型的流(AVStream)都有其对应的解码线程,如下表所示;

类型 PacketQueue FrameQueue clock Decoder 解码线程
音频 audioq sampq audclk auddec audio_thread
视频 videoq pictq vidclk viddec video_thread
字幕 subtitleq sampq ---- subdec subtitle_thread

1.视频解码线程

read_thread 后,获取视频流的stramIndex ,使用 stream_component_open 函数开启相应的流和相应的解码线程;音频流/视频流/字幕流)也是同样的逻辑;

视频解码函数调用堆栈顺序如下:

video_thread
   ffpipenode_run_sync
      func_run_sync
         ffp_video_thread
            ffplay_video_thread

video_thread 最终调用到 ffplay_video_thread 函数主要完成了AVPacket -> AVFrame的功能,该函数主要功能如下

  • 1.1 获取解码后的视频并送入视频FrameQueue
1.1 获取解码后的视频并送入 FrameQueue
 static int ffplay_video_thread(void *arg)
{
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    /// 初始化frame
    AVFrame *frame = av_frame_alloc();
    double pts;
    double duration;
    int ret;
    AVRational tb = is->video_st->time_base;
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
    //省略部分代码...

#if CONFIG_AVFILTER
    ///滤镜代码、暂不分析
#else
    ffp_notify_msg2(ffp, FFP_MSG_VIDEO_ROTATION_CHANGED, ffp_get_video_rotate_degrees(ffp));
#endif

    if (!frame) {
#if CONFIG_AVFILTER
    ///滤镜代码、暂不分析
#endif
        return AVERROR(ENOMEM);
    }

    /// 获取视频Frame循环
    for (;;) {
        /// 获取解码后的视频帧
        ret = get_video_frame(ffp, frame);
        if (ret < 0)
            goto the_end;
        if (!ret)
            continue;

        if (ffp->get_frame_mode) {
           /// 默认配置0 ,暂不分析该代码
           ///省略代码。。。 
        }

#if CONFIG_AVFILTER
        ///滤镜代码、暂不分析
#endif
            /// 获取视频帧播放时间
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            /// 视频Frame 展示展示时间pts
            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);
#if CONFIG_AVFILTER
        }
#endif

        if (ret < 0)
            goto the_end;
    }
 the_end:
#if CONFIG_AVFILTER
    ///滤镜代码、暂不分析
#endif
    av_log(NULL, AV_LOG_INFO, "convert image convert_frame_count = %d\n", convert_frame_count);
    av_frame_free(&frame);
    return 0;
}

2.视频渲染线程

视频渲染线程调用堆栈如下:

video_refresh_thread
  video_refresh
    video_display2
      video_image_display2
        SDL_VoutDisplayYUVOverlay
            vout_display_overlay
              vout_display_overlay_l
static int video_refresh_thread(void *arg)
{
    /// 播放器ffp
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    double remaining_time = 0.0;
    /// 再非中断的情况下
    while (!is->abort_request) {
        if (remaining_time > 0.0)
            av_usleep((int)(int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            /// 刷新视频 并获取等待时间
            video_refresh(ffp, &remaining_time);
    }
    return 0;
}
video_refresh 函数分析

在分析 video_refresh()函数的时候,先来解释一下这个函数中用到的一些变量和方法 以方便大家理解;
Frame *vp, *lastvp 分别表示为 当前帧 和 上一帧;
is->frame_timer 表示当前窗口正在显示的帧的播放时刻 (单位s)
last_duration 表示上一帧播放时间
delay 表示当前帧需要播放的时间
av_gettime_relative() 表示获取系统时间函数
frame_queue_nb_remaining() 表示获取当前队列frame 数量
frame_queue_next() 表示获取队列头部的下一帧
ffp->display_disable 表示是否禁用显示功能
is->force_refresh 表示是否强制刷新 (当下一帧可播放,或者视频size发生变化的时候为1 )
is->show_mode 默认为SHOW_MODE_VIDEO表示显示视频画面,

static void video_refresh(FFPlayer *opaque, double *remaining_time)
{
    FFPlayer *ffp = opaque;
    VideoState *is = ffp->is;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
        check_external_clock_speed(is);

    if (!ffp->display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + ffp->rdftspeed < time) {
            video_display2(ffp);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + ffp->rdftspeed - time);
    }
    /// 判断视频流存在
    if (is->video_st) {
retry:
        if (frame_queue_nb_remaining(&is->pictq) == 0) {
            /// 队列中没有视频帧 什么也不做
        } else {
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* dequeue the picture */
            /// 获取上一帧
            lastvp = frame_queue_peek_last(&is->pictq);
            /// 获取准备播放的当前帧
            vp = frame_queue_peek(&is->pictq);

            if (vp->serial != is->videoq.serial) {
                frame_queue_next(&is->pictq);
                goto retry;
            }

            if (lastvp->serial != vp->serial)
                /// 当seek或者快进、快退的情况重新赋值 frame_time 时间
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                /// 暂停的情况重复显示上一帧
                goto display;

            /* compute nominal last_duration */
            /// last_duration 表示上一帧播放时间
            last_duration = vp_duration(is, lastvp, vp);
            /// delay 表示当前帧需要播放的时间
            delay = compute_target_delay(ffp, last_duration, is);

            time= av_gettime_relative()/1000000.0;
            if (isnan(is->frame_timer) || time < is->frame_timer)
                /// is->frame_timer 不准的情况下更新
                is->frame_timer = time;
            if (time < is->frame_timer + delay) {
                /// 即将播放的帧+播放时长 大于 当前时间,则可以播放,跳转到display播放
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }
            /// 更新 is->frame_timer时间
            is->frame_timer += delay;
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                is->frame_timer = time;

            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                /// 更新视频时钟
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                /// 队列视频帧>1 的情况 当前帧展示时间 大于当前时间,则丢掉该帧
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);
                if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {
                    frame_queue_next(&is->pictq);
                    goto retry;
                }
            }

            if (is->subtitle_st) {
                /// 视频字幕流的情况 暂不分析
                while (frame_queue_nb_remaining(&is->subpq) > 0) {
                    sp = frame_queue_peek(&is->subpq);

                    if (frame_queue_nb_remaining(&is->subpq) > 1)
                        sp2 = frame_queue_peek_next(&is->subpq);
                    else
                        sp2 = NULL;

                    if (sp->serial != is->subtitleq.serial
                            || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                            || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                    {
                        if (sp->uploaded) {
                            ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, "", 1);
                        }
                        frame_queue_next(&is->subpq);
                    } else {
                        break;
                    }
                }
            }

            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            SDL_LockMutex(ffp->is->play_mutex);
            if (is->step) {
                is->step = 0;
                if (!is->paused)
                    stream_update_pause_l(ffp);
            }
            SDL_UnlockMutex(ffp->is->play_mutex);
        }
display:
        /* display picture */
        /// 渲染开启、force_refresh ==1 、show_mode 默认为 SHOW_MODE_VIDEO 的情况渲染
        if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display2(ffp);
    }
    is->force_refresh = 0;
    if (ffp->show_status) {
        /// show_status 默认为 0,这部分逻辑不分析
        省略部分代码...
        
    }
}

3.Opengl ES 绘制视频帧

看了一下,视频图像的绘制分支还是蛮多的,其中需要有一定的图形图像渲染的基础知识,否则啃起来会比较吃力;
没有基础的可以去看一下我的 Opengl ES 系列文章
OpenGLES - 绘制三角形
OpenGLES - 绘制图片
OpenGLES - 图片翻转
OpenGLES - 绘制金字塔
OpenGLES - YUV格式视频绘制
OpenGLES - RGB格式视频绘制
从上一章节的介绍中,我们知道播放器初始化的时候创建了 实例,在的方法调用中将解码后的视频数据送到了渲染层;
介绍一下渲染中用的 Class ,极其一些渲染相关的文件是干嘛的吧;
IJKSDLGLView 渲染层View,内部通过 CAEAGLLayer实现;SDL_VoutOverlay 视频帧中间层,内部存放着渲染一个视频所需要的数据
renderer_rgb.c 渲染rgb 类型的视频帧的实现逻辑
renderer_yuv420p.c 渲染YUV420P 类型的视频帧的实现逻辑
renderer_yuv420sp_vtb.m 渲染YUV420P_VTB 类型的视频帧的实现逻辑
renderer_yuv420sp.c
renderer_yuv444p10le.c

简化的关系图如下图所示


简化的关系图.jpg

下面的文章以为yuv420sp 类型的视频帧绘制为例子,来做分析;

3.1 IJKSDLGLView 介绍

Opengl环境初始化

EAGLLayer 的初始化没什么特别好讲的,都是模版套路;配置一下透明度,缩放比例

 /// 初始化opengGL 上下文及其相关参数
 [self setupGLOnce];

初始化相关类型Render

/// 检查render 是否存在,和当前渲染的overlayer 格式是否一致,不一致的情况,重新创建
if (!IJK_GLES2_Renderer_isValid(_renderer) ||
    !IJK_GLES2_Renderer_isFormat(_renderer, overlay->format)) {

    /// 重置并释放掉老的_render实例
    IJK_GLES2_Renderer_reset(_renderer);
    IJK_GLES2_Renderer_freeP(&_renderer);
    /// 根据overlayer 类型创建 相应的render实例
    _renderer = IJK_GLES2_Renderer_create(overlay);
    if (!IJK_GLES2_Renderer_isValid(_renderer))
        return NO;

    if (!IJK_GLES2_Renderer_use(_renderer))
        return NO;
    //_rendererGravity 参数决定画面的展示模式/ 填充/适应/裁剪填充 的模式
    IJK_GLES2_Renderer_setGravity(_renderer, _rendererGravity, _backingWidth, _backingHeight);
}

3.2 IJK_GLES2_Renderer 解析

IJK_GLES2_Renderer 封装了绘制视频帧的操作逻辑;

  • IJK_GLES2_Renderer_reset //重置Render 内的所有参数
  • IJK_GLES2_Renderer_free //释放Render 结构体
  • IJK_GLES2_Renderer_freeP //释放Render指针
  • IJK_GLES2_Renderer_create_base //通过片源着色器创建特定类型Render
  • IJK_GLES2_Renderer_create //通过overlayer创建Render 的实现方法
  • IJK_GLES2_Renderer_isValid //验证Render program 程序是否可用
  • IJK_GLES2_Renderer_setupGLES //Render 初始化OpenGL ES 配置
  • IJK_GLES2_Renderer_Vertices_reset //顶点数据重置
  • IJK_GLES2_Renderer_Vertices_apply //根据ASPECT 配置计算后生成新的顶点数据
  • IJK_GLES2_Renderer_Vertices_reloadVertex //更新定点数据
  • IJK_GLES2_Renderer_setGravity //ASPECT 配置方法
  • IJK_GLES2_Renderer_TexCoords_reset //重置纹理坐标
  • IJK_GLES2_Renderer_TexCoords_cropRight //
  • IJK_GLES2_Renderer_use //
  • IJK_GLES2_Renderer_renderOverlay //

Render的创建,根据overlay的视频帧格式创建不同的render

IJK_GLES2_Renderer *IJK_GLES2_Renderer_create(SDL_VoutOverlay *overlay)
{
    if (!overlay)
        return NULL;
    省略代码...
    IJK_GLES2_Renderer *renderer = NULL;
    switch (overlay->format) {
        case SDL_FCC_RV16:      renderer = IJK_GLES2_Renderer_create_rgb565(); break;
        case SDL_FCC_RV24:      renderer = IJK_GLES2_Renderer_create_rgb888(); break;
        case SDL_FCC_RV32:      renderer = IJK_GLES2_Renderer_create_rgbx8888(); break;
#ifdef __APPLE__
        case SDL_FCC_NV12:      renderer = IJK_GLES2_Renderer_create_yuv420sp(); break;
        case SDL_FCC__VTB:      renderer = IJK_GLES2_Renderer_create_yuv420sp_vtb(overlay); break;
#endif
        case SDL_FCC_YV12:      renderer = IJK_GLES2_Renderer_create_yuv420p(); break;
        case SDL_FCC_I420:      renderer = IJK_GLES2_Renderer_create_yuv420p(); break;
        case SDL_FCC_I444P10LE: renderer = IJK_GLES2_Renderer_create_yuv444p10le(); break;
        default:
            ALOGE("[GLES2] unknown format %4s(%d)\n", (char *)&overlay->format, overlay->format);
            return NULL;
    }
    renderer->format = overlay->format;
    return renderer;
}
3.3 yuv420sp 类型 IJK_GLES2_Renderer 解析

创建yuv420sp 类型Render

IJK_GLES2_Renderer *IJK_GLES2_Renderer_create_yuv420sp()
{
    IJK_GLES2_Renderer *renderer = IJK_GLES2_Renderer_create_base(IJK_GLES2_getFragmentShader_yuv420sp());
    if (!renderer)
        goto fail;
    /// 从programe中获取 Y 纹理标识符 存入 us2_sampler[0] 中
    renderer->us2_sampler[0] = glGetUniformLocation(renderer->program, "us2_SamplerX"); IJK_GLES2_checkError_TRACE("glGetUniformLocation(us2_SamplerX)");
    /// 从programe中获取 UV 纹理标识符 存入 us2_sampler[1] 中
    renderer->us2_sampler[1] = glGetUniformLocation(renderer->program, "us2_SamplerY"); IJK_GLES2_checkError_TRACE("glGetUniformLocation(us2_SamplerY)");
    /// 从programe中获取 颜色矩阵标识符
    renderer->um3_color_conversion = glGetUniformLocation(renderer->program, "um3_ColorConversion"); IJK_GLES2_checkError_TRACE("glGetUniformLocation(um3_ColorConversionMatrix)");
    /// 函数指针指向YUV420sp 相应函数的实现
    renderer->func_use            = yuv420sp_use;
    renderer->func_getBufferWidth = yuv420sp_getBufferWidth;
    renderer->func_uploadTexture  = yuv420sp_uploadTexture;

    return renderer;
fail:
    IJK_GLES2_Renderer_free(renderer);
    return NULL;
}

将视频帧的图像数据传入FrameBuffer

static GLboolean yuv420sp_uploadTexture(IJK_GLES2_Renderer *renderer, SDL_VoutOverlay *overlay)
{
    if (!renderer || !overlay)
        return GL_FALSE;

    /// 获取图像的width、height、图像数据byte存放地址
    const GLsizei widths[2]    = { overlay->pitches[0], overlay->pitches[1] / 2 };
    const GLsizei heights[2]   = { overlay->h,          overlay->h / 2 };
    const GLubyte *pixels[2]   = { overlay->pixels[0],  overlay->pixels[1] };

    switch (overlay->format) {
        case SDL_FCC__VTB:
            break;
        default:
            ALOGE("[yuv420sp] unexpected format %x\n", overlay->format);
            return GL_FALSE;
    }
    /// 绑定Y纹理,传入 Y 数据到相应的纹理空间
    glBindTexture(GL_TEXTURE_2D, renderer->plane_textures[0]);
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RED_EXT,
                 widths[0],
                 heights[0],
                 0,
                 GL_RED_EXT,
                 GL_UNSIGNED_BYTE,
                 pixels[0]);
    /// 绑定UV纹理,传入 UV 数据到相应的纹理空间
    glBindTexture(GL_TEXTURE_2D, renderer->plane_textures[1]);
    glTexImage2D(GL_TEXTURE_2D,
                 0,
                 GL_RG_EXT,
                 widths[1],
                 heights[1],
                 0,
                 GL_RG_EXT,
                 GL_UNSIGNED_BYTE,
                 pixels[1]);

    return GL_TRUE;
}

激活纹理,并设置纹理过滤参数,最终显示图像;

static GLboolean yuv420sp_use(IJK_GLES2_Renderer *renderer)
{
    ALOGI("use render yuv420sp\n");
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    glUseProgram(renderer->program);            IJK_GLES2_checkError_TRACE("glUseProgram");

    if (0 == renderer->plane_textures[0])
        glGenTextures(2, renderer->plane_textures);

    for (int i = 0; i < 2; ++i) {
        glActiveTexture(GL_TEXTURE0 + i);
        glBindTexture(GL_TEXTURE_2D, renderer->plane_textures[i]);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

        glUniform1i(renderer->us2_sampler[i], i);
    }

    glUniformMatrix3fv(renderer->um3_color_conversion, 1, GL_FALSE, IJK_GLES2_getColorMatrix_bt709());

    return GL_TRUE;
}

至此,ijkplayer的视频解码和渲染分析完了,渲染这块写的不是很详细,后续有时间再来修改,因为OpenGL这块真是不是一个章节就能说的明白的,它太庞大了;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容