TextureView+SurfaceTexture+OpenGL ES來播放視頻(三)

做好的Demo截圖


opengl-video
前言

講了這么多,可能有人要問了,播放視頻用個android封裝的VideoView或者用MediaPlayer+SurfaceView來進行播放視頻不就得了嗎,干嘛還要整這么麻煩。OK,為了回答這個問題,我們先看看OpenGL ES干什么的,它是OpenGL三維圖形API的子集,圖形硬件的一種軟件接口,針對手機、PDA和游戲主機等嵌入式設備而設計。我想如果是做游戲類開發的肯定對一些圖形庫不會陌生,其實很多游戲引擎內部都是封裝的像OpenGL,DirectX 3D之類的圖形庫,然后開發者就可以通過這些圖形庫,來開發很多好玩的東西,如游戲,動畫等等,那么我現在用Opengl es來繪制視頻是不是也可以定制很多有意思的東西,比如開發一個左右分屏視頻播放器,然后在虛擬現實(VR)頭盔上來觀看2d的視頻,如果用opengl去繪制,那簡直分分中搞定,因為這里,每一幀的視頻在opengl 看來只是一張紋理貼圖而已,那么我想把這個貼圖貼在哪里就貼在哪里。總之,用opengl可以開發出很多有意思的二維,三維的圖形應用出來。

正文

說了這么多,咱們開始吧,

/***
 * 在這個類里面對視頻紋理進行繪制工作,繼承了 {@link TextureSurfaceRenderer},
 * 并實現了{@link SurfaceTexture.OnFrameAvailableListener}
 */
public class VideoTextureSurfaceRenderer extends TextureSurfaceRenderer implements
        SurfaceTexture.OnFrameAvailableListener
{

    public static final String TAG = VideoTextureSurfaceRenderer.class.getSimpleName();
    /**繪制的區域尺寸*/
    private static float squareSize = 1.0f;
    private static float squareCoords[] = {
            -squareSize,  squareSize, 0.0f,   // top left
            -squareSize, -squareSize, 0.0f,   // bottom left
            squareSize, -squareSize, 0.0f,    // bottom right
            squareSize,  squareSize, 0.0f     // top right
    };
    /**繪制次序*/
    private static short drawOrder[] = {
         0, 1, 2, 
         0, 2, 3
     };
    /**
     * 用來緩存紋理坐標,因為紋理都是要在后臺被繪制好,然
     * 后不斷的替換最前面顯示的紋理圖像
     */
    private FloatBuffer textureBuffer;
    /**紋理坐標*/
    private float textureCoords[] = {
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 1.0f, 0.0f, 1.0f
    };
    /**生成的真實紋理數組*/
    private int[] textures = new int[1];
    /**著色器腳本程序的handle(句柄)*/
    private int shaderProgram;
    /**squareCoords的的頂點緩存*/
    private FloatBuffer vertexBuffer;
    /**繪制次序的緩存*/
    private ShortBuffer drawOrderBuffer;
    
    /**矩陣來變換紋理坐標,(具體含義下面再解釋)*/
    private float[] videoTextureTransform;
    /**當前的視頻幀是否可以得到*/
    private boolean frameAvailable = false;
    private Context context;
    private SurfaceTexture videoTexture;    // 從視頻流捕獲幀作為Opengl ES 的Texture
    
    /**
     * @param texture 從TextureView獲取到的SurfaceTexture,目的是為了配置EGL 的native window
    */
    public VideoTextureSurfaceRenderer(Context context, SurfaceTexture texture, int width, int height)
    {
        super(texture, width, height);  //先調用父類去做EGL初始化工作
        this.context = context;
        videoTextureTransform = new float[16];      
    }
   
    // 代碼略
}

對上面的代碼再稍稍解釋一下吧,在繪制之前,我們首先選定一塊區域來讓圖像就在這塊區域來繪制,則有如下定義:

private static float squareSize = 1.0f;
    private static float squareCoords[] = {
            -squareSize,  squareSize, 0.0f,   // top left
            -squareSize, -squareSize, 0.0f,   // bottom left
            squareSize, -squareSize, 0.0f,    // bottom right
            squareSize,  squareSize, 0.0f     // top right
    };

上幅圖來解釋解釋:

square

squareSize=1.0時,square的面積就是整個手機屏幕,若squareSize=0.5f則每個邊長都為屏幕的一半。數組的坐標順序為:左上->左下->右下->右上。

然后接著定義一個紋理坐標數組:

 private float textureCoords[] = {
            0.0f, 1.0f, 0.0f, 1.0f,  //左上
            0.0f, 0.0f, 0.0f, 1.0f,  //左下
            1.0f, 0.0f, 0.0f, 1.0f,  //右下
            1.0f, 1.0f, 0.0f, 1.0f   //右上
    };
   

接著上圖:


texCoord

