Android OpenGL ES(五)-結(jié)合相機(jī)進(jìn)行預(yù)覽/錄制及添加濾鏡

cover.png

上文中我們已經(jīng)實(shí)現(xiàn)了在紋理上添加濾鏡的效果。這編文章就是將OpenGl和相機(jī)結(jié)合到一起摆出。

預(yù)覽與拍照


整體流程理解

預(yù)覽的整體流程.png
  1. Camera中得到的ImageStreamSurfaceTexture接受葫掉,并轉(zhuǎn)換成OpenGL ES紋理。
  2. 創(chuàng)建GLSurfaceView。在OpenGL環(huán)境下,用GLSurfaceView.Render將這個紋理繪制出來比伏。
  3. 整體的ImageStream的流向就是
Camera ==>SurfaceTexture==>texture(samplerExternalOES) ==>draw to GLSurfaceView

各個部分詳解

Camra Api

首先是相機(jī)的Api的書寫。

Camera Interface

為我們相機(jī)的操作定義一個接口疆导。因?yàn)槲覀兊南鄼C(jī)Api赁项。有Camera2和Camera的區(qū)別。這里還是簡單的使用Camera來完成澈段。

/**
 * 定義個相機(jī)的功能接口
 */
public interface ICamera {
    boolean open(int cameraId);

    /**
     * 設(shè)置畫面的比例
     */
    void setAspectRatio(AspectRatio aspectRatio);

    /**
     * 開啟預(yù)覽
     */
    boolean preview();

    /**
     * 關(guān)閉相機(jī)
     *
     * @return
     */
    boolean close();

    /**
     * 使用SurfaceTexture 來作為預(yù)覽的畫面
     *
     * @param surfaceTexture
     */
    void setPreviewTexture(SurfaceTexture surfaceTexture);

    CameraSize getPreviewSize();
    CameraSize getPictureSize();
}

定義一個相機(jī)的接口悠菜。我們知道。我們需要相機(jī)做的幾個通常的操作均蜜。

CameraApi14
/**
 * for api 14
 * <p>
 * Camera主要涉及參數(shù)
 * 1. 預(yù)覽畫面的大小
 * 2. pic圖片的大小
 * 3. 對焦模式
 * 4. 閃光燈模式
 */
public class CameraApi14 implements ICamera {
    /*
    當(dāng)前的相機(jī)Id
     */
    private int mCameraId;
    /*
    當(dāng)前的相機(jī)對象
     */
    private Camera mCamera;
    /*
    當(dāng)前的相機(jī)參數(shù)
     */
    private Camera.Parameters mCameraParameters;


    //想要的尺寸李剖。
    private int mDesiredHeight = 1920;
    private int mDesiredWidth = 1080;
    private boolean mAutoFocus;
    public CameraSize mPreviewSize;
    public CameraSize mPicSize;
    /*
     * 當(dāng)前相機(jī)的高寬比
     */
    private AspectRatio mDesiredAspectRatio;


    public CameraApi14() {
        mDesiredHeight = 1920;
        mDesiredWidth = 1080;
        //創(chuàng)建默認(rèn)的比例.因?yàn)楹笾脭z像頭的比例,默認(rèn)的情況下囤耳,都是旋轉(zhuǎn)了270
        mDesiredAspectRatio = AspectRatio.of(mDesiredWidth, mDesiredHeight).inverse();
    }

    @Override
    public boolean open(int cameraId) {
        /*
            預(yù)覽的尺寸和照片的尺寸
        */
        final CameraSize.ISizeMap mPreviewSizes = new CameraSize.ISizeMap();
        final CameraSize.ISizeMap mPictureSizes = new CameraSize.ISizeMap();
        if (mCamera != null) {
            releaseCamera();
        }
        mCameraId = cameraId;
        mCamera = Camera.open(mCameraId);
        if (mCamera != null) {
            mCameraParameters = mCamera.getParameters();

            mPreviewSizes.clear();
            //先收集參數(shù).因?yàn)槊總€手機(jī)能夠得到的攝像頭參數(shù)都不一致篙顺。所以將可能的尺寸都得到。
            for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
                mPreviewSizes.add(new CameraSize(size.width, size.height));
            }

            mPictureSizes.clear();
            for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
                mPictureSizes.add(new CameraSize(size.width, size.height));
            }
            //挑選出最需要的參數(shù)
            adJustParametersByAspectRatio2(mPreviewSizes, mPictureSizes);
            return true;
        }
        return false;
    }

    private void adJustParametersByAspectRatio(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
        //得到當(dāng)前預(yù)期比例的size
        SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
        if (sizes == null) {  //表示不支持.
            // TODO: 2018/9/14 這里應(yīng)該拋出異常充择?
            return;
        }
        //當(dāng)前先不考慮Orientation
        CameraSize previewSize;
        mPreviewSize = new CameraSize(mDesiredWidth, mDesiredHeight);
        if (mCameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
            mPreviewSize = new CameraSize(mDesiredHeight, mDesiredWidth);
            mCameraParameters.setRotation(90);
        } else {
//            previewSize = mPreviewSize;
        }

        //默認(rèn)去取最大的尺寸
        mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();

        mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());

        //設(shè)置對角和閃光燈
        setAutoFocusInternal(mAutoFocus);
        //先不設(shè)置閃光燈
//        mCameraParameters.setFlashMode("FLASH_MODE_OFF");

        //設(shè)置到camera中
