22、MediaMuxer实战:视频加滤镜(Filter)——为视频添加色彩滤镜效果。
说实话,到了这一章,咱们才算真正开始玩点「酷」的东西。
前面几章我们一直在捣鼓 MediaMuxer 的封装、音视频的合成与分离。那些都是基础功,就像练武先扎马步。但今天不一样——我们要给视频「化妆」了。
视频加滤镜,说白了就是逐帧处理。你把每一帧画面拿出来,用 OpenGL 或者别的渲染引擎给它改改颜色,再重新编码回去。嗯,这里要注意:滤镜不是在 MediaMuxer 里加的,MediaMuxer 只管「封装」。真正的滤镜逻辑在 MediaCodec 和 OpenGL 之间。
22.1 滤镜的本质是什么?
我刚开始做滤镜时,以为是什么高深莫测的算法。后来发现,说白了就是「像素级的颜色变换」。
一张图片由无数像素组成,每个像素有 R、G、B 三个通道。滤镜就是给这三个通道做数学运算。比如冷色调滤镜,就是把红色通道压低,蓝色通道拉高。
核心公式:
输出颜色 = 输入颜色 × 滤镜矩阵
这个矩阵,就是滤镜的灵魂。
在 Android 上做视频滤镜,主流方案是 OpenGL ES + 片段着色器(Fragment Shader)。片段着色器里写的就是这个矩阵运算。MediaCodec 解码出原始帧,丢给 OpenGL 渲染,渲染完再喂给 MediaCodec 编码器。
我个人习惯把整个流程画成一条流水线:
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?流程是这样的:
- MediaCodec 解码器输出 Surface
- OpenGL 从这个 Surface 读取纹理
- 片段着色器处理纹理
- 渲染到编码器的输入 Surface
- 编码器输出编码后的数据
- 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