Fork me on GitHub

一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇

一步步实现windows版ijkplayer系列文章之一——Windows10平台编译ffmpeg 4.0.2,生成ffplay
一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇
一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇
一步步实现windows版ijkplayer系列文章之四——windows下编译ijkplyer版ffmpeg
一步步实现windows版ijkplayer系列文章之五——使用automake一步步生成makefile
一步步实现windows版ijkplayer系列文章之六——SDL2源码分析之OpenGL ES在windows上的渲染过程
一步步实现windows版ijkplayer系列文章之七——终结篇(附源码)

一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇

ijkplayer只支持Android和IOS平台,最近由于项目需要,需要一个windows平台的播放器,之前对ijkplayer播放器有一些了解了,所以想在此基础上尝试去实现出来。Ijkplayer的数据接收,数据解析和解码部分用的是ffmepg的代码。这些部分不同平台下都是能够通用的(视频硬解码除外),因此差异的部分就是音视频的输出部分。如果实现windows下的ijkplayer就需要把这部分代码吃透。自己研究了一段时间,现在把一些理解记录下来。如果有说错的地方,希望大家能够指正。

一些相关的知识

SDL

FFmpeg自己实现了一个简易的播放器,它的渲染使用了SDL,我已经在windows平台把ffplayer编译出来了。SDL可以从网络下载或者自己编译都可。

  • SDL是什么?

SDL (Simple DirectMedia Layer)是一套开源代码的跨平台多媒体开发库,使用C语言写成。SDL提供了数种控制图像、声音、输出入的函数,让开发者只要用相同或是相似的代码就可以开发出跨多个平台(Linux、Windows、Mac OS等)的应用软件。目前 SDL 多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。用下面这张图可以很明确地说明 SDL 的用途。

SDL最基本的功能,说的简单点,它为不同平台的窗口创建,surface创建和渲染(render)提供了接口。其中,surface是用EGL创建的,render由OpenGLES来完成。

OpenGL ES

什么是openGL ES

OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 三维图形API的子集,针对手机、PDA和游戏主机等嵌入式设备而设计,各显卡制造商和系统制造商来实现这组 API

EGL

什么是EGL

EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现。EGL提供如下机制:

  • 与设备的原生窗口系统通信
  • 查询绘图表面的可用类型和配置
  • 创建绘图表面
  • 在OpenGL ES 和其他图形渲染API之间同步渲染
  • 管理纹理贴图等渲染资源
  • 为了让OpenGL ES能够绘制在当前设备上,我们需要EGL作为OpenGL ES与设备的桥梁。

OpenGL ES和EGL的关系

使用EGL绘图的一般步骤

  1. 获取 EGL Display 对象:eglGetDisplay()
  2. 初始化与 EGLDisplay 之间的连接:eglInitialize()
  3. 获取 EGLConfig 对象:eglChooseConfig()
  4. 创建 EGLContext 实例:eglCreateContext()
  5. 创建 EGLSurface 实例:eglCreateWindowSurface()
  6. 连接 EGLContext 和 EGLSurface:eglMakeCurrent()
  7. 使用 OpenGL ES API 绘制图形:gl_*()
  8. 切换 front buffer 和 back buffer 送显:eglSwapBuffer()
  9. 断开并释放与 EGLSurface 关联的 EGLContext 对象:eglRelease()
  10. 删除 EGLSurface 对象
  11. 删除 EGLContext 对象
  12. 终止与 EGLDisplay 之间的连接

Ijkplayer通过EGL的绘图过程基本上就是使用上面的流程。

源码分析

现在把音视频输出的源码从头梳理一遍。以安卓平台为例。

图像渲染相关结构体

struct SDL_Vout {
SDL_mutex *mutex;

SDL_Class       *opaque_class;
SDL_Vout_Opaque *opaque;
SDL_VoutOverlay *(*create_overlay)(int width, int height, int frame_format, SDL_Vout *vout);
void (*free_l)(SDL_Vout *vout);
int (*display_overlay)(SDL_Vout *vout, SDL_VoutOverlay *overlay);
Uint32 overlay_format;
};

