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

news2024/11/26 6:13:39

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

本系列主要讲述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;
    }

    @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使用

/**
 * 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 context
        createWindowSurface(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 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代码如下:

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

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库包结构如下:

说明
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.coloradmin.cn/o/2201819.html

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

相关文章

单片机(学习)2024.10.9

目录 汇编整体分类 1.指令 2.伪操作 3.伪指令 汇编代码 汇编初始化 数据搬运指令 算术运算指令 加法 减法 乘法 比较指令 跳转指令 逻辑运算指令 与或&#xff0c;异或 左移右移 内存操作 LOAD/STORE 指令 写 读 CPU的栈机制 栈的概念 栈的种类 1.空栈(…

设备多久(60/50/40min)未上报,类似场景发送通知实现方案

场景描述 设备比较多&#xff0c;几十万甚至上百万&#xff0c;设备在时不时会上报消息。 用户可以设置设备60分钟、50分钟、40分钟、30分钟未上报数据&#xff0c;发送通知给用户&#xff0c;消息要及时可靠。 基本思路 思路&#xff1a; 由于设备在一直上报&#xff0c;如果…

叶国富的永辉填坑之旅

叶国富体验了一把过山车&#xff01;永辉的难题逐渐转移到名创优品&#xff0c;后者是否能应对这些问题&#xff0c;以及其股价的徘徊&#xff0c;都预示着挑战才刚刚开始。 转载&#xff1a;原创新熵 作者丨樱木 编辑丨蕨影 低迷了3年的二级市场&#xff0c;迎来了超级反转&…

【金九银十】笔试通关 + 小学生都能学会的堆排序

算法原理 堆排序是一种基于比较的排序算法&#xff0c;它利用了数据结构中的堆&#xff08;Heap&#xff09;。堆是一种特殊的完全二叉树&#xff0c;分为最大堆&#xff08;Max-Heap&#xff09;和最小堆&#xff08;Min-Heap&#xff09;。在最大堆中&#xff0c;每个父节点…

单场数字人直播爆量300万,GMV狂增80%,电商人如何玩转数字人直播?

单场直播带货300万&#xff0c;在头部主播那里也许不算什么。但如果告诉你&#xff0c;这是数字人直播做出的成绩&#xff0c;你会惊讶吗&#xff1f; 苏宁借力电商数字人开播&#xff0c;直播时长比以往能增加3倍&#xff0c;GMV增量80%&#xff0c;下单转化57%&#xff0c;不…

通过祖先序列重建辅助工程化UDP-糖基转移酶-文献精读64

Engineering the Substrate Specificity of UDP-Glycosyltransferases for Synthesizing Triterpenoid Glycosides with a Linear Trisaccharide as Aided by Ancestral Sequence Reconstruction 通过祖先序列重建辅助工程化UDP-糖基转移酶的底物特异性&#xff0c;用于合成具…

RWKV-CHN模型部署教程

一、模型介绍 RWKV 语言模型&#xff08;用纯 100%RNN 达到 GPT 能力&#xff0c;甚至更强&#xff09;&#xff0c;该项目旨在通过为您自动化所有事情来消除使用大型语言模型的障碍。您需要的是一个只有几兆字节的轻量级可执行程序。此外&#xff0c;该项目还提供了一个接口兼…

Vue打印网页pdf,并且有按钮调整缩小放大

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

[AutoSar]BSW_Diagnostic_005 RoutineControl service (0x31)介绍

目录 关键词平台说明背景一、请求格式二、sub-function definition三、响应格式四、NRC五、case 关键词 嵌入式、C语言、autosar、OS、BSW、UDS、diagnostic 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商TI编程语言C&#xff0c;C编译器HighTec (GCC)autosar版…

录屏工具分享

遇到问题 现在很多录屏工具都是要会员 要么就不清&#xff0c;压缩画质 解决方案 &#xff08;1&#xff09;QQ录屏 QQ录屏缺点就是界面上会有个录屏计时阻挡。没有影响的话可以使用。录几分钟出来也是几百M的容量 &#xff08;2&#xff09;格式工厂 录的视频很清晰&…

打造梦幻AI开发环境:一步步解锁高效配置的魅力

作者简介&#xff1a;我是团团儿&#xff0c;是一名专注于云计算领域的专业创作者&#xff0c;感谢大家的关注 座右铭&#xff1a; 云端筑梦&#xff0c;数据为翼&#xff0c;探索无限可能&#xff0c;引领云计算新纪元 个人主页&#xff1a;团儿.-CSDN博客 目录 前言&#…

【如何保存Pixso中原型图的图标】

【如何保存Pixso中原型图的图标】 在软件UI设计完成后&#xff0c;设计师需要将设计中的图标导出为开发团队所需的图片文件&#xff0c;以便进行后续开发工作。pixso中原型图的图标到处如下图&#xff0c;按照序号操作流程即可到处图片。

wireshark获取QQ图片

今天随手写下之前做的一个比较有意思的实验&#xff1a;Wireshark抓取qq图片 1.前提 手机和电脑处于同一网络之中&#xff0c;这里我使用了校园网。 接着使用手机向电脑发出图片 2.wireshark流量抓包 先查看好手机的ip地址&#xff0c;随后使用命令&#xff1a;ip.src10.33.X…

MybatisPlus的日常使用

一、基础接口 public interface BaseMapper<T> {/*** 插入一条记录* param entity 实体对象*/int insert(T entity);/*** 根据 ID 删除* param id 主键ID*/int deleteById(Serializable id);/*** 根据 columnMap 条件&#xff0c;删除记录* param columnMap 表字段 map …

Anthropic Message Batches API 满足批量处理大量请求

现在开发的系统有大量知识汇总统计、跑批处理需求的同学可以尝试一下&#xff0c;看看能不能解决自己目前的问题~~ 可能是一个解决方案 Anthropic 推出的 Message Batches API &#xff0c;专门用于帮助开发者批量处理大量请求。它的主要目的是通过一次性处理大量非实时任务&a…

Linux工具的使用——【gcc/g++的使用】【make/Makefile的使用】【如何让普通用户使用sudo】

目录 Linux工具的使用-021.如何让普通用户使用sudo1.1为什么无法使用sudo1.2解决步骤1.3验证 2.编译器gcc/g的使用2.1预处理2.2编译2.3汇编2.4链接2.5函数库2.5.1静态库与动态库2.5.1.1动态链接2.5.1.2静态链接 2.6gcc的默认链接方式2.7gcc的静态链接2.8g的使用2.8.1g的静态链接…

Apache Flink Dashboard

1、Overview Apache Flink Web Dashboardhttp://110.40.130.231:8081/#/overview 这张图片显示的是Apache Flink的Web UI界面&#xff0c;其中包含了以下几个部分&#xff1a; Available Task Slots: 显示当前可用的任务槽位数量。任务槽位是指Flink集群中可用于运行任务的资…

【华为HCIP实战课程十】OSPF网络DR和BDR实战讲解,网络工程师

一、DR与BDR的基础介绍 点到点同步LSA成本小 多点接入网络同步LSA成本大,需要DR/BDR 由于MA网络中,任意两台路由器都需要传递路由信息,网络中有n台路由器,则需要建立n*(n-1)/2个邻接关系。任何一台路由器的路由变化都会导致多次传递,浪费了带宽资源,DR和BDR应运而生!…

大数据存储计算平台EasyMR:多集群统一管理助力企业高效运维

随着全球企业进入数字化转型的快车道&#xff0c;数据已成为企业运营、决策和增长的核心驱动力。为了处理海量数据&#xff0c;同时应对数据处理的复杂性和确保系统的高可用性&#xff0c;企业往往选择部署多个Hadoop集群&#xff0c;这样的策略可以将生产环境、测试环境和灾备…

分布式 ID

背景 在复杂分布式系统中&#xff0c;往往需要对大量的数据和消息进行唯一标识。随着数据日渐增长&#xff0c;对数据分库分表后也需要有一个唯一ID来标识一条数据或消息&#xff0c;数据库的自增 ID 显然不能满足需求&#xff1b;此时一个能够生成全局唯一 ID 的系统是非常必…