Android录制视频,实现特效与滤镜的几种方式(二)

Android实现特效或滤镜预览的几种方式

前言

本文并非专业音视频领域的文章,只不过是其在 Android 方向的 Camera 硬件下结合一些常用的应用场景而已。

所以本文并不涉及到太专业的音视频知识,你只需要稍微了解一些以下知识点即可流畅阅读。

  1. Android 三种 Camera 分别如何预览,有什么区别?
  2. 三种 Camera 回调的数据 byte[] 格式有什么区别?如何转换如何旋转?
  3. 录制视频中常用的 NV21,I420,Surface 三种输入格式对哪一种COLOR_FORMAT完成编码?
  4. 如何配置 MediaCodec 的基本配置,帧率,分辨率,比特率,关键I帧的概念是否大致清楚。
  5. OpenGL的简单配置使用

了解这些之后,我们基于系统的录制 API 已经可以基本完成对应的自定义录制流程了。如果不是很了解,也可以参考看看我之前的文章或源代码,都有对应的示例。

而 OpenGL 我们一般使用 GLSurfaceView呈现 + Render渲染,一些配置都是固定的代码,对于使用滤镜绘制我们可以参考第三方的滤镜显示代码。对于GLSurface如何与Camera关联,网上很多教程(主要是我也不太了解)。

那么话接前文,既然我们最终是为了实现录制的特效直出,首先我们得实现特效啊,一般又分为滤镜,特效,贴纸等多种效果。

目前市面上大部分的效果都是基于 OpenGL 实现的,Android中可以使用GLSurfaceView呈现 + Render渲染实现的,目前 github 也有很多开源的滤镜以及一些封装。

下面一起看看都有哪几种实现方式。

一、直接用 GPUImage 图片展示

这里以大名鼎鼎的第三方特效库 GupImage为例 【传送门】

如果要以图片的方式展示特效,我们可以用最简单的方案,使用 CameraX 配置预览,并且在回调中拿到Image对象,对其进行处理。

转换YUV数据,旋转YUV数据,转换BitMap数据,然后通过GPUImage转换特效图片,展示到ImageView上。

我们以之前我们封装的 CameraX 代码为例:

kotlin 复制代码
  fun setUpCamera(context: Context, surfaceProvider: Preview.SurfaceProvider) {

        //获取屏幕的分辨率与宽高比
        val displayMetrics = context.resources.displayMetrics
        val screenAspectRatio = aspectRatio(displayMetrics.widthPixels, displayMetrics.heightPixels)

        val cameraProviderFuture = ProcessCameraProvider.getInstance(context)

        cameraProviderFuture.addListener({

            mCameraProvider = cameraProviderFuture.get()

            //镜头选择
            mLensFacing = lensFacing
            mCameraSelector = CameraSelector.Builder().requireLensFacing(mLensFacing).build()

            //预览对象
            val preview: Preview = Preview.Builder()
                .setTargetAspectRatio(screenAspectRatio)
                .build()

            preview.setSurfaceProvider(surfaceProvider)

            val imageAnalysis =  ImageAnalysis.Builder()
                .setTargetAspectRatio(screenAspectRatio)
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()

            // 在每一帧上应用颜色矩阵
            imageAnalysis.setAnalyzer(mExecutorService, object : ImageAnalysis.Analyzer {
                @SuppressLint("UnsafeOptInUsageError")
                override fun analyze(image: ImageProxy) {


                 // 使用C库获取到I420格式,对应 COLOR_FormatYUV420Planar
                 val yuvFrame = yuvUtils.convertToI420(image)

                 // 与MediaFormat的编码格式宽高对应
                 val yuvFrameRotate = yuvUtils.rotate(yuvFrame, 90)

                 bitmap = Bitmap.createBitmap(yuvFrameRotate.width, yuvFrameRotate.height, Bitmap.Config.ARGB_8888)
                 yuvUtils.yuv420ToArgb(yuvFrameRotate, bitmap!!)

                 mImageCallback?.invoke(image.image)

                 image.close()
                }
            });


            //绑定到页面
            mCameraProvider?.unbindAll()
            val camera = mCameraProvider?.bindToLifecycle(
                context as LifecycleOwner,
                mCameraSelector!!,
                preview,
                imageAnalysis,
            )

            val cameraInfo = camera?.cameraInfo
            val cameraControl = camera?.cameraControl

        }, ContextCompat.getMainExecutor(context))
    }

