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

鄧輕舟發表於2018-10-13

拍照流程圖

直接看拍照聚焦和曝光

上手就用 AVKit 搞音視訊。靈活的控制,就要用到 AVFoundation


要點:

  • 使用資源(一般指照片庫裡面的視訊,圖片,live photo),
  • 播放,
  • 編輯,
  • 捕捉(拍照和錄視訊)
  • 匯出資源(把處理編輯過的資源,拍的照片,編輯的視訊,匯出到相簿)

AVFoundation , 視訊的載入與匯出,大量使用非同步。 簡單的發訊息, 肯定是不行的。阻塞當前執行緒, 造成卡頓。 AVFoundation 就是為了充分利用64位的硬體和多執行緒設計的。


首先是播放,

播放本地的視訊檔案, 和遠端的視訊與流媒體。

本地檔案,單個播放

先講 AVKit 裡面的 AVPlayerViewController. AVPlayerViewController 是 ViewController 的子類,

AVPlayerViewController

AVPlayerViewController 在 TV OS 上,非常強大。(本文僅介紹 iOS 平臺下)

蘋果自帶的 AVPlayerViewController 裡面有很多播放的控制元件。 回播中,就是播放本地檔案中,可以播放、暫停、快進、快退,調整視訊的長寬比例( 即畫面在螢幕中適中,或者鋪滿螢幕)。

播放視訊,蘋果設計的很簡單,程式碼如下:

    //  拿一個 url , 建立一個 AVPlayer 例項
    let player = AVPlayer(url: "你的 url")
    //  再建立一個 AVPlayerViewController 例項
    let playerViewController = AVPlayerViewController()
    playerViewController.player = queuePlayer
    present(playerViewController, animated: true) {
        playerViewController.player!.play()
    }// 這裡有一個閉包, 介面出現了,再播放。
複製程式碼

本地檔案,多個連續播放

連著放,使用 AVQueuePlayer,把多個視訊放在一個視訊佇列中,依次連續播放 AVQueuePlayer 是 AVPlayer 的子類。 按順序,播放多個資源。

AVQueuePlayer

AVPlayerItem 包含很多視訊資源資訊,除了資源定位 URI , 還有軌跡資訊,視訊的持續時長等。

蘋果文件上說, AVPlayerItem 用於管理播放器播放的資源的計時和呈現狀態。他有一個 AVAsset 播放資源的屬性。

   var queue = [AVPlayerItem]()   
   let videoClip = AVPlayerItem(url: url)
   queue.append(videoClip)
    //   queue 佇列可以繼續新增 AVPlayerItem 例項
    let queuePlayer = AVQueuePlayer(items: queue)

    let playerViewController = AVPlayerViewController()
    playerViewController.player = queuePlayer
    
    present(playerViewController, animated: true) {
        playerViewController.player!.play()
    }
複製程式碼

iPad 中的畫中畫功能

iPad 中的畫中畫功能,通過給 AVAudioSession 支援後臺音效, 在 AppdelegatedidFinishLaunchingWithOptions 中新增下面的這段程式碼,使用後臺模式, 首先在Xcode 的 target 的 Capability 中勾選相關的後臺功能。

    let session = AVAudioSession.sharedInstance()
    do {
        try session.setCategory(AVAudioSessionCategoryPlayback)
        try session.setActive(true)
    } catch let error {
        print("AVFoundation configuration error: \(error.localizedDescription) \n\n AV 配置 有問題")
    }
    // 很有必要這樣,因為畫中畫的視訊功能,apple 是當後臺任務處理的。
複製程式碼

流媒體播放和網路視訊播放

本地的資源路徑 URL ,替換為網路的 URL, 就可以了。

優化,播放完成後,退出播放介面

   override func viewDidLoad() {
        super.viewDidLoad()
        // 新增播放完成的監聽
        NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
    }

  //  執行退出的介面控制
   @objc func playerItemDidReachEnd(){
        self.presentedViewController?.dismiss(animated: true, completion: {})
    }
複製程式碼

接著來, 拍照, 設定捕捉的 session ,並實時預覽。

設定前後攝像頭,聚焦與曝光,拍照(靜態圖片)

攝像用到的核心類是 AVCaptureSession ,應用和 iOS 建立一個視訊流的會話。 AVCaptureSession 作為排程中心, 控制裝置的輸入/輸出流, 具體就是相機和麥克風。

