如題所示,本文的目標是將5段獨立的小視頻合成一段完整的視頻,各視頻間穿插溶解消失、從右往左推的轉場過渡效果。git
涉及到的類在AVFoundation框架中的關係如圖所示,可知要達成開頭的目標,核心是要構建出兩個類,AVCompostion和AVVideoComposition。(這兩個類雖然從名字上看有某種關係,但事實上並不存在繼承或什麼關係)github
AVComposition是AVAsset的子類,從概念上能夠理解爲AVAsset是資源的宏觀總體描述,AVCompostion,組合,更偏向於微觀的概念。組合,顧名思義,能夠將幾段視頻、幾段音頻、字幕等等組合排列成可播放可導出的媒體資源。數組
AVVideoComposion,視頻組合,描述了終端該如何處理、顯示AVCompostion中的多個視頻軌道。畫面(AVCompostion) + 如何顯示(AVVideoCompostion) = 最終效果bash
此處建議配合代碼食用,效果更佳。 demosession
override func viewDidLoad() {
super.viewDidLoad()
prepareResource()
buildCompositionVideoTracks()
buildCompositionAudioTracks()
buildVideoComposition()
export()
}
複製代碼
能夠看到代碼思路和上一節是一致的:框架
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雙軌道的思路,以下圖所示。async
AVVideoCompostion對象的layerInstruction數組屬性,會按排列順序顯示對應軌道的畫面。咱們能夠經過自定義共存區的顯示邏輯來塑造出不一樣的轉場效果。以1,2共存區爲例,在duration內,1畫面alpha逐漸到0,2畫面alpha逐漸到1,就會有溶解的效果;ide
所以,爲了實現轉場效果,在構造視頻軌道時,就採用了AB交叉的思路。若是隻是單純的視頻拼接,徹底能夠放到同一條視頻軌道中。 post
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之拍攝+實時濾鏡+實時寫入