//        mCameraParameters.setRotation(90);
        mCamera.setParameters(mCameraParameters);
//        mCamera.setDisplayOrientation(90);
//        setCameraDisplayOrientation();
    }

    private void adJustParametersByAspectRatio2(CameraSize.ISizeMap previewSizes, CameraSize.ISizeMap pictureSizes) {
        //得到當(dāng)前預(yù)期比例的size
        SortedSet<CameraSize> sizes = previewSizes.sizes(mDesiredAspectRatio);
        if (sizes == null) {  //表示不支持.
            // TODO: 2018/9/14 這里應(yīng)該拋出異常德玫?
            return;
        }
        //當(dāng)前先不考慮Orientation
        mPreviewSize = sizes.first();
        //默認(rèn)去取最大的尺寸
        mPicSize = pictureSizes.sizes(mDesiredAspectRatio).first();
        mCameraParameters.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        mCameraParameters.setPictureSize(mPicSize.getWidth(), mPicSize.getHeight());

        mPreviewSize = mPreviewSize.inverse();
        mPicSize = mPicSize.inverse();
        //設(shè)置對角和閃光燈
        setAutoFocusInternal(mAutoFocus);
        //先不設(shè)置閃光燈
//        mCameraParameters.setFlashMode("FLASH_MODE_OFF");

        //設(shè)置到camera中
//        mCameraParameters.setRotation(90);
        mCamera.setParameters(mCameraParameters);
//        mCamera.setDisplayOrientation(90);
//        setCameraDisplayOrientation();
    }

    private boolean setAutoFocusInternal(boolean autoFocus) {
        mAutoFocus = autoFocus;
//        if (isCameraOpened()) {
        final List<String> modes = mCameraParameters.getSupportedFocusModes();
        if (autoFocus && modes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
            mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
        } else if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
            mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
        } else if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
            mCameraParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
        } else {
            mCameraParameters.setFocusMode(modes.get(0));
        }
        return true;
