Реалізація накладення фільтрів на відео в мобільному застосунку
Фільтри на фото та фільтри на відео — різні завдання за складністю. Фото — один кадр, можна обробити за 50 мс. Відео 30 fps — 30 кадрів на секунду, кожний потрібно обробити швидше ніж за 33 мс. Інакше відтворення заїкується.
Два режими: попередній перегляд та експорт
Попередній перегляд у реальному часі (під час відтворення). Вимагає обробки на GPU без записування результату у файл.
Експорт (фіналь запис із застосованим фільтром). Може займати секунди, але результат повинен бути покадрово точним.
iOS: AVVideoComposition + Core Image
AVVideoCompositionCoreAnimationTool для статичних оверлеїв — підходить для тексту та логотипів. Для піксельних фільтрів — AVVideoComposition з користувацьким AVVideoCompositing:
class FilterCompositor: NSObject, AVVideoCompositing {
func startRequest(_ asyncVideoCompositionRequest: AVAsynchronousVideoCompositionRequest) {
guard let frame = asyncVideoCompositionRequest.sourceFrame(byTrackID: trackID) else { return }
let ciImage = CIImage(cvPixelBuffer: frame)
let filtered = applyFilter(ciImage)
let output = asyncVideoCompositionRequest.renderContext.newPixelBuffer()!
ciContext.render(filtered, to: output)
asyncVideoCompositionRequest.finish(withComposedVideoFrame: output)
}
}
CIContext із Metal backend обов'язковий — CIContext(mtlDevice: MTLCreateSystemDefaultDevice()!). Без явного вказання деякі пристрої використовують CPU-рендеринг та відео зависає.
Для LUT-фільтрів — CIColorCubeWithColorSpace із таблицею 64×64×64. Один CIContext на весь застосунок — його створення дороге.
Android: GPUImage та media3 Effects
Попередній перегляд у реальному часі — GPUImageView із користувацьким GPUImageFilter. Шейдер написаний на GLSL:
precision mediump float;
uniform sampler2D inputImageTexture;
varying vec2 textureCoordinate;
void main() {
vec4 color = texture2D(inputImageTexture, textureCoordinate);
float luma = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gl_FragColor = vec4(mix(vec3(luma), color.rgb, 1.3), color.a);
}
Для експорту з фільтром — media3 Transformer із GlEffect. MatrixTextureProcessor приймає шейдер та застосовує його до кожного кадру при перекодуванні.
Flutter. Повнофункціональні відеофільтри у реальному часі вимагають коду платформи. Рідні плагіни з MethodChannel — єдиний робочий підхід для production-застосунків. Пакети типу video_editor (pub.dev) надають UI, але фільтрацію роблять через FFmpeg на CPU — повільно на старих пристроях.
Типові помилки при експорті
Створення нового CIContext на кожен кадр — смерть продуктивності. На iPhone 13 це дає 3–4 fps замість 30. Контекст створюється один раз і перевикористовується.
CVPixelBuffer із renderContext.newPixelBuffer() повинен бути повернений одразу після finish(withComposedVideoFrame:). Утримування посилання веде до витоку пула буферів та краху з kCVReturnInvalidArgument.
На Android: GPUImageFilter не thread-safe — не можна застосовувати до одного примірника з кількох потоків одночасно.
Застосування фільтра до запису з камери у реальному часі
Ще більш вимогливий сценарій — фільтр при зніманні. На iOS: AVCaptureSession + AVCaptureVideoDataOutput → SampleBufferDelegate → Metal-шейдер → рендеринг у MTKView. Затримка ланцюжка повинна бути < 16 мс для 60fps. kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange — формат YUV із камери швидше конвертується в текстуру Metal, ніж BGRA.
На Android: CameraX Analysis + ImageAnalysis.Analyzer → GLES-шейдер → GLSurfaceView. ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST — відбрасуємо старі кадри, якщо GPU не встиг обробити попередній.
Орієнтири за часом
5–7 робочих днів: розроблення шейдерів для 5–8 фільтрів, інтеграція попереднього перегляду та експорту на iOS та Android. Flutter-версія з рідним кодом — плюс 2 дні.







