Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ DerivedData/
.netrc

Scripts/cuckoo_generator
Scripts/cuckoonator
Scripts/run

Tests/Tests/Mock/GeneratedMocks.swift
Expand Down
11 changes: 7 additions & 4 deletions Scripts/gen-mocks.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
#!/bin/bash

ROOT_PATH="../"
ROOT_DIR_NAME=$(basename "$(cd "$ROOT_PATH" && pwd)")
if [ "$ROOT_DIR_NAME" != "Aespa" ]; then
echo "❌ Error: Script's not called in proper path."
ROOT_PATH=$(git rev-parse --show-toplevel)
if [ -z "$ROOT_PATH" ]; then
echo "❌ Error: Could not find repository root."
exit 1
fi

if [ ! -f run ]; then
curl -Lo run https://raw.githubusercontent.com/Brightify/Cuckoo/master/run && chmod +x run
fi

if ! grep -q "cuckoo_generator" run; then
perl -pi -e 's/grep "\$GENERATOR_NAME"/grep -E "\$GENERATOR_NAME|cuckoo_generator"/' run
fi

PROJECT_NAME="Aespa"
TESTER_NAME="TestHostApp"
PACKAGE_SOURCE_PATH="${ROOT_PATH}/Sources/Aespa"
Expand Down
1 change: 1 addition & 0 deletions Scripts/version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.10.3
59 changes: 47 additions & 12 deletions Sources/Aespa/Aespa.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
// Created by 이영빈 on 2023/06/02.
//

import Foundation

