热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:Ijkplayer播放器源码分析之音视频输出——视频篇

本文由编程笔记#小编为大家整理,主要介绍了Ijkplayer播放器源码分析之音视频输出——视频篇相关的知识,希望对你有一定的参考价值。 Ijkplayer播放器源码分析之音视频输出——视频篇ijkpl
本文由编程笔记#小编为大家整理,主要介绍了Ijkplayer播放器源码分析之音视频输出——视频篇相关的知识,希望对你有一定的参考价值。



Ijkplayer播放器源码分析之音视频输出——视频篇

ijkplayer只支持androidios平台,最近由于项目需要,需要一个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,另外一种是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
");
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
");
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 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
", __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


推荐阅读
  • 基于Net Core 3.0与Web API的前后端分离开发:Vue.js在前端的应用
    本文介绍了如何使用Net Core 3.0和Web API进行前后端分离开发,并重点探讨了Vue.js在前端的应用。后端采用MySQL数据库和EF Core框架进行数据操作,开发环境为Windows 10和Visual Studio 2019,MySQL服务器版本为8.0.16。文章详细描述了API项目的创建过程、启动步骤以及必要的插件安装,为开发者提供了一套完整的开发指南。 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • 深入解析Android 4.4中的Fence机制及其应用
    在Android 4.4中,Fence机制是处理缓冲区交换和同步问题的关键技术。该机制广泛应用于生产者-消费者模式中,确保了不同组件之间高效、安全的数据传输。通过深入解析Fence机制的工作原理和应用场景,本文探讨了其在系统性能优化和资源管理中的重要作用。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
  • 本文探讨了资源访问的学习路径与方法,旨在帮助学习者更高效地获取和利用各类资源。通过分析不同资源的特点和应用场景,提出了多种实用的学习策略和技术手段,为学习者提供了系统的指导和建议。 ... [详细]
  • FastDFS Nginx 扩展模块的源代码解析与技术剖析
    FastDFS Nginx 扩展模块的源代码解析与技术剖析 ... [详细]
  • 在处理遗留数据库的映射时,反向工程是一个重要的初始步骤。由于实体模式已经在数据库系统中存在,Hibernate 提供了自动化工具来简化这一过程,帮助开发人员快速生成持久化类和映射文件。通过反向工程,可以显著提高开发效率并减少手动配置的错误。此外,该工具还支持对现有数据库结构进行分析,自动生成符合 Hibernate 规范的配置文件,从而加速项目的启动和开发周期。 ... [详细]
  • 在处理大图片时,PHP 常常会遇到内存溢出的问题。为了避免这种情况,建议避免使用 `setImageBitmap`、`setImageResource` 或 `BitmapFactory.decodeResource` 等方法直接加载大图。这些函数在处理大图片时会消耗大量内存,导致应用崩溃。推荐采用分块处理、图像压缩和缓存机制等策略,以优化内存使用并提高处理效率。此外,可以考虑使用第三方库如 ImageMagick 或 GD 库来处理大图片,这些库提供了更高效的内存管理和图像处理功能。 ... [详细]
  • ### 优化后的摘要本学习指南旨在帮助读者全面掌握 Bootstrap 前端框架的核心知识点与实战技巧。内容涵盖基础入门、核心功能和高级应用。第一章通过一个简单的“Hello World”示例,介绍 Bootstrap 的基本用法和快速上手方法。第二章深入探讨 Bootstrap 与 JSP 集成的细节,揭示两者结合的优势和应用场景。第三章则进一步讲解 Bootstrap 的高级特性,如响应式设计和组件定制,为开发者提供全方位的技术支持。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • Python默认字符解析:深入理解Python中的字符串处理
    在Python中,字符串是编程中最基本且常用的数据类型之一。尽管许多初学者是从C语言开始接触字符串,通常通过经典的“Hello, World!”程序入门,但Python对字符串的处理方式更为灵活和强大。本文将深入探讨Python中的字符串处理机制,包括字符串的创建、操作、格式化以及编码解码等方面,帮助读者全面理解Python字符串的特性和应用。 ... [详细]
  • 在Java编程中,利用Scanner类可以有效地接收和处理用户输入。本文介绍了Scanner类的基本概念及其使用方法,重点讲解了三种常用的输入方式,并提供了详细的代码示例和注意事项,帮助开发者更好地理解和应用这一功能。 ... [详细]
  • 基址获取与驱动开发:内核中提取ntoskrnl模块的基地址方法解析
    基址获取与驱动开发:内核中提取ntoskrnl模块的基地址方法解析 ... [详细]
author-avatar
手机用户2602898385
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有