//        } else {
//            return false;
//        }
    }

    private void releaseCamera() {
        if (mCamera != null) {
            mCamera.release();
            mCamera = null;
        }
    }

    @Override
    public void setAspectRatio(AspectRatio aspectRatio) {
        this.mDesiredAspectRatio = aspectRatio;
    }

    @Override
    public boolean preview() {
        if (mCamera != null) {
            mCamera.startPreview();
            return true;
        }
        return false;
    }

    @Override
    public boolean close() {
        if (mCamera != null) {
            try {
                //stop preview時,可能爆出異常
                mCamera.stopPreview();
                mCamera.release();
                mCamera = null;
                return true;
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
        return false;
    }

    @Override
    public void setPreviewTexture(SurfaceTexture surfaceTexture) {
        if (mCamera != null) {
            try {
                mCamera.setPreviewTexture(surfaceTexture);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public CameraSize getPreviewSize() {
        return mPreviewSize;
    }

    @Override
    public CameraSize getPictureSize() {
        return mPicSize;
    }
}

這里的代碼就是使用Camera來實(shí)現(xiàn)上面的功能椎麦。

  1. 因?yàn)槭褂梦覀兤谕麑amera中得到的數(shù)據(jù)傳遞到紋理上宰僧,所以需要setPreviewTexture(SurfaceTexture texture)。讓這個SurfaceTexture來承載观挎。
@Override
    public void setPreviewTexture(SurfaceTexture surfaceTexture) {
        if (mCamera != null) {
            try {
                mCamera.setPreviewTexture(surfaceTexture);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
  1. 選擇相機(jī)的預(yù)覽的尺寸和旋轉(zhuǎn)的角度
    相機(jī)的parameter的選擇琴儿,只要選對了對應(yīng)的想要的比例就行了。沒有其他需要的嘁捷。
    設(shè)備坐標(biāo)和紋理坐標(biāo)之間的方向不同問題造成,由后面紋理的矩陣來控制就好了。

SurfaceTexture

可以從圖像流中捕獲幀作為OpenGL ES紋理雄嚣。

  1. 直接使用創(chuàng)建的紋理晒屎,來創(chuàng)建SurfaceTexture就可以了。
mSurfaceTexture = new SurfaceTexture(mTextureId);
  1. 然后再將其設(shè)置給Camera.同時每次SurfaceTexture刷新的時候缓升,都必須刷新GLSurfaceView鼓鲁。
  mCameraApi.setPreviewTexture(mCameraDrawer.getSurfaceTexture());
  //默認(rèn)使用的GLThread.每次刷新的時候,都強(qiáng)制要求是刷新這個GLSurfaceView
  mCameraDrawer.getSurfaceTexture().setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
            @Override
            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
                requestRender();
            }
        });
注意事項

使用時必須要注意的是

  1. 紋理對象使用GL_TEXTURE_EXTERNAL_OES紋理目標(biāo)港谊,該目標(biāo)由GL_OES_EGL_image_externalOpenGL ES擴(kuò)展定義骇吭。
    每次綁定紋理時,它必須綁定到GL_TEXTURE_EXTERNAL_OES目標(biāo)而不是GL_TEXTURE_2D目標(biāo)封锉。在OpenGL ES 2.0著色器必須使用
#extension GL_OES_EGL_image_external:require
  1. 著色器還必須使用samplerExternalOES GLSL采樣器類型訪問紋理绵跷。
uniform samplerExternalOES uTexture;

GLSurfaceView.Render

GLSL部分
  • oes_base_vertex.glsl
attribute vec4 aPosition;
attribute vec2 aCoordinate;
uniform mat4 uMatrix;
uniform mat4 uCoordinateMatrix;
varying vec2 vTextureCoordinate;

void main(){
    gl_Position = uMatrix*aPosition;
    vTextureCoordinate = (uCoordinateMatrix*vec4(aCoordinate,0.1,0.1)).xy;
}

頂點(diǎn)著色器對比相對簡單膘螟。這里需要注意的就是矩陣相乘的順序問題。

//這個是正確的順序碾局。相稱的順序相反荆残,圖像是反的!>坏薄内斯!
gl_Position = uMatrix*aPosition;
  • oes_base_fragment.glsl
    這里就是如上面注意事項中說的。必須使用samplerExternalOES來采樣像啼。
#extension GL_OES_EGL_image_external : require
precision mediump float;

varying vec2 vTextureCoordinate;
uniform samplerExternalOES uTexture;
void main() {
    gl_FragColor = texture2D(uTexture,vTextureCoordinate);
}
其他的部分

其他的部分和前幾編文章中提到的相差不多俘闯。

0. 生成紋理

這里就是上面所說的。只能用GLES11Ext.GL_TEXTURE_EXTERNAL_OES這種紋理忽冻。

 private int genOesTextureId() {
        int[] textureObjectId = new int[1];
        GLES20.glGenTextures(1, textureObjectId, 0);
        //綁定紋理
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureObjectId[0]);
        //設(shè)置放大縮小真朗。設(shè)置邊緣測量
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
        return textureObjectId[0];
    }
1. 視圖矩陣。

因?yàn)樵O(shè)備坐標(biāo)和紋理的坐標(biāo)不同僧诚。而前置攝像頭和后置攝像頭的翻轉(zhuǎn)的方向也不同遮婶。所以要做下面的處理

   //計算需要變化的矩陣
    private void calculateMatrix() {
        //得到通用的顯示的matrix
        Gl2Utils.getShowMatrix(mModelMatrix, mPreviewWidth, mPreviewHeight, this.mSurfaceWidth, this.mSurfaceHeight);

        if (mCameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) {  //前置攝像頭
            Gl2Utils.flip(mModelMatrix, true, false);
            Gl2Utils.rotate(mModelMatrix, 90);
        } else {  //后置攝像頭
            int rotateAngle = 270;
            Gl2Utils.rotate(mModelMatrix, rotateAngle);
        }
        mOesFilter.setMatrix(mModelMatrix);
    }
  1. 得到標(biāo)準(zhǔn)的透視視圖矩陣
 public static void getShowMatrix(float[] matrix,int imgWidth,int imgHeight,int viewWidth,int
        viewHeight){
        if(imgHeight>0&&imgWidth>0&&viewWidth>0&&viewHeight>0){
            float sWhView=(float)viewWidth/viewHeight;
            float sWhImg=(float)imgWidth/imgHeight;
            float[] projection=new float[16];
            float[] camera=new float[16];
            if(sWhImg>sWhView){
                Matrix.orthoM(projection,0,-sWhView/sWhImg,sWhView/sWhImg,-1,1,1,3);
            }else{
                Matrix.orthoM(projection,0,-1,1,-sWhImg/sWhView,sWhImg/sWhView,1,3);
            }
            Matrix.setLookAtM(camera,0,0,0,1,0,0,0,0,1,0);
            Matrix.multiplyMM(matrix,0,projection,0,camera,0);
        }
    }

這部分就是標(biāo)準(zhǔn)的處理方式了。誰的比例大湖笨,用誰的旗扑。

  1. 處理不同攝像頭的旋轉(zhuǎn)
    如果是前置攝像頭的話,需要進(jìn)行左右的翻轉(zhuǎn)慈省。然后旋轉(zhuǎn)90度臀防。
    后置攝像頭的話,只需要旋轉(zhuǎn)270度就可以了边败。
2. 繪制圖形

重溫一下繪制整體的流程

//draw step
public void draw() {
   //step0 clear
   onClear();
   //step1 use program
   onUseProgram();
   //step2 active and bind custom data
   onSetExpandData();
   //step3 bind texture
   onBindTexture();
   //step4 normal draw
   onDraw();
 }
  • onClear
   GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
   GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

這里有一個疑問袱衷。這里其實(shí)不用每次都清除的。只要初始化清除就可以了吧笑窜?

  • onUseProgram
    這一步祟昭,就是使用我們之前已經(jīng)創(chuàng)建和link好的program
 private void onUseProgram() {
        GLES20.glUseProgram(mProgram);
    }
  • onSetExpandData
    這里做的其實(shí)就是我們額外給glsl添加的屬性。應(yīng)用上面我們變化的矩陣怖侦。
 private void onSetExpandData() {
        GLES20.glUniformMatrix4fv(mUMatrix, 1, false, matrix, 0);
        GLES20.glUniformMatrix4fv(mUCoordMatrix, 1, false, mCoordMatrix, 0);
 }
  • onBindTexture
    接著就是激活和綁定紋理數(shù)據(jù).
 private void onBindTexture() {
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + getTextureType());
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, getTextureId());
        GLES20.glUniform1i(mUTexture, getTextureType());
    }

