How to take UIImage of AVCaptureVideoPreviewLayer instead of AVCapturePhotoOutput capture

前端 未结 3 565
难免孤独
难免孤独 2020-12-16 05:04

I want to \"stream\" the preview layer to my server, however, I only want specific frames to be sent. Basically, I want to take a snapshot of the AVCaptureVideoPreviewLayer,

相关标签:
3条回答
  • 2020-12-16 05:37

    Details

    • Xcode 10.2.1 (10E1001), Swift 5

    Features

    This solution allow:

    • to check camera access
    • to select front or back camera
    • if no access to the camera show alert with link to the app settings page
    • to make o photo
    • to play standard capture photo sound

    Solution

    CameraService

    import UIKit
    import AVFoundation
    import Vision
    
    class CameraService: NSObject {
    
        private weak var previewView: UIView?
        private(set) var cameraIsReadyToUse = false
        private let session = AVCaptureSession()
        private weak var previewLayer: AVCaptureVideoPreviewLayer?
        private lazy var sequenceHandler = VNSequenceRequestHandler()
        private lazy var capturePhotoOutput = AVCapturePhotoOutput()
        private lazy var dataOutputQueue = DispatchQueue(label: "FaceDetectionService",
                                                         qos: .userInitiated, attributes: [],
                                                         autoreleaseFrequency: .workItem)
        private var captureCompletionBlock: ((UIImage) -> Void)?
        private var preparingCompletionHandler: ((Bool) -> Void)?
        private var snapshotImageOrientation = UIImage.Orientation.upMirrored
    
        private var cameraPosition = AVCaptureDevice.Position.front {
            didSet {
                switch cameraPosition {
                    case .front: snapshotImageOrientation = .upMirrored
                    case .unspecified, .back: fallthrough
                    @unknown default: snapshotImageOrientation = .up
                }
            }
        }
        func prepare(previewView: UIView,
                     cameraPosition: AVCaptureDevice.Position,
                     completion: ((Bool) -> Void)?) {
            self.previewView = previewView
            self.preparingCompletionHandler = completion
            self.cameraPosition = cameraPosition
            checkCameraAccess { allowed in
                if allowed { self.setup() }
                completion?(allowed)
                self.preparingCompletionHandler = nil
            }
        }
    
        private func setup() { configureCaptureSession() }
        func start() { if cameraIsReadyToUse { session.startRunning() } }
        func stop() { session.stopRunning() }
    }
    
    extension CameraService {
    
        private func askUserForCameraPermission(_ completion:  ((Bool) -> Void)?) {
            AVCaptureDevice.requestAccess(for: AVMediaType.video) { (allowedAccess) -> Void in
                DispatchQueue.main.async { completion?(allowedAccess) }
            }
        }
    
        private func checkCameraAccess(completion: ((Bool) -> Void)?) {
            askUserForCameraPermission { [weak self] allowed in
                guard let self = self, let completion = completion else { return }
                self.cameraIsReadyToUse = allowed
                if allowed {
                    completion(true)
                } else {
                    self.showDisabledCameraAlert(completion: completion)
                }
            }
        }
    
        private func configureCaptureSession() {
            guard let previewView = previewView else { return }
            // Define the capture device we want to use
    
            guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: cameraPosition) else {
                let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "No front camera available"])
                show(error: error)
                return
            }
    
            // Connect the camera to the capture session input
            do {
    
                try camera.lockForConfiguration()
                defer { camera.unlockForConfiguration() }
    
                if camera.isFocusModeSupported(.continuousAutoFocus) {
                    camera.focusMode = .continuousAutoFocus
                }
    
                if camera.isExposureModeSupported(.continuousAutoExposure) {
                    camera.exposureMode = .continuousAutoExposure
                }
    
                let cameraInput = try AVCaptureDeviceInput(device: camera)
                session.addInput(cameraInput)
    
            } catch {
                show(error: error as NSError)
                return
            }
    
            // Create the video data output
            let videoOutput = AVCaptureVideoDataOutput()
            videoOutput.setSampleBufferDelegate(self, queue: dataOutputQueue)
            videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
    
            // Add the video output to the capture session
            session.addOutput(videoOutput)
    
            let videoConnection = videoOutput.connection(with: .video)
            videoConnection?.videoOrientation = .portrait
    
            // Configure the preview layer
            let previewLayer = AVCaptureVideoPreviewLayer(session: session)
            previewLayer.videoGravity = .resizeAspectFill
            previewLayer.frame = previewView.bounds
            previewView.layer.insertSublayer(previewLayer, at: 0)
            self.previewLayer = previewLayer
        }
    }
    
    extension CameraService: AVCaptureVideoDataOutputSampleBufferDelegate {
        func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
            guard   captureCompletionBlock != nil,
                    let outputImage = UIImage(sampleBuffer: sampleBuffer, orientation: snapshotImageOrientation) else { return }
            DispatchQueue.main.async { [weak self] in
                guard let self = self else { return }
                if let captureCompletionBlock = self.captureCompletionBlock{
                    captureCompletionBlock(outputImage)
                    AudioServicesPlayAlertSound(SystemSoundID(1108))
                }
                self.captureCompletionBlock = nil
            }
        }
    }
    
    // Navigation
    
    extension CameraService {
    
        private func show(alert: UIAlertController) {
            DispatchQueue.main.async {
                UIApplication.topViewController?.present(alert, animated: true, completion: nil)
            }
        }
    
        private func showDisabledCameraAlert(completion: ((Bool) -> Void)?) {
            let alertVC = UIAlertController(title: "Enable Camera Access",
                                            message: "Please provide access to your camera",
                                            preferredStyle: .alert)
            alertVC.addAction(UIAlertAction(title: "Go to Settings", style: .default, handler: { action in
                guard   let previewView = self.previewView,
                        let settingsUrl = URL(string: UIApplication.openSettingsURLString),
                        UIApplication.shared.canOpenURL(settingsUrl) else { return }
                UIApplication.shared.open(settingsUrl) { [weak self] _ in
                    guard let self = self else { return }
                    self.prepare(previewView: previewView,
                                  cameraPosition: self.cameraPosition,
                                  completion: self.preparingCompletionHandler)
                }
            }))
            alertVC.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in completion?(false) }))
            show(alert: alertVC)
        }
    
        private func show(error: NSError) {
            let alertVC = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
            alertVC.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: nil ))
            show(alert: alertVC)
        }
    }
    
    extension CameraService: AVCapturePhotoCaptureDelegate {
        func capturePhoto(completion: ((UIImage) -> Void)?) { captureCompletionBlock = completion }
    }
    

    Helpers

    ///////////////////////////////////////////////////////////////////////////
    
    import UIKit
    import AVFoundation
    
    extension UIImage {
    
        convenience init?(sampleBuffer: CMSampleBuffer, orientation: UIImage.Orientation = .upMirrored) {
            guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return nil }
            CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
            defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
            let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
            let width = CVPixelBufferGetWidth(pixelBuffer)
            let height = CVPixelBufferGetHeight(pixelBuffer)
            let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
            guard let context = CGContext(data: baseAddress, width: width, height: height,
                                          bitsPerComponent: 8, bytesPerRow: bytesPerRow,
                                          space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else { return nil }
    
            guard let cgImage = context.makeImage() else { return nil }
            self.init(cgImage: cgImage, scale: 1, orientation: orientation)
        }
    }
    
    ///////////////////////////////////////////////////////////////////////////
    
    
    import UIKit
    
    extension UIApplication {
        private class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
            if let navigationController = controller as? UINavigationController {
                return topViewController(controller: navigationController.visibleViewController)
            }
            if let tabController = controller as? UITabBarController {
                if let selected = tabController.selectedViewController {
                    return topViewController(controller: selected)
                }
            }
            if let presented = controller?.presentedViewController {
                return topViewController(controller: presented)
            }
            return controller
        }
    
        class var topViewController: UIViewController? { return topViewController() }
    }
    

    Usage

    private lazy var cameraService = CameraService()
    
    //...
    
    cameraService.prepare(previewView: previewView, cameraPosition: .front) { [weak self] success in
        if success { self?.cameraService.start() }
    }
    
    //...
    
    cameraService.capturePhoto { [weak self] image in
       //...
    }
    

    Full sample

    import UIKit
    
    class ViewController: UIViewController {
    
        private lazy var cameraService = CameraService()
        private weak var button: UIButton?
        private weak var imagePreviewView: UIImageView?
        private var cameraInited = false
    
        private enum ButtonState { case cancel, makeSnapshot }
        private var buttonState = ButtonState.makeSnapshot {
            didSet {
                switch buttonState {
                    case .makeSnapshot: button?.setTitle("Make a photo", for: .normal)
                    case .cancel: button?.setTitle("Cancel", for: .normal)
                }
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
            setupCameraPreviewView()
            setupButton()
            // Do any additional setup after loading the view.
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            cameraService.start()
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            cameraService.stop()
        }
    
        // Ensure that the interface stays locked in Portrait.
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            return .portrait
        }
    
        // Ensure that the interface stays locked in Portrait.
        override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
            return .portrait
        }
    }
    
    
    extension ViewController {
    
        private func setupCameraPreviewView() {
            let previewView = UIView(frame: .zero)
            view.addSubview(previewView)
            previewView.translatesAutoresizingMaskIntoConstraints = false
            previewView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            previewView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            previewView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            previewView.layoutIfNeeded()
            cameraService.prepare(previewView: previewView, cameraPosition: .front) { [weak self] success in
                if success { self?.cameraService.start() }
            }
        }
    
        private func setupButton() {
            let button = UIButton(frame: .zero)
            button.addTarget(self, action: #selector(buttonTouchedUpInside), for: .touchUpInside)
            view.addSubview(button)
            self.button = button
            buttonState = .makeSnapshot
            button.translatesAutoresizingMaskIntoConstraints = false
            button.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            button.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            button.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            button.heightAnchor.constraint(equalToConstant: 44).isActive = true
            button.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        }
    
        private func show(image: UIImage) {
            let imageView = UIImageView(frame: .zero)
            view.insertSubview(imageView, at: 1)
            imagePreviewView = imageView
            imageView.translatesAutoresizingMaskIntoConstraints = false
            imageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
            imageView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
            imageView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
            imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            imageView.image = image
        }
    
        @objc func buttonTouchedUpInside() {
            switch buttonState {
            case .makeSnapshot:
                cameraService.capturePhoto { [weak self] image in
                    guard let self = self else {return }
                    self.cameraService.stop()
                    self.buttonState = .cancel
                    self.show(image: image)
                }
            case .cancel:
                buttonState = .makeSnapshot
                cameraService.start()
                imagePreviewView?.removeFromSuperview()
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-16 05:43

    @ninjaproger's answer was great! Simply writing this as a Swift 4 version of the answer for future reference.

    import UIKit
    import AVFoundation
    
    var customPreviewLayer: AVCaptureVideoPreviewLayer?
    
    class ViewController: UIViewController {
        @IBOutlet weak var imageView: UIImageView!
        override func viewDidLoad() {
            super.viewDidLoad()
            CaptureManager.shared.statSession()
            CaptureManager.shared.delegate = self
        }
    }
    
    extension ViewController: CaptureManagerDelegate {
        func processCapturedImage(image: UIImage) {
            self.imageView.image = image
        }
    }
    
    protocol CaptureManagerDelegate: class {
        func processCapturedImage(image: UIImage)
    }
    
    class CaptureManager: NSObject {
        internal static let shared = CaptureManager()
        weak var delegate: CaptureManagerDelegate?
        var session: AVCaptureSession?
    
        override init() {
            super.init()
            session = AVCaptureSession()
    
            //setup input
            let device =  AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
            let input = try! AVCaptureDeviceInput(device: device!)
            session?.addInput(input)
    
            //setup output
            let output = AVCaptureVideoDataOutput()
            output.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String: kCVPixelFormatType_32BGRA]
            output.setSampleBufferDelegate(self, queue: DispatchQueue.main)
            session?.addOutput(output)
        }
    
        func statSession() {
            session?.startRunning()
        }
    
        func stopSession() {
            session?.stopRunning()
        }
    
        func getImageFromSampleBuffer(sampleBuffer: CMSampleBuffer) ->UIImage? {
            guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                return nil
            }
            CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
            let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
            let width = CVPixelBufferGetWidth(pixelBuffer)
            let height = CVPixelBufferGetHeight(pixelBuffer)
            let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
            guard let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
                return nil
            }
            guard let cgImage = context.makeImage() else {
                return nil
            }
            let image = UIImage(cgImage: cgImage, scale: 1, orientation:.right)
            CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
            return image
        }
    }
    
    extension CaptureManager: AVCaptureVideoDataOutputSampleBufferDelegate {
        func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
            guard let outputImage = getImageFromSampleBuffer(sampleBuffer: sampleBuffer) else {
                return
            }
            delegate?.processCapturedImage(image: outputImage)
        }
    }
    
    0 讨论(0)
  • 2020-12-16 05:54

    Basically instead of using AVCaptureVideoPreviewLayer for grabbing frames you should use AVCaptureVideoDataOutputSampleBufferDelegate. Here is example:

    import Foundation
    import UIKit
    import AVFoundation
    
    protocol CaptureManagerDelegate: class {
        func processCapturedImage(image: UIImage)
    }
    
    class CaptureManager: NSObject {
        internal static let shared = CaptureManager()
        weak var delegate: CaptureManagerDelegate?
        var session: AVCaptureSession?
    
        override init() {
            super.init()
            session = AVCaptureSession()
    
            //setup input
            let device =  AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
            let input = try! AVCaptureDeviceInput(device: device)
            session?.addInput(input)
    
            //setup output
            let output = AVCaptureVideoDataOutput()
            output.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable: kCVPixelFormatType_32BGRA]
            output.setSampleBufferDelegate(self, queue: DispatchQueue.main)
            session?.addOutput(output)
        }
    
        func statSession() {
            session?.startRunning()
        }
    
        func stopSession() {
            session?.stopRunning()
        }
    
        func getImageFromSampleBuffer(sampleBuffer: CMSampleBuffer) ->UIImage? {
            guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
                return nil
            }
            CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
            let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
            let width = CVPixelBufferGetWidth(pixelBuffer)
            let height = CVPixelBufferGetHeight(pixelBuffer)
            let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
            guard let context = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) else {
                return nil
            }
            guard let cgImage = context.makeImage() else {
                return nil
            }
            let image = UIImage(cgImage: cgImage, scale: 1, orientation:.right)
            CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
            return image
        }
    }
    
    extension CaptureManager: AVCaptureVideoDataOutputSampleBufferDelegate {
        func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
            guard let outputImage = getImageFromSampleBuffer(sampleBuffer: sampleBuffer) else {
                return
            }
            delegate?.processCapturedImage(image: outputImage)
        }
    }
    

    Update: To process images you should implement a processCapturedImage method of the CaptureManagerDelegate protocol in any other class where you want, like:

    import UIKit
    
    class ViewController: UIViewController {
        @IBOutlet weak var imageView: UIImageView!
        override func viewDidLoad() {
            super.viewDidLoad()
            CaptureManager.shared.statSession()
            CaptureManager.shared.delegate = self
        }
    }
    
    extension ViewController: CaptureManagerDelegate {
        func processCapturedImage(image: UIImage) {
            self.imageView.image = image
        }
    }
    
    0 讨论(0)
提交回复
热议问题