MijickCamera/Sources/Internal/Manager/CameraManager+PhotoOutput.swift

128 lines
4.9 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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
}
}
}