视频画中画实际就是两张纹理叠加,其中之一为前景,另外一个为背景。前景通常是一个小窗口叠加在背景上。

在 Android 应用中,通常视频源来自 Camera 或者其他经过硬解码(MediaCodec)而来的视频流。我们知道不论是 Camera 还是硬解码后的视频流都是可以直接送显到 Surface 上的(存在相应的 API 直接调用),所以可以通过 OpenGL ES 生成对应的纹理,然后包装成 Surface 交给对应的视频源去输出。需要注意的是,片段着色器中需要声明为 GL_OES_EGL_image_external,也就是外部扩展纹理。

有了以上方案基础就不难实现相应的代码了。首先来开发前景和背景纹理使用 Drawer。前景可经过一定的矩阵变换(缩小、平移),可以改变其位置和大小。

/**
 * author : liuhongwei
 * e-mail : 
 * date   : 2021/7/22 16:28
 * desc   : OES绘制器
 * version: 1.0
 */
public class OesDrawer {
    private static final String TAG = "OesDrawer";

    private final float[] mVertexCoors = new float[]{
            -1.0F, -1.0F,
            1.0F, -1.0F,
            -1.0F, 1.0F,
            1.0F, 1.0F};
    private final float[] mTextureCoors = new float[]{
            0.0F, 1.0F,
            1.0F, 1.0F,
            0.0F, 0.0F,
            1.0F, 0.0F};

    private int mTextureId = -1;
    private SurfaceTexture mSurfaceTexture;
    private int mProgram = -1;
    private int mVertexMatrixHandler = -1;
    private int mVertexPosHandler = -1;
    private int mTexturePosHandler = -1;
    private int mTextureHandler = -1;
    private int mAlphaHandler = -1;
    private FloatBuffer mVertexBuffer;
    private FloatBuffer mTextureBuffer;
    private final float[] mMatrix = new float[16];
    private float mAlpha = 1.0F;

    public OesDrawer(){
        initPos();
        Matrix.setIdentityM(mMatrix, 0);
    }

    public void scale(float sx, float sy) {
        Matrix.scaleM(mMatrix, 0, sx, sy, 1f);
    }

    public void translate(float x, float y, float z) {
        Matrix.translateM(mMatrix, 0, x, y, z);
    }

    private void initPos() {
        ByteBuffer bb = ByteBuffer.allocateDirect(mVertexCoors.length * 4);
        bb.order(ByteOrder.nativeOrder());
        //将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序
        mVertexBuffer = bb.asFloatBuffer();
        mVertexBuffer.put(mVertexCoors);
        mVertexBuffer.position(0);

        ByteBuffer cc = ByteBuffer.allocateDirect(mTextureCoors.length * 4);
        cc.order(ByteOrder.nativeOrder());
        mTextureBuffer = cc.asFloatBuffer();
        mTextureBuffer.put(mTextureCoors);
        mTextureBuffer.position(0);
    }

    public void setAlpha(float alpha) {
        this.mAlpha = alpha;
    }

    public void setTextureID(int id) {
        mTextureId = id;
        mSurfaceTexture = new SurfaceTexture(id);
    }

    public SurfaceTexture getSurfaceTexture() {
        return mSurfaceTexture;
    }

    private void createGLPrg() {
        if (mProgram == -1) {
            int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, getVertexShader());
            int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, getFragmentShader());
            //创建OpenGL ES程序,注意:需要在OpenGL渲染线程中创建,否则无法渲染
            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);
        //将激活的纹理单元传递到着色器里面
        GLES20.glUniform1i(mTextureHandler, 0);
        //配置边缘过渡参数
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR * 1.0f);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR * 1.0f);
        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);
    }

    public void updateTexture() {
        if (null != mSurfaceTexture) {
            mSurfaceTexture.updateTexImage();
        }
    }

    public void doDraw() {
        if (mTextureId != -1) {
            createGLPrg();
            activateTexture();
            //启用顶点的句柄
            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, 4);
        }
    }

    public void release() {
        GLES20.glDisableVertexAttribArray(mVertexPosHandler);
        GLES20.glDisableVertexAttribArray(mTexturePosHandler);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        GLES20.glDeleteTextures(1, new int[]{mTextureId}, 0);
        GLES20.glDeleteProgram(mProgram);
    }

    private String getVertexShader() {
        return "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 getFragmentShader() {
        //一定要加换行"\n",否则会和下一行的precision混在一起,导致编译出错
        return "#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 loadShader(int type, String shaderCode) {
        //根据type创建顶点着色器或者片元着色器
        int shader = GLES20.glCreateShader(type);
        //将资源加入到着色器中,并编译
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }
}

前景和背景使用的 Drawer 开发好之后,需要更进一步,也就是将两张纹理直接绘制到 FBO,如此可以更加灵活的定制,比如还要贴一个水印 logo 都可以在 FBO Drawer 中处理。

/**
 * author : liuhongwei
 * e-mail : 
 * date   : 2021/7/26 14:59
 * desc   : FBO绘制器
 * version: 1.0
 */