如上圖,這就是2D OpenGL ES紋理坐標,它由四個列向量(s, t, 0, 1)組成,其中s, t ∈[0, 1]。
不知道大家有沒有發現,繪制區域(quareCoords[])的頂點數組坐標順序跟紋理數組坐標的順序都是從 左上方開始->到右上方結束,為什么要這樣做呢? 其實目的就是為了統一方向,因為我們知道手機屏幕的坐標是左上角為原點(0.0, 0.0),所以為了以后不必要的轉換工作,最好將繪制的順序的都統一起來。

然后則看看紋理的繪制次序drawOrder[] = {0,1,2, 0, 2, 3} ; 這又是個什么次序呢? 好,再來個圖看看

drawOrder

其實,opengl es 在繪制一個多邊形時,都是用一個基本的圖元即三角形拼湊出來的,比如一個矩形它可以用(0->1->2)和(0->2->3)的次序,通過兩個三角形給拼湊出來的,當然也可是其它的組合比如(1,3,2)(1,3,0)等等,總之得用兩個三角形拼湊成一個矩形,不過建議還是都按找同一鐘次序,比如都是按照順時針或都按照逆時針來拼湊。

OK,接下來再貼出省略的代碼:

    /**
    * 重寫父類方法,初始化組件
    */
    @Override
    protected void initGLComponents()
    {
        setupVertexBuffer();
        setupTexture();
        loadShaders();
    }
/***
* 設置頂點緩存
*/
 private void setupVertexBuffer()
    {
        /** Draw Order buffer*/
        ByteBuffer orderByteBuffer = ByteBuffer.allocateDirect(drawOrder. length * 2); 
        orderByteBuffer.order(ByteOrder.nativeOrder());  //Modifies this buffer's byte order
        drawOrderBuffer = orderByteBuffer.asShortBuffer();  //創建此緩沖區的視圖,作為一個short緩沖區.
        drawOrderBuffer.put(drawOrder);
        drawOrderBuffer.position(0); //下一個要被讀或寫的元素的索引,從0 開始

        // Initialize the texture holder
        ByteBuffer bb = ByteBuffer.allocateDirect(squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);
    }

ByteBuffer.allocateDirect(drawOrder. length * 2)表示的是直接從系統中分配大小為 (drawOrder. length * 2)的內存,2 代表一個short型占兩個字節,(int占4個字節)。

/**接著初始化紋理*/
private void setupTexture()
    {
        ByteBuffer texturebb = ByteBuffer.allocateDirect(textureCoords.length * 4);
        texturebb.order(ByteOrder.nativeOrder());

        textureBuffer = texturebb.asFloatBuffer();
        textureBuffer.put(textureCoords);
        textureBuffer.position(0);

        // 啟用紋理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        //1表示只需要一個紋理索引
        GLES20.glGenTextures(1, textures, 0);         
       GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textures[0]);

      videoTexture = new SurfaceTexture(textures[0]);
      videoTexture.setOnFrameAvailableListener(this);
    }

關于glBindTexure(int target, int texture) 中的參數target,表示指定這是一張什么類型的紋理,在此是GLES11Ext.GL_BLEND_EQUATION_RGB_OES,也可以是常用的2D紋理如GLES20.GL_TEXTURE_2D等;第二個參數texture,在程序第一次使用這個參數時,這個函數會創建一個新的對象,并把這個對象分配給它,之后這個texture就成了一個活動的紋理對象。如果texture=0 則OpenGL就停止使用紋理對象,并返回到初始的默認紋理。

/**加載頂點與片段著色器*/
 private void loadShaders()
    {
        final String vertexShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.vetext_sharder);
        final String fragmentShader = RawResourceReader.readTextFileFromRawResource(context, R.raw.fragment_sharder);

        final int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertexShader);
        final int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragmentShader);
        shaderProgram = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle,
                new String[]{"texture","vPosition","vTexCoordinate","textureTransform"});
    }

關于可編程著色器在計算機圖形領域本身就是個很大的主題,已超出了本文的范圍。那么就稍稍解釋下,OpenGL著色語言(OpenGL shading Language),它是一種編程語言,用于創建可編程的著色器。在opengl es 2.0以前使用的是一種“固定功能管線“,而2.0以后就是使用的著這鐘可“編程的管線“,這種管線又分為頂點處理管線與片段處理管線(涉及到OpenGL的渲染管線的一些機制,大家自行的查吧)。
我現在就說說這個GLSL著色器是怎么使用的吧:有關使用GLSL創建著色器流程如下圖(參照OpenGL 編程指南):


shaderCreate

大家可以參照這幅圖來看看程序中shader的使用。
在本項目的res目錄下新建一個raw文件夾,然后分別創建vertext_shader.glsl和fragment_sharder.glsl文件,代碼如下:
vertext_sharder:

attribute vec4 vPosition;    //頂點著色器輸入變量由attribute來聲明
attribute vec4 vTexCoordinate;
//uniform表示一個變量的值由應用程序在著色器執行之前指定,
//并且在圖元處理過程中不會發生任何變化。mat4表示一個4x4矩陣
uniform mat4 textureTransform; 
varying vec2 v_TexCoordinate;   //片段著色器輸入變量用arying來聲明

