美文网首页iOS 提升
ijkplayer 源码解析4(视频解码+渲染)

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

作者: pengxiaochao | 来源:发表于2023-02-08 19:17 被阅读0次

    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这块真是不是一个章节就能说的明白的,它太庞大了;

    相关文章

      网友评论

        本文标题:ijkplayer 源码解析4(视频解码+渲染)

        本文链接:https://www.haomeiwen.com/subject/tvstkdtx.html