移動端濾鏡開發(三)OpenGL實現預覽播放效果

寫在前面的話

<p>
上一篇文章簡單介紹了OpenGl的使用,并實現了OpenGl顯示圖片的效果,但是濾鏡的效果不僅僅只用在圖片上面,一般來說現在視頻和拍照取景也是會有濾鏡的需求的,所以這一篇,就是介紹用OpenGL實現預覽效果

攝像預覽其實就是Android的Camera開發,對于Android的Camera開發,一般有兩種方式,一種是借助Intent和MediaStroe調用系統Camera App程序來實現拍照和攝像功能,另外一種是根據Camera API自寫Camera程序,自然第一種不能作為我們的濾鏡開發,所以我們采用第二種方式。

視頻播放則是使用了主要用到MediaPlayer。

兩者的實現方式基本一致

Camera預覽開發

<p>
工欲善其事必先利其器,首先我們了解一下Camera API

一.Camera API

<p>

Camera的初始化需要使用靜態方法通過API calledCamera.open提供并初始化相機對象

Camera mCamera =  Camera.open(); 

簡單看下Camera類提供的方法

  • getCameraInfo(int cameraId, Camera.CameraInfo cameraInfo) 它返回一個特定攝像機信息

  • getNumberOfCameras() 它返回限定的可用的設備上的照相機的整數

  • lock()它被用來鎖定相機,所以沒有其他應用程序可以訪問它

  • release() 它被用來釋放在鏡頭鎖定,所以其他應用程序可以訪問它

  • open(int cameraId) 它是用來打開特定相機時,支持多個攝像機

  • enableShutterSound(boolean enabled) 它被用來使能/禁止圖像俘獲的默認快門聲音

  • startPreview() 開始預覽

  • startFaceDetection() 此功能啟動人臉檢測相機

  • stopFaceDetection() 它是用來阻止其通過上述功能啟用的臉部檢測

  • startSmoothZoom(int value) 這需要一個整數值,并調整攝像機的焦距非常順暢的值

  • stopSmoothZoom() 它是用來阻止攝像機的變焦

  • stopPreview() 它是用來阻止相機的預覽給用戶

  • takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback
    raw, Camera.PictureCallback jpeg)
    它被用來使能/禁止圖像拍攝的默認快門聲音

實現預覽效果呢其實有好幾種方法,主要都是與SurfaceView,GLSurfaceView,SurfaceTexture,TextureView這幾個有關,那這里就簡單介紹下這幾個區別

SurfaceView, GLSurfaceView, SurfaceTexture, TextureView的區別

由于這里的東西其實牽扯很多,就不做太詳細的介紹,有興趣的可以自行百度,我就做簡單的區分介紹

關鍵字:View
SurfaceView
GLSurfaceView
TextureView
這三個的后綴都是View,所以這三個都是用來顯示的

SurfaceView是Android1.0(API level1)時期就存在的,雖然是繼承于View,但是他包含一個Surface模塊(簡單地說Surface對應了一塊屏幕緩沖區,每個window對應一個Surface,任何View都是畫在Surface上的,傳統的view共享一塊屏幕緩沖區,所有的繪制必須在UI線程中進行),所以SurfaceView與普通View的區別就在于他的渲染在單獨的線程的,這對于一些游戲、視頻等性能相關的應用非常有益,因為它不會影響主線程對事件的響應。同時由于這個特性它的顯示也不受View的屬性控制,所以不能進行平移,縮放等變換,也不能放在其它ViewGroup中,一些View中的特性也無法使用。

GLSurfaceView從Android 1.5(API level 3)開始加入,它的加入是為了解決SurfaceView渲染線程要單獨寫導致的統一性不好的狀態,在SurfaceView的基礎上,它加入了EGL的管理,并自帶了渲染線程。另外它定義了用戶需要實現的Render接口,提供了用Strategy pattern更改具體Render行為的靈活性。作為GLSurfaceView的Client,只需要將實現了渲染函數的Renderer的實現類設置給GLSurfaceView即可。概括一句話就是 使用了模板的 SurfaceView。

TextureView在4.0(API level 14)中引入。TextureView重載了draw()方法,其中主要把SurfaceTexture中收到的圖像數據作為紋理更新到對應的HardwareLayer中。所以TextureView必須在硬件加速的窗口中。因為TextureView不包含Surface,所以其實就是一個普通的View,可以和其它普通View一樣進行移動,旋轉,縮放,動畫等變化。

關鍵字:Texture
SurfaceTexture

SurfaceTexture從Android 3.0(API level 11)加入。和SurfaceView不同的是,它對圖像流的處理并不直接顯示,而是轉為GL外部紋理,因此可用于圖像流數據的二次處理(如Camera濾鏡,桌面特效等)。

接下來就來實現預覽效果,分別從簡單到復雜

二.SurfaceView實現預覽效果

<p>
首先我們來創建一個相機預覽加載View,該類繼承SurfaceView.Callback接口類,并且需要實現里面的接口方法以便監聽SurfaceView控件的創建以及銷毀事件的回調,在回調方法中關聯相機預覽顯示。

