@iternio/react-native-auto-play 0.5.3 → 0.5.4

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.
@@ -64,6 +64,8 @@ class HybridVoice : HybridVoiceSpec() {
64
64
  silenceThresholdMs: Double?,
65
65
  maxDurationMs: Double?,
66
66
  listeningText: String?,
67
+ listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?,
68
+ listeningImageRepeats: Boolean?,
67
69
  preferSpeechToText: Boolean?,
68
70
  onChunk: ((chunk: VoiceInputChunk) -> Unit)?,
69
71
  language: String?
@@ -22,7 +22,6 @@ class HybridAutoPlay: HybridAutoPlaySpec {
22
22
  private static var renderStateListeners = [String: [RenderStateListener]]()
23
23
  private static var safeAreaInsetsListeners = [String: [SafeAreaListener]]()
24
24
 
25
-
26
25
  override init() {
27
26
  HybridAutoPlay.listeners.removeAll()
28
27
  HybridAutoPlay.renderStateListeners.removeAll()
@@ -32,6 +32,8 @@ class HybridVoice: HybridVoiceSpec {
32
32
  silenceThresholdMs: Double?,
33
33
  maxDurationMs: Double?,
34
34
  listeningText: String?,
35
+ listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?,
36
+ listeningImageRepeats: Bool?,
35
37
  preferSpeechToText: Bool?,
36
38
  onChunk: ((_ chunk: VoiceInputChunk) -> Void)?,
37
39
  language: String?
@@ -49,6 +51,8 @@ class HybridVoice: HybridVoiceSpec {
49
51
  silenceThresholdMs: silenceThresholdMs ?? 1_500,
50
52
  maxDurationMs: maxDurationMs ?? 10_000,
51
53
  listeningText: listeningText ?? "Listening...",
54
+ listeningImage: listeningImage,
55
+ listeningImageRepeats: listeningImageRepeats,
52
56
  preferSpeechToText: preferSpeechToText ?? false,
53
57
  onChunk: onChunk,
54
58
  language: language
@@ -6,6 +6,7 @@
6
6
  //
7
7
 
8
8
  import CarPlay
9
+ import ImageIO
9
10
  import UIKit
10
11
 
11
12
  struct HeaderActions {
@@ -939,6 +940,100 @@ class Parser {
939
940
  )
940
941
  }
941
942
 
943
+ // MARK: - Animated image decoding
944
+
945
+ /// Decodes raw image data via ImageIO, walking every frame so animated GIF/APNG/WebP all
946
+ /// animate. UIImage(data:) only ever decodes the first frame for any of these formats.
947
+ /// `maxDuration` caps the assembled cycle length; pass `.greatestFiniteMagnitude` to skip capping.
948
+ static func decodeImage(data: Data, scale: CGFloat, maxDuration: TimeInterval = .greatestFiniteMagnitude)
949
+ -> UIImage?
950
+ {
951
+ guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
952
+ let frameCount = CGImageSourceGetCount(source)
953
+ guard frameCount > 0 else { return nil }
954
+
955
+ guard frameCount > 1 else {
956
+ guard let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else { return nil }
957
+ return UIImage(cgImage: cgImage, scale: scale, orientation: .up)
958
+ }
959
+
960
+ var frames: [UIImage] = []
961
+ var totalDuration: TimeInterval = 0
962
+ for index in 0..<frameCount {
963
+ guard let cgImage = CGImageSourceCreateImageAtIndex(source, index, nil) else { continue }
964
+ totalDuration += frameDuration(source: source, index: index)
965
+ frames.append(UIImage(cgImage: cgImage, scale: scale, orientation: .up))
966
+ }
967
+ guard !frames.isEmpty else { return nil }
968
+ return UIImage.animatedImage(with: frames, duration: min(totalDuration, maxDuration))
969
+ }
970
+
971
+ /// Reads the per-frame delay from whichever format dictionary ImageIO populated
972
+ /// (GIF, APNG, or WebP), falling back to a sane default if none is present.
973
+ private static func frameDuration(source: CGImageSource, index: Int) -> TimeInterval {
974
+ let defaultDuration: TimeInterval = 0.1
975
+ guard
976
+ let properties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any]
977
+ else { return defaultDuration }
978
+
979
+ if let gif = properties[kCGImagePropertyGIFDictionary] as? [CFString: Any] {
980
+ if let unclamped = gif[kCGImagePropertyGIFUnclampedDelayTime] as? Double, unclamped > 0 {
981
+ return unclamped
982
+ }
983
+ if let delay = gif[kCGImagePropertyGIFDelayTime] as? Double, delay > 0 {
984
+ return delay
985
+ }
986
+ }
987
+
988
+ if let png = properties[kCGImagePropertyPNGDictionary] as? [CFString: Any] {
989
+ if let unclamped = png[kCGImagePropertyAPNGUnclampedDelayTime] as? Double, unclamped > 0 {
990
+ return unclamped
991
+ }
992
+ if let delay = png[kCGImagePropertyAPNGDelayTime] as? Double, delay > 0 {
993
+ return delay
994
+ }
995
+ }
996
+
997
+ if let webp = properties[kCGImagePropertyWebPDictionary] as? [CFString: Any],
998
+ let delay = webp[kCGImagePropertyWebPDelayTime] as? Double, delay > 0
999
+ {
1000
+ return delay
1001
+ }
1002
+
1003
+ return defaultDuration
1004
+ }
1005
+
1006
+ private static func targetSize(for size: CGSize, max maxSize: CGSize) -> CGSize {
1007
+ guard size.width > maxSize.width || size.height > maxSize.height else { return size }
1008
+ let scale = min(maxSize.width / size.width, maxSize.height / size.height)
1009
+ return CGSize(width: (size.width * scale).rounded(), height: (size.height * scale).rounded())
1010
+ }
1011
+
1012
+ static func resize(_ image: UIImage, max maxSize: CGSize) -> UIImage {
1013
+ let target = targetSize(for: image.size, max: maxSize)
1014
+ guard target != image.size else { return image }
1015
+ return UIGraphicsImageRenderer(size: target).image { _ in
1016
+ image.draw(in: CGRect(origin: .zero, size: target))
1017
+ }
1018
+ }
1019
+
1020
+ /// Resizes every frame of an animated UIImage while preserving the per-frame timing.
1021
+ /// UIImage.draw(in:) only renders the current frame, so resize() alone would collapse
1022
+ /// the animation to a still image.
1023
+ static func resizeAnimated(_ image: UIImage, max maxSize: CGSize) -> UIImage {
1024
+ guard let frames = image.images, !frames.isEmpty else {
1025
+ return resize(image, max: maxSize)
1026
+ }
1027
+ let target = targetSize(for: image.size, max: maxSize)
1028
+ guard target != image.size else { return image }
1029
+ let resizedFrames = frames.map { frame in
1030
+ UIGraphicsImageRenderer(size: target).image { _ in
1031
+ frame.draw(in: CGRect(origin: .zero, size: target))
1032
+ }
1033
+ }
1034
+ return UIImage.animatedImage(with: resizedFrames, duration: image.duration) ?? image
1035
+ }
1036
+
942
1037
  static func imageFromLanes(
943
1038
  laneImages: Array<NitroImage>.SubSequence,
944
1039
  traitCollection: UITraitCollection
@@ -65,6 +65,8 @@ class VoiceInputManager {
65
65
  silenceThresholdMs: Double,
66
66
  maxDurationMs: Double,
67
67
  listeningText: String,
68
+ listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?,
69
+ listeningImageRepeats: Bool?,
68
70
  preferSpeechToText: Bool,
69
71
  onChunk: ((_ chunk: VoiceInputChunk) -> Void)?,
70
72
  language: String?
@@ -82,6 +84,8 @@ class VoiceInputManager {
82
84
  silenceThresholdMs: silenceThresholdMs,
83
85
  maxDurationMs: maxDurationMs,
84
86
  listeningText: listeningText,
87
+ listeningImage: listeningImage,
88
+ listeningImageRepeats: listeningImageRepeats,
85
89
  preferSpeechToText: preferSpeechToText,
86
90
  onChunk: onChunk,
87
91
  box: box,
@@ -128,6 +132,8 @@ class VoiceInputManager {
128
132
  silenceThresholdMs: Double,
129
133
  maxDurationMs: Double,
130
134
  listeningText: String,
135
+ listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?,
136
+ listeningImageRepeats: Bool?,
131
137
  preferSpeechToText: Bool,
132
138
  onChunk: ((_ chunk: VoiceInputChunk) -> Void)?,
133
139
  box: ResultBox,
@@ -142,13 +148,20 @@ class VoiceInputManager {
142
148
  try session.setActive(true)
143
149
 
144
150
  if let interfaceController {
145
- presentVoiceTemplate(interfaceController: interfaceController, listeningText: listeningText)
151
+ presentVoiceTemplate(
152
+ interfaceController: interfaceController,
153
+ listeningText: listeningText,
154
+ listeningImage: listeningImage,
155
+ listeningImageRepeats: listeningImageRepeats
156
+ )
146
157
  }
147
158
 
148
159
  var activeRecognitionRequest: SFSpeechAudioBufferRecognitionRequest? = nil
149
160
 
150
161
  if preferSpeechToText, SFSpeechRecognizer.authorizationStatus() == .authorized,
151
- let recognizer = language != nil ? SFSpeechRecognizer(locale: Locale(identifier: language!)) : SFSpeechRecognizer(locale: Locale.current),
162
+ let recognizer = language != nil
163
+ ? SFSpeechRecognizer(locale: Locale(identifier: language!))
164
+ : SFSpeechRecognizer(locale: Locale.current),
152
165
  recognizer.isAvailable
153
166
  {
154
167
  let request = SFSpeechAudioBufferRecognitionRequest()
@@ -311,12 +324,81 @@ class VoiceInputManager {
311
324
  return VoiceInputResult(transcription: nil, audio: buffer)
312
325
  }
313
326
 
314
- private func presentVoiceTemplate(interfaceController: AutoPlayInterfaceController, listeningText: String) {
327
+ // CPVoiceControlState enforces a maximum image size of 150x150 points.
328
+ private static let voiceImageMaxSize = CGSize(width: 150, height: 150)
329
+
330
+ // CPVoiceControlState also enforces a 0.3s–5s animation cycle; the 0.3s floor is applied
331
+ // by the system regardless of what we pass, so we only need to clamp our own ceiling.
332
+ private static let maxVoiceImageCycleDuration: TimeInterval = 5.0
333
+
334
+ // Bypasses RCTConvert for asset images: it collapses animated UIImages to a single frame
335
+ // via CGImage during scale adjustment. Parser.decodeImage preserves animation frames by
336
+ // walking every frame in the source via ImageIO — UIImage(data:) never builds a multi-frame
337
+ // .images array itself, for GIF, APNG, or WebP. Tinting is skipped for animated images since
338
+ // frames cannot be tinted individually.
339
+ private func loadVoiceImage(image: Variant_GlyphImage_AssetImage_RemoteImage?, traitCollection: UITraitCollection)
340
+ -> UIImage?
341
+ {
342
+ guard let image else { return nil }
343
+
344
+ if let assetImage = image.assetImage {
345
+ guard let url = URL(string: assetImage.uri),
346
+ let data = try? Data(contentsOf: url),
347
+ let uiImage = Parser.decodeImage(
348
+ data: data,
349
+ scale: CGFloat(assetImage.scale),
350
+ maxDuration: VoiceInputManager.maxVoiceImageCycleDuration
351
+ )
352
+ else { return nil }
353
+
354
+ if uiImage.images != nil {
355
+ return Parser.resizeAnimated(uiImage, max: VoiceInputManager.voiceImageMaxSize)
356
+ }
357
+
358
+ if assetImage.color == nil {
359
+ return Parser.resize(uiImage, max: VoiceInputManager.voiceImageMaxSize)
360
+ }
361
+
362
+ guard let tinted = Parser.parseAssetImage(assetImage: assetImage, traitCollection: traitCollection) else {
363
+ return nil
364
+ }
365
+ return Parser.resize(tinted, max: VoiceInputManager.voiceImageMaxSize)
366
+ }
367
+
368
+ if let glyphImage = image.glyphImage {
369
+ return
370
+ SymbolFont
371
+ .imageFromGlyph(
372
+ glyphImage: glyphImage,
373
+ size: 150, // according to docs on CPVoiceControlState.image
374
+ foregroundColor: glyphImage.color,
375
+ backgroundColor: glyphImage.backgroundColor,
376
+ fontScale: glyphImage.fontScale ?? 1.0,
377
+ traitCollection: traitCollection
378
+ )
379
+ }
380
+
381
+ return nil
382
+ }
383
+
384
+ private func presentVoiceTemplate(
385
+ interfaceController: AutoPlayInterfaceController,
386
+ listeningText: String,
387
+ listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?,
388
+ listeningImageRepeats: Bool?
389
+ ) {
390
+ let traitCollection = SceneStore.getRootTraitCollection() ?? UITraitCollection.current
391
+ let image = loadVoiceImage(
392
+ image: listeningImage,
393
+ traitCollection: traitCollection
394
+ )
395
+
396
+ let repeats = listeningImageRepeats ?? (image?.images != nil)
315
397
  let listeningState = CPVoiceControlState(
316
398
  identifier: "listening",
317
399
  titleVariants: [listeningText],
318
- image: nil,
319
- repeats: true
400
+ image: image,
401
+ repeats: repeats
320
402
  )
321
403
  let template = CPVoiceControlTemplate(voiceControlStates: [listeningState])
322
404
  initTemplate(template: template, id: "voice-input")
@@ -1,8 +1,10 @@
1
1
  import { NitroModules } from 'react-native-nitro-modules';
2
+ import { NitroImageUtil } from '../utils/NitroImage';
2
3
  const _native = NitroModules.createHybridObject('Voice');
3
4
  const startVoiceInput = async (options) => {
4
- const { onChunk, silenceThresholdMs, maxDurationMs, listeningText, preferSpeechToText, language, } = options ?? {};
5
- return await _native.startVoiceInput(silenceThresholdMs, maxDurationMs, listeningText, preferSpeechToText, onChunk, language);
5
+ const { onChunk, silenceThresholdMs, maxDurationMs, listeningText, listeningImage, preferSpeechToText, language, } = options ?? {};
6
+ const listeningImageRepeats = listeningImage?.type === 'asset' ? listeningImage.repeats : undefined;
7
+ return await _native.startVoiceInput(silenceThresholdMs, maxDurationMs, listeningText, NitroImageUtil.convert(listeningImage), listeningImageRepeats, preferSpeechToText, onChunk, language);
6
8
  };
7
9
  export const HybridVoice = {
8
10
  /**
@@ -1,11 +1,12 @@
1
1
  import type { HybridObject } from 'react-native-nitro-modules';
2
2
  import type { VoiceInputChunk, VoiceInputResult } from '../types/Voice';
3
+ import type { NitroImage } from '../utils/NitroImage';
3
4
  export interface Voice extends HybridObject<{
4
5
  android: 'kotlin';
5
6
  ios: 'swift';
6
7
  }> {
7
8
  hasVoiceInputPermission(): boolean;
8
9
  requestVoiceInputPermission(): Promise<boolean>;
9
- startVoiceInput(silenceThresholdMs?: number, maxDurationMs?: number, listeningText?: string, preferSpeechToText?: boolean, onChunk?: (chunk: VoiceInputChunk) => void, language?: string): Promise<VoiceInputResult>;
10
+ startVoiceInput(silenceThresholdMs?: number, maxDurationMs?: number, listeningText?: string, listeningImage?: NitroImage, listeningImageRepeats?: boolean, preferSpeechToText?: boolean, onChunk?: (chunk: VoiceInputChunk) => void, language?: string): Promise<VoiceInputResult>;
10
11
  stopVoiceInput(): void;
11
12
  }
@@ -4,7 +4,7 @@ import type { NitroMapButton } from '../utils/NitroMapButton';
4
4
  export type ActionButton<T> = ActionButtonAndroid<T> | ActionButtonIos<T>;
5
5
  export type HeaderActionsIos<T> = {
6
6
  /**
7
- * @platform iOS - the back button can not be hidden or disabled, if you don't define it iOS will provide a default back button popping the template
7
+ * @namespace iOS - the back button can not be hidden or disabled, if you don't define it iOS will provide a default back button popping the template
8
8
  */
9
9
  backButton?: BackButton<T>;
10
10
  leadingNavigationBarButtons?: [ActionButtonIos<T>, ActionButtonIos<T>] | [ActionButtonIos<T>];
@@ -66,4 +66,20 @@ export type AutoImage = AutoGlyph | {
66
66
  timeoutMs?: number;
67
67
  type: 'remote';
68
68
  };
69
+ /**
70
+ * Image for the CarPlay voice control overlay. Animated GIF, APNG, and WebP all animate.
71
+ * `color` is ignored for animated assets — frames cannot be tinted individually. Static
72
+ * assets and glyphs are resized/capped to fit within 150×150pt (`fontScale` controls a
73
+ * glyph's relative size). CarPlay enforces a 0.3s–5s animation cycle duration: shorter
74
+ * source animations are stretched to 0.3s by the system, longer ones are capped to 5s.
75
+ * @namespace ios
76
+ */
77
+ export type VoiceInputImage = AutoGlyph | {
78
+ type: 'asset';
79
+ image: ImageSourcePropType;
80
+ /** Tints the image. Ignored for animated assets — frames cannot be tinted individually. */
81
+ color?: ThemedColor | string;
82
+ /** Whether the animation loops. Defaults to `true` for animated images, `false` for static. */
83
+ repeats?: boolean;
84
+ };
69
85
  export {};
@@ -1,3 +1,4 @@
1
+ import type { VoiceInputImage } from './Image';
1
2
  export interface VoiceInputChunk {
2
3
  partial?: string;
3
4
  audio?: ArrayBuffer;
@@ -10,6 +11,10 @@ export interface VoiceInputOptions {
10
11
  silenceThresholdMs?: number;
11
12
  maxDurationMs?: number;
12
13
  listeningText?: string;
14
+ /** Image displayed in the CarPlay voice control overlay. See {@link VoiceInputImage}.
15
+ * @namespace ios
16
+ */
17
+ listeningImage?: VoiceInputImage;
13
18
  preferSpeechToText?: boolean;
14
19
  onChunk?: (chunk: VoiceInputChunk) => void;
15
20
  language?: string;
@@ -9,6 +9,14 @@
9
9
 
10
10
  // Forward declaration of `VoiceInputResult` to properly resolve imports.
11
11
  namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputResult; }
12
+ // Forward declaration of `GlyphImage` to properly resolve imports.
13
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct GlyphImage; }
14
+ // Forward declaration of `AssetImage` to properly resolve imports.
15
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct AssetImage; }
16
+ // Forward declaration of `RemoteImage` to properly resolve imports.
17
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct RemoteImage; }
18
+ // Forward declaration of `NitroColor` to properly resolve imports.
19
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct NitroColor; }
12
20
  // Forward declaration of `VoiceInputChunk` to properly resolve imports.
13
21
  namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputChunk; }
14
22
 
@@ -20,6 +28,16 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputC
20
28
  #include <optional>
21
29
  #include <NitroModules/ArrayBuffer.hpp>
22
30
  #include <NitroModules/JArrayBuffer.hpp>
31
+ #include "GlyphImage.hpp"
32
+ #include "AssetImage.hpp"
33
+ #include "RemoteImage.hpp"
34
+ #include <variant>
35
+ #include "JVariant_GlyphImage_AssetImage_RemoteImage.hpp"
36
+ #include "JGlyphImage.hpp"
37
+ #include "NitroColor.hpp"
38
+ #include "JNitroColor.hpp"
39
+ #include "JAssetImage.hpp"
40
+ #include "JRemoteImage.hpp"
23
41
  #include "VoiceInputChunk.hpp"
24
42
  #include <functional>
25
43
  #include "JFunc_void_VoiceInputChunk.hpp"
@@ -80,9 +98,9 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
80
98
  return __promise;
81
99
  }();
82
100
  }
83
- std::shared_ptr<Promise<VoiceInputResult>> JHybridVoiceSpec::startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) {
84
- static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JDouble> /* silenceThresholdMs */, jni::alias_ref<jni::JDouble> /* maxDurationMs */, jni::alias_ref<jni::JString> /* listeningText */, jni::alias_ref<jni::JBoolean> /* preferSpeechToText */, jni::alias_ref<JFunc_void_VoiceInputChunk::javaobject> /* onChunk */, jni::alias_ref<jni::JString> /* language */)>("startVoiceInput_cxx");
85
- auto __result = method(_javaPart, silenceThresholdMs.has_value() ? jni::JDouble::valueOf(silenceThresholdMs.value()) : nullptr, maxDurationMs.has_value() ? jni::JDouble::valueOf(maxDurationMs.value()) : nullptr, listeningText.has_value() ? jni::make_jstring(listeningText.value()) : nullptr, preferSpeechToText.has_value() ? jni::JBoolean::valueOf(preferSpeechToText.value()) : nullptr, onChunk.has_value() ? JFunc_void_VoiceInputChunk_cxx::fromCpp(onChunk.value()) : nullptr, language.has_value() ? jni::make_jstring(language.value()) : nullptr);
101
+ std::shared_ptr<Promise<VoiceInputResult>> JHybridVoiceSpec::startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, const std::optional<std::variant<GlyphImage, AssetImage, RemoteImage>>& listeningImage, std::optional<bool> listeningImageRepeats, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) {
102
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPromise::javaobject>(jni::alias_ref<jni::JDouble> /* silenceThresholdMs */, jni::alias_ref<jni::JDouble> /* maxDurationMs */, jni::alias_ref<jni::JString> /* listeningText */, jni::alias_ref<JVariant_GlyphImage_AssetImage_RemoteImage> /* listeningImage */, jni::alias_ref<jni::JBoolean> /* listeningImageRepeats */, jni::alias_ref<jni::JBoolean> /* preferSpeechToText */, jni::alias_ref<JFunc_void_VoiceInputChunk::javaobject> /* onChunk */, jni::alias_ref<jni::JString> /* language */)>("startVoiceInput_cxx");
103
+ auto __result = method(_javaPart, silenceThresholdMs.has_value() ? jni::JDouble::valueOf(silenceThresholdMs.value()) : nullptr, maxDurationMs.has_value() ? jni::JDouble::valueOf(maxDurationMs.value()) : nullptr, listeningText.has_value() ? jni::make_jstring(listeningText.value()) : nullptr, listeningImage.has_value() ? JVariant_GlyphImage_AssetImage_RemoteImage::fromCpp(listeningImage.value()) : nullptr, listeningImageRepeats.has_value() ? jni::JBoolean::valueOf(listeningImageRepeats.value()) : nullptr, preferSpeechToText.has_value() ? jni::JBoolean::valueOf(preferSpeechToText.value()) : nullptr, onChunk.has_value() ? JFunc_void_VoiceInputChunk_cxx::fromCpp(onChunk.value()) : nullptr, language.has_value() ? jni::make_jstring(language.value()) : nullptr);
86
104
  return [&]() {
87
105
  auto __promise = Promise<VoiceInputResult>::create();
88
106
  __result->cthis()->addOnResolvedListener([=](const jni::alias_ref<jni::JObject>& __boxedResult) {
@@ -56,7 +56,7 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
56
56
  // Methods
57
57
  bool hasVoiceInputPermission() override;
58
58
  std::shared_ptr<Promise<bool>> requestVoiceInputPermission() override;
59
- std::shared_ptr<Promise<VoiceInputResult>> startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) override;
59
+ std::shared_ptr<Promise<VoiceInputResult>> startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, const std::optional<std::variant<GlyphImage, AssetImage, RemoteImage>>& listeningImage, std::optional<bool> listeningImageRepeats, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) override;
60
60
  void stopVoiceInput() override;
61
61
 
62
62
  private:
@@ -37,12 +37,12 @@ abstract class HybridVoiceSpec: HybridObject() {
37
37
  @Keep
38
38
  abstract fun requestVoiceInputPermission(): Promise<Boolean>
39
39
 
40
- abstract fun startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?, preferSpeechToText: Boolean?, onChunk: ((chunk: VoiceInputChunk) -> Unit)?, language: String?): Promise<VoiceInputResult>
40
+ abstract fun startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?, listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?, listeningImageRepeats: Boolean?, preferSpeechToText: Boolean?, onChunk: ((chunk: VoiceInputChunk) -> Unit)?, language: String?): Promise<VoiceInputResult>
41
41
 
42
42
  @DoNotStrip
43
43
  @Keep
44
- private fun startVoiceInput_cxx(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?, preferSpeechToText: Boolean?, onChunk: Func_void_VoiceInputChunk?, language: String?): Promise<VoiceInputResult> {
45
- val __result = startVoiceInput(silenceThresholdMs, maxDurationMs, listeningText, preferSpeechToText, onChunk?.let { it }, language)
44
+ private fun startVoiceInput_cxx(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?, listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?, listeningImageRepeats: Boolean?, preferSpeechToText: Boolean?, onChunk: Func_void_VoiceInputChunk?, language: String?): Promise<VoiceInputResult> {
45
+ val __result = startVoiceInput(silenceThresholdMs, maxDurationMs, listeningText, listeningImage, listeningImageRepeats, preferSpeechToText, onChunk?.let { it }, language)
46
46
  return __result
47
47
  }
48
48
 
@@ -16,6 +16,14 @@ namespace ReactNativeAutoPlay { class HybridVoiceSpec_cxx; }
16
16
  namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputResult; }
17
17
  // Forward declaration of `ArrayBufferHolder` to properly resolve imports.
18
18
  namespace NitroModules { class ArrayBufferHolder; }
19
+ // Forward declaration of `GlyphImage` to properly resolve imports.
20
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct GlyphImage; }
21
+ // Forward declaration of `AssetImage` to properly resolve imports.
22
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct AssetImage; }
23
+ // Forward declaration of `RemoteImage` to properly resolve imports.
24
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct RemoteImage; }
25
+ // Forward declaration of `NitroColor` to properly resolve imports.
26
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct NitroColor; }
19
27
  // Forward declaration of `VoiceInputChunk` to properly resolve imports.
20
28
  namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputChunk; }
21
29
 
@@ -25,6 +33,11 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputC
25
33
  #include <optional>
26
34
  #include <NitroModules/ArrayBuffer.hpp>
27
35
  #include <NitroModules/ArrayBufferHolder.hpp>
36
+ #include "GlyphImage.hpp"
37
+ #include "AssetImage.hpp"
38
+ #include "RemoteImage.hpp"
39
+ #include <variant>
40
+ #include "NitroColor.hpp"
28
41
  #include "VoiceInputChunk.hpp"
29
42
  #include <functional>
30
43
 
@@ -94,8 +107,8 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
94
107
  auto __value = std::move(__result.value());
95
108
  return __value;
96
109
  }