接收到Bitmap,展示到ImageView上:

markdown 复制代码
    videoCameraXRecoderUtils.setBitmapCallback {
        it?.let {

            val bitmap = gpuImage!!.getBitmapWithFilterApplied(it)

            iv_catch.post {
                iv_catch.setImageBitmap(bitmap)
            }
        }
    }

效果:

二、TextureView + GLSurfaceView 实现

虽然GIF的录制效果并不好,但是也能看出ImageView的吃力,明显比预览页面要卡顿,对于这种实时刷新渲染的画面就不是它能承载的,就不是它做的事。

那怎么办?我们用SurfaceView啊,是的 GPUImage 可以设置显示到GLSurfaceView上面,此时我们的思路就是,一个 TextureView 展示 Camera 的预览画面,然后在每一帧的回调中获取到Image对象,传递给 GPUImage ,由于GPUImage 绑定了 GlSurfaceView,此时特效的画面就展示在 Surface 上了,会不会更流畅?

我们这里以 Camera1 为例子快速展示预览页面:

先添加用于预览 Camera 的 TextureView,和用于展示特效的 GLSurfaceView。

kotlin 复制代码
private fun setupCamera(container: FrameLayout) {
        container.removeAllViews()

        textureView = TextureView(this)
        textureView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)

        gpuimage = GPUImage(this)
        val glSurfaceView = GLSurfaceView(this)
        glSurfaceView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
        gpuimage.setFilter(GPUImageSketchFilter())
        gpuimage.setScaleType(GPUImage.ScaleType.CENTER_CROP)
        gpuimage.setGLSurfaceView(glSurfaceView)

        textureView.getViewTreeObserver().addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                textureView.post { setupCameraHelper() }
                textureView.getViewTreeObserver().removeOnGlobalLayoutListener(this)
            }
        })

        glSurfaceView.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceCreated(holder: SurfaceHolder) {
                val surface = holder.surface
                YYLogUtils.w("surfaceCreated")
            }

            override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
                YYLogUtils.w("surfaceChanged")
            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {
                YYLogUtils.w("surfaceDestroyed")
            }
        })

        container.addView(textureView)
        container.addView(glSurfaceView)

    }

下面是绑定预览画面,并且更新预览画面的时候传递Image给GPUImage对象。

kotlin 复制代码
    private fun setupCameraHelper() {

        cameraHelper = CameraHelper.Builder()
            .previewViewSize(Point(textureView.measuredWidth, textureView.measuredHeight))
            .rotation(windowManager.defaultDisplay.rotation)
            .specificCameraId(Camera.CameraInfo.CAMERA_FACING_BACK)
            .isMirror(false)
            .previewOn(textureView)
            .cameraListener(object : CameraListener {
                override fun onCameraOpened(camera: Camera?, cameraId: Int, displayOrientation: Int, isMirror: Boolean) {
                }

                override fun onPreview(data: ByteArray?, camera: Camera?) {
                }

                override fun onCameraClosed() {
                }

                override fun onCameraError(e: Exception?) {
                }

                override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
                }

                override fun onSurfaceTextureUpdated() {
                    val currentFrame: Bitmap? = textureView.bitmap
                    gpuimage.setImage(currentFrame)
                    gpuimage.requestRender()
                }
            })
            .build()

        cameraHelper.start()

    }

每帧画面数据传递给了 GPUImage 对象之后,确定 render 渲染之后,我们就能展示特效的画面了,这是由于 GPUImage 库内部已经帮助我们封装了 Render 的渲染与滤镜的绘制。

效果:

这样实现的效果是比直接用ImageView好多了。

三、GLSurfaceView + Sharder 自定义实现

虽然我们实现了效果,但是一些第三方库并不能满足我们的需求,也不好扩展,我们实际开发中更多的还是自己实现一些滤镜与特效效果,或者使用音视频工程师提供好的sharder脚本。

下面就使用网上随便找的一个开源灰度滤镜来简单演示如何使用 GLSurfaceView + Sharder 的方式应用它。

我们以 CameraX 为例,定义一个自定义的 GLSurfaceView 用于 PreView 的用例,绑定到 CameraX 上。

