Реалізація наложення оверлеїв (текст, логотип) на стрім з мобільного пристрою
Наложити логотип на стрім виглядає просто — поки не сталкнешся з тим, що 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 тижні. Вартість розраховується індивідуально.







