MijickCamera/Sources/Internal/UI/Camera View/CameraView+Metal.swift

280 lines
9.7 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.

//
// CameraView+Metal.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 SwiftUI
import MetalKit
import AVKit
@MainActor class CameraMetalView: MTKView {
private(set) var parent: CameraManager!
private(set) var ciContext: CIContext!
private(set) var commandQueue: MTLCommandQueue!
private(set) var currentFrame: CIImage?
private(set) var focusIndicator: CameraFocusIndicatorView = .init()
private(set) var isAnimating: Bool = false
}
// MARK: Setup
extension CameraMetalView {
func setup(parent: CameraManager) throws(MCameraError) {
guard let metalDevice = MTLCreateSystemDefaultDevice() else { throw .cannotSetupMetalDevice }
self.assignInitialValues(parent: parent, metalDevice: metalDevice)
self.configureMetalView(metalDevice: metalDevice)
self.addToParent(parent.cameraView)
}
}
private extension CameraMetalView {
func assignInitialValues(parent: CameraManager, metalDevice: MTLDevice) {
self.parent = parent
self.ciContext = CIContext(mtlDevice: metalDevice)
self.commandQueue = metalDevice.makeCommandQueue()
}
func configureMetalView(metalDevice: MTLDevice) {
self.parent.cameraView.alpha = 0
self.delegate = self
self.device = metalDevice
self.isPaused = true
self.enableSetNeedsDisplay = false
self.framebufferOnly = false
self.autoResizeDrawable = false
self.contentMode = .scaleAspectFill
self.clipsToBounds = true
}
}
// MARK: - ANIMATIONS
// MARK: Camera Entrance
extension CameraMetalView {
func performCameraEntranceAnimation() { UIView.animate(withDuration: 0.33) { [self] in
parent.cameraView.alpha = 1
}}
}
// MARK: Image Capture
extension CameraMetalView {
func performImageCaptureAnimation() {
let blackMatte = createBlackMatte()
parent.cameraView.addSubview(blackMatte)
animateBlackMatte(blackMatte)
}
/// Shows screen flash for front camera, calls completion when ready to capture
func performScreenFlash(completion: @escaping @MainActor () -> Void) {
guard let cameraView = parent?.cameraView else {
completion()
return
}
let flashColor = parent.attributes.screenFlashColor ?? .white
let flashView = createScreenFlashView(color: flashColor)
let originalBrightness = UIScreen.main.brightness
// Add flash overlay to the window for full screen coverage
if let window = cameraView.window {
flashView.frame = window.bounds
window.addSubview(flashView)
} else {
flashView.frame = cameraView.bounds
cameraView.addSubview(flashView)
}
// Boost brightness and show flash
UIScreen.main.brightness = 1.0
UIView.animate(withDuration: 0.05, animations: {
flashView.alpha = 1.0
}) { _ in
// Small delay for camera to adjust to new lighting, then capture
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
completion()
// Fade out flash after capture is triggered
try? await Task.sleep(for: .milliseconds(150))
UIView.animate(withDuration: 0.2, animations: {
flashView.alpha = 0
}) { _ in
flashView.removeFromSuperview()
UIScreen.main.brightness = originalBrightness
}
}
}
}
}
private extension CameraMetalView {
func createBlackMatte() -> UIView {
let view = UIView()
view.frame = parent.cameraView.frame
view.backgroundColor = .init(resource: .mijickBackgroundPrimary)
view.alpha = 0
return view
}
func animateBlackMatte(_ view: UIView) {
UIView.animate(withDuration: 0.16, animations: { view.alpha = 1 }) { _ in
UIView.animate(withDuration: 0.16, animations: { view.alpha = 0 }) { _ in
view.removeFromSuperview()
}
}
}
func createScreenFlashView(color: UIColor) -> UIView {
let view = UIView()
view.frame = UIScreen.main.bounds
view.backgroundColor = color
view.alpha = 0
return view
}
}
// MARK: Camera Flip
extension CameraMetalView {
func beginCameraFlipAnimation() async {
let snapshot = createSnapshot()
isAnimating = true
insertBlurView(snapshot)
animateBlurFlip()
await Task.sleep(seconds: 0.01)
}
func finishCameraFlipAnimation() async {
guard let blurView = parent.cameraView.viewWithTag(.blurViewTag) else { return }
await Task.sleep(seconds: 0.44)
UIView.animate(withDuration: 0.3, animations: { blurView.alpha = 0 }) { [self] _ in
blurView.removeFromSuperview()
isAnimating = false
}
}
}
private extension CameraMetalView {
func createSnapshot() -> UIImage? {
guard let currentFrame else { return nil }
let image = UIImage(ciImage: currentFrame)
return image
}
func insertBlurView(_ snapshot: UIImage?) {
let blurView = UIImageView(frame: parent.cameraView.frame)
blurView.image = snapshot
blurView.contentMode = .scaleAspectFill
blurView.clipsToBounds = true
blurView.tag = .blurViewTag
blurView.applyBlurEffect(style: .regular)
parent.cameraView.addSubview(blurView)
}
func animateBlurFlip() {
UIView.transition(with: parent.cameraView, duration: 0.44, options: cameraFlipAnimationTransition) {}
}
}
private extension CameraMetalView {
var cameraFlipAnimationTransition: UIView.AnimationOptions { parent.attributes.cameraPosition == .back ? .transitionFlipFromLeft : .transitionFlipFromRight }
}
// MARK: Camera Focus
extension CameraMetalView {
func performCameraFocusAnimation(touchPoint: CGPoint) {
removeExistingFocusIndicatorAnimations()
let focusIndicator = focusIndicator.create(at: touchPoint)
parent.cameraView.addSubview(focusIndicator)
animateFocusIndicator(focusIndicator)
}
}
private extension CameraMetalView {
func removeExistingFocusIndicatorAnimations() { if let view = parent.cameraView.viewWithTag(.focusIndicatorTag) {
view.removeFromSuperview()
}}
func animateFocusIndicator(_ focusIndicator: UIImageView) {
UIView.animate(withDuration: 0.44, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, animations: { focusIndicator.transform = .init(scaleX: 1, y: 1) }) { _ in
UIView.animate(withDuration: 0.44, delay: 1.44, animations: { focusIndicator.alpha = 0.2 }) { _ in
UIView.animate(withDuration: 0.44, delay: 1.44, animations: { focusIndicator.alpha = 0 })
}
}
}
}
// MARK: Camera Orientation
extension CameraMetalView {
func beginCameraOrientationAnimation(if shouldAnimate: Bool) async { if shouldAnimate {
parent.cameraView.alpha = 0
await Task.sleep(seconds: 0.1)
}}
func finishCameraOrientationAnimation(if shouldAnimate: Bool) { if shouldAnimate {
UIView.animate(withDuration: 0.2, delay: 0.1) { self.parent.cameraView.alpha = 1 }
}}
}
// MARK: - CAPTURING FRAMES
// MARK: Capture
extension CameraMetalView: @preconcurrency AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let cvImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let currentFrame = captureCurrentFrame(cvImageBuffer)
let currentFrameWithFiltersApplied = applyingFiltersToCurrentFrame(currentFrame)
redrawCameraView(currentFrameWithFiltersApplied)
}
}
private extension CameraMetalView {
func captureCurrentFrame(_ cvImageBuffer: CVImageBuffer) -> CIImage {
let currentFrame = CIImage(cvImageBuffer: cvImageBuffer)
return currentFrame.oriented(parent.attributes.frameOrientation)
}
func applyingFiltersToCurrentFrame(_ currentFrame: CIImage) -> CIImage {
currentFrame.applyingFilters(parent.attributes.cameraFilters)
}
func redrawCameraView(_ frame: CIImage) {
currentFrame = frame
draw()
}
}
// MARK: Draw
extension CameraMetalView: MTKViewDelegate {
func draw(in view: MTKView) {
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let ciImage = currentFrame,
let currentDrawable = view.currentDrawable
else { return }
changeDrawableSize(view, ciImage)
renderView(view, currentDrawable, commandBuffer, ciImage)
commitBuffer(currentDrawable, commandBuffer)
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
}
private extension CameraMetalView {
func changeDrawableSize(_ view: MTKView, _ ciImage: CIImage) {
view.drawableSize = ciImage.extent.size
}
func renderView(_ view: MTKView, _ currentDrawable: any CAMetalDrawable, _ commandBuffer: any MTLCommandBuffer, _ ciImage: CIImage) { ciContext.render(
ciImage,
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: .init(origin: .zero, size: view.drawableSize),
colorSpace: CGColorSpaceCreateDeviceRGB()
)}
func commitBuffer(_ currentDrawable: any CAMetalDrawable, _ commandBuffer: any MTLCommandBuffer) {
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}