cordova-plugin-mlkit-suite 0.2.0 → 0.2.1
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/CHANGELOG.md +5 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/plugin.xml +3 -1
- package/src/android/java/BarcodeScanActivity.java +4 -59
- package/src/android/java/FaceCaptureActivity.java +15 -2
- package/src/android/java/FaceCaptureConfig.java +7 -2
- package/src/android/java/MlKitSuccessBeep.java +75 -0
- package/src/ios/BarcodeScanViewController.swift +2 -69
- package/src/ios/FaceCaptureTypes.swift +3 -1
- package/src/ios/FaceCaptureViewController.swift +10 -1
- package/src/ios/MlKitSuccessBeep.swift +82 -0
- package/tests/ios-face-capture.test.js +4 -0
- package/tests/ios-live-barcode.test.js +5 -1
- package/www/MlKit.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.1 - 2026-10-26
|
|
6
|
+
|
|
7
|
+
- Added the optional `beepEnabled` Face Capture confirmation sound, enabled by default on Android and iOS.
|
|
8
|
+
- Reused one synthesized native confirmation tone for Barcode Scanning and Face Capture.
|
|
9
|
+
|
|
5
10
|
## 0.2.0 - 2026-10-26
|
|
6
11
|
|
|
7
12
|
- Added default-model Image Labeling on Android and iOS, including configurable Android model delivery.
|
package/README.md
CHANGED
|
@@ -429,6 +429,7 @@ All checks are enabled by default. Capture starts automatically only after they
|
|
|
429
429
|
| `jpegQuality` | integer `50–100` | `95` | JPEG encoding quality. |
|
|
430
430
|
| `showCloseButton` | `boolean` | `true` | Shows the native close button. |
|
|
431
431
|
| `debugMode` | `boolean` | `false` | Shows face bounds and live quality values without weakening validation. |
|
|
432
|
+
| `beepEnabled` | `boolean` | `true` | Plays a short synthesized confirmation beep after a successful capture. Device silent mode is respected. |
|
|
432
433
|
| `instructionText` | `string \| null` | `"Place your face inside the oval"` | Text shown above the guide. An empty string hides it. |
|
|
433
434
|
| `moduleLoadingMessage` | `string \| null` | localized | Android text shown only while downloading Face Detection. Empty string shows only the spinner. |
|
|
434
435
|
| `locale` | `string` | `"auto"` | Plugin UI/error locale (`en`, `es`, or BCP-47). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cordova-plugin-mlkit-suite",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Extensible Google ML Kit suite for Cordova document, text, barcode, image labeling, selfie and subject segmentation, and guided face processing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Andres Zs",
|
package/plugin.xml
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
4
4
|
xmlns:tools="http://schemas.android.com/tools"
|
|
5
5
|
id="cordova-plugin-mlkit-suite"
|
|
6
|
-
version="0.2.
|
|
6
|
+
version="0.2.1">
|
|
7
7
|
<name>ML Kit Suite</name>
|
|
8
8
|
<description>Extensible Google ML Kit suite for document, text, barcode, image labeling, selfie and subject segmentation, and guided face processing.</description>
|
|
9
9
|
<license>MIT</license>
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
<resource-file src="src/android/res/values/mlkit_suite_strings.xml" target="res/values/mlkit_suite_strings.xml" />
|
|
82
82
|
<resource-file src="src/android/res/values-es/mlkit_suite_strings.xml" target="res/values-es/mlkit_suite_strings.xml" />
|
|
83
83
|
<source-file src="src/android/java/MlKitLocalization.java" target-dir="app/src/main/java/com/andreszs/plugins/mlkitsuite" />
|
|
84
|
+
<source-file src="src/android/java/MlKitSuccessBeep.java" target-dir="app/src/main/java/com/andreszs/plugins/mlkitsuite" />
|
|
84
85
|
<source-file src="src/android/java/MlKitSuitePlugin.java" target-dir="app/src/main/java/com/andreszs/plugins/mlkitsuite" />
|
|
85
86
|
<source-file src="src/android/java/FaceCaptureActivity.java" target-dir="app/src/main/java/com/andreszs/plugins/mlkitsuite" />
|
|
86
87
|
<source-file src="src/android/java/FaceCaptureConfig.java" target-dir="app/src/main/java/com/andreszs/plugins/mlkitsuite" />
|
|
@@ -115,6 +116,7 @@
|
|
|
115
116
|
</config-file>
|
|
116
117
|
|
|
117
118
|
<source-file src="src/ios/FaceCaptureTypes.swift" />
|
|
119
|
+
<source-file src="src/ios/MlKitSuccessBeep.swift" />
|
|
118
120
|
<source-file src="src/ios/MlKitLocalization.swift" />
|
|
119
121
|
<source-file src="src/ios/FaceOverlayView.swift" />
|
|
120
122
|
<source-file src="src/ios/FaceStillProcessor.swift" />
|
|
@@ -5,10 +5,6 @@ import android.content.Intent;
|
|
|
5
5
|
import android.graphics.Bitmap;
|
|
6
6
|
import android.graphics.Color;
|
|
7
7
|
import android.graphics.Rect;
|
|
8
|
-
import android.media.AudioAttributes;
|
|
9
|
-
import android.media.AudioFormat;
|
|
10
|
-
import android.media.AudioManager;
|
|
11
|
-
import android.media.AudioTrack;
|
|
12
8
|
import android.os.Bundle;
|
|
13
9
|
import android.os.Handler;
|
|
14
10
|
import android.os.Looper;
|
|
@@ -66,7 +62,7 @@ public final class BarcodeScanActivity extends Activity
|
|
|
66
62
|
private ImageAnalysis imageAnalysis;
|
|
67
63
|
private BarcodeFrameAnalyzer analyzer;
|
|
68
64
|
private Camera camera;
|
|
69
|
-
private
|
|
65
|
+
private MlKitSuccessBeep successBeep;
|
|
70
66
|
private boolean torchEnabled;
|
|
71
67
|
private boolean completed;
|
|
72
68
|
private Runnable timeoutRunnable;
|
|
@@ -82,6 +78,7 @@ public final class BarcodeScanActivity extends Activity
|
|
|
82
78
|
try {
|
|
83
79
|
config = BarcodeScanConfig.fromJson(this, new JSONObject(
|
|
84
80
|
getIntent().getStringExtra(EXTRA_OPTIONS_JSON)));
|
|
81
|
+
successBeep = new MlKitSuccessBeep(this, mainHandler);
|
|
85
82
|
} catch (Exception error) {
|
|
86
83
|
finishWithError("INVALID_ARGUMENT", "Invalid barcode scan options.", false);
|
|
87
84
|
return;
|
|
@@ -124,7 +121,7 @@ public final class BarcodeScanActivity extends Activity
|
|
|
124
121
|
if (imageAnalysis != null) imageAnalysis.clearAnalyzer();
|
|
125
122
|
if (analyzer != null) analyzer.close();
|
|
126
123
|
if (cameraProvider != null) cameraProvider.unbindAll();
|
|
127
|
-
|
|
124
|
+
if (successBeep != null) successBeep.close();
|
|
128
125
|
analysisExecutor.shutdownNow();
|
|
129
126
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
|
|
130
127
|
super.onDestroy();
|
|
@@ -330,59 +327,7 @@ public final class BarcodeScanActivity extends Activity
|
|
|
330
327
|
}
|
|
331
328
|
|
|
332
329
|
private void playSuccessBeep() {
|
|
333
|
-
if (
|
|
334
|
-
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
|
335
|
-
if (audioManager != null && audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
final int sampleRate = 22050;
|
|
339
|
-
final int sampleCount = sampleRate / 10;
|
|
340
|
-
byte[] pcm = new byte[sampleCount * 2];
|
|
341
|
-
for (int index = 0; index < sampleCount; index++) {
|
|
342
|
-
double time = index / (double) sampleRate;
|
|
343
|
-
double remaining = (sampleCount - index) / (double) sampleRate;
|
|
344
|
-
double envelope = Math.min(1d, time / 0.005d) * Math.min(1d, remaining / 0.025d);
|
|
345
|
-
short sample = (short) Math.round(Math.sin(2d * Math.PI * 1800d * time)
|
|
346
|
-
* envelope * Short.MAX_VALUE * 0.55d);
|
|
347
|
-
pcm[index * 2] = (byte) (sample & 0xff);
|
|
348
|
-
pcm[index * 2 + 1] = (byte) ((sample >>> 8) & 0xff);
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
AudioTrack track = new AudioTrack.Builder()
|
|
352
|
-
.setAudioAttributes(new AudioAttributes.Builder()
|
|
353
|
-
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
|
|
354
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
355
|
-
.build())
|
|
356
|
-
.setAudioFormat(new AudioFormat.Builder()
|
|
357
|
-
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
358
|
-
.setSampleRate(sampleRate)
|
|
359
|
-
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
|
360
|
-
.build())
|
|
361
|
-
.setTransferMode(AudioTrack.MODE_STATIC)
|
|
362
|
-
.setBufferSizeInBytes(pcm.length)
|
|
363
|
-
.build();
|
|
364
|
-
if (track.write(pcm, 0, pcm.length) <= 0) {
|
|
365
|
-
track.release();
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
releaseSuccessBeep();
|
|
369
|
-
successBeep = track;
|
|
370
|
-
track.play();
|
|
371
|
-
mainHandler.postDelayed(() -> {
|
|
372
|
-
if (successBeep == track) releaseSuccessBeep();
|
|
373
|
-
}, 250L);
|
|
374
|
-
} catch (Exception ignored) {
|
|
375
|
-
releaseSuccessBeep();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
private void releaseSuccessBeep() {
|
|
380
|
-
if (successBeep == null) return;
|
|
381
|
-
try {
|
|
382
|
-
successBeep.stop();
|
|
383
|
-
} catch (Exception ignored) { }
|
|
384
|
-
successBeep.release();
|
|
385
|
-
successBeep = null;
|
|
330
|
+
if (config.beepEnabled && successBeep != null) successBeep.play();
|
|
386
331
|
}
|
|
387
332
|
|
|
388
333
|
private FrameLayout.LayoutParams matchParent() {
|
|
@@ -41,6 +41,8 @@ import java.util.concurrent.Executors;
|
|
|
41
41
|
public final class FaceCaptureActivity extends Activity
|
|
42
42
|
implements LifecycleOwner, FaceFrameAnalyzer.Listener {
|
|
43
43
|
|
|
44
|
+
private static final long SUCCESS_BEEP_DELAY_MS = 250L;
|
|
45
|
+
|
|
44
46
|
public static final String EXTRA_OPTIONS_JSON = "mlKitSuite.face.options";
|
|
45
47
|
public static final String EXTRA_IMAGE_WEB_BASE_URL = "mlKitSuite.face.imageWebBaseUrl";
|
|
46
48
|
public static final String EXTRA_RESULT_JSON = "mlKitSuite.face.result";
|
|
@@ -60,6 +62,7 @@ public final class FaceCaptureActivity extends Activity
|
|
|
60
62
|
private ImageAnalysis imageAnalysis;
|
|
61
63
|
private FaceFrameAnalyzer analyzer;
|
|
62
64
|
private FaceStillProcessor stillProcessor;
|
|
65
|
+
private MlKitSuccessBeep successBeep;
|
|
63
66
|
private String imageWebBaseUrl;
|
|
64
67
|
private boolean capturing;
|
|
65
68
|
private boolean completed;
|
|
@@ -78,6 +81,7 @@ public final class FaceCaptureActivity extends Activity
|
|
|
78
81
|
getIntent().getStringExtra(EXTRA_OPTIONS_JSON)));
|
|
79
82
|
imageWebBaseUrl = getIntent().getStringExtra(EXTRA_IMAGE_WEB_BASE_URL);
|
|
80
83
|
stillProcessor = new FaceStillProcessor(config);
|
|
84
|
+
successBeep = new MlKitSuccessBeep(this, mainHandler);
|
|
81
85
|
} catch (Exception error) {
|
|
82
86
|
finishWithError("INVALID_ARGUMENT", "Invalid face capture options.", false);
|
|
83
87
|
return;
|
|
@@ -121,6 +125,7 @@ public final class FaceCaptureActivity extends Activity
|
|
|
121
125
|
if (analyzer != null) analyzer.close();
|
|
122
126
|
if (stillProcessor != null) stillProcessor.close();
|
|
123
127
|
if (cameraProvider != null) cameraProvider.unbindAll();
|
|
128
|
+
if (successBeep != null) successBeep.close();
|
|
124
129
|
analysisExecutor.shutdownNow();
|
|
125
130
|
captureExecutor.shutdownNow();
|
|
126
131
|
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
|
|
@@ -291,8 +296,16 @@ public final class FaceCaptureActivity extends Activity
|
|
|
291
296
|
} catch (Exception ignored) { }
|
|
292
297
|
Intent intent = new Intent();
|
|
293
298
|
intent.putExtra(EXTRA_RESULT_JSON, result.toString());
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
if (config.beepEnabled && successBeep != null) {
|
|
300
|
+
successBeep.play();
|
|
301
|
+
mainHandler.postDelayed(() -> {
|
|
302
|
+
setResult(RESULT_OK, intent);
|
|
303
|
+
finish();
|
|
304
|
+
}, SUCCESS_BEEP_DELAY_MS);
|
|
305
|
+
} else {
|
|
306
|
+
setResult(RESULT_OK, intent);
|
|
307
|
+
finish();
|
|
308
|
+
}
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
private void cancelByUser() {
|
|
@@ -13,6 +13,7 @@ final class FaceCaptureConfig {
|
|
|
13
13
|
final int jpegQuality;
|
|
14
14
|
final boolean showCloseButton;
|
|
15
15
|
final boolean debugMode;
|
|
16
|
+
final boolean beepEnabled;
|
|
16
17
|
final String instructionText;
|
|
17
18
|
final MlKitLocalization localization;
|
|
18
19
|
final long timeoutMs;
|
|
@@ -24,6 +25,7 @@ final class FaceCaptureConfig {
|
|
|
24
25
|
int jpegQuality,
|
|
25
26
|
boolean showCloseButton,
|
|
26
27
|
boolean debugMode,
|
|
28
|
+
boolean beepEnabled,
|
|
27
29
|
String instructionText,
|
|
28
30
|
MlKitLocalization localization,
|
|
29
31
|
long timeoutMs,
|
|
@@ -34,6 +36,7 @@ final class FaceCaptureConfig {
|
|
|
34
36
|
this.jpegQuality = jpegQuality;
|
|
35
37
|
this.showCloseButton = showCloseButton;
|
|
36
38
|
this.debugMode = debugMode;
|
|
39
|
+
this.beepEnabled = beepEnabled;
|
|
37
40
|
this.instructionText = instructionText;
|
|
38
41
|
this.localization = localization;
|
|
39
42
|
this.timeoutMs = timeoutMs;
|
|
@@ -51,6 +54,7 @@ final class FaceCaptureConfig {
|
|
|
51
54
|
int quality = strictInteger(options, "jpegQuality", 95, 50, 100);
|
|
52
55
|
boolean close = strictBoolean(options, "showCloseButton", true);
|
|
53
56
|
boolean debug = strictBoolean(options, "debugMode", false);
|
|
57
|
+
boolean beep = strictBoolean(options, "beepEnabled", true);
|
|
54
58
|
String instruction = strictString(options, "instructionText",
|
|
55
59
|
localization.text("face_instruction"));
|
|
56
60
|
long timeout = strictLong(options, "timeoutMs", 0L, 0L, 600000L);
|
|
@@ -62,7 +66,7 @@ final class FaceCaptureConfig {
|
|
|
62
66
|
}
|
|
63
67
|
rawChecks = (JSONObject) value;
|
|
64
68
|
}
|
|
65
|
-
return new FaceCaptureConfig(width, height, quality, close, debug, instruction,
|
|
69
|
+
return new FaceCaptureConfig(width, height, quality, close, debug, beep, instruction,
|
|
66
70
|
localization, timeout, Checks.fromJson(rawChecks));
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -72,7 +76,8 @@ final class FaceCaptureConfig {
|
|
|
72
76
|
String key = keys.next();
|
|
73
77
|
if (!("outputWidth".equals(key) || "outputHeight".equals(key)
|
|
74
78
|
|| "jpegQuality".equals(key) || "showCloseButton".equals(key)
|
|
75
|
-
|| "debugMode".equals(key) || "
|
|
79
|
+
|| "debugMode".equals(key) || "beepEnabled".equals(key)
|
|
80
|
+
|| "instructionText".equals(key)
|
|
76
81
|
|| "timeoutMs".equals(key) || "checks".equals(key)
|
|
77
82
|
|| "moduleLoadingMessage".equals(key) || "locale".equals(key))) {
|
|
78
83
|
throw new IllegalArgumentException("Unknown face capture option: " + key + ".");
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package com.andreszs.plugins.mlkitsuite;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.media.AudioAttributes;
|
|
5
|
+
import android.media.AudioFormat;
|
|
6
|
+
import android.media.AudioManager;
|
|
7
|
+
import android.media.AudioTrack;
|
|
8
|
+
import android.os.Handler;
|
|
9
|
+
|
|
10
|
+
final class MlKitSuccessBeep implements AutoCloseable {
|
|
11
|
+
private final Context context;
|
|
12
|
+
private final Handler mainHandler;
|
|
13
|
+
private AudioTrack track;
|
|
14
|
+
|
|
15
|
+
MlKitSuccessBeep(Context context, Handler mainHandler) {
|
|
16
|
+
this.context = context.getApplicationContext();
|
|
17
|
+
this.mainHandler = mainHandler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
void play() {
|
|
21
|
+
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
|
22
|
+
if (audioManager != null && audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
final int sampleRate = 22050;
|
|
26
|
+
final int sampleCount = sampleRate / 10;
|
|
27
|
+
byte[] pcm = new byte[sampleCount * 2];
|
|
28
|
+
for (int index = 0; index < sampleCount; index++) {
|
|
29
|
+
double time = index / (double) sampleRate;
|
|
30
|
+
double remaining = (sampleCount - index) / (double) sampleRate;
|
|
31
|
+
double envelope = Math.min(1d, time / 0.005d) * Math.min(1d, remaining / 0.025d);
|
|
32
|
+
short sample = (short) Math.round(Math.sin(2d * Math.PI * 1800d * time)
|
|
33
|
+
* envelope * Short.MAX_VALUE * 0.55d);
|
|
34
|
+
pcm[index * 2] = (byte) (sample & 0xff);
|
|
35
|
+
pcm[index * 2 + 1] = (byte) ((sample >>> 8) & 0xff);
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
AudioTrack nextTrack = new AudioTrack.Builder()
|
|
39
|
+
.setAudioAttributes(new AudioAttributes.Builder()
|
|
40
|
+
.setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
|
|
41
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
42
|
+
.build())
|
|
43
|
+
.setAudioFormat(new AudioFormat.Builder()
|
|
44
|
+
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
45
|
+
.setSampleRate(sampleRate)
|
|
46
|
+
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
|
|
47
|
+
.build())
|
|
48
|
+
.setTransferMode(AudioTrack.MODE_STATIC)
|
|
49
|
+
.setBufferSizeInBytes(pcm.length)
|
|
50
|
+
.build();
|
|
51
|
+
if (nextTrack.write(pcm, 0, pcm.length) <= 0) {
|
|
52
|
+
nextTrack.release();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
close();
|
|
56
|
+
track = nextTrack;
|
|
57
|
+
nextTrack.play();
|
|
58
|
+
mainHandler.postDelayed(() -> {
|
|
59
|
+
if (track == nextTrack) close();
|
|
60
|
+
}, 200L);
|
|
61
|
+
} catch (Exception ignored) {
|
|
62
|
+
close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Override
|
|
67
|
+
public void close() {
|
|
68
|
+
if (track == null) return;
|
|
69
|
+
try {
|
|
70
|
+
track.stop();
|
|
71
|
+
} catch (Exception ignored) { }
|
|
72
|
+
track.release();
|
|
73
|
+
track = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -24,7 +24,7 @@ final class BarcodeScanViewController: UIViewController {
|
|
|
24
24
|
private var overlayView: BarcodeOverlayView!
|
|
25
25
|
private var closeButton: UIButton?
|
|
26
26
|
private var torchButton: UIButton?
|
|
27
|
-
private
|
|
27
|
+
private let successBeep = MlKitSuccessBeep()
|
|
28
28
|
private var camera: AVCaptureDevice?
|
|
29
29
|
private var processingFrame = false
|
|
30
30
|
private var analysisFinished = false
|
|
@@ -372,7 +372,7 @@ final class BarcodeScanViewController: UIViewController {
|
|
|
372
372
|
imageSize: imageSize,
|
|
373
373
|
message: options.localization.text("barcode_success")
|
|
374
374
|
)
|
|
375
|
-
|
|
375
|
+
if options.beepEnabled { successBeep.play() }
|
|
376
376
|
sessionQueue.async { [session] in
|
|
377
377
|
if session.isRunning { session.stopRunning() }
|
|
378
378
|
}
|
|
@@ -391,73 +391,6 @@ final class BarcodeScanViewController: UIViewController {
|
|
|
391
391
|
return UIImage(cgImage: cgImage, scale: 1, orientation: .right)
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
-
private func playSuccessBeep() {
|
|
395
|
-
guard options.beepEnabled else { return }
|
|
396
|
-
do {
|
|
397
|
-
let session = AVAudioSession.sharedInstance()
|
|
398
|
-
try session.setCategory(.ambient, mode: .default, options: [.mixWithOthers])
|
|
399
|
-
try session.setActive(true)
|
|
400
|
-
let player = try AVAudioPlayer(data: Self.successBeepData)
|
|
401
|
-
player.volume = 1
|
|
402
|
-
player.prepareToPlay()
|
|
403
|
-
successBeepPlayer = player
|
|
404
|
-
player.play()
|
|
405
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
|
406
|
-
guard let self = self, self.successBeepPlayer === player else { return }
|
|
407
|
-
player.stop()
|
|
408
|
-
self.successBeepPlayer = nil
|
|
409
|
-
try? session.setActive(false, options: [.notifyOthersOnDeactivation])
|
|
410
|
-
}
|
|
411
|
-
} catch {
|
|
412
|
-
successBeepPlayer = nil
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
private static let successBeepData: Data = {
|
|
417
|
-
let sampleRate: UInt32 = 22_050
|
|
418
|
-
let sampleCount = Int(sampleRate / 10)
|
|
419
|
-
let dataSize = UInt32(sampleCount * 2)
|
|
420
|
-
var data = Data()
|
|
421
|
-
|
|
422
|
-
func appendASCII(_ value: String) {
|
|
423
|
-
data.append(contentsOf: value.utf8)
|
|
424
|
-
}
|
|
425
|
-
func appendUInt16(_ value: UInt16) {
|
|
426
|
-
var littleEndian = value.littleEndian
|
|
427
|
-
Swift.withUnsafeBytes(of: &littleEndian) { data.append(contentsOf: $0) }
|
|
428
|
-
}
|
|
429
|
-
func appendUInt32(_ value: UInt32) {
|
|
430
|
-
var littleEndian = value.littleEndian
|
|
431
|
-
Swift.withUnsafeBytes(of: &littleEndian) { data.append(contentsOf: $0) }
|
|
432
|
-
}
|
|
433
|
-
func appendInt16(_ value: Int16) {
|
|
434
|
-
appendUInt16(UInt16(bitPattern: value))
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
appendASCII("RIFF")
|
|
438
|
-
appendUInt32(36 + dataSize)
|
|
439
|
-
appendASCII("WAVE")
|
|
440
|
-
appendASCII("fmt ")
|
|
441
|
-
appendUInt32(16)
|
|
442
|
-
appendUInt16(1)
|
|
443
|
-
appendUInt16(1)
|
|
444
|
-
appendUInt32(sampleRate)
|
|
445
|
-
appendUInt32(sampleRate * 2)
|
|
446
|
-
appendUInt16(2)
|
|
447
|
-
appendUInt16(16)
|
|
448
|
-
appendASCII("data")
|
|
449
|
-
appendUInt32(dataSize)
|
|
450
|
-
|
|
451
|
-
for index in 0..<sampleCount {
|
|
452
|
-
let time = Double(index) / Double(sampleRate)
|
|
453
|
-
let remaining = Double(sampleCount - index) / Double(sampleRate)
|
|
454
|
-
let envelope = min(1, time / 0.005) * min(1, remaining / 0.025)
|
|
455
|
-
let value = sin(2 * Double.pi * 1_800 * time)
|
|
456
|
-
* envelope * Double(Int16.max) * 0.55
|
|
457
|
-
appendInt16(Int16(value.rounded()))
|
|
458
|
-
}
|
|
459
|
-
return data
|
|
460
|
-
}()
|
|
461
394
|
|
|
462
395
|
private func fail(_ error: MlKitSuiteError) {
|
|
463
396
|
guard !completed else { return }
|
|
@@ -88,6 +88,7 @@ struct FaceCaptureOptions {
|
|
|
88
88
|
let jpegQuality: Int
|
|
89
89
|
let showCloseButton: Bool
|
|
90
90
|
let debugMode: Bool
|
|
91
|
+
let beepEnabled: Bool
|
|
91
92
|
let instructionText: String
|
|
92
93
|
let localization: MlKitLocalization
|
|
93
94
|
let timeoutMs: Int
|
|
@@ -97,7 +98,7 @@ struct FaceCaptureOptions {
|
|
|
97
98
|
localization = try MlKitLocalization(dictionary: dictionary)
|
|
98
99
|
let known = Set([
|
|
99
100
|
"outputWidth", "outputHeight", "jpegQuality", "showCloseButton",
|
|
100
|
-
"debugMode", "instructionText", "timeoutMs", "checks",
|
|
101
|
+
"debugMode", "beepEnabled", "instructionText", "timeoutMs", "checks",
|
|
101
102
|
"moduleLoadingMessage", "locale"
|
|
102
103
|
])
|
|
103
104
|
for key in dictionary.keys where !known.contains(key) {
|
|
@@ -133,6 +134,7 @@ struct FaceCaptureOptions {
|
|
|
133
134
|
jpegQuality = try integer("jpegQuality", fallback: 95, minimum: 50, maximum: 100)
|
|
134
135
|
showCloseButton = try boolean("showCloseButton", fallback: true)
|
|
135
136
|
debugMode = try boolean("debugMode", fallback: false)
|
|
137
|
+
beepEnabled = try boolean("beepEnabled", fallback: true)
|
|
136
138
|
timeoutMs = try integer("timeoutMs", fallback: 0, minimum: 0, maximum: 600_000)
|
|
137
139
|
if let raw = dictionary["instructionText"], !(raw is NSNull) {
|
|
138
140
|
guard let string = raw as? String else {
|
|
@@ -19,6 +19,7 @@ final class FaceCaptureViewController: UIViewController {
|
|
|
19
19
|
private let processingQueue = DispatchQueue(label: "com.andreszs.mlkitsuite.processing", qos: .userInitiated)
|
|
20
20
|
private let liveDetector: FaceDetector
|
|
21
21
|
private let stillProcessor: FaceStillProcessor
|
|
22
|
+
private let successBeep = MlKitSuccessBeep()
|
|
22
23
|
private var previewLayer: AVCaptureVideoPreviewLayer!
|
|
23
24
|
private var overlayView: FaceOverlayView!
|
|
24
25
|
private var videoOutput: AVCaptureVideoDataOutput?
|
|
@@ -351,7 +352,15 @@ final class FaceCaptureViewController: UIViewController {
|
|
|
351
352
|
guard !completed else { return }
|
|
352
353
|
completed = true
|
|
353
354
|
timeoutItem?.cancel()
|
|
354
|
-
|
|
355
|
+
if options.beepEnabled {
|
|
356
|
+
successBeep.play()
|
|
357
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
|
|
358
|
+
guard let self = self else { return }
|
|
359
|
+
self.delegate?.faceCapture(self, didFinish: result)
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
delegate?.faceCapture(self, didFinish: result)
|
|
363
|
+
}
|
|
355
364
|
}
|
|
356
365
|
|
|
357
366
|
private func fail(_ error: MlKitSuiteError) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
|
|
3
|
+
final class MlKitSuccessBeep {
|
|
4
|
+
private var player: AVAudioPlayer?
|
|
5
|
+
|
|
6
|
+
func play() {
|
|
7
|
+
do {
|
|
8
|
+
let session = AVAudioSession.sharedInstance()
|
|
9
|
+
try session.setCategory(.ambient, mode: .default, options: [.mixWithOthers])
|
|
10
|
+
try session.setActive(true)
|
|
11
|
+
let nextPlayer = try AVAudioPlayer(data: Self.data)
|
|
12
|
+
nextPlayer.volume = 1
|
|
13
|
+
nextPlayer.prepareToPlay()
|
|
14
|
+
player = nextPlayer
|
|
15
|
+
nextPlayer.play()
|
|
16
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
|
17
|
+
guard let self = self, self.player === nextPlayer else { return }
|
|
18
|
+
self.stop()
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
player = nil
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func stop() {
|
|
26
|
+
player?.stop()
|
|
27
|
+
player = nil
|
|
28
|
+
try? AVAudioSession.sharedInstance().setActive(
|
|
29
|
+
false, options: [.notifyOthersOnDeactivation]
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deinit {
|
|
34
|
+
stop()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static let data: Data = {
|
|
38
|
+
let sampleRate: UInt32 = 22_050
|
|
39
|
+
let sampleCount = Int(sampleRate / 10)
|
|
40
|
+
let dataSize = UInt32(sampleCount * 2)
|
|
41
|
+
var data = Data()
|
|
42
|
+
|
|
43
|
+
func appendASCII(_ value: String) {
|
|
44
|
+
data.append(contentsOf: value.utf8)
|
|
45
|
+
}
|
|
46
|
+
func appendUInt16(_ value: UInt16) {
|
|
47
|
+
var littleEndian = value.littleEndian
|
|
48
|
+
Swift.withUnsafeBytes(of: &littleEndian) { data.append(contentsOf: $0) }
|
|
49
|
+
}
|
|
50
|
+
func appendUInt32(_ value: UInt32) {
|
|
51
|
+
var littleEndian = value.littleEndian
|
|
52
|
+
Swift.withUnsafeBytes(of: &littleEndian) { data.append(contentsOf: $0) }
|
|
53
|
+
}
|
|
54
|
+
func appendInt16(_ value: Int16) {
|
|
55
|
+
appendUInt16(UInt16(bitPattern: value))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
appendASCII("RIFF")
|
|
59
|
+
appendUInt32(36 + dataSize)
|
|
60
|
+
appendASCII("WAVE")
|
|
61
|
+
appendASCII("fmt ")
|
|
62
|
+
appendUInt32(16)
|
|
63
|
+
appendUInt16(1)
|
|
64
|
+
appendUInt16(1)
|
|
65
|
+
appendUInt32(sampleRate)
|
|
66
|
+
appendUInt32(sampleRate * 2)
|
|
67
|
+
appendUInt16(2)
|
|
68
|
+
appendUInt16(16)
|
|
69
|
+
appendASCII("data")
|
|
70
|
+
appendUInt32(dataSize)
|
|
71
|
+
|
|
72
|
+
for index in 0..<sampleCount {
|
|
73
|
+
let time = Double(index) / Double(sampleRate)
|
|
74
|
+
let remaining = Double(sampleCount - index) / Double(sampleRate)
|
|
75
|
+
let envelope = min(1, time / 0.005) * min(1, remaining / 0.025)
|
|
76
|
+
let value = sin(2 * Double.pi * 1_800 * time)
|
|
77
|
+
* envelope * Double(Int16.max) * 0.55
|
|
78
|
+
appendInt16(Int16(value.rounded()))
|
|
79
|
+
}
|
|
80
|
+
return data
|
|
81
|
+
}()
|
|
82
|
+
}
|
|
@@ -6,6 +6,7 @@ var path = require('path');
|
|
|
6
6
|
|
|
7
7
|
var root = path.resolve(__dirname, '..');
|
|
8
8
|
var controller = fs.readFileSync(path.join(root, 'src/ios/FaceCaptureViewController.swift'), 'utf8');
|
|
9
|
+
var types = fs.readFileSync(path.join(root, 'src/ios/FaceCaptureTypes.swift'), 'utf8');
|
|
9
10
|
var overlay = fs.readFileSync(path.join(root, 'src/ios/FaceOverlayView.swift'), 'utf8');
|
|
10
11
|
|
|
11
12
|
assert(controller.includes('checks: options.checks'));
|
|
@@ -13,6 +14,9 @@ assert(controller.includes('bufferHeight >= bufferWidth ? .up : .leftMirrored'))
|
|
|
13
14
|
assert(controller.includes('previewLayer.connection, mirrored: true'));
|
|
14
15
|
assert(controller.includes('beginCapture(analysis, pixelBuffer: pixelBuffer)'));
|
|
15
16
|
assert(controller.includes('pixelBuffer: pixelBuffer'));
|
|
17
|
+
assert(types.includes('beepEnabled = try boolean("beepEnabled", fallback: true)'));
|
|
18
|
+
assert(controller.includes('if options.beepEnabled'));
|
|
19
|
+
assert(controller.includes('successBeep.play()'));
|
|
16
20
|
assert(!controller.includes('AVCapturePhotoOutput'));
|
|
17
21
|
assert(!controller.includes('AVCapturePhotoCaptureDelegate'));
|
|
18
22
|
assert(!controller.includes('fileDataRepresentation'));
|
|
@@ -10,6 +10,7 @@ var plugin = fs.readFileSync(path.join(root, 'src/ios/MlKitSuitePlugin.swift'),
|
|
|
10
10
|
var controller = fs.readFileSync(path.join(root, 'src/ios/BarcodeScanViewController.swift'), 'utf8');
|
|
11
11
|
var faceController = fs.readFileSync(path.join(root, 'src/ios/FaceCaptureViewController.swift'), 'utf8');
|
|
12
12
|
var types = fs.readFileSync(path.join(root, 'src/ios/BarcodeScanTypes.swift'), 'utf8');
|
|
13
|
+
var successBeep = fs.readFileSync(path.join(root, 'src/ios/MlKitSuccessBeep.swift'), 'utf8');
|
|
13
14
|
var spanish = fs.readFileSync(path.join(
|
|
14
15
|
root, 'src/ios/MlKitSuiteResources.bundle/es.lproj/Localizable.strings'
|
|
15
16
|
), 'utf8');
|
|
@@ -18,6 +19,7 @@ var spanish = fs.readFileSync(path.join(
|
|
|
18
19
|
'src/ios/BarcodeScanTypes.swift',
|
|
19
20
|
'src/ios/BarcodeOverlayView.swift',
|
|
20
21
|
'src/ios/BarcodeScanViewController.swift',
|
|
22
|
+
'src/ios/MlKitSuccessBeep.swift',
|
|
21
23
|
'src/ios/MlKitLocalization.swift'
|
|
22
24
|
].forEach(function (source) {
|
|
23
25
|
assert(pluginXml.includes('<source-file src="' + source + '" />'), source + ' must be packaged.');
|
|
@@ -47,7 +49,9 @@ assert(controller.includes('options.localization.text("barcode_center")'));
|
|
|
47
49
|
assert(controller.includes('overlayView.contains('));
|
|
48
50
|
assert(types.includes('guideColor must be a color in #RRGGBB format.'));
|
|
49
51
|
assert(types.includes('beepEnabled = try boolean("beepEnabled", fallback: true)'));
|
|
50
|
-
assert(controller.includes('
|
|
52
|
+
assert(controller.includes('private let successBeep = MlKitSuccessBeep()'));
|
|
53
|
+
assert(controller.includes('if options.beepEnabled { successBeep.play() }'));
|
|
54
|
+
assert(successBeep.includes('AVAudioPlayer(data: Self.data)'));
|
|
51
55
|
assert(controller.includes('setTorchModeOn'));
|
|
52
56
|
assert(controller.includes('UIImage(systemName: systemName'));
|
|
53
57
|
assert(controller.includes('accessibilityLabel'));
|
package/www/MlKit.js
CHANGED
|
@@ -270,7 +270,7 @@ exports.face = {
|
|
|
270
270
|
* Opens the native front-camera guide and automatically captures a JPEG.
|
|
271
271
|
*
|
|
272
272
|
* @param {{outputWidth?: number, outputHeight?: number, jpegQuality?: number,
|
|
273
|
-
* showCloseButton?: boolean, debugMode?: boolean,
|
|
273
|
+
* showCloseButton?: boolean, debugMode?: boolean, beepEnabled?: boolean,
|
|
274
274
|
* instructionText?: string|null, timeoutMs?: number,
|
|
275
275
|
* moduleLoadingMessage?: string|null, locale?: string,
|
|
276
276
|
* checks?: {singleFace?: boolean, faceSize?: boolean,
|