我們的紋理都掛載在GLES20.GL_TEXTURE0 + getTextureType()上。同時一定要注意的是谜叹,相機(jī)這里需要的是GLES11Ext.GL_TEXTURE_EXTERNAL_OES這種拓展類型的紋理采樣匾寝。

  • onDraw
    繪制圖像的話,同之前相同荷腊,只需要繪制一個長方形就可以了艳悔。
 private void onDraw() {
        //設(shè)置定點(diǎn)數(shù)據(jù)
        GLES20.glEnableVertexAttribArray(mAPosition);
        GLES20.glVertexAttribPointer(
                mAPosition,
                2,
                GLES20.GL_FLOAT,
                false,
                0,
                mVerBuffer);
        //
        GLES20.glEnableVertexAttribArray(mACoord);
        GLES20.glVertexAttribPointer(
                mACoord,
                2,
                GLES20.GL_FLOAT,
                false,
                0,
                mTextureCoordinate);
        //繪制三角形帶
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        GLES20.glDisableVertexAttribArray(mAPosition);
        GLES20.glDisableVertexAttribArray(mACoord);
    }

GLSurfaceView

最后在GLSurfaceView的對應(yīng)的生命周期內(nèi)調(diào)用方法就可以了~~

錄制


整體流程理解

錄制的整體流程.png

在原來預(yù)覽的基礎(chǔ)上,我們需要加入MediaCodec進(jìn)行視頻編碼女仰。

  1. 圖中的EglCore著保存EglContext猜年。EglSurfaceEglConfig的配置抡锈。WindowSurface就是將EglContextSurface相互關(guān)聯(lián)的幫助類。

  2. Encoder是在EncoderThread中進(jìn)行乔外。兩個線程(和原來的GLTHread)需要共享EGLContext床三。

  3. 使用MediaCodec進(jìn)行視頻編碼,只要通過它的InputSurface杨幼,將數(shù)據(jù)輸入就可以撇簿。所以通過共享的EGLContext,來創(chuàng)建一個WindowSurface差购。然后再通過在該線程內(nèi)GL的draw方法四瘫,就可以將EGLContext中的Oes紋理,繪制到Surface上欲逃。這樣MediaCodec就可以得到數(shù)據(jù)找蜜。

  4. 整體流向
    當(dāng)我們接受到frame時,我們需要

  • GLSurfaceView的渲染線程,將數(shù)據(jù)渲染到SurfaceView
  • Encoder的線程稳析,將frame渲染到MediacodecInputSurface
Camera==>
  SurfaceTexture.onFrameAvailable==>
  GLSurfaceView.requestRender ==>
  {
    //通知更新下一幀
    mSurfaceTexture.updateTexImage()
    //在`Encoder`的線程洗做,將`frame`渲染到`Mediacodec`的`InputSurface`中。通知編碼器線程繪制并編碼
    mVideoEncoder.frameAvailable(mSurfaceTexture) ==>
      {
        //通知編碼器進(jìn)行編碼
        mVideoEncoder.drainEncoder(false);
        //刷入數(shù)據(jù)
        mFullScreen.drawFrame(mTextureId, transform);
        //給InputWindow設(shè)置時間戳
        mInputWindowSurface.setPresentationTime(timestampNanos);
        //刷新之后迈着,編碼器得到數(shù)據(jù)竭望?
        mInputWindowSurface.swapBuffers();
      }
    //同時Render繪制到屏幕上。在`GLSurfaceView`的渲染線程,將數(shù)據(jù)渲染到`SurfaceView`
    mOesFilter.draw();
  }

各個部分詳解

TextureMovieEncoder

主要還是添加了一個這個類裕菠。

理想狀態(tài)下咬清,我們創(chuàng)建Video Encoder,然后為它創(chuàng)建EGLContext奴潘,然后將這個context傳入GLSurfaceView來共享旧烧。 但是這里的Api做不到這樣,所以我們只能反著來画髓。當(dāng)GLSurfaceView torn down時掘剪,(可能時我們旋轉(zhuǎn)了屏幕),EGLContext也會同樣被拋棄奈虾。這樣意味這當(dāng)它回來的時候夺谁,我們就需要重新為Video encoder創(chuàng)建EGLContext.(而且,"preserve EGLContext on pause" 這樣的功能肉微,也不啟作用匾鸥。就是上一個暫停狀態(tài)的EGLContext,在這里也不能用)我們可以通過使用TextureView 來替代GLSurfaceView來做一些簡化碉纳。但是這樣會由一點(diǎn)性能的問題勿负。

創(chuàng)建EGL環(huán)境
獲取EGLContext

可以直接在GLThread中通過EGL14.eglGetCurrentContext(),就可以得到和線程綁定的EGLContext(EGLContext其實(shí)也是存在于ThreadLocal當(dāng)中)

 private void startRecord() {
        mOutputFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "camera-test" + System.currentTimeMillis() + ".mp4");
        Log.d(TAG, "file path = " + mOutputFile.getAbsolutePath());
        // start recording
        mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
                mOutputFile, mPreviewHeight, mPreviewWidth, 1000000, EGL14.eglGetCurrentContext()));
        mRecordingStatus = RECORDING_ON;
    }
線程的通信是通過Handler來完成。
public void startRecording(EncoderConfig config) {
        Log.d(TAG, "Encoder: startRecording()");
        //mReadyFence 這個鎖是來鎖這個線程的所有操作的劳曹。包括開始奴愉。停止琅摩。繪制。
        synchronized (mReadyFence) {
            if (mRunning) {
                Log.w(TAG, "Encoder thread already running");
                return;
            }
            mRunning = true;
            new Thread(this, "TextureMovieEncoder").start();
            while (!mReady) {
                try {
                    mReadyFence.wait();
                } catch (InterruptedException ie) {
                    // ignore
                }
            }
        }

        mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
    }
