128 lines
4.9 KiB
Swift
128 lines
4.9 KiB
Swift
//
|
||
// CameraManager+PhotoOutput.swift of MijickCamera
|
||
//
|
||
// Created by Tomasz Kurylik. Sending ❤️ from Kraków!
|
||
// - Mail: tomasz.kurylik@mijick.com
|
||
// - GitHub: https://github.com/FulcrumOne
|
||
// - Medium: https://medium.com/@mijick
|
||
//
|
||
// Copyright ©2024 Mijick. All rights reserved.
|
||
|
||
|
||
import AVKit
|
||
|
||
@MainActor class CameraManagerPhotoOutput: NSObject {
|
||
private(set) var parent: CameraManager!
|
||
private(set) var output: AVCapturePhotoOutput = .init()
|
||
}
|
||
|
||
// MARK: Setup
|
||
extension CameraManagerPhotoOutput {
|
||
func setup(parent: CameraManager) throws(MCameraError) {
|
||
self.parent = parent
|
||
try self.parent.captureSession.add(output: output)
|
||
}
|
||
}
|
||
|
||
|
||
// MARK: - CAPTURE PHOTO
|
||
|
||
|
||
|
||
// MARK: Capture
|
||
extension CameraManagerPhotoOutput {
|
||
func capture() {
|
||
guard let parent else {
|
||
print("CameraManagerPhotoOutput: parent is nil, cannot capture")
|
||
return
|
||
}
|
||
|
||
configureOutput()
|
||
|
||
// Check if we should disable iOS Retina Flash (when using custom screen flash from SwiftUI layer)
|
||
let disableBuiltInFlash = parent.attributes.screenFlashColor != nil &&
|
||
parent.attributes.cameraPosition == .front &&
|
||
parent.attributes.flashMode != .off
|
||
|
||
let settings = getPhotoOutputSettings(disableFlash: disableBuiltInFlash)
|
||
output.capturePhoto(with: settings, delegate: self)
|
||
parent.cameraMetalView.performImageCaptureAnimation()
|
||
}
|
||
}
|
||
private extension CameraManagerPhotoOutput {
|
||
func getPhotoOutputSettings(disableFlash: Bool) -> AVCapturePhotoSettings {
|
||
let settings = AVCapturePhotoSettings()
|
||
|
||
// When using custom screen flash for front camera, disable iOS's built-in flash
|
||
// to prevent double-flash (our custom flash + iOS Retina Flash)
|
||
if disableFlash {
|
||
settings.flashMode = .off
|
||
} else {
|
||
// For back camera, use the requested flash mode if supported
|
||
let desiredFlashMode = parent.attributes.flashMode.toDeviceFlashMode()
|
||
if output.supportedFlashModes.contains(desiredFlashMode) {
|
||
settings.flashMode = desiredFlashMode
|
||
} else {
|
||
settings.flashMode = .off
|
||
}
|
||
}
|
||
|
||
return settings
|
||
}
|
||
func configureOutput() {
|
||
guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return }
|
||
|
||
connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front
|
||
connection.videoOrientation = parent.attributes.deviceOrientation
|
||
}
|
||
}
|
||
|
||
// MARK: Receive Data
|
||
extension CameraManagerPhotoOutput: @preconcurrency AVCapturePhotoCaptureDelegate {
|
||
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: (any Error)?) {
|
||
guard let imageData = photo.fileDataRepresentation(),
|
||
let ciImage = CIImage(data: imageData)
|
||
else { return }
|
||
|
||
let capturedCIImage = prepareCIImage(ciImage, parent.attributes.cameraFilters)
|
||
let capturedCGImage = prepareCGImage(capturedCIImage)
|
||
let capturedUIImage = prepareUIImage(capturedCGImage)
|
||
let capturedMedia = MCameraMedia(data: capturedUIImage)
|
||
|
||
parent.setCapturedMedia(capturedMedia)
|
||
}
|
||
}
|
||
private extension CameraManagerPhotoOutput {
|
||
func prepareCIImage(_ ciImage: CIImage, _ filters: [CIFilter]) -> CIImage {
|
||
ciImage.applyingFilters(filters)
|
||
}
|
||
func prepareCGImage(_ ciImage: CIImage) -> CGImage? {
|
||
CIContext().createCGImage(ciImage, from: ciImage.extent)
|
||
}
|
||
func prepareUIImage(_ cgImage: CGImage?) -> UIImage? {
|
||
guard let cgImage else { return nil }
|
||
|
||
let frameOrientation = getFixedFrameOrientation()
|
||
let orientation = UIImage.Orientation(frameOrientation)
|
||
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: orientation)
|
||
return uiImage
|
||
}
|
||
}
|
||
private extension CameraManagerPhotoOutput {
|
||
func getFixedFrameOrientation() -> CGImagePropertyOrientation {
|
||
guard UIDevice.current.orientation != parent.attributes.deviceOrientation.toDeviceOrientation() else { return parent.attributes.frameOrientation }
|
||
|
||
return switch (parent.attributes.deviceOrientation, parent.attributes.cameraPosition) {
|
||
case (.portrait, .front): .left
|
||
case (.portrait, .back): .right
|
||
case (.landscapeLeft, .back): .down
|
||
case (.landscapeRight, .back): .up
|
||
case (.landscapeLeft, .front) where parent.attributes.mirrorOutput: .up
|
||
case (.landscapeLeft, .front): .upMirrored
|
||
case (.landscapeRight, .front) where parent.attributes.mirrorOutput: .down
|
||
case (.landscapeRight, .front): .downMirrored
|
||
default: .right
|
||
}
|
||
}
|
||
}
|