Home > database >  How to handle a device rotation for AVCaptureVideoPreviewLayer?
How to handle a device rotation for AVCaptureVideoPreviewLayer?

Time:04-02

I have a simple camera preview implementation:

import SwiftUI
import AVFoundation

struct CameraView: View {
    @StateObject var model = CameraModel()
    var body: some View {
        CameraPreview(camera: model)
            .safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
                Color.clear
                    .frame(height: 0)
                    .background(Material.bar)
            }
            .ignoresSafeArea(.all, edges: .top)
            .onAppear() {
                model.check()
            }
    }
}

struct CameraPreview: UIViewRepresentable {
    @ObservedObject var camera: CameraModel
    
    func makeUIView(context: Context) -> some UIView {
        let view = UIView(frame: UIScreen.main.bounds)
        camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
        camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
        camera.preview.frame = view.frame
        view.layer.addSublayer(camera.preview)
        camera.start()
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

struct CameraView_Previews: PreviewProvider {
    static var previews: some View {
        CameraView()
    }
}

class CameraModel: ObservableObject {
    @Published var session = AVCaptureSession()
    @Published var alert = false
    @Published var preview: AVCaptureVideoPreviewLayer!
    
    func check() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            setUp()
            break
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { (status) in
                if status {
                    self.setUp()
                }
            }
            break
        case .denied:
            self.alert.toggle()
            break
        default:
            break
        }
    }
    
    func setUp() {
        do {
            self.session.beginConfiguration()
            let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
            let input = try AVCaptureDeviceInput(device: device!)
            
            if self.session.canAddInput(input) {
                self.session.addInput(input)
            }
            
            self.session.commitConfiguration()
        }
        catch {
            print(error.localizedDescription)
        }
    }
    
    func start() {
        self.session.startRunning()
    }
}

The problem is that it doesn't handle screen rotations:

Screenshot

I found similar topics, for example, this one, but I am a noobie in iOS development, I can't even understand where to put this solution. I've checked neither View, nor UIViewRepresentable have such methods to override.

How to handle screen rotation in AVCaptureVideoPreviewLayer?

CodePudding user response:

If you want to update layer frame in rotation, you need to create custom UIView and override layoutSubviews(). Inside layoutSubviews(), you need to update frame for sublayers.

The code will be as below.

struct CameraPreview: UIViewRepresentable {
    @ObservedObject var camera: CameraModel
    
    class LayerView: UIView {
        override func layoutSubviews() {
            super.layoutSubviews()
            // To disable default animation of layer. You can comment out those lines with `CATransaction` if you want to include
            CATransaction.begin() 
            CATransaction.setDisableActions(true)
            layer.sublayers?.forEach({ layer in
                layer.frame = frame
            })
            CATransaction.commit()
        }
    }
    
    func makeUIView(context: Context) -> some UIView {
        let view = LayerView()
        camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
        camera.preview.frame = view.frame
        camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
        view.layer.addSublayer(camera.preview)
        camera.session.startRunning()
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

CodePudding user response:

This is a working variant with video rotation based on Dscyre Scotti'es answer:

struct CameraView: View {
    @StateObject var model = CameraModel()
    var body: some View {
        CameraPreview(camera: model)
            .safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
                Color.clear
                    .frame(height: 0)
                    .background(Material.bar)
            }
            .ignoresSafeArea(.all, edges: [.top, .horizontal])
            .onAppear() {
                model.check()
            }
    }
}

struct CameraPreview: UIViewRepresentable {
    @ObservedObject var camera: CameraModel
    
    class LayerView: UIView {
        var parent: CameraPreview! = nil
        
        override func layoutSubviews() {
            super.layoutSubviews()
            // To disable default animation of layer. You can comment out those lines with `CATransaction` if you want to include
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            layer.sublayers?.forEach({ layer in
                layer.frame = UIScreen.main.bounds
            })
            self.parent.camera.rotate(orientation: UIDevice.current.orientation)
            CATransaction.commit()
        }
    }
    
    func makeUIView(context: Context) -> some UIView {
        let view = LayerView()
        view.parent = self
        camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
        camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
        camera.preview.frame = view.frame
        view.layer.addSublayer(camera.preview)
        camera.start()
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
}

struct CameraView_Previews: PreviewProvider {
    static var previews: some View {
        CameraView()
    }
}

class CameraModel: ObservableObject {
    @Published var session = AVCaptureSession()
    @Published var alert = false
    @Published var preview: AVCaptureVideoPreviewLayer!
    
    func check() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            setUp()
            break
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { (status) in
                if status {
                    self.setUp()
                }
            }
            break
        case .denied:
            self.alert.toggle()
            break
        default:
            break
        }
    }
    
    func setUp() {
        do {
            self.session.beginConfiguration()
            let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
            let input = try AVCaptureDeviceInput(device: device!)
            
            if self.session.canAddInput(input) {
                self.session.addInput(input)
            }
            
            self.session.commitConfiguration()
        }
        catch {
            print(error.localizedDescription)
        }
    }
    
    func start() {
        self.session.startRunning()
    }
    
    func rotate(orientation: UIDeviceOrientation) {
        let videoConnection = self.preview.connection
        switch orientation {
        case .portraitUpsideDown:
            videoConnection?.videoOrientation = .portraitUpsideDown
        case .landscapeLeft:
            videoConnection?.videoOrientation = .landscapeRight
        case .landscapeRight:
            videoConnection?.videoOrientation = .landscapeLeft
        case .faceDown:
            videoConnection?.videoOrientation = .portraitUpsideDown
        default:
            videoConnection?.videoOrientation = .portrait
        }
    }
}
  • Related