Реализация AI-виртуального фона (Background Replacement) для видеозвонков
Стандартная реализация виртуального фона через WebRTC и сторонний сервис работает пока сеть стабильна. На мобильном устройстве при 4G-деградации roundtrip для отправки кадра на сервер и получения маскированного результата вырастает до 150–300 мс, что даёт артефакты в реальном времени. Правильный подход — сегментация на устройстве.
Почему серверная сегментация не работает на мобильном
Задача — выделить силуэт человека на каждом кадре видеопотока (30 fps), применить фон и вернуть результат в пайплайн до энкодирования. Это означает бюджет ~33 мс на кадр включая захват, инференс модели, постобработку и рендеринг.
Серверный вариант: захват → отправка → инференс → ответ → рендеринг. Даже при идеальной сети roundtrip добавляет 40–80 мс. На практике — рывки контура, «призрак» при движении.
На устройстве: захват → инференс → рендеринг. Всё в одном пайплайне.
iOS: MLKit + CoreImage или Vision
На iOS используем Vision фреймворк с моделью VNGeneratePersonSegmentationRequest. Apple добавила её в iOS 15 — работает на Neural Engine без явной загрузки модели. Точность хорошая для фронтальной камеры, но начинает давать рваный контур при сложных причёсках и прозрачных элементах одежды.
// Настройка сегментации
let request = VNGeneratePersonSegmentationRequest()
request.qualityLevel = .balanced // .accurate даёт лучший контур, но тяжелее
request.outputPixelFormat = kCVPixelFormatType_OneComponent8
// В обработчике кадров AVFoundation
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
try handler.perform([request])
guard let mask = request.results?.first?.pixelBuffer else { return }
// mask — CVPixelBuffer 8-bit, применяем через CIBlendWithMask
CIBlendWithMask с CIContext(options: [.workingColorSpace: NSNull()]) — рендерим в Metal, избегая конвертации цветового пространства. Без этого каждый кадр добавляет ~5 мс только на конвертацию.
Для более качественной сегментации — конвертируем TFLite модель DeepLab v3 или MediaPipe SelfieSegmentation в Core ML через coremltools и загружаем через MLModel. MediaPipe даёт стабильный контур даже при размытых краях.
Android: MLKit Selfie Segmentation
val segmenter = Segmentation.getClient(
SelfieSegmenterOptions.Builder()
.setDetectorMode(SelfieSegmenterOptions.STREAM_MODE) // оптимизировано для видео
.enableRawSizeMask()
.build()
)
// В обработчике CameraX ImageAnalysis
override fun analyze(imageProxy: ImageProxy) {
val inputImage = InputImage.fromMediaImage(imageProxy.image!!, imageProxy.imageInfo.rotationDegrees)
segmenter.process(inputImage)
.addOnSuccessListener { segmentationMask ->
val mask = segmentationMask.buffer
// Применяем фон через RenderScript или Vulkan compute shader
applyBackground(mask, imageProxy)
}
.addOnCompleteListener { imageProxy.close() }
}
STREAM_MODE критичен — он держит внутренний стейт между кадрами и работает быстрее SINGLE_IMAGE_MODE. На Pixel 6 с Tensor G2 инференс занимает 8–12 мс. На бюджетных устройствах (Snapdragon 695) — 20–28 мс. Для постобработки маски — RenderScript (deprecated в API 31+) или Vulkan compute shader через RenderEffect на Android 12+.
Применение фона: три варианта
Статичное изображение — простейший случай. CIBlendWithMask на iOS, PorterDuff compositing на Android.
Размытие (blur) — фильтр CIGaussianBlur с radius 12–20 применяется к исходному кадру, затем маска выбирает между оригиналом и размытым. На Android — RenderEffect.createBlurEffect (API 31+) или собственный blur через Vulkan.
Видеофон — нужен декодер, синхронизированный с таймингом видеозвонка. На iOS — AVPlayerItemVideoOutput + Metal текстура. Тяжело по памяти: буфер видеофона + буфер камеры + буфер маски + результат. На iPhone 12 с 4 GB это ок, на iPhone SE 2nd gen (3 GB) нужен aggressive buffer reuse.
Интеграция в WebRTC-пайплайн
Большинство мобильных решений для звонков строятся на WebRTC — через LiveKit, Daily.co, Agora или нативный WebRTC. Все они предоставляют механизм кастомного VideoSource/VideoProcessor для подмены кадров до энкодирования.
В LiveKit SDK для iOS это VideoProcessor протокол:
class BackgroundReplacementProcessor: VideoProcessor {
func process(frame: RTCVideoFrame) -> RTCVideoFrame? {
// Сегментация + применение фона
// Возвращаем новый RTCVideoFrame с обработанным буфером
}
}
room.localParticipant?.videoTracks.first?.processor = BackgroundReplacementProcessor()
Важно: RTCVideoFrame работает в CVPixelBuffer с форматом kCVPixelFormatType_420YpCbCr8BiPlanarFullRange. Конвертация в RGB для ML-инференса и обратно — это потери. Если модель принимает YUV — оставляем формат нетронутым.
Оценка и процесс
Начинаем с аудита существующего WebRTC-стека: какой SDK используется, как организован пайплайн кадров, какие устройства в ЦА. Затем прототип с Vision/MLKit, замеры на реальных девайсах из списка минимальных требований.
Критичные этапы: подбор модели сегментации под качество/скорость, оптимизация постобработки маски (antialiasing контура, feathering краёв), тестирование на граничных условиях — неравномерное освещение, сложный фон, быстрые движения.
Ориентиры по срокам
Базовая реализация с размытием фона (одна платформа) — 2–3 недели. Полная реализация с поддержкой статичных изображений и видеофонов, двух платформ, интеграция в существующий WebRTC-стек — 5–8 недель.







