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