typedef struct SDL_Vout_Opaque {
ANativeWindow   *native_window;//视频图像窗口
SDL_AMediaCodec *acodec;
int              null_native_window_warned; // reduce log for null window
int              next_buffer_id;

ISDL_Array       overlay_manager;
ISDL_Array       overlay_pool;

IJK_EGL         *egl;//
} SDL_Vout_Opaque;

typedef struct IJK_EGL
{
SDL_Class      *opaque_class;
IJK_EGL_Opaque *opaque;

EGLNativeWindowType window;

EGLDisplay display;
EGLSurface surface;
EGLContext context;

EGLint width;
EGLint height;
} IJK_EGL;

初始化播放器的渲染对象

通过调用SDL_VoutAndroid_CreateForAndroidSurface来生成渲染对象:

IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
    ...
    mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();
    if (!mp->ffplayer->vout)
        goto fail;
    ...

} 

最后通过调用 SDL_VoutAndroid_CreateForAndroidSurface来生成播放器渲染对象,看一下播放器渲染对象的几个成员:

  • func_create_overlay用于创建视频帧渲染对象。
  • func_display_overlay为图像显示接口函数。
  • func_free_l用于释放资源。

视频解码后将相关数据存入每个视频帧的渲染对象中,然后通过调用func_display_overlay函数将图像渲染显示。

SDL_Vout *SDL_VoutAndroid_CreateForANativeWindow()
{
SDL_Vout *vout = SDL_Vout_CreateInternal(sizeof(SDL_Vout_Opaque));
if (!vout)
    return NULL;

SDL_Vout_Opaque *opaque = vout->opaque;
opaque->native_window = NULL;
if (ISDL_Array__init(&opaque->overlay_manager, 32))
    goto fail;
if (ISDL_Array__init(&opaque->overlay_pool, 32))
    goto fail;

opaque->egl = IJK_EGL_create();
if (!opaque->egl)
    goto fail;

vout->opaque_class    = &g_nativewindow_class;
vout->create_overlay  = func_create_overlay;
vout->free_l          = func_free_l;
vout->display_overlay = func_display_overlay;

return vout;
fail:
func_free_l(vout);
return NULL;
}

视频帧渲染对象的创建

创建渲染对象函数:

static SDL_VoutOverlay *func_create_overlay_l(int width, int height, int frame_format, SDL_Vout *vout)
{
switch (frame_format) {
case IJK_AV_PIX_FMT__ANDROID_MEDIACODEC:
    return SDL_VoutAMediaCodec_CreateOverlay(width, height, vout);
default:
    return SDL_VoutFFmpeg_CreateOverlay(width, height, frame_format, vout);
}
}

可以看到andorid平台下的图像渲染有两种方式,一种是MediaCodeC,另外一种是OpenGL。因为OpenGL是平台无关的,因此我们着重研究这种图像渲染方式。

视频解码器每解码出一帧图像,都会把此帧插入帧队列中。播放器会对插入队列的帧做一些处理。比如,它会为每一帧通过调用SDL_VoutOverlay创建一个渲染对象。看下面的代码:

static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial){
 ...
    if (!(vp = frame_queue_peek_writable(&is->pictq)))//将队尾的可写视频帧取出来
    return -1;
 ...
   alloc_picture(ffp, src_frame->format);//此函数中调用SDL_Vout_CreateOverlay为当前帧创建(初始化)渲染对象
   ...
   if (SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame) < 0) {//将相关数据填充到渲染对象中
        av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
        exit(1);
    }
    
    ....
  frame_queue_push(&is->pictq);//最后push到帧队列中供渲染显示函数处理。
}

在alloc_picture中为视频帧队列中的视频帧创建渲染对象。

static void alloc_picture(FFPlayer *ffp, int frame_format)
{
...
vp->bmp = SDL_Vout_CreateOverlay(vp->width, vp->height,
                               frame_format,
                               ffp->vout);   
...
} 

继续看一下渲染对象的创建:

SDL_VoutOverlay *SDL_VoutFFmpeg_CreateOverlay(int width, int height, int frame_format, SDL_Vout *display)

看一下此函数的参数,前两个参数为图像的宽度和高度,第三个参数为视频帧的格式,第四个参数为上面我们提到的播放器的渲染对象。播放器的渲染对象中也有一个成员为视频帧格式,但是没有在上面提到的初始化函数中初始化。最后搜了一下,有两个地方可以对播放器的视频帧格式进行初始化,一个是下面的函数:

inline static void ffp_reset_internal(FFPlayer *ffp)
{
     ....
    ffp->overlay_format         = SDL_FCC_RV32;
    ...
}

还有一个地方是通过配置项配置的:

{ "overlay-format",                 "fourcc of overlay format",
    OPTION_OFFSET(overlay_format),  OPTION_INT(SDL_FCC_RV32, INT_MIN, INT_MAX),
    .unit = "overlay-format" },
    

在java代码中通过如下方式指定视频帧图像格式:

  m_IjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
  

回到视频帧渲染对象的创建函数中:

Uint32 overlay_format = display->overlay_format;
switch (overlay_format) {
    case SDL_FCC__GLES2: {
        switch (frame_format) {
            case AV_PIX_FMT_YUV444P10LE:
                overlay_format = SDL_FCC_I444P10LE;
                break;
            case AV_PIX_FMT_YUV420P:
            case AV_PIX_FMT_YUVJ420P:
            default:
#if defined(__ANDROID__)
                overlay_format = SDL_FCC_YV12;
#else
                overlay_format = SDL_FCC_I420;
#endif
                break;
        }
        break;
    }
}

上面的几行代码意思是如果播放器采用OpenGL渲染图像,需要将图像格式转换成ijkplayer自定义的图像格式。

处理完视频帧后会将相关数据保存到如下的对象中:

SDL_VoutOverlay_Opaque *opaque = overlay->opaque;

为渲染对象指定视频帧处理函数:

overlay->func_fill_frame    = func_fill_frame;

接下来定义和初始化managed_frame和linked_frame

opaque->managed_frame = opaque_setup_frame(opaque, ff_format, buf_width, buf_height);
if (!opaque->managed_frame) {
    ALOGE("overlay->opaque->frame allocation failed\n");
    goto fail;
}
overlay_fill(overlay, opaque->managed_frame, opaque->planes);

关于这两种帧的区别,下面会提到。

视频帧的处理

关于视频帧的处理,看一下func_fill_frame这个函数 :

static int func_fill_frame(SDL_VoutOverlay *overlay, const AVFrame *frame)

它的两个参数,第一个是我们之前提到的在alloc_picture中初始化的渲染对象,frame为解码出来的视频帧。
此函数中一开始对播放器中指定的图像格式和视频帧的图像格式做了比较,如果两个图像格式一致,例如,图像格式都为YUV420,那么就不需要调用sws_scale函数进行图像格式的转换,反之,则需要做转换。不需要转换的通过linked_frame来填充渲染对象,需要转换则通过manged_frame进行填充。

好了,视频帧的渲染对象中填好了数据,并且将其插入视频帧队列中了,接下来就是显示了。

视频渲染线程

