НейроАгент

Эффективная отрисовка YUV с доступом к кадрам на Android

Узнайте, как добиться аппаратно-ускоренной отрисовки YUV на Android при сохранении доступа к кадрам для RTSP-потоков. Изучите методы оптимизации для устранения узких мест при преобразовании YUV в RGB и поддержания производительности 30 кадров в секунду.

Вопрос

Эффективное аппаратно-ускоренное рендеринг YUV на Android с доступом к кадрам

Постановка проблемы

Мы разрабатываем приложение для Android, которое обрабатывает живой RTSP-поток с видеокадрами H.264 с частотой 30 кадров в секунду. Наша цель:

  1. Рендерить декодированные YUV-кадры на экране с аппаратным ускорением
  2. Предоставлять доступ к декодированным кадрам, чтобы пользователи SDK могли сохранять их на диск при необходимости

Текущие проблемы реализации

При использовании MediaCodec для декодирования мы столкнулись с фундаментальным ограничением:

  • Если мы предоставляем Surface в метод configure MediaCodec, мы можем рендерить на экран, но теряем доступ к outputImage и outputBuffer в формате YUV, необходимым для сохранения кадров
  • Если мы передаем null в качестве Surface, мы получаем доступ к данным YUV, но должны преобразовать их в RGB для рендеринга, что создает узкие места в производительности

Наш текущий подход

Мы реализовали решение с использованием SurfaceTexture и OpenGL для преобразования YUV в RGB:

Настройка MediaDecoder

kotlin
val decoder = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply {
    setByteBuffer("csd-0", ByteBuffer.wrap(sps))
    setByteBuffer("csd-1", ByteBuffer.wrap(pps))
}

decoder.setCallback(object : MediaCodec.Callback() {
    // ... реализация обратного вызова для извлечения YUV-кадров
})

decoder.configure(format, null, null, 0)
decoder.start()

OpenGL рендерер

kotlin
internal class YuvGLRenderer(private val surfaceTexture: SurfaceTexture) {
    // ... полная реализация OpenGL с шейдерами преобразования YUV в RGB
}

Интеграция с View

kotlin
class RTSPPlayerView : TextureView, TextureView.SurfaceTextureListener {
    // ... интеграция с SurfaceTexture и рендерером
}

Проблемы с производительностью

Несмотря на нашу реализацию, мы сталкиваемся со значительными проблемами задержки при текущем подходе. Преобразование YUV в RGB в OpenGL кажется излишне сложным для нашего случая использования, и мы ищем более эффективное решение.

Вопрос

Не могли бы вы дать совет о том, как эффективно рендерить YUV-изображения на экране, сохраняя при этом доступ к необработанным данным кадров для сохранения? В частности, мы ищем:

  1. Подход с аппаратным ускорением, который избегает узкого места преобразования YUV в RGB
  2. Решение, хорошо работающее с живыми потоками 30 кадров в секунду
  3. Минимальную задержку между декодированием и рендерингом
  4. Чистую интеграцию с MediaCodec и конвейером рендеринга Android

Будем благодарны за любые альтернативные архитектуры, оптимизации или лучшие практики.

НейроАгент

Аппаратно-ускоренная отрисовка YUV в Android с доступом к кадрам

Аппаратно-ускоренная отрисовка YUV в Android с доступом к кадрам представляет собой фундаментальный компромисс в медиапайплайне Android. Проблема, с которой вы столкнулись, хорошо задокументирована - при использовании MediaCodec необходимо выбирать между аппаратным ускорением (через Surface) и доступом к кадрам (через SurfaceTexture/null), но не одновременно. Однако существует несколько продвинутых подходов, которые позволяют достичь ваших целей при правильной оптимизации.

Содержание

Понимание компромисса MediaCodec Surface

Фундаментальное ограничение, которое вы выявили, связано с дизайном архитектуры медиа в Android. При настройке MediaCodec с Surface декодер использует аппаратное ускорение, но передает вывод непосредственно в дисплейный пайплайн, делая данные кадра недоступными. При передаче null вы получаете доступ к выходным буферам, но теряете преимущества аппаратной отрисовки.

Согласно документации Android, аппаратное ускорение означает “все операции отрисовки, выполняемые на холсте View, используют GPU”. Это создает дилемму, с которой вы столкнулись - GPU может эффективно отрисовывать, но не предоставляет доступ к необработанным данным.