創(chuàng)建WindowSurface锭硼。將EGLContextEncoder.InputSurface關(guān)聯(lián)在一起
 private void prepareEncoder(EGLContext sharedContext, int width, int height, int bitRate,
                                File outputFile) {
        try {
            //這個就算MediaCodec的封裝房资。包括MediaCodec進(jìn)行編碼。MediaMuxer進(jìn)行視頻封裝
            mVideoEncoder = new VideoEncoderCore(width, height, bitRate, outputFile);
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
        //通過EglContext創(chuàng)建EglCore
        mEglCore = new EglCore(sharedContext, EglCore.FLAG_RECORDABLE);
        //創(chuàng)建inputWindowSurface
        mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
        //在完成EGL的初始化之后,需要通過eglMakeCurrent()函數(shù)來將當(dāng)前的上下文切換,這樣opengl的函數(shù)才能啟動作用账忘。
        mInputWindowSurface.makeCurrent();

        mFullScreen = new FullFrameRect(
                new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
    }

這里進(jìn)行了一系列的初始化工作志膀。

  • 初始化了VideoEncoderCore。它是MediaCodecMediaMuxer的封裝鳖擒。
public VideoEncoderCore(int width, int height, int bitRate, File outputFile)
            throws IOException {
        //MediaCodec的BufferInfo的緩存溉浙。通過這個BufferInfo不斷的運(yùn)輸數(shù)據(jù)。(原始=>編碼后的)
        mBufferInfo = new MediaCodec.BufferInfo();
        //創(chuàng)建MediaFormat MIME_TYPE = "video/avc"
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);

        //設(shè)置我們想要的參數(shù)蒋荚。如果參數(shù)不合法的話戳稽,在configure時,就會報錯的
        //這個ColorFormat很重要期升,這里一定要設(shè)置COLOR_FormatSurface
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        //這里的設(shè)置5 seconds 在相鄰的 I-frames惊奇,why?
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
        if (VERBOSE) Log.d(TAG, "format: " + format);

        //創(chuàng)建編碼器
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        //得到對應(yīng)InputSureface
        mInputSurface = mEncoder.createInputSurface();
        //啟動
        mEncoder.start();

        //創(chuàng)建MediaMuxer。我們不能直接在這里開始muxer.因?yàn)镸ediaFormat 還沒得到輸入播赁。必須要在編碼器得到輸入之后颂郎,才能添加。這里先不添加音頻容为。
        mMuxer = new MediaMuxer(outputFile.toString(),
                MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        mTrackIndex = -1;
        mMuxerStarted = false;
    }
  • 初始化了EglCore乓序。主要是管理EGL的state,包括 (display, context, config)坎背。
  //主要是初始化display 和EglContext
  public EglCore(EGLContext sharedContext, int flags) {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("EGL already set up");
        }

        if (sharedContext == null) {
            sharedContext = EGL14.EGL_NO_CONTEXT;
        }
        //先創(chuàng)建一個默認(rèn)的Display
        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("unable to get EGL14 display");
        }
        //檢查是否創(chuàng)建成功
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            mEGLDisplay = null;
            throw new RuntimeException("unable to initialize EGL14");
        }

        // Try to get a GLES3 context, if requested.
        if ((flags & FLAG_TRY_GLES3) != 0) {
            //Log.d(TAG, "Trying GLES 3");
            EGLConfig config = getConfig(flags, 3);
            if (config != null) {
                int[] attrib3_list = {
                        EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
                        EGL14.EGL_NONE
                };
                EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
                        attrib3_list, 0);

                if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
                    //Log.d(TAG, "Got GLES 3 config");
                    mEGLConfig = config;
                    mEGLContext = context;
                    mGlVersion = 3;
                }
            }
        }
        if (mEGLContext == EGL14.EGL_NO_CONTEXT) {  // GLES 2 only, or GLES 3 attempt failed
            //Log.d(TAG, "Trying GLES 2");
            //獲取GL的Config
            EGLConfig config = getConfig(flags, 2);
            if (config == null) {
                throw new RuntimeException("Unable to find a suitable EGLConfig");
            }
            int[] attrib2_list = {
                    EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                    EGL14.EGL_NONE
            };
            //獲取EGLContext關(guān)鍵方法就是它替劈,EGL14.eglCreateContext
            EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
                    attrib2_list, 0);
            checkEglError("eglCreateContext");
            mEGLConfig = config;
            mEGLContext = context;
            mGlVersion = 2;
        }

        // Confirm with query.
        int[] values = new int[1];
        EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
                values, 0);
        Log.d(TAG, "EGLContext created, client version " + values[0]);
    }
    
    //EGL的配置。類似鍵值對的數(shù)組得滤。
   private EGLConfig getConfig(int flags, int version) {
        int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
        if (version >= 3) {
            renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
        }

        // The actual surface is generally RGBA or RGBX, so situationally omitting alpha
        // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()
        // when reading into a GL_RGBA buffer.
        int[] attribList = {
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                //EGL14.EGL_DEPTH_SIZE, 16,
                //EGL14.EGL_STENCIL_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, renderableType,
                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]
                EGL14.EGL_NONE
        };
        if ((flags & FLAG_RECORDABLE) != 0) {
            attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
            attribList[attribList.length - 2] = 1;
        }
        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
                numConfigs, 0)) {
            Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
            return null;
        }
        return configs[0];
    }
  • 初始化了WindowSurface陨献。并通過eglMakeCurrent()函數(shù),切換到當(dāng)前的上下文懂更。
    /**
     * 將EGL和原生的window surface關(guān)聯(lián)在一起
     * 如果傳入releaseSurface為true的話眨业,當(dāng)你調(diào)用release方法時,這個Surface就會自動被release沮协。
     * 但時如果是使用了SurfaceView的Surface等坛猪,Android框架創(chuàng)建的Surface時需要注意,
     * 它會干涉原生框架的調(diào)用皂股,比如上述的SurfaceView的Surface,release之后,surfaceDestroyed()回調(diào)將不會再收到
     */
    public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
        super(eglCore);
        createWindowSurface(surface);
        mSurface = surface;
        mReleaseSurface = releaseSurface;
    }
    /**
     * 創(chuàng)建 window surface.我們的之前的信息提前就保存再EglCore內(nèi)了
     * <p>
     * @param surface May be a Surface or SurfaceTexture.
     */
    public void createWindowSurface(Object surface) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglCore.createWindowSurface(surface);
    }

    /**
     * 如果我們是為了MediaCodec創(chuàng)建命黔,那么EGLConfig需要有"recordable"的attribute.這個部分呜呐,在上面初始化EglCore時就斤,已經(jīng)完成了EGLConfig和EGLDisplay的配置
     */
    public EGLSurface createWindowSurface(Object surface) {
        if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
            throw new RuntimeException("invalid surface: " + surface);
        }

        // Create a window surface, and attach it to the Surface we received.
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        //這就是我們想要的EGLSurface的創(chuàng)建方式  EGL14.eglCreateWindowSurface
        EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
                surfaceAttribs, 0);
        checkEglError("eglCreateWindowSurface");
        if (eglSurface == null) {
            throw new RuntimeException("surface was null");
        }
        return eglSurface;
    }
  • 初始化FullFrameRect。它是OpenGl繪制命令等的封裝蘑辑。