97
- inline std::shared_ptr<Promise<VoiceInputResult>> startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) override {
98
- auto __result = _swiftPart.startVoiceInput(silenceThresholdMs, maxDurationMs, listeningText, preferSpeechToText, onChunk, language);
110
+ inline std::shared_ptr<Promise<VoiceInputResult>> startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, const std::optional<std::variant<GlyphImage, AssetImage, RemoteImage>>& listeningImage, std::optional<bool> listeningImageRepeats, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) override {
111
+ auto __result = _swiftPart.startVoiceInput(silenceThresholdMs, maxDurationMs, listeningText, listeningImage, listeningImageRepeats, preferSpeechToText, onChunk, language);
99
112
  if (__result.hasError()) [[unlikely]] {
100
113
  std::rethrow_exception(__result.error());
101
114
  }
@@ -15,7 +15,7 @@ public protocol HybridVoiceSpec_protocol: HybridObject {
15
15
  // Methods
16
16
  func hasVoiceInputPermission() throws -> Bool
17
17
  func requestVoiceInputPermission() throws -> Promise<Bool>
18
- func startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?, preferSpeechToText: Bool?, onChunk: ((_ chunk: VoiceInputChunk) -> Void)?, language: String?) throws -> Promise<VoiceInputResult>
18
+ func startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?, listeningImage: Variant_GlyphImage_AssetImage_RemoteImage?, listeningImageRepeats: Bool?, preferSpeechToText: Bool?, onChunk: ((_ chunk: VoiceInputChunk) -> Void)?, language: String?) throws -> Promise<VoiceInputResult>
19
19
  func stopVoiceInput() throws -> Void
