Android硬编码特效视频录制的终究结束

前言

根据前文咱们了解到特效的结束办法,一般咱们为了方便和扩展,一般运用 GLSurfaceView呈现 + Render烘托 。

至于怎么自定义硬编,结合 Surface + COLOR_FormatSurface 的组合进行硬编码。

在之前的文章中咱们通过这样的办法结合 Camera2 的办法绑定到 Preview 目标,能够直接把摄像头硬件数据输入到 Surface 中录制出来。

而关于自定义特效的作用,咱们则需求运用一个包装类 WindowSurface 把数据传递过来。

google 的开源项目 grafika 早就已经给出示例。

关于怎么运用录制的办法,怎么运用第三方的 GLRender ,怎么运用,前文之中也别离有涉及到,有兴趣的能够翻看前文。

怎么结合结束特效录制直出,下面会给出部分核心代码,因为全体全文代码实在太多,有兴趣能够去文章底部的源码中查看。

作用:

一、思路与运用

对应怎么处理特效?咱们之前的文章也说到过,因为这一次特效的结束是在 GLSurfaceView.Renderer 的 onDrawFrame 回调中结束的。

对应只要一个滤镜/特效的用法,之前的做法是直接调用制作,

    override fun onDrawFrame(gl: GL10?) {
        if (gl == null || surfaceTexture == null) return
        gl.glClearColor(0f, 0f, 0f, 0f)   // 设置背景色
        gl.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 清空颜色缓冲区
        surfaceTexture.updateTexImage()
        surfaceTexture.getTransformMatrix(textureMatrix)
        startRecording() //敞开录制,状态赋值
        filter?.setTransformMatrix(textureMatrix)
        filter?.onDrawFrame(textures[0])
                if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON) {
            videoEncoder?.setTextureId(fTexture[0])
            videoEncoder?.frameAvailable(surfaceTexture)
        }
    }

假如是多个滤镜的使用,则是发动多个纹路,一层一层传递与包装,终究制作出来。

核心伪代码如下:

  @Override
    public void onDrawFrame(GL10 gl) {
        //更新界面中的数据
        mSurfaceTextrue.updateTexImage();
        EasyGlUtils.bindFrameTexture(fFrame[0], fTexture[0]);
        GLES20.glViewport(0, 0, mPreviewWidth, mPreviewHeight);
        drawFilter.draw();
        EasyGlUtils.unBindFrameBuffer();
        mBeautyFilter.setTextureId(fTexture[0]);
        mBeautyFilter.draw();
        mProcessFilter.setTextureId(mBeautyFilter.getOutputTexture());
        mProcessFilter.draw();
        mSlideFilterGroup.onDrawFrame(mProcessFilter.getOutputTexture());
        // mSlideFilterGroup 没有制作,把烘托后的纹路给下面一个Filter展现
        mAGroupfFilter.setTextureId(mSlideFilterGroup.getOutputTexture());
        mAGroupfFilter.draw();  //展现选中的滤镜作用
        recording();   //敞开录制,赋值修正一些状态
        //制作显现的filter
        GLES20.glViewport(0, 0, width, height);
        // showFilter.setTextureId(fTexture[0]); //设置这样的预览的时分不会显现滤镜的,相当于原生画面
        showFilter.setTextureId(mAGroupfFilter.getOutputTexture()); // 要用处理之后的纹路才能实在显现出来
        showFilter.draw(); //只要showFilter设置了显现的矩阵才能实在显现,上面的Filter都仅仅处理不会实在显现。
        if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON) {
//            videoEncoder.setTextureId(fTexture[0]); //设置这样的录制的时分不会显现滤镜的,相当于原生画面
            videoEncoder.setTextureId(mAGroupfFilter.getOutputTexture());
            videoEncoder.frameAvailable();
        }
    }

当把特效的纹路设置给 videoEncoder ,而且当时 Frame 制作结束之后调用到 frameAvailable 之后,剩余的进程就来到了 WindowSurface 与 VideoEncoder 相关处理。

首要咱们把 grafika 项目中关于 WindowSurface 与 VideoEncoder 相关的代码拿过来。

