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

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

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

Alt

本章我们讲解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核心工具类

/*** 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;}@Overrideprotected 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使用

/*** 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

/*** 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 contextcreateWindowSurface(mSurface);  // create new surface}
}

2. 自定义RenderThread

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

public class RenderThread extends Thread {public void surfaceAvailable(Object surface) {}public void surfaceChanged(int width, int height) {}public void surfaceDestroyed() {}public void frameAvailable() {}
}

完整代码:

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 framesprivate 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();}@Overridepublic 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 recordingmVideoEncoder.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:// yaybreak;default:throw new RuntimeException("unknown status " + mRecordingStatus);}} else {switch (mRecordingStatus) {case RECORDING_ON:case RECORDING_RESUMED:// stop recordingLog.d(TAG, "STOP recording");mVideoEncoder.stopRecord();mRecordingStatus = RECORDING_OFF;break;case RECORDING_OFF:// yaybreak;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 framebufferswapResult = 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();// 切换为屏幕SurfacemWindowSurface.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();// RestoreGLES20.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代码如下:

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 formatsendMessage(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 RenderThreadpublic 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

public abstract class BaseGLTextureView extends TextureView implementsTextureView.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();}@Overrideprotected 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*/@Overridepublic SurfaceTexture getSurfaceTexture() {return mPreviewSurfaceTexture;}@Overridepublic 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);}}}@Overridepublic 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);}}}@Overridepublic 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;}@Overridepublic void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {}@Overridepublic 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.*/@Overridepublic void onGLSurfaceCreated(SurfaceTexture st) {Logs.i(TAG, "onGLSurfaceCreated " + st);mPreviewSurfaceTexture = st;hasSurface = true;mPreviewSurfaceTexture.setOnFrameAvailableListener(this);openCamera();}/*** 打开摄像头并预览*/@Overridepublic void onResume() {if (hasSurface) {// 当activity暂停,但是并未停止的时候,surface仍然存在,所以 surfaceCreated()// 并不会调用,需要在此处初始化摄像头openCamera();}}/*** 停止预览并关闭摄像头*/@Overridepublic void onPause() {closeCamera();}@Overridepublic 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();}@Overridepublic void onOpen() {mCameraManager.startPreview(mPreviewSurfaceTexture);}@Overridepublic void onOpenError(int error, String msg) {}@Overridepublic 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);}@Overridepublic void onPreviewError(int error, String msg) {}@Overridepublic void onClose() {}public void setRecordListener(MediaRecordListener recordListener) {if (mMovieEncoder != null) {mMovieEncoder.setRecordListener(recordListener);}}
}

最后

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

lib-camera库包结构如下:

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

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

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

参考:

