Show "zoomed in" version of AVCaptureVideoPreviewLayer while capturing non-zoomed in image (with tap to focus)

We have a camera application which is attempting to display a zoomed in area of the AVCaptureVideoPreviewLayer from an AVCaptureDeviceInput, while still capturing images of the non-zoomed in area of the chosen camera. The user also needs to be able to tap to focus.

We are currently using the below SwiftUI component to achieve this. However, when handling the tap focus events using videoPreviewLayer.captureDevicePointConverted with videoPreviewCropFactor set to 2.0 (effectively zooming in the camera preview by 2x), the captureDevicePoint do not correspond with the correct positions in the capture device reference frame.

import AVFoundation
import SwiftUI

@available(iOS 15.0, *)
struct CameraPreview: UIViewRepresentable {
  var preview: AVCaptureVideoPreviewLayer
  var cropFactor: Double

  func makeUIView(context _: Context) -> UIView {
    let uiView = UIView()

    uiView.layer.addSublayer(self.preview)

    DispatchQueue.main.async {
      self.preview.bounds = self.getPreviewBounds(uiView)
      self.preview.position = uiView.layer.position
    }

    return uiView
  }

  func updateUIView(_ uiView: UIView, context _: Context) {
    self.preview.bounds = self.getPreviewBounds(uiView)
    self.preview.position = uiView.layer.position
  }

  func getPreviewBounds(_ uiView: UIView) -> CGRect {
    return CGRect(x: 0, y: 0,
                  width: uiView.layer.bounds.width * self.cropFactor,
                  height: uiView.layer.bounds.height * self.cropFactor)
  }
}

// parent component
struct YieldMultiCaptureView: View {
var body: some View {
    ZStack {
       CameraPreview(preview: cameraController.videoPreviewLayer, cropFactor: self.videoPreviewCropFactor)
          .ignoresSafeArea()
          .opacity(cameraController.isCaptureInProgress ? 0.2 : 1)
          .onTouch(type: .started, perform: onCameraViewTap)
...

  func onCameraViewTap(_ location: CGPoint) {
      let captureDevicePoint = cameraController.videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: location)
      cameraController.focusCamera(focusPoint: captureDevicePoint)
  }

...

Note: previous answers I have seen suggest adding a zoom factor to the device input. However, this will not work in this case since we still want to capture the non-zoomed region of the camera.

Replies

Hello,

The location that you are passing as a "layer point" isn't actually in the layer's coordinate space, which is why this isn't working as you'd expect. location is in the local view space of the CameraPreview. To convert this to a point in the AVCaptureVideoPreviewLayer's coordinate space, you need to consider the relationship between the origins of these two coordinate spaces.

In this case, I believe the view's layer's coordinate space happens to match the view's coordinate space, so convert(_:to:) could help.