/// Top-level class that serves as the main access point for video recording sessions.
open class Aespa {
nonisolated open class Aespa {
/// The core `AespaSession` that manages the actual video recording session.
private static var core: AespaSession?
nonisolated(unsafe) private static var core: AespaSession?
private static let coreLock = NSRecursiveLock()

/// Creates a new `AespaSession` with the given options.
///
Expand All @@ -19,7 +22,17 @@ open class Aespa {
with option: AespaOption,
onComplete: @escaping CompletionHandler = { _ in }
) -> AespaSession {
if let core { return core }
let existingCore: AespaSession? = withCoreLock {
if let core {
return core
}

return nil
}

if let existingCore {
return existingCore
}

let newCore = AespaSession(option: option)

Expand All @@ -28,16 +41,28 @@ open class Aespa {

// Configure session now
Task {
guard
case .permitted = await AuthorizationChecker.checkCaptureAuthorizationStatus()
else {
throw AespaError.permission(reason: .denied)
do {
guard
case .permitted = await AuthorizationChecker.checkCaptureAuthorizationStatus()
else {
throw AespaError.permission(reason: .denied)
}

newCore.startSession { result in
if case .success = result {
Self.withCoreLock {
if core == nil {
core = newCore
}
}
}
onComplete(result)
}
} catch {
onComplete(.failure(error))
}

newCore.startSession(onComplete)
}

core = newCore
return newCore
}

Expand All @@ -46,13 +71,23 @@ open class Aespa {
/// If a session has been started, it stops the session and releases resources.
/// After termination, a new session needs to be configured to start recording again.
public static func terminate(_ onComplete: @escaping CompletionHandler = { _ in }) throws {
guard let core = core else {
guard let core = withCoreLock({ core }) else {
return
}

core.terminateSession { result in
self.core = nil
Self.withCoreLock {
self.core = nil
}
onComplete(result)
}
}
}

nonisolated private extension Aespa {
static func withCoreLock<T>(_ work: () throws -> T) rethrows -> T {
coreLock.lock()
defer { coreLock.unlock() }
return try work()
}
}
12 changes: 6 additions & 6 deletions Sources/Aespa/AespaOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public typealias FileNamingRule = () -> String

/// `AespaOption` allows customization of various aspects of the video recording process,
/// such as the video asset configuration, session settings and logging preferences.
public struct AespaOption {
nonisolated public struct AespaOption: @unchecked Sendable {
/// `Asset` configuration object which encapsulates options related to the media assets such as
/// the album name, file extension and naming convention for the files.
public var asset: Asset
Expand Down Expand Up @@ -52,10 +52,10 @@ public struct AespaOption {
}
}

public extension AespaOption {
nonisolated public extension AespaOption {
/// `Asset` provides options for configuring the video assets,
/// such as the album name, file naming rule, and file extension.
struct Asset {
struct Asset: @unchecked Sendable {
/// The name of the album where recorded assets will be saved.
public var albumName: String

Expand Down Expand Up @@ -99,7 +99,7 @@ public extension AespaOption {

/// `Session` provides options for configuring the video recording session,
/// such as automatic video orientation.
struct Session {
struct Session: Sendable {
/// An `AVCaptureDevice.DeviceType` value that determines camera device.
/// If not specified, the device is automatically selected.
public var cameraDevicePreference: AVCaptureDevice.DeviceType?
Expand All @@ -110,7 +110,7 @@ public extension AespaOption {
}

/// `Log` provides an option for enabling or disabling logging.
struct Log {
struct Log: Sendable {
/// Enable logging
public var loggingEnabled: Bool = true

Expand All @@ -121,7 +121,7 @@ public extension AespaOption {
}

/// `AespaOption` extension related to file naming rules and file extensions.
public extension AespaOption {
nonisolated public extension AespaOption {
/// `FileNamingRulePreset` provides pre-configured file naming rules.
enum FileNamingRulePreset {
public struct Timestamp {
Expand Down
45 changes: 28 additions & 17 deletions Sources/Aespa/AespaSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import AVFoundation
/// For more option, you can use customization method to handle session with your own logic.
///
/// It also includes functionalities to fetch video files.
open class AespaSession {
nonisolated open class AespaSession: @unchecked Sendable {
let option: AespaOption
let coreSession: AespaCoreSession
private let albumManager: AespaCoreAlbumManager
Expand Down Expand Up @@ -142,14 +142,18 @@ open class AespaSession {
/// Publishes events related to video assets,
/// allowing subscribers to react to delete or add event in video assets.
public var videoAssetEventPublisher: AnyPublisher<AssetEvent, Never> {
eventManager.videoAssetEventPublihser.eraseToAnyPublisher()
eventManager.videoAssetEventPublihser
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}


/// Publishes events related to photo assets,
/// allowing subscribers to react to delete or add event in photo assets.
public var photoAssetEventPublisher: AnyPublisher<AssetEvent, Never> {
eventManager.photoAssetEventPublihser.eraseToAnyPublisher()
eventManager.photoAssetEventPublihser
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}

/// This property indicates whether the camera device is set to monitor changes in the subject area.
Expand All @@ -166,11 +170,13 @@ open class AespaSession {
/// A log message is printed to the console every time a new layer is pushed.
/// If you don't want to show logs, set `enableLogging` to `false` from `AespaOption.Log`
public var previewLayerPublisher: AnyPublisher<AVCaptureVideoPreviewLayer, Never> {
previewLayerSubject.handleEvents(receiveOutput: { _ in
Logger.log(message: "Preview layer is updated")
})
.compactMap { $0 }
.eraseToAnyPublisher()
previewLayerSubject
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.handleEvents(receiveOutput: { _ in
Logger.log(message: "Preview layer is updated")
})
.eraseToAnyPublisher()
}

// MARK: - Utilities
Expand Down Expand Up @@ -218,7 +224,7 @@ open class AespaSession {
}
}

extension AespaSession: CommonContext {
nonisolated extension AespaSession: CommonContext {
public var underlyingCommonContext: AespaSession {
self
}
Expand Down Expand Up @@ -265,7 +271,7 @@ extension AespaSession: CommonContext {
}
}

extension AespaSession: VideoContext {
nonisolated extension AespaSession: VideoContext {
public typealias AespaVideoSessionContext = AespaVideoContext<AespaSession>

public var underlyingVideoContext: AespaVideoSessionContext {
Expand All @@ -292,7 +298,7 @@ extension AespaSession: VideoContext {
videoContext.startRecording(at: path, autoVideoOrientationEnabled: autoVideoOrientationEnabled, onComplete)
}

public func stopRecording(_ completionHandler: @escaping (Result<VideoFile, Error>) -> Void = { _ in }) {
public func stopRecording(_ completionHandler: @escaping ResultHandler<VideoFile> = { _ in }) {
videoContext.stopRecording(completionHandler)
}

Expand All @@ -310,7 +316,7 @@ extension AespaSession: VideoContext {
}
}

extension AespaSession: PhotoContext {
nonisolated extension AespaSession: PhotoContext {
public var underlyingPhotoContext: AespaPhotoContext {
photoContext
}
Expand All @@ -325,7 +331,7 @@ extension AespaSession: PhotoContext {

public func capturePhoto(
autoVideoOrientationEnabled: Bool = false,
_ completionHandler: @escaping (Result<PhotoFile, Error>) -> Void = { _ in }
_ completionHandler: @escaping ResultHandler<PhotoFile> = { _ in }
) {
photoContext.capturePhoto(autoVideoOrientationEnabled: autoVideoOrientationEnabled, completionHandler)

Expand All @@ -345,13 +351,18 @@ extension AespaSession: PhotoContext {
}
}

extension AespaSession {
nonisolated extension AespaSession {
func startSession(_ onComplete: @escaping CompletionHandler) {
do {
try coreSession.start()
previewLayerSubject.send(previewLayer)
previewLayerSubject.sendOnMainThread(previewLayer)
DispatchQueue.main.async {
onComplete(.success(()))
}
} catch let error {
onComplete(.failure(error))
DispatchQueue.main.async {
onComplete(.failure(error))
}
}
}

Expand All @@ -362,7 +373,7 @@ extension AespaSession {
}

// MARK: - Deprecated methods
extension AespaSession {
nonisolated extension AespaSession {
@available(*, deprecated, message: "Please use `common` instead.")
@discardableResult
public func quality(
Expand Down
Loading