30、MediaMuxer实战:视频加特效(Effect)——实现粒子特效、模糊特效等。
终于到了特效环节。说实话,这是整个音视频编辑里最让人兴奋的部分。你想想看,一段平平无奇的视频,加上粒子特效或者模糊效果,瞬间就有了电影感。我在做短视频SDK的时候,特效模块是用户反馈最多、也是最容易出bug的地方。今天我们就来啃这块硬骨头。
30.1 特效的本质:逐帧处理
视频特效,说白了就是逐帧处理。每一帧画面,我们拿到它的像素数据,然后按照算法去修改这些像素。粒子特效是往画面上叠加粒子,模糊特效是让像素变得模糊。它们都绕不开一个核心操作——从MediaCodec解码器拿到原始帧,处理完再交给MediaMuxer编码输出。
我习惯把特效处理放在解码和编码之间。流程大概是这样的:
MediaExtractor → MediaCodec(解码) → Surface(接收帧) → 特效处理 → MediaCodec(编码) → MediaMuxer
这里有个关键点:解码后的帧数据,我们得从Surface上取下来。Android里最常用的方式是使用OpenGL ES,把Surface上的纹理绑定到FBO(帧缓冲对象),然后在GPU上做特效计算。嗯,这样做效率最高,因为GPU天生适合做图像并行计算。
核心思路:解码器输出到Surface → 用OpenGL读取纹理 → 在片段着色器里做特效 → 渲染到编码器的输入Surface。
30.2 粒子特效实现
粒子特效,就是一堆小点(粒子)在画面上运动。每个粒子有自己的位置、速度、颜色、生命周期。我最早做粒子特效时,是在CPU上逐像素计算的,结果720p视频都跑不动。后来全部迁移到GPU,用顶点着色器和片段着色器来做,性能直接翻了几十倍。
先看一个简单的粒子着色器:
// 顶点着色器
attribute vec4 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uMVPMatrix;
varying vec2 vTexCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTexCoord = aTexCoord;
}
// 片段着色器 - 粒子效果
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform float uTime; // 时间变量,用于控制粒子运动
void main() {
vec4 color = texture2D(uTexture, vTexCoord);
// 粒子位置计算:基于像素坐标和时间
vec2 pos = vTexCoord * 2.0 - 1.0;
float dist = length(pos);
// 粒子大小和透明度随时间变化
float particle = sin(dist * 20.0 - uTime * 3.0) * 0.5 + 0.5;
particle *= 0.3; // 控制粒子强度
// 混合粒子颜色(这里用白色粒子)
vec3 particleColor = vec3(1.0, 1.0, 1.0);
gl_FragColor = vec4(color.rgb + particleColor * particle, color.a);
}
这段代码看起来简单,但效果已经很不错了。粒子会从画面中心向外扩散,形成类似星光的效果。我在项目中遇到过一个问题:粒子数量太多时,GPU负载会飙升。后来我加了一个粒子密度参数,让用户自己调节,既保证了效果又兼顾了性能。
我的经验:粒子特效的性能瓶颈通常在片段着色器的计算量上。尽量用数学公式代替纹理采样,因为纹理采样在GPU上比较慢。另外,粒子数量控制在200-500个之间,效果和性能最平衡。
30.3 模糊特效实现
模糊特效,最常见的是高斯模糊。原理很简单:每个像素取周围像素的加权平均值。但直接做全图高斯模糊,计算量非常大。一张1080p的图片,如果用9x9的核,每个像素要算81次,太慢了。
我常用的优化方法是两次一维高斯模糊。先横向模糊,再纵向模糊。这样9x9的核就变成了9+9=18次计算,效率提升了好几倍。来看代码:
// 横向模糊片段着色器
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D uTexture;
uniform float uBlurSize; // 模糊程度
void main() {
vec4 color = vec4(0.0);
float weightSum = 0.0;
// 横向采样9个点
for (int i = -4; i <= 4; i++) {
float weight = exp(-float(i * i) / (2.0 * uBlurSize * uBlurSize));
vec2 offset = vec2(float(i) / 1920.0, 0.0); // 1920是视频宽度
color += texture2D(uTexture, vTexCoord + offset) * weight;
weightSum += weight;
}
gl_FragColor = color / weightSum;
}
// 纵向模糊片段着色器(类似,只是偏移方向改为y轴)
这里有个细节:uBlurSize参数控制模糊半径。值越大,模糊越强。但要注意,模糊半径太大时,画面边缘会出现黑边。我曾经踩过这个坑,后来在采样时做了边缘处理,用最近的像素值填充,才解决了问题。
注意:模糊特效对性能影响很大。在低端手机上,建议模糊半径不超过10。如果要做大范围模糊,可以考虑降低分辨率再模糊,然后放大回原尺寸。我做过测试,先缩放到1/4分辨率再模糊,效果几乎一样,但性能提升了5倍。
30.4 特效与MediaMuxer的整合
特效做完了,怎么和MediaMuxer配合呢?流程是这样的:
- 用MediaExtractor读取视频文件,分离出视频轨道。
- 创建MediaCodec解码器,配置输出Surface。
- 创建MediaCodec编码器,配置输入Surface。
- 在OpenGL线程中,从解码Surface获取纹理,应用特效着色器,渲染到编码Surface。
- 编码器输出数据,交给MediaMuxer写入文件。
核心代码片段:
// 特效渲染循环
while (!isFinished) {
// 从解码器获取帧
int inputIndex = decoder.dequeueInputBuffer(10000);
if (inputIndex >= 0) {
// 交给解码器处理
decoder.queueInputBuffer(...);
}
int outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex >= 0) {
// 释放解码帧,触发渲染
decoder.releaseOutputBuffer(outputIndex, true);
// 在OpenGL中做特效处理
// 1. 绑定解码Surface的纹理
// 2. 应用特效着色器
// 3. 渲染到编码Surface
// 通知编码器有新帧
encoder.signalEndOfInputStream();
}
// 从编码器获取输出
int encoderOutputIndex = encoder.dequeueOutputBuffer(encoderBufferInfo, 10000);
if (encoderOutputIndex >= 0) {
// 写入MediaMuxer
muxer.writeSampleData(trackIndex, encoderOutputBuffer, encoderBufferInfo);
encoder.releaseOutputBuffer(encoderOutputIndex);
}
}
这里有个容易出错的地方:时间戳同步。解码器输出的帧有时间戳,编码器输入也需要对应的时间戳。我刚开始做的时候,忘了传递时间戳,结果生成的视频播放速度不对。后来我专门写了一个时间戳管理类,确保每一帧的时间戳都正确传递。
关键点:特效处理必须在解码和编码之间同步进行。解码器输出一帧,特效处理一帧,编码器输入一帧。不能异步,否则会出现帧错乱。
30.5 性能优化与避坑
做特效最怕什么?卡顿、掉帧、内存溢出。我总结了几条经验:
- 使用GPU而不是CPU:所有特效计算都放在着色器里,不要用CPU逐像素处理。CPU处理一帧可能要几十毫秒,GPU只要几毫秒。
- 控制特效复杂度:粒子数量、模糊半径、特效层数,这些参数要可调节。在低端手机上自动降低特效质量。
- 复用OpenGL资源:纹理、FBO、着色器程序,创建一次,反复使用。不要每帧都创建新资源。
- 注意内存泄漏:MediaCodec和MediaMuxer的释放顺序很重要。先释放编码器,再释放解码器,最后关闭MediaMuxer。
避坑指南:我曾经在项目里遇到一个诡异的问题——特效在某些手机上显示不出来。查了两天才发现,是OpenGL ES版本的问题。有些手机只支持OpenGL ES 2.0,不支持3.0。所以写着色器时,尽量用ES 2.0的语法,兼容性最好。
30.6 知识体系总览
下面这张图,把特效处理的整体流程和关键组件串起来了。你可以对照着看,心里有个全局概念。
这张图里,特效处理是核心。解码器输出帧,特效模块处理,编码器输入帧。整个流程是流水线式的,每一帧都要经过这三个阶段。我建议你在实现时,用三个线程分别处理解码、特效、编码,用队列传递数据,这样效率最高。
好了,特效这块的内容就讲到这里。粒子特效和模糊特效是最基础、最常用的两种效果。掌握了它们,其他特效(比如颜色滤镜、扭曲、老电影效果)都是类似的思路——在着色器里做像素运算。多动手写代码,多调试,你会发现特效其实没那么神秘。