20
20
  }
21
21
 
@@ -156,7 +156,7 @@ open class HybridVoiceSpec_cxx {
156
156
  }
157
157
 
158
158
  @inline(__always)
159
- public final func startVoiceInput(silenceThresholdMs: bridge.std__optional_double_, maxDurationMs: bridge.std__optional_double_, listeningText: bridge.std__optional_std__string_, preferSpeechToText: bridge.std__optional_bool_, onChunk: bridge.std__optional_std__function_void_const_VoiceInputChunk_____chunk______, language: bridge.std__optional_std__string_) -> bridge.Result_std__shared_ptr_Promise_VoiceInputResult___ {
159
+ public final func startVoiceInput(silenceThresholdMs: bridge.std__optional_double_, maxDurationMs: bridge.std__optional_double_, listeningText: bridge.std__optional_std__string_, listeningImage: bridge.std__optional_std__variant_GlyphImage__AssetImage__RemoteImage__, listeningImageRepeats: bridge.std__optional_bool_, preferSpeechToText: bridge.std__optional_bool_, onChunk: bridge.std__optional_std__function_void_const_VoiceInputChunk_____chunk______, language: bridge.std__optional_std__string_) -> bridge.Result_std__shared_ptr_Promise_VoiceInputResult___ {
160
160
  do {
161
161
  let __result = try self.__implementation.startVoiceInput(silenceThresholdMs: { () -> Double? in
162
162
  if bridge.has_value_std__optional_double_(silenceThresholdMs) {
@@ -179,6 +179,35 @@ open class HybridVoiceSpec_cxx {
179
179
  } else {
180
180
  return nil
181
181
  }
182
+ }(), listeningImage: { () -> Variant_GlyphImage_AssetImage_RemoteImage? in
183
+ if bridge.has_value_std__optional_std__variant_GlyphImage__AssetImage__RemoteImage__(listeningImage) {
184
+ let __unwrapped = bridge.get_std__optional_std__variant_GlyphImage__AssetImage__RemoteImage__(listeningImage)
185
+ return { () -> Variant_GlyphImage_AssetImage_RemoteImage in
186
+ let __variant = bridge.std__variant_GlyphImage__AssetImage__RemoteImage_(__unwrapped)
187
+ switch __variant.index() {
188
+ case 0:
189
+ let __actual = __variant.get_0()
190
+ return .first(__actual)
191
+ case 1:
192
+ let __actual = __variant.get_1()
193
+ return .second(__actual)
194
+ case 2:
195
+ let __actual = __variant.get_2()
196
+ return .third(__actual)
197
+ default:
198
+ fatalError("Variant can never have index \(__variant.index())!")
199
+ }
200
+ }()
201
+ } else {
202
+ return nil
203
+ }
204
+ }(), listeningImageRepeats: { () -> Bool? in
205
+ if bridge.has_value_std__optional_bool_(listeningImageRepeats) {
206
+ let __unwrapped = bridge.get_std__optional_bool_(listeningImageRepeats)
207
+ return __unwrapped
208
+ } else {
209
+ return nil
210
+ }
182
211
  }(), preferSpeechToText: { () -> Bool? in
183
212
  if bridge.has_value_std__optional_bool_(preferSpeechToText) {
184
213
  let __unwrapped = bridge.get_std__optional_bool_(preferSpeechToText)
@@ -15,6 +15,12 @@
15
15
 
16
16
  // Forward declaration of `VoiceInputResult` to properly resolve imports.
17
17
  namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputResult; }
18
+ // Forward declaration of `GlyphImage` to properly resolve imports.
19
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct GlyphImage; }
20
+ // Forward declaration of `AssetImage` to properly resolve imports.
21
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct AssetImage; }
22
+ // Forward declaration of `RemoteImage` to properly resolve imports.
23
+ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct RemoteImage; }
18
24
  // Forward declaration of `VoiceInputChunk` to properly resolve imports.
19
25
  namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputChunk; }
20
26
 
@@ -22,6 +28,10 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay { struct VoiceInputC
22
28
  #include "VoiceInputResult.hpp"
23
29
  #include <optional>
24
30
  #include <string>
31
+ #include "GlyphImage.hpp"
32
+ #include "AssetImage.hpp"
33
+ #include "RemoteImage.hpp"
34
+ #include <variant>
25
35
  #include "VoiceInputChunk.hpp"
26
36
  #include <functional>
27
37
 
@@ -58,7 +68,7 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
58
68
  // Methods
59
69
  virtual bool hasVoiceInputPermission() = 0;
60
70
  virtual std::shared_ptr<Promise<bool>> requestVoiceInputPermission() = 0;
61
- virtual std::shared_ptr<Promise<VoiceInputResult>> startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) = 0;
71
+ virtual std::shared_ptr<Promise<VoiceInputResult>> startVoiceInput(std::optional<double> silenceThresholdMs, std::optional<double> maxDurationMs, const std::optional<std::string>& listeningText, const std::optional<std::variant<GlyphImage, AssetImage, RemoteImage>>& listeningImage, std::optional<bool> listeningImageRepeats, std::optional<bool> preferSpeechToText, const std::optional<std::function<void(const VoiceInputChunk& /* chunk */)>>& onChunk, const std::optional<std::string>& language) = 0;
62
72
  virtual void stopVoiceInput() = 0;
63
73
 
64
74
  protected:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iternio/react-native-auto-play",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -1,6 +1,7 @@
1
1
  import { NitroModules } from 'react-native-nitro-modules';
2
2
  import type { Voice } from '../specs/Voice.nitro';
3
3
  import type { VoiceInputOptions, VoiceInputResult } from '../types/Voice';
4
+ import { NitroImageUtil } from '../utils/NitroImage';
4
5
 
5
6
  const _native = NitroModules.createHybridObject<Voice>('Voice');
6
7
 
@@ -17,14 +18,20 @@ const startVoiceInput: StartVoiceInput = async (options?: VoiceInputOptions) =>
17
18
  silenceThresholdMs,
18
19
  maxDurationMs,
19
20
  listeningText,
21
+ listeningImage,
20
22
  preferSpeechToText,
21
23
  language,
22
24
  } = options ?? {};
23
25
 
26
+ const listeningImageRepeats =
27
+ listeningImage?.type === 'asset' ? listeningImage.repeats : undefined;
28
+
24
29
  return await _native.startVoiceInput(
25
30
  silenceThresholdMs,
26
31
  maxDurationMs,
27
32
  listeningText,
33
+ NitroImageUtil.convert(listeningImage),
34
+ listeningImageRepeats,
28
35
  preferSpeechToText,
29
36
  onChunk,
30
37
  language
@@ -1,5 +1,6 @@
1
1
  import type { HybridObject } from 'react-native-nitro-modules';
2
2
  import type { VoiceInputChunk, VoiceInputResult } from '../types/Voice';
3
+ import type { NitroImage } from '../utils/NitroImage';
3
4
 
4
5
  export interface Voice extends HybridObject<{ android: 'kotlin'; ios: 'swift' }> {
5
6
  hasVoiceInputPermission(): boolean;
@@ -8,6 +9,8 @@ export interface Voice extends HybridObject<{ android: 'kotlin'; ios: 'swift' }>
8
9
  silenceThresholdMs?: number,
9
10
  maxDurationMs?: number,
10
11
  listeningText?: string,
12
+ listeningImage?: NitroImage,
13
+ listeningImageRepeats?: boolean,
11
14
  preferSpeechToText?: boolean,
12
15
  onChunk?: (chunk: VoiceInputChunk) => void,
13
16
  language?: string
@@ -8,7 +8,7 @@ export type ActionButton<T> = ActionButtonAndroid<T> | ActionButtonIos<T>;
8
8
 
9
9
  export type HeaderActionsIos<T> = {
10
10
  /**
11
- * @platform iOS - the back button can not be hidden or disabled, if you don't define it iOS will provide a default back button popping the template
11
+ * @namespace iOS - the back button can not be hidden or disabled, if you don't define it iOS will provide a default back button popping the template
12
12
  */
13
13
  backButton?: BackButton<T>;
14
14
  leadingNavigationBarButtons?: [ActionButtonIos<T>, ActionButtonIos<T>] | [ActionButtonIos<T>];
@@ -79,3 +79,22 @@ export type AutoImage =
79
79
  timeoutMs?: number;
80
80
  type: 'remote';
81
81
  };
82
+
83
+ /**
84
+ * Image for the CarPlay voice control overlay. Animated GIF, APNG, and WebP all animate.
85
+ * `color` is ignored for animated assets — frames cannot be tinted individually. Static
86
+ * assets and glyphs are resized/capped to fit within 150×150pt (`fontScale` controls a
87
+ * glyph's relative size). CarPlay enforces a 0.3s–5s animation cycle duration: shorter
88
+ * source animations are stretched to 0.3s by the system, longer ones are capped to 5s.
89
+ * @namespace ios
90
+ */
91
+ export type VoiceInputImage =
92
+ | AutoGlyph
93
+ | {
94
+ type: 'asset';
95
+ image: ImageSourcePropType;
96
+ /** Tints the image. Ignored for animated assets — frames cannot be tinted individually. */
97
+ color?: ThemedColor | string;
98
+ /** Whether the animation loops. Defaults to `true` for animated images, `false` for static. */
99
+ repeats?: boolean;
100
+ };
@@ -1,3 +1,5 @@
1
+ import type { VoiceInputImage } from './Image';
2
+
1
3
  export interface VoiceInputChunk {
2
4
  partial?: string;
3
5
  audio?: ArrayBuffer;
@@ -12,6 +14,10 @@ export interface VoiceInputOptions {
12
14
  silenceThresholdMs?: number;
13
15
  maxDurationMs?: number;
14
16
  listeningText?: string;
17
+ /** Image displayed in the CarPlay voice control overlay. See {@link VoiceInputImage}.
18
+ * @namespace ios
19
+ */
20
+ listeningImage?: VoiceInputImage;
15
21
  preferSpeechToText?: boolean;
16
22
  onChunk?: (chunk: VoiceInputChunk) => void;
17
23
  language?: string;