ExoPlayer Transformer 视频处理实战:5步从零搭建视频剪辑导出引擎
【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer
"产品经理又来了:'用户录完视频,想裁出 15 秒精华,套一层胶片滤镜,再盖上自己的水印,一键导出发朋友圈。'"你翻遍 Android 自带的 MediaPlayer,只会发现一个残酷事实——它只会播,不会剪,更别提导出。想要 ExoPlayer 视频处理能力,又不想从零去写 MediaCodec、OpenGL、MP4 封装这一大摊底层活?ExoPlayer 生态里的 Transformer 模块正是为这个场景准备的。它是播放器的"离线双胞胎",把"解码 → 处理 → 编码 → 封装"整条流水线封装成几个类。这篇文章我们就沿着"先跑通、再叠加、后打磨"的路线,亲手把一套视频剪辑导出引擎从 0 搭到 1。全程代码都可在仓库的demos/transformer/找到对应实现。
第一步:装好管道,5 行代码跑通第一次导出
先别急着想滤镜,任何剪辑功能都建立在"能导出"之上。Transformer 的思路很好理解:普通播放器把帧送到屏幕,而 Transformer 把帧送到编码器再写进文件。中间的"解码端"完全复用 ExoPlayer 的组件,所以你能处理的所有格式,它几乎都能导出。
先加依赖:
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1' implementation 'com.google.android.exoplayer:exoplayer-effect:2.19.1'然后是核心导出代码:
Transformer transformer = new Transformer.Builder(context) .addListener(new Transformer.Listener() { @Override public void onCompleted(Composition composition, ExportResult result) { // 导出完成,result 里带有输出时长、文件大小等信息 } @Override public void onError(Composition composition, ExportResult result, ExportException e) { // 导出失败,e.errorCode 会告诉你具体原因 } }) .build(); EditedMediaItem item = new EditedMediaItem.Builder(MediaItem.fromUri(videoUri)).build(); Composition composition = new Composition.Builder(new EditedMediaItemSequence(item)).build(); transformer.start(composition, new File(context.getCacheDir(), "output.mp4").getAbsolutePath());这段代码做了三件事:构建导出器、把素材包装成"可编辑条目"、启动导出。EditedMediaItem是素材的包装盒,之后所有裁剪、滤镜都会塞进这个盒子里。跑通后,你会在cacheDir下得到一个重编码的 mp4——虽然它和原视频长得一模一样,但整条管道已经打通了。
管道通了,可它只会"原封不动地洗一遍"。用户要的是剪辑,不是复制。所以下一步,我们让视频"听话"。
第二步:让视频听话——裁剪、拼接与旋转
需求很朴素:把 30 秒的视频只留中间 15 秒。MediaItem本身就支持裁剪配置,这和你播放时设置起止位置是同一套 API:
MediaItem mediaItem = new MediaItem.Builder() .setUri(videoUri) .setClippingConfiguration( new MediaItem.ClippingConfiguration.Builder() .setStartPositionMs(5_000) .setEndPositionMs(20_000) .build()) .build();为什么要用 MediaItem 而不是手写时间轴?因为裁剪在"解码前"生效,省掉了后面整段无用帧的处理开销,比导出后再切快得多。
再进阶一点:把两段素材拼成一个视频。只需把多个EditedMediaItem放进同一个EditedMediaItemSequence:
EditedMediaItemSequence sequence = new EditedMediaItemSequence(ImmutableList.of( new EditedMediaItem.Builder(clipA).build(), new EditedMediaItem.Builder(clipB).build())); Composition composition = new Composition.Builder(sequence).build();Transformer 会按顺序播放并导出它们,段与段之间无缝衔接——这正是"短视频拼接"功能的最小实现。当然,直接拼会很生硬,想让画面旋转、缩放?交给ScaleAndRotateTransformation:
Effects effects = new Effects( ImmutableList.of(), // 音频处理器,先留空 ImmutableList.of(new ScaleAndRotateTransformation.Builder() .setRotationDegrees(90f) .setScale(0.5f, 0.5f) .build())); EditedMediaItem item = new EditedMediaItem.Builder(mediaItem).setEffects(effects).build();旋转后的效果就像这样——素材库里的测试帧在rotate90处理后整个横了过来:
到这里,视频已经能剪、能拼、能转,用户终于能产出"作品"了。但还差点意思——嗯,还差一层"妆"。
第三步:给视频上妆——滤镜与文字水印
滤镜的本质是逐帧改变像素颜色。Transformer 内置了一批开箱即用的效果类,灰度、反色、对比度、HSL 调节,全是现成的:
List<Effect> videoEffects = new ArrayList<>(); videoEffects.add(RgbFilter.createGrayscaleFilter()); // 灰度滤镜 videoEffects.add(RgbFilter.createInvertedFilter()); // 反色滤镜 videoEffects.add(new HslAdjustment.Builder() .adjustSaturation(1.2f) // 饱和度 +20% .adjustLightness(0.1f) .build());加一个效果只需要往列表里 add 一行,多个效果会按顺序串成一条链逐帧执行。看一组实际效果,左边是原始画面,右边是同帧经过灰度处理后的结果:
如果想让画面更有层次,Contrast控制对比度、RgbAdjustment单独调节 RGB 三通道,组合使用就能做出冷暖色调——这些类的源码都在library/effect/src/main/java/com/google/android/exoplayer2/effect/下,读起来比想象中容易。
滤镜之外,水印是短视频的刚需。用TextOverlay加文字、BitmapOverlay加图片,再用OverlayEffect把多个叠加层打包:
SpannableString watermark = new SpannableString("My First Edit"); watermark.setSpan(new ForegroundColorSpan(Color.WHITE), 0, watermark.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); OverlayEffect overlayEffect = new OverlayEffect( TextOverlay.createStaticTextOverlay( watermark, new OverlaySettings.Builder().setAlpha(0.8f).build())); videoEffects.add(overlayEffect); EditedMediaItem item = new EditedMediaItem.Builder(mediaItem) .setEffects(new Effects(ImmutableList.of(), videoEffects)) .build();OverlaySettings里的矩阵参数可以控制水印的位置、缩放和透明度,这是从"能加水印"到"水印放得好看"的关键。叠加文字后的输出长这样:
功能齐全了,可真的上架后,导出黑屏、卡在 99%、闪退这些"野生问题"会接踵而来。最后一步,我们把体验打磨到能交付。
第四步:把导出体验打磨到可用——进度、回退与避坑清单
先解决"不知道导到哪了"。Transformer 用ProgressHolder提供进度,注意它需要你在主线程定时轮询:
ProgressHolder holder = new ProgressHolder(); Handler mainHandler = new Handler(Looper.getMainLooper()); mainHandler.postDelayed(new Runnable() { @Override public void run() { if (transformer.getProgress(holder) != Transformer.PROGRESS_STATE_NOT_STARTED) { progressBar.setProgress(holder.progress); // 0 ~ 100 mainHandler.postDelayed(this, 500); // 每 500ms 刷新一次 } } }, 500);每 500ms 轮询一次对 UI 线程压力极小,用户却能实时看到进度条在动,体感提升巨大。
再解决"设备编码器不给力"。安卓碎片化的直接后果是:这台手机能导出的格式,换一台就报错。打开编码回退开关,让 Transformer 在首选编码器失败时自动降级:
Transformer transformer = new Transformer.Builder(context) .setEncoderFactory(new DefaultEncoderFactory.Builder(context) .setEnableFallback(true) .build()) .build();这行配置能把"某机型导出必崩"这类事故率降下一大截。
最后对照这张避坑清单自查一遍:
| 坑 | 表现 | 对策 |
|---|---|---|
| 导出中再次调用 start | 抛IllegalStateException | 用布尔标记锁住入口 |
| 输出文件指向已存在文件 | 导出失败或行为异常 | 先删除旧文件再写入getCacheDir() |
| 主线程频繁忙轮询进度 | 掉帧卡顿 | 用 Handler 定时 500ms 轮询 |
| 低端机导出超大分辨率 | 慢且易 OOM | 加Presentation.createForHeight(720)压缩输出 |
| Android 13 读取相册视频 | 权限异常 | 申请READ_MEDIA_VIDEO,输出走私有目录 |
⚠️ 特别提醒:Presentation.createForHeight(720)是控制输出分辨率的利器——把 1080p 压到 720p 后,编码数据量直接降到原来的约四分之一,导出速度和成功率都会有肉眼可见的提升,非常适合"默认导出"场景。而调试期如果怀疑滤镜或画面出了问题,Transformer.Builder上还留了setDebugViewProvider钩子,可以实时预览处理中的每一帧,demos/transformer/TransformerActivity.java里就有完整用法。
进阶方向:从"能用"到"专业"
到这里,你已经拥有一个能裁剪、拼接、加滤镜、加水印并导出 MP4 的剪辑引擎了。往下走还有三条值得探索的路:
- 动态效果:仓库
demos/transformer/里的MatrixTransformationFactory实现了 3D 旋转、缩放推近等转场,PeriodicVignetteShaderProgram展示如何手写 GLSL 做周期性暗角,它们是通往自定义特效的样板。 - 慢动作与变速:
EditedMediaItem.Builder的setFlattenForSlowMotion配合音频处理器(如SonicAudioProcessor)可以玩出变速效果。 - HDR 内容:
tone_map_pq_to_sdr.png这类测试图告诉你,Transformer 连 HDR 到 SDR 的色调映射都管了,新项目请直接关注 media3 的迁移方向。
看完这一篇,你的下一版 App 或许就敢在产品评审会上接"导出剪辑"这个需求了。如果文章对你有帮助,欢迎留言说说你最想实现的视频功能——下一期,我们聊聊如何用 GLSL 手写一个"抖音式"实时特效,敬请期待。
【免费下载链接】ExoPlayerAn extensible media player for Android项目地址: https://gitcode.com/gh_mirrors/exop/ExoPlayer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考