关于WindowSurface的运用比较简略,把 MediaCodec 供给的 inputSurface 直接用结构包装即可。

创立 WindowSurface :

    mEglCore = new EglCore(eGLContext, EglCore.FLAG_RECORDABLE);
    mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
    mInputWindowSurface.makeCurrent();

当然假如是切换滤镜或作用,此刻GL上下文变换了,咱们需求从头创立 WindowSurface :

        mInputWindowSurface.releaseEglSurface();
        mEglCore.release();
        mEglCore = new EglCore(newSharedContext, EglCore.FLAG_RECORDABLE);
        mInputWindowSurface.recreate(mEglCore);
        mInputWindowSurface.makeCurrent();

当咱们有数据帧过来的时分,写入进 WindowSurface 即可给 MediaCodec 处理数据。

    private void handleFrameAvailable() {
        mVideoEncoder.drainEncoder(false);
        mNoShowFilter.setTextureId(mTextureId);
        mNoShowFilter.draw();
        if (baseTimeStamp == -1) {
            baseTimeStamp = System.nanoTime();
            mVideoEncoder.startRecord();
        }
        long nano = System.nanoTime();
        long time = nano - baseTimeStamp - pauseDelayTime;
        mInputWindowSurface.setPresentationTime(time);
        mInputWindowSurface.swapBuffers();
    }

代码这里都懂,仅仅这里为什么还需求一个 Filter 目标去 draw 呢,其实这个 Filter 并不是实在特效的 Filter,仅仅默认的一个 NoneFilter ,也并没有设置矩阵大小,仅仅为了使用以及有特效的纹路,并不会上屏显现,能够作为一个离屏的烘托触发器。

当 WindowSurface 填充数据之后就会把数据发送到 MediaCodec 结束根据 Surface 的硬编。也便是咱们最开端的那一套流程了。

二、Camera1的结束

怎么结合Camera1进行运用?

在咱们自定义的 GLSurfaceView 中,因为 Camera1 的发动比较简略,咱们能够直接定敞开相机的办法。

    private void open(int cameraId) {
        mCameraController.close();
        mCameraController.open(cameraId);
        mCameraDrawer.setCameraId(cameraId);
        final Point previewSize = mCameraController.getPreviewSize();
        dataWidth = previewSize.x;
        dataHeight = previewSize.y;
        SurfaceTexture texture = mCameraDrawer.getTexture();
        texture.setOnFrameAvailableListener(this);
        mCameraController.setPreviewTexture(texture);
        mCameraController.preview();
    }
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        mCameraDrawer.onSurfaceCreated(gl, config);
        if (!isSetParm) {
            open(cameraId);
        }
        mCameraDrawer.setPreviewSize(dataWidth, dataHeight);
    }
    public void switchCamera() {
        cameraId = cameraId == 0 ? 1 : 0;
        open(cameraId);
    }

内部咱们直接运用自行封装的 mCameraController 绑定到 Camera1 即可结束配置。

运用的时分直接参加容器即可:

    val mRecordCameraView = GLCamera1View(this)
    mRecordCameraView.setOnFilterChangeListener(this)
    flContainer.addView(mRecordCameraView)

作用图如文章最初所示。

三、Camerax的结束

与上面的办法不同的是 CameraX 需求结束 Preview.SurfaceProvider,供给一个 Surface 去预览。

大致上都是这么一个流程:

    var texture: SurfaceTexture? = null
        private set
    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
       it.glGenTextures(1, textures, 0)
      textureID = textures[0]
       texture = SurfaceTexture(textureID)
    }
    override fun onSurfaceRequested(request: SurfaceRequest) {
        val resetTexture = resetPreviewTexture(request.resolution) ?: return
        val surface = Surface(resetTexture)
        request.provideSurface(surface, executor) {
            surface.release()
            if (!fromCameraLensFacing) {
                surfaceTexture?.release()
            }
        }
    }
    @WorkerThread
    private fun resetPreviewTexture(size: Size): SurfaceTexture? {
        mCameraDrawer.texture?.setOnFrameAvailableListener(this)
        mCameraDrawer.texture?.setDefaultBufferSize(size.width, size.height)
        return mCameraDrawer.texture
    }

