Android Camera系列(四):TextureView+OpenGL ES+Camera

别人贪婪时我恐惧,别人恐惧时我贪婪

Android Camera系列(一):SurfaceView+Camera

Android Camera系列(二):TextureView+Camera

Android Camera系列(三):GLSurfaceView+Camera

Android Camera系列(四):TextureView+OpenGL ES+Camera

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

本章我们讲解TextureView+OpenGL ES进行Camera预览。上一篇我们已经讲了GLSurfaceView的用法,它实际上是SurfaceView+OpenGL ES的实现,只不过官方已经把EGL的环境搭建封装好了。我们现在要使用TextureView就只能自己搭建EGL环境了。

一.CameraGLTextureView使用

如果要使用TextureView+OpenGL ES预览Camera数据,首先要搭建EGL环境(不了解EGL是什么,请看之前的文章:Android OpenGLES开发:EGL环境搭建),环境搭建好后,剩下的预览操作基本上和GLSurfaceView中相同,用到的滤镜也完全一样。与GLSurfaceView区别如下:

  1. EGL环境封装
  2. 自定义渲染线程RenderThread和通信类RenderHandler

1. EGL封装

创建EglCore核心工具类

java 复制代码
/**
 * Core EGL state (display, context, config).
 * <p>
 * The EGLContext must only be attached to one thread at a time.  This class is not thread-safe.
 */
public final class EglCore {
    private static final String TAG = EglCore.class.getSimpleName();

    /**
     * Constructor flag: surface must be recordable.  This discourages EGL from using a
     * pixel format that cannot be converted efficiently to something usable by the video
     * encoder.
     */
    public static final int FLAG_RECORDABLE = 0x01;

    /**
     * Constructor flag: ask for GLES3, fall back to GLES2 if not available.  Without this
     * flag, GLES2 is used.
     */
    public static final int FLAG_TRY_GLES3 = 0x02;

    // Android-specific extension.
    private static final int EGL_RECORDABLE_ANDROID = 0x3142;

    private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
    private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
    private EGLConfig mEGLConfig = null;
    private int mGlVersion = -1;


    /**
     * Prepares EGL display and context.
     * <p>
     * Equivalent to EglCore(null, 0).
     */
    public EglCore() {
        this(null, 0);
    }

    /**
     * Prepares EGL display and context.
     * <p>
     * @param sharedContext The context to share, or null if sharing is not desired.
     * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
     */
    public EglCore(EGLContext sharedContext, int flags) {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("EGL already set up");
        }