AVCaptureDeviceInput 類是視訊流的輸入源,預覽介面呈現的就是他的資料,匯出的視訊檔案也是他負責的。 視訊流 session 物件生成後,可以重新配置。這就可以動態修改視訊流 session 的配置資訊。視訊流 session 的輸入輸出的路由,也可以動態改。例如,只需要一個 session. 可以通過 AVCapturePhotoOutput 匯出照片,可以匯出視訊檔案 AVCaptureMovieFileOutput.

開啟視訊會話

captureSession.startRunning() 之前,先要新增輸入 AVCaptureDeviceInput 和輸出 AVCapturePhotoOutput/AVCaptureMovieFileOutput,準備預覽介面 AVCaptureVideoPreviewLayer

// 有一個 captureSession 物件
let captureSession = AVCaptureSession()
// 兩個輸出,輸出照片, 和輸出視訊
let imageOutput = AVCapturePhotoOutput()
let movieOutput = AVCaptureMovieFileOutput()

func setupSession() -> Bool{
        captureSession.sessionPreset = AVCaptureSession.Preset.high
        // 首先設定 session 的解析度 。sessionPreset 屬性,設定了輸出的視訊的質量   
        let camera = AVCaptureDevice.default(for: .video)
        // 預設的相機是 back-facing camera 朝前方拍攝, 不是自拍的。

        do {
            let input = try AVCaptureDeviceInput(device: camera!)
            if captureSession.canAddInput(input){
                captureSession.addInput(input)
                activeInput = input
                // 新增拍照, 錄影的輸入
            }
        } catch {
            print("Error settings device input: \(error)")
            return false
        }
        
        // 設定麥克風
        let microphone = AVCaptureDevice.default(for: .audio)
        do{
            let micInput = try AVCaptureDeviceInput(device: microphone!)
            if captureSession.canAddInput(micInput){
                captureSession.addInput(micInput)
                //   新增麥克風的輸入
            }
        }catch{
            print("Error setting device audio input: \(String(describing: error.localizedDescription))")
            fatalError("Mic")
        }
        
        //  新增兩個輸出,輸出照片, 和輸出視訊
        if captureSession.canAddOutput(imageOutput){
            captureSession.addOutput(imageOutput)
        }
        if captureSession.canAddOutput(movieOutput){
            captureSession.addOutput(movieOutput)
        }
        return true
    }
複製程式碼
設定視訊會話的預覽介面

AVCaptureVideoPreviewLayer 是 CALayer 的子類,用於展示相機拍的介面。

    func setupPreview() {
        // 配置預覽介面 previewLayer
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        // previewLayeris 通過 captureSession 初始化  
        // 再設定相關屬性, 尺寸和視訊播放時的拉伸方式 videoGravity
        previewLayer.frame = camPreview.bounds
        previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
        camPreview.layer.addSublayer(previewLayer)
       //  camPreview 是一個 UIView ,鋪在 self.view 上面
}
複製程式碼
拍, startSession

啟動視訊流的方法,啟動了,就不用管。沒啟動,就處理。 啟動視訊流是耗時操作,為不阻塞主執行緒,一般用系統預設執行緒佇列作非同步。

let videoQueue = DispatchQueue.global(qos: .default)

func startSession(){
        if !captureSession.isRunning{
            videoQueue.async {
                self.captureSession.startRunning()
            }
        }
    }
複製程式碼

拍照片,下面的程式碼是拍攝靜態照片 JPEG,不是 Live Photo.

var outputSetting = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
    //  靜態圖的配置

   func capturePhoto() {
        guard PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized else{
            PHPhotoLibrary.requestAuthorization(requestAuthorizationHander)
            return
        }
        let settings = AVCapturePhotoSettings(from: outputSetting)
        imageOutput.capturePhoto(with: settings, delegate: self)
    //  imageOutput 輸出流裡面的取樣緩衝中,捕獲出靜態圖
    }

