AVFoundation 視頻經常使用套路: 視頻合成與導出,拍視頻手電筒,拍照閃光燈

拍照是手機的重要用途,有必要了解下拍照、視頻處理相關。html

拍視頻,把視頻文件導出到相冊

處理 AVFoundation,套路就是配置 session, 添加輸入輸出, 把視頻流的管道打通。 用 device 做爲輸入,獲取信息,用 session 做爲輸入輸出的橋樑,控制與調度,最後指定咱們想要的輸出類型。 拍視頻與拍照不一樣,會有聲音,輸入源就要加上麥克風了 AVCaptureDevice.default(for: .audio),視頻流的輸出就要用到 AVCaptureMovieFileOutput 類了。git

拍視頻的代碼以下:github

func captureMovie() {
        //  首先,作一個確認與切換。當前攝像頭不在拍攝中,就拍攝
        guard movieOutput.isRecording == false else {
            print("movieOutput.isRecording\n")
            stopRecording()
            return;
        }
        //  獲取視頻輸出的鏈接
        let connection = movieOutput.connection(with: .video)
        //    控制鏈接的方位,視頻的橫豎屏比例與手機的一致  
        //    點擊拍攝按鈕拍攝的這一刻,根據當前設備的方向來設置錄像的方向
        if (connection?.isVideoOrientationSupported)!{
            connection?.videoOrientation = currentVideoOrientation()
        }
        // 設置鏈接的視頻自動穩定,手機會選擇合適的拍攝格式和幀率
        if (connection?.isVideoStabilizationSupported)!{
            connection?.preferredVideoStabilizationMode = AVCaptureVideoStabilizationMode.auto
        }
        
        let device = activeInput.device
        //  由於須要攝像頭可以靈敏地聚焦
        if device.isSmoothAutoFocusSupported{
            do{
                try device.lockForConfiguration()
                device.isSmoothAutoFocusEnabled = false
                // 若是設置爲 true,   lens movements  鏡頭移動會慢一些
                device.unlockForConfiguration()
            }catch{
                print("Error setting configuration: \(String(describing: error.localizedDescription))")
            }
        }
        let output = URL.tempURL
        movieOutput.startRecording(to: output!, recordingDelegate: self)
    }
複製代碼

與拍照不一樣,錄像使用的是鏈接, movieOutput.connection(with: .video).bash

拍視頻,天然會有完成的時候,

AVCaptureFileOutputRecordingDelegate 類的代理方法裏面,保存視頻文件,更新 UI網絡

outputFileURL 參數, 是系統代理完成回調給開發者的,系統把視頻文件寫入 app 沙盒的資源定位符。要作的是把沙盒裏面的視頻文件,拷貝到系統相冊。session

func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
        if let error = error{
            print("Error, recording movie: \(String(describing: error.localizedDescription))")
        }
        else{
            // 保存到相冊, 具體代碼見 github repo
            saveMovieToLibrary(movieURL: outputFileURL)
            // 更改 UI
            captureButton.setImage(UIImage(named: "Capture_Butt"), for: .normal)
            //  中止計時器
            stopTimer()
        }
    }

複製代碼
拍視頻的時候,可以知道錄的怎麼樣了,比較好。

用計時器記錄,有一個 Label 展現app

