mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
329 lines
11 KiB
Swift
329 lines
11 KiB
Swift
/*
|
|
* Atoll (DynamicIsland)
|
|
* Copyright (C) 2024-2026 Atoll Contributors
|
|
*
|
|
* Originally from boring.notch project
|
|
* Modified and adapted for Atoll (DynamicIsland)
|
|
* See NOTICE for details.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import AVFoundation
|
|
import SwiftUI
|
|
|
|
class WebcamManager: NSObject, ObservableObject {
|
|
static let shared = WebcamManager()
|
|
|
|
@Published var previewLayer: AVCaptureVideoPreviewLayer? {
|
|
didSet {
|
|
objectWillChange.send()
|
|
}
|
|
}
|
|
|
|
private var captureSession: AVCaptureSession?
|
|
@Published var isSessionRunning: Bool = false {
|
|
didSet {
|
|
objectWillChange.send()
|
|
}
|
|
}
|
|
|
|
@Published var authorizationStatus: AVAuthorizationStatus = .notDetermined {
|
|
didSet {
|
|
objectWillChange.send()
|
|
}
|
|
}
|
|
|
|
@Published var cameraAvailable: Bool = false {
|
|
didSet {
|
|
objectWillChange.send()
|
|
}
|
|
}
|
|
|
|
private let sessionQueue = DispatchQueue(label: "DynamicIsland.WebcamManager.SessionQueue", qos: .userInitiated)
|
|
|
|
private var isCleaningUp: Bool = false
|
|
|
|
// MARK: - Constants
|
|
|
|
enum WebcamError: Error, LocalizedError {
|
|
case deviceUnavailable
|
|
case accessDenied
|
|
case configurationFailed(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .deviceUnavailable:
|
|
return "No camera devices available"
|
|
case .accessDenied:
|
|
return "Camera access denied"
|
|
case .configurationFailed(let message):
|
|
return "Camera configuration failed: \(message)"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Properties
|
|
|
|
private override init() {
|
|
super.init()
|
|
NotificationCenter.default.addObserver(self, selector: #selector(deviceWasDisconnected), name: .AVCaptureDeviceWasDisconnected, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(deviceWasConnected), name: .AVCaptureDeviceWasConnected, object: nil)
|
|
checkCameraAvailability()
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
|
|
if let session = captureSession {
|
|
if session.isRunning {
|
|
session.stopRunning()
|
|
}
|
|
}
|
|
captureSession = nil
|
|
|
|
previewLayer = nil
|
|
}
|
|
|
|
// MARK: - Camera Management
|
|
|
|
/// Checks current authorization status and requests access if needed
|
|
func checkAndRequestVideoAuthorization() {
|
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
DispatchQueue.main.async {
|
|
self.authorizationStatus = status
|
|
}
|
|
|
|
switch status {
|
|
case .authorized:
|
|
checkCameraAvailability() // Check availability if authorized
|
|
case .notDetermined:
|
|
requestVideoAccess()
|
|
case .denied, .restricted:
|
|
NSLog("Camera access denied or restricted")
|
|
@unknown default:
|
|
NSLog("Unknown authorization status")
|
|
}
|
|
}
|
|
|
|
/// Requests access to the camera
|
|
private func requestVideoAccess() {
|
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
|
DispatchQueue.main.async {
|
|
self?.authorizationStatus = granted ? .authorized : .denied
|
|
if granted {
|
|
self?.checkCameraAvailability() // Check availability if access granted
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Checks if any camera devices are available and sets up capture session if needed
|
|
func checkCameraAvailability() {
|
|
let availableDevices = AVCaptureDevice.DiscoverySession(
|
|
deviceTypes: [.external, .builtInWideAngleCamera],
|
|
mediaType: .video,
|
|
position: .unspecified
|
|
).devices
|
|
|
|
let hasAvailableDevices = !availableDevices.isEmpty
|
|
|
|
DispatchQueue.main.async {
|
|
self.cameraAvailable = hasAvailableDevices
|
|
}
|
|
}
|
|
|
|
/// Sets up the capture session with a completion handler
|
|
private func setupCaptureSession(completion: @escaping (Bool) -> Void) {
|
|
sessionQueue.async { [weak self] in
|
|
guard let self = self else {
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
// Clean up any existing session before creating a new one
|
|
self.cleanupExistingSession()
|
|
|
|
let session = AVCaptureSession()
|
|
|
|
do {
|
|
// Get available devices and prefer external camera if available
|
|
let discoverySession = AVCaptureDevice.DiscoverySession(
|
|
deviceTypes: [.external, .builtInWideAngleCamera],
|
|
mediaType: .video,
|
|
position: .unspecified
|
|
)
|
|
|
|
guard let videoDevice = discoverySession.devices.first else {
|
|
NSLog("No video devices available")
|
|
DispatchQueue.main.async {
|
|
self.isSessionRunning = false
|
|
self.cameraAvailable = false
|
|
}
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
NSLog("Using camera: \(videoDevice.localizedName)")
|
|
|
|
// Lock device for configuration
|
|
try videoDevice.lockForConfiguration()
|
|
defer { videoDevice.unlockForConfiguration() }
|
|
|
|
let videoInput = try AVCaptureDeviceInput(device: videoDevice)
|
|
guard session.canAddInput(videoInput) else {
|
|
throw NSError(domain: "DynamicIsland.WebcamManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Cannot add video input"])
|
|
}
|
|
|
|
session.beginConfiguration()
|
|
session.sessionPreset = .high
|
|
session.addInput(videoInput)
|
|
|
|
let videoOutput = AVCaptureVideoDataOutput()
|
|
videoOutput.setSampleBufferDelegate(nil, queue: nil)
|
|
if session.canAddOutput(videoOutput) {
|
|
session.addOutput(videoOutput)
|
|
}
|
|
session.commitConfiguration()
|
|
|
|
self.captureSession = session
|
|
|
|
// Create and set up preview layer on main thread
|
|
DispatchQueue.main.async {
|
|
self.cameraAvailable = true
|
|
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
|
|
previewLayer.videoGravity = .resizeAspectFill
|
|
self.previewLayer = previewLayer
|
|
|
|
// Setup is complete, let the caller know
|
|
completion(true)
|
|
}
|
|
|
|
NSLog("Capture session setup completed successfully")
|
|
} catch {
|
|
NSLog("Failed to setup capture session: \(error.localizedDescription)")
|
|
DispatchQueue.main.async {
|
|
self.isSessionRunning = false
|
|
self.cameraAvailable = false
|
|
self.previewLayer = nil
|
|
}
|
|
completion(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Cleans up an existing capture session, removing all inputs and outputs
|
|
private func cleanupExistingSession() {
|
|
if let existingSession = self.captureSession {
|
|
// First stop the session if running
|
|
if existingSession.isRunning {
|
|
existingSession.stopRunning()
|
|
}
|
|
|
|
// Then perform configuration cleanup
|
|
existingSession.beginConfiguration()
|
|
|
|
// Remove all inputs and outputs
|
|
for input in existingSession.inputs {
|
|
existingSession.removeInput(input)
|
|
}
|
|
for output in existingSession.outputs {
|
|
existingSession.removeOutput(output)
|
|
}
|
|
|
|
existingSession.commitConfiguration()
|
|
self.captureSession = nil
|
|
|
|
// Clear preview layer on main thread
|
|
DispatchQueue.main.async {
|
|
self.previewLayer = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func deviceWasDisconnected(notification: Notification) {
|
|
NSLog("Camera device was disconnected")
|
|
sessionQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.stopSession()
|
|
DispatchQueue.main.async {
|
|
self.cameraAvailable = false
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func deviceWasConnected(notification: Notification) {
|
|
NSLog("Camera device was connected")
|
|
sessionQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
self.checkCameraAvailability()
|
|
}
|
|
}
|
|
|
|
private func updateSessionState() {
|
|
let isRunning = self.captureSession?.isRunning ?? false
|
|
DispatchQueue.main.async {
|
|
self.isSessionRunning = isRunning
|
|
}
|
|
}
|
|
|
|
func startSession() {
|
|
sessionQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
// If no session exists, create new session
|
|
if self.captureSession == nil {
|
|
self.setupCaptureSession { success in
|
|
if success {
|
|
// Only start the session if setup was successful
|
|
self.startRunningCaptureSession()
|
|
}
|
|
}
|
|
} else {
|
|
// Session already exists, just start it
|
|
self.startRunningCaptureSession()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startRunningCaptureSession() {
|
|
sessionQueue.async { [weak self] in
|
|
guard let self = self, let session = self.captureSession, !session.isRunning else {
|
|
return
|
|
}
|
|
|
|
session.startRunning()
|
|
|
|
// Update state on main thread
|
|
self.updateSessionState()
|
|
|
|
NSLog("Capture session started successfully")
|
|
}
|
|
}
|
|
|
|
func stopSession() {
|
|
sessionQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
// Update state to indicate we're stopping
|
|
DispatchQueue.main.async {
|
|
self.isSessionRunning = false
|
|
}
|
|
|
|
self.cleanupExistingSession()
|
|
|
|
NSLog("Capture session stopped and cleaned up")
|
|
}
|
|
}
|
|
}
|