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:)
领取专属 10元无门槛券
私享最新 技术干货