一步步实现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