        if (sharedContext == null) {
            sharedContext = EGL14.EGL_NO_CONTEXT;
        }

        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("unable to get EGL14 display");
        }
        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            mEGLDisplay = null;
            throw new RuntimeException("unable to initialize EGL14");
        }

        // Try to get a GLES3 context, if requested.
        if ((flags & FLAG_TRY_GLES3) != 0) {
            //Log.d(TAG, "Trying GLES 3");
            EGLConfig config = getConfig(flags, 3);
            if (config != null) {
                int[] attrib3_list = {
                        EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
                        EGL14.EGL_NONE
                };
                EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
                        attrib3_list, 0);

                if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
                    //Log.d(TAG, "Got GLES 3 config");
                    mEGLConfig = config;
                    mEGLContext = context;
                    mGlVersion = 3;
                }
            }
        }
        if (mEGLContext == EGL14.EGL_NO_CONTEXT) {  // GLES 2 only, or GLES 3 attempt failed
            //Log.d(TAG, "Trying GLES 2");
            EGLConfig config = getConfig(flags, 2);
            if (config == null) {
                throw new RuntimeException("Unable to find a suitable EGLConfig");
            }
            int[] attrib2_list = {
                    EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                    EGL14.EGL_NONE
            };
            EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
                    attrib2_list, 0);
            checkEglError("eglCreateContext");
            mEGLConfig = config;
            mEGLContext = context;
            mGlVersion = 2;
        }

        // Confirm with query.
        int[] values = new int[1];
        EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
                values, 0);
        Log.d(TAG, "EGLContext created, client version " + values[0]);
    }

    /**
     * Finds a suitable EGLConfig.
     *
     * @param flags Bit flags from constructor.
     * @param version Must be 2 or 3.
     */
    private EGLConfig getConfig(int flags, int version) {
        int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
        if (version >= 3) {
            renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
        }

        // The actual surface is generally RGBA or RGBX, so situationally omitting alpha
        // doesn't really help.  It can also lead to a huge performance hit on glReadPixels()
        // when reading into a GL_RGBA buffer.
        int[] attribList = {
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                //EGL14.EGL_DEPTH_SIZE, 16,
                //EGL14.EGL_STENCIL_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, renderableType,
                EGL14.EGL_NONE, 0,      // placeholder for recordable [@-3]
                EGL14.EGL_NONE
        };
        if ((flags & FLAG_RECORDABLE) != 0) {
            attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
            attribList[attribList.length - 2] = 1;
        }
        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
                numConfigs, 0)) {
            Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
            return null;
        }
        return configs[0];
    }

    /**
     * Discards all resources held by this class, notably the EGL context.  This must be
     * called from the thread where the context was created.
     * <p>
     * On completion, no context will be current.
     */
    public void release() {
        if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
            // Android is unusual in that it uses a reference-counted EGLDisplay.  So for
            // every eglInitialize() we need an eglTerminate().
            EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                    EGL14.EGL_NO_CONTEXT);
            EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
            EGL14.eglReleaseThread();
            EGL14.eglTerminate(mEGLDisplay);
        }

        mEGLDisplay = EGL14.EGL_NO_DISPLAY;
        mEGLContext = EGL14.EGL_NO_CONTEXT;
        mEGLConfig = null;
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
                // We're limited here -- finalizers don't run on the thread that holds
                // the EGL state, so if a surface or context is still current on another
                // thread we can't fully release it here.  Exceptions thrown from here
                // are quietly discarded.  Complain in the log file.
                Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked");
                release();
            }
        } finally {
            super.finalize();
        }
    }

    /**
     * Destroys the specified surface.  Note the EGLSurface won't actually be destroyed if it's
     * still current in a context.
     */
    public void releaseSurface(EGLSurface eglSurface) {
        EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
    }

    /**
     * Creates an EGL surface associated with a Surface.
     * <p>
     * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
     */
    public EGLSurface createWindowSurface(Object surface) {
        if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
            throw new RuntimeException("invalid surface: " + surface);
        }

        // Create a window surface, and attach it to the Surface we received.
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
                surfaceAttribs, 0);
        checkEglError("eglCreateWindowSurface");
        if (eglSurface == null) {
            throw new RuntimeException("surface was null");
        }
        return eglSurface;
    }

    /**
     * Creates an EGL surface associated with an offscreen buffer.
     */
    public EGLSurface createOffscreenSurface(int width, int height) {
        int[] surfaceAttribs = {
                EGL14.EGL_WIDTH, width,
                EGL14.EGL_HEIGHT, height,
                EGL14.EGL_NONE
        };
        EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
                surfaceAttribs, 0);
        checkEglError("eglCreatePbufferSurface");
        if (eglSurface == null) {
            throw new RuntimeException("surface was null");
        }
        return eglSurface;
    }

    /**
     * Makes our EGL context current, using the supplied surface for both "draw" and "read".
     */
    public void makeCurrent(EGLSurface eglSurface) {
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            // called makeCurrent() before create?
            Log.d(TAG, "NOTE: makeCurrent w/o display");
        }
        if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }
    }

    /**
     * Makes our EGL context current, using the supplied "draw" and "read" surfaces.
     */
    public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            // called makeCurrent() before create?
            Log.d(TAG, "NOTE: makeCurrent w/o display");
        }
        if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent(draw,read) failed");
        }
    }

    /**
     * Makes no context current.
     */
    public void makeNothingCurrent() {
        if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }
    }

    /**
     * Calls eglSwapBuffers.  Use this to "publish" the current frame.
     *
     * @return false on failure
     */
    public boolean swapBuffers(EGLSurface eglSurface) {
        return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
    }

    /**
     * Sends the presentation time stamp to EGL.  Time is expressed in nanoseconds.
     */
    public void setPresentationTime(EGLSurface eglSurface, long nsecs) {
        EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs);
    }

    /**
     * Returns true if our context and the specified surface are current.
     */
    public boolean isCurrent(EGLSurface eglSurface) {
        return mEGLContext.equals(EGL14.eglGetCurrentContext()) &&
            eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW));
    }

    /**
     * Performs a simple surface query.
     */
    public int querySurface(EGLSurface eglSurface, int what) {
        int[] value = new int[1];
        EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
        return value[0];
    }

    /**
     * Queries a string value.
     */
    public String queryString(int what) {
        return EGL14.eglQueryString(mEGLDisplay, what);
    }

    /**
     * Returns the GLES version this context is configured for (currently 2 or 3).
     */
    public int getGlVersion() {
        return mGlVersion;
    }

    /**
     * Writes the current display, context, and surface to the log.
     */
    public static void logCurrent(String msg) {
        EGLDisplay display;
        EGLContext context;
        EGLSurface surface;

        display = EGL14.eglGetCurrentDisplay();
        context = EGL14.eglGetCurrentContext();
        surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
        Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context +
                ", surface=" + surface);
    }

    /**
     * Checks for EGL errors.  Throws an exception if an error has been raised.
     */
    private void checkEglError(String msg) {
        int error;
        if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
            throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
        }
    }
}

创建EglSurfaceBase类,用于操作EGLSurface使用

java 复制代码
/**
 * Common base class for EGL surfaces.
 * <p>
 * There can be multiple surfaces associated with a single context.
 */
public class EglSurfaceBase {
    protected static final String TAG = EglSurfaceBase.class.getSimpleName();

    // EglCore object we're associated with.  It may be associated with multiple surfaces.
    protected EglCore mEglCore;

    private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
    private int mWidth = -1;
    private int mHeight = -1;

    protected EglSurfaceBase(EglCore eglCore) {
        mEglCore = eglCore;
    }

    /**
     * Creates a window surface.
     * <p>
     * @param surface May be a Surface or SurfaceTexture.
     */
    public void createWindowSurface(Object surface) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglCore.createWindowSurface(surface);
    }

    /**
     * Creates an off-screen surface.
     */
    public void createOffscreenSurface(int width, int height) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglCore.createOffscreenSurface(width, height);
        mWidth = width;
        mHeight = height;
    }

    /**
     * Returns the surface's width, in pixels.
     * <p>
     * If this is called on a window surface, and the underlying surface is in the process
     * of changing size, we may not see the new size right away (e.g. in the "surfaceChanged"
     * callback).  The size should match after the next buffer swap.
     */
    public int getWidth() {
        if (mWidth < 0) {
            return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
        } else {
            return mWidth;
        }
    }

    /**
     * Returns the surface's height, in pixels.
     */
    public int getHeight() {
        if (mHeight < 0) {
            return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
        } else {
            return mHeight;
        }
    }

    /**
     * Release the EGL surface.
     */
    public void releaseEglSurface() {
        mEglCore.releaseSurface(mEGLSurface);
        mEGLSurface = EGL14.EGL_NO_SURFACE;
        mWidth = mHeight = -1;
    }

    /**
     * Makes our EGL context and surface current.
     */
    public void makeCurrent() {
        mEglCore.makeCurrent(mEGLSurface);
    }

    /**
     * Makes our EGL context and surface current for drawing, using the supplied surface
     * for reading.
     */
    public void makeCurrentReadFrom(EglSurfaceBase readSurface) {
        mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
    }

    /**
     * Calls eglSwapBuffers.  Use this to "publish" the current frame.
     *
     * @return false on failure
     */
    public boolean swapBuffers() {
        boolean result = mEglCore.swapBuffers(mEGLSurface);
        if (!result) {
            Log.d(TAG, "WARNING: swapBuffers() failed");
        }
        return result;
    }

    /**
     * Sends the presentation time stamp to EGL.
     *
     * @param nsecs Timestamp, in nanoseconds.
     */
    public void setPresentationTime(long nsecs) {
        mEglCore.setPresentationTime(mEGLSurface, nsecs);
    }

    /**
     * Saves the EGL surface to a file.
     * <p>
     * Expects that this object's EGL surface is current.
     */
    public void saveFrame(File file) throws IOException {
        if (!mEglCore.isCurrent(mEGLSurface)) {
            throw new RuntimeException("Expected EGL context/surface is not current");
        }

        String filename = file.toString();

        int width = getWidth();
        int height = getHeight();
        ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);
        buf.order(ByteOrder.LITTLE_ENDIAN);
        GLES20.glReadPixels(0, 0, width, height,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
        GLESUtils.checkGlError("glReadPixels");
        buf.rewind();

        BufferedOutputStream bos = null;
        try {
            bos = new BufferedOutputStream(new FileOutputStream(filename));
            Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            bmp.copyPixelsFromBuffer(buf);
            bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
            bmp.recycle();
        } finally {
            if (bos != null) bos.close();
        }
        Log.d(TAG, "Saved " + width + "x" + height + " frame as '" + filename + "'");
    }
}

创建WindowSurface继承EglSurfaceBase

java 复制代码
/**
 * Recordable EGL window surface.
 * <p>
 * It's good practice to explicitly release() the surface, preferably from a "finally" block.
 */
public class WindowSurface extends EglSurfaceBase {
    private Surface mSurface;
    private boolean mReleaseSurface;

    /**
     * Associates an EGL surface with the native window surface.
     * <p>
     * Set releaseSurface to true if you want the Surface to be released when release() is
     * called.  This is convenient, but can interfere with framework classes that expect to
     * manage the Surface themselves (e.g. if you release a SurfaceView's Surface, the
     * surfaceDestroyed() callback won't fire).
     */
    public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
        super(eglCore);
        createWindowSurface(surface);
        mSurface = surface;
        mReleaseSurface = releaseSurface;
    }

    /**
     * Associates an EGL surface with the SurfaceTexture.
     */
    public WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) {
        super(eglCore);
        createWindowSurface(surfaceTexture);
    }

    /**
     * Releases any resources associated with the EGL surface (and, if configured to do so,
     * with the Surface as well).
     * <p>
     * Does not require that the surface's EGL context be current.
     */
    public void release() {
        releaseEglSurface();
        if (mSurface != null) {
            if (mReleaseSurface) {
                mSurface.release();
            }
            mSurface = null;
        }
    }

    /**
     * Recreate the EGLSurface, using the new EglBase.  The caller should have already
     * freed the old EGLSurface with releaseEglSurface().
     * <p>
     * This is useful when we want to update the EGLSurface associated with a Surface.
     * For example, if we want to share with a different EGLContext, which can only
     * be done by tearing down and recreating the context.  (That's handled by the caller;
     * this just creates a new EGLSurface for the Surface we were handed earlier.)
     * <p>
     * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a
     * context somewhere, the create call will fail with complaints from the Surface
     * about already being connected.
     */
    public void recreate(EglCore newEglCore) {
        if (mSurface == null) {
            throw new RuntimeException("not yet implemented for SurfaceTexture");
        }
        mEglCore = newEglCore;          // switch to new context
        createWindowSurface(mSurface);  // create new surface
    }
}