func startTimer(){
        // 銷燬舊的
        if updateTimer != nil {
            updateTimer.invalidate()
        }
        //  開啓新的
        updateTimer = Timer(timeInterval: 0.5, target: self, selector: #selector(self.updateTimeDisplay), userInfo: nil, repeats: true)
        RunLoop.main.add(updateTimer, forMode: .commonModes)
    }
複製代碼

拍照環境較暗,就要亮燈了,都是調整 AVCaptureDevice 類裏的屬性。

拍照用閃光燈, 用 flashMode, 配置 AVCapturePhotoSettings。 每次拍照,都要新建 AVCapturePhotoSettings.AVCapturePhotoSettings 具備原子性 atomic.async

拍視頻用手電筒, 用 TorchMode, 配置的是 device.torchMode 直接修改 AVCaptureDevice 的屬性ide

蘋果設計的很好。輸出類型決定亮燈模式。 拍照用閃光燈,是按瞬間動做配置。 拍視頻,就是長亮了。oop

// MARK: Flash Modes (Still Photo), 閃光燈
    func setFlashMode(isCancelled: Bool = false) {
        let device = activeInput.device
        // 閃光燈, 只有後置攝像頭有。 前置攝像頭是,增長屏幕亮度
        if device.isFlashAvailable{

            //  這段代碼, 就是控制閃光燈的 off, auto , on 三種狀態, 來回切換
            var currentMode = currentFlashOrTorchMode().mode
            currentMode += 1
            if currentMode > 2 || isCancelled == true{
                currentMode = 0
            }

            let new_mode = AVCaptureDevice.FlashMode(rawValue: currentMode)
            self.outputSetting.flashMode = new_mode!;
            flashLabel.text = currentFlashOrTorchMode().name
        }
    }

// MARK: Torch Modes (Video), 手電筒
    
    func setTorchMode(isCancelled: Bool = false) {
        let device = activeInput.device
        if device.hasTorch{

          //  這段代碼, 就是控制手電筒的 off, auto , on 三種狀態, 來回切換
            var currentMode = currentFlashOrTorchMode().mode
            currentMode += 1
            if currentMode > 2 || isCancelled == true{
                currentMode = 0
            }

            let new_mode = AVCaptureDevice.TorchMode(rawValue: currentMode)
            if device.isTorchModeSupported(new_mode!){
                do{
                    // 與前面操做相似,須要 lock 一下
                    try device.lockForConfiguration()
                    device.torchMode = new_mode!
                    device.unlockForConfiguration()
                    flashLabel.text = currentFlashOrTorchMode().name
                    
                }catch{
                    print("Error setting flash mode: \(String(describing: error.localizedDescription))")
                }
                
            }
            
        }
    }
複製代碼

視頻合成,將多個音頻、視頻片斷合成爲一個視頻文件。給視頻增長背景音樂

AVComposition

合成視頻, 操做的就是視頻資源, AVAsset .

AVAsset 的有一個子類 AVComposition . 通常經過 AVComposition 的子類 AVMutableComposition 合成視頻。

AVComposition 能夠把多個資源媒體文件,在時間上自由安排,合成想要的視頻。 具體的就是藉助一組音視頻軌跡 AVMutableCompositionTrack。

AVCompositionTrack 包含一組軌跡的片斷。AVCompositionTrack 的子類 AVMutableCompositionTrack,能夠增刪他的軌跡片斷,也能夠調整軌跡的時間比例。

拿 AVMutableCompositionTrack 添加視頻資源 AVAsset, 做爲軌跡的片斷。

用 AVPlayer 的實例預覽合成的視頻資源 AVCompositions, 用 AVAssetExportSession 導出合成的文件。

預覽合成的視頻

套路就是拿資源的 URL 建立 AVAsset。 拍的視頻 AVAsset 包含音頻信息(背景音,說話的聲音, 單純的噪音)和視頻信息。

用 AVComposition 的子類 AVMutableComposition,添加音軌 composition.addMutableTrack(withMediaType: .audio 和視頻軌跡 composition.addMutableTrack(withMediaType: .video

var previewURL: URL?
    // 記錄直接合成的文件地址

    @IBAction func previewComposition(_ sender: UIButton) {
        // 首先要合成,
        //  要合成,就得有資源, 並確保當前沒有進行合成的任務
        guard videoURLs.count > 0 , activityIndicator.isAnimating == false else{
            return
        }
        // 最後就很簡單了, 拿資源播放
        var player: AVPlayer!
        defer {
            let playerViewController = AVPlayerViewController()
            playerViewController.allowsPictureInPicturePlayback = true
            playerViewController.player = player
            present(playerViewController, animated: true) {
                playerViewController.player!.play()
            }
        }
        
        guard previewURL == nil else {
            player = AVPlayer(url: previewURL!)
            return
        }
        //  以前, 沒合成寫入文件, 就合成預覽
        var videoAssets = [AVAsset]() 
        //  有了 視頻資源的 URL,  AVMutableComposition 使用的是  AVAsset
        //  拿視頻資源的 URL , 逐個建立 AVAsset
        for urlOne in videoURLs{
            let av_asset = AVAsset(url: urlOne)
            videoAssets.append(av_asset)
        }
        // 用 AVComposition 的子類 AVMutableComposition, 來修改合成的軌跡
        let composition = AVMutableComposition()
        //  建立兩條軌跡, 音軌軌跡和視頻軌跡
        let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
        let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)

        var startTime = kCMTimeZero
       // 遍歷剛纔建立的 AVAsset, 放入 AVComposition 添加的音軌和視頻軌跡中
        for asset in videoAssets{
            do{
               // 插入視頻軌跡
                try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .video)[0], at: startTime)
            }catch{
                print("插入合成視頻軌跡, 視頻有錯誤")
            }
            do{
               // 插入音軌, 
                try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .audio)[0], at: startTime)
            }catch{
                print("插入合成視頻軌跡, 音頻有錯誤")
            }
            //  讓媒體文件一個接一個播放,更新音軌和視頻軌跡中的開始時間
            startTime = CMTimeAdd(startTime, asset.duration)
        }
        let playItem = AVPlayerItem(asset: composition)
        player = AVPlayer(playerItem: playItem)
    }
