一、引言
在短视频、Vlog、剪辑工具日益流行的今天,给视频添加 Logo、水印、时间戳或动态贴纸,已经成为非常常见的功能需求。这类效果看似简单,其实背后都涉及到“图层合成”的处理:如何将一个静态或动态的图层(如文字、图片、动画)与原始视频内容进行有效叠加,并最终导出成可播放的视频文件?
在 AVFoundation 中,这类功能主要依赖两个关键能力:
- AVMutableVideoComposition:用于控制视频渲染过程,包括输出尺寸、帧率、图层结构等;
- AVVideoCompositionCoreAnimationTool:负责将 Core Animation 中的 CALayer 图层渲染到视频帧中。
借助这套机制,我们不仅可以给视频打水印、添加动态文字,还可以实现富有表现力的贴纸动画,甚至是一些 UI 动效。
本篇文章,我们将从理论出发,深入讲解 AVFoundation 图层合成的实现原理与关键组件;在下一篇中,我们将结合 Demo,动手实现一个视频添加水印与贴纸的完整流程。
二、AVMutableVideoComposition 简介
在 AVFoundation 中,AVMutableVideoComposition 是一个非常重要的类,它描述了如何将一个或多个视频轨道中的帧,渲染成最终输出的视频帧序列。如果说 AVComposition 管理的是时间线与素材轨道的关系,那么 AVMutableVideoComposition 就是对最终“视觉输出效果”的控制。
简单来说,它负责解决两个问题:
- 输出的视频画面长什么样?(尺寸、帧率、背景、变换等)
- 每一帧的渲染顺序与合成逻辑是什么?(比如加滤镜、加图层)
2.1 主要属性解析
✅ renderSize
- 指定最终输出的视频画面尺寸(例如:1080x1920)。
- 所有图层都必须在这个坐标系统内进行布局。
- 如果设置错误,可能导致图层不显示、导出失败等问题。
✅ frameDuration
- 控制每一帧的时间间隔,常见为 CMTime(value: 1, timescale: 30),表示 30fps。
- 若设置与素材帧率不符,可能会影响播放流畅度。
✅ instructions
- 类型为 [AVVideoCompositionInstructionProtocol],用于指定视频轨道在不同时间段的渲染逻辑。
- 每个 AVVideoCompositionInstruction 可以配置一个或多个 AVVideoCompositionLayerInstruction。例如:视频的缩放、旋转、透明度变化;多视频轨的合成顺序。
2.2 在视频编辑流程中的作用
可以把 AVMutableVideoComposition 理解为“渲染层上的指挥官”:
- 它并不直接操作素材,而是告诉 AVFoundation:“请按这个尺寸、顺序和方式来渲染画面。”
- 你可以在其上套用滤镜、叠加文字、添加动画图层等。
配合使用 AVVideoCompositionCoreAnimationTool,它甚至可以将 UIKit/CoreAnimation 的图层(如 CALayer、CATextLayer、CAShapeLayer)渲染进每一帧视频中,从而实现丰富的视觉效果。
2.3 一个最简单的使用例子(代码预览)
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = CGSize(width: 1080, height: 1920)
videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
videoComposition.instructions = [mainInstruction]
这个对象可以作为参数传给 AVAssetExportSession,用于控制导出时的画面合成方式。
三、添加图层的关键机制:Core Animation Tool
虽然 AVMutableVideoComposition 能够控制视频渲染的尺寸和帧率,但它本身并不负责图层的绘制。如果我们想在视频中叠加水印、文字、贴纸甚至动画,必须借助 AVFoundation 提供的图层合成机制 —— AVVideoCompositionCoreAnimationTool。
这个工具类是连接 AVFoundation 与 Core Animation 的桥梁,它允许我们把 CALayer 图层树渲染到每一帧视频画面上,从而实现丰富的视觉效果。
3.1 什么是 AVVideoCompositionCoreAnimationTool?
AVVideoCompositionCoreAnimationTool 是 AVVideoComposition 的一个可选属性,用于在视频导出时,把你设置的图层渲染到输出帧中。
其典型用途包括:
- 添加图片水印(如 Logo)
- 添加文本(标题、时间戳)
- 添加动画贴纸、表情
- 使用 CAAnimation 实现复杂动画效果(如移动、淡入淡出等)
一句话总结:它让你能“画在视频上”。
3.2 如何使用 Core Animation Tool?
使用它的方式非常固定,关键是构造一个图层结构并设置:
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(
postProcessingAsVideoLayer: videoLayer,
in: parentLayer
)
- videoLayer:承载视频帧的图层,系统会把每帧画面渲染到这个图层上。
- parentLayer:容器图层,包含 videoLayer 以及其他你希望叠加的图层(如水印图层、文字图层等)。
最终,整个图层结构被合成渲染到每一帧输出画面中。
3.3 图层结构示意图
推荐使用如下结构(从上到下是层级):
parentLayer
├── videoLayer (负责承载视频帧)
├── watermarkLayer (图片水印)
├── textLayer (文字/字幕)
└── animationLayer (动态贴纸等)
⚠️ 注意:所有图层的尺寸都应该与 videoComposition.renderSize 完全一致,否则可能出现错位、无法渲染等问题。
3.4 坐标系说明(易错点)
- CALayer 使用的是 左上角为 (0, 0) 的坐标系(与 UIKit 相反)
- 所有位置、尺寸都要基于 renderSize 计算,比如:
watermarkLayer.frame = CGRect(x: renderSize.width - 100, y: renderSize.height - 100, width: 80, height: 80)
- 图层默认透明背景,叠加时会自动覆盖下方内容
3.5 添加动画图层
由于 CALayer 支持 CAAnimation,你可以给图层添加任意动画,例如:
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1.0
animation.beginTime = AVCoreAnimationBeginTimeAtZero + 2.0
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
watermarkLayer.add(animation, forKey: "fadeIn")
结合 beginTime,你甚至可以控制水印在第几秒出现,第几秒消失,做出“动态水印”效果。
3.6 常见问题与陷阱
问题 |
原因 |
---|---|
图层不显示 |
坐标错误 / 图层尺寸不匹配 / 未正确加入 parentLayer |
图层变形 |
renderSize 与原素材尺寸不一致 / 图层未正确拉伸 |
动画无效 |
没设置 beginTime / 忘记设置 fillMode / 图层动画未添加成功 |
导出失败 |
图层中含有不支持的动画类型(建议使用基本动画) |
四、视频图层合成的基本结构
在上一节中我们了解了 AVVideoCompositionCoreAnimationTool 的作用和基本用法。接下来我们来具体拆解:如何构建图层结构,将多个内容合成到视频画面中。
在 AVFoundation 的图层合成中,最常见的操作就是:将原始视频帧作为底层图层,并在其上叠加其他视觉元素,例如图片水印、文本信息、动画贴纸等。
这背后依赖的是 Core Animation 的图层树结构。
4.1 推荐图层结构
通常我们推荐使用如下的分层方式:
let parentLayer = CALayer()
let videoLayer = CALayer()
let watermarkLayer = CALayer()
let textLayer = CATextLayer()
// 可选:更多图层(如动态贴纸、时间戳)
parentLayer.frame = CGRect(origin: .zero, size: renderSize)
videoLayer.frame = parentLayer.frame
watermarkLayer.frame = CGRect(x: ..., y: ..., width: ..., height: ...)
textLayer.frame = CGRect(x: ..., y: ..., width: ..., height: ...)
// 添加顺序很关键
parentLayer.addSublayer(videoLayer) // 视频在底层
parentLayer.addSublayer(watermarkLayer) // 水印在上
parentLayer.addSublayer(textLayer) // 文字层
然后将这个 parentLayer 和 videoLayer 一起交给 AVVideoCompositionCoreAnimationTool:
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(
postProcessingAsVideoLayer: videoLayer,
in: parentLayer
)
4.2 图层尺寸与坐标系说明
构建图层结构时,最容易出错的是尺寸和坐标系:
属性 |
要点说明 |
---|---|
尺寸(frame) |
所有图层尺寸必须与 renderSize 匹配,否则位置和缩放会异常 |
坐标系 |
Core Animation 的坐标原点在左上角,y 值向下增长(与 UIKit 相反) |
图片缩放 |
图层内容如图片需要根据目标尺寸进行适配,否则可能拉伸或被裁剪 |
4.3 各类图层添加方式
✅ 图片水印(Logo)
let image = UIImage(named: "logo")!
let watermarkLayer = CALayer()
watermarkLayer.contents = image.cgImage
watermarkLayer.frame = CGRect(x: renderSize.width - 100, y: 20, width: 80, height: 80)
watermarkLayer.opacity = 0.8
✅ 文本图层(如标题、用户名)
let textLayer = CATextLayer()
textLayer.string = "演示视频 by Pang"
textLayer.fontSize = 24
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.alignmentMode = .center
textLayer.frame = CGRect(x: 0, y: 20, width: renderSize.width, height: 40)
textLayer.contentsScale = UIScreen.main.scale
✅ 动态图层(贴纸/动画)
let stickerLayer = CALayer()
stickerLayer.contents = UIImage(named: "star")?.cgImage
stickerLayer.frame = CGRect(x: 30, y: 30, width: 50, height: 50)
// 添加简单动画(如旋转)
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0
rotation.toValue = Double.pi * 2
rotation.duration = 2
rotation.repeatCount = .infinity
stickerLayer.add(rotation, forKey: "rotate")
4.4 小贴士:透明背景与抗锯齿
- CALayer 默认背景是透明的,无需特殊设置
- 为避免文字模糊,textLayer.contentsScale 建议设置为 UIScreen.main.scale
- 所有图层请避免使用 masksToBounds = true,以免意外裁剪动画或子图层
4.5 图层生命周期说明
这些图层的渲染,仅在视频导出(或播放合成 AVPlayerItem 时)被一次性处理。它们在导出完成后就“烧录”进视频文件中,无法再修改或交互。因此:
- 不支持用户拖动、点击图层
- 动画必须提前规划好时间、路径、透明度等
五、动态内容支持
前面我们已经构建好了图层结构,添加了静态的水印图像和文字图层。但在实际项目中,用户往往希望能看到**“动”的效果**:
比如水印淡入淡出、字幕逐行滚动、贴纸旋转跳动……这些都需要借助 Core Animation 来实现动态图层合成。
AVFoundation 本身并不负责动画逻辑,而是通过 AVVideoCompositionCoreAnimationTool 把 Core Animation 的动画“烧录”进每一帧输出画面中。因此,我们完全可以使用 Core Animation 的动画能力,来制作动态效果图层。
5.1 常见的动态图层形式
动态效果 |
实现方式 |
---|---|
水印淡入淡出 |
CABasicAnimation 作用于 opacity |
贴纸旋转 |
CABasicAnimation 作用于 transform.rotation.z |
图层移动 |
CABasicAnimation 作用于 position |
路径动画 |
CAKeyframeAnimation 配合贝塞尔曲线路径 |
动画序列帧 |
定时切换 contents 或使用 CAKeyframeAnimation |
动态文本滚动 |
修改 position.y 并设置线性动画 |
5.2 控制动画时机的关键参数
每个动画图层必须明确告诉系统:什么时候开始动、动多久。这依赖几个重要参数:
✅ beginTime
表示动画的起始时间(相对于视频时间的 0 秒)
- 通常设置为:AVCoreAnimationBeginTimeAtZero + 1.0(表示从第 1 秒开始)
- 如果不设置,动画可能不会生效
✅ duration
动画持续时长(单位为秒)
✅ fillMode
控制动画结束后的状态(常用 .forwards)
✅ isRemovedOnCompletion
设置为 false 可让动画结束后保持最终状态(比如淡入后常驻)
5.3 示例:水印淡入
let fadeIn = CABasicAnimation(keyPath: "opacity")
fadeIn.fromValue = 0
fadeIn.toValue = 1
fadeIn.duration = 1.0
fadeIn.beginTime = AVCoreAnimationBeginTimeAtZero + 2.0
fadeIn.isRemovedOnCompletion = false
fadeIn.fillMode = .forwards
watermarkLayer.add(fadeIn, forKey: "fadeIn")
此动画表示:第 2 秒开始,1 秒内从透明渐变为可见。
5.4 示例:贴纸旋转
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0
rotate.toValue = Double.pi * 2
rotate.duration = 2
rotate.repeatCount = .infinity
stickerLayer.add(rotate, forKey: "rotate")
这段代码会让贴纸图层无限循环地旋转。
5.5 示例:沿路径移动
let move = CAKeyframeAnimation(keyPath: "position")
move.path = UIBezierPath(ovalIn: CGRect(x: 100, y: 100, width: 200, height: 200)).cgPath
move.duration = 4.0
move.beginTime = AVCoreAnimationBeginTimeAtZero + 1.0
move.repeatCount = 1
move.fillMode = .forwards
move.isRemovedOnCompletion = false
animatedLayer.add(move, forKey: "orbit")
你甚至可以让图层沿椭圆路径飞行!
5.6 动态文本:标题/字幕动效
let titleLayer = CATextLayer()
titleLayer.string = "AVFoundation 视频合成演示"
titleLayer.fontSize = 28
titleLayer.foregroundColor = UIColor.white.cgColor
titleLayer.alignmentMode = .center
titleLayer.frame = CGRect(x: 0, y: renderSize.height, width: renderSize.width, height: 40)
let scroll = CABasicAnimation(keyPath: "position.y")
scroll.fromValue = renderSize.height + 20
scroll.toValue = renderSize.height - 80
scroll.duration = 2
scroll.beginTime = AVCoreAnimationBeginTimeAtZero + 1.0
scroll.fillMode = .forwards
scroll.isRemovedOnCompletion = false
titleLayer.add(scroll, forKey: "scrollIn")
让标题文字从屏幕底部“滑入”到中间位置,很适合视频片头效果。
5.7 动态图层注意事项
注意点 |
说明 |
---|---|
图层必须添加到 parentLayer 中 |
否则不会渲染 |
动画必须设置 beginTime 和 fillMode |
防止动画不播放或一闪而过 |
所有动画基于 Core Animation 离屏渲染 |
导出时性能消耗较高,建议控制动画数量和复杂度 |
导出时间可能显著增加 |
动画越复杂,合成时间越长 |
六、结语
本文我们围绕 AVMutableVideoComposition 和 AVVideoCompositionCoreAnimationTool,深入讲解了视频图层合成的核心机制。无论是静态水印、动态贴纸,还是滑入滑出的字幕效果,其本质都是通过构建一个完整的 CALayer树,并借助 AVFoundation 渲染到每一帧视频中。
总结起来,视频图层合成的核心步骤包括:
- 使用 AVMutableVideoComposition 配置输出尺寸与帧率;
- 构建 parentLayer 图层树,添加视频层、图像层、文本层等;
- 通过 AVVideoCompositionCoreAnimationTool 将图层合成绑定到视频;
- 根据需要添加 CABasicAnimation 或 CAKeyframeAnimation 实现动效;
- 最终配合 AVAssetExportSession 导出合成后的视频文件。
虽然过程看起来略显繁琐,但一旦理解其中原理,就能灵活实现各种视觉叠加效果,为视频内容增添专业感与表现力。
下一篇文章中,我们将结合实战 Demo,实现一个支持添加动态水印与字幕动画的导出工具,欢迎继续关注~