capacitor-camera-module 0.0.45 → 0.0.46
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.
|
@@ -2,49 +2,56 @@ import Capacitor
|
|
|
2
2
|
import AVFoundation
|
|
3
3
|
import Vision
|
|
4
4
|
import UIKit
|
|
5
|
-
|
|
5
|
+
import Photos
|
|
6
6
|
|
|
7
7
|
@objc(CameraModule)
|
|
8
|
-
public class CameraModulePlugin: CAPPlugin,
|
|
9
|
-
|
|
8
|
+
public class CameraModulePlugin: CAPPlugin,
|
|
9
|
+
AVCaptureVideoDataOutputSampleBufferDelegate {
|
|
10
|
+
|
|
11
|
+
// MARK: - Camera
|
|
10
12
|
|
|
11
13
|
private var previewView: UIView?
|
|
12
14
|
private var captureSession: AVCaptureSession?
|
|
13
15
|
private var videoPreviewLayer: AVCaptureVideoPreviewLayer?
|
|
16
|
+
private let photoOutput = AVCapturePhotoOutput()
|
|
17
|
+
private var photoDelegate: PhotoDelegate?
|
|
18
|
+
|
|
19
|
+
// MARK: - Barcode
|
|
14
20
|
|
|
15
|
-
private var photoOutput = AVCapturePhotoOutput()
|
|
16
21
|
private var isScanning = false
|
|
17
22
|
private var scanCall: CAPPluginCall?
|
|
18
|
-
|
|
19
|
-
private var photoDelegate: PhotoDelegate?
|
|
20
|
-
|
|
21
23
|
private var barcodeRequest: VNDetectBarcodesRequest?
|
|
22
|
-
|
|
23
24
|
private var videoDataOutput: AVCaptureVideoDataOutput?
|
|
24
|
-
|
|
25
25
|
private var lastScannedValue: String?
|
|
26
26
|
private var lastScanTime: Date?
|
|
27
|
-
private let scanDebounceInterval: TimeInterval = 0.5
|
|
27
|
+
private let scanDebounceInterval: TimeInterval = 0.5
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
public let jsName = "CameraModule"
|
|
29
|
+
// MARK: - Gallery
|
|
31
30
|
|
|
31
|
+
private var galleryCall: CAPPluginCall?
|
|
32
|
+
|
|
33
|
+
// MARK: - Capacitor Bridge
|
|
32
34
|
|
|
33
35
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
CAPPluginMethod(name: "checkPermission", returnType: CAPPluginReturnPromise),
|
|
37
|
+
CAPPluginMethod(name: "requestPermission", returnType: CAPPluginReturnPromise),
|
|
38
|
+
|
|
39
|
+
CAPPluginMethod(name: "checkGalleryPermission", returnType: CAPPluginReturnPromise),
|
|
40
|
+
CAPPluginMethod(name: "requestGalleryPermission", returnType: CAPPluginReturnPromise),
|
|
41
|
+
CAPPluginMethod(name: "checkAndRequestGalleryPermission", returnType: CAPPluginReturnPromise),
|
|
42
|
+
CAPPluginMethod(name: "pickImageBase64", returnType: CAPPluginReturnPromise),
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
CAPPluginMethod(name: "startPreview", returnType: CAPPluginReturnPromise),
|
|
45
|
+
CAPPluginMethod(name: "stopPreview", returnType: CAPPluginReturnPromise),
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
CAPPluginMethod(name: "toggleFlash", returnType: CAPPluginReturnPromise),
|
|
48
|
+
CAPPluginMethod(name: "hasFlash", returnType: CAPPluginReturnPromise),
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
CAPPluginMethod(name: "takePhotoBase64", returnType: CAPPluginReturnPromise),
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
CAPPluginMethod(name: "startBarcodeScan", returnType: CAPPluginReturnPromise),
|
|
53
|
+
CAPPluginMethod(name: "stopBarcodeScan", returnType: CAPPluginReturnPromise)
|
|
54
|
+
]
|
|
48
55
|
|
|
49
56
|
// MARK: - Lifecycle
|
|
50
57
|
|
|
@@ -53,12 +60,10 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
53
60
|
bridge?.webView?.backgroundColor = .clear
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
// MARK: - Permissions
|
|
63
|
+
// MARK: - Camera Permission
|
|
58
64
|
|
|
59
65
|
@objc func checkPermission(_ call: CAPPluginCall) {
|
|
60
66
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
61
|
-
|
|
62
67
|
call.resolve([
|
|
63
68
|
"granted": status == .authorized,
|
|
64
69
|
"status": status == .authorized ? "granted" : "prompt"
|
|
@@ -76,11 +81,71 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
// MARK: - Gallery Permission
|
|
85
|
+
|
|
86
|
+
@objc func checkGalleryPermission(_ call: CAPPluginCall) {
|
|
87
|
+
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
88
|
+
|
|
89
|
+
switch status {
|
|
90
|
+
case .authorized, .limited:
|
|
91
|
+
call.resolve(["granted": true, "status": "granted"])
|
|
92
|
+
case .notDetermined:
|
|
93
|
+
call.resolve(["granted": false, "status": "prompt"])
|
|
94
|
+
default:
|
|
95
|
+
call.resolve(["granted": false, "status": "denied"])
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@objc func requestGalleryPermission(_ call: CAPPluginCall) {
|
|
100
|
+
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
|
101
|
+
DispatchQueue.main.async {
|
|
102
|
+
let granted = (status == .authorized || status == .limited)
|
|
103
|
+
call.resolve([
|
|
104
|
+
"granted": granted,
|
|
105
|
+
"status": granted ? "granted" : "denied"
|
|
106
|
+
])
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@objc func checkAndRequestGalleryPermission(_ call: CAPPluginCall) {
|
|
112
|
+
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
113
|
+
if status == .authorized || status == .limited {
|
|
114
|
+
call.resolve(["granted": true, "status": "granted"])
|
|
115
|
+
} else {
|
|
116
|
+
requestGalleryPermission(call)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// MARK: - Pick Image
|
|
121
|
+
|
|
122
|
+
@objc func pickImageBase64(_ call: CAPPluginCall) {
|
|
123
|
+
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
124
|
+
guard status == .authorized || status == .limited else {
|
|
125
|
+
call.reject("Gallery permission not granted")
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
guard galleryCall == nil else {
|
|
130
|
+
call.reject("Gallery already open")
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
galleryCall = call
|
|
135
|
+
|
|
136
|
+
DispatchQueue.main.async {
|
|
137
|
+
let picker = UIImagePickerController()
|
|
138
|
+
picker.sourceType = .photoLibrary
|
|
139
|
+
picker.delegate = self
|
|
140
|
+
picker.modalPresentationStyle = .fullScreen
|
|
141
|
+
self.bridge?.viewController?.present(picker, animated: true)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
79
145
|
// MARK: - Camera Preview
|
|
80
146
|
|
|
81
147
|
@objc func startPreview(_ call: CAPPluginCall) {
|
|
82
148
|
DispatchQueue.main.async {
|
|
83
|
-
|
|
84
149
|
if self.previewView != nil {
|
|
85
150
|
call.resolve()
|
|
86
151
|
return
|
|
@@ -89,11 +154,8 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
89
154
|
let session = AVCaptureSession()
|
|
90
155
|
session.sessionPreset = .photo
|
|
91
156
|
|
|
92
|
-
guard let
|
|
93
|
-
|
|
94
|
-
position: .back),
|
|
95
|
-
let input = try? AVCaptureDeviceInput(device: camera)
|
|
96
|
-
else {
|
|
157
|
+
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
|
|
158
|
+
let input = try? AVCaptureDeviceInput(device: device) else {
|
|
97
159
|
call.reject("Camera not available")
|
|
98
160
|
return
|
|
99
161
|
}
|
|
@@ -102,16 +164,15 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
102
164
|
session.addOutput(self.photoOutput)
|
|
103
165
|
|
|
104
166
|
let view = UIView(frame: UIScreen.main.bounds)
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
view.layer.addSublayer(previewLayer)
|
|
167
|
+
let layer = AVCaptureVideoPreviewLayer(session: session)
|
|
168
|
+
layer.videoGravity = .resizeAspectFill
|
|
169
|
+
layer.frame = view.bounds
|
|
110
170
|
|
|
171
|
+
view.layer.addSublayer(layer)
|
|
111
172
|
self.bridge?.viewController?.view.insertSubview(view, at: 0)
|
|
112
173
|
|
|
113
174
|
self.previewView = view
|
|
114
|
-
self.videoPreviewLayer =
|
|
175
|
+
self.videoPreviewLayer = layer
|
|
115
176
|
self.captureSession = session
|
|
116
177
|
|
|
117
178
|
session.startRunning()
|
|
@@ -121,16 +182,12 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
121
182
|
|
|
122
183
|
@objc func stopPreview(_ call: CAPPluginCall) {
|
|
123
184
|
DispatchQueue.main.async {
|
|
124
|
-
|
|
125
185
|
self.captureSession?.stopRunning()
|
|
126
186
|
self.captureSession = nil
|
|
127
|
-
|
|
128
187
|
self.videoPreviewLayer?.removeFromSuperlayer()
|
|
129
|
-
self.videoPreviewLayer = nil
|
|
130
|
-
|
|
131
188
|
self.previewView?.removeFromSuperview()
|
|
189
|
+
self.videoPreviewLayer = nil
|
|
132
190
|
self.previewView = nil
|
|
133
|
-
|
|
134
191
|
call.resolve()
|
|
135
192
|
}
|
|
136
193
|
}
|
|
@@ -138,14 +195,9 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
138
195
|
// MARK: - Flash
|
|
139
196
|
|
|
140
197
|
@objc func toggleFlash(_ call: CAPPluginCall) {
|
|
141
|
-
guard let enable = call.getBool("enable")
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
guard let device = AVCaptureDevice.default(for: .video),
|
|
147
|
-
device.hasTorch
|
|
148
|
-
else {
|
|
198
|
+
guard let enable = call.getBool("enable"),
|
|
199
|
+
let device = AVCaptureDevice.default(for: .video),
|
|
200
|
+
device.hasTorch else {
|
|
149
201
|
call.reject("Flash not available")
|
|
150
202
|
return
|
|
151
203
|
}
|
|
@@ -161,14 +213,14 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
161
213
|
}
|
|
162
214
|
|
|
163
215
|
@objc func hasFlash(_ call: CAPPluginCall) {
|
|
164
|
-
|
|
165
|
-
|
|
216
|
+
call.resolve([
|
|
217
|
+
"hasFlash": AVCaptureDevice.default(for: .video)?.hasTorch ?? false
|
|
218
|
+
])
|
|
166
219
|
}
|
|
167
220
|
|
|
168
221
|
// MARK: - Photo Capture
|
|
169
222
|
|
|
170
223
|
@objc func takePhotoBase64(_ call: CAPPluginCall) {
|
|
171
|
-
|
|
172
224
|
guard captureSession?.isRunning == true else {
|
|
173
225
|
call.reject("Camera not started")
|
|
174
226
|
return
|
|
@@ -177,26 +229,20 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
177
229
|
let settings = AVCapturePhotoSettings()
|
|
178
230
|
settings.flashMode = .off
|
|
179
231
|
|
|
180
|
-
|
|
232
|
+
photoDelegate = PhotoDelegate { [weak self] base64 in
|
|
181
233
|
call.resolve([
|
|
182
234
|
"base64": base64,
|
|
183
235
|
"mimeType": "image/jpeg"
|
|
184
236
|
])
|
|
185
|
-
|
|
186
237
|
self?.photoDelegate = nil
|
|
187
238
|
}
|
|
188
239
|
|
|
189
|
-
|
|
190
|
-
with: settings,
|
|
191
|
-
delegate: self.photoDelegate!
|
|
192
|
-
)
|
|
240
|
+
photoOutput.capturePhoto(with: settings, delegate: photoDelegate!)
|
|
193
241
|
}
|
|
194
242
|
|
|
195
|
-
|
|
196
243
|
// MARK: - Barcode Scan
|
|
197
244
|
|
|
198
245
|
@objc func startBarcodeScan(_ call: CAPPluginCall) {
|
|
199
|
-
|
|
200
246
|
guard let session = captureSession else {
|
|
201
247
|
call.reject("Preview not started")
|
|
202
248
|
return
|
|
@@ -212,23 +258,20 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
212
258
|
call.keepAlive = true
|
|
213
259
|
|
|
214
260
|
barcodeRequest = VNDetectBarcodesRequest { request, _ in
|
|
215
|
-
guard let
|
|
216
|
-
let
|
|
217
|
-
let value = barcode.payloadStringValue
|
|
218
|
-
else { return }
|
|
261
|
+
guard let barcode = (request.results as? [VNBarcodeObservation])?.first,
|
|
262
|
+
let value = barcode.payloadStringValue else { return }
|
|
219
263
|
|
|
220
|
-
// Debounce
|
|
221
264
|
let now = Date()
|
|
222
|
-
if
|
|
265
|
+
if self.lastScannedValue == value,
|
|
223
266
|
let lastTime = self.lastScanTime,
|
|
224
|
-
|
|
267
|
+
now.timeIntervalSince(lastTime) < self.scanDebounceInterval {
|
|
225
268
|
return
|
|
226
269
|
}
|
|
227
270
|
|
|
228
271
|
self.lastScannedValue = value
|
|
229
272
|
self.lastScanTime = now
|
|
230
|
-
|
|
231
273
|
self.isScanning = false
|
|
274
|
+
|
|
232
275
|
self.scanCall?.resolve([
|
|
233
276
|
"rawValue": value,
|
|
234
277
|
"format": barcode.symbology.rawValue
|
|
@@ -237,7 +280,6 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
237
280
|
self.scanCall = nil
|
|
238
281
|
}
|
|
239
282
|
|
|
240
|
-
|
|
241
283
|
if videoDataOutput == nil {
|
|
242
284
|
let output = AVCaptureVideoDataOutput()
|
|
243
285
|
output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "barcode.queue"))
|
|
@@ -247,59 +289,78 @@ public class CameraModulePlugin: CAPPlugin,CAPBridgedPlugin,
|
|
|
247
289
|
}
|
|
248
290
|
|
|
249
291
|
@objc func stopBarcodeScan(_ call: CAPPluginCall) {
|
|
250
|
-
|
|
251
292
|
isScanning = false
|
|
252
|
-
|
|
253
|
-
if let output = videoDataOutput,
|
|
254
|
-
let session = captureSession {
|
|
293
|
+
if let output = videoDataOutput, let session = captureSession {
|
|
255
294
|
session.removeOutput(output)
|
|
256
|
-
videoDataOutput = nil
|
|
257
295
|
}
|
|
258
|
-
|
|
296
|
+
videoDataOutput = nil
|
|
259
297
|
scanCall?.keepAlive = false
|
|
260
298
|
scanCall = nil
|
|
261
|
-
|
|
262
|
-
lastScannedValue = nil
|
|
263
|
-
lastScanTime = nil
|
|
264
|
-
|
|
265
299
|
call.resolve()
|
|
266
300
|
}
|
|
267
301
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
didOutput sampleBuffer: CMSampleBuffer,
|
|
272
|
-
from connection: AVCaptureConnection
|
|
273
|
-
) {
|
|
302
|
+
public func captureOutput(_ output: AVCaptureOutput,
|
|
303
|
+
didOutput sampleBuffer: CMSampleBuffer,
|
|
304
|
+
from connection: AVCaptureConnection) {
|
|
274
305
|
guard isScanning,
|
|
275
|
-
let request = barcodeRequest
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
|
|
279
|
-
return
|
|
280
|
-
}
|
|
306
|
+
let request = barcodeRequest,
|
|
307
|
+
let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
|
|
281
308
|
|
|
282
309
|
let handler = VNImageRequestHandler(
|
|
283
310
|
cvPixelBuffer: pixelBuffer,
|
|
284
|
-
orientation:
|
|
311
|
+
orientation: .right,
|
|
285
312
|
options: [:]
|
|
286
313
|
)
|
|
287
314
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
315
|
+
try? handler.perform([request])
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// MARK: - UIImagePicker Delegate
|
|
320
|
+
|
|
321
|
+
extension CameraModulePlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
|
291
322
|
|
|
323
|
+
public func imagePickerController(_ picker: UIImagePickerController,
|
|
324
|
+
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
|
325
|
+
|
|
326
|
+
picker.dismiss(animated: true)
|
|
327
|
+
|
|
328
|
+
guard let image = info[.originalImage] as? UIImage,
|
|
329
|
+
let resized = image.resized(maxSize: 1024),
|
|
330
|
+
let jpeg = resized.jpegData(compressionQuality: 0.8) else {
|
|
331
|
+
galleryCall?.reject("Unable to process image")
|
|
332
|
+
galleryCall = nil
|
|
333
|
+
return
|
|
292
334
|
}
|
|
335
|
+
|
|
336
|
+
galleryCall?.resolve([
|
|
337
|
+
"base64": jpeg.base64EncodedString(),
|
|
338
|
+
"mimeType": "image/jpeg"
|
|
339
|
+
])
|
|
340
|
+
galleryCall = nil
|
|
293
341
|
}
|
|
294
342
|
|
|
295
|
-
func
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
case .landscapeLeft: return .up
|
|
300
|
-
case .landscapeRight: return .down
|
|
301
|
-
default: return .right
|
|
302
|
-
}
|
|
343
|
+
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
|
344
|
+
picker.dismiss(animated: true)
|
|
345
|
+
galleryCall?.reject("Image selection canceled")
|
|
346
|
+
galleryCall = nil
|
|
303
347
|
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// MARK: - UIImage Resize
|
|
351
|
+
|
|
352
|
+
extension UIImage {
|
|
353
|
+
func resized(maxSize: CGFloat) -> UIImage? {
|
|
354
|
+
let maxSide = max(size.width, size.height)
|
|
355
|
+
if maxSide <= maxSize { return self }
|
|
304
356
|
|
|
357
|
+
let scale = maxSize / maxSide
|
|
358
|
+
let newSize = CGSize(width: size.width * scale, height: size.height * scale)
|
|
359
|
+
|
|
360
|
+
UIGraphicsBeginImageContextWithOptions(newSize, false, 1)
|
|
361
|
+
draw(in: CGRect(origin: .zero, size: newSize))
|
|
362
|
+
let img = UIGraphicsGetImageFromCurrentImageContext()
|
|
363
|
+
UIGraphicsEndImageContext()
|
|
364
|
+
return img
|
|
365
|
+
}
|
|
305
366
|
}
|