一篇文章吃透 ExoPlayer 视频转码:Transformer 离线编辑与导出的完整实战
【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer
做了多年 Android 播放器,最头疼的需求往往不是"把视频播出来",而是"把视频改一改再交还用户"——加个开场滤镜、去掉片尾几秒、把横屏改成竖屏、把两段素材拼成一个文件。原生 MediaCodec 管线又长又绕,AVAsset 在 Android 上根本不存在,很多团队被迫堆 FFmpeg 二进制。而 ExoPlayer 视频转码能力(Transformer 模块)把这条链路压缩到了几乎"配置即所得"的程度:不用自己写编码循环、不用管理复用/释放缓冲,只要描述"我想对画面做什么",剩下交给框架。
读完本文你将掌握:
- Transformer 离线转码与 Effect 渲染管线的前世今生
- 依赖引入与官方 demo 的运行路径
- 裁剪、旋转、缩放等几何变换的落地写法
- 亮度、对比度、HSL、色彩 LUT 等调色效果的组合姿势
- 多段视频拼接与导出进度监听的方法
- 避免转码翻车的性能与调试清单
先搞清楚一个前提:为什么要用 Transformer 而不是实时滤镜
实时滤镜(demos/gl里的GlEffect链路)处理的是"播放中"的每一帧,画面直接送屏幕,不落盘。而本文的主角 Transformer 是离线导出:它读取输入源,逐帧送入VideoFrameProcessor组成的处理链,再交给DefaultEncoderFactory编码、DefaultMuxer封装,最终写成一个新文件。
一句话区分:实时滤镜是"看得见摸不着",Transformer 视频转码是"看得见、改得动、存得下"。
渲染架构见官方架构图,它解释了视频帧在处理器之间流转的方式:
这张图来自 docs/glossary.md,值得反复看。理解它的关键是记住三个名词:
| 组件 | 职责 |
|---|---|
Effect | 描述"对帧做什么"的抽象接口,视频效果与音频处理器两大类 |
VideoFrameProcessor | 帧处理器的执行引擎,管理 OpenGL 纹理与线程 |
Transformer | 导出编排器,连接读取、处理、编码、封装四个环节 |
环境准备:依赖、demo、一条龙验证
Transformer 位于独立模块library/transformer,效果类在library/effect。在build.gradle中补齐依赖:
dependencies { implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-transformer:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-effect:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' }版本号请以项目根目录 RELEASENOTES.md 中的最新发布信息为准。
如果你只是想先跑起来,无需从零搭工程。项目自带一个完整的转码 demo,代码在 demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/,它会展示输入画面、实时显示导出进度、并在完成后回放输出文件。建议对照它阅读本文的每个示例——里面用到的PeriodicVignetteShaderProgram(周期性暗角着色器)正是自定义效果的现成范本。
任务一:让画面转起来——几何变换的最短路径
先从一个最朴素的需求切入:用户录了一段竖屏视频,想顺时针转 90° 再横着看。在 Transformer 的世界里,这不需要碰任何矩阵数学——直接使用现成的ScaleAndRotateTransformation:
ScaleAndRotateTransformation rotation = new ScaleAndRotateTransformation.Builder() .setRotationDegrees(90f) // 逆时针 90 度,配合视频自带旋转可得到竖转横 .build();真正决定"转完怎么摆放"的是Presentation效果。它控制输出画幅与输入画面之间的适配策略,对应测试资源里那些scale_to_fit、stretch_to_fit、crop的命名:
Presentation presentation = new Presentation.Builder() .setWidth(1080) .setHeight(1920) .build(); // 默认走 scaleToFit,可改为 setScaleToFitBehavior 精细控制下面两张测试帧是同一段素材在旋转后的效果对比,第一张是原始帧,第二张是旋转后的输出:
任务二:给画面换气质——把调色玩出排列组合
几何变换之外,library/effect还内置了一整套颜色处理效果,它们全部实现RgbMatrix或RgbFilter接口,可以任意串联。下面是随手可用的几个:
| 效果类 | 参数含义 | 典型用途 |
|---|---|---|
Brightness | -1 ~ 1,0 为不变 | 整体提亮/压暗 |
Contrast | 0 ~ 2,1 为不变 | 增强/减弱明暗反差 |
HslAdjustment | 色调/饱和度/亮度三维 | 氛围调色 |
RgbAdjustment | 单通道偏移 | 冷调/暖调偏色 |
SingleColorLut | 颜色查找表 | 复刻复古、赛博等风格 |
以最常见的"先提亮再降饱和"为例,两个效果按顺序塞进一个Effects容器即可:
Effects effects = new Effects( /* audioProcessors= */ ImmutableList.of(), /* videoEffects= */ ImmutableList.of( new Brightness(0.1f), // 亮度加 0.1 new HslAdjustment.Builder() .adjustSaturation(-0.3f) // 饱和度下调 30% .build())); EditedMediaItem item = new EditedMediaItem.Builder(mediaItem) .setEffects(effects) .build();效果列表的执行顺序就是渲染管线的串联顺序,谁在前谁先生效。对比一下测试素材里灰度化前后的效果,能直观看到单帧处理的威力:
任务三:进阶玩法——画中画、贴纸与文字叠加
如果你觉得调色不过瘾,OverlayEffect才是重头戏。它允许把BitmapOverlay(位图)、DrawableOverlay(Drawable 资源)、TextureOverlay(纹理)甚至TextOverlay(文字)叠加到视频之上,还能通过OverlaySettings精确控制位置、缩放与透明度——这正是"贴纸/水印/片头字"类功能的通用底座。
OverlayEffect overlayEffect = new OverlayEffect( ImmutableList.of( new BitmapOverlay(bitmapLoader, bitmapUri), new TextOverlay("EXOPLAYER", textStyle))); Effects effects = new Effects( /* audioProcessors= */ ImmutableList.of(), /* videoEffects= */ ImmutableList.of( new Presentation.Builder().setWidth(1080).setHeight(1920).build(), overlayEffect));位图需要异步加载,BitmapOverlay接受一个BitmapLoader负责把 URI 变成Bitmap;demo 里用的是DataSourceBitmapLoader,可参照 demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/TransformerActivity.java 的组装方式。叠加了默认文字水印的帧长这样:
任务四:拼盘输出——多段视频合成一个文件
现在把前面所有能力串起来,完成"多段素材 + 各自特效 + 导出成一个 MP4"的完整闭环。Composition是导出的顶层描述:一个EditedMediaItemSequence是顺序播放的一组素材,多个 sequence 时间重叠的部分会做混流;每个素材又可以挂独立的Effects。
EditedMediaItemSequence sequence = new EditedMediaItemSequence( ImmutableList.of( new EditedMediaItem.Builder(firstItem) .setEffects(openingEffects) // 开场滤镜 .build(), new EditedMediaItem.Builder(secondItem) .setRemoveAudio(true) // 第二段静音 .build())); Composition composition = new Composition.Builder(ImmutableList.of(sequence)) .setEffects(compositionWideEffects) // 作用于整段的全局效果 .build();随后创建Transformer实例,注册监听器并启动导出。导出是异步的,进度通过ProgressHolder轮询获取:
Transformer transformer = new Transformer.Builder(context) .addListener(new Transformer.Listener() { @Override public void onCompleted(Composition composition, ExportResult exportResult) { // 导出成功,result 中包含时长、编解码耗时等统计 } @Override public void onError( Composition composition, ExportResult exportResult, ExportException error) { // 导出失败,根据 error.errorCode 分类处理 } }) .build(); ProgressHolder progressHolder = new ProgressHolder(); transformer.start(composition, outputFilePath); // 轮询进度:transformer.getProgress(progressHolder), // 返回 PROGRESS_STATE_AVAILABLE 时读 progressHolder.progress核心就两条 API:start(Composition, String)开干,getProgress(ProgressHolder)看进度,cancel()随时叫停。官方对导出生命周期、错误码、TransformationRequest的详细约定,见 docs/transforming-media.md。
性能优化与常见翻车点
离线转码是重活,几个高频问题提前给你排雷:
- 分辨率匹配:输出分辨率应尽量贴近源分辨率,避免无谓的缩放上采样;
Presentation的scaleToFit策略默认会等比适配,别随手设一个与源相差过大的尺寸。 - 音频处理器别乱挂:
Effects的audioProcessors列表只接收AudioProcessor(如SonicAudioProcessor变速、SilenceSkippingAudioProcessor去静音),误把视频效果塞进去会在构建阶段直接抛异常。 - HDR 输入要交代:部分效果(如
Brightness)明确不支持 HDR,遇到 HDR 素材会校验失败。传参前先确认源格式,必要时走 tone mapping 转 SDR。 - 线程模型:
Transformer实例必须由单一应用线程访问,绝大多数场景就是主线程;跨线程操作会触发非法状态异常。 - 看日志:导出阶段的关键节点会输出
ExoPlayerImplInternal/DefaultVideoFrameProcessor等模块日志,配合 docs/debug-logging.md 里LogcatLogger的接法能快速定位是哪一环卡住。 - 真机验证编码能力:不同设备支持的编码器参数不同,
TransformationRequest中声明的目标参数可能被降级,onFallbackApplied回调会告诉你实际用了什么。
总结与下一步
回顾这条从"播放"到"转码"的能力升级路径,你已经拿到了三个关键认知:Transformer 视频转码的核心是"描述意图"而非"手写管线";Effect接口是几何、调色、叠加三大类效果的统一入口;Composition把单素材编辑升级为多段拼接与混流。测试素材下积累了数百张首帧截图(见testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/),任何自定义效果都能用它快速肉眼验证。
如果想更进一步,建议按这个顺序深入:
- 自定义
GlEffect/GlShaderProgram:参考 demos/transformer/src/main/assets/ 下的 GLSL 模板与PeriodicVignetteShaderProgram的实现,写出你自己的风格化效果; - 吃透
TransformationRequest:掌握视频/音频 MIME、码率、帧率的精细控制; - 研究 demo 的矩阵工厂:MatrixTransformationFactory.java 展示了如何用
MatrixTransformation实现更自由的视角变换。
如果这篇 ExoPlayer 视频转码实战对你有帮助,点赞收藏支持一下。下一期我们聊聊如何把 Transformer 的导出管线与demos/gl的实时滤镜链路打通,让"预览即导出、所见即所得",敬请期待。
【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考