给视频播放器装上“效果引擎“:ExoPlayer 的 OpenGL 滤镜链路,从原理到落地 给视频播放器装上效果引擎ExoPlayer 的 OpenGL 滤镜链路从原理到落地【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer做视频播放器的时候我几乎每次都会被问到同一个需求能不能给画面加一层滤镜灰度、复古、调亮度像短视频 App 那样随手一滑就换效果。原生MediaPlayer在这件事上基本是条死路——它把渲染完全封装死了我们插不进任何一步。而 ExoPlayer 作为 Google 开源的 Android 可扩展媒体播放器恰好把渲染链路的最后一公里开放了出来。这篇文章会带你走一遍它背后的 OpenGL 处理管线并给出一个能直接跑通的自定义滤镜让给视频变脸这件事从不可为变成半小时就能做完。先拆一拆为什么给画面加滤镜这么难表面上看滤镜只是逐像素改颜色但在 Android 上它横着三道坎。第一道解码器输出的视频帧并不是一张我们能随手改的 Bitmap而是 GPU 上的纹理Texture它走的是底层图形 API不是 Java 层能碰的东西。第二道视频是持续渲染的每秒 30 帧意味着你的处理逻辑必须挂在渲染循环里任何卡顿都会直接变成画面丢帧。第三道播放、解码、渲染往往分属不同线程处理效果的人和消费帧的人得有一套同步机制。所以问题的本质是我们要在帧从解码器出来、还没到屏幕的这段路上插进一个自定义的加工步骤。ExoPlayer 的 effect 模块就是为这个场景准备的。先看清画面是怎么到达屏幕的在进入代码之前值得花两分钟搞清楚 ExoPlayer 的渲染链路上有哪些环节。视频流经解码器MediaCodec之后会进入一个可替换的渲染组件默认情况下它直接把帧画到一个Surface上这个 Surface 通常就是播放器界面的画布。而当我们引入VideoFrameProcessor时事情就变成了这样解码器 → OpenGL 纹理 → 效果链逐帧处理→ 输出 Surface → 屏幕VideoFrameProcessor在一个后台线程上管理 OpenGL 上下文负责按顺序执行一串效果再把结果交给显示层。它的实现藏在 library/effect/src/main/java/com/google/android/exoplayer2/effect/DefaultVideoFrameProcessor.java而 demos/gl 目录下的示例应用则展示了如何用一块GLSurfaceView把这条链路接进真实播放流程。明白这条链路之后我们只需要回答一个问题效果本身长什么样怎么写一个效果到底长什么样ExoPlayer 的效果体系围绕两个接口展开。GlEffect是效果的说明书它不碰任何 GL 细节只负责一件事——在需要时把自己转换成实际的着色器程序library/effect/src/main/java/com/google/android/exoplayer2/effect/GlEffect.java 里定义了一个核心方法toGlShaderProgram(Context, boolean useHdr)第二个参数useHdr直接决定了你的颜色计算是在 HDR 还是 SDR 色彩空间里进行这个细节后面还会踩到。而GlShaderProgram才是真正干活的那个它承载 GLSL 着色器负责纹理的加载、绘制与释放。两者分离的好处很明显一个效果对象可以被多个播放器实例共享且描述我要灰度与实现怎么算灰度可以独立演化。更妙的是框架里已经内置了一条捷径——RgbMatrix接口。只要你的滤镜本质上是一个颜色变换矩阵连着色器都不用自己写框架会用DefaultShaderProgram帮你把矩阵灌进片段着色器library/effect/src/main/java/com/google/android/exoplayer2/effect/RgbMatrix.java 里已经写好了这个默认实现。先别急着写 GLSL官方轮子够用吗在动手之前我建议先看一眼官方的成品。RgbFilter提供了现成的灰度与反色滤镜构造方式十分直白// 灰度 反色两个效果可以放进同一个列表 GlEffect grayscale RgbFilter.createGrayscaleFilter(); GlEffect inverted RgbFilter.createInvertedFilter();这段代码做了什么用工厂方法拿到两个官方内置的颜色滤镜对象它们可以直接作为效果链的成员参与渲染。官方实现里灰度矩阵的系数是有讲究的——SDR 用 BT.709 的亮度系数HDR 用 BT.2020 的系数library/effect/src/main/java/com/google/android/exoplayer2/effect/RgbFilter.java 里两种矩阵都备好了。这意味着同一个效果对象在播放普通视频和 HDR 视频时会自动切换合适的色彩变换无需你操心。这就是效果描述与实现分离设计带来的实际收益。轮子不够用时自己造一个也不难假设我们要一个官网上没有的复古棕褐效果。基于RgbMatrix只需要提供一组 4×4 的颜色矩阵public final class SepiaEffect implements RgbMatrix { // 列主序存储三行分别是 R/G/B 的加权系数 private static final float[] SEPIA_MATRIX { 0.393f, 0.349f, 0.272f, 0f, 0.769f, 0.686f, 0.534f, 0f, 0.189f, 0.168f, 0.131f, 0f, 0f, 0f, 0f, 1f }; Override public float[] getMatrix(long presentationTimeUs, boolean useHdr) { return SEPIA_MATRIX; } }这段代码做了什么定义了一个颜色变换矩阵让每个像素的 R 通道从三个原通道按 0.393/0.769/0.189 加权而来G、B 同理这就是经典棕褐色滤镜的数学本质。因为RgbMatrix自带默认的着色器实现这个类写完就能直接用。如果你想要更灵活的效果比如九宫格分屏、浮雕、水印叠加那就需要直接实现GlEffect并提供一个自定义着色器片段。demos/gl 里的水印叠加示例就是个很好的参照它在片段着色器中同时采样视频纹理和水印纹理动态叠加画面demos/gl/src/main/assets/bitmap_overlay_video_processor_fragment.glsl。帧率水印这类效果本质上就是把uTexSampler1的采样结果按坐标混入主画面。下面这张测试图来自项目自带的回归数据可以直观感受同一帧画面经滤镜处理前后的差异原始帧细节完整、色彩自然而叠加了灰度与红色通道增强后画面呈现鲜明的调色风格——这正是效果链逐帧作用的结果。效果写好了往哪接两条落地路径滤镜做好之后实际使用有两条截然不同的路线取决于你要的是转码还是预览。路线一离线转码。如果需求是导出成品视频比如分享出去的视频应该用Transformer它复用同一套GlEffect体系把效果链应用到整段视频再输出 MP4。项目文档 docs/transforming-media.md 给出了完整的用法Transformer transformer new Transformer.Builder(context) .setVideoEffects(List.of(new SepiaEffect())) .addListener(transformerListener) .build(); transformer.startTransformation(inputMediaItem, outputPath);这段代码做了什么构建一个转码器实例把自定义的棕褐滤镜注入视频处理管线然后启动异步转码把处理后的视频写入指定路径。路线二播放中实时预览。如果滤镜要跟着播放实时切换就需要把渲染管线改成GLSurfaceView方案——ExoPlayer 解码后的帧不直接上屏而是作为纹理送进你的 GL 绘制循环。demos/gl 的MainActivity就是这么干的demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java它把自定义的VideoProcessor挂到GLSurfaceView上播放器照常工作画面却在每次draw回调里被加工过。切换效果时只要替换效果对象并刷新渲染即可播放进度不受影响。两个值得提前踩平的坑第一个坑是色彩空间。toGlShaderProgram里的useHdr参数不是摆设处理 HDR 源时帧内颜色是线性 RGB BT.2020如果你的着色器按 SDR 的 BT.709 系数硬算出来的颜色会完全不对。所以自写滤镜时要么参考RgbFilter的做法同时准备两套参数要么明确你的输入源不会切到 HDR。第二个坑是资源回收。GlShaderProgram的输出纹理如果不及时释放播放几分钟后内存会肉眼可见地涨。框架里BaseGlShaderProgram已经封装了纹理池复用自定义实现时务必走它的生命周期方法不要自己裸调glGenTextures又忘了glDeleteTextures。下一步还能做什么到这里我们已经走完了一个完整闭环从理解渲染链路到写一个可用的自定义滤镜再到选对落地路径。这条路再往前走空间其实很大——可以把效果链按时间轴动态切换做出前半段灰度、后半段彩色的分段滤镜可以把多个效果合并进同一个着色器把绘制次数降到一次也可以研究OverlayEffect与Presentation把贴纸、字幕、画面比例调整统统纳入同一套效果体系。顺带一提这个仓库的代码已经标注了向 AndroidX media3 迁移的方向本文讲的效果接口体系在新版本里依然成立迁移成本远比你想象的低。下一次打开播放器时不妨想想眼前这帧画面还能被改写成什么样子。【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考