extension ViewController: AVCapturePhotoCaptureDelegate{
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
//  如果視訊流的取樣緩衝裡面有資料,就拆包
        if let imageData = photo.fileDataRepresentation(){
            let image = UIImage(data: imageData)
            let photoBomb = image?.penguinPhotoBomb(image: image!)
            self.savePhotoToLibrary(image: photoBomb!)
            //  最後,合成照片儲存到系統相簿
            //  這裡有一個照片合成,具體見下面的 Github Repo.
        }
        else{
            print("Error capturing photo: \(String(describing: error?.localizedDescription))")
        }
    }
}
複製程式碼

到自拍了,就是支援前置攝像頭,front-facing camera.

首先,要確認手機要有多個攝像頭。有多個,才可以切換攝像頭輸入。 具體套路就是開始配置,修改,與提交修改。

captureSession.beginConfiguration() ,接著寫修改,直到 captureSession.commitConfiguration() 提交了,才生效。

類似的還有 UIView 渲染機制。 CATransaction, 開始,設定,提交,就可以在螢幕上看到重新整理的介面了。

    // 配置拍攝前面(自拍),拍後面
    @IBAction func switchCameras(_ sender: UIButton) {
        guard movieOutput.isRecording == false else{
            return
        }
  //  確認手機要有多個攝像頭
        guard let frontCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front), let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else{
            return;
        }
        // 建立新的 AVCaptureDeviceInput ,來切換。更新 captureSession 的配置。
        do{
            var input: AVCaptureDeviceInput?
            //  通過識別當前的攝像頭,找出另一個(我們需要的)
            if activeInput.device == frontCamera{
                input = try AVCaptureDeviceInput(device: backCamera)
            }
            else{
                input = try AVCaptureDeviceInput(device: frontCamera)
            }
           // 得到了新的輸入源,就可以開始配置了
            captureSession.beginConfiguration()
            // 去掉舊的輸入源,即不讓當前的攝像頭輸入
            captureSession.removeInput(activeInput)
            // 增加新的輸入源,即讓其他的攝像頭輸入
            if captureSession.canAddInput(input!){
                captureSession.addInput(input!)
                activeInput = input
            }
            // captureSession.beginConfiguration() 之後,就開始修改,直到下一句提交了,才生效。
            captureSession.commitConfiguration()
        }catch{
            print("Error , switching cameras: \(String(describing: error))")
        }

    }

複製程式碼

聚焦功能 POI : 點選螢幕,拍照聚焦到興趣點

具體實現是把螢幕 UI 座標,也就是預覽圖層的座標,轉換到相機的座標系中, 再用預覽圖層的座標點,設定聚焦的 point 和 mode 。

配置聚焦,屬於使用者輸入,並要用到手機的攝像頭硬體。配置 POI 的時候,可能有干擾 ( 比如後臺程式的影響 ),這樣就要用裝置鎖定了。 device.lockForConfiguration()

注意: 自拍是不可以聚焦的。前置攝像頭,沒有 POI 功能。

    @objc
    func tapToFocus(recognizer: UIGestureRecognizer){
        if activeInput.device.isFocusPointOfInterestSupported{
            // 得到螢幕中點選的座標,轉化為預覽圖層裡的座標點
            let point = recognizer.location(in: camPreview)
            //  將預覽圖層中的座標點,轉換到相機的座標系中
            let pointOfInterest = previewLayer.captureDevicePointConverted(fromLayerPoint: point)
            //  自由設定相關 UI
            showMarkerAtPoint(point: point, marker: focusMarker)
            focusAtPoint(pointOfInterest)
        }
    }
    
    //   用預覽圖層的座標點,配置聚焦。
    func focusAtPoint(_ point: CGPoint){
        let device = activeInput.device
      // 首先判斷手機能不能聚焦
        if device.isFocusPointOfInterestSupported , device.isFocusModeSupported(.autoFocus){
            do{
                // 鎖定裝置來配置
                try device.lockForConfiguration()
                device.focusPointOfInterest = point
                device.focusMode = .autoFocus
                device.unlockForConfiguration()
                // 配置完成,解除鎖定
            }
            catch{
                print("Error focusing on POI: \(String(describing: error.localizedDescription))")
            }
        }
    }
複製程式碼

拍照曝光功能,雙擊設定曝光座標

類似聚焦,具體實現是把螢幕 UI 座標,也就是預覽圖層的座標,轉換到相機的座標系中, 再用預覽圖層的座標點,設定曝光的 point 和 mode 。 同聚焦不一樣,曝光要改兩次 mode.