如下

  public class CameraView extends SurfaceView implements SurfaceHolder.Callback{

    private SurfaceHolder holder;
    private Camera mCamera;

    public CameraView(Context context) {
        this(context,null);
    }

    public CameraView(Context context, AttributeSet attrs) {
        super(context, attrs);
        holder = getHolder();
        holder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            mCamera = getCameraInstance();
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        }catch (IOException e){

        }
    }

    public static Camera getCameraInstance(){
        Camera c = null;
        try {
            c = Camera.open(); // attempt to get a Camera instance
        }
        catch (Exception e){
            // Camera is not available (in use or does not exist)
        }
        return c; // returns null if camera is unavailable
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

    public void  releaseCamera(){
        mCamera.release();
    }
}

接下來就是將這個View放到布局文件中,當onPause時候調用View的releaseCamera方法即可

運行如下

圖1 Camera預覽圖

雖然是顯示出來了,但是顯示的界面確實不正常的,這是為什么呢?

手機Camera的圖像數據都是來自于攝像頭硬件的圖像傳感器(Image Sensor),這個Sensor被固定到手機之后是有一個默認的取景方向的,這個方向如下圖所示,坐標原點位于手機橫放時的左上角:

圖2 Camera的圖像數據方向

所以呢我們需要使用camera的setDisplayOrientation方法來調整方向,在初始化的時候mCamera.setDisplayOrientation(90);即可

運行如下:

圖3 Camera預覽圖
三.TextureView實現預覽效果

<p>

同樣我們創建一個相機預覽加載View,該類繼承TextureView與TextureView.SurfaceTextureListener接口類,在onSurfaceTextureAvailable方法內,初始化Camera類,并設置Camera的setPreviewTexture方法為onSurfaceTextureAvailable方法參數中的SurfaceTexture即可

代碼如下:

public class CameraTextureView extends TextureView implements TextureView.SurfaceTextureListener{

    Context mContext;
    private Camera mCamera;


    public CameraTextureView(Context context){
        super(context);
        mContext = context;
        this.setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {

        try {
            mCamera = Camera.open();
            mCamera.setDisplayOrientation(90);
            mCamera.setPreviewTexture(surface);
            mCamera.startPreview();

        } catch (IOException t) {
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        mCamera.stopPreview();
        mCamera.release();
        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }
}

接下來就是將這個View放到布局文件中

運行如下

圖4 Camera預覽圖
四.GLSurfaceView實現預覽效果

<p>

GLSurfaceView和前面兩個預覽View不同的是,他需要拿到數據自己進行渲染預覽,大致流程如下:

GLSurfaceView->setRender->onSurfaceCreated回調方法中構造一個SurfaceTexture對象,然后設置到Camera預覽中->SurfaceTexture中的回調方法onFrameAvailable來得知一幀的數據準備好了->requestRender通知Render來繪制數據->在Render的回調方法onDrawFrame中調用SurfaceTexture的updateTexImage方法獲取一幀數據,然后開始使用GL來進行繪制預覽。

OK接下來就是用代碼來實現這個流程了

其實這里就是將之前的OpenGl繪制圖片與上面的Camera相關的應用結合到一起就可以實現相關的功能

在OpenGl繪制圖片基礎上我們進行相關修改

1.添加Camera相關

Camera相關其實和前面一樣我們這里要給Camera對象一個SurfaceTexture,這個SurfaceTexture我們需要自己創建,在onSurfaceCreated中出創建這個對象,并設置當Camera有新數據的回調函數,如下

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    ...
    surfaceTexture = new SurfaceTexture(0);
    surfaceTexture.setOnFrameAvailableListener(onFrameAvailableListener);
}

private SurfaceTexture.OnFrameAvailableListener onFrameAvailableListener = new SurfaceTexture.OnFrameAvailableListener() {

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        requestRender();
    }
};

這個回調函數作用是當有新的Camera數據時候去讓OpenGl的onDrawFrame調用,進行重新繪制

接下來就是在onSurfaceChanged去打開Camera,如下

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {

  openCamera();

}

private Camera mCamera;

private void openCamera(){
    try {
        mCamera = getCameraInstance();
        mCamera.setPreviewTexture(surfaceTexture);
        mCamera.startPreview();
    }catch (IOException e){

    }
}

public static Camera getCameraInstance(){
    Camera c = null;
    try {
        c = Camera.open(); // attempt to get a Camera instance
    }
    catch (Exception e){
        // Camera is not available (in use or does not exist)
    }
    return c; // returns null if camera is unavailable
}

到這里關于Camera相關的設置就已經完畢了,接下來就是獲取數據去顯示了

2.片段著色器程序修改

當Camera有新的數據則會通過OnFrameAvailableListener通知到我們,然后我們會通過requestRender()來觸發OpenGl的重繪

我們通過surfaceTexture.updateTexImage()來獲取新的Camera預覽數據

這個方法的官網解釋如下:

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

大意是,從圖像流中更新紋理圖像到最近的幀中。這個函數僅僅當擁有這個紋理的Opengl ES上下文當前正處在繪制線程時被調用。它將隱式的綁定到這個擴展的GL_TEXTURE_EXTERNAL_OES 目標紋理

