@mentra/bluetooth-sdk 3.2.0-dev.217 → 3.2.0-dev.222
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 +9 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +23 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +16 -38
- package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +120 -2
- package/android/src/main/java/com/mentra/bluetoothsdk/GeneratedReleaseMetadata.kt +5 -5
- package/android/src/main/java/com/mentra/bluetoothsdk/LogForwardingBudget.kt +50 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +12 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +11 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +38 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/MicSourcePin.kt +31 -0
- package/android/src/main/java/com/mentra/bluetoothsdk/utils/audio/PcmStreamPlayer.kt +39 -5
- package/android/src/test/java/com/mentra/bluetoothsdk/LogForwardingBudgetTest.kt +49 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/MicPcmEventSourceTest.kt +75 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/StreamRequestCaptureAudioTest.kt +44 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/MicSourcePinTest.kt +68 -0
- package/android/src/test/java/com/mentra/bluetoothsdk/utils/NativeLogTest.kt +15 -0
- package/build/BluetoothSdk.types.d.ts +27 -0
- package/build/BluetoothSdk.types.d.ts.map +1 -1
- package/build/BluetoothSdk.types.js.map +1 -1
- package/build/_private/BluetoothSdkModule.d.ts +18 -2
- package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
- package/build/_private/BluetoothSdkModule.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/ios/BluetoothSdkModule.swift +15 -1
- package/ios/Source/BluetoothSdkDefaults.swift +1 -1
- package/ios/Source/Bridge.swift +13 -45
- package/ios/Source/GeneratedReleaseMetadata.swift +5 -5
- package/ios/Source/streaming/StreamModels.swift +48 -2
- package/package.json +1 -1
- package/src/BluetoothSdk.types.ts +28 -0
- package/src/_private/BluetoothSdkModule.ts +24 -2
- package/src/generated/releaseMetadata.ts +5 -5
|
@@ -640,8 +640,11 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
|
|
|
640
640
|
|
|
641
641
|
// MARK: - Live PCM output stream (miniapp speaker.createStream)
|
|
642
642
|
|
|
643
|
+
// jitterMs is accepted for signature parity with Android and ignored: AVAudioEngine's
|
|
644
|
+
// scheduled-buffer playout has no equivalent fixed track buffer to size, so there is no
|
|
645
|
+
// knob here that would mean the same thing.
|
|
643
646
|
AsyncFunction("pcmStreamOpen") {
|
|
644
|
-
(streamId: String, sampleRate: Int, channels: Int, volume: Double) async throws in
|
|
647
|
+
(streamId: String, sampleRate: Int, channels: Int, volume: Double, _: Int?) async throws in
|
|
645
648
|
try PcmStreamManager.open(
|
|
646
649
|
streamId: streamId,
|
|
647
650
|
sampleRate: sampleRate,
|
|
@@ -711,6 +714,17 @@ public class BluetoothSdkModule: Module, MentraBluetoothSDKDelegate {
|
|
|
711
714
|
}
|
|
712
715
|
}
|
|
713
716
|
|
|
717
|
+
/**
|
|
718
|
+
* Android-only today. Present so a host that pins the microphone for a call does not have
|
|
719
|
+
* to branch on platform, and so a caller cannot mistake a missing function for a pin that
|
|
720
|
+
* was taken: the ACS capability gate excludes iOS precisely because this cannot honour it.
|
|
721
|
+
*/
|
|
722
|
+
AsyncFunction("setMicSourcePin") { (source: String?) in
|
|
723
|
+
if source != nil {
|
|
724
|
+
Bridge.log("BluetoothSdkModule: setMicSourcePin(\(source ?? "")) is not implemented on iOS; ignoring")
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
714
728
|
AsyncFunction("restartTranscriber") {
|
|
715
729
|
await MainActor.run {
|
|
716
730
|
DeviceManager.shared.restartTranscriber()
|
|
@@ -11,7 +11,7 @@ enum BluetoothSdkDefaults {
|
|
|
11
11
|
static let voiceActivityDetectionEnabled = false
|
|
12
12
|
static let loudnessGateEnabled = false
|
|
13
13
|
private static let infoSdkVersionKey = "MentraBluetoothSdkVersion"
|
|
14
|
-
private static let swiftPackageSdkVersion = "3.2.0-dev.
|
|
14
|
+
private static let swiftPackageSdkVersion = "3.2.0-dev.222"
|
|
15
15
|
private static let swiftPackageSdkVersionPlaceholder = "__MENTRA" + "_BLUETOOTH_SDK_VERSION__"
|
|
16
16
|
|
|
17
17
|
private static func normalizedSdkVersion(_ value: String?) -> String? {
|
package/ios/Source/Bridge.swift
CHANGED
|
@@ -15,17 +15,6 @@ class Bridge {
|
|
|
15
15
|
private static let micChannels = 1
|
|
16
16
|
private static let lc3FrameDurationMs = 10
|
|
17
17
|
private static let defaultLc3FrameSizeBytes = 60
|
|
18
|
-
private static let audioTraceMetadataKeys = [
|
|
19
|
-
"sampleRate",
|
|
20
|
-
"bitsPerSample",
|
|
21
|
-
"channels",
|
|
22
|
-
"encoding",
|
|
23
|
-
"frameDurationMs",
|
|
24
|
-
"frameSizeBytes",
|
|
25
|
-
"bitrate",
|
|
26
|
-
"packetizedFromGlasses",
|
|
27
|
-
"voiceActivityDetectionEnabled",
|
|
28
|
-
]
|
|
29
18
|
private static let eventSinkLock = NSLock()
|
|
30
19
|
private static let defaultEventSinkId = "default"
|
|
31
20
|
private static var eventSinks: [String: (String, [String: Any]) -> Void] = [:]
|
|
@@ -148,6 +137,10 @@ class Bridge {
|
|
|
148
137
|
"channels": micChannels,
|
|
149
138
|
"encoding": "pcm_s16le",
|
|
150
139
|
"voiceActivityDetectionEnabled": voiceActivityDetectionEnabled,
|
|
140
|
+
// Stamped per frame so a consumer that asked for a specific microphone can verify it
|
|
141
|
+
// rather than assume it. Read now, not captured: the point is which microphone is
|
|
142
|
+
// selected for this buffer. Empty when the SDK has not selected one.
|
|
143
|
+
"source": DeviceStore.shared.get("bluetooth", "currentMic") as? String ?? "",
|
|
151
144
|
]
|
|
152
145
|
}
|
|
153
146
|
|
|
@@ -700,47 +693,22 @@ class Bridge {
|
|
|
700
693
|
dispatchEvent(type, body)
|
|
701
694
|
}
|
|
702
695
|
|
|
696
|
+
/// Returns nil for events that must not be traced.
|
|
697
|
+
///
|
|
698
|
+
/// "log" is excluded so tracing never recurses back through the log event. Audio payload
|
|
699
|
+
/// events are excluded because they arrive at frame rate, which made every microphone
|
|
700
|
+
/// frame emit a second bridge event. On Android that overflowed the JNI global reference
|
|
701
|
+
/// table and aborted the process; here it is wasted work on the audio path. Audio faults
|
|
702
|
+
/// (sequence gaps, decode failures) are still reported through "mic_health", and healthy
|
|
703
|
+
/// frames need no trace.
|
|
703
704
|
private static func tracePayloadForTypedMessage(_ type: String, body: [String: Any]) -> [String: Any]? {
|
|
704
|
-
if type == "log" {
|
|
705
|
+
if type == "log" || isAudioPayloadEvent(type) {
|
|
705
706
|
return nil
|
|
706
707
|
}
|
|
707
|
-
if isAudioPayloadEvent(type) {
|
|
708
|
-
return audioTracePayload(type, body: body)
|
|
709
|
-
}
|
|
710
708
|
return body
|
|
711
709
|
}
|
|
712
710
|
|
|
713
711
|
private static func isAudioPayloadEvent(_ type: String) -> Bool {
|
|
714
712
|
type == "mic_pcm" || type == "mic_lc3"
|
|
715
713
|
}
|
|
716
|
-
|
|
717
|
-
private static func audioTracePayload(_ type: String, body: [String: Any]) -> [String: Any] {
|
|
718
|
-
var payload: [String: Any] = [
|
|
719
|
-
"type": type,
|
|
720
|
-
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
721
|
-
"payloadOmitted": true,
|
|
722
|
-
"payloadOmittedReason": "audio",
|
|
723
|
-
]
|
|
724
|
-
|
|
725
|
-
switch type {
|
|
726
|
-
case "mic_pcm":
|
|
727
|
-
if let data = body["pcm"] as? Data {
|
|
728
|
-
payload["audioBytes"] = data.count
|
|
729
|
-
}
|
|
730
|
-
case "mic_lc3":
|
|
731
|
-
if let data = body["lc3"] as? Data {
|
|
732
|
-
payload["audioBytes"] = data.count
|
|
733
|
-
}
|
|
734
|
-
default:
|
|
735
|
-
break
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
for key in audioTraceMetadataKeys {
|
|
739
|
-
if let value = body[key] {
|
|
740
|
-
payload[key] = value
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
return payload
|
|
745
|
-
}
|
|
746
714
|
}
|
|
@@ -3,9 +3,9 @@ import Foundation
|
|
|
3
3
|
/// Generated by release CI. Do not edit in a release checkout.
|
|
4
4
|
enum GeneratedReleaseMetadata {
|
|
5
5
|
static let familyBaseVersion = "3.2.0"
|
|
6
|
-
static let releaseIdentity = "3.2.0-dev.
|
|
7
|
-
static let releaseSetId = "mentra-3.2.0-dev.
|
|
8
|
-
static let sourceCommit = "
|
|
9
|
-
static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
10
|
-
static let otaManifestSha256 = "
|
|
6
|
+
static let releaseIdentity = "3.2.0-dev.222"
|
|
7
|
+
static let releaseSetId = "mentra-3.2.0-dev.222"
|
|
8
|
+
static let sourceCommit = "7865d7f192a2d9a732269857bb877a3a8a6bf4b8"
|
|
9
|
+
static let otaManifestUrl = "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.222.json"
|
|
10
|
+
static let otaManifestSha256 = "8c33e8beb5ba61eb0821d89b58d81fe2bbf05641d0a99b2e1972926863cc6ab7"
|
|
11
11
|
}
|
|
@@ -87,6 +87,34 @@ public struct StreamAudioConfig {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/// ICE overrides for a WHIP stream. Ignored by the RTMP and SRT paths.
|
|
91
|
+
public struct StreamIceConfig {
|
|
92
|
+
/// STUN server the glasses use while gathering candidates.
|
|
93
|
+
///
|
|
94
|
+
/// `nil` and empty are different answers, and both have to survive the BLE round trip. `nil`
|
|
95
|
+
/// leaves the glasses on their default Cloudflare STUN server; an empty string is an explicit
|
|
96
|
+
/// request for host-only gathering, which is what a WHIP server on the glasses' own hotspot
|
|
97
|
+
/// needs, since a reflexive candidate there is meaningless and unreachable.
|
|
98
|
+
public let stun: String?
|
|
99
|
+
|
|
100
|
+
public init(stun: String? = nil) {
|
|
101
|
+
self.stun = stun
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var dictionary: [String: Any] {
|
|
105
|
+
var values: [String: Any] = [:]
|
|
106
|
+
// Not filtered on emptiness, unlike the other optional string fields here: "" is the
|
|
107
|
+
// host-only signal, so dropping it would silently restore the default STUN server.
|
|
108
|
+
if let stun { values["stun"] = stun }
|
|
109
|
+
return values
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
init?(values: [String: Any]?) {
|
|
113
|
+
guard let values else { return nil }
|
|
114
|
+
self.init(stun: values["stun"] as? String ?? values["s"] as? String)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
/// Effective video settings reported by the glasses after defaults and clamps.
|
|
91
119
|
public struct StreamResolvedVideoConfig: Equatable {
|
|
92
120
|
/// Encoded output width sent to the stream endpoint.
|
|
@@ -286,6 +314,10 @@ public struct StreamRequest {
|
|
|
286
314
|
public let audio: StreamAudioConfig?
|
|
287
315
|
public let authToken: String?
|
|
288
316
|
public let captureAudio: Bool
|
|
317
|
+
public let ice: StreamIceConfig?
|
|
318
|
+
/// Correlation id the glasses echo in every SOFTAP_TRACE line, so phone and glasses logs can
|
|
319
|
+
/// be joined despite unsynchronised clocks.
|
|
320
|
+
public let traceId: String?
|
|
289
321
|
|
|
290
322
|
public init(
|
|
291
323
|
streamUrl: String,
|
|
@@ -294,7 +326,9 @@ public struct StreamRequest {
|
|
|
294
326
|
video: StreamVideoConfig? = nil,
|
|
295
327
|
audio: StreamAudioConfig? = nil,
|
|
296
328
|
authToken: String? = nil,
|
|
297
|
-
captureAudio: Bool = true
|
|
329
|
+
captureAudio: Bool = true,
|
|
330
|
+
ice: StreamIceConfig? = nil,
|
|
331
|
+
traceId: String? = nil
|
|
298
332
|
) {
|
|
299
333
|
self.streamUrl = streamUrl
|
|
300
334
|
self.streamId = streamId
|
|
@@ -303,6 +337,8 @@ public struct StreamRequest {
|
|
|
303
337
|
self.audio = audio
|
|
304
338
|
self.authToken = authToken
|
|
305
339
|
self.captureAudio = captureAudio
|
|
340
|
+
self.ice = ice
|
|
341
|
+
self.traceId = traceId
|
|
306
342
|
}
|
|
307
343
|
|
|
308
344
|
init(values: [String: Any]) {
|
|
@@ -317,7 +353,11 @@ public struct StreamRequest {
|
|
|
317
353
|
video: StreamVideoConfig(values: values["video"] as? [String: Any]),
|
|
318
354
|
audio: StreamAudioConfig(values: values["audio"] as? [String: Any]),
|
|
319
355
|
authToken: values["authToken"] as? String ?? values["auth_token"] as? String,
|
|
320
|
-
captureAudio: (values["captureAudio"] as? Bool) ?? (values["ca"] as? Bool) ?? true
|
|
356
|
+
captureAudio: (values["captureAudio"] as? Bool) ?? (values["ca"] as? Bool) ?? true,
|
|
357
|
+
ice: StreamIceConfig(
|
|
358
|
+
values: (values["ice"] as? [String: Any]) ?? (values["i"] as? [String: Any])
|
|
359
|
+
),
|
|
360
|
+
traceId: values["traceId"] as? String
|
|
321
361
|
)
|
|
322
362
|
}
|
|
323
363
|
|
|
@@ -339,6 +379,12 @@ public struct StreamRequest {
|
|
|
339
379
|
if !captureAudio {
|
|
340
380
|
values["captureAudio"] = false
|
|
341
381
|
}
|
|
382
|
+
if let iceValues = ice?.dictionary, !iceValues.isEmpty {
|
|
383
|
+
values["ice"] = iceValues
|
|
384
|
+
}
|
|
385
|
+
if let traceId, !traceId.isEmpty {
|
|
386
|
+
values["traceId"] = traceId
|
|
387
|
+
}
|
|
342
388
|
return values
|
|
343
389
|
}
|
|
344
390
|
}
|
package/package.json
CHANGED
|
@@ -714,6 +714,20 @@ export type StreamAudioConfig = {
|
|
|
714
714
|
noiseSuppression?: boolean
|
|
715
715
|
}
|
|
716
716
|
|
|
717
|
+
/** ICE overrides for a WHIP stream. Ignored by the RTMP and SRT paths. */
|
|
718
|
+
export type StreamIceConfig = {
|
|
719
|
+
/**
|
|
720
|
+
* STUN server the glasses should use while gathering candidates.
|
|
721
|
+
*
|
|
722
|
+
* Omit to keep the default Cloudflare STUN server. Pass an empty string to force host-only
|
|
723
|
+
* gathering, which is what SoftAP calling needs: the WHIP server runs on the phone across the
|
|
724
|
+
* glasses' own hotspot, so a server-reflexive candidate is meaningless and there is no route to
|
|
725
|
+
* a STUN server anyway. A value that is not a `stun:`/`stuns:` URI is ignored and the default is
|
|
726
|
+
* kept, so a malformed override cannot silently disable ICE.
|
|
727
|
+
*/
|
|
728
|
+
stun?: string
|
|
729
|
+
}
|
|
730
|
+
|
|
717
731
|
export type StreamStartRequest = {
|
|
718
732
|
type?: "start_stream"
|
|
719
733
|
streamUrl: string
|
|
@@ -721,8 +735,16 @@ export type StreamStartRequest = {
|
|
|
721
735
|
sound?: boolean
|
|
722
736
|
video?: StreamVideoConfig
|
|
723
737
|
audio?: StreamAudioConfig
|
|
738
|
+
ice?: StreamIceConfig
|
|
724
739
|
/** When false, glasses skip mic capture. Defaults to true. */
|
|
725
740
|
captureAudio?: boolean
|
|
741
|
+
/**
|
|
742
|
+
* Correlation id echoed by the glasses in every SOFTAP_TRACE log line, so phone and glasses
|
|
743
|
+
* logs can be joined despite having unsynchronised clocks.
|
|
744
|
+
*
|
|
745
|
+
* TEMPORARY: part of the SoftAP diagnostic trace layer.
|
|
746
|
+
*/
|
|
747
|
+
traceId?: string
|
|
726
748
|
}
|
|
727
749
|
|
|
728
750
|
export type StreamKeepAliveRequest = {
|
|
@@ -791,6 +813,12 @@ export type MicPcmEvent = {
|
|
|
791
813
|
channels: 1
|
|
792
814
|
encoding: "pcm_s16le"
|
|
793
815
|
voiceActivityDetectionEnabled: boolean
|
|
816
|
+
/**
|
|
817
|
+
* The microphone this buffer came from (`"glasses"`, `"phone"`, `"bluetooth"`, or `""` when none
|
|
818
|
+
* is selected). Stamped per frame because the SDK can move the source mid-stream, so a consumer
|
|
819
|
+
* that told a remote party which microphone it is sending can check rather than assume.
|
|
820
|
+
*/
|
|
821
|
+
source: string
|
|
794
822
|
}
|
|
795
823
|
|
|
796
824
|
export type MicLc3Event = {
|
|
@@ -209,6 +209,15 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
209
209
|
// Microphone Commands
|
|
210
210
|
setMicState(enabled: boolean, useGlassesMic?: boolean, sendTranscript?: boolean, sendLc3Data?: boolean): Promise<void>
|
|
211
211
|
setPreferredMic(preferredMic: MicPreference): Promise<void>
|
|
212
|
+
/**
|
|
213
|
+
* Lock microphone selection to one source (`"glasses"`) until released with `null`.
|
|
214
|
+
*
|
|
215
|
+
* Stronger than [setPreferredMic]: a preference is a ranking the SDK falls through when a source
|
|
216
|
+
* is unavailable, while a pin has no fallback and reports `mic_health` with
|
|
217
|
+
* `reason: "pinned-source-unavailable"` instead. Android only; the iOS implementation is a no-op,
|
|
218
|
+
* so a caller that depends on the guarantee must gate on platform.
|
|
219
|
+
*/
|
|
220
|
+
setMicSourcePin(source: string | null): Promise<void>
|
|
212
221
|
restartTranscriber(): Promise<void>
|
|
213
222
|
|
|
214
223
|
// Audio Playback Monitoring
|
|
@@ -220,8 +229,21 @@ declare class BluetoothSdkNativeModule extends NativeModule<BluetoothSdkModuleEv
|
|
|
220
229
|
// ? 16-bit LE PCM chunks into a streaming AudioTrack (USAGE_MEDIA, so it
|
|
221
230
|
// follows the phone's media route, e.g. A2DP to glasses). Implemented with
|
|
222
231
|
// AudioTrack on Android and AVAudioEngine on iOS.
|
|
223
|
-
/**
|
|
224
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Open a PCM stream session. One AudioTrack per id; caller manages ids.
|
|
234
|
+
*
|
|
235
|
+
* `jitterMs` sizes the playout buffer and is therefore also the stream's floor latency, since a
|
|
236
|
+
* streaming track fills to its buffer and stays there. Omit it for one-way media, where the
|
|
237
|
+
* default cushion is inaudible; pass a small value for conversational audio, where the same
|
|
238
|
+
* cushion is delay before the far end is heard. Android only — iOS accepts and ignores it.
|
|
239
|
+
*/
|
|
240
|
+
pcmStreamOpen(
|
|
241
|
+
streamId: string,
|
|
242
|
+
sampleRate: number,
|
|
243
|
+
channels: number,
|
|
244
|
+
volume: number,
|
|
245
|
+
jitterMs?: number,
|
|
246
|
+
): Promise<void>
|
|
225
247
|
/**
|
|
226
248
|
* Append base64 PCM. Resolves with the queued-but-unplayed backlog in ms;
|
|
227
249
|
* blocks (on a background dispatcher) while the backlog is above the
|
|
@@ -12,9 +12,9 @@ export interface BluetoothSdkReleaseMetadata {
|
|
|
12
12
|
export const BLUETOOTH_SDK_RELEASE_METADATA: Readonly<BluetoothSdkReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "3.2.0",
|
|
15
|
-
"releaseIdentity": "3.2.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.222",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.222",
|
|
17
|
+
"sourceCommit": "7865d7f192a2d9a732269857bb877a3a8a6bf4b8",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.222.json",
|
|
19
|
+
"otaManifestSha256": "8c33e8beb5ba61eb0821d89b58d81fe2bbf05641d0a99b2e1972926863cc6ab7"
|
|
20
20
|
})
|