frameAvailable

在接受到Frame時洋机,進(jìn)行編碼

mVideoEncoder.frameAvailable(mSurfaceTexture);
  • 時間戳和tranfrom矩陣
  float[] transform = new float[16];      // TODO - avoid alloc every frame
        st.getTransformMatrix(transform);
        long timestamp = st.getTimestamp();
        if (timestamp == 0) {
            // Seeing this after device is toggled off/on with power button.  The
            // first frame back has a zero timestamp.
            //
            // MPEG4Writer thinks this is cause to abort() in native code, so it's very
            // important that we just ignore the frame.
            Log.w(TAG, "HEY: got SurfaceTexture with timestamp of zero");
            return;
        }

        mHandler.sendMessage(mHandler.obtainMessage(MSG_FRAME_AVAILABLE,
                (int) (timestamp >> 32), (int) timestamp, transform));
  • 編碼
  private void handleFrameAvailable(float[] transform, long timestampNanos) {
        if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform);
        //視頻編碼
        mVideoEncoder.drainEncoder(false);
        mFullScreen.drawFrame(mTextureId, transform);
        //設(shè)置時間戳
        mInputWindowSurface.setPresentationTime(timestampNanos);
        mInputWindowSurface.swapBuffers();
    }

  /**
     * 從encoder中得到數(shù)據(jù),再寫入到muxer中洋魂。
     * 下面這段代碼就是通用的編碼的代碼了
     */
    public void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;
        if (VERBOSE) Log.d(TAG, "drainEncoder(" + endOfStream + ")");
        
        //如果通知編碼器結(jié)束绷旗,就會signalEndOfInputStream
        if (endOfStream) {
            if (VERBOSE) Log.d(TAG, "sending EOS to encoder");
            mEncoder.signalEndOfInputStream();
        }
        
        //得到outputBuffer
        ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
        //不斷循環(huán),當(dāng)讀取所有數(shù)據(jù)時
        while (true) {
            //上面換成的BufferInfo副砍,送入到Encoder中衔肢,去查詢狀態(tài)
            int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            //如果時繼續(xù)等待,就暫時不用處理豁翎。大多數(shù)情況角骤,都是從這兒跳出循環(huán)
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
                }
            //outputBuffer發(fā)生變化了。就重新去獲取
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                // not expected for an encoder
                encoderOutputBuffers = mEncoder.getOutputBuffers();
            //格式發(fā)生變化心剥。這個第一次configure之后也會調(diào)用一次邦尊。在這里進(jìn)行muxer的初始化
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                if (mMuxerStarted) {
                    throw new RuntimeException("format changed twice");
                }
                MediaFormat newFormat = mEncoder.getOutputFormat();
                Log.d(TAG, "encoder output format changed: " + newFormat);

                // now that we have the Magic Goodies, start the muxer
                mTrackIndex = mMuxer.addTrack(newFormat);
                mMuxer.start();
                mMuxerStarted = true;
            } else if (encoderStatus < 0) {
                Log.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
                        encoderStatus);
                // let's ignore it
            } else {
                //寫入數(shù)據(jù)
                ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                if (encodedData == null) {
                    throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
                            " was null");
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    if (VERBOSE) Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    if (!mMuxerStarted) {
                        throw new RuntimeException("muxer hasn't started");
                    }
                    //切到對應(yīng)的位置,進(jìn)行書寫
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    encodedData.position(mBufferInfo.offset);
                    encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
                    //寫入
                    mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                    if (VERBOSE) {
                        Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
                                mBufferInfo.presentationTimeUs);
                    }
                }
                //重新釋放优烧,為了下一次的輸入
                mEncoder.releaseOutputBuffer(encoderStatus, false);

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    //到達(dá)最后了蝉揍,就跳出循環(huán)
                    if (!endOfStream) {
                        Log.w(TAG, "reached end of stream unexpectedly");
                    } else {
                        if (VERBOSE) Log.d(TAG, "end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

添加濾鏡


整體流程理解

添加濾鏡后的整體流程.png

上面,我們是直接繪制OES的紋理畦娄。這里又沾,因?yàn)橐砑訛V鏡的效果。所以我們需要將紋理進(jìn)行處理纷责。

離屏繪制

離屏繪制.png

先將OES紋理捍掺,綁定到FrameBuffer上。同時會在FrameBuffer上綁定一個新的textureId(這里命名為OffscreenTextureId)再膳。然后調(diào)用繪制OES紋理的方法挺勿,數(shù)據(jù)就會傳遞到FBO上。而我們可以通過綁定在其上的OffscreenTextureId得到其數(shù)據(jù)喂柒。通常情況下不瓶,我們把綁定FrameBuffer和繪制這個新的OffscreenTextureId代表的紋理的過程,稱為離屏繪制灾杰。

綁定和生成FrameBuffer的時機(jī)

創(chuàng)建FrameBuffer蚊丐。因?yàn)镽enderBuffer的存儲大小要和當(dāng)前的顯示的寬和高相關(guān)。所以會在onSurfaceChanged生命周期方法時候調(diào)用艳吠。

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //在這里監(jiān)聽到尺寸的改變麦备。做出對應(yīng)的變化
        prepareFramebuffer(width, height);
        //...
    }

    //生成frameBuffer的時機(jī)
    private void prepareFramebuffer(int width, int height) {
        int[] values = new int[1];
        //申請一個與FrameBuffer綁定的textureId
        GLES20.glGenTextures(1, values, 0);
        mOffscreenTextureId = values[0];
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mOffscreenTextureId);
         // Create texture storage.
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);

        // Set parameters.  We're probably using non-power-of-two dimensions, so
        // some values may not be available for use.
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
                GLES20.GL_NEAREST);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_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);

        //創(chuàng)建FrameBuffer Object并且綁定它
        GLES20.glGenFramebuffers(1, values, 0);
        mFrameBuffer = values[0];
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);

        // 創(chuàng)建RenderBuffer Object并且綁定它
        GLES20.glGenRenderbuffers(1, values, 0);
        mRenderBuffer = values[0];
        GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, mRenderBuffer);

        //為我們的RenderBuffer申請存儲空間
        GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);

        // 將renderBuffer掛載到frameBuffer的depth attachment 上。就上面申請了OffScreenId和FrameBuffer相關(guān)聯(lián)
        GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, mRenderBuffer);
        // 將text2d掛載到frameBuffer的color attachment上
        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, mOffscreenTextureId, 0);

        // See if GLES is happy with all this.
        int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
        if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
            throw new RuntimeException("Framebuffer not complete, status=" + status);
        }
        // 先不使用FrameBuffer,將其切換掉凛篙。到開始繪制的時候黍匾,在綁定回來
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
    }
    
    //在onDrawFrame中添加代碼
    @Override
    public void onDrawFrame(GL10 gl) {
        //...省略

        //重新切換到FrameBuffer上。
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffer);
        //這里的繪制呛梆,就會將數(shù)據(jù)掛載到FrameBuffer上了锐涯。
        mOesFilter.draw();
        //解除綁定,結(jié)束FrameBuffer部分的數(shù)據(jù)寫入
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
        
        //....省略
    }


  • FrameBuffer 幀緩沖對象
    openGL繪制流程.png

