使用 AVFoundation 实现自定义相机拍照

2018-06-29  本文已影响67人  l蓝色梦幻

使用摄像头拍照

使用 AVFoundation 拍照需要至少使用以下类:

    private(set) var session = AVCaptureSession()
    private(set) var device: AVCaptureDevice?
    private(set) var input: AVCaptureDeviceInput?
    private(set) var imageOutput: AVCapturePhotoOutput?
    private(set) var previewLayer: AVCaptureVideoPreviewLayer?

其中, AVCaptureSession 用于视频捕获,AVCaptureDevice用于管理设备,AVCaptureDeviceInput是当前工作的摄像头(不限于摄像头),AVCapturePhotoOutput是用来输出图像,AVCaptureVideoPreviewLayer用于显示当前捕捉到的视频。

开始捕捉视频图像

    // 照相功能初始化
    private func setupCaptureSession() {
        // 判断是否初始化完成
        var successful = true
        defer {
            if !successful {
                // log error msg...
                print("error setting capture session")
            }
        }
        
        // 判断至少有一个摄像头,并对涉嫌头初始化
        guard let d = AVCaptureDevice.default(for: .video), let device = tunedCaptureDevice(device: d) else {
            successful = false
            return
        }
        
        // 配置 session 
        session.beginConfiguration()
        defer {
            session.commitConfiguration()
        }
        
        session.sessionPreset = .photo
        
        do {
            input = try AVCaptureDeviceInput(device: device)
            if session.canAddInput(input!) {
                session.addInput(input!)
            } else {
                successful = false
                return
            }
        } catch {
            print(error.localizedDescription)
            successful = false
            return
        }
        
        imageOutput = AVCapturePhotoOutput()
        let settings = AVCapturePhotoSettings.init(format: [AVVideoCodecKey: AVVideoCodecJPEG])
        imageOutput?.setPreparedPhotoSettingsArray([settings]) { (a, e) in
            print(e?.localizedDescription ?? "")
        }
        
        if session.canAddOutput(imageOutput!) {
            session.addOutput(imageOutput!)
        } else {
            successful = false
            return
        }
        
        // 图像显示 layer 配置
        previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer!.videoGravity = .resizeAspectFill
        previewLayer!.frame = bounds
        self.layer.insertSublayer(previewLayer!, at: 0)
    }
    
    // 摄像头参数配置
    private func tunedCaptureDevice(device: AVCaptureDevice) -> AVCaptureDevice? {
        do {
            try device.lockForConfiguration()
            if device.isFocusModeSupported(.continuousAutoFocus) {
                device.focusMode = .continuousAutoFocus
            }
            if device.isExposureModeSupported(.continuousAutoExposure) {
                device.exposureMode = .continuousAutoExposure
            }
            if device.isWhiteBalanceModeSupported(.continuousAutoWhiteBalance) {
                device.whiteBalanceMode = .continuousAutoWhiteBalance
            }
            device.unlockForConfiguration()
            return device
        } catch {
            print(error.localizedDescription)
            return nil
        }
    }

关于捕捉到的图像与设备之前的横竖屏切换的设置

捕捉到的图像并不是根据设备自动切换横竖屏的,因此需要在代码里面手动的设置捕捉到的图像的状态。

    // 根据当前设备的状态计算捕捉到的图像的状态
    var videoOrientationFromCurrentDeviceOrientation: AVCaptureVideoOrientation? {
        get {
            switch UIDevice.current.orientation {
            case .landscapeLeft:
                return .landscapeRight
            case .landscapeRight:
                return .landscapeLeft
//            case .portraitUpsideDown:
//                return .portraitUpsideDown
            case .portrait:
                return .portrait
            default:
                return nil
            }
        }
    }
    
    // 切换图像横竖屏状态
    func updateOrientation() {
        guard let v = videoOrientationFromCurrentDeviceOrientation else {
            return
        }
        previewLayer?.connection?.videoOrientation = v
        imageOutput?.connection(with: .video)?.videoOrientation = v
    }

前后摄像头切换