  1. https://github.com/afei-cn/CameraDemo
  2. https://github.com/saki4510t/UVCCamera
  3. https://github.com/google/grafika

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1558597.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

一键生成PPT在线使用的保姆级教程:告别加班就靠它

已经过完24年所有的法定节假日的你&#xff0c;上班状态还好吗&#xff1f; 小编人倒是挺飘忽的&#xff0c;就那种人在工位&#xff0c;魂仍在青青大草原的感觉&#xff0c;都是牛马却失去了自由奔跑的权利...... 尤其是还要面对节前一堆没完成的工作&#xff0c;手动完成不…

Day-1 java入门

什么是JAVA? Java是美国的sun 公司(Stanford University Network)在1995年推出的一门计算机高级编程语言。 sun公司于2009年被Oracle(甲骨文)公司收购。 普遍认同Java的联合创始人之一:詹姆斯 高斯林 (James Gosling)为ava之父。 JAVA三大技术平台 Java SE(Java Standa…

大模型 + 在线运行 + ISRealsoft ⇒ 编程运行验证一体化

最近在撰写《计算机的错误计算》系列内容。其中用到三种在线工具&#xff0c;分别是大语言模型、在线运行软件以及 ISRealsoft 在线软件。 系列主要介绍各种软硬件关于数值计算的误差或错误计算。比如&#xff0c;各种数学库 math 中涉及的函数的误差&#xff0c;或算术表达式&…

用伪代码Prompt让LLM进行图推理,生成更精准内容

最近有研究发现&#xff0c;当LLM面对结构化数据&#xff0c;特别是图数据时&#xff0c;LLM的表现却不尽如人意。这几天&#xff0c;来自希腊和法国的研究团队提出了一种创新方法——利用伪代码提示来增强LLM的图推理能力。我基于这项研究先写了一个伪代码的SYSYTEM PROMPT运行…

【智能算法应用】正切搜索算法求解二维路径规划问题

摘要 本文提出了基于正切搜索算法的二维路径规划方法&#xff0c;用于解决包含障碍物的复杂路径规划问题。通过在二维平面中建立障碍物模型和路径目标点&#xff0c;利用正切搜索算法进行路径搜索&#xff0c;找出从起点到终点的最优路径。实验结果显示&#xff0c;该算法在不…

5. PH47 代码框架软件开发环境搭建

5.1. 概述 PH47 软件开发环境搭建比较简单&#xff0c;但毫无疑问非常重要。主要涉及到 stm32 编译链接工具 Keil&#xff1b;代码编辑器 Visual Studio 或者 VS code。 若需要更进一步进行 PH47 框架在不同 stm32 芯片间移植&#xff0c;那么还需要Stm32CubeMx 初始化代码生成…

Python 如何使用 SQLAlchemy 进行复杂查询

Python 如何使用 SQLAlchemy 进行复杂查询 一、引言 SQLAlchemy 是 Python 生态系统中非常流行的数据库处理库&#xff0c;它提供了一种高效、简洁的方式与数据库进行交互。SQLAlchemy 是一个功能强大的数据库工具&#xff0c;支持结构化查询语言&#xff08;SQL&#xff09;…

小白入门《大模型应用开发极简入门》学习成为善用 AI 的人!

《大模型应用开发极简入门&#xff1a;基于 GPT-4 和 ChatGPT》这本书旨在为读者提供一个从零开始&#xff0c;快速掌握大语言模型&#xff08;LLM&#xff09;开发的入门指南&#xff0c;特别是基于 GPT-4 和 ChatGPT 的应用开发。书中内容涵盖了大模型的基础概念、架构原理、…

PCL 计算点云包围盒

目录 一、概述二、代码三、结果 一、概述 PCL中计算点云包围盒的简单使用案例 二、代码 moment_of_inertia.cpp #include <vector> #include <thread>#include <pcl/features/moment_of_inertia_estimation.h> #include <pcl/io/pcd_io.h> #include…

使用java分别输出二叉树的深度遍历和广度遍历

代码功能 这段Java代码定义了一个二叉树&#xff0c;并实现了两种遍历方法&#xff1a;深度优先搜索&#xff08;DFS&#xff09;和广度优先搜索&#xff08;BFS&#xff09;。通过DFS&#xff0c;代码从根节点开始&#xff0c;优先访问子节点&#xff0c;直至最深的节点&…

常用的十款文件加密软件分享|2024办公文件怎么加密?赶快码住!

在现代办公环境中&#xff0c;数据安全和隐私保护变得尤为重要&#xff0c;尤其是随着远程办公、跨平台协作的普及&#xff0c;文件的加密需求大大增加。为了保障敏感信息的安全性&#xff0c;选择合适的加密软件成为必不可少的一步。本文将为大家推荐2024年常用的十款文件加密…

‌视频画面添加滚动字幕剪辑:提升观众体验的创意技巧

在视频制作中&#xff0c;字幕不仅是传达信息的重要工具&#xff0c;也是提升观众体验的关键元素。本文将探讨如何在视频画面中添加滚动字幕剪辑&#xff0c;以提升观众的观看体验。 1打开软件&#xff0c;在功能栏里切换到“任务剪辑”版块上 2添加原视频导入到表格里&#x…

简单花20分钟学会top 命令手册 (linux上的任务管理器)

1. 介绍 top 是一个常用的 Linux 命令行工具&#xff0c;用于实时监视系统资源和进程的运行情况。用户可以通过 top 命令查看系统的 CPU 使用率、内存占用情况、进程列表等重要信息&#xff0c;帮助快速了解系统运行状态并进行性能监控。该工具可以认为相当于windows上的任务管…

探索Theine:Python中的AI缓存新贵

文章目录 探索Theine&#xff1a;Python中的AI缓存新贵背景&#xff1a;为何选择Theine&#xff1f;Theine是什么&#xff1f;如何安装Theine&#xff1f;简单的库函数使用方法场景应用场景一&#xff1a;Web应用缓存场景二&#xff1a;分布式系统中的数据共享场景三&#xff1…

【DFDT】DFDT: An End-to-End DeepFake Detection Framework Using Vision Transformer

文章目录 DFDT: An End-to-End DeepFake Detection Framework Using Vision Transformerkey points贡献方法补丁提取和嵌入基于注意力的补丁选择多流transformer块多尺度分类器实验DFDT: An End-to-End DeepFake Detection Framework Using Vision Transformer 会议/期刊:App…

Java 函数式编程(1 万字)

此笔记来自于B站黑马程序员 good Java 历史版本及其优势 函数式编程, Stream API 一.函数伊始函数、函数对象 函数对象 行为参数法 延迟执行 a-lambda b-方法引用 复习小测 Math::random () -> Math.random()Math::sqrt (double number) -> Math.sqrt(number)Student:…

光路科技TSN交换机:驱动自动驾驶技术革新,保障高精度实时数据传输

自动驾驶技术正快速演进&#xff0c;对实时数据处理能力的需求激增。光路科技推出的TSN&#xff08;时间敏感网络&#xff09;交换机&#xff0c;在比亚迪最新车型中的成功应用&#xff0c;显著推动了这一领域的技术进步。 自动驾驶技术面临的挑战 自动驾驶系统需整合来自雷达…

揭秘!尤雨溪成立的VoidZero如何改变前端世界

前言 Vue和Vite之父尤雨溪宣布成立公司 VoidZero&#xff0c;目前已经融资3200万。这篇文章欧阳将带你了解VoidZero是如何改变javascript的世界&#xff01; 加入欧阳的高质量vue源码交流群、欧阳平时写文章参考的多本vue源码电子书 痛点1: 工具太多&#xff0c;学不动 公司…

Library介绍(四)

标准单元描述 标准单元主要由以下几个部分构成&#xff0c;分别是引脚电容、power、timing组成。其中引脚电容主要包含input/output pin的电容值。 power主要包含每个pin的leakage power和internal power。 timing主要包括cell的input pin到output pin的rise delay和fall del…

Shuffle Net系列详解 (1) Shuffle Net论 V1论文理论部分详解

Shuffle Net 系列 论文精讲部分0.摘要1. 引文2. 相关工作3. Approach方法3.1 Channel Shuffle for Group Convolutions 通道重排针对分组卷积3.2 模型块Blocka Blockb Blockc Block 3.3 模型整体架构 4 实验5 总结 论文精讲部分 本专栏致力于深度剖析轻量级模型相关的学术论文…