2. 自定义RenderThread

使用OpenGL ES渲染Camera预览数据,其主要实现3个核心方法和SurfaceTexture的生命周期相匹配,以及帧预览数据回调接口

java 复制代码
public class RenderThread extends Thread {

    public void surfaceAvailable(Object surface) {}

    public void surfaceChanged(int width, int height) {}

    public void surfaceDestroyed() {}

    public void frameAvailable() {}
}

完整代码:

java 复制代码
public class RenderThread extends Thread {

    private static final String TAG = RenderThread.class.getSimpleName();
    private static final int RECORDING_OFF = 0;
    private static final int RECORDING_ON = 1;
    private static final int RECORDING_RESUMED = 2;

    // Used to wait for the thread to start.
    private Object mStartLock = new Object();
    private Context mContext;
    private boolean mReady = false;
    private Handler mMainHandler;
    private RenderHandler mHandler;

    // width/height of the incoming camera preview frames
    private SurfaceTexture mPreviewTexture;
    private int mTextureId;

    private float[] mDisplayProjectionMatrix = new float[16];
    private EglCore mEglCore;
    private WindowSurface mWindowSurface;
    private CameraFilter mCameraFilter;
    private CameraFilter mFBOFilter;
    private ScreenFilter mScreenFilter;
    private int mCameraPreviewWidth, mCameraPreviewHeight;
    private boolean mSizeUpdated;

    private File mOutputFile;
    private TextureMovieEncoder2 mVideoEncoder;
    private boolean mRecordingEnabled;
    private int mRecordingStatus;
    private long mVideoMillis;

    public RenderThread(Context context, TextureMovieEncoder2 textureMovieEncoder) {
        super("Renderer Thread");
        mContext = context;
        mMainHandler = new Handler(mContext.getMainLooper());
        mVideoEncoder = textureMovieEncoder;
        mSizeUpdated = false;
        mCameraFilter = new CameraFilter();
        mFBOFilter = new CameraFilter();
        mFBOFilter.setFBO(true);
        mScreenFilter = new ScreenFilter();
    }

    @Override
    public void run() {
        super.run();
        Logs.i(TAG, "Render Thread start.");
        Looper.prepare();

        // We need to create the Handler before reporting ready.
        mHandler = new RenderHandler(this);
        synchronized (mStartLock) {
            mReady = true;
            mStartLock.notify();    // signal waitUntilReady()
        }

        // Prepare EGL and open the camera before we start handling messages.
        mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE | EglCore.FLAG_TRY_GLES3);
        Logs.i(TAG, "Egl version:" + mEglCore.getGlVersion());

        Looper.loop();
        mHandler = null;
        releaseGl();
        mEglCore.release();

