拍視頻,把視頻文件導出到相冊
-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)
.網絡
在 AVCaptureFileOutputRecordingDelegate
類的代理方法裏面,保存視頻文件,更新 UIsession
outputFileURL 參數, 是系統代理完成回調給開發者的,系統把視頻文件寫入 app 沙盒的資源定位符。要作的是把沙盒裏面的視頻文件,拷貝到系統相冊。app
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 展現async
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) }
拍照用閃光燈, 用 flashMode, 配置 AVCapturePhotoSettings。
每次拍照,都要新建 AVCapturePhotoSettings.AVCapturePhotoSettings 具備原子性 atomic.ide
拍視頻用手電筒, 用 TorchMode, 配置的是 device.torchMode
直接修改 AVCaptureDevice 的屬性oop
蘋果設計的很好。輸出類型決定亮燈模式。
拍照用閃光燈,是按瞬間動做配置。
拍視頻,就是長亮了。post
// 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))") } } } }
視頻合成,將多個音頻、視頻片斷合成爲一個視頻文件。給視頻增長背景音樂
-
合成視頻, 操做的就是視頻資源, 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 經過軌跡來建立let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
.
經過資源文件 AVAsset 的信息配置。
視頻的文件寬度高度 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) } } }
最後是,關於給視頻添加圖形覆蓋和動畫。
=
推薦資源:
WWDC 2016: Advances in iOS Photography