Реализация наложения оверлеев (текст, логотип) на стрим с мобильного устройства
Наложить логотип на стрим выглядит просто — пока не сталкиваешься с тем, что overlay нужно рендерить не на экран, а напрямую в видеопоток до энкодера. UIKit/SwiftUI-вьюшки здесь не помогут: они рисуют в display pipeline, который не имеет отношения к CVPixelBuffer, уходящему в VideoToolbox.
Где правильно встроить оверлей
Пайплайн выглядит так:
AVCaptureVideoDataOutput → CMSampleBuffer → CVPixelBuffer → [оверлей] → H.264-энкодер → RTMP/SRT
Оверлей должен применяться к CVPixelBuffer до передачи в энкодер. Два способа:
Metal (рекомендуемый). Создаём MTLTexture из CVPixelBuffer через CVMetalTextureCacheCreateTextureFromImage, рендерим оверлей поверх через Metal render pass, записываем результат обратно в CVPixelBuffer. Работает на GPU — нагрузка на CPU минимальна.
CoreImage. Используем CISourceOverCompositing фильтр: накладываем CIImage логотипа на CIImage из CMSampleBuffer. Проще в коде, но на iPhone 12 и старше при 1080p30 добавляет 4–6ms к обработке каждого кадра на главном CPU — на грани дропа.
На продакшн-проектах с требованием 1080p30 без дропов — только Metal.
Реализация через Metal
class OverlayRenderer {
private let device: MTLDevice
private let commandQueue: MTLCommandQueue
private var textureCache: CVMetalTextureCache?
private var overlayTexture: MTLTexture? // предзагруженный логотип
func apply(to pixelBuffer: CVPixelBuffer) -> CVPixelBuffer {
var cvTexture: CVMetalTexture?
CVMetalTextureCacheCreateTextureFromImage(
nil, textureCache!, pixelBuffer, nil,
.bgra8Unorm,
CVPixelBufferGetWidth(pixelBuffer),
CVPixelBufferGetHeight(pixelBuffer),
0, &cvTexture
)
guard let texture = CVMetalTextureGetTexture(cvTexture!) else { return pixelBuffer }
let commandBuffer = commandQueue.makeCommandBuffer()!
// render pass: основная текстура + overlayTexture поверх
// ...
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
return pixelBuffer // модифицированный in-place
}
}
Логотип (overlayTexture) загружаем один раз при старте сессии из PNG с альфа-каналом. Не загружайте UIImage на каждый кадр — это 2–3ms аллокации на каждый вызов.
Текстовые оверлеи: отдельная проблема
Статический текст (название канала) — просто Metal-текстура, подготовленная один раз через CoreText. Проблема в динамическом тексте: счётчик зрителей, таймер, donation-сообщения. Их нельзя рендерить через Metal напрямую — Metal не работает с текстом, только с геометрией и текстурами.
Решение: создаём offscreen CALayer с CATextLayer, рисуем его в UIGraphicsImageRenderer, получаем UIImage, конвертируем в MTLTexture. Это делаем в фоновом потоке не чаще, чем раз в 500ms для счётчиков и по событию для donation-сообщений. На экране текст обновляется плавно, в стрим уходит без задержки.
Android: аналогичный подход через OpenGL ES / Vulkan
На Android аналог — SurfaceTexture + OpenGL ES 2.0. Камера рендерит в SurfaceTexture, мы накладываем оверлей через GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA), результат уходит в MediaCodec через Surface. Vulkan мощнее, но поддерживается с Android 7+ и требует значительно больше boilerplate — оправдан только при сложных эффектах.
Позиционирование и адаптация оверлея
Позиция логотипа хранится как относительные координаты (0.0–1.0 от размера кадра), а не абсолютные пиксели. Это позволяет корректно работать при смене разрешения или ориентации без перерасчёта логики. При landscape-ротации — пересчитываем overlayRect в Metal render pass автоматически.
Fade-in/fade-out для donation-текста реализуем через изменение альфа-канала MTLTexture между кадрами — плавное появление за 15–20 кадров (0.5–0.7 секунды).
Сроки
Статический логотип + Metal-пайплайн на iOS: 1–1.5 недели. Динамический текст, анимации оверлеев, поддержка iOS + Android: 3–4 недели. Стоимость рассчитывается индивидуально.