        Logs.v(TAG, "Render Thread exit.");
    }

    /**
     * Notifies the renderer that we want to stop or start recording.
     */
    public void changeRecordingState(boolean isRecording) {
        Log.d(TAG, "changeRecordingState: was " + mRecordingEnabled + " now " + isRecording);
        mRecordingEnabled = isRecording;
        if (!mRecordingEnabled) {
            notifyStopRecord();
        }
    }

    public void notifyStopRecord() {
        if (mVideoEncoder != null && mVideoEncoder.isRecording()) {
            mVideoEncoder.stopRecord();
            mRecordingStatus = RECORDING_OFF;
        }
    }

    /**
     * Waits until the render thread is ready to receive messages.
     * <p>
     * Call from the UI thread.
     */
    public void waitUntilReady() {
        synchronized (mStartLock) {
            while (!mReady) {
                try {
                    mStartLock.wait();
                } catch (InterruptedException ie) { /* not expected */ }
            }
        }
    }

    /**
     * Shuts everything down.
     */
    public void shutdown() {
        Log.d(TAG, "shutdown");
        Looper.myLooper().quit();
    }

    public RenderHandler getHandler() {
        return mHandler;
    }

    public void surfaceAvailable(Object surface) {
        mRecordingEnabled = mVideoEncoder.isRecording();
        if (mRecordingEnabled) {
            mRecordingStatus = RECORDING_RESUMED;
        } else {
            mRecordingStatus = RECORDING_OFF;
        }
        if (surface instanceof SurfaceHolder) {
            mWindowSurface = new WindowSurface(mEglCore, ((SurfaceHolder) surface).getSurface(), false);
        } else if (surface instanceof SurfaceTexture) {
            mWindowSurface = new WindowSurface(mEglCore, (SurfaceTexture) surface);
        }

        mWindowSurface.makeCurrent();

        // Set up the texture blitter that will be used for on-screen display.  This
        // is *not* applied to the recording, because that uses a separate shader.

        mTextureId = GLESUtils.createOESTexture();
        mPreviewTexture = new SurfaceTexture(mTextureId);
        mCameraFilter.surfaceCreated();
        mFBOFilter.surfaceCreated();
        mScreenFilter.surfaceCreated();

        mMainHandler.post(() -> {
            if (mGLSurfaceTextureCallback != null) {
                mGLSurfaceTextureCallback.onGLSurfaceCreated(mPreviewTexture);
            }
        });
    }

    public void surfaceChanged(int width, int height) {
        mCameraFilter.surfaceChanged(width, height);
        mFBOFilter.surfaceChanged(width, height);
        mScreenFilter.surfaceChanged(width, height);
    }

    public void surfaceDestroyed() {
        // In practice this never appears to be called -- the activity is always paused
        // before the surface is destroyed.  In theory it could be called though.
        Log.d(TAG, "RenderThread surfaceDestroyed");
        releaseGl();
    }

    public void frameAvailable() {
        draw();
    }

    public void draw() {
        if (mPreviewTexture == null) return;
        if (mWindowSurface == null) return;

        mPreviewTexture.updateTexImage();
        GLESUtils.checkGlError("draw start");

        // If the recording state is changing, take care of it here.  Ideally we wouldn't
        // be doing all this in onDrawFrame(), but the EGLContext sharing with GLSurfaceView
        // makes it hard to do elsewhere.
        if (mRecordingEnabled) {
            switch (mRecordingStatus) {
                case RECORDING_OFF:
                    Log.d(TAG, "START recording");
                    // 开始录制前删除之前的视频文件
                    String name = "VID_" + ImageUtils.DATE_FORMAT.format(new Date(System.currentTimeMillis())) + ".mp4";
                    mOutputFile = new File(ImageUtils.getVideoPath(), name);
                    // start recording
                    mVideoEncoder.startRecord(new TextureMovieEncoder2.EncoderConfig(
                            mOutputFile, mCameraPreviewHeight, mCameraPreviewWidth, mCameraPreviewWidth * mCameraPreviewHeight * 10, mEglCore));
                    mRecordingStatus = RECORDING_ON;
                    break;
                case RECORDING_RESUMED:
                    Log.d(TAG, "RESUME recording");
                    mRecordingStatus = RECORDING_ON;
                    break;
                case RECORDING_ON:
                    // yay
                    break;
                default:
                    throw new RuntimeException("unknown status " + mRecordingStatus);
            }
        } else {
            switch (mRecordingStatus) {
                case RECORDING_ON:
                case RECORDING_RESUMED:
                    // stop recording
                    Log.d(TAG, "STOP recording");
                    mVideoEncoder.stopRecord();
                    mRecordingStatus = RECORDING_OFF;
                    break;
                case RECORDING_OFF:
                    // yay
                    break;
                default:
                    throw new RuntimeException("unknown status " + mRecordingStatus);
            }
        }

        if (mCameraPreviewWidth <= 0 || mCameraPreviewHeight <= 0) {
            return;
        }
        if (mSizeUpdated) {
            mSizeUpdated = false;
        }

        boolean swapResult;
        if (!mVideoEncoder.isRecording()) {
            swapResult = drawScreen();
        } else {
            swapResult = drawRecord();
        }

        if (!swapResult) {
            // This can happen if the Activity stops without waiting for us to halt.
            Log.w(TAG, "swapBuffers failed, killing renderer thread");
            shutdown();
        }
    }

    /**
     * 绘制到屏幕Surface中,用来在界面上显示
     */
    private boolean drawScreen() {
        mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
        mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
        return mWindowSurface.swapBuffers();
    }

    /**
     * 绘制到视频Surface中,用来录制视频
     */
    private boolean drawRecord() {
        boolean swapResult;
        WindowSurface recordWindowSurface = mVideoEncoder.getInputWindowSurface();

        if (recordWindowSurface != null && mEglCore.getGlVersion() >= 3) { // draw blit framebuffer
            swapResult = drawBlitFrameBuffer(recordWindowSurface);
        } else if (recordWindowSurface != null) { // draw twice or draw FBO
            // 两种方式任选其一
            swapResult = drawTwice(recordWindowSurface);
//            swapResult = drawFBO(recordWindowSurface);
        } else {
            swapResult = drawScreen();
        }
        return swapResult;
    }

    /**
     * BlitFramebuffer方式
     *
     * @param recordWindowSurface
     * @return
     */
    private boolean drawBlitFrameBuffer(WindowSurface recordWindowSurface) {
        boolean swapResult;
        // 先绘制到屏幕上
        mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
        mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);

        mVideoEncoder.frameAvailable();
        // 把屏幕Surface渲染数据拷贝到视频Surface中
        // 该中方式的效率是最高的,一次渲染输出给多个目标,但是只有OpenGL3.0才有该方法
        recordWindowSurface.makeCurrentReadFrom(mWindowSurface);
        GLES30.glBlitFramebuffer(
                0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight(),
                0, 0, mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight(),
                GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_NEAREST);
        int err;
        if ((err = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {
            Log.w(TAG, "ERROR: glBlitFramebuffer failed: 0x" +
                    Integer.toHexString(err));
        }
        recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
        recordWindowSurface.swapBuffers();

        // 切换为屏幕Surface
        mWindowSurface.makeCurrent();
        swapResult = mWindowSurface.swapBuffers();
        return swapResult;
    }

    /**
     * 二次渲染方式
     *
     * @param recordWindowSurface
     * @return
     */
    private boolean drawTwice(WindowSurface recordWindowSurface) {
        boolean swapResult;
        // 先绘制到屏幕上
        mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
        mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
        swapResult = mWindowSurface.swapBuffers();

        // 再绘制到视频Surface中
        mVideoEncoder.frameAvailable();
        recordWindowSurface.makeCurrent();
        GLES20.glViewport(0, 0,
                mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight());
        mCameraFilter.draw(mTextureId, mDisplayProjectionMatrix);
        recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
        recordWindowSurface.swapBuffers();

        // Restore
        GLES20.glViewport(0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight());
        mWindowSurface.makeCurrent();
        return swapResult;
    }


    /**
     * 离屏渲染
     *
     * @param recordWindowSurface
     * @return
     */
    private boolean drawFBO(WindowSurface recordWindowSurface) {
        boolean swapResult;
        // 将数据绘制到FBO Buffer中
        mPreviewTexture.getTransformMatrix(mDisplayProjectionMatrix);
        int fboId = mFBOFilter.draw(mTextureId, mDisplayProjectionMatrix);

        // 将离屏FrameBuffer绘制到视频Surface中
        mVideoEncoder.frameAvailable();
        recordWindowSurface.makeCurrent();
        GLES20.glViewport(0, 0,
                mVideoEncoder.getVideoWidth(), mVideoEncoder.getVideoHeight());
        mScreenFilter.draw(fboId, mDisplayProjectionMatrix);
        recordWindowSurface.setPresentationTime(mPreviewTexture.getTimestamp());
        recordWindowSurface.swapBuffers();

        // 将离屏FrameBuffer绘制到屏幕Surface中
        mWindowSurface.makeCurrent();
        GLES20.glViewport(0, 0, mWindowSurface.getWidth(), mWindowSurface.getHeight());
        mScreenFilter.draw(fboId, mDisplayProjectionMatrix);
        swapResult = mWindowSurface.swapBuffers();

        return swapResult;
    }

    public void setCameraPreviewSize(int width, int height) {
        mCameraPreviewWidth = width;
        mCameraPreviewHeight = height;
        mSizeUpdated = true;
    }

    /**
     * Releases most of the GL resources we currently hold (anything allocated by
     * surfaceAvailable()).
     * <p>
     * Does not release EglCore.
     */
    private void releaseGl() {
        GLESUtils.checkGlError("releaseGl start");

        if (mWindowSurface != null) {
            mWindowSurface.release();
            mWindowSurface = null;
        }
        if (mPreviewTexture != null) {
            mPreviewTexture.release();
            mPreviewTexture = null;
        }
        if (mCameraFilter != null) {
            mCameraFilter.release();
        }
        if (mFBOFilter != null) {
            mFBOFilter.release();
        }
        if (mScreenFilter != null) {
            mScreenFilter.release();
        }
        GLESUtils.checkGlError("releaseGl done");

        mEglCore.makeNothingCurrent();
    }

    private GLSurfaceTextureCallback mGLSurfaceTextureCallback;

    public void setGLSurfaceTextureCallback(GLSurfaceTextureCallback GLSurfaceTextureCallback) {
        mGLSurfaceTextureCallback = GLSurfaceTextureCallback;
    }

    public interface GLSurfaceTextureCallback {
        void onGLSurfaceCreated(SurfaceTexture surfaceTexture);
    }
}