我們自己創(chuàng)建的FrameBuffer其實(shí)只是一個容器填物。所以我們要將數(shù)據(jù)掛載上去纹腌,它才算是完整。

FrameBuffer.png

所以滞磺,我們可以看到申請F(tuán)rameBuffer需要進(jìn)行下面的三步

  1. 生成一個FrameBuffer
  2. 申請一個RenderBuffer,并且掛載GL_DEPTH_ATTACHMENT上升薯。

RenderBuffer也是一個渲染緩沖區(qū)對象。RenderBuffer對象是新引入的用于離屏渲染雁刷。它允許將場景直接渲染到Renderbuffer對象覆劈,而不是渲染到紋理對象。
Renderbuffer只是一個包含可渲染內(nèi)部格式的單個映像的數(shù)據(jù)存儲對象沛励。它用于存儲沒有相應(yīng)紋理格式的OpenGL邏輯緩沖區(qū)责语,如模板或深度緩沖區(qū)。

  1. 申請一個textureId,掛載到GL_COLOR_ATTACHMENT0上目派。
  2. 重新切換到FrameBuffer上(綁定)坤候,然后繪制。

我們就可以通過這個紋理企蹭,得到保存在FBO上的數(shù)據(jù)了

添加濾鏡的繪制
添加濾鏡.png

