Skip to content

Commit

Permalink
CameraKage launch-up
Browse files Browse the repository at this point in the history
  • Loading branch information
andreilob committed May 28, 2023
1 parent e25e9d6 commit 51860d0
Show file tree
Hide file tree
Showing 19 changed files with 1,347 additions and 0 deletions.
9 changes: 9 additions & 0 deletions CameraKage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
24 changes: 24 additions & 0 deletions CameraKage/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "CameraKage",
platforms: [
.iOS(.v15)
],
products: [
.library(
name: "CameraKage",
targets: ["CameraKage"]),
],
targets: [
.target(
name: "CameraKage",
path: "Sources"),
.testTarget(
name: "CameraKageTests",
dependencies: ["CameraKage"]),
]
)
3 changes: 3 additions & 0 deletions CameraKage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# CameraKage

A description of this package.
305 changes: 305 additions & 0 deletions CameraKage/Sources/CameraKage/CameraKage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import UIKit
import AVFoundation

/// The main interface to use the `CameraKage` camera features.
public class CameraKage: UIView {
private var sessionComposer: SessionComposable = SessionComposer()
private let sessionQueue = DispatchQueue(label: "LA.cameraKage.sessionQueue")
private let permissionManager: PermissionsManagerProtocol = PermissionsManager()
private var cameraComponent: CameraComponent!
private let delegates: NSHashTable<AnyObject> = NSHashTable.weakObjects()

/// Determines if the `AVCaptureSession` of `CameraKage` is running.
public var isSessionRunning: Bool { sessionComposer.isSessionRunning }

/// Determines if `CameraKage` has a video recording in progress.
public private(set) var isRecording: Bool = false

/**
Register a listener for the `CameraKage` to receive notifications regarding the camera session.

- parameter delegate: The object that will receive the notifications.
*/
public func registerDelegate(_ delegate: CameraKageDelegate) {
delegates.add(delegate as AnyObject)
}

/**
Unregisters a listener from receiving `CameraKage` notifications.

- parameter delegate: The object to be removed.
*/
public func unregisterDelegate(_ delegate: CameraKageDelegate) {
delegates.remove(delegate as AnyObject)
}

/**
Prompts the user with the system alert to grant permission for the camera usage.

- returns: Returns asynchronously a `Bool` specifying if the access was granted or not.

- important: Info.plist key `NSCameraUsageDescription` must be set otherwise the application will crash.
*/
public func requestCameraPermission() async -> Bool {
await permissionManager.requestAccess(for: .video)
}

/**
Prompts the user with the system alert to grant permission for the camera usage.

- parameter completion: Callback containing a `Bool` result specifying if access was granted or not.

- important: Info.plist key `NSCameraUsageDescription` must be set otherwise the application will crash.
*/
public func requestCameraPermission(completion: @escaping((Bool) -> Void)) {
permissionManager.requestAccess(for: .video, completion: completion)
}

/**
Prompts the user with the system alert to grant permission for the microphone usage.

- returns: Returns asynchronously a `Bool` specifying if the access was granted or not.

- important: Info.plist key `NSMicrophoneUsageDescription` must be set otherwise the application will crash.
*/
public func requestMicrophonePermission() async -> Bool {
await permissionManager.requestAccess(for: .audio)
}

/**
Prompts the user with the system alert to grant permission for the microphone usage.

- parameter completion: Completion containing `Bool` result specifying if access was granted or not.

- important: Info.plist key `NSMicrophoneUsageDescription` must be set otherwise the application will crash.
*/
public func requestMicrophonePermission(completion: @escaping((Bool) -> Void)) {
permissionManager.requestAccess(for: .audio, completion: completion)
}

/**
Checks the current camera permission status.

- returns: Returns the current status.

- important: `getCameraPermissionStatus()` won't request access to the user. Use `requestCameraPermission()` to prompt the system alert.
*/
public func getCameraPermissionStatus() -> PermissionStatus {
permissionManager.getAuthorizationStatus(for: .video)
}

/**
Checks the current microphone permission status.

- returns: Returns the current status.

- important: `getMicrophonePermissionStatus()` won't request access to the user. Use `requestMicrophonePermission()` to prompt the system alert.
*/
public func getMicrophonePermissionStatus() -> PermissionStatus {
permissionManager.getAuthorizationStatus(for: .audio)
}

/**
Starts a discovery session to get the available camera devices for the client's phone.

- returns: Returns the list of available `AVCaptureDevice`.
*/
public func getSupportedCameraDevices() -> [AVCaptureDevice] {
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [
AVCaptureDevice.DeviceType.builtInWideAngleCamera,
AVCaptureDevice.DeviceType.builtInUltraWideCamera,
AVCaptureDevice.DeviceType.builtInTelephotoCamera,
AVCaptureDevice.DeviceType.builtInDualCamera,
AVCaptureDevice.DeviceType.builtInDualWideCamera,
AVCaptureDevice.DeviceType.builtInTripleCamera,
AVCaptureDevice.DeviceType.builtInTrueDepthCamera
],
mediaType: .video,
position: .unspecified)
return discoverySession.devices
}

