這里的視頻錄制,錄制的視頻只是直接存放到文件中
畫大餅

使用 AVFoundation 可以說是挺有模塊化思想的:
- 創(chuàng)建一個視頻輸入(AVCaptureDevice + AVCaptureDeviceInput)
- 創(chuàng)建一個音頻輸入(AVCaptureDevice + AVCaptureDeviceInput)
- 創(chuàng)建一個音頻視頻的輸出(AVCaptureMovieFileOutput)
- 將視頻輸入和音頻輸入,音頻視頻輸出添加到一個叫 Session 的東西里面進(jìn)行統(tǒng)一的管理(AVCaptureSession)
因此,數(shù)據(jù)的流動方向?qū)⑹牵?/p>
(圖像, 聲音) -> AVCaptureDevice -> AVCaptureDeviceInput -> AVCaptureSession -> AVCaptureMovieFileOuput -> (文件)
配置視頻輸入
func setupCameraInput() throws -> AVCaptureDeviceInput {
let cameraDevice = AVCaptureDevice.default(for: .video)
do {
let cameraInput = try AVCaptureDeviceInput(device: cameraDevice!)
return cameraInput
} catch {
throw error
}
}
以上配置使用了視頻輸入的默認(rèn)設(shè)置
通過 AVCaptureDevice.default(for: .video) 獲得硬件的訪問對象,再通過這個對象創(chuàng)建一個軟件上的對象
配置音頻輸入
func setupMicInput() throws -> AVCaptureDeviceInput {
let micDevice = AVCaptureDevice.default(for: .audio)
do {
let audioInput = try AVCaptureDeviceInput(device: micDevice!)
return audioInput
} catch {
throw error
}
}
以上配置使用了音頻輸入的默認(rèn)設(shè)置
通過 AVCaptureDevice.default(for: .audio) 獲得硬件的訪問對象,再通過這個對象創(chuàng)建一個軟件上的對象
配置 Session
func setupCaptureSession() throws -> AVCaptureSession {
do {
let cameraInput = try setupCameraInput()
let micInput = try setupMicInput()
let captureSession = AVCaptureSession()
if captureSession.canAddInput(cameraInput) {
captureSession.addInput(cameraInput)
}
if captureSession.canAddInput(micInput) {
captureSession.addInput(micInput)
}
return captureSession
} catch {
throw error
}
}
以上過程,將視頻和音頻的輸入配置到了一個 Session 中
需要注意的是,對 session 的調(diào)用都是阻塞式的,因此,調(diào)用有關(guān) session API 的過程,應(yīng)該在一個后臺的串行隊列中進(jìn)行,即,以上的配置過程,應(yīng)該在一個后臺的串行隊列中進(jìn)行
配置輸出
由于配置視頻和音頻時,有可能會拋出異常,因此,這里會在當(dāng)視頻和音頻成功配置后,才會再進(jìn)行輸出的配置
self.fileOutput = AVCaptureMovieFileOutput()
if self.captureSession.canAddOutput(self.fileOutput!) {
self.captureSession.addOutput(self.fileOutput!)
}
配置視頻的文件輸出比較簡單,只需要創(chuàng)建一個對象,并添加到 session 中即可
寫一個總的配置過程
配置輸入
func prepareCapture(completion: @escaping (Bool, AVCaptureSession?, Error?) -> Void) {
serialQ.async { [unowned self] in
do {
let captureSession = try self.setupCaptureSession()
completion(true, captureSession, nil)
} catch {
completion(false, nil, error)
}
}
}
serialQ 是一個后臺的串行隊列,用來進(jìn)行有關(guān) session 的操作,避免阻塞
let serialQ = DispatchQueue(label: "com.wenyongyang.createCaptureSession")
completion 是一個異步回調(diào),在配置輸入完成后進(jìn)行回調(diào),無論成功或失敗
配置輸出和預(yù)覽界面
prepareCapture { [unowned self] (success, session, error) in
if success {
self.captureSession = session!
self.fileOutput = AVCaptureMovieFileOutput()
if self.captureSession.canAddOutput(self.fileOutput!) {
self.captureSession.addOutput(self.fileOutput!)
}
self.captureSession.startRunning()
let previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
DispatchQueue.main.async {
previewLayer.frame = self.view.bounds
self.view.layer.insertSublayer(previewLayer, below: self.btnStart.layer)
}
} else {
print("\(error!.localizedDescription)")
}
}
回調(diào)參數(shù) (success, session, error)
success 指示輸入配置是否成功,true 為成功,否則 false
session 為 optional,若輸入配置成功,則是一個 AVCaptureSession 對象,否則為 nil
error 為 optional,若輸入配置成功,則為 nil,否則會是配置輸入時拋出的異常
若輸入配置成功,將會接著配置輸出
并且,將會創(chuàng)建一個用于預(yù)覽的 layer
let previewLayer = AVCaptureVideoPreviewLayer(session: self.captureSession)
DispatchQueue.main.async {
previewLayer.frame = self.view.bounds
self.view.layer.insertSublayer(previewLayer, below: self.btnStart.layer)
}
self.captureSession.startRunning() 這一句,并不是真正的錄制開始,而只是開始捕捉畫面并顯示在預(yù)覽層上
開始錄制
serialQ.async { [unowned self] in
let saveURL = self.savePath()
if !self.captureSession.isRunning { self.captureSession.startRunning() }
self.fileOutput?.startRecording(to: saveURL, recordingDelegate: self)
}
要進(jìn)行真正的錄制,即將視頻存儲到了文件中,需要調(diào)用輸出對象(AVCaptureMovieFileOutput)的方法,同時還需要指定存儲的路徑
recordingDelegate 的協(xié)議是 AVCaptureFileOutputRecordingDelegate
在這個協(xié)議中,我們可以獲悉到錄制的開始與結(jié)束的時機(jī),如
/// 錄制開始
func fileOutput(_ output: AVCaptureFileOutput, didStartRecordingTo fileURL: URL, from connections: [AVCaptureConnection]) {
print("start")
}
/// 錄制結(jié)束
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
print("\(outputFileURL)")
print("finish")
}
停止錄制
serialQ.async { [unowned self] in
self.captureSession.stopRunning()
self.fileOutput?.stopRecording()
}
注意事項
- 攝像頭的隱私訪問描述字段 -> NSCameraUsageDescription
- 麥克風(fēng)的隱私訪問描述字段 -> NSMicrophoneUsageDescription
- 當(dāng)需要改變 session 的配置時,如改變錄制質(zhì)量,切換前后攝像頭等操作時,需要調(diào)用
lockForConfiguration()來獨占訪問權(quán)限,并使用unlockForConfiguration()進(jìn)行釋放權(quán)限 - 當(dāng)用戶未授權(quán)時,對于錄制視頻或音頻的嘗試,得到的將是黑色畫面和無聲