我們可以通過FBO白筹,進(jìn)行濾鏡處理。我們將得到的數(shù)據(jù)谅摄,再次進(jìn)行繪制徒河,在這次的繪制中,我們就可以添加上我們想要的濾鏡處理了送漠。

但是這里不僅僅是要繪制到屏幕上顽照,同時要在開啟錄制的時候,輸入給Encoder進(jìn)行視頻的編碼和封裝闽寡。
所以我們需要將數(shù)據(jù)再寫寫入一個新的FrameBuffer中聪廉,然后再其輸出的outputTexture中中姜,就可以得到應(yīng)用了紋理的數(shù)據(jù)了。

  • 濾鏡處理
    就算將上面的OffscreenTextureId作為這里濾鏡的輸入Id.
    @Override
    public void onDrawFrame(GL10 gl) {
         //...省略
        //經(jīng)過路徑處理
        mColorFilter.setTextureId(mOffscreenTextureId);
        mColorFilter.onDrawFrame();
        int outputTextureId = mColorFilter.getOutputTextureId();
        //...省略
    }

同時濾鏡內(nèi)肢扯,也按照上面的FrameBuffer的處理流程喘沿。將數(shù)據(jù)掛載到FrameBuffer上吁脱。得到掛載在FrameBuffer上的outputTextureId

代碼同上涯保,省略
將應(yīng)用了濾鏡的紋理分別繪制到GLViewEncoder當(dāng)中
image.png
@Override
    public void onDrawFrame(GL10 gl) {
        //省略...
        //經(jīng)過濾鏡處理
        mColorFilter.setTextureId(mOffscreenTextureId);
        mColorFilter.onDrawFrame();
        int outputTextureId = mColorFilter.getOutputTextureId();

        //將得到的outputTextureId一死,輸入encoder,進(jìn)行編碼
        mVideoEncoder.setTextureId(outputTextureId);

        mVideoEncoder.frameAvailable(mSurfaceTexture);

        //將得到的outputTextureId,再次Draw,因?yàn)闆]有FrameBuffer,所以這次Draw的數(shù)據(jù)鹿响,就直接到了Surface上了。
        mShowFilter.setTextureId(outputTextureId);
        mShowFilter.onDrawFrame();
    }
  1. 將得到的outputTextureId支救,輸入EncoderInputSurface中,通知內(nèi)部進(jìn)行draw 和進(jìn)行編碼抢野。
  2. 將得到的outputTextureId,再次Draw,因?yàn)闆]有FrameBuffer,所以這次draw的數(shù)據(jù),就直接到了Surface上了各墨。也就是直接繪制到了我們的GLSurfaceView上了。

小結(jié)

  1. 對比之前繪制流程启涯。上文是直接將紋理繪制到了GLView上顯示贬堵,而這里是將紋理繪制到綁定的FrameBuffer中,而且
    繪制的結(jié)果不直接顯示出來结洼。所以可以形象的理解離屏繪制黎做,就是將繪制的結(jié)果保存在與FrameBuffer綁定的一個新的textureIdOffscreenTextureId)中,不直接繪制到屏幕上松忍。
  2. 把握好整體流程之后蒸殿,這部分的處理也會變得簡單起來。后面就可以如何添加更加炫酷的濾鏡和玩法了鸣峭。

最后

整編文章就重要的部分還是在理解整個紋理中數(shù)據(jù)傳遞的路線宏所。
后續(xù),會對這里的相機(jī)的預(yù)覽添加其他的濾鏡摊溶。
Demo位置
https://github.com/deepsadness/ZeroToOpenGL

系列文章地址
Android OpenGL ES(一)-開始描繪一個平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面圖形
Android OpenGL ES(四)-為平面圖添加濾鏡
Android OpenGL ES(五)-結(jié)合相機(jī)進(jìn)行預(yù)覽/錄制及添加濾鏡
Android OpenGL ES(六) - 將輸入源換成視頻
Android OpenGL ES(七) - 生成抖音照片電影

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末爬骤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子莫换,更是在濱河造成了極大的恐慌霞玄,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拉岁,死亡現(xiàn)場離奇詭異坷剧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)喊暖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門惫企,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哄啄,你說我怎么就攤上這事雅任。” “怎么了咨跌?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵沪么,是天一觀的道長。 經(jīng)常有香客問我锌半,道長禽车,這世上最難降的妖魔是什么寇漫? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮殉摔,結(jié)果婚禮上州胳,老公的妹妹穿的比我還像新娘。我一直安慰自己逸月,他們只是感情好栓撞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碗硬,像睡著了一般瓤湘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恩尾,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天弛说,我揣著相機(jī)與錄音,去河邊找鬼翰意。 笑死木人,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冀偶。 我是一名探鬼主播醒第,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔫磨!你這毒婦竟也來了淘讥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤堤如,失蹤者是張志新(化名)和其女友劉穎蒲列,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搀罢,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝗岖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了榔至。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抵赢。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖唧取,靈堂內(nèi)的尸體忽然破棺而出铅鲤,到底是詐尸還是另有隱情,我是刑警寧澤枫弟,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布邢享,位于F島的核電站,受9級特大地震影響淡诗,放射性物質(zhì)發(fā)生泄漏骇塘。R本人自食惡果不足惜伊履,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望款违。 院中可真熱鬧唐瀑,春花似錦、人聲如沸插爹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赠尾。三九已至柔滔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間萍虽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工形真, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杉编,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓咆霜,卻偏偏與公主長得像邓馒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛾坯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容