/**
Starts the camera session.

- parameter options: Options used for the camera setup

- important: Before calling `startCameraSession`, `requestCameraPermission()` and `requestMicrophonePermission()` methods can be called for custom UI usage. If permission requests aren't used, the system will call the alerts automatically.
*/
public func startCameraSession(with options: CameraComponentParsedOptions = CameraComponentParsedOptions(nil)) {
setupCameraComponent(with: options)
setupSessionDelegate()
sessionQueue.async { [weak self] in
guard let self else { return }
sessionComposer.startSession()
}
}

/**
Stops the camera session and destroys the camera component.
*/
public func stopCameraSession() {
destroyCameraComponent()
sessionQueue.async { [weak self] in
guard let self else { return }
sessionComposer.stopSession()
}
}

/**
Captures a photo from the camera. Resulted photo will be delivered via `CameraKageDelegate`.

- parameter flashOption: Indicates what flash option should be used when capturing the photo. Default is `.off`.
- parameter redEyeCorrection: Determines if red eye correction should be applied or not. Default is `true`.
*/
public func capturePhoto(_ flashOption: AVCaptureDevice.FlashMode = .off,
redEyeCorrection: Bool = true) {
sessionQueue.async { [weak self] in
guard let self else { return }
cameraComponent.capturePhoto(flashOption, redEyeCorrection: redEyeCorrection)
}
}

/**
Starts a video recording for the camera. `CameraKageDelegate` sends a notification when the recording has started.
*/
public func startVideoRecording() {
sessionQueue.async { [weak self] in
guard let self, !isRecording else { return }
isRecording = true
cameraComponent.startMovieRecording()
}
}

/**
Stops the video recording. `CameraKageDelegate` sends a notification containing the URL where the video file is stored.
*/
public func stopVideoRecording() {
sessionQueue.async { [weak self] in
guard let self, isRecording else { return }
isRecording = false
cameraComponent.stopMovieRecording()
}
}

/**
Flips the camera from back to front and vice-versa.

- important: Camera can't be flipped while recording a video. Session is restarted when flipping the camera.
*/
public func flipCamera() {
sessionQueue.async { [weak self] in
guard let self, !isRecording else { return }
sessionComposer.pauseSession()
cameraComponent.flipCamera()
sessionComposer.resumeSession()
}
}

/**
Adjusts the focus and the exposure of the camera.

- parameter focusMode: Focus mode of the camera. Default is `.autoFocus`.
- parameter exposureMode: Exposure mode of the camera. Default is `.autoExpose`.
- parameter devicePoint: The point of the camera where the focus should be switched to.
- parameter monitorSubjectAreaChange: If set `true`, it registers the camera to receive notifications about area changes for the user to re-focus if needed. Default is `true`.
*/
public func adjustFocusAndExposure(with focusMode: AVCaptureDevice.FocusMode = .autoFocus,
exposureMode: AVCaptureDevice.ExposureMode = .autoExpose,
at devicePoint: CGPoint,
monitorSubjectAreaChange: Bool = true) {
sessionQueue.async { [weak self] in
guard let self else { return }
cameraComponent.focus(with: focusMode,
exposureMode: exposureMode,
at: devicePoint,
monitorSubjectAreaChange: monitorSubjectAreaChange)
}
}

