别人贪婪时我恐惧,别人恐惧时我贪婪
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区别如下:
- EGL环境封装
- 自定义渲染线程
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/AndroidCamera,https://gitee.com/xiaozhi003/android-camera
参考: