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