Android Camera2:即时更改输出表面集的最佳和快速方法

Android Camera2: the most optimal and fast way to change the output surface set on-the-fly

我正在制作一个视频流应用程序,它可以根据可用的上行链路带宽调整视频比特率,我希望它能够动态更改视频分辨率,以便在较低的比特率下不会出现太多的压缩伪影。虽然我通过释放 MediaCodec 并在 CameraCaptureSession 上调用 abortCaptures()stopRepeating() 然后为新分辨率配置所有内容来完成这项工作,但这会导致非常明显的中断在流中 - 在我的测试中至少有半秒。

当相机本身不支持所需的分辨率时,我使用 OpenGL 缩放图像,类似于 this。我使用两个表面初始化捕获会话 - 一个用于向用户预览(使用 TextureView),另一个用于编码器,即 MediaCodec 的直接输入表面或我的 OpenGL 纹理表面。

这可能会通过使用 MediaCodec.createPersistentInputSurface() 来解决,因为我将能够在分辨率更改时重用缩放器的这个实例,并且不必对捕获会话执行任何操作,因为不会发生表面更改就相机而言,但它仅在 API 23 之后可用,我需要此实现也支持 API 21。

然后还有表面失效和重新创建的问题。例如,当用户按下后退按钮时,其中包含的 activity 和 TextureView 将被销毁,从而使预览表面无效。然后,当用户再次导航到 activity 时,会创建一个新的 TextureView,我需要开始在其中显示预览,而不会给 scaler/encoder 看到的流带来任何延迟。

所以,我的一般问题是:如何更改 CameraCaptureSession 中的输出表面集或重新创建 CameraCaptureSession,同时尽可能少地在视频流中引入延迟?

事实证明,实际上保存纹理的 OpenGL 上下文(包括相机向其提供帧的纹理)与任何特定的输出目标无关。所以我可以让我的视频缩放器在初始化后改变它的输出表面,像这样:

...
}else if(inputMessage.what==MSG_CHANGE_SURFACE){
    // Detach the current thread from the context, as a precaution
    EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
    checkEglError("eglMakeCurrent 1");

    // Destroy the old EGL surface and release its Android counterpart
    // the old surface belongs to the previous, now released, MediaCodec instance
    EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
    checkEglError("eglDestroySurface");
    surface.release(); // surface is a field that holds the current MediaCodec encoder input surface

    surface=(Surface) inputMessage.obj;
    dstW=inputMessage.arg1; // these are used in glViewport and the fragment shader
    dstH=inputMessage.arg2;

    // Create a new EGL surface for the new MediaCodec instance
    int[] surfaceAttribs={
        EGL14.EGL_NONE
    };
    mEGLSurface=EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], surface, surfaceAttribs, 0);
    checkEglError("eglCreateWindowSurface");

    // Make it current for the current thread
    EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext);
    checkEglError("eglMakeCurrent 2");

    // That's it, any subsequent draw calls will render to the new surface
}

使用这种方法,不需要重新初始化 CameraCaptureSession,因为相机输出到的表面集没有变化。