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 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.0",
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.0">
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 AudioTrack successBeep;
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
- releaseSuccessBeep();
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 (!config.beepEnabled) return;
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
- setResult(RESULT_OK, intent);
295
- finish();
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) || "instructionText".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 var successBeepPlayer: AVAudioPlayer?
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
- playSuccessBeep()
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
- delegate?.faceCapture(self, didFinish: result)
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('AVAudioPlayer(data: Self.successBeepData)'));
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,