capacitor-camera-view 2.3.1 → 3.0.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/CapacitorCameraView.podspec +1 -1
- package/Package.swift +1 -1
- package/README.md +341 -48
- package/android/build.gradle +0 -1
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraError.kt +42 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +946 -205
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +87 -43
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt +28 -2
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraDevice.kt +6 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +15 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/TorchModeState.kt +15 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/WebBoundingRect.kt +1 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +73 -19
- package/dist/docs.json +487 -30
- package/dist/esm/definitions.d.ts +494 -27
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/utils.d.ts +59 -15
- package/dist/esm/utils.js +79 -38
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/web.d.ts +191 -13
- package/dist/esm/web.js +626 -142
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +713 -180
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +713 -180
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +147 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +41 -19
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -1
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +78 -23
- package/ios/Sources/CameraViewPlugin/CameraViewManager+DeferredStart.swift +37 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Focus.swift +131 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Lifecycle.swift +156 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +73 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+ResolutionSelection.swift +57 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Rotation.swift +195 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +24 -12
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +22 -73
- package/ios/Sources/CameraViewPlugin/CameraViewManager+Zoom.swift +113 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +450 -403
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +111 -69
- package/ios/Sources/CameraViewPlugin/Utils.swift +61 -7
- package/package.json +25 -9
package/dist/plugin.js
CHANGED
|
@@ -16,7 +16,11 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
16
16
|
return dataUrl.split(',')[1];
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Calculates the visible area of the video based on object-fit: cover
|
|
19
|
+
* Calculates the visible area of the video based on object-fit: cover.
|
|
20
|
+
*
|
|
21
|
+
* The `output*` dimensions default to the visible crop's intrinsic (source)
|
|
22
|
+
* pixel size, so a capture keeps the full detail the stream delivers for that
|
|
23
|
+
* region instead of being downscaled to CSS pixels.
|
|
20
24
|
*/
|
|
21
25
|
function calculateVisibleArea(video) {
|
|
22
26
|
// Get the displayed dimensions of the video element
|
|
@@ -44,6 +48,10 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
44
48
|
sourceHeight = videoWidth / displayAspect;
|
|
45
49
|
sourceY = (videoHeight - sourceHeight) / 2;
|
|
46
50
|
}
|
|
51
|
+
// Rasterize the capture at the visible crop's native resolution. Rounding
|
|
52
|
+
// keeps the canvas dimensions integral without distorting the aspect ratio.
|
|
53
|
+
const outputWidth = Math.round(sourceWidth);
|
|
54
|
+
const outputHeight = Math.round(sourceHeight);
|
|
47
55
|
return {
|
|
48
56
|
sourceX,
|
|
49
57
|
sourceY,
|
|
@@ -51,32 +59,82 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
51
59
|
sourceHeight,
|
|
52
60
|
displayWidth,
|
|
53
61
|
displayHeight,
|
|
62
|
+
outputWidth,
|
|
63
|
+
outputHeight,
|
|
54
64
|
};
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
57
|
-
*
|
|
67
|
+
* The full video frame as a {@link VisibleArea}: no cropping, rasterized at
|
|
68
|
+
* the stream's intrinsic resolution.
|
|
69
|
+
*
|
|
70
|
+
* Used when the session was started with an explicit `aspectRatio`, where the
|
|
71
|
+
* cross-platform contract is that `capture()` returns the full sensor-ratio
|
|
72
|
+
* frame rather than the on-screen cover-cropped region.
|
|
73
|
+
*/
|
|
74
|
+
function calculateFullFrameArea(video) {
|
|
75
|
+
const videoRect = video.getBoundingClientRect();
|
|
76
|
+
return {
|
|
77
|
+
sourceX: 0,
|
|
78
|
+
sourceY: 0,
|
|
79
|
+
sourceWidth: video.videoWidth,
|
|
80
|
+
sourceHeight: video.videoHeight,
|
|
81
|
+
displayWidth: videoRect.width,
|
|
82
|
+
displayHeight: videoRect.height,
|
|
83
|
+
outputWidth: video.videoWidth,
|
|
84
|
+
outputHeight: video.videoHeight,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Further crops a {@link VisibleArea} to account for a CSS `transform: scale()`
|
|
89
|
+
* zoom applied to the video element.
|
|
90
|
+
*
|
|
91
|
+
* The CSS-fallback zoom magnifies the preview about its center, but
|
|
92
|
+
* `getBoundingClientRect()` reports the post-transform size and therefore
|
|
93
|
+
* leaves the crop unchanged. Tightening the source rectangle by `scale`, kept
|
|
94
|
+
* centered, makes a capture match what the user actually sees.
|
|
95
|
+
*
|
|
96
|
+
* A `scale <= 1` (no zoom) returns the area unchanged.
|
|
97
|
+
*/
|
|
98
|
+
function applyCssZoomCrop(area, scale) {
|
|
99
|
+
if (!(scale > 1)) {
|
|
100
|
+
return area;
|
|
101
|
+
}
|
|
102
|
+
const sourceWidth = area.sourceWidth / scale;
|
|
103
|
+
const sourceHeight = area.sourceHeight / scale;
|
|
104
|
+
return Object.assign(Object.assign({}, area), { sourceX: area.sourceX + (area.sourceWidth - sourceWidth) / 2, sourceY: area.sourceY + (area.sourceHeight - sourceHeight) / 2, sourceWidth,
|
|
105
|
+
sourceHeight, outputWidth: Math.round(sourceWidth), outputHeight: Math.round(sourceHeight) });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Draws the visible area of the video to the canvas at the crop's target
|
|
109
|
+
* output resolution (see {@link VisibleArea.outputWidth}).
|
|
58
110
|
*/
|
|
59
111
|
function drawVisibleAreaToCanvas(canvas, videoElement, area) {
|
|
60
|
-
const { sourceX, sourceY, sourceWidth, sourceHeight,
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
canvas.
|
|
112
|
+
const { sourceX, sourceY, sourceWidth, sourceHeight, outputWidth, outputHeight } = area;
|
|
113
|
+
// Sized to the crop's intrinsic pixel size rather than the CSS-pixel preview
|
|
114
|
+
// size, so the JPEG keeps the stream's real detail.
|
|
115
|
+
canvas.width = outputWidth;
|
|
116
|
+
canvas.height = outputHeight;
|
|
64
117
|
const ctx = canvas.getContext('2d', { alpha: false });
|
|
65
118
|
if (!ctx) {
|
|
66
119
|
throw new Error('Could not get canvas context');
|
|
67
120
|
}
|
|
68
121
|
// Draw only the visible portion of the video to match what the user sees
|
|
69
|
-
ctx.drawImage(videoElement, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0,
|
|
122
|
+
ctx.drawImage(videoElement, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, outputWidth, outputHeight);
|
|
70
123
|
}
|
|
71
124
|
/**
|
|
72
|
-
* Transforms barcode coordinates from the video source space to display space
|
|
73
|
-
* accounting for
|
|
125
|
+
* Transforms barcode coordinates from the video source space to display space,
|
|
126
|
+
* accounting for how the video element is scaled into its container.
|
|
127
|
+
*
|
|
128
|
+
* Both `object-fit` modes are a single-scale, centered mapping and share one
|
|
129
|
+
* formula, differing only in which axis scale wins: `'cover'` takes the larger
|
|
130
|
+
* (center-cropped), `'fit'` the smaller (letterboxed).
|
|
74
131
|
*
|
|
75
132
|
* @param barcodeBoundingBox The original barcode bounding box from the detector
|
|
76
133
|
* @param videoElement The video element with the camera stream
|
|
134
|
+
* @param scaleMode Whether the preview uses `cover` or `fit` scaling
|
|
77
135
|
* @returns The transformed bounding box coordinates in display space
|
|
78
136
|
*/
|
|
79
|
-
function transformBarcodeBoundingBox(barcodeBoundingBox, videoElement) {
|
|
137
|
+
function transformBarcodeBoundingBox(barcodeBoundingBox, videoElement, scaleMode = 'cover') {
|
|
80
138
|
// Get the video element's displayed dimensions
|
|
81
139
|
const videoRect = videoElement.getBoundingClientRect();
|
|
82
140
|
const displayWidth = videoRect.width;
|
|
@@ -84,35 +142,18 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
84
142
|
// Get original video dimensions
|
|
85
143
|
const videoWidth = videoElement.videoWidth;
|
|
86
144
|
const videoHeight = videoElement.videoHeight;
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const scaledVideoWidth = videoWidth * scale;
|
|
95
|
-
const cropX = (scaledVideoWidth - displayWidth) / 2;
|
|
96
|
-
scaledWidth = barcodeBoundingBox.width * scale;
|
|
97
|
-
scaledHeight = barcodeBoundingBox.height * scale;
|
|
98
|
-
scaledX = barcodeBoundingBox.x * scale - cropX;
|
|
99
|
-
scaledY = barcodeBoundingBox.y * scale;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
// Video is taller than display area - width matches, height is centered and cropped
|
|
103
|
-
const scale = displayWidth / videoWidth;
|
|
104
|
-
const scaledVideoHeight = videoHeight * scale;
|
|
105
|
-
const cropY = (scaledVideoHeight - displayHeight) / 2;
|
|
106
|
-
scaledWidth = barcodeBoundingBox.width * scale;
|
|
107
|
-
scaledHeight = barcodeBoundingBox.height * scale;
|
|
108
|
-
scaledX = barcodeBoundingBox.x * scale;
|
|
109
|
-
scaledY = barcodeBoundingBox.y * scale - cropY;
|
|
110
|
-
}
|
|
145
|
+
const scaleX = displayWidth / videoWidth;
|
|
146
|
+
const scaleY = displayHeight / videoHeight;
|
|
147
|
+
const scale = scaleMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
148
|
+
// Centering offset of the scaled frame within the container: negative on a
|
|
149
|
+
// cropped axis, positive on a letterboxed one.
|
|
150
|
+
const offsetX = (displayWidth - videoWidth * scale) / 2;
|
|
151
|
+
const offsetY = (displayHeight - videoHeight * scale) / 2;
|
|
111
152
|
return {
|
|
112
|
-
x:
|
|
113
|
-
y:
|
|
114
|
-
width:
|
|
115
|
-
height:
|
|
153
|
+
x: barcodeBoundingBox.x * scale + offsetX,
|
|
154
|
+
y: barcodeBoundingBox.y * scale + offsetY,
|
|
155
|
+
width: barcodeBoundingBox.width * scale,
|
|
156
|
+
height: barcodeBoundingBox.height * scale,
|
|
116
157
|
};
|
|
117
158
|
}
|
|
118
159
|
|
|
@@ -128,12 +169,50 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
128
169
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
129
170
|
};
|
|
130
171
|
var _CameraViewWeb_isRunning;
|
|
172
|
+
/**
|
|
173
|
+
* Suppression window in milliseconds during which a repeat of the same barcode
|
|
174
|
+
* (identical value + type) is not re-emitted. A genuinely new code still emits
|
|
175
|
+
* immediately. Kept consistent with the iOS and Android implementations.
|
|
176
|
+
*/
|
|
177
|
+
const BARCODE_SUPPRESSION_WINDOW_MS = 500;
|
|
178
|
+
/**
|
|
179
|
+
* Once the per-key dedupe map grows past this size, expired entries are pruned
|
|
180
|
+
* so a long session scanning many different codes stays bounded. Kept
|
|
181
|
+
* consistent with the iOS and Android implementations.
|
|
182
|
+
*/
|
|
183
|
+
const BARCODE_DEDUPE_MAP_PRUNE_THRESHOLD = 64;
|
|
184
|
+
/**
|
|
185
|
+
* Backstop for the "wait until the video is ready" step in
|
|
186
|
+
* {@link CameraViewWeb.startBarcodeDetection}. If the video element never
|
|
187
|
+
* fires `loadeddata` (e.g. a stalled stream), the wait settles anyway after
|
|
188
|
+
* this many milliseconds instead of leaving a dangling listener and a
|
|
189
|
+
* permanently pending promise.
|
|
190
|
+
*/
|
|
191
|
+
const BARCODE_VIDEO_READY_TIMEOUT_MS = 5000;
|
|
192
|
+
/**
|
|
193
|
+
* Baseline `ideal` capture resolution requested from `getUserMedia` in
|
|
194
|
+
* {@link CameraViewWeb.start}. Without any width/height hint, browsers default
|
|
195
|
+
* to a low-resolution stream (often 640x480), which caps capture quality.
|
|
196
|
+
*
|
|
197
|
+
* These are `ideal` (not `exact`/`min`) so devices that cannot deliver
|
|
198
|
+
* 1080p-class video still start at their best available resolution rather
|
|
199
|
+
* than failing acquisition.
|
|
200
|
+
*/
|
|
201
|
+
const DEFAULT_IDEAL_CAPTURE_WIDTH = 1920;
|
|
202
|
+
/**
|
|
203
|
+
* Bounds for the CSS `transform: scale()` zoom simulation used when the browser
|
|
204
|
+
* does not expose a native `zoom` track capability. `getZoom` reports this
|
|
205
|
+
* range and `setZoom` clamps the applied scale to it in fallback mode.
|
|
206
|
+
*/
|
|
207
|
+
const SIMULATED_ZOOM_MIN = 1.0;
|
|
208
|
+
const SIMULATED_ZOOM_MAX = 3.0;
|
|
131
209
|
const BARCODE_TYPE_TO_WEB_FORMAT = {
|
|
132
210
|
qr: 'qr_code',
|
|
133
211
|
code128: 'code_128',
|
|
134
212
|
code39: 'code_39',
|
|
135
213
|
code39Mod43: null,
|
|
136
214
|
code93: 'code_93',
|
|
215
|
+
codabar: 'codabar',
|
|
137
216
|
ean8: 'ean_8',
|
|
138
217
|
ean13: 'ean_13',
|
|
139
218
|
interleaved2of5: 'itf',
|
|
@@ -141,8 +220,63 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
141
220
|
pdf417: 'pdf417',
|
|
142
221
|
aztec: 'aztec',
|
|
143
222
|
dataMatrix: 'data_matrix',
|
|
223
|
+
upcA: 'upc_a',
|
|
144
224
|
upce: 'upc_e',
|
|
145
225
|
};
|
|
226
|
+
/**
|
|
227
|
+
* Inverse of {@link BARCODE_TYPE_TO_WEB_FORMAT}: maps the web BarcodeDetector
|
|
228
|
+
* format back onto the cross-platform {@link BarcodeType} vocabulary so the
|
|
229
|
+
* `barcodeDetected` event emits the same `type` values as iOS and Android.
|
|
230
|
+
*
|
|
231
|
+
* Note: `interleaved2of5` and `itf14` both map to the web format `itf`, so the
|
|
232
|
+
* inversion has a collision that is resolved by insertion order — `itf14` comes
|
|
233
|
+
* last in {@link BARCODE_TYPE_TO_WEB_FORMAT} and wins, which is the intended
|
|
234
|
+
* result for `itf` detections.
|
|
235
|
+
*/
|
|
236
|
+
const WEB_FORMAT_TO_BARCODE_TYPE = Object.entries(BARCODE_TYPE_TO_WEB_FORMAT).reduce((acc, [barcodeType, webFormat]) => {
|
|
237
|
+
if (webFormat) {
|
|
238
|
+
acc[webFormat] = barcodeType;
|
|
239
|
+
}
|
|
240
|
+
return acc;
|
|
241
|
+
}, {});
|
|
242
|
+
/**
|
|
243
|
+
* Error thrown by the web implementation for a rejected plugin call.
|
|
244
|
+
*
|
|
245
|
+
* Carries a stable `code` from the {@link CameraErrorCode} vocabulary, the
|
|
246
|
+
* same public contract iOS and Android provide, so consumers can `switch` on
|
|
247
|
+
* `error.code` instead of matching on the human-readable `message`.
|
|
248
|
+
*
|
|
249
|
+
* Methods that reject via `WebPlugin.unimplemented()` are the one exception:
|
|
250
|
+
* those keep Capacitor's own `UNIMPLEMENTED` convention.
|
|
251
|
+
*/
|
|
252
|
+
class CameraViewError extends Error {
|
|
253
|
+
constructor(message, code) {
|
|
254
|
+
super(message);
|
|
255
|
+
this.name = 'CameraViewError';
|
|
256
|
+
this.code = code;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Classifies a `getUserMedia` failure into the closest-fitting
|
|
261
|
+
* {@link CameraErrorCode}, shared by every acquisition site.
|
|
262
|
+
*
|
|
263
|
+
* `kind` selects which media type's dedicated codes apply for `NotFoundError`/
|
|
264
|
+
* `NotReadableError` so a missing/busy camera and a missing/busy microphone
|
|
265
|
+
* aren't conflated. Anything unmappable falls back to `UNKNOWN_ERROR`.
|
|
266
|
+
*/
|
|
267
|
+
function classifyGetUserMediaErrorCode(err, kind) {
|
|
268
|
+
if (err instanceof DOMException) {
|
|
269
|
+
switch (err.name) {
|
|
270
|
+
case 'NotAllowedError':
|
|
271
|
+
return 'PERMISSION_DENIED';
|
|
272
|
+
case 'NotFoundError':
|
|
273
|
+
return kind === 'camera' ? 'CAMERA_UNAVAILABLE' : 'AUDIO_DEVICE_UNAVAILABLE';
|
|
274
|
+
case 'NotReadableError':
|
|
275
|
+
return kind === 'camera' ? 'DEVICE_LOCKED' : 'AUDIO_INPUT_ADDITION_FAILED';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return 'UNKNOWN_ERROR';
|
|
279
|
+
}
|
|
146
280
|
/**
|
|
147
281
|
* Web implementation of the CameraViewPlugin.
|
|
148
282
|
* Optimized for performance and battery efficiency.
|
|
@@ -159,10 +293,38 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
159
293
|
// Configuration state
|
|
160
294
|
this.currentCamera = 'environment'; // Default to back camera
|
|
161
295
|
this.currentZoom = 1.0;
|
|
296
|
+
// Whether the current zoom is applied through the native track `zoom`
|
|
297
|
+
// capability (`applyConstraints`) rather than the CSS `transform: scale()`
|
|
298
|
+
// simulation. Capture only needs to compensate for the transform in the
|
|
299
|
+
// CSS-fallback case.
|
|
300
|
+
this.usingNativeZoom = false;
|
|
162
301
|
this.currentFlashMode = 'off';
|
|
302
|
+
// The aspect ratio the current session was started with, or null when the
|
|
303
|
+
// option was omitted. Selects the capture contract: with an explicit ratio,
|
|
304
|
+
// capture() returns the full sensor-ratio frame (cross-platform contract);
|
|
305
|
+
// without it, the legacy web behavior of capturing the visible
|
|
306
|
+
// (cover-cropped) preview region is preserved.
|
|
307
|
+
this.sessionAspectRatio = null;
|
|
308
|
+
// How the current session scales the preview into its container. `'fit'`
|
|
309
|
+
// (object-fit: contain) letterboxes the whole frame; `'cover'` (the default)
|
|
310
|
+
// center-crops it. Selects the barcode-transform variant and, together with
|
|
311
|
+
// the aspect ratio, the capture crop.
|
|
312
|
+
this.sessionPreviewScaleMode = 'cover';
|
|
313
|
+
// Resolution/aspect-ratio constraints of the current session, kept so
|
|
314
|
+
// flipCamera() re-acquires the stream with the same resolution contract
|
|
315
|
+
// instead of falling back to the browser default.
|
|
316
|
+
this.sessionResolutionConstraints = {};
|
|
163
317
|
// Barcode detection support
|
|
164
318
|
this.barcodeDetectionSupported = false;
|
|
165
319
|
this.barcodeDetector = null;
|
|
320
|
+
// Scopes the barcode detection loop to a single start()/stop() session so a
|
|
321
|
+
// rapid stop() -> start() can't let the old loop mistake the new session's
|
|
322
|
+
// running flag for its own and keep polling a detached video element.
|
|
323
|
+
this.barcodeDetectionAbortController = null;
|
|
324
|
+
// The most recently scheduled `requestAnimationFrame` id for the barcode
|
|
325
|
+
// detection loop, so `stop()` can cancel a queued-but-not-yet-run frame
|
|
326
|
+
// outright.
|
|
327
|
+
this.barcodeAnimationFrameId = null;
|
|
166
328
|
// Recording state
|
|
167
329
|
this.mediaRecorder = null;
|
|
168
330
|
this.recordedChunks = [];
|
|
@@ -175,41 +337,88 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
175
337
|
* Start the camera with the given configuration
|
|
176
338
|
*/
|
|
177
339
|
async start(options) {
|
|
340
|
+
var _a, _b, _c, _d;
|
|
341
|
+
// A session is already running. Per the cross-platform contract, reject
|
|
342
|
+
// instead of silently reconfiguring or no-op'ing: callers who need a
|
|
343
|
+
// different configuration (e.g. a different position or resolution) must
|
|
344
|
+
// call `stop()` first and then `start()` again with the new options.
|
|
178
345
|
if (__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f")) {
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
const permissionStatus = await this.requestPermissions();
|
|
182
|
-
if (permissionStatus.camera !== 'granted') {
|
|
183
|
-
throw new Error('Camera permission was not granted');
|
|
346
|
+
throw new CameraViewError('Camera session is already running. Call stop() first.', 'SESSION_ALREADY_RUNNING');
|
|
184
347
|
}
|
|
185
348
|
try {
|
|
186
349
|
// Set up video element if it doesn't exist
|
|
187
350
|
if (!this.videoElement) {
|
|
188
351
|
await this.setupVideoElement(options === null || options === void 0 ? void 0 : options.containerElementId);
|
|
189
352
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
// The only failure path in setupVideoElement() is the target container
|
|
356
|
+
// element not being found, which is the web equivalent of "could not
|
|
357
|
+
// find the view to render the camera preview into".
|
|
358
|
+
throw new CameraViewError(`Failed to start camera: ${this.formatError(err)}`, 'WEBVIEW_UNAVAILABLE');
|
|
359
|
+
}
|
|
360
|
+
// Apply the preview scale mode to the video element. `'fit'` letterboxes
|
|
361
|
+
// the whole frame (object-fit: contain); the empty bars show the container's
|
|
362
|
+
// own background. `'cover'` keeps the long-standing center-cropped preview.
|
|
363
|
+
this.sessionPreviewScaleMode = (_a = options === null || options === void 0 ? void 0 : options.previewScaleMode) !== null && _a !== void 0 ? _a : 'cover';
|
|
364
|
+
if (this.videoElement) {
|
|
365
|
+
this.videoElement.style.objectFit = this.sessionPreviewScaleMode === 'fit' ? 'contain' : 'cover';
|
|
366
|
+
}
|
|
367
|
+
// Set up video constraints based on options
|
|
368
|
+
this.sessionAspectRatio = (_b = options === null || options === void 0 ? void 0 : options.aspectRatio) !== null && _b !== void 0 ? _b : null;
|
|
369
|
+
this.sessionResolutionConstraints = this.buildResolutionConstraints(options);
|
|
370
|
+
const videoConstraints = Object.assign({}, this.sessionResolutionConstraints);
|
|
371
|
+
// Prefer deviceId if specified
|
|
372
|
+
if (options === null || options === void 0 ? void 0 : options.deviceId) {
|
|
373
|
+
videoConstraints.deviceId = { exact: options.deviceId };
|
|
374
|
+
// Remember the current camera mode (though we're using a specific device)
|
|
375
|
+
this.currentCamera = (options === null || options === void 0 ? void 0 : options.position) === 'front' ? 'user' : 'environment';
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// Fall back to facing mode
|
|
379
|
+
const facingMode = (options === null || options === void 0 ? void 0 : options.position) === 'front' ? 'user' : 'environment';
|
|
380
|
+
this.currentCamera = facingMode;
|
|
381
|
+
videoConstraints.facingMode = facingMode;
|
|
382
|
+
}
|
|
383
|
+
const constraints = {
|
|
384
|
+
video: videoConstraints,
|
|
385
|
+
audio: false,
|
|
386
|
+
};
|
|
387
|
+
// Acquire the camera exactly once, using the real constraints, and derive
|
|
388
|
+
// the permission outcome from this single call. Probing permission with a
|
|
389
|
+
// throwaway acquisition first would double startup latency, flash the
|
|
390
|
+
// camera indicator twice, and risk a spurious NotReadableError on devices
|
|
391
|
+
// where the camera is exclusive.
|
|
392
|
+
try {
|
|
208
393
|
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
throw this.mapStartAcquisitionError(err);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
209
399
|
if (this.videoElement) {
|
|
210
400
|
this.videoElement.srcObject = this.stream;
|
|
211
|
-
|
|
401
|
+
// Some browsers' autoplay policies can reject `play()` (e.g. lack of a
|
|
402
|
+
// recent user gesture). The element is muted + playsInline, which satisfies
|
|
403
|
+
// autoplay policies in virtually all cases, but a rejection must still be
|
|
404
|
+
// handled here rather than left as an unhandled promise rejection.
|
|
405
|
+
try {
|
|
406
|
+
await this.videoElement.play();
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.warn('[CameraView] Failed to autoplay the video preview', err);
|
|
410
|
+
}
|
|
212
411
|
__classPrivateFieldSet(this, _CameraViewWeb_isRunning, true, "f");
|
|
412
|
+
// Apply the initial zoom. This also re-establishes zoom on the freshly
|
|
413
|
+
// created video element after a stop()/start() cycle, keeping the
|
|
414
|
+
// applied zoom in sync with `currentZoom` instead of silently resetting
|
|
415
|
+
// to an untransformed element. A zoom failure must not abort start().
|
|
416
|
+
try {
|
|
417
|
+
await this.setZoom({ level: (_c = options === null || options === void 0 ? void 0 : options.zoomFactor) !== null && _c !== void 0 ? _c : 1.0 });
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
console.warn('[CameraView] Failed to apply initial zoom factor', err);
|
|
421
|
+
}
|
|
213
422
|
// If barcode detection is enabled and supported, start detection
|
|
214
423
|
if (options === null || options === void 0 ? void 0 : options.enableBarcodeDetection) {
|
|
215
424
|
await this.checkBarcodeDetectionSupport();
|
|
@@ -221,22 +430,90 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
221
430
|
}
|
|
222
431
|
}
|
|
223
432
|
catch (err) {
|
|
224
|
-
|
|
433
|
+
// The stream was already acquired here, so its tracks are still live.
|
|
434
|
+
// Tear down the partially-initialized session state so the thrown error
|
|
435
|
+
// leaves a clean slate for a subsequent start().
|
|
436
|
+
(_d = this.stream) === null || _d === void 0 ? void 0 : _d.getTracks().forEach((track) => track.stop());
|
|
437
|
+
this.stream = null;
|
|
438
|
+
__classPrivateFieldSet(this, _CameraViewWeb_isRunning, false, "f");
|
|
439
|
+
if (this.videoElement) {
|
|
440
|
+
this.videoElement.srcObject = null;
|
|
441
|
+
}
|
|
442
|
+
throw new CameraViewError(`Failed to start camera: ${this.formatError(err)}`, 'UNKNOWN_ERROR');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Builds the resolution/aspect-ratio part of the `getUserMedia` video
|
|
447
|
+
* constraints for a session.
|
|
448
|
+
*
|
|
449
|
+
* `captureMaxDimension` replaces the ideal width (the longer edge in the
|
|
450
|
+
* stream's landscape-oriented coordinate space) and the ideal height is
|
|
451
|
+
* derived from the configured ratio. Everything stays `ideal` so acquisition
|
|
452
|
+
* degrades gracefully on devices that cannot deliver the request.
|
|
453
|
+
*/
|
|
454
|
+
buildResolutionConstraints(options) {
|
|
455
|
+
var _a;
|
|
456
|
+
const ratio = (options === null || options === void 0 ? void 0 : options.aspectRatio) === '4:3' ? 4 / 3 : 16 / 9;
|
|
457
|
+
const idealWidth = (_a = options === null || options === void 0 ? void 0 : options.captureMaxDimension) !== null && _a !== void 0 ? _a : DEFAULT_IDEAL_CAPTURE_WIDTH;
|
|
458
|
+
const idealHeight = Math.round(idealWidth / ratio);
|
|
459
|
+
const constraints = {
|
|
460
|
+
width: { ideal: idealWidth },
|
|
461
|
+
height: { ideal: idealHeight },
|
|
462
|
+
};
|
|
463
|
+
if (options === null || options === void 0 ? void 0 : options.aspectRatio) {
|
|
464
|
+
constraints.aspectRatio = { ideal: ratio };
|
|
465
|
+
}
|
|
466
|
+
return constraints;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Map a `getUserMedia` failure from the real-constraints acquisition in
|
|
470
|
+
* `start()` onto the plugin's error contract.
|
|
471
|
+
*
|
|
472
|
+
* `NotAllowedError` maps to `PERMISSION_DENIED`, `NotFoundError` (no
|
|
473
|
+
* matching device) to `CAMERA_UNAVAILABLE`, and `NotReadableError` (device
|
|
474
|
+
* claimed by another process) to `DEVICE_LOCKED`, matching the codes
|
|
475
|
+
* iOS/Android use. Anything else falls back to `UNKNOWN_ERROR`.
|
|
476
|
+
*/
|
|
477
|
+
mapStartAcquisitionError(err) {
|
|
478
|
+
if (err instanceof DOMException) {
|
|
479
|
+
switch (err.name) {
|
|
480
|
+
case 'NotAllowedError':
|
|
481
|
+
return new CameraViewError('Camera permission was not granted', 'PERMISSION_DENIED');
|
|
482
|
+
case 'NotFoundError':
|
|
483
|
+
return new CameraViewError('Failed to start camera: No camera matching the requested configuration was found.', 'CAMERA_UNAVAILABLE');
|
|
484
|
+
case 'NotReadableError':
|
|
485
|
+
return new CameraViewError('Failed to start camera: The camera could not be started, possibly because it is already in use by another application.', 'DEVICE_LOCKED');
|
|
486
|
+
}
|
|
225
487
|
}
|
|
488
|
+
return new CameraViewError(`Failed to start camera: ${this.formatError(err)}`, 'UNKNOWN_ERROR');
|
|
226
489
|
}
|
|
227
490
|
/**
|
|
228
491
|
* Stop the camera and release resources
|
|
229
492
|
*/
|
|
230
493
|
async stop() {
|
|
231
|
-
var _a;
|
|
494
|
+
var _a, _b, _c;
|
|
232
495
|
if (!__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f")) {
|
|
233
496
|
return;
|
|
234
497
|
}
|
|
235
498
|
try {
|
|
499
|
+
// Tear down the barcode detection session tied to this camera session
|
|
500
|
+
// first: abort settles any pending video-ready wait in
|
|
501
|
+
// startBarcodeDetection, and cancelling the animation frame stops a
|
|
502
|
+
// queued detectFrame call from ever running. Together these ensure no
|
|
503
|
+
// stale loop can survive into a subsequent start().
|
|
504
|
+
(_a = this.barcodeDetectionAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
505
|
+
this.barcodeDetectionAbortController = null;
|
|
506
|
+
if (this.barcodeAnimationFrameId !== null) {
|
|
507
|
+
cancelAnimationFrame(this.barcodeAnimationFrameId);
|
|
508
|
+
this.barcodeAnimationFrameId = null;
|
|
509
|
+
}
|
|
236
510
|
// Stop any active recording
|
|
237
511
|
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
238
|
-
// Reject any pending stopRecording promise since we're force-stopping
|
|
239
|
-
|
|
512
|
+
// Reject any pending stopRecording promise since we're force-stopping.
|
|
513
|
+
// There is no dedicated code for this case, so it falls back to
|
|
514
|
+
// UNKNOWN_ERROR per the plugin's documented "anything unmappable"
|
|
515
|
+
// contract.
|
|
516
|
+
(_b = this.recordingReject) === null || _b === void 0 ? void 0 : _b.call(this, new CameraViewError('Camera session stopped while recording', 'UNKNOWN_ERROR'));
|
|
240
517
|
this.recordingResolve = null;
|
|
241
518
|
this.recordingReject = null;
|
|
242
519
|
this.mediaRecorder.stop();
|
|
@@ -252,14 +529,19 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
252
529
|
this.stream.getTracks().forEach((track) => track.stop());
|
|
253
530
|
this.stream = null;
|
|
254
531
|
}
|
|
255
|
-
//
|
|
532
|
+
// Detach the stream and remove the video element from the DOM
|
|
256
533
|
if (this.videoElement) {
|
|
534
|
+
this.videoElement.pause();
|
|
535
|
+
this.videoElement.srcObject = null;
|
|
536
|
+
(_c = this.videoElement.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(this.videoElement);
|
|
257
537
|
this.videoElement = null;
|
|
258
538
|
}
|
|
259
539
|
__classPrivateFieldSet(this, _CameraViewWeb_isRunning, false, "f");
|
|
260
540
|
}
|
|
261
541
|
catch (err) {
|
|
262
|
-
|
|
542
|
+
// No dedicated code for a teardown failure; falls back to UNKNOWN_ERROR
|
|
543
|
+
// per the plugin's documented "anything unmappable" contract.
|
|
544
|
+
throw new CameraViewError(`Failed to stop camera: ${this.formatError(err)}`, 'UNKNOWN_ERROR');
|
|
263
545
|
}
|
|
264
546
|
}
|
|
265
547
|
/**
|
|
@@ -273,21 +555,41 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
273
555
|
* Preserves what the user actually sees in the UI, including cropping from object-fit: cover.
|
|
274
556
|
*/
|
|
275
557
|
async capture(options) {
|
|
558
|
+
var _a;
|
|
276
559
|
const videoElement = this.videoElement;
|
|
277
560
|
if (!__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f") || !videoElement) {
|
|
278
|
-
throw new
|
|
561
|
+
throw new CameraViewError('Camera is not running', 'SESSION_NOT_RUNNING');
|
|
279
562
|
}
|
|
280
563
|
try {
|
|
281
564
|
const canvas = this.getCanvasElement();
|
|
282
|
-
|
|
565
|
+
// `fit` mode letterboxes the whole frame and an explicit `aspectRatio`
|
|
566
|
+
// contractually returns the full sensor-ratio frame, so both capture
|
|
567
|
+
// uncropped. Otherwise capture the visible (cover-cropped) region so the
|
|
568
|
+
// output matches what the user sees.
|
|
569
|
+
const captureArea = this.sessionPreviewScaleMode === 'fit' || this.sessionAspectRatio
|
|
570
|
+
? calculateFullFrameArea(videoElement)
|
|
571
|
+
: calculateVisibleArea(videoElement);
|
|
572
|
+
// In CSS-fallback zoom mode the preview is magnified about its center via
|
|
573
|
+
// `transform: scale()`, so tighten the source crop to match the zoomed
|
|
574
|
+
// viewport (this preserves the frame's aspect ratio). Native-zoom mode
|
|
575
|
+
// needs no compensation: the track already delivers the zoomed frame.
|
|
576
|
+
const visibleArea = applyCssZoomCrop(captureArea, this.getCssZoomScale());
|
|
283
577
|
drawVisibleAreaToCanvas(canvas, videoElement, visibleArea);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
578
|
+
// Mirror the native platforms' default: `quality` is optional and defaults
|
|
579
|
+
// to 90 when omitted. Without this, `options?.quality / 100` would be
|
|
580
|
+
// `NaN` for an undefined quality, which is passed silently to `toBlob`/
|
|
581
|
+
// `toDataURL` (both treat an invalid quality as "use the default"), so a
|
|
582
|
+
// caller relying on the documented default would get a different result
|
|
583
|
+
// on web than on iOS/Android.
|
|
584
|
+
const requestedQuality = (_a = options === null || options === void 0 ? void 0 : options.quality) !== null && _a !== void 0 ? _a : 90;
|
|
585
|
+
const quality = Math.min(1.0, Math.max(0.1, requestedQuality / 100));
|
|
586
|
+
if (options === null || options === void 0 ? void 0 : options.saveToFile) {
|
|
587
|
+
// Create a blob from canvas and return a blob URL.
|
|
588
|
+
// `path` is native-only (no filesystem path on web), so it is omitted here.
|
|
287
589
|
return new Promise((resolve, reject) => {
|
|
288
590
|
canvas.toBlob((blob) => {
|
|
289
591
|
if (!blob) {
|
|
290
|
-
reject(new
|
|
592
|
+
reject(new CameraViewError('Failed to create blob from canvas', 'IMAGE_COMPRESSION_FAILED'));
|
|
291
593
|
return;
|
|
292
594
|
}
|
|
293
595
|
const url = URL.createObjectURL(blob);
|
|
@@ -302,7 +604,7 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
302
604
|
}
|
|
303
605
|
}
|
|
304
606
|
catch (err) {
|
|
305
|
-
throw new
|
|
607
|
+
throw new CameraViewError(`Failed to capture photo: ${this.formatError(err)}`, 'FRAME_CAPTURE_ERROR');
|
|
306
608
|
}
|
|
307
609
|
}
|
|
308
610
|
/**
|
|
@@ -316,10 +618,10 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
316
618
|
*/
|
|
317
619
|
async startRecording(options) {
|
|
318
620
|
if (!__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f") || !this.videoElement) {
|
|
319
|
-
throw new
|
|
621
|
+
throw new CameraViewError('Camera is not running', 'SESSION_NOT_RUNNING');
|
|
320
622
|
}
|
|
321
623
|
if (this.mediaRecorder) {
|
|
322
|
-
throw new
|
|
624
|
+
throw new CameraViewError('Recording is already in progress', 'RECORDING_ALREADY_IN_PROGRESS');
|
|
323
625
|
}
|
|
324
626
|
try {
|
|
325
627
|
let stream = this.stream;
|
|
@@ -369,7 +671,10 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
369
671
|
this.mediaRecorder = null;
|
|
370
672
|
this.recordedChunks = [];
|
|
371
673
|
const errorMessage = (_b = (_a = event.error) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : 'Unknown recording error';
|
|
372
|
-
|
|
674
|
+
// A generic MediaRecorder runtime failure has no dedicated code, so
|
|
675
|
+
// it falls back to UNKNOWN_ERROR per the plugin's documented
|
|
676
|
+
// "anything unmappable" contract.
|
|
677
|
+
(_c = this.recordingReject) === null || _c === void 0 ? void 0 : _c.call(this, new CameraViewError('Recording error: ' + errorMessage, 'UNKNOWN_ERROR'));
|
|
373
678
|
this.recordingResolve = null;
|
|
374
679
|
this.recordingReject = null;
|
|
375
680
|
};
|
|
@@ -382,15 +687,43 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
382
687
|
}
|
|
383
688
|
this.mediaRecorder = null;
|
|
384
689
|
this.recordedChunks = [];
|
|
385
|
-
throw new
|
|
690
|
+
throw new CameraViewError(`Failed to start recording: ${this.formatError(err)}`, this.mapRecordingStartErrorCode(err));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Maps a failure caught by `startRecording()`'s try/catch onto the closest-
|
|
695
|
+
* fitting `CameraErrorCode`, without altering the existing wrapped message.
|
|
696
|
+
*
|
|
697
|
+
* A `DOMException` here can only come from the microphone `getUserMedia`
|
|
698
|
+
* call, so it is classified with `kind: 'microphone'`. The two other
|
|
699
|
+
* distinguishable failures are plain `Error`s matched by message text.
|
|
700
|
+
*/
|
|
701
|
+
mapRecordingStartErrorCode(err) {
|
|
702
|
+
if (err instanceof DOMException) {
|
|
703
|
+
return classifyGetUserMediaErrorCode(err, 'microphone');
|
|
704
|
+
}
|
|
705
|
+
if (err instanceof Error) {
|
|
706
|
+
if (err.message === 'No camera stream available') {
|
|
707
|
+
return 'CAMERA_UNAVAILABLE';
|
|
708
|
+
}
|
|
709
|
+
if (err.message === 'No supported video recording format found') {
|
|
710
|
+
return 'CONFIGURATION_FAILED';
|
|
711
|
+
}
|
|
386
712
|
}
|
|
713
|
+
return 'UNKNOWN_ERROR';
|
|
387
714
|
}
|
|
388
715
|
/**
|
|
389
716
|
* Stop the current video recording
|
|
390
717
|
*/
|
|
391
718
|
async stopRecording() {
|
|
392
719
|
if (!this.mediaRecorder) {
|
|
393
|
-
throw new
|
|
720
|
+
throw new CameraViewError('No recording is in progress', 'NO_RECORDING_IN_PROGRESS');
|
|
721
|
+
}
|
|
722
|
+
// A stop is already pending. Reject this second call instead of
|
|
723
|
+
// overwriting the pending callbacks, which would orphan the first caller's
|
|
724
|
+
// promise forever.
|
|
725
|
+
if (this.recordingResolve || this.recordingReject) {
|
|
726
|
+
throw new CameraViewError('stopRecording() is already pending', 'UNKNOWN_ERROR');
|
|
394
727
|
}
|
|
395
728
|
return new Promise((resolve, reject) => {
|
|
396
729
|
var _a;
|
|
@@ -403,45 +736,95 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
403
736
|
* Flip between front and back camera
|
|
404
737
|
*/
|
|
405
738
|
async flipCamera() {
|
|
739
|
+
var _a;
|
|
406
740
|
if (!__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f")) {
|
|
407
|
-
throw new
|
|
741
|
+
throw new CameraViewError('Camera is not running', 'SESSION_NOT_RUNNING');
|
|
408
742
|
}
|
|
743
|
+
// Flipping restarts the stream and stops the tracks the MediaRecorder is
|
|
744
|
+
// consuming, which would silently freeze an in-progress recording. Reject
|
|
745
|
+
// instead so the caller can stop recording first; the recording stays
|
|
746
|
+
// intact.
|
|
747
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
748
|
+
throw new CameraViewError('Cannot flip camera while a recording is in progress', 'RECORDING_ALREADY_IN_PROGRESS');
|
|
749
|
+
}
|
|
750
|
+
// The candidate facing mode, kept local until the new stream is actually
|
|
751
|
+
// live: `currentCamera` (and the previous stream) must not be touched
|
|
752
|
+
// before that point, or a failed re-acquisition below would leave the
|
|
753
|
+
// session state pointing at a camera that isn't running while the
|
|
754
|
+
// previous camera's tracks have already been stopped.
|
|
755
|
+
const nextCamera = this.currentCamera === 'user' ? 'environment' : 'user';
|
|
756
|
+
// Acquire the new-facing stream with the new facing mode, keeping the
|
|
757
|
+
// session's resolution/aspect-ratio constraints so the flipped stream
|
|
758
|
+
// honors the same contract as the one it replaces.
|
|
759
|
+
const constraints = {
|
|
760
|
+
video: Object.assign(Object.assign({}, this.sessionResolutionConstraints), { facingMode: nextCamera }),
|
|
761
|
+
audio: false,
|
|
762
|
+
};
|
|
763
|
+
let newStream;
|
|
409
764
|
try {
|
|
410
|
-
|
|
411
|
-
this.currentCamera = this.currentCamera === 'user' ? 'environment' : 'user';
|
|
412
|
-
// Stop current stream
|
|
413
|
-
if (this.stream) {
|
|
414
|
-
this.stream.getTracks().forEach((track) => track.stop());
|
|
415
|
-
}
|
|
416
|
-
// Restart with new facing mode
|
|
417
|
-
const constraints = {
|
|
418
|
-
video: {
|
|
419
|
-
facingMode: this.currentCamera,
|
|
420
|
-
},
|
|
421
|
-
audio: false,
|
|
422
|
-
};
|
|
423
|
-
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
424
|
-
if (this.videoElement) {
|
|
425
|
-
this.videoElement.srcObject = this.stream;
|
|
426
|
-
}
|
|
765
|
+
newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
427
766
|
}
|
|
428
767
|
catch (err) {
|
|
429
|
-
|
|
768
|
+
// Acquisition failed: `stream`/`currentCamera` haven't been touched, so
|
|
769
|
+
// the previous camera is still attached and running. Just surface the
|
|
770
|
+
// error. The only `getUserMedia` call in this method re-acquires the
|
|
771
|
+
// camera (never the microphone), so classify with kind: 'camera'.
|
|
772
|
+
throw new CameraViewError(`Failed to flip camera: ${this.formatError(err)}`, classifyGetUserMediaErrorCode(err, 'camera'));
|
|
773
|
+
}
|
|
774
|
+
// The new stream is live: safe to stop the previous stream's tracks and
|
|
775
|
+
// commit the flipped state.
|
|
776
|
+
(_a = this.stream) === null || _a === void 0 ? void 0 : _a.getTracks().forEach((track) => track.stop());
|
|
777
|
+
this.stream = newStream;
|
|
778
|
+
this.currentCamera = nextCamera;
|
|
779
|
+
if (this.videoElement) {
|
|
780
|
+
this.videoElement.srcObject = newStream;
|
|
781
|
+
}
|
|
782
|
+
// Re-apply the session's zoom level to the new stream through the normal
|
|
783
|
+
// setZoom() path - a flip otherwise silently drops native-zoom's applied
|
|
784
|
+
// constraint (reset on the fresh track) or leaves CSS-fallback's
|
|
785
|
+
// transform stale against the fresh element state. Best-effort: a zoom
|
|
786
|
+
// failure must not fail the flip itself.
|
|
787
|
+
try {
|
|
788
|
+
await this.setZoom({ level: this.currentZoom });
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
console.warn('[CameraView] Failed to re-apply zoom after flipping camera', err);
|
|
430
792
|
}
|
|
431
793
|
}
|
|
432
794
|
/**
|
|
433
|
-
* Get available camera devices
|
|
795
|
+
* Get available camera devices.
|
|
796
|
+
*
|
|
797
|
+
* Position detection prefers the `facingMode` capability of the active video
|
|
798
|
+
* track, since it is a standardized signal rather than a locale-dependent
|
|
799
|
+
* string. Every other device falls back to matching the English word "front"
|
|
800
|
+
* in `device.label` — which is empty for all devices until camera permission
|
|
801
|
+
* has been granted once, so the fallback resolves to `'back'` in that case.
|
|
434
802
|
*/
|
|
435
803
|
async getAvailableDevices() {
|
|
804
|
+
var _a;
|
|
436
805
|
try {
|
|
437
806
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
438
807
|
const videoDevices = devices.filter((device) => device.kind === 'videoinput');
|
|
808
|
+
const activeTrack = this.getVideoTrack();
|
|
809
|
+
const activeDeviceId = activeTrack === null || activeTrack === void 0 ? void 0 : activeTrack.getSettings().deviceId;
|
|
810
|
+
const activeFacingMode = activeTrack && typeof activeTrack.getCapabilities === 'function'
|
|
811
|
+
? (_a = activeTrack.getCapabilities().facingMode) === null || _a === void 0 ? void 0 : _a[0]
|
|
812
|
+
: undefined;
|
|
439
813
|
return {
|
|
440
|
-
devices: videoDevices.map((device) =>
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
814
|
+
devices: videoDevices.map((device) => {
|
|
815
|
+
const position = device.deviceId === activeDeviceId && activeFacingMode
|
|
816
|
+
? activeFacingMode === 'user'
|
|
817
|
+
? 'front'
|
|
818
|
+
: 'back'
|
|
819
|
+
: device.label.toLowerCase().includes('front')
|
|
820
|
+
? 'front'
|
|
821
|
+
: 'back';
|
|
822
|
+
return {
|
|
823
|
+
id: device.deviceId,
|
|
824
|
+
name: device.label || `Camera ${device.deviceId.substring(0, 5)}`,
|
|
825
|
+
position,
|
|
826
|
+
};
|
|
827
|
+
}),
|
|
445
828
|
};
|
|
446
829
|
}
|
|
447
830
|
catch (err) {
|
|
@@ -450,31 +833,106 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
450
833
|
}
|
|
451
834
|
}
|
|
452
835
|
/**
|
|
453
|
-
* Get current zoom information
|
|
836
|
+
* Get current zoom information.
|
|
837
|
+
*
|
|
838
|
+
* When the active video track exposes a native `zoom` capability (Chromium
|
|
839
|
+
* on capable cameras), the real min/max/current are reported. Otherwise the
|
|
840
|
+
* simulated CSS-scale range is returned.
|
|
454
841
|
*/
|
|
455
842
|
async getZoom() {
|
|
456
|
-
|
|
457
|
-
|
|
843
|
+
const track = this.getVideoTrack();
|
|
844
|
+
const capability = this.getNativeZoomCapability(track);
|
|
845
|
+
if (track && capability) {
|
|
846
|
+
const settings = track.getSettings();
|
|
847
|
+
return {
|
|
848
|
+
min: capability.min,
|
|
849
|
+
max: capability.max,
|
|
850
|
+
current: typeof settings.zoom === 'number' ? settings.zoom : this.currentZoom,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
// No native zoom: report the simulated CSS-scale range.
|
|
458
854
|
return {
|
|
459
|
-
min:
|
|
460
|
-
max:
|
|
855
|
+
min: SIMULATED_ZOOM_MIN,
|
|
856
|
+
max: SIMULATED_ZOOM_MAX,
|
|
461
857
|
current: this.currentZoom,
|
|
462
858
|
};
|
|
463
859
|
}
|
|
464
860
|
/**
|
|
465
|
-
* Set zoom level
|
|
861
|
+
* Set the zoom level.
|
|
862
|
+
*
|
|
863
|
+
* Prefers real zoom via `track.applyConstraints({ advanced: [{ zoom }] })`
|
|
864
|
+
* when the browser exposes the native `zoom` capability, clamping to the
|
|
865
|
+
* reported range. Falls back to a CSS `transform: scale()` simulation
|
|
866
|
+
* otherwise.
|
|
466
867
|
*/
|
|
467
868
|
async setZoom(options) {
|
|
468
|
-
|
|
869
|
+
const track = this.getVideoTrack();
|
|
870
|
+
const capability = this.getNativeZoomCapability(track);
|
|
871
|
+
if (track && capability) {
|
|
872
|
+
const clamped = Math.max(capability.min, Math.min(options.level, capability.max));
|
|
873
|
+
await track.applyConstraints({ advanced: [{ zoom: clamped }] });
|
|
874
|
+
this.currentZoom = clamped;
|
|
875
|
+
this.usingNativeZoom = true;
|
|
876
|
+
// Clear any CSS transform left over from a previous fallback so the two
|
|
877
|
+
// zoom mechanisms can't stack.
|
|
878
|
+
if (this.videoElement) {
|
|
879
|
+
this.videoElement.style.transform = '';
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
// CSS-transform fallback.
|
|
884
|
+
this.usingNativeZoom = false;
|
|
469
885
|
this.currentZoom = options.level;
|
|
470
|
-
// Apply visual zoom using CSS transform when native zoom isn't supported
|
|
471
886
|
if (this.videoElement) {
|
|
472
887
|
this.videoElement.style.transition = options.ramp ? 'transform 0.2s ease-in-out' : 'none';
|
|
473
|
-
|
|
474
|
-
this.videoElement.style.transform = `scale(${scale})`;
|
|
888
|
+
this.videoElement.style.transform = `scale(${this.getCssZoomScale()})`;
|
|
475
889
|
this.videoElement.style.transformOrigin = 'center';
|
|
476
890
|
}
|
|
477
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* The video track backing the active stream, or `null`.
|
|
894
|
+
*/
|
|
895
|
+
getVideoTrack() {
|
|
896
|
+
var _a, _b;
|
|
897
|
+
return (_b = (_a = this.stream) === null || _a === void 0 ? void 0 : _a.getVideoTracks()[0]) !== null && _b !== void 0 ? _b : null;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Reads the native `zoom` capability off a track, if the browser both
|
|
901
|
+
* supports `getCapabilities()` and exposes a usable `zoom` range on this
|
|
902
|
+
* device. Returns `null` when native zoom is unavailable (CSS fallback).
|
|
903
|
+
*/
|
|
904
|
+
getNativeZoomCapability(track) {
|
|
905
|
+
if (!track || typeof track.getCapabilities !== 'function') {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
const zoom = track.getCapabilities().zoom;
|
|
909
|
+
if (zoom && typeof zoom.min === 'number' && typeof zoom.max === 'number' && zoom.max > zoom.min) {
|
|
910
|
+
return zoom;
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* The effective CSS `transform: scale()` factor currently applied to the
|
|
916
|
+
* preview: `1` when native zoom is in use (no transform), otherwise
|
|
917
|
+
* `currentZoom` clamped to the simulated range. Used both to set the
|
|
918
|
+
* transform and to compensate captures for it.
|
|
919
|
+
*/
|
|
920
|
+
getCssZoomScale() {
|
|
921
|
+
if (this.usingNativeZoom) {
|
|
922
|
+
return 1;
|
|
923
|
+
}
|
|
924
|
+
return Math.max(SIMULATED_ZOOM_MIN, Math.min(this.currentZoom, SIMULATED_ZOOM_MAX));
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Set the focus/metering point (not supported in web).
|
|
928
|
+
*
|
|
929
|
+
* The `pointsOfInterest` media-track constraint has effectively no browser
|
|
930
|
+
* support, so this rejects with `unimplemented` rather than silently doing
|
|
931
|
+
* nothing. Left as a hook for a future implementation.
|
|
932
|
+
*/
|
|
933
|
+
async setFocusPoint() {
|
|
934
|
+
throw this.unimplemented('Focus point control is not supported in the web implementation.');
|
|
935
|
+
}
|
|
478
936
|
/**
|
|
479
937
|
* Get current flash mode
|
|
480
938
|
*/
|
|
@@ -489,11 +947,16 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
489
947
|
return { flashModes: ['off'] };
|
|
490
948
|
}
|
|
491
949
|
/**
|
|
492
|
-
* Set flash mode (limited support in web)
|
|
950
|
+
* Set flash mode (limited support in web).
|
|
951
|
+
*
|
|
952
|
+
* Only `'off'` is supported on web; any other mode rejects rather than
|
|
953
|
+
* silently accepting a mode it cannot apply.
|
|
493
954
|
*/
|
|
494
955
|
async setFlashMode(options) {
|
|
495
|
-
|
|
496
|
-
|
|
956
|
+
if (options.mode !== 'off') {
|
|
957
|
+
throw this.unimplemented('Flash mode control is not supported in the web implementation.');
|
|
958
|
+
}
|
|
959
|
+
this.currentFlashMode = 'off';
|
|
497
960
|
}
|
|
498
961
|
/**
|
|
499
962
|
* Check if torch is available (not supported in web)
|
|
@@ -503,11 +966,14 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
503
966
|
return { available: false };
|
|
504
967
|
}
|
|
505
968
|
/**
|
|
506
|
-
* Get torch mode (not supported in web)
|
|
969
|
+
* Get torch mode (not supported in web).
|
|
970
|
+
*
|
|
971
|
+
* Follows the documented contract for `getTorchMode()`: callers must check
|
|
972
|
+
* `isTorchAvailable()` first, which always reports `false` on web, so this throws
|
|
973
|
+
* rather than returning a fabricated "off" state.
|
|
507
974
|
*/
|
|
508
975
|
async getTorchMode() {
|
|
509
|
-
|
|
510
|
-
return { enabled: false, level: 0.0 };
|
|
976
|
+
throw this.unimplemented('Torch control is not supported in web implementation.');
|
|
511
977
|
}
|
|
512
978
|
/**
|
|
513
979
|
* Set torch mode (not supported in web)
|
|
@@ -520,35 +986,38 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
520
986
|
* Check camera and microphone permission without requesting
|
|
521
987
|
*/
|
|
522
988
|
async checkPermissions() {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
989
|
+
const [camera, microphone] = await Promise.all([
|
|
990
|
+
this.checkSinglePermission('camera'),
|
|
991
|
+
this.checkSinglePermission('microphone'),
|
|
992
|
+
]);
|
|
993
|
+
return { camera, microphone };
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Resolves the current state of a single permission.
|
|
997
|
+
*
|
|
998
|
+
* Queried independently per permission name so one unsupported query (e.g.
|
|
999
|
+
* Firefox does not support querying `'microphone'`) rejects on its own
|
|
1000
|
+
* instead of collapsing *both* permissions to `'prompt'`.
|
|
1001
|
+
*/
|
|
1002
|
+
async checkSinglePermission(name) {
|
|
1003
|
+
if (navigator.permissions) {
|
|
1004
|
+
try {
|
|
1005
|
+
const result = await navigator.permissions.query({ name: name });
|
|
1006
|
+
return result.state === 'granted' ? 'granted' : result.state === 'denied' ? 'denied' : 'prompt';
|
|
1007
|
+
}
|
|
1008
|
+
catch (_a) {
|
|
1009
|
+
// This permission name is not supported by the Permissions API in this
|
|
1010
|
+
// browser; fall through to the best-effort fallback below instead of
|
|
1011
|
+
// failing the other permission's check too.
|
|
538
1012
|
}
|
|
539
|
-
// If Permissions API is not available, fall back to checking the active stream
|
|
540
|
-
return {
|
|
541
|
-
camera: this.stream ? 'granted' : 'prompt',
|
|
542
|
-
microphone: 'prompt',
|
|
543
|
-
};
|
|
544
1013
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
};
|
|
1014
|
+
// If the Permissions API is unavailable/unsupported for this name, fall back to
|
|
1015
|
+
// checking the active stream for camera; there is no equivalent signal for
|
|
1016
|
+
// microphone without an active audio track, so it stays 'prompt'.
|
|
1017
|
+
if (name === 'camera') {
|
|
1018
|
+
return this.stream ? 'granted' : 'prompt';
|
|
551
1019
|
}
|
|
1020
|
+
return 'prompt';
|
|
552
1021
|
}
|
|
553
1022
|
/**
|
|
554
1023
|
* Request camera and/or microphone permissions from the user.
|
|
@@ -593,70 +1062,125 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
593
1062
|
* Start barcode detection if supported
|
|
594
1063
|
*/
|
|
595
1064
|
async startBarcodeDetection() {
|
|
1065
|
+
var _a;
|
|
596
1066
|
const barcodeDetector = this.barcodeDetector;
|
|
597
1067
|
const videoElement = this.videoElement;
|
|
598
1068
|
if (!this.barcodeDetectionSupported || !barcodeDetector || !videoElement) {
|
|
599
1069
|
return;
|
|
600
1070
|
}
|
|
601
|
-
//
|
|
1071
|
+
// Scope this loop to its own session. Aborting the previous controller
|
|
1072
|
+
// (defensive - start() only calls in here once per session) and handing
|
|
1073
|
+
// out a fresh signal means a stale detectFrame closure from an earlier
|
|
1074
|
+
// session can never mistake a later session's #isRunning === true for
|
|
1075
|
+
// its own "keep going" signal.
|
|
1076
|
+
(_a = this.barcodeDetectionAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
1077
|
+
const abortController = new AbortController();
|
|
1078
|
+
this.barcodeDetectionAbortController = abortController;
|
|
1079
|
+
const { signal } = abortController;
|
|
1080
|
+
// Make sure video is fully loaded before starting detection. The wait
|
|
1081
|
+
// settles - without starting detection - as soon as the session is
|
|
1082
|
+
// stopped (`signal` aborts), and a timeout backstops the case where the
|
|
1083
|
+
// video never fires `loadeddata` at all, so neither path leaves a
|
|
1084
|
+
// dangling listener or a permanently pending promise.
|
|
602
1085
|
if (videoElement.readyState < 2) {
|
|
603
|
-
await new Promise((resolve) => {
|
|
604
|
-
const
|
|
1086
|
+
const videoReady = await new Promise((resolve) => {
|
|
1087
|
+
const cleanup = () => {
|
|
605
1088
|
videoElement.removeEventListener('loadeddata', loadHandler);
|
|
606
|
-
|
|
1089
|
+
signal.removeEventListener('abort', abortHandler);
|
|
1090
|
+
clearTimeout(timeoutId);
|
|
607
1091
|
};
|
|
1092
|
+
const loadHandler = () => {
|
|
1093
|
+
cleanup();
|
|
1094
|
+
resolve(true);
|
|
1095
|
+
};
|
|
1096
|
+
const abortHandler = () => {
|
|
1097
|
+
cleanup();
|
|
1098
|
+
resolve(false);
|
|
1099
|
+
};
|
|
1100
|
+
const timeoutId = setTimeout(() => {
|
|
1101
|
+
cleanup();
|
|
1102
|
+
resolve(false);
|
|
1103
|
+
}, BARCODE_VIDEO_READY_TIMEOUT_MS);
|
|
608
1104
|
videoElement.addEventListener('loadeddata', loadHandler);
|
|
1105
|
+
signal.addEventListener('abort', abortHandler);
|
|
609
1106
|
});
|
|
1107
|
+
if (!videoReady || signal.aborted) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (signal.aborted) {
|
|
1112
|
+
return;
|
|
610
1113
|
}
|
|
611
1114
|
// Add throttling to reduce CPU usage
|
|
612
1115
|
let lastDetectionTime = 0;
|
|
613
1116
|
const minTimeBetweenDetections = 100; // ms
|
|
1117
|
+
// Dedupe state: timestamps of recently emitted barcodes keyed by value +
|
|
1118
|
+
// type. A per-key map (rather than a single "last" slot) is required so
|
|
1119
|
+
// multiple codes in frame can't alternate and defeat the suppression
|
|
1120
|
+
// window. Closure-local, so it resets on every start().
|
|
1121
|
+
const recentBarcodeEmitTimes = new Map();
|
|
614
1122
|
// Set up periodic frame analysis for barcode detection
|
|
615
1123
|
const detectFrame = async () => {
|
|
616
|
-
|
|
1124
|
+
var _a;
|
|
1125
|
+
// `signal.aborted` is this loop's own session check: it stays true for
|
|
1126
|
+
// this closure even if a rapid stop() -> start() flips #isRunning back
|
|
1127
|
+
// to true for a *new* session before this frame runs. `#isRunning` is
|
|
1128
|
+
// kept as a defensive secondary check.
|
|
1129
|
+
if (signal.aborted || !__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f") || !videoElement || !barcodeDetector) {
|
|
617
1130
|
return;
|
|
618
1131
|
}
|
|
619
|
-
|
|
1132
|
+
// Monotonic clock: a backward wall-clock jump must not extend the
|
|
1133
|
+
// throttle or suppression windows arbitrarily.
|
|
1134
|
+
const now = performance.now();
|
|
620
1135
|
if (now - lastDetectionTime >= minTimeBetweenDetections) {
|
|
621
1136
|
try {
|
|
622
1137
|
const barcodes = await barcodeDetector.detect(videoElement);
|
|
623
1138
|
lastDetectionTime = now;
|
|
624
1139
|
if (barcodes.length > 0) {
|
|
625
1140
|
const barcode = barcodes[0];
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
1141
|
+
// Normalize the web BarcodeDetector format onto the shared BarcodeType
|
|
1142
|
+
// vocabulary. Fall back to the raw format for the few detector formats
|
|
1143
|
+
// that have no BarcodeType equivalent (e.g. 'unknown'); the emitted
|
|
1144
|
+
// `type` is therefore `BarcodeType | string`.
|
|
1145
|
+
const type = (_a = WEB_FORMAT_TO_BARCODE_TYPE[barcode.format]) !== null && _a !== void 0 ? _a : barcode.format;
|
|
1146
|
+
// Rate control: suppress re-emission of the same code (value + type)
|
|
1147
|
+
// within the suppression window. A genuinely new code has a different
|
|
1148
|
+
// key and emits immediately. Timestamps are tracked per key so
|
|
1149
|
+
// multiple codes in frame can't alternate and defeat the window.
|
|
1150
|
+
const barcodeKey = `${type}\u0000${barcode.rawValue}`;
|
|
1151
|
+
const lastEmit = recentBarcodeEmitTimes.get(barcodeKey);
|
|
1152
|
+
if (lastEmit === undefined || now - lastEmit >= BARCODE_SUPPRESSION_WINDOW_MS) {
|
|
1153
|
+
// Prune expired entries once the map grows, keeping it bounded
|
|
1154
|
+
// during long sessions that scan many different codes.
|
|
1155
|
+
if (recentBarcodeEmitTimes.size > BARCODE_DEDUPE_MAP_PRUNE_THRESHOLD) {
|
|
1156
|
+
for (const [key, emitTime] of recentBarcodeEmitTimes) {
|
|
1157
|
+
if (now - emitTime >= BARCODE_SUPPRESSION_WINDOW_MS) {
|
|
1158
|
+
recentBarcodeEmitTimes.delete(key);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
recentBarcodeEmitTimes.set(barcodeKey, now);
|
|
1163
|
+
// Transform barcode coordinates using the utility function,
|
|
1164
|
+
// accounting for the session's preview scale mode (cover crops,
|
|
1165
|
+
// fit letterboxes) so the rect lands over the on-screen barcode.
|
|
1166
|
+
const boundingRect = transformBarcodeBoundingBox(barcode.boundingBox, videoElement, this.sessionPreviewScaleMode);
|
|
1167
|
+
this.notifyListeners('barcodeDetected', {
|
|
1168
|
+
value: barcode.rawValue,
|
|
1169
|
+
type,
|
|
1170
|
+
boundingRect,
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
633
1173
|
}
|
|
634
1174
|
}
|
|
635
1175
|
catch (err) {
|
|
636
1176
|
console.error('Barcode detection error', err);
|
|
637
1177
|
}
|
|
638
1178
|
}
|
|
639
|
-
if (__classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f")) {
|
|
640
|
-
requestAnimationFrame(detectFrame);
|
|
1179
|
+
if (!signal.aborted && __classPrivateFieldGet(this, _CameraViewWeb_isRunning, "f")) {
|
|
1180
|
+
this.barcodeAnimationFrameId = requestAnimationFrame(detectFrame);
|
|
641
1181
|
}
|
|
642
1182
|
};
|
|
643
|
-
requestAnimationFrame(detectFrame);
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Clean up resources when the plugin is disposed
|
|
647
|
-
*/
|
|
648
|
-
async handleOnDestroy() {
|
|
649
|
-
var _a;
|
|
650
|
-
await this.stop();
|
|
651
|
-
// Remove elements from DOM
|
|
652
|
-
if ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.parentNode) {
|
|
653
|
-
this.videoElement.parentNode.removeChild(this.videoElement);
|
|
654
|
-
this.videoElement = null;
|
|
655
|
-
}
|
|
656
|
-
if (this.canvasElement) {
|
|
657
|
-
this.canvasElement = null;
|
|
658
|
-
}
|
|
659
|
-
this.barcodeDetector = null;
|
|
1183
|
+
this.barcodeAnimationFrameId = requestAnimationFrame(detectFrame);
|
|
660
1184
|
}
|
|
661
1185
|
/**
|
|
662
1186
|
* Check if barcode detection is supported in this browser
|
|
@@ -763,8 +1287,17 @@ var capacitorCameraView = (function (exports, core) {
|
|
|
763
1287
|
|
|
764
1288
|
var web = /*#__PURE__*/Object.freeze({
|
|
765
1289
|
__proto__: null,
|
|
1290
|
+
BARCODE_DEDUPE_MAP_PRUNE_THRESHOLD: BARCODE_DEDUPE_MAP_PRUNE_THRESHOLD,
|
|
1291
|
+
BARCODE_SUPPRESSION_WINDOW_MS: BARCODE_SUPPRESSION_WINDOW_MS,
|
|
766
1292
|
BARCODE_TYPE_TO_WEB_FORMAT: BARCODE_TYPE_TO_WEB_FORMAT,
|
|
767
|
-
|
|
1293
|
+
BARCODE_VIDEO_READY_TIMEOUT_MS: BARCODE_VIDEO_READY_TIMEOUT_MS,
|
|
1294
|
+
CameraViewError: CameraViewError,
|
|
1295
|
+
CameraViewWeb: CameraViewWeb,
|
|
1296
|
+
DEFAULT_IDEAL_CAPTURE_WIDTH: DEFAULT_IDEAL_CAPTURE_WIDTH,
|
|
1297
|
+
SIMULATED_ZOOM_MAX: SIMULATED_ZOOM_MAX,
|
|
1298
|
+
SIMULATED_ZOOM_MIN: SIMULATED_ZOOM_MIN,
|
|
1299
|
+
WEB_FORMAT_TO_BARCODE_TYPE: WEB_FORMAT_TO_BARCODE_TYPE,
|
|
1300
|
+
classifyGetUserMediaErrorCode: classifyGetUserMediaErrorCode
|
|
768
1301
|
});
|
|
769
1302
|
|
|
770
1303
|
exports.CameraView = CameraView;
|