static int video_refresh_thread(void *arg)
{
    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函数中:

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

会查看解码出来的帧是否为当前帧,如果不是会一直等待。然后进行音视频的同步,如果当前视频帧在显示时间范围内,则调用显示函数显示:

   if (time < is->frame_timer + delay) {
            *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
            goto display;
        }

还有一个goto到进行显示的地方,不知道为什么在pause的情况下也会跳到display。

   if (is->paused)
            goto display;

最终会跳到下面的函数中进行显示:

static int func_display_overlay_l(SDL_Vout *vout, SDL_VoutOverlay *overlay);

下面是显示前的一些准备工作。

Surface创建

Surface是用java代码生成的,并且通过JNI方法传递到native代码中。

public void setDisplay(SurfaceHolder sh) {
    mSurfaceHolder = sh;
    Surface surface;
    if (sh != null) {
        surface = sh.getSurface();
    } else {
        surface = null;
    }
    _setVideoSurface(surface);
    updateSurfaceScreenOn();
}

JNI 方法

static JNINativeMethod g_methods[] = {
{
...,
{ "_setVideoSurface",       "(Landroid/view/Surface;)V", (void *) IjkMediaPlayer_setVideoSurface },
...
}

窗口创建

native代码使用传递过来的surface初始化窗口:

void SDL_VoutAndroid_SetAndroidSurface(JNIEnv *env, SDL_Vout *vout, jobject android_surface)
{
    ANativeWindow *native_window = NULL;
    if (android_surface) {
    native_window = ANativeWindow_fromSurface(env, android_surface);//初始化窗口
    if (!native_window) {
        ALOGE("%s: ANativeWindow_fromSurface: failed\n", __func__);
        // do not return fail here;
    }
}

SDL_VoutAndroid_SetNativeWindow(vout, native_window);
if (native_window)
    ANativeWindow_release(native_window);

}

视频渲染方式的选择

窗口创建好之后,回去再看一下渲染显示函数:

static int func_display_overlay_l(SDL_Vout *vout, SDL_VoutOverlay *overlay)

两个参数,第一个为前面提到的播放器渲染对象,第二个是视频帧的渲染对象。采用什么样的渲染方式取决于两个渲染对象中图像格式的设定。目前我自己看到的,为视频帧对象中的format成员赋值的就是播放器渲染对象的图像格式:

SDL_VoutOverlay *SDL_VoutFFmpeg_CreateOverlay(int width, int height, int frame_format, SDL_Vout *display)
{
        Uint32 overlay_format = display->overlay_format;
        ...

        SDL_VoutOverlay *overlay = SDL_VoutOverlay_CreateInternal(sizeof(SDL_VoutOverlay_Opaque));
        if (!overlay) {
            ALOGE("overlay allocation failed");
            return NULL;
        }
        ...

        overlay->format       = overlay_format;
        ...

        return overlay;
}

渲染方式有下面三种判断:

  • 如果视频帧图像格式为SDL_FCC__AMC(MediaCodec),则只支持native渲染方式。所以把openGL渲染用到的egl对象释放掉。
  • 如果视频帧图像格式为SDL_FCC_RV24,SDL_FCC_I420或者SDL_FCC_I444P10LE,使用OpenGL渲染。
  • 其余的图像格式即有可能是native渲染也有可能是OpenGL渲染。取决于播放器设定的图像渲染方式是否为SDL_FCC__GLES2,如果是,则采用OpenGL渲染,否则采用native方式渲染。

native渲染方式比较简单,把overlay中存储的图像信息拷贝到ANativeWindow_Buffer即可。OpenGL渲染比较复杂一些。

OpenGL 渲染

前面介绍过了,使用OpenGL进行渲染需要使用EGL同底层API进行通信。看一下渲染的整个过程:

EGLBoolean IJK_EGL_display(IJK_EGL* egl, EGLNativeWindowType window, SDL_VoutOverlay *overlay)
{
    EGLBoolean ret = EGL_FALSE;
    if (!egl)
        return EGL_FALSE;

    IJK_EGL_Opaque *opaque = egl->opaque;
    if (!opaque)
        return EGL_FALSE;

    if (!IJK_EGL_makeCurrent(egl, window))
        return EGL_FALSE;

    ret = IJK_EGL_display_internal(egl, window, overlay);
    eglMakeCurrent(egl->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    eglReleaseThread(); // FIXME: call at thread exit
    return ret;
}

三个参数,第一个参数为初始化的EGL对象,第二个为已经创建好的nativewindow,第三个为视频帧渲染对象。 IJK_EGL_makeCurrent这个函数进行的是前面说明的EGL绘图的第一步到第六步,将EGL的初始化数据保存到 egl变量中。

 static EGLBoolean IJK_EGL_makeCurrent(IJK_EGL* egl, EGLNativeWindowType window)

IJK_EGL_display_internal 函数里面进行的是创建render,然后调用OpenGL API渲染数据。

 static EGLBoolean IJK_EGL_display_internal(IJK_EGL* egl, EGLNativeWindowType window, SDL_VoutOverlay *overlay)

参考

https://woshijpf.github.io/android/2017/09/04/Android系统图形栈OpenGLES和EGL介绍.html

https://blog.csdn.net/leixiaohua1020/article/details/14215391

https://blog.csdn.net/leixiaohua1020/article/details/14214577

https://blog.csdn.net/xipiaoyouzi/article/details/53584798

https://www.jianshu.com/p/4b60cea7fa85

posted @ 2018-09-20 16:27  HarlanC  阅读(...)  评论(... 编辑 收藏