@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.
- package/README.md +103 -3
- package/ReactNativeAutoPlay.podspec +0 -4
- package/android/src/automotive/AndroidManifest.xml +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +91 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +214 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/MapTemplate.kt +2 -5
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +117 -38
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/SymbolFont.kt +29 -30
- package/ios/extensions/NitroImageExtensions.swift +10 -1
- package/ios/hybrid/HybridAutoPlay.swift +51 -4
- package/ios/templates/GridTemplate.swift +7 -0
- package/ios/templates/MapTemplate.swift +14 -0
- package/ios/templates/Parser.swift +91 -4
- package/ios/utils/SymbolFont.swift +44 -44
- package/ios/utils/VoiceInputManager.swift +233 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/specs/AutoPlay.nitro.d.ts +31 -1
- package/lib/templates/MapTemplate.d.ts +4 -1
- package/lib/types/Image.d.ts +46 -4
- package/lib/types/Maneuver.d.ts +2 -10
- package/lib/utils/NitroImage.d.ts +29 -3
- package/lib/utils/NitroImage.js +64 -3
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JGlyphImage.hpp +6 -1
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
- package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
- package/nitrogen/generated/android/c++/JNitroImage.hpp +20 -3
- package/nitrogen/generated/android/c++/JNitroManeuver.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroMapButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroMessageManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroNavigationAlert.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRow.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroSection.hpp +3 -1
- package/nitrogen/generated/android/c++/JPreferredImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JRemoteImage.hpp +68 -0
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.cpp +30 -0
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.hpp +92 -0
- package/nitrogen/generated/android/c++/JVariant_PreferredImageLane_ImageLane.hpp +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +5 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +14 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
- package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +4 -1
- package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/GlyphImage.swift +7 -2
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
- package/nitrogen/generated/ios/swift/ImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/MessageTemplateConfig.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
- package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
- package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
- package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
- package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
- package/nitrogen/generated/shared/c++/GlyphImage.hpp +6 -1
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
- package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
- package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
- package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
- package/package.json +2 -3
- package/src/index.ts +1 -0
- package/src/specs/AutoPlay.nitro.ts +39 -1
- package/src/templates/MapTemplate.ts +4 -1
- package/src/types/Image.ts +65 -16
- package/src/types/Maneuver.ts +3 -10
- package/src/utils/NitroImage.ts +81 -6
- package/android/src/main/res/font/materialsymbolsoutlined_regular.ttf +0 -0
- package/ios/Assets/MaterialSymbolsOutlined-Regular.ttf +0 -0
- package/lib/types/Glyphmap.d.ts +0 -4105
- package/lib/types/Glyphmap.js +0 -4105
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
- 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
|
|
12
|
+
private static var cachedFontName: String?
|
|
13
|
+
private static var cachedPSName: String?
|
|
13
14
|
|
|
14
|
-
private static
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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:
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
glyphImage: GlyphImage,
|
|
57
58
|
foregroundColor: UIColor,
|
|
58
59
|
backgroundColor: UIColor,
|
|
59
60
|
size: CGFloat,
|
|
60
61
|
fontScale: CGFloat
|
|
61
62
|
) -> UIImage? {
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
guard let font = uiFont(for: glyphImage, size: size, fontScale: fontScale) else {
|
|
64
|
+
return nil
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
guard let
|
|
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
|
|
100
|
+
let uiImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
103
101
|
UIGraphicsEndImageContext()
|
|
104
102
|
|
|
105
|
-
return
|
|
103
|
+
return uiImage
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
static func imageFromGlyph(
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
glyphImage: image,
|
|
184
184
|
foregroundColor: foregroundColor,
|
|
185
185
|
backgroundColor: backgroundColor,
|
|
186
186
|
size: size,
|
|
187
|
-
fontScale:
|
|
187
|
+
fontScale: fontScale
|
|
188
188
|
)
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
return SymbolFont.imageFromGlyph(
|
|
192
|
-
|
|
192
|
+
glyphImage: image,
|
|
193
193
|
size: size,
|
|
194
194
|
foregroundColor: image.color,
|
|
195
195
|
backgroundColor: image.backgroundColor,
|
|
196
|
-
fontScale:
|
|
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
|
@@ -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
|
|
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
|
-
*
|
|
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
|
};
|
package/lib/types/Image.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { ImageSourcePropType } from 'react-native';
|
|
2
2
|
import type { ThemedColor } from '../utils/NitroColor';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {};
|
package/lib/types/Maneuver.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ImageSourcePropType } from 'react-native';
|
|
2
2
|
import type { ThemedColor } from '../utils/NitroColor';
|
|
3
|
-
import type {
|
|
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
|
-
*
|
|
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: {
|