一般我们默认使用 CameraX 的时候,是在xml中定义一个 PreviewView 的自定义View,然后把 mPreviewView.surfaceProvider 设置给 CameraX 的 Preview 预览。

如果要做到展示滤镜的效果,我们就不能使用原生的 PreviewView ,我们需要使用 GLSurfaceView 并且内部实现 Preview.SurfaceProvider 接口,把 GLSurfaceView 的 SurfaceTexture 对象暴露出去给 CameraX 用作预览。

在前文 CameraX 的预览封装工具类中,我再结合 GLSurfaceView 的定义,实现自己的自定义 Render 类,其中的绘制与暴露预览的逻辑都在 Render 中。

实现自定义的 GLSurfaceView 用于 xml 中添加布局。 当Surface创建的时候 setUpCamera 绑定到 CameraX 的预览上:

kotlin 复制代码
class MyGLSurfaceView : GLSurfaceView {

    private val cameraXController: CameraXController = CameraXController()

    private val callback = object : MyGLRenderCallback {
        override fun onSurfaceChanged() {
            //关联并绑定CameraX
            setUpCamera()
        }

        override fun onFrameAvailable() {
            //确认渲染
            requestRender()
        }
    }

    // GLSurfaceView.Renderer 渲染对象
    private val cameraRender = MyGLRender(context, callback)

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    init {
        setEGLContextClientVersion(2)

        //设置GL渲染对象
        setRenderer(cameraRender)

        renderMode = RENDERMODE_WHEN_DIRTY
    }

    private fun setUpCamera() {
        cameraXController.setUpCamera(context, cameraRender)
    }

    fun getCameraXController(): CameraXController {
        return cameraXController
    }

}

具体的逻辑在自定义的 Render 中。GL渲染对象,实现 CameraX的 Preview.SurfaceProvider 与 SurfaceTexture.OnFrameAvailableListener 用于CameraX中预览使用。具体的流程如下:

  1. 在onSurfaceCreated方法中,生成一个纹理对象并创建一个SurfaceTexture。SurfaceTexture可以从CameraX获取摄像头预览数据, 并将其作为纹理供OpenGL渲染使用。
  2. 在onSurfaceChanged方法中,通知回调对象(MyGLRenderCallback)表明Surface的大小已经变化。 然后,初始化一个滤镜对象(Filter)并调用其onReady方法,准备渲染。
  3. 在onDrawFrame方法中,执行实际的渲染操作。首先清除颜色缓冲区,然后使用SurfaceTexture更新纹理图像和纹理变换矩阵。 最后,将纹理传递给滤镜对象进行渲染。
  4. MyGLRender还实现了Preview.SurfaceProvider接口,用于绑定CameraX。 在onSurfaceRequested方法中,通过创建一个新的SurfaceTexture,并将其提供给CameraX来获取摄像头预览数据的Surface。
  5. 在onFrameAvailable方法中,当CameraX有新的帧可用时,通知回调对象(MyGLRenderCallback)。然后确定渲染回调执行到3 onDrawFrame()

具体实现代码如下:

kotlin 复制代码
class MyGLRender(private val context: Context, private val callback: MyGLRenderCallback) : GLSurfaceView.Renderer,
    Preview.SurfaceProvider, SurfaceTexture.OnFrameAvailableListener {

    private var textures: IntArray = IntArray(1)
    private var surfaceTexture: SurfaceTexture? = null
    private var textureMatrix: FloatArray = FloatArray(16)
    private val executor = Executors.newSingleThreadExecutor()
    private var filter: Filter? = null

    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        gl?.let {
            it.glGenTextures(textures.size, textures, 0)
            surfaceTexture = SurfaceTexture(textures[0])

            filter = ScreenFilter(context)
        }
    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        callback.onSurfaceChanged()
        filter?.onReady(width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        val surfaceTexture = this.surfaceTexture
        if (gl == null || surfaceTexture == null) return

        gl.glClearColor(0f, 0f, 0f, 0f)   // 设置背景色
        gl.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 清空颜色缓冲区

        surfaceTexture.updateTexImage()
        surfaceTexture.getTransformMatrix(textureMatrix)


        filter?.setTransformMatrix(textureMatrix)
        filter?.onDrawFrame(textures[0])
    }

    // Preview.SurfaceProvider 接口的实现,用于绑定CameraX
    override fun onSurfaceRequested(request: SurfaceRequest) {
        val resetTexture = resetPreviewTexture(request.resolution) ?: return
        val surface = Surface(resetTexture)
        //提供一个明确的Surface给CameraX预览
        request.provideSurface(surface, executor) {
            surface.release()
            surfaceTexture?.release()
        }
    }

    @WorkerThread
    private fun resetPreviewTexture(size: Size): SurfaceTexture? {
        return this.surfaceTexture?.let { surfaceTexture ->
            surfaceTexture.setOnFrameAvailableListener(this)
            surfaceTexture.setDefaultBufferSize(size.width, size.height)
            surfaceTexture
        }
    }

    // 当PreView.SurfaceProvider 设置完成,surface设置已完成之后回调出去
    override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
        callback.onFrameAvailable()
    }

}

