@iternio/react-native-auto-play 0.3.11 → 0.3.13

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.
Files changed (137) hide show
  1. package/README.md +103 -3
  2. package/ReactNativeAutoPlay.podspec +0 -4
  3. package/android/src/automotive/AndroidManifest.xml +1 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +91 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +214 -0
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/MapTemplate.kt +2 -5
  8. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +117 -38
  9. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
  10. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/SymbolFont.kt +29 -30
  11. package/ios/extensions/NitroImageExtensions.swift +10 -1
  12. package/ios/hybrid/HybridAutoPlay.swift +51 -4
  13. package/ios/templates/GridTemplate.swift +7 -0
  14. package/ios/templates/MapTemplate.swift +14 -0
  15. package/ios/templates/Parser.swift +91 -4
  16. package/ios/utils/SymbolFont.swift +44 -44
  17. package/ios/utils/VoiceInputManager.swift +233 -0
  18. package/lib/index.d.ts +1 -0
  19. package/lib/index.js +1 -0
  20. package/lib/specs/AutoPlay.nitro.d.ts +31 -1
  21. package/lib/templates/MapTemplate.d.ts +4 -1
  22. package/lib/types/Image.d.ts +46 -4
  23. package/lib/types/Maneuver.d.ts +2 -10
  24. package/lib/utils/NitroImage.d.ts +29 -3
  25. package/lib/utils/NitroImage.js +64 -3
  26. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
  27. package/nitrogen/generated/android/c++/JGlyphImage.hpp +6 -1
  28. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
  29. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
  30. package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
  31. package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
  32. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
  33. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
  34. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
  35. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
  36. package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
  37. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
  38. package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
  39. package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
  40. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
  41. package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
  42. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
  43. package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +3 -1
  44. package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
  45. package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
  46. package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
  47. package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
  48. package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
  49. package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
  50. package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
  51. package/nitrogen/generated/android/c++/JNitroImage.hpp +20 -3
  52. package/nitrogen/generated/android/c++/JNitroManeuver.hpp +3 -1
  53. package/nitrogen/generated/android/c++/JNitroMapButton.hpp +2 -0
  54. package/nitrogen/generated/android/c++/JNitroMessageManeuver.hpp +7 -5
  55. package/nitrogen/generated/android/c++/JNitroNavigationAlert.hpp +7 -5
  56. package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +7 -5
  57. package/nitrogen/generated/android/c++/JNitroRow.hpp +7 -5
  58. package/nitrogen/generated/android/c++/JNitroSection.hpp +3 -1
  59. package/nitrogen/generated/android/c++/JPreferredImageLane.hpp +2 -0
  60. package/nitrogen/generated/android/c++/JRemoteImage.hpp +68 -0
  61. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +3 -1
  62. package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +3 -1
  63. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.cpp +30 -0
  64. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.hpp +92 -0
  65. package/nitrogen/generated/android/c++/JVariant_PreferredImageLane_ImageLane.hpp +3 -1
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +5 -2
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +3 -3
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +3 -3
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +14 -2
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +2 -2
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
  77. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
  78. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
  79. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
  80. package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
  81. package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +4 -1
  82. package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
  83. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  84. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
  85. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
  86. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
  87. package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
  88. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
  89. package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
  90. package/nitrogen/generated/ios/swift/GlyphImage.swift +7 -2
  91. package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
  92. package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
  93. package/nitrogen/generated/ios/swift/ImageLane.swift +9 -4
  94. package/nitrogen/generated/ios/swift/MessageTemplateConfig.swift +16 -11
  95. package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
  96. package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
  97. package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
  98. package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
  99. package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
  100. package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
  101. package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
  102. package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
  103. package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
  104. package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
  105. package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
  106. package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
  107. package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
  108. package/nitrogen/generated/shared/c++/GlyphImage.hpp +6 -1
  109. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
  110. package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
  111. package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
  112. package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
  113. package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
  114. package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
  115. package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
  116. package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
  117. package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
  118. package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
  119. package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
  120. package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
  121. package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
  122. package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
  123. package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
  124. package/package.json +2 -3
  125. package/src/index.ts +1 -0
  126. package/src/specs/AutoPlay.nitro.ts +39 -1
  127. package/src/templates/MapTemplate.ts +4 -1
  128. package/src/types/Image.ts +65 -16
  129. package/src/types/Maneuver.ts +3 -10
  130. package/src/utils/NitroImage.ts +81 -6
  131. package/android/src/main/res/font/materialsymbolsoutlined_regular.ttf +0 -0
  132. package/ios/Assets/MaterialSymbolsOutlined-Regular.ttf +0 -0
  133. package/lib/types/Glyphmap.d.ts +0 -4105
  134. package/lib/types/Glyphmap.js +0 -4105
  135. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
  136. package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
  137. package/src/types/Glyphmap.ts +0 -4107
