16、MediaMuxer实战:视频镜像(Mirror)——实现视频的水平或垂直翻转
视频镜像,说白了就是把画面左右或上下颠倒一下。你自拍的时候,手机前置摄像头拍出来的就是镜像效果——左右是反的。但后置摄像头拍出来的,又是正常的。
这个功能在短视频编辑里太常见了。我做过一个视频剪辑App,用户反馈最多的需求之一就是「能不能把视频镜像一下」。嗯,今天我们就用MediaMuxer配合MediaCodec,把这个功能实现出来。
16.1 镜像的本质是什么?
视频镜像,本质上是对每一帧画面的像素坐标进行变换。
- 水平镜像(Horizontal Mirror):以画面垂直中轴线为基准,左右互换。坐标变换公式:
x' = width - x - 1 - 垂直镜像(Vertical Mirror):以画面水平中轴线为基准,上下互换。坐标变换公式:
y' = height - y - 1
你想想看,这个变换是在像素级别完成的。但我们在Android上做视频处理,不可能一帧一帧去操作像素——那太慢了。实际工程中,我们利用OpenGL ES的纹理坐标变换,或者使用MediaCodec的Surface输入配合Matrix变换。
核心思路:解码 → 画面变换 → 编码。解码器输出帧,经过镜像变换,再送入编码器。MediaMuxer负责把编码后的数据封装成MP4。
16.2 技术选型:三种实现方式
我这些年做视频编辑,镜像功能至少用过三种方案。各有优劣,我列个表给你看:
| 方案 | 原理 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 方案一:OpenGL ES + 纹理坐标变换 | 修改纹理坐标的u/v值 | 高(GPU加速) | 中 | 实时预览、高性能需求 |
| 方案二:MediaCodec + Matrix变换 | 利用Surface的变换矩阵 | 中 | 低 | 简单镜像,无需额外库 |
| 方案三:CPU逐帧像素操作 | Bitmap操作或YUV数据重排 | 低 | 高 | 不推荐,仅用于学习 |
我个人习惯用方案一。为什么?因为OpenGL ES是GPU加速的,处理1080P视频也能保持流畅。方案二虽然代码少,但灵活性差一些——你没法做更复杂的变换,比如同时镜像+旋转。
小提示:如果你的需求只是简单的水平翻转,方案二确实够用。但如果你后面要做滤镜、转场、画中画,那还是老老实实上OpenGL ES吧。我在项目中吃过这个亏——一开始图省事用了方案二,后来加功能时全重写了。
16.3 核心流程:解码→变换→编码→封装
整个流程我用一张图来说明:
16.4 关键代码实现
好了,理论说完了,咱们直接上代码。这里我给出最核心的OpenGL ES片段着色器代码——镜像变换的关键就在这儿。
16.4.1 片段着色器:水平镜像
// 水平镜像片段着色器
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D sTexture;
void main() {
// 水平镜像:u坐标取反
vec2 mirrorCoord = vec2(1.0 - vTexCoord.x, vTexCoord.y);
fragColor = texture(sTexture, mirrorCoord);
}
16.4.2 片段着色器:垂直镜像
// 垂直镜像片段着色器
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D sTexture;
void main() {
// 垂直镜像:v坐标取反
vec2 mirrorCoord = vec2(vTexCoord.x, 1.0 - vTexCoord.y);
fragColor = texture(sTexture, mirrorCoord);
}
注意:纹理坐标的范围是[0, 1]。水平镜像时,1.0 - vTexCoord.x会把左边的像素映射到右边,右边的映射到左边。我曾经犯过一个错误——直接用-vTexCoord.x,结果画面全黑了。因为负坐标会被纹理采样器截断为0。
16.4.3 同时支持水平和垂直镜像
如果你想同时做水平和垂直镜像(也就是旋转180度),可以这样:
// 同时水平+垂直镜像
#version 300 es
precision mediump float;
in vec2 vTexCoord;
out vec4 fragColor;
uniform sampler2D sTexture;
uniform bool uHorizontalMirror;
uniform bool uVerticalMirror;
void main() {
vec2 coord = vTexCoord;
if (uHorizontalMirror) {
coord.x = 1.0 - coord.x;
}
if (uVerticalMirror) {
coord.y = 1.0 - coord.y;
}
fragColor = texture(sTexture, coord);
}
16.5 MediaMuxer封装流程
镜像处理完,接下来就是把编码后的数据写进MP4文件。这部分我直接复用之前讲过的MediaMuxer标准流程:
- 创建MediaMuxer,指定输出路径和格式(MP4)
- 添加视频轨道(从编码器获取MediaFormat)
- 循环从编码器取出编码后的Buffer
- 调用
muxer.writeSampleData()写入数据 - 所有帧处理完后,调用
muxer.stop()和muxer.release()
经验之谈:编码器的输出Buffer时间戳必须严格递增。我遇到过一个问题——解码器出来的时间戳是乱的,直接传给编码器导致输出视频花屏。解决办法是在解码时重新计算PTS,保证单调递增。
16.6 避坑指南
做视频镜像,有几个坑我替你们踩过了:
- 纹理方向问题:Android相机输出的纹理方向可能是旋转过的。如果你直接做镜像,画面可能歪了。建议先做一次旋转校正,再做镜像。
- 性能瓶颈:如果视频分辨率是4K,GPU压力会很大。我建议先降采样到1080P处理,处理完再升采样。用户肉眼基本看不出区别。
- 音频同步:镜像只处理视频,音频要保持原样。别忘了在MediaMuxer里同时添加音频轨道,否则输出视频没声音。
- 关键帧对齐:编码器输出的关键帧间隔可能不固定。如果你要做精确的裁剪或拼接,记得用
MediaCodec.BUFFER_FLAG_KEY_FRAME标记。
16.7 完整代码结构
最后,我给出一个完整的类结构,方便你组织代码:
public class VideoMirrorProcessor {
private MediaExtractor extractor;
private MediaCodec decoder;
private MediaCodec encoder;
private MediaMuxer muxer;
private EGLSurface inputSurface;
private EGLSurface outputSurface;
private int textureId;
// 核心方法
public void process(String inputPath, String outputPath, boolean horizontal, boolean vertical) {
// 1. 初始化提取器、解码器、编码器、混合器
// 2. 设置OpenGL环境和纹理
// 3. 循环解码 → 渲染 → 镜像 → 编码
// 4. 释放资源
}
// 镜像变换(在渲染循环中调用)
private void applyMirror(boolean horizontal, boolean vertical) {
// 修改片段着色器的uniform变量
// 或者直接修改顶点坐标
}
}
嗯,代码框架就是这样。具体实现细节,我建议你结合前面几章讲的MediaCodec和OpenGL ES内容一起看。镜像本身不复杂,但把它嵌入到完整的编解码管线里,才是真正的挑战。
总结一下:视频镜像的核心就是纹理坐标变换。水平镜像改u坐标,垂直镜像改v坐标。用OpenGL ES做,性能最好。MediaMuxer负责把处理完的数据封装成文件。记住我说的那几个坑,尤其是纹理方向和PTS问题——这两个最容易翻车。
公众号:蓝海资料掘金营,微信deep3321