内部包含的 Filter 对象就是具体的滤镜与绘制逻辑,这个一般来说是由搞特效的提供的,我这里就放一些开源的滤镜效果与绘制代码:

kotlin 复制代码
interface Filter {
    fun onDrawFrame(textureId: Int): Int
    fun setTransformMatrix(mtx: FloatArray)
    fun onReady(width: Int, height: Int)
}
kotlin 复制代码
class ScreenFilter(context: Context): Filter {
    private val vPosition: Int
    private val vCoord: Int
    private val vTexture: Int
    private val vMatrix: Int
    private var mtx: FloatArray = FloatArray(16)
    private var mWidth: Int = 0
    private var mHeight: Int = 0
    private val textureBuffer: FloatBuffer
    private val vertexBuffer: FloatBuffer


    //顶点坐标
    private val VERTEX = floatArrayOf(
        -1.0f, -1.0f,
        1.0f, -1.0f,
        -1.0f, 1.0f,
        1.0f, 1.0f
    )

    //纹理坐标
    private val TEXTURE = floatArrayOf(
        0.0f, 0.0f,
        1.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f
    )

    private val program: Int

    init {
        vertexBuffer = ByteBuffer.allocateDirect(4 * 4 * 2)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
        vertexBuffer.clear()
        vertexBuffer.put(VERTEX)

        textureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
            .order(ByteOrder.nativeOrder())
            .asFloatBuffer()
        textureBuffer.clear()
        textureBuffer.put(TEXTURE)

        val vertexShader = OpenGLUtils.readRawTextFile(context, R.raw.camera_vert)
        val textureShader = OpenGLUtils.readRawTextFile(context, R.raw.camera_frag)

        program = OpenGLUtils.loadProgram(vertexShader, textureShader)

        vPosition = GLES20.glGetAttribLocation(program, "vPosition")
        vCoord = GLES20.glGetAttribLocation(program, "vCoord")
        vTexture = GLES20.glGetUniformLocation(program, "vTexture")
        vMatrix = GLES20.glGetUniformLocation(program, "vMatrix")

    }

    override fun onDrawFrame(textureId: Int): Int {
        // 1.设置窗口大小
        GLES20.glViewport(0, 0, mWidth, mHeight)
        // 2.使用着色器程序
        GLES20.glUseProgram(program)

        // 3.给着色器程序中传值
        // 3.1 给顶点坐标数据传值
        vertexBuffer.position(0)
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer)
        // 激活
        GLES20.glEnableVertexAttribArray(vPosition)
        // 3.2 给纹理坐标数据传值
        textureBuffer.position(0)
        GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, textureBuffer)
        GLES20.glEnableVertexAttribArray(vCoord)

        // 3.3 变化矩阵传值
        GLES20.glUniformMatrix4fv(vMatrix, 1, false, mtx, 0)

        // 3.4 给片元着色器中的 采样器绑定
        // 激活图层
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        // 图像数据
        GLES20.glBindTexture(GLES11Ext.GL_SAMPLER_EXTERNAL_OES, textureId)
        // 传递参数
        GLES20.glUniform1i(vTexture, 0)

        //参数传递完毕,通知 opengl开始画画
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4)

        // 解绑
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0)
        return textureId
    }

    override fun setTransformMatrix(mtx: FloatArray) {
        this.mtx = mtx
    }

    override fun onReady(width: Int, height: Int) {
        mWidth = width
        mHeight = height
    }

    fun release() {
        GLES20.glDeleteProgram(program)
    }
}

灰度效果的滤镜程序:

