一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇...
一步步实现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系列文章之二——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;
#elseoverlay_format = SDL_FCC_I420;
#endifbreak;}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 exitreturn 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
一步步实现windows版ijkplayer系列文章之二——Ijkplayer播放器源码分析之音视频输出——视频篇...相关推荐
- 一步步实现windows版ijkplayer系列文章之三——Ijkplayer播放器源码分析之音视频输出——音频篇
https://www.cnblogs.com/harlanc/p/9693983.html 目录 OpenSL ES & AudioTrack 源码分析 创建播放器音频输出对象 配置并创建音 ...
- 2022最新轻量级影视搜索播放器源码+已修复版
正文: 2022最新轻量级影视搜索播放器源码+已修复版,因为之前API失效了,需要重新写规则,所以本次我花了点时间修复了下源码,有兴趣自行去研究. 更新日志: 2020/7/14 前端修正了一些显示, ...
- TiKV 源码解析系列文章(二)raft-rs proposal 示例情景分析
作者:屈鹏 本文为 TiKV 源码解析系列的第二篇,按照计划首先将为大家介绍 TiKV 依赖的周边库 raft-rs .raft-rs 是 Raft 算法的 Rust 语言实现.Raft 是分布式领域 ...
- SkeyePlayer RTSP/RTMP流媒体超低延迟播放器源码解析系列之H264一帧多NAL写MP4录像花屏问题解决方案
接上一篇[SkeyePlayer源码解析系列之录像写MP4]之续篇,我们来讲解一下关于H264编码格式中的一帧多nal(Network Abstract Layer, 即网络抽象层),关于H264和N ...
- 图解VC++版PE文件解析器源码分析
该源码下载自 http://download.csdn.net/download/witch_soya/4979587 1 Understand 分析的图表 2 PE结构解析的主要代码简要分析 首先看 ...
- cocos creator麻将教程系列(八)—— 达达麻将语音聊天源码分析
达达麻将语音聊天源码分析 达达麻将版图 语音聊天 1:语音聊天只支持Native平台,iOS与android; 2: 语音聊天的音频格式为amr; 3: native平台实现了语音的录制和播放,可以移 ...
- 动态代理最全详解系列[2]-Proxy生成代理类对象源码分析
之前我们通过JDK中的Proxy实现了动态代理,Proxy用起来是比较简便的,但理解起来不是那么清晰,是因为我们并没有看见代理类是怎么生成的,代理类怎么调用的被代理类方法,所以下面我们进入源码看一 ...
- Spring IOC 容器源码分析系列文章导读
1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...
- Spring IOC 容器源码分析系列文章导读 1
1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...
最新文章
- matlab最大化函数,求助,最大化一个函数
- 驴友生涯的开始--香八拉路线精选
- Jmeter 命令行选项目录
- vs显示堆栈数据分析_什么是“数据分析堆栈”?
- grep -e命令详解_grep中的正则表达式
- 老粉丝来:再来一波免费送书
- 组装式AI落地新模式,降低企业AI试错成本
- 博客大全被百度标注为风险网站
- 拓端tecdat|R语言时变向量自回归(TV-VAR)模型分析时间序列和可视化
- 283.判断二分图(力扣leetcode) 博主可答疑该问题
- 计算机专业期末总结200字,本学期自我总结200字
- 长假将至,携程滴滴都太老土了!俺们区块链的出行方式是酱紫的……
- VALSE 4月12日 下午 第一会场 深度学习模型设计 会议记录
- 计算机增加睡眠模式,如果我的计算机处于睡眠模式,Windows 10是否会更新? | MOS86...
- 重装战姬服务器维护,重装战姬更新维护公告(2020年3月26日)
- Python实现数字转人民币(大写汉字)源代码
- nb移动udp_【一点资讯】华为L3 | 集采补考真题及答案 www.yidianzixun.com
- 近期FTDI的FT232RL杀疯了,给大家介绍一款国产替代P TO P的高性能GR232RL
- 学校计算机室上机记录,学生上机记录表
- 通过python爬取笔趣阁小说,获取图片保存本地,数据保存mysql
热门文章
- activiti报错:cvc-datatype-valid.1.2.1: '1' 不是 'NCName' 的有效值。
- 【分享论坛】最新steam离线账号/单机资源/优质软件/资源问题解答
- iaaS 基础设施服务,PaaS 平台服务,SaaS 软件服务- 的区别
- python 变量命名规范
- Markdown插件mavon-editor上传图片
- 频率、波长、传输距离、传输速率之间的关系
- 1561_AURIX_TC275_电源管理以及监控
- 百度召开Web App应用开发者大会分享应用开放平台
- bartender mysql_详解BarTender连接数据库方法
- 应用决策树算法进行股票财务特征分析