LearningAVFoundation之視訊合成+轉場過渡動畫
)
如題所示,本文的目標是將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) } }) } }) } 複製程式碼