前言
之前的博客涉及的很多都是UI和质量相关的文章,我们基本涉及了很多方面,最近一篇是基于Jetpack Compose实现的ScrollConnection基准型布局,大家有兴趣可以看看。
当然,本篇我们继续做播放相关的文章,实际上在我之前也出了好多篇播放相关的文章,只是不在掘金平台,后续有机会我们转移过来。
关于GLSurfaceView
对于音视频的开发者而言,对于GLSurfaceView实际上既熟悉又陌生,主要原因是在Android体系中,我本身是单独绘制的且能和SurfaceFlinger直接通信,GLSurfaceView他和SurfaceView存在继承关系,但渲染方面区别很大,特别是具体用法上,GLSurfaceView并不能直接使用SurfaceHolder中的Surface,因为surface会被EglSurface引用,如果要实现渲染,需要单独创建纹理。
当然,相似的地方包括生命周期都是一致的,这也是意味着,只要Activity在后台,那么GLSurfaceView的绘制功能也会受到相应的影响。比如surface销毁。
但是,SurfaceView的Surface销毁并不意味着opengl纹理销毁,因此视频渲染仍然会继续,并且SurfaceView的Surface重新创建时,关联EglSurface之后画面继续。这和SurfaceView的调用逻辑完全不同,后者Surface的失效一般会引发MediaCodec解码异常。
opengl渲染有很多好处,利用GLSurfaceView不仅仅可以实现特效和水印,而且可以解决SurfaceView动画不支持的问题、以及feed流滑动引发画面漂移的问题。
本篇对于开发者而言,其实是入门篇,不过这里我们先需要简单理解下GLSurfaceView。
EGL环境
实际上,在Android系统中使用open gl es渲染并不意味着一定要使用GLSurfaceView,对于SurfaceView、TextureView同样也可以使用open gl,即便是普通View,我们依然可以拿到rgb buffer转换的Bitmap进行渲染。
但是,open gl es需要渲染环境的支持,目前Android平台是EGL,EGL可以看画布相关的环境,而open gl可以看做画笔,当然还有其他平台的另类环境也同样支持 open gl渲染。
Surface创建和渲染问题
GLSurfaceView和SurfaceView有很多不同的地方,最大的地方无疑是Surface的创建。在Android系统中Surface是具有双buffer的绘制通道。创建Surface的方法也很多,主要分为下面几个
- SurfaceView:内部创建
- TextureView: 使用surfaceTexture创建
- MediaCodec: 提供Surface,实现录制
- ImageReader: YUV数据读取,但要注意的是,部分设备上需要设置MediaCodec的色彩空间COLOR_FormatYUV420Flexible,否则拿不到数据。
- open gl es: 生成纹理创建Surface
最简单的录屏
Surface我们之前在做《黑客代码雨》和《烟花效果中》都使用过,其特点还有就是支持异步绘制。当然,看到一些人想做录屏的效果,很多人都去想着使用MediaProjection,实际上录屏很简单,只需要我们获取到DecorView,然后使用DecorView#draw方法UI到Bitmap上.
private void captureActivityWindow(Activity activity,String fileName) {
try {
View decorView = activity.getWindow().getDecorView();
int width = decorView.getWidth();
int height = decorView.getHeight();
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); //
Canvas canvas = new Canvas(bitmap);
decorView.draw(canvas);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 480, 270, true);
File file = new File(MediaType.LOG.getPath(), fileName);
if (file.exists()) {
file.delete();
}
saveBitmap(scaledBitmap, file);
scaledBitmap.recycle();
bitmap.recycle();
}catch (Throwable e){
e.printStackTrace();
}
}
然后绘制到MediaCodec#createInputSurface()上即可
Canvas canvas = surface.lockCanvas(null);
//调用lockHardwareCanvas会后台再回来会出现画面卡主,这里我们使用lockCanvas比较保险
canvas.drawBitmap(bitmap,0,0,null);
surface.unlockCanvasAndPost(canvas);
bitmap.recycle();
画面控制在24帧即可,将上面的代码定时1000/24ms执行即可。
Surface使用异常
Surface 最容易触发的两大错误分别是失效绘制和重复引用
错误码如下:
- 失效后渲染: -19
- 被多个播放器同时引用: -22
如下是Surface失效导致的问题,这种情况会导致播放器渲染错误
09-09 10:52:21.324 23846 11142 D SurfaceUtils: disconnecting from surface 0xab257008, reason disconnectFromSurface
09-09 10:52:21.324 1385 1661 I TrafficMonitor: update:rxPkts:78,txPkts:53,rxBytes:31585,txBytes:11069
09-09 10:52:21.324 23846 11142 E SurfaceUtils: Failed to disconnect from surface 0xab257008, err -19
09-09 10:52:21.324 23846 11142 W MediaCodec: nativeWindowDisconnect returned an error: No such device (-19)
遗憾的是以上错误码只能从系统日志中看到,所以必要时需要拦截native层日志才能获取到错误码。
SurfaceView 遮挡的问题
SurfaceView遮挡问题是开发者的重灾区,谷歌官方似乎也没有很好的办法,逼急了他会建议你使用TextureView,但是对于太卡顿的设备,这种无异于饮鸩止渴。
SurfaceView遮挡SurfaceView解决方法
如果UI上存在多个SurfaceView,SurfaceView与SurfaceView如果存在遮盖,这种方式相对而言还是比较好处理的,使用如下方式即可
setZOrderMediaOverlay(true);
##setZOrderOnTop(true); //一般不建议
SurfaceView 遮挡普通View
但是如果SurfaceView和普通View出现遮盖问题,特别是明明普通的View在SurfaceView上面,却展示不出来,这个时候使用上面的方式是无效的,那么怎么处理呢?
SurfaceView遮挡上方View
其实方法很简单,就是在surfaceCreated被调用之前,设置SurfaceView的背景,就能解决遮住问题
this.setBackgroundColor(Color.TRANSPARENT); //修复遮住上层View的问题
SurfaceView遮挡下方View
但是,上面的办法并不总是有用,这种只能解决在SurfaceView布局上面的普通View被遮挡的问题,一些实际开发中往往存在很奇怪的现象,明明SurfaceView大小为200x200,但是会导致SurfaceView下方普通View,甚至不重叠的普通View也无法展示。
这种情况是最复杂的,也是更多人会遇到的,甚至有些人会束手无策,这种场景处理不当,导致兴致勃勃的开发者SurfaceView优化性能时止步不前。
那么,怎么解决这种问题呢?
其实,方法是有的,但是需要按原则优化:
- 保持SurfaceView在View树种层级更加靠近底部,也就是说SurfaceView越接近根布局越好
- 保持SurfaceView在同一个布局中的顺序索引越小越好,也就是尽可能让SurfaceView优先被addView到布局中
其实就一句话:尽量让普通View在SurfaceView上方,SurfaceView尽量在下方
SurfaceView 黑屏问题
实际上,这部分原因比较复杂,但是有一种可行的方案
解决方法:
- this.setBackgroundColor(Color.TRANSPARENT);
- surfaceCreated 绘制Surface,将其背景使用Canvas清空颜色
注意:不保证适用所有场景
关于ExoPlayer
ExoPlayer 作为Android官方力推的播放器,其兼容性和可扩展性优势很大。MediaPlayer作为系统播放器,其局限性在之前的文章中我们说过,MediaPlayer作为C/S架构,我们仅仅能处理Client层的问题,至于Server属于系统层代码,Android官方是不会允许你修改Server的,因而MediaPlayer无法扩展,同时兼容性稍差。不过好处是,由于核心逻辑运行在系统进程,可以帮助应用层app减少内存占用,相当于薅羊毛了。
下面是简单的用法
DefaultRenderersFactory defaultRenderersFactory = new DefaultRenderersFactory(getApplicationContext());
defaultRenderersFactory.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);
ExoPlayer player = new ExoPlayer.Builder(getApplicationContext()).setRenderersFactory(defaultRenderersFactory).build();
player.setRepeatMode(Player.REPEAT_MODE_ALL);
player.setMediaSource(mediaSource);
player.prepare();
player.setVideoSurface(surface);
用法
GLSurfaceView的Renderer
GLSurfaceView的使用是存在一些难度的,相比SurfaceView,GLSurfaceView使用Renderer
android.opengl.GLSurfaceView#setRenderer
下面我们以官方demo为例,做一些说明。
private final class VideoRenderer implements GLSurfaceView.Renderer, VideoFrameMetadataListener {
private final VideoProcessor videoProcessor;
private final AtomicBoolean frameAvailable;
private final TimedValueQueue<Long> sampleTimestampQueue;
private final float[] transformMatrix;
private int texture;
@Nullable private SurfaceTexture surfaceTexture;
private boolean initialized;
private int width;
private int height;
private long frameTimestampUs;
public VideoRenderer(VideoProcessor videoProcessor) {
this.videoProcessor = videoProcessor;
frameAvailable = new AtomicBoolean();
sampleTimestampQueue = new TimedValueQueue<>();
width = -1;
height = -1;
frameTimestampUs = C.TIME_UNSET;
transformMatrix = new float[16]; //转换矩阵 4x4
}
@Override
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
try {
texture = GlUtil.createExternalTexture();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to create an external texture", e);
}
surfaceTexture = new SurfaceTexture(texture);
surfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> {
frameAvailable.set(true);
requestRender();
});
onSurfaceTextureAvailable(surfaceTexture);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
this.width = width;
this.height = height;
}
//下面方法负责绘制每一帧画面
@Override
public void onDrawFrame(GL10 gl) {
if (videoProcessor == null) {
return;
}
if (!initialized) {
videoProcessor.initialize();
initialized = true;
}
if (width != -1 && height != -1) {
videoProcessor.setSurfaceSize(width, height);
width = -1;
height = -1;
}
if (frameAvailable.compareAndSet(true, false)) {
SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture);
surfaceTexture.updateTexImage();
long lastFrameTimestampNs = surfaceTexture.getTimestamp();
@Nullable Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
if (frameTimestampUs != null) {
this.frameTimestampUs = frameTimestampUs;
}
surfaceTexture.getTransformMatrix(transformMatrix);
}
videoProcessor.draw(texture, frameTimestampUs, transformMatrix);
}
@Override
public void onVideoFrameAboutToBeRendered(
long presentationTimeUs,
long releaseTimeNs,
Format format,
@Nullable MediaFormat mediaFormat) {
sampleTimestampQueue.add(releaseTimeNs, presentationTimeUs);
}
}
在上面的代码中,我们要注意的是SurfaceTexture的创建
texture = GlUtil.createExternalTexture();
其最终代码是 这里创建了纹理,用于创建SurfaceTexture,但是我们绑定OES,为后续Shader脚本中的Sampler2D服务。
private static int generateTexture() throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] texId = new int[1];
GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0);
checkGlError();
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);
return texId[0];
}
初始化EGL
下面方法用于初始化EGL环境,设置版本号为2,其次设置Renderer,但是ExoPayer的demo还做了一些环境修改,实际上使用Android平台默认的也行,不过下面的修改有一些问题需要注意一下,其中EGL_PROTECTED_CONTENT_EXT会导致我们无法使用glReadPixeles读取数据。
public VideoProcessingGLSurfaceView(
Context context, boolean requireSecureContext, VideoProcessor videoProcessor) {
super(context);
renderer = new VideoRenderer(videoProcessor);
mainHandler = new Handler();
setEGLContextClientVersion(2);
setEGLConfigChooser(
/* redSize= */ 8,
/* greenSize= */ 8,
/* blueSize= */ 8,
/* alphaSize= */ 8,
/* depthSize= */ 0,
/* stencilSize= */ 0);
setEGLContextFactory(
new EGLContextFactory() {
@Override
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
int[] glAttributes;
if (requireSecureContext) {
glAttributes =
new int[] {
EGL14.EGL_CONTEXT_CLIENT_VERSION,
2,
EGL_PROTECTED_CONTENT_EXT,
EGL14.EGL_TRUE,
EGL14.EGL_NONE
};
} else {
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
}
return egl.eglCreateContext(
display, eglConfig, /* share_context= */ EGL10.EGL_NO_CONTEXT, glAttributes);
}
@Override
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
egl.eglDestroyContext(display, context);
}
});
setEGLWindowSurfaceFactory(
new EGLWindowSurfaceFactory() {
@Override
public EGLSurface createWindowSurface(
EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
int[] attribsList =
requireSecureContext
? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}
: new int[] {EGL10.EGL_NONE};
return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
}
@Override
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
egl.eglDestroySurface(display, surface);
}
});
setRenderer(renderer);
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); //手动刷新调用onDrawFrame而不是自动刷新
}
上面的代码中,我们尤其要注意EGLSurface的创建,没有EGLSurface降到纹理无法渲染,我们可以将EGLSurface看做egl环境的画布。这里相当于将SurfaceView的Surface和EGL环境绑定。
public EGLSurface createWindowSurface(
EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) {
int[] attribsList =
requireSecureContext
? new int[] {EGL_PROTECTED_CONTENT_EXT, EGL14.EGL_TRUE, EGL10.EGL_NONE}
: new int[] {EGL10.EGL_NONE};
return egl.eglCreateWindowSurface(display, config, nativeWindow, attribsList);
}
至于eglCreateWindowSurface,其实现主要是为了SurfaceView,具体代码如下
public static EGLSurface eglCreateWindowSurface(EGLDisplay dpy,
EGLConfig config,
Object win,
int[] attrib_list,
int offset
){
Surface sur = null;
if (win instanceof SurfaceView) {
SurfaceView surfaceView = (SurfaceView)win;
sur = surfaceView.getHolder().getSurface();
} else if (win instanceof SurfaceHolder) {
SurfaceHolder holder = (SurfaceHolder)win;
sur = holder.getSurface();
} else if (win instanceof Surface) {
sur = (Surface) win;
}
EGLSurface surface;
if (sur != null) {
surface = _eglCreateWindowSurface(dpy, config, sur, attrib_list, offset);
} else if (win instanceof SurfaceTexture) {
surface = _eglCreateWindowSurfaceTexture(dpy, config,
win, attrib_list, offset);
} else {
throw new java.lang.UnsupportedOperationException(
"eglCreateWindowSurface() can only be called with an instance of " +
"Surface, SurfaceView, SurfaceTexture or SurfaceHolder at the moment, " +
"this will be fixed later.");
}
return surface;
}
绘制逻辑
官方demo提供的是一个水印效果,代码量稍微有些大,因此我代码里注释一下。我们先看Shader,在open gl中环境中,从渲染到着色有多个步骤,这个在我们之前的文章中也说过,这里就不在赘述了。
顶点Shader
attribute vec4 aFramePosition;
attribute vec4 aTexCoords;
uniform mat4 uTexTransform; //矩阵变换
varying vec2 vTexCoords; //输出到片断Shader中
void main() {
gl_Position = aFramePosition; //定点坐标
vTexCoords = (uTexTransform * aTexCoords).xy;
}
片段Shader
#extension GL_OES_EGL_image_external : require
precision mediump float;
// External texture containing video decoder output.
uniform samplerExternalOES uTexSampler0;
// Texture containing the overlap bitmap.
uniform sampler2D uTexSampler1;
// Horizontal scaling factor for the overlap bitmap.
uniform float uScaleX;
// Vertical scaling factory for the overlap bitmap.
uniform float uScaleY;
varying vec2 vTexCoords;
void main() {
vec4 videoColor = texture2D(uTexSampler0, vTexCoords); //视频帧坐标
vec4 overlayColor = texture2D(uTexSampler1,
vec2(vTexCoords.x * uScaleX,
vTexCoords.y * uScaleY)); // bitmap 坐标
// Blend the video decoder output and the overlay bitmap.
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a; //最终纹理结果
}
注意,视频视同的纹理采样变量类型是samplerExternalOES,而图片使用sampler2D
上面是主要了两个Shader,主要在GPU中执行和计算,第一个负责计算顶点坐标,第二个用于顶点坐标着色。
/* package */ final class BitmapOverlayVideoProcessor
implements VideoProcessingGLSurfaceView.VideoProcessor {
private static final String TAG = "BitmapOverlayVP";
private static final int OVERLAY_WIDTH = 512; // 水印长度
private static final int OVERLAY_HEIGHT = 256; //水印高度
private final Context context;
private final Paint paint;
private final int[] textures;
private final Bitmap overlayBitmap; // 其他图片
private final Bitmap logoBitmap; //logo
private final Canvas overlayCanvas;
private @MonotonicNonNull GlProgram program;
private float bitmapScaleX;
private float bitmapScaleY;
public BitmapOverlayVideoProcessor(Context context) {
this.context = context.getApplicationContext();
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
textures = new int[1];
overlayBitmap = Bitmap.createBitmap(OVERLAY_WIDTH, OVERLAY_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
try {
logoBitmap =
((BitmapDrawable)
context.getPackageManager().getApplicationIcon(context.getPackageName()))
.getBitmap();
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
}
@Override
public void initialize() {
try {
program =
new GlProgram(
context,
/* vertexShaderFilePath= */ "bitmap_overlay_video_processor_vertex.glsl",
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to initialize the shader program", e);
return;
}
program.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
program.setBufferAttribute(
"aTexCoords",
GlUtil.getTextureCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
GLES20.glGenTextures(1, textures, 0); //创建图片纹理
/**
*下面载入overlayBitmap,并且载入双线性过滤,要注意的是,图片和视频的线性过滤是不一样的
*/
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
}
@Override
public void setSurfaceSize(int width, int height) {
bitmapScaleX = (float) width / OVERLAY_WIDTH;
bitmapScaleY = (float) height / OVERLAY_HEIGHT;
}
@Override
public void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix) {
//绘制Bitmap
// Draw to the canvas and store it in a texture.
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(logoBitmap, /* left= */ 32, /* top= */ 32, paint);
overlayCanvas.drawText(text, /* x= */ 200, /* y= */ 130, paint);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); //绑定Bitmap纹理
GLUtils.texSubImage2D(
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
try {
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to populate the texture", e);
}
// Run the shader program.
GlProgram program = checkNotNull(this.program);
//绑定图片纹理
program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* texUnitIndex= */ 0);
//绑定视频纹理
program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* texUnitIndex= */ 1);
program.setFloatUniform("uScaleX", bitmapScaleX);
program.setFloatUniform("uScaleY", bitmapScaleY);
program.setFloatsUniform("uTexTransform", transformMatrix);
try {
program.bindAttributesAndUniforms();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to update the shader program", e);
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
try {
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to draw a frame", e);
}
}
@Override
public void release() {
if (program != null) {
try {
program.delete();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to delete the shader program", e);
}
}
}
}
上面的代码中,我们要注意图片的纹理和视频的纹理存在一些差别,主要如下
- 过滤器: 视频使用GL_TEXTURE_EXTERNAL_OES,而图片使用GL_TEXTURE_2D
- 纹理载入:图片需要使用texSubImage2D,而视频不需要
- 缩放:上面的逻辑会对图片纹理进行缩放,便于计算gl_FragColor
以上就是完整的调用流程
SurfaceTexture 宽高为0的问题
上面的代码实现播放视频完全是没有问题的,但是如果是下面则存在问题
GLSurface->surfaceTexure->EGLSurface->EglSwappers
然而,当我们使用eglCreatePbufferSurface 离屏渲染,将GLSurfaceView的SurfaceTexture使用createWindowSurface绑定EGLSurface时,就会发现EGLSurface大小为0,主要原因是SurfaceTexture大小是不对的,但是使用SurfaceView和TextureView的完全正常播放?
原因是本篇我们的SurfaceTexture没有设置大小,这个可以使用EGL14.queryEglSurface去查询宽高,结果是0。
解决方法:
设置大小即可解决此问题
this.surfaceTexture.setDefaultBufferSize(width,height);
总结
实际上本篇主要还是GLSurfaceView的用法,对于这部分而言,我们能看到open gl其本身不是面向对象的,因此一些处理流程的可视化和关联性很难去理解,但是好处是open gl是和线程、EGL Context一一对应的,因此我们从这方面去理解要简单的多。
好了,本篇就到这里,希望对你有所帮助。