1. 程式人生 > >Android OpenGL使用GLSurfaceView預覽視訊

Android OpenGL使用GLSurfaceView預覽視訊

前言

一年之前做過一些即時通訊視訊相關的工作,主要是做視訊渲染這一部分的工作,由於2016畢業來到了華為,華為對研究生的安排就是“哪裡需要去哪裡”,和你專業和擅長的沒有太大的關係,所以一直在適應當下的工作,現在基本上可以勝任現在的工作,可以抽出一些時間來總結一下之前瞭解過的OpenGL相關知識。

第一章 相關知識介紹

在介紹具體的功能之前,先對一些主要的類和方法進行一些介紹,這樣可以更好的理解整個程式

1.1 GLSurfaceView

在谷歌的官方文件中是這樣解釋GLSurfaceView的:

An implementation of SurfaceView that uses the dedicated surface for displaying OpenGL rendering.

大意是GLSurfaceView是一個繼承了SurfaceView類,它是專門用來顯示OpenGL的渲染。通俗的來說,GLSurfaceView可以用來顯示視訊、影象和3D模型等檢視,在接下來的章節中主要是使用它來顯示Camera視訊資料,大家可能會有一些問題,SurfaceView也可用來預覽Camera,那麼這兩者有什麼區別嗎?GLSurfaceView能夠真正做到讓Camera的資料和顯示分離,我們就可以在此基礎上對視訊資料做一些處理,例如美圖,增加特效等。

1.2 GLSurfaceView.Renderer

如果說GLSurfaceView是畫布,那麼僅僅有一張白紙是沒用的,我們還需要一支畫筆,Renderer的功能就是這裡說的畫筆。Renderer是一個介面,主要包含3個抽象的函式:onSurfaceCreated

onDrawFrameonSurfaceChanged,從名字就可以看出,分別是在SurfaceView建立、檢視大小發生改變和繪製圖形時呼叫。

1.3 Camera

從Android 5.0開始(API Level 21),可以完全控制安卓設別相機的新API Camera2(android.hardware.Camera2)被引進來了。雖然新的Camera2不管在功能上還是友好度上都強於舊的Camera,但是我們這裡還是使用的舊的Camera,由於新的Camera2暫時還沒有找到可以獲取視訊幀的介面,因為後面肯能會對Canmera視訊幀做一些處理,所以這裡暫時還是使用舊的Camera。

第二章 開始繪製

2.1 CameraGLSurfaceView

public class CameraGLSurfaceView extends GLSurfaceView implements Renderer, SurfaceTexture.OnFrameAvailableListener {
    private Context mContext;
    private SurfaceTexture mSurface;
    private int mTextureID = -1;
    private DirectDrawer mDirectDrawer;

    public CameraGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        // 設定OpenGl ES的版本為2.0
        setEGLContextClientVersion(2);
        // 設定與當前GLSurfaceView繫結的Renderer
        setRenderer(this);
        // 設定渲染的模式
        setRenderMode(RENDERMODE_WHEN_DIRTY);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        // TODO Auto-generated method stub
        LOG.logI("onSurfaceCreated...");
        mTextureID = GlUtil.createTextureID();
        mSurface = new SurfaceTexture(mTextureID);
        mSurface.setOnFrameAvailableListener(this);
        mDirectDrawer = new DirectDrawer(mTextureID);
        CameraCapture.get().openBackCamera();

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        // TODO Auto-generated method stub
        LOG.logI("onSurfaceChanged...");
        // 設定OpenGL場景的大小,(0,0)表示視窗內部視口的左下角,(w,h)指定了視口的大小
        GLES20.glViewport(0, 0, width, height);
        if (!CameraCapture.get().isPreviewing()) {
            CameraCapture.get().doStartPreview(mSurface);
        }


    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // TODO Auto-generated method stub
        LOG.logI("onDrawFrame...");
        // 設定白色為清屏
        GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
        // 清除螢幕和深度快取
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
        // 更新紋理
        mSurface.updateTexImage();

        mDirectDrawer.draw();

    }

    @Override
    public void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
        CameraCapture.get().doStopCamera();
    }


    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        // TODO Auto-generated method stub
        LOG.logI("onFrameAvailable...");
        this.requestRender();
    }

}

