Android硬编码特效视频录制的最终实现
前言
基于前文我们了解到特效的实现方式,一般我们为了方便和扩展,一般使用 GLSurfaceView呈现 + Render渲染 。
至于如何自定义硬编,结合 Surface + COLOR_FormatSurface 的组合进行硬编码。
在之前的文章中我们通过这样的方式结合 Camera2 的方式绑定到 Preview 对象,可以直接把摄像头硬件数据输入到 Surface 中录制出来。
而对于自定义特效的效果,我们则需要使用一个包装类 WindowSurface 把数据传递过来。
google 的开源项目 grafika 早就已经给出示例。
对于如何使用录制的方法,如何使用第三方的 GLRender ,如何使用,前文之中也分别有涉及到,有兴趣的可以翻看前文。
如何结合实现特效录制直出,下面会给出部分核心代码,由于整体全文代码实在太多,有兴趣可以去文章底部的源码中查看。
效果:
一、思路与使用
对应如何处理特效?我们之前的文章也说到过,由于这一次特效的实现是在 GLSurfaceView.Renderer 的 onDrawFrame 回调中实现的。
对应只有一个滤镜/特效的用法,之前的做法是直接调用绘制,
scss
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)
}
}
如果是多个滤镜的应用,则是启动多个纹理,一层一层传递与包装,最终绘制出来。
核心伪代码如下:
scss
@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 :
ini
mEglCore = new EglCore(eGLContext, EglCore.FLAG_RECORDABLE);
mInputWindowSurface = new WindowSurface(mEglCore, mVideoEncoder.getInputSurface(), true);
mInputWindowSurface.makeCurrent();
当然如果是切换滤镜或效果,此时GL上下文变换了,我们需要重新创建 WindowSurface :
ini
mInputWindowSurface.releaseEglSurface();
mEglCore.release();
mEglCore = new EglCore(newSharedContext, EglCore.FLAG_RECORDABLE);
mInputWindowSurface.recreate(mEglCore);
mInputWindowSurface.makeCurrent();
当我们有数据帧过来的时候,写入进 WindowSurface 即可给 MediaCodec 处理数据。
ini
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 的启动比较简单,我们可以直接定开启相机的方法。
ini
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 即可完成配置。
使用的时候直接加入容器即可:
kotlin
val mRecordCameraView = GLCamera1View(this)
mRecordCameraView.setOnFilterChangeListener(this)
flContainer.addView(mRecordCameraView)
效果图如文章开头所示。
三、Camerax的实现
与上面的方法不同的是 CameraX 需要实现 Preview.SurfaceProvider,提供一个 Surface 去预览。
大致上都是这么一个流程:
kotlin
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() 的渲染触发逻辑。
kotlin
override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) {
requestRender()
}
override fun onDrawFrame(gl: GL10?) {
mCameraDrawer.onDrawFrame(gl)
}
使用:
kotlin
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 展示,伪代码:
ini
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,这一期就此完结。