capacitor-camera-view 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -9
- package/android/build.gradle +8 -5
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +217 -126
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +70 -30
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +94 -5
- package/dist/docs.json +81 -0
- package/dist/esm/definitions.d.ts +44 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +7 -1
- package/dist/esm/web.js +67 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +68 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +68 -2
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +97 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -2
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +45 -13
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +112 -41
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +83 -84
- package/ios/Sources/CameraViewPlugin/TempFileManager.swift +181 -0
- package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
- package/package.json +17 -17
|
@@ -5,24 +5,24 @@ import Foundation
|
|
|
5
5
|
/// Please read the Capacitor iOS Plugin Development Guide
|
|
6
6
|
/// here: https://capacitorjs.com/docs/plugins/ios
|
|
7
7
|
@objc(CameraViewPlugin)
|
|
8
|
-
public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
8
|
+
public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate {
|
|
9
9
|
public let identifier = "CameraViewPlugin"
|
|
10
10
|
public let jsName = "CameraView"
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
/// Maps string flash mode values to AVCaptureDevice.FlashMode enum values.
|
|
13
13
|
private let strToFlashModeMap: [String: AVCaptureDevice.FlashMode] = [
|
|
14
14
|
"off": .off,
|
|
15
15
|
"on": .on,
|
|
16
16
|
"auto": .auto
|
|
17
17
|
]
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
/// Maps AVCaptureDevice.FlashMode enum values to string values.
|
|
20
20
|
private let flashModeToStrMap: [AVCaptureDevice.FlashMode: String] = [
|
|
21
21
|
.off: "off",
|
|
22
22
|
.on: "on",
|
|
23
23
|
.auto: "auto"
|
|
24
24
|
]
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
27
27
|
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
|
|
28
28
|
CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
|
|
@@ -42,45 +42,29 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
42
42
|
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
|
|
43
43
|
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise)
|
|
44
44
|
]
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
private let implementation = CameraViewManager()
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
49
48
|
override public func load() {
|
|
50
|
-
|
|
51
|
-
notificationObserver = NotificationCenter.default.addObserver(
|
|
52
|
-
forName: Notification.Name("barcodeDetected"),
|
|
53
|
-
object: nil,
|
|
54
|
-
queue: .main
|
|
55
|
-
) { [weak self] notification in
|
|
56
|
-
guard let self = self,
|
|
57
|
-
let barcodeData = notification.userInfo as? [String: Any] else {
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Emit event to JS
|
|
62
|
-
self.notifyListeners("barcodeDetected", data: barcodeData)
|
|
63
|
-
}
|
|
49
|
+
implementation.eventEmitter.delegate = self
|
|
64
50
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
NotificationCenter.default.removeObserver(observer)
|
|
69
|
-
}
|
|
51
|
+
|
|
52
|
+
public func cameraDidDetectBarcode(_ event: BarcodeDetectedEvent) {
|
|
53
|
+
notifyListeners("barcodeDetected", data: event.toDictionary())
|
|
70
54
|
}
|
|
71
|
-
|
|
55
|
+
|
|
72
56
|
@objc func start(_ call: CAPPluginCall) {
|
|
73
57
|
guard let webView = self.webView else {
|
|
74
58
|
call.reject("Cannot find web view")
|
|
75
59
|
return
|
|
76
60
|
}
|
|
77
|
-
|
|
61
|
+
|
|
78
62
|
maybeRequestCameraAccess { [weak self] granted in
|
|
79
63
|
guard granted else {
|
|
80
64
|
call.reject("Camera access denied")
|
|
81
65
|
return
|
|
82
66
|
}
|
|
83
|
-
|
|
67
|
+
|
|
84
68
|
self?.implementation.startSession(
|
|
85
69
|
configuration: sessionConfigFromPluginCall(call),
|
|
86
70
|
webView: webView,
|
|
@@ -93,55 +77,69 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
93
77
|
})
|
|
94
78
|
}
|
|
95
79
|
}
|
|
96
|
-
|
|
80
|
+
|
|
97
81
|
@objc func stop(_ call: CAPPluginCall) {
|
|
98
82
|
implementation.stopSession {
|
|
99
83
|
call.resolve()
|
|
100
84
|
}
|
|
101
85
|
}
|
|
102
|
-
|
|
86
|
+
|
|
103
87
|
@objc func isRunning(_ call: CAPPluginCall) {
|
|
104
88
|
call.resolve([
|
|
105
89
|
"isRunning": implementation.isRunning()
|
|
106
90
|
])
|
|
107
91
|
}
|
|
108
|
-
|
|
92
|
+
|
|
109
93
|
@objc func capture(_ call: CAPPluginCall) {
|
|
110
94
|
let quality = call.getDouble("quality", 90.0)
|
|
111
95
|
let saveToFile = call.getBool("saveToFile", false)
|
|
112
|
-
|
|
96
|
+
|
|
113
97
|
guard quality >= 0.0 && quality <= 100.0 else {
|
|
114
98
|
call.reject("Quality must be between 0 and 100")
|
|
115
99
|
return
|
|
116
100
|
}
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
|
|
102
|
+
// Use optimized Data-based capture to avoid double JPEG encoding
|
|
103
|
+
implementation.capturePhotoData(completion: { [weak self] (data, error) in
|
|
119
104
|
if let error = error {
|
|
120
105
|
call.reject("Failed to capture image", nil, error)
|
|
121
106
|
return
|
|
122
107
|
}
|
|
123
|
-
|
|
124
|
-
guard let
|
|
108
|
+
|
|
109
|
+
guard let originalData = data else {
|
|
125
110
|
call.reject("No image data")
|
|
126
111
|
return
|
|
127
112
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
113
|
+
|
|
114
|
+
// Determine final image data based on quality setting
|
|
115
|
+
// For quality >= 90%, use original camera JPEG data to avoid re-encoding
|
|
116
|
+
// For lower quality, re-encode to reduce file size
|
|
117
|
+
let imageData: Data
|
|
118
|
+
if quality >= 90.0 {
|
|
119
|
+
// Use original JPEG data from camera (avoids quality loss and CPU overhead)
|
|
120
|
+
imageData = originalData
|
|
121
|
+
} else {
|
|
122
|
+
// Re-encode at lower quality for smaller file size
|
|
123
|
+
guard let image = UIImage(data: originalData),
|
|
124
|
+
let compressedData = image.jpegData(compressionQuality: quality / 100.0) else {
|
|
125
|
+
call.reject("Failed to compress image")
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
imageData = compressedData
|
|
132
129
|
}
|
|
133
|
-
|
|
130
|
+
|
|
134
131
|
if saveToFile {
|
|
132
|
+
// Use TempFileManager for tracked temp files with automatic cleanup
|
|
133
|
+
let tempFileURL = TempFileManager.shared.createTempImageFile()
|
|
135
134
|
do {
|
|
136
|
-
let tempFileURL = try createTempImageFile()
|
|
137
135
|
try imageData.write(to: tempFileURL)
|
|
138
|
-
|
|
136
|
+
|
|
139
137
|
// Convert file URL to webView-accessible path using Capacitor bridge
|
|
140
|
-
guard let webPath = self
|
|
138
|
+
guard let webPath = self?.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
|
|
141
139
|
call.reject("Failed to create web-accessible path")
|
|
142
140
|
return
|
|
143
141
|
}
|
|
144
|
-
|
|
142
|
+
|
|
145
143
|
call.resolve(["webPath": webPath])
|
|
146
144
|
} catch {
|
|
147
145
|
call.reject("Failed to save image to file", nil, error)
|
|
@@ -154,43 +152,44 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
154
152
|
}
|
|
155
153
|
})
|
|
156
154
|
}
|
|
157
|
-
|
|
155
|
+
|
|
158
156
|
@objc func captureSample(_ call: CAPPluginCall) {
|
|
159
157
|
let quality = call.getDouble("quality", 90.0)
|
|
160
158
|
let saveToFile = call.getBool("saveToFile", false)
|
|
161
|
-
|
|
159
|
+
|
|
162
160
|
guard quality >= 0.0 && quality <= 100.0 else {
|
|
163
161
|
call.reject("Quality must be between 0 and 100")
|
|
164
162
|
return
|
|
165
163
|
}
|
|
166
|
-
|
|
167
|
-
implementation.captureSnapshot { (image, error) in
|
|
164
|
+
|
|
165
|
+
implementation.captureSnapshot { [weak self] (image, error) in
|
|
168
166
|
if let error = error {
|
|
169
167
|
call.reject("Failed to capture frame", nil, error)
|
|
170
168
|
return
|
|
171
169
|
}
|
|
172
|
-
|
|
170
|
+
|
|
173
171
|
guard let image = image else {
|
|
174
172
|
call.reject("No frame data")
|
|
175
173
|
return
|
|
176
174
|
}
|
|
177
|
-
|
|
175
|
+
|
|
178
176
|
guard let imageData = image.jpegData(compressionQuality: quality / 100.0) else {
|
|
179
177
|
call.reject("Failed to compress image")
|
|
180
178
|
return
|
|
181
179
|
}
|
|
182
|
-
|
|
180
|
+
|
|
183
181
|
if saveToFile {
|
|
182
|
+
// Use TempFileManager for tracked temp files with automatic cleanup
|
|
183
|
+
let tempFileURL = TempFileManager.shared.createTempImageFile()
|
|
184
184
|
do {
|
|
185
|
-
let tempFileURL = try createTempImageFile()
|
|
186
185
|
try imageData.write(to: tempFileURL)
|
|
187
|
-
|
|
186
|
+
|
|
188
187
|
// Convert file URL to webView-accessible path using Capacitor bridge
|
|
189
|
-
guard let webPath = self
|
|
188
|
+
guard let webPath = self?.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
|
|
190
189
|
call.reject("Failed to create web-accessible path")
|
|
191
190
|
return
|
|
192
191
|
}
|
|
193
|
-
|
|
192
|
+
|
|
194
193
|
call.resolve(["webPath": webPath])
|
|
195
194
|
} catch {
|
|
196
195
|
call.reject("Failed to save sample to file", nil, error)
|
|
@@ -203,10 +202,10 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
203
202
|
}
|
|
204
203
|
}
|
|
205
204
|
}
|
|
206
|
-
|
|
205
|
+
|
|
207
206
|
@objc func getAvailableDevices(_ call: CAPPluginCall) {
|
|
208
207
|
let devices = implementation.getAvailableDevices()
|
|
209
|
-
|
|
208
|
+
|
|
210
209
|
var result = JSArray()
|
|
211
210
|
for device in devices {
|
|
212
211
|
var deviceInfo = JSObject()
|
|
@@ -216,12 +215,12 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
216
215
|
deviceInfo["deviceType"] = convertToStringCameraType(device.deviceType)
|
|
217
216
|
result.append(deviceInfo)
|
|
218
217
|
}
|
|
219
|
-
|
|
218
|
+
|
|
220
219
|
call.resolve([
|
|
221
220
|
"devices": result
|
|
222
221
|
])
|
|
223
222
|
}
|
|
224
|
-
|
|
223
|
+
|
|
225
224
|
@objc func flipCamera(_ call: CAPPluginCall) {
|
|
226
225
|
do {
|
|
227
226
|
try implementation.flipCamera()
|
|
@@ -231,25 +230,25 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
231
230
|
return
|
|
232
231
|
}
|
|
233
232
|
}
|
|
234
|
-
|
|
233
|
+
|
|
235
234
|
@objc func getZoom(_ call: CAPPluginCall) {
|
|
236
235
|
let zoom = implementation.getSupportedZoomFactors()
|
|
237
|
-
|
|
236
|
+
|
|
238
237
|
call.resolve([
|
|
239
238
|
"min": zoom.min,
|
|
240
239
|
"max": zoom.max,
|
|
241
240
|
"current": zoom.current
|
|
242
241
|
])
|
|
243
242
|
}
|
|
244
|
-
|
|
243
|
+
|
|
245
244
|
@objc func setZoom(_ call: CAPPluginCall) {
|
|
246
245
|
guard let level = call.getDouble("level") else {
|
|
247
246
|
call.reject("Zoom level must be provided")
|
|
248
247
|
return
|
|
249
248
|
}
|
|
250
|
-
|
|
249
|
+
|
|
251
250
|
let ramp = call.getBool("ramp") ?? false
|
|
252
|
-
|
|
251
|
+
|
|
253
252
|
do {
|
|
254
253
|
try implementation.setZoomFactor(level, ramp: ramp)
|
|
255
254
|
call.resolve()
|
|
@@ -258,35 +257,35 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
258
257
|
return
|
|
259
258
|
}
|
|
260
259
|
}
|
|
261
|
-
|
|
260
|
+
|
|
262
261
|
@objc func getFlashMode(_ call: CAPPluginCall) {
|
|
263
262
|
let flashMode = implementation.getFlashMode()
|
|
264
|
-
|
|
263
|
+
|
|
265
264
|
call.resolve([
|
|
266
265
|
"flashMode": flashModeToStrMap[flashMode] ?? "off"
|
|
267
266
|
])
|
|
268
267
|
}
|
|
269
|
-
|
|
268
|
+
|
|
270
269
|
@objc func getSupportedFlashModes(_ call: CAPPluginCall) {
|
|
271
270
|
let supportedFlashModes = implementation.getSupportedFlashModes()
|
|
272
271
|
let supportedFlashModeStrArr = supportedFlashModes.map { flashModeToStrMap[$0] }
|
|
273
|
-
|
|
272
|
+
|
|
274
273
|
call.resolve([
|
|
275
274
|
"flashModes": supportedFlashModeStrArr
|
|
276
275
|
])
|
|
277
276
|
}
|
|
278
|
-
|
|
277
|
+
|
|
279
278
|
@objc func setFlashMode(_ call: CAPPluginCall) {
|
|
280
279
|
guard let mode = call.getString("mode") else {
|
|
281
280
|
call.reject("Flash mode must be provided")
|
|
282
281
|
return
|
|
283
282
|
}
|
|
284
|
-
|
|
283
|
+
|
|
285
284
|
guard let flashMode = strToFlashModeMap[mode] else {
|
|
286
285
|
call.reject("Invalid flash mode")
|
|
287
286
|
return
|
|
288
287
|
}
|
|
289
|
-
|
|
288
|
+
|
|
290
289
|
do {
|
|
291
290
|
try implementation.setFlashMode(flashMode)
|
|
292
291
|
call.resolve()
|
|
@@ -294,14 +293,14 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
294
293
|
call.reject("Failed to set flash mode", nil, error)
|
|
295
294
|
}
|
|
296
295
|
}
|
|
297
|
-
|
|
296
|
+
|
|
298
297
|
@objc func isTorchAvailable(_ call: CAPPluginCall) {
|
|
299
298
|
let available = implementation.isTorchAvailable()
|
|
300
299
|
call.resolve([
|
|
301
300
|
"available": available
|
|
302
301
|
])
|
|
303
302
|
}
|
|
304
|
-
|
|
303
|
+
|
|
305
304
|
@objc func getTorchMode(_ call: CAPPluginCall) {
|
|
306
305
|
let torchState = implementation.getTorchMode()
|
|
307
306
|
call.resolve([
|
|
@@ -309,20 +308,20 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
309
308
|
"level": torchState.level
|
|
310
309
|
])
|
|
311
310
|
}
|
|
312
|
-
|
|
311
|
+
|
|
313
312
|
@objc func setTorchMode(_ call: CAPPluginCall) {
|
|
314
313
|
guard let enabled = call.getBool("enabled") else {
|
|
315
314
|
call.reject("Enabled parameter is required")
|
|
316
315
|
return
|
|
317
316
|
}
|
|
318
|
-
|
|
317
|
+
|
|
319
318
|
let level = call.getFloat("level") ?? 1.0
|
|
320
|
-
|
|
319
|
+
|
|
321
320
|
guard level >= 0.0 && level <= 1.0 else {
|
|
322
321
|
call.reject("Level must be between 0.0 and 1.0")
|
|
323
322
|
return
|
|
324
323
|
}
|
|
325
|
-
|
|
324
|
+
|
|
326
325
|
do {
|
|
327
326
|
try implementation.setTorchMode(enabled: enabled, level: level)
|
|
328
327
|
call.resolve()
|
|
@@ -330,10 +329,10 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
330
329
|
call.reject("Failed to set torch mode", nil, error)
|
|
331
330
|
}
|
|
332
331
|
}
|
|
333
|
-
|
|
332
|
+
|
|
334
333
|
@objc override public func checkPermissions(_ call: CAPPluginCall) {
|
|
335
334
|
let cameraState: String
|
|
336
|
-
|
|
335
|
+
|
|
337
336
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
338
337
|
case .notDetermined:
|
|
339
338
|
cameraState = "prompt"
|
|
@@ -344,18 +343,18 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
344
343
|
@unknown default:
|
|
345
344
|
cameraState = "prompt"
|
|
346
345
|
}
|
|
347
|
-
|
|
346
|
+
|
|
348
347
|
call.resolve([
|
|
349
348
|
"camera": cameraState
|
|
350
349
|
])
|
|
351
350
|
}
|
|
352
|
-
|
|
351
|
+
|
|
353
352
|
@objc override public func requestPermissions(_ call: CAPPluginCall) {
|
|
354
353
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] _ in
|
|
355
354
|
self?.checkPermissions(call)
|
|
356
355
|
}
|
|
357
356
|
}
|
|
358
|
-
|
|
357
|
+
|
|
359
358
|
private func maybeRequestCameraAccess(completion: @escaping (Bool) -> Void) {
|
|
360
359
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
361
360
|
if status == .authorized {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Manages temporary files created during camera capture operations.
|
|
5
|
+
/// Provides automatic cleanup to prevent disk space leaks.
|
|
6
|
+
///
|
|
7
|
+
/// This singleton tracks all temporary files created by the camera plugin
|
|
8
|
+
/// and provides cleanup at various lifecycle points:
|
|
9
|
+
/// - On app activation (cleans up stale files older than 30 minutes)
|
|
10
|
+
/// - On app termination (cleans up all tracked files)
|
|
11
|
+
public final class TempFileManager: @unchecked Sendable {
|
|
12
|
+
/// Shared singleton instance
|
|
13
|
+
public static let shared = TempFileManager()
|
|
14
|
+
|
|
15
|
+
/// Serial queue for thread-safe file tracking
|
|
16
|
+
private let queue = DispatchQueue(label: "com.michaelwolz.capacitorcameraview.tempFileManager")
|
|
17
|
+
|
|
18
|
+
/// Set of tracked temporary file URLs
|
|
19
|
+
private var trackedFiles = Set<URL>()
|
|
20
|
+
|
|
21
|
+
/// Directory prefix used to identify camera capture temp files
|
|
22
|
+
private let tempFilePrefix = "camera_capture_"
|
|
23
|
+
|
|
24
|
+
/// Stale file threshold in seconds (30 minutes)
|
|
25
|
+
private let staleThresholdSeconds: TimeInterval = 1800
|
|
26
|
+
|
|
27
|
+
private init() {
|
|
28
|
+
setupAppLifecycleObservers()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
deinit {
|
|
32
|
+
NotificationCenter.default.removeObserver(self)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Public API
|
|
36
|
+
|
|
37
|
+
/// Creates a temporary file URL for storing captured images and tracks it for cleanup.
|
|
38
|
+
///
|
|
39
|
+
/// - Returns: A URL pointing to the temporary file location
|
|
40
|
+
/// - Throws: Never throws, always returns a valid temp directory URL
|
|
41
|
+
public func createTempImageFile() -> URL {
|
|
42
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
43
|
+
let fileName = "\(tempFilePrefix)\(timestamp).jpg"
|
|
44
|
+
let tempDir = FileManager.default.temporaryDirectory
|
|
45
|
+
let fileURL = tempDir.appendingPathComponent(fileName)
|
|
46
|
+
|
|
47
|
+
queue.sync {
|
|
48
|
+
trackedFiles.insert(fileURL)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fileURL
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Cleans up all tracked temporary files.
|
|
55
|
+
/// Call this when the camera session stops.
|
|
56
|
+
public func cleanupSessionFiles() {
|
|
57
|
+
queue.async { [weak self] in
|
|
58
|
+
guard let self = self else { return }
|
|
59
|
+
|
|
60
|
+
let filesToDelete = self.trackedFiles
|
|
61
|
+
self.trackedFiles.removeAll()
|
|
62
|
+
|
|
63
|
+
for fileURL in filesToDelete {
|
|
64
|
+
self.deleteFile(at: fileURL)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Cleans up stale temporary files that are older than the threshold.
|
|
70
|
+
/// This helps recover from cases where cleanup was missed.
|
|
71
|
+
public func cleanupStaleFiles() {
|
|
72
|
+
queue.async { [weak self] in
|
|
73
|
+
guard let self = self else { return }
|
|
74
|
+
|
|
75
|
+
let fileManager = FileManager.default
|
|
76
|
+
let tempDir = fileManager.temporaryDirectory
|
|
77
|
+
|
|
78
|
+
guard let contents = try? fileManager.contentsOfDirectory(
|
|
79
|
+
at: tempDir,
|
|
80
|
+
includingPropertiesForKeys: [.creationDateKey],
|
|
81
|
+
options: .skipsHiddenFiles
|
|
82
|
+
) else { return }
|
|
83
|
+
|
|
84
|
+
let staleDate = Date().addingTimeInterval(-self.staleThresholdSeconds)
|
|
85
|
+
|
|
86
|
+
for fileURL in contents {
|
|
87
|
+
// Only process our camera capture files
|
|
88
|
+
guard fileURL.lastPathComponent.hasPrefix(self.tempFilePrefix) else { continue }
|
|
89
|
+
|
|
90
|
+
// Check file age
|
|
91
|
+
if let attributes = try? fileManager.attributesOfItem(atPath: fileURL.path),
|
|
92
|
+
let creationDate = attributes[.creationDate] as? Date,
|
|
93
|
+
creationDate < staleDate {
|
|
94
|
+
self.deleteFile(at: fileURL)
|
|
95
|
+
self.trackedFiles.remove(fileURL)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Cleans up all camera capture temporary files in the temp directory.
|
|
102
|
+
/// Use this for aggressive cleanup on app termination.
|
|
103
|
+
public func cleanupAllCaptureFiles() {
|
|
104
|
+
queue.async { [weak self] in
|
|
105
|
+
guard let self = self else { return }
|
|
106
|
+
|
|
107
|
+
let fileManager = FileManager.default
|
|
108
|
+
let tempDir = fileManager.temporaryDirectory
|
|
109
|
+
|
|
110
|
+
guard let contents = try? fileManager.contentsOfDirectory(
|
|
111
|
+
at: tempDir,
|
|
112
|
+
includingPropertiesForKeys: nil,
|
|
113
|
+
options: .skipsHiddenFiles
|
|
114
|
+
) else { return }
|
|
115
|
+
|
|
116
|
+
for fileURL in contents {
|
|
117
|
+
if fileURL.lastPathComponent.hasPrefix(self.tempFilePrefix) {
|
|
118
|
+
self.deleteFile(at: fileURL)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
self.trackedFiles.removeAll()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - Private Methods
|
|
127
|
+
|
|
128
|
+
private func deleteFile(at url: URL) {
|
|
129
|
+
do {
|
|
130
|
+
try FileManager.default.removeItem(at: url)
|
|
131
|
+
} catch {
|
|
132
|
+
// Silently ignore - file may already be deleted or inaccessible
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func setupAppLifecycleObservers() {
|
|
137
|
+
NotificationCenter.default.addObserver(
|
|
138
|
+
self,
|
|
139
|
+
selector: #selector(handleAppDidBecomeActive),
|
|
140
|
+
name: UIApplication.didBecomeActiveNotification,
|
|
141
|
+
object: nil
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
NotificationCenter.default.addObserver(
|
|
145
|
+
self,
|
|
146
|
+
selector: #selector(handleAppWillTerminate),
|
|
147
|
+
name: UIApplication.willTerminateNotification,
|
|
148
|
+
object: nil
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@objc private func handleAppDidBecomeActive() {
|
|
153
|
+
// Clean up any stale files when app becomes active
|
|
154
|
+
cleanupStaleFiles()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@objc private func handleAppWillTerminate() {
|
|
158
|
+
// Clean up all capture files on termination
|
|
159
|
+
// Note: This is synchronous to ensure cleanup happens before termination
|
|
160
|
+
queue.sync { [weak self] in
|
|
161
|
+
guard let self = self else { return }
|
|
162
|
+
|
|
163
|
+
let fileManager = FileManager.default
|
|
164
|
+
let tempDir = fileManager.temporaryDirectory
|
|
165
|
+
|
|
166
|
+
if let contents = try? fileManager.contentsOfDirectory(
|
|
167
|
+
at: tempDir,
|
|
168
|
+
includingPropertiesForKeys: nil,
|
|
169
|
+
options: .skipsHiddenFiles
|
|
170
|
+
) {
|
|
171
|
+
for fileURL in contents {
|
|
172
|
+
if fileURL.lastPathComponent.hasPrefix(self.tempFilePrefix) {
|
|
173
|
+
try? fileManager.removeItem(at: fileURL)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
self.trackedFiles.removeAll()
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|