一步步實現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
ijkplayer只支持Android和IOS平臺,最近因爲項目須要,須要一個windows平臺的播放器,以前對ijkplayer播放器有一些瞭解了,因此想在此基礎上嘗試去實現出來。Ijkplayer的數據接收,數據解析和解碼部分用的是ffmepg的代碼。這些部分不一樣平臺下都是可以通用的(視頻硬解碼除外),所以差別的部分就是音視頻的輸出部分。若是實現windows下的ijkplayer就須要把這部分代碼吃透。本身研究了一段時間,如今把一些理解記錄下來。若是有說錯的地方,但願你們可以指正。java
FFmpeg本身實現了一個簡易的播放器,它的渲染使用了SDL,我已經在windows平臺把ffplayer編譯出來了。SDL能夠從網絡下載或者本身編譯均可。android
SDL (Simple DirectMedia Layer)是一套開源代碼的跨平臺多媒體開發庫,使用C語言寫成。SDL提供了數種控制圖像、聲音、輸出入的函數,讓開發者只要用相同或是類似的代碼就能夠開發出跨多個平臺(Linux、Windows、Mac OS等)的應用軟件。目前 SDL 多用於開發遊戲、模擬器、媒體播放器等多媒體應用領域。用下面這張圖能夠很明確地說明 SDL 的用途。
git
SDL最基本的功能,說的簡單點,它爲不一樣平臺的窗口建立,surface建立和渲染(render)提供了接口。其中,surface是用EGL建立的,render由OpenGLES來完成。github
OpenGL ES(OpenGL for Embedded Systems)是 OpenGL 三維圖形API的子集,針對手機、PDA和遊戲主機等嵌入式設備而設計,各顯卡製造商和系統製造商來實現這組 APIwindows
EGL 是 OpenGL ES 渲染 API 和本地窗口系統(native platform window system)之間的一箇中間接口層,它主要由系統製造商實現。EGL提供以下機制:網絡
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_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是用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; }
渲染方式有下面三種判斷:
native渲染方式比較簡單,把overlay中存儲的圖像信息拷貝到ANativeWindow_Buffer便可。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