获取摄像头

    // 当前设备支持的摄像头
    var cameras: [AVCaptureDevice] {
        get {
            let s = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: .video, position: .unspecified)
            return s.devices
        }
    }

    // 根据摄像头位置获取前后摄像头
    private func camera(position: AVCaptureDevice.Position) -> AVCaptureDevice? {
        let s = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: .video, position: position)
        
        for d in s.devices {
            if d.position == position {
                return tunedCaptureDevice(device: d)
            }
        }
        return nil
    }

切换摄像头

    func changeCamera() {
        if cameras.count > 1 {
            let animation = CATransition()
            animation.duration = 0.5
            animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
            animation.type = "oglFlip"
            
            var newCamera: AVCaptureDevice?
            
            let position = input?.device.position
            if position == .back {
                newCamera = camera(position: .front)
                animation.subtype = kCATransitionFromLeft
            } else {
                newCamera = camera(position: .back)
                animation.subtype = kCATransitionFromRight
            }
            
            guard let ca = newCamera, let newInput = try? AVCaptureDeviceInput(device: ca) else {
                return
            }
            
            OnMainThreadAsync {
                self.previewLayer?.add(animation, forKey: nil)
                self.session.beginConfiguration()
                defer {
                    self.session.commitConfiguration()
                }
                if self.input != nil {
                    self.session.removeInput(self.input!)
                }
                    
                if self.session.canAddInput(newInput) {
                    self.session.addInput(newInput)
                    self.input = newInput
                } else {
                    if self.input != nil {
                        self.session.addInput(self.input!)
                    }
                    return
                }
            }
        }
    }

拍照

func captureImage() {
        OnMainThreadAsync {
            if let output = self.session.outputs.first as? AVCapturePhotoOutput {
                output.isHighResolutionCaptureEnabled = true
                let settings = AVCapturePhotoSettings()
                settings.isHighResolutionPhotoEnabled = true
//                settings.flashMode = AVCaptureDevice.FlashMode.auto
                let previewPixelType = settings.availablePreviewPhotoPixelFormatTypes.first!
                let previewFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPixelType,
                                     kCVPixelBufferWidthKey as String: 160,
                                     kCVPixelBufferHeightKey as String: 160,
                                     ]
                settings.previewPhotoFormat = previewFormat
                if output.supportedFlashModes.contains(AVCaptureDevice.FlashMode.auto) {
                    settings.flashMode = AVCaptureDevice.FlashMode.auto
                } else if output.supportedFlashModes.contains(AVCaptureDevice.FlashMode.on) {
                    settings.flashMode = AVCaptureDevice.FlashMode.on
                } else if output.supportedFlashModes.contains(AVCaptureDevice.FlashMode.off) {
                    settings.flashMode = AVCaptureDevice.FlashMode.off
                }
                
                output.capturePhoto(with: settings, delegate: self)
            }
        }
    }
    
    // AVCapturePhotoCaptureDelegate
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
        
        guard let didFinishProcessingPhoto = photoSampleBuffer, let previewPhoto = previewPhotoSampleBuffer else {
            self.delegate?.ErrorOnTakePhoto(view: self, error: error)
           return
        }
        
        guard let data = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: didFinishProcessingPhoto, previewPhotoSampleBuffer: previewPhoto) else {
            self.delegate?.ErrorOnTakePhoto(view: self, error: error)
            return
        }
        guard let image = UIImage(data: data) else {
            self.delegate?.ErrorOnTakePhoto(view: self, error: error)
            return
        }
        self.delegate?.ImageDidSelected(view: self, image: image)
        self.session.stopRunning()
    }

遇到的问题。

  1. 拍照的时候概率性的出现 Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSUnderlyingError=0x17025db20 {Error Domain=NSOSStatusErrorDomain Code=-16800 "(null)"}, NSLocalizedFailureReason=An unknown error occurred (-16800), NSLocalizedDescription=The operation could not be completed} 这个错误。这个错误引起的原因是 output.capturePhoto 之后立即执行 session.stopRunning() 了,造成同一个 frametime 有n帧图片,这样就会报错。

    解决方法:
    在代理中停止 session。

  2. 横竖屏设置的时候图像横屏的时候总是反的。原因在于 AVCaptureVideoOrientation 的 landscapeRight 对应的是 UIDeviceOrientation 的 landscapeLeft。Right 对应的是 Left。

上一篇下一篇

猜你喜欢

热点阅读