VideoLab 是开源的,高性能且灵活的 iOS 视频剪辑与特效框架,提供了更 AE(Adobe After Effect)化的使用方式。框架核心基于 AVFoundation 与 Metal。目前已有的特性:
以下是一些特性的 GIF 示例:
多图层
文字动画
关键帧动画
预合成
转场
仓库地址:https://github.com/ruanjx/VideoLab
本文将和大家分享 AVFoundation 视频剪辑流程,以及 VideoLab 框架的设计与实现。
在开始介绍之前,建议刚接触视频剪辑的同学可以先看下如下 WWDC 视频:
让我们来看下 AVFoundation 视频剪辑的整体工作流程:
我们来拆解下步骤:
AVAsset。AVComposition、AVVideoComposition 及 AVAudioMix。其中 AVComposition 指定了音视频轨道的时间对齐,AVVideoComposition 指定了视频轨道在任何给定时间点的几何变换与混合,AVAudioMix 管理音频轨道的混合参数。AVPlayerItem,并从中创建一个 AVPlayer 来播放编辑效果。AVAssetExportSession,用来将编辑结果写入文件。让我们先来看下 AVComposition,AVComposition 是一个或多个 AVCompositionTrack 音视频轨道的集合。其中 AVCompositionTrack 又可以包含来自多个 AVAsset 的 AVAssetTrack。
下图的例子,将两个 AVAsset 中的音视频 AVAssetTrack 组合到 AVComposition 的音视频 AVCompositionTrack 中。
设想下图所示的场景, AVComposition 包含两个 AVCompositionTrack。我们在 T1 时间点需要混合两个 AVCompositionTrack 的图像。为了达到这个目的,我们需要使用 AVVideoComposition。
AVVideoComposition 可以用来指定渲染大小和渲染缩放,以及帧率。此外,还存储了实现 AVVideoCompositionInstructionProtocol 协议的 Instruction(指令)数组,这些 Instruction 存储了混合的参数。有了这些混合参数之后,AVVideoComposition 可以通过一个实现 AVVideoCompositing 协议的 Compositor(混合器) 来混合对应的图像帧。
整体工作流如下图所示:
让我们聚焦到 Compositor,我们有多个原始帧,需要处理并输出新的一帧。工作流程如下图所示:
流程可分解为:
AVAsynchronousVideoCompositionRequest 绑定了当前时间的一系列原始帧,以及当前时间所在的 Instruction。startVideoCompositionRequest: 回调,并接收到这个 Request。finishWithComposedVideoFrame: 交付渲染后的帧。使用 AVAudioMix,你可以在 AVComposition 的音频轨道上处理音频。AVAudioMix 包含一组的 AVAudioMixInputParameters,每个 AVAudioMixInputParameters 对应一个音频的 AVCompositionTrack。如下图所示:
AVAudioMixInputParameters 包含一个 MTAudioProcessingTap,你可以使用它来实时处理音频。当然,对于线性音量变化可以直接使用音量斜率接口 setVolumeRampFromStartVolume:toEndVolume:timeRange:
此外,AVAudioMixInputParameters 还包含一个 AVAudioTimePitchAlgorithm,你可以使用它来设置音高。
前面我们介绍了 AVFoundation 视频剪辑流程,接下来我们介绍下 VideoLab 框架的设计。
先简要介绍下 AE(Adobe After Effect),AE 是特效设计师常用的动态图形和视觉效果软件(更多介绍参见AE官网)。AE 通过”层“控制视频、音频及静态图片的合成,每个媒体(视频、音频及静态图片)对象都有自己独立的轨道。
下图是在 AE 中合成两个视频的示例。
我们来分解下这张示例图:
基于对 AE 的分析,我们可以设计相似的描述方式:
RenderComposition,对应 AE 中的合成(Composition)。包含一组 RenderLayer(对应 AE 中的层)。此外,RenderComposition 还包含 BackgroundColor、FrameDuration、RenderSize,分别对应背景色、帧率及渲染大小等剪辑相关参数。RenderLayer,对应 AE 中的层(Layer)。包含了 Source、TimeRange、Transform、AudioConfiguration、Operations,分别对应素材来源、在时间轴的时间区间、变换(位置、旋转、缩放)、音频配置及特效操作组。RenderLayerGroup,对应 AE 的预合成。RenderLayerGroup 继承自 RenderLayer,包含一组 RenderLayer。KeyframeAnimation,对应 AE 的关键帧动画。包含了 KeyPath、Values、KeyTimes、TimingFunctions,分别对应关键路径、数值数组、关键时间数组、缓动函数数组。以上介绍了 RenderComposition、RenderLayer、RenderLayerGroup 以及 KeyframeAnimation。从前面的 AVFoundation 介绍可知,我们需要生成 AVPlayerItem 与 AVAssetExportSession 用于播放与导出。因此,我们需要有一个对象可以解析这几个描述对象,并用 AVFoundation 的方法生成 AVPlayerItem 与 AVAssetExportSession。框架将这个对象命名为 VideoLab,可以理解成这是一个实验室。
整体的工作流程如下:
我们来拆解下步骤:
RenderLayer。RenderComposition,设置其 BackgroundColor、FrameDuration、RenderSize,以及 RenderLayer 数组。RenderComposition 创建 VideoLab。VideoLab 生成 AVPlayerItem 或 AVAssetExportSession。这个章节主要介绍了框架的设计思路。设计思路总的来说,希望框架是类 AE 化灵活的方式设计。
从前面的介绍,我们知道一个 RenderLayer 可能包含一个素材来源。素材来源可以是视频、音频及静态图片等。框架抽象了 Source 协议,以下是 Source 协议的核心代码:
public protocol Source { var selectedTimeRange: CMTimeRange { get set } func tracks(for type: AVMediaType) -> [AVAssetTrack] func texture(at time: CMTime) -> Texture?}复制代码
selectedTimeRange 是素材本身的选择时间区间,如一段长 2 分钟的视频,我们选择 60s-70s 的区间作为编辑素材,那么 selectedTimeRange 就是 [60s-70s)(实际代码使用 CMTime)。tracks(for:) 方法,用于根据 AVMediaType 获取 AVAssetTrack。texture(at:) 方法,用于根据时间获取 Texture(纹理)。框架提供了 4 种内置的源,分别为:1. AVAssetSource,AVAsset;2. ImageSource,静态图片;3. PHAssetVideoSource,相册视频;4. PHAssetImageSource,相册图片。我们也可以实现 Source 协议,提供自定义的素材来源。
到目前为止我们已经知道了 RenderComposition、RenderLayer、RenderLayerGroup、KeyframeAnimation、Source,接下来将介绍 VideoLab 类如何利用这些对象创建 AVComposition、AVVideoComposition 以及 AVAudioMix。
让我们先来看下 AVComposition,我们需要给 AVComposition 分别添加视频轨道与音频轨道。
让我们结合一个示例来说明这个过程,如下图所示,这个 RenderComposition 有 RenderLayer1(包含视频/音频)、RenderLayer2(仅视频)、RenderLayer3(图片)、RenderLayer4(仅特效操作组)以及一个 RenderLayerGroup(包含 RenderLayer5、RenderLayer6,均包含视频/音频)。
让我们先聊下添加视频轨道,添加视频轨道包含以下步骤:
1. 将 RenderLayer 转换为 VideoRenderLayer
VideoRenderLayer 是框架内部对象,包含一个 RenderLayer,主要负责将 RenderLayer 的视频轨道添加到 AVComposition 中。可转换为 VideoRenderLayer 的 RenderLayer 包含以下几类:1. Source 包含视频轨道;2. Source 为图片类型;3. 特效操作组不为空(Operations)。
VideoRenderLayerGroup 是 RenderLayerGroup 对应视频的框架内部对象,包含一个 RenderLayerGroup。可转换为 VideoRenderLayerGroup 的 RenderLayerGroup 只需满足一个条件:包含的 RenderLayer 组有一个可以转化为 VideoRenderLayer。
转换 VideoRenderLayer 之后如下图所示:
2. 将 VideoRenderLayer 视频轨道添加到 AVComposition 中
对于 RenderLayer 的 Source 包含视频轨道的 VideoRenderLayer,从 Source 中获取视频 AVAssetTrack,添加到 AVComposition。
对于 RenderLayer 的 Source 为图片类型或仅有特效操作组类型(Source 为空)的 VideoRenderLayer,使用空视频添加一个新的视频轨道(这里的空视频是指视频轨道是黑帧且不包含音频轨道的视频)
添加完之后 AVComposition 的视频轨道如下图所示:
如图所示,VideoRenderLayer1 与 VideoRenderLayer5 共用了一个视频轨道。这是由于苹果对视频轨道数量有限制,我们需要尽量的重用视频轨道(每条视频轨道对应一个解码器,当解码器数量超出系统限制时,会出现无法解码的错误)。
框架视频轨道重用的原则是,如果要放入的 VideoRenderLayer 与之前视频轨道的 VideoRenderLayer 在时间上没有交集,则可以重用这个视频轨道,所有视频轨道都重用不了则新增一个视频轨道。
让我们接着聊下添加音频轨道,添加音频轨道包含以下步骤:
1. 将 RenderLayer 转换为 AudioRenderLayer
AudioRenderLayer 是框架内部对象,包含一个 RenderLayer,主要负责将 RenderLayer 的音频轨道添加到 AVComposition 中。可转换为 AudioRenderLayer 的 RenderLayer 只需满足一个条件:Source 包含音频轨道。
AudioRenderLayerGroup 是 RenderLayerGroup 对应音频的框架内部对象,包含一个 RenderLayerGroup。可转换为 AudioRenderLayerGroup 的 RenderLayerGroup 只需满足一个条件:包含的 RenderLayer 组有一个可以转化为 AudioRenderLayer。
转换 AudioRenderLayer 之后如下图所示:
2. 将 AudioRenderLayer 音频轨道添加到 AVComposition 中
对于 RenderLayer 的 Source 包含音频轨道的 AudioRenderLayer,从 Source 中获取音频 AVAssetTrack,添加到 AVComposition。
添加完之后 AVComposition 的音频轨道如下图所示:
如图所示,不同于视频轨道的重用,音频的每个 AudioRenderLayer 都对应一个音频轨道。这是由于一个 AVAudioMixInputParameters 与一个音频的轨道一一对应,而其音高设置(audioTimePitchAlgorithm)作用于整个音频轨道。如果重用的话,会存在一个音频轨道有多个 AudioRenderLayer 的情况,这样会导致所有的 AudioRenderLayer 都要配置同样的音高,这显然是不合理的。
从前面的 AVFoundation 介绍可知,AVVideoComposition 可以用来指定渲染大小和渲染缩放,以及帧率。此外,还有一组存储了混合参数的 Instruction(指令)。有了这些混合参数之后,AVVideoComposition 可以通过自定义 Compositor(混合器) 来混合对应的图像帧。
这个章节将主要介绍如何生成这组 Instruction(指令),以及创建 AVVideoComposition。我们将使用上个章节生成的 VideoRenderLayer,生成这组 Instruction(指令)。
让我们结合一个简单示例来说明这个过程,如下图所示,这个 AVComposition 有 VideoRenderLayer1、VideoRenderLayer2、VideoRenderLayer3 三个 VideoRenderLayer。转换过程包含以下步骤:
VideoRenderLayer 的起始时间点与结束时间点(如下图 T1-T6)。VideoRenderLayer,都作为 Instruction 的混合参数(如下图 Instruction1-Instruction5)。接着我们创建 AVVideoComposition,并设置帧率、渲染大小、Instruction 组以及自定义的 Compositor。核心代码如下:
let videoComposition = AVMutableVideoComposition()videoComposition.frameDuration = renderComposition.frameDurationvideoComposition.renderSize = renderComposition.renderSizevideoComposition.instructions = instructionsvideoComposition.customVideoCompositorClass = VideoCompositor.self复制代码
到目前为止,我们已经有了渲染所需的 Instruction 组与混合参数,我们继续介绍如何利用它们在 Compositor 中绘制帧画面。我们对前面的 Compositor 工作流程做一个更新,将混合参数更新为与 Instruction 有交集的 VideoRenderLayer 组。
我们同样以一个示例来说明视频混合的规则,如下图所示,在 T1 时间点,我们想要混合这几个 VideoRenderLayer 的画面。
我们的渲染混合规则如下:
VideoRenderLayer 组,依据其所包含的 RenderLayer 的 layerLevel。如上图所示在纵向从高到低的排序。VideoRenderLayer 组,对每个 VideoRenderLayer 分为以下三种混合方式:VideoRenderLayer 是 VideoRenderLayerGroup,即为预合成方式。遍历处理完自己内部的 VideoRenderLayer 组,生成一张纹理,混合到前面的纹理。VideoRenderLayer 的 Source 包含视频轨道或 Source 为图片类型,拿到纹理处理自己的特效操作组(Operations),接着混合到前面的纹理。VideoRenderLayer 仅特效操作组,所有的操作作用于前面混合的纹理。渲染混合规则总结来说,按层级渲染,从下往上。如当前层级有纹理则先处理自己的纹理,再混合进前面的纹理。如当前层级没有纹理,则特效直接作用于前面的纹理。
让我们将规则用在上图的示例中,假设我们最后输出的纹理为 Output Texture:
从前面的 AVFoundation 介绍可知,AVAudioMix 用于处理音频。AVAudioMix 包含一组的 AVAudioMixInputParameters,可以设置 MTAudioProcessingTap 实时处理音频,设置 AVAudioTimePitchAlgorithm 指定音高算法。
这个章节将主要介绍如何生成这组 AVAudioMixInputParameters,以及创建 AVAudioMix。我们将使用 AVComposition 章节生成的 AudioRenderLayer,生成这组 AVAudioMixInputParameters。
让我们结合一个简单示例来说明这个过程,如下图所示,这个 AVComposition 有 AudioRenderLayer1、AudioRenderLayer2、AudioRenderLayer3 三个 AudioRenderLayer。转换过程包含以下步骤:
AudioRenderLayer 创建了一个 AVAudioMixInputParameters
AVAudioMixInputParameters 设置一个 MTAudioProcessingTap。MTAudioProcessingTap 用于实时处理音频,从 RenderLayer 的 AudioConfiguration 获取音频配置,实时计算当前时间点的音量。AVAudioMixInputParameters 设置 AVAudioTimePitchAlgorithm。AVAudioTimePitchAlgorithm 用于设置音高算法,从 RenderLayer 的 AudioConfiguration 获取音高算法配置。接着我们创建 AVAudioMix,并设置 AVAudioMixInputParameters 组。代码如下:
let audioMix = AVMutableAudioMix()audioMix.inputParameters = inputParameters复制代码
以上几个章节从大的维度介绍了框架的实现,对于 Metal 部分的介绍,后续会考虑再起一篇文章介绍。接下来的几个章节,介绍下框架的后续计划、开发框架过程逆向其他应用的一些分享以及推荐的学习资料。
笔者在开等。发框架过程中,逆向了国内外一众视。频编辑器。在比较各自的方案之后,选用了 AVFoundation 加 Metal 的方案作为框架核心。这里简要分享下逆向 Videoleap 的一些亮点:
makeTexture(descriptor:iosurface:plane:)
texImageIOSurface(_:target:internalFormat:width:height:format:type:plane:)