基于GLSurfaceView的视频播放器偶现无画面的问题分析
文章目录
- 一、 问题背景
- 二、 逐步排查
- 2.1 增加log,复现问题
- 2.2 查看ijkplayer源码
- 2.3 查看AOSP源码
- 三、 分析原因
- 3.1 Renderer回调onSurfaceCreated
- 3.2 Player回调onPrepared
- 3.3 总结
- 四、 解决方案
- 4.1 串行
- 4.2 并行
- 五、 反思总结
一、 问题背景
博主所在项目中,涉及到视频动画播放功能,其实现方案采用的是bilibili开源项目ijkplayer播放器+GLSurfaceView+自定义渲染器:
- ijkplayer提供视频解码能力,回调帧数据
- 自定义Renderer实现shader操作,对帧画面修改
- GLSurfaceView作为画布,进行展示
整个视频动画播放流程如下:
图1.1 视频动画播放流程
在长达近一年时间里,会偶现视频播放无画面的问题,具体表现为:视频动画开始播放到结束期间,没有任何帧画面。
该问题到了博主手里有半年时间,受限于对视频解码、OpenGL等技术领域知识体系的匮乏,尽管每隔一段时间把该问题捞出来分析一天,但每次都不了了之。并且也认为自己搞不定这个问题,无从下手。
这周趁着需求空档期,有些时间,决定调整思路,再系统地分析一遍这个问题。
二、 逐步排查
2.1 增加log,复现问题
- 在SurfaceTexture#OnFrameAvailableListener的
onFrameAvailable
回调中增加日志,正常情况下每一帧都会回调该方法。 - ijkplayer提供了外部注入日志打印的能力,通过IjkLogConfig.setIjkLog设置一个接收日志的对象,加上自己的TAG。
在测试环境下不停送礼触发礼物视频动画,压测上百次后,复现出该问题,抓取日志,发现其中大量如下异常信息:
04-25 21:21:20.568 E/BufferQueueProducer(16697): [SurfaceTexture-1-16697-76] query: BufferQueue has been abandoned
04-25 21:21:20.568 E/BufferQueueProducer(16697): [SurfaceTexture-1-16697-76] query: BufferQueue has been abandoned
04-25 21:21:20.568 E/BufferQueueProducer(16697): [SurfaceTexture-1-16697-76] dequeueBuffer: BufferQueue has been abandoned
04-25 21:21:20.568 E/Surface (16697): dequeueBuffer failed (No such device)
04-25 21:21:20.568 E/IJKMEDIA(16697): SDL_Android_NativeWindow_display_l: ANativeWindow_lock: failed -19
04-25 21:21:20.579 E/IJKMEDIA(16697): SDL_AMediaCodecJava_dequeueInputBuffer return -1
04-25 21:21:20.580 E/IJKMEDIA(16697): SDL_AMediaCodec_dequeueInputBuffer 1 fail
04-25 21:21:20.580 I/IJKMEDIA(16697): SDL_AMediaCodec_dequeueInputBuffer 1 fail
04-25 21:21:20.583 E/IJKMEDIA(16697): av_read_frame error = -541478725
打印频率符合每帧打印一次,而onFrameAvailable
回调仅首帧打印了一次。
根据日志,在native层解码器从缓冲队列出队数据时,发生了异常,错误码-19
,因此,先从ijkplayer源码开始分析错误码具体含义。
2.2 查看ijkplayer源码
在ijkplayer的Android源码中,全局搜索SDL库的方法SDL_Android_NativeWindow_display_l
,任选一个CPU平台,这里以arm64
为例:
int SDL_Android_NativeWindow_display_l(ANativeWindow *native_window, SDL_VoutOverlay *overlay)
{int retval;...ANativeWindow_Buffer out_buffer;retval = ANativeWindow_lock(native_window, &out_buffer, NULL);if (retval < 0) {ALOGE("SDL_Android_NativeWindow_display_l: ANativeWindow_lock: failed %d", retval);return retval;}...retval = ANativeWindow_unlockAndPost(native_window);if (retval < 0) {ALOGE("SDL_Android_NativeWindow_display_l: ANativeWindow_unlockAndPost: failed %d", retval);return retval;}return render_ret;
}
报错日志即上面这一行代码所输出,看到成对出现的lock和unlock,第一反应是canvas绘制时的操作步骤,结合这里的方法名,推测这里也是要执行绘制相关操作。
全局搜索未找到ANativeWindow_lock
这个方法,因此前往AOSP中查找。
2.3 查看AOSP源码
以Android Q为例,在ANativeWindow中找到:
其调用具体实现位于Surface中:
继续追踪调用链:
这里打印的log符合前面复现问题时的日志。虽然不懂native层渲染逻辑具体实现和原理,但从类名和方法名来看,这里应该是要从缓冲队列中出队帧数据,继续向下追踪:
该方法中有两处给result
赋值的地方,后面一处在小于0时会打印错误级别的日志,而本地复现日志中没有对应记录,因此错误码-19
就是这里返回的。
在BufferQueueProducer中:
在源码中,NO_INIT
的值定义为-ENODEV
,而ENODEV
正好等于19。
现在需要分析的是:mCore->mIsAbandoned
在什么时候为true。
与Producer相对应,在BufferQueueConsumer中找到了答案,位于disconnect
方法中,这个方法也有对应的connect
方法。
BufferQueueCore中对mIsAbandoned
的声明如下:
- 表明从IGraphicBufferProducer接口入队到BufferQueue中的图像缓冲,不会再被消费
- 初始值为false,执行
consumerDisconnect
方法后置为true - 对于已废弃的BufferQueue,从IGraphicBufferProducer接口调过来时都会返回
NO_INIT
错误
在IGraphicBufferConsumer中有两处consumerDisconnect
的调用:
和
前者跨进程调用给后者的IBinder,然后后者在进程内调用,这是因为native渲染流程位于一个与应用进程独立的进程。
从现在开始倒推分析,均位于应用进程。
ConsumerBase的abandonLocked
方法被SurfaceTexture覆写,这在头文件中有声明:
看到SurfaceTexture的native类,不禁想到Bitmap也是这样设计,Java层只是一个壳,封装一些基本的API,本质上是通过JNI调用native方法,核心逻辑全部位于Native层的同名类中。
abandonLocked
方法又是由abandon
调用,abandon
由SurfaceTexture的JNI调用:
回到Java层的SurfaceTexture:
- 用于释放缓冲区资源,将SurfaceTexture置为
abandoned
状态且不可逆转 - 当处于
abandoned
状态,调用IGraphicBufferProducer接口的任何方法都会返回NO_INIT
错误,即错误码-19
- 调用后会释放这个SurfaceTexture关联的所有缓冲,如果有客户端或OpenGL ES通过纹理的方式引用这些缓冲,则继续保留
- 当不再使用该SurfaceTexture时,需要调用这个方法,避免后续资源分配受阻
这和前面看到的BufferQueueCore中对mIsAbandoned
字段的描述基本上是一回事。
由此可知,以上释放资源的步骤主要流程如下:
图2.3.1 视频动画释放资源主要流程
三、 分析原因
根据前面的分析,出现无画面问题的原因是,使用了一个已经释放资源的SurfaceTexture,从而导致缓冲区出队帧数据时报错。
回过头来看前面的视频动画播放流程3,Player播放有两个前置条件:
- 播放器准备就绪(初始化环境资源等):由播放器异步回调
onPrepared
,主线程 - 设置Surface:由Renderer回调
onSurfaceCreated
时创建的SurfaceTexture,再创建出Surface,GL子线程
以上两个条件位于两个不同的线程,如果未做线程同步校验,那么无法保证在条件一播放器准备就绪时,条件二新的Surface已经创建好,如果每次视频动画执行结束后未将旧的变量置空,就会导致使用上一次释放过的对象传给Player,从日志中,也证实了出现问题时使用的旧的SurfaceTexture对象。
那么,为什么绝大部分情况下都能正常播放,仅仅偶现无画面的问题呢?这得从两个条件的回调时机着手分析。
3.1 Renderer回调onSurfaceCreated
在GLSurfaceView中,定义了静态内部类GLThread,其run
方法执行的核心逻辑为guardedRun
方法:
private void guardedRun() throws InterruptedException {mHaveEglContext = false;...boolean createEglContext = false;boolean askedToReleaseEglContext = false;...while(true) {synchronized (sGLThreadManager) {while(true) {...// If we don't have an EGL context, try to acquire one.if (! mHaveEglContext) {if (askedToReleaseEglContext) {askedToReleaseEglContext = false;} else {try {mEglHelper.start();} catch (RuntimeException t) {sGLThreadManager.releaseEglContextLocked(this);throw t;}mHaveEglContext = true;createEglContext = true;sGLThreadManager.notifyAll();}}...}}...if (createEglContext) {if (LOG_RENDERER) {Log.w("GLThread", "onSurfaceCreated");}GLSurfaceView view = mGLSurfaceViewWeakRef.get();if (view != null) {try {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceCreated");view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}}createEglContext = false;}...}...
}private Renderer mRenderer;
内层死循环设置标识位,跳出循环后,会创建Egl环境,其中便有回调Renderer的onSurfaceCreated
方法。
而线程启动的地方有两处:
public void setRenderer(Renderer renderer) {...mRenderer = renderer;mGLThread = new GLThread(mThisWeakRef);mGLThread.start();
}@Override
protected void onAttachedToWindow() {super.onAttachedToWindow();if (LOG_ATTACH_DETACH) {Log.d(TAG, "onAttachedToWindow reattach =" + mDetached);}if (mDetached && (mRenderer != null)) {int renderMode = RENDERMODE_CONTINUOUSLY;if (mGLThread != null) {renderMode = mGLThread.getRenderMode();}mGLThread = new GLThread(mThisWeakRef);if (renderMode != RENDERMODE_CONTINUOUSLY) {mGLThread.setRenderMode(renderMode);}mGLThread.start();}mDetached = false;
}
- 首次设置Renderer时
- GLSurfaceView使用过后从窗口移除,后续复用添加到窗口时
对于回调onSurfaceCreated
的耗时点,前者等于创建线程到线程真正开始执行这段时间,取决于系统当前分配资源以及CPU分配时间片的耗时,通常很短;后者等于将GLSurfaceView添加到窗口的耗时加上前者的耗时,而添加到窗口的耗时,在主线程流畅的情况下,会在调用addView
后的下一帧添加到窗口,也就是一个VSYNC
信号的间隔时长,但在丢帧的情况下,即VSYNC
信号到来时,无法及时响应Choreographer中的doFrame
操作,遍历View树,回调新View的onAttachedToWindow
,因此耗时会成倍增加。
3.2 Player回调onPrepared
以原生的MediaPlayer为例(IjkMediaPlayer类似),播放器准备操作的大致流程如下:
图3.2.1 播放器准备操作大致流程
Native层具体操作不作详细阐述。经多次测试,这个耗时大致在20ms——150ms之间浮动,大于一个VSYNC
信号间隔16.7ms(60Hz刷新率下)。
3.3 总结
从以上两点分析可知,在播放视频动画前的准备阶段,如果主线程没有卡顿问题,则通常都能正常播放。而对于丢帧的情景,该问题复现概率理论上会显著提高,读者可以通过主线程执行耗时任务模拟卡顿来证明。
四、 解决方案
该问题本质上是一个多线程环境下的时序问题,解决方法有两种,分别进行说明。
4.1 串行
Player的播放依赖于Surface,那么在Surface创建完毕后才开始执行Player的准备操作:
图4.1.1 视频播放串行准备流程
对于GLSurfaceView提前添加或默认添加到布局的场景下,如果较早设置了Renderer,则可以较早地创建SurfaceTexture,那么无需关注该时机问题,只需要在场景触发播放视频时,正常设置资源和监听、准备、开始播放。
但对于仅在需要时才将GLSurfaceView添加到窗口,即节约系统资源的场景下,必须关注该时机问题,那么串行将导致视频动画真正渲染上屏的首帧时间,被延后一到多个VSYNC
信号周期。
4.2 并行
为了兼顾“节约系统资源”、“缩短首帧耗时”,可以通过多线程并行+同步校验的方式:
图4.2.1 视频播放并行准备流程
GLSurfaceView在需要播放视频时调用addView
添加到窗口,在动画结束后调用removeView
及时从窗口移除。在addView
同时间对Player进行初始化和准备。
无论是Renderer的onSurfaceCreated
回调还是Player的onPrepare
回调,都去调用同一个校验方法,当SurfaceTexture创建好且Player准备就绪时,设置Surface并开始播放。需要注意的是,onSurfaceCreated
的回调位于子线程,需要切换到主线程。
五、 反思总结
最终,博主采用了方案二来解决这个“祖传bug”。整个问题从系统分析到找到原因耗时不到一天,回顾过去的几个月,其实都是在做无用功。这个问题的整个处理过程,也颇有反思:
- 对于不熟悉的技术领域,应当尽可能一边快速学习一边分析问题,如果不迈出第一步,则永远无法拓宽技术栈
- 不轻易否定自己,尤其是在没有系统思考和查阅检索的情况下,这是逃避问题不负责任的表现
- 当问题卡壳时,借助图形辅助手段,梳理流程和思路,找准问题核心原因,避免在错误的方向上浪费时间精力
路漫漫其修远兮,这也算是职业生涯的成长过程吧。
基于GLSurfaceView的视频播放器偶现无画面的问题分析相关推荐
- rtsp协议_基于libVLC的视频播放器(支持RTSP协议)
VLC Media Player是一款优秀的播放器,大多数多媒体开发人员都使用过.VLC是一款开源的.跨平台的.可扩展的.多媒体播放器.流媒体服务器及框架,可播放大多数多媒体文件,以及DVD.音频CD ...
- 基于android的视频播放器,基于Android的视频播放器的设计与实现-.doc
基于Android的视频播放器的设计与实现- 基于Android的视频播放器的设计与实现 中 文 摘 要 如今,随着基于Android的智能手机的,.因此,设计一个基于android手机的多功能播放器 ...
- 50行代码实现的一个最简单的基于 DirectShow 的视频播放器
本文介绍一个最简单的基于 DirectShow 的视频播放器.该播放器对于初学者来说是十分有用的,它包含了使用 DirectShow 播放视频所有必备的函数. 直接贴上代码,具体代码的含义都写在注释中 ...
- 基于 IjkPlayer 的视频播放器
dkplayer 项目地址:dueeeke/dkplayer 简介:基于 IjkPlayer 的视频播放器,支持直播点播,悬浮窗播放,广告播放,边播边缓存:支持重力感应自动全屏:完美实现 ListV ...
- 最简单的基于libVLC的例子:最简单的基于libVLC的视频播放器
最简单的基于libVLC的例子文章列表: 最简单的基于libVLC的例子:最简单的基于libVLC的视频播放器 最简单的基于libVLC的例子:最简单的基于libVLC的视频播放器(图形界面版) 最简 ...
- 最简单的基于 DirectShow 的视频播放器
源程序下载链接:https://download.csdn.net/download/bmjhappy/11250669 50行代码实现的一个最简单的基于 DirectShow 的视频播放器 本文介绍 ...
- QT软件开发-基于FFMPEG设计视频播放器-解码音频(三)
QT软件开发-基于FFMPEG设计视频播放器-CPU软解视频(一) https://xiaolong.blog.csdn.net/article/details/126832537 QT软件开发-基于 ...
- QT软件开发-基于FFMPEG设计视频播放器-软解图像(一)
QT软件开发-基于FFMPEG设计视频播放器-CPU软解视频(一) https://xiaolong.blog.csdn.net/article/details/126832537 QT软件开发-基于 ...
- 简单小项目:基于Mplayer的视频播放器(Linux)
目录 0 视频演示 1 makefile 2 头文件 public.h show.h do.c 3 源文件.c 主函数main.c show.c do.c 4 项目资源下载 https://downl ...
最新文章
- Python Numpy 从文件中读取数据
- python打印进度条starting...done_python打印进度条-tqdm
- FioriLaunchpad.html的三个round-trip
- iOS - UIEvent事件及UIResponder响应者
- 如何在一年内从零基础到前端就业?
- Taproot激活时间将改由MTP计算
- 机器学习:神经网络的代价函数及反向传播算法
- linux系统之上搭建maven 之nexus服务篇
- Axure 进阶教程
- 知识图谱(knowledge graph)——概述
- 网站入侵工具之wscan使用详解
- SQL Server小数取整
- 数据结构和算法(五)--栈(Stack)
- ★★中国著名“正版”软体的网站★★
- 用计算机玩穿越火线,为什么每次玩穿越火线电脑就特别卡?
- 计算机主机的硬盘件,硬盘组件、硬盘减振装置以及计算机主机的制作方法
- LuaJIT Bytecode介绍
- php导出excel格式文件,PHP导入与导出Excel文件的方法
- 怎样从传销人员手中解救朋友
- 参加2009英雄会有感
热门文章
- 百度官方大拿分享:移动搜索下的新机遇新挑战
- The event in requested index is outdated and cleared (the requested history has been cleared ) 问题和修复
- 应试教育的死穴,恰在于堵死了孩子“犯错”的空间?
- org.eclipse.wst.common.component文件位置
- DotNetTextBox V3.0 所见即所得编辑器控件Ver3.2.4 Free(免费版)
- SAP学习第一篇——SAP模块介绍
- c#和python哪个效率高_【为什么C#排名和Python相差越来越大?】-看准网
- 筑基_C_5_对数组的二分查找
- 自媒体达人早已月入过万,为何同是自媒体人却依旧没收益
- C语言数组的初始化总结