一步步實現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系列文章之七——終結篇(附源碼)html

一步步實現windows版ijkplayer系列文章之二——Ijkplayer播放器源碼分析之音視頻輸出——視頻篇

ijkplayer只支持Android和IOS平臺,最近因爲項目須要,須要一個windows平臺的播放器,以前對ijkplayer播放器有一些瞭解了,因此想在此基礎上嘗試去實現出來。Ijkplayer的數據接收,數據解析和解碼部分用的是ffmepg的代碼。這些部分不一樣平臺下都是可以通用的(視頻硬解碼除外),所以差別的部分就是音視頻的輸出部分。若是實現windows下的ijkplayer就須要把這部分代碼吃透。本身研究了一段時間,如今把一些理解記錄下來。若是有說錯的地方,但願你們可以指正。java

一些相關的知識

SDL

FFmpeg本身實現了一個簡易的播放器,它的渲染使用了SDL,我已經在windows平臺把ffplayer編譯出來了。SDL能夠從網絡下載或者本身編譯均可。android

  • SDL是什麼?

SDL (Simple DirectMedia Layer)是一套開源代碼的跨平臺多媒體開發庫,使用C語言寫成。SDL提供了數種控制圖像、聲音、輸出入的函數,讓開發者只要用相同或是類似的代碼就能夠開發出跨多個平臺(Linux、Windows、Mac OS等)的應用軟件。目前 SDL 多用於開發遊戲、模擬器、媒體播放器等多媒體應用領域。用下面這張圖能夠很明確地說明 SDL 的用途。
git

SDL最基本的功能,說的簡單點,它爲不一樣平臺的窗口建立,surface建立和渲染(render)提供了接口。其中,surface是用EGL建立的,render由OpenGLES來完成。github

OpenGL ES

什麼是openGL ES

OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 三維圖形API的子集,針對手機、PDA和遊戲主機等嵌入式設備而設計,各顯卡製造商和系統製造商來實現這組 APIwindows

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的繪圖過程基本上就是使用上面的流程。ide

源碼分析

如今把音視頻輸出的源碼從頭梳理一遍。以安卓平臺爲例。函數

圖像渲染相關結構體

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來生成渲染對象:oop

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

相關文章
相關標籤/搜索