camera_vert:

ini 复制代码
//  顶点坐标
attribute vec4 vPosition;
//  纹理坐标
attribute vec4 vCoord;

uniform mat4 vMatrix;
//  传给片元着色器的像素点
varying vec2 aCoord;

void main() {
    gl_Position = vPosition;
    aCoord = (vMatrix * vec4(vCoord.x, vCoord.y, 1.0, 1.0)).xy;
}

camera_frag:

ini 复制代码
#extension GL_OES_EGL_image_external : require

precision mediump float;

//采样点的坐标
varying vec2 aCoord;

//采样器
uniform samplerExternalOES vTexture;

void main() {

    /// 正常
        gl_FragColor = texture2D(vTexture, aCoord);
        vec4 rgba = texture2D(vTexture, aCoord);

    /// 灰度图 305911
        float c = rgba.r*0.3+rgba.g*0.59+rgba.b*0.11;
        gl_FragColor = vec4(c, c, c, rgba.a);
}

到此就定义完毕,使用的时候我们和之前使用 CameraX 一样的,只是之前我们添加的是 默认的 PreiviewView ,现在我们添加的是 MyGLSurfaceView 。

部分代码:

kotlin 复制代码
    override fun initCamera(context: Context): View {
        mGLSurfaceView = MyGLSurfaceView(context)

        mContext = context
        mGLSurfaceView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)

        return mGLSurfaceView
    }

    override fun startCameraRecord() {
        mGLSurfaceView.getCameraXController().startCameraRecord(outFile)
    }

    override fun stopCameraRecord(cameraCallback: ICameraCallback) {
        mGLSurfaceView.getCameraXController().stopCameraRecord(cameraCallback)
    }

我们就可以把 MyGLSurfaceView 添加到容器 FrameLayout 中实现预览的效果了。

简单的灰度滤镜效果如下:

总结

本文总结了几种实现特效预览的几种方式,如果只是想预览的画面实现特效那么都能实现。

如果想做到本文开头说的实现预览加录制的特效直出,我们最好的方法还是第三种完全自定义实现,把滤镜特效的 Filter 抽取出来,再预览的同时把画面与滤镜同步给录制的 Surface ,再通过 MediaCodec 硬编码(软编也行)的方式合成为 带特效的MP4文件。

在了解了特效的实现方式之后,下一篇我们就会真正实现特效的录制了。

本文如果贴出的代码有不全的,可以点击源码打开项目进行查看,【传送门】。同时你也可以关注我的开源项目,后续一些改动与优化还有新功能都会持续更新。

首先必须承认我是音视频菜鸟,写的并不专业,这些也是实现效果的过程与一些摸索总结,最终我们实现的效果是类似拼多多的视频评论页面,可以特效录制视频,也可以选择本地视频进行处理,可以选择去除原始音频加入自定义的背景音乐,可以添加简单的文本字幕或标签,进行简单的裁剪之类的功能(并没有剪映抖音快手那么专业的效果)。对于一个轻量级别的使用,2023年的今天我觉得这些轻量级别的音视频使用已经是我们应用开发者也应该了解并且要会用的。

如果本文的讲解有什么错漏的地方,希望同学们一定要指出哦。有疑问也可以评论区交流学习进步,谢谢!

当然如果觉得本文还不错对你有些帮助的话,还请点赞支持一下哦,你的支持是我最大的动力啦!

Ok,这一期就此完结。

相关推荐
青山渺渺1 小时前
简单记录一下Android四大组件
android
每次的天空2 小时前
Android学习总结之OKHttp拦截器和缓存
android·学习·okhttp
aaajj3 小时前
【Android】ContentResolver的使用
android
时光少年3 小时前
Android ExoPlayer版本升级遇上系统的”瓜“
android·前端
你说你说你来说5 小时前
安卓布局详解
android·笔记
奔跑吧 android5 小时前
【android bluetooth 框架分析 02】【Module详解 3】【HciHal 模块介绍】
android·bluetooth·bt·gd·aosp13·hcihal
好学人5 小时前
Activity的四种启动模型
android
好学人6 小时前
一文了解 Android MVI 架构
android
增强6 小时前
Jetpack Compose + CameraX+ MlKit 实现 二维码扫描(二)
android
studyForMokey6 小时前
【Android读书笔记】读书笔记记录
android