Android MediaPlayer + GLSurfaceView 播放视频

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
    };

参考

  1. 10.GLSurfaceView+MediaPlayer播放视频.md
  2. 【Android 音视频开发打怪升级:OpenGL渲染视频画面篇】二、使用OpenGL渲染视频画面
  3. Android 最简单的视频播放器之OpenGL ES视频渲染工具封装(三)
相关推荐
阿巴斯甜7 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker8 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95279 小时前
Andorid Google 登录接入文档
android
黄林晴10 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android