22、MediaMuxer实战:视频加滤镜(Filter)——为视频添加色彩滤镜效果。

说实话,到了这一章,咱们才算真正开始玩点「酷」的东西。

前面几章我们一直在捣鼓 MediaMuxer 的封装、音视频的合成与分离。那些都是基础功,就像练武先扎马步。但今天不一样——我们要给视频「化妆」了。

视频加滤镜,说白了就是逐帧处理。你把每一帧画面拿出来,用 OpenGL 或者别的渲染引擎给它改改颜色,再重新编码回去。嗯,这里要注意:滤镜不是在 MediaMuxer 里加的,MediaMuxer 只管「封装」。真正的滤镜逻辑在 MediaCodec 和 OpenGL 之间。

22.1 滤镜的本质是什么?

我刚开始做滤镜时,以为是什么高深莫测的算法。后来发现,说白了就是「像素级的颜色变换」。

一张图片由无数像素组成,每个像素有 R、G、B 三个通道。滤镜就是给这三个通道做数学运算。比如冷色调滤镜,就是把红色通道压低,蓝色通道拉高。

核心公式:

输出颜色 = 输入颜色 × 滤镜矩阵

这个矩阵,就是滤镜的灵魂。

在 Android 上做视频滤镜,主流方案是 OpenGL ES + 片段着色器(Fragment Shader)。片段着色器里写的就是这个矩阵运算。MediaCodec 解码出原始帧,丢给 OpenGL 渲染,渲染完再喂给 MediaCodec 编码器。

我个人习惯把整个流程画成一条流水线:

视频滤镜处理流水线 MediaCodec 解码 OpenGL 渲染 片段着色器 MediaCodec 编码 原始帧 纹理输入 滤镜矩阵运算 输出帧 MediaMuxer 封装

22.2 实战:写一个灰度滤镜

咱们先别整太复杂的。从最简单的灰度滤镜开始。灰度滤镜的数学原理是:

// 灰度公式(人眼感知加权)
gray = 0.299 * R + 0.587 * G + 0.114 * B

为什么是这三个系数?因为人眼对绿色最敏感,对蓝色最不敏感。这是 ITU-R BT.601 标准里定义的。我在项目中试过直接用平均值(R+G+B)/3,效果发灰,不好看。

对应的片段着色器代码:

// 灰度滤镜片段着色器
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D sTexture;

void main() {
    vec4 color = texture(sTexture, vTexCoord);
    float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
    fragColor = vec4(gray, gray, gray, color.a);
}

这段代码看着简单,但它是所有滤镜的基础。你改改系数,就能变成暖色调、冷色调、怀旧风。

22.3 暖色调滤镜实现

暖色调其实就是让画面偏黄偏红。我习惯的做法是:

  • 红色通道乘以 1.1
  • 绿色通道保持不变
  • 蓝色通道乘以 0.8
// 暖色调滤镜
void main() {
    vec4 color = texture(sTexture, vTexCoord);
    color.r *= 1.1;
    color.g *= 1.0;
    color.b *= 0.8;
    fragColor = color;
}

小技巧: 实际项目中,我会把系数做成可调节的。用户拖一个滑块,系数从 0.5 到 1.5 变化。这样同一个滤镜就能调出不同强度。

22.4 冷色调滤镜实现

冷色调跟暖色调反过来:

  • 红色通道乘以 0.8
  • 绿色通道乘以 0.9
  • 蓝色通道乘以 1.2
// 冷色调滤镜
void main() {
    vec4 color = texture(sTexture, vTexCoord);
    color.r *= 0.8;
    color.g *= 0.9;
    color.b *= 1.2;
    fragColor = color;
}

你想想看,是不是很简单?但效果差别很大。暖色调适合夕阳、室内场景;冷色调适合雪景、科技感画面。

22.5 怀旧复古滤镜

这个稍微复杂一点。怀旧滤镜要模拟老照片的感觉:整体偏黄、对比度降低、饱和度降低。

// 怀旧复古滤镜
void main() {
    vec4 color = texture(sTexture, vTexCoord);
    
    // 1. 降低饱和度
    float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
    vec3 desaturated = vec3(gray);
    vec3 blended = mix(desaturated, color.rgb, 0.6);
    
    // 2. 叠加黄色调
    blended.r *= 1.1;
    blended.g *= 1.0;
    blended.b *= 0.7;
    
    // 3. 降低对比度(拉近中间值)
    blended = (blended - 0.5) * 0.8 + 0.5;
    
    fragColor = vec4(blended, color.a);
}

注意: 怀旧滤镜的系数很敏感。我曾经调了一下午,发现 0.6 的饱和度混合比 0.5 更自然。0.8 的对比度压缩比 0.7 更有「胶片感」。这些数字没有标准答案,全靠肉眼调。

22.6 滤镜与 MediaMuxer 的衔接

滤镜处理完的帧,怎么交给 MediaMuxer?流程是这样的:

  1. MediaCodec 解码器输出 Surface
  2. OpenGL 从这个 Surface 读取纹理
  3. 片段着色器处理纹理
  4. 渲染到编码器的输入 Surface
  5. 编码器输出编码后的数据
  6. MediaMuxer 写入 MP4 文件

关键代码片段:

// 创建编码器输入 Surface
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
MediaFormat format = MediaFormat.createVideoFormat(...);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface encoderInputSurface = encoder.createInputSurface();

// OpenGL 渲染到 encoderInputSurface
// 伪代码:
while (有帧需要处理) {
    // 1. 从解码器获取帧纹理
    // 2. 绑定片段着色器(灰度/暖色/冷色/怀旧)
    // 3. 渲染到 encoderInputSurface
    // 4. 通知编码器
    encoder.signalEndOfInputStream();
}

// MediaMuxer 封装
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 添加编码后的轨道
// 写入数据

核心要点: MediaMuxer 不关心你中间做了什么滤镜处理。它只负责把编码器吐出来的数据打包成 MP4。滤镜是 OpenGL 的事,封装是 MediaMuxer 的事。两者通过 Surface 桥接。

22.7 性能优化建议

我在项目里踩过不少坑,总结几条:

  • 不要每帧都创建 OpenGL 资源。 纹理、着色器、程序对象,在初始化时创建一次,循环复用。
  • 使用 FBO(帧缓冲对象) 做离屏渲染。直接渲染到编码器 Surface 上,省掉一次拷贝。
  • 滤镜参数用 uniform 传递,不要硬编码在着色器里。这样切换滤镜时只需改 uniform,不用重新编译着色器。
  • 注意帧率同步。 解码器和编码器的帧率要匹配。我曾经遇到过解码比编码快,导致内存暴涨。

我的经验: 刚开始做滤镜时,我每帧都 new 了一个着色器对象。结果 30fps 的视频处理完只剩 5fps。后来改成预编译 + uniform 传参,直接飙到 28fps。性能优化,说白了就是「少创建、多复用」。

22.8 本章小结

视频加滤镜,本质上就是「解码 → 像素变换 → 编码 → 封装」四个步骤。MediaMuxer 只负责最后一步。滤镜的核心在片段着色器里,几行代码就能实现灰度、暖色、冷色、怀旧等效果。

你可能会问:为什么不用 CPU 逐像素处理?因为 GPU 并行计算比 CPU 快几十倍。1080p 的视频,一帧有 200 多万个像素。CPU 一个个算,GPU 几千个核心一起算,速度完全不是一个量级。

嗯,这一章的内容就到这里。滤镜的世界很大,今天我们只开了个头。后面还有更复杂的 LUT 滤镜、美颜滤镜、特效滤镜等着我们。


公众号:蓝海资料掘金营,微信deep3321