android.media.ImageWriter投递图像帧时隔帧出现绿屏如何解决
问题描述
我开发了一款示例应用,可基于预定义的JPEG测试数据生成YUV图像,将其投递至ImageWriter以模拟Camera API,用于端到端测试,项目名称为Bitmap2YUV。
核心实现代码如下:
package com.example.bitmap2yuv import android.graphics.* import android.media.ImageWriter import android.os.Bundle import android.view.Surface import android.view.TextureView import androidx.appcompat.app.AppCompatActivity import androidx.core.graphics.scale import androidx.lifecycle.lifecycleScope import io.github.crow_misia.libyuv.AbgrBuffer import io.github.crow_misia.libyuv.I420Buffer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch class MainActivity : AppCompatActivity(), TextureView.SurfaceTextureListener { private lateinit var textureView: TextureView private var bitmap: Bitmap? = null private var imageWriter: ImageWriter? = null private var yuvBuffer: I420Buffer? = null private var running = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) textureView = TextureView(this) textureView.surfaceTextureListener = this setContentView(textureView) } private fun createImageWriter(surface: Surface, width: Int, height: Int) { yuvBuffer = I420Buffer.allocate(width, height) bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.BLUE) } val argbBuffer = AbgrBuffer.allocate(width, height) bitmap?.copyPixelsToBuffer(argbBuffer.asBuffer()) argbBuffer.convertTo(yuvBuffer!!) imageWriter = ImageWriter.newInstance(surface, 1, ImageFormat.YUV_420_888) startRendering() } private fun startRendering() { imageWriter?.let { imageWriter -> yuvBuffer?.let { yuvBuffer -> running = true lifecycleScope.launch(Dispatchers.IO) { while (running) { delay(1000) val image = imageWriter.dequeueInputImage() image.planes[0].buffer.put(yuvBuffer.planeY.buffer) image.planes[1].buffer.put(yuvBuffer.planeU.buffer) image.planes[2].buffer.put(yuvBuffer.planeV.buffer) imageWriter.queueInputImage(image) } } } } } override fun onResume() { super.onResume() startRendering() } override fun onStop() { super.onStop() running = false } override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) { createImageWriter(Surface(surfaceTexture), width, height) } override fun onSurfaceTextureSizeChanged(p0: SurfaceTexture, p1: Int, p2: Int) { } override fun onSurfaceTextureDestroyed(p0: SurfaceTexture): Boolean { return true } override fun onSurfaceTextureUpdated(p0: SurfaceTexture) { } }
预期运行效果为页面持续显示蓝色画面(帧率1fps),实际运行时每隔一帧就会出现绿屏,呈现蓝、绿屏交替的异常现象。
故障原因
绿屏交替的核心诱因有三个:
- Buffer位置未复位:从ImageWriter取出的Image各平面Buffer,写入前position默认停留在Buffer末尾,直接调用
put()会从当前position开始写入数据,Buffer起始位置的内存保留默认值0。YUV色彩空间中U、V分量为0时画面呈绿色,第一次写入时如果Buffer初始position为0可以正常显示蓝色,第二次取出Image时Buffer position停在上次写入的末尾,写入失败就会显示默认值对应的绿屏,形成交替现象。 - 未适配YUV_420_888内存布局:libyuv输出的I420格式是连续紧凑存储的,但Android的YUV_420_888格式没有规定统一内存布局,不同设备上平面可能存在行尾填充、U/V交错存储的情况,直接整块连续拷贝会导致数据错位引发色彩异常。
- 重复启动渲染协程:
onResume()和createImageWriter()都会调用startRendering(),没有做运行状态判断会启动多个协程同时写帧,进一步加剧帧数据混乱。
修复方案
- 写入每个平面前,必须将源Buffer和目标Buffer的position复位到0,逐行拷贝时根据行步长跳过填充字节,交错存储场景下按像素步长写入对应分量。
- 在
startRendering()入口增加运行状态判断,避免重复启动多个写帧协程。 - 不再使用整块Buffer拷贝的写法,逐行/逐像素适配不同设备的YUV内存布局。
核心修复后的写帧逻辑参考:
private fun startRendering() { // 防止重复启动渲染协程 if (running) return imageWriter?.let { imageWriter -> yuvBuffer?.let { yuvBuffer -> running = true val width = yuvBuffer.width val height = yuvBuffer.height lifecycleScope.launch(Dispatchers.IO) { while (running) { delay(1000) val image = imageWriter.dequeueInputImage() // 写入Y平面 val yPlane = image.planes[0] val yDst = yPlane.buffer val ySrc = yuvBuffer.planeY.buffer yDst.position(0) ySrc.position(0) repeat(height) { row -> ySrc.limit(ySrc.position() + width) yDst.put(ySrc) yDst.position(yDst.position() + yPlane.rowStride - width) } // 写入U平面 val uPlane = image.planes[1] val uDst = uPlane.buffer val uSrc = yuvBuffer.planeU.buffer uDst.position(0) uSrc.position(0) if (uPlane.pixelStride == 1) { // 连续存储布局 repeat(height / 2) { row -> uSrc.limit(uSrc.position() + width / 2) uDst.put(uSrc) uDst.position(uDst.position() + uPlane.rowStride - width / 2) } } else { // U/V交错存储布局 repeat(height / 2) { row -> repeat(width / 2) { col -> uDst.put(uSrc.get()) uDst.position(uDst.position() + uPlane.pixelStride - 1) } uDst.position((row + 1) * uPlane.rowStride) } } // 写入V平面 val vPlane = image.planes[2] val vDst = vPlane.buffer val vSrc = yuvBuffer.planeV.buffer vSrc.position(0) if (vPlane.pixelStride == 1) { vDst.position(0) repeat(height / 2) { row -> vSrc.limit(vSrc.position() + width / 2) vDst.put(vSrc) vDst.position(vDst.position() + vPlane.rowStride - width / 2) } } else { vDst.position(1) repeat(height / 2) { row -> repeat(width / 2) { col -> vDst.put(vSrc.get()) vDst.position(vDst.position() + vPlane.pixelStride - 1) } vDst.position((row + 1) * vPlane.rowStride + 1) } } imageWriter.queueInputImage(image) } } } } }
替换原有逻辑后即可稳定显示蓝色画面,不会再出现交替绿屏问题。
内容的提问来源于stack exchange,提问作者Adjorno
相关产品推荐
相关产品推荐

