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繪圖的一般步驟
- 獲取 EGL Display 對象:eglGetDisplay()
- 初始化與 EGLDisplay 之間的連接:eglInitialize()
- 獲取 EGLConfig 對象:eglChooseConfig()
- 創建 EGLContext 實例:eglCreateContext()
- 創建 EGLSurface 實例:eglCreateWindowSurface()
- 連接 EGLContext 和 EGLSurface:eglMakeCurrent()
- 使用 OpenGL ES API 繪制圖形:gl_*()
- 切換 front buffer 和 back buffer 送顯:eglSwapBuffer()
- 斷開並釋放與 EGLSurface 關聯的 EGLContext 對象:eglRelease()
- 刪除 EGLSurface 對象
- 刪除 EGLContext 對象
- 終止與 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,另外一種是ffmpeg使用的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
Ijkplayer播放器源碼分析之音視頻輸出——視頻篇