Реализация наложения фильтров на видео в мобильном приложении
Фильтры на фото и фильтры на видео — разные задачи по сложности. Фото — один кадр, можно обработать за 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 бэкендом обязателен — 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 дня.







