Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 72 additions & 33 deletions Sources/Internal/Manager/CameraManager+MotionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import CoreMotion
import AVKit

@MainActor class CameraManagerMotionManager {
private(set) var parent: CameraManager!
private(set) weak var parent: CameraManager?
private(set) var manager: CMMotionManager = .init()
}

Expand All @@ -27,7 +27,7 @@ extension CameraManagerMotionManager {
}
private extension CameraManagerMotionManager {
func handleAccelerometerUpdates(_ data: CMAccelerometerData?, _ error: Error?) {
guard let data, error == nil else { return }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe with all of that, the ! should be removed from:

    private(set) weak var parent: CameraManager!

We should not pretend it can not be nil if it can.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bogdan updated in 8fde7fb. Sorry about that.

guard let data, error == nil, parent != nil else { return }

let newDeviceOrientation = getDeviceOrientation(data.acceleration)
updateDeviceOrientation(newDeviceOrientation)
Expand All @@ -37,67 +37,106 @@ private extension CameraManagerMotionManager {
}
}
private extension CameraManagerMotionManager {
func getDeviceOrientation(_ acceleration: CMAcceleration) -> AVCaptureVideoOrientation { switch acceleration {
func getDeviceOrientation(_ acceleration: CMAcceleration) -> AVCaptureVideoOrientation {
switch acceleration {
case let acceleration where acceleration.x >= 0.75: .landscapeLeft
case let acceleration where acceleration.x <= -0.75: .landscapeRight
case let acceleration where acceleration.y <= -0.75: .portrait
case let acceleration where acceleration.y >= 0.75: .portraitUpsideDown
default: parent.attributes.deviceOrientation
}}
func updateDeviceOrientation(_ newDeviceOrientation: AVCaptureVideoOrientation) { if newDeviceOrientation != parent.attributes.deviceOrientation {
parent.attributes.deviceOrientation = newDeviceOrientation
}}
default: parent?.attributes.deviceOrientation ?? AVCaptureVideoOrientation.portrait
}
}
func updateDeviceOrientation(_ newDeviceOrientation: AVCaptureVideoOrientation) {
if newDeviceOrientation != parent?.attributes.deviceOrientation {
parent?.attributes.deviceOrientation = newDeviceOrientation
}
}
func updateUserBlockedScreenRotation() {
let newUserBlockedScreenRotation = getNewUserBlockedScreenRotation()
if newUserBlockedScreenRotation != parent.attributes.userBlockedScreenRotation { parent.attributes.userBlockedScreenRotation = newUserBlockedScreenRotation }
if newUserBlockedScreenRotation != parent?.attributes.userBlockedScreenRotation {
parent?.attributes.userBlockedScreenRotation = newUserBlockedScreenRotation
}
}
func updateFrameOrientation() {
guard let parent else { return }

if UIDevice.current.orientation != .portraitUpsideDown {
let newFrameOrientation = getNewFrameOrientation(parent.attributes.orientationLocked ? .portrait : UIDevice.current.orientation)
updateFrameOrientation(newFrameOrientation)
}
}
func redrawGrid() {
guard let parent else { return }

if !parent.attributes.orientationLocked {
parent.cameraGridView.draw(.zero)
}
}
func updateFrameOrientation() { if UIDevice.current.orientation != .portraitUpsideDown {
let newFrameOrientation = getNewFrameOrientation(parent.attributes.orientationLocked ? .portrait : UIDevice.current.orientation)
updateFrameOrientation(newFrameOrientation)
}}
func redrawGrid() { if !parent.attributes.orientationLocked {
parent.cameraGridView.draw(.zero)
}}
}
private extension CameraManagerMotionManager {
func getNewUserBlockedScreenRotation() -> Bool { switch parent.attributes.deviceOrientation.rawValue == UIDevice.current.orientation.rawValue {
func getNewUserBlockedScreenRotation() -> Bool {
guard let parent else { return false }

return switch parent.attributes.deviceOrientation.rawValue == UIDevice.current.orientation.rawValue {
case true: false
case false: !parent.attributes.orientationLocked
}}
func getNewFrameOrientation(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch parent.attributes.cameraPosition {
}
}
func getNewFrameOrientation(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation {
guard let parent else { return .up }

return switch parent.attributes.cameraPosition {
case .back: getNewFrameOrientationForBackCamera(orientation)
case .front: getNewFrameOrientationForFrontCamera(orientation)
}}
func updateFrameOrientation(_ newFrameOrientation: CGImagePropertyOrientation) { if newFrameOrientation != parent.attributes.frameOrientation {
let shouldAnimate = shouldAnimateFrameOrientationChange(newFrameOrientation)
updateFrameOrientation(withAnimation: shouldAnimate, newFrameOrientation: newFrameOrientation)
}}
}
}
func updateFrameOrientation(_ newFrameOrientation: CGImagePropertyOrientation) {
guard let parent else { return }

if newFrameOrientation != parent.attributes.frameOrientation {
let shouldAnimate = shouldAnimateFrameOrientationChange(newFrameOrientation)
updateFrameOrientation(withAnimation: shouldAnimate, newFrameOrientation: newFrameOrientation)
}
}
}
private extension CameraManagerMotionManager {
func getNewFrameOrientationForBackCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch orientation {
func getNewFrameOrientationForBackCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation {
guard let parent else { return .up }

return switch orientation {
case .portrait: parent.attributes.mirrorOutput ? .leftMirrored : .right
case .landscapeLeft: parent.attributes.mirrorOutput ? .upMirrored : .up
case .landscapeRight: parent.attributes.mirrorOutput ? .downMirrored : .down
default: parent.attributes.mirrorOutput ? .leftMirrored : .right
}}
func getNewFrameOrientationForFrontCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation { switch orientation {
}
}
func getNewFrameOrientationForFrontCamera(_ orientation: UIDeviceOrientation) -> CGImagePropertyOrientation {
guard let parent else { return .up }

return switch orientation {
case .portrait: parent.attributes.mirrorOutput ? .right : .leftMirrored
case .landscapeLeft: parent.attributes.mirrorOutput ? .down : .downMirrored
case .landscapeRight: parent.attributes.mirrorOutput ? .up : .upMirrored
default: parent.attributes.mirrorOutput ? .right : .leftMirrored
}}
}
}
func shouldAnimateFrameOrientationChange(_ newFrameOrientation: CGImagePropertyOrientation) -> Bool {
guard let parent else { return false }

let backCameraOrientations: [CGImagePropertyOrientation] = [.left, .right, .up, .down],
frontCameraOrientations: [CGImagePropertyOrientation] = [.leftMirrored, .rightMirrored, .upMirrored, .downMirrored]

return (backCameraOrientations.contains(newFrameOrientation) && backCameraOrientations.contains(parent.attributes.frameOrientation)) ||
(frontCameraOrientations.contains(parent.attributes.frameOrientation) && frontCameraOrientations.contains(newFrameOrientation))
}
func updateFrameOrientation(withAnimation shouldAnimate: Bool, newFrameOrientation: CGImagePropertyOrientation) { Task {
await parent.cameraMetalView.beginCameraOrientationAnimation(if: shouldAnimate)
parent.attributes.frameOrientation = newFrameOrientation
parent.cameraMetalView.finishCameraOrientationAnimation(if: shouldAnimate)
}}
func updateFrameOrientation(withAnimation shouldAnimate: Bool, newFrameOrientation: CGImagePropertyOrientation) {
Task {
guard let parent else { return }
await parent.cameraMetalView.beginCameraOrientationAnimation(if: shouldAnimate)
parent.attributes.frameOrientation = newFrameOrientation
parent.cameraMetalView.finishCameraOrientationAnimation(if: shouldAnimate)
}
}
}

// MARK: Reset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import Foundation

@MainActor class CameraManagerNotificationCenter {
private(set) var parent: CameraManager!
private(set) weak var parent: CameraManager?
}

// MARK: Setup
Expand All @@ -24,6 +24,8 @@ extension CameraManagerNotificationCenter {
}
private extension CameraManagerNotificationCenter {
@objc func handleSessionWasInterrupted() {
guard let parent else { return }

parent.attributes.lightMode = .off
parent.videoOutput.reset()
}
Expand Down
23 changes: 16 additions & 7 deletions Sources/Internal/Manager/CameraManager+PhotoOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
import AVKit

@MainActor class CameraManagerPhotoOutput: NSObject {
private(set) var parent: CameraManager!
private(set) weak 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)
try parent.captureSession.add(output: output)
}
}

Expand All @@ -36,17 +36,21 @@ extension CameraManagerPhotoOutput {

configureOutput()
output.capturePhoto(with: settings, delegate: self)
parent.cameraMetalView.performImageCaptureAnimation()
parent?.cameraMetalView.performImageCaptureAnimation()
}
}
private extension CameraManagerPhotoOutput {
func getPhotoOutputSettings() -> AVCapturePhotoSettings {
let settings = AVCapturePhotoSettings()
settings.flashMode = parent.attributes.flashMode.toDeviceFlashMode()
if let parent {
settings.flashMode = parent.attributes.flashMode.toDeviceFlashMode()
}
return settings
}
func configureOutput() {
guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return }
guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported,
let parent
else { return }

connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front
connection.videoOrientation = parent.attributes.deviceOrientation
Expand All @@ -57,7 +61,8 @@ private extension CameraManagerPhotoOutput {
extension CameraManagerPhotoOutput: @preconcurrency AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: (any Error)?) {
guard let imageData = photo.fileDataRepresentation(),
let ciImage = CIImage(data: imageData)
let ciImage = CIImage(data: imageData),
let parent
else { return }

let capturedCIImage = prepareCIImage(ciImage, parent.attributes.cameraFilters)
Expand Down Expand Up @@ -86,7 +91,11 @@ private extension CameraManagerPhotoOutput {
}
private extension CameraManagerPhotoOutput {
func getFixedFrameOrientation() -> CGImagePropertyOrientation {
guard UIDevice.current.orientation != parent.attributes.deviceOrientation.toDeviceOrientation() else { return parent.attributes.frameOrientation }
guard let parent,
UIDevice.current.orientation != parent.attributes.deviceOrientation.toDeviceOrientation()
else {
return parent?.attributes.frameOrientation ?? .up
}

return switch (parent.attributes.deviceOrientation, parent.attributes.cameraPosition) {
case (.portrait, .front): .left
Expand Down
44 changes: 30 additions & 14 deletions Sources/Internal/Manager/CameraManager+VideoOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftUI
import MijickTimer

@MainActor class CameraManagerVideoOutput: NSObject {
private(set) var parent: CameraManager!
private(set) weak var parent: CameraManager?
private(set) var output: AVCaptureMovieFileOutput = .init()
private(set) var timer: MTimer = .init(.camera)
private(set) var recordingTime: MTime = .zero
Expand Down Expand Up @@ -58,29 +58,42 @@ private extension CameraManagerVideoOutput {
storeLastFrame()
output.startRecording(to: url, recordingDelegate: self)
startRecordingTimer()
parent.objectWillChange.send()
parent?.objectWillChange.send()
}
}
private extension CameraManagerVideoOutput {
func prepareUrlForVideoRecording() -> URL? {
FileManager.prepareURLForVideoOutput()
}
func configureOutput() {
guard let connection = output.connection(with: .video), connection.isVideoMirroringSupported else { return }
guard let connection = output.connection(with: .video),
connection.isVideoMirroringSupported,
let parent
else { return }

connection.isVideoMirrored = parent.attributes.mirrorOutput ? parent.attributes.cameraPosition != .front : parent.attributes.cameraPosition == .front
connection.videoOrientation = parent.attributes.deviceOrientation
}
func storeLastFrame() {
guard let texture = parent.cameraMetalView.currentDrawable?.texture,
guard let texture = parent?.cameraMetalView.currentDrawable?.texture,
let ciImage = CIImage(mtlTexture: texture, options: nil),
let cgImage = parent.cameraMetalView.ciContext.createCGImage(ciImage, from: ciImage.extent)
let cgImage = parent?.cameraMetalView.ciContext.createCGImage(ciImage, from: ciImage.extent),
let orientation = parent?.attributes.deviceOrientation.toImageOrientation()
else { return }

firstRecordedFrame = UIImage(cgImage: cgImage, scale: 1.0, orientation: parent.attributes.deviceOrientation.toImageOrientation())
firstRecordedFrame = UIImage(
cgImage: cgImage,
scale: 1.0,
orientation: orientation
)
}
func startRecordingTimer() { try? timer
.publish(every: 1) { [self] in
guard let parent else {
timer.cancel()
return
}

recordingTime = $0
parent.objectWillChange.send()
}
Expand All @@ -99,19 +112,22 @@ private extension CameraManagerVideoOutput {
private extension CameraManagerVideoOutput {
func presentLastFrame() {
let firstRecordedFrame = MCameraMedia(data: firstRecordedFrame)
parent.setCapturedMedia(firstRecordedFrame)
parent?.setCapturedMedia(firstRecordedFrame)
}
}

// MARK: Receive Data
extension CameraManagerVideoOutput: @preconcurrency AVCaptureFileOutputRecordingDelegate {
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: (any Error)?) { Task {
let videoURL = try await prepareVideo(outputFileURL: outputFileURL, cameraFilters: parent.attributes.cameraFilters)
let capturedVideo = MCameraMedia(data: videoURL)

await Task.sleep(seconds: Animation.duration)
parent.setCapturedMedia(capturedVideo)
}}
func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: (any Error)?) {
guard let cameraFilters = parent?.attributes.cameraFilters else { return }
Task {
let videoURL = try await prepareVideo(outputFileURL: outputFileURL, cameraFilters: cameraFilters)
let capturedVideo = MCameraMedia(data: videoURL)

await Task.sleep(seconds: Animation.duration)
parent?.setCapturedMedia(capturedVideo)
}
}
}
private extension CameraManagerVideoOutput {
func prepareVideo(outputFileURL: URL, cameraFilters: [CIFilter]) async throws -> URL {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Internal/UI/Camera View/CameraView+Grid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import SwiftUI

class CameraGridView: UIView {
var parent: CameraManager!
weak var parent: CameraManager?
}

// MARK: Setup
Expand All @@ -28,7 +28,7 @@ extension CameraGridView {
extension CameraGridView {
func setVisibility(_ isVisible: Bool) {
UIView.animate(withDuration: 0.2) { self.alpha = isVisible ? 1 : 0 }
parent.attributes.isGridVisible = isVisible
parent?.attributes.isGridVisible = isVisible
}
}

Expand Down
Loading