咱们能够在其中的回调 onFrameAvailable 中调用 requestRender() 的烘托触发逻辑。

    override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
        requestRender()
    }
    override fun onDrawFrame(gl: GL10?) {
        mCameraDrawer.onDrawFrame(gl)
    }

运用:

    mRecordCameraView = GLCameraXView(this)
    mRecordCameraView.setupCameraId(true, 1)
    mRecordCameraView.setOnFilterChangeListener(this)
    cameraXController.setUpCamera(this, mRecordCameraView)
    flContainer.addView(mRecordCameraView)
    //切换前后摄像头
    changeCamera.click {
        val facing = cameraXController.switchCamera(this)
        mRecordCameraView.setupCameraId(true, facing)
    }
    //切换滤镜
    changeFilter.click {
        mRecordCameraView.nextFilter()
    }
    ...

作用相同如文章最初所示。

四、Camera2的结束

Camera2 的绑定其实和 CameraX 相似,也是需求拿到 Render 那边的 SurfaceTexture,

然后把这个 SurfaceTexture 绑定给 Camera2 展现,伪代码:


    private void open(int cameraId) {
        mCameraController.close();
        mCameraController.open(cameraId);
        mCameraDrawer.setCameraId(cameraId);
        final Point previewSize = mCameraController.getPreviewSize();
        dataWidth = previewSize.x;
        dataHeight = previewSize.y;
        SurfaceTexture texture = mCameraDrawer.getTexture();
        texture.setOnFrameAvailableListener(this);
        mCameraController.startCamera(texture);
        mCameraController.preview();
    }
    ...
   public void startCamera(SurfaceTexture previewSurfaceTex) {
        startBackgroundThread();
        if (previewSurfaceTex != null) {
            previewSurfaceTex.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            mPreviewSurface = new Surface(previewSurfaceTex);
        } else {
            mPreviewImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2);
            mPreviewImageReader.setOnImageAvailableListener(mOnPreviewImageAvailableListener, mBackgroundHandler);
            mPreviewSurface = mPreviewImageReader.getSurface();
        }
        openCamera();
    }

后边便是相同的流程,给 SurfaceTexture 设置每帧回调,调用 requestRender 开端烘托。

总结

本文简略的展现了 Surface + COLOR_FormatSurface 的组合进行硬编码,当然咱们相同能够选择 I420 + COLOR_FormatYUV420Planar 相同能够进行带特效硬编,或者直接拿到 I420 + FFmpeg 进行带特效软编,也都是能够的。

当然 I420 + FFmpeg 相比 COLOR_FormatYUV420Planar 的兼容性更好,而且它们的结束办法也略有不同,直接拿数据帧就不用通过 WindowSurface 这种包装办法进行,而是直接拿到硬件数据帧,直接通过离屏烘托添加作用,然后上屏展现,在离屏烘托的同时复制一份 Image 数据进行 I420/NV21 的编码。

这差不多便是目前用的比较多的两种办法。假如后边有机会写到软编的话,会独自再过一下流程。

关于特效硬编直出的文章就到此结束了,本文的许多代码都是伪代码,首要是为了理清思路与逻辑。

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

首要有必要供认我是音视频菜鸟,写的并不专业,这些也是结束作用的进程与一些摸索总结,终究咱们结束的作用是相似拼多多的视频谈论页面,能够特效录制视频,也能够选择本地视频进行处理,能够选择去除原始音频参加自定义的背景音乐,能够添加简略的文本字幕或标签,进行简略的裁剪之类的功用(并没有剪映抖音快手那么专业的作用)。关于一个轻量级其他运用,2023年的今天我觉得这些轻量级其他音视频运用已经是咱们使用开发者也应该了解而且要会用的。

假如本文的讲解有什么错漏的当地,希望同学们一定要指出哦。有疑问也能够谈论区交流学习进步,谢谢!

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

Ok,这一期就此结束。

Android录制视频,硬编实现特效视频的录制(三)