所以我們需要更改我們的片段著色器程序如下

#extension GL_OES_EGL_image_external : require

precision mediump float;
varying vec2 v_texCoord;
uniform samplerExternalOES s_texture;

void main() 
  gl_FragColor = texture2D( s_texture, v_texCoord )
}

并且在onDrawFrame()方法內部調用surfaceTexture.updateTexImage(),即可以達到預覽效果

@Override
public void onDrawFrame(GL10 gl) {
  ...
  if (surfaceTexture == null)
      return;
  surfaceTexture.updateTexImage();
  ...
}       

運行如下

圖5 camera預覽圖

可以看到這里視頻是不對的,通過設置setDisplayOrientation也沒有效果,我們只能通過去修改頂點著色器位置與片段著色器位置來進行修正了

修改如下

private static final float[] VERTEX = {
    -1.0f, 1.0f, 0.0f,
    -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f,
    1.0f, 1.0f, 0.0f,
};  

private static final float[] UV_TEX_VERTEX = {  
    0.0f, 1.0f,
    1.0f, 1.0f,
    1.0f, 0.0f,
    0.0f, 0.0f,
};

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);

接下來運行,如下

圖6 正確的camera預覽圖

到這里GLSurfaceView實現預覽效果就完成啦

視頻播放開發

<p>
前面實現了相機預覽效果,這里則是來實現視頻播放效果,其實SurfaceView這些設計之初就是為了解決視頻播放,游戲等問題,所以我們可以來看一下之前相機預覽的方法是否適用于這里。如果可以適用的話,那么就簡單很多了。

我們看上面的三種方式,主要是用了Camera的方法mCamera.setPreviewTexture(surface)與mCamera.setPreviewDisplay(holder),所以只要視頻播放也有相同的方法就可以了。

視頻播放主要用到MediaPlayer,值得高興的是MediaPlayer中確實是有上述的兩種方法,那么這樣我們就可以用上一篇文章相同的方式來實現播放效果了

一.SurfaceView實現視頻播放效果

<p>

這里其實和之前的流程一樣,只是將之前的相機預覽相關改成視頻播放,這里就直接將代碼貼上來了,不做過多的介紹

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback{

    private SurfaceHolder holder;

    private MediaPlayer mediaPlayer;

    public final String videoPath = Environment.getExternalStorageDirectory().getPath()+"/one.mp4";

    public CameraSurfaceView(Context context) {
        this(context, null);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        holder = getHolder();
        holder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (mediaPlayer == null) {
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                }
            });
            mediaPlayer.setDisplay(holder);
            try {
                mediaPlayer.setDataSource(videoPath);
                mediaPlayer.prepareAsync();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            mediaPlayer.start();
        }
    }


    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

}

運行如下

圖1 SurfaceView 實現視頻播放
二.TextureView實現視頻播放效果

<p>

同上,代碼如下

public class CameraTextureView extends TextureView implements TextureView.SurfaceTextureListener{

    Context mContext;

    private MediaPlayer mediaPlayer;

    public final String videoPath = Environment.getExternalStorageDirectory().getPath()+"/one.mp4";


    public CameraTextureView(Context context){
        super(context);
        mContext = context;
        this.setSurfaceTextureListener(this);
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {

        if (mediaPlayer == null) {
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                }
            });
            Surface surfaces = new Surface(surface);
            mediaPlayer.setSurface(surfaces);
            surfaces.release();
            try {
                mediaPlayer.setDataSource(videoPath);
                mediaPlayer.prepareAsync();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            mediaPlayer.start();
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {

        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }
}

運行如下

圖2 TextureView實現視頻播放
三.GLSurfaceView實現視頻播放效果

<p>

這里和之前基本一樣只需要將OpenCamera部分的代碼改成打開MediaPlayer就好了,這里我就貼上這一部分的代碼,如下

private MediaPlayer mediaPlayer;

public final String videoPath = Environment.getExternalStorageDirectory().getPath()+"/one.mp4";


private void openMediaPlayer(){
    if (mediaPlayer == null) {
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                mp.start();
            }
        });
        Surface surface = new Surface(surfaceTexture);
        mediaPlayer.setSurface(surface);
        surface.release();
        try {
            mediaPlayer.setDataSource(videoPath);
            mediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    } else {
        mediaPlayer.start();
    }
}

接下來運行如下

圖2 GLSurfaceView實現視頻播放

寫在后面的話

<p>

這里基本把所有的實現預覽相關的方式都講解過了,但是基于要對Camera預覽的與視頻播放數據要進行處理,所以我們采用最后一種GLSurfaceView方式來作為濾鏡開發的主要方式,到這里基本就完成了所以的準備工作,包括圖片,相機預覽,視頻播放這三者都通過OpenGL方式實現了,接下來就可以開始我們的濾鏡開發了,然而OpenGL自帶的媒體效果框架就可以實現濾鏡的效果?這不是在逗我們嘛,好吧好吧,下一篇見,peace~~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容