複製代碼

合成視頻中,更加精細的控制, 經過 AVMutableVideoCompositionLayerInstruction

AVMutableVideoCompositionLayerInstruction 這個類, 能夠調整合成軌跡的變形(平移和縮放)、裁剪和透明度等屬性。

設置 AVMutableVideoCompositionLayerInstruction 通常須要兩個參數,

AVMutableVideoCompositionLayerInstruction 經過軌跡來建立 let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track).

經過資源文件 AVAsset 的信息配置。

通常拍照的屏幕是 375X667 , 相對視頻的文件的長度比較小。視頻的文件寬度高度 1280.0 X 720.0, 遠超手機屏幕 。須要作一個縮小
func videoCompositionInstructionForTrack(track: AVCompositionTrack, asset: AVAsset) -> AVMutableVideoCompositionLayerInstruction{
        let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
        let assetTrack = asset.tracks(withMediaType: .video)[0]
        // 經過視頻文件 asset 的 preferredTransform 屬性,瞭解視頻是豎着的,仍是橫着的,區分處理
        let transfrom = assetTrack.preferredTransform
        //  orientationFromTransform() 方法,見 github repo 
        let assetInfo = transfrom.orientationFromTransform()
        //  爲了屏幕可以呈現高清的橫向視頻
        var scaleToFitRatio = HDVideoSize.width / assetTrack.naturalSize.width
 
        if assetInfo.isPortrait  {
            // 豎向
            scaleToFitRatio = HDVideoSize.height / assetTrack.naturalSize.width
            let scaleFactor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
            let concatTranform = assetTrack.preferredTransform.concatenating(scaleFactor)
            instruction.setTransform(concatTranform, at: kCMTimeZero)
        }
        else{
            //  橫向
            let scale_factor = CGAffineTransform(scaleX: scaleToFitRatio, y: scaleToFitRatio)
            let scale_factor_two = CGAffineTransform(rotationAngle: .pi/2.0)
            let concat_transform = assetTrack.preferredTransform.concatenating(scale_factor).concatenating(scale_factor_two)
            instruction.setTransform(concat_transform, at: kCMTimeZero)
        }
        // 將處理好的 AVMutableVideoCompositionLayerInstruction 返回
        return instruction
    }
複製代碼
視頻合成,並導出到相冊。 這是一個耗時操做

導出的套路是拿 AVMutableComposition, 建立 AVAssetExportSession, 用 AVAssetExportSession 對象的 exportAsynchronously 方法導出。 直接寫入到相冊,對應的 URL 是 session.outputURL