mode 從預設鎖定的 .locked 到選定座標點的連續自動曝光 .continuousAutoExposure, 最後系統調好了,再切換回預設的鎖定 .locked 。 因為不知道系統什麼時候連續自動曝光處理好,所以要用到 KVO. 監聽 activeInput.device 的 adjustingExposure 屬性。 當曝光調節結束了,就鎖定曝光模式。

( 呼叫時機挺好的, 雙擊螢幕,手機攝像頭自動曝光的時候,就防止干擾。曝光完成後,馬上改曝光模式為鎖定 。這樣就不會老是處在曝光中。)

(這個有點像監聽鍵盤,那裡一般用系統通知。)

配置曝光,屬於使用者輸入,並要用到手機的攝像頭硬體。配置曝光的時候,可能有干擾 ( 比如後臺程式的影響 ),這樣就要用鎖了。 device.lockForConfiguration()

其他: 自拍是有曝光效果的

// 單指雙擊,設定曝光, 更多見下面的 github repo
    @objc
    func tapToExpose(recognizer: UIGestureRecognizer){
        if activeInput.device.isExposurePointOfInterestSupported{
            //  與聚焦一樣,得到螢幕中點選的座標,轉化為預覽圖層裡的座標點
            let point = recognizer.location(in: camPreview)
            //  將預覽圖層中的座標點,轉換到相機的座標系中
            let pointOfInterest = previewLayer.captureDevicePointConverted(fromLayerPoint: point)
            showMarkerAtPoint(point: point, marker: exposureMarker)
            exposeAtPoint(pointOfInterest)
        }
    }
    
    private var adjustingExposureContext: String = "Exposure"
    private let kExposure = "adjustingExposure"

    func exposeAtPoint(_ point: CGPoint){
        let device = activeInput.device
        if device.isExposurePointOfInterestSupported, device.isFocusModeSupported(.continuousAutoFocus){
            do{
                try device.lockForConfiguration()
                device.exposurePointOfInterest = point
                device.exposureMode = .continuousAutoExposure
                //  先判斷手機,能不能鎖定曝光。可以就監聽手機攝像頭的調整曝光屬性
                if device.isFocusModeSupported(.locked){
                   //   同聚焦不一樣,曝光要改兩次 mode.
                    //  這裡有一個不受控制的耗時操作( 不清楚什麼時候系統處理好),需要用到 KVO
                    device.addObserver(self, forKeyPath: kExposure, options: .new, context: &adjustingExposureContext)
                    // 變化好了, 操作結束
                    device.unlockForConfiguration()
                }
            }
            catch{
                print("Error Exposing on POI: \(String(describing: error.localizedDescription))")
            }
        }
    }
    
    // 使用 KVO
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        //  先確認,監聽到的是指定的上下文
        if context == &adjustingExposureContext {
            let device = object as! AVCaptureDevice
            //    如果手機攝像頭不處於曝光調整中,也就是完成曝光了,就可以處理了
            if !device.isAdjustingExposure , device.isExposureModeSupported(.locked){
                // 觀察屬性,變化了, 一次性注入呼叫, 就銷燬 KVO
                // 然後到主佇列中非同步配置
                device.removeObserver(self, forKeyPath: kExposure, context: &adjustingExposureContext)
                DispatchQueue.main.async {
                    do{
                        //  完成後,將曝光狀態復原
                        try device.lockForConfiguration()
                        device.exposureMode = .locked
                        device.unlockForConfiguration()
                    }
                    catch{
                        print("Error exposing on POI: \(String(describing: error.localizedDescription))")
                    }   
                }
            }
        }
        else{
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
    

複製程式碼

全部程式碼見: github.com/BoxDengJZ/A…

More:


其次是拍視訊,還有視訊檔案匯出到相簿。

還有視訊的合成,將多個視訊片段合成為一個視訊檔案。

AVFoundation 視訊常用套路: 視訊合成與匯出,拍視訊手電筒,拍照閃光燈


最後是,關於給視訊新增圖形覆蓋和動畫。


推薦資源:

WWDC 2016: Advances in iOS Photography

AVFoundation Programming Guide 蘋果文件

視訊教程

大佬部落格, AVPlayer 本地、網路視訊播放相關

相關文章