這個類主要做了以下幾件事情:

  • 實現Renderer這個介面,並且實現GLSurfaceView的初始化。在CameraGLSurfaceView的建構函式中設定了GLSurfaceView的版本:setEGLContextClientVersion(2),如果沒有這個設定,GLSurfaceView是什麼也繪製不出來的,因為Android支援OpenGL ES1.1、2.0以及3.+等版本,而且版本間的差別很大,不宣告版本號,GLSurfaceView是不知道使用哪個版本進行渲染;設定Renderer與當前的View繫結,然後再設定渲染的模式為RENDERMODE_WHEN_DIRTY。渲染模式的設定也很關鍵,渲染模式有兩種:RENDERMODE_WHEN_DIRTYRENDERMODE_CONTINUOUSLY。DIRYT的含義是隻有當被通知的時候才會去渲染檢視,而CONTINUOUSLY的含義是視訊會一直連續的渲染。
  • onSurfaceCreated()函式中,建立一個渲染的紋理,這個紋理就是用來顯示Camera的影象,所以需要新建立的SurfaceTexture繫結在一起,而SurfaceTexture是作為渲染的載體,另一方面需要和DirectDrawer繫結在一起,DirectDrawer是用來繪製圖像的,下面會具體介紹。最後是初始化Camera。
  • 因為在初始化的時候這是了渲染的模式為RENDERMODE_WHEN_DIRTY,所以我們就通知GLSurfaceView什麼時候需要渲染影象,而介面SurfaceTexture.OnFrameAvailableListener完成這項工作,函式onFrameAvailable()在有新資料到來時,會被呼叫,在其中呼叫requestRender(),就可以完成新資料的渲染。
  • onSurfaceChanged()函式中,設定了OpenGL視窗的大小,(0,0)表示視窗內部視口的左下角,(w,h)指定了視口的大小;開啟Camera的預覽。
  • 最後,在onDrawFrame()函式中繪製更新的紋理。

2.2 DirectDrawer

這個類非常重要,負責將SurfaceTexture(紋理的控制代碼)內容繪製到螢幕上。

public class DirectDrawer {
    private FloatBuffer vertexBuffer, mTextureCoordsBuffer;
    private ShortBuffer drawListBuffer;
    private final int mProgram;
    private int mPositionHandle;
    private int mTextureCoordHandle;
    private int mMVPMatrixHandle;

    private short drawOrder[] = {0, 2, 1, 0, 3, 2}; // order to draw vertices

    // number of coordinates per vertex in this array
    private final int COORDS_PER_VERTEX = 2;

    private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

    private float mVertices[] = new float[8];

    private float mTextureCoords[] = new float[8];
    private float mTextHeightRatio = 0.1f;

    private int texture;
    public float[] mMVP = new float[16];

    public void resetMatrix() {
        mat4f_LoadOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, mMVP);
    }


    public DirectDrawer(int texture) {
        String vertextShader = TextResourceReader.readTextFileFromResource(MyApplication.getContext()
                , R.raw.video_vertex_shader);
        String fragmentShader = TextResourceReader.readTextFileFromResource(MyApplication.getContext()
                , R.raw.video_normal_fragment_shader);

        mProgram = GlUtil.createProgram(vertextShader, fragmentShader);

        if (mProgram == 0) {
            throw new RuntimeException("Unable to create program");
        }

        //get handle to vertex shader's vPosition member
        mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
        GlUtil.checkLocation(mPositionHandle, "vPosition");

        mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
        GlUtil.checkLocation(mTextureCoordHandle, "inputTextureCoordinate");

        mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        GlUtil.checkLocation(mMVPMatrixHandle, "uMVPMatrix");


        this.texture = texture;
        // initialize vertex byte buffer for shape coordinates
        updateVertices();

        setTexCoords();

        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);

        mat4f_LoadOrtho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, mMVP);
    }

    public void draw() {
        GLES20.glUseProgram(mProgram);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);

        // get handle to vertex shader's vPosition member

        // Enable a handle to the triangle vertices
        GLES20.glEnableVertexAttribArray(mPositionHandle);

        // Prepare the <insert shape here> coordinate data
        GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);

        GLES20.glEnableVertexAttribArray(mTextureCoordHandle);

        GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, mTextureCoordsBuffer);

        // Apply the projection and view transformation
        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVP, 0);
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

        // Disable vertex array
        GLES20.glDisableVertexAttribArray(mPositionHandle);
        GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
    }

    public static void mat4f_LoadOrtho(float left, float right, float bottom, float top, float near, float far, float[] mout) {
        float r_l = right - left;
        float t_b = top - bottom;
        float f_n = far - near;
        float tx = -(right + left) / (right - left);
        float ty = -(top + bottom) / (top - bottom);
        float tz = -(far + near) / (far - near);

        mout[0] = 2.0f / r_l;
        mout[1] = 0.0f;
        mout[2] = 0.0f;
        mout[3] = 0.0f;

        mout[4] = 0.0f;
        mout[5] = 2.0f / t_b;
        mout[6] = 0.0f;
        mout[7] = 0.0f;

        mout[8] = 0.0f;
        mout[9] = 0.0f;
        mout[10] = -2.0f / f_n;
        mout[11] = 0.0f;

        mout[12] = tx;
        mout[13] = ty;
        mout[14] = tz;
        mout[15] = 1.0f;
    }

    public void updateVertices() {
        final float w = 1.0f;
        final float h = 1.0f;
        mVertices[0] = -w;
        mVertices[1] = h;
        mVertices[2] = -w;
        mVertices[3] = -h;
        mVertices[4] = w;
        mVertices[5] = -h;
        mVertices[6] = w;
        mVertices[7] = h;
        vertexBuffer = ByteBuffer.allocateDirect(mVertices.length * 4).order(ByteOrder.nativeOrder())
                .asFloatBuffer().put(mVertices);
        vertexBuffer.position(0);
    }

    public void setTexCoords() {
        mTextureCoords[0] = 0;
        mTextureCoords[1] = 1 - mTextHeightRatio;
        mTextureCoords[2] = 1;
        mTextureCoords[3] = 1 - mTextHeightRatio;
        mTextureCoords[4] = 1;
        mTextureCoords[5] = 0 + mTextHeightRatio;
        mTextureCoords[6] = 0;
        mTextureCoords[7] = 0 + mTextHeightRatio;
        mTextureCoordsBuffer = ByteBuffer.allocateDirect(mTextureCoords.length * 4).order(ByteOrder.nativeOrder())
                .asFloatBuffer().put(mTextureCoords);
        mTextureCoordsBuffer.position(0);
    }
}

