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 核心流程:解码→变换→编码→封装

整个流程我用一张图来说明:

视频镜像处理核心流程 输入视频 MP4文件 MediaCodec解码 输出YUV/RGBA帧 OpenGL ES镜像变换 修改纹理坐标 水平/垂直翻转 编码 H.264 MediaMuxer封装 输出镜像后的MP4 关键点:解码器输出 → 渲染到OpenGL纹理 → 修改纹理坐标实现镜像 → 读取渲染结果 → 送入编码器 水平镜像:u' = 1.0 - u 垂直镜像:v' = 1.0 - v

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标准流程:

  1. 创建MediaMuxer,指定输出路径和格式(MP4)
  2. 添加视频轨道(从编码器获取MediaFormat)
  3. 循环从编码器取出编码后的Buffer
  4. 调用muxer.writeSampleData()写入数据
  5. 所有帧处理完后,调用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