渲染线程通信Handler代码如下:

java 复制代码
public class RenderHandler extends Handler {

    private static final String TAG = RenderHandler.class.getSimpleName();
    private static final int MSG_SURFACE_AVAILABLE = 0;
    private static final int MSG_SURFACE_CHANGED = 1;
    private static final int MSG_SURFACE_DESTROYED = 2;
    private static final int MSG_SHUTDOWN = 3;
    private static final int MSG_FRAME_AVAILABLE = 4;
    private static final int MSG_ZOOM_VALUE = 5;
    private static final int MSG_SIZE_VALUE = 6;
    private static final int MSG_ROTATE_VALUE = 7;
    private static final int MSG_POSITION = 8;
    private static final int MSG_REDRAW = 9;
    private static final int MSG_RECORD_STATE = 10;

    // This shouldn't need to be a weak ref, since we'll go away when the Looper quits,
    // but no real harm in it.
    private WeakReference<RenderThread> mWeakRenderThread;

    /**
     * Call from render thread.
     */
    public RenderHandler(RenderThread rt) {
        mWeakRenderThread = new WeakReference<>(rt);
    }

    /**
     * Sends the "surface available" message.  If the surface was newly created (i.e.
     * this is called from surfaceCreated()), set newSurface to true.  If this is
     * being called during Activity startup for a previously-existing surface, set
     * newSurface to false.
     * <p>
     * The flag tells the caller whether or not it can expect a surfaceChanged() to
     * arrive very soon.
     * <p>
     * Call from UI thread.
     */
    public void sendSurfaceAvailable(Object surface) {
        sendMessage(obtainMessage(MSG_SURFACE_AVAILABLE, 0, 0, surface));
    }

    /**
     * Sends the "surface changed" message, forwarding what we got from the SurfaceHolder.
     * <p>
     * Call from UI thread.
     */
    public void sendSurfaceChanged(int format, int width,
                                   int height) {
        // ignore format
        sendMessage(obtainMessage(MSG_SURFACE_CHANGED, width, height));
    }