Ключевое понимание заключается в том, что аппаратное декодирование и аппаратная отрисовка - это отдельные оптимизации, которые не обязательно работают вместе, когда вам нужен доступ к кадрам.

Продвинутые стратегии оптимизации SurfaceTexture

Ваш текущий подход с использованием SurfaceTexture и OpenGL концептуально верен, но может потребовать оптимизации. Вот несколько стратегий для улучшения производительности:

1. Подход с двойным буфером и TextureView

kotlin
class OptimizedRTSPPlayerView : TextureView, TextureView.SurfaceTextureListener {
    private var mediaCodec: MediaCodec? = null
    private var surfaceTexture: SurfaceTexture? = null
    private var renderer: YuvGLRenderer? = null
    private var frameProcessingEnabled = false
    
    fun enableFrameProcessing(enable: Boolean) {
        frameProcessingEnabled = enable
        // Переключение между прямой отрисовкой и обработкой кадров
        updateRenderingMode()
    }
    
    private fun updateRenderingMode() {
        mediaCodec?.let { decoder ->
            val surface = if (frameProcessingEnabled) {
                // Использование SurfaceTexture для доступа к кадрам
                surfaceTexture?.let { Surface(it) }
            } else {
                // Использование прямого Surface для аппаратной отрисовки
                Surface(surfaceTexture)
            }
            decoder.setOutputSurface(surface)
        }
    }
}

Этот подход позволяет переключаться между режимами в зависимости от того, в данный момент ли нужна обработка кадров.

2. Асинхронный пайплайн обработки кадров

kotlin
class AsyncFrameProcessor {
    private val processingQueue = ConcurrentLinkedQueue<FrameData>()
    private val processingThread = HandlerThread("FrameProcessor").apply { start() }
    private val processingHandler = Handler(processingThread.looper)
    
    fun submitFrame(frame: FrameData) {
        if (shouldProcessFrame(frame)) {
            processingQueue.offer(frame)
            processingHandler.post { processNextFrame() }
        }
    }
    
    private fun processNextFrame() {
        val frame = processingQueue.poll() ?: return
        // Обработка кадра в асинхронном режиме
        processFrameInBackground(frame)
    }
    
    private fun shouldProcessFrame(frame: FrameData): Boolean {
        // Реализация логики определения, какие кадры нуждаются в обработке
        return frame.timestamp % frameSkipInterval == 0L
    }
}

Гибридная архитектура отрисовки

Сложный подход включает создание гибридной архитектуры, которая стратегически использует как Surface, так и SurfaceTexture:

kotlin
class HybridMediaDecoder {
    private val mainCodec: MediaCodec
    private val processingCodec: MediaCodec?
    private val mainSurface: Surface
    private val processingSurface: Surface?
    
    init {
        // Основной декодер для отображения
        mainCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
        val format = createVideoFormat()
        mainCodec.configure(format, createDisplaySurface(), null, 0)
        mainCodec.start()
        
        // Опциональный второй декодер для доступа к кадрам
        if (needFrameAccess) {
            processingCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
            processingCodec.configure(format, createProcessingSurface(), null, 0)
            processingCodec.start()
        } else {
            processingCodec = null
            processingSurface = null
        }
    }
    
    private fun createDisplaySurface(): Surface {
        return Surface(textureView.surfaceTexture)
    }
    
    private fun createProcessingSurface(): Surface {
        val surfaceTexture = SurfaceTexture(textureId).apply {
            setDefaultBufferSize(videoWidth, videoHeight)
        }
        return Surface(surfaceTexture)
    }
    
    fun requestFrameAccess(): Boolean {
        return synchronized(this) {
            if (processingCodec != null) {
                processingCodec.requestKeyFrame()
                true
            } else {
                false
            }
        }
    }
}

Прямая отрисовка YUV с помощью OpenGL

Вместо преобразования YUV в RGB во фрагментном шейдере, рассмотрите возможность прямой отрисовки в формате YUV, когда это возможно. Это полностью устраняет этап преобразования:

glsl
// Вертексный шейдер
attribute vec4 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;

void main() {
    gl_Position = aPosition;
    vTexCoord = aTexCoord;
}

// Фрагментный шейдер для прямой отрисовки YUV
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D yTexture;
uniform sampler2D uvTexture;

