Эффективное аппаратно-ускоренное рендеринг YUV на Android с доступом к кадрам
Постановка проблемы
Мы разрабатываем приложение для Android, которое обрабатывает живой RTSP-поток с видеокадрами H.264 с частотой 30 кадров в секунду. Наша цель:
- Рендерить декодированные YUV-кадры на экране с аппаратным ускорением
- Предоставлять доступ к декодированным кадрам, чтобы пользователи SDK могли сохранять их на диск при необходимости
Текущие проблемы реализации
При использовании MediaCodec для декодирования мы столкнулись с фундаментальным ограничением:
- Если мы предоставляем Surface в метод configure MediaCodec, мы можем рендерить на экран, но теряем доступ к outputImage и outputBuffer в формате YUV, необходимым для сохранения кадров
- Если мы передаем null в качестве Surface, мы получаем доступ к данным YUV, но должны преобразовать их в RGB для рендеринга, что создает узкие места в производительности
Наш текущий подход
Мы реализовали решение с использованием SurfaceTexture и OpenGL для преобразования YUV в RGB:
Настройка MediaDecoder
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 рендерер
internal class YuvGLRenderer(private val surfaceTexture: SurfaceTexture) {
// ... полная реализация OpenGL с шейдерами преобразования YUV в RGB
}
Интеграция с View
class RTSPPlayerView : TextureView, TextureView.SurfaceTextureListener {
// ... интеграция с SurfaceTexture и рендерером
}
Проблемы с производительностью
Несмотря на нашу реализацию, мы сталкиваемся со значительными проблемами задержки при текущем подходе. Преобразование YUV в RGB в OpenGL кажется излишне сложным для нашего случая использования, и мы ищем более эффективное решение.
Вопрос
Не могли бы вы дать совет о том, как эффективно рендерить YUV-изображения на экране, сохраняя при этом доступ к необработанным данным кадров для сохранения? В частности, мы ищем:
- Подход с аппаратным ускорением, который избегает узкого места преобразования YUV в RGB
- Решение, хорошо работающее с живыми потоками 30 кадров в секунду
- Минимальную задержку между декодированием и рендерингом
- Чистую интеграцию с MediaCodec и конвейером рендеринга Android
Будем благодарны за любые альтернативные архитектуры, оптимизации или лучшие практики.
Аппаратно-ускоренная отрисовка YUV в Android с доступом к кадрам
Аппаратно-ускоренная отрисовка YUV в Android с доступом к кадрам представляет собой фундаментальный компромисс в медиапайплайне Android. Проблема, с которой вы столкнулись, хорошо задокументирована - при использовании MediaCodec необходимо выбирать между аппаратным ускорением (через Surface) и доступом к кадрам (через SurfaceTexture/null), но не одновременно. Однако существует несколько продвинутых подходов, которые позволяют достичь ваших целей при правильной оптимизации.
Содержание
- Понимание компромисса MediaCodec Surface
- Продвинутые стратегии оптимизации SurfaceTexture
- Гибридная архитектура отрисовки
- Прямая отрисовка YUV с помощью OpenGL
- Управление буферами с аппаратным ускорением
- Техники оптимизации производительности
- Особенности конкретных устройств
- Рекомендации по реализации
Понимание компромисса MediaCodec Surface
Фундаментальное ограничение, которое вы выявили, связано с дизайном архитектуры медиа в Android. При настройке MediaCodec с Surface декодер использует аппаратное ускорение, но передает вывод непосредственно в дисплейный пайплайн, делая данные кадра недоступными. При передаче null вы получаете доступ к выходным буферам, но теряете преимущества аппаратной отрисовки.
Согласно документации Android, аппаратное ускорение означает “все операции отрисовки, выполняемые на холсте View, используют GPU”. Это создает дилемму, с которой вы столкнулись - GPU может эффективно отрисовывать, но не предоставляет доступ к необработанным данным.
Ключевое понимание заключается в том, что аппаратное декодирование и аппаратная отрисовка - это отдельные оптимизации, которые не обязательно работают вместе, когда вам нужен доступ к кадрам.
Продвинутые стратегии оптимизации SurfaceTexture
Ваш текущий подход с использованием SurfaceTexture и OpenGL концептуально верен, но может потребовать оптимизации. Вот несколько стратегий для улучшения производительности:
1. Подход с двойным буфером и TextureView
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. Асинхронный пайплайн обработки кадров
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:
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, когда это возможно. Это полностью устраняет этап преобразования:
// Вертексный шейдер
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);
}
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()
}
}
Управление буферами с аппаратным ускорением
Реализуйте эффективное управление буферами для минимизации задержки:
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. Пропуск кадров при обработке
class SmartFrameProcessor {
private var lastProcessedFrame = 0L
private val processInterval = 3 // Обрабатывать каждый 3-й кадр
fun shouldProcessFrame(frameNumber: Long): Boolean {
return frameNumber % processInterval == 0L
}
}
2. Операции сохранения с ускорением GPU
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. Пул памяти для буферов кадров
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 и возможности аппаратного ускорения:
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, которые специфичны для конкретного оборудования и должны обрабатываться с осторожностью.
Рекомендации по реализации
На основе исследований и лучших практик, вот ключевые рекомендации:
-
Используйте SurfaceTexture с оптимизированными шейдерами: Ваш текущий подход жизнеспособен, но требует оптимизации. Сфокусируйтесь на минимизации сложности шейдеров и использовании эффективных матриц преобразования YUV-to-RGB.
-
Реализуйте обработку на основе кадров: Не обрабатывайте каждый кадр. Используйте интеллектуальный пропуск кадров для снижения нагрузки при сохранении достаточного доступа к кадрам.
-
Рассмотрите гибридную архитектуру: Для производственного использования реализуйте подход с двойным декодером, который может переключаться между режимами прямой отрисовки и обработки кадров.
-
Оптимизируйте управление буферами: Используйте пулы памяти и эффективную переработку буферов для минимизации накладных расходов на выделение.
-
Профилируйте и оптимизируйте: Используйте инструменты профилирования производительности Android для определения узких мест в вашем конкретном случае использования.
-
Учитывайте различия устройств: Реализуйте механизмы отката для устройств с ограниченной поддержкой аппаратного ускорения.
Обсуждение на форуме подчеркивает важный принцип: “Ключевое заключается не в необходимости извлекать данные пикселей YUV из декодера для передачи в YUV EGL рендерер. Поверхностная отрисовка пропускает этот этап цикла и отрисовывает напрямую”. Это указывает на то, что когда это возможно, используйте пути прямой отрисовки и обращайтесь к данным кадров только при абсолютной необходимости.
Источники
- Android MediaCodec output format: GLES External Texture (YUV / NV12) to GLES Texture (RGB)
- Hardware accelerated H.264/HEVC video decode on Android to OpenGL FBO or texture
- Access violation in native code with hardware accelerated Android MediaCodec decoder
- Hardware acceleration | Views | Android Developers
- How to use hardware accelerated video decoding on Android?
- MediaCodec under Android - ODROID
- Understanding Android camera SurfaceTexture and MediaCodec Surface usage
- TextureView with MediaCodec decoder for H264 streams
- Android* Hardware Codec – MediaCodec
Заключение
Эффективная аппаратно-ускоренная отрисовка YUV с доступом к кадрам на Android требует сложного подхода, который балансирует производительность с функциональными потребностями. Ключевые выводы включают:
-
Гибридные архитектуры наиболее эффективны для производственных приложений, позволяя переключаться между режимами прямой отрисовки и обработки кадров в зависимости от текущих требований.
-
Оптимизированная OpenGL-отрисовка с эффективными матрицами преобразования YUV-to-RGB и минимальной сложностью шейдеров может значительно снизить узкое место производительности, с которым вы столкнулись.
-
Интеллектуальные стратегии обработки кадров, включая пропуск кадров и асинхронную обработку, необходимы для поддержания производительности 30fps при обеспечении доступа к кадрам.
-
Особенности конкретных устройств должны учитываться, так как разные устройства поддерживают разные форматы YUV и возможности аппаратного ускорения.
-
Управление памятью критически важно - правильное пулирование и переработка буферов могут значительно снизить накладные расходы на выделение и улучшить производительность.
Для вашего приложения потоковой передачи RTSP я рекомендую реализовать гибридный подход с оптимизированной отрисовкой SurfaceTexture в качестве основного пути и переходом на прямую отрисовку Surface, когда обработка кадров не требуется. Это даст вам лучшее из двух миров - аппаратно-ускоренную отрисовку, когда это возможно, и доступ к кадрам, когда это необходимо, с минимальным влиянием на производительность.