@iternio/react-native-auto-play 0.5.2 → 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.
- package/README.md +7 -0
- package/android/src/auto/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidTelemetryObserver.kt +4 -7
- package/android/src/automotive/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidTelemetryObserver.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoice.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/TelemetryObserver.kt +6 -0
- package/ios/hybrid/HybridAutoPlay.swift +0 -1
- package/ios/hybrid/HybridVoice.swift +4 -0
- package/ios/templates/Parser.swift +95 -0
- package/ios/utils/VoiceInputManager.swift +87 -5
- package/lib/hybrid/HybridVoice.js +4 -2
- package/lib/specs/Voice.nitro.d.ts +2 -1
- package/lib/templates/Template.d.ts +1 -1
- package/lib/types/Image.d.ts +16 -0
- package/lib/types/Voice.d.ts +5 -0
- package/nitrogen/generated/android/c++/JHybridVoiceSpec.cpp +21 -3
- package/nitrogen/generated/android/c++/JHybridVoiceSpec.hpp +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoiceSpec.kt +3 -3
- package/nitrogen/generated/ios/c++/HybridVoiceSpecSwift.hpp +15 -2
- package/nitrogen/generated/ios/swift/HybridVoiceSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridVoiceSpec_cxx.swift +30 -1
- package/nitrogen/generated/shared/c++/HybridVoiceSpec.hpp +11 -1
- package/package.json +1 -1
- package/src/hybrid/HybridVoice.ts +7 -0
- package/src/specs/Voice.nitro.ts +3 -0
- package/src/templates/Template.ts +1 -1
- package/src/types/Image.ts +19 -0
- package/src/types/Voice.ts +6 -0
package/README.md
CHANGED
|
@@ -196,6 +196,13 @@ In case you wanna open up your CarPlay app from one of the CarPlay dashboard but
|
|
|
196
196
|
### Android Auto
|
|
197
197
|
No platform specific setup required - we got you covered with all the required stuff.
|
|
198
198
|
|
|
199
|
+
#### ProGuard
|
|
200
|
+
In case you have ProGuard enabled (`def enableProguardInReleaseBuilds = true` in `android/app/build.gradle`), add the following rule to `android/app/proguard-rules.pro`:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
-keep class com.margelo.nitro.swe.iternio.reactnativeautoplay.** { *; }
|
|
204
|
+
```
|
|
205
|
+
|
|
199
206
|
### Android Auto Customization
|
|
200
207
|
You can customize certain behaviors of the library on Android Auto by setting properties in your app's `android/gradle.properties` file.
|
|
201
208
|
|
|
@@ -9,7 +9,6 @@ import androidx.car.app.hardware.info.Mileage
|
|
|
9
9
|
import androidx.car.app.hardware.info.Model
|
|
10
10
|
import androidx.car.app.hardware.info.Speed
|
|
11
11
|
import androidx.car.app.versioning.CarAppApiLevels
|
|
12
|
-
import androidx.core.content.ContextCompat
|
|
13
12
|
|
|
14
13
|
object AndroidTelemetryObserver : TelemetryObserver() {
|
|
15
14
|
private var carContext: CarContext? = null
|
|
@@ -81,8 +80,6 @@ object AndroidTelemetryObserver : TelemetryObserver() {
|
|
|
81
80
|
throw UnsupportedOperationException("Telemetry not supported for this API level ${carContext.carAppApiLevel}")
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
val carHardwareExecutor = ContextCompat.getMainExecutor(carContext)
|
|
85
|
-
|
|
86
83
|
val carHardwareManager = carContext.getCarService(
|
|
87
84
|
CarHardwareManager::class.java
|
|
88
85
|
)
|
|
@@ -90,7 +87,7 @@ object AndroidTelemetryObserver : TelemetryObserver() {
|
|
|
90
87
|
|
|
91
88
|
// Request any single shot values.
|
|
92
89
|
try {
|
|
93
|
-
carInfo.fetchModel(
|
|
90
|
+
carInfo.fetchModel(telemetryExecutor, mModelListener)
|
|
94
91
|
} catch (_: SecurityException) {
|
|
95
92
|
} catch (_: NullPointerException) {
|
|
96
93
|
}
|
|
@@ -102,19 +99,19 @@ object AndroidTelemetryObserver : TelemetryObserver() {
|
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
try {
|
|
105
|
-
carInfo.addEnergyLevelListener(
|
|
102
|
+
carInfo.addEnergyLevelListener(telemetryExecutor, mEnergyLevelListener)
|
|
106
103
|
} catch (_: SecurityException) {
|
|
107
104
|
} catch (_: NullPointerException) {
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
try {
|
|
111
|
-
carInfo.addSpeedListener(
|
|
108
|
+
carInfo.addSpeedListener(telemetryExecutor, mSpeedListener)
|
|
112
109
|
} catch (_: SecurityException) {
|
|
113
110
|
} catch (_: NullPointerException) {
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
try {
|
|
117
|
-
carInfo.addMileageListener(
|
|
114
|
+
carInfo.addMileageListener(telemetryExecutor, mMileageListener)
|
|
118
115
|
} catch (_: SecurityException) {
|
|
119
116
|
} catch (_: NullPointerException) {
|
|
120
117
|
}
|
|
@@ -160,7 +160,7 @@ object AndroidTelemetryObserver : TelemetryObserver() {
|
|
|
160
160
|
// create new instance so we can access all props after permissions were granted
|
|
161
161
|
mCar?.disconnect()
|
|
162
162
|
|
|
163
|
-
val car = Car.createCar(NitroModules.applicationContext)
|
|
163
|
+
val car = Car.createCar(NitroModules.applicationContext, handler)
|
|
164
164
|
mCar = car
|
|
165
165
|
|
|
166
166
|
fetchStaticData(car)
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridVoice.kt
CHANGED
|
@@ -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?
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/TelemetryObserver.kt
CHANGED
|
@@ -3,6 +3,7 @@ package com.margelo.nitro.swe.iternio.reactnativeautoplay
|
|
|
3
3
|
import android.os.Handler
|
|
4
4
|
import android.os.HandlerThread
|
|
5
5
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
6
|
+
import java.util.concurrent.Executor
|
|
6
7
|
|
|
7
8
|
abstract class TelemetryObserver {
|
|
8
9
|
abstract fun startTelemetryObserver(): Boolean
|
|
@@ -12,11 +13,16 @@ abstract class TelemetryObserver {
|
|
|
12
13
|
val telemetryHolder = AndroidAutoTelemetryHolder()
|
|
13
14
|
var isObserverRunning = false
|
|
14
15
|
val handler: Handler
|
|
16
|
+
val telemetryExecutor: Executor
|
|
15
17
|
|
|
16
18
|
init {
|
|
17
19
|
val thread = HandlerThread("AndroidTelemetryThread")
|
|
18
20
|
thread.start()
|
|
19
21
|
handler = Handler(thread.looper)
|
|
22
|
+
|
|
23
|
+
telemetryExecutor = Executor { command ->
|
|
24
|
+
handler.post(command)
|
|
25
|
+
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
fun addListener(callback: (Telemetry?) -> Unit): () -> Unit {
|
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
319
|
-
repeats:
|
|
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
|
-
|
|
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
|
-
* @
|
|
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>];
|
package/lib/types/Image.d.ts
CHANGED
|
@@ -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 {};
|
package/lib/types/Voice.d.ts
CHANGED
|
@@ -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,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
|
package/src/specs/Voice.nitro.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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>];
|
package/src/types/Image.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/types/Voice.ts
CHANGED
|
@@ -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;
|