热门标签 | 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


推荐阅读
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文将带你快速了解 SpringMVC 框架的基本使用方法,通过实现一个简单的 Controller 并在浏览器中访问,展示 SpringMVC 的强大与简便。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • 【妙】bug称它为数组越界的妙用
    1、聊一聊首先跟大家推荐一首非常温柔的歌曲,跑步的常听。本文主要把自己对C语言中柔性数组、零数组等等的理解分享给大家,并聊聊如何构建一种统一化的学习思想 ... [详细]
  • STM32串口通信:完整指南
    众所周知,串口通信是MCU最基本的通信方式,对于STM32来说也是如此。本文重点讲述STM32单片机的串口通信,主要包括的内容是:通信基础知识、串口通信原理、USART有关寄存器和 ... [详细]
  • 探讨异步 Rust 中多线程代码无法实现并行化的原因及解决方案。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 原文网址:https:www.cnblogs.comysoceanp7476379.html目录1、AOP什么?2、需求3、解决办法1:使用静态代理4 ... [详细]
  • 基于Linux开源VOIP系统LinPhone[四]
    ****************************************************************************************** ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 开发日志:高效图片压缩与上传技术解析 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • 本文详细解析了Java类加载系统的父子委托机制。在Java程序中,.java源代码文件编译后会生成对应的.class字节码文件,这些字节码文件需要通过类加载器(ClassLoader)进行加载。ClassLoader采用双亲委派模型,确保类的加载过程既高效又安全,避免了类的重复加载和潜在的安全风险。该机制在Java虚拟机中扮演着至关重要的角色,确保了类加载的一致性和可靠性。 ... [详细]
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社区 版权所有