這個類的主要功能就是繪製圖像。

(1) 定義Vertex Shader(頂點著色器,用來繪製圖形的形狀)Fragment Shader(片段著色器,用來繪製圖形的顏色或者紋理)Program(OpenGL ES物件,包含了用來繪製一個或者多個形狀的shader),然後接下來都是圍繞著這三個變數,最後通過呼叫OpenGL方法進行繪製。具體的過程可以參考前面的部落格

(2) 既然我們需要預覽Camera的視訊資料,那麼我們可以知道現實的區域的形狀大部分都是四邊形,但是在OpenGL中只有提供了繪製三角形的方法,我們就需要將兩個三角形拼接成一個正方形,所以需要定義一個大小為8的陣列,如下面程式碼所示:

static float squareCoords[] = {  
       -1.0f,  1.0f,  // 左上點
       -1.0f, -1.0f,  // 左下點
        1.0f, -1.0f,  // 右下點
        1.0f,  1.0f,  // 有上點
    };  

此時,我們就有了一個四邊形的4個點的資料了。但是,OpenGL並不是對陣列的資料直接進行操作的,而是在直接記憶體中,即操作的資料需要儲存到NIO裡面的Buffer物件中。而我們上面生命的float[]物件儲存在陣列中,因此我們需要將float[]物件轉換為Java.nio.Buffer物件,程式碼如下:

 public void updateVertices() {
        final float w = 1.0f;
        final float h = 1.0f;
        mVertices[0] = -w;
        mVertices[1] = h;
        mVertices[2] = -w;
        mVertices[3] = -h;
        mVertices[4] = w;
        mVertices[5] = -h;
        mVertices[6] = w;
        mVertices[7] = h;
        vertexBuffer = ByteBuffer.allocateDirect(mVertices.length * 4).order(ByteOrder.nativeOrder())
                .asFloatBuffer().put(mVertices);
        vertexBuffer.position(0);
    }

注意,ByteBuffer和FloatBuffer以及IntBuffer都是繼承自抽象類java.nio.Buffer。
另外,OpenGL在底層的實現是C語言,與Java預設的資料儲存位元組順序可能不同,即大端小端問題。因此,為了保險起見,在將資料傳遞給OpenGL之前,我們需要指明使用本機的儲存順序。
此時,我們順利地將float[]轉為了FloatBuffer,後面繪製三角形的時候,直接通過成員變數mTriangleBuffer即可。

(3) 最後就是將準備好的資料繪製到螢幕上,OpenGL 提供了兩個繪製的方法glDrawArrays(int mode, int first, int count)glDrawElements(int mode,int count, int type, Buffer indices)兩個方法,在這裡我們使用的第二種繪製的方法,關於mode有幾種模式供我們選擇:

  • GL_POINTS:繪製獨立的點到螢幕
  • GL_LINE_STRIP:連續的連線,第n個頂點與第n-1個頂點繪製一條直線
  • GL_LINE_LOOP:與上一個相同,但是需要首尾相聯接
  • GL_LINES:形成對的獨立的線段
  • GL_TRIANGLE_STRIP:繪製一系列的三角形,先是頂點v0,v1,v2,然後是v2,v1,v3(注意規律),然後v2,v3,v4等。該規律確保所有的三角形都以相同的方向繪製
  • GL_TRIANGLE_FANGL_TRANGLE_STRIP類似,但其縣繪製v0,v1,v2,再是v0,v2,v3,然後v0,v3,v4等。

(4) 需要注意的是,在這個類中,定義了mMVP這個陣列,這個陣列的功能是對視訊幀資料進行轉換的,例如旋轉影象等。

第三章 總結

到此為止,使用GLSurfaceView預覽Camera的介紹就完了,這篇文章,僅僅介紹了CameraGLSurfaceViewDirectDrawer這兩個類,但是如何對Camera進行操作的並沒有介紹,這不是本文的重點,所以就省略了。接下來還會介紹一些有關GLSurfaceView的文章。

下載程式碼