Files
RoundCode/Sources/Public/RCCameraViewController.swift
2020-05-07 17:07:24 +04:00

209 lines
8.3 KiB
Swift

// MIT License
// Copyright (c) 2020 Haik Aslanyan
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import UIKit
import AVFoundation
public final class RCCameraViewController: UIViewController {
//MARK: Public properties
public weak var delegate: RCCameraViewControllerDelegate?
public var coder = RCCoder()
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
.portrait
}
public override var prefersStatusBarHidden: Bool {
true
}
//Private properties
private var brightness = CGFloat(0)
private var captureSession = AVCaptureSession()
private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
private var maskLayer = CAShapeLayer()
private var circleLayer = CAShapeLayer()
private lazy var cameraView: UIView = {
let cameraView = UIView()
cameraView.backgroundColor = .clear
self.view.addSubview(cameraView)
cameraView.translatesAutoresizingMaskIntoConstraints = false
cameraView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0).isActive = true
cameraView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0).isActive = true
cameraView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
cameraView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
return cameraView
}()
//MARK: Inits
public required init?(coder: NSCoder) {
super.init(coder: coder)
modalPresentationStyle = .fullScreen
view.backgroundColor = .black
}
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
modalPresentationStyle = .fullScreen
view.backgroundColor = .black
}
public convenience init() {
self.init(nibName: nil, bundle: nil)
modalPresentationStyle = .fullScreen
view.backgroundColor = .black
}
deinit {
videoPreviewLayer?.removeFromSuperlayer()
}
}
//MARK: ViewController lifecycle
public extension RCCameraViewController {
override func viewDidLoad() {
super.viewDidLoad()
configureMaskLayer()
configureVideoStream()
configureVideoPreview()
cameraView.layer.addSublayer(maskLayer)
cameraView.layer.addSublayer(circleLayer)
configureCancelButton()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
captureSession.startRunning()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
captureSession.stopRunning()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
brightness = UIScreen.main.brightness
UIScreen.main.setBrightness(to: 1.0)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
UIScreen.main.setBrightness(to: brightness)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
configureMaskLayer()
videoPreviewLayer?.frame = view.bounds
}
}
//MARK: Private methods
extension RCCameraViewController {
private func configureCancelButton() {
let cancelButton = UIButton(type: .custom)
cancelButton.addTarget(self, action: #selector(cancelPressed), for: .touchUpInside)
cancelButton.setImage(UIImage(systemName: "xmark"), for: .normal)
cancelButton.tintColor = .white
view.addSubview(cancelButton)
cancelButton.translatesAutoresizingMaskIntoConstraints = false
view.trailingAnchor.constraint(equalTo: cancelButton.trailingAnchor, constant: 30).isActive = true
cancelButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 20).isActive = true
}
@objc private func cancelPressed() {
captureSession.stopRunning()
delegate?.cameraViewControllerDidCancel()
dismiss(animated: true)
}
private func configureMaskLayer() {
let path = UIBezierPath(roundedRect: view.bounds, cornerRadius: 0)
let centerPoint = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let radius = min(view.bounds.width, view.bounds.height) * 0.9 / 2
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
path.append(circlePath)
path.usesEvenOddFillRule = true
maskLayer.path = path.cgPath
maskLayer.fillRule = .evenOdd
maskLayer.fillColor = UIColor(white: 0, alpha: 0.6).cgColor
circleLayer.path = circlePath.cgPath
circleLayer.lineWidth = 2
circleLayer.strokeColor = UIColor.white.cgColor
circleLayer.fillColor = UIColor.clear.cgColor
}
private func configureVideoStream() {
guard let captureDevice = AVCaptureDevice.default(for: .video) else { return }
do {
captureSession.sessionPreset = .hd1280x720
let input = try AVCaptureDeviceInput(device: captureDevice)
input.device.activeVideoMaxFrameDuration = CMTimeMake(value: 1, timescale: 30)
captureSession.addInput(input)
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.global(qos: .userInteractive))
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)]
captureSession.addOutput(videoOutput)
} catch {
print(error.localizedDescription)
}
}
private func configureVideoPreview(orientation: AVCaptureVideoOrientation = .portrait) {
videoPreviewLayer?.removeFromSuperlayer()
videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
videoPreviewLayer?.videoGravity = .resizeAspectFill
videoPreviewLayer?.connection?.videoOrientation = orientation
videoPreviewLayer?.frame = view.layer.bounds
view.layer.addSublayer(videoPreviewLayer!)
}
}
//MARK: AVCaptureVideoDataOutputSampleBufferDelegate
extension RCCameraViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
connection.videoOrientation = .portrait
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
let bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0)
let bufferHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0)
let bufferWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0)
let size = min(bufferWidth, bufferHeight)
let origin = (max(bufferWidth, bufferHeight) - size) / 2
let lumaBaseAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)?.advanced(by: bytesPerRow * origin)
let lumaCopy = UnsafeMutableRawPointer.allocate(byteCount: bytesPerRow * size, alignment: MemoryLayout<UInt8>.alignment)
lumaCopy.copyMemory(from: lumaBaseAddress!, byteCount: bytesPerRow * size)
coder.imageDecoder.size = size
coder.imageDecoder.bytesPerRow = bytesPerRow
if let message = try? coder.decode(buffer: lumaCopy.assumingMemoryBound(to: UInt8.self)) {
captureSession.stopRunning()
DispatchQueue.main.async {[weak self] in
self?.delegate?.cameraViewController(didFinishScanning: message)
self?.dismiss(animated: true)
}
}
lumaCopy.deallocate()
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
}
}