private func setupCameraComponent(with options: CameraComponentParsedOptions) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
cameraComponent = CameraComponent(sessionComposer: sessionComposer,
options: options,
delegate: self)
addSubview(cameraComponent)
cameraComponent.layoutToFill(inView: self)
cameraComponent.configureSession()
}
}

private func destroyCameraComponent() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
cameraComponent.removeObserver()
cameraComponent.removeFromSuperview()
cameraComponent = nil
}
}

private func setupSessionDelegate() {
sessionComposer.onSessionStart = { [weak self] in
guard let self else { return }
invokeDelegates { $0.cameraSessionDidStart(self) }
}

sessionComposer.onSessionStop = { [weak self] in
guard let self else { return }
invokeDelegates { $0.cameraSessionDidStop(self) }
}

sessionComposer.onSessionInterruption = { [weak self] reason in
guard let self else { return }
invokeDelegates { $0.camera(self, sessionWasInterrupted: reason) }
}

sessionComposer.onSessionInterruptionEnd = { [weak self] in
guard let self else { return }
invokeDelegates { $0.cameraSessionInterruptionEnded(self) }
}

sessionComposer.onSessionReceiveRuntimeError = { [weak self] isRestartable, avError in
guard let self else { return }
if isRestartable {
sessionQueue.async { [weak self] in
guard let self else { return }
sessionComposer.resumeSession()
}
}
let sessionError = CameraError.CameraSessionErrorReason.runtimeError(avError)
invokeDelegates { $0.camera(self, didEncounterError: .cameraSessionError(reason: sessionError))}
}

sessionComposer.onDeviceSubjectAreaChange = { [weak self] in
guard let self else { return }
invokeDelegates { $0.cameraDeviceDidChangeSubjectArea(self) }
}
}

private func invokeDelegates(_ execute: (CameraKageDelegate) -> Void) {
delegates.allObjects.forEach { delegate in
guard let delegate = delegate as? CameraKageDelegate else { return }
execute(delegate)
}
}
}

// MARK: - CameraComponentDelegate
extension CameraKage: CameraComponentDelegate {
func cameraComponent(_ cameraComponent: CameraComponent, didCapturePhoto photo: Data) {
invokeDelegates { $0.camera(self, didOutputPhotoWithData: photo)}
}

func cameraComponent(_ cameraComponent: CameraComponent, didStartRecordingVideo atFileURL: URL) {
invokeDelegates { $0.camera(self, didStartRecordingVideoAtFileURL: atFileURL)}
}

func cameraComponent(_ cameraComponent: CameraComponent, didRecordVideo videoURL: URL) {
invokeDelegates { $0.camera(self, didOutputVideoAtFileURL: videoURL)}
}

func cameraComponent(_ cameraComponent: CameraComponent, didFail withError: CameraError) {
invokeDelegates { $0.camera(self, didEncounterError: withError) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// UIView+LayoutToFill.swift
// CameraKage
//
// Created by Lobont Andrei on 21.05.2023.
//

import UIKit

extension UIView {
func layoutToFill(inView: UIView) {
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([topAnchor.constraint(equalTo: inView.topAnchor),
leadingAnchor.constraint(equalTo: inView.leadingAnchor),
bottomAnchor.constraint(equalTo: inView.bottomAnchor),
trailingAnchor.constraint(equalTo: inView.trailingAnchor)])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// URL+TemporaryURL.swift
// CameraKage
//
// Created by Lobont Andrei on 22.05.2023.
//

import Foundation

extension URL {
static func makeTempUrl(for type: MediaType) -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
switch type {
case .photo: return url.appendingPathExtension("jpg")
case .video: return url.appendingPathExtension("mov")
}
}
}
Loading

0 comments on commit 51860d0

Please sign in to comment.