Android Camera系列(七):MediaCodec视频编码中-OpenGL ES多线程渲染

214 阅读5分钟

你做的再好,也还是有人指指点点;你即便一塌糊涂,也还是有人唱赞歌。所以不必掉进他人的眼神,你需要讨好的,仅仅是你自己。

本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会。

引言

上一篇我们讲解了使用MediaCodec硬编码YUV数据进行视频录制,由于硬件厂商的原因存在一些兼容性的问题。而MediaCodec提供了COLOR_FormatSurface数据编码的方式,他是通过将数据渲染到MediaCodec提供的Surface中进行编码。该种方式兼容性好API > 4.3即可。本篇我们就详细介绍这种方式录制视频。

渲染数据到Surface?

上面提到要将数据渲染到Surface上,what?貌似我只会将Camera数据显示到SurfaceView或TextureView上呢。如果你仔细看了Android OpenGLES开发:EGL环境搭建Android Camera系列(四):TextureView+OpenGL ES+CameraAndroid Camera系列(五):Camera2这些篇章。那么你至少应该知道我们可以自己构建OpenGL ES环境,并通过Surface构建EGLSurface,然后使用OpenGL ES将数据渲染到Surface上。

还有一种方式就比较简单了,Camera2章节中我们知道预览和获取帧数据都需要Surface参数,而现在要将数据渲染到MediaCodec的Surface上,就需要把MediaCodec的Surface加进去即可,当然这种方式不再本章讨论的范围。

Surface是什么? Surface是Android对后台缓冲区的抽象,它是一块在应用和SurfaceFlinger之间共享的内存区域。所有的Android界面,在往屏幕上绘制内容前,都需要先获得一个或多个Surface,然后使用2D(SKIA)或者3D(OpenGL ES)引擎往这个缓冲区上绘制内容。内容绘制结束后,通知SurfaceFlinger将内容渲染到屏幕(Frame Buffer)上去。

整理下我们现在的需求:

  1. 创建MediaCodec,设置颜色格式COLOR_FormatSurface,并获取输入Surface
  2. 将Camera预览数据渲染到预览Surface(SurfaceView或TextureView)上,同时将预览数据渲染到MediaCodec的Surface上
  3. 编码Surface上的数据

一. 创建MediaCodec

将上一节中的MediaVideoBufferEncoder拷贝一份命名为MediaSurfaceEncoder,需要修改的地方如下:

  1. 配置颜色格式为COLOR_FormatSurface
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);    // API >= 18
  1. 获取Surface
public class MediaSurfaceEncoder extends MediaEncoder implements IVideoEncoder {
	...
    private Surface mSurface;

    protected void prepare() throws IOException {
    	...
        // 配置好MediaCodec后创建Surface
        mSurface = mMediaCodec.createInputSurface();    // API >= 18
        mMediaCodec.start();
        ...
    }
}

以上就得到了MediaCodec中的Surface,我们将Camera数据渲染到该Surface中即可进行编码

二. 多Surface渲染

1. OpenGL ES共享上下文

当我们需要在预览的同时还要将预览的画面传递给别的线程的Surface进行处理时(如:MediaCodec进行编码)。这个时候我们需要用到一个技巧共享EGL Context,通过共享Context我们可以实现纹理在不同的线程进行共享。

可以共享的资源:

  • 纹理
  • shader
  • program着色器程序
  • Buffer 类对象,如 VBO、EBO、RBO等

不可共享的资源:

  • FBO 帧缓冲区对象(不属于Buffer类)
  • VAO 顶点数组对象(不属于Buffer类)

EGL共享Context.jpg

如何共享EGLContext? 在 EGL_VERSION_1_4 (Android 5.0)版本,在当前渲染线程直接调用 eglGetCurrentContext 就可以直接获取到上下文对象 EGLContext 。

EGL14.eglGetCurrentContext()

我们在新线程中使用EGL创建渲染环境时,通过主渲染线程获取的sharedContext来创建新线程的上下文对象。

EGLContext context = eglCreateContext(mEGLDisplay, config,
                                              sharedContext, attrib2_list);

创建EGLSurface,我们获取MediaCodec中的Surface来创建EGLSurface

EGLSurface eglSurface = eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, surfaceAttribs);

2. 多线程渲染

既然是多线程渲染,那么MediaCodec的渲染就要在一个新的线程中执行,我们创建TextureMovieEncoder1类实现Runnable,主要实现如下接口

// 开始录像
public abstract void startRecord(EncoderConfig config);

// 停止录像
public abstract void stopRecord();

// 是否正在录像
public abstract boolean isRecording();

// 更新EGLContext
public abstract void updateSharedContext(EGLContext sharedContext);

// 渲染
public abstract void frameAvailable(SurfaceTexture st);

// 设置纹理id
public abstract void setTextureId(int id);

开始录像中,我们启动一个新的线程,所有的操作都通过Handler发送消息完成

public void startRecord(EncoderConfig config) {
    Log.d(TAG, "Encoder: startRecording()");
    synchronized (mReadyFence) {
        if (mRunning) {
            Log.w(TAG, "Encoder thread already running");
            return;
        }
        mRunning = true;
        new Thread(this, "TextureMovieEncoder").start();
        while (!mReady) {
            try {
                mReadyFence.wait();
            } catch (InterruptedException ie) {
                // ignore
            }
        }
    }

    mHandler.sendMessage(mHandler.obtainMessage(MSG_START_RECORDING, config));
}
public static class EncoderConfig {
    final File mOutputFile;
    final int mWidth;
    final int mHeight;
    final int mBitRate;
    final EGLContext mEglContext;

    public EncoderConfig(File outputFile, int width, int height, int bitRate,
                         EGLContext sharedEglContext) {
        mOutputFile = outputFile;
        mWidth = width;
        mHeight = height;
        mBitRate = bitRate;
        mEglContext = sharedEglContext;
    }
}

渲染纹理到MediaCodec中的Surface上,CameraFilter和渲染到屏幕中的Shader程序一样,现在调用draw方法将纹理绘制到缓冲区中。记得调用swapBuffers交换缓冲区,这样绘制的内容就刷新到MediaCodec中的Surface上了

private void handleFrameAvailable(float[] transform, long timestampNanos) {
    if (VERBOSE) Log.d(TAG, "handleFrameAvailable tr=" + transform);
    busy = true;
    if (mEncoder != null) {
        mEncoder.frameAvailableSoon();
    }
    long start = System.currentTimeMillis();
    mCameraFilter.draw(mTextureId, transform);
    
    mInputWindowSurface.setPresentationTime(timestampNanos);
    mInputWindowSurface.swapBuffers();
    busy = false;
}

三. 编码

Surface编码和Buffer的编码是一样的,这里就不在重复贴代码了,可以看下MediaEncoder

最后

本篇章我们学习了如何使用MediaCodecCOLOR_FormatSurface格式编码视频,其本质主要就是运用OpenGL ES将同一个纹理渲染到不同的Surface中。而本章我们使用的技巧是通过共享EGLContext,进而在多个线程共享了纹理,实现多线程渲染到不同的Surface中。

lib-camera库包结构如下:

说明
cameracamera相关操作功能包,包括Camera和Camera2。以及各种预览视图
encoderMediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制
glesopengles操作相关
permission权限相关
util工具类

每个包都可独立使用做到最低的耦合,方便白嫖

github地址:github.com/xiaozhi003/…如果对你有帮助可以star下,万分感谢^_^

参考:

  1. github.com/saki4510t/U…
  2. github.com/google/graf…