public class FBODrawer {
    private static final String TAG = "FBODrawer";
    //单画面或PIP模式
    private Mode mMode;
    private int[] mFrameBuffers;
    private int[] mFBOTextures;

    private int mFrameWidth;
    private int mFrameHeight;

    private OesDrawer mBgOesDrawer;
    private OesDrawer mFgOesDrawer;

    public FBODrawer(){
        mBgOesDrawer = new OesDrawer();
        mFgOesDrawer = new OesDrawer();
    }

    public SurfaceTexture getBgSurfaceTexture() {
        return mBgOesDrawer.getSurfaceTexture();
    }

    public SurfaceTexture getFgSurfaceTexture() {
        return mFgOesDrawer.getSurfaceTexture();
    }

    private void loadFBO(int frameWidth, int frameHeight) {
        mFrameWidth = frameWidth;
        mFrameHeight = frameHeight;

        if (mFrameBuffers != null) {
            destroyFrameBuffers();
        }
        //创建FrameBuffer
        mFrameBuffers = new int[1];
        GLES20.glGenFramebuffers(mFrameBuffers.length, mFrameBuffers, 0);
        //创建FBO中的纹理
        mFBOTextures = new int[1];
        GlesUtils.genTextures(mFBOTextures);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mFBOTextures[0]);
        Log.d(TAG, "mFBOTextures[0]=" + mFBOTextures[0]);
        //指定FBO纹理的输出图像的格式 RGBA
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, frameWidth, frameHeight,
                0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);

        //将fbo绑定到2d的纹理上
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
                GLES20.GL_TEXTURE_2D, mFBOTextures[0], 0);

        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    }

    private void destroyFrameBuffers() {
        //删除fbo的纹理
        if (mFBOTextures != null) {
            GLES20.glDeleteTextures(1, mFBOTextures, 0);
            mFBOTextures = null;
        }
        //删除fbo
        if (mFrameBuffers != null) {
            GLES20.glDeleteFramebuffers(1, mFrameBuffers, 0);
            mFrameBuffers = null;
        }
    }

    /**
     * 前置纹理应用矩阵转换,右上角
     * @param w 纹理宽
     * @param h 纹理高
     */
    private void applyMatrix2Fg(int w, int h){
        // 先缩放,后平移
        float scale = 0.4f;
        mFgOesDrawer.scale(scale, scale);

        float scaleW = w * scale;
        float scaleH = h * scale;

        float startX = w / 2.0f + scaleW / 2.0f;
        float startY = h / 2.0f + scaleH / 2.0f;

        float deltaX = w - startX;
        float deltaY = h - startY;

        mFgOesDrawer.translate(deltaX / scaleW * 2, deltaY / scaleH * 2, 0);
        mFgOesDrawer.setAlpha(1.0f);
    }

    public void prepareDraw(int frameWidth, int frameHeight) {
        applyMatrix2Fg(frameWidth, frameHeight);

        loadFBO(frameWidth, frameHeight);

        int[] textureIds = GlesUtils.createTextureIds(2);

        mBgOesDrawer.setTextureID(textureIds[0]);
        mFgOesDrawer.setTextureID(textureIds[1]);
        Log.d(TAG, "textureIds[0]=" + textureIds[0]);
        Log.d(TAG, "textureIds[1]=" + textureIds[1]);
        //开启混合,即半透明
        GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
    }

    public void destroyDraw() {
        mBgOesDrawer.release();
        mFgOesDrawer.release();

        if (mFrameBuffers != null) {
            destroyFrameBuffers();
        }
    }

    public int doDraw() {
        //锁定绘制的区域  绘制是从左下角开始的
        GLES20.glViewport(0, 0, mFrameWidth, mFrameHeight);

        //绑定FBO,在FBO上操作
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[0]);

        GLES20.glClearColor(0f, 0f, 0f, 0f);

        mBgOesDrawer.doDraw();

        if (mMode == Mode.PIP) {
            mFgOesDrawer.doDraw();
        }

        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);

        return mFBOTextures[0];
    }

    public void setMode(Mode mode) {
        this.mMode = mode;
    }

    public Mode getMode() {
        return mMode;
    }

    public enum Mode {
        SINGLE, PIP
    }

}

GlesUtils 工具类实现。

/**
 * author : liuhongwei
 * e-mail : 
 * date   : 2021/7/22 13:46
 * desc   : openGL ES 工具类
 * version: 1.0
 */
public class GlesUtils {
    private final static String TAG = "GlesUtils";

    public static int[] createTextureIds(int count) {
        int[] texture = new int[count];
        GLES20.glGenTextures(count, texture, 0); //生成纹理
        return texture;
    }

    public static void genTextures(int[] textures) {
        GLES20.glGenTextures(textures.length, textures, 0);

        for (int texture : textures) {
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);

            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

        }
    }
}

最后,直接将 FBO 绘制到需要显示的 EGL 上,整个画中画预览就实现了。