移动端滤镜开发(三)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~~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容