将Camera2从ImageReader传递图像到MediaRecorder。

10
我正在尝试创建一个能够具备四个输出功能的Camera2 CameraCaptureSession:
1. 屏幕预览 (SurfaceView, 最高1080p) 2. 拍照 (ImageReader, 最高8k分辨率) 3. 录制视频 (MediaRecorder/MediaCodec, 最高4k分辨率) 4. 帧处理 (ImageReader, 最高4k视频帧)
不幸的是,Camera2并不支持同时连接这四个输出(Surface),所以我必须做出妥协。
在我看来,最合理的妥协方案是将两个视频捕获管道合并为一个,这样帧处理输出(#4, ImageReader)就可以重定向到视频捕获输出 (#3, MediaRecorder)。
那么如何从ImageReader中写入图像呢?
val imageReader = ImageReader.newInstance(4000, 2256, ImageFormat.YUV_420_888, 3)
imageReader.setOnImageAvailableListener({ reader ->
  val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
  callback.onVideoFrameCaptured(image)
}, queue.handler)

val captureSession = device.createCaptureSession(.., imageReader.surface)

MediaRecorder录制进入Surface
val surface = MediaCodec.createPersistentInputSurface()
val recorder = MediaRecorder(context)
..
recorder.setInputSurface(surface)

我在考虑这里可能需要一个OpenGL管线,使用一个透传着色器 - 但是我不知道如何将ImageReaderImage转换为OpenGL纹理,所以希望能得到帮助。
我尝试了一下:我研究了HardwareBuffer的API,具体来说。
auto clientBuffer = eglGetNativeClientBufferANDROID(hardwareBuffer);
...
auto image = eglCreateImageKHR(display,
                               EGL_NO_CONTEXT,
                               EGL_NATIVE_BUFFER_ANDROID,
                               clientBuffer,
                               attribs);
...
glEGLImageTargetTexture2DOES(GR_GL_TEXTURE_EXTERNAL, image);

我觉得这个可能行得通,但它需要API Level 28。所以我仍然需要一个适用于API Level 23及以上的解决方案。`image.getPlanes()`函数为我返回了三个`ByteBuffer`,其中包含YUV数据,但我不确定如何从中创建OpenGL纹理..

你有尝试过使用由SurfaceTexture创建的SurfaceCameraCaptureSession吗?这样可以将视频帧作为OpenGL外部纹理可用。 - dev.bmax
@dev.bmax 是的,我确实这样做了,但是我需要Image实例,因为我使用其他只接受Image作为参数的机器学习API。 - mrousavy
换句话说,我需要首先使用ImageReader,然后通过OpenGL管线来绘制到视频录制表面。 - mrousavy
2个回答

1
如果我理解正确,您有一个android.media.Image需要在OpenGL管道中使用。
我建议按照以下步骤进行设置:
1. 使用glGenTextures()创建一个OpenGL纹理。 2. 使用前一步骤中的纹理名称(实际上是一个整数)创建一个SurfaceTexture。 3. 从SurfaceTexture创建一个Surface。 4. 使用前一步骤中的Surface创建一个ImageWriter。 5. 对于每个视频帧,调用queueInputImage(),使用可用的Image。

**在准备SurfaceTexture之后,请不要忘记调用setOnFrameAvailableListener(),然后对每个视频帧进行updateTexImage()

***如果您想正确处理图像方向,可以在顶点着色器中使用getTransformMatrix()并转换纹理坐标。


嘿,谢谢你的回复!我能够跳过OpenGL部分,直接使用"ImageWriter"将图像写入"MediaRecorder"表面(请参阅我上面的答案)。 - mrousavy
这种方法的问题在于有一些小问题,我不知道为什么,你觉得额外的OpenGL路径能解决这个问题吗? - mrousavy
我对MediaRecorder的内部机制不太熟悉,但我怀疑它会忽略图像的呈现时间戳。这可能导致播放不均匀。使用MediaCodecMediaMuxer可能更适合这个任务。 - dev.bmax
是的,我很可能会在自定义的OpenGL流水线中使用MediaCodec + MediaMuxer,因为我需要一些更高级的功能。然后,我只需要弄清楚如何将OpenGL纹理写入图像/图像编写器。 - mrousavy
嘿 @dev.bmax!我现在正在使用ImageWriter + MediaRecorder,但出现了一个问题,我无法将图像传递到MediaRecorder的表面。你有什么想法吗?https://stackoverflow.com/questions/77152531 - undefined

1
我(有点)弄明白了!我找到了ImageWriter API,这正是我打算从头开始重建的东西 - 一个从图像到表面的传递管道。
所以现在我将相机帧流式传输到ImageReader中,使用Image调用帧处理器,然后通过ImageWriter作为中间人将Image传递给MediaRecorder :)
val size = config.getOutputSizes(ImageFormat.PRIVATE).max()

// Video Recorder Surface. We need to stream Frames here if we are recording.
val surface = recordingSession.surface
val imageWriter = ImageWriter.newInstance(surface,
                                          VIDEO_OUTPUT_BUFFER_SIZE)

// Image Reader Surface. We stream Frames here for Frame Processor or Recording.
val flags = HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE or HardwareBuffer.USAGE_VIDEO_ENCODE
val imageReader = ImageReader.newInstance(size.width,
                                          size.height,
                                          ImageFormat.PRIVATE,
                                          VIDEO_OUTPUT_BUFFER_SIZE,
                                          flags)

imageReader.setOnImageAvailableListener({ reader ->
  val image = reader.acquireNextImage() ?: return
  image.timestamp = System.nanoTime()

  // Call JS Frame Processor
  frameProcessor?.call(image)
                                         
  // If recording, write to Video File             
  if (isRecording) {
    imageWriter.queueInputImage(image)
  }

  image.close()
}, CameraQueues.videoQueue)

// Camera only streams frames into one single Surface
cameraSession.configure(.., imageReader.surface)

我的唯一问题是,录制的视频有时会在录制约3秒后出现大约1秒的卡顿,我不知道为什么。也许我应该使用MediaCodec而不是MediaRecorder。也许我应该使用不同的ImageFormat。也许我应该调查生成的.mp4文件,看看出了什么问题。也许我应该修复图像时间戳。我不知道。
此外,logcat被这个信息刷屏了:
2023-08-17 11:38:17.977  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
2023-08-17 11:38:18.021  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
2023-08-17 11:38:18.050  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
2023-08-17 11:38:18.082  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
2023-08-17 11:38:18.113  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
2023-08-17 11:38:18.146  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
2023-08-17 11:38:18.179  3780-3899  GraphicBufferSource     com.mrousavy.camera.example          W  released unpopulated slots: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]

但是嘿 - 它可以录视频。这是一个好的开始。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接