void main() {
    vec3 yuv;
    yuv.x = texture2D(yTexture, vTexCoord).r;
    yuv.yz = texture2D(uvTexture, vTexCoord).rg - vec2(0.5, 0.5);
    
    // Преобразование YUV в RGB с использованием оптимизированной матрицы
    vec3 rgb = mat3(
        1.0, 1.0, 1.0,
        0.0, -0.39465, 2.03211,
        1.13983, -0.58060, 0.0
    ) * yuv;
    
    gl_FragColor = vec4(rgb, 1.0);
}
kotlin
class DirectYuvRenderer {
    private val programId: Int
    private val yTextureId: Int
    private val uvTextureId: Int
    
    init {
        programId = createShaderProgram(yuvVertexShader, yuvFragmentShader)
        yTextureId = createTexture()
        uvTextureId = createTexture()
    }
    
    fun renderFrame(yTexture: Int, uvTexture: Int, width: Int, height: Int) {
        glUseProgram(programId)
        
        // Привязка Y текстуры
        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, yTexture)
        glUniform1i(glGetUniformLocation(programId, "yTexture"), 0)
        
        // Привязка UV текстуры
        glActiveTexture(GL_TEXTURE1)
        glBindTexture(GL_TEXTURE_2D, uvTexture)
        glUniform1i(glGetUniformLocation(programId, "uvTexture"), 1)
        
        // Отрисовка квадрата
        drawQuad()
    }
}

Управление буферами с аппаратным ускорением

Реализуйте эффективное управление буферами для минимизации задержки:

kotlin
class OptimizedBufferManager {
    private val availableBuffers = ConcurrentLinkedQueue<Int>()
    private val inUseBuffers = ConcurrentHashMap<Int, Long>()
    private val bufferRecycleTime = 1000L // 1 секунда
    
    fun acquireBuffer(): Int? {
        return availableBuffers.poll() ?: allocateNewBuffer()
    }
    
    fun releaseBuffer(bufferId: Int) {
        synchronized(inUseBuffers) {
            inUseBuffers.remove(bufferId)?.let { timestamp ->
                if (System.currentTimeMillis() - timestamp < bufferRecycleTime) {
                    availableBuffers.offer(bufferId)
                } else {
                    // Буфер слишком старый, освобождаем
                    deleteBuffer(bufferId)
                }
            }
        }
    }
    
    fun markBufferInUse(bufferId: Int) {
        inUseBuffers[bufferId] = System.currentTimeMillis()
    }
    
    private fun allocateNewBuffer(): Int? {
        return try {
            val buffer = IntArray(1)
            glGenBuffers(1, buffer, 0)
            buffer[0]
        } catch (e: Exception) {
            null
        }
    }
}

Техники оптимизации производительности

1. Пропуск кадров при обработке

kotlin
class SmartFrameProcessor {
    private var lastProcessedFrame = 0L
    private val processInterval = 3 // Обрабатывать каждый 3-й кадр
    
    fun shouldProcessFrame(frameNumber: Long): Boolean {
        return frameNumber % processInterval == 0L
    }
}

2. Операции сохранения с ускорением GPU

kotlin
class GpuFrameSaver {
    private val fboId: Int
    private var textureId: Int
    
    init {
        fboId = createFrameBuffer()
        textureId = createTexture()
    }
    
    fun saveFrameToDisk(texture: Int, width: Int, height: Int, path: String) {
        glBindFramebuffer(GL_FRAMEBUFFER, fboId)
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0)
        
        val pixels = ByteBuffer.allocateDirect(width * height * 4)
        glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels)
        
        // Сохранение на диск в фоновом потоке
        Thread {
            saveBitmap(pixels, width, height, path)
        }.start()
    }
}

3. Пул памяти для буферов кадров

kotlin
class FrameBufferPool {
    private val pool = ConcurrentHashMap<Int, FrameBuffer>()
    private val maxSize = 10
    
    fun acquireBuffer(width: Int, height: Int): FrameBuffer? {
        synchronized(pool) {
            return pool.values.find { it.width == width && it.height == height && !it.inUse }
                ?.also { it.inUse = true }
                ?: createNewBuffer(width, height)
        }
    }
    
    fun releaseBuffer(buffer: FrameBuffer) {
        synchronized(pool) {
            buffer.inUse = false
            if (pool.size < maxSize) {
                pool[buffer.id] = buffer
            }
        }
    }
}

Особенности конкретных устройств

Разные устройства Android поддерживают разные форматы YUV и возможности аппаратного ускорения:

kotlin
class DeviceCapabilitiesChecker {
    fun getOptimalOutputFormat(codec: MediaCodec): String {
        val capabilities = codec.codecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC)
        
        // Предпочтение форматам с аппаратным ускорением
        val supportedFormats = capabilities.colorFormats
        return when {
            supportedFormats.contains(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar) -> 
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
            supportedFormats.contains(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar) -> 
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
            else -> MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
        }
    }
}

Согласно результатам исследований, некоторые устройства используют проприетарные форматы, такие как QOMX_COLOR_FormatYUV420PackedSemiPlanar64x32Tile2m8ka, которые специфичны для конкретного оборудования и должны обрабатываться с осторожностью.

Рекомендации по реализации

На основе исследований и лучших практик, вот ключевые рекомендации:

  1. Используйте SurfaceTexture с оптимизированными шейдерами: Ваш текущий подход жизнеспособен, но требует оптимизации. Сфокусируйтесь на минимизации сложности шейдеров и использовании эффективных матриц преобразования YUV-to-RGB.

  2. Реализуйте обработку на основе кадров: Не обрабатывайте каждый кадр. Используйте интеллектуальный пропуск кадров для снижения нагрузки при сохранении достаточного доступа к кадрам.

  3. Рассмотрите гибридную архитектуру: Для производственного использования реализуйте подход с двойным декодером, который может переключаться между режимами прямой отрисовки и обработки кадров.

  4. Оптимизируйте управление буферами: Используйте пулы памяти и эффективную переработку буферов для минимизации накладных расходов на выделение.

  5. Профилируйте и оптимизируйте: Используйте инструменты профилирования производительности Android для определения узких мест в вашем конкретном случае использования.

  6. Учитывайте различия устройств: Реализуйте механизмы отката для устройств с ограниченной поддержкой аппаратного ускорения.

Обсуждение на форуме подчеркивает важный принцип: “Ключевое заключается не в необходимости извлекать данные пикселей YUV из декодера для передачи в YUV EGL рендерер. Поверхностная отрисовка пропускает этот этап цикла и отрисовывает напрямую”. Это указывает на то, что когда это возможно, используйте пути прямой отрисовки и обращайтесь к данным кадров только при абсолютной необходимости.

Источники

  1. Android MediaCodec output format: GLES External Texture (YUV / NV12) to GLES Texture (RGB)
  2. Hardware accelerated H.264/HEVC video decode on Android to OpenGL FBO or texture
  3. Access violation in native code with hardware accelerated Android MediaCodec decoder
  4. Hardware acceleration | Views | Android Developers
  5. How to use hardware accelerated video decoding on Android?
  6. MediaCodec under Android - ODROID
  7. Understanding Android camera SurfaceTexture and MediaCodec Surface usage
  8. TextureView with MediaCodec decoder for H264 streams
  9. Android* Hardware Codec – MediaCodec

Заключение

Эффективная аппаратно-ускоренная отрисовка YUV с доступом к кадрам на Android требует сложного подхода, который балансирует производительность с функциональными потребностями. Ключевые выводы включают:

  1. Гибридные архитектуры наиболее эффективны для производственных приложений, позволяя переключаться между режимами прямой отрисовки и обработки кадров в зависимости от текущих требований.

  2. Оптимизированная OpenGL-отрисовка с эффективными матрицами преобразования YUV-to-RGB и минимальной сложностью шейдеров может значительно снизить узкое место производительности, с которым вы столкнулись.

  3. Интеллектуальные стратегии обработки кадров, включая пропуск кадров и асинхронную обработку, необходимы для поддержания производительности 30fps при обеспечении доступа к кадрам.

  4. Особенности конкретных устройств должны учитываться, так как разные устройства поддерживают разные форматы YUV и возможности аппаратного ускорения.

  5. Управление памятью критически важно - правильное пулирование и переработка буферов могут значительно снизить накладные расходы на выделение и улучшить производительность.

Для вашего приложения потоковой передачи RTSP я рекомендую реализовать гибридный подход с оптимизированной отрисовкой SurfaceTexture в качестве основного пути и переходом на прямую отрисовку Surface, когда обработка кадров не требуется. Это даст вам лучшее из двух миров - аппаратно-ускоренную отрисовку, когда это возможно, и доступ к кадрам, когда это необходимо, с минимальным влиянием на производительность.