效果预览
(加载不出预览的,地址: user-gold-cdn.xitu.io/2018/11/16/…)如题所示,本文的目标是将5段独立的小视频合成一段完整的视频,各视频间穿插溶解消失、从右往左推的转场过渡效果。
整体脉络
涉及到的类在AVFoundation框架中的关系如图所示,可知要达成开头的目标,核心是要构建出两个类,AVCompostion和AVVideoComposition。(这两个类虽然从名字上看有某种关系,但事实上并不存在继承或什么关系)
AVComposition是AVAsset的子类,从概念上可以理解为AVAsset是资源的宏观整体描述,AVCompostion,组合,更偏向于微观的概念。组合,顾名思义,可以将几段视频、几段音频、字幕等等组合排列成可播放可导出的媒体资源。
AVVideoComposion,视频组合,描述了终端该如何处理、显示AVCompostion中的多个视频轨道。画面(AVCompostion) + 如果显示(AVVideoCompostion) = 最终效果
实现细节
总流程
此处建议配合代码食用,效果更佳。 demo
override func viewDidLoad() {
super.viewDidLoad()
prepareResource()
buildCompositionVideoTracks()
buildCompositionAudioTracks()
buildVideoComposition()
export()
}
复制代码
可以看到代码思路和上一节是一致的:
- 从Bundle中读取视频片段;
- 创建空白的视频片段composition,提取资源视频片段中的视频轨道和音频轨道,插入到compostion中的相应轨道中;
- 构建视频组合描述对象,实现转场动画效果;
- 合成导出为新的视频片段;
创建视频轨道
func buildCompositionVideoTracks() {
//使用invalid,系统会自动分配一个有效的trackId
let trackId = kCMPersistentTrackID_Invalid
//创建AB两条视频轨道,视频片段交叉插入到轨道中,通过对两条轨道的叠加编辑各种效果。如0-5秒内,A轨道内容alpha逐渐到0,B轨道内容alpha逐渐到1
guard let trackA = composition.addMutableTrack(withMediaType: .video, preferredTrackID: trackId) else {
return
}
guard let trackB = composition.addMutableTrack(withMediaType: .video, preferredTrackID: trackId) else {
return
}
let videoTracks = [trackA,trackB]
//视频片段插入时间轴时的起始点
var cursorTime = CMTime.zero
//转场动画时间
let transitionDuration = CMTime(value: 2, timescale: 1)
for (index,value) in videos.enumerated() {
//交叉循环A,B轨道
let trackIndex = index % 2
let currentTrack = videoTracks[trackIndex]
//获取视频资源中的视频轨道
guard let assetTrack = value.tracks(withMediaType: .video).first else {
continue
}
do {
//插入提取的视频轨道到 空白(编辑)轨道的指定位置中
try currentTrack.insertTimeRange(CMTimeRange(start: .zero, duration: value.duration), of: assetTrack, at: cursorTime)
//光标移动到视频末尾处,以便插入下一段视频
cursorTime = CMTimeAdd(cursorTime, value.duration)
//光标回退转场动画时长的距离,这一段前后视频重叠部分组合成转场动画
cursorTime = CMTimeSubtract(cursorTime, transitionDuration)
} catch {
}
}
}
复制代码
具体代码含义都有对应注释,需要解释的是A、B双轨道的思路,如下图所示。
AVVideoCompostion对象的layerInstruction数组属性,会按排列顺序显示对应轨道的画面。我们可以通过自定义共存区的显示逻辑来塑造出不同的转场效果。以1,2共存区为例,在duration内,1画面alpha逐渐到0,2画面alpha逐渐到1,就会有溶解的效果;
因此,为了实现转场效果,在构造视频轨道时,就采用了AB交叉的思路。如果只是单纯的视频拼接,完全可以放到同一条视频轨道中。
buildCompositionAudioTracks() 和构造视频轨道思路一致,不再赘述。
筛选多片段共存区域
/// 设置videoComposition来描述A、B轨道该如何显示
func buildVideoComposition() {
//创建默认配置的videoComposition
let videoComposition = AVMutableVideoComposition.init(propertiesOf: composition)
self.videoComposition = videoComposition
filterTransitionInstructions(of: videoComposition)
}
/// 过滤出转场动画指令
func filterTransitionInstructions(of videoCompostion: AVMutableVideoComposition) -> Void {
let instructions = videoCompostion.instructions as! [AVMutableVideoCompositionInstruction]
for (index,instruct) in instructions.enumerated() {
//非转场动画区域只有单轨道(另一个的空的),只有两个轨道重叠的情况是我们要处理的转场区域
guard instruct.layerInstructions.count > 1 else {
continue
}
var transitionType: TransitionType
//需要判断转场动画是从A轨道到B轨道,还是B-A
var fromLayerInstruction: AVMutableVideoCompositionLayerInstruction
var toLayerInstruction: AVMutableVideoCompositionLayerInstruction
//获取前一段画面的轨道id
let beforeTrackId = instructions[index - 1].layerInstructions[0].trackID;
//跟前一段画面同一轨道的为转场起点,另一轨道为终点
let tempTrackId = instruct.layerInstructions[0].trackID
if beforeTrackId == tempTrackId {
fromLayerInstruction = instruct.layerInstructions[0] as! AVMutableVideoCompositionLayerInstruction
toLayerInstruction = instruct.layerInstructions[1] as! AVMutableVideoCompositionLayerInstruction
transitionType = TransitionType.Dissolve
}else{
fromLayerInstruction = instruct.layerInstructions[1] as! AVMutableVideoCompositionLayerInstruction
toLayerInstruction = instruct.layerInstructions[0] as! AVMutableVideoCompositionLayerInstruction
transitionType = TransitionType.Push
}
setupTransition(for: instruct, fromLayer: fromLayerInstruction, toLayer: toLayerInstruction,type: transitionType)
}
}
复制代码
这段代码通过已经构建好音视频轨道的composition对象来初始化对应的VideoCompostion描述对象,再从中筛选出我们关心的描述重叠区域的指令,通过修改指令来达到自定义显示效果的目标。
添加转场效果
func setupTransition(for instruction: AVMutableVideoCompositionInstruction, fromLayer: AVMutableVideoCompositionLayerInstruction, toLayer: AVMutableVideoCompositionLayerInstruction ,type: TransitionType) {
let identityTransform = CGAffineTransform.identity
let timeRange = instruction.timeRange
let videoWidth = self.videoComposition.renderSize.width
if type == TransitionType.Push{
let fromEndTranform = CGAffineTransform(translationX: -videoWidth, y: 0)
let toStartTranform = CGAffineTransform(translationX: videoWidth, y: 0)
fromLayer.setTransformRamp(fromStart: identityTransform, toEnd: fromEndTranform, timeRange: timeRange)
toLayer.setTransformRamp(fromStart: toStartTranform, toEnd: identityTransform, timeRange: timeRange)
}else {
fromLayer.setOpacityRamp(fromStartOpacity: 1.0, toEndOpacity: 0.0, timeRange: timeRange)
}
//重新赋值
instruction.layerInstructions = [fromLayer,toLayer]
}
复制代码
在这里我们可以看到,经过AVFoundation的抽象,我们描述视频画面的动画和平时构建UIView动画的思路是一致,据此可以构建出各式各样的转场动画效果。
合成导出
func export(){
guard let session = AVAssetExportSession.init(asset: composition.copy() as! AVAsset, presetName: AVAssetExportPreset640x480) else {
return
}
session.videoComposition = videoComposition
session.outputURL = CompositionViewController.createTemplateFileURL()
session.outputFileType = AVFileType.mp4
session.exportAsynchronously(completionHandler: {[weak self] in
guard let strongSelf = self else {return}
let status = session.status
if status == AVAssetExportSession.Status.completed {
strongSelf.saveToAlbum(atURL: session.outputURL!, complete: { (success) in
DispatchQueue.main.async {
strongSelf.showSaveResult(isSuccess: success)
}
})
}
})
}
复制代码
尾声
至此核心思路介绍完毕,更过细节见demo.
下一篇文章将会分析基于Timeline理念封装的视频编辑框架Cabbage。
附上一篇文章地址:LearningAVFoundation之拍摄+实时滤镜+实时写入