void main () {
    v_TexCoordinate = (textureTransform * vTexCoordinate).xy;
    gl_Position = vPosition;
}

fragment_shader

/**使用GL_OES_EGL_image_external擴展處理,來增強GLSL*/
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES texture; //定義擴展的的紋理取樣器amplerExternalOES
varying vec2 v_TexCoordinate;

void main () {
    vec4 color = texture2D(texture, v_TexCoordinate);
    gl_FragColor = color;
}

著色器語言非常的類似C語言,也是從main函數開始執行的。其中的很多語法,變量等等,還是大家自行的查查,這不是幾句能說明白的。著色器的創建及編譯過程的代碼都在項目里的一個util包下的三個工具類,RawRourceReader,ShaderHelper,TextureHelper類中,我就不再貼出來了,有興趣大家可以fork或clone下來看看。

ok,終于初始化完了,太不容易了,冏。
好吧,繪制工作開始跑起來。

   @Override
    protected boolean draw()
    {
        synchronized (this)
        {
            if (frameAvailable)
            {
                videoTexture .updateTexImage();
                videoTexture .getTransformMatrix(videoTextureTransform);
                frameAvailable = false;
            }
            else
            {
                return false;
            }

        }
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glViewport(0, 0, width, height);
        this.drawTexture();

        return true;
    }

當視頻的幀可得到時,調用videoTexture .updateTexImage()方法,這個方法的官網解釋如下:
<font color=#008000 >Update the texture image to the most recent frame from the image stream. This may only be called while the OpenGL ES context that owns the texture is current on the calling thread. It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target.</font>
大意是,從的圖像流中更新紋理圖像到最近的幀中。這個函數僅僅當擁有這個紋理的Opengl ES上下文當前正處在繪制線程時被調用。它將隱式的綁定到這個擴展的GL_TEXTURE_EXTERNAL_OES 目標紋理(為什么上面的片段著色代碼中要擴展這個OES,可能就是應為這個吧)。
videoTexture .getTransformMatrix(videoTextureTransform)這又是什么意思呢?當對紋理用amplerExternalOES采樣器采樣時,應該首先使用getTransformMatrix(float[])查詢得到的矩陣來變換紋理坐標,每次調用updateTexImage()的時候,可能會導致變換矩陣發生變化,因此在紋理圖像更新時需要重新查詢,該矩陣將傳統的2D OpenGL ES紋理坐標列向量(s,t,0,1),其中s,t∈[0,1],變換為紋理中對應的采樣位置。該變換補償了圖像流中任何可能導致與傳統OpenGL ES紋理有差異的屬性。例如,從圖像的左下角開始采樣,可以通過使用查詢得到的矩陣來變換列向量(0,0,0,1),而從右上角采樣可以通過變換(1,1,0,1)來得到。

關于 GLES20.glViewport(int x, int y, int width, int height) ,也是個比較重要的函數,這個函數大家網上查查。
ok,接下來到了真正的繪制紋理的時候了。代碼如下:

 private void drawTexture() {
        // Draw texture
        GLES20.glUseProgram(shaderProgram); //繪制時使用著色程序
        int textureParamHandle = GLES20.glGetUniformLocation(shaderProgram, "texture"); //返回一個于著色器程序中變量名為"texture"相關聯的索引
        int textureCoordinateHandle = GLES20.glGetAttribLocation(shaderProgram, "vTexCoordinate");
        int positionHandle = GLES20.glGetAttribLocation(shaderProgram, "vPosition");
        int textureTransformHandle = GLES20.glGetUniformLocation(shaderProgram, "textureTransform");
        //在用VertexAttribArray前必須先激活它    
        GLES20.glEnableVertexAttribArray(positionHandle);
         //指定positionHandle的數據值可以在什么地方訪問。 vertexBuffer在內部(NDK)是個指針,指向數組的第一組值的內存
        GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);               
        GLES20.glBindTexture(GLES20.GL_TEXTURE0, textures[0]);
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        //指定一個當前的textureParamHandle對象為一個全局的uniform 變量
        GLES20.glUniform1i(textureParamHandle, 0);

        GLES20.glEnableVertexAttribArray(textureCoordinateHandle);
        GLES20.glVertexAttribPointer(textureCoordinateHandle, 4, GLES20.GL_FLOAT, false, 0, textureBuffer);

        GLES20.glUniformMatrix4fv(textureTransformHandle, 1, false, videoTextureTransform, 0);
       //GLES20.GL_TRIANGLES(以無數小三角行的模式)去繪制出這個紋理圖像
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawOrderBuffer);
        GLES20.glDisableVertexAttribArray(positionHandle);
        GLES20.glDisableVertexAttribArray(textureCoordinateHandle);
    }

好了所有代碼就分析到這里了,前兩篇鏈接TextureView+SurfaceTexture+OpenGL ES來播放視頻(一) , TextureView+SurfaceTexture+OpenGL ES來播放視頻(二) , 項目地址在here

注: 當然了,其實也沒必要配置EGL環境這么麻煩,android的GLSurfaceView就已經在底層就配置好了EGL環境,且自帶繪制線程
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容