Android使用OpenGL 播放视频
概述
在Android开发中,使用OpenGL ES来渲染视频是一种常见的需求,尤其是在需要实现自定义的视频播放界面或者视频特效时。结合MediaPlayer,我们可以实现一个功能强大的视频播放器。以下是一个简单的示例,展示如何在Android应用中使用OpenGL ES和MediaPlayer播放本地视频。
常规的视频播放方式有VideoView, MediaPlayer + SurfaceView / TextureView, 以TextureView为例, 它与OpenGL(GLSurfaceView)播放的一些比较:
TextureView的优缺点
- 优点 :
- 灵活性高:TextureView可以与其他View叠加使用,非常适合在复杂的视图层次结构中使用。
- 硬件加速支持:由于它在硬件加速层进行渲染,其性能也较优。
- 支持绘制操作:可以从其他线程更新内容,适合用于播放视频、显示实时特效等。
- 缺点 :
- 内存占用较高:TextureView的内部缓冲队列导致比SurfaceView使用更多的内存。
- 在5.0以前在主线程渲染:在5.0版本之前,TextureView在主线程渲染,可能会导致性能问题。
OpenGL的优缺点
- 优点 :
- 高度定制化:OpenGL提供低级别的图形渲染接口,允许开发者高度定制视频播放界面和特效。
- 性能优化:通过优化渲染代码,可以在一定程度上提高视频播放的效率和性能。
- 缺点 :
- 开发复杂度较高:使用OpenGL需要编写大量的底层代码,包括顶点着色器和片段着色器的编写,这增加了开发的复杂度和难度。
TextureView在灵活性、硬件加速支持和多线程更新方面具有优势,适合需要与其他视图交互的场景。而OpenGL则提供了更高的定制化程度,适合需要实现复杂图形效果的场景
实现
梳理收集来的参考代码, 实现视频播放效果如下:
GLVideoActivity.java
java
package com.ansondroider.sdktester.activity;
import android.media.MediaPlayer;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import com.ansondroider.acore.BaseActivity;
import com.ansondroider.sdktester.gl.GLVideoView;
import java.io.IOException;
public class GLVideoActivity extends BaseActivity {
MediaPlayer mmp;
GLVideoView glView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
glView = new GLVideoView(this);
setContentView(glView);
}
@Override
protected void onStart() {
super.onStart();
//postDelayed(new Runnable() {
// @Override
// public void run() {
mmp = new MediaPlayer();
try {
mmp.setDataSource("/sdcard/Movies/376463.mp4");
mmp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
glView.onVideoPrepared(mediaPlayer);
}
});
mmp.prepare();
} catch (IOException e) {
e.printStackTrace();
}
// }
//}, 100);
}
@Override
protected void onStop() {
super.onStop();
mmp.stop();
mmp.release();
}
}
GLVideoView.java
java
package com.ansondroider.sdktester.gl;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.media.MediaPlayer;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* @ProjectName: TheSimpllestplayer
* @Package: com.yw.thesimpllestplayer.renderview
* @ClassName: VideoDrawer
* @Description: 视频渲染器
* @Author: wei.yang
* @CreateDate: 2021/11/6 14:23
* @UpdateUser: 更新者:wei.yang
* @UpdateDate: 2021/11/6 14:23
* @UpdateRemark: 更新说明:
* @Version: 1.0
*/
public class GLVideoView extends GLSurfaceView {
final String TAG = "GLVideoView";
//顶点坐标,此处的坐标系是物体坐标系:中心店坐标是(0,0)
private float[] mVertexCoors = new float[]{
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
//纹理坐标系,中心坐标点为(0.5,0.5),上方向为t从0~1,右边方向为s,从0~1.刚好和计算器物理坐标系是反过来的。
private float[] mTextureCoors = new float[]{
0f, 1f,
1f, 1f,
0f, 0f,
1f, 0f
};
private String vertextShaderSource = "attribute vec4 aPosition;" +
"precision mediump float;" +
"uniform mat4 uMatrix;" +
"attribute vec2 aCoordinate;" +
"varying vec2 vCoordinate;" +
"attribute float alpha;" +
"varying float inAlpha;" +
"void main(){" +
"gl_Position = uMatrix*aPosition;" +
"vCoordinate = aCoordinate;" +
"inAlpha = alpha;" +
"}";
private String fragmentShaderSource = "#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;" +
"varying vec2 vCoordinate;" +
"varying float inAlpha;" +
"uniform samplerExternalOES uTexture;" +
"void main() {" +
"vec4 color = texture2D(uTexture, vCoordinate);" +
"gl_FragColor = vec4(color.r, color.g, color.b, inAlpha);" +
"}";
//视频宽高
private int mVideoWidth = -1;
private int mVideoHeight = -1;
//物理屏幕的宽高
private int mWorldWidth = -1;
private int mWorldHeight = -1;
//纹理ID
private int mTextureId = -1;
//定义SurfaceTexture 为显示视频做准备;
private SurfaceTexture mSurfaceTexture = null;
// 定义OpenGL 程序ID
private int mProgram = -1;
//矩阵变换接受者(shader中)
private int mVertexMatrixHandler = -1;
//顶点坐标接收者
private int mVertexPosHandler = -1;
//纹理坐标接受者
private int mTexturePosHandler = -1;
//纹理接受者
private int mTextureHandler = -1;
//半透明值接受者
private int mAlphaHandler = -1;
//顶点缓冲
private FloatBuffer mVertexBuffer = null;
//纹理缓冲
private FloatBuffer mTextureBuffer = null;
//矩阵
private float[] mMatrix = null;
//透明度
private float mAlpha = 1f;
//旋转角度
private float mWidthRatio = 1f;
private float mHeightRatio = 1f;
private int floatLength = 16;
public GLVideoView(Context context) {
super(context);
init();
}
public GLVideoView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init(){
setEGLContextClientVersion(2);
setRenderer(new VideoRender());
setRenderMode(RENDERMODE_WHEN_DIRTY);
initPos();
}
/**
* 初始化顶点坐标
*/
private void initPos() {
ByteBuffer vByteBuffer = ByteBuffer.allocateDirect(mVertexCoors.length * 4);
vByteBuffer.order(ByteOrder.nativeOrder());
//将坐标转换为floatbuffer,用以传给opengl程序
mVertexBuffer = vByteBuffer.asFloatBuffer();
mVertexBuffer.put(mVertexCoors);
mVertexBuffer.position(0);
ByteBuffer tByteBuffer = ByteBuffer.allocateDirect(mTextureCoors.length * 4);
tByteBuffer.order(ByteOrder.nativeOrder());
mTextureBuffer = tByteBuffer.asFloatBuffer();
mTextureBuffer.put(mTextureCoors);
mTextureBuffer.position(0);
}
/**
* 初始化矩阵变换,主要是防止视频拉伸变形
*/
private void initDefMatrix() {
//Log.d(TAG, "initDefMatrix");
if (mMatrix != null) return;
if (mVideoWidth != -1 && mVideoHeight != -1 &&
mWorldWidth != -1 && mWorldHeight != -1) {
mMatrix = new float[floatLength];
float[] prjMatrix = new float[floatLength];
float originRatio = mVideoWidth / (float) mVideoHeight;
float worldRatio = mWorldWidth / (float) mWorldHeight;
if (mWorldWidth > mWorldHeight) {
if (originRatio > worldRatio) {
mHeightRatio = originRatio / worldRatio;
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
);
} else {// 原始比例小于窗口比例,缩放高度度会导致高度超出,因此,高度以窗口为准,缩放宽度
mWidthRatio = worldRatio / originRatio;
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
);
}
} else {
if (originRatio > worldRatio) {
mHeightRatio = originRatio / worldRatio;
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
);
} else {// 原始比例小于窗口比例,缩放高度会导致高度超出,因此,高度以窗口为准,缩放宽度
mWidthRatio = worldRatio / originRatio;
Matrix.orthoM(
prjMatrix, 0,
-mWidthRatio, mWidthRatio,
-mHeightRatio, mHeightRatio,
3f, 5f
);
}
}
//设置相机位置
float[] viewMatrix = new float[floatLength];
Matrix.setLookAtM(
viewMatrix, 0,
0f, 0f, 5.0f,
0f, 0f, 0f,
0f, 1.0f, 0f
);
//计算变换矩阵
Matrix.multiplyMM(mMatrix, 0, prjMatrix, 0, viewMatrix, 0);
}
}
private Surface mSurface = null;
MediaPlayer mMediaPlayer;
public void onVideoPrepared(MediaPlayer mp){
Log.d(TAG, "onVideoPrepared");
mMediaPlayer = mp;
if(mSurfaceTexture != null) {
int videoWidth = mMediaPlayer.getVideoWidth();
int videoHeight = mMediaPlayer.getVideoHeight();
setVideoSize(videoWidth, videoHeight);
mSurface = new Surface(mSurfaceTexture);
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.start();
}
}
private void setVideoSize(int videoWidth, int videoHeight) {
Log.d(TAG, "setVideoSize " + videoWidth + "x" + videoHeight);
mVideoWidth = videoWidth;
mVideoHeight = videoHeight;
}
private void setWorldSize(int worldWidth, int worldHeight) {
mWorldWidth = worldWidth;
mWorldHeight = worldHeight;
}
@Override
public void setAlpha(float alpha) {
super.setAlpha(alpha);
mAlpha = alpha;
}
private SurfaceTexture getSurfaceTexture() {
return mSurfaceTexture;
}
/**
* 创建并使用opengles程序
*/
private void createGLPrg() {
if (mProgram == -1) {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertextShaderSource);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource);
//创建programe陈谷
mProgram = GLES20.glCreateProgram();
//将顶点着色器加入程序
GLES20.glAttachShader(mProgram, vertexShader);
//将片元着色器加入程序
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
//从程序中获取句柄
mVertexMatrixHandler = GLES20.glGetUniformLocation(mProgram, "uMatrix");
mVertexPosHandler = GLES20.glGetAttribLocation(mProgram, "aPosition");
mTextureHandler = GLES20.glGetUniformLocation(mProgram, "uTexture");
mTexturePosHandler = GLES20.glGetAttribLocation(mProgram, "aCoordinate");
mAlphaHandler = GLES20.glGetAttribLocation(mProgram, "alpha");
}
//使用opengl程序
GLES20.glUseProgram(mProgram);
}
/**
* 激活并绑定纹理单元
*/
private void activateTexture() {
//激活指定纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//绑定纹理ID到纹理单元
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
//将激活并绑定的纹理id传递到着色器里面
GLES20.glUniform1i(mTextureHandler, 0);
//配置边缘过滤参数
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR
);
GLES20.glTexParameterf(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR
);
//配置s轴和t轴的方式
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_CLAMP_TO_EDGE
);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE
);
}
private void updateTexture() {
mSurfaceTexture.updateTexImage();
}
/**
* 加载着色器
*
* @param shaderType 着色器类型
* @param shaderCode 着色器代码
* @return
*/
private int loadShader(int shaderType, String shaderCode) {
//根据着色器类型创建着色器
int shader = GLES20.glCreateShader(shaderType);
//将着色其代码加入到着色器
GLES20.glShaderSource(shader, shaderCode);
//编译zhuoseq
GLES20.glCompileShader(shader);
return shader;
}
/**
* 开始绘制渲染
*/
public void doDraw() {
if(mMatrix == null)return;
//启用顶点坐标句柄
GLES20.glEnableVertexAttribArray(mVertexPosHandler);
GLES20.glEnableVertexAttribArray(mTexturePosHandler);
GLES20.glUniformMatrix4fv(mVertexMatrixHandler, 1, false, mMatrix, 0);
//设置着色器参数, 第二个参数表示一个顶点包含的数据数量,这里为xy,所以为2
GLES20.glVertexAttribPointer(mVertexPosHandler, 2, GLES20.GL_FLOAT, false, 0, mVertexBuffer);
GLES20.glVertexAttribPointer(
mTexturePosHandler,
2,
GLES20.GL_FLOAT,
false,
0,
mTextureBuffer
);
GLES20.glVertexAttrib1f(mAlphaHandler, mAlpha);
//开始绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mVertexCoors.length / 2);
}
@Override
protected void onDetachedFromWindow() {
Log.d(TAG, "onDetachedFromWindow");
super.onDetachedFromWindow();
GLES20.glDisableVertexAttribArray(mVertexPosHandler);
GLES20.glDisableVertexAttribArray(mTexturePosHandler);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glDeleteTextures(1, new int[]{mTextureId}, 0);
GLES20.glDeleteProgram(mProgram);
if(mMediaPlayer != null){
//mMediaPlayer.setSurface(null);
mMediaPlayer.release();
mSurface.release();
}
}
public void translate(float dx, float dy) {
Matrix.translateM(mMatrix, 0, dx * mWidthRatio * 2, -dy * mHeightRatio * 2, 0f);
}
public void scale(float sx, float sy) {
Matrix.scaleM(mMatrix, 0, sx, sy, 1f);
mWidthRatio /= sx;
mHeightRatio /= sy;
}
public class VideoRender implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d(TAG, "onSurfaceCreated");
GLES20.glClearColor(0f, 0f, 0f, 0f);
//开启混合,即半透明
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
int[] textureIds = new int[1];
GLES20.glGenTextures(1, textureIds, 0);
mTextureId = textureIds[0];
//根据textureId初始化一个SurfaceTexture
mSurfaceTexture = new SurfaceTexture(mTextureId);
mSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
requestRender();
}
});
if(mMediaPlayer != null){
onVideoPrepared(mMediaPlayer);
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d(TAG, "onSurfaceChanged");
GLES20.glViewport(0, 0, width, height);
setWorldSize(width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
Log.d(TAG, "onDrawFrame");
if(mMediaPlayer == null || !mMediaPlayer.isPlaying())return;
//清除颜色缓冲和深度缓冲
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
if (mTextureId != -1) {
initDefMatrix();
//2/创建、编译、启动opengles着色器
createGLPrg();
//3.激活并绑定纹理单元
activateTexture();
//4.绑定图元到纹理单元
updateTexture();
//5.开始绘制渲染
doDraw();
}
}
}
}
复杂图形效果的场景
普通的视频播放方式很难实现 曲面 百叶窗 这类的效果, 如:
而使用OpenGL播放, 只需要调整下顶点和纹理的坐标即可:
java
//顶点坐标,此处的坐标系是物体坐标系:中心店坐标是(0,0)
private float[] mVertexCoors = new float[]{
-1f, 1f,
-1f, -1f,
0, 0.5f,
0, -0.5f,
1f, 1f,
1f, -1f,
};
//纹理坐标系,中心坐标点为(0.5,0.5),上方向为t从0~1,右边方向为s,从0~1.刚好和计算器物理坐标系是反过来的。
private float[] mTextureCoors = new float[]{
0f, 0f,
0f, 1f,
0.5f, 0,
0.5f, 1f,
1f, 0f,
1f, 1f
};