AVFoundation 視訊常用套路: 視訊合成與匯出,拍視訊手電筒,拍照閃光燈
拍照是手機的重要用途,有必要了解下拍照、視訊處理相關。
拍視訊,把視訊檔案匯出到相簿
處理 AVFoundation,套路就是配置 session, 新增輸入輸出, 把視訊流的管道打通。 用 device 作為輸入,獲取資訊,用 session 作為輸入輸出的橋樑,控制與排程,最後指定我們想要的輸出型別。 拍視訊與拍照不同,會有聲音,輸入源就要加上麥克風了 AVCaptureDevice.default(for: .audio)
,視訊流的輸出就要用到 AVCaptureMovieFileOutput 類了。
拍視訊的程式碼如下:
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
類的代理方法裡面,儲存視訊檔案,更新 UI
outputFileURL 引數, 是系統代理完成回撥給開發者的,系統把視訊檔案寫入 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 展示
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.
拍視訊用手電筒, 用 TorchMode, 配置的是 device.torchMode 直接修改 AVCaptureDevice 的屬性
蘋果設計的很好。輸出型別決定亮燈模式。 拍照用閃光燈,是按瞬間動作配置。 拍視訊,就是長亮了。
// 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 一般需要兩個引數,
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:
最後是,關於給視訊新增圖形覆蓋和動畫。
推薦資源:
WWDC 2016: Advances in iOS Photography