LearningAVFoundation之視頻合成+轉場過渡動畫

效果預覽

效果圖
(加載不出預覽的,地址: user-gold-cdn.xitu.io/2018/11/16/…)

如題所示,本文的目標是將5段獨立的小視頻合成一段完整的視頻,各視頻間穿插溶解消失、從右往左推的轉場過渡效果。git

總體脈絡

AVFoundation

涉及到的類在AVFoundation框架中的關係如圖所示,可知要達成開頭的目標,核心是要構建出兩個類,AVCompostion和AVVideoComposition。(這兩個類雖然從名字上看有某種關係,但事實上並不存在繼承或什麼關係)github

AVComposition是AVAsset的子類,從概念上能夠理解爲AVAsset是資源的宏觀總體描述,AVCompostion,組合,更偏向於微觀的概念。組合,顧名思義,能夠將幾段視頻、幾段音頻、字幕等等組合排列成可播放可導出的媒體資源。數組

AVVideoComposion,視頻組合,描述了終端該如何處理、顯示AVCompostion中的多個視頻軌道。畫面(AVCompostion) + 如何顯示(AVVideoCompostion) = 最終效果bash

實現細節

總流程

此處建議配合代碼食用,效果更佳。 demosession

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雙軌道的思路,以下圖所示。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之拍攝+實時濾鏡+實時寫入

相關文章
相關標籤/搜索