    /**
     * Sends the "shutdown" message, which tells the render thread to halt.
     * <p>
     * Call from UI thread.
     */
    public void sendSurfaceDestroyed() {
        sendMessage(obtainMessage(MSG_SURFACE_DESTROYED));
    }

    /**
     * Sends the "shutdown" message, which tells the render thread to halt.
     * <p>
     * Call from UI thread.
     */
    public void sendShutdown() {
        sendMessage(obtainMessage(MSG_SHUTDOWN));
    }

    /**
     * Sends the "frame available" message.
     * <p>
     * Call from UI thread.
     */
    public void sendFrameAvailable() {
        sendMessage(obtainMessage(MSG_FRAME_AVAILABLE));
    }

    /**
     * Sends the "rotation" message.
     * <p>
     * Call from UI thread.
     */
    public void sendRotate(int rotation, int cameraId) {
        sendMessage(obtainMessage(MSG_ROTATE_VALUE, rotation, cameraId));
    }

    /**
     * Sends the "preview size" message.
     * <p>
     * Call from UI thread.
     */
    public void sendPreviewSize(int width, int height) {
        sendMessage(obtainMessage(MSG_SIZE_VALUE, width, height));
    }

    public void sendRecordState(boolean state) {
        sendMessage(obtainMessage(MSG_RECORD_STATE, state));
    }

    @Override  // runs on RenderThread
    public void handleMessage(Message msg) {
        int what = msg.what;
        //Log.d(TAG, "RenderHandler [" + this + "]: what=" + what);

        RenderThread renderThread = mWeakRenderThread.get();
        if (renderThread == null) {
            Log.w(TAG, "RenderHandler.handleMessage: weak ref is null");
            return;
        }

        switch (what) {
            case MSG_SURFACE_AVAILABLE:
                renderThread.surfaceAvailable(msg.obj);
                break;
            case MSG_SURFACE_CHANGED:
                renderThread.surfaceChanged(msg.arg1, msg.arg2);
                break;
            case MSG_SURFACE_DESTROYED:
                renderThread.surfaceDestroyed();
                break;
            case MSG_SHUTDOWN:
                renderThread.shutdown();
                break;
            case MSG_FRAME_AVAILABLE:
                renderThread.frameAvailable();
                break;
            case MSG_SIZE_VALUE:
                renderThread.setCameraPreviewSize(msg.arg1, msg.arg2);
                break;
            case MSG_ROTATE_VALUE:
//                    renderThread.setRotate(msg.arg1, msg.arg2);
                break;
            case MSG_RECORD_STATE:
                renderThread.changeRecordingState((boolean) msg.obj);
                break;
            default:
                throw new RuntimeException("unknown message " + what);
        }
    }
}

3. CameraGLTextureView

我们首先定义BaseGLTextureView抽象类,该类可用于Camera和Camera2使用,CameraGLTextureView继承BaseGLTextureView