@@ -9,67 +9,65 @@ import CoreText
9
9
  import UIKit
10
10
 
11
11
  class SymbolFont {
12
- private static let defaultCanvasSize = 32
12
+ private static var cachedFontName: String?
13
+ private static var cachedPSName: String?
13
14
 
14
- private static var isRegistered = false
15
- private static var fontName: String?
16
-
17
- static func loadFont() {
18
- let podBundle = Bundle(for: SymbolFont.self)
15
+ private static func loadFont(named fontName: String) -> String? {
16
+ if fontName == cachedFontName {
17
+ return cachedPSName
18
+ }
19
19
 
20
- guard
21
- let bundleURL = podBundle.url(
22
- forResource: "ReactNativeAutoPlay",
23
- withExtension: "bundle"
24
- ),
25
- let resourceBundle = Bundle(url: bundleURL),
26
- let fontURL = resourceBundle.url(
27
- forResource: "MaterialSymbolsOutlined-Regular",
28
- withExtension: "ttf"
29
- )
30
- else {
31
- return
20
+ guard let url = Bundle.main.url(forResource: fontName, withExtension: "ttf") else {
21
+ print("[AutoPlay] \(fontName).ttf not found in the app bundle — glyph images will not render.")
22
+ return nil
32
23
  }
33
24
 
34
- guard let fontData = try? Data(contentsOf: fontURL) as CFData,
25
+ guard let fontData = try? Data(contentsOf: url) as CFData,
35
26
  let provider = CGDataProvider(data: fontData),
36
27
  let font = CGFont(provider)
37
28
  else {
38
- return
29
+ return nil
39
30
  }
40
31
 
41
32
  var error: Unmanaged<CFError>?
42
33
  CTFontManagerRegisterGraphicsFont(font, &error)
43
- if let error = error?.takeUnretainedValue() {
44
- print("Failed to register font: \(error)")
34
+ // Ignore already-registered errors (e.g. hot reload)
35
+
36
+ guard let psName = font.fullName as? String else {
37
+ return nil
45
38
  }
46
- else {
47
- print("Font \(font.fullName as String? ?? "unknown") registered")
39
+
40
+ cachedFontName = fontName
41
+ cachedPSName = psName
42
+ return psName
43
+ }
44
+
45
+ private static func uiFont(for glyphImage: GlyphImage, size: CGFloat, fontScale: CGFloat) -> UIFont? {
46
+ let pointSize = size * fontScale
47
+
48
+ guard let psName = loadFont(named: glyphImage.fontName) else {
49
+ return nil
48
50
  }
49
51
 
50
- SymbolFont.fontName = font.fullName as? String
51
- SymbolFont.isRegistered = true
52
+ return UIFont(name: psName, size: pointSize)
52
53
  }
53
54
 
54
55
  // creates a single color UIImage
55
56
  static func imageFromGlyph(
56
- glyph: Double,
57
+ glyphImage: GlyphImage,
57
58
  foregroundColor: UIColor,
58
59
  backgroundColor: UIColor,
59
60
  size: CGFloat,
60
61
  fontScale: CGFloat
61
62
  ) -> UIImage? {
62
- if !SymbolFont.isRegistered {
63
- SymbolFont.loadFont()
63
+ guard let font = uiFont(for: glyphImage, size: size, fontScale: fontScale) else {
64
+ return nil
64
65
  }
65
66
 
66
- guard let fontName = SymbolFont.fontName,
67
- let font = UIFont(name: fontName, size: size * fontScale)
68
- else {
67
+ guard let scalar = UnicodeScalar(UInt32(glyphImage.glyph)) else {
69
68
  return nil
70
69
  }
71
-
72
- let codepoint = String(UnicodeScalar(UInt32(glyph))!)
70
+ let codepoint = String(Character(scalar))
73
71
  let canvasSize = CGSize(width: size, height: size)
74
72
  let rect = CGRect(origin: .zero, size: canvasSize)
75
73
 
@@ -99,14 +97,14 @@ class SymbolFont {
99
97
  let y = (canvasSize.height - textSize.height) / 2
100
98
  attrString.draw(at: CGPoint(x: x, y: y))
101
99
 
102
- let image = UIGraphicsGetImageFromCurrentImageContext()
100
+ let uiImage = UIGraphicsGetImageFromCurrentImageContext()
103
101
  UIGraphicsEndImageContext()
104
102
 
105
- return image
103
+ return uiImage
106
104
  }
107
105
 
108
106
  static func imageFromGlyph(
109
- glyph: Double,
107
+ glyphImage: GlyphImage,
110
108
  size: CGFloat,
111
109
  foregroundColor: NitroColor,
112
110
  backgroundColor: NitroColor,
@@ -115,7 +113,7 @@ class SymbolFont {
115
113
  ) -> UIImage? {
116
114
  guard
117
115
  let lightImage = imageFromGlyph(
118
- glyph: glyph,
116
+ glyphImage: glyphImage,
119
117
  foregroundColor: Parser.doubleToColor(
120
118
  value: foregroundColor.lightColor
121
119
  ),
@@ -126,7 +124,7 @@ class SymbolFont {
126
124
  fontScale: fontScale
127
125
  ),
128
126
  let darkImage = imageFromGlyph(
129
- glyph: glyph,
127
+ glyphImage: glyphImage,
130
128
  foregroundColor: Parser.doubleToColor(
131
129
  value: foregroundColor.darkColor
132
130
  ),
@@ -167,6 +165,8 @@ class SymbolFont {
167
165
  ) -> UIImage? {
168
166
  guard let image else { return nil }
169
167
 
168
+ let fontScale = image.fontScale ?? 1.0
169
+
170
170
  if noImageAsset {
171
171
  let foregroundColor = Parser.doubleToColor(
172
172
  value: traitCollection.userInterfaceStyle == .light
@@ -180,21 +180,21 @@ class SymbolFont {
180
180
  )
181
181
 
182
182
  return SymbolFont.imageFromGlyph(
183
- glyph: image.glyph,
183
+ glyphImage: image,
184
184
  foregroundColor: foregroundColor,
185
185
  backgroundColor: backgroundColor,
186
186
  size: size,
187
- fontScale: image.fontScale ?? 1.0
187
+ fontScale: fontScale
188
188
  )
189
189
  }
190
190
 
191
191
  return SymbolFont.imageFromGlyph(
192
- glyph: image.glyph,
192
+ glyphImage: image,
193
193
  size: size,
194
194
  foregroundColor: image.color,
195
195
  backgroundColor: image.backgroundColor,
196
- fontScale: image.fontScale ?? 1.0,
196
+ fontScale: fontScale,
197
197
  traitCollection: traitCollection
198
- )!
198
+ )
199
199
  }
200
200
  }
@@ -0,0 +1,233 @@
1
+ import AVFoundation
2
+ import CarPlay
3
+
4
+ /// Captures audio from the car microphone and buffers raw 16 kHz / 16-bit / mono PCM.
5
+ /// Recording stops automatically when silence is detected or the max duration is reached.
6
+ class VoiceInputManager {
7
+ private var audioEngine: AVAudioEngine?
8
+ private var voiceControlTemplate: CPVoiceControlTemplate?
9
+ private var continuation: CheckedContinuation<[Int16], Error>?
10
+ private var samples: [Int16] = []
11
+ private var isStopping = false
12
+ private let stopLock = NSLock()
13
+
14
+ // Timing
15
+ private var recordingStart: Date?
16
+ private var silenceStart: Date?
17
+
18
+ private static let sampleRate: Double = 16_000
19
+ private static let tapBufferSize: AVAudioFrameCount = 4_096
20
+ private static let silenceAmplitudeThreshold = 500
21
+ private static let warmupMs: Double = 500
22
+
23
+ private static let targetFormat = AVAudioFormat(
24
+ commonFormat: .pcmFormatInt16,
25
+ sampleRate: sampleRate,
26
+ channels: 1,
27
+ interleaved: true
28
+ )!
29
+
30
+ // MARK: - Public
31
+
32
+ func start(
33
+ interfaceController: AutoPlayInterfaceController?,
34
+ silenceThresholdMs: Double,
35
+ maxDurationMs: Double,
36
+ listeningText: String
37
+ ) async throws -> Data {
38
+ let samples = try await withCheckedThrowingContinuation {
39
+ (cont: CheckedContinuation<[Int16], Error>) in
40
+ self.continuation = cont
41
+ self.samples = []
42
+ self.isStopping = false
43
+
44
+ do {
45
+ try self.startCapture(
46
+ interfaceController: interfaceController,
47
+ silenceThresholdMs: silenceThresholdMs,
48
+ maxDurationMs: maxDurationMs,
49
+ listeningText: listeningText
50
+ )
51
+ }
52
+ catch {
53
+ self.stopCapture(interfaceController: interfaceController)
54
+ self.continuation = nil
55
+ cont.resume(throwing: error)
56
+ }
57
+ }
58
+
59
+ return samplesAsData(samples)
60
+ }
61
+
62
+ func stop(interfaceController: AutoPlayInterfaceController? = nil) {
63
+ stopLock.lock()
64
+ guard !isStopping else {
65
+ stopLock.unlock()
66
+ return
67
+ }
68
+ isStopping = true
69
+ let capturedContinuation = continuation
70
+ let capturedSamples = samples
71
+ continuation = nil
72
+ samples = []
73
+ stopLock.unlock()
74
+
75
+ stopCapture(interfaceController: interfaceController)
76
+ capturedContinuation?.resume(returning: capturedSamples)
77
+ }
78
+
79
+ // MARK: - Private
80
+
81
+ private func startCapture(
82
+ interfaceController: AutoPlayInterfaceController?,
83
+ silenceThresholdMs: Double,
84
+ maxDurationMs: Double,
85
+ listeningText: String
86
+ ) throws {
87
+ guard AVAudioSession.sharedInstance().recordPermission == .granted else {
88
+ throw VoiceInputError.microphonePermissionDenied
89
+ }
90
+
91
+ // Activate the session first so inputNode reports the correct hardware format
92
+ let session = AVAudioSession.sharedInstance()
93
+ try session.setCategory(.playAndRecord, mode: .measurement, options: [])
94
+ try session.setActive(true)
95
+
96
+ if let interfaceController {
97
+ presentVoiceTemplate(interfaceController: interfaceController, listeningText: listeningText)
98
+ }
99
+
100
+ let engine = AVAudioEngine()
101
+ let inputNode = engine.inputNode
102
+ let nativeFormat = inputNode.outputFormat(forBus: 0)
103
+
104
+ let targetFormat = VoiceInputManager.targetFormat
105
+ guard let converter = AVAudioConverter(from: nativeFormat, to: targetFormat) else {
106
+ throw VoiceInputError.converterUnavailable
107
+ }
108
+
109
+ recordingStart = Date()
110
+ silenceStart = nil
111
+
112
+ inputNode.installTap(
113
+ onBus: 0,
114
+ bufferSize: VoiceInputManager.tapBufferSize,
115
+ format: nativeFormat
116
+ ) { [weak self] buffer, _ in
117
+ guard let self, !self.isStopping else { return }
118
+
119
+ let outputFrameCapacity = AVAudioFrameCount(
120
+ Double(buffer.frameLength)
121
+ * VoiceInputManager.sampleRate
122
+ / nativeFormat.sampleRate
123
+ )
124
+
125
+ guard
126
+ let outputBuffer = AVAudioPCMBuffer(
127
+ pcmFormat: targetFormat,
128
+ frameCapacity: outputFrameCapacity
129
+ )
130
+ else { return }
131
+
132
+ var conversionError: NSError?
133
+ let status = converter.convert(to: outputBuffer, error: &conversionError) {
134
+ _,
135
+ outStatus in
136
+ outStatus.pointee = .haveData
137
+ return buffer
138
+ }
139
+
140
+ guard status != .error, let int16Data = outputBuffer.int16ChannelData else { return }
141
+
142
+ let frameCount = Int(outputBuffer.frameLength)
143
+ let newSamples = Array(UnsafeBufferPointer(start: int16Data[0], count: frameCount))
144
+ self.samples.append(contentsOf: newSamples)
145
+
146
+ let now = Date()
147
+
148
+ // Max duration check
149
+ if let start = self.recordingStart,
150
+ now.timeIntervalSince(start) * 1000 >= maxDurationMs
151
+ {
152
+ self.triggerAutoStop(interfaceController: interfaceController)
153
+ return
154
+ }
155
+
156
+ // Silence detection — skip during warm-up so the pipeline has time
157
+ // to stabilise before we start measuring amplitude
158
+ if let start = self.recordingStart,
159
+ now.timeIntervalSince(start) * 1000 >= VoiceInputManager.warmupMs
160
+ {
161
+ let peak = newSamples.reduce(0) { max($0, abs(Int($1))) }
162
+ if peak < VoiceInputManager.silenceAmplitudeThreshold {
163
+ if self.silenceStart == nil { self.silenceStart = now }
164
+ if let silenceBegin = self.silenceStart,
165
+ now.timeIntervalSince(silenceBegin) * 1000 >= silenceThresholdMs
166
+ {
167
+ self.triggerAutoStop(interfaceController: interfaceController)
168
+ }
169
+ }
170
+ else {
171
+ self.silenceStart = nil
172
+ }
173
+ }
174
+ }
175
+
176
+ try engine.start()
177
+ audioEngine = engine
178
+ }
179
+
180
+ private func triggerAutoStop(interfaceController: AutoPlayInterfaceController?) {
181
+ DispatchQueue.global(qos: .userInitiated).async {
182
+ self.stop(interfaceController: interfaceController)
183
+ }
184
+ }
185
+
186
+ private func stopCapture(interfaceController: AutoPlayInterfaceController?) {
187
+ audioEngine?.inputNode.removeTap(onBus: 0)
188
+ audioEngine?.stop()
189
+ audioEngine = nil
190
+ recordingStart = nil
191
+ silenceStart = nil
192
+ try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
193
+ if let interfaceController {
194
+ dismissVoiceTemplate(interfaceController: interfaceController)
195
+ }
196
+ }
197
+
198
+ private func presentVoiceTemplate(interfaceController: AutoPlayInterfaceController, listeningText: String) {
199
+ let listeningState = CPVoiceControlState(
200
+ identifier: "listening",
201
+ titleVariants: [listeningText],
202
+ image: nil,
203
+ repeats: true
204
+ )
205
+ let template = CPVoiceControlTemplate(voiceControlStates: [listeningState])
206
+ initTemplate(template: template, id: "voice-input")
207
+ voiceControlTemplate = template
208
+
209
+ Task { @MainActor in
210
+ try? await interfaceController.presentTemplate(template, animated: true)
211
+ template.activateVoiceControlState(withIdentifier: "listening")
212
+ }
213
+ }
214
+
215
+ private func dismissVoiceTemplate(interfaceController: AutoPlayInterfaceController) {
216
+ Task { @MainActor in
217
+ try? await interfaceController.dismissTemplate(animated: true)
218
+ }
219
+ voiceControlTemplate = nil
220
+ }
221
+
222
+ private func samplesAsData(_ samples: [Int16]) -> Data {
223
+ samples.withUnsafeBufferPointer { ptr in
224
+ Data(buffer: ptr)
225
+ }
226
+ }
227
+ }
228
+
229
+ enum VoiceInputError: Error {
230
+ case microphonePermissionDenied
231
+ case converterUnavailable
232
+ case noActiveSession
233
+ }
package/lib/index.d.ts CHANGED
@@ -43,3 +43,4 @@ export * from './types/Trip';
43
43
  export type { AlertPriority, NavigationAlert as Alert, NavigationAlertAction as AlertAction, } from './utils/NitroAlert';
44
44
  export type { ThemedColor } from './utils/NitroColor';
45
45
  export type { GridButton } from './utils/NitroGrid';
46
+ export { setIconFont } from './utils/NitroImage';
package/lib/index.js CHANGED
@@ -45,3 +45,4 @@ export * from './types/SignInMethod';
45
45
  export * from './types/Telemetry';
46
46
  export * from './types/Text';
47
47
  export * from './types/Trip';
48
+ export { setIconFont } from './utils/NitroImage';
@@ -24,12 +24,42 @@ export interface AutoPlay extends HybridObject<{
24
24
  */
25
25
  addListenerRenderState(moduleName: string, callback: (payload: VisibilityState) => void): CleanupCallback;
26
26
  /**
27
- * Adds a listener for voice input events. Not implemented on iOS.
27
+ * Adds a listener for voice input events fired by the OS (Android Auto only).
28
+ * On iOS this is a no-op — use startVoiceInput instead.
28
29
  * @param callback the callback to receive the voice input
29
30
  * @returns callback to remove the listener
30
31
  * @namespace Android
31
32
  */
32
33
  addListenerVoiceInput(callback: (coordinates: Location | undefined, query: string | undefined) => void): CleanupCallback;
34
+ /**
35
+ * Returns true if microphone permission has already been granted.
36
+ */
37
+ hasVoiceInputPermission(): boolean;
38
+ /**
39
+ * Request microphone permission from the user.
40
+ * On Android: uses the car context when Android Auto is connected, otherwise
41
+ * falls back to the React Native application context.
42
+ * On iOS: uses AVAudioApplication (iOS 17+) or AVAudioSession (iOS 15–16).
43
+ * Returns true if permission was granted, false if denied.
44
+ */
45
+ requestVoiceInputPermission(): Promise<boolean>;
46
+ /**
47
+ * Start an in-app voice recording session.
48
+ * On Android: acquires audio focus and captures via CarAudioRecord when
49
+ * Android Auto is connected, otherwise uses standard AudioRecord.
50
+ * On iOS: presents CPVoiceControlTemplate (when a car is connected) and
51
+ * captures audio via AVAudioEngine.
52
+ * Resolves with the complete raw PCM buffer (16 kHz, 16-bit, mono) when
53
+ * silence is detected, the max duration is reached, or stopVoiceInput() is called.
54
+ * Rejects if microphone permission has not been granted or recording fails to start.
55
+ */
56
+ startVoiceInput(silenceThresholdMs?: number, maxDurationMs?: number, listeningText?: string): Promise<ArrayBuffer>;
57
+ /**
58
+ * Stop the active voice recording session early. Causes the Promise returned
59
+ * by startVoiceInput() to resolve with the audio captured so far.
60
+ * No-op if no recording is in progress.
61
+ */
62
+ stopVoiceInput(): void;
33
63
  /**
34
64
  * sets the specified template as root template, initializes a new stack
35
65
  * Promise might contain an error message in case setting root template failed
@@ -96,7 +96,10 @@ export type MapTemplateConfig = Omit<NitroMapTemplateConfig, 'mapButtons' | 'hea
96
96
  */
97
97
  onAutoDriveEnabled?: (template: MapTemplate) => void;
98
98
  /**
99
- * Initial navigation maneuver background color. Mainly useful, when in CarPlay the default loading maneuver does not have the right color.
99
+ * Use this to set the default maneuver background color on iOS.
100
+ * Only used when starting navigation and not providing any maneuvers yet,
101
+ * visible on the system provided loading maneuver then.
102
+ * @namespace iOS
100
103
  */
101
104
  defaultGuidanceBackgroundColor?: ThemedColor | string;
102
105
  };
@@ -1,8 +1,22 @@
1
1
  import type { ImageSourcePropType } from 'react-native';
2
2
  import type { ThemedColor } from '../utils/NitroColor';
3
- import type { GlyphName } from './Glyphmap';
4
- export type AutoImage = {
5
- name: GlyphName;
3
+ /**
4
+ * Augment this interface in your app to get type-safe glyph name autocompletion.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * // autoplay-glyphs.d.ts
9
+ * import type { GlyphName } from './assets/symbolFont/Glyphmap';
10
+ * declare module '@iternio/react-native-auto-play' {
11
+ * interface AutoPlayGlyphMap extends Record<GlyphName, number> {}
12
+ * }
13
+ * ```
14
+ */
15
+ export interface AutoPlayGlyphMap {
16
+ }
17
+ /** Resolves to the augmented glyph name union, or falls back to `string` when not augmented. */
18
+ export type GlyphMapKey = keyof AutoPlayGlyphMap extends never ? string : Extract<keyof AutoPlayGlyphMap, string>;
19
+ type GlyphStyleFields = {
6
20
  /**
7
21
  * Sets the icon dark and light mode color or a single color for both.
8
22
  * Defaults to white for dark mode and black for light mode if not specified.
@@ -15,8 +29,22 @@ export type AutoImage = {
15
29
  */
16
30
  backgroundColor?: ThemedColor | string;
17
31
  fontScale?: number;
32
+ };
33
+ /** Glyph by name — looked up in the glyph map registered via {@link setIconFont}. */
34
+ export type AutoGlyphByName = GlyphStyleFields & {
18
35
  type: 'glyph';
19
- } | {
36
+ /** Key in the glyph map passed to `setIconFont`. */
37
+ name: GlyphMapKey;
38
+ /** Optional override — if set, used instead of the map lookup. */
39
+ codepoint?: number;
40
+ };
41
+ /** Glyph by raw Unicode code point. */
42
+ export type AutoGlyphByCodepoint = GlyphStyleFields & {
43
+ type: 'glyph';
44
+ codepoint: number;
45
+ };
46
+ export type AutoGlyph = AutoGlyphByName | AutoGlyphByCodepoint;
47
+ export type AutoImage = AutoGlyph | {
20
48
  image: ImageSourcePropType;
21
49
  /**
22
50
  * if specified the image gets tinted, if not it will just use the original image
@@ -24,4 +52,18 @@ export type AutoImage = {
24
52
  */
25
53
  color?: ThemedColor | string;
26
54
  type: 'asset';
55
+ } | {
56
+ /** HTTPS URL to a remote image. HTTP is not supported (blocked by App Transport Security). */
57
+ uri: string;
58
+ /**
59
+ * if specified the image gets tinted, if not it will just use the original image
60
+ */
61
+ color?: ThemedColor | string;
62
+ /**
63
+ * Network timeout in milliseconds before the remote fetch is abandoned and `null` is returned.
64
+ * Defaults to 500ms when not specified.
65
+ */
66
+ timeoutMs?: number;
67
+ type: 'remote';
27
68
  };
69
+ export {};
@@ -1,6 +1,6 @@
1
1
  import type { ImageSourcePropType } from 'react-native';
2
2
  import type { ThemedColor } from '../utils/NitroColor';
3
- import type { GlyphName } from './Glyphmap';
3
+ import type { AutoGlyphByCodepoint, AutoGlyphByName } from './Image';
4
4
  import type { TravelEstimates } from './Trip';
5
5
  export declare enum ManeuverType {
6
6
  Depart = 0,
@@ -148,15 +148,7 @@ export interface PreferredLane extends Lane {
148
148
  highlightedAngle: number;
149
149
  isPreferred: boolean;
150
150
  }
151
- export type ManeuverImage = {
152
- name: GlyphName;
153
- /**
154
- * make sure to specify a color with a proper contrast ratio to cardBackgroundColor otherwise it might not get applied
155
- * defaults to white/black for dark/light mode
156
- */
157
- color?: ThemedColor | string;
158
- type: 'glyph';
159
- } | {
151
+ export type ManeuverImage = Pick<AutoGlyphByName, 'type' | 'name' | 'codepoint' | 'color'> | Pick<AutoGlyphByCodepoint, 'type' | 'codepoint' | 'color'> | {
160
152
  image: ImageSourcePropType;
161
153
  /**
162
154
  * if specified the image gets tinted, if not it will just use the original image
@@ -1,21 +1,47 @@
1
1
  import { type ImageResolvedAssetSource } from 'react-native';
2
2
  import type { AutoImage } from '../types/Image';
3
3
  import { type NitroColor } from './NitroColor';
4
+ /**
5
+ * Register the icon font and (optionally) a glyph map for name-based lookups.
6
+ * Must be called **once** before creating any templates. Subsequent calls are ignored.
7
+ *
8
+ * The font name maps directly to a native font asset:
9
+ * - **Android** — `res/font/<name>.ttf` (must be lowercase)
10
+ * - **iOS** — `<name>.ttf` in the app bundle (registered via CoreText automatically)
11
+ *
12
+ * For cross-platform compatibility use lowercase with underscores only.
13
+ *
14
+ * @param name Native font asset name (without extension).
15
+ * @param glyphMap Optional map of glyph names to Unicode code points.
16
+ * When provided, glyphs can use `{ type: 'glyph', name: 'icon_name' }`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { glyphMap } from './assets/Glyphmap';
21
+ * setIconFont('material_symbols', glyphMap);
22
+ * ```
23
+ */
24
+ export declare function setIconFont(name: string, glyphMap?: Record<string, number>): void;
4
25
  interface AssetImage extends ImageResolvedAssetSource {
5
26
  color?: NitroColor;
6
27
  packager_asset: boolean;
7
28
  }
8
29
  interface GlyphImage {
9
30
  glyph: number;
31
+ fontName: string;
10
32
  color: NitroColor;
11
33
  backgroundColor: NitroColor;
12
34
  fontScale?: number;
13
35
  }
36
+ interface RemoteImage {
37
+ uri: string;
38
+ color?: NitroColor;
39
+ timeoutMs?: number;
40
+ }
14
41
  /**
15
- * we need to map the ButtonImage.name from GlyphName to
16
- * the actual numeric value so we need a nitro specific type
42
+ * NitroModules-compatible image types passed to native.
17
43
  */
18
- export type NitroImage = GlyphImage | AssetImage;
44
+ export type NitroImage = GlyphImage | AssetImage | RemoteImage;
19
45
  declare function convert(image: AutoImage): NitroImage;
20
46
  declare function convert(image?: AutoImage): NitroImage | undefined;
21
47
  export declare const NitroImageUtil: {