//  視頻合成,並導出到相冊。 這是一個耗時操做
    private func mergeAndExportVideo(){
        activityIndicator.isHidden = false
        //  亮一朵菊花, 給用戶反饋
        activityIndicator.startAnimating()
        
        //  把記錄的 previewURL 置爲 nil
        //  視頻合成, 導出成功, 就賦新值
        previewURL = nil
        
        // 先建立資源 AVAsset
        var videoAssets = [AVAsset]()
        for url_piece in videoURLs{
            let av_asset = AVAsset(url: url_piece)
            videoAssets.append(av_asset)
        }
        // 建立合成的 AVMutableComposition 對象
        let composition = AVMutableComposition()
        //  建立 AVMutableComposition 對象的音軌
        let audioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
        
        // 經過 AVMutableVideoCompositionInstruction ,調整合成軌跡的比例、位置、裁剪和透明度等屬性。
        // AVMutableVideoCompositionInstruction 對象, 控制一組 layer 對象 AVMutableVideoCompositionLayerInstruction
        let mainInstruction = AVMutableVideoCompositionInstruction()
        var startTime = kCMTimeZero
        // 遍歷每個視頻資源,添加到 AVMutableComposition 的音軌和視頻軌跡
        for asset in videoAssets{
            //  由於 AVMutableVideoCompositionLayerInstruction 對象適用於整個視頻軌跡,
            //  因此這裏一個資源,對應一個軌跡
            let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid))
            do{
                try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .video)[0], at: startTime)
            }catch{
                print("Error creating Video track.")
            }
            
            // 有背景音樂,就不添加視頻自帶的聲音了
            if musicAsset == nil {
                // 插入音頻
                do{
                    try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, asset.duration), of: asset.tracks(withMediaType: .audio)[0], at: startTime)
                }
                catch{
                    print("Error creating Audio track.")
                }
                
            }   
            // 添加了資源,就建立配置文件  AVMutableVideoCompositionLayerInstruction
            let instruction = videoCompositionInstructionForTrack(track: videoTrack!, asset: asset)
            instruction.setOpacity(1.0, at: startTime)
            if asset != videoAssets.last{
                instruction.setOpacity(0.0, at: CMTimeAdd(startTime, asset.duration))
                //  視頻片斷之間, 都添加了過渡, 避免片斷之間的干涉
            }
            mainInstruction.layerInstructions.append(instruction)
            // 這樣, mainInstruction 就添加好了
            startTime = CMTimeAdd(startTime, asset.duration)
        }
        let totalDuration = startTime
        // 有背景音樂,給合成資源插入音軌
        if musicAsset != nil {
            do{
                try audioTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, totalDuration), of: musicAsset!.tracks(withMediaType: .audio)[0], at: kCMTimeZero)
            }
            catch{
                print("Error creating soundtrack total.")
            }
        }

        // 設置 mainInstruction 的時間範圍
        mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, totalDuration)

        //  AVMutableVideoComposition 沿着時間線,設置視頻軌跡如何合成
        //  AVMutableVideoComposition 配置了大小、持續時間,合成視頻幀的渲染間隔, 渲染尺寸

        let videoComposition = AVMutableVideoComposition()
        videoComposition.instructions = [mainInstruction]
        videoComposition.frameDuration = CMTimeMake(1, 30)
        videoComposition.renderSize = HDVideoSize
        videoComposition.renderScale = 1.0
        
        //  拿 composition ,建立 AVAssetExportSession
        let exporter: AVAssetExportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)!
        // 配置輸出的 url
        exporter.outputURL = uniqueURL
        // 設定輸出格式, quick time movie file
        exporter.outputFileType = .mov
        //  優化網絡播放
        exporter.shouldOptimizeForNetworkUse = true
        exporter.videoComposition = videoComposition
        // 開啓輸出會話
        exporter.exportAsynchronously {
            DispatchQueue.main.async {
                self.exportDidFinish_deng(session: exporter)
            }
        }
    }

複製代碼

所有代碼見: github.com/BoxDengJZ/A…

More:


最後是,關於給視頻添加圖形覆蓋和動畫。

相關: 拍照聚焦和曝光,AVFoundation 簡明教程


推薦資源:

WWDC 2016: Advances in iOS Photography

AVFoundation Programming Guide 蘋果文檔

視頻教程

相關文章
相關標籤/搜索