java 复制代码
public abstract class BaseGLTextureView extends TextureView implements
        TextureView.SurfaceTextureListener,
        SurfaceTexture.OnFrameAvailableListener,
        CameraCallback,
        BaseCameraView, RenderThread.GLSurfaceTextureCallback {
    private static final String TAG = BaseGLTextureView.class.getSimpleName();

    private Context mContext;
    private SurfaceTexture mSurfaceTexture;
    private SurfaceTexture mPreviewSurfaceTexture;
    private boolean isMirror;
    private boolean hasSurface; // 是否存在摄像头显示层
    private ICameraManager mCameraManager;
    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
    private int mPreviewWidth;
    private int mPreviewHeight;
    private RenderThread mRenderThread;
    private TextureMovieEncoder2 mMovieEncoder;

    public BaseGLTextureView(@NonNull Context context) {
        super(context);
        init(context);
    }

    public BaseGLTextureView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public BaseGLTextureView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mContext = context;
        mCameraManager = createCameraManager(context);
        mCameraManager.setCameraCallback(this);
        setSurfaceTextureListener(this);
        mMovieEncoder = new TextureMovieEncoder2(context);
        mRenderThread = new RenderThread(mContext, mMovieEncoder);
        mRenderThread.setGLSurfaceTextureCallback(this);
        mRenderThread.start();
        mRenderThread.waitUntilReady();
    }

    public void startRecord() {
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendRecordState(true);
            }
        }
    }

    public void stopRecord() {
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendRecordState(false);
            }
        }
    }

    public abstract ICameraManager createCameraManager(Context context);

    /**
     * 获取摄像头工具类
     *
     * @return
     */
    public ICameraManager getCameraManager() {
        return mCameraManager;
    }

    /**
     * 是否镜像
     *
     * @return
     */
    public boolean isMirror() {
        return isMirror;
    }

    /**
     * 设置是否镜像
     *
     * @param mirror
     */
    public void setMirror(boolean mirror) {
        isMirror = mirror;
        requestLayout();
    }

    private void setAspectRatio(int width, int height) {
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException("Size cannot be negative.");
        }
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        if (0 == mRatioWidth || 0 == mRatioHeight) {
            setMeasuredDimension(width, width * 4 / 3);
        } else {
            if (width < height * mRatioWidth / mRatioHeight) {
                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);
            } else {
                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);
            }
        }

        if (isMirror) {
            android.graphics.Matrix transform = new android.graphics.Matrix();
            transform.setScale(-1, 1, getMeasuredWidth() / 2, 0);
            setTransform(transform);
        } else {
            setTransform(null);
        }
    }

    /**
     * 获取SurfaceTexture
     *
     * @return
     */
    @Override
    public SurfaceTexture getSurfaceTexture() {
        return mPreviewSurfaceTexture;
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        Logs.i(TAG, "onSurfaceTextureAvailable " + width + "x" + height);
        mSurfaceTexture = surfaceTexture;
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendSurfaceAvailable(mSurfaceTexture);
                handler.sendSurfaceChanged(0, width, height);
            }
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
        Logs.i(TAG, "onSurfaceTextureSizeChanged " + width + "x" + height);
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendSurfaceChanged(0, width, height);
            }
        }
    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
        Logs.v(TAG, "onSurfaceTextureDestroyed.");
        closeCamera();
        mSurfaceTexture = null;
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendSurfaceDestroyed();
            }
        }
        hasSurface = false;
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendFrameAvailable();
            }
        }
    }

    /**
     * Connects the SurfaceTexture to the Camera preview output, and starts the preview.
     */
    @Override
    public void onGLSurfaceCreated(SurfaceTexture st) {
        Logs.i(TAG, "onGLSurfaceCreated " + st);
        mPreviewSurfaceTexture = st;
        hasSurface = true;
        mPreviewSurfaceTexture.setOnFrameAvailableListener(this);
        openCamera();
    }

    /**
     * 打开摄像头并预览
     */
    @Override
    public void onResume() {
        if (hasSurface) {
            // 当activity暂停,但是并未停止的时候,surface仍然存在,所以 surfaceCreated()
            // 并不会调用,需要在此处初始化摄像头
            openCamera();
        }
    }

    /**
     * 停止预览并关闭摄像头
     */
    @Override
    public void onPause() {
        closeCamera();
    }

    @Override
    public void onDestroy() {
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendShutdown();
            }
        }
    }

    /**
     * 初始化摄像头,较为关键的内容
     */
    private void openCamera() {
        if (mPreviewSurfaceTexture == null) {
            Logs.e(TAG, "mSurfaceTexture is null.");
            return;
        }
        if (mCameraManager.isOpen()) {
            Logs.w(TAG, "Camera is opened!");
            return;
        }
        mCameraManager.openCamera();
    }

    private void closeCamera() {
        stopRecord();
        mCameraManager.releaseCamera();
    }

    @Override
    public void onOpen() {
        mCameraManager.startPreview(mPreviewSurfaceTexture);
    }

    @Override
    public void onOpenError(int error, String msg) {

    }

    @Override
    public void onPreview(int previewWidth, int previewHeight) {
        Logs.i(TAG, "onPreview " + previewWidth + " " + previewHeight);
        if (mRenderThread != null) {
            RenderHandler handler = mRenderThread.getHandler();
            if (handler != null) {
                handler.sendPreviewSize(previewWidth, previewHeight);
            }
        }

        mPreviewWidth = previewWidth;
        mPreviewHeight = previewHeight;
        setAspectRatio(mPreviewHeight, mPreviewWidth);
    }

    @Override
    public void onPreviewError(int error, String msg) {

    }

    @Override
    public void onClose() {

    }

    public void setRecordListener(MediaRecordListener recordListener) {
        if (mMovieEncoder != null) {
            mMovieEncoder.setRecordListener(recordListener);
        }
    }
}

最后

本文介绍了Camera+TextureView+OpenGL ES的基本操作及关键代码。与GLSurfaceView的区别就在于我们需要自己创建EGL环境以及自定义渲染线程。

lib-camera库包结构如下:

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

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

github地址:https://github.com/xiaozhi003/AndroidCamerahttps://gitee.com/xiaozhi003/android-camera

参考:

  1. https://github.com/afei-cn/CameraDemo
  2. https://github.com/saki4510t/UVCCamera
  3. https://github.com/google/grafika
相关推荐
-seventy-1 小时前
Android 玩机知识储备
android
CYRUS STUDIO1 小时前
frida脚本,自动化寻址JNI方法
android·运维·自动化·逆向·移动安全·jni·frida
暮志未晚Webgl2 小时前
102. UE5 GAS RPG 实现范围技能奥术伤害
android·java·ue5
Patience to do2 小时前
Android Studio项目(算法计算器)
android·算法·android studio
我又来搬代码了5 小时前
【Android】使用TextView实现按钮开关代替Switch开关
android
点PY6 小时前
camera和lidar外参标定
camera·标定·lidar
江-月*夜8 小时前
uniapp vuex 搭建
android·javascript·uni-app
大风起兮云飞扬丶9 小时前
Android——显式/隐式Intent
android
大风起兮云飞扬丶9 小时前
Android——metaData
android
看山还是山,看水还是。9 小时前
Nginx 的 Http 模块介绍(中)
android·运维·网络·nginx·http